Skip to content

Commit

Permalink
Add upload vector layer feature
Browse files Browse the repository at this point in the history
  • Loading branch information
rajtoshranjan committed Jan 19, 2025
1 parent a37eaf6 commit 2d51dab
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 3 deletions.
23 changes: 23 additions & 0 deletions src/assets/icons/selection.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
{
"IcoMoonType": "selection",
"icons": [
{
"icon": {
"paths": [
"M955.077 413.538h-59.077c-15.754 0-29.538-13.785-29.538-29.538v-196.923c0-15.754-13.785-29.538-29.538-29.538h-649.846c-15.754 0-29.538 13.785-29.538 29.538v196.923c0 15.754-13.785 29.538-29.538 29.538h-59.077c-15.754 0-29.538-13.785-29.538-29.538v-265.846c0-43.323 35.446-78.769 78.769-78.769h787.692c43.323 0 78.769 35.446 78.769 78.769v265.846c0 15.754-13.785 29.538-29.538 29.538z",
"M492.308 283.569c11.815-11.815 29.538-11.815 41.354 0l265.846 265.846c11.815 11.815 11.815 29.538 0 41.354l-41.354 41.354c-11.815 11.815-29.538 11.815-41.354 0l-110.277-110.277c-11.815-11.815-33.477-3.938-33.477 13.785v419.446c-1.969 15.754-17.723 29.538-31.508 29.538h-59.077c-15.754 0-29.538-13.785-29.538-29.538v-417.477c0-17.723-21.662-25.6-33.477-13.785l-110.277 110.277c-11.815 11.815-29.538 11.815-41.354 0l-41.354-43.323c-11.815-11.815-11.815-29.538 0-41.354l265.846-265.846z"
],
"attrs": [{}, {}],
"isMulticolor": false,
"isMulticolor2": false,
"grid": 0,
"tags": ["upload"]
},
"attrs": [{}, {}],
"properties": {
"order": 38,
"id": 21,
"name": "upload",
"prevSize": 32
},
"setIdx": 3,
"setId": 2,
"iconIdx": 0
},
{
"icon": {
"paths": [
Expand Down
1 change: 1 addition & 0 deletions src/components/icon/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ export enum IconIdentifier {
Tick = 'tick',
TileLayer = 'tiled-layer',
VectorLayer = 'vector-layer',
Upload = 'upload',
}
103 changes: 103 additions & 0 deletions src/components/input/file-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React, { useCallback, useState } from 'react';
import classNames from 'classnames';
import { Icon, IconIdentifier } from '../icon';

interface FileInputProps {
accept?: string;
onChange?: (file: File) => void;
className?: string;
error?: boolean;
}

export const FileInput: React.FC<FileInputProps> = ({
accept,
onChange,
className,
error,
}) => {
const [isDragging, setIsDragging] = useState(false);
const [fileName, setFileName] = useState<string>();

const handleFile = useCallback(
(file: File) => {
setFileName(file.name);
onChange?.(file);
},
[onChange],
);

const handleDrop = useCallback(
(e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(false);

const file = e.dataTransfer.files?.[0];
if (file) {
handleFile(file);
}
},
[handleFile],
);

const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(true);
}, []);

const handleDragLeave = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(false);
}, []);

const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
handleFile(file);
}
},
[handleFile],
);

return (
<div
className={classNames(
'relative rounded-lg border border-dashed border-gray-600 bg-gray-800/50 p-4 transition-colors',
{
'border-blue-500 bg-blue-500/5': isDragging,
'border-red-400': error,
},
className,
)}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
>
<input
type="file"
accept={accept}
onChange={handleChange}
className="absolute inset-0 cursor-pointer opacity-0"
/>
<div className="flex flex-col items-center justify-center gap-2 text-center">
<Icon
identifier={IconIdentifier.Upload}
className="size-8 text-gray-400"
/>
{fileName ? (
<div className="text-sm text-gray-300">{fileName}</div>
) : (
<>
<div className="text-sm text-gray-300">
Drop your file here, or{' '}
<span className="text-blue-400">browse</span>
</div>
<div className="text-xs text-gray-500">
Supports GeoJSON files (.geojson, .json)
</div>
</>
)}
</div>
</div>
);
};
1 change: 1 addition & 0 deletions src/components/input/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './input';
export * from './types';
export * from './file-input';
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type FormData = {
southLatitude: number;
};

