diff --git a/packages/react-components/src/components/cesium-map/active-layers/active-layers-panel.css b/packages/react-components/src/components/cesium-map/active-layers/active-layers-panel.css index 4dffcfe43..7f83b2555 100644 --- a/packages/react-components/src/components/cesium-map/active-layers/active-layers-panel.css +++ b/packages/react-components/src/components/cesium-map/active-layers/active-layers-panel.css @@ -11,6 +11,12 @@ body[dir='rtl'] .cesium-viewer .activeLayersPanel .cesium-cesiumInspector-sectio } .cesium-viewer .activeLayersPanel .name { + min-width: 0; + flex: 1 1 auto; +} + +.cesium-viewer .activeLayersPanel .name bdi { + display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -34,7 +40,7 @@ body[dir='rtl'] .cesium-viewer .activeLayersPanel .cesium-cesiumInspector-sectio .cesium-viewer .activeLayersPanel .icon { width: 17px; height: 17px; - margin-left: 8px; + margin: 0 8px; cursor: pointer; } diff --git a/packages/react-components/src/components/cesium-map/active-layers/active-layers-panel.tsx b/packages/react-components/src/components/cesium-map/active-layers/active-layers-panel.tsx index a23632be1..6f1510ea4 100644 --- a/packages/react-components/src/components/cesium-map/active-layers/active-layers-panel.tsx +++ b/packages/react-components/src/components/cesium-map/active-layers/active-layers-panel.tsx @@ -1,22 +1,39 @@ -import { Rectangle } from 'cesium'; +import { Cesium3DTileset, Rectangle } from 'cesium'; import { get } from 'lodash'; import React, { useEffect, useState } from 'react'; import { Tooltip, Typography } from '@map-colonies/react-core'; import bbox from '@turf/bbox'; import { Box } from '../../box'; -import { TRANSPARENT_LAYER_ID } from '../layers-manager'; +import { + getImageryProvider, + getImageryProviderName, + getDataLayerName, + getLayerFootprint, + getLayerId, + getLayerName, + ICesiumImageryLayer, + isBaseMapLayer, + isManagedImageryLayer, + isServiceLayer, + TRANSPARENT_LAYER_ID, +} from '../layers-manager'; import { useCesiumMap } from '../map'; import './active-layers-panel.css'; const IMAGERY = 'Imagery'; +const SERVICE = 'Service'; const DATA = 'Data'; +const THREE_D = '3D'; +const TRANSPARENT_LAYER = 'TRANSPARENT_LAYER_FOR_OPTIMIZATION'; +const SERVICE_LAYER = 'LAYER_WITH_NO_ID #'; interface IActiveLayer { id: string; name: string; - rect: Rectangle; - isBaseMap: boolean; + isDisabled: boolean; + rect?: Rectangle; + zoomToTarget?: Cesium3DTileset; } interface ISection { @@ -28,84 +45,158 @@ interface IActiveLayersPanelProps { locale?: { [key: string]: string }; } +const extractModelName = (rawUrl: string): string => { + try { + const { hostname, pathname } = new URL(rawUrl); + const segments = pathname.split('/').filter((s) => s.length > 0); + if (segments.length >= 2) { + return `${segments[segments.length - 2]}/${segments[segments.length - 1]}`; + } + if (segments.length === 1) { + return segments[0]; + } + return hostname; + } catch { + return rawUrl; + } +}; + export const ActiveLayersPanel: React.FC = ({ locale }) => { const mapViewer = useCesiumMap(); - const [sections, setSections] = useState([ { id: IMAGERY, values: [] }, { id: DATA, values: [] } ]); + const [sections, setSections] = useState([ + { id: IMAGERY, values: [] }, + { id: SERVICE, values: [] }, + { id: DATA, values: [] }, + { id: THREE_D, values: [] } + ]); const [collapsedSections, setCollapsedSections] = useState>({}); const getLabel = (key: string) => { return get(locale, key.toUpperCase()) ?? key; }; + const getLayerList = (): ICesiumImageryLayer[] => { + return mapViewer.layersManager?.layerList ?? []; + }; + const getImageryLayers = (): IActiveLayer[] => { - return mapViewer.imageryLayers - ? Array.from({ length: mapViewer.imageryLayers.length }, (_, i) => { - const layer = mapViewer.imageryLayers.get(i); - const meta = (layer as any).meta; + const layerList = getLayerList(); + return layerList.length > 0 + ? layerList.map((layer): IActiveLayer | undefined => { + const meta = get(layer, 'meta'); + const layerId = getLayerId(layer); + if (!isManagedImageryLayer(layerId)) { + return undefined; + } + return { + id: layerId as string, + name: (getLayerName(layer) ?? layerId) as string, + rect: layer.rectangle, + isDisabled: isBaseMapLayer(meta as Record) + }; + }).filter((item): item is IActiveLayer => item !== undefined) + : []; + }; + + const getServiceLayers = (): IActiveLayer[] => { + const layerList = getLayerList(); + return layerList.length > 0 + ? layerList.map((layer, i): IActiveLayer | undefined => { + const layerId = getLayerId(layer); + if (!isServiceLayer(layerId)) { + return undefined; + } + const isTransparentLayer = layerId === TRANSPARENT_LAYER_ID; + const providerName = getImageryProviderName(getImageryProvider(layer)); + const name = isTransparentLayer + ? TRANSPARENT_LAYER + : `${SERVICE_LAYER} ${String(i + 1)}`; + return { - id: meta?.id as string, - name: (get(meta, 'layerRecord.productName') ?? meta?.id) as string, + id: `SERVICE_LAYER_${String(i)}`, + name: isTransparentLayer ? name : providerName ?? name, rect: layer.rectangle, - isBaseMap: mapViewer.layersManager?.isBaseMapLayer(meta) as boolean + isDisabled: true }; - }).filter((layer) => layer.id !== TRANSPARENT_LAYER_ID) + }).filter((item): item is IActiveLayer => item !== undefined) : []; }; const getDataLayers = (): IActiveLayer[] => { return mapViewer.layersManager?.dataLayerList.map((dataLayer) => { return { - id: dataLayer.meta?.id as string, - name: (get(dataLayer.meta, 'featureStructure.aliasLayerName') ?? dataLayer.meta.productName) as string, - rect: Rectangle.fromDegrees(...bbox(dataLayer.meta?.footprint)), - isBaseMap: false + id: getLayerId(dataLayer) as string, + name: (getDataLayerName(dataLayer.meta) ?? getLayerName(dataLayer)) as string, + rect: Rectangle.fromDegrees(...bbox(getLayerFootprint(dataLayer.meta))), + isDisabled: false }; }) || []; }; + const get3DModels = (): IActiveLayer[] => { + return (mapViewer.layersManager?.modelList ?? []).map((model, index): IActiveLayer => { + const modelUrl = get(model.tileset, 'resource.url') as string | undefined; + const modelName = getLayerName(model) ?? extractModelName(modelUrl ?? `Model #${String(index + 1)}`); + return { + id: (getLayerId(model) as string) ?? `3D_MODEL_${String(index)}`, + name: modelName, + zoomToTarget: model.tileset, + isDisabled: false, + }; + }); + }; + + const refreshSections = (): void => { + setSections([ + { + id: IMAGERY, + values: getImageryLayers(), + }, + { + id: SERVICE, + values: getServiceLayers(), + }, + { + id: DATA, + values: getDataLayers(), + }, + { + id: THREE_D, + values: get3DModels(), + }, + ]); + }; + useEffect(() => { - const updateSections = () => { - const newSections = [ - { - id: IMAGERY, - values: getImageryLayers() - }, - { - id: DATA, - values: getDataLayers() - }, - ]; - setSections(newSections); - setCollapsedSections(newSections.reduce((acc, section) => ({ ...acc, [section.id]: true }), {})); - }; - updateSections(); + refreshSections(); + setCollapsedSections({ + [IMAGERY]: true, + [SERVICE]: true, + [DATA]: true, + [THREE_D]: true, + }); }, []); useEffect(() => { - if (!mapViewer.layersManager) return; + if (!mapViewer.layersManager) { return; } const handleLayerEvent = (): void => { - setSections((prev) => - prev.map((item) => - item.id === IMAGERY - ? { - ...item, - values: getImageryLayers() - } - : item - ) - ); + refreshSections(); }; mapViewer.layersManager.addLayerUpdatedListener(handleLayerEvent); + mapViewer.imageryLayers.layerAdded.addEventListener(handleLayerEvent); mapViewer.imageryLayers.layerRemoved.addEventListener(handleLayerEvent); + mapViewer.imageryLayers.layerMoved.addEventListener(handleLayerEvent); return () => { if (get(mapViewer, '_cesiumWidget') !== undefined) { mapViewer.layersManager?.removeLayerUpdatedListener(handleLayerEvent); + mapViewer.imageryLayers.layerAdded.removeEventListener(handleLayerEvent); mapViewer.imageryLayers.layerRemoved.removeEventListener(handleLayerEvent); + mapViewer.imageryLayers.layerMoved.removeEventListener(handleLayerEvent); } }; - }, [mapViewer.layersManager?.layerList]); + }, [mapViewer.layersManager]); useEffect(() => { - if (!mapViewer.layersManager) return; + if (!mapViewer.layersManager) { return; } const handleDataLayerEvent = (): void => { setSections((prev) => prev.map((item) => @@ -124,12 +215,38 @@ export const ActiveLayersPanel: React.FC = ({ locale }) }; }, [mapViewer.layersManager?.dataLayerList]); + useEffect(() => { + if (!mapViewer.layersManager) { return; } + const handle3DModelEvent = (): void => { + setSections((prev) => + prev.map((item) => + item.id === THREE_D + ? { + ...item, + values: get3DModels() + } + : item + ) + ); + }; + mapViewer.layersManager.addModelUpdatedListener(handle3DModelEvent); + return () => { + mapViewer.layersManager?.removeModelUpdatedListener(handle3DModelEvent); + }; + }, [mapViewer.layersManager?.modelList]); + const toggleSection = (id: string) => { setCollapsedSections((prev) => ({ ...prev, [id]: !prev[id] })); }; - const handleFlyTo = (rect: Rectangle) => { - mapViewer.camera.flyTo({ destination: rect }); + const handleFlyTo = (activeLayer: IActiveLayer) => { + if (activeLayer.zoomToTarget !== undefined) { + void mapViewer.zoomTo(activeLayer.zoomToTarget); + return; + } + if (activeLayer.rect !== undefined) { + mapViewer.camera.flyTo({ destination: activeLayer.rect }); + } }; return ( @@ -149,11 +266,11 @@ export const ActiveLayersPanel: React.FC = ({ locale }) section.values.map((activeLayer: IActiveLayer) => ( - {activeLayer.name} + {activeLayer.name} - { event.stopPropagation(); handleFlyTo(activeLayer.rect); }}> + { event.stopPropagation(); handleFlyTo(activeLayer); }}> @@ -161,7 +278,7 @@ export const ActiveLayersPanel: React.FC = ({ locale }) {/* - { event.stopPropagation(); }}> + { event.stopPropagation(); }}> diff --git a/packages/react-components/src/components/cesium-map/context-menu.stories.tsx b/packages/react-components/src/components/cesium-map/context-menu.stories.tsx index a549f9d58..306f12f70 100644 --- a/packages/react-components/src/components/cesium-map/context-menu.stories.tsx +++ b/packages/react-components/src/components/cesium-map/context-menu.stories.tsx @@ -1,9 +1,10 @@ import React, { useEffect, useState } from 'react'; -import { Menu, MenuItem, MenuSurfaceAnchor } from '@map-colonies/react-core'; +import { get } from 'lodash'; +import { Button, Menu, MenuItem, MenuSurfaceAnchor } from '@map-colonies/react-core'; import { Story, Meta } from '@storybook/react'; import { Box } from '../box'; import { BASE_MAPS } from './helpers/constants'; -import { ICesiumImageryLayer, IRasterLayer } from './layers-manager'; +import { getLayerId, ICesiumImageryLayer, IRasterLayer } from './layers-manager'; import { CesiumMap, IBaseMaps, IContextMenuData, useCesiumMap } from './map'; import { CesiumCartesian2, CesiumSceneMode } from './proxied.types'; @@ -19,25 +20,38 @@ interface ILayersMozaikProps { layers: IRasterLayer[]; } +const getDebugLayerText = (layer: unknown): string => { + const imageryLayer = layer as ICesiumImageryLayer; + const layerId = getLayerId(imageryLayer) ?? 'UNKNOWN_LAYER_ID'; + const zIndex = get(layer, 'meta.zIndex') ?? 'NA'; + return `${layerId} <--> ${String(zIndex)}`; +}; + const mapDivStyle = { height: '90%', width: '100%', position: 'absolute' as const, }; -const layers = [ +const layerManagerMetaMapping = { + layer: { + id: 'id', + name: 'layerRecord.productName', + footprint: 'layerRecord.footprint', + }, +}; + +const layers: IRasterLayer[] = [ { id: 'near_amphy', type: 'XYZ_LAYER', + zIndex: 0, opacity: 1, show: true, - meta: { - zIndex: 0, - }, options: { url: 'https://tiles.openaerialmap.org/5a9f90c42553e6000ce5ad6c/0/eee1a570-128e-4947-9ffa-1e69c1efab7c/{z}/{x}/{y}.png', }, - details: { + layerRecord: { footprint: { type: 'Polygon', coordinates: [ @@ -55,15 +69,13 @@ const layers = [ { id: 'coin_zoom_17', type: 'XYZ_LAYER', + zIndex: 1, opacity: 1, show: true, - meta: { - zIndex: 1, - }, options: { url: 'https://tiles.openaerialmap.org/5a8316e22553e6000ce5ac7f/0/c3fcbe99-d339-41b6-8ec0-33d90ccca020/{z}/{x}/{y}.png', }, - details: { + layerRecord: { footprint: { type: 'Polygon', coordinates: [ @@ -81,15 +93,13 @@ const layers = [ { id: 'biggest', type: 'XYZ_LAYER', + zIndex: 2, opacity: 1, show: true, - meta: { - zIndex: 2, - }, options: { url: 'https://tiles.openaerialmap.org/5a831b4a2553e6000ce5ac80/0/d02ddc76-9c2e-4994-97d4-a623eb371456/{z}/{x}/{y}.png', }, - details: { + layerRecord: { footprint: { type: 'Polygon', coordinates: [ @@ -106,7 +116,7 @@ const layers = [ }, ]; -const ContextMenu: React.FC = ({ data, position, style, size, handleClose }) => { +const ContextMenu: React.FC = ({ data, position, style, handleClose }) => { const mapViewer = useCesiumMap(); const [pickedLayers, setPickedLayers] = useState(); @@ -140,12 +150,8 @@ const ContextMenu: React.FC = ({ data, position, style, size, From POI {data.length} layers overlapping
- {data?.map((layer) => { - return ( -

{`${(layer as unknown as Record).meta?.id} <--> ${ - (layer as unknown as Record).meta?.meta?.zIndex - }`}

- ); + {data?.map((layer, index) => { + return

{getDebugLayerText(layer)}

; })}
@@ -154,16 +160,12 @@ const ContextMenu: React.FC = ({ data, position, style, size, From PICK API {pickedLayers?.length} layers at point
- {pickedLayers?.map((layer) => { - return ( -

{`${(layer as unknown as Record).meta?.id} <--> ${ - (layer as unknown as Record).meta?.meta?.zIndex - }`}

- ); + {pickedLayers?.map((layer, index) => { + return

{getDebugLayerText(layer)}

; })}
- handleClose()} style={{ visibility: 'hidden', width: '100%' }}> + handleClose()} style={{ visibility: 'hidden', width: '100%' }}> @@ -183,7 +185,7 @@ const ContextMenu: React.FC = ({ data, position, style, size, >

No data found

- handleClose()} style={{ visibility: 'hidden', width: '100%' }}> + handleClose()} style={{ visibility: 'hidden', width: '100%' }}> @@ -234,63 +236,121 @@ const LayersMozaik: React.FC = (props) => { setAllShow(!allShow); }; + const controlsContainerStyle = { + display: 'flex', + flexWrap: 'wrap' as const, + alignItems: 'center', + gap: '8px', + padding: '10px 12px', + borderRadius: '10px', + background: 'rgba(0, 0, 0, 0.75)', + border: '1px solid rgba(255, 255, 255, 0.2)', + color: 'white', + position: 'absolute' as const, + top: '10px', + left: '50%', + transform: 'translateX(-50%)', + zIndex: 2, + maxWidth: 'calc(100% - 20px)', + }; + + const messageStyle = { + margin: 0, + color: '#8dff9f', + fontSize: '18px', + fontWeight: 700, + flexBasis: '100%', + }; + + const fieldStyle = { + height: '30px', + borderRadius: '6px', + border: '1px solid rgba(255, 255, 255, 0.3)', + background: 'rgba(255, 255, 255, 0.08)', + color: 'white', + padding: '0 8px', + }; + + const buttonStyle = { + height: '30px', + borderRadius: '6px', + border: '1px solid rgba(255, 255, 255, 0.25)', + background: 'rgba(96, 165, 250, 0.25)', + color: 'white', + padding: '0 10px', + cursor: 'pointer', + fontWeight: 600, + }; + return ( -
-

Change BASE MAP to see effective layers

+
+

Change BASE MAP to see effective layers

{ setTimes(parseInt(evt.target.value)); }} > - - - - - +
); }; @@ -303,16 +363,18 @@ export const MapWithLayersManagerAndContextMenu: Story = () => { } imageryContextMenuSize={{ height: 340, width: 200 }} - layerManagerFootprintMetaFieldPath={'details.footprint'} + showDebuggerTool={true} + layerManagerMetaMapping={layerManagerMetaMapping} >
); }; + +MapWithLayersManagerAndContextMenu.storyName = 'Layers Manager and Context Menu'; diff --git a/packages/react-components/src/components/cesium-map/data-sources/drawings.data-source.stories.tsx b/packages/react-components/src/components/cesium-map/data-sources/drawings.data-source.stories.tsx index 57cf482c5..20967aa33 100644 --- a/packages/react-components/src/components/cesium-map/data-sources/drawings.data-source.stories.tsx +++ b/packages/react-components/src/components/cesium-map/data-sources/drawings.data-source.stories.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { Story, Meta } from '@storybook/react/types-6-0'; import { BboxCorner, DrawType } from '../../models'; +import { BASE_MAPS } from '../helpers/constants'; import { CesiumMap } from '../map'; import { CesiumColor, CesiumSceneMode } from '../proxied.types'; import { CesiumDrawingsDataSource, IDrawing, IDrawingEvent } from './drawings.data-source'; @@ -19,6 +20,13 @@ const mapDivStyle = { position: 'absolute' as const, }; +const layerManagerMetaMapping = { + layer: { + id: 'id', + name: 'layerRecord.productName', + }, +}; + interface IDrawingObject { type: DrawType; handler: (drawing: IDrawingEvent) => void; @@ -29,7 +37,7 @@ export const Drawings: Story = (args) => { const [drawPrimitive, setDrawPrimitive] = useState({ type: DrawType.UNKNOWN, // eslint-disable-next-line @typescript-eslint/no-empty-function - handler: (drawing: IDrawingEvent) => {}, + handler: (_drawing: IDrawingEvent) => {}, }); const [drawEntities, setDrawEntities] = useState([ { @@ -88,7 +96,7 @@ export const Drawings: Story = (args) => { setDrawPrimitive({ type: DrawType.UNKNOWN, // eslint-disable-next-line @typescript-eslint/no-empty-function - handler: (drawing: IDrawingEvent) => {}, + handler: (_drawing: IDrawingEvent) => {}, }); }} > @@ -136,7 +144,13 @@ export const Drawings: Story = (args) => { Draw rectangle by coordinates
- + ; -} - -export type IActiveFeatureTypes = IFeatureTypeMetadata & { - zoomLevel: number; -}; - export interface IDebuggerWidgetProps extends IWidgetProps { locale?: { [key: string]: string }; } -interface LayerMetaItem { - layerId?: string; - meta?: Record; +interface LayerDebugMeta { + id?: string; + isRelevantToExtent?: boolean; + [key: string]: unknown; +} + +interface LayerDebugItem { + layerId: string; + layerName?: string; + meta: LayerDebugMeta; } type DebuggerSectionId = 'data' | 'layers' | 'tools'; const DebuggerComponent: React.FC = ({ locale, isOpen, setIsOpen }) => { - const [featureTypes, setFeatureTypes] = useState([]); - const [layersMeta, setLayersMeta] = useState([]); + const [featureTypes, setFeatureTypes] = useState([]); + const [layersMeta, setLayersMeta] = useState([]); const [collapsedSections, setCollapsedSections] = useState>({ data: false, layers: false, @@ -64,19 +58,23 @@ const DebuggerComponent: React.FC = ({ locale, isOpen, set })); }; - const updateLayerMeta = (): void => { - if (!mapViewer.layersManager?.layerList) return; - setLayersMeta( - mapViewer.layersManager.layerList - .filter((layer): boolean => layer.meta?.id !== TRANSPARENT_LAYER_ID) - .map( - (layer): LayerMetaItem => ({ - layerId: layer.meta?.id as string | undefined, - meta: layer.meta as Record | undefined, - }) - ) - ); - }; + const updateLayersMeta = useCallback((): void => { + if (!mapViewer.layersManager?.layerList) { return; } + const nextLayersMeta = mapViewer.layersManager.layerList + .map((layer): LayerDebugItem | undefined => { + const layerId = getLayerId(layer); + if (!isManagedImageryLayer(layerId)) { + return undefined; + } + return { + layerId: layerId as string, + layerName: getLayerName(layer), + meta: (layer.meta ?? {}) as LayerDebugMeta, + }; + }) + .filter((item): item is LayerDebugItem => item !== undefined); + setLayersMeta(nextLayersMeta); + }, [mapViewer.layersManager]); useEffect(() => { let moveEndRefreshTimeoutId: ReturnType | undefined; @@ -85,76 +83,77 @@ const DebuggerComponent: React.FC = ({ locale, isOpen, set clearTimeout(moveEndRefreshTimeoutId); } moveEndRefreshTimeoutId = setTimeout(() => { - updateLayerMeta(); + updateLayersMeta(); }, 0); }; const removeTileLoad = mapViewer.scene.globe.tileLoadProgressEvent.addEventListener((tilesLoadingCount) => { if (tilesLoadingCount === 0) { - updateLayerMeta(); + updateLayersMeta(); removeTileLoad(); } }); const removeMoveEnd = mapViewer.camera.moveEnd.addEventListener(() => { scheduleLayerMetaRefresh(); }); + const removeLayerMoved = mapViewer.imageryLayers.layerMoved.addEventListener(() => { + scheduleLayerMetaRefresh(); + }); + const removeLayerAdded = mapViewer.imageryLayers.layerAdded.addEventListener(() => { + scheduleLayerMetaRefresh(); + }); const removeLayerRemoved = mapViewer.imageryLayers.layerRemoved.addEventListener(() => { scheduleLayerMetaRefresh(); }); - mapViewer.layersManager?.addLayerUpdatedListener(updateLayerMeta); + mapViewer.layersManager?.addLayerUpdatedListener(updateLayersMeta); return (): void => { if (moveEndRefreshTimeoutId !== undefined) { clearTimeout(moveEndRefreshTimeoutId); } removeTileLoad(); removeMoveEnd(); + removeLayerMoved(); + removeLayerAdded(); removeLayerRemoved(); - mapViewer.layersManager?.removeLayerUpdatedListener(updateLayerMeta); + mapViewer.layersManager?.removeLayerUpdatedListener(updateLayersMeta); }; - }, []); + }, [mapViewer, updateLayersMeta]); useEffect(() => { - updateLayerMeta(); - }, [viewState?.shouldOptimizedTileRequests]); + updateLayersMeta(); + }, [updateLayersMeta, viewState?.shouldOptimizedTileRequests]); useEffect(() => { - if (!mapViewer.layersManager) return; - - const handleDataLayerUpdated = (dataLayers: ICesiumWFSLayer[], LayerId?: string | undefined): void => { + if (!mapViewer.layersManager) { return; } + const handleDataLayerUpdated = (dataLayers: ICesiumWFSLayer[], layerId?: string | undefined): void => { dataLayers.forEach((layer: ICesiumWFSLayer): void => { - if (LayerId !== undefined && LayerId !== layer.meta.id) { + if (layerId !== undefined && layerId !== getLayerId(layer)) { return; } - const { options, meta } = layer; const { zoomLevel } = options; - const { id, items, total, cache, currentZoomLevel, featureStructure } = meta as unknown as IFeatureTypeMetadata; - + const { id, items, total, cache, currentZoomLevel, layerRecord } = meta; setFeatureTypes((prevFeatureTypes) => { - const existingIndex = prevFeatureTypes.findIndex((type) => type.id === id); + const existingIndex = prevFeatureTypes.findIndex((featureType) => getLayerIdFromMeta(featureType) === id); if (existingIndex >= 0) { if ( JSON.stringify(prevFeatureTypes[existingIndex]) !== - JSON.stringify({ id, items, total, cache, currentZoomLevel, featureStructure, zoomLevel }) + JSON.stringify({ id, items, total, cache, currentZoomLevel, layerRecord, zoomLevel }) ) { const updatedFeatureTypes = [...prevFeatureTypes]; - updatedFeatureTypes[existingIndex] = { id, items, total, cache, currentZoomLevel, featureStructure, zoomLevel }; + updatedFeatureTypes[existingIndex] = { id, items, total, cache, currentZoomLevel, layerRecord, zoomLevel }; return updatedFeatureTypes; } } else { - return [...prevFeatureTypes, { id, items, total, cache, currentZoomLevel, featureStructure, zoomLevel }]; + return [...prevFeatureTypes, { id, items, total, cache, currentZoomLevel, layerRecord, zoomLevel }]; } return prevFeatureTypes; }); }); - - const activeDataLayerIds = new Set(mapViewer.layersManager?.dataLayerList.map((layer) => layer.meta.id)); - - setFeatureTypes((prevFeatureTypes) => prevFeatureTypes.filter((type) => activeDataLayerIds.has(type.id))); + const activeDataLayerIds = new Set(mapViewer.layersManager?.dataLayerList.map((layer) => getLayerId(layer))); + setFeatureTypes((prevFeatureTypes) => prevFeatureTypes.filter((featureType) => activeDataLayerIds.has(getLayerIdFromMeta(featureType)))); }; - mapViewer.layersManager.addDataLayerUpdatedListener(handleDataLayerUpdated); - return () => { mapViewer.layersManager?.removeDataLayerUpdatedListener(handleDataLayerUpdated); }; @@ -210,47 +209,52 @@ const DebuggerComponent: React.FC = ({ locale, isOpen, set })); }} /> - {viewState?.shouldOptimizedTileRequests === true && ( - - {[...layersMeta].reverse().map((layer, index) => { - const idText = layer.layerId ?? `LAYER-${layersMeta.length - index}`; - const nameText = (get(layer.meta, 'layerRecord.productName') as string | undefined) ?? idText; - const statusText = - layer.meta?.relevantToExtent === true ? ' → show' : layer.meta?.relevantToExtent === false ? ' → hide' : ''; - const transparencyText = - layer.meta?.hasTransparency === true ? withTransparencyTiles : layer.meta?.hasTransparency === false ? withoutTransparencyTiles : ''; - const tileCoordinatesFromMeta = get(layer.meta, EXAMINED_TILES_META_PROP) as - | Array<{ x?: number; y?: number; level?: number }> - | { x?: number; y?: number; level?: number } - | undefined; - const tileCoordinatesList = Array.isArray(tileCoordinatesFromMeta) - ? tileCoordinatesFromMeta - : tileCoordinatesFromMeta !== undefined - ? [tileCoordinatesFromMeta] - : []; - const formattedTileCoordinates = tileCoordinatesList - .filter((tile) => tile.x !== undefined && tile.y !== undefined && tile.level !== undefined) - .map((tile) => `( L: ${String(tile.level)}, X: ${String(tile.x)}, Y: ${String(tile.y)} )`); - const tooltipContent = - transparencyText === '' - ? undefined - : {transparencyText}: {formattedTileCoordinates.join(', ')}; - const isRelevant = layer.meta?.relevantToExtent !== false; - if (tooltipContent === undefined) { - return ( - - {nameText + statusText} + {viewState?.shouldOptimizedTileRequests === true && layersMeta?.length > 0 && ( + + + + + + {[...layersMeta].reverse().map((layer) => { + const idText = layer.layerId; + const nameText = layer.layerName ?? idText; + const statusText = + layer.meta?.isRelevantToExtent === true ? ' → show' : layer.meta?.isRelevantToExtent === false ? ' → hide' : ''; + const hasTransparency = layer.meta[HAS_TRANSPARENCY_META_PROP] as boolean | undefined; + const transparencyText = + hasTransparency === true ? withTransparencyTiles : hasTransparency === false ? withoutTransparencyTiles : ''; + const tileCoordinatesFromMeta = layer.meta[EXAMINED_TILES_META_PROP] as + | Array<{ x?: number; y?: number; level?: number }> + | { x?: number; y?: number; level?: number } + | undefined; + const tileCoordinatesList = Array.isArray(tileCoordinatesFromMeta) + ? tileCoordinatesFromMeta + : tileCoordinatesFromMeta !== undefined + ? [tileCoordinatesFromMeta] + : []; + const formattedTileCoordinates = tileCoordinatesList + .filter((tile) => tile.x !== undefined && tile.y !== undefined && tile.level !== undefined) + .map((tile) => `( L: ${String(tile.level)}, X: ${String(tile.x)}, Y: ${String(tile.y)} )`); + const tooltipContent = + transparencyText === '' + ? undefined + : {transparencyText}: {formattedTileCoordinates.join(', ')}; + const isRelevant = layer.meta?.isRelevantToExtent !== false; + const itemContent = ( + + {nameText + statusText} ); - } - return ( - - - {nameText + statusText} - - - ); - })} + if (tooltipContent === undefined) { + return {itemContent}; + } + return ( + + {itemContent} + + ); + })} + )} diff --git a/packages/react-components/src/components/cesium-map/debug/wfs.tsx b/packages/react-components/src/components/cesium-map/debug/wfs.tsx index d9fd4946f..76b633a1b 100644 --- a/packages/react-components/src/components/cesium-map/debug/wfs.tsx +++ b/packages/react-components/src/components/cesium-map/debug/wfs.tsx @@ -2,12 +2,13 @@ import { get } from 'lodash'; import React, { useMemo } from 'react'; import { Tooltip } from '@map-colonies/react-core'; import { Box } from '../../box'; -import { IActiveFeatureTypes } from './debugger-widget'; +import { ICesiumWFSLayerMeta } from '../layers/wfs.layer'; +import { getDataLayerName, getLayerIdFromMeta } from '../layers-manager'; import './wfs.css'; interface IWFSProps { - featureTypes: IActiveFeatureTypes[]; + featureTypes: ICesiumWFSLayerMeta[]; locale?: { [key: string]: string }; } @@ -25,20 +26,28 @@ export const WFS: React.FC = ({ featureTypes, locale }) => { return ( <> {featureTypes.length > 0 ? ( - featureTypes.map((type, index) => ( + featureTypes.map((featureType, index) => ( - - - {type.featureStructure.aliasLayerName as string} ({String(type.zoomLevel)}): + {(() => { + const dataLayerName = getDataLayerName(featureType) ?? ''; + const zoomLevel = featureType.zoomLevel ?? 0; + return ( + + + {dataLayerName} ({zoomLevel}): + ); + })()} - {cacheLabel}: {type.cache ?? 0} + {cacheLabel}: {featureType.cache ?? 0} - {type.total > 0 && ( + {(featureType.total ?? 0) > 0 && ( - {extentLabel}: {type.items} / {type.total} + {extentLabel}: {featureType.items} / {featureType.total} )} diff --git a/packages/react-components/src/components/cesium-map/entities/entity.graphics.stories.tsx b/packages/react-components/src/components/cesium-map/entities/entity.graphics.stories.tsx index d2bb0dfa7..29b517709 100644 --- a/packages/react-components/src/components/cesium-map/entities/entity.graphics.stories.tsx +++ b/packages/react-components/src/components/cesium-map/entities/entity.graphics.stories.tsx @@ -1,6 +1,6 @@ -import React from 'react'; import { Cartesian3, Color } from 'cesium'; import { Story, Meta } from '@storybook/react/types-6-0'; +import { BASE_MAPS } from '../helpers/constants'; import { CesiumMap } from '../map'; import { CesiumEntity } from './entity'; import { CesiumEntityStaticDescription } from './entity.description'; @@ -20,9 +20,19 @@ const mapDivStyle = { position: 'absolute' as const, }; +const layerManagerMetaMapping = { + layer: { + id: 'id', + name: 'layerRecord.productName', + }, +}; + export const Polygon: Story = (args) => (
- +

Hello!

diff --git a/packages/react-components/src/components/cesium-map/entities/entity.stories.tsx b/packages/react-components/src/components/cesium-map/entities/entity.stories.tsx index d0f3c3706..42536c45e 100644 --- a/packages/react-components/src/components/cesium-map/entities/entity.stories.tsx +++ b/packages/react-components/src/components/cesium-map/entities/entity.stories.tsx @@ -1,6 +1,7 @@ import React, { useState, useMemo, useEffect, useRef } from 'react'; import { Cartesian3, Color } from 'cesium'; import { Story, Meta } from '@storybook/react/types-6-0'; +import { BASE_MAPS } from '../helpers/constants'; import { CesiumMap } from '../map'; import { CesiumEntity, RCesiumEntityProps } from './entity'; import { CesiumEntityDescription, CesiumEntityStaticDescription } from './entity.description'; @@ -19,6 +20,13 @@ const mapDivStyle = { position: 'absolute' as const, }; +const layerManagerMetaMapping = { + layer: { + id: 'id', + name: 'layerRecord.productName', + }, +}; + const initCanvas = (): HTMLCanvasElement => { const can = document.createElement('canvas'); can.width = 100; @@ -64,7 +72,10 @@ const CanvasEntity: React.FC = (props) => { export const Basic: Story = (args) => (
- + = (args) => { const [count, setCount] = useState(0); return ( - + = (args) => { }; export const AnimatedCanvas: Story = () => ( - + ); diff --git a/packages/react-components/src/components/cesium-map/helpers/customImageryProviders.ts b/packages/react-components/src/components/cesium-map/helpers/customImageryProviders.ts index 8642fc70a..374c0491d 100644 --- a/packages/react-components/src/components/cesium-map/helpers/customImageryProviders.ts +++ b/packages/react-components/src/components/cesium-map/helpers/customImageryProviders.ts @@ -8,8 +8,8 @@ import { ImageryTypes, } from 'cesium'; import { get } from 'lodash'; -import { ICesiumImageryLayer } from '../layers-manager'; -import { CesiumViewer } from '../map'; +import type { ICesiumImageryLayer } from '../layers-manager'; +import type { CesiumViewer } from '../map'; import { imageHasTransparency } from './utils'; export interface CustomImageryProvider extends ImageryProvider { @@ -30,6 +30,10 @@ const NUMBER_OF_TILES_TO_CHECK = 3; export const HAS_TRANSPARENCY_META_PROP = 'hasTransparency'; export const EXAMINED_TILES_META_PROP = 'examinedTiles'; +const getImageryProviderUrl = (layer: ImageryLayer): string | undefined => { + return get(layer, '_imageryProvider._resource._url') as string | undefined; +}; + function customCommonRequestImage( this: CustomImageryProvider, requestImageFn: ImageryProvider['requestImage'], @@ -43,7 +47,7 @@ function customCommonRequestImage( const requestedLayerMeta = this.layerListInstance.find( /* eslint-disable */ (layer: ImageryLayer): boolean => { - return (layer as any)._imageryProvider._resource?._url === (this as any)._resource?._url; + return getImageryProviderUrl(layer) === (this as any)._resource?._url; } /* eslint-enable */ )?.meta; @@ -58,7 +62,7 @@ function customCommonRequestImage( { [EXAMINED_TILES_META_PROP]: this.examinedTilesForTransparencyCheck }, /* eslint-disable */ (layer: ImageryLayer): boolean => { - return (layer as any)._imageryProvider._resource._url === (this as any)._resource._url; + return getImageryProviderUrl(layer) === (this as any)._resource._url; } /* eslint-enable */ ); @@ -67,7 +71,7 @@ function customCommonRequestImage( { [HAS_TRANSPARENCY_META_PROP]: hasTransparency }, /* eslint-disable */ (layer: ImageryLayer): boolean => { - return (layer as any)._imageryProvider._resource._url === (this as any)._resource._url; + return getImageryProviderUrl(layer) === (this as any)._resource._url; } /* eslint-enable */ ); diff --git a/packages/react-components/src/components/cesium-map/helpers/geojson/point.geojson.ts b/packages/react-components/src/components/cesium-map/helpers/geojson/point.geojson.ts index 57de3e439..2d56fe4e5 100644 --- a/packages/react-components/src/components/cesium-map/helpers/geojson/point.geojson.ts +++ b/packages/react-components/src/components/cesium-map/helpers/geojson/point.geojson.ts @@ -2,8 +2,8 @@ import { Math as CesiumMath, Cartesian3, Cartographic, SceneMode, Cartesian2 } f import { GeoJSON } from 'geojson'; import { CesiumViewer } from '../../map'; -const pointToCartographic = (mapViewer: CesiumViewer, x: number, y: number): Cartographic => { - let cartesian; +const pointToCartographic = (mapViewer: CesiumViewer, x: number, y: number): Cartographic | undefined => { + let cartesian: Cartesian3 | undefined; if (mapViewer.scene.mode !== SceneMode.SCENE2D) { cartesian = mapViewer.scene.pickPosition(new Cartesian2(x, y)); @@ -12,12 +12,20 @@ const pointToCartographic = (mapViewer: CesiumViewer, x: number, y: number): Car cartesian = mapViewer.camera.pickEllipsoid(new Cartesian2(x, y), ellipsoid); } - return Cartographic.fromCartesian(cartesian as Cartesian3); + if (cartesian === undefined) { + return undefined; + } + + return Cartographic.fromCartesian(cartesian); }; -export const pointToGeoJSON = (mapViewer: CesiumViewer, x: number, y: number): GeoJSON => { +export const pointToGeoJSON = (mapViewer: CesiumViewer, x: number, y: number): GeoJSON | undefined => { const cartographic = pointToCartographic(mapViewer, x, y); + if (cartographic === undefined) { + return undefined; + } + return { type: 'Feature', properties: {}, @@ -32,6 +40,10 @@ export const pointToLonLat = (mapViewer: CesiumViewer, x: number, y: number): { try { const cartographic = pointToCartographic(mapViewer, x, y); + if (cartographic === undefined) { + return undefined; + } + return { longitude: CesiumMath.toDegrees(cartographic.longitude), latitude: CesiumMath.toDegrees(cartographic.latitude), diff --git a/packages/react-components/src/components/cesium-map/layers-manager.ts b/packages/react-components/src/components/cesium-map/layers-manager.ts index cd69f05c7..edb1f7c14 100644 --- a/packages/react-components/src/components/cesium-map/layers-manager.ts +++ b/packages/react-components/src/components/cesium-map/layers-manager.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { + Cesium3DTileset as CesiumTileset, ImageryLayer, UrlTemplateImageryProvider, WebMapServiceImageryProvider, @@ -8,28 +9,70 @@ import { Rectangle, SingleTileImageryProvider, } from 'cesium'; -import { get, isEmpty } from 'lodash'; +import { get, isEmpty, set } from 'lodash'; import { Feature, Point, Polygon } from 'geojson'; import booleanPointInPolygon from '@turf/boolean-point-in-polygon'; -import { RCesiumOSMLayerOptions, RCesiumWMSLayerOptions, RCesiumWMTSLayerOptions, RCesiumXYZLayerOptions } from './layers'; -import { CesiumViewer, IBaseMap } from './map'; -import { pointToGeoJSON } from './helpers/geojson/point.geojson'; -import { IMapLegend } from './legend'; import { CustomUrlTemplateImageryProvider, CustomWebMapServiceImageryProvider, CustomWebMapTileServiceImageryProvider, HAS_TRANSPARENCY_META_PROP, } from './helpers/customImageryProviders'; +import { pointToGeoJSON } from './helpers/geojson/point.geojson'; import { cesiumRectangleContained } from './helpers/utils'; -import { ICesiumWFSLayer } from './layers/wfs.layer'; -import { CesiumCartesian2 } from './proxied.types'; +import { RCesiumOSMLayerOptions, RCesiumWMSLayerOptions, RCesiumWMTSLayerOptions, RCesiumXYZLayerOptions } from './layers'; +import type { ICesiumWFSLayer, ICesiumWFSLayerMeta } from './layers/wfs.layer'; +import { IMapLegend } from './legend'; +import type { CesiumViewer, IBaseMap } from './map'; +import { CesiumCartesian2, CesiumImageryProvider } from './proxied.types'; const INC = 1; const DEC = -1; +export interface ILayerManagerMetaMapping { + layer: { + id: string; + name: string; + footprint?: string; + }; + dataLayer?: { + name?: string; + fields?: string; + }; +} + +let mapping: ILayerManagerMetaMapping = { + layer: { + id: 'id', + name: 'name', + }, +}; + +const configureLayerManagerMetaMapping = (metaMapping: ILayerManagerMetaMapping): void => { + mapping = { ...metaMapping }; +}; + +export const getLayerManagerMetaMapping = (): ILayerManagerMetaMapping => { + return { ...mapping }; +}; + +export interface ICesiumImageryLayerMeta { + id?: string; + parentBaseMapId?: string; + zIndex?: number; + type?: LayerType; + opacity?: number; + show?: boolean; + options?: RCesiumOSMLayerOptions | RCesiumWMSLayerOptions | RCesiumWMTSLayerOptions | RCesiumXYZLayerOptions; + skipRelevancyCheck?: boolean; + isRelevantToExtent?: boolean; + hasTransparency?: boolean; + examinedTiles?: Array<{ x?: number; y?: number; level?: number }>; + [key: string]: unknown; +} + export interface ICesiumImageryLayer extends InstanceType { - meta?: Record; + meta?: ICesiumImageryLayerMeta; } export type LayerType = 'OSM_LAYER' | 'WMTS_LAYER' | 'WMS_LAYER' | 'XYZ_LAYER'; @@ -39,55 +82,122 @@ export interface IRasterLayer { type: LayerType; opacity: number; zIndex: number; - show?: boolean; options: RCesiumOSMLayerOptions | RCesiumWMSLayerOptions | RCesiumWMTSLayerOptions | RCesiumXYZLayerOptions; - details?: Record; + show?: boolean; + [key: string]: unknown; } -export interface IVectorLayer { - id: string; - opacity: number; - zIndex: number; - url: string; +export interface ICesium3DModelMeta { + id?: string; + [key: string]: unknown; +} + +export interface ICesium3DModel { + tileset: CesiumTileset; + meta: ICesium3DModelMeta; +} + +export interface ICesiumDataLayerField { + fieldName: string; + aliasFieldName: string; + [key: string]: unknown; } export type LegendExtractor = (layers: (any & { meta: any })[]) => IMapLegend[]; export const TRANSPARENT_LAYER_ID = 'TRANSPARENT_BASE_LAYER'; +export const getLayerId = (layer: ICesiumImageryLayer | ICesiumWFSLayer | ICesium3DModel): string | undefined => { + return get(layer.meta, mapping.layer.id) as string | undefined; +}; + +export const getLayerIdFromMeta = (meta: ICesiumImageryLayerMeta | ICesiumWFSLayerMeta | ICesium3DModelMeta | undefined): string | undefined => { + return get(meta, mapping.layer.id) as string | undefined; +}; + +export const getLayerName = (layer: ICesiumImageryLayer | ICesiumWFSLayer | ICesium3DModel): string | undefined => { + return get(layer.meta, mapping.layer.name) as string | undefined; +}; + +export const getLayerFootprint = (meta: ICesiumWFSLayerMeta | undefined): unknown => { + return get(meta, mapping.layer.footprint ?? ''); +}; + +export const getDataLayerName = (meta: ICesiumWFSLayerMeta): string | undefined => { + return get(meta, mapping.dataLayer?.name ?? '') as string | undefined; +}; + +export const getDataLayerFields = (meta: ICesiumWFSLayerMeta | undefined): ICesiumDataLayerField[] => { + return (get(meta, mapping.dataLayer?.fields ?? '') as ICesiumDataLayerField[] | undefined) ?? []; +}; + +export const isServiceLayer = (layerId: string | undefined): boolean => { + return isEmpty(layerId) || layerId === TRANSPARENT_LAYER_ID; +}; + +export const isManagedImageryLayer = (layerId: string | undefined): boolean => { + return !isServiceLayer(layerId); +}; + +export const getParentBaseMapId = (meta: Record | undefined): string | undefined => { + return get(meta, 'parentBaseMapId') as string | undefined; +}; + +export const isBaseMapLayer = (meta: Record | undefined): boolean => { + return !!getParentBaseMapId(meta); +}; + +export const getImageryProvider = (layer: ICesiumImageryLayer): CesiumImageryProvider => { + return get(layer, 'imageryProvider'); +}; + +export const getImageryProviderUrl = (layer: ICesiumImageryLayer): string | undefined => { + return get(layer, '_imageryProvider._resource._url'); +}; + +export const getImageryProviderName = (provider: CesiumImageryProvider): string => { + return provider.constructor.name; +}; + class LayerManager { public mapViewer: CesiumViewer; public legendsList: IMapLegend[]; public layerUpdated: Event; public dataLayerUpdated: Event; + public modelUpdated: Event; private readonly layers: ICesiumImageryLayer[]; private readonly dataLayers: ICesiumWFSLayer[]; + private readonly models: ICesium3DModel[]; private readonly legendsExtractor?: LegendExtractor; - private readonly layerManagerFootprintMetaFieldPath: string | undefined; + private readonly layerManagerFootprintMetaFieldPath?: string; private shouldOptimizedTileRequests?: boolean; private relevancyListenersCleanup: Array<() => void>; private relevancyLayerUpdatedHandler?: (meta: Record) => void; public constructor( mapViewer: CesiumViewer, + layerManagerMetaMapping: ILayerManagerMetaMapping, legendsExtractor?: LegendExtractor, onLayersUpdate?: () => void, - layerManagerFootprintMetaFieldPath?: string, shouldOptimizedTileRequests?: boolean ) { this.mapViewer = mapViewer; // eslint-disable-next-line this.layers = (this.mapViewer.imageryLayers as any)._layers; this.dataLayers = []; + this.models = []; this.legendsList = []; this.legendsExtractor = legendsExtractor; this.layerUpdated = new Event(); this.dataLayerUpdated = new Event(); - this.layerManagerFootprintMetaFieldPath = layerManagerFootprintMetaFieldPath; + this.modelUpdated = new Event(); + this.layerManagerFootprintMetaFieldPath = layerManagerMetaMapping.layer.footprint; this.shouldOptimizedTileRequests = shouldOptimizedTileRequests ?? false; this.relevancyListenersCleanup = []; + configureLayerManagerMetaMapping(layerManagerMetaMapping); + if (onLayersUpdate) { this.addLayerUpdatedListener(onLayersUpdate); } @@ -106,8 +216,8 @@ class LayerManager { return this.dataLayers; } - public isBaseMapLayer(meta: any): boolean { - return !!get(meta, 'parentBasetMapId'); + public get modelList(): ICesium3DModel[] { + return this.models; } public addDataLayer(dataLayer: ICesiumWFSLayer): void { @@ -115,7 +225,7 @@ class LayerManager { this.dataLayerUpdated.raiseEvent(this.dataLayers); } - // A general place to extend layer's data. Should be done when all providers(different types) are initialized + // A general place to extend layer's data. Should be done when all providers (different types) are initialized public addMetaToLayer(meta: any, layerPredicate: (layer: ImageryLayer, idx: number) => boolean): void { Promise.resolve().then(() => { const layer = this.layers.find(layerPredicate); @@ -128,10 +238,14 @@ class LayerManager { } public addMetaToDataLayer(meta: any): void { - const dataLayer = this.findDataLayerById(meta.id); + const dataLayerId = getLayerIdFromMeta(meta); + if (dataLayerId === undefined) { + return; + } + const dataLayer = this.findDataLayerById(dataLayerId); if (dataLayer) { dataLayer.meta = { ...(dataLayer.meta ?? {}), ...meta }; - this.dataLayerUpdated.raiseEvent(this.dataLayers, meta.id); + this.dataLayerUpdated.raiseEvent(this.dataLayers, dataLayerId as any); } } @@ -201,7 +315,7 @@ class LayerManager { if (cesiumLayer) { cesiumLayer.alpha = layer.opacity; cesiumLayer.meta = { - parentBasetMapId: parentId, + parentBaseMapId: parentId, ...layer, }; if (layer.show !== undefined) { @@ -230,7 +344,7 @@ class LayerManager { public removeBaseMapLayers(): void { const layerToDelete = this.layers.filter((layer) => { - return this.isBaseMapLayer(layer.meta); + return isBaseMapLayer(layer.meta); }); layerToDelete.forEach((layer) => { this.mapViewer.imageryLayers.remove(layer, true); @@ -240,8 +354,7 @@ class LayerManager { public removeNotBaseMapLayers(): void { const layerToDelete = this.layers.filter((layer) => { - const parentId = get(layer.meta, 'parentBasetMapId') as string; - return parentId ? false : true; + return !isBaseMapLayer(layer.meta); }); layerToDelete.forEach((layer) => { this.mapViewer.imageryLayers.remove(layer, true); @@ -260,6 +373,7 @@ class LayerManager { } this.updateLayersOrder(layerId, order, order + positions); + this.reinvokeOptimizationAfterOrderChange(); } public lower(layerId: string, positions = 1): void { @@ -279,6 +393,7 @@ class LayerManager { } this.updateLayersOrder(layerId, order, order - positions); + this.reinvokeOptimizationAfterOrderChange(); } public raiseToTop(layerId: string): void { @@ -290,6 +405,7 @@ class LayerManager { } this.updateLayersOrder(layerId, order, this.mapViewer.imageryLayers.length - this.getBaseLayersCount() - 1); + this.reinvokeOptimizationAfterOrderChange(); } public lowerToBottom(layerId: string): void { @@ -319,11 +435,13 @@ class LayerManager { public showAllNotBase(isShow: boolean): void { const nonBaseLayers = this.layers.filter((layer) => { - const parentId = get(layer.meta, 'parentBasetMapId') as string; - return parentId ? false : true; + return !isBaseMapLayer(layer.meta); }); nonBaseLayers.forEach((layer: ICesiumImageryLayer) => { - this.show(layer.meta?.id as string, isShow); + const layerId = getLayerId(layer); + if (layerId !== undefined) { + this.show(layerId, isShow); + } }); } @@ -341,8 +459,7 @@ class LayerManager { if (pickRay) { nonBaseLayers = this.mapViewer.imageryLayers.pickImageryLayers(pickRay, this.mapViewer.scene)?.filter((layer: ICesiumImageryLayer) => { - const parentId = get(layer.meta, 'parentBasetMapId') as string; - return parentId ? false : true; + return !isBaseMapLayer(layer.meta); }); } @@ -351,11 +468,11 @@ class LayerManager { public findLayerByPOI(x: number, y: number, onlyShown = true): ICesiumImageryLayer[] | undefined { if (this.layerManagerFootprintMetaFieldPath) { - const position = pointToGeoJSON(this.mapViewer, x, y) as Feature; + const position = pointToGeoJSON(this.mapViewer, x, y) as Feature | undefined; + if (position === undefined) return undefined; const nonBaseLayers = this.layers.filter((layer) => { - const parentId = get(layer.meta, 'parentBasetMapId') as string; - return parentId ? false : true; + return !isBaseMapLayer(layer.meta); }); const selectedVisibleLayers = nonBaseLayers.filter((layer) => { @@ -389,22 +506,20 @@ class LayerManager { public addTransparentImageryProvider(): void { // Worldwide transparent layer const transparentTileUrl = `${import.meta.env.BASE_URL}assets/img/transparent-tile.png`; + /* eslint-disable @typescript-eslint/no-magic-numbers */ + const rectangle = new Rectangle(-3.141592653589793, -1.5707963267948966, 3.141592653589793, 1.5707963267948966); + /* eslint-enable @typescript-eslint/no-magic-numbers */ - const transparentLayer = this.mapViewer.imageryLayers.addImageryProvider( - new SingleTileImageryProvider({ - url: transparentTileUrl, - /* eslint-disable @typescript-eslint/no-magic-numbers */ - rectangle: new Rectangle(-3.141592653589793, -1.5707963267948966, 3.141592653589793, 1.5707963267948966), - /* eslint-enable @typescript-eslint/no-magic-numbers */ - }), - 0 - ); - - (transparentLayer as ICesiumImageryLayer).meta = { - id: TRANSPARENT_LAYER_ID, - skipRelevancyCheck: true, - parentBasetMapId: 'TRANSPARENT_LAYER', - }; + void SingleTileImageryProvider.fromUrl(transparentTileUrl, { rectangle }).then((provider) => { + const transparentLayer = this.mapViewer.imageryLayers.addImageryProvider(provider, 0); + + const transparentLayerMeta: Record = { + skipRelevancyCheck: true, + parentBaseMapId: 'TRANSPARENT_LAYER', + }; + set(transparentLayerMeta, mapping.layer.id, TRANSPARENT_LAYER_ID); + (transparentLayer as ICesiumImageryLayer).meta = transparentLayerMeta; + }); } public addLayerUpdatedListener(callback: (meta: any) => void): void { @@ -423,6 +538,14 @@ class LayerManager { this.dataLayerUpdated.removeEventListener(callback, this); } + public addModelUpdatedListener(callback: (models: ICesium3DModel[]) => void): void { + this.modelUpdated.addEventListener(callback, this); + } + + public removeModelUpdatedListener(callback: (models: ICesium3DModel[]) => void): void { + this.modelUpdated.removeEventListener(callback, this); + } + public setShouldOptimizedTileRequests(shouldOptimize: boolean): void { if (this.shouldOptimizedTileRequests === shouldOptimize) { return; @@ -447,10 +570,30 @@ class LayerManager { public findDataLayerById(dataLayerId: string): ICesiumWFSLayer | undefined { return this.dataLayers.find((dataLayer) => { - return dataLayer.meta.id === dataLayerId; + return getLayerId(dataLayer) === dataLayerId; }); } + public addModel(model: ICesium3DModel): void { + this.models.push({ ...model }); + this.modelUpdated.raiseEvent(this.models); + } + + public removeModel(modelId: string): void { + const model = this.findModelById(modelId); + if (model) { + const index = this.models.indexOf(model); + if (index > -1) { + this.models.splice(index, 1); + } + this.modelUpdated.raiseEvent(this.models); + } + } + + public findModelById(modelId: string): ICesium3DModel | undefined { + return this.models.find((model) => getLayerId(model) === modelId); + } + private setLegends(): void { if (typeof this.legendsExtractor !== 'undefined') { this.legendsList = this.legendsExtractor(this.layers); @@ -459,16 +602,14 @@ class LayerManager { private getBaseLayersCount(): number { const baseLayers = this.layers.filter((layer) => { - const parentId = get(layer.meta, 'parentBasetMapId') as string; - return parentId ? true : false; + return isBaseMapLayer(layer.meta); }); return baseLayers.length; } private findLayerById(layerId: string): ICesiumImageryLayer | undefined { return this.layers.find((layer) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return layer.meta !== undefined ? layer.meta.id === layerId : false; + return getLayerId(layer) === layerId; }); } @@ -476,10 +617,8 @@ class LayerManager { const move = from > to ? INC : DEC; const min = from < to ? from : to; const max = from < to ? to : from; - this.layers.forEach((layer) => { - const parentId = get(layer.meta, 'parentBasetMapId') as string; - if (!parentId) { + if (!isBaseMapLayer(layer.meta)) { const layerOrder = layer.meta?.zIndex as number; (layer.meta as Record).zIndex = layerOrder >= min && layerOrder <= max && layerOrder !== from ? layerOrder + move : layerOrder === from ? to : layerOrder; @@ -489,24 +628,23 @@ class LayerManager { private hideNonRelevantLayers(): void { for (const layer of this.layers) { - if (layer.meta?.id === TRANSPARENT_LAYER_ID) { + if (getLayerId(layer) === TRANSPARENT_LAYER_ID) { continue; } - - const relevantToExtent = layer.meta?.relevantToExtent; - if (typeof relevantToExtent !== 'boolean') { + const isRelevantToExtent = layer.meta?.isRelevantToExtent; + if (typeof isRelevantToExtent !== 'boolean') { continue; } - if (relevantToExtent !== layer.show) { - layer.show = relevantToExtent; + if (isRelevantToExtent !== layer.show) { + layer.show = isRelevantToExtent; } } } private restoreAllLayersVisibility(): void { for (const layer of this.layers) { - if (layer.meta?.id === TRANSPARENT_LAYER_ID) { + if (getLayerId(layer) === TRANSPARENT_LAYER_ID) { continue; } layer.show = true; @@ -515,12 +653,12 @@ class LayerManager { private clearLayersRelevancy(): void { for (const layer of this.layers) { - if (layer.meta?.id === TRANSPARENT_LAYER_ID) { + if (getLayerId(layer) === TRANSPARENT_LAYER_ID) { continue; } - if (layer.meta && 'relevantToExtent' in layer.meta) { - const { relevantToExtent, ...restMeta } = layer.meta; - void relevantToExtent; + if (layer.meta && 'isRelevantToExtent' in layer.meta) { + const { isRelevantToExtent, ...restMeta } = layer.meta; + void isRelevantToExtent; layer.meta = restMeta; } } @@ -581,6 +719,14 @@ class LayerManager { this.relevancyLayerUpdatedHandler = undefined; } + private reinvokeOptimizationAfterOrderChange(): void { + if (!this.shouldOptimizedTileRequests) { + return; + } + this.markRelevantLayersForExtent(); + this.hideNonRelevantLayers(); + } + private markRelevantLayersForExtent(): void { try { const extent = this.mapViewer.camera.computeViewRectangle() as Rectangle; @@ -592,11 +738,11 @@ class LayerManager { const layer = this.layers[i]; const intersectsExtent = !isEmpty(layer.rectangle) && Rectangle.intersection(extent, layer.rectangle) instanceof Rectangle; if (layer.meta?.skipRelevancyCheck === true) { - layer.meta = { ...layer.meta, relevantToExtent: true }; + layer.meta = { ...layer.meta, isRelevantToExtent: true }; continue; } if (!intersectsExtent) { - layer.meta = { ...(layer.meta ?? {}), relevantToExtent: false }; + layer.meta = { ...(layer.meta ?? {}), isRelevantToExtent: false }; continue; } let isOccludedByOpaqueLayerAbove = false; @@ -619,7 +765,7 @@ class LayerManager { // Layer is relevant if it intersects extent and has no opaque layer above it layer.meta = { ...(layer.meta ?? {}), - relevantToExtent: !isOccludedByOpaqueLayerAbove, + isRelevantToExtent: !isOccludedByOpaqueLayerAbove, }; } } catch (e) { diff --git a/packages/react-components/src/components/cesium-map/layers/3d.tileset.stories.tsx b/packages/react-components/src/components/cesium-map/layers/3d.tileset.stories.tsx index 9e27d6270..63d835142 100644 --- a/packages/react-components/src/components/cesium-map/layers/3d.tileset.stories.tsx +++ b/packages/react-components/src/components/cesium-map/layers/3d.tileset.stories.tsx @@ -1,4 +1,5 @@ -import { ArcGISTiledElevationTerrainProvider } from 'cesium'; +import { ArcGISTiledElevationTerrainProvider, TerrainProvider } from 'cesium'; +import React, { useState, useEffect } from 'react'; import { Story, Meta } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import { getValue } from '../../utils/config'; @@ -20,16 +21,30 @@ const mapDivStyle = { position: 'absolute' as const, }; -const ArcGisProvider = new ArcGISTiledElevationTerrainProvider({ - url: 'https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer', -}); +const layerManagerMetaMapping = { + layer: { + id: 'id', + name: 'layerRecord.productName', + }, +}; + +const ARCGIS_TERRAIN_URL = 'https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer'; + +const useArcGisTerrainProvider = (): TerrainProvider | undefined => { + const [provider, setProvider] = useState(undefined); + useEffect(() => { + void ArcGISTiledElevationTerrainProvider.fromUrl(ARCGIS_TERRAIN_URL).then(setProvider); + }, []); + return provider; +}; export const Cesium3DTilesetLayer: Story = (args: Record) => (
- + ) => (
- + ) => ( -
- - - -
-); +export const CesiumSolar3DTilesetLayer: Story = (args: Record) => { + const terrainProvider = useArcGisTerrainProvider(); + return ( +
+ + + +
+ ); +}; CesiumSolar3DTilesetLayer.argTypes = { baseMaps: { defaultValue: BASE_MAPS, }, - terrainProvider: { - defaultValue: ArcGisProvider, - }, center: { defaultValue: [34.811, 31.908], }, diff --git a/packages/react-components/src/components/cesium-map/layers/3d.tileset.tsx b/packages/react-components/src/components/cesium-map/layers/3d.tileset.tsx index f876dc5a0..b2c9b34c2 100644 --- a/packages/react-components/src/components/cesium-map/layers/3d.tileset.tsx +++ b/packages/react-components/src/components/cesium-map/layers/3d.tileset.tsx @@ -1,23 +1,38 @@ -import React, { ComponentProps } from 'react'; -import { Cartesian3, Cartographic, Matrix4 } from 'cesium'; +import React, { ComponentProps, useEffect, useRef } from 'react'; +import { Cartesian3, Cartographic, Matrix4, Cesium3DTileset as CesiumTileset } from 'cesium'; import { Cesium3DTileset as Resium3DTileset } from 'resium'; +import { getLayerIdFromMeta, ICesium3DModelMeta } from '../layers-manager'; import { CesiumViewer, useCesiumMap } from '../map'; const GROUND_LEVEL = 0.0; -export interface RCesium3DTilesetProps extends ComponentProps { +export interface ICesium3DTileset extends ComponentProps { isZoomTo?: boolean; heightFromGround?: number; + meta?: ICesium3DModelMeta; } -export const Cesium3DTileset: React.FC = (props) => { +export const Cesium3DTileset: React.FC = ({ meta, ...props }) => { const mapViewer: CesiumViewer = useCesiumMap(); + const tilesetRef = useRef(null); + + useEffect(() => { + return () => { + const modelId = getLayerIdFromMeta(meta); + if (tilesetRef.current !== null && modelId !== undefined) { + mapViewer.layersManager?.removeModel(modelId); + } + }; + }, []); + return ( { - // props.onReady?.(tileset); - + tilesetRef.current = tileset; + if (meta !== undefined) { + mapViewer.layersManager?.addModel({ tileset, meta }); + } if (props.isZoomTo === true) { void mapViewer.zoomTo(tileset); } @@ -31,6 +46,7 @@ export const Cesium3DTileset: React.FC = (props) => { const translation = Cartesian3.subtract(offset, surface, new Cartesian3()); tileset.modelMatrix = Matrix4.fromTranslation(translation); } + props.onReady?.(tileset); }} /> ); diff --git a/packages/react-components/src/components/cesium-map/layers/3d.tileset.with.update.tsx b/packages/react-components/src/components/cesium-map/layers/3d.tileset.with.update.tsx index cbeb5ed19..93c23a5de 100644 --- a/packages/react-components/src/components/cesium-map/layers/3d.tileset.with.update.tsx +++ b/packages/react-components/src/components/cesium-map/layers/3d.tileset.with.update.tsx @@ -8,14 +8,16 @@ import React, { useEffect, useState } from 'react'; import { Cesium3DTileset, Cesium3DTile, Cartographic, Cartesian3, defined, sampleTerrainMostDetailed, Cesium3DTileContent } from 'cesium'; +import { getLayerIdFromMeta, ICesium3DModelMeta } from '../layers-manager'; import { CesiumViewer, useCesiumMap } from '../map'; -export interface Cesium3DTilesetWithUpdateProps { +export interface ICesium3DTilesetWithUpdate { url: string; withUpdate?: boolean; + meta?: ICesium3DModelMeta; } -export const Cesium3DTilesetWithUpdate: React.FC = ({ url, withUpdate }) => { +export const Cesium3DTilesetWithUpdate: React.FC = ({ url, withUpdate, meta }) => { const mapViewer: CesiumViewer = useCesiumMap(); const scene = mapViewer.scene; const [tileset, setTileset] = useState(undefined); @@ -38,6 +40,19 @@ export const Cesium3DTilesetWithUpdate: React.FC // eslint-disable-next-line react-hooks/exhaustive-deps }, [url]); + useEffect(() => { + if (meta === undefined || tileset === undefined) { + return; + } + mapViewer.layersManager?.addModel({ tileset, meta }); + return () => { + const modelId = getLayerIdFromMeta(meta); + if (modelId !== undefined) { + mapViewer.layersManager?.removeModel(modelId); + } + }; + }, [mapViewer.layersManager]); + const updateContent = (model: Cesium3DTileContent, boundingVolume: any): void => { const height = boundingVolume.minimumHeight ? boundingVolume.minimumHeight : boundingVolume.center.z - boundingVolume.radius; // @ts-ignore @@ -45,7 +60,7 @@ export const Cesium3DTilesetWithUpdate: React.FC const normal = scene.globe.ellipsoid.geodeticSurfaceNormal(center, new Cartesian3()); const offset = Cartesian3.multiplyByScalar(normal, height, new Cartesian3()); const carto = Cartographic.fromCartesian(center); - void new Promise((resolve, reject) => { + void new Promise((resolve) => { // @ts-ignore if (scene.terrainProvider._ready !== true) { const result = { ...carto }; diff --git a/packages/react-components/src/components/cesium-map/layers/geojson.layer.stories.tsx b/packages/react-components/src/components/cesium-map/layers/geojson.layer.stories.tsx index 96e8869ec..ba747bacb 100644 --- a/packages/react-components/src/components/cesium-map/layers/geojson.layer.stories.tsx +++ b/packages/react-components/src/components/cesium-map/layers/geojson.layer.stories.tsx @@ -19,6 +19,13 @@ const mapDivStyle = { position: 'absolute' as const, }; +const layerManagerMetaMapping = { + layer: { + id: 'id', + name: 'layerRecord.productName', + }, +}; + const data = { type: 'FeatureCollection', features: [ @@ -108,7 +115,10 @@ const onLoadAction = action('onLoad'); export const MapWithGeojsonLayer: Story = (args: Record) => (
- + ( -
- - - - -
-); -MapWithImageryLayers.storyName = 'With 2 imagery layers'; diff --git a/packages/react-components/src/components/cesium-map/layers/imagery.layer.tsx b/packages/react-components/src/components/cesium-map/layers/imagery.layer.tsx index 557730284..dba89b49b 100644 --- a/packages/react-components/src/components/cesium-map/layers/imagery.layer.tsx +++ b/packages/react-components/src/components/cesium-map/layers/imagery.layer.tsx @@ -1,9 +1,13 @@ import React, { ComponentProps, useLayoutEffect } from 'react'; +import { ImageryLayer } from 'cesium'; import { ImageryLayer as ResiumImageryLayer } from 'resium'; import { CesiumViewer, useCesiumMap } from '../map'; export interface RCesiumImageryLayerProps extends ComponentProps { - meta?: any; + meta: { + searchLayerPredicate: (layer: ImageryLayer, idx: number) => boolean; + [key: string]: unknown; + }; } export const CesiumImageryLayer: React.FC = (props) => { diff --git a/packages/react-components/src/components/cesium-map/layers/layers.rect.stories.tsx b/packages/react-components/src/components/cesium-map/layers/layers.rect.stories.tsx index 2bfc5dfaa..c368f4d00 100644 --- a/packages/react-components/src/components/cesium-map/layers/layers.rect.stories.tsx +++ b/packages/react-components/src/components/cesium-map/layers/layers.rect.stories.tsx @@ -1,9 +1,9 @@ -import { Rectangle } from 'cesium'; +import { ImageryLayer, Rectangle } from 'cesium'; import React, { useLayoutEffect } from 'react'; import { Story, Meta } from '@storybook/react/types-6-0'; import bbox from '@turf/bbox'; import { BASE_MAPS } from '../helpers/constants'; -import { IRasterLayer, LayerType } from '../layers-manager'; +import { getImageryProviderUrl, IRasterLayer, LayerType } from '../layers-manager'; import { CesiumMap, CesiumMapProps, useCesiumMap } from '../map'; import { CesiumXYZLayer } from './xyz.layer'; @@ -21,6 +21,13 @@ const mapDivStyle = { position: 'absolute' as const, }; +const layerManagerMetaMapping = { + layer: { + id: 'id', + name: 'layerRecord.productName', + }, +}; + const mapViewProps: CesiumMapProps = { center: [34.811, 31.908], zoom: 14, @@ -48,10 +55,22 @@ const optionsRectXYZ = { const childLayerRect = Rectangle.fromDegrees(...bbox(optionsRectXYZ.footprint)); +const layerMetaRectXYZ = { + id: 'xyz-rect-layer', + layerRecord: { + productName: 'XYZ Rect Layer', + }, + options: { ...optionsRectXYZ }, + searchLayerPredicate: (layer: ImageryLayer): boolean => getImageryProviderUrl(layer) === optionsRectXYZ.url, +}; + export const MapWithXYZLayersAndRect: Story = () => (
- - + +
); @@ -72,8 +91,11 @@ export const MapWithSettings: Story = () => { return (
- - + +
); @@ -90,7 +112,7 @@ const LayerViewer: React.FC = (props) => { // For testing the exposure of current zoom level on map viewer setInterval(() => { - console.log('######################### Zoom level: ', mapViewer.currentZoomLevel); + console.log('######################### Zoom level: ', (mapViewer as { currentZoomLevel?: number }).currentZoomLevel); }, 2000); // Mockin footprint data on layer meta diff --git a/packages/react-components/src/components/cesium-map/layers/optimized-tile-requests.stories.tsx b/packages/react-components/src/components/cesium-map/layers/optimized-tile-requests.stories.tsx index a8a094fee..f21987d71 100644 --- a/packages/react-components/src/components/cesium-map/layers/optimized-tile-requests.stories.tsx +++ b/packages/react-components/src/components/cesium-map/layers/optimized-tile-requests.stories.tsx @@ -1,9 +1,9 @@ import React, { ReactNode, useState } from 'react'; import { ImageryLayer, Rectangle } from 'cesium'; -import { get } from 'lodash'; import { Story, Meta } from '@storybook/react'; import bbox from '@turf/bbox'; import { BASE_MAPS } from '../helpers/constants'; +import { getImageryProviderUrl } from '../layers-manager'; import { CesiumMap, CesiumMapProps, IBaseMaps } from '../map'; import { CesiumXYZLayer } from './xyz.layer'; @@ -21,6 +21,13 @@ const mapDivStyle = { position: 'absolute' as const, }; +const layerManagerMetaMapping = { + layer: { + id: 'id', + name: 'layerRecord.productName', + }, +}; + const mapViewProps: CesiumMapProps = { center: [-117.30644008676421, 33.117098433617564], zoom: 14, @@ -82,8 +89,7 @@ const LayersContainer: React.FC = () => { id: 'Transparent Layer', options: { ...optionsXYZTransparency }, searchLayerPredicate: (layer: ImageryLayer): boolean => - get(layer, 'imageryProvider.url') === optionsXYZTransparency.url || - get(layer, 'imageryProvider._url') === optionsXYZTransparency.url, + getImageryProviderUrl(layer) === optionsXYZTransparency.url }} rectangle={Rectangle.fromDegrees(...bbox(optionsXYZTransparency.footprint))} options={optionsXYZTransparency} @@ -102,8 +108,7 @@ const LayersContainer: React.FC = () => { id: 'Opaque Layer', options: { ...optionsXYZOpaque }, searchLayerPredicate: (layer: ImageryLayer): boolean => - get(layer, 'imageryProvider.url') === optionsXYZOpaque.url || - get(layer, 'imageryProvider._url') === optionsXYZOpaque.url, + getImageryProviderUrl(layer) === optionsXYZOpaque.url }} rectangle={Rectangle.fromDegrees(...bbox(optionsXYZOpaque.footprint))} options={optionsXYZOpaque} @@ -122,7 +127,11 @@ const LayersContainer: React.FC = () => { export const OptimizedTileRequestingMap: Story = () => { return (
- +
diff --git a/packages/react-components/src/components/cesium-map/layers/osm.layer.stories.tsx b/packages/react-components/src/components/cesium-map/layers/osm.layer.stories.tsx index 3342bf9ff..a3fc34875 100644 --- a/packages/react-components/src/components/cesium-map/layers/osm.layer.stories.tsx +++ b/packages/react-components/src/components/cesium-map/layers/osm.layer.stories.tsx @@ -1,5 +1,8 @@ import { useState } from 'react'; +import { ImageryLayer } from 'cesium'; import { Story, Meta } from '@storybook/react/types-6-0'; +import { BASE_MAPS } from '../helpers/constants'; +import { getImageryProviderUrl } from '../layers-manager'; import { CesiumMap } from '../map'; import { CesiumSceneMode } from '../proxied.types'; import { CesiumOSMLayer } from './osm.layer'; @@ -19,6 +22,13 @@ const mapDivStyle = { position: 'absolute' as const, }; +const layerManagerMetaMapping = { + layer: { + id: 'id', + name: 'layerRecord.productName', + }, +}; + const optionsOSM = { url: 'https://a.tile.openstreetmap.org/', }; @@ -26,13 +36,37 @@ const optionsXYZ = { url: `https://tiles.openaerialmap.org/5b25fa612b6a08001185f80f/0/5b25fa612b6a08001185f810/{z}/{x}/{y}.png`, }; -export const MapWithOSMLayers: Story = (args) => { +const osmLayerMeta = { + id: 'osm-layer', + layerRecord: { + productName: 'OSM Layer', + }, + options: { ...optionsOSM }, + searchLayerPredicate: (layer: ImageryLayer): boolean => getImageryProviderUrl(layer) === optionsOSM.url, +}; + +const xyzLayerMeta = { + id: 'xyz-layer', + layerRecord: { + productName: 'XYZ Layer', + }, + options: { ...optionsXYZ }, + searchLayerPredicate: (layer: ImageryLayer): boolean => getImageryProviderUrl(layer) === optionsXYZ.url, +}; + +export const MapWithOSMLayers: Story = () => { const [center] = useState<[number, number]>([34.82, 32.04]); return (
- - - + + +
); diff --git a/packages/react-components/src/components/cesium-map/layers/osm.layer.tsx b/packages/react-components/src/components/cesium-map/layers/osm.layer.tsx index 98922452a..c4dd07b19 100644 --- a/packages/react-components/src/components/cesium-map/layers/osm.layer.tsx +++ b/packages/react-components/src/components/cesium-map/layers/osm.layer.tsx @@ -5,7 +5,7 @@ import { CesiumImageryLayer, RCesiumImageryLayerProps } from './imagery.layer'; export interface RCesiumOSMLayerOptions extends OpenStreetMapImageryProvider.ConstructorOptions {} -export interface RCesiumOSMLayerProps extends Partial { +export interface RCesiumOSMLayerProps extends Omit { options: RCesiumOSMLayerOptions; } diff --git a/packages/react-components/src/components/cesium-map/layers/wfs.layer.stories.tsx b/packages/react-components/src/components/cesium-map/layers/wfs.layer.stories.tsx index b64726d7b..dfef4a3bc 100644 --- a/packages/react-components/src/components/cesium-map/layers/wfs.layer.stories.tsx +++ b/packages/react-components/src/components/cesium-map/layers/wfs.layer.stories.tsx @@ -1,4 +1,5 @@ -import { useRef, useState } from 'react'; +import React, { useState, useEffect } from 'react'; +import { TerrainProvider } from 'cesium'; import { BBox } from 'geojson'; import area from '@turf/area'; import intersect from '@turf/intersect'; @@ -9,6 +10,7 @@ import bboxPolygon from '@turf/bbox-polygon'; import { Story, Meta } from '@storybook/react/types-6-0'; import { getValue } from '../../utils/config'; import { BASE_MAPS, DEFAULT_TERRAIN_PROVIDER_URL } from '../helpers/constants'; +import { getLayerIdFromMeta } from '../layers-manager'; import { CesiumMap, CesiumViewer } from '../map'; import { CesiumMath, @@ -35,6 +37,14 @@ import { import { CesiumWFSLayer, ICesiumWFSLayerLabelTextField } from './wfs.layer'; import { Cesium3DTileset } from './3d.tileset'; +const useCesiumTerrainProvider = (url: string): TerrainProvider | undefined => { + const [provider, setProvider] = useState(undefined); + useEffect(() => { + void CesiumCesiumTerrainProvider.fromUrl(url).then(setProvider); + }, [url]); + return provider; +}; + export default { title: 'Cesium Map/Layers/WFSLayer', component: CesiumWFSLayer, @@ -56,14 +66,26 @@ const BRIGHT_GREEN = '#01FF1F'; const LIGHT_BLUE = '#24AEE9'; const BRIGHT_PURPLE = '#B734EB'; +const layerManagerMetaMapping = { + layer: { + id: 'id', + name: 'layerRecord.productName', + footprint: 'layerRecord.footprint', + }, + dataLayer: { + name: 'layerRecord.featureStructure.aliasLayerName', + fields: 'layerRecord.featureStructure.fields', + }, +}; + // #region STORY PP component export const MapWithPPWFSLayer: Story = (args: Record) => { + const terrainProvider = useCesiumTerrainProvider(DEFAULT_TERRAIN_PROVIDER_URL); return (
-
Go to ME
- + ) => { + const terrainProvider = useCesiumTerrainProvider(DEFAULT_TERRAIN_PROVIDER_URL); return (
- - + + {/* */} @@ -136,9 +160,6 @@ MapWithWFSLayer.argTypes = { showDebuggerTool: { defaultValue: SHOW_DEBUGGER_TOOL, }, - terrainProvider: { - defaultValue: new CesiumCesiumTerrainProvider({ url: DEFAULT_TERRAIN_PROVIDER_URL }), - }, }; MapWithWFSLayer.storyName = 'WFS Vector layer'; @@ -146,6 +167,7 @@ MapWithWFSLayer.storyName = 'WFS Vector layer'; // #region STORY VECTOR APP SCENARIO component (NO VISUALIZER) export const MapWithWFSLayerAPPScenario: Story = (args: Record) => { + const terrainProvider = useCesiumTerrainProvider(DEFAULT_TERRAIN_PROVIDER_URL); function MyWFSLayer() { const [show, setShow] = useState(false); return ( @@ -158,24 +180,33 @@ export const MapWithWFSLayerAPPScenario: Story = (args: Record) value={`SHOW WFS LAYER (${show})`} style={{ zIndex: '2', position: 'absolute' }} > - { - show && + {show && ( - } + )} ); } return (
- - + +
@@ -197,9 +228,6 @@ MapWithWFSLayerAPPScenario.argTypes = { showDebuggerTool: { defaultValue: SHOW_DEBUGGER_TOOL, }, - terrainProvider: { - defaultValue: new CesiumCesiumTerrainProvider({ url: DEFAULT_TERRAIN_PROVIDER_URL }), - }, }; MapWithWFSLayerAPPScenario.storyName = 'WFS Vector layer(APP Scenario)'; @@ -207,12 +235,17 @@ MapWithWFSLayerAPPScenario.storyName = 'WFS Vector layer(APP Scenario)'; // #region STORY VECTOR component (CUSTOM VISUALIZER) export const MapWithWFSLayerWithVisualizer: Story = (args: Record) => { + const terrainProvider = useCesiumTerrainProvider(DEFAULT_TERRAIN_PROVIDER_URL); return (
- - + + { const screenPosition = CesiumSceneTransforms.wgs84ToWindowCoordinates(scene, position); - if (!screenPosition) return null; + if (!screenPosition) { + return null; + } const xRight = screenPosition.x + pixelWidth; const yBottom = screenPosition.y + pixelHeight; @@ -861,57 +907,71 @@ const optionsBuildings = { const metaBuildings = { id: '1111111', - keywords: 'buildings, osm', - links: 'buildings,,WFS,http://geoserver-vector', - type: 'RECORD_VECTOR', - classification: '5', - productName: 'מבנים', - description: 'Buildings layer', - srsId: '4326', - srsName: '4326', - producerName: 'Moria', - footprint: {"type":"Polygon","coordinates":[[[-180,-90],[180,-90],[180,90],[-180,90],[-180,-90]]]}, - productType: 'VECTOR_BEST', - featureStructure: { - layerName: 'buildings', - aliasLayerName: 'מבנים', - fields: [ - { - fieldName: 'osm_id', - aliasFieldName: 'מזהה OSM', - type: 'String', - }, - { - fieldName: 'id', - aliasFieldName: 'מזהה', - type: 'String', - }, - { - fieldName: 'building_type', - aliasFieldName: 'סוג', - type: 'String', - }, - { - fieldName: 'sensitivity', - aliasFieldName: 'רגישות', - type: 'String', - }, - { - fieldName: 'entity_id', - aliasFieldName: 'מזהה יישות', - type: 'String', - }, - { - fieldName: 'is_sensitive', - aliasFieldName: 'רגיש', - type: 'Boolean', - }, - { - fieldName: 'date', - aliasFieldName: 'תאריך', - type: 'Date', - }, - ], + layerRecord: { + id: '1111111', + keywords: 'buildings, osm', + links: 'buildings,,WFS,http://geoserver-vector', + type: 'RECORD_VECTOR', + classification: '5', + productName: 'מבנים', + description: 'Buildings layer', + srsId: '4326', + srsName: '4326', + producerName: 'Moria', + footprint: { + type: 'Polygon', + coordinates: [ + [ + [-180, -90], + [180, -90], + [180, 90], + [-180, 90], + [-180, -90], + ], + ], + }, + productType: 'VECTOR_BEST', + featureStructure: { + layerName: 'buildings', + aliasLayerName: 'מבנים', + fields: [ + { + fieldName: 'osm_id', + aliasFieldName: 'מזהה OSM', + type: 'String', + }, + { + fieldName: 'id', + aliasFieldName: 'מזהה', + type: 'String', + }, + { + fieldName: 'building_type', + aliasFieldName: 'סוג', + type: 'String', + }, + { + fieldName: 'sensitivity', + aliasFieldName: 'רגישות', + type: 'String', + }, + { + fieldName: 'entity_id', + aliasFieldName: 'מזהה יישות', + type: 'String', + }, + { + fieldName: 'is_sensitive', + aliasFieldName: 'רגיש', + type: 'Boolean', + }, + { + fieldName: 'date', + aliasFieldName: 'תאריך', + type: 'Date', + }, + ], + }, }, }; diff --git a/packages/react-components/src/components/cesium-map/layers/wfs.layer.tsx b/packages/react-components/src/components/cesium-map/layers/wfs.layer.tsx index 51e990eda..9227743ad 100644 --- a/packages/react-components/src/components/cesium-map/layers/wfs.layer.tsx +++ b/packages/react-components/src/components/cesium-map/layers/wfs.layer.tsx @@ -19,6 +19,7 @@ import pMap from 'p-map'; import { v4 as uuidv4 } from 'uuid'; import booleanValid from '@turf/boolean-valid'; import { distance, center, rectangle2bbox, computeLimitedViewRectangle, defaultVisualizationHandler, rectangle2Feature } from '../helpers/utils'; +import { getDataLayerFields, getLayerIdFromMeta } from '../layers-manager'; import { CesiumViewer, useCesiumMap, useCesiumMapViewstate } from '../map'; export interface ICesiumWFSLayerLabelTextField { @@ -85,9 +86,19 @@ export interface ICesiumWFSLayerOptions { labeling?: ICesiumWFSLayerLabelingOptions; } +export interface ICesiumWFSLayerMeta { + id: string; + items?: number; + total?: number; + cache?: number; + currentZoomLevel?: number; + zoomLevel?: number; + [key: string]: unknown; +} + export interface ICesiumWFSLayer extends React.Attributes { options: ICesiumWFSLayerOptions; - meta: Record; + meta: ICesiumWFSLayerMeta; visualizationHandler?: (mapViewer: CesiumViewer, wfsDataSource: GeoJsonDataSource, processEntityIds: string[], extent?: BBox) => void; withGeometryValidation?: boolean; } @@ -111,6 +122,7 @@ export const CesiumWFSLayer: React.FC = (props) => { const wfsCache = useRef(new Set()); const page = useRef(0); const [metadata, setMetadata] = useState(meta); + const dataLayerId = getLayerIdFromMeta(meta); const geojsonHoveredColor = useMemo(() => CesiumColor.fromCssColorString((hover as string) ?? '#24AEE9').withAlpha(0.5), [hover]); const dataSourceName = useMemo(() => `wfs_${featureType}_${uuidv4()}`, [featureType]); const hasRunFetchRef = useRef(false); @@ -130,27 +142,25 @@ export const CesiumWFSLayer: React.FC = (props) => { const describe = (properties: Record): string => { const rows: string[] = []; - const featureStructure = meta.featureStructure as { fields: { fieldName: string; aliasFieldName: string; type: string }[] }; - if (featureStructure && featureStructure.fields) { - for (const field of featureStructure.fields) { - const { fieldName, aliasFieldName } = field; - const key = aliasFieldName; - const value = properties[fieldName] ?? 'N/A'; - const keyMaxWidth = Math.max(100, Math.min(180, key.length * 10)); - const valueMaxWidth = '260px'; - rows.push(` - - - ${key}: - - - ${value} - - - `); - } + const dataLayerFields = getDataLayerFields(meta); + for (const field of dataLayerFields) { + const { fieldName, aliasFieldName } = field; + const key = aliasFieldName; + const value = properties[fieldName] ?? 'N/A'; + const keyMaxWidth = Math.max(100, Math.min(180, key.length * 10)); + const valueMaxWidth = '260px'; + rows.push(` + + + ${key}: + + + ${value} + + + `); } - const isRightToLeft = featureStructure.fields.some((field) => field.aliasFieldName !== field.fieldName); + const isRightToLeft = dataLayerFields.some((field) => field.aliasFieldName !== field.fieldName); return ` @@ -300,11 +310,20 @@ export const CesiumWFSLayer: React.FC = (props) => { }; const fetchWfsData = async (wfsDataUrl: string, options: RequestInit = { method: 'GET' }): Promise => { - const response = await fetch(wfsDataUrl, options); - if (response.status === 200) { - return await response.json(); + if (!wfsDataUrl) { + throw new Error('WFS request URL is missing'); + } + let response: Response; + try { + response = await fetch(wfsDataUrl, options); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`WFS network request failed for ${wfsDataUrl}: ${message}`); } - return undefined; + if (!response.ok) { + throw new Error(`WFS request failed (${response.status} ${response.statusText}) for ${wfsDataUrl}`); + } + return await response.json(); }; // Create a temporary canvas to measure max width @@ -602,6 +621,11 @@ export const CesiumWFSLayer: React.FC = (props) => { const waitForTilesLoaded = () => { return new Promise((resolve) => { const interval = setInterval(() => { + if (get(mapViewer, '_cesiumWidget') === undefined) { + clearInterval(interval); + resolve(); + return; + } if (mapViewer.scene.globe.tilesLoaded) { clearInterval(interval); resolve(); @@ -617,18 +641,22 @@ export const CesiumWFSLayer: React.FC = (props) => { }; useEffect((): void => { + if (!mapViewer?.scene || !mapViewer?.dataSources) { + return; + } const dataSource = mapViewer.dataSources.getByName(dataSourceName)[0] as GeoJsonDataSource; if (dataSource) { applyVisualization(mapViewer, dataSource, [], undefined); } - }, [mapViewer.scene.mode]); + }, [mapViewer?.scene?.mode]); useEffect(() => { // Happens each time the metadata from STATE changes if ( mapViewer.layersManager && mapViewer.layersManager.dataLayerList.length > 0 && - mapViewer.layersManager.findDataLayerById(meta.id as string) !== undefined + dataLayerId !== undefined && + mapViewer.layersManager.findDataLayerById(dataLayerId) !== undefined ) { mapViewer.layersManager.addMetaToDataLayer(metadata); } @@ -640,6 +668,9 @@ export const CesiumWFSLayer: React.FC = (props) => { }, [mapViewer.layersManager]); useEffect(() => { + if (!mapViewer?.scene || !mapViewer?.dataSources) { + return; + } // DataSource mapViewer.dataSources.add(wfsDataSource); @@ -666,7 +697,9 @@ export const CesiumWFSLayer: React.FC = (props) => { fetchMetadata.current.clear(); mapViewer.dataSources.remove(mapViewer.dataSources.getByName(`${labeling?.dataSourcePrefix}${wfsDataSource.name}`)[0]); mapViewer.dataSources.remove(wfsDataSource, true); - mapViewer.layersManager?.removeDataLayer(meta.id as string); + if (dataLayerId !== undefined) { + mapViewer.layersManager?.removeDataLayer(dataLayerId); + } mapViewer.scene.camera.moveEnd.removeEventListener(fetchHandler); handler.removeInputAction(ScreenSpaceEventType.MOUSE_MOVE); } diff --git a/packages/react-components/src/components/cesium-map/layers/wms.layer.stories.tsx b/packages/react-components/src/components/cesium-map/layers/wms.layer.stories.tsx index 7e50d5b36..2eebe00c8 100644 --- a/packages/react-components/src/components/cesium-map/layers/wms.layer.stories.tsx +++ b/packages/react-components/src/components/cesium-map/layers/wms.layer.stories.tsx @@ -1,5 +1,7 @@ -import React from 'react'; import { Story, Meta } from '@storybook/react/types-6-0'; +import { ImageryLayer } from 'cesium'; +import { BASE_MAPS } from '../helpers/constants'; +import { getImageryProvider } from '../layers-manager'; import { CesiumMap } from '../map'; import { CesiumWMSLayer } from './wms.layer'; @@ -17,6 +19,13 @@ const mapDivStyle = { position: 'absolute' as const, }; +const layerManagerMetaMapping = { + layer: { + id: 'id', + name: 'layerRecord.productName', + }, +}; + const optionsWMS = { url: 'https://ahocevar.com/geoserver/wms', layers: 'ne:NE1_HR_LC_SR_W_DR', @@ -27,11 +36,38 @@ const optionsWMS2 = { layers: 'opengeo:countries', }; +const wmsLayerMeta = { + id: 'wms-layer', + layerRecord: { + productName: 'WMS Layer', + }, + options: { ...optionsWMS }, + searchLayerPredicate: (layer: ImageryLayer): boolean => { + const provider = getImageryProvider(layer) as { layers?: string }; + return provider.layers === optionsWMS.layers; + }, +}; + +const wmsLayerMeta2 = { + id: 'wms-layer-2', + layerRecord: { + productName: 'WMS Layer 2', + }, + options: { ...optionsWMS2 }, + searchLayerPredicate: (layer: ImageryLayer): boolean => { + const provider = getImageryProvider(layer) as { layers?: string }; + return provider.layers === optionsWMS2.layers; + }, +}; + export const MapWithWMSLayers: Story = () => (
- - - + + +
); diff --git a/packages/react-components/src/components/cesium-map/layers/wms.layer.tsx b/packages/react-components/src/components/cesium-map/layers/wms.layer.tsx index 7d35a5f9f..4899d60c7 100644 --- a/packages/react-components/src/components/cesium-map/layers/wms.layer.tsx +++ b/packages/react-components/src/components/cesium-map/layers/wms.layer.tsx @@ -6,7 +6,7 @@ import { CesiumImageryLayer, RCesiumImageryLayerProps } from './imagery.layer'; export interface RCesiumWMSLayerOptions extends WebMapServiceImageryProvider.ConstructorOptions {} -export interface RCesiumWMSLayerProps extends Partial { +export interface RCesiumWMSLayerProps extends Omit { options: RCesiumWMSLayerOptions; } diff --git a/packages/react-components/src/components/cesium-map/layers/wmts.layer.stories.tsx b/packages/react-components/src/components/cesium-map/layers/wmts.layer.stories.tsx index be69d27b1..9d046c97c 100644 --- a/packages/react-components/src/components/cesium-map/layers/wmts.layer.stories.tsx +++ b/packages/react-components/src/components/cesium-map/layers/wmts.layer.stories.tsx @@ -1,6 +1,8 @@ -import React from 'react'; import { Story, Meta } from '@storybook/react/types-6-0'; import { Credit } from 'cesium'; +import { ImageryLayer } from 'cesium'; +import { BASE_MAPS } from '../helpers/constants'; +import { getImageryProviderUrl } from '../layers-manager'; import { CesiumMap } from '../map'; import { CesiumWMTSLayer } from './wmts.layer'; @@ -18,6 +20,13 @@ const mapDivStyle = { position: 'absolute' as const, }; +const layerManagerMetaMapping = { + layer: { + id: 'id', + name: 'layerRecord.productName', + }, +}; + const optionsWMTS = { url: 'http://basemap.nationalmap.gov/arcgis/rest/services/USGSShadedReliefOnly/MapServer/WMTS', layer: 'USGSShadedReliefOnly', @@ -40,11 +49,32 @@ const optionsWMTS2 = { credit: new Credit('U. S. Geological Survey'), }; +const wmtsLayerMeta = { + id: 'wmts-layer', + layerRecord: { + productName: 'WMTS Layer', + }, + options: { ...optionsWMTS }, + searchLayerPredicate: (layer: ImageryLayer): boolean => getImageryProviderUrl(layer) === optionsWMTS.url, +}; + +const wmtsLayerMeta2 = { + id: 'wmts-layer-2', + layerRecord: { + productName: 'WMTS Layer 2', + }, + options: { ...optionsWMTS2 }, + searchLayerPredicate: (layer: ImageryLayer): boolean => getImageryProviderUrl(layer) === optionsWMTS2.url, +}; + export const MapWithWMTSLayers: Story = () => (
- - - + + +
); diff --git a/packages/react-components/src/components/cesium-map/layers/wmts.layer.tsx b/packages/react-components/src/components/cesium-map/layers/wmts.layer.tsx index e7168d169..0ea32a3dd 100644 --- a/packages/react-components/src/components/cesium-map/layers/wmts.layer.tsx +++ b/packages/react-components/src/components/cesium-map/layers/wmts.layer.tsx @@ -6,7 +6,7 @@ import { CesiumImageryLayer, RCesiumImageryLayerProps } from './imagery.layer'; export interface RCesiumWMTSLayerOptions extends WebMapTileServiceImageryProvider.ConstructorOptions {} -export interface RCesiumWMTSLayerProps extends Partial { +export interface RCesiumWMTSLayerProps extends Omit { options: RCesiumWMTSLayerOptions; } diff --git a/packages/react-components/src/components/cesium-map/layers/xyz.layer.stories.tsx b/packages/react-components/src/components/cesium-map/layers/xyz.layer.stories.tsx index 564c758ab..5535f15ca 100644 --- a/packages/react-components/src/components/cesium-map/layers/xyz.layer.stories.tsx +++ b/packages/react-components/src/components/cesium-map/layers/xyz.layer.stories.tsx @@ -1,5 +1,7 @@ -import React from 'react'; import { Story, Meta } from '@storybook/react/types-6-0'; +import { ImageryLayer } from 'cesium'; +import { BASE_MAPS } from '../helpers/constants'; +import { getImageryProviderUrl } from '../layers-manager'; import { CesiumMap } from '../map'; import { CesiumXYZLayer } from './xyz.layer'; @@ -17,6 +19,13 @@ const mapDivStyle = { position: 'absolute' as const, }; +const layerManagerMetaMapping = { + layer: { + id: 'id', + name: 'layerRecord.productName', + }, +}; + const optionsXYZ = { url: 'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', }; @@ -25,11 +34,32 @@ const optionsXYZ2 = { url: 'https://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=0e6fc415256d4fbb9b5166a718591d71', }; +const xyzLayerMeta = { + id: 'xyz-layer', + layerRecord: { + productName: 'XYZ Layer', + }, + options: { ...optionsXYZ }, + searchLayerPredicate: (layer: ImageryLayer): boolean => getImageryProviderUrl(layer) === optionsXYZ.url, +}; + +const xyzLayerMeta2 = { + id: 'xyz-layer-2', + layerRecord: { + productName: 'XYZ Layer 2', + }, + options: { ...optionsXYZ2 }, + searchLayerPredicate: (layer: ImageryLayer): boolean => getImageryProviderUrl(layer) === optionsXYZ2.url, +}; + export const MapWithXYZLayers: Story = () => (
- - - + + +
); diff --git a/packages/react-components/src/components/cesium-map/layers/xyz.layer.tsx b/packages/react-components/src/components/cesium-map/layers/xyz.layer.tsx index 6c4cdfdea..c90cec724 100644 --- a/packages/react-components/src/components/cesium-map/layers/xyz.layer.tsx +++ b/packages/react-components/src/components/cesium-map/layers/xyz.layer.tsx @@ -6,7 +6,7 @@ import { CesiumImageryLayer, RCesiumImageryLayerProps } from './imagery.layer'; export interface RCesiumXYZLayerOptions extends UrlTemplateImageryProvider.ConstructorOptions {} -export interface RCesiumXYZLayerProps extends Partial { +export interface RCesiumXYZLayerProps extends Omit { options: UrlTemplateImageryProvider.ConstructorOptions; } diff --git a/packages/react-components/src/components/cesium-map/legend/legends-sidebar.stories.tsx b/packages/react-components/src/components/cesium-map/legend/legends-sidebar.stories.tsx index f256b88c3..62de0902a 100644 --- a/packages/react-components/src/components/cesium-map/legend/legends-sidebar.stories.tsx +++ b/packages/react-components/src/components/cesium-map/legend/legends-sidebar.stories.tsx @@ -1,9 +1,10 @@ import { useState } from 'react'; +import { ImageryLayer } from 'cesium'; import { Story, Meta } from '@storybook/react/types-6-0'; import { BASE_MAPS } from '../helpers/constants'; +import { getImageryProviderUrl } from '../layers-manager'; import { CesiumXYZLayer } from '../layers/xyz.layer'; import { CesiumMap } from '../map'; -import { CesiumSceneMode } from '../proxied.types'; export default { title: 'Cesium Map', @@ -19,10 +20,26 @@ const mapDivStyle = { position: 'absolute' as const, }; +const layerManagerMetaMapping = { + layer: { + id: 'id', + name: 'layerRecord.productName', + }, +}; + const optionsXYZSanDiego = { url: 'https://tiles.openaerialmap.org/5d73614588556200055f10d6/0/5d73614588556200055f10d7/{z}/{x}/{y}', }; +const layerMetaSanDiego = { + id: 'layer-san-diego', + layerRecord: { + productName: 'San Diego Layer', + }, + options: { ...optionsXYZSanDiego }, + searchLayerPredicate: (layer: ImageryLayer): boolean => getImageryProviderUrl(layer) === optionsXYZSanDiego.url, +}; + export const MapWithLegends: Story = () => { const [center] = useState<[number, number]>([-117.30644008676421, 33.117098433617564]); //Sandiego Poinsettia Park return ( @@ -30,8 +47,6 @@ export const MapWithLegends: Story = () => { { title: 'Map Legends', emptyText: 'No legends for this basemap', }} + layerManagerMetaMapping={layerManagerMetaMapping} > - + ); diff --git a/packages/react-components/src/components/cesium-map/map.stories.tsx b/packages/react-components/src/components/cesium-map/map.stories.tsx index ec8b69abd..90e6e727f 100644 --- a/packages/react-components/src/components/cesium-map/map.stories.tsx +++ b/packages/react-components/src/components/cesium-map/map.stories.tsx @@ -1,13 +1,53 @@ import { Feature } from 'geojson'; +import React, { useState, useEffect } from 'react'; import { Story, Meta } from '@storybook/react/types-6-0'; import { ThemeProvider } from '@map-colonies/react-core'; import { getValue } from '../utils/config'; import { Proj } from '../utils/projections'; import { GeocoderOptions } from './geocoder/geocoder-panel'; import { BASE_MAPS, DEFAULT_TERRAIN_PROVIDER_URL, TERRAIN_COMBINED, TERRAIN_SRTM100 } from './helpers/constants'; -import { CesiumMap, CesiumMapProps } from './map'; +import { CesiumMap, CesiumMapProps, ITerrain } from './map'; import { CesiumCesiumTerrainProvider, CesiumSceneMode } from './proxied.types'; +const useTerrains = (): ITerrain[] | undefined => { + const [terrains, setTerrains] = useState(undefined); + useEffect(() => { + void Promise.all([ + CesiumCesiumTerrainProvider.fromUrl(DEFAULT_TERRAIN_PROVIDER_URL), + CesiumCesiumTerrainProvider.fromUrl(TERRAIN_SRTM100), + CesiumCesiumTerrainProvider.fromUrl(TERRAIN_COMBINED), + ]).then(([p1, p2, p3]) => { + setTerrains([ + { + id: '1', + url: DEFAULT_TERRAIN_PROVIDER_URL, + title: 'Default Terrain', + thumbnail: 'Cesium/Widgets/Images/TerrainProviders/Ellipsoid.png', + isCurrent: true, + terrainProvider: p1, + }, + { + id: '2', + url: TERRAIN_SRTM100, + title: 'srtm100', + thumbnail: 'Cesium/Widgets/Images/TerrainProviders/Ellipsoid.png', + isCurrent: false, + terrainProvider: p2, + }, + { + id: '3', + url: TERRAIN_COMBINED, + title: 'combined_srtm_30_100_il_ever', + thumbnail: 'Cesium/Widgets/Images/TerrainProviders/Ellipsoid.png', + isCurrent: false, + terrainProvider: p3, + }, + ]); + }); + }, []); + return terrains; +}; + export default { title: 'Cesium Map', component: CesiumMap, @@ -22,14 +62,23 @@ const mapDivStyle = { position: 'absolute' as const, }; +const layerManagerMetaMapping = { + layer: { + id: 'id', + name: 'layerRecord.productName', + }, +}; + const triggerCallbackFunc = (data: Feature, options: GeocoderOptions, i: number) => { const baseUrl = getValue('GEOCODER', 'CALLBACK_URL'); const properties = data.properties; const requestId = properties?.headers['request_id']; - if(!requestId) { - console.warn('GEOCODING[FEEDBACK]: Missing request_id in response header. Ensure the "Access-Control-Expose-Headers" header includes "request_id".'); + if (!requestId) { + console.warn( + 'GEOCODING[FEEDBACK]: Missing request_id in response header. Ensure the "Access-Control-Expose-Headers" header includes "request_id".' + ); } if (!baseUrl || !properties) return; @@ -37,14 +86,14 @@ const triggerCallbackFunc = (data: Feature, options: GeocoderOptions, i: number) const body = { request_id: requestId, chosen_result_id: i, - user_id: 'catalog-app@mapcolonies.net' - } + user_id: 'catalog-app@mapcolonies.net', + }; const url = `${baseUrl}?token=${getValue('GLOBAL', 'TOKEN')}`; fetch(url, { method: 'POST', - body: JSON.stringify(body) + body: JSON.stringify(body), }); }; @@ -58,7 +107,7 @@ const GEOCODER_OPTIONS = [ queryText: 'query', geoContext: { name: 'geo_context', - relatedParams: [['geo_context_mode', 'filter']], + relatedParams: [['geo_context_mode', 'filter']], }, }, static: [ @@ -68,7 +117,9 @@ const GEOCODER_OPTIONS = [ ], }, title: 'Location', - callbackFunc: (data, options, i) => { triggerCallbackFunc(data, options, i) } + callbackFunc: (data, options, i) => { + triggerCallbackFunc(data, options, i); + }, }, { baseUrl: getValue('GEOCODER', 'URL'), @@ -89,7 +140,9 @@ const GEOCODER_OPTIONS = [ ], }, title: 'Control Tiles', - callbackFunc: (data, options, i) => { triggerCallbackFunc(data, options, i) } + callbackFunc: (data, options, i) => { + triggerCallbackFunc(data, options, i); + }, }, { baseUrl: getValue('GEOCODER', 'URL'), @@ -110,7 +163,9 @@ const GEOCODER_OPTIONS = [ ], }, title: 'Control Data', - callbackFunc: (data, options, i) => { triggerCallbackFunc(data, options, i) } + callbackFunc: (data, options, i) => { + triggerCallbackFunc(data, options, i); + }, }, { baseUrl: getValue('GEOCODER', 'URL'), @@ -124,13 +179,13 @@ const GEOCODER_OPTIONS = [ relatedParams: [['geo_context_mode', 'filter']], }, }, - static: [ - ['token', getValue('GLOBAL', 'TOKEN')], - ] + static: [['token', getValue('GLOBAL', 'TOKEN')]], // "geo_context": { "bbox": [-180, -90, 180, 90] }, }, title: 'Control Routes', - callbackFunc: (data, options, i) => { triggerCallbackFunc(data, options, i) } + callbackFunc: (data, options, i) => { + triggerCallbackFunc(data, options, i); + }, }, ] satisfies GeocoderOptions[]; @@ -154,7 +209,9 @@ const LOCALIZED_GEOCODER_OPTIONS = [ ], }, title: 'מיקום', - callbackFunc: (data, options, i) => { triggerCallbackFunc(data, options, i) } + callbackFunc: (data, options, i) => { + triggerCallbackFunc(data, options, i); + }, }, { baseUrl: getValue('GEOCODER', 'URL'), @@ -175,7 +232,9 @@ const LOCALIZED_GEOCODER_OPTIONS = [ ], }, title: 'אריחי שליטה (נצ"א)', - callbackFunc: (data, options, i) => { triggerCallbackFunc(data, options, i) } + callbackFunc: (data, options, i) => { + triggerCallbackFunc(data, options, i); + }, }, { baseUrl: getValue('GEOCODER', 'URL'), @@ -196,7 +255,9 @@ const LOCALIZED_GEOCODER_OPTIONS = [ ], }, title: 'נתוני שליטה', - callbackFunc: (data, options, i) => { triggerCallbackFunc(data, options, i) } + callbackFunc: (data, options, i) => { + triggerCallbackFunc(data, options, i); + }, }, { baseUrl: getValue('GEOCODER', 'URL'), @@ -210,55 +271,34 @@ const LOCALIZED_GEOCODER_OPTIONS = [ relatedParams: [['geo_context_mode', 'filter']], }, }, - static: [ - ['token', getValue('GLOBAL', 'TOKEN')], - ] + static: [['token', getValue('GLOBAL', 'TOKEN')]], // "geo_context": { "bbox": [-180, -90, 180, 90] }, }, title: 'צירי שליטה', - callbackFunc: (data, options, i) => { triggerCallbackFunc(data, options, i) } + callbackFunc: (data, options, i) => { + triggerCallbackFunc(data, options, i); + }, }, ] satisfies GeocoderOptions[]; -export const BaseMap: Story = (args: CesiumMapProps) => ( -
- -
-); +export const BaseMap: Story = (args: CesiumMapProps) => { + const terrains = useTerrains(); + return ( +
+ +
+ ); +}; BaseMap.argTypes = { baseMaps: { defaultValue: BASE_MAPS, }, - terrains: { - defaultValue: [{ - id: '1', - url: DEFAULT_TERRAIN_PROVIDER_URL, - title: 'Default Terrain', - thumbnail: 'Cesium/Widgets/Images/TerrainProviders/Ellipsoid.png', - isCurrent: true, - terrainProvider: new CesiumCesiumTerrainProvider({ url: DEFAULT_TERRAIN_PROVIDER_URL }) - },{ - id: '2', - url: TERRAIN_SRTM100, - title: 'srtm100', - thumbnail: 'Cesium/Widgets/Images/TerrainProviders/Ellipsoid.png', - isCurrent: false, - terrainProvider: new CesiumCesiumTerrainProvider({ url: TERRAIN_SRTM100 }) - },{ - id: '3', - url: TERRAIN_COMBINED, - title: 'combined_srtm_30_100_il_ever', - thumbnail: 'Cesium/Widgets/Images/TerrainProviders/Ellipsoid.png', - isCurrent: false, - terrainProvider: new CesiumCesiumTerrainProvider({ url: TERRAIN_COMBINED }) - }], - }, }; export const ZoomedMap: Story = (args: CesiumMapProps) => (
- +
); @@ -289,7 +329,7 @@ const cesiumTheme = { export const GeocoderPanel: Story = (args: CesiumMapProps) => (
- +
); @@ -324,7 +364,7 @@ GeocoderPanel.storyName = 'Geocoder'; export const MapWithProjection: Story = (args: CesiumMapProps) => (
- +
); @@ -354,7 +394,7 @@ MapWithProjection.argTypes = { export const Map2DWithProjection: Story = (args: CesiumMapProps) => (
- +
); @@ -388,7 +428,7 @@ Map2DWithProjection.storyName = '2D Map With Projection'; export const LocalizedMap: Story = (args: CesiumMapProps) => (
- +
); @@ -412,7 +452,7 @@ LocalizedMap.argTypes = { CESIUM_INSPECTOR_CHECKBOX: 'כלי בדיקה של סזיום', WITH_TRANSPARENCY_TOOLTIP: 'שכבה זו מכילה אריחים עם שקיפות', WITHOUT_TRANSPARENCY_TOOLTIP: 'שכבה זו מכילה אריחים ללא שקיפות', - SHOW_FEATURE_ON_MAP: "הראה על המפה", + SHOW_FEATURE_ON_MAP: 'הראה על המפה', IN_MAP_EXTENT: 'חיפוש בתיחום נוכחי', SEARCH_PLACEHOLDER: 'חיפוש...', NO_RESULTS: 'אין תוצאות', @@ -422,7 +462,9 @@ LocalizedMap.argTypes = { NO_DATA_LAYERS: 'לא נמצאו שכבות מידע בתצוגה', ACTIVE_LAYERS_TITLE: 'שכבות פעילות', IMAGERY: 'ראסטר', + SERVICE: 'שירות', DATA: 'מידע', + '3D': 'תלת-מימד', FLY_TO: 'הצג מיקום', REMOVE: 'הסר', BASE_MAP_TITLE: 'מפות בסיס', diff --git a/packages/react-components/src/components/cesium-map/map.tsx b/packages/react-components/src/components/cesium-map/map.tsx index 9c483ec9d..a1a323104 100644 --- a/packages/react-components/src/components/cesium-map/map.tsx +++ b/packages/react-components/src/components/cesium-map/map.tsx @@ -26,7 +26,7 @@ import { GeocoderOptions } from './geocoder/geocoder-panel'; import { GeocoderWidget } from './geocoder/geocoder-widget'; import { DEFAULT_TERRAIN_PROVIDER_URL } from './helpers/constants'; import { pointToLonLat } from './helpers/geojson/point.geojson'; -import LayerManager, { IRasterLayer, LegendExtractor } from './layers-manager'; +import LayerManager, { IRasterLayer, LegendExtractor, type ILayerManagerMetaMapping } from './layers-manager'; import { LegendWidget, IMapLegend, LegendSidebar } from './legend'; import { CesiumCompassTool } from './tools/cesium-compass.tool'; import { CoordinatesTrackerTool } from './tools/coordinates-tracker.tool'; @@ -134,6 +134,7 @@ interface ILegends { } export interface CesiumMapProps extends ViewerProps { + layerManagerMetaMapping: ILayerManagerMetaMapping; showMousePosition?: boolean; showZoomLevel?: boolean; showScale?: boolean; @@ -156,7 +157,6 @@ export interface CesiumMapProps extends ViewerProps { dynamicHeightIncrement?: number; }; legends?: ILegends; - layerManagerFootprintMetaFieldPath?: string; geocoderPanel?: GeocoderOptions[]; } @@ -292,15 +292,17 @@ export const CesiumMap: React.FC = (props) => { if (!mapViewRef) return null; if (!mapViewRef.layersManager) { - mapViewRef.layersManager = new LayerManager( - mapViewRef, - props.legends?.mapLegendsExtractor, - () => { - setLegendsList(mapViewRef.layersManager?.legendsList as IMapLegend[]); - }, - props.layerManagerFootprintMetaFieldPath, - viewState?.shouldOptimizedTileRequests - ); + Object.assign(mapViewRef, { + layersManager: new LayerManager( + mapViewRef, + props.layerManagerMetaMapping, + props.legends?.mapLegendsExtractor, + () => { + setLegendsList(mapViewRef.layersManager?.legendsList as IMapLegend[]); + }, + viewState?.shouldOptimizedTileRequests + ), + }); } return { @@ -308,7 +310,7 @@ export const CesiumMap: React.FC = (props) => { viewState, setViewState, }; - }, [mapViewRef, props.legends, props.layerManagerFootprintMetaFieldPath, viewState]); + }, [props.legends, props.layerManagerMetaMapping, mapViewRef, viewState]); useEffect(() => { setBaseMaps(props.baseMaps); diff --git a/packages/react-components/src/components/cesium-map/proxied.types.ts b/packages/react-components/src/components/cesium-map/proxied.types.ts index 1a2857c5c..601a3bea5 100644 --- a/packages/react-components/src/components/cesium-map/proxied.types.ts +++ b/packages/react-components/src/components/cesium-map/proxied.types.ts @@ -5,6 +5,7 @@ import { Cartesian3, Cartographic, CesiumTerrainProvider, + Color, ConstantPositionProperty, ConstantProperty, Ellipsoid, @@ -12,18 +13,18 @@ import { GeographicTilingScheme, HeightReference, HorizontalOrigin, + ImageryProvider, JulianDate, LabelStyle, + PolygonHierarchy, PolylineDashMaterialProperty, PolylineGraphics, PositionProperty, Rectangle, Resource, - VerticalOrigin, - SceneMode, Scene, - Color, - PolygonHierarchy, + SceneMode, + VerticalOrigin, } from 'cesium'; // PROXIED CLASSES @@ -67,6 +68,8 @@ export class CesiumPolygonHierarchy extends PolygonHierarchy {} export class CesiumScene extends Scene {} +export class CesiumImageryProvider extends ImageryProvider {} + // PROXIED ENUMS // eslint-disable-next-line @typescript-eslint/naming-convention export const CesiumVerticalOrigin = VerticalOrigin; diff --git a/packages/react-components/src/components/cesium-map/terrain-providers/terrain-provider-heights-tool.stories.tsx b/packages/react-components/src/components/cesium-map/terrain-providers/terrain-provider-heights-tool.stories.tsx index 6454cd914..431737413 100644 --- a/packages/react-components/src/components/cesium-map/terrain-providers/terrain-provider-heights-tool.stories.tsx +++ b/packages/react-components/src/components/cesium-map/terrain-providers/terrain-provider-heights-tool.stories.tsx @@ -11,7 +11,6 @@ import { getValue } from '../../utils/config'; import { BASE_MAPS } from '../helpers/constants'; import { Cesium3DTileset } from '../layers'; import { CesiumMap, CesiumViewer, useCesiumMap } from '../map'; -import { InspectorTool } from '../tools/inspector.tool'; import { TerrainianHeightTool } from '../tools/terranian-height.tool'; export default { @@ -28,47 +27,24 @@ const mapDivStyle = { position: 'absolute' as const, }; -const EllipsoidProvider = new EllipsoidTerrainProvider({}); +const terrainControlsStyle = { + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'flex-start', +}; -//#region TILER MATERIALS -// const TTCesiumProviderSrtm30 = new CesiumTerrainProvider({ -// url: new Resource({ -// url: 'http://localhost:8002/srtm30', -// }), -// }); -// const TTCesiumProviderSrtm100 = new CesiumTerrainProvider({ -// url: new Resource({ -// url: 'http://localhost:8002/srtm100', -// }), -// }); -// const TTCesiumProviderMergedDescending = new CesiumTerrainProvider({ -// url: new Resource({ -// url: 'http://localhost:8002/mergedDescending', -// }), -// }); -//#endregion +const layerManagerMetaMapping = { + layer: { + id: 'id', + name: 'layerRecord.productName', + }, +}; -//#region CTBD MATERIALS -// const CTBDCesiumProviderSrtm30 = new CesiumTerrainProvider({ -// url: new Resource({ -// url: 'http://localhost:3000/srtm30', -// }), -// }); -// const CTBDCesiumProviderSrtm100 = new CesiumTerrainProvider({ -// url: new Resource({ -// url: 'http://localhost:3000/srtm100', -// }), -// }); -// const CTBDCesiumProviderMergedAscending = new CesiumTerrainProvider({ -// url: new Resource({ -// url: 'http://localhost:3000/mergedAscending', -// }), -// }); -//#endregion +const EllipsoidProvider = new EllipsoidTerrainProvider({}); -const ArcGisProvider = new ArcGISTiledElevationTerrainProvider({ - url: 'https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer', -}); +const ArcGisProvider = ArcGISTiledElevationTerrainProvider.fromUrl( + 'https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer' +); const terrainProviderListQmesh = [ { @@ -83,7 +59,7 @@ const terrainProviderListQmesh = [ interface ITerrainProviderItem { id: string; - value: TerrainProvider | undefined; + value: TerrainProvider | Promise | undefined; } interface ITerrainProviderSelectorProps { @@ -94,19 +70,36 @@ const TerrainProviderSelector: React.FC = ({ terr const mapViewer: CesiumViewer = useCesiumMap(); return ( - <> +
- +
); }; @@ -117,13 +110,15 @@ export const QuantizedMeshHeightsTool: Story = () => { - - - - + +
+ + +
); diff --git a/packages/react-components/src/components/cesium-map/terrain-providers/terrain-provider.stories.tsx b/packages/react-components/src/components/cesium-map/terrain-providers/terrain-provider.stories.tsx index 021bef3e1..1f4a3f312 100644 --- a/packages/react-components/src/components/cesium-map/terrain-providers/terrain-provider.stories.tsx +++ b/packages/react-components/src/components/cesium-map/terrain-providers/terrain-provider.stories.tsx @@ -14,7 +14,6 @@ import { getValue } from '../../utils/config'; import { BASE_MAPS } from '../helpers/constants'; import { Cesium3DTileset } from '../layers'; import { CesiumMap, useCesiumMap } from '../map'; -import { InspectorTool } from '../tools/inspector.tool'; import QuantizedMeshTerrainProvider from './custom/quantized-mesh-terrain-provider'; export default { @@ -31,24 +30,45 @@ const mapDivStyle = { position: 'absolute' as const, }; -const EllipsoidProvider = new EllipsoidTerrainProvider({}); +const layerManagerMetaMapping = { + layer: { + id: 'id', + name: 'layerRecord.productName', + }, +}; -const CesiumProvider = new CesiumTerrainProvider({ - url: new Resource({ - url: 'https://my-assets.cesium.com/1', - headers: { - authorization: 'Bearer ', - }, - }), -}); +const EllipsoidProvider = new EllipsoidTerrainProvider({}); -const VRTheWorldProvider = new VRTheWorldTerrainProvider({ - url: 'http://www.vr-theworld.com/vr-theworld/tiles1.0.0/73/', -}); +const withFallbackProvider = (providerPromise: Promise, providerName: string): Promise => { + return providerPromise.catch((error) => { + console.warn(`[TerrainProvider] Failed to initialize ${providerName}. Falling back to Ellipsoid terrain.`, error); + return EllipsoidProvider; + }); +}; -const ArcGisProvider = new ArcGISTiledElevationTerrainProvider({ - url: 'https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer', -}); +const CesiumProvider = withFallbackProvider( + CesiumTerrainProvider.fromUrl( + new Resource({ + url: 'https://my-assets.cesium.com/1', + headers: { + authorization: 'Bearer ', + }, + }) + ), + 'Cesium Terrain Provider' +); + +const VRTheWorldProvider = withFallbackProvider( + VRTheWorldTerrainProvider.fromUrl('https://www.vr-theworld.com/vr-theworld/tiles1.0.0/73/'), + 'VR The World Terrain Provider' +); + +const ArcGisProvider = withFallbackProvider( + ArcGISTiledElevationTerrainProvider.fromUrl( + 'https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer' + ), + 'ArcGIS Terrain Provider' +); const QuantizedMeshProvider = new QuantizedMeshTerrainProvider({ getUrl: (x: number, y: number, level: number): string => { @@ -86,7 +106,7 @@ const terrainProviderList = [ interface ITerrainProviderItem { id: string; - value: TerrainProvider | undefined; + value: TerrainProvider | QuantizedMeshTerrainProvider | Promise | undefined; } interface ITerrainProviderSelectorProps { @@ -105,22 +125,35 @@ const TerrainProviderSelector: React.FC = ({ terr }; return ( - <> +
+
+ + +
-
- - - +
); }; @@ -132,13 +165,13 @@ export const QuantizedMeshProviders: Story = () => { - + - ); diff --git a/packages/react-components/src/components/cesium-map/tools/inspector.tool.tsx b/packages/react-components/src/components/cesium-map/tools/inspector.tool.tsx index 2df9c79c8..c1986f000 100644 --- a/packages/react-components/src/components/cesium-map/tools/inspector.tool.tsx +++ b/packages/react-components/src/components/cesium-map/tools/inspector.tool.tsx @@ -1,7 +1,8 @@ import React, { useEffect } from 'react'; -import { viewerCesiumInspectorMixin } from 'cesium'; +import { viewerCesiumInspectorMixin, TileCoordinatesImageryProvider } from 'cesium'; import { Box } from '../../box'; import { CesiumViewer, useCesiumMap } from '../map'; +import { getImageryProvider, getImageryProviderName } from '../layers-manager'; interface ICesiumInspectorInstance { container?: HTMLElement; @@ -19,6 +20,21 @@ const applyInspectorContainerStyles = (container: HTMLElement): void => { container.style.position = 'relative'; }; +const keepTileCoordinatesLayerOnTop = (viewer: CesiumViewer): void => { + const layerList = viewer.layersManager?.layerList; + const tileCoordinatesLayer = layerList?.find((layer) => { + const provider = getImageryProvider(layer); + return provider instanceof TileCoordinatesImageryProvider || getImageryProviderName(provider) === 'TileCoordinatesImageryProvider'; + }); + if (tileCoordinatesLayer === undefined) { + return; + } + const topLayer = layerList?.[layerList.length - 1]; + if (topLayer !== tileCoordinatesLayer) { + viewer.imageryLayers.raiseToTop(tileCoordinatesLayer); + } +}; + export const InspectorTool: React.FC = () => { const mapViewer: CesiumViewer = useCesiumMap(); @@ -40,7 +56,20 @@ export const InspectorTool: React.FC = () => { applyInspectorContainerStyles(inspectorContainer); } + const refreshTileCoordinatesOrder = (): void => { + keepTileCoordinatesLayerOnTop(mapViewer); + }; + + const removeLayerAdded = mapViewer.imageryLayers.layerAdded.addEventListener(refreshTileCoordinatesOrder); + const removeLayerMoved = mapViewer.imageryLayers.layerMoved.addEventListener(refreshTileCoordinatesOrder); + const removeLayerRemoved = mapViewer.imageryLayers.layerRemoved.addEventListener(refreshTileCoordinatesOrder); + + setTimeout(refreshTileCoordinatesOrder, 0); + return () => { + removeLayerAdded(); + removeLayerMoved(); + removeLayerRemoved(); if (inspectorContainer) { inspectorContainer.style.display = 'none'; } diff --git a/packages/react-components/src/components/index.ts b/packages/react-components/src/components/index.ts index f7ebccc20..f1952bf18 100644 --- a/packages/react-components/src/components/index.ts +++ b/packages/react-components/src/components/index.ts @@ -1,7 +1,7 @@ export * from './date-range-picker'; export * from './date-range-picker-with-presets'; export * from './date-picker'; -export * from './map-filter-container'; +export * from './ol-map/filter'; export * from './models'; export * from './ol-map'; export * from './smart-table'; diff --git a/packages/react-components/src/components/map-filter-container/container-map.css b/packages/react-components/src/components/ol-map/filter/container-map.css similarity index 100% rename from packages/react-components/src/components/map-filter-container/container-map.css rename to packages/react-components/src/components/ol-map/filter/container-map.css diff --git a/packages/react-components/src/components/map-filter-container/container-map.tsx b/packages/react-components/src/components/ol-map/filter/container-map.tsx similarity index 71% rename from packages/react-components/src/components/map-filter-container/container-map.tsx rename to packages/react-components/src/components/ol-map/filter/container-map.tsx index 20c5669bc..02f920f5b 100644 --- a/packages/react-components/src/components/map-filter-container/container-map.tsx +++ b/packages/react-components/src/components/ol-map/filter/container-map.tsx @@ -2,14 +2,14 @@ import React, { PropsWithChildren } from 'react'; import { Geometry } from 'geojson'; import rewind from '@turf/rewind'; import { Polygon } from 'geojson'; -import { Map } from '../ol-map/map'; -import { TileLayer } from '../ol-map/layers/tile-layer'; -import { VectorSource } from '../ol-map/source/vector-source'; -import { GeoJSONFeature } from '../ol-map/feature'; -import { TileOsm } from '../ol-map/source/osm'; -import { VectorLayer } from '../ol-map/layers/vector-layer'; -import { DrawInteraction } from '../ol-map/interactions/draw'; -import { DrawType } from '../models/enums'; +import { Map } from '../map'; +import { TileLayer } from '../layers/tile-layer'; +import { VectorSource } from '../source/vector-source'; +import { GeoJSONFeature } from '../feature'; +import { TileOsm } from '../source/osm'; +import { VectorLayer } from '../layers/vector-layer'; +import { DrawInteraction } from '../interactions/draw'; +import { DrawType } from '../../models/enums'; import './container-map.css'; interface ContainerMapProps { diff --git a/packages/react-components/src/components/map-filter-container/index.ts b/packages/react-components/src/components/ol-map/filter/index.ts similarity index 100% rename from packages/react-components/src/components/map-filter-container/index.ts rename to packages/react-components/src/components/ol-map/filter/index.ts diff --git a/packages/react-components/src/components/map-filter-container/map-filter-container.tsx b/packages/react-components/src/components/ol-map/filter/map-filter-container.tsx similarity index 98% rename from packages/react-components/src/components/map-filter-container/map-filter-container.tsx rename to packages/react-components/src/components/ol-map/filter/map-filter-container.tsx index d051a8564..d52257394 100644 --- a/packages/react-components/src/components/map-filter-container/map-filter-container.tsx +++ b/packages/react-components/src/components/ol-map/filter/map-filter-container.tsx @@ -2,9 +2,9 @@ import React, { PropsWithChildren, useState } from 'react'; import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; import { Paper } from '@material-ui/core'; import { Polygon } from 'geojson'; -import { DrawType } from '../models/enums'; -import { PolygonSelectionUi } from './polygon-selection-ui'; +import { DrawType } from '../../models/enums'; import { ContainerMap } from './container-map'; +import { PolygonSelectionUi } from './polygon-selection-ui'; const PLACEMENT_SPACING_FACTOR = 1.5; const WIDTH_SPACING_FACTOR = 80; diff --git a/packages/react-components/src/components/map-filter-container/polygon-selection-ui.spec.tsx b/packages/react-components/src/components/ol-map/filter/polygon-selection-ui.spec.tsx similarity index 97% rename from packages/react-components/src/components/map-filter-container/polygon-selection-ui.spec.tsx rename to packages/react-components/src/components/ol-map/filter/polygon-selection-ui.spec.tsx index 0f8092d75..ce5cc390b 100644 --- a/packages/react-components/src/components/map-filter-container/polygon-selection-ui.spec.tsx +++ b/packages/react-components/src/components/ol-map/filter/polygon-selection-ui.spec.tsx @@ -1,7 +1,6 @@ -import React from 'react'; import { shallow } from 'enzyme'; import { MenuItem, Menu, Button } from '@map-colonies/react-core'; -import { DrawType } from '../models'; +import { DrawType } from '../../models'; import { PolygonSelectionUi } from './polygon-selection-ui'; const startDraw = jest.fn(); diff --git a/packages/react-components/src/components/map-filter-container/polygon-selection-ui.tsx b/packages/react-components/src/components/ol-map/filter/polygon-selection-ui.tsx similarity index 97% rename from packages/react-components/src/components/map-filter-container/polygon-selection-ui.tsx rename to packages/react-components/src/components/ol-map/filter/polygon-selection-ui.tsx index 8d362cfbc..ffad1fcbd 100644 --- a/packages/react-components/src/components/map-filter-container/polygon-selection-ui.tsx +++ b/packages/react-components/src/components/ol-map/filter/polygon-selection-ui.tsx @@ -1,10 +1,8 @@ import React from 'react'; import { Menu, MenuItem, Button, Tooltip } from '@map-colonies/react-core'; - import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'; - -import { DrawType } from '../models/enums'; -import { Box } from '../box'; +import { DrawType } from '../../models/enums'; +import { Box } from '../../box'; const WIDTH_SPACING_FACTOR = 18; const useStyle = makeStyles((theme: Theme) => diff --git a/packages/react-components/src/components/map-filter-container/stories/Map.stories.tsx b/packages/react-components/src/components/ol-map/filter/stories/Map.stories.tsx similarity index 87% rename from packages/react-components/src/components/map-filter-container/stories/Map.stories.tsx rename to packages/react-components/src/components/ol-map/filter/stories/Map.stories.tsx index 675fdd615..7d7c0f0c2 100644 --- a/packages/react-components/src/components/map-filter-container/stories/Map.stories.tsx +++ b/packages/react-components/src/components/ol-map/filter/stories/Map.stories.tsx @@ -1,14 +1,14 @@ import { action } from '@storybook/addon-actions'; import { Button, Typography } from '@map-colonies/react-core'; import { MapFilterContainer } from '../map-filter-container'; -import { CSFStory } from '../../utils/story'; -import { Box } from '../..'; -import { VectorLayer } from '../../ol-map/layers/vector-layer'; -import { VectorSource } from '../../ol-map/source/vector-source'; -import { GeoJSONFeature } from '../../ol-map/feature'; +import { CSFStory } from '../../../utils/story'; +import { Box } from '../../..'; +import { GeoJSONFeature } from '../../feature'; +import { VectorLayer } from '../../layers/vector-layer'; +import { VectorSource } from '../../source/vector-source'; const story = { - title: 'Map Filter Container', + title: 'OL Map/Map/Filter', component: MapFilterContainer, parameters: { layout: 'fullscreen', diff --git a/packages/react-components/src/components/ol-map/index.ts b/packages/react-components/src/components/ol-map/index.ts index 107be29f1..4d408b99e 100644 --- a/packages/react-components/src/components/ol-map/index.ts +++ b/packages/react-components/src/components/ol-map/index.ts @@ -5,3 +5,4 @@ export * from './feature'; export * from './map'; export * from '../utils/projections'; export * from './legend'; +export * from './filter'; diff --git a/packages/react-components/src/components/ol-map/source/stories/mvt.stories.tsx b/packages/react-components/src/components/ol-map/source/stories/mvt.stories.tsx index fbf630ecc..afa5d511c 100644 --- a/packages/react-components/src/components/ol-map/source/stories/mvt.stories.tsx +++ b/packages/react-components/src/components/ol-map/source/stories/mvt.stories.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Style, Fill, Circle, Stroke } from 'ol/style'; import { Proj } from '../../../utils/projections'; import { VectorTileLayer } from '../../layers/vector-tile-layer'; @@ -8,7 +7,7 @@ import { MVTSource, getMVTOptions } from '../mvt'; import { TileOsm } from '..'; export default { - title: 'Map/Map Tiles/MVT', + title: 'OL Map/Tiles/MVT', component: VectorTileLayer, subcomponents: MVTSource, parameters: { @@ -22,7 +21,7 @@ const mapDivStyle = { position: 'absolute' as const, }; -export const Basic = (): JSX.Element => ( +export const VectorTiles = (): JSX.Element => (
diff --git a/packages/react-components/src/components/ol-map/source/stories/vector-source.stories.tsx b/packages/react-components/src/components/ol-map/source/stories/vector-source.stories.tsx deleted file mode 100644 index 1412db9d0..000000000 --- a/packages/react-components/src/components/ol-map/source/stories/vector-source.stories.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import { Geometries } from '@turf/helpers'; -import { Fill, Stroke, Style } from 'ol/style'; -import { Proj } from '../../../utils/projections'; -import { Map } from '../../map'; -import { TileLayer, VectorLayer } from '../../layers'; -import { GeoJSONFeature } from '../../feature'; -import { TileOsm } from '..'; -import { VectorSource } from '../vector-source'; - -export default { - title: 'Map/Map Tiles/Geojson', - component: VectorLayer, - subcomponents: GeoJSONFeature, - parameters: { - layout: 'fullscreen', - }, -}; - -const geometries: Geometries[] = [ - { - type: 'Polygon', - coordinates: [ - [ - [3864197.52, 3750764.97], - [3884682.65, 3750764.98], - [3884682.65, 3766052.38], - [3864197.53, 3766052.38], - [3864197.52, 3750764.97], - ], - ], - }, - { - type: 'Polygon', - coordinates: [ - [ - [3904403.4, 3765899.51], - [3896912.58, 3758255.81], - [3905779.27, 3743579.9], - [3918162.07, 3755962.7], - [3904403.4, 3765899.51], - ], - ], - }, - { - type: 'LineString', - coordinates: [ - [3931767.86, 3763147.78], - [3931003.49, 3724776.39], - ], - }, - { - type: 'Point', - coordinates: [3890186.12, 3734254.58], - }, -]; - -const mapDivStyle = { - height: '100%', - width: '100%', - position: 'absolute' as const, -}; - -export const Basic = (): JSX.Element => ( -
- - - - - - - {geometries.map((geometry, index) => { - const selected_polygon_style = new Style({ - stroke: new Stroke({ - width: 5, - color: '#ff0000', - }), - fill: new Fill({ - color: '#aa2727', - }), - }); - let featStyle = index === 0 ? selected_polygon_style : undefined; - - return ; - })} - - - -
-); diff --git a/packages/react-components/src/components/ol-map/source/stories/wms.stories.tsx b/packages/react-components/src/components/ol-map/source/stories/wms.stories.tsx index f67fc935f..00344963c 100644 --- a/packages/react-components/src/components/ol-map/source/stories/wms.stories.tsx +++ b/packages/react-components/src/components/ol-map/source/stories/wms.stories.tsx @@ -19,12 +19,12 @@ const mapDivStyle = { }; const story = { - title: 'Map/Map Tiles/WMS', + title: 'OL Map/Tiles/WMS', component: TileWMS, }; export default story; -export const Basic: CSFStory = () => ( +export const WmsTiles: CSFStory = () => (
@@ -34,7 +34,7 @@ export const Basic: CSFStory = () => (
); -Basic.argTypes = { +WmsTiles.argTypes = { options: { description: `{ Options } from 'ol/source/TileWMS'`, table: { diff --git a/packages/react-components/src/components/ol-map/source/stories/wmts.stories.tsx b/packages/react-components/src/components/ol-map/source/stories/wmts.stories.tsx index 332b98ebb..06248d208 100644 --- a/packages/react-components/src/components/ol-map/source/stories/wmts.stories.tsx +++ b/packages/react-components/src/components/ol-map/source/stories/wmts.stories.tsx @@ -32,13 +32,13 @@ const mapDivStyle = { }; const story = { - title: 'Map/Map Tiles/WMTS', + title: 'OL Map/Tiles/WMTS', component: TileWMTS, }; export default story; -export const Basic: CSFStory = () => ( +export const WmtsTiles: CSFStory = () => (
@@ -54,7 +54,7 @@ export const Basic: CSFStory = () => (
); -Basic.argTypes = { +WmtsTiles.argTypes = { options: { description: `{ Options } from 'ol/source/WMTS'`, table: { diff --git a/packages/react-components/src/components/ol-map/source/stories/xyz.stories.tsx b/packages/react-components/src/components/ol-map/source/stories/xyz.stories.tsx index 3e70c727c..8c167fb17 100644 --- a/packages/react-components/src/components/ol-map/source/stories/xyz.stories.tsx +++ b/packages/react-components/src/components/ol-map/source/stories/xyz.stories.tsx @@ -15,13 +15,13 @@ const mapDivStyle = { }; const story = { - title: 'Map/Map Tiles/XYZ', + title: 'OL Map/Tiles/XYZ', component: TileXYZ, }; export default story; -export const Basic: CSFStory = () => ( +export const XyzTiles: CSFStory = () => (
@@ -31,7 +31,7 @@ export const Basic: CSFStory = () => (
); -Basic.argTypes = { +XyzTiles.argTypes = { options: { description: `{ Options } from 'ol/source/XYZ'`, table: { diff --git a/packages/react-components/src/components/ol-map/source/stories/legend.stories.tsx b/packages/react-components/src/components/ol-map/stories/legend.stories.tsx similarity index 83% rename from packages/react-components/src/components/ol-map/source/stories/legend.stories.tsx rename to packages/react-components/src/components/ol-map/stories/legend.stories.tsx index d04d6adf9..efabe1373 100644 --- a/packages/react-components/src/components/ol-map/source/stories/legend.stories.tsx +++ b/packages/react-components/src/components/ol-map/stories/legend.stories.tsx @@ -1,17 +1,16 @@ -import React from 'react'; import { Geometries } from '@turf/helpers'; import { Fill, Stroke, Style } from 'ol/style'; import { Vector } from 'ol/layer'; -import { Proj } from '../../../utils/projections'; -import { Map } from '../../map'; -import { TileLayer, VectorLayer } from '../../layers'; -import { Legend, LegendItem } from '../../legend'; -import { GeoJSONFeature } from '../../feature'; -import { TileOsm } from '..'; -import { VectorSource } from '../vector-source'; +import { Proj } from '../../utils/projections'; +import { Map } from '../map'; +import { TileLayer, VectorLayer } from '../layers'; +import { Legend, LegendItem } from '../legend'; +import { GeoJSONFeature } from '../feature'; +import { TileOsm } from '../source'; +import { VectorSource } from '../source/vector-source'; export default { - title: 'Map/Map Tiles/Legend', + title: 'OL Map/Map', component: Legend, subcomponents: GeoJSONFeature, parameters: { @@ -110,7 +109,7 @@ const LegendsArray: LegendItem[] = [ }, ]; -export const Basic = (): JSX.Element => ( +export const GeojsonFeaturesWithLegend = (): JSX.Element => (
@@ -128,3 +127,5 @@ export const Basic = (): JSX.Element => (
); + +GeojsonFeaturesWithLegend.storyName = 'GeoJSON Features with Legend'; diff --git a/packages/react-components/src/components/ol-map/stories/map.stories.tsx b/packages/react-components/src/components/ol-map/stories/map.stories.tsx index ec7e6f491..ed2381373 100644 --- a/packages/react-components/src/components/ol-map/stories/map.stories.tsx +++ b/packages/react-components/src/components/ol-map/stories/map.stories.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Story, Meta } from '@storybook/react/types-6-0'; import { Map } from '../map'; import { TileOsm } from '../source'; @@ -6,7 +5,7 @@ import { TileLayer } from '../layers'; import { Proj } from '../../utils/projections'; export default { - title: 'Map', + title: 'OL Map/Map', component: Map, parameters: { layout: 'fullscreen', @@ -49,12 +48,11 @@ BaseMap.argTypes = { export const ConfiguredMap: Story = () => (
- +
); - -ConfiguredMap.storyName = 'with zoom and center'; +ConfiguredMap.storyName = 'Map with Zoom and Center';