From 798a94cc8e0b1278a00d581b0b76ae443892d260 Mon Sep 17 00:00:00 2001 From: lokeshramchand-ctrl Date: Tue, 10 Jun 2025 00:54:38 +0530 Subject: [PATCH 1/4] Abstraction --- src/components/Methods/Layers.tsx | 39 ++++++++++++++++ src/components/OpenLayerMap.tsx | 75 ++++++++++--------------------- 2 files changed, 62 insertions(+), 52 deletions(-) create mode 100644 src/components/Methods/Layers.tsx diff --git a/src/components/Methods/Layers.tsx b/src/components/Methods/Layers.tsx new file mode 100644 index 0000000..5e4f96f --- /dev/null +++ b/src/components/Methods/Layers.tsx @@ -0,0 +1,39 @@ +import { Map } from 'ol'; +import 'ol/ol.css'; +import VectorLayer from 'ol/layer/Vector'; +import VectorSource from 'ol/source/Vector'; +import GeoJSON from 'ol/format/GeoJSON'; +import Style from 'ol/style/Style'; + + +export const loadAndRenderGeoJsonLayer = async ( + url: string, + map: Map, + layerRef: React.MutableRefObject, + style: Style +) => { + try { + const res = await fetch(url); + const geojsonData = await res.json(); + + if (layerRef.current) { + map.removeLayer(layerRef.current); + } + + const vectorSource = new VectorSource({ + features: new GeoJSON().readFeatures(geojsonData, { + featureProjection: 'EPSG:3857', + }), + }); + + const vectorLayer = new VectorLayer({ + source: vectorSource, + style: style, + }); + + map.addLayer(vectorLayer); + layerRef.current = vectorLayer; + } catch (err) { + console.error(`Failed to load layer from ${url}:`, err); + } +}; \ No newline at end of file diff --git a/src/components/OpenLayerMap.tsx b/src/components/OpenLayerMap.tsx index 73926c7..0575c87 100644 --- a/src/components/OpenLayerMap.tsx +++ b/src/components/OpenLayerMap.tsx @@ -7,13 +7,13 @@ import { fromLonLat } from 'ol/proj'; import { Coordinate } from 'ol/coordinate'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; -import GeoJSON from 'ol/format/GeoJSON'; import Style from 'ol/style/Style'; import Stroke from 'ol/style/Stroke'; import Fill from 'ol/style/Fill'; import Icon from 'ol/style/Icon'; import Point from 'ol/geom/Point'; import Feature from 'ol/Feature'; +import {loadAndRenderGeoJsonLayer} from './Methods/Layers' const OpenLayersMap = () => { const [term, setTerm] = useState(''); @@ -85,60 +85,31 @@ useEffect(() => { mapRef.current!.addLayer(markerLayer); markerLayerRef.current = markerLayer; - const radius = 100; + const radius = 1; const parcelUrl = `https://geo.sandag.org/server/rest/services/Hosted/Parcels/FeatureServer/0/query?geometry=${lon},${lat}&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelIntersects&distance=${radius}&units=esriSRUnit_Meter&outFields=*&returnGeometry=true&f=geojson`; const sewerUrl = `https://geo.sandag.org/server/rest/services/Hosted/Sewer_Main_SD/FeatureServer/0/query?geometry=${lon},${lat}&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelIntersects&distance=${radius}&units=esriSRUnit_Meter&outFields=*&returnGeometry=true&f=geojson`; + // 🟩 Parcel Layer + loadAndRenderGeoJsonLayer( + parcelUrl, + mapRef.current!, + parcelLayerRef, + new Style({ + stroke: new Stroke({ color: 'green', width: 1.5 }), + fill: new Fill({ color: 'rgba(0, 255, 0, 0.1)' }), + }) + ); - fetch(parcelUrl) - .then((res) => res.json()) - .then((parcelData) => { - if (parcelLayerRef.current) { - mapRef.current?.removeLayer(parcelLayerRef.current); - } - - const parcelSource = new VectorSource({ - features: new GeoJSON().readFeatures(parcelData, { - featureProjection: 'EPSG:3857', - }), - }); - - const parcelLayer = new VectorLayer({ - source: parcelSource, - style: new Style({ - stroke: new Stroke({ color: 'green', width: 1.5 }), - fill: new Fill({ color: 'rgba(0, 255, 0, 0.1)' }), - }), - }); - - mapRef.current?.addLayer(parcelLayer); - parcelLayerRef.current = parcelLayer; - }); - - fetch(sewerUrl) - .then((res) => res.json()) - .then((sewerData) => { - if (sewerLayerRef.current) { - mapRef.current?.removeLayer(sewerLayerRef.current); - } - - const sewerSource = new VectorSource({ - features: new GeoJSON().readFeatures(sewerData, { - featureProjection: 'EPSG:3857', - }), - }); - - const sewerLayer = new VectorLayer({ - source: sewerSource, - style: new Style({ - stroke: new Stroke({ color: 'blue', width: 2 }), - }), - }); - - mapRef.current?.addLayer(sewerLayer); - sewerLayerRef.current = sewerLayer; - }); - } -}, [address]); + // 🔵 Sewer Layer + loadAndRenderGeoJsonLayer( + sewerUrl, + mapRef.current!, + sewerLayerRef, + new Style({ + stroke: new Stroke({ color: 'blue', width: 2 }), + }) + ); + } + }, [address]); //UI Downstairs From 2f779130eef07b1bca5748532ae0da769a74b5a3 Mon Sep 17 00:00:00 2001 From: lokeshramchand-ctrl Date: Tue, 10 Jun 2025 23:13:41 +0530 Subject: [PATCH 2/4] 10th Jun --- src/components/Methods/Layers.tsx | 1 + src/components/OpenLayerMap.tsx | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Methods/Layers.tsx b/src/components/Methods/Layers.tsx index 5e4f96f..d0ace1e 100644 --- a/src/components/Methods/Layers.tsx +++ b/src/components/Methods/Layers.tsx @@ -16,6 +16,7 @@ export const loadAndRenderGeoJsonLayer = async ( const res = await fetch(url); const geojsonData = await res.json(); + if (layerRef.current) { map.removeLayer(layerRef.current); } diff --git a/src/components/OpenLayerMap.tsx b/src/components/OpenLayerMap.tsx index 0575c87..fa002b0 100644 --- a/src/components/OpenLayerMap.tsx +++ b/src/components/OpenLayerMap.tsx @@ -88,7 +88,6 @@ useEffect(() => { const radius = 1; const parcelUrl = `https://geo.sandag.org/server/rest/services/Hosted/Parcels/FeatureServer/0/query?geometry=${lon},${lat}&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelIntersects&distance=${radius}&units=esriSRUnit_Meter&outFields=*&returnGeometry=true&f=geojson`; const sewerUrl = `https://geo.sandag.org/server/rest/services/Hosted/Sewer_Main_SD/FeatureServer/0/query?geometry=${lon},${lat}&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelIntersects&distance=${radius}&units=esriSRUnit_Meter&outFields=*&returnGeometry=true&f=geojson`; - // 🟩 Parcel Layer loadAndRenderGeoJsonLayer( parcelUrl, mapRef.current!, @@ -99,7 +98,6 @@ useEffect(() => { }) ); - // 🔵 Sewer Layer loadAndRenderGeoJsonLayer( sewerUrl, mapRef.current!, From a6b448b8bb168db7d757bf6040ebf9bd1cfd8c38 Mon Sep 17 00:00:00 2001 From: lokeshramchand-ctrl Date: Wed, 11 Jun 2025 00:52:15 +0530 Subject: [PATCH 3/4] Added all Layers --- src/components/Methods/LayerConfig.tsx | 137 +++++++++++++++++++++++++ src/components/OpenLayerMap.tsx | 124 +++++++++++----------- 2 files changed, 195 insertions(+), 66 deletions(-) create mode 100644 src/components/Methods/LayerConfig.tsx diff --git a/src/components/Methods/LayerConfig.tsx b/src/components/Methods/LayerConfig.tsx new file mode 100644 index 0000000..bfbe441 --- /dev/null +++ b/src/components/Methods/LayerConfig.tsx @@ -0,0 +1,137 @@ +// src/config/layerConfig.ts + +import { Style, Stroke, Fill } from 'ol/style'; + +export interface LayerConfig { + radius: number; + topic: string; + getUrl: (lon: number, lat: number, radius: number) => string; + style: Style; +} + +const buildUrl = + (topic: string) => + (lon: number, lat: number, radius: number) => + `https://geo.sandag.org/server/rest/services/Hosted/${topic}/FeatureServer/0/query?geometry=${lon},${lat}&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelIntersects&distance=${radius}&units=esriSRUnit_Meter&outFields=*&returnGeometry=true&f=geojson`; + +const baseConfigs: Omit[] = [ + { + radius: 1, + topic: 'Parcels', + style: new Style({ + stroke: new Stroke({ color: 'green', width: 1.5 }), + fill: new Fill({ color: 'rgba(0, 255, 0, 0.1)' }), + }), + }, + { + radius: 100, + topic: 'Sewer_Main_SD', + style: new Style({ + stroke: new Stroke({ color: 'purple', width: 2 }), + }), + }, + { + radius: 50, + topic: 'Zoning_Base_SD', + style: new Style({ + stroke: new Stroke({ color: 'red', width: 1 }), + }), + }, + { + radius: 50, + topic: 'Water_Main_SD', + style: new Style({ + stroke: new Stroke({ color: 'blue', width: 5 }), + }), + }, + { + radius: 1000, + topic: 'Sustainable_Development_Areas', + style: new Style({ + stroke: new Stroke({ color: 'green', width: 3 }), + }), + }, + { + radius: 1000, + topic: 'Housing_Solution_Areas', + style: new Style({ + stroke: new Stroke({ color: 'black', width: 2.5 }), + fill: new Fill({ color: 'rgba(255, 200, 0, 0.1)' }), + }), + }, + { + radius: 100000, + topic: 'Promise_Zones_SD', + style: new Style({ + stroke: new Stroke({ color: 'black', width: 2.5 }), + fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), + }), + }, + { + radius: 100000, + topic: 'MHPA_SD', + style: new Style({ + stroke: new Stroke({ color: 'orange', width: 2.5 }), + fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), + }), + }, + { + radius: 100000, + topic: 'Coastal_Zones', + style: new Style({ + stroke: new Stroke({ color: 'skin', width: 2.5 }), + fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), + }), + }, + { + radius: 100000, + topic: 'Community_Plan_Implementation_Overlay_Zone_SD', + style: new Style({ + stroke: new Stroke({ color: 'darkblue', width: 2.5 }), + fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), + }), + + }, + { + radius: 100000, + topic: 'Airport_Influence_Area', + style: new Style({ + stroke: new Stroke({ color: 'darkyellow', width: 2.5 }), + fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), + }), + + }, + { + radius: 100000, + topic: 'Historic_Districts_SD', + style: new Style({ + stroke: new Stroke({ color: 'white', width: 2.5 }), + fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), + }), + + }, + { + radius: 100000, + topic: 'HRB_Designated_Resources_SD', + style: new Style({ + stroke: new Stroke({ color: 'darkgreen', width: 2.5 }), + fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), + }), + + }, + { + radius: 100000, + topic: 'Fire_Hazard_Severity_Zones_SD', + style: new Style({ + stroke: new Stroke({ color: 'yellow', width: 1.5 }), + fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), + }), + + }, + +]; + +export const layerConfigs: LayerConfig[] = baseConfigs.map(cfg => ({ + ...cfg, + getUrl: buildUrl(cfg.topic), +})); diff --git a/src/components/OpenLayerMap.tsx b/src/components/OpenLayerMap.tsx index fa002b0..ce5d3f8 100644 --- a/src/components/OpenLayerMap.tsx +++ b/src/components/OpenLayerMap.tsx @@ -8,12 +8,11 @@ import { Coordinate } from 'ol/coordinate'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import Style from 'ol/style/Style'; -import Stroke from 'ol/style/Stroke'; -import Fill from 'ol/style/Fill'; +import { layerConfigs } from './Methods/LayerConfig'; import Icon from 'ol/style/Icon'; import Point from 'ol/geom/Point'; import Feature from 'ol/Feature'; -import {loadAndRenderGeoJsonLayer} from './Methods/Layers' +import { loadAndRenderGeoJsonLayer } from './Methods/Layers' const OpenLayersMap = () => { const [term, setTerm] = useState(''); @@ -38,74 +37,67 @@ const OpenLayersMap = () => { const [clickedCoordinate, setClickedCoordinate] = useState(); const mapRef = useRef(null); const markerLayerRef = useRef(null); - const parcelLayerRef = useRef(null); - const sewerLayerRef = useRef(null); + const layerRefs = useRef>>( + Object.fromEntries( + layerConfigs.map((layer) => [layer.topic, { current: null }]) + ) + ); -useEffect(() => { - if (!mapRef.current) { - const map = new Map({ - target: mapDivRef.current as HTMLDivElement, - layers: [new TileLayer({ source: new OSM() })], - }); - mapRef.current = map; + useEffect(() => { + if (!mapRef.current) { + const map = new Map({ + target: mapDivRef.current as HTMLDivElement, + layers: [new TileLayer({ source: new OSM() })], + }); + mapRef.current = map; - map.on('click', (e) => { - setClickedCoordinate(e.coordinate); - }); - } - - if (address && address.length > 0) { - const lat = parseFloat(address[0].lat); - const lon = parseFloat(address[0].lon); - const coords = fromLonLat([lon, lat]); - - mapRef.current!.setView(new View({ - center: coords, - zoom: 15, - })); - - const markerFeature = new Feature({ - geometry: new Point(coords), - }); - - markerFeature.setStyle(new Style({ - image: new Icon({ - anchor: [0.5, 1], - src: 'https://cdn-icons-png.flaticon.com/512/684/684908.png', - scale: 0.05, - }), - })); - - const markerSource = new VectorSource({ features: [markerFeature] }); - const markerLayer = new VectorLayer({ source: markerSource }); - - if (markerLayerRef.current) { - mapRef.current!.removeLayer(markerLayerRef.current); + map.on('click', (e) => { + setClickedCoordinate(e.coordinate); + }); } - mapRef.current!.addLayer(markerLayer); - markerLayerRef.current = markerLayer; - const radius = 1; - const parcelUrl = `https://geo.sandag.org/server/rest/services/Hosted/Parcels/FeatureServer/0/query?geometry=${lon},${lat}&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelIntersects&distance=${radius}&units=esriSRUnit_Meter&outFields=*&returnGeometry=true&f=geojson`; - const sewerUrl = `https://geo.sandag.org/server/rest/services/Hosted/Sewer_Main_SD/FeatureServer/0/query?geometry=${lon},${lat}&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelIntersects&distance=${radius}&units=esriSRUnit_Meter&outFields=*&returnGeometry=true&f=geojson`; - loadAndRenderGeoJsonLayer( - parcelUrl, - mapRef.current!, - parcelLayerRef, - new Style({ - stroke: new Stroke({ color: 'green', width: 1.5 }), - fill: new Fill({ color: 'rgba(0, 255, 0, 0.1)' }), - }) - ); + if (address && address.length > 0) { + const lat = parseFloat(address[0].lat); + const lon = parseFloat(address[0].lon); + const coords = fromLonLat([lon, lat]); - loadAndRenderGeoJsonLayer( - sewerUrl, - mapRef.current!, - sewerLayerRef, - new Style({ - stroke: new Stroke({ color: 'blue', width: 2 }), - }) - ); + mapRef.current!.setView(new View({ + center: coords, + zoom: 15, + })); + + const markerFeature = new Feature({ + geometry: new Point(coords), + }); + + markerFeature.setStyle(new Style({ + image: new Icon({ + anchor: [0.5, 1], + src: 'https://cdn-icons-png.flaticon.com/512/684/684908.png', + scale: 0.05, + }), + })); + + const markerSource = new VectorSource({ features: [markerFeature] }); + const markerLayer = new VectorLayer({ source: markerSource }); + + if (markerLayerRef.current) { + mapRef.current!.removeLayer(markerLayerRef.current); + } + mapRef.current!.addLayer(markerLayer); + markerLayerRef.current = markerLayer; + + layerConfigs.forEach((layerConfig) => { + const url = layerConfig.getUrl(lon, lat, layerConfig.radius); + const ref = layerRefs.current[layerConfig.topic]; + + loadAndRenderGeoJsonLayer( + url, + mapRef.current!, + ref, + layerConfig.style + ); + }); } }, [address]); From a7f1066bf10458643ec161f969e93c428cefaa94 Mon Sep 17 00:00:00 2001 From: lokeshramchand-ctrl Date: Sun, 15 Jun 2025 10:08:47 +0530 Subject: [PATCH 4/4] Layers Sidebar --- package-lock.json | 99 ++++++++- package.json | 3 + src/App.tsx | 8 +- src/components/Methods/GeoJson_Load.tsx | 40 ++++ src/components/Methods/LayerConfig.tsx | 137 ------------- src/components/Methods/Layers.tsx | 163 ++++++++++++--- src/components/Methods/Marker.tsx | 49 +++++ src/components/OpenLayerMap.tsx | 255 ++++++++++++++++++------ src/components/UI/Dock.css | 56 ++++++ src/components/UI/Dock.tsx | 205 +++++++++++++++++++ src/components/UI/Map_page.css | 84 ++++++++ src/components/UI/Map_page.tsx | 64 ++++++ 12 files changed, 929 insertions(+), 234 deletions(-) create mode 100644 src/components/Methods/GeoJson_Load.tsx delete mode 100644 src/components/Methods/LayerConfig.tsx create mode 100644 src/components/Methods/Marker.tsx create mode 100644 src/components/UI/Dock.css create mode 100644 src/components/UI/Dock.tsx create mode 100644 src/components/UI/Map_page.css create mode 100644 src/components/UI/Map_page.tsx diff --git a/package-lock.json b/package-lock.json index 9359193..3865fbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,14 @@ "dependencies": { "-": "^0.0.1", "@types/leaflet": "^1.9.18", + "framer-motion": "^12.17.3", "leaflet": "^1.9.4", "ol": "^10.5.0", "ol-ext": "^4.0.31", "react": "^19.1.0", + "react-dock": "^0.8.0", "react-dom": "^19.0.0", + "react-icons": "^5.5.0", "react-leaflet": "^5.0.0", "react-router-dom": "^7.5.0", "save": "^2.9.0", @@ -1316,6 +1319,21 @@ "@types/geojson": "*" } }, + "node_modules/@types/lodash": { + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.17.tgz", + "integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/ol": { "version": "6.5.3", "resolved": "https://registry.npmjs.org/@types/ol/-/ol-6.5.3.tgz", @@ -1338,7 +1356,6 @@ "version": "19.1.1", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.1.tgz", "integrity": "sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==", - "dev": true, "dependencies": { "csstype": "^3.0.2" } @@ -1839,8 +1856,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/debug": { "version": "4.4.0", @@ -2239,6 +2255,33 @@ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "devOptional": true }, + "node_modules/framer-motion": { + "version": "12.17.3", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.17.3.tgz", + "integrity": "sha512-+1GMxQvZGUu20clzYdUWQY+iUWGBFc6UmpxvZBJjWu++rPdj7imXcQITzVs+QLKyjOpLoPOOar2KAOeYL88YQg==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.17.3", + "motion-utils": "^12.12.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/from": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", @@ -2501,6 +2544,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, "node_modules/lodash.assign": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", @@ -2568,6 +2617,21 @@ "node": "*" } }, + "node_modules/motion-dom": { + "version": "12.17.3", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.17.3.tgz", + "integrity": "sha512-nJrH4O9Z6lBSgllD5t+2lF1O8LkBxOq9QSUUmygHJVzHiTLaYpZRgd+U70919IL0CMbFBaFGEJ8hDSwnrmcyZw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.12.1" + } + }, + "node_modules/motion-utils": { + "version": "12.12.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.12.1.tgz", + "integrity": "sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2885,6 +2949,20 @@ "node": ">=0.10.0" } }, + "node_modules/react-dock": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/react-dock/-/react-dock-0.8.0.tgz", + "integrity": "sha512-/sUBWnsNlHIxmxoUHFPq8gcaH5k7MWGuHHeG3UjnXH3UblByxnARlUYewlMBtCUqcmbpPquYE5+GGvaG/hRkkg==", + "license": "MIT", + "dependencies": { + "@types/lodash-es": "^4.17.12", + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "@types/react": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", @@ -2896,6 +2974,15 @@ "react": "^19.1.0" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-leaflet": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", @@ -3255,6 +3342,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 277e8d6..6072c19 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,14 @@ "dependencies": { "-": "^0.0.1", "@types/leaflet": "^1.9.18", + "framer-motion": "^12.17.3", "leaflet": "^1.9.4", "ol": "^10.5.0", "ol-ext": "^4.0.31", "react": "^19.1.0", + "react-dock": "^0.8.0", "react-dom": "^19.0.0", + "react-icons": "^5.5.0", "react-leaflet": "^5.0.0", "react-router-dom": "^7.5.0", "save": "^2.9.0", diff --git a/src/App.tsx b/src/App.tsx index 8244078..62a9ac8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' + import './App.css' import OpenLayerMap from './components/OpenLayerMap'; import OpenLayerEsri from './components/OpenLayerEsri'; @@ -7,12 +7,12 @@ import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; function App() { - //return - //o + return ( } /> + } /> } /> } /> @@ -20,4 +20,4 @@ return ( ); } -export default App +export default App; diff --git a/src/components/Methods/GeoJson_Load.tsx b/src/components/Methods/GeoJson_Load.tsx new file mode 100644 index 0000000..d0ace1e --- /dev/null +++ b/src/components/Methods/GeoJson_Load.tsx @@ -0,0 +1,40 @@ +import { Map } from 'ol'; +import 'ol/ol.css'; +import VectorLayer from 'ol/layer/Vector'; +import VectorSource from 'ol/source/Vector'; +import GeoJSON from 'ol/format/GeoJSON'; +import Style from 'ol/style/Style'; + + +export const loadAndRenderGeoJsonLayer = async ( + url: string, + map: Map, + layerRef: React.MutableRefObject, + style: Style +) => { + try { + const res = await fetch(url); + const geojsonData = await res.json(); + + + if (layerRef.current) { + map.removeLayer(layerRef.current); + } + + const vectorSource = new VectorSource({ + features: new GeoJSON().readFeatures(geojsonData, { + featureProjection: 'EPSG:3857', + }), + }); + + const vectorLayer = new VectorLayer({ + source: vectorSource, + style: style, + }); + + map.addLayer(vectorLayer); + layerRef.current = vectorLayer; + } catch (err) { + console.error(`Failed to load layer from ${url}:`, err); + } +}; \ No newline at end of file diff --git a/src/components/Methods/LayerConfig.tsx b/src/components/Methods/LayerConfig.tsx deleted file mode 100644 index bfbe441..0000000 --- a/src/components/Methods/LayerConfig.tsx +++ /dev/null @@ -1,137 +0,0 @@ -// src/config/layerConfig.ts - -import { Style, Stroke, Fill } from 'ol/style'; - -export interface LayerConfig { - radius: number; - topic: string; - getUrl: (lon: number, lat: number, radius: number) => string; - style: Style; -} - -const buildUrl = - (topic: string) => - (lon: number, lat: number, radius: number) => - `https://geo.sandag.org/server/rest/services/Hosted/${topic}/FeatureServer/0/query?geometry=${lon},${lat}&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelIntersects&distance=${radius}&units=esriSRUnit_Meter&outFields=*&returnGeometry=true&f=geojson`; - -const baseConfigs: Omit[] = [ - { - radius: 1, - topic: 'Parcels', - style: new Style({ - stroke: new Stroke({ color: 'green', width: 1.5 }), - fill: new Fill({ color: 'rgba(0, 255, 0, 0.1)' }), - }), - }, - { - radius: 100, - topic: 'Sewer_Main_SD', - style: new Style({ - stroke: new Stroke({ color: 'purple', width: 2 }), - }), - }, - { - radius: 50, - topic: 'Zoning_Base_SD', - style: new Style({ - stroke: new Stroke({ color: 'red', width: 1 }), - }), - }, - { - radius: 50, - topic: 'Water_Main_SD', - style: new Style({ - stroke: new Stroke({ color: 'blue', width: 5 }), - }), - }, - { - radius: 1000, - topic: 'Sustainable_Development_Areas', - style: new Style({ - stroke: new Stroke({ color: 'green', width: 3 }), - }), - }, - { - radius: 1000, - topic: 'Housing_Solution_Areas', - style: new Style({ - stroke: new Stroke({ color: 'black', width: 2.5 }), - fill: new Fill({ color: 'rgba(255, 200, 0, 0.1)' }), - }), - }, - { - radius: 100000, - topic: 'Promise_Zones_SD', - style: new Style({ - stroke: new Stroke({ color: 'black', width: 2.5 }), - fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), - }), - }, - { - radius: 100000, - topic: 'MHPA_SD', - style: new Style({ - stroke: new Stroke({ color: 'orange', width: 2.5 }), - fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), - }), - }, - { - radius: 100000, - topic: 'Coastal_Zones', - style: new Style({ - stroke: new Stroke({ color: 'skin', width: 2.5 }), - fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), - }), - }, - { - radius: 100000, - topic: 'Community_Plan_Implementation_Overlay_Zone_SD', - style: new Style({ - stroke: new Stroke({ color: 'darkblue', width: 2.5 }), - fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), - }), - - }, - { - radius: 100000, - topic: 'Airport_Influence_Area', - style: new Style({ - stroke: new Stroke({ color: 'darkyellow', width: 2.5 }), - fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), - }), - - }, - { - radius: 100000, - topic: 'Historic_Districts_SD', - style: new Style({ - stroke: new Stroke({ color: 'white', width: 2.5 }), - fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), - }), - - }, - { - radius: 100000, - topic: 'HRB_Designated_Resources_SD', - style: new Style({ - stroke: new Stroke({ color: 'darkgreen', width: 2.5 }), - fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), - }), - - }, - { - radius: 100000, - topic: 'Fire_Hazard_Severity_Zones_SD', - style: new Style({ - stroke: new Stroke({ color: 'yellow', width: 1.5 }), - fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), - }), - - }, - -]; - -export const layerConfigs: LayerConfig[] = baseConfigs.map(cfg => ({ - ...cfg, - getUrl: buildUrl(cfg.topic), -})); diff --git a/src/components/Methods/Layers.tsx b/src/components/Methods/Layers.tsx index d0ace1e..bfbe441 100644 --- a/src/components/Methods/Layers.tsx +++ b/src/components/Methods/Layers.tsx @@ -1,40 +1,137 @@ -import { Map } from 'ol'; -import 'ol/ol.css'; -import VectorLayer from 'ol/layer/Vector'; -import VectorSource from 'ol/source/Vector'; -import GeoJSON from 'ol/format/GeoJSON'; -import Style from 'ol/style/Style'; +// src/config/layerConfig.ts +import { Style, Stroke, Fill } from 'ol/style'; -export const loadAndRenderGeoJsonLayer = async ( - url: string, - map: Map, - layerRef: React.MutableRefObject, - style: Style -) => { - try { - const res = await fetch(url); - const geojsonData = await res.json(); +export interface LayerConfig { + radius: number; + topic: string; + getUrl: (lon: number, lat: number, radius: number) => string; + style: Style; +} +const buildUrl = + (topic: string) => + (lon: number, lat: number, radius: number) => + `https://geo.sandag.org/server/rest/services/Hosted/${topic}/FeatureServer/0/query?geometry=${lon},${lat}&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelIntersects&distance=${radius}&units=esriSRUnit_Meter&outFields=*&returnGeometry=true&f=geojson`; - if (layerRef.current) { - map.removeLayer(layerRef.current); - } +const baseConfigs: Omit[] = [ + { + radius: 1, + topic: 'Parcels', + style: new Style({ + stroke: new Stroke({ color: 'green', width: 1.5 }), + fill: new Fill({ color: 'rgba(0, 255, 0, 0.1)' }), + }), + }, + { + radius: 100, + topic: 'Sewer_Main_SD', + style: new Style({ + stroke: new Stroke({ color: 'purple', width: 2 }), + }), + }, + { + radius: 50, + topic: 'Zoning_Base_SD', + style: new Style({ + stroke: new Stroke({ color: 'red', width: 1 }), + }), + }, + { + radius: 50, + topic: 'Water_Main_SD', + style: new Style({ + stroke: new Stroke({ color: 'blue', width: 5 }), + }), + }, + { + radius: 1000, + topic: 'Sustainable_Development_Areas', + style: new Style({ + stroke: new Stroke({ color: 'green', width: 3 }), + }), + }, + { + radius: 1000, + topic: 'Housing_Solution_Areas', + style: new Style({ + stroke: new Stroke({ color: 'black', width: 2.5 }), + fill: new Fill({ color: 'rgba(255, 200, 0, 0.1)' }), + }), + }, + { + radius: 100000, + topic: 'Promise_Zones_SD', + style: new Style({ + stroke: new Stroke({ color: 'black', width: 2.5 }), + fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), + }), + }, + { + radius: 100000, + topic: 'MHPA_SD', + style: new Style({ + stroke: new Stroke({ color: 'orange', width: 2.5 }), + fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), + }), + }, + { + radius: 100000, + topic: 'Coastal_Zones', + style: new Style({ + stroke: new Stroke({ color: 'skin', width: 2.5 }), + fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), + }), + }, + { + radius: 100000, + topic: 'Community_Plan_Implementation_Overlay_Zone_SD', + style: new Style({ + stroke: new Stroke({ color: 'darkblue', width: 2.5 }), + fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), + }), - const vectorSource = new VectorSource({ - features: new GeoJSON().readFeatures(geojsonData, { - featureProjection: 'EPSG:3857', - }), - }); + }, + { + radius: 100000, + topic: 'Airport_Influence_Area', + style: new Style({ + stroke: new Stroke({ color: 'darkyellow', width: 2.5 }), + fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), + }), - const vectorLayer = new VectorLayer({ - source: vectorSource, - style: style, - }); + }, + { + radius: 100000, + topic: 'Historic_Districts_SD', + style: new Style({ + stroke: new Stroke({ color: 'white', width: 2.5 }), + fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), + }), - map.addLayer(vectorLayer); - layerRef.current = vectorLayer; - } catch (err) { - console.error(`Failed to load layer from ${url}:`, err); - } -}; \ No newline at end of file + }, + { + radius: 100000, + topic: 'HRB_Designated_Resources_SD', + style: new Style({ + stroke: new Stroke({ color: 'darkgreen', width: 2.5 }), + fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), + }), + + }, + { + radius: 100000, + topic: 'Fire_Hazard_Severity_Zones_SD', + style: new Style({ + stroke: new Stroke({ color: 'yellow', width: 1.5 }), + fill: new Fill({ color: 'rgba(116, 3, 255, 0.1)' }), + }), + + }, + +]; + +export const layerConfigs: LayerConfig[] = baseConfigs.map(cfg => ({ + ...cfg, + getUrl: buildUrl(cfg.topic), +})); diff --git a/src/components/Methods/Marker.tsx b/src/components/Methods/Marker.tsx new file mode 100644 index 0000000..b658996 --- /dev/null +++ b/src/components/Methods/Marker.tsx @@ -0,0 +1,49 @@ +// src/Methods/addMarker.ts + +import Feature from 'ol/Feature'; +import Point from 'ol/geom/Point'; +import VectorLayer from 'ol/layer/Vector'; +import VectorSource from 'ol/source/Vector'; +import Style from 'ol/style/Style'; +import Icon from 'ol/style/Icon'; +import { Map } from 'ol'; +import { Coordinate } from 'ol/coordinate'; + +/** + * Adds a custom icon marker to the map at a given coordinate, + * replacing the previous marker layer if one exists. + */ +export const addMarker = ( + coords: Coordinate, + map: Map, + existingMarkerLayer: VectorLayer | null +): VectorLayer => { + // Create feature for the marker + const markerFeature = new Feature({ + geometry: new Point(coords), + }); + + markerFeature.setStyle( + new Style({ + image: new Icon({ + anchor: [0.5, 1], + src: 'https://cdn-icons-png.flaticon.com/512/684/684908.png', + scale: 0.05, + }), + }) + ); + + // Create new layer with this feature + const markerSource = new VectorSource({ features: [markerFeature] }); + const markerLayer = new VectorLayer({ source: markerSource }); + + // Remove old marker layer if it exists + if (existingMarkerLayer) { + map.removeLayer(existingMarkerLayer); + } + + // Add the new marker layer + map.addLayer(markerLayer); + + return markerLayer; +}; diff --git a/src/components/OpenLayerMap.tsx b/src/components/OpenLayerMap.tsx index ce5d3f8..94eea53 100644 --- a/src/components/OpenLayerMap.tsx +++ b/src/components/OpenLayerMap.tsx @@ -6,33 +6,36 @@ import { OSM } from 'ol/source'; import { fromLonLat } from 'ol/proj'; import { Coordinate } from 'ol/coordinate'; import VectorLayer from 'ol/layer/Vector'; -import VectorSource from 'ol/source/Vector'; -import Style from 'ol/style/Style'; -import { layerConfigs } from './Methods/LayerConfig'; -import Icon from 'ol/style/Icon'; -import Point from 'ol/geom/Point'; -import Feature from 'ol/Feature'; -import { loadAndRenderGeoJsonLayer } from './Methods/Layers' +import { layerConfigs } from './Methods/Layers'; +import { loadAndRenderGeoJsonLayer } from './Methods/GeoJson_Load' +import { addMarker } from './Methods/Marker'; const OpenLayersMap = () => { const [term, setTerm] = useState(''); const [address, setAddress] = useState(); + const [layerVisibility, setLayerVisibility] = useState>({}); + const [searchCompleted, setSearchCompleted] = useState(false); - //Submit Button Logic + // Submit Button Logic const submitForm = async (event: React.FormEvent) => { // Preventing the page from reloading event.preventDefault(); - const response = await fetch(`https://nominatim.openstreetmap.org/search?q=${term}&format=json&polygon=1&addressdetails=1`); const json = await response.json(); console.log(json); setAddress(json); console.log(address); + setSearchCompleted(true); + + // Initialize all layers as visible + const initialVisibility = Object.fromEntries( + layerConfigs.map(layer => [layer.topic, true]) + ); + setLayerVisibility(initialVisibility); + alert(term); } - //Submit Button Logic - //const [data, setData] = useState({}); const mapDivRef = useRef(null); const [clickedCoordinate, setClickedCoordinate] = useState(); const mapRef = useRef(null); @@ -43,6 +46,26 @@ const OpenLayersMap = () => { ) ); + // Toggle layer visibility + const toggleLayer = (layerTopic: string) => { + const layerRef = layerRefs.current[layerTopic]; + const isCurrentlyVisible = layerVisibility[layerTopic]; + + if (layerRef.current) { + layerRef.current.setVisible(!isCurrentlyVisible); + } + + setLayerVisibility(prev => ({ + ...prev, + [layerTopic]: !isCurrentlyVisible + })); + }; + + // Format layer name for display + const formatLayerName = (topic: string) => { + return topic.replace(/_/g, ' ').replace(/SD$/, '').trim(); + }; + useEffect(() => { if (!mapRef.current) { const map = new Map({ @@ -50,7 +73,6 @@ const OpenLayersMap = () => { layers: [new TileLayer({ source: new OSM() })], }); mapRef.current = map; - map.on('click', (e) => { setClickedCoordinate(e.coordinate); }); @@ -60,37 +82,16 @@ const OpenLayersMap = () => { const lat = parseFloat(address[0].lat); const lon = parseFloat(address[0].lon); const coords = fromLonLat([lon, lat]); - mapRef.current!.setView(new View({ center: coords, zoom: 15, })); - const markerFeature = new Feature({ - geometry: new Point(coords), - }); - - markerFeature.setStyle(new Style({ - image: new Icon({ - anchor: [0.5, 1], - src: 'https://cdn-icons-png.flaticon.com/512/684/684908.png', - scale: 0.05, - }), - })); - - const markerSource = new VectorSource({ features: [markerFeature] }); - const markerLayer = new VectorLayer({ source: markerSource }); - - if (markerLayerRef.current) { - mapRef.current!.removeLayer(markerLayerRef.current); - } - mapRef.current!.addLayer(markerLayer); - markerLayerRef.current = markerLayer; - + markerLayerRef.current = addMarker(coords, mapRef.current!, markerLayerRef.current); + layerConfigs.forEach((layerConfig) => { const url = layerConfig.getUrl(lon, lat, layerConfig.radius); const ref = layerRefs.current[layerConfig.topic]; - loadAndRenderGeoJsonLayer( url, mapRef.current!, @@ -101,31 +102,171 @@ const OpenLayersMap = () => { } }, [address]); - - //UI Downstairs + // UI return ( - <> -
-
-
- setTerm(e.target.value)} - type="text" - placeholder="Enter a term" - className="input" - /> - -
+
+ {/* Sidebar */} +
+

+ Map Layers +

+ + {/* Search Form */} +
+ setTerm(e.target.value)} + type="text" + placeholder="Enter address to search" + style={{ + width: '100%', + padding: '10px', + border: '1px solid #ddd', + borderRadius: '4px', + marginBottom: '10px', + fontSize: '14px' + }} + /> + +
+ + {/* Layer Toggle List */} +
+

+ Available Layers ({layerConfigs.length}) +

+ + {layerConfigs.map((layer) => ( +
+ + {formatLayerName(layer.topic)} + + + {searchCompleted && ( + + )} + + {!searchCompleted && ( + + Search first + + )} +
+ ))}
+ +
- {clickedCoordinate && ( - - You clicked at: {clickedCoordinate[0]} /{' '} {clickedCoordinate[1]} - - )} - + + {/* Map Container */} +
+
+ + {clickedCoordinate && ( +
+ Coordinates: {clickedCoordinate[0].toFixed(2)} / {clickedCoordinate[1].toFixed(2)} +
+ )} +
+
); }; -export default OpenLayersMap; +export default OpenLayersMap; \ No newline at end of file diff --git a/src/components/UI/Dock.css b/src/components/UI/Dock.css new file mode 100644 index 0000000..d2c3bd7 --- /dev/null +++ b/src/components/UI/Dock.css @@ -0,0 +1,56 @@ +.dock-outer { + margin: 0 0.5rem; + display: flex; + max-width: 100%; + align-items: center; +} + +.dock-panel { + position: absolute; + bottom: 0.5rem; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: flex-end; + width: fit-content; + gap: 1rem; + border-radius: 1rem; + background-color: #060010; + border: 1px solid #222; + padding: 0 0.5rem 0.5rem; +} + +.dock-item { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 10px; + background-color: #060010; + border: 1px solid #222; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); + cursor: pointer; + outline: none; +} + +.dock-icon { + display: flex; + align-items: center; + justify-content: center; +} + +.dock-label { + position: absolute; + top: -1.5rem; + left: 50%; + width: fit-content; + white-space: pre; + border-radius: 0.375rem; + border: 1px solid #222; + background-color: #060010; + padding: 0.125rem 0.5rem; + font-size: 0.75rem; + color: #fff; + transform: translateX(-50%); +} \ No newline at end of file diff --git a/src/components/UI/Dock.tsx b/src/components/UI/Dock.tsx new file mode 100644 index 0000000..a16e807 --- /dev/null +++ b/src/components/UI/Dock.tsx @@ -0,0 +1,205 @@ +"use client"; +import "./Dock.css"; + +import { + motion, + MotionValue, + useMotionValue, + useSpring, + useTransform, + type SpringOptions, + AnimatePresence, +} from "framer-motion"; +import React, { + Children, + cloneElement, + useEffect, + useMemo, + useRef, + useState, +} from "react"; + + +export type DockItemData = { + icon: React.ReactNode; + label: React.ReactNode; + onClick: () => void; + className?: string; +}; + +export type DockProps = { + items: DockItemData[]; + className?: string; + distance?: number; + panelHeight?: number; + baseItemSize?: number; + dockHeight?: number; + magnification?: number; + spring?: SpringOptions; +}; + +type DockItemProps = { + className?: string; + children: React.ReactNode; + onClick?: () => void; + mouseX: MotionValue; + spring: SpringOptions; + distance: number; + baseItemSize: number; + magnification: number; +}; + +function DockItem({ + children, + className = "", + onClick, + mouseX, + spring, + distance, + magnification, + baseItemSize, +}: DockItemProps) { + const ref = useRef(null); + const isHovered = useMotionValue(0); + + const mouseDistance = useTransform(mouseX, (val) => { + const rect = ref.current?.getBoundingClientRect() ?? { + x: 0, + width: baseItemSize, + }; + return val - rect.x - baseItemSize / 2; + }); + + const targetSize = useTransform( + mouseDistance, + [-distance, 0, distance], + [baseItemSize, magnification, baseItemSize] + ); + const size = useSpring(targetSize, spring); + + return ( + isHovered.set(1)} + onHoverEnd={() => isHovered.set(0)} + onFocus={() => isHovered.set(1)} + onBlur={() => isHovered.set(0)} + onClick={onClick} + className={`dock-item ${className}`} + tabIndex={0} + role="button" + aria-haspopup="true" + > + {Children.map(children, (child) => + cloneElement(child as React.ReactElement, { }) + )} + + ); +} + +type DockLabelProps = { + className?: string; + children: React.ReactNode; +}; + +function DockLabel({ children, className = "", ...rest }: DockLabelProps) { + const { isHovered } = rest as { isHovered: MotionValue }; + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + const unsubscribe = isHovered.on("change", (latest) => { + setIsVisible(latest === 1); + }); + return () => unsubscribe(); + }, [isHovered]); + + return ( + + {isVisible && ( + + {children} + + )} + + ); +} + +type DockIconProps = { + className?: string; + children: React.ReactNode; +}; + +function DockIcon({ children, className = "" }: DockIconProps) { + return
{children}
; +} + +export default function Dock({ + items, + className = "", + spring = { mass: 0.1, stiffness: 150, damping: 12 }, + magnification = 70, + distance = 200, + panelHeight = 68, + dockHeight = 256, + baseItemSize = 50, +}: DockProps) { + const mouseX = useMotionValue(Infinity); + const isHovered = useMotionValue(0); + + const maxHeight = useMemo( + () => Math.max(dockHeight, magnification + magnification / 2 + 4), + [magnification, dockHeight] + ); + const heightRow = useTransform(isHovered, [0, 1], [panelHeight, maxHeight]); + const height = useSpring(heightRow, spring); + + return ( + + { + isHovered.set(1); + mouseX.set(pageX); + }} + onMouseLeave={() => { + isHovered.set(0); + mouseX.set(Infinity); + }} + className={`dock-panel ${className}`} + style={{ height: panelHeight }} + role="toolbar" + aria-label="Application dock" + > + {items.map((item, index) => ( + + {item.icon} + {item.label} + + ))} + + + ); +} diff --git a/src/components/UI/Map_page.css b/src/components/UI/Map_page.css new file mode 100644 index 0000000..ac68790 --- /dev/null +++ b/src/components/UI/Map_page.css @@ -0,0 +1,84 @@ +/* Full height setup */ +html, body, #root { + margin: 0; + padding: 0; + height: 100%; + width: 100%; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + background-color: #f0f2f5; +} + +/* Page container with space for navbar and sidebar */ +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + height: 100vh; + padding-top: 80px; /* Reserve space for a top bar */ + padding-left: 240px; /* Reserve space for a sidebar if needed */ + box-sizing: border-box; +} + +/* Card-like map container */ +.map-container { + width: 700px; + height: 700px; + background-color: white; + border-radius: 20px; + box-shadow: 0 10px 20px rgba(0,0,0,0.05); + border: 1px solid #e0e0e0; + overflow: hidden; + position: relative; +} + +/* The actual OpenLayers map inside the container */ +.map { + width: 700px; + height: 700px; + +} +.submit { + background: linear-gradient(135deg, #007AFF, #0051D5); /* iOS blue gradient */ + color: white; + padding: 12px 28px; + border: none; + border-radius: 12px; + font-size: 16px; + font-weight: 500; + cursor: pointer; + box-shadow: 0 6px 12px rgba(0, 122, 255, 0.3); + transition: all 0.2s ease-in-out; + outline: none; +} + +.submit:hover { + background: linear-gradient(135deg, #005AE0, #0042B1); + transform: translateY(-2px); + box-shadow: 0 10px 18px rgba(0, 122, 255, 0.4); +} + +.submit:active { + transform: translateY(1px); + box-shadow: 0 4px 10px rgba(0, 122, 255, 0.2); +} + +.input-field { + width: 80%; + padding: 12px 16px; + font-size: 16px; + border: 1px solid #d1d1d6; + border-radius: 12px; + outline: none; + margin-bottom: 20px; + background-color: #f9f9f9; + color: #333; + transition: border 0.2s, box-shadow 0.2s; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); +} + +.input-field:focus { + border-color: #007AFF; + box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.2); + background-color: #fff; +} diff --git a/src/components/UI/Map_page.tsx b/src/components/UI/Map_page.tsx new file mode 100644 index 0000000..6741dd3 --- /dev/null +++ b/src/components/UI/Map_page.tsx @@ -0,0 +1,64 @@ +import { useEffect, useRef } from 'react'; +import { Map, View } from 'ol'; +import TileLayer from 'ol/layer/Tile'; +import { OSM } from 'ol/source'; +import { fromLonLat } from 'ol/proj'; +import './Map_page.css'; +import Dock from './Dock'; +// ✅ Make sure these are imported from 'react-icons/vsc' +import { VscHome, VscArchive, VscAccount, VscSettingsGear } from 'react-icons/vsc'; + +const Map_page = () => { + const mapRef = useRef(null); + const mapDivRef = useRef(null); + + useEffect(() => { + if (!mapRef.current) { + const map = new Map({ + target: mapDivRef.current as HTMLDivElement, + layers: [new TileLayer({ source: new OSM() })], + view: new View({ + center: fromLonLat([-117.1611, 32.7157]), // Default to San Diego + zoom: 10, + }), + }); + mapRef.current = map; + } + }, []); + + const items = [ + { icon: , label: 'Home', onClick: () => alert('Home!') }, + { icon: , label: 'Archive', onClick: () => alert('Archive!') }, + { icon: , label: 'Profile', onClick: () => alert('Profile!') }, + { icon: , label: 'Settings', onClick: () => alert('Settings!') }, + ]; + + return ( + <> +
+
+
+
+ +
+ + +
+ + +
+ + ); +}; + +export default Map_page;