Work on Popups

This commit is contained in:
Fuhrmann 2023-10-25 14:51:44 +02:00
parent a810f87461
commit 0d509d4e00
9 changed files with 1179 additions and 302 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
}

View File

@ -10,8 +10,8 @@ import MapImageLayer from '@arcgis/core/layers/MapImageLayer';
import Sublayer from '@arcgis/core/layers/support/Sublayer'; import Sublayer from '@arcgis/core/layers/support/Sublayer';
import ButtonMenuItem from '@arcgis/core/widgets/FeatureTable/Grid/support/ButtonMenuItem'; import ButtonMenuItem from '@arcgis/core/widgets/FeatureTable/Grid/support/ButtonMenuItem';
export default function Layers({ view, tableRoot }: { view: MapView; tableRoot: HTMLDivElement }) { export default function Layers({ view, tableDiv }: { view: MapView; tableDiv: HTMLDivElement }) {
const htmlDiv = useRef<HTMLDivElement>(null); const layerListDiv = useRef<HTMLDivElement | null>(null);
const featureTable = useRef<FeatureTable | null>(null); const featureTable = useRef<FeatureTable | null>(null);
const rendered = useRef<boolean>(false); const rendered = useRef<boolean>(false);
@ -25,9 +25,9 @@ export default function Layers({ view, tableRoot }: { view: MapView; tableRoot:
const tableContainer = document.createElement('div'); const tableContainer = document.createElement('div');
tableContainer.className = 'h-full w-full'; tableContainer.className = 'h-full w-full';
if (tableRoot) { if (tableDiv) {
tableRoot.classList.remove('hidden'); tableDiv.classList.remove('hidden');
tableRoot.append(tableContainer); tableDiv.append(tableContainer);
} }
const featureLayer = await layer.createFeatureLayer(); const featureLayer = await layer.createFeatureLayer();
@ -43,8 +43,8 @@ export default function Layers({ view, tableRoot }: { view: MapView; tableRoot:
iconClass: 'esri-icon-close', iconClass: 'esri-icon-close',
clickFunction: function () { clickFunction: function () {
featureTable.current?.destroy(); featureTable.current?.destroy();
if (tableRoot) { if (tableDiv) {
tableRoot.classList.add('hidden'); tableDiv.classList.add('hidden');
} }
}, },
} as unknown as ButtonMenuItem, } as unknown as ButtonMenuItem,
@ -53,13 +53,13 @@ export default function Layers({ view, tableRoot }: { view: MapView; tableRoot:
}); });
}; };
if (htmlDiv.current) { if (layerListDiv.current) {
const arcGISAPIWidgetContainer = document.createElement('div'); const arcGISAPIWidgetContainer = document.createElement('div');
htmlDiv.current.append(arcGISAPIWidgetContainer); layerListDiv.current.append(arcGISAPIWidgetContainer);
const layerList = new LayerList({ const layerList = new LayerList({
view, view,
container: htmlDiv.current, container: layerListDiv.current,
selectionEnabled: true, selectionEnabled: true,
listItemCreatedFunction: async function (event) { listItemCreatedFunction: async function (event) {
const item = event.item; const item = event.item;
@ -139,9 +139,7 @@ export default function Layers({ view, tableRoot }: { view: MapView; tableRoot:
} }
}); });
} }
}, [t, tableDiv, view]);
return () => {}; return <div ref={layerListDiv}></div>;
});
return <div ref={htmlDiv}></div>;
} }

View File

