mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
227 lines
6.7 KiB
JavaScript
227 lines
6.7 KiB
JavaScript
/*
|
|
* Copyright (c) 2026 by Christian Kellner.
|
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
|
*/
|
|
|
|
import { useEffect, useRef } from 'react';
|
|
import maplibregl from 'maplibre-gl';
|
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
|
|
import { fixMapboxDrawCompatibility, addDrawingControl, setupAreaFilterEventListeners } from './MapDrawingExtension.js';
|
|
import { getBoundsFromCoords } from '../../views/listings/mapUtils.js';
|
|
import './Map.less';
|
|
|
|
export const GERMANY_BOUNDS = [
|
|
[5.866, 47.27], // Southwest coordinates
|
|
[15.042, 55.059], // Northeast coordinates
|
|
];
|
|
|
|
export const STYLES = {
|
|
STANDARD: 'https://tiles.openfreemap.org/styles/bright',
|
|
SATELLITE: {
|
|
version: 8,
|
|
sources: {
|
|
'satellite-tiles': {
|
|
type: 'raster',
|
|
tiles: ['https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'],
|
|
tileSize: 256,
|
|
attribution:
|
|
'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
|
|
},
|
|
'satellite-labels': {
|
|
type: 'raster',
|
|
tiles: [
|
|
'https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}',
|
|
],
|
|
tileSize: 256,
|
|
attribution: '© Esri',
|
|
},
|
|
},
|
|
layers: [
|
|
{
|
|
id: 'satellite-tiles',
|
|
type: 'raster',
|
|
source: 'satellite-tiles',
|
|
minzoom: 0,
|
|
maxzoom: 19,
|
|
},
|
|
{
|
|
id: 'satellite-labels',
|
|
type: 'raster',
|
|
source: 'satellite-labels',
|
|
minzoom: 0,
|
|
maxzoom: 19,
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
export default function Map({
|
|
style = 'STANDARD',
|
|
show3dBuildings = false,
|
|
onMapReady = null,
|
|
enableDrawing = false,
|
|
initialSpatialFilter = null,
|
|
onDrawingChange = null,
|
|
}) {
|
|
const mapContainerRef = useRef(null);
|
|
const mapRef = useRef(null);
|
|
const drawRef = useRef(null);
|
|
const hasFittedToInitialAreaRef = useRef(false);
|
|
|
|
// Initialize map - ONLY when container changes, never reinitialize
|
|
useEffect(() => {
|
|
if (mapRef.current) return; // Map already exists, don't reinitialize
|
|
|
|
mapRef.current = new maplibregl.Map({
|
|
container: mapContainerRef.current,
|
|
style: STYLES[style],
|
|
center: [10.4515, 51.1657], // Center of Germany
|
|
zoom: 4,
|
|
maxBounds: GERMANY_BOUNDS,
|
|
antialias: true,
|
|
});
|
|
|
|
mapRef.current.addControl(
|
|
new maplibregl.NavigationControl({
|
|
showCompass: true,
|
|
visualizePitch: true,
|
|
visualizeRoll: true,
|
|
}),
|
|
'top-right',
|
|
);
|
|
|
|
mapRef.current.addControl(
|
|
new maplibregl.GeolocateControl({
|
|
positionOptions: {
|
|
enableHighAccuracy: true,
|
|
},
|
|
trackUserLocation: true,
|
|
}),
|
|
);
|
|
|
|
// Initialize drawing extension only if enabled
|
|
if (enableDrawing) {
|
|
fixMapboxDrawCompatibility();
|
|
drawRef.current = addDrawingControl(mapRef.current);
|
|
}
|
|
|
|
// Call onMapReady callback if provided
|
|
if (onMapReady) {
|
|
onMapReady(mapRef.current);
|
|
}
|
|
|
|
return () => {
|
|
if (mapRef.current) {
|
|
mapRef.current.remove();
|
|
mapRef.current = null;
|
|
}
|
|
};
|
|
}, [mapContainerRef]); // ONLY depend on mapContainerRef - nothing else!
|
|
|
|
// Load spatial filter and setup area filter event listeners
|
|
useEffect(() => {
|
|
if (!mapRef.current || !drawRef.current || !enableDrawing) return;
|
|
|
|
// Load initial spatial filter if provided
|
|
if (initialSpatialFilter) {
|
|
try {
|
|
drawRef.current.set(initialSpatialFilter);
|
|
} catch (error) {
|
|
console.error('Error loading spatial filter:', error);
|
|
}
|
|
|
|
if (!hasFittedToInitialAreaRef.current) {
|
|
const coords = initialSpatialFilter.features.flatMap((feature) =>
|
|
feature.geometry?.type === 'Polygon' ? feature.geometry.coordinates.flat() : [],
|
|
);
|
|
const bounds = getBoundsFromCoords(coords);
|
|
if (bounds) {
|
|
mapRef.current.fitBounds(bounds, { padding: 50, maxZoom: 15, duration: 0 });
|
|
hasFittedToInitialAreaRef.current = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Setup drawing event listeners
|
|
const cleanup = setupAreaFilterEventListeners(mapRef.current, drawRef.current, onDrawingChange);
|
|
|
|
return cleanup;
|
|
}, [initialSpatialFilter, onDrawingChange, enableDrawing]);
|
|
|
|
// Handle style changes
|
|
useEffect(() => {
|
|
if (mapRef.current) {
|
|
mapRef.current.setStyle(STYLES[style]);
|
|
}
|
|
}, [style]);
|
|
|
|
// Handle 3D buildings layer
|
|
useEffect(() => {
|
|
if (!mapRef.current) return;
|
|
|
|
const add3dLayer = () => {
|
|
if (!mapRef.current || !mapRef.current.isStyleLoaded()) return;
|
|
if (show3dBuildings) {
|
|
if (!mapRef.current.getSource('openfreemap')) {
|
|
mapRef.current.addSource('openfreemap', {
|
|
type: 'vector',
|
|
url: 'https://tiles.openfreemap.org/planet',
|
|
});
|
|
}
|
|
if (!mapRef.current.getLayer('3d-buildings')) {
|
|
const layers = mapRef.current.getStyle().layers;
|
|
let labelLayerId;
|
|
for (let i = 0; i < layers.length; i++) {
|
|
if (layers[i].type === 'symbol' && layers[i].layout?.['text-field']) {
|
|
labelLayerId = layers[i].id;
|
|
break;
|
|
}
|
|
}
|
|
mapRef.current.addLayer(
|
|
{
|
|
id: '3d-buildings',
|
|
source: 'openfreemap',
|
|
'source-layer': 'building',
|
|
type: 'fill-extrusion',
|
|
minzoom: 15,
|
|
filter: ['!=', ['get', 'hide_3d'], true],
|
|
paint: {
|
|
'fill-extrusion-color': [
|
|
'interpolate',
|
|
['linear'],
|
|
['get', 'render_height'],
|
|
0,
|
|
'lightgray',
|
|
200,
|
|
'royalblue',
|
|
400,
|
|
'lightblue',
|
|
],
|
|
'fill-extrusion-height': ['interpolate', ['linear'], ['zoom'], 15, 0, 16, ['get', 'render_height']],
|
|
'fill-extrusion-base': ['case', ['>=', ['get', 'zoom'], 16], ['get', 'render_min_height'], 0],
|
|
'fill-extrusion-opacity': 0.6,
|
|
},
|
|
},
|
|
labelLayerId,
|
|
);
|
|
}
|
|
} else {
|
|
if (mapRef.current.getLayer('3d-buildings')) {
|
|
mapRef.current.removeLayer('3d-buildings');
|
|
}
|
|
}
|
|
};
|
|
|
|
add3dLayer();
|
|
}, [show3dBuildings, style]);
|
|
|
|
// Handle pitch for 3D
|
|
useEffect(() => {
|
|
if (!mapRef.current) return;
|
|
mapRef.current.setPitch(show3dBuildings ? 45 : 0);
|
|
}, [show3dBuildings]);
|
|
|
|
return <div ref={mapContainerRef} className="map-container" />;
|
|
}
|