Add map functionality components
This commit is contained in:
parent
6ab4bb6daa
commit
c8046c39f1
649 changed files with 21412 additions and 727 deletions
150
app/[locale]/basemap-list.tsx
Normal file
150
app/[locale]/basemap-list.tsx
Normal file
|
@ -0,0 +1,150 @@
|
|||
import { useRef, useEffect, RefObject } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import initI18n from './i18n';
|
||||
|
||||
import MapView from '@arcgis/core/views/MapView';
|
||||
import Basemap from '@arcgis/core/Basemap';
|
||||
import esriRequest from '@arcgis/core/request';
|
||||
import BaseTileLayer from '@arcgis/core/layers/BaseTileLayer';
|
||||
import BasemapGallery from '@arcgis/core/widgets/BasemapGallery';
|
||||
import VectorTileLayer from '@arcgis/core/layers/VectorTileLayer';
|
||||
import TileLayer from '@arcgis/core/layers/TileLayer';
|
||||
|
||||
const BEVURL = 'https://maps.bev.gv.at/tiles/{z}/{x}/{y}.png';
|
||||
|
||||
// load localized strings
|
||||
const match = location.pathname.match(/\/(\w+)/);
|
||||
if (match && match.length > 1) {
|
||||
const locale = match[1];
|
||||
initI18n(locale);
|
||||
}
|
||||
|
||||
export default function Basemaps({ view }: { view: MapView }) {
|
||||
const basemapGalleryContainer = useRef<HTMLDivElement | null>(null);
|
||||
const rendered = useRef<boolean>(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const BEVTileLayer = (BaseTileLayer as any).createSubclass({
|
||||
// define instance properties
|
||||
properties: {
|
||||
urlTemplate: null,
|
||||
},
|
||||
|
||||
// generate the tile url for a given level, row and column
|
||||
getTileUrl: function (level: number, row: number, col: number) {
|
||||
const z = level.toString().padStart(2, '0');
|
||||
let x = row.toString().padStart(9, '0');
|
||||
let y = col.toString().padStart(9, '0');
|
||||
|
||||
// this is the BEV tiling scheme
|
||||
x = x.substring(0, 3) + '/' + x.substring(3, 3) + '/' + x.substring(6, 3);
|
||||
y = y.substring(0, 3) + '/' + y.substring(3, 3) + '/' + y.substring(6, 3);
|
||||
|
||||
return this.urlTemplate.replace(/\{z\}/g, z).replace(/\{x\}/g, y).replace(/\{y\}/g, x);
|
||||
},
|
||||
|
||||
// This method fetches tiles for the specified level and size.
|
||||
fetchTile: function (level: number, row: number, col: number, options: any) {
|
||||
// get the url for this tile
|
||||
const url = this.getTileUrl(level, row, col);
|
||||
|
||||
// request for tiles based on the generated url
|
||||
// the signal option ensures that obsolete requests are aborted
|
||||
return esriRequest(url, {
|
||||
responseType: 'image',
|
||||
}).then(
|
||||
function (this: any, response: any) {
|
||||
// when esri request resolves successfully
|
||||
// get the image from the response
|
||||
const image = response.data;
|
||||
const width = this.tileInfo.size[0];
|
||||
const height = this.tileInfo.size[0];
|
||||
|
||||
// create a canvas with 2D rendering context
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
// Draw the blended image onto the canvas.
|
||||
context?.drawImage(image, 0, 0, width, height);
|
||||
const imageData = context?.getImageData(0, 0, canvas.width, canvas.height);
|
||||
if (imageData) context?.putImageData(imageData, 0, 0);
|
||||
|
||||
return canvas;
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
copyright: t('basemaps.copyright-bev'),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (rendered.current) return;
|
||||
rendered.current = true;
|
||||
|
||||
if (view) {
|
||||
const topoLayer = new BEVTileLayer({
|
||||
urlTemplate: BEVURL,
|
||||
title: 'BEV-Topografie',
|
||||
});
|
||||
|
||||
const basemapBEV = new Basemap({
|
||||
baseLayers: [topoLayer],
|
||||
title: t('basemaps.basemap-bev-title'),
|
||||
id: 'basemap',
|
||||
thumbnailUrl:
|
||||
'https://gis.geosphere.at/portal/sharing/rest/content/items/1a94736328a1458abe435d23508f1822/data',
|
||||
});
|
||||
|
||||
const basemapAT = new Basemap({
|
||||
portalItem: {
|
||||
id: 'df7d6cc9be754db8970614d2ee661f57',
|
||||
portal: {
|
||||
url: 'https://gis.geosphere.at/portal',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const basemapOSM = new Basemap({
|
||||
portalItem: {
|
||||
id: '5f6efd84434842fb9dcdcf6b9116dcd9',
|
||||
portal: { url: 'https://gis.geosphere.at/portal' },
|
||||
},
|
||||
});
|
||||
|
||||
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: t('basemaps.basemap-esri-title'),
|
||||
thumbnailUrl:
|
||||
'https://gis.geosphere.at/portal/sharing/rest/content/items/3eb1510943be4f29ae01c01ce229d8ba/data',
|
||||
});
|
||||
|
||||
const basemapGalleryDiv = document.createElement('div');
|
||||
basemapGalleryContainer.current?.append(basemapGalleryDiv);
|
||||
|
||||
new BasemapGallery({
|
||||
container: basemapGalleryDiv,
|
||||
view: view,
|
||||
source: [basemapAT, basemapBEV, basemapOSM, basemapEsri],
|
||||
});
|
||||
}
|
||||
|
||||
return () => {};
|
||||
});
|
||||
|
||||
return <div ref={basemapGalleryContainer}></div>;
|
||||
}
|
|
@ -1,20 +1,44 @@
|
|||
{
|
||||
"heading": "PDF drucken",
|
||||
"titleLabel": "Titel",
|
||||
"scaleLabel": "Maßstab",
|
||||
"formatLabel": "Format",
|
||||
"button": {
|
||||
"print": "Drucken",
|
||||
"cancel": "Abbrechen"
|
||||
"basemaps": {
|
||||
"title": "Grundkarten",
|
||||
"copyright-bev": "© BEV, Bundesamt für Eich- und Vermessungswesen",
|
||||
"basemap-esri-title": "Grau mit Geländeschummerung",
|
||||
"basemap-bev-title": "BEV Topografie"
|
||||
},
|
||||
"formats": {
|
||||
"A4 Hochformat": "A4 Hochformat",
|
||||
"A4 Hochformat mit Legende": "A4 Hochformat mit Legende",
|
||||
"A4 Querformat": "A4 Querformat",
|
||||
"A4 Querformat mit Legende": "A4 Querformat mit Legende",
|
||||
"A3 Hochformat": "A3 Hochformat",
|
||||
"A3 Hochformat mit Legende": "A3 Hochformat mit Legende",
|
||||
"A3 Querformat": "A3 Querformat",
|
||||
"A3 Querformat mit Legende": "A3 Querformat mit Legende"
|
||||
"search": {
|
||||
"placeholder": "Orts- oder Adresssuche",
|
||||
"popup-title": "Relevante Daten"
|
||||
},
|
||||
"legend": {
|
||||
"title": "Legende"
|
||||
},
|
||||
"print": {
|
||||
"heading": "Drucken",
|
||||
"sub-heading": "PDF drucken",
|
||||
"titleLabel": "Titel",
|
||||
"scaleLabel": "Maßstab",
|
||||
"formatLabel": "Format",
|
||||
"button": {
|
||||
"print": "Drucken",
|
||||
"cancel": "Abbrechen"
|
||||
},
|
||||
"formats": {
|
||||
"A4 Hochformat": "A4 Hochformat",
|
||||
"A4 Hochformat mit Legende": "A4 Hochformat mit Legende",
|
||||
"A4 Querformat": "A4 Querformat",
|
||||
"A4 Querformat mit Legende": "A4 Querformat mit Legende",
|
||||
"A3 Hochformat": "A3 Hochformat",
|
||||
"A3 Hochformat mit Legende": "A3 Hochformat mit Legende",
|
||||
"A3 Querformat": "A3 Querformat",
|
||||
"A3 Querformat mit Legende": "A3 Querformat mit Legende"
|
||||
}
|
||||
},
|
||||
"layers": {
|
||||
"title": "Layer",
|
||||
"increaseOpacity": "Deckkraft erhöhen",
|
||||
"decreaseOpacity": "Deckkraft verringern",
|
||||
"table": {
|
||||
"close": "Schließen"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,44 @@
|
|||
{
|
||||
"heading": "Print PDF",
|
||||
"titleLabel": "Title",
|
||||
"scaleLabel": "Scale",
|
||||
"formatLabel": "Format",
|
||||
"button": {
|
||||
"print": "Print",
|
||||
"cancel": "Cancel"
|
||||
"basemaps": {
|
||||
"title": "Basemaps",
|
||||
"copyright-bev": "© BEV, Federal Office of Metrology and Surveying",
|
||||
"basemap-esri-title": "Gray with hillshade",
|
||||
"basemap-bev-title": "BEV topography"
|
||||
},
|
||||
"formats": {
|
||||
"A4 Hochformat": "A4 portrait",
|
||||
"A4 Hochformat mit Legende": "A4 portrait with legend",
|
||||
"A4 Querformat": "A4 landscape",
|
||||
"A4 Querformat mit Legende": "A4 landscape with legend",
|
||||
"A3 Hochformat": "A3 portrait",
|
||||
"A3 Hochformat mit Legende": "A3 portrait with legend",
|
||||
"A3 Querformat": "A3 landscape",
|
||||
"A3 Querformat mit Legende": "A3 landscape with legend"
|
||||
"search": {
|
||||
"placeholder": "Search for place or address",
|
||||
"popup-title": "Relevant data"
|
||||
},
|
||||
"print": {
|
||||
"heading": "Print",
|
||||
"sub-heading": "Print PDF",
|
||||
"titleLabel": "Title",
|
||||
"scaleLabel": "Scale",
|
||||
"formatLabel": "Format",
|
||||
"button": {
|
||||
"print": "Print",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"formats": {
|
||||
"A4 Hochformat": "A4 portrait",
|
||||
"A4 Hochformat mit Legende": "A4 portrait with legend",
|
||||
"A4 Querformat": "A4 landscape",
|
||||
"A4 Querformat mit Legende": "A4 landscape with legend",
|
||||
"A3 Hochformat": "A3 portrait",
|
||||
"A3 Hochformat mit Legende": "A3 portrait with legend",
|
||||
"A3 Querformat": "A3 landscape",
|
||||
"A3 Querformat mit Legende": "A3 landscape with legend"
|
||||
}
|
||||
},
|
||||
"layers": {
|
||||
"title": "Layers",
|
||||
"increaseOpacity": "Increase opacity",
|
||||
"decreaseOpacity": "Decrease opacity",
|
||||
"table": {
|
||||
"close": "Close"
|
||||
}
|
||||
},
|
||||
"legend": {
|
||||
"title": "Legend"
|
||||
}
|
||||
}
|
||||
|
|
155
app/[locale]/layer-list.tsx
Normal file
155
app/[locale]/layer-list.tsx
Normal file
|
@ -0,0 +1,155 @@
|
|||
import { useRef, useEffect, RefObject } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import initI18n from './i18n';
|
||||
|
||||
import MapView from '@arcgis/core/views/MapView';
|
||||
import LayerList from '@arcgis/core/widgets/LayerList';
|
||||
import FeatureTable from '@arcgis/core/widgets/FeatureTable';
|
||||
import ImageryTileLayer from '@arcgis/core/layers/ImageryTileLayer';
|
||||
import MapImageLayer from '@arcgis/core/layers/MapImageLayer';
|
||||
import Sublayer from '@arcgis/core/layers/support/Sublayer';
|
||||
import ButtonMenuItem from '@arcgis/core/widgets/FeatureTable/Grid/support/ButtonMenuItem';
|
||||
|
||||
// load localized strings
|
||||
const match = location.pathname.match(/\/(\w+)/);
|
||||
if (match && match.length > 1) {
|
||||
const locale = match[1];
|
||||
initI18n(locale);
|
||||
}
|
||||
|
||||
export default function Layers({ view, tableRoot }: { view: MapView; tableRoot: RefObject<HTMLDivElement> }) {
|
||||
const htmlDiv = useRef<HTMLDivElement>(null);
|
||||
const featureTable = useRef<FeatureTable | null>(null);
|
||||
const rendered = useRef<boolean>(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (rendered.current) return;
|
||||
rendered.current = true;
|
||||
|
||||
const createTable = async (layer: Sublayer, view: MapView) => {
|
||||
const tableContainer = document.createElement('div');
|
||||
tableContainer.className = 'h-full w-full';
|
||||
|
||||
if (tableRoot && tableRoot.current) {
|
||||
tableRoot.current.classList.remove('hidden');
|
||||
tableRoot.current.append(tableContainer);
|
||||
}
|
||||
|
||||
const featureLayer = await layer.createFeatureLayer();
|
||||
|
||||
featureTable.current = new FeatureTable({
|
||||
view: view,
|
||||
layer: featureLayer,
|
||||
container: tableContainer,
|
||||
menuConfig: {
|
||||
items: [
|
||||
{
|
||||
label: t('layers.table.close'),
|
||||
iconClass: 'esri-icon-close',
|
||||
clickFunction: function () {
|
||||
featureTable.current?.destroy();
|
||||
if (tableRoot && tableRoot.current) {
|
||||
tableRoot.current.classList.add('hidden');
|
||||
}
|
||||
},
|
||||
} as unknown as ButtonMenuItem,
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (htmlDiv.current) {
|
||||
const arcGISAPIWidgetContainer = document.createElement('div');
|
||||
htmlDiv.current.append(arcGISAPIWidgetContainer);
|
||||
|
||||
const layerList = new LayerList({
|
||||
view,
|
||||
container: htmlDiv.current,
|
||||
selectionEnabled: true,
|
||||
listItemCreatedFunction: async function (event) {
|
||||
const item = event.item;
|
||||
const type = item.layer.type;
|
||||
|
||||
if (type === 'imagery-tile' || type === 'map-image') {
|
||||
item.actionsSections = [
|
||||
[
|
||||
{
|
||||
title: 'Info',
|
||||
className: 'esri-icon-description',
|
||||
id: 'info',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
title: t('layers.increaseOpacity'),
|
||||
className: 'esri-icon-up',
|
||||
id: 'increase-opacity',
|
||||
},
|
||||
{
|
||||
title: t('layers.decreaseOpacity'),
|
||||
className: 'esri-icon-down',
|
||||
id: 'decrease-opacity',
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if (item.layer.declaredClass === 'esri.layers.support.Sublayer') {
|
||||
item.actionsSections = [
|
||||
[
|
||||
{
|
||||
title: 'Table',
|
||||
className: 'esri-icon-table',
|
||||
id: 'table',
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
layerList.on('trigger-action', async (event) => {
|
||||
const id = event.action.id;
|
||||
const item = event.item;
|
||||
const layer = item.layer;
|
||||
const type = layer.type;
|
||||
|
||||
if (id === 'info') {
|
||||
if (type === 'imagery-tile') {
|
||||
window.open((layer as ImageryTileLayer).portalItem?.itemPageUrl);
|
||||
}
|
||||
|
||||
if (type === 'map-image') {
|
||||
window.open((layer as MapImageLayer).portalItem?.itemPageUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (id === 'increase-opacity') {
|
||||
if (layer.opacity < 1) {
|
||||
layer.opacity += 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
if (id === 'decrease-opacity') {
|
||||
if (layer.opacity > 0) {
|
||||
layer.opacity -= 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
if (id === 'table') {
|
||||
featureTable.current?.destroy();
|
||||
try {
|
||||
createTable(layer as unknown as Sublayer, view);
|
||||
} catch (error) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () => {};
|
||||
});
|
||||
|
||||
return <div ref={htmlDiv}></div>;
|
||||
}
|
|
@ -1,121 +1,278 @@
|
|||
import { useRef, useEffect, lazy } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { useRef, useState, useEffect, lazy } from 'react';
|
||||
import { Root, createRoot } from 'react-dom/client';
|
||||
|
||||
import '@esri/calcite-components/dist/calcite/calcite.css';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import initI18n from './i18n';
|
||||
|
||||
import MapView from '@arcgis/core/views/MapView.js';
|
||||
import WebMap from '@arcgis/core/WebMap.js';
|
||||
import esriConfig from '@arcgis/core/config.js';
|
||||
import LayerList from '@arcgis/core/widgets/LayerList';
|
||||
import Expand from '@arcgis/core/widgets/Expand';
|
||||
import ScaleBar from '@arcgis/core/widgets/ScaleBar.js';
|
||||
import MapView from '@arcgis/core/views/MapView';
|
||||
import WebMap from '@arcgis/core/WebMap';
|
||||
import esriConfig from '@arcgis/core/config';
|
||||
import ScaleBar from '@arcgis/core/widgets/ScaleBar';
|
||||
import Legend from '@arcgis/core/widgets/Legend';
|
||||
import WMTSLayer from '@arcgis/core/layers/WMTSLayer';
|
||||
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 reactiveUtils from '@arcgis/core/core/reactiveUtils.js';
|
||||
import * as intl from '@arcgis/core/intl.js';
|
||||
import * as intl from '@arcgis/core/intl';
|
||||
|
||||
// set asset path for ArcGIS Maps SDK widgets
|
||||
esriConfig.assetsPath = './assets';
|
||||
|
||||
let mapView: MapView;
|
||||
const webMapID = '7d0768f73d3e4be2b32c22274c600cb3';
|
||||
// ids of web map items in portal
|
||||
const webMapDEID = '7d0768f73d3e4be2b32c22274c600cb3';
|
||||
const webMapENID = 'dbf5532d06954c6a989d4f022de83f70';
|
||||
|
||||
// lazy load Print component
|
||||
// lazy load print component
|
||||
const Print = lazy(() => import('./print'));
|
||||
const Layers = lazy(() => import('./layer-list'));
|
||||
const Basemaps = lazy(() => import('./basemap-list'));
|
||||
const Search = lazy(() => import('./search'));
|
||||
|
||||
// import Calcite components
|
||||
import '@esri/calcite-components/dist/calcite/calcite.css';
|
||||
|
||||
import { setAssetPath } from '@esri/calcite-components/dist/components';
|
||||
setAssetPath(window.location.href);
|
||||
|
||||
import '@esri/calcite-components/dist/components/calcite-shell';
|
||||
import '@esri/calcite-components/dist/components/calcite-shell-panel';
|
||||
import '@esri/calcite-components/dist/components/calcite-shell-center-row';
|
||||
import '@esri/calcite-components/dist/components/calcite-action-bar';
|
||||
import '@esri/calcite-components/dist/components/calcite-action';
|
||||
import '@esri/calcite-components/dist/components/calcite-panel';
|
||||
import {
|
||||
CalciteShell,
|
||||
CalciteShellPanel,
|
||||
CalciteActionBar,
|
||||
CalciteAction,
|
||||
CalcitePanel,
|
||||
} from '@esri/calcite-components-react';
|
||||
|
||||
// load localized strings
|
||||
const match = location.pathname.match(/\/(\w+)/);
|
||||
if (match && match.length > 1) {
|
||||
const locale = match[1];
|
||||
initI18n(locale);
|
||||
}
|
||||
|
||||
export default function MapComponent({ locale }: { locale: string }) {
|
||||
const printRoot = useRef<any>(null);
|
||||
const mapRef = useRef(null);
|
||||
const maskRef = useRef<HTMLDivElement>(null);
|
||||
const printRoot = useRef<Root | null>(null);
|
||||
const maskRoot = useRef<HTMLDivElement>(null);
|
||||
const tableRoot = useRef<HTMLDivElement>(null);
|
||||
const mapView = useRef<MapView | null>(null);
|
||||
const previousId = useRef<string | null>(null);
|
||||
|
||||
const [actionBarExpanded, setActionBarExpanded] = useState<boolean>(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (mapRef.current) {
|
||||
if (!mapView.current) {
|
||||
// set locale for ArcGIS Maps SDK widgets
|
||||
intl.setLocale(locale);
|
||||
|
||||
const webMap = new WebMap({
|
||||
portalItem: {
|
||||
id: webMapID,
|
||||
id: locale === 'de' ? webMapDEID : webMapENID,
|
||||
portal: {
|
||||
url: 'https://gis.geosphere.at/portal',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const wmtsLayer = new WMTSLayer({
|
||||
url: 'https://mapsneu.wien.gv.at/basemapneu',
|
||||
});
|
||||
// const wmtsLayer = new WMTSLayer({
|
||||
// url: 'https://mapsneu.wien.gv.at/basemapneu',
|
||||
// });
|
||||
|
||||
const basemap = new Basemap({
|
||||
baseLayers: [wmtsLayer],
|
||||
// const basemap = new Basemap({
|
||||
// baseLayers: [wmtsLayer],
|
||||
// });
|
||||
|
||||
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: basemap,
|
||||
basemap: basemapEsri,
|
||||
});
|
||||
|
||||
const mapView = new MapView({
|
||||
container: mapRef.current,
|
||||
const view = new MapView({
|
||||
container: 'map-container',
|
||||
map: map,
|
||||
padding: {
|
||||
left: 49,
|
||||
},
|
||||
popup: {
|
||||
dockOptions: {
|
||||
position: 'auto',
|
||||
breakpoint: {
|
||||
width: 5000,
|
||||
},
|
||||
},
|
||||
},
|
||||
extent: {
|
||||
ymax: 6424330,
|
||||
xmin: 923200,
|
||||
xmax: 2017806,
|
||||
ymin: 5616270,
|
||||
spatialReference: {
|
||||
wkid: 3857,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mapView.current = view;
|
||||
view.ui.empty('top-left');
|
||||
|
||||
webMap.load().then(() => {
|
||||
map.layers = webMap.layers;
|
||||
createRoot(document.createElement('div')).render(<Search view={view}></Search>);
|
||||
});
|
||||
|
||||
const layerList = new LayerList({
|
||||
view: mapView,
|
||||
});
|
||||
|
||||
const layerListExpand = new Expand({
|
||||
content: layerList,
|
||||
});
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
const printExpand = new Expand({
|
||||
content: container,
|
||||
expandIcon: 'print',
|
||||
label: 'Print',
|
||||
});
|
||||
|
||||
reactiveUtils.watch(
|
||||
() => printExpand.expanded,
|
||||
(expanded) => {
|
||||
if (expanded) {
|
||||
printRoot.current = createRoot(container);
|
||||
printRoot.current.render(<Print view={mapView} mask={maskRef}></Print>);
|
||||
} else {
|
||||
maskRef.current?.classList.add('hidden');
|
||||
printRoot.current.unmount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const scaleBar = new ScaleBar({
|
||||
view: mapView,
|
||||
view: view,
|
||||
unit: 'metric',
|
||||
});
|
||||
|
||||
mapView.ui.add([layerListExpand, printExpand], 'top-right');
|
||||
mapView.ui.add([scaleBar], 'bottom-left');
|
||||
view.ui.add([scaleBar], 'bottom-left');
|
||||
|
||||
new Legend({
|
||||
view: view,
|
||||
container: 'legend-container',
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
mapView.destroy();
|
||||
if (printRoot.current) {
|
||||
printRoot.current.unmount();
|
||||
|
||||
return () => {};
|
||||
}, [locale]);
|
||||
|
||||
const handleCalciteActionBarToggle = () => {
|
||||
setActionBarExpanded(!actionBarExpanded);
|
||||
if (mapView.current) {
|
||||
mapView.current.padding = !actionBarExpanded ? { left: 150 } : { left: 49 };
|
||||
}
|
||||
|
||||
if (tableRoot.current) {
|
||||
if (!actionBarExpanded) {
|
||||
tableRoot.current.classList.add('left-40');
|
||||
tableRoot.current.classList.remove('left-14');
|
||||
} else {
|
||||
tableRoot.current.classList.add('left-14');
|
||||
tableRoot.current.classList.remove('left-40');
|
||||
}
|
||||
};
|
||||
}, [mapRef, maskRef]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (event: any) => {
|
||||
if (event.target.tagName !== 'CALCITE-ACTION') {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextId = event.target.dataset.actionId;
|
||||
|
||||
if (previousId.current) {
|
||||
const previousPanel = document.querySelector(`[data-panel-id=${previousId.current}]`) as HTMLCalcitePanelElement;
|
||||
if (previousPanel) {
|
||||
previousPanel.hidden = true;
|
||||
}
|
||||
|
||||
if (previousId.current === 'print') {
|
||||
maskRoot.current?.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
const nextPanel = document.querySelector(`[data-panel-id=${nextId}]`) as HTMLCalcitePanelElement;
|
||||
if (nextPanel && nextId !== previousId.current) {
|
||||
nextPanel.hidden = false;
|
||||
previousId.current = nextId;
|
||||
|
||||
if (nextId === 'print') {
|
||||
maskRoot.current?.classList.remove('hidden');
|
||||
}
|
||||
} else {
|
||||
previousId.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen overflow-hidden">
|
||||
<div ref={mapRef} className="h-screen"></div>
|
||||
<div
|
||||
ref={maskRef}
|
||||
className="hidden absolute bg-red-300 border-2 border-red-600 pointer-events-none opacity-50"
|
||||
></div>
|
||||
<div>
|
||||
<CalciteShell contentBehind>
|
||||
<h2 id="header-title" slot="header"></h2>
|
||||
<CalciteShellPanel slot="panel-start" displayMode="float">
|
||||
<CalciteActionBar
|
||||
slot="action-bar"
|
||||
onCalciteActionBarToggle={handleCalciteActionBarToggle}
|
||||
className="border-r border-r-gray-400"
|
||||
>
|
||||
<CalciteAction
|
||||
data-action-id="layers"
|
||||
icon="layers"
|
||||
text={t('layers.title')}
|
||||
onClick={handleClick}
|
||||
></CalciteAction>
|
||||
<CalciteAction
|
||||
data-action-id="basemaps"
|
||||
icon="basemap"
|
||||
text={t('basemaps.title')}
|
||||
onClick={handleClick}
|
||||
></CalciteAction>
|
||||
<CalciteAction
|
||||
data-action-id="legend"
|
||||
icon="legend"
|
||||
text={t('legend.title')}
|
||||
onClick={handleClick}
|
||||
></CalciteAction>
|
||||
<CalciteAction
|
||||
data-action-id="print"
|
||||
icon="print"
|
||||
text={t('print.heading')}
|
||||
onClick={handleClick}
|
||||
></CalciteAction>
|
||||
<CalciteAction data-action-id="info" icon="information" text="Info" onClick={handleClick}></CalciteAction>
|
||||
</CalciteActionBar>
|
||||
|
||||
<CalcitePanel data-panel-id="layers" heading={t('layers.title')} hidden>
|
||||
{mapView.current && <Layers view={mapView.current} tableRoot={tableRoot}></Layers>}
|
||||
</CalcitePanel>
|
||||
|
||||
<CalcitePanel data-panel-id="basemaps" heading={t('basemaps.title')} hidden>
|
||||
{mapView.current && <Basemaps view={mapView.current}></Basemaps>}
|
||||
</CalcitePanel>
|
||||
|
||||
<CalcitePanel data-panel-id="legend" heading={t('legend.title')} hidden>
|
||||
<div id="legend-container"></div>
|
||||
</CalcitePanel>
|
||||
|
||||
<CalcitePanel data-panel-id="print" heading={t('print.heading')} hidden>
|
||||
{mapView.current && <Print view={mapView.current} maskRoot={maskRoot}></Print>}
|
||||
</CalcitePanel>
|
||||
|
||||
<CalcitePanel data-panel-id="info" heading="Info" hidden>
|
||||
<div id="info-container"></div>
|
||||
</CalcitePanel>
|
||||
</CalciteShellPanel>
|
||||
<div className="h-screen w-full" id="map-container">
|
||||
<div ref={tableRoot} className="hidden absolute left-14 bottom-5 h-1/3 right-2 border border-gray-400"></div>
|
||||
</div>
|
||||
<div
|
||||
ref={maskRoot}
|
||||
className="hidden absolute bg-red-300 border-2 border-red-600 opacity-50 pointer-events-none"
|
||||
></div>
|
||||
</CalciteShell>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useRef, useState, useEffect, RefObject } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import initI18n from './i18n';
|
||||
|
||||
import MapView from '@arcgis/core/views/MapView';
|
||||
|
@ -71,11 +71,14 @@ if (match && match.length > 1) {
|
|||
initI18n(locale);
|
||||
}
|
||||
|
||||
export default function Print({ view, mask }: { view: MapView; mask: RefObject<HTMLDivElement> | null }) {
|
||||
const [title, setTitle] = useState('GeoSphere Austria');
|
||||
export default function Print({ view, maskRoot }: { view: MapView; maskRoot: RefObject<HTMLDivElement> }) {
|
||||
const [title, setTitle] = useState<string>('GeoSphere Austria');
|
||||
const [format, setFormat] = useState<string>(Object.keys(formats)[0]);
|
||||
const [scale, setScale] = useState<number>(10000);
|
||||
const [scale, setScale] = useState<number>(scales[0]);
|
||||
const [printing, setPrinting] = useState<boolean>(false);
|
||||
|
||||
const rendered = useRef<boolean>(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const currentScale = useRef<number>();
|
||||
|
@ -88,6 +91,7 @@ export default function Print({ view, mask }: { view: MapView; mask: RefObject<H
|
|||
|
||||
const handlePrint = () => {
|
||||
setPrinting(true);
|
||||
|
||||
const template = new PrintTemplate({
|
||||
layout: format,
|
||||
format: 'pdf',
|
||||
|
@ -97,7 +101,7 @@ export default function Print({ view, mask }: { view: MapView; mask: RefObject<H
|
|||
customTextElements: [],
|
||||
},
|
||||
exportOptions: {
|
||||
dpi: 98,
|
||||
dpi: 96,
|
||||
},
|
||||
scalePreserved: true,
|
||||
outScale: scale,
|
||||
|
@ -157,40 +161,42 @@ export default function Print({ view, mask }: { view: MapView; mask: RefObject<H
|
|||
setMask(width, height);
|
||||
};
|
||||
|
||||
// set the mask for print preview
|
||||
const setMask = (width: number, height: number) => {
|
||||
const center = view.center;
|
||||
const xmin = center.x - width / 2;
|
||||
const xmax = center.x + width / 2;
|
||||
const ymin = center.y - height / 2;
|
||||
const ymax = center.y + height / 2;
|
||||
if (center) {
|
||||
const xmin = center.x - width / 2;
|
||||
const xmax = center.x + width / 2;
|
||||
const ymin = center.y - height / 2;
|
||||
const ymax = center.y + height / 2;
|
||||
|
||||
const upperLeft = view.toScreen(
|
||||
new Point({
|
||||
x: xmin,
|
||||
y: ymax,
|
||||
spatialReference: view.spatialReference,
|
||||
})
|
||||
);
|
||||
const upperLeft = view.toScreen(
|
||||
new Point({
|
||||
x: xmin,
|
||||
y: ymax,
|
||||
spatialReference: view.spatialReference,
|
||||
})
|
||||
);
|
||||
|
||||
const lowerRight = view.toScreen(
|
||||
new Point({
|
||||
x: xmax,
|
||||
y: ymin,
|
||||
spatialReference: view.spatialReference,
|
||||
})
|
||||
);
|
||||
const lowerRight = view.toScreen(
|
||||
new Point({
|
||||
x: xmax,
|
||||
y: ymin,
|
||||
spatialReference: view.spatialReference,
|
||||
})
|
||||
);
|
||||
|
||||
const left = clamp(Math.round(upperLeft.x), 0, view.width);
|
||||
const top = clamp(Math.round(upperLeft.y), 0, view.height);
|
||||
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 left = clamp(Math.round(upperLeft.x), 0, view.width);
|
||||
const top = clamp(Math.round(upperLeft.y), 0, view.height);
|
||||
const maskWidth = clamp(Math.round(lowerRight.x - upperLeft.x), 0, view.width);
|
||||
const maskHeight = clamp(Math.round(lowerRight.y - upperLeft.y), 0, view.height);
|
||||
|
||||
if (mask && mask.current) {
|
||||
mask.current.classList.remove('hidden');
|
||||
mask.current.style.left = left + 'px';
|
||||
mask.current.style.top = top + 'px';
|
||||
mask.current.style.width = maskWidth + 'px';
|
||||
mask.current.style.height = maskHeight + 'px';
|
||||
if (maskRoot && maskRoot.current) {
|
||||
maskRoot.current.style.left = left + 'px';
|
||||
maskRoot.current.style.top = top + 'px';
|
||||
maskRoot.current.style.width = maskWidth + 'px';
|
||||
maskRoot.current.style.height = maskHeight + 'px';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -199,19 +205,23 @@ export default function Print({ view, mask }: { view: MapView; mask: RefObject<H
|
|||
updatePreview(currentScale.current, currentFormat.current);
|
||||
}
|
||||
|
||||
const handle = reactiveUtils.watch(
|
||||
() => view?.extent,
|
||||
() => {
|
||||
if (currentScale.current && currentFormat.current) {
|
||||
updatePreview(currentScale.current, currentFormat.current);
|
||||
let handle: any;
|
||||
if (!rendered.current) {
|
||||
handle = reactiveUtils.watch(
|
||||
() => view?.extent,
|
||||
() => {
|
||||
if (currentScale.current && currentFormat.current) {
|
||||
updatePreview(currentScale.current, currentFormat.current);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
rendered.current = true;
|
||||
return () => {
|
||||
handle.remove();
|
||||
handle?.remove();
|
||||
};
|
||||
}, []);
|
||||
});
|
||||
|
||||
const handleAbort = () => {
|
||||
if (controllerRef.current) {
|
||||
|
@ -221,23 +231,23 @@ export default function Print({ view, mask }: { view: MapView; mask: RefObject<H
|
|||
};
|
||||
|
||||
return (
|
||||
<CalcitePanel heading={t('heading')} className="px-3 w-80">
|
||||
<CalcitePanel heading={t('print.sub-heading')} className="px-3">
|
||||
<div>{printing && <CalciteProgress type="indeterminate"></CalciteProgress>}</div>
|
||||
<CalciteLabel className="mt-5 mx-5">
|
||||
{t('titleLabel')}
|
||||
{t('print.titleLabel')}
|
||||
<CalciteInputText
|
||||
placeholder={t('titleLabel')}
|
||||
placeholder={t('print.titleLabel')}
|
||||
onCalciteInputTextInput={handleTitleChange}
|
||||
className="mx-0"
|
||||
></CalciteInputText>
|
||||
</CalciteLabel>
|
||||
<CalciteLabel className="mx-5">
|
||||
{t('formatLabel')}
|
||||
{t('print.formatLabel')}
|
||||
<CalciteSelect label="format" onCalciteSelectChange={handleFormatChange}>
|
||||
{Object.keys(formats).map((formatName) => {
|
||||
return (
|
||||
<CalciteOption key={`${formatName}`} value={`${formatName}`}>
|
||||
{t(`formats.${formatName}`)}
|
||||
{t(`print.formats.${formatName}`)}
|
||||
</CalciteOption>
|
||||
);
|
||||
})}
|
||||
|
@ -245,7 +255,7 @@ export default function Print({ view, mask }: { view: MapView; mask: RefObject<H
|
|||
</CalciteLabel>
|
||||
|
||||
<CalciteLabel className="mx-5">
|
||||
{t('scaleLabel')}
|
||||
{t('print.scaleLabel')}
|
||||
<CalciteSelect label="scale" onCalciteSelectChange={handleScaleChange}>
|
||||
{scales.map((num) => (
|
||||
<CalciteOption value={`${num}`} key={num}>{`1:${num
|
||||
|
@ -258,12 +268,12 @@ export default function Print({ view, mask }: { view: MapView; mask: RefObject<H
|
|||
|
||||
{!printing ? (
|
||||
<CalciteButton width="half" slot="footer" onClick={handlePrint}>
|
||||
{t('button.print')}
|
||||
{t('print.button.print')}
|
||||
</CalciteButton>
|
||||
) : (
|
||||
<>
|
||||
<CalciteButton width="half" slot="footer" onClick={handleAbort}>
|
||||
{t('button.cancel')}
|
||||
{t('print.button.cancel')}
|
||||
</CalciteButton>
|
||||
</>
|
||||
)}
|
||||
|
|
629
app/[locale]/search.tsx
Normal file
629
app/[locale]/search.tsx
Normal file
|
@ -0,0 +1,629 @@
|
|||
import { useRef, useState, useEffect } from 'react';
|
||||
import { Root, createRoot } from 'react-dom/client';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import initI18n from './i18n';
|
||||
|
||||
// import Calcite components
|
||||
import '@esri/calcite-components/dist/calcite/calcite.css';
|
||||
|
||||
import { setAssetPath } from '@esri/calcite-components/dist/components';
|
||||
setAssetPath(window.location.href);
|
||||
|
||||
import '@esri/calcite-components/dist/components/calcite-accordion';
|
||||
import '@esri/calcite-components/dist/components/calcite-accordion-item';
|
||||
import '@esri/calcite-components/dist/components/calcite-link';
|
||||
import '@esri/calcite-components/dist/components/calcite-list';
|
||||
import '@esri/calcite-components/dist/components/calcite-list-item';
|
||||
import '@esri/calcite-components/dist/components/calcite-action';
|
||||
import {
|
||||
CalciteAccordion,
|
||||
CalciteAccordionItem,
|
||||
CalciteLink,
|
||||
CalciteList,
|
||||
CalciteListItem,
|
||||
CalciteAction,
|
||||
} from '@esri/calcite-components-react';
|
||||
|
||||
import MapView from '@arcgis/core/views/MapView';
|
||||
import Search from '@arcgis/core/widgets/Search';
|
||||
import FeatureLayer from '@arcgis/core/layers/FeatureLayer';
|
||||
import Graphic from '@arcgis/core/Graphic';
|
||||
import { eachAlways } from '@arcgis/core/core/promiseUtils.js';
|
||||
import SearchSource from '@arcgis/core/widgets/Search/SearchSource';
|
||||
import Point from '@arcgis/core/geometry/Point';
|
||||
import Polyline from '@arcgis/core/geometry/Polyline';
|
||||
import Polygon from '@arcgis/core/geometry/Polygon';
|
||||
import { geodesicBuffer } from '@arcgis/core/geometry/geometryEngine';
|
||||
import MapImageLayer from '@arcgis/core/layers/MapImageLayer';
|
||||
import SimpleFillSymbol from '@arcgis/core/symbols/SimpleFillSymbol';
|
||||
import SimpleMarkerSymbol from '@arcgis/core/symbols/SimpleMarkerSymbol';
|
||||
import Sublayer from '@arcgis/core/layers/support/Sublayer';
|
||||
import PopupTemplate from '@arcgis/core/PopupTemplate';
|
||||
|
||||
// create feaure layer from URL of data index layer
|
||||
const datenIndexURL = 'https://gis.geosphere.at/maps/rest/services/datenindex/raster_5000/MapServer/0';
|
||||
const indexLayer = new FeatureLayer({ url: datenIndexURL, title: 'Datenindex 1:5.000', opacity: 0 });
|
||||
|
||||
// custom type definitions
|
||||
interface CustomLayer {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
interface CustomGroupLayer {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
visible: boolean;
|
||||
layers: (CustomGroupLayer | CustomLayer)[];
|
||||
}
|
||||
|
||||
interface LayerToFeaturesMap {
|
||||
[key: string]: string[];
|
||||
}
|
||||
|
||||
// load localized strings
|
||||
const match = location.pathname.match(/\/(\w+)/);
|
||||
if (match && match.length > 1) {
|
||||
const locale = match[1];
|
||||
initI18n(locale);
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
// 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;
|
||||
|
||||
if (geometry.x && geometry.y) {
|
||||
view.goTo(
|
||||
new Point({
|
||||
x: geometry.x,
|
||||
y: geometry.y,
|
||||
spatialReference: sr,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (geometry.paths) {
|
||||
view.goTo(
|
||||
new Polyline({
|
||||
paths: geometry.paths,
|
||||
spatialReference: sr,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (geometry.rings) {
|
||||
view.goTo(
|
||||
new Polygon({
|
||||
rings: geometry.rings,
|
||||
spatialReference: sr,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// remove layers from layer tree by given filter
|
||||
const removeLayers = (layers: (CustomGroupLayer | CustomLayer)[], keepLayer: any): any => {
|
||||
return layers
|
||||
.filter((layer) => keepLayer(layer))
|
||||
.map((layer) => {
|
||||
if (layer.type === 'group' && (layer as CustomGroupLayer).layers) {
|
||||
return { ...layer, layers: removeLayers((layer as CustomGroupLayer).layers, keepLayer) };
|
||||
} else {
|
||||
return layer;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// get custom layer objects from layer tree
|
||||
const getLayerObjects = (layers: any) => {
|
||||
return layers.map((layer: any) => {
|
||||
if (layer.layers) {
|
||||
return {
|
||||
id: layer.id,
|
||||
type: layer.type,
|
||||
title: layer.title,
|
||||
visible: layer.visible,
|
||||
layers: getLayerObjects(layer.layers.toArray()),
|
||||
};
|
||||
} else {
|
||||
return { id: layer.id, type: layer.type, title: layer.title, visible: layer.visible };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// build query string for custom search source (BEV geocoding service)
|
||||
const buildQueryString = (searchTerm: string) => `?term=${encodeURI(searchTerm)}`;
|
||||
|
||||
const url = 'https://kataster.bev.gv.at/api/all4map';
|
||||
const customSearchSource = new SearchSource({
|
||||
placeholder: t('search.placeholder'),
|
||||
|
||||
// provide suggestions to the Search widget
|
||||
getSuggestions: async (params) => {
|
||||
const res = await fetch(url + buildQueryString(params.suggestTerm));
|
||||
const json = await res.json();
|
||||
|
||||
if (json.data && json.data.features?.length > 0) {
|
||||
return json.data.features.map((feature: any) => {
|
||||
return {
|
||||
key: 'bev',
|
||||
text: feature.properties.name,
|
||||
sourceIndex: params.sourceIndex,
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// find results from suggestions
|
||||
getResults: async (params) => {
|
||||
const res = await fetch(url + buildQueryString(params.suggestResult.text));
|
||||
const json = await res.json();
|
||||
|
||||
if (json.data && json.data.features?.length > 0) {
|
||||
const searchResults = json.data.features.map((feature: any) => {
|
||||
// create a graphic for the search widget
|
||||
if (feature.geometry.type === 'Point') {
|
||||
const graphic = new Graphic({
|
||||
geometry: new Point({
|
||||
x: feature.geometry.coordinates[0],
|
||||
y: feature.geometry.coordinates[1],
|
||||
spatialReference: {
|
||||
wkid: 4326,
|
||||
},
|
||||
}),
|
||||
symbol: {
|
||||
type: 'simple-marker',
|
||||
style: 'circle',
|
||||
color: [51, 51, 204, 0.5],
|
||||
size: '8px',
|
||||
outline: {
|
||||
color: 'white',
|
||||
width: 1,
|
||||
},
|
||||
} as unknown as SimpleMarkerSymbol,
|
||||
});
|
||||
|
||||
return {
|
||||
extent: null,
|
||||
feature: graphic,
|
||||
target: new Graphic({
|
||||
// create buffer for point geometries to allow zoom to
|
||||
geometry: geodesicBuffer(graphic.geometry, 1000, 'meters') as Polygon,
|
||||
}),
|
||||
name: feature.properties.name,
|
||||
};
|
||||
} else {
|
||||
const coords = feature.geometry.coordinates;
|
||||
const graphic = new Graphic({
|
||||
geometry: new Polygon({
|
||||
rings: coords,
|
||||
spatialReference: {
|
||||
wkid: 4326,
|
||||
},
|
||||
}),
|
||||
symbol: {
|
||||
type: 'simple-fill',
|
||||
color: [51, 51, 204, 0.5],
|
||||
style: 'solid',
|
||||
outline: {
|
||||
color: 'white',
|
||||
width: 1,
|
||||
},
|
||||
} as unknown as SimpleFillSymbol,
|
||||
});
|
||||
|
||||
return {
|
||||
extent: graphic.geometry.extent,
|
||||
feature: graphic,
|
||||
target: graphic,
|
||||
name: feature.properties.name,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// return search results
|
||||
return searchResults;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// create query from cellcode for 3x3 neighbourhood
|
||||
const createQueryFromCellcode = (cellcode: string) => {
|
||||
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 cellcodeQueries = operations.map(
|
||||
(operation) => `cellcode = '10kmN${northNumber + operation[0]}E${eastNumber + operation[1]}'`
|
||||
);
|
||||
return `cellcode = '${cellcode}' OR ` + cellcodeQueries.join(' OR ');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (rendered.current) return;
|
||||
rendered.current = true;
|
||||
|
||||
// add data index layer
|
||||
view.map.layers.push(indexLayer);
|
||||
|
||||
// add search widget with custom search source
|
||||
const search = new Search({
|
||||
view: view,
|
||||
popupEnabled: false,
|
||||
sources: [customSearchSource],
|
||||
includeDefaultSources: false,
|
||||
});
|
||||
|
||||
view.ui.add(search, 'top-left');
|
||||
|
||||
// add event handler for select-result events
|
||||
search.on('select-result', (event) => {
|
||||
view.closePopup();
|
||||
|
||||
// get selected feature and display it on map
|
||||
const graphic = event.result.feature;
|
||||
if (graphic.geometry.type === 'point') {
|
||||
setCurrentTarget(
|
||||
new Graphic({
|
||||
// create buffer for point geometries to allow zoom to
|
||||
geometry: geodesicBuffer(graphic.geometry, 1000, 'meters') as Polygon,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
setCurrentTarget(graphic);
|
||||
}
|
||||
|
||||
view.graphics.removeAll();
|
||||
view.graphics.add(graphic);
|
||||
|
||||
// query for intersecting features
|
||||
indexLayer
|
||||
.queryFeatures({
|
||||
geometry: graphic.geometry,
|
||||
spatialRelationship: 'intersects',
|
||||
returnGeometry: false,
|
||||
outFields: ['fid', 'cellcode'],
|
||||
})
|
||||
.then((featureSet) => {
|
||||
if (featureSet.features.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if feature has an extent just return intersecting raster cells
|
||||
// otherwise create a query including a 3x3 neighbourhood
|
||||
if (event.result.extent) {
|
||||
const objectIds = featureSet.features.map((feature) => feature.attributes['fid'] as number);
|
||||
queryRelatedFeaturesFromOIds(objectIds, false);
|
||||
} else {
|
||||
const cellcode = featureSet.features[0].attributes['cellcode'];
|
||||
const query = createQueryFromCellcode(cellcode);
|
||||
|
||||
indexLayer
|
||||
.queryFeatures({
|
||||
where: query,
|
||||
returnGeometry: false,
|
||||
outFields: ['fid'],
|
||||
})
|
||||
.then((resultSet) => {
|
||||
const objectIds = resultSet.features.map((feature) => feature.attributes['fid']);
|
||||
queryRelatedFeaturesFromOIds(objectIds, false);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// for each raster cell given by its OID and it each relationship class
|
||||
// of the index layer - query related features
|
||||
const queryRelatedFeaturesFromOIds = (objectIds: number[], isPopupContent: boolean) => {
|
||||
const relatedFeaturesByLayer: LayerToFeaturesMap = {};
|
||||
const promises = indexLayer.relationships.map((relationship) => {
|
||||
return indexLayer
|
||||
.queryRelatedFeatures({
|
||||
outFields: ['url'],
|
||||
relationshipId: relationship.id,
|
||||
objectIds: objectIds,
|
||||
})
|
||||
.then((relatedFeatureSets) => {
|
||||
Object.keys(relatedFeatureSets).forEach((objectId) => {
|
||||
if (!relatedFeatureSets[objectId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const regex = /^https:\/\/.+\/MapServer\/\d+/g;
|
||||
const relatedFeatures = relatedFeatureSets[objectId].features;
|
||||
|
||||
// get url of related feature layer
|
||||
let layerURL: string = '';
|
||||
if (relatedFeatures.length > 0) {
|
||||
const matches = relatedFeatures[0].attributes.url.match(regex);
|
||||
if (matches.length > 0) {
|
||||
layerURL = matches[0];
|
||||
}
|
||||
}
|
||||
const urls = relatedFeatures.map((feature: any) => feature.attributes.url);
|
||||
|
||||
if (relatedFeaturesByLayer[layerURL]) {
|
||||
relatedFeaturesByLayer[layerURL] = relatedFeaturesByLayer[layerURL].concat(urls);
|
||||
} else {
|
||||
relatedFeaturesByLayer[layerURL] = urls;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// wait until all promises are fulfilled
|
||||
return eachAlways(promises).then(() => {
|
||||
const relatedFeatures: { [key: string]: JSX.Element[] } = {};
|
||||
const mapImageLayers: MapImageLayer[] = [];
|
||||
Object.keys(relatedFeaturesByLayer).forEach((layerURL) => {
|
||||
const relatedFeatureURLs = [...new Set(relatedFeaturesByLayer[layerURL])];
|
||||
const mapImageLayer = getMapImageLayer(layerURL);
|
||||
|
||||
let mapImageLayerId: string = '';
|
||||
let sublayer: Sublayer | undefined;
|
||||
if (mapImageLayer) {
|
||||
mapImageLayerId = mapImageLayer.id;
|
||||
mapImageLayers.push(mapImageLayer);
|
||||
|
||||
mapImageLayer.sublayers.forEach((layer) => {
|
||||
if (layer.url === layerURL) {
|
||||
sublayer = layer;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const text = sublayer?.id ? sublayer?.id.toString() : '';
|
||||
// create UI item for sublayer
|
||||
const accordionItem = (
|
||||
<CalciteAccordionItem heading={sublayer?.title} key={sublayer?.id}>
|
||||
<CalciteAction
|
||||
slot="actions-end"
|
||||
icon={sublayer?.visible ? 'view-visible' : 'view-hide'}
|
||||
text={text}
|
||||
appearance="transparent"
|
||||
onClick={() => {
|
||||
if (sublayer) {
|
||||
sublayer.visible = !sublayer.visible;
|
||||
}
|
||||
}}
|
||||
></CalciteAction>
|
||||
|
||||
<CalciteList>
|
||||
{relatedFeatureURLs.map((relatedFeatureURL) => {
|
||||
const regex = /\d+$/g;
|
||||
const matches = relatedFeatureURL.match(regex);
|
||||
let featureId;
|
||||
if (matches && matches.length > 0) featureId = matches[0];
|
||||
return (
|
||||
<CalciteListItem key={featureId}>
|
||||
<CalciteAction
|
||||
slot="actions-start"
|
||||
icon="magnifying-glass"
|
||||
text={relatedFeatureURL}
|
||||
appearance="transparent"
|
||||
onClick={handleZoomTo}
|
||||
></CalciteAction>
|
||||
<CalciteLink
|
||||
href={relatedFeatureURL}
|
||||
target="_blank"
|
||||
slot="content"
|
||||
>{`Feature ${featureId}`}</CalciteLink>
|
||||
</CalciteListItem>
|
||||
);
|
||||
})}
|
||||
</CalciteList>
|
||||
</CalciteAccordionItem>
|
||||
);
|
||||
|
||||
if (!relatedFeatures[mapImageLayerId]) {
|
||||
relatedFeatures[mapImageLayerId] = [accordionItem];
|
||||
} else {
|
||||
relatedFeatures[mapImageLayerId].push(accordionItem);
|
||||
}
|
||||
});
|
||||
|
||||
// create cutom layer objects tree
|
||||
const layerObjects = getLayerObjects(view.map.layers.toArray());
|
||||
|
||||
// remove layers that are not in the seach result
|
||||
let remainingLayers = removeLayers(layerObjects, (layer: CustomGroupLayer | CustomLayer) => {
|
||||
if (
|
||||
layer.type === 'map-image' &&
|
||||
mapImageLayers.findIndex((mapImageLayer) => mapImageLayer.id === layer.id) === -1
|
||||
) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
remainingLayers = removeLayers(remainingLayers, (layer: CustomGroupLayer | CustomLayer) => {
|
||||
if (
|
||||
(layer.type === 'group' &&
|
||||
((layer as CustomGroupLayer).layers.length === 0 ||
|
||||
(layer as CustomGroupLayer).layers.every(
|
||||
(child) => child.type === 'imagery' || child.type === 'imagery-tile'
|
||||
))) ||
|
||||
layer.type === 'feature' ||
|
||||
layer.type === 'imagery' ||
|
||||
layer.type === 'imagery-tile'
|
||||
) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// recursively build UI from remaining layers
|
||||
const buildLayerTree = (layer: CustomGroupLayer | CustomLayer) => {
|
||||
if (layer.type === 'group') {
|
||||
return (
|
||||
<CalciteAccordionItem heading={layer.title} key={layer.id}>
|
||||
<CalciteAction
|
||||
slot="actions-end"
|
||||
icon={layer.visible ? 'view-visible' : 'view-hide'}
|
||||
text={layer.id}
|
||||
appearance="transparent"
|
||||
onClick={handleToggleVisibility}
|
||||
></CalciteAction>
|
||||
{(layer as CustomGroupLayer).layers &&
|
||||
(layer as CustomGroupLayer).layers.map((sublayer) => buildLayerTree(sublayer))}
|
||||
</CalciteAccordionItem>
|
||||
);
|
||||
} else if (layer.type === 'map-image') {
|
||||
return (
|
||||
<CalciteAccordionItem heading={layer.title} key={layer.id}>
|
||||
<CalciteAction
|
||||
slot="actions-end"
|
||||
icon={layer.visible ? 'view-visible' : 'view-hide'}
|
||||
text={layer.id}
|
||||
appearance="transparent"
|
||||
onClick={handleToggleVisibility}
|
||||
></CalciteAction>
|
||||
{relatedFeatures[layer.id]}
|
||||
</CalciteAccordionItem>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const layertree = (
|
||||
<CalciteAccordion appearance="transparent" iconPosition="start" scale="s">
|
||||
{remainingLayers.map((layer: CustomGroupLayer | CustomLayer) => buildLayerTree(layer))}
|
||||
</CalciteAccordion>
|
||||
);
|
||||
|
||||
// render react components into div element
|
||||
const contentDiv = document.createElement('div');
|
||||
const root: Root = createRoot(contentDiv);
|
||||
root.render(layertree);
|
||||
|
||||
if (isPopupContent) {
|
||||
return contentDiv;
|
||||
} else {
|
||||
let location;
|
||||
if (currentGraphicRef.current?.geometry?.type === 'polygon') {
|
||||
location = (currentGraphicRef.current?.geometry as Polygon).centroid;
|
||||
} else {
|
||||
location = currentGraphicRef.current?.geometry;
|
||||
}
|
||||
view.openPopup({
|
||||
title: t('search.popup-title'),
|
||||
content: contentDiv,
|
||||
location: location,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// create popup content for popup template of index layer
|
||||
const createPopupContent = async (target: any) => {
|
||||
const cellcode = target.graphic.attributes['cellcode'];
|
||||
const query = createQueryFromCellcode(cellcode);
|
||||
|
||||
const objectIds = await indexLayer
|
||||
.queryFeatures({
|
||||
where: query,
|
||||
returnGeometry: false,
|
||||
outFields: ['fid'],
|
||||
})
|
||||
.then((resultSet) => {
|
||||
// return Ids of neighbouring features (3x3 neighbourhood)
|
||||
return resultSet.features.map((feature) => feature.attributes['fid']);
|
||||
});
|
||||
|
||||
return queryRelatedFeaturesFromOIds(objectIds, true);
|
||||
};
|
||||
|
||||
const popupTemplate = {
|
||||
title: t('search.popup-title'),
|
||||
content: createPopupContent,
|
||||
returnGeometry: false,
|
||||
outFields: [],
|
||||
};
|
||||
|
||||
indexLayer.popupTemplate = popupTemplate as unknown as PopupTemplate;
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
|
@ -22,6 +22,10 @@ body {
|
|||
background: rgb(var(--background-end-rgb));
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
Loading…
Add table
editor.link_modal.header
Reference in a new issue