Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Map Update #186

Merged
merged 11 commits into from
May 28, 2024
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
Loading