geothermie-atlas/app/ews/potenzialberechnung/potenzialkarte.tsx

488 lines
16 KiB
TypeScript

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<SetStateAction<string[]>>,
setClosenessWarning: Dispatch<SetStateAction<boolean>>,
setOutsideWarning: Dispatch<SetStateAction<boolean>>,
setScaleWarning: Dispatch<SetStateAction<boolean>>;
export function initializeInfoPanelHandlers(
setAddressCallback: Dispatch<SetStateAction<string[]>>,
setClosenessWarningCallback: Dispatch<SetStateAction<boolean>>,
setOutsideWarningCallback: Dispatch<SetStateAction<boolean>>,
setScaleWarningCallback: Dispatch<SetStateAction<boolean>>
) {
setAddress = setAddressCallback;
setClosenessWarning = setClosenessWarningCallback;
setOutsideWarning = setOutsideWarningCallback;
setScaleWarning = setScaleWarningCallback;
}
// initialize calculations menu callback functions
let setPoints: Dispatch<SetStateAction<number[][]>>, setPolygon: Dispatch<SetStateAction<Polygon | null>>;
export function initializeCalculationsMenuHandlers(
setPointsCallback: Dispatch<SetStateAction<number[][]>>,
setPolygonCallback: Dispatch<SetStateAction<Polygon | null>>
) {
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<HTMLDivElement | null>(null);
const calculationsMenuRef = useRef<HTMLDivElement | null>(null);
const loaded = useRef<boolean>(false);
const [loading, setLoading] = useState<boolean>(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 (
<div ref={mapDiv} className="absolute top-16 bottom-0 w-full">
<PanelPotenzialkarte></PanelPotenzialkarte>
<ReduxProvider>
<CalculationsMenu isLoading={setLoading} ref={calculationsMenuRef}></CalculationsMenu>
</ReduxProvider>
{loading ? (
<div
className="absolute left-1/2 top-1/2 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-gray-950 border-r-transparent"
role="status"
></div>
) : null}
</div>
);
}