export const AddLayerModal: React.FC<AddLayerModalProps> = ({
export const AddRasterLayerModal: React.FC<AddLayerModalProps> = ({
show,
onClose,
}) => {
Expand Down
165 changes: 165 additions & 0 deletions src/layer-panel/add-vector-layer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { Fieldset } from '@headlessui/react';
import React, { useCallback, useContext } from 'react';
import { useForm } from 'react-hook-form';
import { Button, IconIdentifier, Input, Modal, FileInput } from '../components';
import { GlobalContext } from '../contexts';
import { DEFAULT_STYLES } from './draw/constants';

type AddLayerModalProps = {
onClose: VoidFunction;
show: boolean;
};

type FormData = {
name: string;
geojsonData: string;
file?: FileList;
};

export const AddVectorLayerModal: React.FC<AddLayerModalProps> = ({
show,
onClose,
}) => {
const { layerManager } = useContext(GlobalContext);
const {
register,
handleSubmit,
reset,
setError,
formState: { errors },
} = useForm<FormData>();

const handleFileUpload = useCallback(
(file: File) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target?.result as string;
const geojson = JSON.parse(content);

layerManager?.addGeoJsonLayer({
name: file.name.replace(/\.[^/.]+$/, ''), // Remove file extension
data: geojson,
styles: DEFAULT_STYLES,
});

reset();
onClose();
} catch {
setError('file', {
type: 'validate',
message: 'Invalid GeoJSON format',
});
}
};
reader.readAsText(file);
},
[layerManager, onClose, reset, setError],
);

const onSubmit = (data: FormData) => {
try {
const geojson = JSON.parse(data.geojsonData);
layerManager?.addGeoJsonLayer({
name: data.name,
data: geojson,
styles: DEFAULT_STYLES,
});
reset();
onClose();
} catch {
setError('geojsonData', {
type: 'validate',
message: 'Invalid GeoJSON format',
});
}
};

return (
<Modal title="Add Vector Layer" show={show} onClose={onClose}>
<Fieldset as="form" onSubmit={handleSubmit(onSubmit)}>
{/* Basic Info */}
<h6 className="text-sm text-gray-500">Basic Info</h6>
<Input
label="Layer Name"
className="mt-2"
error={errors.name}
{...register('name', { required: 'Layer name is required' })}
/>

{/* GeoJSON Data */}
<h6 className="mt-5 text-sm text-gray-500">GeoJSON Data</h6>

{/* File Upload */}
<FileInput
accept=".geojson,.json"
onChange={handleFileUpload}
error={!!errors.file}
className="mt-2"
/>

{/* Divider with OR */}
<div className="relative my-4">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-700"></div>
</div>
<div className="relative flex justify-center">
<span className="rounded bg-gray-700 px-3 text-sm font-medium text-gray-300">
OR
</span>
</div>
</div>

{/* Text Input */}
<div className="mt-4">
<label
htmlFor="geojson-data"
className="mb-1 block text-sm font-medium text-gray-400"
>
Paste GeoJSON Data
</label>
<textarea
id="geojson-data"
className="w-full rounded-lg border border-gray-700 bg-gray-800 p-3 text-sm text-white placeholder:text-gray-500 focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
rows={6}
placeholder='{"type": "FeatureCollection", "features": [...] }'
{...register('geojsonData', {
required: 'GeoJSON data is required',
})}
/>
{errors.geojsonData && (
<p className="mt-1 text-sm text-red-400">
{errors.geojsonData.message}
</p>
)}
</div>

<div className="mt-2 px-1 text-xs">
<p className="text-gray-300">
Enter valid GeoJSON data to create a vector layer. The data should
be in the GeoJSON format with a FeatureCollection containing one or
more features.
</p>
<a
className="text-xs text-blue-400 hover:text-blue-300"
href="https://geojson.org/"
target="_blank"
rel="noreferrer"
>
Learn more about GeoJSON format →
</a>
</div>

<Button
type="submit"
iconIdentifier={IconIdentifier.Plus}
variant="secondary"
className="float-end mt-5 border px-5"
size="sm"
>
Add Layer
</Button>
</Fieldset>
</Modal>
);
};
24 changes: 22 additions & 2 deletions src/layer-panel/layer-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { useContext, useLayoutEffect } from 'react';
import { useLocalStorage, useToggle } from 'usehooks-ts';
import { DropdownMenu, Icon, IconIdentifier } from '../components';
import { GlobalContext } from '../contexts';
import { AddLayerModal } from './add-layer';
import { AddRasterLayerModal } from './add-raster-layer';
import { Draw } from './draw';
import { Layer } from './layer';
import { AddVectorLayerModal } from './add-vector-layer';

export const LayerPanel = () => {
// Context.
Expand All @@ -21,6 +22,12 @@ export const LayerPanel = () => {
const [showAddLayerModal, toggleAddLayerModal, setShowAddLayerModal] =
useToggle(false);

const [
showAddVectorLayerModal,
toggleAddVectorLayerModal,
setShowAddVectorLayerModal,
] = useToggle(false);

// Constants.
const customClassNames = classNames(
'absolute z-10 h-screen-container w-64 p-4 bg-gray-900 border-r border-gray-700 transition-all duration-500 ease-in-out',
Expand Down Expand Up @@ -82,6 +89,10 @@ export const LayerPanel = () => {
<Icon identifier={IconIdentifier.TileLayer} />
Add Raster Layer
</DropdownMenu.Item>
<DropdownMenu.Item onClick={() => setShowAddVectorLayerModal(true)}>
<Icon identifier={IconIdentifier.Layer} />
Add Vector Layer
</DropdownMenu.Item>
</DropdownMenu>
</div>

Expand All @@ -101,7 +112,16 @@ export const LayerPanel = () => {
</div>

{/* Add layer modal */}
<AddLayerModal show={showAddLayerModal} onClose={toggleAddLayerModal} />
<AddRasterLayerModal
show={showAddLayerModal}
onClose={toggleAddLayerModal}
/>

{/* Add vector layer modal */}
<AddVectorLayerModal
show={showAddVectorLayerModal}
onClose={toggleAddVectorLayerModal}
/>
</div>
);
};

0 comments on commit 2d51dab

Please sign in to comment.