Skip to content

Commit

Permalink
Map Update (#186)
Browse files Browse the repository at this point in the history
  • Loading branch information
CelineMP authored May 28, 2024
2 parents bc5f984 + 5582db2 commit 956d763
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 37 deletions.
37 changes: 32 additions & 5 deletions frontend/src/components/DatasetsList/DatasetsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import { TabProps, TabsContext } from "../../contexts/TabsContext";

import "./DatasetsList.css";
import { AlertContext } from "../../contexts/AlertContext";
import { FeatureCollection } from "geojson";
import L, { Icon as LIcon, DivIcon } from "leaflet";
import { createRoot } from "react-dom/client";
import { flushSync } from "react-dom";

// Dataset Type
export type Dataset = {
Expand All @@ -20,19 +24,42 @@ export type Dataset = {
description: string;
type: string;
datasetIcon: Icon;
markerIcon: Icon | undefined;
data: JSON[];
markerIcon: LIcon | DivIcon | undefined;
data: FeatureCollection;
};

// Define an empty FeatureCollection
const emptyFeatureCollection: FeatureCollection = {
type: "FeatureCollection",
features: [],
};

// Utility function to render a React component to HTML string
const renderToHtml = (Component: React.FC) => {
const div = document.createElement("div");
const root = createRoot(div);
flushSync(() => {
root.render(<Component />);
});
return div.innerHTML;
};

const divIconChargingStation: DivIcon = L.divIcon({
html: renderToHtml(() => <ChargingStation size={32} weight="duotone" />),
className: "", // Optional: add a custom class name
iconSize: [34, 34],
iconAnchor: [17, 17], // Adjust the anchor point as needed
});

const datasetsData: Dataset[] = [
{
id: "charging_stations",
displayName: "Charging stations",
description: "Locations of all charging stations in Germany.",
type: "markers",
datasetIcon: ChargingStation,
markerIcon: ChargingStation,
data: [],
markerIcon: divIconChargingStation,
data: emptyFeatureCollection,
},
{
id: "house_footprints",
Expand All @@ -41,7 +68,7 @@ const datasetsData: Dataset[] = [
type: "areas",
datasetIcon: Blueprint,
markerIcon: undefined,
data: [],
data: emptyFeatureCollection,
},
];

Expand Down
30 changes: 23 additions & 7 deletions frontend/src/components/MapView/DataFetch.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
import { FeatureCollection, Geometry } from "geojson";
import data from "./FeatureCollection.json";
import defaultCityLocationData from "./FeatureCollection.json";
import defaultPolygonData from "./gemeinden_simplify20.json";
import { LatLngBounds } from "leaflet";
import { useEffect, useState } from "react";
const geojsonData: FeatureCollection = data as FeatureCollection;
const geojsonCities: FeatureCollection =
defaultCityLocationData as FeatureCollection;
const geojsonGemeindenPolygons: FeatureCollection =
defaultPolygonData as FeatureCollection;

const useGeoData = (
id: string,
bounds: LatLngBounds,
zoom: number
zoom: number,
onUpdate: (data: FeatureCollection<Geometry>) => void
): FeatureCollection<Geometry> | undefined => {
const [data, setData] = useState<FeatureCollection<Geometry>>();

useEffect(() => {
const fetchData = async () => {
/* eslint-disable */
const fetchData = async (_bounds: LatLngBounds) => {
/* eslint-enable */
if (id === "house_footprints") {
setData(geojsonGemeindenPolygons as FeatureCollection<Geometry>);
onUpdate(geojsonGemeindenPolygons);
return;
}

try {
// const bottomLat = bounds.getSouth();
// const bottomLong = bounds.getWest();
Expand All @@ -25,14 +39,16 @@ const useGeoData = (
}
const result = await response.json();
setData(result as FeatureCollection<Geometry>);
onUpdate(result);
} catch (error) {
console.error("Fetching data failed, using local GeoJSON data:", error);
setData(geojsonData as FeatureCollection<Geometry>);
setData(geojsonCities as FeatureCollection<Geometry>);
onUpdate(geojsonCities);
}
};

fetchData();
}, [bounds, zoom]);
fetchData(bounds);
}, [bounds, zoom, id, onUpdate]);

return data;
};
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/components/MapView/MapOptions.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,14 @@
display: flex;
flex-direction: column;
}
.switch-map-icon {
width: 2rem;
height: 2rem;
position: absolute;
bottom: 4rem;
right: 1rem;
cursor: pointer;
z-index: 2;
display: flex;
flex-direction: column;
}
15 changes: 13 additions & 2 deletions frontend/src/components/MapView/MapOptions.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { useState } from "react";
import { MagnifyingGlass } from "@phosphor-icons/react";
import { ArrowsClockwise, MagnifyingGlass } from "@phosphor-icons/react";
import "./MapOptions.css";
import { Tooltip } from "@mui/material";
import SearchPopUp from "../PopUp/SearchPopUp";

const MapOptions: React.FC = () => {
interface MapOptionsProps {
toggleShowSatellite: () => void;
}

const MapOptions: React.FC<MapOptionsProps> = ({ toggleShowSatellite }) => {
// Stores the state of if the search popup is open
const [ifOpenedDialog, setIfOpenedDialog] = useState(false);
const toggleIfOpenedDialog = () => {
Expand All @@ -20,6 +24,13 @@ const MapOptions: React.FC = () => {
onClick={toggleIfOpenedDialog}
/>
</Tooltip>
<Tooltip arrow title="Switch satellite / openstreetmap">
<ArrowsClockwise
weight="duotone"
className="switch-map-icon"
onClick={toggleShowSatellite}
/>
</Tooltip>
<SearchPopUp
onToggleIfOpenedDialog={toggleIfOpenedDialog}
ifOpenedDialog={ifOpenedDialog}
Expand Down
165 changes: 143 additions & 22 deletions frontend/src/components/MapView/MapView.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { useContext } from "react";
import { useCallback, useContext, useEffect, useState } from "react";
import { MapContainer } from "react-leaflet/MapContainer";
import { Marker } from "react-leaflet/Marker";
import { Popup } from "react-leaflet/Popup";
import { TileLayer } from "react-leaflet/TileLayer";
import "leaflet/dist/leaflet.css";
import "./MapView.css";
import { useMap, useMapEvents } from "react-leaflet/hooks";
import L from "leaflet";
import L, { DivIcon, LatLng } from "leaflet";
import icon from "leaflet/dist/images/marker-icon.png";
import iconShadow from "leaflet/dist/images/marker-shadow.png";
import MapOptions from "./MapOptions";
import useGeoData from "./DataFetch";
import { GeoJSON } from "react-leaflet";
import { GeoJSON, WMSTileLayer } from "react-leaflet";
import { MapContext } from "../../contexts/MapContext";
import { TabProps, TabsContext } from "../../contexts/TabsContext";
import { FeatureCollection } from "geojson";
import { Dataset } from "../DatasetsList/DatasetsList";
import { createRoot } from "react-dom/client";
import { flushSync } from "react-dom";
import { MapPin } from "@phosphor-icons/react";

const DefaultIcon = L.icon({
iconUrl: icon,
Expand All @@ -22,22 +28,73 @@ const DefaultIcon = L.icon({

L.Marker.prototype.options.icon = DefaultIcon;

const svgIcon = L.divIcon({
html: `
<svg width="34" height="34" viewBox="0 0 34 34" xmlns="http://www.w3.org/2000/svg">
<circle cx="17" cy="17" r="14" stroke="white" stroke-width="2" fill="transparent"/>
<circle cx="17" cy="17" r="12" stroke="red" stroke-width="3" fill="transparent"/>
<circle cx="17" cy="17" r="9" stroke="white" stroke-width="1" fill="transparent"/>
</svg>
`,
// Utility function to render a React component to HTML string
const renderToHtml = (Component: React.FC) => {
const div = document.createElement("div");
const root = createRoot(div);
flushSync(() => {
root.render(<Component />);
});
return div.innerHTML;
};

const divIconMarker: DivIcon = L.divIcon({
html: renderToHtml(() => <MapPin size={36} color="#ff0000" weight="fill" />),
className: "", // Optional: add a custom class name
iconSize: [34, 34],
iconAnchor: [17, 17], // Adjust the anchor point as needed
iconSize: [36, 36],
iconAnchor: [18, 36], // Adjust the anchor point as needed
});

const MapView: React.FC = () => {
interface MapViewProps {
datasetId: string;
}

const MapView: React.FC<MapViewProps> = ({ datasetId }) => {
const { currentTabsCache, setCurrentTabsCache } = useContext(TabsContext);
const [map, setMap] = useState<L.Map | null>(null);
const { currentMapCache, setCurrentMapCache } = useContext(MapContext);
const geoData = useGeoData(currentMapCache.mapBounds, currentMapCache.zoom);
const [showSatellite, setShowSatellite] = useState<boolean>(false);
const toggleShowSatellite = () => {
setShowSatellite((prevShowSatellite) => !prevShowSatellite);
};

const updateDatasetData = useCallback(
(newData: FeatureCollection) => {
setCurrentTabsCache((prevCache) => {
const updatedTabs = prevCache.openedTabs.map((tab) => {
if (tab.dataset.id === datasetId) {
return {
...tab,
dataset: {
...tab.dataset,
data: newData,
},
};
}
return tab;
});

return {
...prevCache,
openedTabs: updatedTabs,
};
});
},
[datasetId, setCurrentTabsCache]
);

const geoData = useGeoData(
datasetId,
currentMapCache.mapBounds,
currentMapCache.zoom,
updateDatasetData
);

useEffect(() => {
if (map) {
setCurrentMapCache((prev) => ({ ...prev, mapInstance: map }));
}
}, [map, setCurrentMapCache]);

const MapEventsHandler = () => {
const map = useMap();
Expand All @@ -59,7 +116,10 @@ const MapView: React.FC = () => {
},
});
return (
<Marker position={currentMapCache.selectedCoordinates} icon={svgIcon}>
<Marker
position={currentMapCache.selectedCoordinates}
icon={divIconMarker}
>
<Popup>
<span
// Get the current location of the user
Expand Down Expand Up @@ -92,21 +152,82 @@ const MapView: React.FC = () => {
setCurrentMapCache({ ...currentMapCache, selectedCoordinates: latlng });
}

// Get the feature collections from pinned tabs
const pinnedFeatureCollections = currentTabsCache.openedTabs
.filter((tab: TabProps) => tab.ifPinned)
.map((tab: TabProps) => tab.dataset);

const tabProps = currentTabsCache.openedTabs.find(
(tab: TabProps) => tab.dataset.id === datasetId
);

// Check if the current geoData is in the pinnedFeatureCollections
const isCurrentDataPinned = pinnedFeatureCollections.some(
(dataset: Dataset) => dataset.data === geoData
);

return (
<div className="tab-map-container">
<MapOptions />
<MapOptions toggleShowSatellite={toggleShowSatellite} />
<MapContainer
center={currentMapCache.mapCenter}
zoom={currentMapCache.zoom}
className="map"
ref={setMap}
>
{geoData && <GeoJSON data={geoData} />}
{pinnedFeatureCollections.map((dataset: Dataset, index: number) => (
<GeoJSON
style={{ fillOpacity: 0.1 }}
key={index}
data={dataset.data}
pointToLayer={(_geoJsonPoint, latlng: LatLng) => {
if (dataset.markerIcon)
return L.marker(latlng, { icon: dataset.markerIcon });
else return L.marker(latlng);
}}
/>
))}
{!isCurrentDataPinned && geoData && (
<GeoJSON
style={{ fillOpacity: 0.1 }}
data={geoData}
pointToLayer={(_geoJsonPoint, latlng: LatLng) => {
if (tabProps && tabProps.dataset.markerIcon)
return L.marker(latlng, { icon: tabProps.dataset.markerIcon });
else return L.marker(latlng);
}}
/>
)}

<MapEventsHandler />
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>

{showSatellite ? (
<div>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
/>
<WMSTileLayer
url="https://sg.geodatenzentrum.de/wms_sentinel2_de"
layers="rgb_2020"
format="image/png"
transparent={true}
attribution='&copy; Bundesamt für Kartographie und Geodäsie (BKG), Bayerische Vermessungverwaltung, <a href="http://sg.geodatenzentrum.de/web_public/gdz/datenquellen/Datenquellen_TopPlusOpen.pdf">Sources</a>'
/>
<WMSTileLayer
url="https://geoservices.bayern.de/od/wms/dop/v1/dop40?"
layers="by_dop40c"
format="image/png"
transparent={true}
attribution="&copy; © Europäische Union, enthält Copernicus Sentinel-2 Daten 2020, verarbeitet durch das Bundesamt für Kartographie und Geodäsie (BKG)"
/>
</div>
) : (
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
)}
</MapContainer>
</div>
);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/MapView/gemeinden_simplify20.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion frontend/src/components/MultiMap/MultiMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ const MultiMap = () => {
<span className="tab-description-container">
{tab.dataset.description}
</span>
<MapView />
<MapView datasetId={tab.dataset.id} />
</div>
</TabPanel>
);
Expand Down
Loading

0 comments on commit 956d763

Please sign in to comment.