├── .gitignore ├── .gitlab-ci.yml ├── .idea ├── .gitignore ├── codeStyles │ └── codeStyleConfig.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jsLibraryMappings.xml ├── jsLinters │ └── eslint.xml ├── misc.xml ├── modules.xml ├── prettier.xml ├── vcmap-core.iml └── vcs.xml ├── .npmrc ├── .prettierignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── build ├── .eslintrc ├── postBuild.js └── postinstall.js ├── documentation ├── VcsLayer.png ├── VcsLayer.uxf ├── clipping.md ├── editor.md ├── interaction.md ├── layers.md ├── maps.md ├── navigation.md ├── renderScreenshot.md ├── style.md ├── vcsApp.md ├── vcsModule.md ├── vcsTemplate.md ├── vectorClusterGroup.md └── vectorProperties.md ├── index.ts ├── jsconfig.json ├── package-lock.json ├── package.json ├── src ├── category │ ├── category.ts │ └── categoryCollection.ts ├── cesium │ ├── cesium.d.ts │ ├── cesium3DTileFeature.ts │ ├── cesium3DTilePointFeature.ts │ ├── cesiumVcsCameraPrimitive.ts │ ├── clippingPolygon.ts │ ├── clippingPolygonCollection.ts │ ├── entity.ts │ └── wallpaperMaterial.js ├── classRegistry.ts ├── featureProvider │ ├── abstractFeatureProvider.ts │ ├── featureProviderSymbols.ts │ ├── tileProviderFeatureProvider.ts │ └── wmsFeatureProvider.ts ├── global.d.ts ├── interaction │ ├── abstractInteraction.ts │ ├── coordinateAtPixel.ts │ ├── eventHandler.ts │ ├── featureAtPixelInteraction.ts │ ├── featureProviderInteraction.ts │ ├── interactionChain.ts │ └── interactionType.ts ├── layer │ ├── cesium │ │ ├── cesiumTilesetCesiumImpl.ts │ │ ├── dataSourceCesiumImpl.ts │ │ ├── openStreetMapCesiumImpl.ts │ │ ├── rasterLayerCesiumImpl.ts │ │ ├── resourceHelper.ts │ │ ├── singleImageCesiumImpl.ts │ │ ├── sourceVectorContextSync.ts │ │ ├── terrainCesiumImpl.ts │ │ ├── tmsCesiumImpl.ts │ │ ├── vcsTile │ │ │ ├── vcsChildTile.ts │ │ │ ├── vcsDebugTile.ts │ │ │ ├── vcsNoDataTile.ts │ │ │ ├── vcsQuadtreeTileProvider.ts │ │ │ ├── vcsTileHelpers.ts │ │ │ └── vcsVectorTile.ts │ │ ├── vectorCesiumImpl.ts │ │ ├── vectorContext.ts │ │ ├── vectorRasterTileCesiumImpl.ts │ │ ├── vectorTileCesiumImpl.ts │ │ ├── vectorTileImageryProvider.ts │ │ ├── wmsCesiumImpl.ts │ │ └── wmtsCesiumImpl.ts │ ├── cesiumTilesetLayer.ts │ ├── czmlLayer.ts │ ├── dataSourceLayer.ts │ ├── featureLayer.ts │ ├── featureStoreFeatureVisibility.ts │ ├── featureStoreLayer.ts │ ├── featureStoreLayerChanges.ts │ ├── featureStoreLayerState.ts │ ├── featureVisibility.ts │ ├── flatGeobufHelpers.ts │ ├── flatGeobufLayer.ts │ ├── geojsonHelpers.ts │ ├── geojsonLayer.ts │ ├── globalHider.ts │ ├── layer.ts │ ├── layerImplementation.ts │ ├── layerState.ts │ ├── layerSymbols.ts │ ├── oblique │ │ ├── layerObliqueImpl.ts │ │ ├── obliqueHelpers.ts │ │ ├── sourceObliqueSync.ts │ │ └── vectorObliqueImpl.ts │ ├── openStreetMapLayer.ts │ ├── openlayers │ │ ├── layerOpenlayersImpl.ts │ │ ├── loadFunctionHelpers.ts │ │ ├── openStreetMapOpenlayersImpl.ts │ │ ├── rasterLayerOpenlayersImpl.ts │ │ ├── singleImageOpenlayersImpl.ts │ │ ├── tileDebugOpenlayersImpl.ts │ │ ├── tmsOpenlayersImpl.ts │ │ ├── vectorOpenlayersImpl.ts │ │ ├── vectorTileOpenlayersImpl.ts │ │ ├── wmsOpenlayersImpl.ts │ │ └── wmtsOpenlayersImpl.ts │ ├── pointCloudLayer.ts │ ├── rasterLayer.ts │ ├── singleImageLayer.ts │ ├── terrainHelpers.ts │ ├── terrainLayer.ts │ ├── tileLoadedHelper.ts │ ├── tileProvider │ │ ├── flatGeobufTileProvider.ts │ │ ├── mvtTileProvider.ts │ │ ├── staticFeatureTileProvider.ts │ │ ├── staticGeojsonTileProvider.ts │ │ ├── tileProvider.ts │ │ └── urlTemplateTileProvider.ts │ ├── tmsLayer.ts │ ├── vectorHelpers.ts │ ├── vectorLayer.ts │ ├── vectorProperties.ts │ ├── vectorSymbols.ts │ ├── vectorTileLayer.ts │ ├── wfsLayer.ts │ ├── wmsHelpers.ts │ ├── wmsLayer.ts │ └── wmtsLayer.ts ├── map │ ├── baseOLMap.ts │ ├── cameraLimiter.ts │ ├── cesiumMap.ts │ ├── mapState.ts │ ├── navigation │ │ ├── cameraHelper.ts │ │ ├── cesiumNavigation.ts │ │ ├── controller │ │ │ ├── controller.ts │ │ │ ├── controllerInput.ts │ │ │ └── keyboardController.ts │ │ ├── easingHelper.ts │ │ ├── navigation.ts │ │ ├── navigationImpl.ts │ │ ├── obliqueNavigation.ts │ │ ├── openlayersNavigation.ts │ │ └── viewHelper.ts │ ├── obliqueMap.ts │ ├── openlayersMap.ts │ └── vcsMap.ts ├── moduleIdSymbol.ts ├── oblique │ ├── defaultObliqueCollection.ts │ ├── helpers.ts │ ├── obliqueCollection.ts │ ├── obliqueDataSet.ts │ ├── obliqueImage.ts │ ├── obliqueImageMeta.ts │ ├── obliqueProvider.ts │ ├── obliqueView.ts │ ├── obliqueViewDirection.ts │ └── parseImageJson.ts ├── ol │ ├── feature.ts │ ├── geojson.d.ts │ ├── geom │ │ ├── circle.ts │ │ └── geometryCollection.js │ ├── ol.d.ts │ ├── render │ │ └── canvas │ │ │ └── canvasTileRenderer.js │ └── source │ │ ├── ClusterEnhancedVectorSource.ts │ │ └── VcsCluster.ts ├── overrideClassRegistry.ts ├── style │ ├── arcStyle.ts │ ├── arrowStyle.ts │ ├── declarativeStyleItem.ts │ ├── modelFill.ts │ ├── shapesCategory.ts │ ├── styleFactory.ts │ ├── styleHelpers.ts │ ├── styleItem.ts │ ├── vectorStyleItem.ts │ └── writeStyle.ts ├── util │ ├── clipping │ │ ├── clippingObject.ts │ │ ├── clippingObjectManager.ts │ │ ├── clippingPlaneHelper.ts │ │ ├── clippingPolygonHelper.ts │ │ ├── clippingPolygonObject.ts │ │ └── clippingPolygonObjectCollection.ts │ ├── collection.ts │ ├── displayQuality │ │ └── displayQuality.ts │ ├── editor │ │ ├── createFeatureSession.ts │ │ ├── editFeaturesSession.ts │ │ ├── editGeometrySession.ts │ │ ├── editorHelpers.ts │ │ ├── editorSessionHelpers.ts │ │ ├── editorSymbols.ts │ │ ├── interactions │ │ │ ├── createBBoxInteraction.ts │ │ │ ├── createCircleInteraction.ts │ │ │ ├── createLineStringInteraction.ts │ │ │ ├── createPointInteraction.ts │ │ │ ├── createPolygonInteraction.ts │ │ │ ├── creationSnapping.ts │ │ │ ├── editFeaturesMouseOverInteraction.ts │ │ │ ├── editGeometryMouseOverInteraction.ts │ │ │ ├── ensureHandlerSelectionInteraction.ts │ │ │ ├── insertVertexInteraction.ts │ │ │ ├── layerSnapping.ts │ │ │ ├── mapInteractionController.ts │ │ │ ├── removeVertexInteraction.ts │ │ │ ├── rightClickInteraction.ts │ │ │ ├── segmentLengthInteraction.ts │ │ │ ├── selectFeatureMouseOverInteraction.ts │ │ │ ├── selectMultiFeatureInteraction.ts │ │ │ ├── selectSingleFeatureInteraction.ts │ │ │ ├── translateVertexInteraction.ts │ │ │ └── translationSnapping.ts │ │ ├── selectFeaturesSession.ts │ │ ├── snappingHelpers.ts │ │ ├── transformation │ │ │ ├── create2DHandlers.ts │ │ │ ├── create3DHandlers.ts │ │ │ ├── extrudeInteraction.ts │ │ │ ├── rotateInteraction.ts │ │ │ ├── scaleInteraction.ts │ │ │ ├── transformationHandler.ts │ │ │ ├── transformationTypes.ts │ │ │ └── translateInteraction.ts │ │ └── validateGeoemetry.ts │ ├── exclusiveManager.ts │ ├── extent.ts │ ├── featureconverter │ │ ├── arcToCesium.ts │ │ ├── circleToCesium.ts │ │ ├── clampedPrimitive.ts │ │ ├── convert.ts │ │ ├── extent3D.ts │ │ ├── lineStringToCesium.ts │ │ ├── pointHelpers.ts │ │ ├── pointToCesium.ts │ │ ├── polygonToCesium.ts │ │ ├── storeyHelpers.ts │ │ ├── vectorGeometryFactory.ts │ │ └── vectorHeightInfo.ts │ ├── fetch.ts │ ├── flight │ │ ├── flightAnchor.ts │ │ ├── flightCollection.ts │ │ ├── flightHelpers.ts │ │ ├── flightInstance.ts │ │ ├── flightPlayer.ts │ │ └── flightVisualizer.ts │ ├── geometryHelpers.ts │ ├── hiddenObjects.ts │ ├── indexedCollection.ts │ ├── isMobile.ts │ ├── layerCollection.ts │ ├── locale.ts │ ├── mapCollection.ts │ ├── math.ts │ ├── overrideCollection.ts │ ├── projection.ts │ ├── renderScreenshot.ts │ ├── rotation.ts │ ├── urlHelpers.ts │ ├── vcsTemplate.ts │ └── viewpoint.ts ├── vcsApp.ts ├── vcsEvent.ts ├── vcsModule.ts ├── vcsModuleHelpers.ts ├── vcsObject.ts └── vectorCluster │ ├── vectorClusterCesiumContext.ts │ ├── vectorClusterGroup.ts │ ├── vectorClusterGroupCesiumImpl.ts │ ├── vectorClusterGroupCollection.ts │ ├── vectorClusterGroupImpl.ts │ ├── vectorClusterGroupObliqueImpl.ts │ ├── vectorClusterGroupOpenlayersImpl.ts │ ├── vectorClusterStyleItem.ts │ └── vectorClusterSymbols.ts ├── tests ├── .eslintrc ├── data │ ├── dynamicPointCzml.json │ ├── oblique │ │ ├── imageData │ │ │ ├── imagev34.json │ │ │ ├── imagev35.json │ │ │ └── imagev35PerImageSize.json │ │ └── tiledImageData │ │ │ ├── 12 │ │ │ ├── 2199 │ │ │ │ ├── 1342.json │ │ │ │ ├── 1343.json │ │ │ │ └── 1344.json │ │ │ ├── 2200 │ │ │ │ ├── 1342.json │ │ │ │ ├── 1343.json │ │ │ │ └── 1344.json │ │ │ └── 2201 │ │ │ │ ├── 1342.json │ │ │ │ ├── 1343.json │ │ │ │ └── 1344.json │ │ │ └── image.json │ ├── terrain │ │ ├── 13 │ │ │ ├── 8800 │ │ │ │ ├── 6485.terrain │ │ │ │ └── 6486.terrain │ │ │ └── 8801 │ │ │ │ ├── 6485.terrain │ │ │ │ └── 6486.terrain │ │ └── layer.json │ ├── testGeoJSON.json │ ├── tile.pbf │ └── wgs84Points.fgb ├── setup.js ├── setupJsdom.js ├── tsconfig.json ├── unit │ ├── category │ │ ├── category.spec.js │ │ └── categoryCollection.spec.js │ ├── exclusiveManager.spec.js │ ├── featureProvider │ │ ├── abstractFeatureProvider.spec.js │ │ └── wmsFeatureProvider.spec.js │ ├── helpers │ │ ├── cesiumHelpers.js │ │ ├── getFileNameFromUrl.js │ │ ├── helpers.ts │ │ ├── imageHelpers.js │ │ ├── importJSON.js │ │ ├── obliqueData.js │ │ ├── obliqueHelpers.js │ │ ├── openlayersHelpers.js │ │ └── terrain │ │ │ └── terrainData.js │ ├── interaction │ │ ├── abstractInteraction.spec.js │ │ ├── coordinateAtPixel.spec.js │ │ ├── eventHandler.spec.js │ │ ├── featureAtPixelInteraction.spec.ts │ │ ├── featureProviderInteraction.spec.ts │ │ └── interactionChain.spec.js │ ├── layer │ │ ├── cesium │ │ │ ├── cesiumTilesetCesiumImpl.spec.js │ │ │ ├── dataSourceCesiumImpl.spec.js │ │ │ ├── getDummyCesium3DTileset.js │ │ │ ├── sourceVectorContextSync.spec.ts │ │ │ ├── vcsTile │ │ │ │ ├── vcsQuadtreeTileProvider.spec.ts │ │ │ │ ├── vcsTileHelpers.spec.ts │ │ │ │ └── vcsVectorTile.spec.ts │ │ │ ├── vectorCesiumImpl.spec.ts │ │ │ └── vectorContext.spec.ts │ │ ├── cesiumTilesetLayer.spec.js │ │ ├── czmlLayer.spec.js │ │ ├── dataSourceLayer.spec.js │ │ ├── featureLayer.spec.js │ │ ├── featureStoreFeatureVisibility.spec.ts │ │ ├── featureStoreLayer.spec.js │ │ ├── featureStoreLayerChanges.spec.js │ │ ├── featureVisibility.spec.ts │ │ ├── flatGeobufHelpers.spec.ts │ │ ├── geojsonHelpers.spec.ts │ │ ├── geojsonLayer.spec.js │ │ ├── globalHider.spec.js │ │ ├── layer.spec.js │ │ ├── layerImplementation.spec.js │ │ ├── oblique │ │ │ ├── obliqueHelpers.spec.ts │ │ │ └── sourceObliqueSync.spec.ts │ │ ├── openStreetMapLayer.spec.ts │ │ ├── openlayers │ │ │ ├── layerOpenlayersImpl.spec.js │ │ │ └── vectorTileOpenlayersImpl.spec.js │ │ ├── pointCloudLayer.spec.js │ │ ├── rasterLayer.spec.ts │ │ ├── singleImageLayer.spec.js │ │ ├── terrainHelpers.spec.js │ │ ├── terrainLayer.spec.js.js │ │ ├── tileProvider │ │ │ ├── flatGeobufTileProvider.spec.ts │ │ │ ├── mvtTileProvider.spec.js │ │ │ ├── staticGeojsonTileProvider.spec.js │ │ │ ├── tileProvider.spec.ts │ │ │ └── urlTemplateTileProvider.spec.js │ │ ├── tmsLayer.spec.js │ │ ├── vectorHelpers.spec.js │ │ ├── vectorLayer.spec.ts │ │ ├── vectorProperties.spec.ts │ │ ├── vectorTileLayer.spec.js │ │ ├── wfsLayer.spec.ts │ │ ├── wmsLayer.spec.ts │ │ └── wmtsLayer.spec.js │ ├── map │ │ ├── baseOLMap.spec.ts │ │ ├── cameraLimiter.spec.js │ │ ├── cesiumMap.spec.js │ │ ├── navigation │ │ │ ├── cameraHelper.spec.ts │ │ │ ├── cesiumNavigation.spec.ts │ │ │ ├── easingHelper.spec.ts │ │ │ ├── navigation.spec.ts │ │ │ ├── openlayersNavigation.spec.ts │ │ │ └── viewHelper.spec.ts │ │ ├── obliqueMap.spec.js │ │ ├── openlayersMap.spec.js │ │ └── vcsMap.spec.js │ ├── oblique │ │ ├── obliqueCollection.spec.js │ │ ├── obliqueDataSet.spec.js │ │ ├── obliqueImage.spec.js │ │ ├── obliqueImageMeta.spec.js │ │ ├── obliqueProvider.spec.js │ │ └── parseImageJson.spec.js │ ├── ol │ │ ├── geom │ │ │ ├── circle.spec.js │ │ │ └── geometryCollection.spec.js │ │ └── render │ │ │ └── canvas │ │ │ └── canvasTileRenderer.spec.js │ ├── overrideClassRegistry.spec.js │ ├── style │ │ ├── arcStyle.spec.js │ │ ├── arrowStyle.spec.js │ │ ├── declarativeStyleItem.spec.js │ │ ├── styleFactory.spec.ts │ │ ├── styleHelpers.spec.js │ │ └── writeStyle.spec.js │ ├── util │ │ ├── clipping │ │ │ ├── clippingObject.spec.js │ │ │ ├── clippingObjectManager.spec.js │ │ │ ├── clippingPlaneHelper.spec.js │ │ │ ├── clippingPolygonObject.spec.ts │ │ │ └── clippingPolygonObjectCollection.spec.ts │ │ ├── collection.spec.js │ │ ├── displayQuality │ │ │ └── displayQuality.spec.ts │ │ ├── editor │ │ │ ├── createFeatureSession.spec.ts │ │ │ ├── editFeaturesSession.spec.ts │ │ │ ├── editGeometrySession.spec.ts │ │ │ ├── editorHelpers.spec.ts │ │ │ ├── interactions │ │ │ │ ├── createBBoxInteraction.spec.ts │ │ │ │ ├── createCircleInteraction.spec.ts │ │ │ │ ├── createLineStringInteraction.spec.ts │ │ │ │ ├── createPointInteraction.spec.ts │ │ │ │ ├── createPolygonInteraction.spec.ts │ │ │ │ ├── creationSnapping.spec.ts │ │ │ │ ├── editFeaturesMouseOverInteraction.spec.js │ │ │ │ ├── editGeometryMouseOverInteraction.spec.js │ │ │ │ ├── ensureHandlerSelectionInteraction.spec.js │ │ │ │ ├── insertVertexInteraction.spec.ts │ │ │ │ ├── layerSnapping.spec.ts │ │ │ │ ├── mapInteractionController.spec.js │ │ │ │ ├── segmentLengthInteraction.spec.ts │ │ │ │ ├── selectFeatureMouseOverInteraction.spec.js │ │ │ │ ├── selectMultiFeatureInteraction.spec.js │ │ │ │ ├── selectSingleFeatureInteraction.spec.js │ │ │ │ ├── translateVertexInteraction.spec.ts │ │ │ │ └── translationSnapping.spec.ts │ │ │ ├── selectFeaturesSession.spec.js │ │ │ └── transformation │ │ │ │ ├── create2DHandlers.spec.ts │ │ │ │ ├── create3DHandlers.spec.ts │ │ │ │ ├── extrudeInteraction.spec.js │ │ │ │ ├── rotateInteraction.spec.js │ │ │ │ ├── scaleInteraction.spec.ts │ │ │ │ ├── setupTransformationHandler.ts │ │ │ │ ├── transformationHandler.spec.ts │ │ │ │ └── translateInteraction.spec.js │ │ ├── extent.spec.js │ │ ├── featureconverter │ │ │ ├── circleToCesium.spec.ts │ │ │ ├── clampedPrimitive.spec.ts │ │ │ ├── convert.spec.ts │ │ │ ├── extent3D.spec.ts │ │ │ ├── lineStringToCesium.spec.ts │ │ │ ├── pointHelpers.spec.ts │ │ │ ├── pointToCesium.spec.ts │ │ │ ├── polygonToCesium.spec.ts │ │ │ ├── storeyHelpers.spec.ts │ │ │ ├── vectorGeometryFactory.spec.ts │ │ │ └── vectorHeightInfo.spec.ts │ │ ├── flight │ │ │ ├── flightCollection.spec.ts │ │ │ ├── flightHelpers.spec.ts │ │ │ ├── flightInstance.spec.ts │ │ │ ├── flightPlayer.spec.ts │ │ │ ├── flightVisualizer.spec.ts │ │ │ └── getDummyFlightInstance.ts │ │ ├── geometryHelpers.spec.ts │ │ ├── hiddenObject.spec.ts │ │ ├── indexedCollection.spec.js │ │ ├── layerCollection.spec.ts │ │ ├── mapCollection.spec.js │ │ ├── math.spec.ts │ │ ├── overrideCollection.spec.js │ │ ├── projection.spec.js │ │ ├── rotation.spec.ts │ │ ├── urlHelpers.spec.ts │ │ ├── vcsTemplate.spec.ts │ │ └── viewpoint.spec.ts │ ├── vcsApp.spec.ts │ ├── vcsEvent.spec.ts │ ├── vcsModule.spec.ts │ ├── vcsObject.spec.js │ └── vectorCluster │ │ ├── vectorClusterCesiumContext.spec.ts │ │ ├── vectorClusterGroup.spec.ts │ │ ├── vectorClusterGroupCesiumImpl.spec.ts │ │ ├── vectorClusterGroupCollection.spec.ts │ │ ├── vectorClusterGroupImpl.spec.ts │ │ ├── vectorClusterGroupObliqueImpl.spec.ts │ │ ├── vectorClusterGroupOpenlayersImpl.spec.ts │ │ └── vectorClusterStyleItem.spec.ts └── vcs.js ├── tsconfig.json ├── typedoc.json └── types ├── rbush-knn.d.ts └── rbush.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | build/types/vcs.d.ts 4 | build/types/Cesium_module.d.ts 5 | test-results.xml 6 | docs/ 7 | dist/ 8 | .tests/ 9 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jsLinters/eslint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/prettier.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /.idea/vcmap-core.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | index.d.ts 2 | build/types/vcs.d.ts 3 | build/types/Cesium_module.d.ts 4 | coverage/ 5 | docs/ 6 | dist/ 7 | .tests/ 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 virtualcitySYSTEMS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /build/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "import/no-extraneous-dependencies": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /build/postBuild.js: -------------------------------------------------------------------------------- 1 | import { dirname, join as joinPath, basename } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { readFile, writeFile, appendFile } from 'node:fs/promises'; 4 | import { EOL } from 'node:os'; 5 | 6 | const dirName = dirname(fileURLToPath(import.meta.url)); 7 | const rootDir = joinPath(dirName, '..'); 8 | const distDir = joinPath(rootDir, 'dist'); 9 | 10 | const augmentations = [ 11 | joinPath(rootDir, 'src', 'cesium', 'cesium.d.ts'), 12 | joinPath(rootDir, 'src', 'ol', 'ol.d.ts'), 13 | joinPath(rootDir, 'src', 'ol', 'geojson.d.ts'), 14 | ]; 15 | 16 | async function moveAugmentation(filePath) { 17 | let content = await readFile(filePath, 'utf-8'); 18 | content = content.replace(/from\s'\.\.\//g, "from './src/"); 19 | const distName = joinPath(distDir, basename(filePath)); 20 | await writeFile(distName, content); 21 | } 22 | async function moveAugmentations() { 23 | await Promise.all(augmentations.map(moveAugmentation)); 24 | const importStatements = augmentations 25 | .map((a) => basename(a)) 26 | .map((name) => `import './${name}';`); 27 | 28 | const indexFileName = joinPath(distDir, 'index.d.ts'); 29 | await appendFile(indexFileName, importStatements.join(EOL)); 30 | } 31 | 32 | moveAugmentations() 33 | .then(() => { 34 | console.log('Augmentations moved.'); 35 | }) 36 | .catch((e) => { 37 | console.error('Failed to move augmentations.'); 38 | console.error(e); 39 | }); 40 | -------------------------------------------------------------------------------- /documentation/VcsLayer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/861d2a016536d461cccd0a8fcf6fcd6ddeec249f/documentation/VcsLayer.png -------------------------------------------------------------------------------- /documentation/renderScreenshot.md: -------------------------------------------------------------------------------- 1 | # Render Screenshot Utility Documentation 2 | 3 | ## Overview 4 | 5 | print provides functionality to create screenshots from different map types (Cesium, Openlayers, Oblique) and handle the resulting image blob. The main function described here is `renderScreenshot`. 6 | 7 | ## Functions 8 | 9 | ### `renderScreenshot` 10 | 11 | This function prepares the map for a screenshot and returns a canvas element with the rendered image. 12 | 13 | #### Parameters 14 | 15 | - `app` (`VcsApp`): The VcsApp instance. 16 | - `width` (`number`): The width of the screenshot in pixels. 17 | 18 | #### Returns 19 | 20 | - `Promise`: A promise that resolves to the canvas element containing the screenshot. 21 | 22 | #### Usage 23 | 24 | ```typescript 25 | import { renderScreenshot } from './src/util/print'; 26 | import VcsApp from './src/vcsApp'; 27 | 28 | const app = new VcsApp(); 29 | const width = 1920; 30 | 31 | renderScreenshot(app, width).then((canvas) => { 32 | // Use the canvas element 33 | }); 34 | ``` 35 | 36 | ## Notes 37 | 38 | - Ensure that the map instance is properly initialized before calling this function. 39 | - The function supports different map types including Cesium, Openlayers, and Oblique. 40 | - The function handles the preparation and resetting of the map state before and after taking the screenshot. 41 | -------------------------------------------------------------------------------- /documentation/vcsApp.md: -------------------------------------------------------------------------------- 1 | # VcsApp 2 | 3 | The [VcsApp](../src/vcsApp.ts) is the main class of a VC Map application. 4 | One or multiple instances of a VcsApp can (co)exist and be embedded in a Website. 5 | 6 | The VcsApp implements the module concept, which allows to build modular applications. 7 | It has the capability to serialize and deserialize its modules. 8 | 9 | ## Collections 10 | 11 | An VcsApp consists of the following [collections](../src/util/collection.ts) containing deserialized items defining the VcsApp's content: 12 | 13 | - modules 14 | - [maps](./maps.md) 15 | - [layers](./layers.md) 16 | - obliqueCollections 17 | - [styles](./style.md) 18 | - viewpoints 19 | - categories 20 | - hiddenObjects 21 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es6", 4 | "target": "es6", 5 | "baseUrl": "./", 6 | "moduleResolution": "node", 7 | "paths": { 8 | "@vcmap/core": ["index.js"] 9 | } 10 | }, 11 | "include": ["./src/**/*", "index"] 12 | } 13 | -------------------------------------------------------------------------------- /src/cesium/cesium3DTileFeature.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Cesium3DTileFeature, 3 | Cesium3DTilePointFeature, 4 | } from '@vcmap-cesium/engine'; 5 | 6 | Cesium3DTileFeature.prototype.getId = function getId( 7 | this: Cesium3DTileFeature, 8 | ): string | number { 9 | return ( 10 | (this.getProperty('id') as string | number) || 11 | `${this.content.url}${this._batchId}` 12 | ); // XXX there is a new property `featureId` on the Cesium3DTileset. this may cause issues when picking b3dm. 13 | }; 14 | 15 | // eslint-disable-next-line import/prefer-default-export 16 | export function getAttributes( 17 | this: Cesium3DTileFeature | Cesium3DTilePointFeature, 18 | ): Record { 19 | if ((this.tileset.asset as { version: string })?.version === '1.1') { 20 | const attributes: Record = {}; 21 | this.getPropertyIds().forEach((id) => { 22 | attributes[id] = this.getProperty(id); 23 | }); 24 | return attributes; 25 | } 26 | return this.getProperty('attributes') as Record; 27 | } 28 | 29 | Cesium3DTileFeature.prototype.getAttributes = getAttributes; 30 | -------------------------------------------------------------------------------- /src/cesium/cesium3DTilePointFeature.ts: -------------------------------------------------------------------------------- 1 | import { Cesium3DTilePointFeature } from '@vcmap-cesium/engine'; 2 | import { getAttributes } from './cesium3DTileFeature.js'; 3 | 4 | Cesium3DTilePointFeature.prototype.getId = function getId( 5 | this: Cesium3DTilePointFeature, 6 | ): string | number { 7 | return ( 8 | (this.getProperty('id') as string | number) || 9 | `${this.content.url}${this._batchId}` 10 | ); 11 | }; 12 | 13 | Cesium3DTilePointFeature.prototype.getAttributes = getAttributes; 14 | -------------------------------------------------------------------------------- /src/cesium/clippingPolygon.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Cartesian3, 3 | defined, 4 | ClippingPolygon, 5 | Rectangle, 6 | } from '@vcmap-cesium/engine'; 7 | 8 | function equalArrayCartesian3( 9 | flatPositions: number[], 10 | cartesian3s: Cartesian3[], 11 | ): boolean { 12 | if (defined(flatPositions) !== defined(cartesian3s)) { 13 | return false; 14 | } 15 | if (flatPositions.length !== cartesian3s.length * 3) { 16 | return false; 17 | } 18 | const n = cartesian3s.length; 19 | for (let i = 0; i < n; i++) { 20 | if ( 21 | flatPositions[i * 3] !== cartesian3s[i].x || 22 | flatPositions[i * 3 + 1] !== cartesian3s[i].y || 23 | flatPositions[i * 3 + 2] !== cartesian3s[i].z 24 | ) { 25 | return false; 26 | } 27 | } 28 | return true; 29 | } 30 | // eslint-disable-next-line @typescript-eslint/unbound-method 31 | const originalComputeRectangle = ClippingPolygon.prototype.computeRectangle; 32 | ClippingPolygon.prototype.computeRectangle = function computeRectangle( 33 | result, 34 | ): Rectangle { 35 | if (equalArrayCartesian3(this._cachedPackedCartesians, this.positions)) { 36 | return Rectangle.clone(this._cachedRectangle, result); 37 | } 38 | this._cachedPackedCartesians = Cartesian3.packArray( 39 | this.positions, 40 | new Array(this.positions.length * 3), 41 | ); 42 | const rectangle = originalComputeRectangle.call(this, result); 43 | this._cachedRectangle = Rectangle.clone(rectangle); 44 | return rectangle; 45 | }; 46 | -------------------------------------------------------------------------------- /src/cesium/clippingPolygonCollection.ts: -------------------------------------------------------------------------------- 1 | import { ClippingPolygonCollection } from '@vcmap-cesium/engine'; 2 | 3 | ClippingPolygonCollection.prototype.setDirty = function setDirty(): void { 4 | this._totalPositions = -1; 5 | }; 6 | -------------------------------------------------------------------------------- /src/cesium/entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '@vcmap-cesium/engine'; 2 | 3 | Entity.prototype.getId = function getId(this: Entity): string | number { 4 | return this.id; 5 | }; 6 | 7 | /** 8 | * To be used for cesium 3D style functions 9 | */ 10 | Entity.prototype.getProperty = function getProperty( 11 | this: Entity, 12 | property: string, 13 | ): any { 14 | return this[property as keyof Entity]; 15 | }; 16 | 17 | Entity.prototype.getAttributes = function getAttributes(): Record< 18 | string, 19 | unknown 20 | > { 21 | return this.properties ?? {}; 22 | }; 23 | 24 | /** 25 | * To be used for cesium 3D style functions 26 | */ 27 | Entity.prototype.getPropertyInherited = function getPropertyInherited( 28 | this: Entity, 29 | property: string, 30 | ): any { 31 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 32 | return this.getProperty(property); 33 | }; 34 | -------------------------------------------------------------------------------- /src/cesium/wallpaperMaterial.js: -------------------------------------------------------------------------------- 1 | import { Material, Cartesian2 } from '@vcmap-cesium/engine'; 2 | 3 | /** 4 | * @file Wallpaper Material to implement openlayers pattern support in cesium 5 | */ 6 | 7 | // Call this once at application startup 8 | // eslint-disable-next-line no-underscore-dangle 9 | Material._materialCache.addMaterial('Wallpaper', { 10 | fabric: { 11 | type: 'Wallpaper', 12 | uniforms: { 13 | image: Material.DefaultImageId, 14 | anchor: new Cartesian2(0, 0), 15 | }, 16 | components: { 17 | diffuse: 18 | 'texture2D(image, fract((gl_FragCoord.xy - anchor.xy) / vec2(imageDimensions.xy))).rgb', 19 | alpha: 20 | 'texture2D(image, fract((gl_FragCoord.xy - anchor.xy) / vec2(imageDimensions.xy))).a', 21 | }, 22 | }, 23 | translucent: false, 24 | }); 25 | 26 | // //Create an instance and assign to anything that has a material property. 27 | // //scene - the scene 28 | // //image - the image (I think both a url or Image object are supported) 29 | // //anchor - A Cartesian3 that is the most southern and westard point of the geometry 30 | // var WallPaperMaterialProperty = function(scene, image, anchor) { 31 | // this._scene = scene; 32 | // this._image = image; 33 | // this._anchor = anchor; 34 | // this.definitionChanged = new Cesium.Event(); 35 | // this.isConstant = true; 36 | // }; 37 | // 38 | // WallPaperMaterialProperty.prototype.getType = function(time) { 39 | // return 'Wallpaper'; 40 | // }; 41 | // 42 | // WallPaperMaterialProperty.prototype.getValue = function(time, result) { 43 | // if (!Cesium.defined(result)) { 44 | // result = { 45 | // image : undefined, 46 | // anchor : undefined 47 | // }; 48 | // } 49 | // 50 | // result.image = this._image; 51 | // result.anchor = Cesium.SceneTransforms.wgs84ToDrawingBufferCoordinates(this._scene, this._anchor, result.anchor); 52 | // if(Cesium.defined(result.anchor)){ 53 | // result.anchor.x = Math.floor(result.anchor.x); 54 | // result.anchor.y = Math.floor(result.anchor.y); 55 | // } else { 56 | // result.anchor = new Cesium.Cartesian2(0, 0); 57 | // } 58 | // return result; 59 | // }; 60 | // 61 | // WallPaperMaterialProperty.prototype.equals = function(other) { 62 | // return this === other || // 63 | // (other instanceof WallPaperMaterialProperty && // 64 | // this._image === other._image); 65 | // }; 66 | -------------------------------------------------------------------------------- /src/featureProvider/featureProviderSymbols.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Added to ol.Feature, if they are not part of a layer, but provided by an {@link AbstractFeatureProvider}. 3 | */ 4 | export const isProvidedFeature: unique symbol = Symbol('isProvidedFeature'); 5 | 6 | /** 7 | * Added to ol.Feature, if a {@link AbstractFeatureProvider} provides more than one feature for one location. 8 | * The provided feature is a cluster feature. The single features can be accessed by `feature.get('features')`. 9 | */ 10 | export const isProvidedClusterFeature = Symbol('isProvidedClusterFeature'); 11 | -------------------------------------------------------------------------------- /src/featureProvider/tileProviderFeatureProvider.ts: -------------------------------------------------------------------------------- 1 | import type { Coordinate } from 'ol/coordinate.js'; 2 | import type { Feature } from 'ol/index.js'; 3 | import AbstractFeatureProvider, { 4 | type AbstractFeatureProviderOptions, 5 | } from './abstractFeatureProvider.js'; 6 | import { featureProviderClassRegistry } from '../classRegistry.js'; 7 | import type TileProvider from '../layer/tileProvider/tileProvider.js'; 8 | 9 | export type TileProviderFeatureProviderOptions = 10 | AbstractFeatureProviderOptions & { 11 | tileProvider: TileProvider; 12 | }; 13 | 14 | class TileProviderFeatureProvider extends AbstractFeatureProvider { 15 | static get className(): string { 16 | return 'TileProviderFeatureProvider'; 17 | } 18 | 19 | tileProvider: TileProvider; 20 | 21 | /** 22 | * @param layerName 23 | * @param options 24 | */ 25 | constructor(layerName: string, options: TileProviderFeatureProviderOptions) { 26 | super(layerName, options); 27 | 28 | this.mapTypes = ['CesiumMap']; 29 | this.tileProvider = options.tileProvider; 30 | } 31 | 32 | async getFeaturesByCoordinate( 33 | coordinate: Coordinate, 34 | resolution: number, 35 | headers?: Record, 36 | ): Promise { 37 | const features = await this.tileProvider.getFeaturesByCoordinate( 38 | coordinate, 39 | resolution, 40 | headers, 41 | ); 42 | const checkShow = (feature: Feature): boolean => 43 | this.style ? !!this.style.cesiumStyle.show.evaluate(feature) : true; 44 | return features.filter((feature) => { 45 | return ( 46 | this.vectorProperties.getAllowPicking(feature) && checkShow(feature) 47 | ); 48 | }); 49 | } 50 | 51 | destroy(): void { 52 | this.tileProvider.destroy(); 53 | super.destroy(); 54 | } 55 | } 56 | 57 | export default TileProviderFeatureProvider; 58 | featureProviderClassRegistry.registerClass( 59 | TileProviderFeatureProvider.className, 60 | TileProviderFeatureProvider, 61 | ); 62 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | import type VcsApp from './vcsApp.js'; 2 | import { mouseOverSymbol } from './util/editor/editorSymbols.js'; 3 | // eslint-disable-next-line import/no-named-default 4 | import type { default as VcsModule, VcsModuleConfig } from './vcsModule.js'; 5 | 6 | declare global { 7 | interface Window { 8 | vcs: { 9 | apps: Map; 10 | createModuleFromConfig: (config: VcsModuleConfig) => VcsModule; 11 | }; 12 | opera?: string; 13 | } 14 | interface CSSStyleDeclaration { 15 | [mouseOverSymbol]?: string; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/interaction/featureProviderInteraction.ts: -------------------------------------------------------------------------------- 1 | import type { Coordinate } from 'ol/coordinate.js'; 2 | 3 | import Point from 'ol/geom/Point.js'; 4 | import Feature from 'ol/Feature.js'; 5 | import AbstractInteraction, { 6 | type InteractionEvent, 7 | } from './abstractInteraction.js'; 8 | import { 9 | EventType, 10 | ModificationKeyType, 11 | PointerKeyType, 12 | } from './interactionType.js'; 13 | import { 14 | isProvidedClusterFeature, 15 | isProvidedFeature, 16 | } from '../featureProvider/featureProviderSymbols.js'; 17 | 18 | /** 19 | * @group Interaction 20 | */ 21 | class FeatureProviderInteraction extends AbstractInteraction { 22 | constructor() { 23 | super(EventType.CLICK, ModificationKeyType.ALL, PointerKeyType.ALL); 24 | 25 | this.setActive(); 26 | } 27 | 28 | // eslint-disable-next-line class-methods-use-this 29 | async pipe(event: InteractionEvent): Promise { 30 | if (event.feature) { 31 | return event; 32 | } 33 | 34 | const layersWithProvider = [...event.map.layerCollection] 35 | .filter((l) => { 36 | return ( 37 | l.featureProvider && 38 | l.active && 39 | l.isSupported(event.map) && 40 | l.featureProvider.isSupported(event.map) 41 | ); 42 | }) 43 | .reverse(); 44 | 45 | if (layersWithProvider.length > 0) { 46 | const resolution = event.map.getCurrentResolution( 47 | event.position as Coordinate, 48 | ); 49 | // TODO make sure the layers are rendered, check min/max RenderingResolution 50 | const features = ( 51 | await Promise.all( 52 | layersWithProvider.map( 53 | (l) => 54 | l.featureProvider?.getFeaturesByCoordinate?.( 55 | event.position as Coordinate, 56 | resolution, 57 | l.headers, 58 | ), 59 | ), 60 | ) 61 | ) 62 | .filter((f) => !!f) 63 | .flat(); 64 | if (features.length === 1) { 65 | event.feature = features[0]; 66 | } else if (features.length > 1) { 67 | const feature = new Feature({ features }); 68 | feature[isProvidedFeature] = true; // backward compatibility, may remove in future 69 | feature[isProvidedClusterFeature] = true; 70 | feature.setGeometry(new Point(event.position as Coordinate)); 71 | event.feature = feature; 72 | } 73 | } 74 | 75 | return event; 76 | } 77 | } 78 | 79 | export default FeatureProviderInteraction; 80 | -------------------------------------------------------------------------------- /src/interaction/interactionType.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enumeration of modification key types 3 | */ 4 | export enum ModificationKeyType { 5 | NONE = 2, 6 | ALT = 4, 7 | CTRL = 8, 8 | SHIFT = 16, 9 | ALL = NONE | ALT | CTRL | SHIFT, 10 | } 11 | 12 | /** 13 | * Enumeration of pointer event types 14 | */ 15 | export enum EventType { 16 | NONE = 0, 17 | CLICK = 32, 18 | DBLCLICK = 64, 19 | DRAG = 128, 20 | DRAGSTART = 256, 21 | DRAGEND = 512, 22 | MOVE = 1024, 23 | DRAGEVENTS = DRAG | DRAGSTART | DRAGEND, 24 | CLICKMOVE = CLICK | MOVE, 25 | ALL = CLICK | DBLCLICK | DRAG | DRAGSTART | DRAGEND | MOVE, 26 | } 27 | 28 | /** 29 | * Enumeration of pointer keys. 30 | */ 31 | export enum PointerKeyType { 32 | LEFT = 2048, 33 | RIGHT = 4096, 34 | MIDDLE = 8192, 35 | ALL = LEFT | RIGHT | MIDDLE, 36 | } 37 | 38 | /** 39 | * Enumeration of pointer key events. 40 | */ 41 | export enum PointerEventType { 42 | DOWN = 1, 43 | UP = 2, 44 | MOVE = 3, 45 | } 46 | -------------------------------------------------------------------------------- /src/layer/cesium/openStreetMapCesiumImpl.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OpenStreetMapImageryProvider, 3 | ImageryLayer as CesiumImageryLayer, 4 | } from '@vcmap-cesium/engine'; 5 | import RasterLayerCesiumImpl from './rasterLayerCesiumImpl.js'; 6 | 7 | /** 8 | * represents a specific OpenStreetMapLayer layer for cesium. 9 | */ 10 | class OpenStreetMapCesiumImpl extends RasterLayerCesiumImpl { 11 | static get className(): string { 12 | return 'OpenStreetMapCesiumImpl'; 13 | } 14 | 15 | getCesiumLayer(): Promise { 16 | const layerOptions = this.getCesiumLayerOptions(); 17 | return Promise.resolve( 18 | new CesiumImageryLayer( 19 | new OpenStreetMapImageryProvider({ maximumLevel: this.maxLevel }), 20 | layerOptions, 21 | ), 22 | ); 23 | } 24 | } 25 | 26 | export default OpenStreetMapCesiumImpl; 27 | -------------------------------------------------------------------------------- /src/layer/cesium/resourceHelper.ts: -------------------------------------------------------------------------------- 1 | import { Resource } from '@vcmap-cesium/engine'; 2 | 3 | // eslint-disable-next-line import/prefer-default-export 4 | export function getResourceOrUrl( 5 | url: string, 6 | headers?: Record, 7 | ): string | Resource { 8 | if (headers) { 9 | return new Resource({ 10 | url, 11 | headers, 12 | }); 13 | } 14 | return url; 15 | } 16 | -------------------------------------------------------------------------------- /src/layer/cesium/singleImageCesiumImpl.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Rectangle, 3 | SingleTileImageryProvider, 4 | ImageryLayer, 5 | } from '@vcmap-cesium/engine'; 6 | import RasterLayerCesiumImpl from './rasterLayerCesiumImpl.js'; 7 | import { wgs84Projection } from '../../util/projection.js'; 8 | import type { SingleImageImplementationOptions } from '../singleImageLayer.js'; 9 | import type CesiumMap from '../../map/cesiumMap.js'; 10 | import { getResourceOrUrl } from './resourceHelper.js'; 11 | 12 | /** 13 | * represents a specific Cesium SingleTileImagery Layer class. 14 | */ 15 | class SingleImageCesiumImpl extends RasterLayerCesiumImpl { 16 | static get className(): string { 17 | return 'SingleImageCesiumImpl'; 18 | } 19 | 20 | credit: string | undefined; 21 | 22 | constructor(map: CesiumMap, options: SingleImageImplementationOptions) { 23 | super(map, options); 24 | this.credit = options.credit; 25 | } 26 | 27 | async getCesiumLayer(): Promise { 28 | const options: SingleTileImageryProvider.fromUrlOptions = { 29 | credit: this.credit, 30 | }; 31 | 32 | const extent = this.extent?.getCoordinatesInProjection(wgs84Projection); 33 | if (extent) { 34 | options.rectangle = Rectangle.fromDegrees( 35 | extent[0], 36 | extent[1], 37 | extent[2], 38 | extent[3], 39 | ); 40 | } 41 | 42 | const imageryProvider = await SingleTileImageryProvider.fromUrl( 43 | getResourceOrUrl(this.url!, this.headers), 44 | options, 45 | ); 46 | const layerOptions = this.getCesiumLayerOptions(); 47 | layerOptions.rectangle = options.rectangle; 48 | return new ImageryLayer(imageryProvider, layerOptions); 49 | } 50 | } 51 | 52 | export default SingleImageCesiumImpl; 53 | -------------------------------------------------------------------------------- /src/layer/cesium/terrainCesiumImpl.ts: -------------------------------------------------------------------------------- 1 | import type { CesiumTerrainProvider } from '@vcmap-cesium/engine'; 2 | import LayerImplementation from '../layerImplementation.js'; 3 | import { vcsLayerName } from '../layerSymbols.js'; 4 | import { getTerrainProviderForUrl } from '../terrainHelpers.js'; 5 | import CesiumMap from '../../map/cesiumMap.js'; 6 | import type { TerrainImplementationOptions } from '../terrainLayer.js'; 7 | 8 | /** 9 | * TerrainLayer implementation for {@link CesiumMap} 10 | */ 11 | class TerrainCesiumImpl extends LayerImplementation { 12 | static get className(): string { 13 | return 'TerrainCesiumImpl'; 14 | } 15 | 16 | requestVertexNormals: boolean; 17 | 18 | requestWaterMask: boolean; 19 | 20 | terrainProvider: CesiumTerrainProvider | undefined = undefined; 21 | 22 | constructor(map: CesiumMap, options: TerrainImplementationOptions) { 23 | super(map, options); 24 | 25 | this.requestVertexNormals = options.requestVertexNormals; 26 | this.requestWaterMask = options.requestWaterMask; 27 | } 28 | 29 | async initialize(): Promise { 30 | if (!this.initialized) { 31 | this.terrainProvider = await getTerrainProviderForUrl( 32 | this.url!, 33 | { 34 | requestVertexNormals: this.requestVertexNormals, 35 | requestWaterMask: this.requestWaterMask, 36 | }, 37 | this.headers, 38 | ); 39 | this.terrainProvider[vcsLayerName] = this.name; 40 | } 41 | return super.initialize(); 42 | } 43 | 44 | async activate(): Promise { 45 | await super.activate(); 46 | if (this.active && this.terrainProvider) { 47 | this.map.setTerrainProvider(this.terrainProvider); 48 | } 49 | } 50 | 51 | deactivate(): void { 52 | super.deactivate(); 53 | if (this.terrainProvider) { 54 | this.map.unsetTerrainProvider(this.terrainProvider); 55 | } 56 | } 57 | 58 | destroy(): void { 59 | if (this.terrainProvider) { 60 | this.map.unsetTerrainProvider(this.terrainProvider); 61 | } 62 | this.terrainProvider = undefined; 63 | super.destroy(); 64 | } 65 | } 66 | 67 | export default TerrainCesiumImpl; 68 | -------------------------------------------------------------------------------- /src/layer/cesium/tmsCesiumImpl.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Rectangle, 3 | GeographicTilingScheme, 4 | TileMapServiceImageryProvider, 5 | ImageryLayer as CesiumImageryLayer, 6 | } from '@vcmap-cesium/engine'; 7 | import RasterLayerCesiumImpl from './rasterLayerCesiumImpl.js'; 8 | import { wgs84Projection } from '../../util/projection.js'; 9 | import { TilingScheme } from '../rasterLayer.js'; 10 | import type CesiumMap from '../../map/cesiumMap.js'; 11 | import type { TMSImplementationOptions } from '../tmsLayer.js'; 12 | import { getResourceOrUrl } from './resourceHelper.js'; 13 | 14 | /** 15 | * TmsLayer implementation for {@link CesiumMap}. 16 | */ 17 | class TmsCesiumImpl extends RasterLayerCesiumImpl { 18 | static get className(): string { 19 | return 'TmsCesiumImpl'; 20 | } 21 | 22 | format: string; 23 | 24 | constructor(map: CesiumMap, options: TMSImplementationOptions) { 25 | super(map, options); 26 | this.format = options.format; 27 | } 28 | 29 | async getCesiumLayer(): Promise { 30 | const options: TileMapServiceImageryProvider.ConstructorOptions = { 31 | fileExtension: this.format, 32 | maximumLevel: this.maxLevel, 33 | minimumLevel: this.minLevel, 34 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 35 | // @ts-ignore 36 | show: false, 37 | }; 38 | 39 | if (this.extent && this.extent.isValid()) { 40 | const extent = this.extent.getCoordinatesInProjection(wgs84Projection); 41 | options.rectangle = Rectangle.fromDegrees( 42 | extent[0], 43 | extent[1], 44 | extent[2], 45 | extent[3], 46 | ); 47 | } 48 | if (this.tilingSchema === TilingScheme.GEOGRAPHIC) { 49 | options.tilingScheme = new GeographicTilingScheme(); 50 | } 51 | const imageryProvider = await TileMapServiceImageryProvider.fromUrl( 52 | getResourceOrUrl(this.url!, this.headers), 53 | options, 54 | ); 55 | 56 | const layerOptions = this.getCesiumLayerOptions(); 57 | return new CesiumImageryLayer(imageryProvider, layerOptions); 58 | } 59 | } 60 | 61 | export default TmsCesiumImpl; 62 | -------------------------------------------------------------------------------- /src/layer/cesium/vcsTile/vcsChildTile.ts: -------------------------------------------------------------------------------- 1 | import { QuadtreeTile, TileBoundingRegion } from '@vcmap-cesium/engine'; 2 | import { 3 | getTileBoundingRegion, 4 | VcsTile, 5 | VcsTileState, 6 | VcsTileType, 7 | } from './vcsTileHelpers.js'; 8 | import type CesiumMap from '../../../map/cesiumMap.js'; 9 | 10 | export default class VcsChildTile implements VcsTile { 11 | state = VcsTileState.LOADING; 12 | 13 | type = VcsTileType.CHILD; 14 | 15 | tileBoundingRegion: TileBoundingRegion; 16 | 17 | private _tile: QuadtreeTile; 18 | 19 | constructor(tile: QuadtreeTile, map: CesiumMap) { 20 | this.tileBoundingRegion = getTileBoundingRegion(tile, map); 21 | this.state = VcsTileState.READY; 22 | this._tile = tile; 23 | } 24 | 25 | get show(): boolean { 26 | return this._tile.parent?.data?.show ?? false; 27 | } 28 | 29 | // eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-empty-function 30 | set show(_show: boolean) {} 31 | } 32 | -------------------------------------------------------------------------------- /src/layer/cesium/vcsTile/vcsNoDataTile.ts: -------------------------------------------------------------------------------- 1 | import { QuadtreeTile, TileBoundingRegion } from '@vcmap-cesium/engine'; 2 | import type CesiumMap from '../../../map/cesiumMap.js'; 3 | import { 4 | getTileBoundingRegion, 5 | VcsTile, 6 | VcsTileState, 7 | VcsTileType, 8 | } from './vcsTileHelpers.js'; 9 | 10 | export default class VcsNoDataTile implements VcsTile { 11 | state = VcsTileState.LOADING; 12 | 13 | type = VcsTileType.NO_DATA; 14 | 15 | tileBoundingRegion: TileBoundingRegion; 16 | 17 | constructor(tile: QuadtreeTile, map: CesiumMap) { 18 | this.tileBoundingRegion = getTileBoundingRegion(tile, map); 19 | 20 | this.state = VcsTileState.READY; 21 | } 22 | 23 | // eslint-disable-next-line class-methods-use-this 24 | get show(): boolean { 25 | return false; 26 | } 27 | 28 | // eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-empty-function 29 | set show(_show: boolean) {} 30 | } 31 | -------------------------------------------------------------------------------- /src/layer/cesium/vectorTileCesiumImpl.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PrimitiveCollection, 3 | QuadtreePrimitive, 4 | SplitDirection, 5 | } from '@vcmap-cesium/engine'; 6 | import StyleItem from '../../style/styleItem.js'; 7 | import LayerImplementation from '../layerImplementation.js'; 8 | import type CesiumMap from '../../map/cesiumMap.js'; 9 | import { 10 | VectorTileImplementation, 11 | VectorTileImplementationOptions, 12 | } from '../vectorTileLayer.js'; 13 | import { vcsLayerName } from '../layerSymbols.js'; 14 | import VcsQuadtreeTileProvider from './vcsTile/vcsQuadtreeTileProvider.js'; 15 | 16 | export default class VectorTileCesiumImpl 17 | extends LayerImplementation 18 | implements VectorTileImplementation 19 | { 20 | static get className(): string { 21 | return 'VectorTileCesiumImpl'; 22 | } 23 | 24 | private _quadtreeProvider: VcsQuadtreeTileProvider; 25 | 26 | private _quadtreePrimitive: QuadtreePrimitive; 27 | 28 | private _primitiveCollection = new PrimitiveCollection(); 29 | 30 | constructor(map: CesiumMap, options: VectorTileImplementationOptions) { 31 | super(map, options); 32 | this._quadtreeProvider = new VcsQuadtreeTileProvider( 33 | map, 34 | this._primitiveCollection, 35 | options, 36 | ); 37 | this._quadtreePrimitive = new QuadtreePrimitive({ 38 | tileProvider: this._quadtreeProvider, 39 | }); 40 | this._primitiveCollection.add(this._quadtreePrimitive); 41 | this._primitiveCollection[vcsLayerName] = this.name; 42 | this._primitiveCollection.show = false; 43 | } 44 | 45 | updateTiles(_tiles: string[], featureVisibility: boolean): void { 46 | if (!featureVisibility) { 47 | this._quadtreePrimitive.invalidateAllTiles(); // XXX this we can do bette 48 | } 49 | } 50 | 51 | async initialize(): Promise { 52 | if (!this.initialized) { 53 | this.map.addPrimitiveCollection(this._primitiveCollection); 54 | } 55 | await super.initialize(); 56 | } 57 | 58 | async activate(): Promise { 59 | this._primitiveCollection.show = true; 60 | return super.activate(); 61 | } 62 | 63 | deactivate(): void { 64 | this._primitiveCollection.show = false; 65 | super.deactivate(); 66 | } 67 | 68 | updateStyle(style: StyleItem, _silent?: boolean): void { 69 | this._quadtreeProvider.updateStyle(style); 70 | this._quadtreePrimitive.invalidateAllTiles(); 71 | } 72 | 73 | updateSplitDirection(direction: SplitDirection): void { 74 | this._quadtreeProvider.updateSplitDirection(direction); 75 | this._quadtreePrimitive.invalidateAllTiles(); 76 | } 77 | 78 | destroy(): void { 79 | if (!this.isDestroyed) { 80 | this._quadtreeProvider.destroy(); 81 | this._quadtreePrimitive.invalidateAllTiles(); 82 | if (this.map.initialized) { 83 | this.map.removePrimitiveCollection(this._primitiveCollection); 84 | } else { 85 | this._primitiveCollection.destroy(); 86 | } 87 | } 88 | 89 | super.destroy(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/layer/cesium/wmsCesiumImpl.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ImageryLayer as CesiumImageryLayer, 3 | Rectangle, 4 | WebMercatorTilingScheme, 5 | WebMapServiceImageryProvider, 6 | } from '@vcmap-cesium/engine'; 7 | import type { Size } from 'ol/size.js'; 8 | 9 | import RasterLayerCesiumImpl from './rasterLayerCesiumImpl.js'; 10 | import { wgs84Projection } from '../../util/projection.js'; 11 | import type { WMSImplementationOptions } from '../wmsLayer.js'; 12 | import type CesiumMap from '../../map/cesiumMap.js'; 13 | import { getResourceOrUrl } from './resourceHelper.js'; 14 | 15 | /** 16 | * represents a specific Cesium WmsCesiumImpl Layer class. 17 | */ 18 | class WmsCesiumImpl extends RasterLayerCesiumImpl { 19 | static get className(): string { 20 | return 'WmsCesiumImpl'; 21 | } 22 | 23 | parameters: Record; 24 | 25 | highResolution: boolean; 26 | 27 | tileSize: Size; 28 | 29 | constructor(map: CesiumMap, options: WMSImplementationOptions) { 30 | super(map, options); 31 | this.parameters = options.parameters; 32 | this.highResolution = options.highResolution; 33 | this.tileSize = options.tileSize; 34 | } 35 | 36 | getCesiumLayer(): Promise { 37 | const parameters = { ...this.parameters }; 38 | if (this.highResolution) { 39 | parameters.width = String(this.tileSize[0] * 2); 40 | parameters.height = String(this.tileSize[1] * 2); 41 | } 42 | const options: WebMapServiceImageryProvider.ConstructorOptions = { 43 | url: getResourceOrUrl(this.url!, this.headers), 44 | layers: parameters.LAYERS, 45 | minimumLevel: this.minLevel, 46 | maximumLevel: this.maxLevel, 47 | parameters, 48 | tileWidth: this.tileSize[0], 49 | tileHeight: this.tileSize[1], 50 | }; 51 | 52 | if (this.extent && this.extent.isValid()) { 53 | const extent = this.extent.getCoordinatesInProjection(wgs84Projection); 54 | if (extent) { 55 | options.rectangle = Rectangle.fromDegrees( 56 | extent[0], 57 | extent[1], 58 | extent[2], 59 | extent[3], 60 | ); 61 | } 62 | } 63 | if (this.tilingSchema === 'mercator') { 64 | options.tilingScheme = new WebMercatorTilingScheme(); 65 | } 66 | 67 | const imageryProvider = new WebMapServiceImageryProvider(options); 68 | const layerOptions = this.getCesiumLayerOptions(); 69 | return Promise.resolve( 70 | new CesiumImageryLayer(imageryProvider, layerOptions), 71 | ); 72 | } 73 | } 74 | 75 | export default WmsCesiumImpl; 76 | -------------------------------------------------------------------------------- /src/layer/featureStoreFeatureVisibility.ts: -------------------------------------------------------------------------------- 1 | import FeatureVisibility, { HighlightStyleType } from './featureVisibility.js'; 2 | import type FeatureStoreLayerChanges from './featureStoreLayerChanges.js'; 3 | 4 | export default class FeatureStoreFeatureVisibility extends FeatureVisibility { 5 | private _changeTracker: FeatureStoreLayerChanges; 6 | 7 | constructor(changeTracker: FeatureStoreLayerChanges) { 8 | super(); 9 | this._changeTracker = changeTracker; 10 | } 11 | 12 | highlight(toHighlight: Record): void { 13 | const isTracking = this._changeTracker.active; 14 | if (isTracking) { 15 | this._changeTracker.pauseTracking('changefeature'); 16 | } 17 | super.highlight(toHighlight); 18 | if (isTracking) { 19 | this._changeTracker.track(); 20 | } 21 | } 22 | 23 | unHighlight(toUnHighlight: (string | number)[]): void { 24 | const isTracking = this._changeTracker.active; 25 | if (isTracking) { 26 | this._changeTracker.pauseTracking('changefeature'); 27 | } 28 | super.unHighlight(toUnHighlight); 29 | if (isTracking) { 30 | this._changeTracker.track(); 31 | } 32 | } 33 | 34 | hideObjects(toHide: (string | number)[]): void { 35 | const isTracking = this._changeTracker.active; 36 | if (isTracking) { 37 | this._changeTracker.pauseTracking('changefeature'); 38 | } 39 | super.hideObjects(toHide); 40 | if (isTracking) { 41 | this._changeTracker.track(); 42 | } 43 | } 44 | 45 | showObjects(unHide: (string | number)[]): void { 46 | const isTracking = this._changeTracker.active; 47 | if (isTracking) { 48 | this._changeTracker.pauseTracking('changefeature'); 49 | } 50 | super.showObjects(unHide); 51 | if (isTracking) { 52 | this._changeTracker.track(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/layer/featureStoreLayerState.ts: -------------------------------------------------------------------------------- 1 | export const featureStoreStateSymbol = Symbol('vcsFeatureType'); 2 | 3 | /** 4 | * Enumeration of feature store item states 5 | */ 6 | export type FeatureStoreLayerState = 7 | | 'dynamic' 8 | | 'static' 9 | | 'edited' 10 | | 'deleted' 11 | | 'removed'; 12 | -------------------------------------------------------------------------------- /src/layer/flatGeobufHelpers.ts: -------------------------------------------------------------------------------- 1 | import Feature from 'ol/Feature.js'; 2 | import { fromFeature } from 'flatgeobuf/lib/mjs/ol/feature.js'; 3 | import { HttpReader } from 'flatgeobuf/lib/mjs/http-reader.js'; 4 | import Projection, { 5 | mercatorProjection, 6 | parseEPSGCode, 7 | } from '../util/projection.js'; 8 | import Extent from '../util/extent.js'; 9 | import { alreadyTransformedToMercator } from './vectorSymbols.js'; 10 | 11 | export async function getValidReader( 12 | url: string, 13 | projection: Projection, 14 | ): Promise { 15 | const reader = await HttpReader.open(url, false); 16 | const { crs } = reader.header; 17 | if (crs) { 18 | const epsgCode = parseEPSGCode(crs.code, crs.org ?? undefined); 19 | if (epsgCode !== projection.epsg) { 20 | throw new Error( 21 | `The crs of the data does not match the projection of the layer. Data crs: ${epsgCode}, layer projection: ${projection.epsg}`, 22 | ); 23 | } 24 | } 25 | return reader; 26 | } 27 | 28 | export async function getOlFeatures( 29 | reader: HttpReader, 30 | projection: Projection, 31 | extent: Extent, 32 | ): Promise { 33 | const features = []; 34 | const isMercator = projection.epsg === mercatorProjection.epsg; 35 | const dataExtent = extent.getCoordinatesInProjection(projection); 36 | 37 | for await (const feature of reader.selectBbox({ 38 | minX: dataExtent[0], 39 | minY: dataExtent[1], 40 | maxX: dataExtent[2], 41 | maxY: dataExtent[3], 42 | })) { 43 | const olFeature = fromFeature( 44 | feature.id, 45 | feature.feature, 46 | reader.header, 47 | ) as Feature; 48 | const geometry = olFeature.getGeometry(); 49 | if (geometry && !isMercator) { 50 | geometry.transform(projection.proj, mercatorProjection.proj); 51 | geometry[alreadyTransformedToMercator] = true; 52 | } 53 | 54 | features.push(olFeature); 55 | } 56 | 57 | return features; 58 | } 59 | -------------------------------------------------------------------------------- /src/layer/flatGeobufLayer.ts: -------------------------------------------------------------------------------- 1 | import VectorLayer, { VectorOptions } from './vectorLayer.js'; 2 | import { wgs84Projection } from '../util/projection.js'; 3 | import { layerClassRegistry } from '../classRegistry.js'; 4 | import Extent from '../util/extent.js'; 5 | import { getOlFeatures, getValidReader } from './flatGeobufHelpers.js'; 6 | 7 | export type FlatGeobufLayerOptions = VectorOptions & { 8 | url: string | Record; 9 | }; 10 | 11 | export default class FlatGeobufLayer extends VectorLayer { 12 | static get className(): string { 13 | return 'FlatGeobufLayer'; 14 | } 15 | 16 | static getDefaultOptions(): FlatGeobufLayerOptions { 17 | return { 18 | ...super.getDefaultOptions(), 19 | url: '', 20 | }; 21 | } 22 | 23 | private _dataFetchedPromise: Promise | undefined; 24 | 25 | async initialize(): Promise { 26 | if (!this.initialized) { 27 | await super.initialize(); 28 | 29 | if (this._url) { 30 | await this.fetchData(); 31 | } 32 | } 33 | } 34 | 35 | async fetchData(): Promise { 36 | if (this._dataFetchedPromise) { 37 | return this._dataFetchedPromise; 38 | } 39 | 40 | const reader = await getValidReader(this.url, this.projection); 41 | let resolve: () => void; 42 | const promise = new Promise((r) => { 43 | resolve = r; 44 | }); 45 | this._dataFetchedPromise = promise; 46 | const worldExtent = new Extent({ 47 | coordinates: Extent.WGS_84_EXTENT, 48 | projection: wgs84Projection.toJSON(), 49 | }); 50 | const features = await getOlFeatures(reader, this.projection, worldExtent); 51 | if (this._dataFetchedPromise === promise) { 52 | this.addFeatures(features); 53 | } 54 | resolve!(); 55 | return this._dataFetchedPromise; 56 | } 57 | 58 | async reload(): Promise { 59 | if (this._dataFetchedPromise) { 60 | this._dataFetchedPromise = undefined; 61 | await this.fetchData(); 62 | } 63 | return this.forceRedraw(); 64 | } 65 | } 66 | layerClassRegistry.registerClass(FlatGeobufLayer.className, FlatGeobufLayer); 67 | -------------------------------------------------------------------------------- /src/layer/layerImplementation.ts: -------------------------------------------------------------------------------- 1 | import VcsObject from '../vcsObject.js'; 2 | import LayerState from './layerState.js'; 3 | import type VcsMap from '../map/vcsMap.js'; 4 | import type { LayerImplementationOptions } from './layer.js'; 5 | 6 | /** 7 | * represents an implementation for a Layer for a specific Map 8 | */ 9 | class LayerImplementation extends VcsObject { 10 | static get className(): string { 11 | return 'LayerImplementation'; 12 | } 13 | 14 | private _map: M | undefined; 15 | 16 | url: string | undefined; 17 | 18 | protected _state: LayerState = LayerState.INACTIVE; 19 | 20 | private _initialized = false; 21 | 22 | headers?: Record; 23 | 24 | constructor(map: M, options: LayerImplementationOptions) { 25 | super(options); 26 | this._map = map; 27 | this.url = options.url; 28 | this.headers = options.headers; 29 | } 30 | 31 | get map(): M { 32 | if (!this._map) { 33 | throw new Error('Accessing destroyed implementation'); 34 | } 35 | return this._map; 36 | } 37 | 38 | /** 39 | * Whether this implementation has been initialized (e.g. activated at least once) 40 | */ 41 | get initialized(): boolean { 42 | return this._initialized; 43 | } 44 | 45 | get active(): boolean { 46 | return this._state === LayerState.ACTIVE; 47 | } 48 | 49 | get loading(): boolean { 50 | return this._state === LayerState.LOADING; 51 | } 52 | 53 | /** 54 | * interface to initialize this implementation, is used to setup elements which have to be created only once. 55 | * Has to set this.initialized = true; 56 | */ 57 | initialize(): Promise { 58 | this._initialized = true; 59 | return Promise.resolve(); 60 | } 61 | 62 | /** 63 | * activates the implementation, if the map is also active. calls initialize (only use internally) 64 | * Once the promise resolves, the layer can still be inactive, if deactivate was called while initializing the layer. 65 | */ 66 | async activate(): Promise { 67 | if (this.map.active && !this.active) { 68 | this._state = LayerState.LOADING; 69 | await this.initialize(); 70 | if (this.loading) { 71 | this._state = LayerState.ACTIVE; 72 | } 73 | } 74 | } 75 | 76 | /** 77 | * deactivates the implementation (only use internally) 78 | */ 79 | deactivate(): void { 80 | this._state = LayerState.INACTIVE; 81 | } 82 | 83 | /** 84 | * destroys this implementation, after destroying the implementation cannot be used anymore. 85 | */ 86 | destroy(): void { 87 | this._initialized = false; 88 | this._state = LayerState.INACTIVE; 89 | this._map = undefined; 90 | super.destroy(); 91 | } 92 | } 93 | 94 | export default LayerImplementation; 95 | -------------------------------------------------------------------------------- /src/layer/layerState.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enumeration of possible layer states. 3 | * State machine: inactive <-> loading -> active -> inactive 4 | */ 5 | enum LayerState { 6 | INACTIVE = 1, 7 | ACTIVE = 2, 8 | LOADING = 4, 9 | } 10 | 11 | export default LayerState; 12 | -------------------------------------------------------------------------------- /src/layer/layerSymbols.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Symbol to declare a layers name on its visualizations, e.g. ol.layer.Layer, Cesium.Cesium3DTileset* 3 | */ 4 | export const vcsLayerName: unique symbol = Symbol('vcsLayerName'); 5 | 6 | /** 7 | * Symbol added to Cesium3DTilesets to suppress picking. 8 | */ 9 | export const allowPicking: unique symbol = Symbol('allowPicking'); 10 | -------------------------------------------------------------------------------- /src/layer/oblique/layerObliqueImpl.ts: -------------------------------------------------------------------------------- 1 | import type { Layer as OLLayer } from 'ol/layer.js'; 2 | import LayerImplementation from '../layerImplementation.js'; 3 | import { vcsLayerName } from '../layerSymbols.js'; 4 | import type ObliqueMap from '../../map/obliqueMap.js'; 5 | 6 | class LayerObliqueImpl extends LayerImplementation { 7 | olLayer: OLLayer | null = null; 8 | 9 | initialize(): Promise { 10 | if (!this.initialized) { 11 | this.olLayer = this.getOLLayer(); 12 | this.olLayer[vcsLayerName] = this.name; 13 | this.map.addOLLayer(this.olLayer); 14 | } 15 | return super.initialize(); 16 | } 17 | 18 | async activate(): Promise { 19 | await super.activate(); 20 | if (this.active && this.olLayer) { 21 | this.olLayer.setVisible(true); 22 | } 23 | } 24 | 25 | deactivate(): void { 26 | super.deactivate(); 27 | if (this.olLayer) { 28 | this.olLayer.setVisible(false); 29 | } 30 | } 31 | 32 | /** 33 | * returns the ol Layer 34 | */ 35 | // eslint-disable-next-line class-methods-use-this 36 | getOLLayer(): OLLayer { 37 | throw new Error(); 38 | } 39 | 40 | destroy(): void { 41 | if (this.olLayer) { 42 | this.map.removeOLLayer(this.olLayer); 43 | } 44 | this.olLayer = null; 45 | super.destroy(); 46 | } 47 | } 48 | 49 | export default LayerObliqueImpl; 50 | -------------------------------------------------------------------------------- /src/layer/openlayers/loadFunctionHelpers.ts: -------------------------------------------------------------------------------- 1 | import type { LoadFunction } from 'ol/Tile.js'; 2 | import type { ImageTile } from 'ol'; 3 | import TileState from 'ol/TileState.js'; 4 | import { getInitForUrl, requestObjectUrl } from '../../util/fetch.js'; 5 | 6 | // eslint-disable-next-line import/prefer-default-export 7 | export function getTileLoadFunction( 8 | headers: Record, 9 | ): LoadFunction { 10 | return function tileLoadFunction(imageTile, src): void { 11 | const image = (imageTile as ImageTile).getImage() as HTMLImageElement; 12 | const init = getInitForUrl(src, headers); 13 | requestObjectUrl(src, init) 14 | .then((blobUrl) => { 15 | image.src = blobUrl; 16 | image.onload = (): void => { 17 | URL.revokeObjectURL(blobUrl); 18 | }; 19 | }) 20 | .catch(() => { 21 | imageTile.setState(TileState.ERROR); 22 | }); 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/layer/openlayers/openStreetMapOpenlayersImpl.ts: -------------------------------------------------------------------------------- 1 | import Tile from 'ol/layer/Tile.js'; 2 | import OSM from 'ol/source/OSM.js'; 3 | import RasterLayerOpenlayersImpl from './rasterLayerOpenlayersImpl.js'; 4 | 5 | /** 6 | * represents a specific OpenStreetMapLayer layer for openlayers. 7 | */ 8 | class OpenStreetMapOpenlayersImpl extends RasterLayerOpenlayersImpl { 9 | static get className(): string { 10 | return 'OpenStreetMapOpenlayersImpl'; 11 | } 12 | 13 | getOLLayer(): Tile { 14 | return new Tile({ 15 | opacity: this.opacity, 16 | source: new OSM({ 17 | maxZoom: this.maxLevel, 18 | }), 19 | minZoom: this.minRenderingLevel, 20 | maxZoom: this.maxRenderingLevel, 21 | }); 22 | } 23 | } 24 | 25 | export default OpenStreetMapOpenlayersImpl; 26 | -------------------------------------------------------------------------------- /src/layer/openlayers/rasterLayerOpenlayersImpl.ts: -------------------------------------------------------------------------------- 1 | import LayerOpenlayersImpl from './layerOpenlayersImpl.js'; 2 | import type { 3 | RasterLayerImplementation, 4 | RasterLayerImplementationOptions, 5 | TilingScheme, 6 | } from '../rasterLayer.js'; 7 | import type Extent from '../../util/extent.js'; 8 | import type OpenlayersMap from '../../map/openlayersMap.js'; 9 | 10 | class RasterLayerOpenlayersImpl 11 | extends LayerOpenlayersImpl 12 | implements RasterLayerImplementation 13 | { 14 | static get className(): string { 15 | return 'RasterLayerOpenlayersImpl'; 16 | } 17 | 18 | minLevel: number; 19 | 20 | maxLevel: number; 21 | 22 | minRenderingLevel: number | undefined; 23 | 24 | maxRenderingLevel: number | undefined; 25 | 26 | tilingSchema: TilingScheme; 27 | 28 | extent: Extent; 29 | 30 | opacity: number; 31 | 32 | constructor(map: OpenlayersMap, options: RasterLayerImplementationOptions) { 33 | super(map, options); 34 | this.minLevel = options.minLevel; 35 | this.maxLevel = options.maxLevel; 36 | this.minRenderingLevel = options.minRenderingLevel; 37 | this.maxRenderingLevel = options.maxRenderingLevel; 38 | this.tilingSchema = options.tilingSchema; 39 | this.extent = options.extent as Extent; 40 | this.opacity = options.opacity; 41 | } 42 | 43 | updateOpacity(opacity: number): void { 44 | this.opacity = opacity; 45 | if (this.initialized) { 46 | this.olLayer!.setOpacity(this.opacity); 47 | } 48 | } 49 | } 50 | 51 | export default RasterLayerOpenlayersImpl; 52 | -------------------------------------------------------------------------------- /src/layer/openlayers/singleImageOpenlayersImpl.ts: -------------------------------------------------------------------------------- 1 | import ImageLayer from 'ol/layer/Image.js'; 2 | import { TrustedServers } from '@vcmap-cesium/engine'; 3 | import ImageStatic, { 4 | type Options as ImageStaticOptions, 5 | } from 'ol/source/ImageStatic.js'; 6 | import RasterLayerOpenlayersImpl from './rasterLayerOpenlayersImpl.js'; 7 | import { wgs84Projection } from '../../util/projection.js'; 8 | import { isSameOrigin } from '../../util/urlHelpers.js'; 9 | import type { SingleImageImplementationOptions } from '../singleImageLayer.js'; 10 | import type OpenlayersMap from '../../map/openlayersMap.js'; 11 | import { getInitForUrl, requestObjectUrl } from '../../util/fetch.js'; 12 | 13 | /** 14 | * represents a specific OpenLayers SingleImageLayer Layer class. 15 | */ 16 | class SingleImageOpenlayersImpl extends RasterLayerOpenlayersImpl { 17 | static get className(): string { 18 | return 'SingleImageOpenlayersImpl'; 19 | } 20 | 21 | credit: string | undefined; 22 | 23 | constructor(map: OpenlayersMap, options: SingleImageImplementationOptions) { 24 | super(map, options); 25 | this.credit = options.credit; 26 | } 27 | 28 | /** 29 | * returns the ol Layer 30 | */ 31 | getOLLayer(): ImageLayer { 32 | const options: ImageStaticOptions = { 33 | attributions: this.credit, 34 | url: this.url as string, 35 | projection: 'EPSG:4326', 36 | imageExtent: this.extent.getCoordinatesInProjection(wgs84Projection), 37 | }; 38 | if (TrustedServers.contains(options.url)) { 39 | options.crossOrigin = 'use-credentials'; 40 | } else if (!isSameOrigin(this.url as string)) { 41 | options.crossOrigin = 'anonymous'; 42 | } 43 | 44 | if (this.headers) { 45 | options.imageLoadFunction = (imageWrapper, src): void => { 46 | const init = getInitForUrl(src, this.headers); 47 | requestObjectUrl(src, init) 48 | .then((blobUrl) => { 49 | const image = imageWrapper.getImage() as HTMLImageElement; 50 | image.src = blobUrl; 51 | image.onload = (): void => { 52 | URL.revokeObjectURL(blobUrl); 53 | }; 54 | }) 55 | .catch(() => { 56 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 57 | // @ts-ignore 58 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call,no-underscore-dangle 59 | imageWrapper.handleImageError_(); 60 | }); 61 | }; 62 | } 63 | 64 | return new ImageLayer({ 65 | source: new ImageStatic(options), 66 | opacity: this.opacity, 67 | minZoom: this.minRenderingLevel, 68 | maxZoom: this.maxRenderingLevel, 69 | }); 70 | } 71 | } 72 | 73 | export default SingleImageOpenlayersImpl; 74 | -------------------------------------------------------------------------------- /src/layer/openlayers/tileDebugOpenlayersImpl.ts: -------------------------------------------------------------------------------- 1 | import Tile from 'ol/layer/Tile.js'; 2 | import TileDebug from 'ol/source/TileDebug.js'; 3 | import LayerOpenlayersImpl from './layerOpenlayersImpl.js'; 4 | import { VectorTileImplementation } from '../vectorTileLayer.js'; 5 | import type StyleItem from '../../style/styleItem.js'; 6 | 7 | /** 8 | * layer Implementation to render tile boundaries. 9 | */ 10 | class TileDebugOpenlayersImpl 11 | extends LayerOpenlayersImpl 12 | implements VectorTileImplementation 13 | { 14 | static get className(): string { 15 | return 'TileDebugOpenlayersImpl'; 16 | } 17 | 18 | // eslint-disable-next-line class-methods-use-this 19 | getOLLayer(): Tile { 20 | return new Tile({ 21 | source: new TileDebug(), 22 | }); 23 | } 24 | 25 | // eslint-disable-next-line class-methods-use-this,no-unused-vars 26 | updateStyle(_styleItem: StyleItem, _silent?: boolean): void {} 27 | 28 | // eslint-disable-next-line class-methods-use-this,no-unused-vars 29 | updateTiles(_args: string[]): void {} 30 | } 31 | 32 | export default TileDebugOpenlayersImpl; 33 | -------------------------------------------------------------------------------- /src/layer/openlayers/tmsOpenlayersImpl.ts: -------------------------------------------------------------------------------- 1 | import { TrustedServers } from '@vcmap-cesium/engine'; 2 | import XYZ, { type Options as XYZOptions } from 'ol/source/XYZ.js'; 3 | import Tile from 'ol/layer/Tile.js'; 4 | import { type Options as TileOptions } from 'ol/layer/BaseTile.js'; 5 | import type { Size } from 'ol/size.js'; 6 | import { mercatorProjection } from '../../util/projection.js'; 7 | import RasterLayerOpenlayersImpl from './rasterLayerOpenlayersImpl.js'; 8 | import { TilingScheme } from '../rasterLayer.js'; 9 | import { isSameOrigin } from '../../util/urlHelpers.js'; 10 | import type { TMSImplementationOptions } from '../tmsLayer.js'; 11 | import type OpenlayersMap from '../../map/openlayersMap.js'; 12 | import { getTileLoadFunction } from './loadFunctionHelpers.js'; 13 | 14 | /** 15 | * TmsLayer implementation for {@link OpenlayersMap}. 16 | */ 17 | class TmsOpenlayersImpl extends RasterLayerOpenlayersImpl { 18 | static get className(): string { 19 | return 'TmsOpenlayersImpl'; 20 | } 21 | 22 | format: string; 23 | 24 | tileSize: Size; 25 | 26 | /** 27 | * @param map 28 | * @param options 29 | */ 30 | constructor(map: OpenlayersMap, options: TMSImplementationOptions) { 31 | super(map, options); 32 | this.format = options.format; 33 | this.tileSize = options.tileSize; 34 | } 35 | 36 | getOLLayer(): Tile { 37 | const sourceOptions: XYZOptions = { 38 | tileUrlFunction: (tileCoord) => { 39 | const baseUrl = this.url!.replace(/\/$/, ''); 40 | const y = (1 << tileCoord[0]) - tileCoord[2] - 1; 41 | return `${baseUrl}/${tileCoord[0]}/${tileCoord[1]}/${y}.${this.format}`; 42 | }, 43 | tileSize: this.tileSize, 44 | minZoom: this.minLevel, 45 | maxZoom: this.maxLevel, 46 | wrapX: false, 47 | }; 48 | if (TrustedServers.contains(this.url as string)) { 49 | sourceOptions.crossOrigin = 'use-credentials'; 50 | } else if (!isSameOrigin(this.url as string)) { 51 | sourceOptions.crossOrigin = 'anonymous'; 52 | } 53 | if (this.tilingSchema === TilingScheme.GEOGRAPHIC) { 54 | sourceOptions.projection = 'EPSG:4326'; 55 | } 56 | if (this.headers) { 57 | sourceOptions.tileLoadFunction = getTileLoadFunction(this.headers); 58 | } 59 | 60 | const tileOptions: TileOptions = { 61 | source: new XYZ(sourceOptions), 62 | opacity: this.opacity, 63 | minZoom: this.minRenderingLevel, 64 | maxZoom: this.maxRenderingLevel, 65 | }; 66 | if (this.extent && this.extent.isValid()) { 67 | tileOptions.extent = 68 | this.extent.getCoordinatesInProjection(mercatorProjection); 69 | } 70 | return new Tile(tileOptions); 71 | } 72 | } 73 | 74 | export default TmsOpenlayersImpl; 75 | -------------------------------------------------------------------------------- /src/layer/openlayers/wmsOpenlayersImpl.ts: -------------------------------------------------------------------------------- 1 | import Tile from 'ol/layer/Tile.js'; 2 | import type { Size } from 'ol/size.js'; 3 | import type TileWMS from 'ol/source/TileWMS.js'; 4 | import ImageWMS from 'ol/source/ImageWMS.js'; 5 | import ImageLayer from 'ol/layer/Image.js'; 6 | import RasterLayerOpenlayersImpl from './rasterLayerOpenlayersImpl.js'; 7 | import { getImageWMSSource, getWMSSource } from '../wmsHelpers.js'; 8 | import type { WMSImplementationOptions } from '../wmsLayer.js'; 9 | import type OpenlayersMap from '../../map/openlayersMap.js'; 10 | import { mercatorProjection } from '../../util/projection.js'; 11 | 12 | /** 13 | * represents a specific Cesium WmsOpenlayersImpl Layer class. 14 | */ 15 | class WmsOpenlayersImpl extends RasterLayerOpenlayersImpl { 16 | static get className(): string { 17 | return 'WmsOpenlayersImpl'; 18 | } 19 | 20 | parameters: Record; 21 | 22 | version: string; 23 | 24 | tileSize: Size; 25 | 26 | singleImage2d: boolean; 27 | 28 | constructor(map: OpenlayersMap, options: WMSImplementationOptions) { 29 | super(map, options); 30 | this.parameters = options.parameters; 31 | this.version = options.version; 32 | this.tileSize = options.tileSize; 33 | this.singleImage2d = options.singleImage2d; 34 | } 35 | 36 | getOLLayer(): Tile | ImageLayer { 37 | if (this.singleImage2d) { 38 | return new ImageLayer({ 39 | extent: this.extent.getCoordinatesInProjection(mercatorProjection), 40 | visible: false, 41 | source: getImageWMSSource({ 42 | url: this.url as string, 43 | parameters: this.parameters, 44 | tilingSchema: this.tilingSchema, 45 | version: this.version, 46 | headers: this.headers, 47 | }), 48 | opacity: this.opacity, 49 | minZoom: this.minRenderingLevel, 50 | maxZoom: this.maxRenderingLevel, 51 | }); 52 | } 53 | return new Tile({ 54 | visible: false, 55 | source: getWMSSource({ 56 | url: this.url as string, 57 | parameters: this.parameters, 58 | version: this.version, 59 | extent: this.extent, 60 | tileSize: this.tileSize, 61 | minLevel: this.minLevel, 62 | maxLevel: this.maxLevel, 63 | tilingSchema: this.tilingSchema, 64 | headers: this.headers, 65 | }), 66 | opacity: this.opacity, 67 | minZoom: this.minRenderingLevel, 68 | maxZoom: this.maxRenderingLevel, 69 | }); 70 | } 71 | } 72 | 73 | export default WmsOpenlayersImpl; 74 | -------------------------------------------------------------------------------- /src/layer/tileLoadedHelper.ts: -------------------------------------------------------------------------------- 1 | import type { Globe } from '@vcmap-cesium/engine'; 2 | import CesiumTilesetCesiumImpl from './cesium/cesiumTilesetCesiumImpl.js'; 3 | import CesiumTilesetLayer from './cesiumTilesetLayer.js'; 4 | import FeatureStoreLayer from './featureStoreLayer.js'; 5 | 6 | function waitForImplTilesLoaded( 7 | impl: CesiumTilesetCesiumImpl, 8 | timeout?: number, 9 | ): Promise { 10 | return new Promise((resolve) => { 11 | let timeoutNr: number | undefined | NodeJS.Timeout; 12 | const remover = 13 | impl.cesium3DTileset?.allTilesLoaded.addEventListener(() => { 14 | if (timeoutNr) { 15 | clearTimeout(timeoutNr); 16 | } 17 | remover(); 18 | resolve(); 19 | }) ?? ((): void => {}); 20 | 21 | if (timeout != null) { 22 | timeoutNr = setTimeout(() => { 23 | remover(); 24 | resolve(); 25 | }, timeout); 26 | } 27 | }); 28 | } 29 | 30 | export async function tiledLayerLoaded( 31 | layer: CesiumTilesetLayer | FeatureStoreLayer, 32 | timeout?: number, 33 | ): Promise { 34 | const impls = layer 35 | .getImplementations() 36 | .filter((i) => i instanceof CesiumTilesetCesiumImpl); 37 | if (!layer.active || impls.every((i) => i.cesium3DTileset?.tilesLoaded)) { 38 | return; 39 | } 40 | 41 | await Promise.all(impls.map((i) => waitForImplTilesLoaded(i, timeout))); 42 | } 43 | 44 | export function globeLoaded(globe: Globe, timeout?: number): Promise { 45 | if (globe.tilesLoaded) { 46 | return Promise.resolve(); 47 | } 48 | 49 | return new Promise((resolve) => { 50 | let timeoutNr: number | undefined | NodeJS.Timeout; 51 | const remover = globe.tileLoadProgressEvent.addEventListener((count) => { 52 | if (count < 1) { 53 | if (timeoutNr) { 54 | clearTimeout(timeoutNr); 55 | } 56 | remover(); 57 | resolve(); 58 | } 59 | }); 60 | 61 | if (timeout != null) { 62 | timeoutNr = setTimeout(() => { 63 | remover(); 64 | resolve(); 65 | }, timeout); 66 | } 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /src/layer/tileProvider/staticFeatureTileProvider.ts: -------------------------------------------------------------------------------- 1 | import { Feature } from 'ol'; 2 | import TileProvider, { TileProviderOptions } from './tileProvider.js'; 3 | 4 | export type StaticFeatureTileProviderOptions = Omit< 5 | TileProviderOptions, 6 | 'baseLevels' 7 | > & { 8 | features: Feature[]; 9 | }; 10 | 11 | export default class StaticFeatureTileProvider extends TileProvider { 12 | static get className(): string { 13 | return 'StaticFeatureTileProvider'; 14 | } 15 | 16 | static getDefaultOptions(): StaticFeatureTileProviderOptions { 17 | return { 18 | ...TileProvider.getDefaultOptions(), 19 | features: [], 20 | }; 21 | } 22 | 23 | private _features: Feature[]; 24 | 25 | constructor(options: StaticFeatureTileProviderOptions) { 26 | const defaultOptions = StaticFeatureTileProvider.getDefaultOptions(); 27 | super({ ...options, baseLevels: [0] }); 28 | this._features = options.features || defaultOptions.features; 29 | } 30 | 31 | // eslint-disable-next-line no-unused-vars 32 | loader( 33 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 34 | _x: number, 35 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 36 | _y: number, 37 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 38 | _z: number, 39 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 40 | _headers?: Record, 41 | ): Promise { 42 | return Promise.resolve(this._features); 43 | } 44 | 45 | toJSON(): StaticFeatureTileProviderOptions { 46 | const config: TileProviderOptions = super.toJSON(); 47 | 48 | delete config.baseLevels; 49 | const staticFeatureConfig: StaticFeatureTileProviderOptions = { 50 | ...structuredClone(config), 51 | features: this._features, 52 | }; 53 | 54 | return staticFeatureConfig; 55 | } 56 | 57 | destroy(): void { 58 | this._features = []; 59 | super.destroy(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/layer/tileProvider/staticGeojsonTileProvider.ts: -------------------------------------------------------------------------------- 1 | import type { GeoJSONObject } from 'ol/format/GeoJSON.js'; 2 | import type { Feature } from 'ol/index.js'; 3 | import { parseGeoJSON } from '../geojsonHelpers.js'; 4 | import TileProvider, { TileProviderOptions } from './tileProvider.js'; 5 | import { getInitForUrl, requestJson } from '../../util/fetch.js'; 6 | import { tileProviderClassRegistry } from '../../classRegistry.js'; 7 | 8 | export type StaticGeoJSONTileProviderOptions = TileProviderOptions & { 9 | url: string; 10 | }; 11 | 12 | /** 13 | * Loads the provided geojson url and tiles the content in memory, data is only requested once 14 | */ 15 | class StaticGeoJSONTileProvider extends TileProvider { 16 | static get className(): string { 17 | return 'StaticGeoJSONTileProvider'; 18 | } 19 | 20 | static getDefaultOptions(): StaticGeoJSONTileProviderOptions { 21 | return { 22 | ...TileProvider.getDefaultOptions(), 23 | url: '', 24 | baseLevels: [0], 25 | }; 26 | } 27 | 28 | url: string; 29 | 30 | constructor(options: StaticGeoJSONTileProviderOptions) { 31 | const defaultOptions = StaticGeoJSONTileProvider.getDefaultOptions(); 32 | super({ ...options, baseLevels: defaultOptions.baseLevels }); 33 | 34 | this.url = options.url || defaultOptions.url; 35 | } 36 | 37 | // eslint-disable-next-line no-unused-vars 38 | async loader( 39 | _x: number, 40 | _y: number, 41 | _z: number, 42 | headers?: Record, 43 | ): Promise { 44 | const init = getInitForUrl(this.url, headers); 45 | const data = await requestJson(this.url, init); 46 | const { features } = parseGeoJSON(data, { dynamicStyle: true }); 47 | return features; 48 | } 49 | 50 | toJSON(): StaticGeoJSONTileProviderOptions { 51 | const config: Partial = super.toJSON(); 52 | delete config.baseLevels; 53 | 54 | if (this.url) { 55 | config.url = this.url; 56 | } 57 | return config as StaticGeoJSONTileProviderOptions; 58 | } 59 | } 60 | 61 | export default StaticGeoJSONTileProvider; 62 | tileProviderClassRegistry.registerClass( 63 | StaticGeoJSONTileProvider.className, 64 | StaticGeoJSONTileProvider, 65 | ); 66 | -------------------------------------------------------------------------------- /src/layer/vectorSymbols.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Attached to a geometry to indicate, it is already in mercator and not the layers default projection 3 | */ 4 | export const alreadyTransformedToMercator: unique symbol = Symbol( 5 | 'alreadyTransformedToMercator', 6 | ); 7 | 8 | /** 9 | * Attached to a geometry to indicate, it is already in oblique image coordiantes and not mercator 10 | */ 11 | export const alreadyTransformedToImage: unique symbol = Symbol( 12 | 'alreadyTransformedToImage', 13 | ); 14 | 15 | /** 16 | * Attached to an ol/Feature to reference the underlying oblique geometry 17 | */ 18 | export const obliqueGeometry: unique symbol = Symbol('obliqueGeometry'); 19 | 20 | /** 21 | * Attached to an ol/Feature which should only exist in oblqie coordinates and not be transformed to mercator on change 22 | */ 23 | export const doNotTransform: unique symbol = Symbol('doNotTransform'); 24 | 25 | /** 26 | * Attached to oblique features to reference the underlying original ol/Feature 27 | */ 28 | export const originalFeatureSymbol: unique symbol = Symbol('OriginalFeature'); 29 | 30 | /** 31 | * Attached to mercator or oblique geometries which are polygons but have a circular counterpart. Used to not 32 | * mess up circle drawing in oblique 33 | */ 34 | export const actuallyIsCircle: unique symbol = Symbol('ActuallyIsCircle'); 35 | 36 | /** 37 | * Can be attached to features to have the primitives be created sync instead of async. Use this 38 | * for faster response times to changes. Do not use this on bulk insertion etc. since sync creation blocks 39 | * the rendering thread 40 | */ 41 | export const createSync: unique symbol = Symbol('createSync'); 42 | 43 | /** 44 | * Can be present on ol/Feature to indicate the current primitives / billboards / models / labels associated with this feature 45 | */ 46 | export const primitives: unique symbol = Symbol('primitives'); 47 | -------------------------------------------------------------------------------- /src/map/mapState.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The state of a map. 3 | * State machine: inactive <-> loading -> active -> inactive 4 | */ 5 | enum MapState { 6 | INACTIVE = 1, 7 | ACTIVE = 2, 8 | LOADING = 4, 9 | } 10 | 11 | export default MapState; 12 | -------------------------------------------------------------------------------- /src/map/navigation/controller/controller.ts: -------------------------------------------------------------------------------- 1 | import { Math as CesiumMath } from '@vcmap-cesium/engine'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { 4 | ControllerInput, 5 | checkThreshold, 6 | multiplyComponents, 7 | } from './controllerInput.js'; 8 | 9 | export type ControllerOptions = { 10 | id: string; 11 | scales?: ControllerInput; 12 | inputThreshold?: number; 13 | }; 14 | 15 | class Controller { 16 | static get className(): string { 17 | return 'Controller'; 18 | } 19 | 20 | static getDefaultOptions(): ControllerOptions { 21 | return { 22 | id: '', 23 | scales: undefined, 24 | inputThreshold: CesiumMath.EPSILON1, 25 | }; 26 | } 27 | 28 | readonly id: string; 29 | 30 | scales?: ControllerInput; 31 | 32 | inputThreshold: number; 33 | 34 | constructor(options: ControllerOptions) { 35 | const defaultOptions = Controller.getDefaultOptions(); 36 | 37 | this.id = options.id || uuidv4(); 38 | this.scales = options.scales || defaultOptions.scales; 39 | this.inputThreshold = 40 | options.inputThreshold || defaultOptions.inputThreshold!; 41 | } 42 | 43 | // eslint-disable-next-line class-methods-use-this 44 | setMapTarget(_target: HTMLElement | null): void {} 45 | 46 | // eslint-disable-next-line class-methods-use-this 47 | getControllerInput(): ControllerInput | null { 48 | return null; 49 | } 50 | 51 | getInputs(): ControllerInput | null { 52 | const input = this.getControllerInput(); 53 | if (input) { 54 | if (checkThreshold(input, this.inputThreshold)) { 55 | return this.scales 56 | ? multiplyComponents(input, this.scales, input) 57 | : input; 58 | } 59 | } 60 | return null; 61 | } 62 | 63 | toJSON(): ControllerOptions { 64 | const defaultOptions = Controller.getDefaultOptions(); 65 | const config: ControllerOptions = { 66 | id: this.id, 67 | }; 68 | if (this.scales) { 69 | config.scales = this.scales; 70 | } 71 | if (defaultOptions.inputThreshold !== this.inputThreshold) { 72 | config.inputThreshold = this.inputThreshold; 73 | } 74 | return config; 75 | } 76 | 77 | // eslint-disable-next-line class-methods-use-this 78 | destroy(): void {} 79 | } 80 | 81 | export default Controller; 82 | -------------------------------------------------------------------------------- /src/map/navigation/easingHelper.ts: -------------------------------------------------------------------------------- 1 | import { type Movement } from './navigation.js'; 2 | import { 3 | ControllerInput, 4 | getZeroInput, 5 | lerpRound, 6 | } from './controller/controllerInput.js'; 7 | 8 | const inputScratch = getZeroInput(); 9 | 10 | export type NavigationEasing = { 11 | startTime: number; 12 | target: ControllerInput; 13 | getMovementAtTime(time: number): { 14 | movement: Movement; 15 | finished: boolean; 16 | }; 17 | }; 18 | 19 | export function createEasing( 20 | startTime: number, 21 | duration: number, 22 | origin: ControllerInput = getZeroInput(), 23 | target: ControllerInput = getZeroInput(), 24 | ): NavigationEasing { 25 | return { 26 | startTime, 27 | target, 28 | getMovementAtTime(time: number): { 29 | movement: Movement; 30 | finished: boolean; 31 | } { 32 | const normalizedTime = (time - startTime) / duration; 33 | if (normalizedTime < 1) { 34 | const movement: Movement = { 35 | time: normalizedTime, 36 | duration, 37 | input: lerpRound(origin, target, normalizedTime, inputScratch, 3), 38 | }; 39 | return { movement, finished: time >= startTime + duration }; 40 | } 41 | return { 42 | movement: { 43 | time: normalizedTime, 44 | duration, 45 | input: structuredClone(target), 46 | }, 47 | finished: true, 48 | }; 49 | }, 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/map/navigation/navigationImpl.ts: -------------------------------------------------------------------------------- 1 | import VcsMap from '../vcsMap.js'; 2 | import { Movement } from './navigation.js'; 3 | 4 | export type NavigationImplOptions = { 5 | /** 6 | * base translation speed in m/s 7 | */ 8 | baseTranSpeed?: number; 9 | /** 10 | * base rotation speed in rad/s 11 | */ 12 | baseRotSpeed?: number; 13 | }; 14 | 15 | class NavigationImpl { 16 | static get className(): string { 17 | return 'NavigationImpl'; 18 | } 19 | 20 | static getDefaultOptions(): NavigationImplOptions { 21 | return { 22 | baseTranSpeed: 0.02, // 20 m/s 23 | baseRotSpeed: 0.02, // 20 rad/s 24 | }; 25 | } 26 | 27 | protected _map: M; 28 | 29 | /** 30 | * base translation speed in m/s 31 | */ 32 | baseTranSpeed: number; 33 | 34 | /** 35 | * base rotation speed in rad/s 36 | */ 37 | baseRotSpeed: number; 38 | 39 | constructor(map: M, options?: NavigationImplOptions) { 40 | const defaultOptions = NavigationImpl.getDefaultOptions(); 41 | this._map = map; 42 | this.baseTranSpeed = 43 | options?.baseTranSpeed || defaultOptions.baseTranSpeed!; 44 | this.baseRotSpeed = options?.baseRotSpeed || defaultOptions.baseRotSpeed!; 45 | } 46 | 47 | /** 48 | * Update the camera movement and rotation with easing applied. 49 | */ 50 | // eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-unused-vars 51 | update(_movement: Movement): void {} 52 | 53 | toJSON(): NavigationImplOptions { 54 | const defaultOptions = NavigationImpl.getDefaultOptions(); 55 | const config: NavigationImplOptions = {}; 56 | if (this.baseTranSpeed !== defaultOptions.baseTranSpeed) { 57 | config.baseTranSpeed = this.baseTranSpeed; 58 | } 59 | if (this.baseRotSpeed !== defaultOptions.baseRotSpeed) { 60 | config.baseRotSpeed = this.baseRotSpeed; 61 | } 62 | return config; 63 | } 64 | } 65 | 66 | export default NavigationImpl; 67 | -------------------------------------------------------------------------------- /src/map/navigation/openlayersNavigation.ts: -------------------------------------------------------------------------------- 1 | import OpenlayersMap from '../openlayersMap.js'; 2 | import NavigationImpl, { NavigationImplOptions } from './navigationImpl.js'; 3 | import { Movement } from './navigation.js'; 4 | import { moveView } from './viewHelper.js'; 5 | 6 | export type OpenlayersNavigationOptions = NavigationImplOptions; 7 | 8 | class OpenlayersNavigation extends NavigationImpl { 9 | static get className(): string { 10 | return 'OpenlayersNavigation'; 11 | } 12 | 13 | static getDefaultOptions(): OpenlayersNavigationOptions { 14 | return { ...NavigationImpl.getDefaultOptions() }; 15 | } 16 | 17 | update(movement: Movement): void { 18 | moveView(this._map, movement.input, this.baseTranSpeed); 19 | } 20 | } 21 | 22 | export default OpenlayersNavigation; 23 | -------------------------------------------------------------------------------- /src/map/navigation/viewHelper.ts: -------------------------------------------------------------------------------- 1 | import BaseOLMap from '../baseOLMap.js'; 2 | import { getScaleFromDistance } from './cameraHelper.js'; 3 | import { ControllerInput } from './controller/controllerInput.js'; 4 | 5 | // eslint-disable-next-line import/prefer-default-export 6 | export function moveView( 7 | map: BaseOLMap, 8 | input: ControllerInput, 9 | baseTranSpeed: number, 10 | ): void { 11 | const view = map.olMap?.getView(); 12 | if (view) { 13 | if (Math.abs(input.up) > 0) { 14 | const zoom = view.getZoom(); 15 | if (zoom) { 16 | view.setZoom(zoom - input.up * baseTranSpeed); 17 | } 18 | } 19 | 20 | if (Math.abs(input.forward) > 0 || Math.abs(input.right) > 0) { 21 | const distance = map.getViewpointSync()?.distance ?? 16; 22 | const scale = getScaleFromDistance(distance); 23 | const center = view.getCenter(); 24 | if (center) { 25 | view.setCenter([ 26 | center[0] + input.right * baseTranSpeed * scale, 27 | center[1] + input.forward * baseTranSpeed * scale, 28 | ]); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/moduleIdSymbol.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export const moduleIdSymbol: unique symbol = Symbol('moduleId'); 3 | -------------------------------------------------------------------------------- /src/oblique/defaultObliqueCollection.ts: -------------------------------------------------------------------------------- 1 | import type { Coordinate } from 'ol/coordinate.js'; 2 | import ObliqueCollection from './obliqueCollection.js'; 3 | import ObliqueImage, { isDefaultImageSymbol } from './obliqueImage.js'; 4 | import ObliqueImageMeta from './obliqueImageMeta.js'; 5 | import { ObliqueViewDirection } from './obliqueViewDirection.js'; 6 | import { mercatorProjection } from '../util/projection.js'; 7 | 8 | const defaultMeta = new ObliqueImageMeta({ 9 | name: 'defaultObliqueMeta', 10 | size: [512, 512], 11 | tileSize: [512, 512], 12 | tileResolution: [1], 13 | projection: mercatorProjection, 14 | format: 'png', 15 | url: '', 16 | }); 17 | 18 | /** 19 | * This is a special oblique collection wich is shown, if no other oblique collection is set on an ObliqueMap map. 20 | * It will render a single image which indicates that no images can be loaded. 21 | */ 22 | class DefaultObliqueCollection extends ObliqueCollection { 23 | constructor() { 24 | super({}); 25 | } 26 | 27 | getImageForCoordinate( 28 | mercatorCoordinate: Coordinate, 29 | _viewDirection: ObliqueViewDirection, 30 | ): ObliqueImage { 31 | const groundCoordinates = [ 32 | [mercatorCoordinate[0] - 100, mercatorCoordinate[1] - 100, 0], 33 | [mercatorCoordinate[0] + 100, mercatorCoordinate[1] - 100, 0], 34 | [mercatorCoordinate[0] + 100, mercatorCoordinate[1] + 100, 0], 35 | [mercatorCoordinate[0] - 100, mercatorCoordinate[1] + 100, 0], 36 | ]; 37 | 38 | const image = new ObliqueImage({ 39 | meta: defaultMeta, 40 | viewDirection: ObliqueViewDirection.NORTH, 41 | viewDirectionAngle: 0, 42 | name: this.name, 43 | groundCoordinates, 44 | centerPointOnGround: mercatorCoordinate, 45 | }); 46 | 47 | image[isDefaultImageSymbol] = true; 48 | return image; 49 | } 50 | } 51 | 52 | export default DefaultObliqueCollection; 53 | -------------------------------------------------------------------------------- /src/oblique/obliqueViewDirection.ts: -------------------------------------------------------------------------------- 1 | export enum ObliqueViewDirection { 2 | NORTH = 1, 3 | EAST = 2, 4 | SOUTH = 3, 5 | WEST = 4, 6 | NADIR = 5, 7 | } 8 | 9 | export const obliqueViewDirectionNames = { 10 | north: ObliqueViewDirection.NORTH, 11 | east: ObliqueViewDirection.EAST, 12 | south: ObliqueViewDirection.SOUTH, 13 | west: ObliqueViewDirection.WEST, 14 | nadir: ObliqueViewDirection.NADIR, 15 | }; 16 | 17 | export function getDirectionName( 18 | direction: ObliqueViewDirection, 19 | ): string | undefined { 20 | const entry = Object.entries(obliqueViewDirectionNames).find( 21 | ([, namedDirection]) => namedDirection === direction, 22 | ); 23 | 24 | return entry?.[0]; 25 | } 26 | -------------------------------------------------------------------------------- /src/ol/geojson.d.ts: -------------------------------------------------------------------------------- 1 | import { GeoJsonProperties, Geometry } from 'geojson'; 2 | import type { VcsMeta } from '../layer/vectorProperties.js'; 3 | import { FeatureStoreLayerState } from '../layer/featureStoreLayerState.js'; 4 | 5 | declare module 'geojson' { 6 | interface Point { 7 | olcs_radius?: number; 8 | } 9 | 10 | interface Feature< 11 | G extends Geometry | null = Geometry, 12 | P = GeoJsonProperties, 13 | > { 14 | _id?: string; 15 | radius?: G extends Point ? number : never; 16 | vcsMeta?: VcsMeta; 17 | state?: FeatureStoreLayerState; 18 | } 19 | 20 | interface FeatureCollection { 21 | crs?: 22 | | { type: 'name'; properties: { name: string } } 23 | | { type: 'EPSG'; properties: { code: string } }; 24 | vcsMeta?: VcsMeta; 25 | vcsEmbeddedIcons?: string[]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ol/geom/circle.ts: -------------------------------------------------------------------------------- 1 | import type { Coordinate } from 'ol/coordinate.js'; 2 | import type { GeometryLayout } from 'ol/geom/Geometry.js'; 3 | import Circle from 'ol/geom/Circle.js'; 4 | import { check } from '@vcsuite/check'; 5 | import { cartesian2DDistance, cartesian3DDistance } from '../../util/math.js'; 6 | 7 | /** 8 | * @returns {Array} returns an Array where the first coordinate is the center, and the second the center with an x offset of radius 9 | */ 10 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 11 | // @ts-ignore 12 | Circle.prototype.getCoordinates = function getCoordinates( 13 | this: Circle, 14 | ): Coordinate[] { 15 | return [this.getCenter(), this.getLastCoordinate()]; 16 | }; 17 | 18 | /** 19 | * @param {Array} coordinates - array of length two. The first coordinate is treated as the center, the second as the center with an x offset of radius 20 | * @param {import("ol/geom/Geometry").GeometryLayout=} optLayout 21 | */ 22 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 23 | // @ts-ignore 24 | Circle.prototype.setCoordinates = function setCoordinates( 25 | this: Circle, 26 | coordinates: [Coordinate, Coordinate], 27 | optLayout?: GeometryLayout, 28 | ): void { 29 | check(coordinates as [Coordinate, Coordinate], [[Number]]); 30 | check(coordinates.length, 2); 31 | 32 | const layout = optLayout || this.getLayout(); 33 | const getRadius = /XYM?/.test(layout) 34 | ? cartesian2DDistance 35 | : cartesian3DDistance; 36 | this.setCenterAndRadius( 37 | coordinates[0], 38 | getRadius(coordinates[0], coordinates[1]), 39 | optLayout, 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/ol/source/ClusterEnhancedVectorSource.ts: -------------------------------------------------------------------------------- 1 | import VectorSource from 'ol/source/Vector.js'; 2 | import Feature from 'ol/Feature.js'; 3 | 4 | /** 5 | * @class 6 | * @extends {import("ol/source").Vector} 7 | * @memberOf ol 8 | */ 9 | class ClusterEnhancedVectorSource extends VectorSource { 10 | /** 11 | * @param {import("ol").Feature} feature 12 | * @param {boolean=} silent 13 | */ 14 | removeFeature(feature: Feature, silent?: boolean): void { 15 | if (!feature) { 16 | return; 17 | } 18 | const removed = this.removeFeatureInternal(feature); 19 | if (removed && !silent) { 20 | this.changed(); 21 | } 22 | } 23 | 24 | /** 25 | * @param {import("ol").Feature} feature 26 | * @param {boolean=} silent 27 | */ 28 | addFeature(feature: Feature, silent?: boolean): void { 29 | this.addFeatureInternal(feature); 30 | if (!silent) { 31 | this.changed(); 32 | } 33 | } 34 | } 35 | 36 | export default ClusterEnhancedVectorSource; 37 | -------------------------------------------------------------------------------- /src/ol/source/VcsCluster.ts: -------------------------------------------------------------------------------- 1 | import Cluster, { type Options } from 'ol/source/Cluster.js'; 2 | import Feature from 'ol/Feature.js'; 3 | import { Point } from 'ol/geom.js'; 4 | import { vectorClusterGroupName } from '../../vectorCluster/vectorClusterSymbols.js'; 5 | import { hidden } from '../../layer/featureVisibility.js'; 6 | 7 | /** 8 | * @class 9 | * @extends {import("ol/source/Cluster").default} 10 | * @memberOf ol 11 | */ 12 | class VcsCluster extends Cluster { 13 | private _paused = false; 14 | 15 | constructor( 16 | props: Options, 17 | private _name: string, 18 | ) { 19 | props.geometryFunction = 20 | props.geometryFunction ?? 21 | ((feature: Feature): Point | null => { 22 | if (feature[hidden]) { 23 | return null; 24 | } 25 | return feature.getGeometry() as Point; 26 | }); 27 | 28 | super(props); 29 | /** 30 | * @type {boolean} 31 | */ 32 | this._paused = false; 33 | } 34 | 35 | addFeatures(features: Feature[]): void { 36 | features.forEach((f) => { 37 | f[vectorClusterGroupName] = this._name; 38 | }); 39 | super.addFeatures(features); 40 | } 41 | 42 | get paused(): boolean { 43 | return this._paused; 44 | } 45 | 46 | set paused(pause: boolean) { 47 | this._paused = pause; 48 | } 49 | 50 | refresh(): void { 51 | if (this._paused) { 52 | return; 53 | } 54 | super.refresh(); 55 | } 56 | } 57 | 58 | export default VcsCluster; 59 | -------------------------------------------------------------------------------- /src/style/modelFill.ts: -------------------------------------------------------------------------------- 1 | import { Fill } from 'ol/style.js'; 2 | 3 | class ModelFill extends Fill { 4 | static fromFill(fill: Fill): ModelFill { 5 | return new ModelFill({ color: fill.getColor() }); 6 | } 7 | 8 | toFill(result?: Fill): Fill { 9 | const fill = result ?? new Fill(); 10 | fill.setColor(this.getColor()); 11 | return fill; 12 | } 13 | } 14 | 15 | export default ModelFill; 16 | -------------------------------------------------------------------------------- /src/style/shapesCategory.ts: -------------------------------------------------------------------------------- 1 | import Fill from 'ol/style/Fill.js'; 2 | import Stroke from 'ol/style/Stroke.js'; 3 | import RegularShape, { 4 | type Options as RegularShapeOptions, 5 | } from 'ol/style/RegularShape.js'; 6 | import Circle, { type Options as CircleOptions } from 'ol/style/Circle.js'; 7 | import type { VectorStyleItemImage } from './vectorStyleItem.js'; 8 | 9 | export function getShapeFromOptions( 10 | options: VectorStyleItemImage, 11 | ): RegularShape | Circle { 12 | if (options.fill && !(options.fill instanceof Fill)) { 13 | options.fill = new Fill(options.fill); 14 | } 15 | if (options.stroke && !(options.stroke instanceof Stroke)) { 16 | options.stroke = new Stroke(options.stroke); 17 | } 18 | return options.points 19 | ? new RegularShape(options as RegularShapeOptions) 20 | : new Circle(options as CircleOptions); 21 | } 22 | 23 | class ShapeCategory { 24 | shapes: VectorStyleItemImage[] = []; 25 | 26 | addImage(options: VectorStyleItemImage): void { 27 | const shape = getShapeFromOptions({ ...options }); 28 | 29 | const canvas = shape.getImage(1); 30 | options.src = canvas.toDataURL(); 31 | this.shapes.push(options); 32 | } 33 | } 34 | 35 | /** 36 | * TODO refactor to getdefaultShapeCategory... 37 | */ 38 | export const shapeCategory = new ShapeCategory(); 39 | const defaultShapeOptions = { 40 | fill: new Fill({ color: [255, 255, 255, 1] }), 41 | stroke: new Stroke({ color: [0, 0, 0, 1], width: 1 }), 42 | radius: 16, 43 | }; 44 | [ 45 | null, 46 | { points: 3 }, 47 | { points: 3, angle: Math.PI }, 48 | { points: 4, angle: Math.PI / 4 }, 49 | { points: 6 }, 50 | ].forEach((additionalOptions) => { 51 | const shapeOptions = additionalOptions 52 | ? Object.assign(additionalOptions, defaultShapeOptions) 53 | : defaultShapeOptions; 54 | 55 | shapeCategory.addImage(shapeOptions); 56 | }); 57 | -------------------------------------------------------------------------------- /src/style/styleFactory.ts: -------------------------------------------------------------------------------- 1 | import { is, oneOf } from '@vcsuite/check'; 2 | import StyleItem, { StyleItemOptions } from './styleItem.js'; 3 | import { 4 | DeclarativeStyleItemOptions, 5 | defaultDeclarativeStyle, 6 | } from './declarativeStyleItem.js'; 7 | import { styleClassRegistry } from '../classRegistry.js'; 8 | import VectorStyleItem, { VectorStyleItemOptions } from './vectorStyleItem.js'; 9 | 10 | // eslint-disable-next-line import/prefer-default-export 11 | export function getStyleOrDefaultStyle( 12 | styleOptions?: 13 | | DeclarativeStyleItemOptions 14 | | VectorStyleItemOptions 15 | | StyleItem, 16 | defaultStyle?: StyleItem, 17 | ): StyleItem { 18 | if (is(styleOptions, oneOf(StyleItem, { type: String }))) { 19 | if (styleOptions instanceof StyleItem) { 20 | return styleOptions; 21 | } else { 22 | const styleItem = styleClassRegistry.createFromTypeOptions( 23 | styleOptions as StyleItemOptions, 24 | ); 25 | if (styleItem) { 26 | if ( 27 | styleItem instanceof VectorStyleItem && 28 | defaultStyle instanceof VectorStyleItem 29 | ) { 30 | return styleItem.assign(defaultStyle.clone().assign(styleItem)); 31 | } 32 | return styleItem; 33 | } 34 | } 35 | } 36 | 37 | return defaultStyle || defaultDeclarativeStyle.clone(); 38 | } 39 | -------------------------------------------------------------------------------- /src/style/writeStyle.ts: -------------------------------------------------------------------------------- 1 | import VectorStyleItem, { VectorStyleItemOptions } from './vectorStyleItem.js'; 2 | import DeclarativeStyleItem from './declarativeStyleItem.js'; 3 | import { type VcsMeta, vcsMetaVersion } from '../layer/vectorProperties.js'; 4 | import StyleItem from './styleItem.js'; 5 | 6 | export function embedIconsInStyle( 7 | obj: VectorStyleItemOptions, 8 | embeddedIcons?: string[], 9 | ): VectorStyleItemOptions { 10 | if (obj.image && obj.image.src && /^data:/.test(obj.image.src)) { 11 | if (embeddedIcons) { 12 | let index = embeddedIcons.indexOf(obj.image.src); 13 | if (index === -1) { 14 | embeddedIcons.push(obj.image.src); 15 | index = embeddedIcons.length - 1; 16 | } 17 | obj.image.src = `:${index}`; 18 | } else { 19 | obj.image = { 20 | // XXX is this the correct fallback? 21 | radius: 5, 22 | }; 23 | } 24 | } 25 | return obj; 26 | } 27 | 28 | function writeStyle( 29 | style: StyleItem, 30 | vcsMeta: VcsMeta = { version: vcsMetaVersion }, 31 | ): VcsMeta { 32 | // XXX this entire function is not what is to be expected. feature store expects styles as refs to be possible 33 | if (style instanceof VectorStyleItem) { 34 | vcsMeta.style = embedIconsInStyle(style.toJSON(), vcsMeta.embeddedIcons); 35 | } else if (style instanceof DeclarativeStyleItem) { 36 | vcsMeta.style = style.toJSON(); 37 | } 38 | return vcsMeta; 39 | } 40 | 41 | export default writeStyle; 42 | -------------------------------------------------------------------------------- /src/util/clipping/clippingPolygonHelper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Cesium3DTileset, 3 | ClippingPolygon, 4 | ClippingPolygonCollection, 5 | Globe, 6 | } from '@vcmap-cesium/engine'; 7 | import type CesiumMap from '../../map/cesiumMap.js'; 8 | import { vcsLayerName } from '../../layer/layerSymbols.js'; 9 | 10 | export function getTargetTilesets( 11 | map: CesiumMap, 12 | layerNames: string[] | 'all' = 'all', 13 | ): Cesium3DTileset[] { 14 | const tilesets = map 15 | .getVisualizations() 16 | .filter((v) => v instanceof Cesium3DTileset); 17 | if (Array.isArray(layerNames)) { 18 | return tilesets.filter((v) => layerNames.includes(v[vcsLayerName])); 19 | } 20 | return tilesets; 21 | } 22 | 23 | export function addClippingPolygon( 24 | clippee: Globe | Cesium3DTileset, 25 | polygon: ClippingPolygon | undefined, 26 | ): void { 27 | if (polygon) { 28 | if (clippee.clippingPolygons === undefined) { 29 | clippee.clippingPolygons = new ClippingPolygonCollection(); 30 | } 31 | if (!clippee.clippingPolygons.contains(polygon)) { 32 | clippee.clippingPolygons.setDirty(); 33 | clippee.clippingPolygons.add(polygon); 34 | } 35 | } 36 | } 37 | 38 | export function removeClippingPolygon( 39 | clippee: Globe | Cesium3DTileset, 40 | polygon: ClippingPolygon | undefined, 41 | ): void { 42 | if ( 43 | polygon && 44 | clippee.clippingPolygons && 45 | clippee.clippingPolygons.contains(polygon) 46 | ) { 47 | clippee.clippingPolygons.remove(polygon); 48 | } 49 | } 50 | 51 | export function addClippingPolygonObjectToMap( 52 | map: CesiumMap, 53 | polygon: ClippingPolygon | undefined, 54 | terrain: boolean, 55 | layerNames: string[] | 'all', 56 | ): void { 57 | if (terrain) { 58 | const globe = map.getScene()?.globe; 59 | if (globe) { 60 | addClippingPolygon(globe, polygon); 61 | } 62 | } 63 | 64 | const tilesets = getTargetTilesets(map, layerNames); 65 | tilesets.forEach((tileset) => { 66 | addClippingPolygon(tileset, polygon); 67 | }); 68 | } 69 | 70 | export function removeClippingPolygonFromMap( 71 | map: CesiumMap, 72 | polygon: ClippingPolygon | undefined, 73 | terrain: boolean, 74 | layerNames: string[] | 'all', 75 | ): void { 76 | if (terrain) { 77 | const globe = map.getScene()?.globe; 78 | if (globe) { 79 | removeClippingPolygon(globe, polygon); 80 | } 81 | } 82 | 83 | const tilesets = getTargetTilesets(map, layerNames); 84 | tilesets.forEach((tileset) => { 85 | removeClippingPolygon(tileset, polygon); 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /src/util/editor/editorSymbols.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Symbol to identify a {@link Vertex} 3 | */ 4 | export const vertexSymbol = Symbol('Vertex'); 5 | /** 6 | * Symbol to denote the vertexes index in the vertices array. This is important for snapping & bbox operations 7 | */ 8 | export const vertexIndexSymbol = Symbol('VertexIndex'); 9 | /** 10 | * Symbol added to primitives and features to denote that these are handlers. It is expected, that the value of the symobl is 11 | * equal to an {@link AxisAndPlanes} 12 | */ 13 | export const handlerSymbol = Symbol('Handler'); 14 | /** 15 | * Symbol to identify which was the last editor mouse over handler that edited the cursor style. 16 | */ 17 | export const mouseOverSymbol = Symbol('MouseOver'); 18 | -------------------------------------------------------------------------------- /src/util/editor/interactions/createPointInteraction.ts: -------------------------------------------------------------------------------- 1 | import Point from 'ol/geom/Point.js'; 2 | import AbstractInteraction, { 3 | EventAfterEventHandler, 4 | } from '../../../interaction/abstractInteraction.js'; 5 | import VcsEvent from '../../../vcsEvent.js'; 6 | import { EventType } from '../../../interaction/interactionType.js'; 7 | import { 8 | alreadyTransformedToImage, 9 | alreadyTransformedToMercator, 10 | } from '../../../layer/vectorSymbols.js'; 11 | import ObliqueMap from '../../../map/obliqueMap.js'; 12 | import { CreateInteraction } from '../createFeatureSession.js'; 13 | 14 | /** 15 | * @extends {AbstractInteraction} 16 | * @implements {CreateInteraction} 17 | */ 18 | class CreatePointInteraction 19 | extends AbstractInteraction 20 | implements CreateInteraction 21 | { 22 | private _geometry: Point | null = null; 23 | 24 | finished = new VcsEvent(); 25 | 26 | created = new VcsEvent(); 27 | 28 | constructor() { 29 | super(EventType.CLICK); 30 | this.setActive(); 31 | } 32 | 33 | pipe(event: EventAfterEventHandler): Promise { 34 | this._geometry = new Point(event.positionOrPixel); 35 | if (event.map instanceof ObliqueMap) { 36 | this._geometry[alreadyTransformedToImage] = true; 37 | } else { 38 | this._geometry[alreadyTransformedToMercator] = true; 39 | } 40 | this.created.raiseEvent(this._geometry); 41 | this.finish(); 42 | return Promise.resolve(event); 43 | } 44 | 45 | /** 46 | * Finish the current creation. Calls finish and sets the interaction to be inactive 47 | */ 48 | finish(): void { 49 | if (this.active !== EventType.NONE) { 50 | this.setActive(false); 51 | this.finished.raiseEvent(this._geometry); 52 | } 53 | } 54 | 55 | destroy(): void { 56 | this.finished.destroy(); 57 | this.created.destroy(); 58 | super.destroy(); 59 | } 60 | } 61 | 62 | export default CreatePointInteraction; 63 | -------------------------------------------------------------------------------- /src/util/editor/interactions/editFeaturesMouseOverInteraction.ts: -------------------------------------------------------------------------------- 1 | import type { Feature } from 'ol/index.js'; 2 | import { handlerSymbol, mouseOverSymbol } from '../editorSymbols.js'; 3 | import AbstractInteraction, { 4 | EventAfterEventHandler, 5 | } from '../../../interaction/abstractInteraction.js'; 6 | import { 7 | ModificationKeyType, 8 | EventType, 9 | } from '../../../interaction/interactionType.js'; 10 | import { cursorMap } from './editGeometryMouseOverInteraction.js'; 11 | 12 | /** 13 | * A class to handle mouse over effects on features for editor sessions. 14 | * @extends {AbstractInteraction} 15 | */ 16 | class EditFeaturesMouseOverInteraction extends AbstractInteraction { 17 | private _currentHandler: Feature | null = null; 18 | 19 | cursorStyle: CSSStyleDeclaration | undefined; 20 | 21 | constructor() { 22 | super(EventType.MOVE, ModificationKeyType.NONE); 23 | 24 | this.setActive(); 25 | } 26 | 27 | pipe(event: EventAfterEventHandler): Promise { 28 | if (event.feature && (event.feature as Feature)[handlerSymbol]) { 29 | this._currentHandler = event.feature as Feature; 30 | } else { 31 | this._currentHandler = null; 32 | } 33 | if (!this.cursorStyle && event.map?.target) { 34 | this.cursorStyle = event.map.target.style; 35 | } 36 | this._evaluate(); 37 | return Promise.resolve(event); 38 | } 39 | 40 | setActive(active?: boolean | number): void { 41 | super.setActive(active); 42 | this.reset(); 43 | } 44 | 45 | /** 46 | * Reset the cursorStyle to auto 47 | */ 48 | reset(): void { 49 | if (this.cursorStyle && this.cursorStyle.cursor) { 50 | this.cursorStyle.cursor = cursorMap.auto; 51 | this.cursorStyle = undefined; 52 | } 53 | } 54 | 55 | private _evaluate(): void { 56 | if (!this.cursorStyle) { 57 | return; 58 | } 59 | if (this._currentHandler) { 60 | this.cursorStyle.cursor = cursorMap.translate; 61 | this.cursorStyle[mouseOverSymbol] = this.id; 62 | } else if (this.cursorStyle?.[mouseOverSymbol] === this.id) { 63 | this.cursorStyle.cursor = cursorMap.auto; 64 | delete this.cursorStyle[mouseOverSymbol]; 65 | } 66 | } 67 | 68 | destroy(): void { 69 | this.reset(); 70 | super.destroy(); 71 | } 72 | } 73 | 74 | export default EditFeaturesMouseOverInteraction; 75 | -------------------------------------------------------------------------------- /src/util/editor/interactions/ensureHandlerSelectionInteraction.ts: -------------------------------------------------------------------------------- 1 | import type { Feature } from 'ol/index.js'; 2 | import type { Scene } from '@vcmap-cesium/engine'; 3 | import AbstractInteraction, { 4 | EventAfterEventHandler, 5 | } from '../../../interaction/abstractInteraction.js'; 6 | import { EventType } from '../../../interaction/interactionType.js'; 7 | import { handlerSymbol } from '../editorSymbols.js'; 8 | import CesiumMap from '../../../map/cesiumMap.js'; 9 | 10 | /** 11 | * This interaction ensure a potential handler is dragged in 3D when it is obscured by a transparent feature. 12 | * It uses drillPick on MOVE if: the map is 3D, there is a feature at said position, there is a feature selected in 13 | * the feature selection & the feature at the position is _not_ a handler 14 | */ 15 | class EnsureHandlerSelectionInteraction extends AbstractInteraction { 16 | private _featureSelection: Feature[]; 17 | 18 | /** 19 | * @param selectedFeatures Reference to the selected features. 20 | */ 21 | constructor(selectedFeatures: Feature[]) { 22 | super(EventType.DRAGSTART | EventType.MOVE); 23 | 24 | this._featureSelection = selectedFeatures; 25 | } 26 | 27 | pipe(event: EventAfterEventHandler): Promise { 28 | if ( 29 | event.feature && 30 | this._featureSelection.length > 0 && 31 | !(event.feature as Feature)[handlerSymbol] && 32 | event.map instanceof CesiumMap 33 | ) { 34 | const scene = event.map.getScene() as Scene; 35 | const drillPicks = scene.drillPick( 36 | event.windowPosition, 37 | undefined, 38 | 10, 39 | 10, 40 | ) as { primitive?: { olFeature?: Feature } }[]; 41 | const handler = drillPicks.find((p) => { 42 | return p?.primitive?.olFeature?.[handlerSymbol]; 43 | }); 44 | if (handler) { 45 | event.feature = handler.primitive!.olFeature; 46 | } 47 | } 48 | return Promise.resolve(event); 49 | } 50 | } 51 | 52 | export default EnsureHandlerSelectionInteraction; 53 | -------------------------------------------------------------------------------- /src/util/editor/interactions/removeVertexInteraction.ts: -------------------------------------------------------------------------------- 1 | import AbstractInteraction, { 2 | EventAfterEventHandler, 3 | } from '../../../interaction/abstractInteraction.js'; 4 | import { 5 | EventType, 6 | ModificationKeyType, 7 | } from '../../../interaction/interactionType.js'; 8 | import VcsEvent from '../../../vcsEvent.js'; 9 | import { isVertex, Vertex } from '../editorHelpers.js'; 10 | 11 | /** 12 | * This interaction will raise the passed in event for each feature clicked with the vertex symbol 13 | * @extends {AbstractInteraction} 14 | */ 15 | class RemoveVertexInteraction extends AbstractInteraction { 16 | vertexRemoved = new VcsEvent(); 17 | 18 | constructor() { 19 | super(EventType.CLICK, ModificationKeyType.SHIFT); 20 | this.setActive(); 21 | } 22 | 23 | pipe(event: EventAfterEventHandler): Promise { 24 | if (isVertex(event.feature)) { 25 | this.vertexRemoved.raiseEvent(event.feature); 26 | } 27 | return Promise.resolve(event); 28 | } 29 | 30 | destroy(): void { 31 | this.vertexRemoved.destroy(); 32 | super.destroy(); 33 | } 34 | } 35 | 36 | export default RemoveVertexInteraction; 37 | -------------------------------------------------------------------------------- /src/util/editor/interactions/rightClickInteraction.ts: -------------------------------------------------------------------------------- 1 | import AbstractInteraction, { 2 | InteractionEvent, 3 | } from '../../../interaction/abstractInteraction.js'; 4 | import { 5 | EventType, 6 | ModificationKeyType, 7 | PointerKeyType, 8 | } from '../../../interaction/interactionType.js'; 9 | import VcsEvent from '../../../vcsEvent.js'; 10 | 11 | function timeout(ms: number): Promise { 12 | return new Promise((resolve) => { 13 | setTimeout(resolve, ms); 14 | }); 15 | } 16 | 17 | export default class RightClickInteraction extends AbstractInteraction { 18 | rightClicked = new VcsEvent(); 19 | 20 | eventChainFinished = new VcsEvent(); 21 | 22 | constructor() { 23 | super(EventType.CLICK, ModificationKeyType.NONE, PointerKeyType.RIGHT); 24 | } 25 | 26 | async pipe(event: InteractionEvent): Promise { 27 | this.rightClicked.raiseEvent(); 28 | event.chainEnded?.addEventListener(() => { 29 | this.eventChainFinished.raiseEvent(); 30 | }); 31 | // we need to wait a bit, otherwise the changing features in the rightClicked Event do not take effect before 32 | // the next interaction. 33 | await timeout(0); 34 | return event; 35 | } 36 | 37 | destroy(): void { 38 | this.rightClicked.destroy(); 39 | this.eventChainFinished.destroy(); 40 | super.destroy(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/util/editor/interactions/translateVertexInteraction.ts: -------------------------------------------------------------------------------- 1 | import AbstractInteraction, { 2 | EventAfterEventHandler, 3 | } from '../../../interaction/abstractInteraction.js'; 4 | import { 5 | EventType, 6 | ModificationKeyType, 7 | } from '../../../interaction/interactionType.js'; 8 | import VcsEvent from '../../../vcsEvent.js'; 9 | import { isVertex, Vertex } from '../editorHelpers.js'; 10 | import { emptyStyle } from '../../../style/styleHelpers.js'; 11 | 12 | /** 13 | * Class to translate a vertex. Will call the passed in vertex changed event with the changed vertex. 14 | * Will modify the vertex in place 15 | */ 16 | class TranslateVertexInteraction extends AbstractInteraction { 17 | readonly vertexChanged = new VcsEvent(); 18 | 19 | private _vertex: Vertex | null = null; 20 | 21 | constructor() { 22 | super( 23 | EventType.DRAGEVENTS, 24 | ModificationKeyType.NONE | ModificationKeyType.CTRL, 25 | ); 26 | this.setActive(); 27 | } 28 | 29 | pipe(event: EventAfterEventHandler): Promise { 30 | if (this._vertex) { 31 | this._vertex.getGeometry()!.setCoordinates(event.positionOrPixel); 32 | this.vertexChanged.raiseEvent(this._vertex); 33 | 34 | if (event.type & EventType.DRAGEND) { 35 | const vertex = this._vertex; 36 | setTimeout(() => { 37 | // timeout to avoid picking the vertex in pickFromRay on the next pass 38 | vertex.setStyle(undefined); 39 | }); 40 | this._vertex = null; 41 | } 42 | } else if (event.type & EventType.DRAGSTART && isVertex(event.feature)) { 43 | this._vertex = event.feature; 44 | this._vertex.setStyle(emptyStyle); 45 | } 46 | return Promise.resolve(event); 47 | } 48 | 49 | destroy(): void { 50 | this.vertexChanged.destroy(); 51 | super.destroy(); 52 | } 53 | } 54 | 55 | export default TranslateVertexInteraction; 56 | -------------------------------------------------------------------------------- /src/util/editor/transformation/transformationTypes.ts: -------------------------------------------------------------------------------- 1 | import { Color } from '@vcmap-cesium/engine'; 2 | import type { Coordinate } from 'ol/coordinate.js'; 3 | import type { Feature } from 'ol/index.js'; 4 | 5 | /** 6 | * Handlers are map specific transformation handlers wich enable the use of the transformation interactions. 7 | * There visualization is {@link TransformationMode} specific. Do not create these handlers yourself 8 | * use {@link createTransformationHandler} instead. 9 | */ 10 | export type Handlers = { 11 | show: boolean; 12 | /** 13 | * update the center of the handlers 14 | */ 15 | setCenter(center: Coordinate): void; 16 | /** 17 | * highlight the given axis 18 | */ 19 | showAxis: AxisAndPlanes; 20 | /** 21 | * display Z axis handlers in grey and do not allow them to be picked 22 | */ 23 | greyOutZ: boolean; 24 | destroy(): void; 25 | }; 26 | 27 | export type TransformationHandler = { 28 | translate(x: number, y: number, z: number): void; 29 | /** 30 | * Copy of the handlers current center 31 | */ 32 | readonly center: Coordinate; 33 | showAxis: AxisAndPlanes; 34 | showing: boolean; 35 | setFeatures(feature: Feature[]): void; 36 | destroy(): void; 37 | }; 38 | 39 | export enum AxisAndPlanes { 40 | X = 'X', 41 | Y = 'Y', 42 | Z = 'Z', 43 | XY = 'XY', 44 | XZ = 'XZ', 45 | YZ = 'YZ', 46 | XYZ = 'XYZ', 47 | NONE = 'NONE', 48 | } 49 | export enum TransformationMode { 50 | TRANSLATE = 'translate', 51 | ROTATE = 'rotate', 52 | SCALE = 'scale', 53 | EXTRUDE = 'extrude', 54 | } 55 | 56 | export const greyedOutColor = Color.GRAY.withAlpha(0.5); 57 | 58 | export function is1DAxis(axis: AxisAndPlanes): boolean { 59 | return ( 60 | axis === AxisAndPlanes.X || 61 | axis === AxisAndPlanes.Y || 62 | axis === AxisAndPlanes.Z 63 | ); 64 | } 65 | 66 | export function is2DAxis(axis: AxisAndPlanes): boolean { 67 | return ( 68 | axis === AxisAndPlanes.XY || 69 | axis === AxisAndPlanes.XZ || 70 | axis === AxisAndPlanes.YZ 71 | ); 72 | } 73 | 74 | export function is3DAxis(axis: AxisAndPlanes): boolean { 75 | return axis === AxisAndPlanes.XYZ; 76 | } 77 | -------------------------------------------------------------------------------- /src/util/editor/validateGeoemetry.ts: -------------------------------------------------------------------------------- 1 | import type { Geometry, LineString, Polygon, Point, Circle } from 'ol/geom.js'; 2 | import { validateLineString } from '../featureconverter/lineStringToCesium.js'; 3 | import { validatePolygon } from '../featureconverter/polygonToCesium.js'; 4 | import { validatePoint } from '../featureconverter/pointToCesium.js'; 5 | import { validateCircle } from '../featureconverter/circleToCesium.js'; 6 | 7 | export default function geometryIsValid(geometry?: Geometry): boolean { 8 | if (!geometry) { 9 | return false; 10 | } 11 | const type = geometry.getType(); 12 | if (type === 'LineString') { 13 | return validateLineString(geometry as LineString); 14 | } else if (type === 'Polygon') { 15 | return validatePolygon(geometry as Polygon); 16 | } else if (type === 'Point') { 17 | return validatePoint(geometry as Point); 18 | } else if (type === 'Circle') { 19 | return validateCircle(geometry as Circle); 20 | } 21 | return false; 22 | } 23 | -------------------------------------------------------------------------------- /src/util/featureconverter/arcToCesium.ts: -------------------------------------------------------------------------------- 1 | import { ArcType, HeightReference } from '@vcmap-cesium/engine'; 2 | import type { Coordinate } from 'ol/coordinate.js'; 3 | import type { LineString } from 'ol/geom.js'; 4 | 5 | import { 6 | createGroundLineGeometries, 7 | createLineGeometries, 8 | createOutlineGeometries, 9 | createSolidGeometries, 10 | validateLineString, 11 | getGeometryOptions as getLineStringGeometryOptions, 12 | } from './lineStringToCesium.js'; 13 | import { 14 | mercatorToCartesianTransformerForHeightInfo, 15 | VectorHeightInfo, 16 | } from './vectorHeightInfo.js'; 17 | import { 18 | PolylineGeometryOptions, 19 | VectorGeometryFactory, 20 | } from './vectorGeometryFactory.js'; 21 | 22 | /** 23 | * Creates the positions & arcType option for the PolylineGeometry 24 | */ 25 | function getGeometryOptions( 26 | coords: Coordinate[], 27 | _geometry: LineString, 28 | heightInfo: VectorHeightInfo, 29 | ): PolylineGeometryOptions { 30 | const coordinateTransformer = 31 | mercatorToCartesianTransformerForHeightInfo(heightInfo); 32 | const positions = coords.map(coordinateTransformer); 33 | return { positions, arcType: ArcType.NONE }; 34 | } 35 | 36 | /** 37 | * @param arcCoords - the coordinates of the arc to use instead of the geometries coordinates if height mode is absolute 38 | * @param altitudeMode 39 | */ 40 | // eslint-disable-next-line import/prefer-default-export 41 | export function getArcGeometryFactory( 42 | arcCoords: Coordinate[], 43 | altitudeMode: HeightReference, 44 | ): VectorGeometryFactory<'arc'> { 45 | return { 46 | type: 'arc', 47 | getGeometryOptions: 48 | altitudeMode === HeightReference.NONE 49 | ? getGeometryOptions.bind(null, arcCoords) 50 | : getLineStringGeometryOptions, 51 | createSolidGeometries, 52 | createOutlineGeometries, 53 | createFillGeometries(): never[] { 54 | return []; 55 | }, 56 | createGroundLineGeometries, 57 | createLineGeometries, 58 | validateGeometry: validateLineString, 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/util/fetch.ts: -------------------------------------------------------------------------------- 1 | import { TrustedServers } from '@vcmap-cesium/engine'; 2 | 3 | export async function requestUrl( 4 | url: string, 5 | init?: RequestInit, 6 | ): Promise { 7 | const response = await fetch(url, init); 8 | if (!response.ok) { 9 | throw new Error( 10 | `Failed fetching url ${url} with status: ${response.status}`, 11 | ); 12 | } 13 | return response; 14 | } 15 | 16 | export async function requestJson( 17 | url: string, 18 | init?: RequestInit, 19 | ): Promise { 20 | const response = await requestUrl(url, init); 21 | return response.json() as Promise; 22 | } 23 | 24 | export async function requestArrayBuffer( 25 | url: string, 26 | init?: RequestInit, 27 | ): Promise { 28 | const response = await requestUrl(url, init); 29 | return response.arrayBuffer(); 30 | } 31 | 32 | export async function requestObjectUrl( 33 | url: string, 34 | init?: RequestInit, 35 | ): Promise { 36 | const response = await requestUrl(url, init); 37 | const blob = await response.blob(); 38 | return URL.createObjectURL(blob); 39 | } 40 | 41 | export function getInitForUrl( 42 | url: string, 43 | headers?: Record, 44 | ): RequestInit { 45 | const init: RequestInit = {}; 46 | if (headers) { 47 | init.headers = headers; 48 | } 49 | if (TrustedServers.contains(url)) { 50 | init.credentials = 'include'; 51 | } 52 | return init; 53 | } 54 | -------------------------------------------------------------------------------- /src/util/flight/flightCollection.ts: -------------------------------------------------------------------------------- 1 | import Collection from '../collection.js'; 2 | import { createFlightPlayer, FlightPlayer } from './flightPlayer.js'; 3 | import VcsEvent from '../../vcsEvent.js'; 4 | import FlightInstance from './flightInstance.js'; 5 | import type VcsApp from '../../vcsApp.js'; 6 | 7 | /** 8 | * A collection of flights. Provides playFlight API, which returns a FlightPlayer. 9 | * Emits playerChanged event, whenever another flight is played. 10 | */ 11 | class FlightCollection extends Collection { 12 | private readonly _app: VcsApp; 13 | 14 | private _player: FlightPlayer | undefined; 15 | 16 | playerChanged: VcsEvent; 17 | 18 | private _playerDestroyedListener: () => void; 19 | 20 | constructor(app: VcsApp) { 21 | super(); 22 | 23 | this._app = app; 24 | this._player = undefined; 25 | this.playerChanged = new VcsEvent(); 26 | this._playerDestroyedListener = (): void => {}; 27 | } 28 | 29 | get player(): FlightPlayer | undefined { 30 | return this._player; 31 | } 32 | 33 | remove(item: FlightInstance): void { 34 | if (this._player?.flightInstanceName === item.name) { 35 | this._player.stop(); 36 | this._player.destroy(); 37 | } 38 | super.remove(item); 39 | } 40 | 41 | /** 42 | * Creates a FlightPlayer for a flight instance, if not already existing for provided instance 43 | * @param flight 44 | */ 45 | async setPlayerForFlight( 46 | flight: FlightInstance, 47 | ): Promise { 48 | if (this._player?.flightInstanceName === flight.name) { 49 | return this._player; 50 | } else if (this._player) { 51 | this._playerDestroyedListener(); 52 | this._player.stop(); 53 | this._player.destroy(); 54 | } 55 | this._player = await createFlightPlayer(flight, this._app); 56 | this.playerChanged.raiseEvent(this._player); 57 | this._playerDestroyedListener = this._player.destroyed.addEventListener( 58 | () => { 59 | this._player = undefined; 60 | this.playerChanged.raiseEvent(undefined); 61 | }, 62 | ); 63 | return this._player; 64 | } 65 | 66 | destroy(): void { 67 | if (this._player) { 68 | this._player.stop(); 69 | this._player.destroy(); 70 | this._player = undefined; 71 | } 72 | this.playerChanged.destroy(); 73 | this._playerDestroyedListener(); 74 | super.destroy(); 75 | } 76 | } 77 | 78 | export default FlightCollection; 79 | -------------------------------------------------------------------------------- /src/util/hiddenObjects.ts: -------------------------------------------------------------------------------- 1 | import type GlobalHider from '../layer/globalHider.js'; 2 | import makeOverrideCollection, { 3 | OverrideCollection, 4 | } from './overrideCollection.js'; 5 | import Collection from './collection.js'; 6 | import { moduleIdSymbol } from '../moduleIdSymbol.js'; 7 | 8 | export type HiddenObject = { 9 | id: string; 10 | [moduleIdSymbol]?: string; 11 | }; 12 | 13 | export function createHiddenObjectsCollection( 14 | getDynamicModuleId: () => string, 15 | globalHider: GlobalHider, 16 | ): OverrideCollection { 17 | const collection = makeOverrideCollection( 18 | new Collection('id'), 19 | getDynamicModuleId, 20 | ); 21 | 22 | collection.added.addEventListener(({ id }) => { 23 | globalHider.hideObjects([id]); 24 | }); 25 | 26 | collection.replaced.addEventListener(({ new: item }) => { 27 | globalHider.showObjects([item.id]); 28 | }); 29 | 30 | collection.removed.addEventListener(({ id }) => { 31 | globalHider.showObjects([id]); 32 | }); 33 | 34 | return collection; 35 | } 36 | -------------------------------------------------------------------------------- /src/util/isMobile.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * returns true if we are on a mobile device including tablets 3 | */ 4 | // eslint-disable-next-line import/prefer-default-export 5 | export function isMobile(): boolean { 6 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 7 | // @ts-ignore 8 | const agent = 9 | navigator.userAgent || navigator.vendor || (window.opera as string); 10 | return ( 11 | /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test( 12 | agent, 13 | ) || 14 | /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test( 15 | agent.substring(0, 4), 16 | ) 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/util/locale.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | /** 3 | * returns the default browserLocale, if not possible 'en' 4 | */ 5 | export function detectBrowserLocale(): string { 6 | if (navigator.language) { 7 | const lang = navigator.language; 8 | return lang.substring(0, 2); 9 | } 10 | return 'en'; 11 | } 12 | -------------------------------------------------------------------------------- /src/util/urlHelpers.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export function isSameOrigin(source: string): boolean { 3 | const { location } = window; 4 | const url = new URL( 5 | source, 6 | `${location.protocol}//${location.host}${location.pathname}`, 7 | ); 8 | // for instance data: URIs have no host information and are implicitly same origin 9 | // see https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy#inherited_origins 10 | if (!url.host) { 11 | return true; 12 | } 13 | return url.protocol === location.protocol && url.host === location.host; 14 | } 15 | -------------------------------------------------------------------------------- /src/vcsEvent.ts: -------------------------------------------------------------------------------- 1 | type Listener = (event: T) => Promise | void; 2 | 3 | class VcsEvent { 4 | private _listeners: Set> = new Set(); 5 | 6 | /** 7 | * The number of listeners 8 | */ 9 | get numberOfListeners(): number { 10 | return this._listeners.size; 11 | } 12 | 13 | /** 14 | * Adds an event listener. An event listener can only be added once. 15 | * A listener added multiple times will only be called once. 16 | * @param listener 17 | * @returns - remove callback. call this function to remove the listener 18 | */ 19 | addEventListener(listener: Listener): () => void { 20 | this._listeners.add(listener); 21 | return () => { 22 | this.removeEventListener(listener); 23 | }; 24 | } 25 | 26 | /** 27 | * Removes the provided listener 28 | * @param listener 29 | * @returns - whether a listener was removed 30 | */ 31 | removeEventListener(listener: Listener): boolean { 32 | if (this._listeners.has(listener)) { 33 | this._listeners.delete(listener); 34 | return true; 35 | } 36 | 37 | return false; 38 | } 39 | 40 | /** 41 | * Raise the event, calling all listeners, if a listener is removed in between calling listeners, the listener is not 42 | * called. 43 | * @param event 44 | */ 45 | raiseEvent(event: T): void { 46 | [...this._listeners].forEach((cb) => { 47 | if (this._listeners.has(cb)) { 48 | // eslint-disable-next-line no-void 49 | void cb(event); 50 | } 51 | }); 52 | } 53 | 54 | async awaitRaisedEvent(event: T): Promise { 55 | const promises: (void | Promise)[] = []; 56 | [...this._listeners].forEach((cb) => { 57 | if (this._listeners.has(cb)) { 58 | promises.push(cb(event)); 59 | } 60 | }); 61 | await Promise.all(promises); 62 | } 63 | 64 | /** 65 | * clears all listeners 66 | */ 67 | destroy(): void { 68 | this._listeners.clear(); 69 | } 70 | } 71 | 72 | export default VcsEvent; 73 | -------------------------------------------------------------------------------- /src/vcsObject.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import { getLogger, type Logger } from '@vcsuite/logger'; 3 | import { moduleIdSymbol } from './moduleIdSymbol.js'; 4 | 5 | export type VcsObjectOptions = { 6 | /** 7 | * the type of object, typically only used in configs 8 | */ 9 | type?: string; 10 | /** 11 | * name of the object, if not given a uuid is generated, is used for the framework functions getObjectByName 12 | */ 13 | name?: string; 14 | /** 15 | * key value store for framework independent values per Object 16 | */ 17 | properties?: Record; 18 | }; 19 | 20 | /** 21 | * baseclass for all Objects 22 | */ 23 | class VcsObject { 24 | static get className(): string { 25 | return 'VcsObject'; 26 | } 27 | 28 | /** 29 | * unique Name 30 | */ 31 | readonly name: string; 32 | 33 | properties: Record; 34 | 35 | isDestroyed: boolean; 36 | 37 | [moduleIdSymbol]?: string; 38 | 39 | constructor(options: VcsObjectOptions) { 40 | this.name = options.name || uuidv4(); 41 | this.properties = options.properties || {}; 42 | this.isDestroyed = false; 43 | } 44 | 45 | get className(): string { 46 | return (this.constructor as typeof VcsObject).className; 47 | } 48 | 49 | getLogger(): Logger { 50 | return getLogger(this.className); 51 | } 52 | 53 | toJSON(): VcsObjectOptions { 54 | const config: VcsObjectOptions = { 55 | type: this.className, 56 | name: this.name, 57 | }; 58 | 59 | if (Object.keys(this.properties).length > 0) { 60 | config.properties = JSON.parse(JSON.stringify(this.properties)) as Record< 61 | string, 62 | unknown 63 | >; 64 | } 65 | 66 | return config; 67 | } 68 | 69 | destroy(): void { 70 | this.isDestroyed = true; 71 | this.properties = {}; 72 | } 73 | } 74 | 75 | export default VcsObject; 76 | -------------------------------------------------------------------------------- /src/vectorCluster/vectorClusterGroupCollection.ts: -------------------------------------------------------------------------------- 1 | import { check } from '@vcsuite/check'; 2 | import VectorClusterGroup from './vectorClusterGroup.js'; 3 | import Collection from '../util/collection.js'; 4 | import GlobalHider from '../layer/globalHider.js'; 5 | 6 | export default class VectorClusterGroupCollection extends Collection { 7 | /** 8 | * The global hider for this collection. 9 | */ 10 | private _globalHider: GlobalHider; 11 | 12 | constructor(globalHider: GlobalHider) { 13 | super(); 14 | this._globalHider = globalHider; 15 | 16 | this.added.addEventListener((g) => { 17 | g.setGlobalHider(this._globalHider); 18 | }); 19 | this.removed.addEventListener((g) => { 20 | g.setGlobalHider(); 21 | }); 22 | } 23 | 24 | /** 25 | * The current global hider of these layers 26 | */ 27 | get globalHider(): GlobalHider { 28 | return this._globalHider; 29 | } 30 | 31 | /** 32 | * The current global hider of these layers 33 | * @param globalHider 34 | */ 35 | set globalHider(globalHider: GlobalHider) { 36 | check(globalHider, GlobalHider); 37 | 38 | this._globalHider = globalHider; 39 | this._array.forEach((vectorClusterGroup) => { 40 | vectorClusterGroup.setGlobalHider(this._globalHider); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/vectorCluster/vectorClusterGroupObliqueImpl.ts: -------------------------------------------------------------------------------- 1 | import OLVectorLayer from 'ol/layer/Vector.js'; 2 | import VectorClusterGroupImpl from './vectorClusterGroupImpl.js'; 3 | import ObliqueMap from '../map/obliqueMap.js'; 4 | import VcsCluster from '../ol/source/VcsCluster.js'; 5 | import { VectorClusterGroupImplementationOptions } from './vectorClusterGroup.js'; 6 | import { 7 | createSourceObliqueSync, 8 | SourceObliqueSync, 9 | } from '../layer/oblique/sourceObliqueSync.js'; 10 | import { vectorClusterGroupName } from './vectorClusterSymbols.js'; 11 | 12 | export default class VectorClusterGroupObliqueImpl extends VectorClusterGroupImpl { 13 | private _clusterSource: VcsCluster; 14 | 15 | private _olLayer: OLVectorLayer | undefined; 16 | 17 | private _sourceObliqueSync: SourceObliqueSync; 18 | 19 | constructor( 20 | map: ObliqueMap, 21 | options: VectorClusterGroupImplementationOptions, 22 | ) { 23 | super(map, options); 24 | 25 | this._sourceObliqueSync = createSourceObliqueSync(options.source, map); 26 | this._clusterSource = new VcsCluster( 27 | { 28 | source: this._sourceObliqueSync.obliqueSource, 29 | distance: options.clusterDistance, 30 | }, 31 | this.name, 32 | ); 33 | } 34 | 35 | get clusterSource(): VcsCluster { 36 | return this._clusterSource; 37 | } 38 | 39 | get olLayer(): OLVectorLayer | undefined { 40 | return this._olLayer; 41 | } 42 | 43 | async initialize(): Promise { 44 | if (!this.initialized) { 45 | const olLayer = new OLVectorLayer({ 46 | visible: false, 47 | source: this._clusterSource, 48 | style: this.style, 49 | }); 50 | olLayer[vectorClusterGroupName] = this.name; 51 | this._olLayer = olLayer; 52 | this.map.addOLLayer(this._olLayer); 53 | } 54 | await super.initialize(); 55 | } 56 | 57 | async activate(): Promise { 58 | await super.activate(); 59 | if (this.active) { 60 | this._olLayer?.setVisible(true); 61 | this._clusterSource.paused = false; 62 | this._clusterSource.refresh(); 63 | this._sourceObliqueSync.activate(); 64 | } 65 | } 66 | 67 | deactivate(): void { 68 | super.deactivate(); 69 | this._olLayer?.setVisible(false); 70 | this._clusterSource.paused = true; 71 | this._sourceObliqueSync.deactivate(); 72 | } 73 | 74 | destroy(): void { 75 | if (this._olLayer) { 76 | this.map.removeOLLayer(this._olLayer); 77 | } 78 | this._olLayer = undefined; 79 | 80 | this._sourceObliqueSync.destroy(); 81 | super.destroy(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/vectorCluster/vectorClusterGroupOpenlayersImpl.ts: -------------------------------------------------------------------------------- 1 | import OLVectorLayer from 'ol/layer/Vector.js'; 2 | import { VectorClusterGroupImplementationOptions } from './vectorClusterGroup.js'; 3 | import OpenlayersMap from '../map/openlayersMap.js'; 4 | import VcsCluster from '../ol/source/VcsCluster.js'; 5 | import VectorClusterGroupImpl from './vectorClusterGroupImpl.js'; 6 | import { vectorClusterGroupName } from './vectorClusterSymbols.js'; 7 | 8 | export default class VectorClusterGroupOpenlayersImpl extends VectorClusterGroupImpl { 9 | static get className(): string { 10 | return 'VectorClusterGroupOpenlayersImpl'; 11 | } 12 | 13 | private _clusterSource: VcsCluster; 14 | 15 | private _olLayer: OLVectorLayer | undefined; 16 | 17 | constructor( 18 | map: OpenlayersMap, 19 | options: VectorClusterGroupImplementationOptions, 20 | ) { 21 | super(map, options); 22 | this._clusterSource = new VcsCluster( 23 | { 24 | source: options.source, 25 | distance: options.clusterDistance, 26 | }, 27 | this.name, 28 | ); 29 | this._clusterSource.paused = true; 30 | } 31 | 32 | async initialize(): Promise { 33 | if (!this.initialized) { 34 | const olLayer = new OLVectorLayer({ 35 | visible: false, 36 | source: this._clusterSource, 37 | style: this.style, 38 | }); 39 | 40 | olLayer[vectorClusterGroupName] = this.name; 41 | this._olLayer = olLayer; 42 | this.map.addOLLayer(this._olLayer); 43 | } 44 | await super.initialize(); 45 | } 46 | 47 | get clusterSource(): VcsCluster { 48 | return this._clusterSource; 49 | } 50 | 51 | get olLayer(): OLVectorLayer | undefined { 52 | return this._olLayer; 53 | } 54 | 55 | async activate(): Promise { 56 | await super.activate(); 57 | if (this.active) { 58 | this._olLayer?.setVisible(true); 59 | this._clusterSource.paused = false; 60 | this._clusterSource.refresh(); 61 | } 62 | } 63 | 64 | deactivate(): void { 65 | super.deactivate(); 66 | this._olLayer?.setVisible(false); 67 | this._clusterSource.paused = true; 68 | } 69 | 70 | destroy(): void { 71 | if (this._olLayer) { 72 | this.map.removeOLLayer(this._olLayer); 73 | } 74 | this._olLayer = undefined; 75 | 76 | this._clusterSource.clear(true); 77 | this._clusterSource.dispose(); 78 | super.destroy(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/vectorCluster/vectorClusterSymbols.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export const vectorClusterGroupName = Symbol('vectorClusterGroupName'); 3 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@vcsuite/eslint-config/mocha"], 3 | "rules": { 4 | "import/extensions": ["error", "always"], 5 | "import/no-unresolved": "off" 6 | }, 7 | "globals": { 8 | "expect": "readonly", 9 | "sinon": "readonly", 10 | "createCanvas": "readonly" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/data/dynamicPointCzml.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "document", 4 | "name": "CZML Point - Time Dynamic", 5 | "version": "1.0", 6 | "clock": { 7 | "interval": "2012-08-04T16:00:00Z/2012-08-04T16:00:32Z", 8 | "currentTime": "2012-08-04T16:00:00Z", 9 | "multiplier": 1 10 | } 11 | }, 12 | { 13 | "id": "point", 14 | "availability": "2012-08-04T16:00:00Z/2012-08-04T16:00:32Z", 15 | "position": { 16 | "epoch": "2012-08-04T16:00:00Z", 17 | "cartographicDegrees": [ 18 | 0, 13.418745425028446, 52.499061993250024, 50, 1, 13.4187361511943, 19 | 52.49911931469231, 50, 2, 13.418708686080118, 52.499174433233264, 50, 3, 20 | 13.418664085154827, 52.49922523071035, 50, 4, 13.418604062406976, 21 | 52.499269755023136, 50, 5, 13.418530924477107, 52.499306295148614, 50, 22 | 6, 13.418447482014843, 52.499333446891654, 50, 7, 13.418356941667218, 23 | 52.499350166844096, 50, 8, 13.418262782849014, 52.499355812479706, 50, 24 | 9, 13.418168624030812, 52.499350166844096, 50, 10, 13.418078083683186, 25 | 52.499333446891654, 50, 11, 13.417994641220924, 52.499306295148614, 50, 26 | 12, 13.417921503291051, 52.499269755023136, 50, 13, 13.417861480543202, 27 | 52.49922523071035, 50, 14, 13.417816879617913, 52.499174433233264, 50, 28 | 15, 13.417789414503728, 52.49911931469231, 50, 16, 13.417780140669585, 29 | 52.499061993250024, 50, 17, 13.417789414503728, 52.49900467173299, 50, 30 | 18, 13.417816879617913, 52.49894955297921, 50, 19, 13.417861480543202, 31 | 52.49889875518363, 50, 20, 13.417921503291051, 52.49885423049514, 50, 32 | 21, 13.417994641220924, 52.498817689993956, 50, 22, 13.418078083683186, 33 | 52.49879053793239, 50, 23, 13.418168624030812, 52.4987738177671, 50, 24, 34 | 13.418262782849014, 52.49876817205677, 50, 25, 13.418356941667218, 35 | 52.4987738177671, 50, 26, 13.418447482014843, 52.49879053793239, 50, 27, 36 | 13.418530924477107, 52.498817689993956, 50, 28, 13.418604062406976, 37 | 52.49885423049514, 50, 29, 13.418664085154827, 52.49889875518363, 50, 38 | 30, 13.418708686080118, 52.49894955297921, 50, 31, 13.4187361511943, 39 | 52.49900467173299, 50, 32, 13.418745425028446, 52.499061993250024, 50 40 | ] 41 | }, 42 | "point": { 43 | "color": { 44 | "rgba": [255, 255, 255, 255] 45 | }, 46 | "outlineColor": { 47 | "rgba": [255, 0, 0, 255] 48 | }, 49 | "outlineWidth": 4, 50 | "pixelSize": 20 51 | } 52 | } 53 | ] 54 | -------------------------------------------------------------------------------- /tests/data/oblique/tiledImageData/image.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v3.5-3-gda4794e_64bit", 3 | "info": "- the corner points of the images are sorted like [lower left, lower right, upper right, upper left] always with respect to north - the view direction angle is either calculated from camera pose and center on ground or the center of footprint edges in view direction - if the images have same size the data can be found in generalImageInfo otherwise the data is stored in the data regarding the images - view-direction is north, east, south, west with 0, 1, 2, 3", 4 | "generalImageInfo": { 5 | "width": 11608, 6 | "height": 8708, 7 | "tile-resolution": [32, 16, 8, 4, 2, 1], 8 | "crs": "+proj=utm +zone=33 +ellps=GRS80 +units=m +no_defs ", 9 | "tile-width": 512, 10 | "tile-height": 512, 11 | "cameraParameter": [ 12 | { 13 | "name": "119_front", 14 | "principal-point": [5805.89, 4373.07], 15 | "pixel-size": [0.0046, 0.0046], 16 | "radial-distorsion-expected-2-found": [ 17 | 0.0000141899, -0.00369415, 0.0000585035, 0.00000290641, -2.00567e-9 18 | ], 19 | "radial-distorsion-found-2-expected": [ 20 | -0.0000126063, 0.00370512, -0.0000585165, -0.00000299588, 4.76671e-9 21 | ] 22 | }, 23 | { 24 | "name": "116_back", 25 | "principal-point": [5813.5, 4348.07], 26 | "pixel-size": [0.0046, 0.0046], 27 | "radial-distorsion-expected-2-found": [ 28 | 0.0000141899, -0.00369415, 0.0000585035, 0.00000290641, -2.00567e-9 29 | ], 30 | "radial-distorsion-found-2-expected": [ 31 | -0.0000126063, 0.00370512, -0.0000585165, -0.00000299588, 4.76671e-9 32 | ] 33 | }, 34 | { 35 | "name": "111_right", 36 | "principal-point": [5813.07, 4338.72], 37 | "pixel-size": [0.0046, 0.0046], 38 | "radial-distorsion-expected-2-found": [ 39 | 0.0000141899, -0.00369415, 0.0000585035, 0.00000290641, -2.00567e-9 40 | ], 41 | "radial-distorsion-found-2-expected": [ 42 | -0.0000126063, 0.00370512, -0.0000585165, -0.00000299588, 4.76671e-9 43 | ] 44 | }, 45 | { 46 | "name": "110_left", 47 | "principal-point": [5793.07, 4378.5], 48 | "pixel-size": [0.0046, 0.0046], 49 | "radial-distorsion-expected-2-found": [ 50 | 0.0000141899, -0.00369415, 0.0000585035, 0.00000290641, -2.00567e-9 51 | ], 52 | "radial-distorsion-found-2-expected": [ 53 | -0.0000126063, 0.00370512, -0.0000585165, -0.00000299588, 4.76671e-9 54 | ] 55 | } 56 | ] 57 | }, 58 | "availableTiles": [ 59 | "12/2199/1342", 60 | "12/2199/1343", 61 | "12/2199/1344", 62 | "12/2200/1342", 63 | "12/2200/1343", 64 | "12/2200/1344", 65 | "12/2201/1342", 66 | "12/2201/1343", 67 | "12/2201/1344" 68 | ], 69 | "tileLevel": 12 70 | } 71 | -------------------------------------------------------------------------------- /tests/data/terrain/13/8800/6485.terrain: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/861d2a016536d461cccd0a8fcf6fcd6ddeec249f/tests/data/terrain/13/8800/6485.terrain -------------------------------------------------------------------------------- /tests/data/terrain/13/8800/6486.terrain: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/861d2a016536d461cccd0a8fcf6fcd6ddeec249f/tests/data/terrain/13/8800/6486.terrain -------------------------------------------------------------------------------- /tests/data/terrain/13/8801/6485.terrain: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/861d2a016536d461cccd0a8fcf6fcd6ddeec249f/tests/data/terrain/13/8801/6485.terrain -------------------------------------------------------------------------------- /tests/data/terrain/13/8801/6486.terrain: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/861d2a016536d461cccd0a8fcf6fcd6ddeec249f/tests/data/terrain/13/8801/6486.terrain -------------------------------------------------------------------------------- /tests/data/terrain/layer.json: -------------------------------------------------------------------------------- 1 | { 2 | "tilejson": "2.1.0", 3 | "qmc-version": "3.5-0-g0f89f13_64bit_QMC_virtualcitySYSTEMS_GmbH", 4 | "version": "1.1536156513599457", 5 | "format": "quantized-mesh-1.0", 6 | "scheme": "tms", 7 | "extensions": ["octvertexnormals"], 8 | "tiles": ["{z}/{x}/{y}.terrain?v={version}"], 9 | "minzoom": 0, 10 | "maxzoom": 13, 11 | "bounds": [-180.0, -90.0, 180.0, 90.0], 12 | "projection": "EPSG:4326", 13 | "available": [ 14 | [ 15 | { 16 | "endY": 0, 17 | "endX": 1, 18 | "startX": 0, 19 | "startY": 0 20 | } 21 | ], 22 | [ 23 | { 24 | "endY": 1, 25 | "endX": 2, 26 | "startX": 2, 27 | "startY": 1 28 | } 29 | ], 30 | [ 31 | { 32 | "endY": 3, 33 | "endX": 4, 34 | "startX": 4, 35 | "startY": 3 36 | } 37 | ], 38 | [ 39 | { 40 | "endY": 6, 41 | "endX": 8, 42 | "startX": 8, 43 | "startY": 6 44 | } 45 | ], 46 | [ 47 | { 48 | "endY": 12, 49 | "endX": 17, 50 | "startX": 17, 51 | "startY": 12 52 | } 53 | ], 54 | [ 55 | { 56 | "endY": 25, 57 | "endX": 34, 58 | "startX": 34, 59 | "startY": 25 60 | } 61 | ], 62 | [ 63 | { 64 | "endY": 50, 65 | "endX": 68, 66 | "startX": 68, 67 | "startY": 50 68 | } 69 | ], 70 | [ 71 | { 72 | "endY": 101, 73 | "endX": 137, 74 | "startX": 137, 75 | "startY": 101 76 | } 77 | ], 78 | [ 79 | { 80 | "endY": 202, 81 | "endX": 275, 82 | "startX": 275, 83 | "startY": 202 84 | } 85 | ], 86 | [ 87 | { 88 | "endY": 405, 89 | "endX": 550, 90 | "startX": 550, 91 | "startY": 405 92 | } 93 | ], 94 | [ 95 | { 96 | "endY": 810, 97 | "endX": 1100, 98 | "startX": 1100, 99 | "startY": 810 100 | } 101 | ], 102 | [ 103 | { 104 | "endY": 1621, 105 | "endX": 2200, 106 | "startX": 2200, 107 | "startY": 1621 108 | } 109 | ], 110 | [ 111 | { 112 | "endY": 3243, 113 | "endX": 4400, 114 | "startX": 4400, 115 | "startY": 3242 116 | } 117 | ], 118 | [ 119 | { 120 | "endY": 6486, 121 | "endX": 8801, 122 | "startX": 8800, 123 | "startY": 6485 124 | } 125 | ] 126 | ] 127 | } 128 | -------------------------------------------------------------------------------- /tests/data/tile.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/861d2a016536d461cccd0a8fcf6fcd6ddeec249f/tests/data/tile.pbf -------------------------------------------------------------------------------- /tests/data/wgs84Points.fgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/861d2a016536d461cccd0a8fcf6fcd6ddeec249f/tests/data/wgs84Points.fgb -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import sinonChai from 'sinon-chai'; 3 | import sinon from 'sinon'; 4 | import canvas from 'canvas'; 5 | import canvasBindings from 'canvas/lib/bindings.js'; 6 | import fetch from 'node-fetch'; 7 | import ResizeObserverPolyfill from 'resize-observer-polyfill'; 8 | 9 | chai.use(sinonChai); 10 | 11 | global.XMLHttpRequest = window.XMLHttpRequest; 12 | global.expect = chai.expect; 13 | global.sinon = sinon; 14 | 15 | global.requestAnimationFrame = window.requestAnimationFrame; 16 | global.cancelAnimationFrame = window.cancelAnimationFrame; 17 | global.canvaslibrary = canvas; 18 | global.CESIUM_BASE_URL = 'cesium/Source/'; 19 | global.FileReader = window.FileReader; 20 | global.DOMParser = window.DOMParser; 21 | global.fetch = fetch; 22 | global.ResizeObserver = ResizeObserverPolyfill; 23 | global.ShadowRoot = Function; 24 | 25 | Object.assign(canvas, { 26 | CanvasGradient: canvasBindings.CanvasGradient, 27 | CanvasPattern: canvasBindings.CanvasPattern, 28 | }); 29 | ['CanvasRenderingContext2D', 'CanvasPattern', 'CanvasGradient'].forEach( 30 | (obj) => { 31 | global[obj] = canvas[obj]; 32 | }, 33 | ); 34 | global.createCanvas = canvas.createCanvas; 35 | -------------------------------------------------------------------------------- /tests/setupJsdom.js: -------------------------------------------------------------------------------- 1 | import jsdomGlobal from 'jsdom-global'; 2 | 3 | jsdomGlobal(undefined, { 4 | pretendToBeVisual: true, 5 | url: 'http://localhost', 6 | referrer: 'http://localhost', 7 | }); 8 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "node16", 5 | "incremental": true, 6 | "lib": ["esnext", "dom"], 7 | "allowJs": true, 8 | "checkJs": false, 9 | "jsx": "preserve", 10 | "outDir": "../.tests", 11 | "rootDir": "..", 12 | /* Strict Type-Checking Options */ 13 | "strict": true, 14 | "noImplicitAny": true, 15 | "strictNullChecks": true, 16 | 17 | /* Additional Checks */ 18 | "noImplicitReturns": false, 19 | "noFallthroughCasesInSwitch": false, 20 | 21 | /* Module Resolution Options */ 22 | "baseUrl": ".", 23 | "paths": { 24 | "rbush-knn": ["../types/rbush-knn"], 25 | "rbush": ["../types/rbush"] 26 | }, 27 | "moduleResolution": "node16", 28 | "resolveJsonModule": true, 29 | "esModuleInterop": true, 30 | "preserveSymlinks": true, 31 | "allowSyntheticDefaultImports": true, 32 | "types": ["mocha", "node"] 33 | }, 34 | "include": ["../types/", "../src/", "../index.ts", "."], 35 | "exclude": ["../dist", "../.tests", "../build"] 36 | } 37 | -------------------------------------------------------------------------------- /tests/unit/helpers/getFileNameFromUrl.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | /** 5 | * @param {string} url 6 | * @param {string} fileName 7 | * @returns {string} 8 | */ 9 | export default function getFileNameFromUrl(url, fileName) { 10 | const dirName = fileURLToPath(url); 11 | return path.join(dirName, fileName); 12 | } 13 | -------------------------------------------------------------------------------- /tests/unit/helpers/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Math as CesiumMath } from '@vcmap-cesium/engine'; 2 | import { expect } from 'chai'; 3 | 4 | /** 5 | * helper function to wait for a timeout use: await timeout(1); 6 | */ 7 | export function timeout(ms: number): Promise { 8 | return new Promise((resolve) => { 9 | setTimeout(resolve, ms); 10 | }); 11 | } 12 | 13 | export function arrayCloseTo( 14 | numbers: T, 15 | expectedNumbers: T, 16 | epsilon = CesiumMath.EPSILON8, 17 | message = '', 18 | ): void { 19 | expect(numbers.length).to.equal(expectedNumbers.length); 20 | numbers.forEach((c, index) => { 21 | expect(c).to.be.closeTo( 22 | expectedNumbers[index], 23 | epsilon, 24 | `Array at index ${index}${message}`, 25 | ); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /tests/unit/helpers/imageHelpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * black Pixel dataURI 3 | * @type {string} 4 | */ 5 | export const blackPixelURI = 6 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQYV2NgYGD4DwABBAEAcCBlCwAAAABJRU5ErkJggg=='; 7 | /** 8 | * green Pixel dataURI 9 | * @type {string} 10 | */ 11 | export const greenPixelURI = 12 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQYV2Ng+M/wHwAEAQH/Xi7hpQAAAABJRU5ErkJggg=='; 13 | /** 14 | * red Pixel dataURI 15 | * @type {string} 16 | */ 17 | export const redPixelURI = 18 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQYV2P4z8DwHwAFAAH/plybXQAAAABJRU5ErkJggg=='; 19 | /** 20 | * blue Pixel dataURI 21 | * @type {string} 22 | */ 23 | export const bluePixelURI = 24 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQYV2NgYPj/HwADAgH/ybKt7gAAAABJRU5ErkJggg=='; 25 | -------------------------------------------------------------------------------- /tests/unit/helpers/importJSON.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | /** 4 | * @param {string} fileName 5 | * @returns {Promise} 6 | */ 7 | export default async function importJSON(fileName) { 8 | if (fs.existsSync(fileName)) { 9 | const content = await fs.promises.readFile(fileName); 10 | return JSON.parse(content.toString()); 11 | } 12 | // eslint-disable-next-line no-console 13 | console.log(`${fileName} does not exist`); 14 | return {}; 15 | } 16 | -------------------------------------------------------------------------------- /tests/unit/helpers/openlayersHelpers.js: -------------------------------------------------------------------------------- 1 | import OpenlayersMap from '../../../src/map/openlayersMap.js'; 2 | 3 | /** 4 | * @param {OpenlayersOptions=} mapOptions 5 | * @returns {Promise} 6 | */ 7 | export async function getOpenlayersMap(mapOptions) { 8 | const map = new OpenlayersMap(mapOptions || {}); 9 | await map.initialize(); 10 | return map; 11 | } 12 | 13 | /** 14 | * @param {VcsApp} app 15 | * @returns {Promise} 16 | */ 17 | export async function setOpenlayersMap(app) { 18 | const map = await getOpenlayersMap({ 19 | layerCollection: app.layers, 20 | target: app.maps.target, 21 | }); 22 | app.maps.add(map); 23 | await app.maps.setActiveMap(map.name); 24 | return map; 25 | } 26 | -------------------------------------------------------------------------------- /tests/unit/helpers/terrain/terrainData.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { CesiumTerrainProvider } from '@vcmap-cesium/engine'; 3 | import importJSON from '../importJSON.js'; 4 | import getFileNameFromUrl from '../getFileNameFromUrl.js'; 5 | 6 | const fileName = getFileNameFromUrl( 7 | import.meta.url, 8 | '../../../../data/terrain/layer.json', 9 | ); 10 | export const layerJson = await importJSON(fileName); 11 | export const terrainFiles = { 12 | 1388006485: './tests/data/terrain/13/8800/6485.terrain', 13 | 1388006486: './tests/data/terrain/13/8800/6486.terrain', 14 | 1388016485: './tests/data/terrain/13/8801/6485.terrain', 15 | 1388016486: './tests/data/terrain/13/8801/6486.terrain', 16 | }; 17 | 18 | /** 19 | * serves http://myTerrainProvider/terrain/ 20 | * @param {import("nock").Scope} scope 21 | */ 22 | export function setTerrainServer(scope) { 23 | scope 24 | .get('/terrain/layer.json') 25 | .reply(200, layerJson, { 'Content-Type': 'application/json' }) 26 | .get(/terrain\/(\d{2})\/(\d{4})\/(\d{4})\.terrain.*/) 27 | .reply((uri) => { 28 | const [x, y] = uri.match(/(\d{4})/g); 29 | const terrainFile = terrainFiles[`13${x}${y}`]; 30 | const res = terrainFile 31 | ? fs.createReadStream(terrainFiles[`13${x}${y}`]) 32 | : Buffer.from(''); 33 | return [200, res, { 'Content-Type': 'application/vnd.quantized-mesh' }]; 34 | }) 35 | .persist(); 36 | } 37 | 38 | /** 39 | * @param {Scope} scope 40 | * @returns {Promise} 41 | */ 42 | export async function getTerrainProvider(scope) { 43 | setTerrainServer(scope); 44 | return CesiumTerrainProvider.fromUrl('http://localhost/terrain/', {}); 45 | } 46 | -------------------------------------------------------------------------------- /tests/unit/interaction/abstractInteraction.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventType, 3 | ModificationKeyType, 4 | PointerKeyType, 5 | } from '../../../src/interaction/interactionType.js'; 6 | import AbstractInteraction from '../../../src/interaction/abstractInteraction.js'; 7 | 8 | describe('AbstractInteraction', () => { 9 | let AI; 10 | beforeEach(() => { 11 | AI = new AbstractInteraction(); 12 | }); 13 | describe('#setModification', () => { 14 | it('should set the modification', () => { 15 | AI.setModification(ModificationKeyType.CTRL); 16 | expect(AI).to.have.property('modificationKey', ModificationKeyType.CTRL); 17 | }); 18 | 19 | it('should reset the default modification key if called without arguments', () => { 20 | AI.setModification(ModificationKeyType.CTRL); 21 | expect(AI).to.have.property('modificationKey', ModificationKeyType.CTRL); 22 | AI.setModification(); 23 | expect(AI).to.have.property('modificationKey', ModificationKeyType.NONE); 24 | }); 25 | }); 26 | 27 | describe('#setPointer', () => { 28 | it('should set the pointer', () => { 29 | AI.setPointer(PointerKeyType.RIGHT); 30 | expect(AI).to.have.property('pointerKey', PointerKeyType.RIGHT); 31 | }); 32 | 33 | it('should reset the default pointer key if called without arguments', () => { 34 | AI.setPointer(PointerKeyType.RIGHT); 35 | expect(AI).to.have.property('pointerKey', PointerKeyType.RIGHT); 36 | AI.setPointer(); 37 | expect(AI).to.have.property('pointerKey', PointerKeyType.LEFT); 38 | }); 39 | }); 40 | 41 | describe('#setActive', () => { 42 | it('should set the active event, if called with a number', () => { 43 | AI.setActive(EventType.MOVE); 44 | expect(AI).to.have.property('active', EventType.MOVE); 45 | }); 46 | 47 | it('should toggle the default active if called with a boolean', () => { 48 | expect(AI).to.have.property('active', EventType.NONE); 49 | expect(AI).to.have.property('modificationKey', ModificationKeyType.NONE); 50 | 51 | AI._defaultActive = EventType.MOVE; 52 | AI.setActive(true); 53 | expect(AI).to.have.property('active', EventType.MOVE); 54 | AI.setModification(ModificationKeyType.CTRL); 55 | AI.setActive(false); 56 | expect(AI).to.have.property('active', EventType.NONE); 57 | expect(AI).to.have.property('modificationKey', ModificationKeyType.CTRL); 58 | }); 59 | 60 | it('should reset all defaults, if called without arguments', () => { 61 | AI.setModification(ModificationKeyType.CTRL); 62 | AI.setPointer(PointerKeyType.MIDDLE); 63 | AI.setActive(); 64 | expect(AI).to.have.property('active', EventType.NONE); 65 | expect(AI).to.have.property('modificationKey', ModificationKeyType.NONE); 66 | expect(AI).to.have.property('pointerKey', PointerKeyType.LEFT); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /tests/unit/interaction/coordinateAtPixel.spec.js: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | import { EventType } from '../../../src/interaction/interactionType.js'; 3 | import CoordinateAtPixel from '../../../src/interaction/coordinateAtPixel.js'; 4 | import { getObliqueMap, setObliqueMap } from '../helpers/obliqueHelpers.js'; 5 | import VcsApp from '../../../src/vcsApp.js'; 6 | 7 | describe('CoordinateAtPixel', () => { 8 | let app; 9 | 10 | before(() => { 11 | app = new VcsApp(); 12 | }); 13 | 14 | after(() => { 15 | app.destroy(); 16 | nock.cleanAll(); 17 | }); 18 | 19 | describe('~obliqueHandler', () => { 20 | it('should transform image coordinates to wgs84, and project to mercator', async () => { 21 | const map = await setObliqueMap(app); 22 | const position = [1, 1, 1]; 23 | const event = await CoordinateAtPixel.obliqueHandler({ 24 | map, 25 | position, 26 | type: EventType.CLICK, 27 | }); 28 | expect(event) 29 | .to.have.property('position') 30 | .and.to.have.members([1488844.5237925982, 6891361.880123189, 0]); 31 | expect(event) 32 | .to.have.property('obliqueParameters') 33 | .and.to.have.property('pixel') 34 | .and.to.have.members([1, 1]); 35 | }); 36 | 37 | it('should stop propagation if no currentImage exists', async () => { 38 | const map = await getObliqueMap(); 39 | const position = [1, 1, 1]; 40 | const event = await CoordinateAtPixel.obliqueHandler({ 41 | map, 42 | position, 43 | type: EventType.CLICK, 44 | }); 45 | expect(event).to.have.property('stopPropagation').and.to.be.true; 46 | map.destroy(); 47 | }); 48 | }); 49 | 50 | describe('with terrainProvider', () => { 51 | let scope; 52 | let map; 53 | 54 | before(async () => { 55 | scope = nock('http://localhost'); 56 | map = await setObliqueMap(app, scope); 57 | }); 58 | 59 | it('should use exact coordinate transformation on CLICK', async () => { 60 | const position = [1, 1, 1]; 61 | const event = await CoordinateAtPixel.obliqueHandler({ 62 | map, 63 | position, 64 | type: EventType.CLICK, 65 | }); 66 | expect(event) 67 | .to.have.property('obliqueParameters') 68 | .and.to.have.property('estimate').and.to.be.false; 69 | }); 70 | 71 | it('should use estimated coordinate transformation on MOVE', async () => { 72 | const position = [1, 1, 1]; 73 | const event = await CoordinateAtPixel.obliqueHandler({ 74 | map, 75 | position, 76 | type: EventType.MOVE, 77 | }); 78 | expect(event) 79 | .to.have.property('obliqueParameters') 80 | .and.to.have.property('estimate').and.to.be.true; 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /tests/unit/layer/cesium/dataSourceCesiumImpl.spec.js: -------------------------------------------------------------------------------- 1 | import { Entity } from '@vcmap-cesium/engine'; 2 | import { setCesiumMap } from '../../helpers/cesiumHelpers.js'; 3 | import VcsApp from '../../../../src/vcsApp.js'; 4 | import DataSourceLayer from '../../../../src/layer/dataSourceLayer.js'; 5 | import GlobalHider from '../../../../src/layer/globalHider.js'; 6 | 7 | describe('DataSourceCesiumImpl', () => { 8 | let app; 9 | let map; 10 | 11 | before(async () => { 12 | app = new VcsApp(); 13 | map = await setCesiumMap(app); 14 | }); 15 | 16 | after(() => { 17 | app.destroy(); 18 | }); 19 | 20 | describe('synchronizing of entity collections', () => { 21 | /** @type {import("@vcmap/core").DataSourceLayer} */ 22 | let layer; 23 | /** @type {import("@vcmap/core").DataSourceCesiumImpl} */ 24 | let impl; 25 | let initialEntity; 26 | 27 | before(async () => { 28 | layer = new DataSourceLayer({}); 29 | initialEntity = new Entity(); 30 | layer.addEntity(initialEntity); 31 | layer.setGlobalHider(new GlobalHider()); 32 | await layer.initialize(); 33 | [impl] = layer.getImplementationsForMap(map); 34 | await impl.initialize(); 35 | }); 36 | 37 | after(() => { 38 | layer.destroy(); 39 | }); 40 | 41 | it('should add initial entities to the data source', () => { 42 | const entity = impl.dataSource.entities.getById(initialEntity.id); 43 | expect(entity).to.equal(initialEntity); 44 | }); 45 | 46 | it('should add newly added entities to the data source', () => { 47 | const id = layer.addEntity({}); 48 | expect(impl.dataSource.entities.getById(id)).to.be.an.instanceof(Entity); 49 | }); 50 | 51 | it('should remove previously added entities', () => { 52 | const id = layer.addEntity({}); 53 | layer.removeEntityById(id); 54 | expect(impl.dataSource.entities.getById(id)).to.be.undefined; 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/unit/layer/cesium/getDummyCesium3DTileset.js: -------------------------------------------------------------------------------- 1 | import { 2 | Event as CesiumEvent, 3 | Cesium3DTileColorBlendMode, 4 | BoundingSphere, 5 | Matrix4, 6 | ClippingPlaneCollection, 7 | Cesium3DTileset as ActualCesium3DTileset, 8 | } from '@vcmap-cesium/engine'; 9 | 10 | class Cesium3DTileset { 11 | constructor() { 12 | this.extras = {}; 13 | this.colorBlendMode = Cesium3DTileColorBlendMode.HIGHLIGHT; 14 | this.tileVisible = new CesiumEvent(); 15 | this.tileUnload = new CesiumEvent(); 16 | this.loadProgress = new CesiumEvent(); 17 | this.clippingPlanes = new ClippingPlaneCollection(); 18 | this.clippingPlanesOriginMatrix = Matrix4.IDENTITY; 19 | this.boundingSphere = new BoundingSphere(undefined, 1); 20 | this.style = null; 21 | this.root = { 22 | transform: Matrix4.clone(Matrix4.IDENTITY), 23 | boundingVolume: {}, 24 | boundingSphere: {}, 25 | }; 26 | } 27 | 28 | destroy() { 29 | this.clippingPlanes = null; 30 | this.tileVisible = null; 31 | this.tileUnload = null; 32 | this.loadProgress = null; 33 | } 34 | } 35 | 36 | /** 37 | * @returns {Cesium/Cesium3DTileset} 38 | */ 39 | function getDummyCesium3DTileset() { 40 | const dummy = new Cesium3DTileset(); 41 | // eslint-disable-next-line no-proto 42 | dummy.__proto__ = ActualCesium3DTileset.prototype; // proto hack to fool instanceof checks 43 | return dummy; 44 | } 45 | 46 | export default getDummyCesium3DTileset; 47 | -------------------------------------------------------------------------------- /tests/unit/layer/cesium/vcsTile/vcsTileHelpers.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { getDataTiles } from '../../../../../src/layer/cesium/vcsTile/vcsTileHelpers.js'; 3 | import { TileProvider } from '../../../../../index.js'; 4 | 5 | describe('getDataTiles', () => { 6 | it('should only load min level, with just one base level', () => { 7 | const { dataLevels, dataRange } = getDataTiles( 8 | 18, 9 | 20, 10 | new TileProvider({ 11 | baseLevels: [10], 12 | }), 13 | ); 14 | expect([...dataLevels]).to.have.ordered.members([18]); 15 | expect(dataRange).to.have.ordered.members([18, 18]); 16 | }); 17 | 18 | it('should extract data levels between min and max level', () => { 19 | const { dataLevels, dataRange } = getDataTiles( 20 | 18, 21 | 20, 22 | new TileProvider({ 23 | baseLevels: [10, 19], 24 | }), 25 | ); 26 | expect([...dataLevels]).to.have.ordered.members([18, 19]); 27 | expect(dataRange).to.have.ordered.members([18, 19]); 28 | }); 29 | 30 | it('should throw, all levels are bellow min level', () => { 31 | expect(() => { 32 | getDataTiles( 33 | 8, 34 | 9, 35 | new TileProvider({ 36 | baseLevels: [10, 19], 37 | }), 38 | ); 39 | }).to.throw(Error); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/unit/layer/czmlLayer.spec.js: -------------------------------------------------------------------------------- 1 | import { JulianDate, DataSourceClock } from '@vcmap-cesium/engine'; 2 | import CzmlLayer from '../../../src/layer/czmlLayer.js'; 3 | import { vcsLayerName } from '../../../src/layer/layerSymbols.js'; 4 | import importJSON from '../helpers/importJSON.js'; 5 | 6 | const dynamicPoint = await importJSON('./tests/data/dynamicPointCzml.json'); 7 | 8 | describe('CzmlLayer', () => { 9 | describe('loading of data', () => { 10 | let sandbox; 11 | let layer; 12 | 13 | before(async () => { 14 | sandbox = sinon.createSandbox(); 15 | const server = sandbox.useFakeServer(); 16 | server.autoRespond = true; 17 | server.respondImmediately = true; 18 | server.respondWith('/dynamicPoint.czml', (res) => { 19 | res.respond( 20 | 200, 21 | { 'Content-Type': 'application/json' }, 22 | JSON.stringify(dynamicPoint), 23 | ); 24 | }); 25 | layer = new CzmlLayer({ 26 | url: '/dynamicPoint.czml', 27 | }); 28 | await layer.initialize(); 29 | }); 30 | 31 | after(() => { 32 | layer.destroy(); 33 | sandbox.restore(); 34 | }); 35 | 36 | it('should load all the entities in the czml', () => { 37 | expect(layer.entities.values).to.have.lengthOf(1); 38 | }); 39 | 40 | it('should set the layer name on all entities', () => { 41 | expect(layer.entities.values[0]).to.have.property( 42 | vcsLayerName, 43 | layer.name, 44 | ); 45 | }); 46 | 47 | it('should set the clock from the czml', () => { 48 | const currentTime = JulianDate.fromIso8601('2012-08-04T16:00:00Z'); 49 | expect(layer.clock).to.be.instanceOf(DataSourceClock); 50 | expect(layer.clock.currentTime).to.eql(currentTime); 51 | }); 52 | }); 53 | 54 | describe('getting a config', () => { 55 | describe('of a default object', () => { 56 | it('should return an object with type and name for default layers', () => { 57 | const configuredLayer = new CzmlLayer({}); 58 | const config = configuredLayer.toJSON(); 59 | expect(config).to.have.all.keys('name', 'type'); 60 | configuredLayer.destroy(); 61 | }); 62 | }); 63 | 64 | describe('of a configured layer', () => { 65 | let inputConfig; 66 | let outputConfig; 67 | let configuredLayer; 68 | 69 | before(() => { 70 | inputConfig = { 71 | sourceUri: 'http://localhost', 72 | }; 73 | configuredLayer = new CzmlLayer(inputConfig); 74 | outputConfig = configuredLayer.toJSON(); 75 | }); 76 | 77 | after(() => { 78 | configuredLayer.destroy(); 79 | }); 80 | 81 | it('should configure sourceUri', () => { 82 | expect(outputConfig).to.have.property( 83 | 'sourceUri', 84 | inputConfig.sourceUri, 85 | ); 86 | }); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /tests/unit/layer/pointCloudLayer.spec.js: -------------------------------------------------------------------------------- 1 | import PointCloudLayer from '../../../src/layer/pointCloudLayer.js'; 2 | import VectorStyleItem from '../../../src/style/vectorStyleItem.js'; 3 | 4 | describe('PointCloudLayer', () => { 5 | let sandbox; 6 | /** @type {import("@vcmap/core").PointCloudLayer} */ 7 | let PCL; 8 | 9 | before(() => { 10 | sandbox = sinon.createSandbox(); 11 | }); 12 | 13 | beforeEach(() => { 14 | PCL = new PointCloudLayer({}); 15 | }); 16 | 17 | afterEach(() => { 18 | PCL.destroy(); 19 | sandbox.restore(); 20 | }); 21 | 22 | describe('setStyle', () => { 23 | it('should not set a vector style item', () => { 24 | const style = new VectorStyleItem({}); 25 | PCL.setStyle(style); 26 | expect(PCL.style).to.not.equal(style); 27 | }); 28 | }); 29 | 30 | describe('clearStyle', () => { 31 | beforeEach(async () => { 32 | await PCL.initialize(); 33 | }); 34 | 35 | it('should set no pointSize, if no default was specified', () => { 36 | PCL.pointSize = 3; 37 | PCL.clearStyle(); 38 | expect(PCL.pointSize).to.be.undefined; 39 | }); 40 | 41 | it('should set the default point size, which is the given pointsize in the constructor', () => { 42 | PCL.defaultPointSize = 3; 43 | PCL.pointSize = undefined; 44 | PCL.clearStyle(); 45 | expect(PCL.pointSize).to.equal(3); 46 | }); 47 | }); 48 | 49 | describe('getting config objects', () => { 50 | describe('of a default object', () => { 51 | it('should return an object with type and name for default layers', () => { 52 | const config = PCL.toJSON(); 53 | expect(config).to.have.all.keys('name', 'type'); 54 | }); 55 | }); 56 | 57 | describe('of a configured layer', () => { 58 | let inputConfig; 59 | let outputConfig; 60 | let configuredLayer; 61 | 62 | before(() => { 63 | inputConfig = { 64 | pointSize: 3, 65 | }; 66 | configuredLayer = new PointCloudLayer(inputConfig); 67 | outputConfig = configuredLayer.toJSON(); 68 | }); 69 | 70 | after(() => { 71 | configuredLayer.destroy(); 72 | }); 73 | 74 | it('should configure pointSize', () => { 75 | expect(outputConfig).to.have.property( 76 | 'pointSize', 77 | inputConfig.pointSize, 78 | ); 79 | }); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /tests/unit/layer/singleImageLayer.spec.js: -------------------------------------------------------------------------------- 1 | import SingleImageLayer from '../../../src/layer/singleImageLayer.js'; 2 | import Extent from '../../../src/util/extent.js'; 3 | import { wgs84Projection } from '../../../src/util/projection.js'; 4 | 5 | describe('SingleImageLayer', () => { 6 | describe('constructing a single image layer', () => { 7 | it('should create a global extent, if the extent is invalid', () => { 8 | const layer = new SingleImageLayer({ 9 | extent: { coordinates: [1, 2, 3], projection: { epsg: 3123 } }, 10 | }); 11 | expect(layer.extent.extent).to.have.ordered.members([-180, -90, 180, 90]); 12 | expect(layer.extent.projection).to.have.property('epsg', 'EPSG:4326'); 13 | layer.destroy(); 14 | }); 15 | }); 16 | 17 | describe('setting the extent', () => { 18 | let layer; 19 | 20 | before(() => { 21 | layer = new SingleImageLayer({}); 22 | }); 23 | 24 | after(() => { 25 | layer.destroy(); 26 | }); 27 | 28 | it('should set a valid extent', () => { 29 | const extent = new Extent({ 30 | projection: wgs84Projection.toJSON(), 31 | coordinates: [0, 0, 180, 90], 32 | }); 33 | layer.setExtent(extent); 34 | expect(layer).to.have.property('extent', extent); 35 | }); 36 | 37 | it('should throw an error if passing in an invalid extent', () => { 38 | const extent = new Extent({ 39 | projection: wgs84Projection.toJSON(), 40 | coordinates: [0, 0], 41 | }); 42 | expect(layer.setExtent.bind(layer, extent)).to.throw(Error); 43 | }); 44 | }); 45 | 46 | describe('getting a config', () => { 47 | describe('of a default object', () => { 48 | it('should return an object with type and name for default layers', () => { 49 | const defaultLayer = new SingleImageLayer({}); 50 | const config = defaultLayer.toJSON(); 51 | expect(config).to.have.all.keys('name', 'type'); 52 | defaultLayer.destroy(); 53 | }); 54 | }); 55 | 56 | describe('of a configured layer', () => { 57 | let inputConfig; 58 | let outputConfig; 59 | let configuredLayer; 60 | 61 | before(() => { 62 | inputConfig = { 63 | credit: 'test', 64 | }; 65 | configuredLayer = new SingleImageLayer(inputConfig); 66 | outputConfig = configuredLayer.toJSON(); 67 | }); 68 | 69 | after(() => { 70 | configuredLayer.destroy(); 71 | }); 72 | 73 | it('should set credit', () => { 74 | expect(outputConfig).to.have.property('credit', inputConfig.credit); 75 | }); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /tests/unit/layer/terrainHelpers.spec.js: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | import { CesiumTerrainProvider } from '@vcmap-cesium/engine'; 3 | import { getTerrainProviderForUrl } from '../../../src/layer/terrainHelpers.js'; 4 | import { setTerrainServer } from '../helpers/terrain/terrainData.js'; 5 | 6 | describe('terrainHelpers', () => { 7 | let scope; 8 | 9 | before(() => { 10 | scope = nock('http://localhost'); 11 | setTerrainServer(scope); 12 | }); 13 | 14 | after(() => { 15 | nock.cleanAll(); 16 | }); 17 | 18 | describe('~getTerrainProviderForUrl', () => { 19 | it('should create a new terrain provider, if non is present for the passed url', async () => { 20 | const TP = await getTerrainProviderForUrl('http://localhost/terrain', {}); 21 | expect(TP).to.be.an.instanceOf(CesiumTerrainProvider); 22 | }); 23 | 24 | it('it should return the previously created terrain provider', async () => { 25 | const createdCTP = await getTerrainProviderForUrl( 26 | 'http://localhost/terrain', 27 | {}, 28 | ); 29 | const secondCTP = await getTerrainProviderForUrl( 30 | 'http://localhost/terrain', 31 | {}, 32 | ); 33 | expect(createdCTP).to.equal(secondCTP); 34 | }); 35 | 36 | it('should set the requestVertexNormals to true', async () => { 37 | const CTP = await getTerrainProviderForUrl('http://localhost/terrain', { 38 | requestVertexNormals: true, 39 | }); 40 | expect(CTP).to.have.property('requestVertexNormals').and.to.be.true; 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /tests/unit/layer/terrainLayer.spec.js.js: -------------------------------------------------------------------------------- 1 | import TerrainLayer from '../../../src/layer/terrainLayer.js'; 2 | 3 | describe('TerrainLayer', () => { 4 | describe('getting config objects', () => { 5 | describe('of a default object', () => { 6 | it('should return an object with type and name for default layers', () => { 7 | const config = new TerrainLayer({}).toJSON(); 8 | expect(config).to.have.all.keys('name', 'type'); 9 | }); 10 | }); 11 | 12 | describe('of a configured layer', () => { 13 | let inputConfig; 14 | let outputConfig; 15 | let configuredLayer; 16 | 17 | before(() => { 18 | inputConfig = { 19 | requestVertexNormals: false, 20 | requestWaterMask: true, 21 | }; 22 | configuredLayer = new TerrainLayer(inputConfig); 23 | outputConfig = configuredLayer.toJSON(); 24 | }); 25 | 26 | after(() => { 27 | configuredLayer.destroy(); 28 | }); 29 | 30 | it('should configure requestVertexNormals', () => { 31 | expect(outputConfig).to.have.property( 32 | 'requestVertexNormals', 33 | inputConfig.requestVertexNormals, 34 | ); 35 | }); 36 | 37 | it('should configure requestWaterMask', () => { 38 | expect(outputConfig).to.have.property( 39 | 'requestWaterMask', 40 | inputConfig.requestWaterMask, 41 | ); 42 | }); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/unit/layer/tileProvider/staticGeojsonTileProvider.spec.js: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | import Feature from 'ol/Feature.js'; 3 | import StaticGeoJSONTileProvider from '../../../../src/layer/tileProvider/staticGeojsonTileProvider.js'; 4 | import importJSON from '../../helpers/importJSON.js'; 5 | 6 | const testGeoJSON = await importJSON('./tests/data/testGeoJSON.json'); 7 | 8 | describe('StaticGeoJSONTileProvider', () => { 9 | /** @type {import("@vcmap/core").StaticGeoJSONTileProvider} */ 10 | let tileProvider; 11 | 12 | before(() => { 13 | tileProvider = new StaticGeoJSONTileProvider({ 14 | url: 'http://myStaticGeojsonTileProvider/tile.json', 15 | tileCacheSize: 10, 16 | baseLevels: [10], 17 | }); 18 | }); 19 | 20 | after(() => { 21 | tileProvider.destroy(); 22 | nock.cleanAll(); 23 | }); 24 | 25 | describe('constructor', () => { 26 | it('should set baseLevels to 0', () => { 27 | expect(tileProvider.baseLevels).to.have.members([0]); 28 | }); 29 | }); 30 | 31 | describe('loader', () => { 32 | let scope; 33 | let loaded; 34 | let requestHeaders; 35 | 36 | beforeEach(() => { 37 | scope = nock('http://myStaticGeojsonTileProvider') 38 | .get('/tile.json') 39 | .reply(function nockReply() { 40 | requestHeaders = this.req.headers; 41 | return [200, testGeoJSON.featureCollection]; 42 | }); 43 | }); 44 | 45 | afterEach(() => { 46 | scope.done(); 47 | requestHeaders = null; 48 | }); 49 | 50 | it('should return parsed features', async () => { 51 | loaded = await tileProvider.loader(1, 2, 3); 52 | expect(loaded).to.have.lengthOf(2); 53 | expect(loaded[0]).to.be.instanceOf(Feature); 54 | }); 55 | it('should send headers', async () => { 56 | await tileProvider.loader(1, 2, 3, { myheader: 't2' }); 57 | expect(requestHeaders).to.have.property('myheader', 't2'); 58 | }); 59 | }); 60 | 61 | describe('serialization', () => { 62 | describe('of a default tile provider', () => { 63 | it('should only return type and name', () => { 64 | const outputConfig = new StaticGeoJSONTileProvider({}).toJSON(); 65 | expect(outputConfig).to.have.all.keys(['type', 'name']); 66 | }); 67 | }); 68 | 69 | describe('of a configured tile provider', () => { 70 | let inputConfig; 71 | let outputConfig; 72 | 73 | before(() => { 74 | inputConfig = { 75 | url: 'myUrl', 76 | baseLevels: [15], 77 | }; 78 | outputConfig = new StaticGeoJSONTileProvider(inputConfig).toJSON(); 79 | }); 80 | 81 | it('should configure url', () => { 82 | expect(outputConfig).to.have.property('url', inputConfig.url); 83 | }); 84 | 85 | it('should never configure baseLevels', () => { 86 | expect(outputConfig).to.not.have.property('baseLevels'); 87 | }); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /tests/unit/layer/tmsLayer.spec.js: -------------------------------------------------------------------------------- 1 | import TMSLayer from '../../../src/layer/tmsLayer.js'; 2 | 3 | describe('TMSLayer', () => { 4 | describe('getting config objects', () => { 5 | describe('of a default object', () => { 6 | it('should return an object with type and name for default layers', () => { 7 | const config = new TMSLayer({}).toJSON(); 8 | expect(config).to.have.all.keys('name', 'type'); 9 | }); 10 | }); 11 | 12 | describe('of a configured layer', () => { 13 | let inputConfig; 14 | let outputConfig; 15 | let configuredLayer; 16 | 17 | before(() => { 18 | inputConfig = { 19 | tilingSchema: 'geographic', 20 | format: 'png', 21 | tileSize: [512, 512], 22 | }; 23 | configuredLayer = new TMSLayer(inputConfig); 24 | outputConfig = configuredLayer.toJSON(); 25 | }); 26 | 27 | after(() => { 28 | configuredLayer.destroy(); 29 | }); 30 | 31 | it('should configure tilingSchema', () => { 32 | expect(outputConfig).to.have.property( 33 | 'tilingSchema', 34 | inputConfig.tilingSchema, 35 | ); 36 | }); 37 | 38 | it('should configure format', () => { 39 | expect(outputConfig).to.have.property('format', inputConfig.format); 40 | }); 41 | 42 | it('should configure tileSize', () => { 43 | expect(outputConfig) 44 | .to.have.property('tileSize') 45 | .and.to.have.members(inputConfig.tileSize); 46 | }); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/unit/layer/wfsLayer.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import WFSLayer, { WFSOptions } from '../../../src/layer/wfsLayer.js'; 3 | 4 | describe('WFSLayer', () => { 5 | describe('getting config objects', () => { 6 | describe('of a configured layer', () => { 7 | let inputConfig: WFSOptions; 8 | let outputConfig: WFSOptions; 9 | let configuredLayer: WFSLayer; 10 | 11 | before(() => { 12 | inputConfig = { 13 | featureType: ['fType'], 14 | version: '2.0.0', 15 | getFeatureOptions: { 16 | TEST: 'true', 17 | }, 18 | featureNS: 'fNS', 19 | featurePrefix: 'fPrefix', 20 | }; 21 | configuredLayer = new WFSLayer(inputConfig); 22 | console.log(configuredLayer); 23 | outputConfig = configuredLayer.toJSON(); 24 | console.log(outputConfig); 25 | }); 26 | 27 | after(() => { 28 | configuredLayer.destroy(); 29 | }); 30 | 31 | it('should configure version', () => { 32 | expect(outputConfig).to.have.property('version', inputConfig.version); 33 | }); 34 | 35 | it('should configure featureType', () => { 36 | expect(outputConfig) 37 | .to.have.property('featureType') 38 | .and.to.have.members(inputConfig.featureType as string[]); 39 | }); 40 | 41 | it('should configure featureNS', () => { 42 | expect(outputConfig).to.have.property( 43 | 'featureNS', 44 | inputConfig.featureNS, 45 | ); 46 | }); 47 | 48 | it('should configure featurePrefix', () => { 49 | expect(outputConfig).to.have.property( 50 | 'featurePrefix', 51 | inputConfig.featurePrefix, 52 | ); 53 | }); 54 | 55 | it('should configure getFeatureOptions', () => { 56 | expect(outputConfig) 57 | .to.have.property('getFeatureOptions') 58 | .and.to.eql(inputConfig.getFeatureOptions); 59 | }); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/unit/map/navigation/cesiumNavigation.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Cartesian3 } from '@vcmap-cesium/engine'; 3 | import CesiumMap from '../../../../src/map/cesiumMap.js'; 4 | import CesiumNavigation from '../../../../src/map/navigation/cesiumNavigation.js'; 5 | import { getCesiumMap } from '../../helpers/cesiumHelpers.js'; 6 | 7 | describe('CesiumNavigation', () => { 8 | let map: CesiumMap; 9 | let cesiumNavigation: CesiumNavigation; 10 | 11 | before(() => { 12 | map = getCesiumMap(); 13 | cesiumNavigation = new CesiumNavigation(map); 14 | }); 15 | 16 | after(() => { 17 | map.destroy(); 18 | }); 19 | 20 | it('should update camera on movement', () => { 21 | const { camera } = map.getScene()!; 22 | const startPosition = camera.position.clone(new Cartesian3()); 23 | cesiumNavigation.update({ 24 | time: 0, 25 | duration: 1, 26 | input: { 27 | forward: 1, 28 | right: 0, 29 | up: 0, 30 | tiltDown: 0, 31 | rollRight: 0, 32 | turnRight: 0, 33 | }, 34 | }); 35 | expect(camera?.position.y).to.be.greaterThan(startPosition.y); 36 | }); 37 | it('should not update camera, if movement is below moveThreshold', () => { 38 | const { camera } = map.getScene()!; 39 | const startPosition = camera.position.clone(new Cartesian3()); 40 | cesiumNavigation.moveThreshold = 5; 41 | cesiumNavigation.update({ 42 | time: 0, 43 | duration: 1, 44 | input: { 45 | forward: 1, 46 | right: 0, 47 | up: 0, 48 | tiltDown: 0, 49 | rollRight: 0, 50 | turnRight: 0, 51 | }, 52 | }); 53 | expect(camera?.position.y).to.equal(startPosition.y); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/unit/map/navigation/easingHelper.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Math as CesiumMath } from '@vcmap-cesium/engine'; 3 | import { 4 | createEasing, 5 | NavigationEasing, 6 | } from '../../../../src/map/navigation/easingHelper.js'; 7 | import { 8 | ControllerInput, 9 | getZeroInput, 10 | fromArray, 11 | isNonZeroInput, 12 | } from '../../../../src/map/navigation/controller/controllerInput.js'; 13 | 14 | describe('Easing', () => { 15 | describe('create easing', () => { 16 | let easing: NavigationEasing; 17 | let time: number; 18 | let origin: ControllerInput; 19 | let target: ControllerInput; 20 | 21 | beforeEach(() => { 22 | time = performance.now(); 23 | origin = getZeroInput(); 24 | target = fromArray([1, 2, 3, 4, 5, 6]); 25 | }); 26 | 27 | it('should create an easing for provided duration', () => { 28 | easing = createEasing(time, 1000, origin, target); 29 | 30 | expect(easing).to.have.property('target', target); 31 | expect(easing).to.have.property('getMovementAtTime'); 32 | }); 33 | 34 | it('should return movement at time', () => { 35 | for (let i = 100; i < 1000; i += 100) { 36 | const { movement, finished } = easing.getMovementAtTime(time + i); 37 | 38 | expect(isNonZeroInput(movement.input)).to.be.true; 39 | expect(movement.time).to.be.closeTo(i / 1000, CesiumMath.EPSILON2); 40 | expect(finished).to.be.false; 41 | } 42 | const { movement, finished } = easing.getMovementAtTime(time + 1000); 43 | 44 | expect(movement.input).to.deep.equal(target); 45 | expect(finished).to.be.true; 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/unit/map/navigation/openlayersNavigation.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import OpenlayersMap from '../../../../src/map/openlayersMap.js'; 3 | import OpenlayersNavigation from '../../../../src/map/navigation/openlayersNavigation.js'; 4 | import { getOpenlayersMap } from '../../helpers/openlayersHelpers.js'; 5 | 6 | describe('OpenlayersNavigation', () => { 7 | let map: OpenlayersMap; 8 | let openlayersNavigation: OpenlayersNavigation; 9 | 10 | before(async () => { 11 | map = await getOpenlayersMap(); 12 | openlayersNavigation = new OpenlayersNavigation(map); 13 | }); 14 | 15 | after(() => { 16 | map.destroy(); 17 | }); 18 | 19 | it('should update camera on movement', () => { 20 | const view = map.olMap!.getView(); 21 | const startPosition = view.getCenter()!; 22 | openlayersNavigation.update({ 23 | time: 0, 24 | duration: 1, 25 | input: { 26 | forward: 1, 27 | right: 0, 28 | up: 0, 29 | tiltDown: 0, 30 | rollRight: 0, 31 | turnRight: 0, 32 | }, 33 | }); 34 | const newCenter = view.getCenter()!; 35 | expect(newCenter[1]).to.be.greaterThan(startPosition[1]); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/unit/map/navigation/viewHelper.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import OpenlayersMap from '../../../../src/map/openlayersMap.js'; 3 | import OpenlayersNavigation from '../../../../src/map/navigation/openlayersNavigation.js'; 4 | import { getOpenlayersMap } from '../../helpers/openlayersHelpers.js'; 5 | import { moveView } from '../../../../src/map/navigation/viewHelper.js'; 6 | import { getZeroInput } from '../../../../src/map/navigation/controller/controllerInput.js'; 7 | 8 | const inputScratch = getZeroInput(); 9 | 10 | describe('viewHelper moveView', () => { 11 | let map: OpenlayersMap; 12 | let openlayersNavigation: OpenlayersNavigation; 13 | 14 | before(async () => { 15 | map = await getOpenlayersMap(); 16 | openlayersNavigation = new OpenlayersNavigation(map); 17 | }); 18 | 19 | after(() => { 20 | map.destroy(); 21 | }); 22 | 23 | it('should move view forward', () => { 24 | const view = map.olMap!.getView(); 25 | const startPosition = view.getCenter()!; 26 | inputScratch.forward = 1; 27 | moveView(map, inputScratch, 1); 28 | const newCenter = view.getCenter()!; 29 | expect(newCenter[0]).to.equal(startPosition[0]); 30 | expect(newCenter[1]).to.be.greaterThan(startPosition[1]); 31 | inputScratch.forward = 0; 32 | }); 33 | 34 | it('should move view right', () => { 35 | const view = map.olMap!.getView(); 36 | const startPosition = view.getCenter()!; 37 | inputScratch.right = 1; 38 | moveView(map, inputScratch, 1); 39 | const newCenter = view.getCenter()!; 40 | expect(newCenter[0]).to.be.greaterThan(startPosition[0]); 41 | expect(newCenter[1]).to.equal(startPosition[1]); 42 | inputScratch.right = 0; 43 | }); 44 | 45 | it('should zoom out', () => { 46 | const view = map.olMap!.getView(); 47 | const initialZoom = view.getZoom()!; 48 | inputScratch.up = 1; 49 | moveView(map, inputScratch, 1); 50 | const newCenter = view.getCenter()!; 51 | expect(view.getZoom()!).to.be.lessThan(initialZoom); 52 | inputScratch.up = 0; 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/unit/oblique/obliqueImageMeta.spec.js: -------------------------------------------------------------------------------- 1 | import ObliqueImageMeta from '../../../src/oblique/obliqueImageMeta.js'; 2 | 3 | describe('ObliqueImageMeta', () => { 4 | let defaultOptions; 5 | 6 | before(() => { 7 | defaultOptions = { 8 | name: 'test', 9 | 'camera-matrix': [ 10 | [0, 0, 0], 11 | [0, 0, 0], 12 | [1, 1, 0], 13 | ], 14 | 'focal-length': 1, 15 | 'principal-point': [1, 1], 16 | 'radial-distorsion-expected-2-found': [1, 1, 1], 17 | }; 18 | }); 19 | 20 | it('should correctly calculate image coordinates with radial distortion - 1', () => { 21 | const meta = new ObliqueImageMeta({ 22 | ...defaultOptions, 23 | 'principal-point': [5834.37, 4362.2], 24 | 'pixel-size': [0.0046, 0.0046], 25 | 'radial-distorsion-found-2-expected': [ 26 | 0.000161722, 0.00421904, 0.0000305735, -0.00000912995, 3.9396e-8, 27 | ], 28 | }); 29 | 30 | const coordinate = [3358.7531972410193, 7119.501739914109]; 31 | 32 | const result = meta.radialDistortionCoordinate(coordinate, true); 33 | expect(result).to.have.members([3353.0790125906424, 7125.8215545120875]); 34 | }); 35 | 36 | it('should correctly calculate image coordinates with radial distortion - 2', () => { 37 | const meta = new ObliqueImageMeta({ 38 | ...defaultOptions, 39 | 'principal-point': [5823.91, 4376.41], 40 | 'pixel-size': [0.0046, 0.0046], 41 | 'radial-distorsion-found-2-expected': [ 42 | -0.000154022, -0.00421231, -0.0000274032, 0.00000871298, -2.8186e-8, 43 | ], 44 | }); 45 | 46 | const coordinate = [453.2752152989915, 8538.086841725162]; 47 | 48 | const result = meta.radialDistortionCoordinate(coordinate, true); 49 | expect(result).to.have.members([439.4363377121408, 8548.810515681307]); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/unit/ol/geom/circle.spec.js: -------------------------------------------------------------------------------- 1 | import Circle from 'ol/geom/Circle.js'; 2 | 3 | describe('ol.geom.Circle', () => { 4 | let circle; 5 | 6 | beforeEach(() => { 7 | circle = new Circle([1, 1, 1], 1, 'XYZ'); 8 | }); 9 | 10 | describe('#getCoordinates', () => { 11 | it('should get two coordinates, the center and the radius', () => { 12 | const coords = circle.getCoordinates(); 13 | expect(coords).to.have.length(2); 14 | expect(coords).to.have.deep.members([ 15 | [1, 1, 1], 16 | [2, 1, 1], 17 | ]); 18 | }); 19 | }); 20 | 21 | describe('#setCoordinates', () => { 22 | it('should set the coordinates based on the new coordinates', () => { 23 | circle.setCoordinates([ 24 | [2, 2, 2], 25 | [4, 2, 2], 26 | ]); 27 | const center = circle.getCenter(); 28 | const radius = circle.getRadius(); 29 | 30 | expect(center).to.have.members([2, 2, 2]); 31 | expect(radius).to.equal(2); 32 | }); 33 | 34 | it('should respect the layout', () => { 35 | circle.setCoordinates( 36 | [ 37 | [2, 2], 38 | [4, 2], 39 | ], 40 | 'XY', 41 | ); 42 | 43 | const center = circle.getCenter(); 44 | const radius = circle.getRadius(); 45 | 46 | expect(center).to.have.members([2, 2]); 47 | expect(radius).to.equal(2); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/unit/ol/render/canvas/canvasTileRenderer.spec.js: -------------------------------------------------------------------------------- 1 | import CanvasTileRenderer from '../../../../../src/ol/render/canvas/canvasTileRenderer.js'; 2 | 3 | describe('CanvasTileRenderer', () => { 4 | let canvasTileRenderer; 5 | 6 | beforeEach(() => { 7 | canvasTileRenderer = new CanvasTileRenderer( 8 | {}, 9 | 1, 10 | [0, 0, 1, 1], 11 | [1, 0, 0, 1, 0, 0], 12 | 0, 13 | undefined, 14 | undefined, 15 | 10, 16 | ); 17 | }); 18 | 19 | describe('imageScale_', () => { 20 | it('should apply the scaleY Factor to imageScale', () => { 21 | canvasTileRenderer.imageScale_ = [1, 1]; 22 | expect(canvasTileRenderer.imageScale_).to.have.ordered.members([1, 10]); 23 | }); 24 | }); 25 | 26 | describe('textScale_', () => { 27 | it('should apply the scaleY Factor to textScale', () => { 28 | canvasTileRenderer.textScale_ = [1, 1]; 29 | expect(canvasTileRenderer.textScale_).to.have.ordered.members([1, 10]); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/unit/style/styleFactory.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import DeclarativeStyleItem from '../../../src/style/declarativeStyleItem.js'; 3 | import VectorStyleItem, { 4 | defaultVectorStyle, 5 | } from '../../../src/style/vectorStyleItem.js'; 6 | import { getStyleOrDefaultStyle } from '../../../src/style/styleFactory.js'; 7 | 8 | describe('getStyleOrDefaultStyle', () => { 9 | it('should return an empty declarative style', () => { 10 | const style = getStyleOrDefaultStyle(undefined); 11 | expect(style).to.be.an.instanceOf(DeclarativeStyleItem); 12 | expect(style).to.have.property('show', 'true'); 13 | }); 14 | 15 | it('should return a new vector style item', () => { 16 | const style = getStyleOrDefaultStyle({ 17 | type: VectorStyleItem.className, 18 | stroke: { width: 5, color: '#FF00FF' }, 19 | name: 'foo', 20 | }) as VectorStyleItem; 21 | expect(style).to.be.an.instanceOf(VectorStyleItem); 22 | expect(style).to.have.property('stroke'); 23 | expect(style.name).to.equal('foo'); 24 | expect(style.stroke?.getWidth()).to.equal(5); 25 | }); 26 | 27 | it('should return a passed defaultStyle', () => { 28 | const style = getStyleOrDefaultStyle(defaultVectorStyle.clone()); 29 | expect(style) 30 | .to.have.property('fillColor') 31 | .to.have.members(defaultVectorStyle.fillColor!); 32 | }); 33 | 34 | it('should return a cloned, assigned to a passed default style', () => { 35 | const defaultStyle = defaultVectorStyle.clone(); 36 | const style = getStyleOrDefaultStyle( 37 | { 38 | type: VectorStyleItem.className, 39 | stroke: { width: 5, color: '#FF00FF' }, 40 | name: 'foo', 41 | }, 42 | defaultStyle, 43 | ) as VectorStyleItem; 44 | expect(style) 45 | .to.have.property('fillColor') 46 | .to.have.members(defaultVectorStyle.fillColor!); 47 | expect(style).to.have.property('stroke'); 48 | expect(style).to.not.equal(defaultStyle); 49 | expect(style.name).to.equal('foo'); 50 | expect(style.stroke?.getWidth()).to.equal(5); 51 | }); 52 | 53 | it('should return a new declarative style item', () => { 54 | const style = getStyleOrDefaultStyle({ 55 | type: DeclarativeStyleItem.className, 56 | declarativeStyle: { color: 'color("#FF00FF")' }, 57 | }); 58 | expect(style).to.be.an.instanceOf(DeclarativeStyleItem); 59 | expect(style).to.have.property('color', 'color("#FF00FF")'); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /tests/unit/style/writeStyle.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-template-curly-in-string */ 2 | import DeclarativeStyleItem from '../../../src/style/declarativeStyleItem.js'; 3 | import writeStyle from '../../../src/style/writeStyle.js'; 4 | import VectorStyleItem from '../../../src/style/vectorStyleItem.js'; 5 | 6 | describe('writeStyle', () => { 7 | it('should write a declarative style', async () => { 8 | const styleItem = new DeclarativeStyleItem({ 9 | declarativeStyle: { 10 | defines: { 11 | hasExtrusion: 'Number(${olcs_extrudedHeight}) > 0', 12 | }, 13 | pointOutlineColor: { 14 | conditions: [['Boolean(${image})===true', 'color("#00FF00")']], 15 | }, 16 | color: { 17 | conditions: [ 18 | ['Boolean(${noFill})===true', 'false'], 19 | [ 20 | '${class} === "up"', 21 | 'color("#FF0000") * vec4(1, 1, 1, ${hasExtrusion} ? 0.5 : 1.0)', 22 | ], 23 | [ 24 | '${class} === "middle"', 25 | 'color("#00FF00") * vec4(1, 1, 1, ${hasExtrusion} ? 0.5 : 1.0)', 26 | ], 27 | [ 28 | '${class} === "down"', 29 | 'color("#0000FF") * vec4(1, 1, 1, ${hasExtrusion} ? 0.5 : 1.0)', 30 | ], 31 | ['${image} === "sensor"', 'color("#FF00FF")'], 32 | ['${image} === "marker"', 'color("#00FFFF")'], 33 | ['true', 'color("#FFFFFF")'], 34 | ], 35 | }, 36 | labelText: '${pegel}', 37 | labelColor: { 38 | conditions: [ 39 | ['${pegel} > 3.5', 'color("#FF0000")'], 40 | ['${pegel} > 3', 'color("#00FF00")'], 41 | ['${pegel} <= 3', 'color("#0000FF")'], 42 | ], 43 | }, 44 | strokeColor: { 45 | conditions: [ 46 | ['${image} === "sensor"', 'color("#FF00FF")'], 47 | ['${image} === "marker"', 'color("#00FFFF")'], 48 | ['true', 'color("#000000")'], 49 | ], 50 | }, 51 | strokeWidth: '2', 52 | }, 53 | }); 54 | await styleItem.cesiumStyle.readyPromise; 55 | const vcsMeta = {}; 56 | writeStyle(styleItem, vcsMeta); 57 | const returnedStyle = new DeclarativeStyleItem(vcsMeta.style); 58 | await returnedStyle.cesiumStyle.readyPromise; 59 | expect(returnedStyle.cesiumStyle.style).to.eql(styleItem.cesiumStyle.style); 60 | }); 61 | 62 | it('should write a vector style', () => { 63 | const styleItem = new VectorStyleItem({}); 64 | const vcsMeta = {}; 65 | writeStyle(styleItem, vcsMeta); 66 | const returnedStyle = new VectorStyleItem(vcsMeta.style); 67 | expect(returnedStyle.style).to.eql(styleItem.style); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /tests/unit/util/editor/interactions/editFeaturesMouseOverInteraction.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Feature } from 'ol'; 3 | import { 4 | handlerSymbol, 5 | mouseOverSymbol, 6 | cursorMap, 7 | AxisAndPlanes, 8 | EditFeaturesMouseOverInteraction, 9 | SelectMultiFeatureInteraction, 10 | VectorLayer, 11 | } from '../../../../../index.js'; 12 | 13 | describe('EditFeaturesMouseOverInteraction', () => { 14 | let interaction; 15 | let layer; 16 | let feature; 17 | let handler; 18 | let cursorStyle; 19 | let selectFeaturesInteraction; 20 | 21 | before(() => { 22 | layer = new VectorLayer({}); 23 | feature = new Feature(); 24 | layer.addFeatures([feature]); 25 | handler = new Feature(); 26 | handler[handlerSymbol] = AxisAndPlanes.X; 27 | }); 28 | 29 | beforeEach(() => { 30 | cursorStyle = { cursor: '' }; 31 | selectFeaturesInteraction = new SelectMultiFeatureInteraction(layer); 32 | interaction = new EditFeaturesMouseOverInteraction(); 33 | interaction.cursorStyle = cursorStyle; 34 | }); 35 | 36 | afterEach(() => { 37 | layer.destroy(); 38 | selectFeaturesInteraction.destroy(); 39 | interaction.destroy(); 40 | }); 41 | 42 | describe('interaction with handler', () => { 43 | it('should change the cursor style, to translate, if hovering over a handler', async () => { 44 | await interaction.pipe({ feature: handler }); 45 | expect(cursorStyle.cursor).to.equal(cursorMap.translate); 46 | }); 47 | 48 | it('should change the cursor style, to auto, if cursor leaves handler', async () => { 49 | await interaction.pipe({ feature: handler }); 50 | await interaction.pipe({ feature: null }); 51 | expect(cursorStyle.cursor).to.equal(cursorMap.auto); 52 | }); 53 | }); 54 | 55 | describe('interaction with other features', () => { 56 | it('should not reset cursor style when style was changed by different interaction', async () => { 57 | cursorStyle.cursor = cursorMap.translateVertex; 58 | cursorStyle[mouseOverSymbol] = 'other_id'; 59 | await interaction.pipe({ feature }); 60 | expect(cursorStyle.cursor).to.equal(cursorMap.translateVertex); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /tests/unit/util/editor/interactions/editGeometryMouseOverInteraction.spec.js: -------------------------------------------------------------------------------- 1 | import { Feature } from 'ol'; 2 | import { 3 | EditGeometryMouseOverInteraction, 4 | ModificationKeyType, 5 | vcsLayerName, 6 | vertexSymbol, 7 | cursorMap, 8 | mouseOverSymbol, 9 | } from '../../../../../index.js'; 10 | 11 | describe('EditGeometryMouseOverInteraction', () => { 12 | let interaction; 13 | let layerName; 14 | let feature; 15 | let vertex; 16 | let cursorStyle; 17 | 18 | before(() => { 19 | layerName = 'foo'; 20 | feature = new Feature(); 21 | feature[vcsLayerName] = layerName; 22 | vertex = new Feature(); 23 | vertex[vertexSymbol] = true; 24 | }); 25 | 26 | beforeEach(() => { 27 | cursorStyle = { cursor: '' }; 28 | interaction = new EditGeometryMouseOverInteraction(layerName); 29 | interaction.cursorStyle = cursorStyle; 30 | }); 31 | 32 | afterEach(() => { 33 | interaction.destroy(); 34 | }); 35 | 36 | describe('interaction with vertices', () => { 37 | it('should change the cursor style, to translate, if hovering over a vertex', async () => { 38 | await interaction.pipe({ feature: vertex }); 39 | expect(cursorStyle.cursor).to.equal(cursorMap.translateVertex); 40 | }); 41 | 42 | it('should change the cursor style, to remove, if hovering over a vertex with shift', async () => { 43 | await interaction.pipe({ 44 | feature: vertex, 45 | key: ModificationKeyType.SHIFT, 46 | }); 47 | expect(cursorStyle.cursor).to.equal(cursorMap.removeVertex); 48 | }); 49 | 50 | it('should change the cursor style, to auto, if cursor leaves vertex', async () => { 51 | await interaction.pipe({ feature: vertex }); 52 | await interaction.pipe({ feature: null }); 53 | expect(cursorStyle.cursor).to.equal(cursorMap.auto); 54 | }); 55 | 56 | it('should change the cursor style, if modification key changes to shift while hovering over a vertex', async () => { 57 | await interaction.pipe({ feature: vertex }); 58 | interaction.modifierChanged(ModificationKeyType.SHIFT); 59 | expect(cursorStyle.cursor).to.equal(cursorMap.removeVertex); 60 | }); 61 | }); 62 | 63 | describe('interaction with other features', () => { 64 | it('should not reset cursor style when style was changed by different interaction', async () => { 65 | cursorStyle.cursor = cursorMap.scaleNESW; 66 | cursorStyle[mouseOverSymbol] = 'other_id'; 67 | await interaction.pipe({ feature }); 68 | expect(cursorStyle.cursor).to.equal(cursorMap.scaleNESW); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /tests/unit/util/editor/interactions/ensureHandlerSelectionInteraction.spec.js: -------------------------------------------------------------------------------- 1 | import { Feature } from 'ol'; 2 | import { Cartesian2 } from '@vcmap-cesium/engine'; 3 | import { getCesiumMap } from '../../../helpers/cesiumHelpers.js'; 4 | import { AxisAndPlanes, handlerSymbol } from '../../../../../index.js'; 5 | import EnsureHandlerSelectionInteraction from '../../../../../src/util/editor/interactions/ensureHandlerSelectionInteraction.js'; 6 | 7 | describe('EnsureHandlerSelectionInteraction', () => { 8 | let map; 9 | let drillResults; 10 | const currentFeatures = []; 11 | let ensureHandlerSelection; 12 | let drillPick; 13 | 14 | before(() => { 15 | map = getCesiumMap(); 16 | drillResults = [ 17 | { primitive: {} }, 18 | { primitive: { olFeature: {} } }, 19 | { primitive: { olFeature: { [handlerSymbol]: AxisAndPlanes.X } } }, 20 | ]; 21 | ensureHandlerSelection = new EnsureHandlerSelectionInteraction( 22 | currentFeatures, 23 | ); 24 | }); 25 | 26 | beforeEach(() => { 27 | drillPick = sinon.stub(map.getScene(), 'drillPick').returns(drillResults); 28 | }); 29 | 30 | afterEach(() => { 31 | currentFeatures.length = 0; 32 | drillPick.restore(); 33 | }); 34 | 35 | after(() => { 36 | map.destroy(); 37 | }); 38 | 39 | it('should ensure a handler is selected, if a feature is selected and a feature is on the event', async () => { 40 | const event = { 41 | feature: new Feature(), 42 | map, 43 | windowPosition: new Cartesian2(0, 0), 44 | }; 45 | currentFeatures.push(new Feature()); 46 | await ensureHandlerSelection.pipe(event); 47 | expect(event.feature).to.equal(drillResults[2].primitive.olFeature); 48 | }); 49 | 50 | it('should not drill pick the scene, if no feature is on the event', async () => { 51 | const event = { 52 | feature: undefined, 53 | map, 54 | windowPosition: new Cartesian2(0, 0), 55 | }; 56 | await ensureHandlerSelection.pipe(event); 57 | expect(drillPick).to.not.have.been.called; 58 | }); 59 | 60 | it('should not drill pick the scene, if no feature is selected', async () => { 61 | const event = { 62 | feature: new Feature(), 63 | map, 64 | windowPosition: new Cartesian2(0, 0), 65 | }; 66 | await ensureHandlerSelection.pipe(event); 67 | expect(drillPick).to.not.have.been.called; 68 | }); 69 | 70 | it('should not drill pick the scene, if the selected feature is a handler', async () => { 71 | const event = { 72 | feature: { [handlerSymbol]: AxisAndPlanes.X }, 73 | map, 74 | windowPosition: new Cartesian2(0, 0), 75 | }; 76 | await ensureHandlerSelection.pipe(event); 77 | expect(drillPick).to.not.have.been.called; 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /tests/unit/util/editor/transformation/setupTransformationHandler.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from 'uuid'; 2 | import { Feature } from 'ol'; 3 | import { Cartesian3, IntersectionTests } from '@vcmap-cesium/engine'; 4 | import { Point } from 'ol/geom.js'; 5 | import sinon from 'sinon'; 6 | import { 7 | AxisAndPlanes, 8 | createTransformationHandler, 9 | handlerSymbol, 10 | mercatorProjection, 11 | TransformationHandler, 12 | TransformationMode, 13 | VcsApp, 14 | VcsMap, 15 | VectorLayer, 16 | } from '../../../../../index.js'; 17 | 18 | export type TransformationSetup = { 19 | transformationHandler: TransformationHandler; 20 | app: VcsApp; 21 | layer: VectorLayer; 22 | scratchLayer: VectorLayer; 23 | destroy: () => void; 24 | }; 25 | 26 | export async function setupTransformationHandler( 27 | map: VcsMap, 28 | mode: TransformationMode, 29 | ): Promise { 30 | const app = new VcsApp(); 31 | app.maps.add(map); 32 | const scratchLayer = new VectorLayer({ 33 | projection: mercatorProjection.toJSON(), 34 | }); 35 | const layer = new VectorLayer({ 36 | projection: mercatorProjection.toJSON(), 37 | }); 38 | app.layers.add(scratchLayer); 39 | app.layers.add(layer); 40 | 41 | await app.maps.setActiveMap(map.name); 42 | await layer.activate(); 43 | await scratchLayer.activate(); 44 | 45 | const transformationHandler = createTransformationHandler( 46 | map, 47 | layer, 48 | scratchLayer, 49 | mode, 50 | ); 51 | return { 52 | transformationHandler, 53 | app, 54 | layer, 55 | scratchLayer, 56 | destroy(): void { 57 | transformationHandler.destroy(); 58 | app.destroy(); 59 | }, 60 | }; 61 | } 62 | 63 | export function createHandlerFeature(axis: AxisAndPlanes): Feature { 64 | const feature = new Feature(); 65 | feature[handlerSymbol] = axis; 66 | return feature; 67 | } 68 | 69 | export function patchPickRay( 70 | calls: Cartesian3[], 71 | sandbox?: sinon.SinonSandbox, 72 | ): () => void { 73 | const stub = (sandbox ?? sinon).stub(IntersectionTests, 'rayPlane'); 74 | calls.forEach((value, index) => { 75 | stub.onCall(index).returns(value); 76 | }); 77 | 78 | return () => { 79 | stub.restore(); 80 | }; 81 | } 82 | 83 | export function createFeatureWithId( 84 | propsOrProps: Point | Record, 85 | ): Feature { 86 | const feature = new Feature(propsOrProps); 87 | feature.setId(v4()); 88 | return feature as Feature; 89 | } 90 | -------------------------------------------------------------------------------- /tests/unit/util/featureconverter/clampedPrimitive.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Cartesian3, 3 | Cartographic, 4 | HeightReference, 5 | Matrix4, 6 | Primitive, 7 | } from '@vcmap-cesium/engine'; 8 | import { Feature } from 'ol'; 9 | import Style from 'ol/style/Style.js'; 10 | import Stroke from 'ol/style/Stroke.js'; 11 | import { LineString, Point } from 'ol/geom.js'; 12 | import { expect } from 'chai'; 13 | import { getMockScene } from '../../helpers/cesiumHelpers.js'; 14 | import VectorProperties from '../../../../src/layer/vectorProperties.js'; 15 | import { setupClampedPrimitive } from '../../../../src/util/featureconverter/clampedPrimitive.js'; 16 | import { convert, Projection } from '../../../../index.js'; 17 | 18 | describe('clampedPrimitive', () => { 19 | let primitive: Primitive; 20 | let vectorProperties: VectorProperties; 21 | let heightCallback: (carto: Cartographic) => void; 22 | 23 | before(async () => { 24 | const scene = getMockScene(); 25 | scene.getHeight = (): number => 2; 26 | scene.updateHeight = ( 27 | _c: Cartographic, 28 | cb: (carto: Cartographic) => void, 29 | _h: HeightReference, 30 | ): (() => void) => { 31 | heightCallback = cb; 32 | return () => { 33 | heightCallback = (): void => {}; 34 | }; 35 | }; 36 | vectorProperties = new VectorProperties({ 37 | altitudeMode: 'absolute', 38 | }); 39 | const geometry = new LineString([ 40 | Projection.wgs84ToMercator([0, 0, 1]), 41 | Projection.wgs84ToMercator([1, 1, 1]), 42 | ]); 43 | const feature = new Feature({ geometry }); 44 | const style = new Style({ 45 | stroke: new Stroke({ 46 | color: '#ff0000', 47 | width: 1, 48 | }), 49 | }); 50 | primitive = (await convert(feature, style, vectorProperties, scene))[0] 51 | .item as Primitive; 52 | 53 | setupClampedPrimitive( 54 | scene, 55 | primitive, 56 | Projection.wgs84ToMercator([0.5, 0.5]) as [number, number], 57 | HeightReference.RELATIVE_TO_TERRAIN, 58 | ); 59 | }); 60 | 61 | after(() => { 62 | primitive.destroy(); 63 | vectorProperties.destroy(); 64 | }); 65 | 66 | it('should set the height, if there is one', () => { 67 | const translation = Matrix4.getTranslation( 68 | primitive.modelMatrix, 69 | new Cartesian3(), 70 | ); 71 | Cartesian3.add(Cartesian3.fromDegrees(0.5, 0.5), translation, translation); 72 | const carto = Cartographic.fromCartesian(translation); 73 | expect(carto.height).to.be.closeTo(2, 0.00000001); 74 | }); 75 | 76 | it('should set the height, if there is one', () => { 77 | heightCallback(new Cartographic(1, 1, 25)); 78 | const translation = Matrix4.getTranslation( 79 | primitive.modelMatrix, 80 | new Cartesian3(), 81 | ); 82 | Cartesian3.add(Cartesian3.fromDegrees(0.5, 0.5), translation, translation); 83 | const carto = Cartographic.fromCartesian(translation); 84 | expect(carto.height).to.be.closeTo(25, 0.000000001); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /tests/unit/util/flight/flightHelpers.spec.ts: -------------------------------------------------------------------------------- 1 | import { LinearSpline } from '@vcmap-cesium/engine'; 2 | import { expect } from 'chai'; 3 | import getDummyFlight from './getDummyFlightInstance.js'; 4 | import { getSplineAndTimesForInstance } from '../../../../src/util/flight/flightHelpers.js'; 5 | import { FlightInstance } from '../../../../index.js'; 6 | 7 | describe('getSplineAndTimesForInstance', () => { 8 | let flight: FlightInstance; 9 | 10 | beforeEach(() => { 11 | flight = getDummyFlight(); 12 | }); 13 | 14 | afterEach(() => { 15 | flight.destroy(); 16 | }); 17 | 18 | it('should create a linear spline, if the flights interpolation method is not SPLINE', () => { 19 | flight.interpolation = 'linear'; 20 | const { destinationSpline } = getSplineAndTimesForInstance(flight); 21 | expect(destinationSpline).to.be.an.instanceOf(LinearSpline); 22 | }); 23 | 24 | it('should set the times based on the viewpoint durations', () => { 25 | [...flight.anchors].forEach((vp) => { 26 | vp.duration = 10; 27 | }); 28 | const { times } = getSplineAndTimesForInstance(flight); 29 | times.forEach((time) => { 30 | expect(time % 10).to.equal(0); 31 | }); 32 | }); 33 | 34 | it('should add the first viewpoint again, if the flight is looped', () => { 35 | flight.loop = true; 36 | const { times } = getSplineAndTimesForInstance(flight); 37 | expect(times).to.have.length(flight.anchors.size + 1); 38 | }); 39 | 40 | it('should set the last viewpoints duration larger 0, if it is looped', () => { 41 | flight.loop = true; 42 | flight.anchors.get(flight.anchors.size - 1).duration = 0; 43 | const { times } = getSplineAndTimesForInstance(flight); 44 | expect(times.at(-1)).to.be.gt(times.at(-2)!); 45 | }); 46 | 47 | it('should set the viewpoints duration to 1, if two viewpoints following each other are identical', () => { 48 | const flight2 = getDummyFlight(2); 49 | flight2.anchors.add(flight2.anchors.get(0), 0); 50 | getSplineAndTimesForInstance(flight2); 51 | expect(flight2.anchors.get(0)).to.have.property('duration', 1); 52 | flight2.destroy(); 53 | }); 54 | 55 | it('should set the last viewpoints duration to 1, if two viewpoints following each other are identical and are looped', () => { 56 | const flight2 = getDummyFlight(2); 57 | flight2.anchors.add(flight2.anchors.get(0), 0); 58 | flight2.loop = true; 59 | getSplineAndTimesForInstance(flight2); 60 | expect(flight2.anchors.get(1)).to.have.property('duration', 1); 61 | flight2.destroy(); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /tests/unit/util/flight/getDummyFlightInstance.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Viewpoint, 3 | FlightInstance, 4 | anchorFromViewpoint, 5 | FlightAnchor, 6 | } from '../../../../index.js'; 7 | 8 | export default function getDummyFlight(numberOfVPs = 5): FlightInstance { 9 | const instance = new FlightInstance({}); 10 | 11 | for (let i = 0; i < numberOfVPs; i++) { 12 | const anchor = anchorFromViewpoint( 13 | new Viewpoint({ 14 | cameraPosition: [i * 2, i * 2, 1], 15 | heading: 0, 16 | pitch: -45, 17 | roll: 0, 18 | duration: 1, 19 | }), 20 | ); 21 | if (anchor) { 22 | instance.anchors.add(anchor); 23 | } 24 | } 25 | 26 | return instance; 27 | } 28 | 29 | export function createAnchor(): FlightAnchor { 30 | return anchorFromViewpoint( 31 | new Viewpoint({ 32 | cameraPosition: [0, 0, 0], 33 | }), 34 | )!; 35 | } 36 | -------------------------------------------------------------------------------- /tests/unit/util/urlHelpers.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { isSameOrigin } from '../../../src/util/urlHelpers.js'; 3 | 4 | describe('urlHelper isSameOrigin', () => { 5 | it('should return true on relative urls', () => { 6 | expect(isSameOrigin('my/relative/path')).to.be.true; 7 | }); 8 | 9 | it('should return true on a data url', () => { 10 | expect(isSameOrigin('data:text/one')).to.be.true; 11 | }); 12 | 13 | it('should return true, if a url has the same base', () => { 14 | const url = new URL('foo', window.location.href); 15 | expect(isSameOrigin(url.toString())).to.be.true; 16 | }); 17 | 18 | it('should return false, if the url has another host', () => { 19 | expect(isSameOrigin('http://test.com/test')).to.be.false; 20 | }); 21 | 22 | it('should return false, if the url has another protocol', () => { 23 | const url = new URL('foo', window.location.href); 24 | url.protocol = 'ftp:'; 25 | expect(isSameOrigin(url.toString())).to.be.false; 26 | }); 27 | 28 | it('should return false, if the url has another port', () => { 29 | const url = new URL('foo', window.location.href); 30 | url.port = '5123'; 31 | expect(isSameOrigin(url.toString())).to.be.false; 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/unit/vectorCluster/vectorClusterGroupCollection.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import VectorClusterGroupCollection from '../../../src/vectorCluster/vectorClusterGroupCollection.js'; 3 | import GlobalHider from '../../../src/layer/globalHider.js'; 4 | import VectorClusterGroup from '../../../src/vectorCluster/vectorClusterGroup.js'; 5 | import { destroyCollection } from '../../../src/vcsModuleHelpers.js'; 6 | 7 | describe('VectorClusterGroupCollection', () => { 8 | let globalHider: GlobalHider; 9 | let collection: VectorClusterGroupCollection; 10 | 11 | beforeEach(() => { 12 | globalHider = new GlobalHider(); 13 | collection = new VectorClusterGroupCollection(globalHider); 14 | }); 15 | 16 | afterEach(() => { 17 | destroyCollection(collection); 18 | }); 19 | 20 | describe('Setting / Unsetting the Global Hider', () => { 21 | it('should set the global hider on added VectorClusterGroup', () => { 22 | const vectorClusterGroup = new VectorClusterGroup({}); 23 | collection.add(vectorClusterGroup); 24 | 25 | expect(vectorClusterGroup.globalHider).to.equal(globalHider); 26 | }); 27 | 28 | it('should unset the global hider on removed VectorClusterGroup', () => { 29 | const vectorClusterGroup = new VectorClusterGroup({}); 30 | collection.add(vectorClusterGroup); 31 | collection.remove(vectorClusterGroup); 32 | 33 | expect(vectorClusterGroup.globalHider).to.be.undefined; 34 | }); 35 | }); 36 | 37 | describe('Setting the Global Hider on the Collection', () => { 38 | it('should set the global hider on all VectorClusterGroups in the collection', () => { 39 | const vectorClusterGroup1 = new VectorClusterGroup({}); 40 | const vectorClusterGroup2 = new VectorClusterGroup({}); 41 | collection.add(vectorClusterGroup1); 42 | collection.add(vectorClusterGroup2); 43 | 44 | const newGlobalHider = new GlobalHider(); 45 | collection.globalHider = newGlobalHider; 46 | 47 | expect(vectorClusterGroup1.globalHider).to.equal(newGlobalHider); 48 | expect(vectorClusterGroup2.globalHider).to.equal(newGlobalHider); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/unit/vectorCluster/vectorClusterGroupImpl.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import Style, { StyleFunction } from 'ol/style/Style.js'; 3 | import VectorClusterGroupImpl from '../../../src/vectorCluster/vectorClusterGroupImpl.js'; 4 | import ClusterEnhancedVectorSource from '../../../src/ol/source/ClusterEnhancedVectorSource.js'; 5 | import FeatureVisibility from '../../../src/layer/featureVisibility.js'; 6 | import VcsMap from '../../../src/map/vcsMap.js'; 7 | import { GlobalHider, VectorProperties } from '../../../index.js'; 8 | import VcsCluster from '../../../src/ol/source/VcsCluster.js'; 9 | 10 | describe('VectorClusterGroupImpl', () => { 11 | let map: VcsMap; 12 | let source: ClusterEnhancedVectorSource; 13 | let featureVisibility: FeatureVisibility; 14 | let style: StyleFunction; 15 | let vectorProperties: VectorProperties; 16 | let globalHider: GlobalHider; 17 | let clusterSource: VcsCluster; 18 | let vectorClusterGroupImpl: VectorClusterGroupImpl; 19 | 20 | before(async () => { 21 | map = new VcsMap({}); 22 | await map.activate(); 23 | source = new ClusterEnhancedVectorSource(); 24 | clusterSource = new VcsCluster({ source }, 'test'); 25 | featureVisibility = new FeatureVisibility(); 26 | vectorProperties = new VectorProperties({}); 27 | style = (): Style => new Style({}); 28 | globalHider = new GlobalHider(); 29 | }); 30 | 31 | beforeEach(async () => { 32 | vectorClusterGroupImpl = new VectorClusterGroupImpl(map, { 33 | name: 'test', 34 | style, 35 | vectorProperties, 36 | source, 37 | featureVisibility, 38 | globalHider, 39 | clusterDistance: 40, 40 | getLayerByName: (_layerName: string): undefined => undefined, 41 | }); 42 | await vectorClusterGroupImpl.activate(); 43 | }); 44 | 45 | afterEach(() => { 46 | vectorClusterGroupImpl.destroy(); 47 | }); 48 | 49 | after(() => { 50 | map.destroy(); 51 | featureVisibility.destroy(); 52 | globalHider.destroy(); 53 | vectorProperties.destroy(); 54 | clusterSource.dispose(); 55 | source.dispose(); 56 | }); 57 | 58 | it('should activate correctly', () => { 59 | expect(vectorClusterGroupImpl.active).to.be.true; 60 | expect(vectorClusterGroupImpl.initialized).to.be.true; 61 | }); 62 | 63 | it('should deactivate correctly', () => { 64 | vectorClusterGroupImpl.deactivate(); 65 | expect(vectorClusterGroupImpl.active).to.be.false; 66 | }); 67 | 68 | it('should destroy correctly', () => { 69 | vectorClusterGroupImpl.destroy(); 70 | expect(vectorClusterGroupImpl.initialized).to.be.false; 71 | expect(() => vectorClusterGroupImpl.map).to.throw(); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /tests/vcs.js: -------------------------------------------------------------------------------- 1 | import '../src/ol/geom/circle.js'; 2 | import '../src/ol/geom/geometryCollection.js'; 3 | import '../src/ol/feature.js'; 4 | import '../src/cesium/wallpaperMaterial.js'; 5 | import '../src/cesium/cesium3DTilePointFeature.js'; 6 | import '../src/cesium/cesium3DTileFeature.js'; 7 | import '../src/cesium/cesiumVcsCameraPrimitive.js'; 8 | 9 | import { setLogLevel } from '@vcsuite/logger'; 10 | import { 11 | mercatorProjection, 12 | setDefaultProjectionOptions, 13 | } from '../src/util/projection.js'; 14 | import { setupCesiumContextLimits } from './unit/helpers/cesiumHelpers.js'; 15 | 16 | setLogLevel(false); 17 | const balloonContainer = document.createElement('div'); 18 | balloonContainer.id = 'balloonContainer'; 19 | const mapContainer = document.createElement('div'); 20 | mapContainer.id = 'mapContainer'; 21 | const overviewMapDiv = document.createElement('div'); 22 | overviewMapDiv.id = 'vcm_overviewmap_container'; 23 | const body = document.getElementsByTagName('body')[0]; 24 | body.appendChild(balloonContainer); 25 | body.appendChild(mapContainer); 26 | body.appendChild(overviewMapDiv); 27 | setDefaultProjectionOptions(mercatorProjection.toJSON()); 28 | setupCesiumContextLimits(); 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "node16", 5 | "incremental": true, 6 | "lib": ["esnext", "dom"], 7 | "allowJs": true, 8 | "checkJs": false, 9 | "declaration": true, 10 | "sourceMap": true, 11 | "outDir": "dist", 12 | "rootDir": ".", 13 | /* Strict Type-Checking Options */ 14 | "strict": true, 15 | "noImplicitAny": true, 16 | "strictNullChecks": true, 17 | 18 | /* Additional Checks */ 19 | "noImplicitReturns": false, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": false, 23 | 24 | /* Module Resolution Options */ 25 | "baseUrl": ".", 26 | "paths": { 27 | "rbush-knn": ["types/rbush-knn"], 28 | "rbush": ["types/rbush"] 29 | }, 30 | "moduleResolution": "node16", 31 | "resolveJsonModule": true, 32 | "esModuleInterop": true, 33 | "preserveSymlinks": true, 34 | "allowSyntheticDefaultImports": true 35 | }, 36 | "exclude": ["dist/", "build/", ".tests/"], 37 | "include": ["types/", "src/", "index.ts", "tests/unit/helpers/"] 38 | } 39 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigation": { 3 | "includeCategories": true, 4 | "includeGroups": true 5 | }, 6 | "categorizeByGroup": true, 7 | "groupOrder": [ 8 | "Application", 9 | "Map", 10 | "Layer", 11 | "Style", 12 | "Viewpoint", 13 | "Editor", 14 | "Interaction", 15 | "Category", 16 | "Classes", 17 | "*" 18 | ], 19 | "searchGroupBoosts": { 20 | "Application": 5, 21 | "Map": 4.5, 22 | "Layer": 4, 23 | "Style": 3.5, 24 | "Viewpoint": 3, 25 | "Editor": 2, 26 | "Interaction": 2, 27 | "Category": 2 28 | }, 29 | "sourceLinkTemplate": "https://github.com/virtualcitySYSTEMS/map-core/tree/main/{path}#L{line}" 30 | } 31 | -------------------------------------------------------------------------------- /types/rbush-knn.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'rbush-knn' { 2 | import RTree from 'rbush'; 3 | 4 | export default function knn( 5 | tree: RTree, 6 | x: number, 7 | y: number, 8 | z: number, 9 | predicate?: (item: T) => boolean, 10 | maxDistance?: number, 11 | ): T[]; 12 | } 13 | --------------------------------------------------------------------------------