merge from lol
This commit is contained in:
commit
ca22a9df76
11 changed files with 967 additions and 134 deletions
99
package-lock.json
generated
99
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 <OpenLayerMap />
|
||||
//o
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<OpenLayerMap />} />
|
||||
<Route path="/open" element={<OpenLayerMap />} />
|
||||
<Route path="/esri" element={<OpenLayerEsri />} />
|
||||
<Route path="/legi" element={<LegiScanAPI />} />
|
||||
</Routes>
|
||||
|
|
@ -20,4 +20,4 @@ return (
|
|||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
|
|
|
|||
40
src/components/Methods/GeoJson_Load.tsx
Normal file
40
src/components/Methods/GeoJson_Load.tsx
Normal file
|
|
@ -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<VectorLayer | null>,
|
||||
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);
|
||||
}
|
||||
};
|
||||
137
src/components/Methods/Layers.tsx
Normal file
137
src/components/Methods/Layers.tsx
Normal file
|
|
@ -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<LayerConfig, 'getUrl'>[] = [
|
||||
{
|
||||
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),
|
||||
}));
|
||||
49
src/components/Methods/Marker.tsx
Normal file
49
src/components/Methods/Marker.tsx
Normal file
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -6,40 +6,65 @@ 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 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 { layerConfigs } from './Methods/Layers';
|
||||
import { loadAndRenderGeoJsonLayer } from './Methods/GeoJson_Load'
|
||||
import { addMarker } from './Methods/Marker';
|
||||
|
||||
const OpenLayersMap = () => {
|
||||
const [term, setTerm] = useState<string>('');
|
||||
const [address, setAddress] = useState();
|
||||
const [layerVisibility, setLayerVisibility] = useState<Record<string, boolean>>({});
|
||||
const [searchCompleted, setSearchCompleted] = useState(false);
|
||||
|
||||
// Submit Button Logic
|
||||
const submitForm = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
// 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<HTMLDivElement>(null);
|
||||
const [clickedCoordinate, setClickedCoordinate] = useState<Coordinate>();
|
||||
const mapRef = useRef<Map | null>(null);
|
||||
const markerLayerRef = useRef<VectorLayer | null>(null);
|
||||
const parcelLayerRef = useRef<VectorLayer | null>(null);
|
||||
const sewerLayerRef = useRef<VectorLayer | null>(null);
|
||||
const layerRefs = useRef<Record<string, React.MutableRefObject<VectorLayer | null>>>(
|
||||
Object.fromEntries(
|
||||
layerConfigs.map((layer) => [layer.topic, { current: null }])
|
||||
)
|
||||
);
|
||||
|
||||
// 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) {
|
||||
|
|
@ -48,7 +73,6 @@ useEffect(() => {
|
|||
layers: [new TileLayer({ source: new OSM() })],
|
||||
});
|
||||
mapRef.current = map;
|
||||
|
||||
map.on('click', (e) => {
|
||||
setClickedCoordinate(e.coordinate);
|
||||
});
|
||||
|
|
@ -58,112 +82,190 @@ useEffect(() => {
|
|||
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),
|
||||
});
|
||||
markerLayerRef.current = addMarker(coords, mapRef.current!, markerLayerRef.current);
|
||||
|
||||
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;
|
||||
|
||||
const radius = 10;
|
||||
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`;
|
||||
|
||||
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;
|
||||
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]);
|
||||
|
||||
|
||||
//UI Downstairs
|
||||
// UI
|
||||
return (
|
||||
<>
|
||||
<div ref={mapDivRef} className='map'>
|
||||
<div className="container">
|
||||
<form onSubmit={submitForm}>
|
||||
<div style={{ display: 'flex', height: '100vh', width: '100vw' }}>
|
||||
{/* Sidebar */}
|
||||
<div style={{
|
||||
width: '320px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRight: '1px solid #dee2e6',
|
||||
padding: '20px',
|
||||
overflowY: 'auto',
|
||||
boxShadow: '2px 0 5px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
<h3 style={{
|
||||
margin: '0 0 20px 0',
|
||||
color: '#333',
|
||||
fontSize: '18px',
|
||||
fontWeight: '600'
|
||||
}}>
|
||||
Map Layers
|
||||
</h3>
|
||||
|
||||
{/* Search Form */}
|
||||
<form onSubmit={submitForm} style={{ marginBottom: '20px' }}>
|
||||
<input
|
||||
value={term}
|
||||
onChange={(e) => setTerm(e.target.value)}
|
||||
type="text"
|
||||
placeholder="Enter a term"
|
||||
className="input"
|
||||
placeholder="Enter address to search"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '10px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
<button type="submit" className="btn">Submit</button>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px',
|
||||
backgroundColor: '#007bff',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{clickedCoordinate && (
|
||||
<span className='coordinates-container'>
|
||||
You clicked at: {clickedCoordinate[0]} /{' '} {clickedCoordinate[1]}
|
||||
|
||||
{/* Layer Toggle List */}
|
||||
<div>
|
||||
<h4 style={{
|
||||
margin: '0 0 15px 0',
|
||||
color: '#666',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px'
|
||||
}}>
|
||||
Available Layers ({layerConfigs.length})
|
||||
</h4>
|
||||
|
||||
{layerConfigs.map((layer) => (
|
||||
<div key={layer.topic} style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '10px 0',
|
||||
borderBottom: '1px solid #eee'
|
||||
}}>
|
||||
<span style={{
|
||||
fontSize: '13px',
|
||||
color: '#333',
|
||||
flex: 1,
|
||||
marginRight: '10px'
|
||||
}}>
|
||||
{formatLayerName(layer.topic)}
|
||||
</span>
|
||||
|
||||
{searchCompleted && (
|
||||
<label style={{
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
width: '44px',
|
||||
height: '24px'
|
||||
}}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={layerVisibility[layer.topic] || false}
|
||||
onChange={() => toggleLayer(layer.topic)}
|
||||
style={{ opacity: 0, width: 0, height: 0 }}
|
||||
/>
|
||||
<span style={{
|
||||
position: 'absolute',
|
||||
cursor: 'pointer',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: layerVisibility[layer.topic] ? '#007bff' : '#ccc',
|
||||
borderRadius: '24px',
|
||||
transition: '0.3s',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<span style={{
|
||||
position: 'absolute',
|
||||
content: '',
|
||||
height: '18px',
|
||||
width: '18px',
|
||||
left: layerVisibility[layer.topic] ? '23px' : '3px',
|
||||
bottom: '3px',
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '50%',
|
||||
transition: '0.3s',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
|
||||
}} />
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
|
||||
{!searchCompleted && (
|
||||
<span style={{
|
||||
fontSize: '11px',
|
||||
color: '#999',
|
||||
fontStyle: 'italic'
|
||||
}}>
|
||||
Search first
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{/* Map Container */}
|
||||
<div style={{ flex: 1, position: 'relative' }}>
|
||||
<div ref={mapDivRef} style={{ width: '100%', height: '100%' }} />
|
||||
|
||||
{clickedCoordinate && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: '20px',
|
||||
left: '20px',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
color: 'white',
|
||||
padding: '10px 15px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
Coordinates: {clickedCoordinate[0].toFixed(2)} / {clickedCoordinate[1].toFixed(2)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
56
src/components/UI/Dock.css
Normal file
56
src/components/UI/Dock.css
Normal file
|
|
@ -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%);
|
||||
}
|
||||
205
src/components/UI/Dock.tsx
Normal file
205
src/components/UI/Dock.tsx
Normal file
|
|
@ -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<HTMLDivElement>(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 (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
}}
|
||||
onHoverStart={() => 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, { })
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
type DockLabelProps = {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
function DockLabel({ children, className = "", ...rest }: DockLabelProps) {
|
||||
const { isHovered } = rest as { isHovered: MotionValue<number> };
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = isHovered.on("change", (latest) => {
|
||||
setIsVisible(latest === 1);
|
||||
});
|
||||
return () => unsubscribe();
|
||||
}, [isHovered]);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 0 }}
|
||||
animate={{ opacity: 1, y: -10 }}
|
||||
exit={{ opacity: 0, y: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className={`dock-label ${className}`}
|
||||
role="tooltip"
|
||||
style={{ x: "-50%" }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
||||
type DockIconProps = {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
function DockIcon({ children, className = "" }: DockIconProps) {
|
||||
return <div className={`dock-icon ${className}`}>{children}</div>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<motion.div
|
||||
style={{ height, scrollbarWidth: "none" }}
|
||||
className="dock-outer"
|
||||
>
|
||||
<motion.div
|
||||
onMouseMove={({ pageX }) => {
|
||||
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) => (
|
||||
<DockItem
|
||||
key={index}
|
||||
onClick={item.onClick}
|
||||
className={item.className}
|
||||
mouseX={mouseX}
|
||||
spring={spring}
|
||||
distance={distance}
|
||||
magnification={magnification}
|
||||
baseItemSize={baseItemSize}
|
||||
>
|
||||
<DockIcon>{item.icon}</DockIcon>
|
||||
<DockLabel>{item.label}</DockLabel>
|
||||
</DockItem>
|
||||
))}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
84
src/components/UI/Map_page.css
Normal file
84
src/components/UI/Map_page.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
64
src/components/UI/Map_page.tsx
Normal file
64
src/components/UI/Map_page.tsx
Normal file
|
|
@ -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<Map | null>(null);
|
||||
const mapDivRef = useRef<HTMLDivElement>(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: <VscHome size={18} />, label: 'Home', onClick: () => alert('Home!') },
|
||||
{ icon: <VscArchive size={18} />, label: 'Archive', onClick: () => alert('Archive!') },
|
||||
{ icon: <VscAccount size={18} />, label: 'Profile', onClick: () => alert('Profile!') },
|
||||
{ icon: <VscSettingsGear size={18} />, label: 'Settings', onClick: () => alert('Settings!') },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="container">
|
||||
<div className="map-container">
|
||||
<div ref={mapDivRef} className="map" />
|
||||
</div>
|
||||
|
||||
<div className="sidebar">
|
||||
<input type="text" placeholder="Enter a location..." className="input-field" />
|
||||
<button className="submit">Submit</button>
|
||||
</div>
|
||||
|
||||
<Dock
|
||||
|
||||
items={items}
|
||||
|
||||
panelHeight={68}
|
||||
|
||||
baseItemSize={50}
|
||||
|
||||
magnification={70}
|
||||
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Map_page;
|
||||
Loading…
Reference in a new issue