@ -1,5 +1,7 @@
'use client';
import { useRef, useState, useEffect } from 'react'; import { useRef, useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client'; import { Root, createRoot } from 'react-dom/client';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
@ -10,22 +12,24 @@ import WebMap from '@arcgis/core/WebMap';
import esriConfig from '@arcgis/core/config'; import esriConfig from '@arcgis/core/config';
import ScaleBar from '@arcgis/core/widgets/ScaleBar'; import ScaleBar from '@arcgis/core/widgets/ScaleBar';
import Legend from '@arcgis/core/widgets/Legend'; import Legend from '@arcgis/core/widgets/Legend';
import VectorTileLayer from '@arcgis/core/layers/VectorTileLayer';
import TileLayer from '@arcgis/core/layers/TileLayer';
import Map from '@arcgis/core/Map.js';
import Basemap from '@arcgis/core/Basemap';
import * as intl from '@arcgis/core/intl'; import * as intl from '@arcgis/core/intl';
import TextContent from '@arcgis/core/popup/content/TextContent';
import ExpressionContent from '@arcgis/core/popup/content/ExpressionContent';
import ElementExpressionInfo from '@arcgis/core/popup/ElementExpressionInfo';
import { watch } from '@arcgis/core/core/reactiveUtils';
// @ts-ignore
import { getColorsForRendererValues } from '@arcgis/core/renderers/support/utils';
// set asset path for ArcGIS Maps SDK widgets // set asset path for ArcGIS Maps SDK widgets
esriConfig.assetsPath = './assets'; esriConfig.assetsPath = '/assets';
// ids of web map items in portal // ids of web map items in portal
const webMapDEID = '7d0768f73d3e4be2b32c22274c600cb3'; const webMapDEID = '7d0768f73d3e4be2b32c22274c600cb3';
const webMapENID = 'dbf5532d06954c6a989d4f022de83f70'; const webMapENID = 'dbf5532d06954c6a989d4f022de83f70';
// lazy load components // lazy load components
const Print = dynamic(() => import('./print'));
const Layers = dynamic(() => import('./layer-list')); const Layers = dynamic(() => import('./layer-list'));
const Print = dynamic(() => import('./print'));
const Basemaps = dynamic(() => import('./basemap-list')); const Basemaps = dynamic(() => import('./basemap-list'));
const Search = dynamic(() => import('./search')); const Search = dynamic(() => import('./search'));
@ -48,20 +52,30 @@ import {
CalciteAction, CalciteAction,
CalcitePanel, CalcitePanel,
} from '@esri/calcite-components-react'; } from '@esri/calcite-components-react';
import MapImageLayer from '@arcgis/core/layers/MapImageLayer';
export default function MapComponent({ locale }: { locale: string }) { export default function MapComponent({ locale }: { locale: string }) {
const legendRoot = useRef<HTMLDivElement>(null); const maskRef = useRef<HTMLDivElement>(null);
const maskRoot = useRef<HTMLDivElement>(null); const tableRef = useRef<HTMLDivElement>(null);
const tableRoot = useRef<HTMLDivElement>(null);
const mapView = useRef<MapView | null>(null); const mapView = useRef<MapView | null>(null);
const previousId = useRef<string | null>(null); const previousId = useRef<string | null>(null);
const mapRef = useRef<HTMLDivElement | null>(null);
const layersRef = useRef<HTMLDivElement>(null);
const layersRoot = useRef<Root | null>(null);
const legendRef = useRef<HTMLDivElement>(null);
const basemapsRef = useRef<HTMLDivElement>(null);
const basemapsRoot = useRef<Root | null>(null);
const printRef = useRef<HTMLDivElement>(null);
const printRoot = useRef<Root | null>(null);
const searchRef = useRef<HTMLDivElement>(document.createElement('div'));
const searchRoot = useRef<Root | null>(null);
const [actionBarExpanded, setActionBarExpanded] = useState<boolean>(false); const [actionBarExpanded, setActionBarExpanded] = useState<boolean>(false);
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
if (!mapView.current) { if (mapRef.current) {
// set locale for ArcGIS Maps SDK widgets // set locale for ArcGIS Maps SDK widgets
intl.setLocale(locale); intl.setLocale(locale);
@ -74,40 +88,16 @@ export default function MapComponent({ locale }: { locale: string }) {
}, },
}); });
const lightgrayBase = new VectorTileLayer({
url: 'https://gis.geosphere.at/portal/sharing/rest/content/items/291da5eab3a0412593b66d384379f89f/resources/styles/root.json',
opacity: 0.5,
});
const lightGrayReference = new VectorTileLayer({
url: 'https://gis.geosphere.at/portal/sharing/rest/content/items/1768e8369a214dfab4e2167d5c5f2454/resources/styles/root.json',
opacity: 1,
});
const worldHillshade = new TileLayer({
url: 'https://services.arcgisonline.com/arcgis/rest/services/Elevation/World_Hillshade/MapServer',
});
const basemapEsri = new Basemap({
baseLayers: [worldHillshade, lightgrayBase, lightGrayReference],
title: 'Esri',
thumbnailUrl:
'https://gis.geosphere.at/portal/sharing/rest/content/items/3eb1510943be4f29ae01c01ce229d8ba/data',
});
const map = new Map({
basemap: basemapEsri,
});
const view = new MapView({ const view = new MapView({
container: 'map-container', container: mapRef.current,
map: map, map: webMap,
padding: { padding: {
left: 49, left: 49,
}, },
popup: { popup: {
dockOptions: { dockOptions: {
position: 'auto', position: 'top-right',
breakpoint: { breakpoint: false,
width: 5000,
},
}, },
dockEnabled: true, dockEnabled: true,
}, },
@ -120,17 +110,15 @@ export default function MapComponent({ locale }: { locale: string }) {
wkid: 3857, wkid: 3857,
}, },
}, },
ui: {
components: ['attribution'],
},
popupEnabled: true,
}); });
mapView.current = view; mapView.current = view;
view.ui.empty('top-left');
webMap.load().then(() => { // add ScaleBar
map.layers = webMap.layers;
createRoot(document.createElement('div')).render(<Search view={view}></Search>);
});
// add further map related UI components
const scaleBar = new ScaleBar({ const scaleBar = new ScaleBar({
view: view, view: view,
unit: 'metric', unit: 'metric',
@ -138,16 +126,124 @@ export default function MapComponent({ locale }: { locale: string }) {
view.ui.add([scaleBar], 'bottom-left'); view.ui.add([scaleBar], 'bottom-left');
if (legendRoot.current) { // render Legend component
if (legendRef.current) {
new Legend({ new Legend({
view: view, view: view,
container: legendRoot.current, container: legendRef.current,
}); });
} }
// render Search component
if (searchRef.current) {
if (!searchRoot.current) {
searchRoot.current = createRoot(searchRef.current);
}
searchRoot.current.render(<Search view={view}></Search>);
}
// render Layers component
if (layersRef.current) {
if (!layersRoot.current) {
layersRoot.current = createRoot(layersRef.current);
}
if (tableRef.current) {
layersRoot.current.render(<Layers view={view} tableDiv={tableRef.current}></Layers>);
}
} }
return () => {}; // render Basemaps component
}, [locale]); if (basemapsRef.current) {
if (!basemapsRoot.current) {
basemapsRoot.current = createRoot(basemapsRef.current);
}
basemapsRoot.current.render(<Basemaps view={mapView.current}></Basemaps>);
}
// // render Print component
if (printRef.current) {
if (!printRoot.current) {
printRoot.current = createRoot(printRef.current);
}
if (maskRef.current) {
printRoot.current.render(<Print view={mapView.current} maskDiv={maskRef.current}></Print>);
}
}
watch(
() => view.popup?.viewModel?.active,
() => console.log(view.popup?.selectedFeature)
);
// view.on('immediate-click', (event) => {
// const mapImageLayer = view.map.layers.find((layer) => layer.title === 'Profilschnitte') as MapImageLayer;
// if (mapImageLayer) {
// mapImageLayer.allSublayers.forEach((sublayer) => {
// if (sublayer.title === 'Bohrprofile') {
// }
// });
// }
// });
// This function fires each time a LayerView is created
// view.on('layerview-create', function (event) {
// // The LayerView for the desired layer
// if (event.layer.type === 'map-image') {
// (event.layer as MapImageLayer).allSublayers.forEach((sublayer) => {
// if (sublayer.title === 'Bohrprofile') {
// let textElement = new TextContent();
// textElement.text = 'Das ist nur ein Test.';
// if (Array.isArray(sublayer.popupTemplate.content)) sublayer.popupTemplate.content.push(textElement);
// sublayer.load().then(async () => {
// const renderer = sublayer.renderer;
// const fieldToValueColorMap = await getColorsForRendererValues(renderer);
// let classBreaks: number[] = [];
// let colors: object[] = [];
// for (const [key, classBreaksToColorMap] of fieldToValueColorMap) {
// for (const [classBreack, color] of classBreaksToColorMap) {
// colors.push(color);
// classBreaks.push(classBreack);
// }
// // while (!stopIteration) {
// // if ($feature['${key}'] > ${classBreaks}[index]) {
// // stopIteration = true;
// // color = ${colors}[index]
// // }
// // }
// const expressionContent = new ExpressionContent({
// expressionInfo: {
// title: 'Legende',
// expression: `
// var stopIteration = true;
// var index = 0;
// return {
// type: "text",
// text: "First class break for key ${key}: ${classBreaks[0]} and value: " + $feature['${key}']
// }
// `,
// },
// });
// if (Array.isArray(sublayer.popupTemplate.content)) {
// sublayer.popupTemplate.content.push(expressionContent);
// }
// }
// });
// }
// });
// }
// });
}
}, [locale, t]);
const handleCalciteActionBarToggle = () => { const handleCalciteActionBarToggle = () => {
setActionBarExpanded(!actionBarExpanded); setActionBarExpanded(!actionBarExpanded);
@ -155,13 +251,13 @@ export default function MapComponent({ locale }: { locale: string }) {
mapView.current.padding = !actionBarExpanded ? { left: 150 } : { left: 49 }; mapView.current.padding = !actionBarExpanded ? { left: 150 } : { left: 49 };
} }
if (tableRoot.current) { if (tableRef.current) {
if (!actionBarExpanded) { if (!actionBarExpanded) {
tableRoot.current.classList.add('left-40'); tableRef.current.classList.add('left-40');
tableRoot.current.classList.remove('left-14'); tableRef.current.classList.remove('left-14');
} else { } else {
tableRoot.current.classList.add('left-14'); tableRef.current.classList.add('left-14');
tableRoot.current.classList.remove('left-40'); tableRef.current.classList.remove('left-40');
} }
} }
}; };
@ -180,7 +276,7 @@ export default function MapComponent({ locale }: { locale: string }) {
} }
if (previousId.current === 'print') { if (previousId.current === 'print') {
maskRoot.current?.classList.add('hidden'); maskRef.current?.classList.add('hidden');
} }
} }
@ -190,7 +286,7 @@ export default function MapComponent({ locale }: { locale: string }) {
previousId.current = nextId; previousId.current = nextId;
if (nextId === 'print') { if (nextId === 'print') {
maskRoot.current?.classList.remove('hidden'); maskRef.current?.classList.remove('hidden');
} }
} else { } else {
previousId.current = null; previousId.current = null;
@ -234,32 +330,30 @@ export default function MapComponent({ locale }: { locale: string }) {
</CalciteActionBar> </CalciteActionBar>
<CalcitePanel data-panel-id="layers" heading={t('layers.title')} hidden> <CalcitePanel data-panel-id="layers" heading={t('layers.title')} hidden>
{mapView.current && tableRoot.current && ( <div ref={layersRef}></div>
<Layers view={mapView.current} tableRoot={tableRoot.current}></Layers>
)}
</CalcitePanel> </CalcitePanel>
<CalcitePanel data-panel-id="basemaps" heading={t('basemaps.title')} hidden> <CalcitePanel data-panel-id="basemaps" heading={t('basemaps.title')} hidden>
{mapView.current && <Basemaps view={mapView.current}></Basemaps>} <div ref={basemapsRef}></div>
</CalcitePanel> </CalcitePanel>
<CalcitePanel data-panel-id="legend" heading={t('legend.title')} hidden> <CalcitePanel data-panel-id="legend" heading={t('legend.title')} hidden>
<div ref={legendRoot}></div> <div ref={legendRef}></div>
</CalcitePanel> </CalcitePanel>
<CalcitePanel data-panel-id="print" heading={t('print.heading')} hidden> <CalcitePanel data-panel-id="print" heading={t('print.heading')} hidden>
{mapView.current && maskRoot.current && <Print view={mapView.current} maskRoot={maskRoot.current}></Print>} <div ref={printRef}></div>
</CalcitePanel> </CalcitePanel>
<CalcitePanel data-panel-id="info" heading="Info" hidden> <CalcitePanel data-panel-id="info" heading="Info" hidden>
<div id="info-container"></div> <div id="info-container"></div>
</CalcitePanel> </CalcitePanel>
</CalciteShellPanel> </CalciteShellPanel>
<div className="h-screen w-full" id="map-container"> <div className="h-screen w-full" ref={mapRef}>
<div ref={tableRoot} className="hidden absolute left-14 bottom-5 h-1/3 right-2 border border-gray-400"></div> <div ref={tableRef} className="hidden absolute left-14 bottom-5 h-1/3 right-2 border border-gray-400"></div>
</div> </div>
<div <div
ref={maskRoot} ref={maskRef}
className="hidden absolute bg-red-300 border-2 border-red-600 opacity-50 pointer-events-none" className="hidden absolute bg-red-300 border-2 border-red-600 opacity-50 pointer-events-none"
></div> ></div>
</CalciteShell> </CalciteShell>

View File

@ -63,7 +63,7 @@ const formats: Format = {
'A3 Querformat mit Legende': [400, 215], 'A3 Querformat mit Legende': [400, 215],
}; };
export default function Print({ view, maskRoot }: { view: MapView; maskRoot: HTMLDivElement }) { export default function Print({ view, maskDiv }: { view: MapView; maskDiv: HTMLDivElement }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [title, setTitle] = useState<string>('GeoSphere Austria'); const [title, setTitle] = useState<string>('GeoSphere Austria');
@ -183,11 +183,11 @@ export default function Print({ view, maskRoot }: { view: MapView; maskRoot: HTM
const maskWidth = clamp(Math.round(lowerRight.x - upperLeft.x), 0, view.width); const maskWidth = clamp(Math.round(lowerRight.x - upperLeft.x), 0, view.width);
const maskHeight = clamp(Math.round(lowerRight.y - upperLeft.y), 0, view.height); const maskHeight = clamp(Math.round(lowerRight.y - upperLeft.y), 0, view.height);
if (maskRoot) { if (maskDiv) {
maskRoot.style.left = left + 'px'; maskDiv.style.left = left + 'px';
maskRoot.style.top = top + 'px'; maskDiv.style.top = top + 'px';
maskRoot.style.width = maskWidth + 'px'; maskDiv.style.width = maskWidth + 'px';
maskRoot.style.height = maskHeight + 'px'; maskDiv.style.height = maskHeight + 'px';
} }
} }
}; };

View File

@ -1,4 +1,4 @@
import { useRef, useState, useEffect } from 'react'; import { useRef, useState, useEffect, MouseEventHandler } from 'react';
import { Root, createRoot } from 'react-dom/client'; import { Root, createRoot } from 'react-dom/client';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -40,14 +40,17 @@ import SimpleLineSymbol from '@arcgis/core/symbols/SimpleLineSymbol';
import SimpleMarkerSymbol from '@arcgis/core/symbols/SimpleMarkerSymbol'; import SimpleMarkerSymbol from '@arcgis/core/symbols/SimpleMarkerSymbol';
import Sublayer from '@arcgis/core/layers/support/Sublayer'; import Sublayer from '@arcgis/core/layers/support/Sublayer';
import PopupTemplate from '@arcgis/core/PopupTemplate'; import PopupTemplate from '@arcgis/core/PopupTemplate';
import { watch } from '@arcgis/core/core/reactiveUtils';
// create feaure layer from URL of data index layer // create feaure layer from URL of data index layer
const datenIndexURL = 'https://gis.geosphere.at/maps/rest/services/datenindex/raster_5000/MapServer/0'; const datenIndexURL = 'https://gis.geosphere.at/maps/rest/services/datenindex/raster_1000/MapServer/0';
const indexLayer = new FeatureLayer({ const indexLayer = new FeatureLayer({
url: datenIndexURL, url: datenIndexURL,
title: 'Datenindex 1:5.000', title: 'Datenindex 1:1.000',
opacity: 0, opacity: 1,
legendEnabled: false, legendEnabled: false,
visible: false,
listMode: 'hide',
}); });
// custom type definitions // custom type definitions
@ -70,152 +73,30 @@ interface LayerToFeaturesMap {
[key: string]: string[]; [key: string]: string[];
} }
// custom React component // create query from cellcode for 3x3 neighbourhood
export default function SearchComponent({ view }: { view: MapView }) { const createQueryFromCellcode = (cellcode: string) => {
const [currentTarget, setCurrentTarget] = useState<Graphic | null>(null); const { north, east }: any = cellcode.match(/N(?<north>\d+)E(?<east>\d+)/)?.groups;
const northNumber = parseInt(north);
const eastNumber = parseInt(east);
const operations = [
[1, -1],
[1, 0],
[1, 1],
[0, -1],
[0, 1],
[-1, -1],
[-1, 0],
[-1, 1],
];
const rendered = useRef<boolean>(false); const cellcodeQueries = operations.map(
const currentGraphicRef = useRef<Graphic | null>(null); (operation) => `cellcode = '1kmN${northNumber + operation[0]}E${eastNumber + operation[1]}'`
currentGraphicRef.current = currentTarget; );
return `cellcode = '${cellcode}' OR ` + cellcodeQueries.join(' OR ');
};
const { t } = useTranslation(); // remove layers from layer tree by given filter
const removeLayers = (layers: (CustomGroupLayer | CustomLayer)[], keepLayer: any): any => {
// get map image layer from sublayer
const getMapImageLayer = (layerURL: string): MapImageLayer | undefined => {
if (view) {
const filteredLayerViews = view.allLayerViews.filter((layerView) => {
const regex = /^https:\/\/.+\/MapServer/g;
const matches = layerURL.match(regex);
let mapImageLayerURL;
if (matches && matches.length > 0) mapImageLayerURL = matches[0];
if (layerView.layer.type === 'map-image') {
return (layerView.layer as MapImageLayer).url === mapImageLayerURL;
} else {
return false;
}
});
let mapImageLayer;
if (filteredLayerViews.length > 0) {
mapImageLayer = filteredLayerViews.at(0).layer as MapImageLayer;
}
return mapImageLayer;
}
};
// handle toggle layer visibility
const handleToggleVisibility = (event: any) => {
const layerItem = event.target;
const layerId = layerItem.getAttribute('text');
const icon = layerItem.getAttribute('icon');
if (layerId) {
const layer = view.map?.findLayerById(layerId);
if (icon === 'view-hide') {
layerItem.setAttribute('icon', 'view-visible');
if (layer) {
layer.visible = true;
}
} else {
layerItem.setAttribute('icon', 'view-hide');
if (layer) {
layer.visible = false;
}
}
}
};
// handle zoom to feature
const handleZoomTo = async (event: any) => {
const layerURL = event.target.getAttribute('text');
const res = await fetch(layerURL + '?f=json');
const json = await res.json();
const mapImageLayer = getMapImageLayer(layerURL);
const sr = mapImageLayer?.spatialReference;
const geometry = json.feature?.geometry;
view.graphics.removeAll();
const higlightOptions = view.highlightOptions;
if (geometry.x && geometry.y && higlightOptions && higlightOptions.color && higlightOptions.fillOpacity) {
const point = new Point({
x: geometry.x,
y: geometry.y,
spatialReference: sr,
});
const graphic = new Graphic({
geometry: point,
symbol: {
type: 'simple-marker',
style: 'circle',
size: '8px',
color: [
higlightOptions.color.r,
higlightOptions.color.g,
higlightOptions.color.b,
higlightOptions.fillOpacity,
],
outline: {
color: [higlightOptions.color.r, higlightOptions.color.g, higlightOptions.color.b, higlightOptions.color.a],
width: 1,
},
} as unknown as SimpleMarkerSymbol,
});
view.graphics.add(graphic);
view.goTo(geodesicBuffer(point, 1000, 'meters'));
return;
}
if (geometry.paths && higlightOptions && higlightOptions.color && higlightOptions.fillOpacity) {
const polyline = new Polyline({
paths: geometry.paths,
spatialReference: sr,
});
const graphic = new Graphic({
geometry: polyline,
symbol: {
type: 'simple-line',
color: [higlightOptions.color.r, higlightOptions.color.g, higlightOptions.color.b],
width: '2px',
} as unknown as SimpleLineSymbol,
});
view.graphics.add(graphic);
view.goTo(geodesicBuffer(polyline.extent.center, 5000, 'meters'));
return;
}
if (geometry.rings && higlightOptions && higlightOptions.color && higlightOptions.fillOpacity) {
const polygon = new Polygon({
rings: geometry.rings,
spatialReference: sr,
});
const graphic = new Graphic({
geometry: polygon,
symbol: {
type: 'simple-fill',
color: [
higlightOptions.color.r,
higlightOptions.color.g,
higlightOptions.color.b,
higlightOptions.fillOpacity,
],
outline: {
color: [higlightOptions.color.r, higlightOptions.color.g, higlightOptions.color.b, higlightOptions.color.a],
width: 1,
},
} as unknown as SimpleFillSymbol,
});
view.graphics.add(graphic);
view.goTo(polygon);
return;
}
};
// remove layers from layer tree by given filter
const removeLayers = (layers: (CustomGroupLayer | CustomLayer)[], keepLayer: any): any => {
return layers return layers
.filter((layer) => keepLayer(layer)) .filter((layer) => keepLayer(layer))
.map((layer) => { .map((layer) => {
@ -225,10 +106,10 @@ export default function SearchComponent({ view }: { view: MapView }) {
return layer; return layer;
} }
}); });
}; };
// get custom layer objects from layer tree // get custom layer objects from layer tree
const getLayerObjects = (layers: any) => { const getLayerObjects = (layers: any) => {
return layers.map((layer: any) => { return layers.map((layer: any) => {
if (layer.layers) { if (layer.layers) {
return { return {
@ -242,10 +123,20 @@ export default function SearchComponent({ view }: { view: MapView }) {
return { id: layer.id, type: layer.type, title: layer.title, visible: layer.visible }; return { id: layer.id, type: layer.type, title: layer.title, visible: layer.visible };
} }
}); });
}; };
// build query string for custom search source (BEV geocoding service) // build query string for custom search source (BEV geocoding service)
const buildQueryString = (searchTerm: string) => `?term=${encodeURI(searchTerm)}`; const buildQueryString = (searchTerm: string) => `?term=${encodeURI(searchTerm)}`;
// custom React component
export default function SearchComponent({ view }: { view: MapView }) {
const [currentTarget, setCurrentTarget] = useState<Graphic | null>(null);
const rendered = useRef<boolean>(false);
const currentGraphicRef = useRef<Graphic | null>(null);
currentGraphicRef.current = currentTarget;
const { t } = useTranslation();
const url = 'https://kataster.bev.gv.at/api/all4map'; const url = 'https://kataster.bev.gv.at/api/all4map';
const customSearchSource = new SearchSource({ const customSearchSource = new SearchSource({
@ -340,32 +231,171 @@ export default function SearchComponent({ view }: { view: MapView }) {
}, },
}); });
// create query from cellcode for 3x3 neighbourhood // get map image layer from sublayer
const createQueryFromCellcode = (cellcode: string) => { const getMapImageLayer = (layerURL: string): MapImageLayer | undefined => {
const { north, east }: any = cellcode.match(/N(?<north>\d+)E(?<east>\d+)/)?.groups; if (view) {
const northNumber = parseInt(north); const filteredLayerViews = view.allLayerViews.filter((layerView) => {
const eastNumber = parseInt(east); const regex = /^https:\/\/.+\/MapServer/g;
const operations = [ const matches = layerURL.match(regex);
[1, -1], let mapImageLayerURL;
[1, 0], if (matches && matches.length > 0) mapImageLayerURL = matches[0];
[1, 1],
[0, -1],
[0, 1],
[-1, -1],
[-1, 0],
[-1, 1],
];
const cellcodeQueries = operations.map( if (layerView.layer.type === 'map-image') {
(operation) => `cellcode = '10kmN${northNumber + operation[0]}E${eastNumber + operation[1]}'` return (layerView.layer as MapImageLayer).url === mapImageLayerURL;
); } else {
return `cellcode = '${cellcode}' OR ` + cellcodeQueries.join(' OR '); return false;
}
});
let mapImageLayer;
if (filteredLayerViews.length > 0) {
mapImageLayer = filteredLayerViews.at(0).layer as MapImageLayer;
}
return mapImageLayer;
}
};
// handle toggle layer visibility
const handleToggleVisibility = (event: any) => {
const layerItem = event.target;
const layerId = layerItem.getAttribute('text');
const icon = layerItem.getAttribute('icon');
if (layerId) {
const layer = view.map?.findLayerById(layerId);
if (icon === 'view-hide') {
layerItem.setAttribute('icon', 'view-visible');
if (layer) {
layer.visible = true;
}
} else {
layerItem.setAttribute('icon', 'view-hide');
if (layer) {
layer.visible = false;
}
}
}
}; };
useEffect(() => { useEffect(() => {
if (rendered.current) return; // handle zoom to feature
rendered.current = true; const handleZoomTo = async (event: any) => {
const layerURL = event.target.getAttribute('text');
const res = await fetch(layerURL + '?f=json');
const json = await res.json();
const mapImageLayer = getMapImageLayer(layerURL);
const sr = mapImageLayer?.spatialReference;
const geometry = json.feature?.geometry;
view.graphics.removeAll();
const higlightOptions = view.highlightOptions;
if (geometry.x && geometry.y && higlightOptions && higlightOptions.color && higlightOptions.fillOpacity) {
const point = new Point({
x: geometry.x,
y: geometry.y,
spatialReference: sr,
});
const graphic = new Graphic({
geometry: point,
symbol: {
type: 'simple-marker',
style: 'circle',
size: '8px',
color: [
higlightOptions.color.r,
higlightOptions.color.g,
higlightOptions.color.b,
higlightOptions.fillOpacity,
],
outline: {
color: [
higlightOptions.color.r,
higlightOptions.color.g,
higlightOptions.color.b,
higlightOptions.color.a,
],
width: 1,
},
} as unknown as SimpleMarkerSymbol,
});
view.graphics.add(graphic);
view.goTo(geodesicBuffer(point, 1000, 'meters'));
return;
}
if (geometry.paths && higlightOptions && higlightOptions.color && higlightOptions.fillOpacity) {
const polyline = new Polyline({
paths: geometry.paths,
spatialReference: sr,
});
const graphic = new Graphic({
geometry: polyline,
symbol: {
type: 'simple-line',
color: [higlightOptions.color.r, higlightOptions.color.g, higlightOptions.color.b],
width: '2px',
} as unknown as SimpleLineSymbol,
});
view.graphics.add(graphic);
view.goTo(geodesicBuffer(polyline.extent.center, 5000, 'meters'));
return;
}
if (geometry.rings && higlightOptions && higlightOptions.color && higlightOptions.fillOpacity) {
const polygon = new Polygon({
rings: geometry.rings,
spatialReference: sr,
});
const graphic = new Graphic({
geometry: polygon,
symbol: {
type: 'simple-fill',
color: [
higlightOptions.color.r,
higlightOptions.color.g,
higlightOptions.color.b,
higlightOptions.fillOpacity,
],
outline: {
color: [
higlightOptions.color.r,
higlightOptions.color.g,
higlightOptions.color.b,
higlightOptions.color.a,
],
width: 1,
},
} as unknown as SimpleFillSymbol,
});
view.graphics.add(graphic);
view.goTo(polygon);
return;
}
};
// watch for visibility changes
const handle = watch(
() => view.map.allLayers.map((layer) => [layer.id, layer.visible]),
(newValues, oldValues) => {
newValues.forEach((value, key) => {
// visibility changed
if (oldValues.at(key) && value[1] !== oldValues.at(key)[1]) {
const layerId = value[0] as string;
const calciteAction = document.querySelector(`calcite-action[text='${layerId}']`);
if (calciteAction) {
if (value[1]) {
calciteAction.setAttribute('icon', 'view-visible');
} else {
calciteAction.setAttribute('icon', 'view-hide');
}
}
}
});
}
);
// add data index layer // add data index layer
view.map.layers.push(indexLayer); view.map.layers.push(indexLayer);
@ -377,11 +407,13 @@ export default function SearchComponent({ view }: { view: MapView }) {
includeDefaultSources: false, includeDefaultSources: false,
}); });
// empty top-left corner of MapView for Search component
view.ui.add(search, 'top-left'); view.ui.add(search, 'top-left');
// add event handler for select-result events // add event handler for select-result events
search.on('select-result', (event) => { search.on('select-result', (event) => {
view.closePopup(); view.closePopup();
view.graphics.removeAll();
// get selected feature and display it on map // get selected feature and display it on map
const graphic = event.result.feature; const graphic = event.result.feature;
@ -396,9 +428,6 @@ export default function SearchComponent({ view }: { view: MapView }) {
setCurrentTarget(graphic); setCurrentTarget(graphic);
} }
view.graphics.removeAll();
view.graphics.add(graphic);
// query for intersecting features // query for intersecting features
indexLayer indexLayer
.queryFeatures({ .queryFeatures({
@ -496,19 +525,69 @@ export default function SearchComponent({ view }: { view: MapView }) {
} }
const text = sublayer?.id ? sublayer?.id.toString() : ''; const text = sublayer?.id ? sublayer?.id.toString() : '';
let sublayerWasVisible: boolean = false;
let mapImageLayerWasVisible: boolean = false;
const handleMouseEnter: MouseEventHandler<HTMLCalciteAccordionItemElement> = (event) => {
(event.target as HTMLElement).classList.add('bg-gray-100');
if (mapImageLayer && !mapImageLayer.visible) {
mapImageLayerWasVisible = false;
mapImageLayer.visible = true;
} else {
mapImageLayerWasVisible = true;
}
if (sublayer && !sublayer.visible) {
sublayerWasVisible = false;
sublayer.visible = true;
}
};
const handleMouseLeave: MouseEventHandler<HTMLCalciteAccordionItemElement> = (event) => {
(event.target as HTMLElement).classList.remove('bg-gray-100');
if (mapImageLayer && !mapImageLayerWasVisible) {
mapImageLayer.visible = false;
}
if (sublayer && !sublayerWasVisible) {
sublayer.visible = false;
}
};
const handleSublayerVisibilityToggle: MouseEventHandler<HTMLCalciteActionElement> = (event: any) => {
if (mapImageLayer && sublayer) {
if (event.target?.getAttribute('icon') === 'view-hide') {
mapImageLayerWasVisible = true;
mapImageLayer.visible = true;
}
if (event.target?.getAttribute('icon') === 'view-visible') {
event.target?.setAttribute('icon', 'view-hide');
sublayer.visible = false;
sublayerWasVisible = false;
} else {
event.target?.setAttribute('icon', 'view-visible');
sublayer.visible = true;
sublayerWasVisible = true;
}
}
};
// create UI item for sublayer // create UI item for sublayer
const accordionItem = ( const accordionItem = (
<CalciteAccordionItem heading={sublayer?.title} key={sublayer?.id}> <CalciteAccordionItem
heading={sublayer?.title}
key={sublayer?.id}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<CalciteAction <CalciteAction
slot="actions-end" slot="actions-end"
icon={sublayer?.visible ? 'view-visible' : 'view-hide'} icon={sublayer?.visible ? 'view-visible' : 'view-hide'}
text={text} text={text}
appearance="transparent" appearance="transparent"
onClick={() => { scale="s"
if (sublayer) { onClick={handleSublayerVisibilityToggle}
sublayer.visible = !sublayer.visible;
}
}}
></CalciteAction> ></CalciteAction>
<CalciteList> <CalciteList>
@ -565,7 +644,7 @@ export default function SearchComponent({ view }: { view: MapView }) {
(layer.type === 'group' && (layer.type === 'group' &&
((layer as CustomGroupLayer).layers.length === 0 || ((layer as CustomGroupLayer).layers.length === 0 ||
(layer as CustomGroupLayer).layers.every( (layer as CustomGroupLayer).layers.every(
(child) => child.type === 'imagery' || child.type === 'imagery-tile' (child) => child.type === 'imagery' || child.type === 'imagery-tile' || child.type === 'tile'
))) || ))) ||
layer.type === 'feature' || layer.type === 'feature' ||
layer.type === 'imagery' || layer.type === 'imagery' ||
@ -666,7 +745,7 @@ export default function SearchComponent({ view }: { view: MapView }) {
}; };
indexLayer.popupTemplate = popupTemplate as unknown as PopupTemplate; indexLayer.popupTemplate = popupTemplate as unknown as PopupTemplate;
}); }, []);
return null; return null;
} }

View File

@ -1,6 +1,6 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
output: 'standalone', // output: 'standalone',
}; };
module.exports = nextConfig; module.exports = nextConfig;

713
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,11 +3,12 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "cross-env NODE_OPTIONS='--inspect' next dev -p 5000", "dev": "next dev",
"build": "npm run copy && next build", "build": "npm run copy-arcgis-maps-assets && npm run copy-calcite-components-assets && next build",
"start": "npm run copy && next start -p 5000", "start": "next start",
"lint": "next lint", "lint": "next lint",
"copy": "ncp ./node_modules/@arcgis/core/assets ./public/assets" "copy-arcgis-maps-assets": "ncp ./node_modules/@arcgis/core/assets ./public/assets",
"copy-calcite-components-assets": "ncp ./node_modules/@esri/calcite-components/dist/calcite/assets ./public/assets/"
}, },
"dependencies": { "dependencies": {
"@arcgis/core": "^4.27.6", "@arcgis/core": "^4.27.6",
@ -19,17 +20,18 @@
"eslint": "8.45.0", "eslint": "8.45.0",
"eslint-config-next": "13.4.12", "eslint-config-next": "13.4.12",
"i18next": "^23.4.4", "i18next": "^23.4.4",
"ncp": "^2.0.0",
"negotiator": "^0.6.3", "negotiator": "^0.6.3",
"next": "^13.5.2", "next": "^13.5.2",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-i18next": "^13.2.2", "react-i18next": "^13.2.2",
"sharp": "^0.32.6",
"typescript": "5.1.6" "typescript": "5.1.6"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"ncp": "^2.0.0",
"postcss": "^8.4.27", "postcss": "^8.4.27",
"tailwindcss": "^3.3.3" "tailwindcss": "^3.3.3"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 912 KiB