'use client'; import { useRef, useEffect, useState, Dispatch, SetStateAction } from 'react'; import { useMediaQuery } from 'react-responsive'; import ArcGISMap from '@arcgis/core/Map'; import Extent from '@arcgis/core/geometry/Extent'; import MapView from '@arcgis/core/views/MapView'; import Basemap from '@arcgis/core/Basemap'; import MapImageLayer from '@arcgis/core/layers/MapImageLayer'; import WMTSLayer from '@arcgis/core/layers/WMTSLayer'; import Search from '@arcgis/core/widgets/Search'; import ScaleBar from '@arcgis/core/widgets/ScaleBar'; import Sketch from '@arcgis/core/widgets/Sketch'; import GraphicsLayer from '@arcgis/core/layers/GraphicsLayer'; import Graphic from '@arcgis/core/Graphic'; import * as geometryEngine from '@arcgis/core/geometry/geometryEngine'; import * as reactiveUtils from '@arcgis/core/core/reactiveUtils'; import Zoom from '@arcgis/core/widgets/Zoom'; import FeatureLayer from '@arcgis/core/layers/FeatureLayer'; import Polygon from '@arcgis/core/geometry/Polygon'; import SimpleFillSymbol from '@arcgis/core/symbols/SimpleFillSymbol'; import SimpleMarkerSymbol from '@arcgis/core/symbols/SimpleMarkerSymbol'; import esriConfig from '@arcgis/core/config'; import Point from '@arcgis/core/geometry/Point'; import { updateComputationResultEWS } from '@/redux/computationsEWSSlice'; import { updateCadastralData } from '@/redux/cadastreSlice'; import identifyAllLayers from '../../utils/identify'; import queryCadastre from '../../utils/queryCadastre'; import getAddress from '../../utils/getAddress'; import './esri-ui-potenzialkarte.css'; import { Vienna, Austria } from '@/public/borders-vienna-austria'; import ReduxProvider from '@/redux/provider'; import { useAppDispatch } from '@/redux/hooks'; import PanelPotenzialkarte from './panel-potenzialkarte'; import CalculationsMenu from './calculations-menu'; // import layer URLs and spatial reference system wkid import { BASEMAP_AT_URL, AMPEL_EWS_URL, RESOURCES_EWS_URL, SRS } from '@/app/config/config'; // set path for local assets esriConfig.assetsPath = '/assets'; const scaleLimit = 1000; const gridSpacing = 10; // initialize info panel callback functions let setAddress: Dispatch>, setClosenessWarning: Dispatch>, setOutsideWarning: Dispatch>, setScaleWarning: Dispatch>; export function initializeInfoPanelHandlers( setAddressCallback: Dispatch>, setClosenessWarningCallback: Dispatch>, setOutsideWarningCallback: Dispatch>, setScaleWarningCallback: Dispatch> ) { setAddress = setAddressCallback; setClosenessWarning = setClosenessWarningCallback; setOutsideWarning = setOutsideWarningCallback; setScaleWarning = setScaleWarningCallback; } // initialize calculations menu callback functions let setPoints: Dispatch>, setPolygon: Dispatch>; export function initializeCalculationsMenuHandlers( setPointsCallback: Dispatch>, setPolygonCallback: Dispatch> ) { setPoints = setPointsCallback; setPolygon = setPolygonCallback; } // initialize the map view container export let view: MapView; export let pointGraphicsLayer: GraphicsLayer; export let boundaryGraphicsLayer: GraphicsLayer; export default function MapComponent() { const dispatch = useAppDispatch(); const isMobile = useMediaQuery({ maxWidth: 640 }); const mapDiv = useRef(null); const calculationsMenuRef = useRef(null); const loaded = useRef(false); const [loading, setLoading] = useState(false); useEffect(() => { let handle: IHandle; if (mapDiv.current && calculationsMenuRef.current) { view = new MapView({ container: mapDiv.current, extent: new Extent({ xmin: -19000, ymin: 325000, xmax: 29000, ymax: 360000, spatialReference: { wkid: SRS }, }), spatialReference: { wkid: SRS }, popupEnabled: false, popup: { dockEnabled: false, dockOptions: { buttonEnabled: false, }, collapseEnabled: false, viewModel: { includeDefaultActions: false, }, }, }); // listen to scale changes handle = reactiveUtils.watch( () => view.scale, () => { if (view.scale < scaleLimit) { setScaleWarning(false); } else { setScaleWarning(true); } } ); // graphic layers let viennaGraphic = new Graphic({ geometry: new Polygon({ rings: [Austria, Vienna], spatialReference: { wkid: SRS } }), symbol: { type: 'simple-fill', color: [209, 213, 219, 0.65], style: 'solid', outline: { color: 'white', width: 0, }, } as unknown as SimpleFillSymbol, }); const viennaGraphicsLayer = new GraphicsLayer({ title: 'Wien', listMode: 'hide' }); viennaGraphicsLayer.add(viennaGraphic); pointGraphicsLayer = new GraphicsLayer({ title: 'Planungslayer - Punkte', listMode: 'hide', }); boundaryGraphicsLayer = new GraphicsLayer({ title: 'EWS - innere Grenze', listMode: 'hide', }); const polygonGraphicsLayer = new GraphicsLayer({ title: 'Grundstücksgrenze', listMode: 'hide', }); const highlightGraphicsLayer = new GraphicsLayer({ title: 'Grundstücksgrenze', listMode: 'hide', }); // thematic layers const ampelkarte_ews = new FeatureLayer({ url: AMPEL_EWS_URL + '/0', title: 'Mögliche Einschränkungen', visible: false, listMode: 'show', opacity: 0.5, }); const ews = new MapImageLayer({ title: 'Ressourcen', url: RESOURCES_EWS_URL, visible: false, listMode: 'show', }); // basemap in Viennese coordinate system due to tranformation inaccuracies from MGI to WGS84 // default transformation in ArcGIS API from MGI to WGS84 is 1306 // transformation 1618 is recommended const basemap_at = new WMTSLayer({ url: BASEMAP_AT_URL, activeLayer: { id: 'geolandbasemap', }, listMode: 'hide', serviceMode: 'KVP', }); let basemap = new Basemap({ baseLayers: [basemap_at], title: 'basemap', id: 'basemap', spatialReference: { wkid: SRS }, }); let arcgisMap = new ArcGISMap({ basemap: basemap, layers: [ ews, ampelkarte_ews, pointGraphicsLayer, boundaryGraphicsLayer, polygonGraphicsLayer, highlightGraphicsLayer, viennaGraphicsLayer, ], }); const search = new Search({ view, popupEnabled: true, }); const scaleBar = new ScaleBar({ view: view, unit: 'metric', }); const zoom = new Zoom({ view, layout: 'horizontal', }); let sketch = new Sketch({ container: calculationsMenuRef.current, layer: pointGraphicsLayer, view: view, availableCreateTools: ['point'], visibleElements: { selectionTools: { 'lasso-selection': true, 'rectangle-selection': true, }, settingsMenu: false, undoRedoMenu: false, }, creationMode: 'single', }); // if (loaded.current) { // sketch.container = calculationsMenuRef.current; // } // loaded.current = true; sketch.on('create', (event) => { if (event.tool === 'point' && event.state === 'complete') { // point symbol const symbol = { type: 'simple-marker', color: [255, 255, 255, 0.25], }; // delete default point symbol pointGraphicsLayer.remove(event.graphic); let point = event.graphic.geometry as Point; let points; // add point to current list of points setPoints((currentPoints) => { points = [...currentPoints, [point.x, point.y]]; return points; }); // check if point is too close to any other point pointGraphicsLayer.graphics.forEach((graphic) => { if (geometryEngine.distance(graphic.geometry, point) < 5) { setClosenessWarning(true); } }); // check if point is outside the parcel if (!geometryEngine.intersects(point, boundaryGraphicsLayer.graphics.at(0).geometry)) { setOutsideWarning(true); } // draw point graphic pointGraphicsLayer.add( new Graphic({ geometry: point, symbol, }) ); } }); sketch.on('update', (event) => { if (event.state === 'start') { // on first load update starts with two identical points // cancel second update if (sketch.updateGraphics.length === 2 && sketch.updateGraphics.at(0) === sketch.updateGraphics.at(1)) { sketch.cancel(); } } if (event.state === 'complete') { // list of updated points let allPoints = pointGraphicsLayer.graphics.map((graphic) => graphic.geometry as Point); // update state of calculations menu setPoints(allPoints.map((point) => [point.x, point.y]).toArray()); // check if all points are inside the boundary let allPointsInside = true; allPoints.forEach((point) => { if (!geometryEngine.intersects(point, boundaryGraphicsLayer.graphics.at(0).geometry)) { allPointsInside = false; return; } }); setOutsideWarning(!allPointsInside); // check if points are too close let tooClose = false; allPoints.forEach((firstPoint) => { allPoints.forEach((secondPoint) => { if (secondPoint !== firstPoint) { if (geometryEngine.distance(firstPoint, secondPoint, 'meters') <= 4.9) { tooClose = true; return; } } }); }); setClosenessWarning(tooClose); } }); // register event handler for mouse clicks view.on('click', ({ mapPoint }) => { // at application start if no polygon is drawn yet if (polygonGraphicsLayer.graphics.length === 0) { startNewQuery(mapPoint); } else { // check if point is selected view .hitTest(view.toScreen(mapPoint), { include: pointGraphicsLayer, }) .then(({ results }) => { if (results && results.length > 0) { // if point was selected then start update const pointGraphic = (results.at(0) as any)?.graphic; sketch.update(pointGraphic); } else { if ( !geometryEngine.intersects(mapPoint, polygonGraphicsLayer.graphics.at(0).geometry) && view.scale < scaleLimit ) { // if click was outside parcel and no point was selected then start new query view.openPopup({ title: 'Wollen Sie ein neues Grundstück auswählen?', location: mapPoint, content: getPopupContent(mapPoint), }); } } }); } }); // listen to move event view.on('pointer-move', (event) => { let mapPoint = view.toMap({ x: event.x, y: event.y }); view .hitTest(view.toScreen(mapPoint), { include: pointGraphicsLayer, }) .then(({ results }) => { // if user hovers over a point if (results.length > 0 && event.buttons === 0 && !sketch.createGraphic) { const graphicHits = results?.filter((hitResult) => hitResult.type === 'graphic'); let graphic = (graphicHits.at(0) as any)?.graphic; sketch.update(graphic); let pointGraphic = new Graphic({ geometry: graphic.geometry, symbol: { type: 'simple-marker', size: '30px', color: null, outline: { color: '#00ffff' }, } as unknown as SimpleMarkerSymbol, }); highlightGraphicsLayer.add(pointGraphic); document.body.style.cursor = 'pointer'; } else { highlightGraphicsLayer.removeAll(); document.body.style.cursor = 'default'; } }); }); const getPopupContent = (mapPoint: Point) => { const popupContent = document.createElement('div'); popupContent.className = 'flex justify-center'; const popupButton = document.createElement('button'); popupButton.textContent = 'Ja'; popupButton.className = 'text-white bg-gray-700 p-2 border-none outline-none cursor-pointer w-2/3'; popupButton.addEventListener('mouseover', handleMouseOver); function handleMouseOver(this: HTMLButtonElement) { this.className = 'text-white bg-gray-300 p-2 border-none outline-none cursor-pointer w-2/3'; } popupButton.addEventListener('mouseout', handleMouseOut); function handleMouseOut(this: HTMLButtonElement) { this.className = 'text-white bg-gray-700 p-2 border-none outline-none cursor-pointer w-2/3'; } popupContent.append(popupButton); popupButton.onclick = () => { startNewQuery(mapPoint); view.popup.close(); }; return popupContent; }; const startNewQuery = (mapPoint: Point) => { // remove existing graphics polygonGraphicsLayer.removeAll(); boundaryGraphicsLayer.removeAll(); pointGraphicsLayer.removeAll(); // clear state setClosenessWarning(false); setOutsideWarning(false); setPolygon(null); setPoints([]); // initialize store in case there was a previous computation dispatch(updateCadastralData({})); dispatch(updateComputationResultEWS({})); // start new layer queries identifyAllLayers(view, mapPoint, dispatch, 'EWS'); getAddress(mapPoint, setAddress); if (view.scale < scaleLimit) { // query cadastral data queryCadastre(view, polygonGraphicsLayer, mapPoint, dispatch, setPolygon, setPoints, gridSpacing); } }; // add map to view view.map = arcgisMap; // add UI components view.ui.components = []; if (!isMobile) { view.ui.add([zoom, search], 'top-left'); view.ui.add(scaleBar, 'bottom-left'); } } return () => { view?.destroy(); handle?.remove(); }; }, [dispatch, isMobile]); return (
{loading ? (
) : null}
); }