diff --git a/src/components/Wizard/ModelSelection/CesiumGeojsonFootprint.tsx b/src/components/Wizard/ModelSelection/CesiumGeojsonFootprint.tsx new file mode 100644 index 0000000..dae5bdb --- /dev/null +++ b/src/components/Wizard/ModelSelection/CesiumGeojsonFootprint.tsx @@ -0,0 +1,51 @@ +import { useEffect, useState } from "react"; +import { CesiumGeojsonLayer, CesiumRectangle, RCesiumGeojsonLayerProps } from "@map-colonies/react-components"; +import { FeatureCollection } from "geojson"; +import { getLayerFootprint } from "../../../utils/Cesium/GetLayerFootprint"; +import { FlyTo } from "../../../utils/Cesium/FlyTo"; + +interface CesiumGeojsonFootprintProps extends RCesiumGeojsonLayerProps { + id: string; + setFinishedFlying?: (finished: boolean) => void; +} + +export const CesiumGeojsonFootprint: React.FC = (props) => { + const [layersFootprints, setlayersFootprints] = useState(); + const [rect, setRect] = useState(undefined); + + useEffect(() => { + let footprintsCollection: FeatureCollection = { + type: 'FeatureCollection', + features: [] + }; + const layer = props.data; + if (layer) { + const footprint = getLayerFootprint(layer, false, true); + if (footprint.type !== 'FeatureCollection') { + footprintsCollection.features.push(footprint); + } else { + footprintsCollection = footprint; + } + } + setlayersFootprints(footprintsCollection); + setRect(new CesiumRectangle()); + }, [props.id]); + + return ( + <> + + { + rect && + + } + + ); +} \ No newline at end of file diff --git a/src/components/Wizard/ModelSelection/ModelSelection.tsx b/src/components/Wizard/ModelSelection/ModelSelection.tsx index f3cf5ba..aafba23 100644 --- a/src/components/Wizard/ModelSelection/ModelSelection.tsx +++ b/src/components/Wizard/ModelSelection/ModelSelection.tsx @@ -1,27 +1,27 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Geometry } from 'geojson'; import { Box, Cesium3DTileset, - CesiumColor, - CesiumConstantProperty, - CesiumGeojsonLayer, CesiumMap, CesiumSceneMode } from '@map-colonies/react-components'; import { fetchCatalog } from '../../../common/services/CatalogService'; import { Curtain } from '../../../common/Curtain/curtain'; -import { getTokenResource } from '../../../utils/cesium'; +import { getTokenResource } from '../../../utils/Cesium/CesiumResource'; import appConfig from '../../../utils/Config'; import { CatalogTree } from '../../common/Tree/CatalogTree/CatalogTree'; import { Terrain } from '../../common/Terrain/Terrain'; -import { CatalogTreeNode, WizardSelectionProps } from '../Wizard.types'; +import { CatalogTreeNode, IDENTIFIER_FIELD, WizardSelectionProps } from '../Wizard.types'; import './ModelSelection.css'; +import { CesiumGeojsonFootprint } from './CesiumGeojsonFootprint'; export const ModelSelection: React.FC = (props) => { const [isLoading, setIsLoading] = useState(true); + const [finishedFlying, setFinishedFlying] = useState(false); + const treeTheme = { "--rst-selected-background-color": '#f8fafc33', "--rst-hover-background-color": '#1e293b80', @@ -68,6 +68,17 @@ export const ModelSelection: React.FC = (props) => { })(); }, []); + const centerCesiumView = useMemo(() => { + return JSON.parse(appConfig.mapCenter); + }, [appConfig.mapCenter]); + + const selectedItemFootprint = useMemo(() => { + if (!props.selectedItem?.['mc:footprint']) { + return; + } + return JSON.parse(props.selectedItem?.['mc:footprint']) as Geometry; + }, [props.selectedItem]); + return ( @@ -94,34 +105,27 @@ export const ModelSelection: React.FC = (props) => { { - props.selectedItem?.['mc:links'] && (props.selectedItem?.isShown as boolean) && - } { - props.selectedItem?.['mc:footprint'] && - { - geojsonDataSource.entities.values.forEach((item) => { - if (item.polyline) { - const color = CesiumColor.CYAN; - (item.polyline.width as CesiumConstantProperty).setValue(5); - // @ts-ignore - item.polyline.material = color; - } - }); - }} + props.selectedItem?.['mc:links'] && (props.selectedItem?.isShown as boolean) && finishedFlying && + } diff --git a/src/components/common/Terrain/Terrain.tsx b/src/components/common/Terrain/Terrain.tsx index 246c592..c40f944 100644 --- a/src/components/common/Terrain/Terrain.tsx +++ b/src/components/common/Terrain/Terrain.tsx @@ -5,7 +5,7 @@ import { CesiumEllipsoidTerrainProvider, useCesiumMap, } from '@map-colonies/react-components'; -import { getTokenResource } from '../../../utils/cesium'; +import { getTokenResource } from '../../../utils/Cesium/CesiumResource'; import appConfig from '../../../utils/Config'; const NONE = 0; diff --git a/src/utils/cesium.ts b/src/utils/Cesium/CesiumResource.ts similarity index 94% rename from src/utils/cesium.ts rename to src/utils/Cesium/CesiumResource.ts index e22f731..ce6d3c0 100644 --- a/src/utils/cesium.ts +++ b/src/utils/Cesium/CesiumResource.ts @@ -1,5 +1,5 @@ import { CesiumResource } from '@map-colonies/react-components'; -import appConfig from './Config'; +import appConfig from '../Config'; export const getTokenResource = (url: string): CesiumResource => { const tokenProps: any = { url }; diff --git a/src/utils/Cesium/FlyTo.tsx b/src/utils/Cesium/FlyTo.tsx new file mode 100644 index 0000000..cf01666 --- /dev/null +++ b/src/utils/Cesium/FlyTo.tsx @@ -0,0 +1,51 @@ +import React, { useEffect } from 'react'; +import { CesiumColor, CesiumRectangle, useCesiumMap } from '@map-colonies/react-components'; +import bbox from '@turf/bbox'; +import { Geometry } from 'geojson'; + +const TRANSPARENT = 0.0; + +interface FlyToProps { + setRect: (rect: CesiumRectangle | undefined) => void; + geometry: Geometry; + tilt?: boolean; + setFinishedFlying?: (finished: boolean) => void; +} + +export const generateLayerRectangle = (geometry: Geometry): CesiumRectangle => { + return CesiumRectangle.fromDegrees(...bbox(geometry)) as CesiumRectangle; +}; + +export const FlyTo: React.FC = ({ setRect, geometry, setFinishedFlying, tilt = false }): JSX.Element => { + const mapViewer = useCesiumMap(); + let rect; + + useEffect(() => { + rect = generateLayerRectangle(geometry); + + const rectangle = mapViewer.entities.add({ + rectangle: { + coordinates: rect, + material: CesiumColor.PURPLE.withAlpha(TRANSPARENT) + }, + }); + + setFinishedFlying?.(false); + void mapViewer.flyTo( + rectangle, !tilt ? { + offset: { + heading: 0, + pitch: -Math.PI / 2, + range: 0 + } + } : {} + ).then(() => { + setFinishedFlying?.(true); + mapViewer.entities.remove(rectangle); + }); + + setRect(undefined); + }, []); + + return <>; +}; diff --git a/src/utils/Cesium/GetLayerFootprint.ts b/src/utils/Cesium/GetLayerFootprint.ts new file mode 100644 index 0000000..dcc2f2c --- /dev/null +++ b/src/utils/Cesium/GetLayerFootprint.ts @@ -0,0 +1,115 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { Feature, FeatureCollection, Geometry, Polygon, Position } from 'geojson'; +import polygonToLine from '@turf/polygon-to-line'; +import bbox from '@turf/bbox'; +import bboxPolygon from '@turf/bbox-polygon'; +import convex from '@turf/convex'; + +const shrinkExtremeCoordinatesInOuterRing = (geometry: Geometry, factor = 0.99) => { + const LAT_THRESHOLD = 84.9; + const LON_THRESHOLD = 179; + + function maybeShrink([lon, lat]: Position) { + const needsShrink = + Math.abs(lat) > LAT_THRESHOLD || Math.abs(lon) > LON_THRESHOLD; + if (needsShrink) { + return [lon * factor, lat * factor]; + } + return [lon, lat]; + } + + function processRing(ring: Position[]) { + const processed = ring.map(maybeShrink); + + // Ensure ring is closed + const first = processed[0]; + const last = processed[processed.length - 1]; + if (first[0] !== last[0] || first[1] !== last[1]) { + processed.push([...first]); + } + + return processed; + } + + if (geometry.type === "Polygon") { + const [outer, ...holes] = geometry.coordinates; + return { + type: "Polygon", + coordinates: [processRing(outer), ...holes] + } as Geometry; + } + + if (geometry.type === "MultiPolygon") { + return { + type: "MultiPolygon", + coordinates: geometry.coordinates.map(polygon => { + const [outer, ...holes] = polygon; + return [processRing(outer), ...holes]; + }) + } as Geometry; + } + + throw new Error("[shrinkExtremeCoordinatesInOuterRing] Unsupported geometry type: " + geometry.type); +} + +export const getLayerFootprint = (layerFootprint: Geometry, isBbox: boolean, isPolylined = true, isConvexHull = false): Feature | FeatureCollection => { + if (layerFootprint === undefined) { + return { + type: 'Feature', + // @ts-ignore + geometry: null + }; + } + + if (isBbox) { + let geometry = layerFootprint; + switch (layerFootprint.type) { + case 'MultiPolygon': + geometry = (bboxPolygon(bbox(geometry)) as Feature).geometry; + break; + default: + break; + } + + if (isPolylined) { + geometry = (polygonToLine(geometry as Polygon) as Feature).geometry; + } + + return { + type: 'Feature', + geometry: { + ...geometry, + }, + properties: {} + }; + } else { + let geometry: Geometry = shrinkExtremeCoordinatesInOuterRing(layerFootprint, 0.999); + if (isConvexHull) { + // @ts-ignore + geometry = isPolylined ? (polygonToLine(convex(geometry)) as Feature).geometry : (convex(geometry) as Feature).geometry; + return { + type: 'Feature', + geometry: { + ...geometry, + }, + properties: {}, + }; + } else { + if (isPolylined) { + return { + // @ts-ignore + ...polygonToLine(geometry), + properties: {}, + } as Feature + } else { + return { + type: 'Feature', + geometry: { + ...geometry, + }, + properties: {}, + }; + } + } + } +}