From a8a24ee4c33be2347c27274596c2fa1bd5a47e50 Mon Sep 17 00:00:00 2001 From: Pascal Barth Date: Fri, 20 Sep 2024 17:28:36 +0200 Subject: [PATCH 01/13] PB-722 : use in-house style converter for Cesium GeoJSON and get rid of the ol-cesium copy we have --- .../components/cesium/CesiumGeoJSONLayer.vue | 113 +- .../components/cesium/CesiumInternalLayer.vue | 8 +- .../utils/addPrimitiveFromOLLayer.mixins.js | 110 -- .../cesium/utils/olcs/FeatureConverter.ts | 1482 ----------------- .../components/cesium/utils/olcs/README.md | 19 - .../utils/olcs/VectorLayerCounterpart.ts | 47 - .../map/components/cesium/utils/olcs/core.ts | 77 - .../map/components/cesium/utils/olcs/util.ts | 43 - .../components/cesium/utils/styleConverter.js | 118 ++ .../utils/useAddPrimitiveLayer.composable.js | 19 + 10 files changed, 185 insertions(+), 1851 deletions(-) delete mode 100644 src/modules/map/components/cesium/utils/addPrimitiveFromOLLayer.mixins.js delete mode 100644 src/modules/map/components/cesium/utils/olcs/FeatureConverter.ts delete mode 100644 src/modules/map/components/cesium/utils/olcs/README.md delete mode 100644 src/modules/map/components/cesium/utils/olcs/VectorLayerCounterpart.ts delete mode 100644 src/modules/map/components/cesium/utils/olcs/core.ts delete mode 100644 src/modules/map/components/cesium/utils/olcs/util.ts create mode 100644 src/modules/map/components/cesium/utils/styleConverter.js create mode 100644 src/modules/map/components/cesium/utils/useAddPrimitiveLayer.composable.js diff --git a/src/modules/map/components/cesium/CesiumGeoJSONLayer.vue b/src/modules/map/components/cesium/CesiumGeoJSONLayer.vue index 7946ffac1..66d93dcfb 100644 --- a/src/modules/map/components/cesium/CesiumGeoJSONLayer.vue +++ b/src/modules/map/components/cesium/CesiumGeoJSONLayer.vue @@ -1,72 +1,51 @@ - + + + diff --git a/src/modules/map/components/cesium/CesiumInternalLayer.vue b/src/modules/map/components/cesium/CesiumInternalLayer.vue index 9243e0269..f58cb19d3 100644 --- a/src/modules/map/components/cesium/CesiumInternalLayer.vue +++ b/src/modules/map/components/cesium/CesiumInternalLayer.vue @@ -41,11 +41,7 @@ @@ -57,10 +53,10 @@ import { mapGetters } from 'vuex' import AbstractLayer from '@/api/layers/AbstractLayer.class' import LayerTypes from '@/api/layers/LayerTypes.enum' +import CesiumGeoJSONLayer from '@/modules/map/components/cesium/CesiumGeoJSONLayer.vue' import CesiumVectorLayer from '@/modules/map/components/cesium/CesiumVectorLayer.vue' import CoordinateSystem from '@/utils/coordinates/CoordinateSystem.class' -import CesiumGeoJSONLayer from './CesiumGeoJSONLayer.vue' import CesiumGPXLayer from './CesiumGPXLayer.vue' import CesiumKMLLayer from './CesiumKMLLayer.vue' import CesiumWMSLayer from './CesiumWMSLayer.vue' diff --git a/src/modules/map/components/cesium/utils/addPrimitiveFromOLLayer.mixins.js b/src/modules/map/components/cesium/utils/addPrimitiveFromOLLayer.mixins.js deleted file mode 100644 index 29d04b783..000000000 --- a/src/modules/map/components/cesium/utils/addPrimitiveFromOLLayer.mixins.js +++ /dev/null @@ -1,110 +0,0 @@ -import { PrimitiveCollection } from 'cesium' -import { Vector as VectorLayer } from 'ol/layer' - -import { IS_TESTING_WITH_CYPRESS } from '@/config/staging.config' -import { PRIMITIVE_DISABLE_DEPTH_TEST_DISTANCE } from '@/modules/map/components/cesium/constants' -import addLayerToViewer from '@/modules/map/components/cesium/utils/addLayerToViewer-mixins' -import FeatureConverter from '@/modules/map/components/cesium/utils/olcs/FeatureConverter' -import { updateCollectionProperties } from '@/modules/map/components/cesium/utils/primitiveLayerUtils' -import log from '@/utils/logging' - -const STYLE_RESOLUTION = 20 - -/** - * Vue mixin that will handle the addition or removal of a Cesium Primitive layer with data provided - * by an OpenLayers Vector layer. This is a centralized way of describing this logic. - * - * This mixin will create a Cesium layer that will be added to the viewer (through dependency - * injection with `getViewer`). The mixin will manage this layer and will remove it from the viewer - * as soon as the component that has incorporated this mixin will be removed from the DOM. - * - * Any component that wants to use this mixin must give/implement : - * - * - `loadDataInOLLayer()`: a method that returns a promise that resolves when all relevant data have - * been loaded into the `this.olLayer` object. - * - `fromProjection`: a {@link CoordinateSystem} that describe in which projection are data described - * in `this.olLayer` (so that they may be reprojected to WebMercator down the line) - * - * Also, this mixin set/update opacity of the layer. - */ -const addPrimitiveFromOLLayerMixins = { - mixins: [addLayerToViewer], - watch: { - opacity(newOpacity) { - updateCollectionProperties(this.layer, { opacity: newOpacity }) - this.getViewer().scene.requestRender() - }, - url() { - this.olLayer.getSource().clear() - this.removeLayer(this.layer) - this.loadLayer().then((projection) => { - if (projection) this.addPrimitive(projection) - }) - }, - }, - created() { - this.layer = new PrimitiveCollection() - this.olLayer = new VectorLayer({ - id: this.layerId, - opacity: this.opacity, - properties: { altitudeMode: 'clampToGround' }, - projection: this.projection.epsg, - }) - this.loadDataInOLLayer() - .then(() => { - this.addPrimitive() - }) - .catch((error) => { - log.error('Error while loading primitives for layer', this.layerId, error) - }) - }, - methods: { - addLayer(layer) { - this.getViewer().scene.primitives.add(layer) - this.isPresentOnMap = true - }, - removeLayer(layer) { - const viewer = this.getViewer() - layer.removeAll() - viewer.scene.primitives.remove(layer) - viewer.scene.requestRender() - this.isPresentOnMap = false - }, - addPrimitive() { - const scene = this.getViewer().scene - const featureConverter = new FeatureConverter(scene) - const counterpart = featureConverter.olVectorLayerToCesium( - this.olLayer, - { - getProjection: () => this.projection.epsg, - getResolution: () => STYLE_RESOLUTION, - }, - {} - ) - // need to wait for terrain loaded otherwise primitives will be placed wrong - if (this.layer) { - const collectionProperties = { - opacity: this.opacity, - disableDepthTestDistance: PRIMITIVE_DISABLE_DEPTH_TEST_DISTANCE, - } - if (scene.globe.tilesLoaded || IS_TESTING_WITH_CYPRESS) { - this.layer.add(counterpart.getRootPrimitive()) - updateCollectionProperties(this.layer, collectionProperties) - this.getViewer().scene.requestRender() - } else { - const unlisten = scene.globe.tileLoadProgressEvent.addEventListener( - (queueLength) => { - if (scene.globe.tilesLoaded && queueLength === 0) { - this.layer.add(counterpart.getRootPrimitive()) - updateCollectionProperties(this.layer, collectionProperties) - this.getViewer().scene.requestRender() - unlisten() - } - } - ) - } - } - }, - }, -} -export default addPrimitiveFromOLLayerMixins diff --git a/src/modules/map/components/cesium/utils/olcs/FeatureConverter.ts b/src/modules/map/components/cesium/utils/olcs/FeatureConverter.ts deleted file mode 100644 index 9279d420d..000000000 --- a/src/modules/map/components/cesium/utils/olcs/FeatureConverter.ts +++ /dev/null @@ -1,1482 +0,0 @@ -import OLStyleIcon from 'ol/style/Icon' -import OLClusterSource from 'ol/source/Cluster' -import { circular as olCreateCircularPolygon } from 'ol/geom/Polygon' -import { boundingExtent, getCenter } from 'ol/extent' -import olGeomSimpleGeometry from 'ol/geom/SimpleGeometry' -import { - convertColorToCesium, - ol4326CoordinateArrayToCsCartesians, - ol4326CoordinateToCesiumCartesian, - olGeometryCloneTo4326, -} from './core' -import type { OlFeatureToCesiumContext } from './VectorLayerCounterpart' -import VectorLayerCounterpart from './VectorLayerCounterpart' -import { getUid, waitReady } from './util' -import { - Billboard, - BillboardCollection, - Cartesian2, - Cartesian3, - CircleGeometry, - CircleOutlineGeometry, - ClassificationType, - Color as CesiumColor, - ColorGeometryInstanceAttribute, - DebugModelMatrixPrimitive, - Ellipsoid, - Geometry as CSGeometry, - GeometryInstance, - GroundPolylineGeometry, - GroundPolylinePrimitive, - GroundPrimitive, - HeightReference, - HorizontalOrigin, - ImageMaterialProperty, - Label, - LabelCollection, - LabelStyle, - Material, - MaterialAppearance, - Matrix4, - Model, - PolygonGeometry, - PolygonHierarchy, - PolygonOutlineGeometry, - PolylineGeometry, - PolylineMaterialAppearance, - Primitive, - PrimitiveCollection, - Rectangle, - RectangleGeometry, - RectangleOutlineGeometry, - Scene, - VerticalOrigin, -} from 'cesium' -import type VectorLayer from 'ol/layer/Vector' -import type ImageLayer from 'ol/layer/Image' -import type { Feature, View } from 'ol' -import type Text from 'ol/style/Text' -import type { ColorLike as OLColorLike, PatternDescriptor } from 'ol/colorlike' -import type { Color as OLColor } from 'ol/color' -import type { ProjectionLike } from 'ol/proj' -import { - type Circle, - Geometry as OLGeometry, - type GeometryCollection, - type LineString, - type MultiLineString, - type MultiPoint, - type MultiPolygon, - type Point, - type Polygon, -} from 'ol/geom' -import type ImageStyle from 'ol/style/Image' -import type { default as Style, StyleFunction } from 'ol/style/Style' -import type { VectorSourceEvent } from 'ol/source/Vector' -import VectorSource from 'ol/source/Vector' -import type { Size } from 'ol/size' -import type { FeatureLike } from 'ol/Feature' -import RenderFeature from 'ol/render/Feature' - -type ModelFromGltfOptions = Parameters[0] - -type PrimitiveLayer = VectorLayer | ImageLayer - -declare module 'cesium' { - // eslint-disable-next-line no-unused-vars - interface Primitive { - olLayer: PrimitiveLayer - olFeature: FeatureLike - } - // eslint-disable-next-line no-unused-vars - interface GroundPolylinePrimitive { - olLayer: PrimitiveLayer - olFeature: FeatureLike - _primitive: Primitive // Missing from types published by Cesium - } - // eslint-disable-next-line no-unused-vars - interface GroundPrimitive { - olLayer: PrimitiveLayer - olFeature: FeatureLike - } - // eslint-disable-next-line no-unused-vars - interface Label { - olLayer: PrimitiveLayer - olFeature: FeatureLike - } - // eslint-disable-next-line no-unused-vars - interface Billboard { - olLayer: PrimitiveLayer - olFeature: FeatureLike - } -} - -interface ModelStyle { - debugModelMatrix?: Matrix4 - cesiumOptions: ModelFromGltfOptions -} - -interface MaterialAppearanceOptions { - flat: boolean - renderState: { - depthTest: { - enabled: boolean - } - lineWidth?: number - } -} - -export default class FeatureConverter { - /** Bind once to have a unique function for using as a listener */ - private boundOnRemoveOrClearFeatureListener_ = this.onRemoveOrClearFeature_.bind(this) - - private defaultBillboardEyeOffset_ = new Cartesian3(0, 0, 10) - - /** - * Concrete base class for converting from OpenLayers3 vectors to Cesium primitives. Extending - * this class is possible provided that the extending class and the library are compiled - * together by the closure compiler. - * - * @param scene Cesium scene. - * @api - */ - constructor(protected scene: Scene) { - this.scene = scene - } - - /** @param evt */ - private onRemoveOrClearFeature_(evt: VectorSourceEvent) { - const source = evt.target - console.assert(source instanceof VectorSource) - - const cancellers = source['olcs_cancellers'] - if (cancellers) { - const feature = evt.feature - if (feature) { - // remove - const id = getUid(feature) - const canceller = cancellers[id] - if (canceller) { - canceller() - delete cancellers[id] - } - } else { - // clear - for (const key in cancellers) { - if (cancellers.hasOwnProperty(key)) { - cancellers[key]() - } - } - source['olcs_cancellers'] = {} - } - } - } - - /** - * @param layer - * @param feature OpenLayers feature. - * @param primitive - */ - protected setReferenceForPicking( - layer: PrimitiveLayer, - feature: FeatureLike, - primitive: GroundPolylinePrimitive | GroundPrimitive | Primitive | Label | Billboard - ) { - primitive.olLayer = layer - primitive.olFeature = feature - } - - /** - * Basics primitive creation using a color attribute. Note that Cesium has 'interior' and - * outline geometries. - * - * @param layer - * @param feature OpenLayers feature. - * @param olGeometry OpenLayers geometry. - * @param geometry - * @param color - * @param opt_lineWidth - * @returns Primitive - */ - protected createColoredPrimitive( - layer: PrimitiveLayer, - feature: FeatureLike, - olGeometry: OLGeometry, - geometry: CSGeometry | CircleGeometry, - color?: CesiumColor, - opt_lineWidth?: number - ): Primitive | GroundPrimitive | null { - const createInstance = function ( - geometry: CSGeometry | CircleGeometry, - color?: CesiumColor - ) { - const instance = new GeometryInstance({ - geometry, - }) - if (color) { - instance.attributes = { - color: ColorGeometryInstanceAttribute.fromColor(color), - } - } - return instance - } - - const options: MaterialAppearanceOptions = { - flat: true, // work with all geometries - renderState: { - depthTest: { - enabled: true, - }, - }, - } - - if (opt_lineWidth !== undefined) { - options.renderState.lineWidth = opt_lineWidth - } - - const instances = createInstance(geometry, color) - - const heightReference = this.getHeightReference(layer, feature, olGeometry) - - let primitive: GroundPrimitive | Primitive - - if (heightReference === HeightReference.CLAMP_TO_GROUND) { - if (!('createShadowVolume' in instances.geometry.constructor)) { - // This is not a ground geometry - return null - } - primitive = new GroundPrimitive({ - geometryInstances: instances, - }) - } else { - primitive = new Primitive({ - geometryInstances: instances, - }) - } - - if (color) { - primitive.appearance = new MaterialAppearance({ - ...options, - material: new Material({ - translucent: color.alpha !== 1, - fabric: { - type: 'Color', - uniforms: { - color, - }, - }, - }), - }) - if ( - primitive instanceof Primitive && - (feature.get('olcs_shadows') || layer.get('olcs_shadows')) - ) { - primitive.shadows = 1 - } - } - this.setReferenceForPicking(layer, feature, primitive) - return primitive - } - - /** - * Return the fill or stroke color from a plain ol style. - * - * @param style - * @param outline - * @returns {CesiumColor | undefined} - */ - protected extractColorFromOlStyle( - style: Style | Text, - outline: boolean - ): CesiumColor | undefined { - const fillColor: OLColorLike | OLColor | PatternDescriptor | null | undefined = style - .getFill() - ?.getColor() - const strokeColor: OLColorLike | OLColor | undefined = style.getStroke()?.getColor() - - let olColor: OLColorLike | OLColor | PatternDescriptor = 'black' - if (strokeColor && outline) { - olColor = strokeColor - } else if (fillColor) { - olColor = fillColor - } - - const cesiumColor: CesiumColor | ImageMaterialProperty = convertColorToCesium(olColor) - if (cesiumColor instanceof ImageMaterialProperty) { - if (cesiumColor.color instanceof CesiumColor) { - return cesiumColor.color - } else { - return undefined - } - } - return cesiumColor - } - - /** - * Return the width of stroke from a plain ol style. - * - * @param style - * @returns {number} - */ - protected extractLineWidthFromOlStyle(style: Style | Text) { - // Handling of line width WebGL limitations is handled by Cesium. - return style.getStroke()?.getWidth() ?? 1 - } - - /** - * Create a primitive collection out of two Cesium geometries. Only the OpenLayers style colors - * will be used. - */ - protected wrapFillAndOutlineGeometries( - layer: PrimitiveLayer, - feature: FeatureLike, - olGeometry: OLGeometry, - fillGeometry: CSGeometry | CircleGeometry, - olStyle: Style, - outlineGeometry?: CSGeometry | CircleOutlineGeometry - ): PrimitiveCollection { - const fillColor: CesiumColor | undefined = this.extractColorFromOlStyle(olStyle, false) - const outlineColor: CesiumColor | undefined = this.extractColorFromOlStyle(olStyle, true) - - const primitives = new PrimitiveCollection() - if (olStyle.getFill()) { - const p1 = this.createColoredPrimitive( - layer, - feature, - olGeometry, - fillGeometry, - fillColor - ) - console.assert(!!p1) - primitives.add(p1) - } - - if (olStyle.getStroke() && outlineGeometry) { - const width = this.extractLineWidthFromOlStyle(olStyle) - const p2 = this.createColoredPrimitive( - layer, - feature, - olGeometry, - outlineGeometry, - outlineColor, - width - ) - if (p2) { - // Some outline geometries are not supported by Cesium in clamp to ground - // mode. These primitives are skipped. - primitives.add(p2) - } - } - - return primitives - } - - // Geometry converters - - // FIXME: would make more sense to only accept primitive collection. - /** - * Create a Cesium primitive if style has a text component. Eventually return a - * PrimitiveCollection including current primitive. - */ - protected addTextStyle( - layer: PrimitiveLayer, - feature: FeatureLike, - geometry: OLGeometry, - style: Style, - primitive: Primitive | PrimitiveCollection | GroundPolylinePrimitive - ): PrimitiveCollection { - let primitives - if (!(primitive instanceof PrimitiveCollection)) { - primitives = new PrimitiveCollection() - primitives.add(primitive) - } else { - primitives = primitive - } - - const text: Text | null = style.getText() - if (!text) { - return primitives - } - const label = this.olGeometry4326TextPartToCesium(layer, feature, geometry, text) - if (label) { - primitives.add(label) - } - return primitives - } - - /** - * Add a billboard to a Cesium.BillboardCollection. Overriding this wrapper allows manipulating - * the billboard options. - * - * @param billboards - * @param bbOptions - * @param layer - * @param feature OpenLayers feature. - * @returns Newly created billboard - * @api - */ - csAddBillboard( - billboards: BillboardCollection, - bbOptions: Parameters[0] | undefined, - layer: PrimitiveLayer, - feature: FeatureLike - ): Billboard { - if (bbOptions && !bbOptions.eyeOffset) { - bbOptions.eyeOffset = this.defaultBillboardEyeOffset_ - } - const bb = billboards.add(bbOptions) - this.setReferenceForPicking(layer, feature, bb) - return bb - } - - /** - * Convert an OpenLayers circle geometry to Cesium. - * - * @api - */ - olCircleGeometryToCesium( - layer: PrimitiveLayer, - feature: FeatureLike, - olGeometry: Circle, - projection: ProjectionLike, - olStyle: Style - ): PrimitiveCollection { - olGeometry = olGeometryCloneTo4326(olGeometry, projection) - console.assert(olGeometry.getType() == 'Circle') - - // ol.Coordinate - const olCenter = olGeometry.getCenter() - const height = olCenter.length == 3 ? olCenter[2] : 0.0 - const olPoint = olCenter.slice() - olPoint[0] += olGeometry.getRadius() - - // Cesium - const center: Cartesian3 = ol4326CoordinateToCesiumCartesian(olCenter) - const point: Cartesian3 = ol4326CoordinateToCesiumCartesian(olPoint) - - // Accurate computation of straight distance - const radius = Cartesian3.distance(center, point) - - const fillGeometry = new CircleGeometry({ - center, - radius, - height, - }) - - let outlinePrimitive: Primitive | GroundPrimitive | GroundPolylinePrimitive | null = null - let outlineGeometry - if ( - this.getHeightReference(layer, feature, olGeometry) === HeightReference.CLAMP_TO_GROUND - ) { - const width = this.extractLineWidthFromOlStyle(olStyle) - if (width) { - const circlePolygon = olCreateCircularPolygon(olGeometry.getCenter(), radius) - const positions = ol4326CoordinateArrayToCsCartesians( - circlePolygon.getLinearRing(0)!.getCoordinates() - ) - const op = (outlinePrimitive = new GroundPolylinePrimitive({ - geometryInstances: new GeometryInstance({ - geometry: new GroundPolylineGeometry({ positions, width }), - }), - appearance: new PolylineMaterialAppearance({ - material: this.olStyleToCesium(olStyle, true), - }), - classificationType: ClassificationType.TERRAIN, - })) - waitReady(outlinePrimitive).then(() => { - this.setReferenceForPicking(layer, feature, op._primitive) - }) - } - } else { - outlineGeometry = new CircleOutlineGeometry({ - center, - radius, - extrudedHeight: height, - height, - }) - } - - const primitives = this.wrapFillAndOutlineGeometries( - layer, - feature, - olGeometry, - fillGeometry, - olStyle, - outlineGeometry - ) - - if (outlinePrimitive) { - primitives.add(outlinePrimitive) - } - return this.addTextStyle(layer, feature, olGeometry, olStyle, primitives) - } - - /** - * Convert an OpenLayers line string geometry to Cesium. - * - * @api - */ - olLineStringGeometryToCesium( - layer: PrimitiveLayer, - feature: FeatureLike, - olGeometry: LineString, - projection: ProjectionLike, - olStyle: Style - ): PrimitiveCollection { - olGeometry = olGeometryCloneTo4326(olGeometry, projection) - console.assert(olGeometry.getType() == 'LineString') - - const positions = ol4326CoordinateArrayToCsCartesians(olGeometry.getCoordinates()) - const width = this.extractLineWidthFromOlStyle(olStyle) - - let outlinePrimitive: Primitive | GroundPolylinePrimitive - const heightReference = this.getHeightReference(layer, feature, olGeometry) - - const appearance = new PolylineMaterialAppearance({ - material: this.olStyleToCesium(olStyle, true), - }) - if (heightReference === HeightReference.CLAMP_TO_GROUND) { - const geometry = new GroundPolylineGeometry({ - positions, - width, - }) - const op = (outlinePrimitive = new GroundPolylinePrimitive({ - appearance, - geometryInstances: new GeometryInstance({ - geometry, - }), - })) - waitReady(outlinePrimitive).then(() => { - this.setReferenceForPicking(layer, feature, op._primitive) - }) - } else { - const geometry = new PolylineGeometry({ - positions, - width, - vertexFormat: appearance.vertexFormat, - }) - outlinePrimitive = new Primitive({ - appearance, - geometryInstances: new GeometryInstance({ - geometry, - }), - }) - } - - this.setReferenceForPicking(layer, feature, outlinePrimitive) - - return this.addTextStyle(layer, feature, olGeometry, olStyle, outlinePrimitive) - } - - /** - * Convert an OpenLayers polygon geometry to Cesium. - * - * @api - */ - olPolygonGeometryToCesium( - layer: PrimitiveLayer, - feature: FeatureLike, - olGeometry: Polygon, - projection: ProjectionLike, - olStyle: Style - ): PrimitiveCollection { - olGeometry = olGeometryCloneTo4326(olGeometry, projection) - console.assert(olGeometry.getType() == 'Polygon') - - const heightReference = this.getHeightReference(layer, feature, olGeometry) - - let fillGeometry, outlineGeometry - let outlinePrimitive: GroundPolylinePrimitive | null = null - if ( - olGeometry.getCoordinates()[0].length == 5 && - feature.get('olcs.polygon_kind') === 'rectangle' - ) { - // Create a rectangle according to the longitude and latitude curves - const coordinates = olGeometry.getCoordinates()[0] - // Extract the West, South, East, North coordinates - const extent = boundingExtent(coordinates) - const rectangle = Rectangle.fromDegrees(extent[0], extent[1], extent[2], extent[3]) - - // Extract the average height of the vertices - let maxHeight = 0.0 - if (coordinates[0].length == 3) { - for (const coordinate of coordinates) { - maxHeight = Math.max(maxHeight, coordinate[2]) - } - } - - const featureExtrudedHeight = feature.get('olcs_extruded_height') - - // Render the cartographic rectangle - fillGeometry = new RectangleGeometry({ - ellipsoid: Ellipsoid.WGS84, - rectangle, - height: maxHeight, - extrudedHeight: featureExtrudedHeight, - }) - - outlineGeometry = new RectangleOutlineGeometry({ - ellipsoid: Ellipsoid.WGS84, - rectangle, - height: maxHeight, - extrudedHeight: featureExtrudedHeight, - }) - } else { - const rings = olGeometry.getLinearRings() - const hierarchy: PolygonHierarchy = { - positions: [], - holes: [], - } - const polygonHierarchy: PolygonHierarchy = hierarchy - console.assert(rings.length > 0) - - for (let i = 0; i < rings.length; ++i) { - const olPos = rings[i].getCoordinates() - const positions = ol4326CoordinateArrayToCsCartesians(olPos) - console.assert(positions && positions.length > 0) - if (i === 0) { - hierarchy.positions = positions - } else { - hierarchy.holes.push({ - positions, - holes: [], - }) - } - } - - const featureExtrudedHeight = feature.get('olcs_extruded_height') - - fillGeometry = new PolygonGeometry({ - polygonHierarchy, - perPositionHeight: true, - extrudedHeight: featureExtrudedHeight, - }) - - // Since Cesium doesn't yet support Polygon outlines on terrain yet (coming soon...?) - // we don't create an outline geometry if clamped, but instead do the polyline method - // for each ring. Most of this code should be removeable when Cesium adds - // support for Polygon outlines on terrain. - if (heightReference === HeightReference.CLAMP_TO_GROUND) { - const width = this.extractLineWidthFromOlStyle(olStyle) - if (width > 0) { - const positions: Cartesian3[][] = [hierarchy.positions] - if (hierarchy.holes) { - for (const hole of hierarchy.holes) { - positions.push(hole.positions) - } - } - const appearance = new PolylineMaterialAppearance({ - material: this.olStyleToCesium(olStyle, true), - }) - const geometryInstances = [] - for (const linePositions of positions) { - const polylineGeometry = new GroundPolylineGeometry({ - positions: linePositions, - width, - }) - geometryInstances.push( - new GeometryInstance({ - geometry: polylineGeometry, - }) - ) - } - outlinePrimitive = new GroundPolylinePrimitive({ - appearance, - geometryInstances, - }) - waitReady(outlinePrimitive).then(() => { - if (outlinePrimitive) { - this.setReferenceForPicking(layer, feature, outlinePrimitive._primitive) - } - }) - } - } else { - // Actually do the normal polygon thing. This should end the removable - // section of code described above. - outlineGeometry = new PolygonOutlineGeometry({ - polygonHierarchy: hierarchy, - perPositionHeight: true, - extrudedHeight: featureExtrudedHeight, - }) - } - } - - const primitives = this.wrapFillAndOutlineGeometries( - layer, - feature, - olGeometry, - fillGeometry, - olStyle, - outlineGeometry - ) - - if (outlinePrimitive) { - primitives.add(outlinePrimitive) - } - - return this.addTextStyle(layer, feature, olGeometry, olStyle, primitives) - } - - /** @api */ - getHeightReference( - layer: PrimitiveLayer, - feature: FeatureLike, - geometry: OLGeometry - ): HeightReference { - // Read from the geometry - let altitudeMode = geometry.get('altitudeMode') - - // Or from the feature - if (altitudeMode === undefined) { - altitudeMode = feature.get('altitudeMode') - } - - // Or from the layer - if (altitudeMode === undefined) { - altitudeMode = layer.get('altitudeMode') - } - - let heightReference = HeightReference.NONE - if (altitudeMode === 'clampToGround') { - heightReference = HeightReference.CLAMP_TO_GROUND - } else if (altitudeMode === 'relativeToGround') { - heightReference = HeightReference.RELATIVE_TO_GROUND - } - - return heightReference - } - - /** - * Convert a point geometry to a Cesium BillboardCollection. - * - * @param {ol.layer.Vector | ol.layer.Image} layer - * @param {!ol.Feature} feature OpenLayers feature.. - * @param {!ol.geom.Point} olGeometry OpenLayers point geometry. - * @param {!ol.ProjectionLike} projection - * @param {!ol.style.Style} style - * @param {!ol.style.Image} imageStyle - * @param {!BillboardCollection} billboards - * @param {function(!Billboard)} [opt_newBillboardCallback] Called when the new billboard is - * added. - * @api - */ - createBillboardFromImage( - layer: PrimitiveLayer, - feature: FeatureLike, - olGeometry: Point, - projection: ProjectionLike, - style: Style, - imageStyle: ImageStyle, - billboards: BillboardCollection, - opt_newBillboardCallback?: (bb: Billboard) => void - ) { - if (imageStyle instanceof OLStyleIcon) { - // make sure the image is scheduled for load - imageStyle.load() - } - - const image: HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | ImageBitmap | null = - imageStyle.getImage(1) // get normal density - const isImageLoaded = function (image: HTMLImageElement) { - return ( - image.src != '' && - image.naturalHeight != 0 && - image.naturalWidth != 0 && - image.complete - ) - } - const self = this - const reallyCreateBillboard = function () { - if (!image) { - return - } - if (!(typeof image === 'string' || image instanceof HTMLCanvasElement)) { - return - } - const center = olGeometry.getCoordinates() - const position = ol4326CoordinateToCesiumCartesian(center) - let color - const opacity = imageStyle.getOpacity() - if (opacity !== undefined) { - color = new CesiumColor(1.0, 1.0, 1.0, opacity) - } - - const scale: number | Size = imageStyle.getScale() - if (Array.isArray(scale)) { - return - } - const heightReference = self.getHeightReference(layer, feature, olGeometry) - - const bbOptions: Parameters[0] = { - image, - color, - scale, - heightReference, - position, - } - - // merge in cesium options from openlayers feature - Object.assign(bbOptions, feature.get('cesiumOptions')) - - if (imageStyle instanceof OLStyleIcon) { - const anchor = imageStyle.getAnchor() - if (anchor) { - const xScale = Array.isArray(scale) ? scale[0] : scale - const yScale = Array.isArray(scale) ? scale[1] : scale - bbOptions.pixelOffset = new Cartesian2( - (image.width / 2 - anchor[0]) * xScale, - (image.height / 2 - anchor[1]) * yScale - ) - } - } - - const bb = self.csAddBillboard(billboards, bbOptions, layer, feature) - if (opt_newBillboardCallback) { - opt_newBillboardCallback(bb) - } - }.bind(this) - - if (image instanceof Image && !isImageLoaded(image)) { - // Cesium requires the image to be loaded - let cancelled = false - const source = layer.getSource() - const canceller = function () { - cancelled = true - } - source.on(['removefeature', 'clear'], this.boundOnRemoveOrClearFeatureListener_) - let cancellers = source['olcs_cancellers'] - if (!cancellers) { - cancellers = source['olcs_cancellers'] = {} - } - - const fuid = getUid(feature) - if (cancellers[fuid]) { - // When the feature change quickly, a canceller may still be present so - // we cancel it here to prevent creation of a billboard. - cancellers[fuid]() - } - cancellers[fuid] = canceller - - const listener = function () { - image.removeEventListener('load', listener) - if (!billboards.isDestroyed() && !cancelled) { - // Create billboard if the feature is still displayed on the map. - reallyCreateBillboard() - } - } - - image.addEventListener('load', listener) - } else { - reallyCreateBillboard() - } - } - - /** - * Convert a point geometry to a Cesium BillboardCollection. - * - * @param layer - * @param feature OpenLayers feature.. - * @param olGeometry OpenLayers point geometry. - * @param projection - * @param style - * @param billboards - * @param opt_newBillboardCallback Called when the new billboard is added. - * @returns Primitives - * @api - */ - olPointGeometryToCesium( - layer: PrimitiveLayer, - feature: FeatureLike, - olGeometry: Point, - projection: ProjectionLike, - style: Style, - billboards: BillboardCollection, - opt_newBillboardCallback?: (bb: Billboard) => void - ): PrimitiveCollection | null { - console.assert(olGeometry.getType() == 'Point') - olGeometry = olGeometryCloneTo4326(olGeometry, projection) - - let modelPrimitive: PrimitiveCollection | null = null - const imageStyle = style.getImage() - if (imageStyle) { - const olcsModelFunction: () => ModelStyle = - olGeometry.get('olcs_model') || feature.get('olcs_model') - if (olcsModelFunction) { - modelPrimitive = new PrimitiveCollection() - const olcsModel = olcsModelFunction() - const options: ModelFromGltfOptions = { - scene: this.scene, - ...olcsModel.cesiumOptions, - } - if ('fromGltf' in Model) { - // pre Cesium v107 - // @ts-ignore - const model = Model.fromGltf(options) - modelPrimitive!.add(model) - } else { - Model.fromGltfAsync(options).then((model) => { - modelPrimitive!.add(model) - }) - } - - if (olcsModel.debugModelMatrix) { - modelPrimitive.add( - new DebugModelMatrixPrimitive({ - modelMatrix: olcsModel.debugModelMatrix, - }) - ) - } - } else { - this.createBillboardFromImage( - layer, - feature, - olGeometry, - projection, - style, - imageStyle, - billboards, - opt_newBillboardCallback - ) - } - } - - if (style.getText()) { - return this.addTextStyle( - layer, - feature, - olGeometry, - style, - modelPrimitive || new Primitive() - ) - } else { - return modelPrimitive - } - } - - /** - * Convert an OpenLayers multi-something geometry to Cesium. - * - * @param {ol.layer.Vector | ol.layer.Image} layer - * @param {!FeatureLike} feature OpenLayers feature.. - * @param {!ol.geom.Geometry} geometry OpenLayers geometry. - * @param {!ProjectionLike} projection - * @param {!ol.style.Style} olStyle - * @param {!BillboardCollection} billboards - * @param {function(!Billboard)} [opt_newBillboardCallback] Called when the new billboard is - * added. - * @returns {Primitive} Primitives - * @api - */ - olMultiGeometryToCesium( - layer: PrimitiveLayer, - feature: FeatureLike, - geometry: OLGeometry, - projection: ProjectionLike, - olStyle: Style, - billboards: BillboardCollection, - opt_newBillboardCallback: (bb: Billboard) => void - ) { - // Do not reproject to 4326 now because it will be done later. - - switch (geometry.getType()) { - case 'MultiPoint': { - const points = (geometry as MultiPoint).getPoints() - if (olStyle.getText()) { - const primitives = new PrimitiveCollection() - points.forEach((geom) => { - console.assert(geom) - const result = this.olPointGeometryToCesium( - layer, - feature, - geom, - projection, - olStyle, - billboards, - opt_newBillboardCallback - ) - if (result) { - primitives.add(result) - } - }) - return primitives - } else { - points.forEach((geom) => { - console.assert(geom) - this.olPointGeometryToCesium( - layer, - feature, - geom, - projection, - olStyle, - billboards, - opt_newBillboardCallback - ) - }) - return null - } - } - case 'MultiLineString': { - const lineStrings = (geometry as MultiLineString).getLineStrings() - // FIXME: would be better to combine all child geometries in one primitive - // instead we create n primitives for simplicity. - const primitives = new PrimitiveCollection() - lineStrings.forEach((geom) => { - const p = this.olLineStringGeometryToCesium( - layer, - feature, - geom, - projection, - olStyle - ) - primitives.add(p) - }) - return primitives - } - case 'MultiPolygon': { - const polygons = (geometry as MultiPolygon).getPolygons() - // FIXME: would be better to combine all child geometries in one primitive - // instead we create n primitives for simplicity. - const primitives = new PrimitiveCollection() - polygons.forEach((geom) => { - const p = this.olPolygonGeometryToCesium( - layer, - feature, - geom, - projection, - olStyle - ) - primitives.add(p) - }) - return primitives - } - default: - console.assert(false, `Unhandled multi geometry type${geometry.getType()}`) - } - } - - /** - * Convert an OpenLayers text style to Cesium. - * - * @api - */ - olGeometry4326TextPartToCesium( - layer: PrimitiveLayer, - feature: FeatureLike, - geometry: OLGeometry, - style: Text - ): LabelCollection | null { - const text: string | string[] | undefined = style.getText() - if (!text || Array.isArray(text)) { - return null - } - - const labels = new LabelCollection({ scene: this.scene }) - // TODO: export and use the text draw position from OpenLayers . - // See src/ol/render/vector.js - const extentCenter = getCenter(geometry.getExtent()) - if (geometry instanceof olGeomSimpleGeometry) { - const first = geometry.getFirstCoordinate() - extentCenter[2] = first.length == 3 ? first[2] : 0.0 - } - const options: Parameters[0] = { - position: ol4326CoordinateToCesiumCartesian(extentCenter), - text, - heightReference: this.getHeightReference(layer, feature, geometry), - } - - const offsetX = style.getOffsetX() - const offsetY = style.getOffsetY() - if (offsetX != 0 || offsetY != 0) { - options.pixelOffset = new Cartesian2(offsetX, offsetY) - } - - options.font = style.getFont() || '10px sans-serif' // OpenLayers default - - let labelStyle = undefined - if (style.getFill()) { - options.fillColor = this.extractColorFromOlStyle(style, false) - labelStyle = LabelStyle.FILL - } - if (style.getStroke()) { - options.outlineWidth = this.extractLineWidthFromOlStyle(style) - options.outlineColor = this.extractColorFromOlStyle(style, true) - labelStyle = LabelStyle.OUTLINE - } - if (style.getFill() && style.getStroke()) { - labelStyle = LabelStyle.FILL_AND_OUTLINE - } - options.style = labelStyle - - let horizontalOrigin - switch (style.getTextAlign()) { - case 'left': - horizontalOrigin = HorizontalOrigin.LEFT - break - case 'right': - horizontalOrigin = HorizontalOrigin.RIGHT - break - case 'center': - default: - horizontalOrigin = HorizontalOrigin.CENTER - } - options.horizontalOrigin = horizontalOrigin - - if (style.getTextBaseline()) { - let verticalOrigin - switch (style.getTextBaseline()) { - case 'top': - verticalOrigin = VerticalOrigin.TOP - break - case 'middle': - verticalOrigin = VerticalOrigin.CENTER - break - case 'bottom': - verticalOrigin = VerticalOrigin.BOTTOM - break - case 'alphabetic': - verticalOrigin = VerticalOrigin.TOP - break - case 'hanging': - verticalOrigin = VerticalOrigin.BOTTOM - break - default: - console.assert(false, `unhandled baseline ${style.getTextBaseline()}`) - } - options.verticalOrigin = verticalOrigin - } - - const l = labels.add(options) - this.setReferenceForPicking(layer, feature, l) - return labels - } - - /** - * Convert an OpenLayers style to a Cesium Material. - * - * @api - */ - olStyleToCesium(style: Style, outline: boolean): Material { - const fill = style.getFill() - const stroke = style.getStroke() - - let olColor: - | OLColorLike - | OLColor - | PatternDescriptor - | CanvasGradient - | CanvasPattern - | string - | null = null - - if (outline && stroke) { - olColor = stroke.getColor() - } else if (fill) { - olColor = fill.getColor() - } - if (!olColor) { - throw new Error('Could not find a matching color') - } - const color: CesiumColor | ImageMaterialProperty = convertColorToCesium(olColor) - - const lineDash = stroke?.getLineDash() - if (outline && lineDash) { - return Material.fromType('PolylineDash', { - dashPattern: dashPattern(lineDash), - color, - }) - } else { - return Material.fromType('Color', { - color, - }) - } - } - - /** - * Compute OpenLayers plain style. Evaluates style function, blend arrays, get default style. - * - * @api - */ - computePlainStyle( - feature: FeatureLike, - fallbackStyleFunction: StyleFunction | undefined, - resolution: number - ): Style[] | null { - const featureStyleFunction: StyleFunction | undefined = feature.getStyleFunction() - - let style: void | Style | Style[] | null = null - - if (featureStyleFunction) { - style = featureStyleFunction(feature, resolution) - } - - if (!style && fallbackStyleFunction) { - style = fallbackStyleFunction(feature, resolution) - } - - if (!style) { - // The feature must not be displayed - return null - } - - // FIXME combine materials as in cesium-materials-pack? - // then this function must return a custom material - // More simply, could blend the colors like described in - // http://en.wikipedia.org/wiki/Alpha_compositing - return Array.isArray(style) ? style : [style] - } - - protected getGeometryFromFeature( - feature: FeatureLike, - style: Style, - opt_geom?: RenderFeature | OLGeometry - ): RenderFeature | OLGeometry | undefined { - if (opt_geom) { - return opt_geom - } - - const geom3d: OLGeometry = feature.get('olcs.3d_geometry') - if (geom3d && geom3d instanceof OLGeometry) { - return geom3d - } - - if (style) { - const geomFuncRes = style.getGeometryFunction()(feature) - if (geomFuncRes instanceof OLGeometry) { - return geomFuncRes - } - } - - return feature.getGeometry() - } - - /** - * Convert one OpenLayers feature up to a collection of Cesium primitives. - * - * @api - */ - olFeatureToCesium( - layer: PrimitiveLayer, - feature: FeatureLike, - style: Style, - context: OlFeatureToCesiumContext, - opt_geom?: OLGeometry - ): PrimitiveCollection | null { - const geom: RenderFeature | OLGeometry | undefined = this.getGeometryFromFeature( - feature, - style, - opt_geom - ) - - if (!geom) { - // OpenLayers features may not have a geometry - // See http://geojson.org/geojson-spec.html#feature-objects - return null - } - - const proj = context.projection - const newBillboardAddedCallback = function (bb: Billboard) { - const featureBb = context.featureToCesiumMap[getUid(feature)] - if (featureBb instanceof Array) { - featureBb.push(bb) - } else { - context.featureToCesiumMap[getUid(feature)] = [bb] - } - } - - switch (geom.getType()) { - case 'GeometryCollection': - const primitives = new PrimitiveCollection() - ;(geom as GeometryCollection).getGeometriesArray().forEach((geom) => { - if (geom) { - const prims = this.olFeatureToCesium(layer, feature, style, context, geom) - if (prims) { - primitives.add(prims) - } - } - }) - return primitives - case 'Point': - const bbs = context.billboards - const result = this.olPointGeometryToCesium( - layer, - feature, - geom as Point, - proj, - style, - bbs, - newBillboardAddedCallback - ) - if (!result) { - // no wrapping primitive - return null - } else { - return result - } - case 'Circle': - return this.olCircleGeometryToCesium(layer, feature, geom as Circle, proj, style) - case 'LineString': - return this.olLineStringGeometryToCesium( - layer, - feature, - geom as LineString, - proj, - style - ) - case 'Polygon': - return this.olPolygonGeometryToCesium(layer, feature, geom as Polygon, proj, style) - case 'MultiPoint': - return ( - this.olMultiGeometryToCesium( - layer, - feature, - geom as MultiPoint, - proj, - style, - context.billboards, - newBillboardAddedCallback - ) || null - ) - case 'MultiLineString': - return ( - this.olMultiGeometryToCesium( - layer, - feature, - geom as MultiLineString, - proj, - style, - context.billboards, - newBillboardAddedCallback - ) || null - ) - case 'MultiPolygon': - return ( - this.olMultiGeometryToCesium( - layer, - feature, - geom as MultiPolygon, - proj, - style, - context.billboards, - newBillboardAddedCallback - ) || null - ) - case 'LinearRing': - throw new Error('LinearRing should only be part of polygon.') - default: - throw new Error(`Ol geom type not handled : ${geom.getType()}`) - } - } - - /** - * Convert an OpenLayers vector layer to Cesium primitive collection. For each feature, the - * associated primitive will be stored in `featurePrimitiveMap`. - * - * @api - */ - olVectorLayerToCesium( - olLayer: VectorLayer>, - olView: View, - featurePrimitiveMap: Record - ): VectorLayerCounterpart { - const proj = olView.getProjection() - const resolution = olView.getResolution() - - if (resolution === undefined || !proj) { - console.assert(false, 'View not ready') - // an assertion is not enough for closure to assume resolution and proj - // are defined - throw new Error('View not ready') - } - - let source = olLayer.getSource() - if (source instanceof OLClusterSource) { - source = source.getSource() - } - - console.assert(source instanceof VectorSource) - const features: FeatureLike[] | undefined = source?.getFeatures() - if (!features) { - throw new Error('Features missing') - } - const counterpart = new VectorLayerCounterpart(proj, this.scene) - const context = counterpart.context - for (const feature of features) { - if (!feature) { - continue - } - const layerStyle: StyleFunction | undefined = olLayer.getStyleFunction() - const styles = this.computePlainStyle(feature, layerStyle, resolution) - if (!styles?.length) { - // only 'render' features with a style - continue - } - - let primitives: PrimitiveCollection | null = null - for (const style of styles) { - const prims = this.olFeatureToCesium(olLayer, feature, style, context) - if (prims) { - if (!primitives) { - primitives = prims - } else { - let i = 0, - prim: Primitive | void - while ((prim = prims.get(i))) { - primitives.add(prim) - i++ - } - } - } - } - if (!primitives) { - continue - } - featurePrimitiveMap[getUid(feature)] = primitives - counterpart.getRootPrimitive().add(primitives) - } - - return counterpart - } -} - -/** - * Transform a canvas line dash pattern to a Cesium dash pattern See - * https://com/learn/cesiumjs/ref-doc/PolylineDashMaterialProperty.html#dashPattern - * - * @param lineDash - */ -export function dashPattern(lineDash: number[]): number { - if (lineDash.length < 2) { - lineDash = [1, 1] - } - const segments = lineDash.length % 2 === 0 ? lineDash : [...lineDash, ...lineDash] - const total = segments.reduce((a, b) => a + b, 0) - const div = total / 16 - // create a 16 bit binary string - let binaryString = segments - .map((segment, index) => { - // we alternate between 1 and 0 - const digit = index % 2 === 0 ? '1' : '0' - // We scale the segment length to fit 16 slots. - let count = Math.round(segment / div) - if (index === 0 && count === 0) { - // We need to start with a 1 - count = 1 - } - return digit.repeat(count) - }) - .join('') - - // We rounded so it might be that the string is too short or too long. - // We try to fix it by padding or truncating the string. - if (binaryString.length < 16) { - binaryString = binaryString.padEnd(16, '0') - } else if (binaryString.length > 16) { - binaryString = binaryString.substring(0, 16) - } - if (binaryString[15] === '1') { - // We need to really finish with a 0 - binaryString = binaryString.substring(0, 15) + '0' - } - console.assert(binaryString.length === 16) - return parseInt(binaryString, 2) -} diff --git a/src/modules/map/components/cesium/utils/olcs/README.md b/src/modules/map/components/cesium/utils/olcs/README.md deleted file mode 100644 index 47392e22a..000000000 --- a/src/modules/map/components/cesium/utils/olcs/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Partial import of ol-cesium - -While migrating to Vite 5.0.11, there were many issues with TypeScript type checking (`vue-tsc`) with the ol-cesium library. - -We only use a small portion of this library, the part that converts vector features, aka KML and GeoJSON, into Cesium primitives. -The easiest/quickest way of dealing with this issue was to import what we needed specifically. - -## List of copied files - -| ol-cesium file | web-mapviewer file | comment | -|----------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|---------------------------------------------| -| [src/olcs/core.ts](https://github.com/openlayers/ol-cesium/blob/master/src/olcs/core.ts) | `src/modules/map/components/cesium/utils/olcs/core.ts` | Removed all unused/unnecessary functions | -| [src/olcs/util.ts](https://github.com/openlayers/ol-cesium/blob/master/src/olcs/util.ts) | `src/modules/map/components/cesium/utils/olcs/util.ts` | Removed all unused/unnecessary functions | -| [src/olcs/FeatureConverter.ts](https://github.com/openlayers/ol-cesium/blob/master/src/olcs/FeatureConverter.ts) | `src/modules/map/components/cesium/utils/olcs/FeatureConverter.ts` | Fixed returned types and rearranged imports | -| [src/olcs/core/VectorLayerCounterpart.ts](https://github.com/openlayers/ol-cesium/blob/master/src/olcs/core/VectorLayerCounterpart.ts) | `src/modules/map/components/cesium/utils/olcs/VectorLayerCounterpart.ts` | Fixed returned types and rearranged imports | - -## Future actions - -Whenever ol-cesium goes above version 2.17, we could try again to add it as npm package, and see if TypeScript type checker let it through. In the meantime, we have to keep it as a shallow copy. diff --git a/src/modules/map/components/cesium/utils/olcs/VectorLayerCounterpart.ts b/src/modules/map/components/cesium/utils/olcs/VectorLayerCounterpart.ts deleted file mode 100644 index 826d7c758..000000000 --- a/src/modules/map/components/cesium/utils/olcs/VectorLayerCounterpart.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { unByKey as olObservableUnByKey } from 'ol/Observable.js' -import Projection from 'ol/proj/Projection.js' -import { Billboard, BillboardCollection, Primitive, PrimitiveCollection, Scene } from 'cesium' -import type { EventsKey } from 'ol/events.js' - -/** Context for feature conversion. */ -export type OlFeatureToCesiumContext = { - projection: Projection | string - billboards: BillboardCollection - featureToCesiumMap: Record> - primitives: PrimitiveCollection -} - -export default class VectorLayerCounterpart { - olListenKeys: EventsKey[] = [] - context: OlFeatureToCesiumContext - private rootCollection_: PrimitiveCollection - /** Result of the conversion of an OpenLayers layer to Cesium. */ - constructor(layerProjection: Projection | string, scene: Scene) { - const billboards = new BillboardCollection({ scene }) - const primitives = new PrimitiveCollection() - this.rootCollection_ = new PrimitiveCollection() - this.context = { - projection: layerProjection, - billboards, - featureToCesiumMap: {}, - primitives, - } - - this.rootCollection_.add(billboards) - this.rootCollection_.add(primitives) - } - - /** Unlisten. */ - destroy() { - this.olListenKeys.forEach(olObservableUnByKey) - this.olListenKeys.length = 0 - } - - getRootPrimitive(): PrimitiveCollection { - return this.rootCollection_ - } -} - -export type PrimitiveCollectionCounterpart = PrimitiveCollection & { - counterpart: VectorLayerCounterpart -} diff --git a/src/modules/map/components/cesium/utils/olcs/core.ts b/src/modules/map/components/cesium/utils/olcs/core.ts deleted file mode 100644 index a46d5f392..000000000 --- a/src/modules/map/components/cesium/utils/olcs/core.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Cartesian3, Color as CesiumColor, ImageMaterialProperty } from 'cesium' -import type { Color as OLColor } from 'ol/color' -import type { Coordinate } from 'ol/coordinate' -import { get as getProjection } from 'ol/proj' -import type { ProjectionLike } from 'ol/proj.js' -import { Geometry } from 'ol/geom' -import type { ColorLike, PatternDescriptor } from 'ol/colorlike' - -/** Convert a 2D or 3D OpenLayers coordinate to Cesium. */ -export function ol4326CoordinateToCesiumCartesian(coordinate: Coordinate): Cartesian3 { - const coo = coordinate - return coo.length > 2 - ? Cartesian3.fromDegrees(coo[0], coo[1], coo[2]) - : Cartesian3.fromDegrees(coo[0], coo[1]) -} - -/** Convert an array of 2D or 3D OpenLayers coordinates to Cesium. */ -export function ol4326CoordinateArrayToCsCartesians(coordinates: Coordinate[]): Cartesian3[] { - console.assert(coordinates !== null) - const toCartesian = ol4326CoordinateToCesiumCartesian - const cartesians = [] - for (const element of coordinates) { - cartesians.push(toCartesian(element)) - } - return cartesians -} - -/** - * Reproject an OpenLayers geometry to EPSG:4326 if needed. The geometry will be cloned only when - * original projection is not EPSG:4326 and the properties will be shallow copied. - */ -export function olGeometryCloneTo4326( - geometry: T, - projection: ProjectionLike -): T { - console.assert(projection) - - const proj4326 = getProjection('EPSG:4326') - const proj = getProjection(projection) - if (proj && proj4326 && proj.getCode() !== proj4326.getCode()) { - const properties = geometry.getProperties() - geometry = geometry.clone() as T - geometry.transform(proj, proj4326) - geometry.setProperties(properties) - } - return geometry -} - -/** Convert an OpenLayers color to Cesium. */ -export function convertColorToCesium( - olColor: ColorLike | OLColor | PatternDescriptor | CanvasGradient | CanvasPattern | string -): CesiumColor | ImageMaterialProperty { - olColor = olColor || 'black' - if (Array.isArray(olColor)) { - return new CesiumColor( - CesiumColor.byteToFloat(olColor[0]), - CesiumColor.byteToFloat(olColor[1]), - CesiumColor.byteToFloat(olColor[2]), - olColor[3] - ) - } else if (typeof olColor == 'string') { - return CesiumColor.fromCssColorString(olColor) - } else if (olColor instanceof CanvasPattern || olColor instanceof CanvasGradient) { - // Render the CanvasPattern/CanvasGradient into a canvas that will be sent to Cesium as material - const canvas = document.createElement('canvas') - const ctx = canvas.getContext('2d') - if (ctx) { - canvas.width = canvas.height = 256 - ctx.fillStyle = olColor - ctx.fillRect(0, 0, canvas.width, canvas.height) - return new ImageMaterialProperty({ - image: canvas, - }) - } - } - throw new Error('No color could be parsed') -} diff --git a/src/modules/map/components/cesium/utils/olcs/util.ts b/src/modules/map/components/cesium/utils/olcs/util.ts deleted file mode 100644 index 5e24aca50..000000000 --- a/src/modules/map/components/cesium/utils/olcs/util.ts +++ /dev/null @@ -1,43 +0,0 @@ - -/** - * Counter for getUid. - * @type {number} - */ -let uidCounter_ = 0; - -/** - * Gets a unique ID for an object. This mutates the object so that further calls - * with the same object as a parameter returns the same value. Unique IDs are generated - * as a strictly increasing sequence. Adapted from goog.getUid. Similar to OL getUid. - * - * @param obj The object to get the unique ID for. - * @return The unique ID for the object. - */ -export function getUid(obj: any): number { - return obj.olcs_uid || (obj.olcs_uid = ++uidCounter_); -} - -export function waitReady(object: Type): Promise { - const o = object as any; - const p = o.readyPromise; - if (p) { - return p; - } - if (o.ready !== undefined) { - if (o.ready) { - return Promise.resolve(object); - } - return new Promise((resolve, _) => { - // FIXME: this is crazy - // alternative: intercept _ready = true - // altnerative: pass a timeout - const stopper = setInterval(() => { - if (o.ready) { - clearInterval(stopper); - resolve(object); - } - }, 20); - }); - } - return Promise.reject(new Error('Not a readyable object')); -} diff --git a/src/modules/map/components/cesium/utils/styleConverter.js b/src/modules/map/components/cesium/utils/styleConverter.js new file mode 100644 index 000000000..73ad8205f --- /dev/null +++ b/src/modules/map/components/cesium/utils/styleConverter.js @@ -0,0 +1,118 @@ +import { + Cartesian2, + Color, + HeightReference, + LabelGraphics, + PointGraphics, + VerticalOrigin, +} from 'cesium' + +function getProcessedEntityLabelTemplate(entity, style) { + const template = style?.label?.template + if (template && entity.properties.propertyNames.length > 0) { + let processedTemplate = template + entity.properties.propertyNames.forEach((prop) => { + processedTemplate = processedTemplate.replace( + `$\{${prop}}`, + entity.properties[prop].getValue() + ) + }) + return processedTemplate + } + return null +} + +function getEntityKeyValue(entity, geoJsonStyle) { + const styleKey = geoJsonStyle.property + return entity.properties[styleKey]?.getValue() +} + +function getStyleForEntity(entity, geoJsonStyle) { + const entityKeyValue = getEntityKeyValue(entity, geoJsonStyle) + const keyType = geoJsonStyle.type + if (entityKeyValue !== null) { + if (keyType === 'range') { + return geoJsonStyle.ranges + .filter( + (range) => range.range[0] <= entityKeyValue && range.range[1] > entityKeyValue + ) + .reduce((previous, current) => { + if (!previous) { + return current + } else if ( + !previous.maxResolution || + previous.maxResolution > current.maxResolution + ) { + return current + } + return previous + }, null) + } else if (keyType === 'unique') { + return geoJsonStyle.values.find((value) => value.value === entityKeyValue) + } + } + return null +} + +export function setEntityStyle(entity, geoJsonStyle) { + const style = getStyleForEntity(entity, geoJsonStyle) + if (style) { + const { vectorOptions } = style + if (style.geomType === 'point') { + const pointGraphicsOptions = { + heightReference: HeightReference.RELATIVE_TO_GROUND, + // disabling depth test + disableDepthTestDistance: Number.POSITIVE_INFINITY, + } + if (vectorOptions.fill) { + pointGraphicsOptions.color = Color.fromCssColorString(vectorOptions.fill.color) + } + if (vectorOptions.radius) { + pointGraphicsOptions.pixelSize = 2 * vectorOptions.radius + } + if (vectorOptions.stroke) { + pointGraphicsOptions.outlineColor = Color.fromCssColorString( + vectorOptions.stroke.color + ) + pointGraphicsOptions.outlineWidth = vectorOptions.stroke.width + } + entity.point = new PointGraphics(pointGraphicsOptions) + } + if (vectorOptions.label) { + const labelGraphicsOptions = { + heightReference: HeightReference.RELATIVE_TO_GROUND, + // disabling depth test + disableDepthTestDistance: Number.POSITIVE_INFINITY, + text: getProcessedEntityLabelTemplate(entity, vectorOptions), + verticalOrigin: VerticalOrigin.BOTTOM, + } + if (vectorOptions.label.text) { + labelGraphicsOptions.font = vectorOptions.label.text.font + } + if (vectorOptions.label.text.fill) { + labelGraphicsOptions.fill = Color.fromCssColorString( + vectorOptions.label.text.fill.color + ) + } + if (vectorOptions.label.text.stroke) { + labelGraphicsOptions.outlineColor = Color.fromCssColorString( + vectorOptions.label.text.stroke.color + ) + labelGraphicsOptions.outlineWidth = vectorOptions.label.text.stroke.width + } + if (vectorOptions.label.text.backgroundFill) { + labelGraphicsOptions.showBackground = true + labelGraphicsOptions.backgroundColor = Color.fromCssColorString( + vectorOptions.label.text.backgroundFill.color + ) + } + if (vectorOptions.label.text.offsetY) { + labelGraphicsOptions.pixelOffset = new Cartesian2( + 0, + vectorOptions.label.text.offsetY + ) + } + entity.label = new LabelGraphics(labelGraphicsOptions) + } + } +} diff --git a/src/modules/map/components/cesium/utils/useAddPrimitiveLayer.composable.js b/src/modules/map/components/cesium/utils/useAddPrimitiveLayer.composable.js new file mode 100644 index 000000000..aff155524 --- /dev/null +++ b/src/modules/map/components/cesium/utils/useAddPrimitiveLayer.composable.js @@ -0,0 +1,19 @@ +import { onBeforeUnmount, onMounted } from 'vue' + +/** + * @param {Viewer} cesiumViewer + * @param {Promise | Cesium3DTileset} tileSet + */ +export default function useAddPrimitiveLayer(cesiumViewer, tileSet) { + let layer + + onMounted(async () => { + layer = cesiumViewer.scene.primitives.add(await tileSet) + }) + + onBeforeUnmount(() => { + layer.show = false + cesiumViewer.scene.primitives.remove(layer) + cesiumViewer.scene.requestRender() + }) +} From 0a4a184cbcf66de5aa289b996f99e80a23652f42 Mon Sep 17 00:00:00 2001 From: Pascal Barth Date: Fri, 20 Sep 2024 17:29:43 +0200 Subject: [PATCH 02/13] PB-722 : group all background and visible layer into one component each like with OL, keeping all the logic for each group isolated from the main component --- .../cesium/CesiumBackgroundLayer.vue | 26 +++++ .../map/components/cesium/CesiumMap.vue | 73 ++------------ .../components/cesium/CesiumVectorLayer.vue | 46 +++++---- .../components/cesium/CesiumVisibleLayers.vue | 74 ++++++++++++++ .../cesium/utils/primitiveLayerUtils.js | 99 ------------------- 5 files changed, 135 insertions(+), 183 deletions(-) create mode 100644 src/modules/map/components/cesium/CesiumBackgroundLayer.vue create mode 100644 src/modules/map/components/cesium/CesiumVisibleLayers.vue diff --git a/src/modules/map/components/cesium/CesiumBackgroundLayer.vue b/src/modules/map/components/cesium/CesiumBackgroundLayer.vue new file mode 100644 index 000000000..ce169cbec --- /dev/null +++ b/src/modules/map/components/cesium/CesiumBackgroundLayer.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/modules/map/components/cesium/CesiumMap.vue b/src/modules/map/components/cesium/CesiumMap.vue index 9b7bc9b2b..178a4c407 100644 --- a/src/modules/map/components/cesium/CesiumMap.vue +++ b/src/modules/map/components/cesium/CesiumMap.vue @@ -11,40 +11,8 @@ @touchcancel="clearLongPressTimer" @contextmenu="onContextMenu" > -
- - - - - -
+ + state.position.camera, uiMode: (state) => state.ui.mode, projection: (state) => state.position.projection, - isFullScreenMode: (state) => state.ui.fullscreenMode, isTimeSliderActive: (state) => state.ui.isTimeSliderActive, layersConfig: (state) => state.layers.config, }), @@ -182,7 +146,6 @@ export default { 'resolution', 'hasDevSiteWarning', 'visibleLayers', - 'backgroundLayersFor3D', 'showFeatureInfoInTooltip', ]), isProjectionWebMercator() { @@ -191,25 +154,6 @@ export default { isDesktopMode() { return this.uiMode === UIModes.DESKTOP }, - visibleImageryLayers() { - return this.visibleLayers - .filter( - (l) => - l instanceof GeoAdminWMTSLayer || - l instanceof GeoAdminWMSLayer || - l instanceof GeoAdminAggregateLayer || - l instanceof ExternalLayer - ) - .map((visibleLayer) => { - if (visibleLayer.idIn3d) { - return ( - this.layersConfig.find((layer) => layer.id === visibleLayer.idIn3d) ?? - visibleLayer - ) - } - return visibleLayer - }) - }, isFeatureInfoInTooltip() { return this.showFeatureInfoInTooltip }, @@ -227,11 +171,6 @@ export default { editFeature() { return this.selectedFeatures.find((feature) => feature.isEditable) }, - startingZIndexForImageryLayers() { - return this.backgroundLayersFor3D.find((layer) => layer.type === LayerTypes.WMTS) - ? 1 - : 0 - }, }, watch: { selectedFeatures: { diff --git a/src/modules/map/components/cesium/CesiumVectorLayer.vue b/src/modules/map/components/cesium/CesiumVectorLayer.vue index 333324506..414d4377a 100644 --- a/src/modules/map/components/cesium/CesiumVectorLayer.vue +++ b/src/modules/map/components/cesium/CesiumVectorLayer.vue @@ -1,8 +1,10 @@ diff --git a/src/store/plugins/sync-camera-lonlatzoom.js b/src/store/plugins/sync-camera-lonlatzoom.js index bd383f006..0154bc6e9 100644 --- a/src/store/plugins/sync-camera-lonlatzoom.js +++ b/src/store/plugins/sync-camera-lonlatzoom.js @@ -21,7 +21,7 @@ export default function syncCameraLonLatZoom(store) { log.debug(`[${self}] ignore mutation triggered by this plugin`, mutation) return } - if (mutation.type === 'setCameraPosition') { + if (mutation.type === 'setCameraPosition' && state.position.camera) { const lon = state.position.camera.x const lat = state.position.camera.y const height = state.position.camera.z From d12a4e00b6876f49c6dda66ba953ec88dc0c1844 Mon Sep 17 00:00:00 2001 From: Pascal Barth Date: Wed, 2 Oct 2024 14:15:04 +0200 Subject: [PATCH 06/13] PB-722 : transform Cesium popover into composition API As refs on child component in the context of Composition API don't give the HTML element anymore, I had to export the size of the MapPopover component in its defineExpose (CesiumPopover needs to know the width of the popover to place it correctly) --- src/modules/map/components/MapPopover.vue | 15 + .../map/components/cesium/CesiumMap.vue | 3 +- .../map/components/cesium/CesiumPopover.vue | 270 ++++++++---------- .../cesium/utils/addImageryLayer-mixins.js | 70 ----- .../cesium/utils/addLayerToViewer-mixins.js | 30 -- 5 files changed, 139 insertions(+), 249 deletions(-) delete mode 100644 src/modules/map/components/cesium/utils/addImageryLayer-mixins.js delete mode 100644 src/modules/map/components/cesium/utils/addLayerToViewer-mixins.js diff --git a/src/modules/map/components/MapPopover.vue b/src/modules/map/components/MapPopover.vue index 6e1bf9ab3..840c78b82 100644 --- a/src/modules/map/components/MapPopover.vue +++ b/src/modules/map/components/MapPopover.vue @@ -70,6 +70,11 @@ const currentHeaderHeight = computed(() => store.state.ui.headerHeight) const isPhoneMode = computed(() => store.getters.isPhoneMode) const isDesktopMode = computed(() => store.getters.isTraditionalDesktopSize) +const size = ref({ + height: popover.value?.clientHeight ?? 0, + width: popover.value?.clientWidth ?? 0, +}) + const cssPositionOnScreen = computed(() => { if (mode.value === MapPopoverMode.FEATURE_TOOLTIP) { return { @@ -104,6 +109,12 @@ const popoverLimits = computed(() => { }) onMounted(() => { + if (popover.value) { + size.value = { + height: popover.value.clientHeight, + width: popover.value.clientWidth, + } + } if (mode.value === MapPopoverMode.FLOATING && popover.value && popoverHeader.value) { useMovableElement(popover.value, { grabElement: popoverHeader, @@ -115,6 +126,10 @@ onMounted(() => { function onClose() { emits('close') } + +defineExpose({ + size: size, +})