├── .npmrc ├── src ├── moduleIdSymbol.ts ├── vectorCluster │ ├── vectorClusterSymbols.ts │ ├── vectorClusterGroupCollection.ts │ ├── vectorClusterGroupOpenlayersImpl.ts │ └── vectorClusterGroupObliqueImpl.ts ├── cesium │ ├── clippingPolygonCollection.ts │ ├── cesium3DTilePointFeature.ts │ ├── entity.ts │ ├── cesium3DTileFeature.ts │ ├── clippingPolygon.ts │ └── wallpaperMaterial.js ├── map │ ├── mapState.ts │ └── navigation │ │ ├── openlayersNavigation.ts │ │ ├── viewHelper.ts │ │ ├── easingHelper.ts │ │ ├── navigationImpl.ts │ │ └── controller │ │ └── controller.ts ├── layer │ ├── layerState.ts │ ├── featureStoreLayerState.ts │ ├── cesium │ │ ├── resourceHelper.ts │ │ ├── openStreetMapCesiumImpl.ts │ │ ├── vcsTile │ │ │ ├── vcsNoDataTile.ts │ │ │ └── vcsChildTile.ts │ │ ├── cogCesiumImpl.ts │ │ ├── singleImageCesiumImpl.ts │ │ ├── tmsCesiumImpl.ts │ │ ├── terrainCesiumImpl.ts │ │ └── wmsCesiumImpl.ts │ ├── layerSymbols.ts │ ├── openlayers │ │ ├── openStreetMapOpenlayersImpl.ts │ │ ├── loadFunctionHelpers.ts │ │ ├── tileDebugOpenlayersImpl.ts │ │ ├── rasterLayerOpenlayersImpl.ts │ │ ├── wmsOpenlayersImpl.ts │ │ ├── cogOpenlayersImpl.ts │ │ ├── tmsOpenlayersImpl.ts │ │ └── singleImageOpenlayersImpl.ts │ ├── oblique │ │ └── layerObliqueImpl.ts │ ├── panorama │ │ └── panoramaDatasetPanoramaImpl.ts │ ├── featureStoreFeatureVisibility.ts │ ├── tileProvider │ │ ├── staticFeatureTileProvider.ts │ │ └── staticGeojsonTileProvider.ts │ ├── tileLoadedHelper.ts │ ├── flatGeobufLayer.ts │ ├── vectorSymbols.ts │ └── layerImplementation.ts ├── util │ ├── locale.ts │ ├── urlHelpers.ts │ ├── editor │ │ ├── editorSymbols.ts │ │ ├── validateGeoemetry.ts │ │ ├── interactions │ │ │ ├── removeVertexInteraction.ts │ │ │ ├── rightClickInteraction.ts │ │ │ ├── translateVertexInteraction.ts │ │ │ ├── createPointInteraction.ts │ │ │ ├── ensureHandlerSelectionInteraction.ts │ │ │ └── editFeaturesMouseOverInteraction.ts │ │ └── transformation │ │ │ └── transformationTypes.ts │ ├── hiddenObjects.ts │ ├── fetch.ts │ ├── featureconverter │ │ └── arcToCesium.ts │ ├── isMobile.ts │ ├── flight │ │ └── flightCollection.ts │ └── clipping │ │ └── clippingPolygonHelper.ts ├── style │ ├── modelFill.ts │ ├── styleFactory.ts │ ├── writeStyle.ts │ └── shapesCategory.ts ├── featureProvider │ ├── featureProviderSymbols.ts │ └── tileProviderFeatureProvider.ts ├── global.d.ts ├── oblique │ ├── obliqueViewDirection.ts │ └── defaultObliqueCollection.ts ├── ol │ ├── geojson.d.ts │ ├── source │ │ ├── ClusterEnhancedVectorSource.ts │ │ └── VcsCluster.ts │ └── geom │ │ └── circle.ts ├── interaction │ ├── interactionType.ts │ ├── panoramaImageSelection.ts │ └── featureProviderInteraction.ts ├── panorama │ └── panoramaTileCache.ts ├── workers │ └── panoramaImageWorker.ts ├── vcsObject.ts └── vcsEvent.ts ├── tests ├── data │ ├── tile.pbf │ ├── cog │ │ ├── test_rgb.tif │ │ ├── test_grey_world.tif │ │ └── test_rgb_world.tif │ ├── wgs84Points.fgb │ ├── panorama │ │ ├── badPosition.tif │ │ ├── lowOverview.tif │ │ ├── noVersionRgb.tif │ │ ├── badOrientation.tif │ │ ├── noVersionDepth.tif │ │ ├── testDepthGeotiff.tif │ │ └── testRgbGeotiff.tif │ ├── terrain │ │ ├── 13 │ │ │ ├── 8800 │ │ │ │ ├── 6485.terrain │ │ │ │ └── 6486.terrain │ │ │ └── 8801 │ │ │ │ ├── 6485.terrain │ │ │ │ └── 6486.terrain │ │ └── layer.json │ └── dynamicPointCzml.json ├── setupJsdom.js ├── unit │ ├── helpers │ │ ├── getFileNameFromUrl.js │ │ ├── importJSON.js │ │ ├── flatGeobufHelpers.ts │ │ ├── openlayersHelpers.js │ │ ├── imageHelpers.js │ │ ├── terrain │ │ │ └── terrainData.js │ │ └── helpers.ts │ ├── util │ │ ├── flight │ │ │ ├── getDummyFlightInstance.ts │ │ │ └── flightHelpers.spec.ts │ │ ├── urlHelpers.spec.ts │ │ └── editor │ │ │ ├── interactions │ │ │ ├── editFeaturesMouseOverInteraction.spec.js │ │ │ └── ensureHandlerSelectionInteraction.spec.js │ │ │ └── transformation │ │ │ └── setupTransformationHandler.ts │ ├── ol │ │ ├── render │ │ │ └── canvas │ │ │ │ └── canvasTileRenderer.spec.js │ │ └── geom │ │ │ └── circle.spec.js │ ├── map │ │ └── navigation │ │ │ ├── openlayersNavigation.spec.ts │ │ │ ├── cesiumNavigation.spec.ts │ │ │ ├── easingHelper.spec.ts │ │ │ └── viewHelper.spec.ts │ ├── layer │ │ ├── cesium │ │ │ ├── vcsTile │ │ │ │ └── vcsTileHelpers.spec.ts │ │ │ ├── getDummyCesium3DTileset.js │ │ │ └── dataSourceCesiumImpl.spec.js │ │ ├── terrainLayer.spec.js │ │ ├── tmsLayer.spec.js │ │ ├── terrainHelpers.spec.js │ │ ├── wfsLayer.spec.ts │ │ ├── cogLayer.spec.ts │ │ ├── pointCloudLayer.spec.js │ │ └── singleImageLayer.spec.js │ ├── oblique │ │ └── obliqueImageMeta.spec.js │ ├── vectorCluster │ │ ├── vectorClusterGroupCollection.spec.ts │ │ └── vectorClusterGroupImpl.spec.ts │ ├── style │ │ ├── styleFactory.spec.ts │ │ └── writeStyle.spec.js │ └── interaction │ │ ├── coordinateAtPixel.spec.js │ │ └── abstractInteraction.spec.js ├── tsconfig.json ├── vcs.js └── setup.js ├── .madgerc ├── documentation ├── VcsLayer.png ├── vcsApp.md └── renderScreenshot.md ├── .idea ├── .gitignore ├── codeStyles │ └── codeStyleConfig.xml ├── misc.xml ├── vcs.xml ├── jsLibraryMappings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml ├── prettier.xml └── vcmap-core.iml ├── .prettierignore ├── .gitignore ├── jsconfig.json ├── types ├── rbush-knn.d.ts └── geotiff.d.ts ├── typedoc.json ├── tsconfig.json ├── LICENSE.md └── eslint.config.js /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /src/moduleIdSymbol.ts: -------------------------------------------------------------------------------- 1 | export const moduleIdSymbol: unique symbol = Symbol('moduleId'); 2 | -------------------------------------------------------------------------------- /tests/data/tile.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/HEAD/tests/data/tile.pbf -------------------------------------------------------------------------------- /.madgerc: -------------------------------------------------------------------------------- 1 | { 2 | "detectiveOptions": { 3 | "ts": { 4 | "skipTypeImports": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /documentation/VcsLayer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/HEAD/documentation/VcsLayer.png -------------------------------------------------------------------------------- /src/vectorCluster/vectorClusterSymbols.ts: -------------------------------------------------------------------------------- 1 | export const vectorClusterGroupName = Symbol('vectorClusterGroupName'); 2 | -------------------------------------------------------------------------------- /tests/data/cog/test_rgb.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/HEAD/tests/data/cog/test_rgb.tif -------------------------------------------------------------------------------- /tests/data/wgs84Points.fgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/HEAD/tests/data/wgs84Points.fgb -------------------------------------------------------------------------------- /tests/data/cog/test_grey_world.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/HEAD/tests/data/cog/test_grey_world.tif -------------------------------------------------------------------------------- /tests/data/cog/test_rgb_world.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/HEAD/tests/data/cog/test_rgb_world.tif -------------------------------------------------------------------------------- /tests/data/panorama/badPosition.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/HEAD/tests/data/panorama/badPosition.tif -------------------------------------------------------------------------------- /tests/data/panorama/lowOverview.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/HEAD/tests/data/panorama/lowOverview.tif -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /tests/data/panorama/noVersionRgb.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/HEAD/tests/data/panorama/noVersionRgb.tif -------------------------------------------------------------------------------- /tests/data/panorama/badOrientation.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/HEAD/tests/data/panorama/badOrientation.tif -------------------------------------------------------------------------------- /tests/data/panorama/noVersionDepth.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/HEAD/tests/data/panorama/noVersionDepth.tif -------------------------------------------------------------------------------- /tests/data/panorama/testDepthGeotiff.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/HEAD/tests/data/panorama/testDepthGeotiff.tif -------------------------------------------------------------------------------- /tests/data/panorama/testRgbGeotiff.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/HEAD/tests/data/panorama/testRgbGeotiff.tif -------------------------------------------------------------------------------- /tests/data/terrain/13/8800/6485.terrain: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/HEAD/tests/data/terrain/13/8800/6485.terrain -------------------------------------------------------------------------------- /tests/data/terrain/13/8800/6486.terrain: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/HEAD/tests/data/terrain/13/8800/6486.terrain -------------------------------------------------------------------------------- /tests/data/terrain/13/8801/6485.terrain: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/HEAD/tests/data/terrain/13/8801/6485.terrain -------------------------------------------------------------------------------- /tests/data/terrain/13/8801/6486.terrain: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualcitySYSTEMS/map-core/HEAD/tests/data/terrain/13/8801/6486.terrain -------------------------------------------------------------------------------- /.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 | *.shader.ts 10 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/util/locale.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * returns the default browserLocale, if not possible 'en' 3 | */ 4 | export function detectBrowserLocale(): string { 5 | if (navigator.language) { 6 | const lang = navigator.language; 7 | return lang.substring(0, 2); 8 | } 9 | return 'en'; 10 | } 11 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /types/rbush-knn.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'rbush-knn' { 2 | import type 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 | -------------------------------------------------------------------------------- /src/layer/cesium/resourceHelper.ts: -------------------------------------------------------------------------------- 1 | import { Resource } from '@vcmap-cesium/engine'; 2 | 3 | export function getResourceOrUrl( 4 | url: string, 5 | headers?: Record, 6 | ): string | Resource { 7 | if (headers) { 8 | return new Resource({ 9 | url, 10 | headers, 11 | }); 12 | } 13 | return url; 14 | } 15 | -------------------------------------------------------------------------------- /.idea/prettier.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /types/geotiff.d.ts: -------------------------------------------------------------------------------- 1 | import type { BaseDecoder, Pool } from 'geotiff'; 2 | 3 | declare module 'geotiff' { 4 | interface GeoTIFFImage { 5 | getTileOrStrip( 6 | x: number, 7 | y: number, 8 | samplesPerPixel: number, 9 | decoder: BaseDecoder | Pool, 10 | abortSignal?: AbortSignal, 11 | ): Promise<{ x: number; y: number; sample: number; data: ArrayBuffer }>; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/util/urlHelpers.ts: -------------------------------------------------------------------------------- 1 | export function isSameOrigin(source: string): boolean { 2 | const { location } = window; 3 | const url = new URL( 4 | source, 5 | `${location.protocol}//${location.host}${location.pathname}`, 6 | ); 7 | // for instance data: URIs have no host information and are implicitly same origin 8 | // see https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy#inherited_origins 9 | if (!url.host) { 10 | return true; 11 | } 12 | return url.protocol === location.protocol && url.host === location.host; 13 | } 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/unit/helpers/flatGeobufHelpers.ts: -------------------------------------------------------------------------------- 1 | import type { ReplyFnContext, ReplyFnResult, Scope } from 'nock'; 2 | import nock from 'nock'; 3 | import fs from 'fs'; 4 | 5 | export function setupFgbNock(buffer?: Buffer): Scope { 6 | const bytes = buffer ?? fs.readFileSync('tests/data/wgs84Points.fgb'); 7 | 8 | function cb(this: ReplyFnContext): ReplyFnResult { 9 | const header = this.req.headers; 10 | const range = header.range.split('=')[1].split('-').map(Number); 11 | range[1] = Math.min(range[1], bytes.length - 1); 12 | return [200, bytes.subarray(range[0], range[1] + 1)]; 13 | } 14 | 15 | return nock('http://localhost').persist().get('/wgs84Points.fgb').reply(cb); 16 | } 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | import type VcsApp from './vcsApp.js'; 2 | import type { 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 | var useVcsCustomShading: boolean | undefined; 8 | 9 | interface Window { 10 | vcs: { 11 | apps: Map; 12 | createModuleFromConfig: (config: VcsModuleConfig) => VcsModule; 13 | getFirstApp: () => VcsApp | undefined; 14 | workerBase?: string; 15 | }; 16 | opera?: string; 17 | } 18 | interface CSSStyleDeclaration { 19 | [mouseOverSymbol]?: string; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/map/navigation/openlayersNavigation.ts: -------------------------------------------------------------------------------- 1 | import type OpenlayersMap from '../openlayersMap.js'; 2 | import type { NavigationImplOptions } from './navigationImpl.js'; 3 | import NavigationImpl from './navigationImpl.js'; 4 | import type { Movement } from './navigation.js'; 5 | import { moveView } from './viewHelper.js'; 6 | 7 | export type OpenlayersNavigationOptions = NavigationImplOptions; 8 | 9 | class OpenlayersNavigation extends NavigationImpl { 10 | static get className(): string { 11 | return 'OpenlayersNavigation'; 12 | } 13 | 14 | static getDefaultOptions(): OpenlayersNavigationOptions { 15 | return { ...NavigationImpl.getDefaultOptions() }; 16 | } 17 | 18 | update(movement: Movement): void { 19 | moveView(this._map, movement.input, this.baseTranSpeed); 20 | } 21 | } 22 | 23 | export default OpenlayersNavigation; 24 | -------------------------------------------------------------------------------- /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 | export function getTileLoadFunction( 7 | headers: Record, 8 | ): LoadFunction { 9 | return function tileLoadFunction(imageTile, src): void { 10 | const image = (imageTile as ImageTile).getImage() as HTMLImageElement; 11 | const init = getInitForUrl(src, headers); 12 | requestObjectUrl(src, init) 13 | .then((blobUrl) => { 14 | image.src = blobUrl; 15 | image.onload = (): void => { 16 | URL.revokeObjectURL(blobUrl); 17 | }; 18 | }) 19 | .catch(() => { 20 | imageTile.setState(TileState.ERROR); 21 | }); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/cesium/entity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Entity } from '@vcmap-cesium/engine'; 3 | 4 | Entity.prototype.getId = function getId(this: Entity): string | number { 5 | return this.id; 6 | }; 7 | 8 | /** 9 | * To be used for cesium 3D style functions 10 | */ 11 | Entity.prototype.getProperty = function getProperty( 12 | this: Entity, 13 | property: string, 14 | ): any { 15 | return this[property as keyof Entity]; 16 | }; 17 | 18 | Entity.prototype.getAttributes = function getAttributes(): Record< 19 | string, 20 | unknown 21 | > { 22 | return this.properties ?? {}; 23 | }; 24 | 25 | /** 26 | * To be used for cesium 3D style functions 27 | */ 28 | Entity.prototype.getPropertyInherited = function getPropertyInherited( 29 | this: Entity, 30 | property: string, 31 | ): any { 32 | return this.getProperty(property); 33 | }; 34 | -------------------------------------------------------------------------------- /src/ol/geojson.d.ts: -------------------------------------------------------------------------------- 1 | import type { GeoJsonProperties, Geometry } from 'geojson'; 2 | import type { VcsMeta } from '../layer/vectorProperties.js'; 3 | import type { 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 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 13 | P = GeoJsonProperties, 14 | > { 15 | _id?: string; 16 | radius?: G extends Point ? number : never; 17 | vcsMeta?: VcsMeta; 18 | state?: FeatureStoreLayerState; 19 | } 20 | 21 | interface FeatureCollection { 22 | crs?: 23 | | { type: 'name'; properties: { name: string } } 24 | | { type: 'EPSG'; properties: { code: string } }; 25 | vcsMeta?: VcsMeta; 26 | vcsEmbeddedIcons?: string[]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /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/util/flight/getDummyFlightInstance.ts: -------------------------------------------------------------------------------- 1 | import type { FlightAnchor } from '../../../../index.js'; 2 | import { 3 | Viewpoint, 4 | FlightInstance, 5 | anchorFromViewpoint, 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 | -------------------------------------------------------------------------------- /.idea/vcmap-core.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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/layer/cesium/vcsTile/vcsNoDataTile.ts: -------------------------------------------------------------------------------- 1 | import type { QuadtreeTile, TileBoundingRegion } from '@vcmap-cesium/engine'; 2 | import type CesiumMap from '../../../map/cesiumMap.js'; 3 | import type { VcsTile } from './vcsTileHelpers.js'; 4 | import { 5 | getTileBoundingRegion, 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,no-empty-function 29 | set show(_show: boolean) {} 30 | } 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/interaction/interactionType.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/prefer-literal-enum-member */ 2 | /** 3 | * Enumeration of modification key types 4 | */ 5 | export enum ModificationKeyType { 6 | NONE = 2, 7 | ALT = 4, 8 | CTRL = 8, 9 | SHIFT = 16, 10 | ALL = NONE | ALT | CTRL | SHIFT, 11 | } 12 | 13 | /** 14 | * Enumeration of pointer event types 15 | */ 16 | export enum EventType { 17 | NONE = 0, 18 | CLICK = 32, 19 | DBLCLICK = 64, 20 | DRAG = 128, 21 | DRAGSTART = 256, 22 | DRAGEND = 512, 23 | MOVE = 1024, 24 | DRAGEVENTS = DRAG | DRAGSTART | DRAGEND, 25 | CLICKMOVE = CLICK | MOVE, 26 | ALL = CLICK | DBLCLICK | DRAG | DRAGSTART | DRAGEND | MOVE, 27 | } 28 | 29 | /** 30 | * Enumeration of pointer keys. 31 | */ 32 | export enum PointerKeyType { 33 | LEFT = 2048, 34 | RIGHT = 4096, 35 | MIDDLE = 8192, 36 | ALL = LEFT | RIGHT | MIDDLE, 37 | } 38 | 39 | /** 40 | * Enumeration of pointer key events. 41 | */ 42 | export enum PointerEventType { 43 | DOWN = 1, 44 | UP = 2, 45 | MOVE = 3, 46 | } 47 | -------------------------------------------------------------------------------- /src/layer/cesium/vcsTile/vcsChildTile.ts: -------------------------------------------------------------------------------- 1 | import type { QuadtreeTile, TileBoundingRegion } from '@vcmap-cesium/engine'; 2 | import type { VcsTile } from './vcsTileHelpers.js'; 3 | import { 4 | getTileBoundingRegion, 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,no-empty-function 30 | set show(_show: boolean) {} 31 | } 32 | -------------------------------------------------------------------------------- /src/map/navigation/viewHelper.ts: -------------------------------------------------------------------------------- 1 | import type BaseOLMap from '../baseOLMap.js'; 2 | import { getScaleFromDistance } from './cameraHelper.js'; 3 | import type { ControllerInput } from './controller/controllerInput.js'; 4 | 5 | export function moveView( 6 | map: BaseOLMap, 7 | input: ControllerInput, 8 | baseTranSpeed: number, 9 | ): void { 10 | const view = map.olMap?.getView(); 11 | if (view) { 12 | if (Math.abs(input.up) > 0) { 13 | const zoom = view.getZoom(); 14 | if (zoom) { 15 | view.setZoom(zoom - input.up * baseTranSpeed); 16 | } 17 | } 18 | 19 | if (Math.abs(input.forward) > 0 || Math.abs(input.right) > 0) { 20 | const distance = map.getViewpointSync()?.distance ?? 16; 21 | const scale = getScaleFromDistance(distance); 22 | const center = view.getCenter(); 23 | if (center) { 24 | view.setCenter([ 25 | center[0] + input.right * baseTranSpeed * scale, 26 | center[1] + input.forward * baseTranSpeed * scale, 27 | ]); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/ol/source/ClusterEnhancedVectorSource.ts: -------------------------------------------------------------------------------- 1 | import VectorSource from 'ol/source/Vector.js'; 2 | import type 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/util/hiddenObjects.ts: -------------------------------------------------------------------------------- 1 | import type GlobalHider from '../layer/globalHider.js'; 2 | import type { OverrideCollection } from './overrideCollection.js'; 3 | import makeOverrideCollection from './overrideCollection.js'; 4 | import Collection from './collection.js'; 5 | import type { moduleIdSymbol } from '../moduleIdSymbol.js'; 6 | 7 | export type HiddenObject = { 8 | id: string; 9 | [moduleIdSymbol]?: string; 10 | }; 11 | 12 | export function createHiddenObjectsCollection( 13 | getDynamicModuleId: () => string, 14 | globalHider: GlobalHider, 15 | ): OverrideCollection { 16 | const collection = makeOverrideCollection( 17 | new Collection('id'), 18 | getDynamicModuleId, 19 | ); 20 | 21 | collection.added.addEventListener(({ id }) => { 22 | globalHider.hideObjects([id]); 23 | }); 24 | 25 | collection.replaced.addEventListener(({ new: item }) => { 26 | globalHider.showObjects([item.id]); 27 | }); 28 | 29 | collection.removed.addEventListener(({ id }) => { 30 | globalHider.showObjects([id]); 31 | }); 32 | 33 | return collection; 34 | } 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 type { 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,@typescript-eslint/no-unused-vars 26 | updateStyle(_styleItem: StyleItem, _silent?: boolean): void {} 27 | 28 | // eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-unused-vars 29 | updateTiles(_args: string[]): void {} 30 | } 31 | 32 | export default TileDebugOpenlayersImpl; 33 | -------------------------------------------------------------------------------- /src/panorama/panoramaTileCache.ts: -------------------------------------------------------------------------------- 1 | import LRUCache from 'ol/structs/LRUCache.js'; 2 | import type { PanoramaTile } from './panoramaTile.js'; 3 | 4 | /** 5 | * A specialized LRU cache 6 | */ 7 | export class PanoramaTileCache extends LRUCache { 8 | deleteOldest(): void { 9 | const entry = this.pop(); 10 | if (entry) { 11 | entry.destroy(); 12 | } 13 | } 14 | 15 | expireCache(usedTiles: Record = {}): void { 16 | while (this.canExpireCache()) { 17 | const tile = this.peekLast(); 18 | if (usedTiles[tile.tileCoordinate.key]) { 19 | break; 20 | } else { 21 | this.pop().destroy(); 22 | } 23 | } 24 | } 25 | } 26 | 27 | /** 28 | * convenience function to cache a tile and cleanup all the non-visible tiles 29 | * @param tile 30 | * @param cache 31 | * @param currentlyVisibleTileKeys 32 | */ 33 | export function addTileToCache( 34 | tile: PanoramaTile, 35 | cache: PanoramaTileCache, 36 | currentlyVisibleTileKeys: Record, 37 | ): void { 38 | cache.set(tile.tileCoordinate.key, tile); 39 | cache.expireCache(currentlyVisibleTileKeys); 40 | } 41 | -------------------------------------------------------------------------------- /src/cesium/cesium3DTileFeature.ts: -------------------------------------------------------------------------------- 1 | import type { Cesium3DTilePointFeature } from '@vcmap-cesium/engine'; 2 | import { Cesium3DTileFeature } from '@vcmap-cesium/engine'; 3 | 4 | Cesium3DTileFeature.prototype.getId = function getId( 5 | this: Cesium3DTileFeature, 6 | ): string | number { 7 | return ( 8 | (this.getProperty('id') as string | number) || 9 | `${this.content.url}${String(this._batchId)}` 10 | ); // XXX there is a new property `featureId` on the Cesium3DTileset. this may cause issues when picking b3dm. 11 | }; 12 | 13 | export function getAttributes( 14 | this: Cesium3DTileFeature | Cesium3DTilePointFeature, 15 | ): Record { 16 | if ( 17 | (this.tileset.asset as { version: string } | undefined)?.version === 18 | '1.1' || 19 | !this.getPropertyIds().includes('attributes') 20 | ) { 21 | const attributes: Record = {}; 22 | this.getPropertyIds().forEach((id) => { 23 | attributes[id] = this.getProperty(id); 24 | }); 25 | return attributes; 26 | } 27 | return this.getProperty('attributes') as Record; 28 | } 29 | 30 | Cesium3DTileFeature.prototype.getAttributes = getAttributes; 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/unit/map/navigation/openlayersNavigation.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import type 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/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 | -------------------------------------------------------------------------------- /src/util/editor/interactions/removeVertexInteraction.ts: -------------------------------------------------------------------------------- 1 | import type { EventAfterEventHandler } from '../../../interaction/abstractInteraction.js'; 2 | import AbstractInteraction from '../../../interaction/abstractInteraction.js'; 3 | import { 4 | EventType, 5 | ModificationKeyType, 6 | } from '../../../interaction/interactionType.js'; 7 | import VcsEvent from '../../../vcsEvent.js'; 8 | import type { Vertex } from '../editorHelpers.js'; 9 | import { isVertex } 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/layer/cesium/cogCesiumImpl.ts: -------------------------------------------------------------------------------- 1 | import type GeoTIFFSource from 'ol/source/GeoTIFF.js'; 2 | import { ImageryLayer as CesiumImageryLayer } from '@vcmap-cesium/engine'; 3 | import RasterLayerCesiumImpl from './rasterLayerCesiumImpl.js'; 4 | import COGImageryProvider from './cogImageryProvider.js'; 5 | import type { COGLayerImplementationOptions } from '../cogLayer.js'; 6 | import type CesiumMap from '../../map/cesiumMap.js'; 7 | 8 | /** 9 | * COG Layer implementation for {@link CesiumMap}. 10 | */ 11 | class COGCesiumImpl extends RasterLayerCesiumImpl { 12 | static get className(): string { 13 | return 'COGCesiumImpl'; 14 | } 15 | 16 | private _source: GeoTIFFSource; 17 | 18 | constructor(map: CesiumMap, options: COGLayerImplementationOptions) { 19 | super(map, options); 20 | this._source = options.source; 21 | } 22 | 23 | async getCesiumLayer(): Promise { 24 | const imageryProvider = new COGImageryProvider(this._source); 25 | const layerOptions = this.getCesiumLayerOptions(); 26 | return Promise.resolve( 27 | // @ts-expect-error: other impl 28 | new CesiumImageryLayer(imageryProvider, { 29 | ...layerOptions, 30 | rectangle: imageryProvider.tilingScheme.rectangle, 31 | }), 32 | ); 33 | } 34 | } 35 | 36 | export default COGCesiumImpl; 37 | -------------------------------------------------------------------------------- /src/vectorCluster/vectorClusterGroupCollection.ts: -------------------------------------------------------------------------------- 1 | import { check } from '@vcsuite/check'; 2 | import type 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/style/styleFactory.ts: -------------------------------------------------------------------------------- 1 | import { is, oneOf } from '@vcsuite/check'; 2 | import type { StyleItemOptions } from './styleItem.js'; 3 | import StyleItem from './styleItem.js'; 4 | import type { DeclarativeStyleItemOptions } from './declarativeStyleItem.js'; 5 | import { defaultDeclarativeStyle } from './declarativeStyleItem.js'; 6 | import { styleClassRegistry } from '../classRegistry.js'; 7 | import type { VectorStyleItemOptions } from './vectorStyleItem.js'; 8 | import VectorStyleItem from './vectorStyleItem.js'; 9 | 10 | export function getStyleOrDefaultStyle( 11 | styleOptions?: 12 | | DeclarativeStyleItemOptions 13 | | VectorStyleItemOptions 14 | | StyleItem, 15 | defaultStyle?: StyleItem, 16 | ): StyleItem { 17 | if (is(styleOptions, oneOf(StyleItem, { type: String }))) { 18 | if (styleOptions instanceof StyleItem) { 19 | return styleOptions; 20 | } else { 21 | const styleItem = styleClassRegistry.createFromTypeOptions( 22 | styleOptions as StyleItemOptions, 23 | ); 24 | if (styleItem) { 25 | if ( 26 | styleItem instanceof VectorStyleItem && 27 | defaultStyle instanceof VectorStyleItem 28 | ) { 29 | return styleItem.assign(defaultStyle.clone().assign(styleItem)); 30 | } 31 | return styleItem; 32 | } 33 | } 34 | } 35 | 36 | return defaultStyle || defaultDeclarativeStyle.clone(); 37 | } 38 | -------------------------------------------------------------------------------- /src/util/editor/interactions/rightClickInteraction.ts: -------------------------------------------------------------------------------- 1 | import type { InteractionEvent } from '../../../interaction/abstractInteraction.js'; 2 | import AbstractInteraction from '../../../interaction/abstractInteraction.js'; 3 | import { 4 | EventType, 5 | ModificationKeyType, 6 | PointerKeyType, 7 | } from '../../../interaction/interactionType.js'; 8 | import VcsEvent from '../../../vcsEvent.js'; 9 | 10 | function timeout(ms: number): Promise { 11 | return new Promise((resolve) => { 12 | setTimeout(resolve, ms); 13 | }); 14 | } 15 | 16 | export default class RightClickInteraction extends AbstractInteraction { 17 | rightClicked = new VcsEvent(); 18 | 19 | eventChainFinished = new VcsEvent(); 20 | 21 | constructor() { 22 | super(EventType.CLICK, ModificationKeyType.NONE, PointerKeyType.RIGHT); 23 | } 24 | 25 | async pipe(event: InteractionEvent): Promise { 26 | this.rightClicked.raiseEvent(); 27 | event.chainEnded?.addEventListener(() => { 28 | this.eventChainFinished.raiseEvent(); 29 | }); 30 | // we need to wait a bit, otherwise the changing features in the rightClicked Event do not take effect before 31 | // the next interaction. 32 | await timeout(0); 33 | return event; 34 | } 35 | 36 | destroy(): void { 37 | this.rightClicked.destroy(); 38 | this.eventChainFinished.destroy(); 39 | super.destroy(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/terrainLayer.spec.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 | -------------------------------------------------------------------------------- /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/map/navigation/easingHelper.ts: -------------------------------------------------------------------------------- 1 | import { type Movement } from './navigation.js'; 2 | import type { ControllerInput } from './controller/controllerInput.js'; 3 | import { getZeroInput, lerpRound } from './controller/controllerInput.js'; 4 | 5 | const inputScratch = getZeroInput(); 6 | 7 | export type NavigationEasing = { 8 | startTime: number; 9 | target: ControllerInput; 10 | getMovementAtTime(time: number): { 11 | movement: Movement; 12 | finished: boolean; 13 | }; 14 | }; 15 | 16 | export function createEasing( 17 | startTime: number, 18 | duration: number, 19 | origin: ControllerInput = getZeroInput(), 20 | target: ControllerInput = getZeroInput(), 21 | ): NavigationEasing { 22 | return { 23 | startTime, 24 | target, 25 | getMovementAtTime(time: number): { 26 | movement: Movement; 27 | finished: boolean; 28 | } { 29 | const normalizedTime = (time - startTime) / duration; 30 | if (normalizedTime < 1) { 31 | const movement: Movement = { 32 | time: normalizedTime, 33 | duration, 34 | input: lerpRound(origin, target, normalizedTime, inputScratch, 3), 35 | }; 36 | return { movement, finished: time >= startTime + duration }; 37 | } 38 | return { 39 | movement: { 40 | time: normalizedTime, 41 | duration, 42 | input: structuredClone(target), 43 | }, 44 | finished: true, 45 | }; 46 | }, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/ol/source/VcsCluster.ts: -------------------------------------------------------------------------------- 1 | import Cluster, { type Options } from 'ol/source/Cluster.js'; 2 | import type Feature from 'ol/Feature.js'; 3 | import type { 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/interaction/panoramaImageSelection.ts: -------------------------------------------------------------------------------- 1 | import type { Feature } from 'ol/index.js'; 2 | import type { InteractionEvent } from './abstractInteraction.js'; 3 | import AbstractInteraction from './abstractInteraction.js'; 4 | import { EventType } from './interactionType.js'; 5 | import PanoramaMap from '../map/panoramaMap.js'; 6 | import type MapCollection from '../util/mapCollection.js'; 7 | import { panoramaFeature } from '../layer/vectorSymbols.js'; 8 | 9 | export default class PanoramaImageSelection extends AbstractInteraction { 10 | constructor(private _mapCollection: MapCollection) { 11 | super(EventType.CLICK); 12 | } 13 | 14 | override async pipe(event: InteractionEvent): Promise { 15 | if (event.feature && (event.feature as Feature)[panoramaFeature]) { 16 | const { dataset, name } = (event.feature as Feature)[panoramaFeature]!; 17 | const panoramaImage = await dataset.createPanoramaImage(name); 18 | event.stopPropagation = true; 19 | 20 | if (event.map instanceof PanoramaMap) { 21 | event.map.setCurrentImage(panoramaImage); 22 | } else { 23 | const firstPanoramaMap = this._mapCollection.getByType( 24 | PanoramaMap.className, 25 | )[0]; 26 | if (firstPanoramaMap) { 27 | await this._mapCollection.activatePanoramaMap( 28 | firstPanoramaMap as PanoramaMap, 29 | panoramaImage, 30 | ); 31 | } 32 | } 33 | } 34 | 35 | return event; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/style/writeStyle.ts: -------------------------------------------------------------------------------- 1 | import type { VectorStyleItemOptions } from './vectorStyleItem.js'; 2 | import VectorStyleItem from './vectorStyleItem.js'; 3 | import DeclarativeStyleItem from './declarativeStyleItem.js'; 4 | import { type VcsMeta, vcsMetaVersion } from '../layer/vectorProperties.js'; 5 | import type StyleItem from './styleItem.js'; 6 | 7 | export function embedIconsInStyle( 8 | obj: VectorStyleItemOptions, 9 | embeddedIcons?: string[], 10 | ): VectorStyleItemOptions { 11 | if (obj.image && obj.image.src && /^data:/.test(obj.image.src)) { 12 | if (embeddedIcons) { 13 | let index = embeddedIcons.indexOf(obj.image.src); 14 | if (index === -1) { 15 | embeddedIcons.push(obj.image.src); 16 | index = embeddedIcons.length - 1; 17 | } 18 | obj.image.src = `:${index}`; 19 | } else { 20 | obj.image = { 21 | // XXX is this the correct fallback? 22 | radius: 5, 23 | }; 24 | } 25 | } 26 | return obj; 27 | } 28 | 29 | function writeStyle( 30 | style: StyleItem, 31 | vcsMeta: VcsMeta = { version: vcsMetaVersion }, 32 | ): VcsMeta { 33 | // XXX this entire function is not what is to be expected. feature store expects styles as refs to be possible 34 | if (style instanceof VectorStyleItem) { 35 | vcsMeta.style = embedIconsInStyle(style.toJSON(), vcsMeta.embeddedIcons); 36 | } else if (style instanceof DeclarativeStyleItem) { 37 | vcsMeta.style = style.toJSON(); 38 | } 39 | return vcsMeta; 40 | } 41 | 42 | export default writeStyle; 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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, [[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/layer/panorama/panoramaDatasetPanoramaImpl.ts: -------------------------------------------------------------------------------- 1 | import VectorTilePanoramaImpl from './vectorTilePanoramaImpl.js'; 2 | import type { VectorTileImplementationOptions } from '../vectorTileLayer.js'; 3 | import type PanoramaMap from '../../map/panoramaMap.js'; 4 | import { panoramaFeature } from '../vectorSymbols.js'; 5 | 6 | export type PanoramaDatasetPanoramaImplOptions = 7 | VectorTileImplementationOptions & { 8 | hideInPanorama?: boolean; 9 | }; 10 | 11 | export default class PanoramaDatasetPanoramaImpl extends VectorTilePanoramaImpl { 12 | private _hideInPanorama = false; 13 | 14 | constructor(map: PanoramaMap, options: PanoramaDatasetPanoramaImplOptions) { 15 | super(map, options); 16 | 17 | this._hideInPanorama = options.hideInPanorama ?? false; 18 | 19 | this.source.on('addfeature', ({ feature }) => { 20 | const panoramaProps = feature![panoramaFeature]!; 21 | if ( 22 | panoramaProps.dataset.tileProvider === options.tileProvider && 23 | panoramaProps.name === this._currentImage?.name 24 | ) { 25 | setTimeout(() => { 26 | this.source.removeFeature(feature!); 27 | }, 0); 28 | } 29 | }); 30 | } 31 | 32 | get hideInPanorama(): boolean { 33 | return this._hideInPanorama; 34 | } 35 | 36 | set hideInPanorama(value: boolean) { 37 | if (this._hideInPanorama !== value) { 38 | this._hideInPanorama = value; 39 | if (this._primitiveCollection) { 40 | this._primitiveCollection.show = !value; 41 | } 42 | } 43 | } 44 | 45 | override async activate(): Promise { 46 | await super.activate(); 47 | this._primitiveCollection.show = !this.hideInPanorama; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /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/map/navigation/cesiumNavigation.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Cartesian3 } from '@vcmap-cesium/engine'; 3 | import type 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 | 38 | it('should not update camera, if movement is below moveThreshold', () => { 39 | const { camera } = map.getScene()!; 40 | const startPosition = camera.position.clone(new Cartesian3()); 41 | cesiumNavigation.moveThreshold = 5; 42 | cesiumNavigation.update({ 43 | time: 0, 44 | duration: 1, 45 | input: { 46 | forward: 1, 47 | right: 0, 48 | up: 0, 49 | tiltDown: 0, 50 | rollRight: 0, 51 | turnRight: 0, 52 | }, 53 | }); 54 | expect(camera?.position.y).to.equal(startPosition.y); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 ${String(index)}${message}`, 25 | ); 26 | }); 27 | } 28 | 29 | export function replaceRequestAnimationFrame(): { 30 | tick: () => void; 31 | cleanup: () => void; 32 | } { 33 | let callbacks: FrameRequestCallback[] = []; 34 | const originalRequestAnimationFrame = global.requestAnimationFrame; 35 | const originalCancelAnimationFrame = global.cancelAnimationFrame; 36 | 37 | global.requestAnimationFrame = (callback: FrameRequestCallback): number => { 38 | callbacks.push(callback); 39 | return 0; 40 | }; 41 | 42 | global.cancelAnimationFrame = (): void => { 43 | callbacks = []; 44 | }; 45 | 46 | return { 47 | tick(time = 0): void { 48 | const toExecute = callbacks.slice(); 49 | callbacks = []; 50 | toExecute.forEach((callback) => { 51 | callback(time); 52 | }); 53 | }, 54 | cleanup(): void { 55 | global.requestAnimationFrame = originalRequestAnimationFrame; 56 | global.cancelAnimationFrame = originalCancelAnimationFrame; 57 | }, 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /src/workers/panoramaImageWorker.ts: -------------------------------------------------------------------------------- 1 | import type { PanoramaFileDirectoryMetadata } from '../panorama/panoramaImage.js'; 2 | 3 | declare let self: Worker; 4 | 5 | type PanoramaMessageEvent = MessageEvent<{ 6 | id: string; 7 | buffer: ArrayBuffer; 8 | fileDirectory: { vcsPanorama: PanoramaFileDirectoryMetadata }; 9 | }>; 10 | 11 | async function createDepthArray(buffer: Blob): Promise { 12 | const deflateStream = new DecompressionStream('deflate'); 13 | const decompressedStream = buffer.stream().pipeThrough(deflateStream); 14 | const arrayBuffer = await new Response(decompressedStream).arrayBuffer(); 15 | 16 | const depthData = new Uint16Array(arrayBuffer); 17 | const result = new Float32Array(depthData.length); 18 | 19 | for (let i = 0; i < depthData.length; i++) { 20 | result[i] = depthData[i] / 65535; // Normalize to [0, 1] 21 | } 22 | 23 | return result; 24 | } 25 | 26 | self.addEventListener('message', (e: PanoramaMessageEvent) => { 27 | const { id, buffer, fileDirectory } = e.data; 28 | const blob = new Blob([buffer]); 29 | 30 | let dataPromise: Promise; 31 | 32 | if (fileDirectory.vcsPanorama?.type === 'image') { 33 | dataPromise = createImageBitmap(blob, { imageOrientation: 'flipY' }); 34 | } else if (fileDirectory.vcsPanorama?.type === 'depth') { 35 | dataPromise = createDepthArray(blob); 36 | } else { 37 | dataPromise = Promise.reject(new Error('Missing vcsPanorama metadata')); 38 | } 39 | 40 | dataPromise 41 | .then((decoded) => { 42 | self.postMessage( 43 | { decoded, id }, 44 | decoded instanceof ImageBitmap ? [decoded] : [decoded.buffer], 45 | ); 46 | }) 47 | .catch((error: unknown) => { 48 | self.postMessage({ error, id }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/unit/map/navigation/easingHelper.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Math as CesiumMath } from '@vcmap-cesium/engine'; 3 | import type { NavigationEasing } from '../../../../src/map/navigation/easingHelper.js'; 4 | import { createEasing } from '../../../../src/map/navigation/easingHelper.js'; 5 | import type { ControllerInput } from '../../../../src/map/navigation/controller/controllerInput.js'; 6 | import { 7 | getZeroInput, 8 | fromArray, 9 | isNonZeroInput, 10 | } from '../../../../src/map/navigation/controller/controllerInput.js'; 11 | 12 | describe('Easing', () => { 13 | describe('create easing', () => { 14 | let easing: NavigationEasing; 15 | let time: number; 16 | let origin: ControllerInput; 17 | let target: ControllerInput; 18 | 19 | beforeEach(() => { 20 | time = performance.now(); 21 | origin = getZeroInput(); 22 | target = fromArray([1, 2, 3, 4, 5, 6]); 23 | }); 24 | 25 | it('should create an easing for provided duration', () => { 26 | easing = createEasing(time, 1000, origin, target); 27 | 28 | expect(easing).to.have.property('target', target); 29 | expect(easing).to.have.property('getMovementAtTime'); 30 | }); 31 | 32 | it('should return movement at time', () => { 33 | for (let i = 100; i < 1000; i += 100) { 34 | const { movement, finished } = easing.getMovementAtTime(time + i); 35 | 36 | expect(isNonZeroInput(movement.input)).to.be.true; 37 | expect(movement.time).to.be.closeTo(i / 1000, CesiumMath.EPSILON2); 38 | expect(finished).to.be.false; 39 | } 40 | const { movement, finished } = easing.getMovementAtTime(time + 1000); 41 | 42 | expect(movement.input).to.deep.equal(target); 43 | expect(finished).to.be.true; 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/layer/featureStoreFeatureVisibility.ts: -------------------------------------------------------------------------------- 1 | import type { HighlightStyleType } from './featureVisibility.js'; 2 | import FeatureVisibility from './featureVisibility.js'; 3 | import type FeatureStoreLayerChanges from './featureStoreLayerChanges.js'; 4 | 5 | export default class FeatureStoreFeatureVisibility extends FeatureVisibility { 6 | private _changeTracker: FeatureStoreLayerChanges; 7 | 8 | constructor(changeTracker: FeatureStoreLayerChanges) { 9 | super(); 10 | this._changeTracker = changeTracker; 11 | } 12 | 13 | highlight(toHighlight: Record): void { 14 | const isTracking = this._changeTracker.active; 15 | if (isTracking) { 16 | this._changeTracker.pauseTracking('changefeature'); 17 | } 18 | super.highlight(toHighlight); 19 | if (isTracking) { 20 | this._changeTracker.track(); 21 | } 22 | } 23 | 24 | unHighlight(toUnHighlight: (string | number)[]): void { 25 | const isTracking = this._changeTracker.active; 26 | if (isTracking) { 27 | this._changeTracker.pauseTracking('changefeature'); 28 | } 29 | super.unHighlight(toUnHighlight); 30 | if (isTracking) { 31 | this._changeTracker.track(); 32 | } 33 | } 34 | 35 | hideObjects(toHide: (string | number)[]): void { 36 | const isTracking = this._changeTracker.active; 37 | if (isTracking) { 38 | this._changeTracker.pauseTracking('changefeature'); 39 | } 40 | super.hideObjects(toHide); 41 | if (isTracking) { 42 | this._changeTracker.track(); 43 | } 44 | } 45 | 46 | showObjects(unHide: (string | number)[]): void { 47 | const isTracking = this._changeTracker.active; 48 | if (isTracking) { 49 | this._changeTracker.pauseTracking('changefeature'); 50 | } 51 | super.showObjects(unHide); 52 | if (isTracking) { 53 | this._changeTracker.track(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/map/navigation/navigationImpl.ts: -------------------------------------------------------------------------------- 1 | import type VcsMap from '../vcsMap.js'; 2 | import type { 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/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 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 30 | _viewDirection: ObliqueViewDirection, 31 | ): ObliqueImage { 32 | const groundCoordinates = [ 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 | [mercatorCoordinate[0] - 100, mercatorCoordinate[1] + 100, 0], 37 | ]; 38 | 39 | const image = new ObliqueImage({ 40 | meta: defaultMeta, 41 | viewDirection: ObliqueViewDirection.NORTH, 42 | viewDirectionAngle: 0, 43 | name: this.name, 44 | groundCoordinates, 45 | centerPointOnGround: mercatorCoordinate, 46 | }); 47 | 48 | image[isDefaultImageSymbol] = true; 49 | return image; 50 | } 51 | } 52 | 53 | export default DefaultObliqueCollection; 54 | -------------------------------------------------------------------------------- /src/layer/tileProvider/staticFeatureTileProvider.ts: -------------------------------------------------------------------------------- 1 | import type { Feature } from 'ol'; 2 | import type { TileProviderOptions } from './tileProvider.js'; 3 | import TileProvider from './tileProvider.js'; 4 | 5 | export type StaticFeatureTileProviderOptions = Omit< 6 | TileProviderOptions, 7 | 'baseLevels' 8 | > & { 9 | features: Feature[]; 10 | }; 11 | 12 | export default class StaticFeatureTileProvider extends TileProvider { 13 | static get className(): string { 14 | return 'StaticFeatureTileProvider'; 15 | } 16 | 17 | static getDefaultOptions(): StaticFeatureTileProviderOptions { 18 | return { 19 | ...TileProvider.getDefaultOptions(), 20 | features: [], 21 | }; 22 | } 23 | 24 | private _features: Feature[]; 25 | 26 | constructor(options: StaticFeatureTileProviderOptions) { 27 | const defaultOptions = StaticFeatureTileProvider.getDefaultOptions(); 28 | super({ ...options, baseLevels: [0] }); 29 | this._features = options.features || defaultOptions.features; 30 | } 31 | 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 | -------------------------------------------------------------------------------- /tests/unit/map/navigation/viewHelper.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import type 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 | 13 | before(async () => { 14 | map = await getOpenlayersMap(); 15 | // eslint-disable-next-line no-new 16 | 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 | expect(view.getZoom()!).to.be.lessThan(initialZoom); 51 | inputScratch.up = 0; 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /tests/unit/layer/wfsLayer.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import type { WFSOptions } from '../../../src/layer/wfsLayer.js'; 3 | import WFSLayer from '../../../src/layer/wfsLayer.js'; 4 | 5 | describe('WFSLayer', () => { 6 | describe('getting config objects', () => { 7 | describe('of a configured layer', () => { 8 | let inputConfig: WFSOptions; 9 | let outputConfig: WFSOptions; 10 | let configuredLayer: WFSLayer; 11 | 12 | before(() => { 13 | inputConfig = { 14 | featureType: ['fType'], 15 | version: '2.0.0', 16 | getFeatureOptions: { 17 | TEST: 'true', 18 | }, 19 | featureNS: 'fNS', 20 | featurePrefix: 'fPrefix', 21 | }; 22 | configuredLayer = new WFSLayer(inputConfig); 23 | outputConfig = configuredLayer.toJSON(); 24 | }); 25 | 26 | after(() => { 27 | configuredLayer.destroy(); 28 | }); 29 | 30 | it('should configure version', () => { 31 | expect(outputConfig).to.have.property('version', inputConfig.version); 32 | }); 33 | 34 | it('should configure featureType', () => { 35 | expect(outputConfig) 36 | .to.have.property('featureType') 37 | .and.to.have.members(inputConfig.featureType as string[]); 38 | }); 39 | 40 | it('should configure featureNS', () => { 41 | expect(outputConfig).to.have.property( 42 | 'featureNS', 43 | inputConfig.featureNS, 44 | ); 45 | }); 46 | 47 | it('should configure featurePrefix', () => { 48 | expect(outputConfig).to.have.property( 49 | 'featurePrefix', 50 | inputConfig.featurePrefix, 51 | ); 52 | }); 53 | 54 | it('should configure getFeatureOptions', () => { 55 | expect(outputConfig) 56 | .to.have.property('getFeatureOptions') 57 | .and.to.eql(inputConfig.getFeatureOptions); 58 | }); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /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/util/editor/interactions/translateVertexInteraction.ts: -------------------------------------------------------------------------------- 1 | import type { EventAfterEventHandler } from '../../../interaction/abstractInteraction.js'; 2 | import AbstractInteraction from '../../../interaction/abstractInteraction.js'; 3 | import { 4 | EventType, 5 | ModificationKeyType, 6 | } from '../../../interaction/interactionType.js'; 7 | import VcsEvent from '../../../vcsEvent.js'; 8 | import type { Vertex } from '../editorHelpers.js'; 9 | import { isVertex } 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/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 type { VectorHeightInfo } from './vectorHeightInfo.js'; 14 | import { mercatorToCartesianTransformerForHeightInfo } from './vectorHeightInfo.js'; 15 | import type { 16 | PolylineGeometryOptions, 17 | VectorGeometryFactory, 18 | } from './vectorGeometryFactory.js'; 19 | 20 | /** 21 | * Creates the positions & arcType option for the PolylineGeometry 22 | */ 23 | function getGeometryOptions( 24 | coords: Coordinate[], 25 | _geometry: LineString, 26 | heightInfo: VectorHeightInfo, 27 | ): PolylineGeometryOptions { 28 | const coordinateTransformer = 29 | mercatorToCartesianTransformerForHeightInfo(heightInfo); 30 | const positions = coords.map(coordinateTransformer); 31 | return { positions, arcType: ArcType.NONE }; 32 | } 33 | 34 | /** 35 | * @param arcCoords - the coordinates of the arc to use instead of the geometries coordinates if height mode is absolute 36 | * @param altitudeMode 37 | */ 38 | 39 | export function getArcGeometryFactory( 40 | arcCoords: Coordinate[], 41 | altitudeMode: HeightReference, 42 | ): VectorGeometryFactory<'arc'> { 43 | return { 44 | type: 'arc', 45 | getGeometryOptions: 46 | altitudeMode === HeightReference.NONE 47 | ? getGeometryOptions.bind(null, arcCoords) 48 | : getLineStringGeometryOptions, 49 | createSolidGeometries, 50 | createOutlineGeometries, 51 | createFillGeometries(): never[] { 52 | return []; 53 | }, 54 | createGroundLineGeometries, 55 | createLineGeometries, 56 | validateGeometry: validateLineString, 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/vcsEvent.ts: -------------------------------------------------------------------------------- 1 | type Listener = (event: T) => Promise | void; 2 | 3 | class VcsEvent { 4 | private _listeners = 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/util/editor/interactions/createPointInteraction.ts: -------------------------------------------------------------------------------- 1 | import Point from 'ol/geom/Point.js'; 2 | import type { EventAfterEventHandler } from '../../../interaction/abstractInteraction.js'; 3 | import AbstractInteraction from '../../../interaction/abstractInteraction.js'; 4 | import VcsEvent from '../../../vcsEvent.js'; 5 | import { EventType } from '../../../interaction/interactionType.js'; 6 | import { 7 | alreadyTransformedToImage, 8 | alreadyTransformedToMercator, 9 | } from '../../../layer/vectorSymbols.js'; 10 | import ObliqueMap from '../../../map/obliqueMap.js'; 11 | import type { CreateInteraction } from '../createFeatureSession.js'; 12 | 13 | /** 14 | * @extends {AbstractInteraction} 15 | * @implements {CreateInteraction} 16 | */ 17 | class CreatePointInteraction 18 | extends AbstractInteraction 19 | implements CreateInteraction 20 | { 21 | private _geometry: Point | null = null; 22 | 23 | finished = new VcsEvent(); 24 | 25 | created = new VcsEvent(); 26 | 27 | constructor() { 28 | super(EventType.CLICK); 29 | this.setActive(); 30 | } 31 | 32 | pipe(event: EventAfterEventHandler): Promise { 33 | this._geometry = new Point(event.positionOrPixel); 34 | if (event.map instanceof ObliqueMap) { 35 | this._geometry[alreadyTransformedToImage] = true; 36 | } else { 37 | this._geometry[alreadyTransformedToMercator] = true; 38 | } 39 | this.created.raiseEvent(this._geometry); 40 | this.finish(); 41 | return Promise.resolve(event); 42 | } 43 | 44 | /** 45 | * Finish the current creation. Calls finish and sets the interaction to be inactive 46 | */ 47 | finish(): void { 48 | if (this.active !== EventType.NONE) { 49 | this.setActive(false); 50 | this.finished.raiseEvent(this._geometry); 51 | } 52 | } 53 | 54 | destroy(): void { 55 | this.finished.destroy(); 56 | this.created.destroy(); 57 | super.destroy(); 58 | } 59 | } 60 | 61 | export default CreatePointInteraction; 62 | -------------------------------------------------------------------------------- /tests/unit/layer/cogLayer.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import type { COGLayerOptions } from '../../../src/layer/cogLayer.js'; 3 | import COGLayer from '../../../src/layer/cogLayer.js'; 4 | 5 | describe('COGLayer', () => { 6 | describe('configuration', () => { 7 | describe('of a default object', () => { 8 | it('should return an object with type and name for default layers', () => { 9 | const config = new COGLayer({}).toJSON(); 10 | expect(config).to.have.all.keys('name', 'type'); 11 | }); 12 | }); 13 | 14 | describe('of a configured layer', () => { 15 | let inputConfig: COGLayerOptions; 16 | let outputConfig: COGLayerOptions; 17 | let configuredLayer: COGLayer; 18 | 19 | before(() => { 20 | inputConfig = { 21 | url: 'http://localhost/test.tiff', 22 | convertToRGB: false, 23 | normalize: true, 24 | interpolate: false, 25 | sourceOptions: { 26 | allowFullFile: true, 27 | }, 28 | }; 29 | configuredLayer = new COGLayer(inputConfig); 30 | outputConfig = configuredLayer.toJSON(); 31 | }); 32 | 33 | after(() => { 34 | configuredLayer.destroy(); 35 | }); 36 | 37 | it('should configure url', () => { 38 | expect(outputConfig).to.have.property('url', inputConfig.url); 39 | }); 40 | 41 | it('should configure convertToRGB', () => { 42 | expect(outputConfig).to.have.property('convertToRGB', false); 43 | }); 44 | 45 | it('should configure normalize', () => { 46 | expect(outputConfig).to.have.property('normalize', true); 47 | }); 48 | 49 | it('should configure interpolate', () => { 50 | expect(outputConfig).to.have.property('interpolate', false); 51 | }); 52 | 53 | it('should configure sourceOptions', () => { 54 | expect(outputConfig) 55 | .to.have.property('sourceOptions') 56 | .and.deep.equals({ allowFullFile: true }); 57 | }); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { configs, createNamingConventionOptions } from '@vcsuite/eslint-config'; 2 | import globals from 'globals'; 3 | 4 | export default [ 5 | ...configs.nodeTs, 6 | { 7 | languageOptions: { 8 | globals: { 9 | ...globals.browser, 10 | }, 11 | }, 12 | rules: { 13 | 'import/no-cycle': 'off', 14 | 'import/no-named-as-default': 'off', 15 | 'import/no-named-as-default-member': 'off', 16 | 'n/no-unsupported-features/node-builtins': 'off', 17 | 'no-console': 'error', 18 | }, 19 | }, 20 | { 21 | ignores: ['node_modules/', 'coverage/', 'docs/', 'dist/', '.tests/'], 22 | }, 23 | { 24 | files: ['**/*.spec.js'], 25 | languageOptions: { 26 | globals: { 27 | expect: 'readonly', 28 | it: 'readonly', 29 | sinon: 'readonly', 30 | createCanvas: 'readonly', 31 | }, 32 | }, 33 | }, 34 | { 35 | files: ['**/build/*.js'], 36 | rules: { 37 | 'no-console': 'off', 38 | }, 39 | }, 40 | { 41 | files: ['**/src/oblique/**'], // legacy oblique uses kebab-case URG 42 | rules: { 43 | '@typescript-eslint/naming-convention': [ 44 | 'error', 45 | ...createNamingConventionOptions(), 46 | { 47 | selector: 'property', 48 | format: null, 49 | filter: { 50 | regex: '^(\\w+-)*\\w+$', 51 | match: true, 52 | }, 53 | }, 54 | ], 55 | }, 56 | }, 57 | { 58 | files: [ 59 | 'src/panorama/panoramaTileMaterial.ts', 60 | 'src/panorama/panoramaTileMaterial.spec.ts', 61 | 'src/panorama/panoramaTileMaterial.test.ts', 62 | ], 63 | rules: { 64 | '@typescript-eslint/naming-convention': [ 65 | 'error', 66 | ...createNamingConventionOptions(), 67 | { 68 | selector: ['property', 'objectLiteralProperty'], 69 | format: null, 70 | filter: { 71 | regex: '^u_.*', 72 | match: true, 73 | }, 74 | }, 75 | ], 76 | }, 77 | }, 78 | ]; 79 | -------------------------------------------------------------------------------- /src/util/editor/interactions/ensureHandlerSelectionInteraction.ts: -------------------------------------------------------------------------------- 1 | import type { Feature } from 'ol/index.js'; 2 | import type { Scene } from '@vcmap-cesium/engine'; 3 | import type { EventAfterEventHandler } from '../../../interaction/abstractInteraction.js'; 4 | import AbstractInteraction from '../../../interaction/abstractInteraction.js'; 5 | import { EventType } from '../../../interaction/interactionType.js'; 6 | import { handlerSymbol } from '../editorSymbols.js'; 7 | import CesiumMap from '../../../map/cesiumMap.js'; 8 | 9 | /** 10 | * This interaction ensure a potential handler is dragged in 3D when it is obscured by a transparent feature. 11 | * It uses drillPick on MOVE if: the map is 3D, there is a feature at said position, there is a feature selected in 12 | * the feature selection & the feature at the position is _not_ a handler 13 | */ 14 | class EnsureHandlerSelectionInteraction extends AbstractInteraction { 15 | private _featureSelection: Feature[]; 16 | 17 | /** 18 | * @param selectedFeatures Reference to the selected features. 19 | */ 20 | constructor(selectedFeatures: Feature[]) { 21 | super(EventType.DRAGSTART | EventType.MOVE); 22 | 23 | this._featureSelection = selectedFeatures; 24 | } 25 | 26 | pipe(event: EventAfterEventHandler): Promise { 27 | if ( 28 | event.feature && 29 | this._featureSelection.length > 0 && 30 | !(event.feature as Feature)[handlerSymbol] && 31 | event.map instanceof CesiumMap 32 | ) { 33 | const scene = event.map.getScene() as Scene; 34 | const drillPicks = scene.drillPick( 35 | event.windowPosition, 36 | undefined, 37 | 10, 38 | 10, 39 | ) as { primitive?: { olFeature?: Feature } }[]; 40 | const handler = drillPicks.find((p) => { 41 | return p?.primitive?.olFeature?.[handlerSymbol]; 42 | }); 43 | if (handler) { 44 | event.feature = handler.primitive!.olFeature; 45 | } 46 | } 47 | return Promise.resolve(event); 48 | } 49 | } 50 | 51 | export default EnsureHandlerSelectionInteraction; 52 | -------------------------------------------------------------------------------- /src/layer/tileLoadedHelper.ts: -------------------------------------------------------------------------------- 1 | import type { Globe } from '@vcmap-cesium/engine'; 2 | import CesiumTilesetCesiumImpl from './cesium/cesiumTilesetCesiumImpl.js'; 3 | import type CesiumTilesetLayer from './cesiumTilesetLayer.js'; 4 | import type 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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/layer/flatGeobufLayer.ts: -------------------------------------------------------------------------------- 1 | import type { VectorOptions } from './vectorLayer.js'; 2 | import VectorLayer from './vectorLayer.js'; 3 | import { wgs84Projection } from '../util/projection.js'; 4 | import { layerClassRegistry } from '../classRegistry.js'; 5 | import Extent from '../util/extent.js'; 6 | import { getOlFeatures, getValidReader } from './flatGeobufHelpers.js'; 7 | 8 | export type FlatGeobufLayerOptions = VectorOptions & { 9 | url: string | Record; 10 | }; 11 | 12 | export default class FlatGeobufLayer extends VectorLayer { 13 | static get className(): string { 14 | return 'FlatGeobufLayer'; 15 | } 16 | 17 | static getDefaultOptions(): FlatGeobufLayerOptions { 18 | return { 19 | ...super.getDefaultOptions(), 20 | url: '', 21 | }; 22 | } 23 | 24 | private _dataFetchedPromise: Promise | undefined; 25 | 26 | async initialize(): Promise { 27 | if (!this.initialized) { 28 | await super.initialize(); 29 | 30 | if (this._url) { 31 | await this.fetchData(); 32 | } 33 | } 34 | } 35 | 36 | async fetchData(): Promise { 37 | if (this._dataFetchedPromise) { 38 | return this._dataFetchedPromise; 39 | } 40 | 41 | const reader = await getValidReader(this.url, this.projection); 42 | let resolve: () => void; 43 | const promise = new Promise((r) => { 44 | resolve = r; 45 | }); 46 | this._dataFetchedPromise = promise; 47 | const worldExtent = new Extent({ 48 | coordinates: Extent.WGS_84_EXTENT, 49 | projection: wgs84Projection.toJSON(), 50 | }); 51 | const features = await getOlFeatures(reader, this.projection, worldExtent); 52 | if (this._dataFetchedPromise === promise) { 53 | this.addFeatures(features); 54 | } 55 | resolve!(); 56 | return this._dataFetchedPromise; 57 | } 58 | 59 | async reload(): Promise { 60 | if (this._dataFetchedPromise) { 61 | this._dataFetchedPromise = undefined; 62 | await this.fetchData(); 63 | } 64 | return this.forceRedraw(); 65 | } 66 | } 67 | layerClassRegistry.registerClass(FlatGeobufLayer.className, FlatGeobufLayer); 68 | -------------------------------------------------------------------------------- /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 type 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 48 | /** 49 | * An INTERNAL symbol used to keep track of the scale of scaled feature primitives. 50 | */ 51 | export const scaleSymbol: unique symbol = Symbol('Scale'); 52 | 53 | /** 54 | * Attached to all panorama features with the properties required to create a panorama image 55 | */ 56 | export const panoramaFeature = Symbol('panoramaFeature'); 57 | -------------------------------------------------------------------------------- /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 type { TileProviderOptions } from './tileProvider.js'; 5 | import TileProvider from './tileProvider.js'; 6 | import { getInitForUrl, requestJson } from '../../util/fetch.js'; 7 | import { tileProviderClassRegistry } from '../../classRegistry.js'; 8 | 9 | export type StaticGeoJSONTileProviderOptions = TileProviderOptions & { 10 | url: string; 11 | }; 12 | 13 | /** 14 | * Loads the provided geojson url and tiles the content in memory, data is only requested once 15 | */ 16 | class StaticGeoJSONTileProvider extends TileProvider { 17 | static get className(): string { 18 | return 'StaticGeoJSONTileProvider'; 19 | } 20 | 21 | static getDefaultOptions(): StaticGeoJSONTileProviderOptions { 22 | return { 23 | ...TileProvider.getDefaultOptions(), 24 | url: '', 25 | baseLevels: [0], 26 | }; 27 | } 28 | 29 | url: string; 30 | 31 | constructor(options: StaticGeoJSONTileProviderOptions) { 32 | const defaultOptions = StaticGeoJSONTileProvider.getDefaultOptions(); 33 | super({ ...options, baseLevels: defaultOptions.baseLevels }); 34 | 35 | this.url = options.url || defaultOptions.url; 36 | } 37 | 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/map/navigation/controller/controller.ts: -------------------------------------------------------------------------------- 1 | import { Math as CesiumMath } from '@vcmap-cesium/engine'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import type { ControllerInput } from './controllerInput.js'; 4 | import { checkThreshold, multiplyComponents } from './controllerInput.js'; 5 | 6 | export type ControllerOptions = { 7 | id: string; 8 | scales?: ControllerInput; 9 | inputThreshold?: number; 10 | }; 11 | 12 | class Controller { 13 | static get className(): string { 14 | return 'Controller'; 15 | } 16 | 17 | static getDefaultOptions(): ControllerOptions { 18 | return { 19 | id: '', 20 | scales: undefined, 21 | inputThreshold: CesiumMath.EPSILON1, 22 | }; 23 | } 24 | 25 | readonly id: string; 26 | 27 | scales?: ControllerInput; 28 | 29 | inputThreshold: number; 30 | 31 | constructor(options: ControllerOptions) { 32 | const defaultOptions = Controller.getDefaultOptions(); 33 | 34 | this.id = options.id || uuidv4(); 35 | this.scales = options.scales || defaultOptions.scales; 36 | this.inputThreshold = 37 | options.inputThreshold || defaultOptions.inputThreshold!; 38 | } 39 | 40 | // eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-unused-vars 41 | setMapTarget(_target: HTMLElement | null): void {} 42 | 43 | // eslint-disable-next-line class-methods-use-this 44 | getControllerInput(): ControllerInput | null { 45 | return null; 46 | } 47 | 48 | getInputs(): ControllerInput | null { 49 | const input = this.getControllerInput(); 50 | if (input) { 51 | if (checkThreshold(input, this.inputThreshold)) { 52 | return this.scales 53 | ? multiplyComponents(input, this.scales, input) 54 | : input; 55 | } 56 | } 57 | return null; 58 | } 59 | 60 | toJSON(): ControllerOptions { 61 | const defaultOptions = Controller.getDefaultOptions(); 62 | const config: ControllerOptions = { 63 | id: this.id, 64 | }; 65 | if (this.scales) { 66 | config.scales = this.scales; 67 | } 68 | if (defaultOptions.inputThreshold !== this.inputThreshold) { 69 | config.inputThreshold = this.inputThreshold; 70 | } 71 | return config; 72 | } 73 | 74 | // eslint-disable-next-line class-methods-use-this 75 | destroy(): void {} 76 | } 77 | 78 | export default Controller; 79 | -------------------------------------------------------------------------------- /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/util/isMobile.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * returns true if we are on a mobile device including tablets 3 | */ 4 | 5 | export function isMobile(): boolean { 6 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 7 | // @ts-ignore 8 | const agent = 9 | // eslint-disable-next-line @typescript-eslint/no-deprecated 10 | navigator.userAgent || navigator.vendor || (window.opera as string); 11 | return ( 12 | /(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( 13 | agent, 14 | ) || 15 | /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( 16 | agent.substring(0, 4), 17 | ) 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/util/editor/interactions/editFeaturesMouseOverInteraction.ts: -------------------------------------------------------------------------------- 1 | import type { Feature } from 'ol/index.js'; 2 | import { handlerSymbol, mouseOverSymbol } from '../editorSymbols.js'; 3 | import type { EventAfterEventHandler } from '../../../interaction/abstractInteraction.js'; 4 | import AbstractInteraction from '../../../interaction/abstractInteraction.js'; 5 | import { 6 | ModificationKeyType, 7 | EventType, 8 | } from '../../../interaction/interactionType.js'; 9 | import { cursorMap } from './editGeometryMouseOverInteraction.js'; 10 | 11 | /** 12 | * A class to handle mouse over effects on features for editor sessions. 13 | * @extends {AbstractInteraction} 14 | */ 15 | class EditFeaturesMouseOverInteraction extends AbstractInteraction { 16 | private _currentHandler: Feature | null = null; 17 | 18 | cursorStyle: CSSStyleDeclaration | undefined; 19 | 20 | constructor() { 21 | super(EventType.MOVE, ModificationKeyType.NONE); 22 | 23 | this.setActive(); 24 | } 25 | 26 | pipe(event: EventAfterEventHandler): Promise { 27 | if (event.feature && (event.feature as Feature)[handlerSymbol]) { 28 | this._currentHandler = event.feature as Feature; 29 | } else { 30 | this._currentHandler = null; 31 | } 32 | if (!this.cursorStyle && event.map?.target) { 33 | this.cursorStyle = event.map.target.style; 34 | } 35 | this._evaluate(); 36 | return Promise.resolve(event); 37 | } 38 | 39 | setActive(active?: boolean | number): void { 40 | super.setActive(active); 41 | this.reset(); 42 | } 43 | 44 | /** 45 | * Reset the cursorStyle to auto 46 | */ 47 | reset(): void { 48 | if (this.cursorStyle && this.cursorStyle.cursor) { 49 | this.cursorStyle.cursor = cursorMap.auto; 50 | this.cursorStyle = undefined; 51 | } 52 | } 53 | 54 | private _evaluate(): void { 55 | if (!this.cursorStyle) { 56 | return; 57 | } 58 | if (this._currentHandler) { 59 | this.cursorStyle.cursor = cursorMap.translate; 60 | this.cursorStyle[mouseOverSymbol] = this.id; 61 | } else if (this.cursorStyle?.[mouseOverSymbol] === this.id) { 62 | this.cursorStyle.cursor = cursorMap.auto; 63 | delete this.cursorStyle[mouseOverSymbol]; 64 | } 65 | } 66 | 67 | destroy(): void { 68 | this.reset(); 69 | super.destroy(); 70 | } 71 | } 72 | 73 | export default EditFeaturesMouseOverInteraction; 74 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/vectorCluster/vectorClusterGroupOpenlayersImpl.ts: -------------------------------------------------------------------------------- 1 | import OLVectorLayer from 'ol/layer/Vector.js'; 2 | import type { VectorClusterGroupImplementationOptions } from './vectorClusterGroup.js'; 3 | import type 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 | -------------------------------------------------------------------------------- /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 | global.ImageBitmap = HTMLCanvasElement; 25 | global.OffscreenCanvas = HTMLCanvasElement; 26 | global.createImageBitmap = (image, sx, sy, sw, sh) => { 27 | if (image instanceof HTMLCanvasElement) { 28 | return image; 29 | } 30 | const canElem = canvas.createCanvas(sw, sh); 31 | const ctx = canElem.getContext('2d'); 32 | const imageData = canvas.createImageData( 33 | new Uint8ClampedArray(image), 34 | sw, 35 | sh, 36 | ); 37 | ctx.putImageData(imageData, sx, sy); 38 | 39 | return Promise.resolve(canElem); 40 | }; 41 | 42 | const OriginalBlob = global.Blob; 43 | global.Blob = function BlobPolyfill(parts, options) { 44 | if (parts.length === 0) { 45 | return new OriginalBlob([], options); 46 | } 47 | if (parts.length > 1) { 48 | throw new Error( 49 | 'Blob constructor takes a single argument which is an array of ArrayBuffer', 50 | ); 51 | } 52 | return parts[0]; 53 | }; 54 | 55 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class 56 | global.Touch = class TouchMock { 57 | constructor({ identifier = 0, target = null, clientX = 0, clientY = 0 }) { 58 | this.identifier = identifier; 59 | this.target = target; 60 | this.clientX = clientX; 61 | this.clientY = clientY; 62 | } 63 | }; 64 | 65 | Object.assign(canvas, { 66 | CanvasGradient: canvasBindings.CanvasGradient, 67 | CanvasPattern: canvasBindings.CanvasPattern, 68 | }); 69 | ['CanvasRenderingContext2D', 'CanvasPattern', 'CanvasGradient'].forEach( 70 | (obj) => { 71 | global[obj] = canvas[obj]; 72 | }, 73 | ); 74 | global.createCanvas = canvas.createCanvas; 75 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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((l) => 53 | l.featureProvider?.getFeaturesByCoordinate?.( 54 | event.position as Coordinate, 55 | resolution, 56 | l.headers, 57 | ), 58 | ), 59 | ) 60 | ) 61 | .filter((f) => !!f) 62 | .flat(); 63 | if (features.length === 1) { 64 | event.feature = features[0]; 65 | } else if (features.length > 1) { 66 | const feature = new Feature({ features }); 67 | feature[isProvidedFeature] = true; // backward compatibility, may remove in future 68 | feature[isProvidedClusterFeature] = true; 69 | feature.setGeometry(new Point(event.position as Coordinate)); 70 | event.feature = feature; 71 | } 72 | } 73 | 74 | return event; 75 | } 76 | } 77 | 78 | export default FeatureProviderInteraction; 79 | -------------------------------------------------------------------------------- /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 type 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 | -------------------------------------------------------------------------------- /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 type { 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 | -------------------------------------------------------------------------------- /src/util/flight/flightCollection.ts: -------------------------------------------------------------------------------- 1 | import Collection from '../collection.js'; 2 | import type { FlightPlayer } from './flightPlayer.js'; 3 | import { createFlightPlayer } from './flightPlayer.js'; 4 | import VcsEvent from '../../vcsEvent.js'; 5 | import type FlightInstance from './flightInstance.js'; 6 | import type VcsApp from '../../vcsApp.js'; 7 | 8 | /** 9 | * A collection of flights. Provides playFlight API, which returns a FlightPlayer. 10 | * Emits playerChanged event, whenever another flight is played. 11 | */ 12 | class FlightCollection extends Collection { 13 | private readonly _app: VcsApp; 14 | 15 | private _player: FlightPlayer | undefined; 16 | 17 | playerChanged: VcsEvent; 18 | 19 | private _playerDestroyedListener: () => void; 20 | 21 | constructor(app: VcsApp) { 22 | super(); 23 | 24 | this._app = app; 25 | this._player = undefined; 26 | this.playerChanged = new VcsEvent(); 27 | this._playerDestroyedListener = (): void => {}; 28 | } 29 | 30 | get player(): FlightPlayer | undefined { 31 | return this._player; 32 | } 33 | 34 | remove(item: FlightInstance): void { 35 | if (this._player?.flightInstanceName === item.name) { 36 | this._player.stop(); 37 | this._player.destroy(); 38 | } 39 | super.remove(item); 40 | } 41 | 42 | /** 43 | * Creates a FlightPlayer for a flight instance, if not already existing for provided instance 44 | * @param flight 45 | */ 46 | async setPlayerForFlight( 47 | flight: FlightInstance, 48 | ): Promise { 49 | if (this._player?.flightInstanceName === flight.name) { 50 | return this._player; 51 | } else if (this._player) { 52 | this._playerDestroyedListener(); 53 | this._player.stop(); 54 | this._player.destroy(); 55 | } 56 | this._player = await createFlightPlayer(flight, this._app); 57 | this.playerChanged.raiseEvent(this._player); 58 | this._playerDestroyedListener = this._player.destroyed.addEventListener( 59 | () => { 60 | this._player = undefined; 61 | this.playerChanged.raiseEvent(undefined); 62 | }, 63 | ); 64 | return this._player; 65 | } 66 | 67 | destroy(): void { 68 | if (this._player) { 69 | this._player.stop(); 70 | this._player.destroy(); 71 | this._player = undefined; 72 | } 73 | this.playerChanged.destroy(); 74 | this._playerDestroyedListener(); 75 | super.destroy(); 76 | } 77 | } 78 | 79 | export default FlightCollection; 80 | -------------------------------------------------------------------------------- /src/util/clipping/clippingPolygonHelper.ts: -------------------------------------------------------------------------------- 1 | import type { ClippingPolygon, Globe } from '@vcmap-cesium/engine'; 2 | import { 3 | Cesium3DTileset, 4 | ClippingPolygonCollection, 5 | } from '@vcmap-cesium/engine'; 6 | import type CesiumMap from '../../map/cesiumMap.js'; 7 | import { vcsLayerName } from '../../layer/layerSymbols.js'; 8 | 9 | export function getTargetTilesets( 10 | map: CesiumMap, 11 | layerNames: string[] | 'all' = 'all', 12 | ): Cesium3DTileset[] { 13 | const tilesets = map 14 | .getVisualizations() 15 | .filter((v) => v instanceof Cesium3DTileset); 16 | if (Array.isArray(layerNames)) { 17 | return tilesets.filter((v) => layerNames.includes(v[vcsLayerName])); 18 | } 19 | return tilesets; 20 | } 21 | 22 | export function addClippingPolygon( 23 | clippee: Globe | Cesium3DTileset, 24 | polygon: ClippingPolygon | undefined, 25 | ): void { 26 | if (polygon) { 27 | if (clippee.clippingPolygons === undefined) { 28 | clippee.clippingPolygons = new ClippingPolygonCollection(); 29 | } 30 | if (!clippee.clippingPolygons.contains(polygon)) { 31 | clippee.clippingPolygons.setDirty(); 32 | clippee.clippingPolygons.add(polygon); 33 | } 34 | } 35 | } 36 | 37 | export function removeClippingPolygon( 38 | clippee: Globe | Cesium3DTileset, 39 | polygon: ClippingPolygon | undefined, 40 | ): void { 41 | if ( 42 | polygon && 43 | clippee.clippingPolygons && 44 | clippee.clippingPolygons.contains(polygon) 45 | ) { 46 | clippee.clippingPolygons.remove(polygon); 47 | } 48 | } 49 | 50 | export function addClippingPolygonObjectToMap( 51 | map: CesiumMap, 52 | polygon: ClippingPolygon | undefined, 53 | terrain: boolean, 54 | layerNames: string[] | 'all', 55 | ): void { 56 | if (terrain) { 57 | const globe = map.getScene()?.globe; 58 | if (globe) { 59 | addClippingPolygon(globe, polygon); 60 | } 61 | } 62 | 63 | const tilesets = getTargetTilesets(map, layerNames); 64 | tilesets.forEach((tileset) => { 65 | addClippingPolygon(tileset, polygon); 66 | }); 67 | } 68 | 69 | export function removeClippingPolygonFromMap( 70 | map: CesiumMap, 71 | polygon: ClippingPolygon | undefined, 72 | terrain: boolean, 73 | layerNames: string[] | 'all', 74 | ): void { 75 | if (terrain) { 76 | const globe = map.getScene()?.globe; 77 | if (globe) { 78 | removeClippingPolygon(globe, polygon); 79 | } 80 | } 81 | 82 | const tilesets = getTargetTilesets(map, layerNames); 83 | tilesets.forEach((tileset) => { 84 | removeClippingPolygon(tileset, polygon); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /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/unit/util/editor/transformation/setupTransformationHandler.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from 'uuid'; 2 | import { Feature } from 'ol'; 3 | import type { Cartesian3 } from '@vcmap-cesium/engine'; 4 | import { IntersectionTests } from '@vcmap-cesium/engine'; 5 | import type { Point } from 'ol/geom.js'; 6 | import sinon from 'sinon'; 7 | import type { 8 | AxisAndPlanes, 9 | TransformationHandler, 10 | TransformationMode, 11 | VcsMap, 12 | } from '../../../../../index.js'; 13 | import { 14 | createTransformationHandler, 15 | handlerSymbol, 16 | mercatorProjection, 17 | VcsApp, 18 | VectorLayer, 19 | } from '../../../../../index.js'; 20 | 21 | export type TransformationSetup = { 22 | transformationHandler: TransformationHandler; 23 | app: VcsApp; 24 | layer: VectorLayer; 25 | scratchLayer: VectorLayer; 26 | destroy: () => void; 27 | }; 28 | 29 | export async function setupTransformationHandler( 30 | map: VcsMap, 31 | mode: TransformationMode, 32 | ): Promise { 33 | const app = new VcsApp(); 34 | app.maps.add(map); 35 | const scratchLayer = new VectorLayer({ 36 | projection: mercatorProjection.toJSON(), 37 | }); 38 | const layer = new VectorLayer({ 39 | projection: mercatorProjection.toJSON(), 40 | }); 41 | app.layers.add(scratchLayer); 42 | app.layers.add(layer); 43 | 44 | await app.maps.setActiveMap(map.name); 45 | await layer.activate(); 46 | await scratchLayer.activate(); 47 | 48 | const transformationHandler = createTransformationHandler( 49 | map, 50 | layer, 51 | scratchLayer, 52 | mode, 53 | ); 54 | return { 55 | transformationHandler, 56 | app, 57 | layer, 58 | scratchLayer, 59 | destroy(): void { 60 | transformationHandler.destroy(); 61 | app.destroy(); 62 | }, 63 | }; 64 | } 65 | 66 | export function createHandlerFeature(axis: AxisAndPlanes): Feature { 67 | const feature = new Feature(); 68 | feature[handlerSymbol] = axis; 69 | return feature; 70 | } 71 | 72 | export function patchPickRay( 73 | calls: Cartesian3[], 74 | sandbox?: sinon.SinonSandbox, 75 | ): () => void { 76 | const stub = (sandbox ?? sinon).stub(IntersectionTests, 'rayPlane'); 77 | calls.forEach((value, index) => { 78 | stub.onCall(index).returns(value); 79 | }); 80 | 81 | return () => { 82 | stub.restore(); 83 | }; 84 | } 85 | 86 | export function createFeatureWithId( 87 | propsOrProps: Point | Record, 88 | ): Feature { 89 | const feature = new Feature(propsOrProps); 90 | feature.setId(v4()); 91 | return feature as Feature; 92 | } 93 | -------------------------------------------------------------------------------- /src/layer/cesium/wmsCesiumImpl.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ImageryLayer as CesiumImageryLayer, 3 | Rectangle, 4 | WebMapServiceImageryProvider, 5 | WebMercatorTilingScheme, 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 | import { TilingScheme } from '../rasterLayer.js'; 15 | 16 | /** 17 | * represents a specific Cesium WmsCesiumImpl Layer class. 18 | */ 19 | class WmsCesiumImpl extends RasterLayerCesiumImpl { 20 | static get className(): string { 21 | return 'WmsCesiumImpl'; 22 | } 23 | 24 | parameters: Record; 25 | 26 | highResolution: boolean; 27 | 28 | tileSize: Size; 29 | 30 | constructor(map: CesiumMap, options: WMSImplementationOptions) { 31 | super(map, options); 32 | this.parameters = options.parameters; 33 | this.highResolution = options.highResolution; 34 | this.tileSize = options.tileSize; 35 | } 36 | 37 | getCesiumLayer(): Promise { 38 | const parameters = { ...this.parameters }; 39 | if (this.highResolution) { 40 | parameters.width = String(this.tileSize[0] * 2); 41 | parameters.height = String(this.tileSize[1] * 2); 42 | } 43 | const options: WebMapServiceImageryProvider.ConstructorOptions = { 44 | url: getResourceOrUrl(this.url!, this.headers), 45 | layers: parameters.LAYERS, 46 | minimumLevel: this.minLevel, 47 | maximumLevel: this.maxLevel, 48 | parameters, 49 | tileWidth: this.tileSize[0], 50 | tileHeight: this.tileSize[1], 51 | }; 52 | 53 | if (this.extent && this.extent.isValid()) { 54 | const extent = this.extent.getCoordinatesInProjection(wgs84Projection); 55 | if (extent) { 56 | options.rectangle = Rectangle.fromDegrees( 57 | extent[0], 58 | extent[1], 59 | extent[2], 60 | extent[3], 61 | ); 62 | } 63 | } 64 | if (this.tilingSchema === TilingScheme.MERCATOR) { 65 | options.tilingScheme = new WebMercatorTilingScheme(); 66 | } 67 | 68 | const imageryProvider = new WebMapServiceImageryProvider(options); 69 | const layerOptions = this.getCesiumLayerOptions(); 70 | return Promise.resolve( 71 | new CesiumImageryLayer(imageryProvider, layerOptions), 72 | ); 73 | } 74 | } 75 | 76 | export default WmsCesiumImpl; 77 | -------------------------------------------------------------------------------- /src/layer/openlayers/cogOpenlayersImpl.ts: -------------------------------------------------------------------------------- 1 | import type GeoTIFFSource from 'ol/source/GeoTIFF.js'; 2 | import WebGLTile from 'ol/layer/WebGLTile.js'; 3 | import { getRenderPixel } from 'ol/render.js'; 4 | import type RenderEvent from 'ol/render/Event.js'; 5 | import { SplitDirection } from '@vcmap-cesium/engine'; 6 | import RasterLayerOpenlayersImpl from './rasterLayerOpenlayersImpl.js'; 7 | import type { COGLayerImplementationOptions } from '../cogLayer.js'; 8 | import type OpenlayersMap from '../../map/openlayersMap.js'; 9 | 10 | const vcsCleared = Symbol('vcsCleared'); 11 | 12 | declare global { 13 | interface WebGL2RenderingContext { 14 | [vcsCleared]: number; 15 | } 16 | } 17 | 18 | /** 19 | * COGLayer implementation for {@link OpenlayersMap}. 20 | */ 21 | class COGOpenlayersImpl extends RasterLayerOpenlayersImpl { 22 | static get className(): string { 23 | return 'COGOpenlayersImpl'; 24 | } 25 | 26 | private _source: GeoTIFFSource; 27 | 28 | constructor(map: OpenlayersMap, options: COGLayerImplementationOptions) { 29 | super(map, options); 30 | this._source = options.source; 31 | } 32 | 33 | getOLLayer(): WebGLTile { 34 | return new WebGLTile({ 35 | source: this._source, 36 | opacity: this.opacity, 37 | minZoom: this.minRenderingLevel, 38 | maxZoom: this.maxRenderingLevel, 39 | }); 40 | } 41 | 42 | protected override _splitPreRender(event: RenderEvent): void { 43 | const gl = event.context as WebGL2RenderingContext; 44 | if (gl[vcsCleared] !== event.frameState?.time) { 45 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 46 | } 47 | gl.enable(gl.SCISSOR_TEST); 48 | 49 | const mapSize = this.map.olMap?.getSize(); 50 | if (!mapSize) { 51 | throw new Error('Map size is not available for scissor test'); 52 | } 53 | 54 | const bottomLeft = getRenderPixel(event, [0, mapSize[1]]); 55 | const topRight = getRenderPixel(event, [mapSize[0], 0]); 56 | 57 | const width = Math.round( 58 | (topRight[0] - bottomLeft[0]) * this.map.splitPosition, 59 | ); 60 | const height = topRight[1] - bottomLeft[1]; 61 | if (this.splitDirection === SplitDirection.LEFT) { 62 | gl.scissor(bottomLeft[0], bottomLeft[1], width, height); 63 | } else { 64 | gl.scissor(bottomLeft[0] + width, bottomLeft[1], topRight[0], height); 65 | } 66 | } 67 | 68 | // eslint-disable-next-line class-methods-use-this 69 | protected override _splitPostReder(event: RenderEvent): void { 70 | const gl = event.context as WebGL2RenderingContext; 71 | gl.disable(gl.SCISSOR_TEST); 72 | } 73 | } 74 | 75 | export default COGOpenlayersImpl; 76 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/vectorCluster/vectorClusterGroupObliqueImpl.ts: -------------------------------------------------------------------------------- 1 | import OLVectorLayer from 'ol/layer/Vector.js'; 2 | import VectorClusterGroupImpl from './vectorClusterGroupImpl.js'; 3 | import type ObliqueMap from '../map/obliqueMap.js'; 4 | import VcsCluster from '../ol/source/VcsCluster.js'; 5 | import type { VectorClusterGroupImplementationOptions } from './vectorClusterGroup.js'; 6 | import type { SourceObliqueSync } from '../layer/oblique/sourceObliqueSync.js'; 7 | import { createSourceObliqueSync } from '../layer/oblique/sourceObliqueSync.js'; 8 | import { vectorClusterGroupName } from './vectorClusterSymbols.js'; 9 | 10 | export default class VectorClusterGroupObliqueImpl extends VectorClusterGroupImpl { 11 | private _clusterSource: VcsCluster; 12 | 13 | private _olLayer: OLVectorLayer | undefined; 14 | 15 | private _sourceObliqueSync: SourceObliqueSync; 16 | 17 | constructor( 18 | map: ObliqueMap, 19 | options: VectorClusterGroupImplementationOptions, 20 | ) { 21 | super(map, options); 22 | 23 | this._sourceObliqueSync = createSourceObliqueSync(options.source, map); 24 | this._clusterSource = new VcsCluster( 25 | { 26 | source: this._sourceObliqueSync.obliqueSource, 27 | distance: options.clusterDistance, 28 | }, 29 | this.name, 30 | ); 31 | } 32 | 33 | get clusterSource(): VcsCluster { 34 | return this._clusterSource; 35 | } 36 | 37 | get olLayer(): OLVectorLayer | undefined { 38 | return this._olLayer; 39 | } 40 | 41 | async initialize(): Promise { 42 | if (!this.initialized) { 43 | const olLayer = new OLVectorLayer({ 44 | visible: false, 45 | source: this._clusterSource, 46 | style: this.style, 47 | }); 48 | olLayer[vectorClusterGroupName] = this.name; 49 | this._olLayer = olLayer; 50 | this.map.addOLLayer(this._olLayer); 51 | } 52 | await super.initialize(); 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 | this._sourceObliqueSync.activate(); 62 | } 63 | } 64 | 65 | deactivate(): void { 66 | super.deactivate(); 67 | this._olLayer?.setVisible(false); 68 | this._clusterSource.paused = true; 69 | this._sourceObliqueSync.deactivate(); 70 | } 71 | 72 | destroy(): void { 73 | if (this._olLayer) { 74 | this.map.removeOLLayer(this._olLayer); 75 | } 76 | this._olLayer = undefined; 77 | 78 | this._sourceObliqueSync.destroy(); 79 | super.destroy(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /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/unit/vectorCluster/vectorClusterGroupImpl.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import type { StyleFunction } from 'ol/style/Style.js'; 3 | import Style from 'ol/style/Style.js'; 4 | import VectorClusterGroupImpl from '../../../src/vectorCluster/vectorClusterGroupImpl.js'; 5 | import ClusterEnhancedVectorSource from '../../../src/ol/source/ClusterEnhancedVectorSource.js'; 6 | import FeatureVisibility from '../../../src/layer/featureVisibility.js'; 7 | import VcsMap from '../../../src/map/vcsMap.js'; 8 | import { GlobalHider, VectorProperties } from '../../../index.js'; 9 | import VcsCluster from '../../../src/ol/source/VcsCluster.js'; 10 | 11 | describe('VectorClusterGroupImpl', () => { 12 | let map: VcsMap; 13 | let source: ClusterEnhancedVectorSource; 14 | let featureVisibility: FeatureVisibility; 15 | let style: StyleFunction; 16 | let vectorProperties: VectorProperties; 17 | let globalHider: GlobalHider; 18 | let clusterSource: VcsCluster; 19 | let vectorClusterGroupImpl: VectorClusterGroupImpl; 20 | 21 | before(async () => { 22 | map = new VcsMap({}); 23 | await map.activate(); 24 | source = new ClusterEnhancedVectorSource(); 25 | clusterSource = new VcsCluster({ source }, 'test'); 26 | featureVisibility = new FeatureVisibility(); 27 | vectorProperties = new VectorProperties({}); 28 | style = (): Style => new Style({}); 29 | globalHider = new GlobalHider(); 30 | }); 31 | 32 | beforeEach(async () => { 33 | vectorClusterGroupImpl = new VectorClusterGroupImpl(map, { 34 | name: 'test', 35 | style, 36 | vectorProperties, 37 | source, 38 | featureVisibility, 39 | globalHider, 40 | clusterDistance: 40, 41 | getLayerByName: (): undefined => undefined, 42 | }); 43 | await vectorClusterGroupImpl.activate(); 44 | }); 45 | 46 | afterEach(() => { 47 | vectorClusterGroupImpl.destroy(); 48 | }); 49 | 50 | after(() => { 51 | map.destroy(); 52 | featureVisibility.destroy(); 53 | globalHider.destroy(); 54 | vectorProperties.destroy(); 55 | clusterSource.dispose(); 56 | source.dispose(); 57 | }); 58 | 59 | it('should activate correctly', () => { 60 | expect(vectorClusterGroupImpl.active).to.be.true; 61 | expect(vectorClusterGroupImpl.initialized).to.be.true; 62 | }); 63 | 64 | it('should deactivate correctly', () => { 65 | vectorClusterGroupImpl.deactivate(); 66 | expect(vectorClusterGroupImpl.active).to.be.false; 67 | }); 68 | 69 | it('should destroy correctly', () => { 70 | vectorClusterGroupImpl.destroy(); 71 | expect(vectorClusterGroupImpl.initialized).to.be.false; 72 | expect(() => vectorClusterGroupImpl.map).to.throw(); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | 11 | beforeEach(() => { 12 | AI = new AbstractInteraction(); 13 | }); 14 | 15 | describe('#setModification', () => { 16 | it('should set the modification', () => { 17 | AI.setModification(ModificationKeyType.CTRL); 18 | expect(AI).to.have.property('modificationKey', ModificationKeyType.CTRL); 19 | }); 20 | 21 | it('should reset the default modification key if called without arguments', () => { 22 | AI.setModification(ModificationKeyType.CTRL); 23 | expect(AI).to.have.property('modificationKey', ModificationKeyType.CTRL); 24 | AI.setModification(); 25 | expect(AI).to.have.property('modificationKey', ModificationKeyType.NONE); 26 | }); 27 | }); 28 | 29 | describe('#setPointer', () => { 30 | it('should set the pointer', () => { 31 | AI.setPointer(PointerKeyType.RIGHT); 32 | expect(AI).to.have.property('pointerKey', PointerKeyType.RIGHT); 33 | }); 34 | 35 | it('should reset the default pointer key if called without arguments', () => { 36 | AI.setPointer(PointerKeyType.RIGHT); 37 | expect(AI).to.have.property('pointerKey', PointerKeyType.RIGHT); 38 | AI.setPointer(); 39 | expect(AI).to.have.property('pointerKey', PointerKeyType.LEFT); 40 | }); 41 | }); 42 | 43 | describe('#setActive', () => { 44 | it('should set the active event, if called with a number', () => { 45 | AI.setActive(EventType.MOVE); 46 | expect(AI).to.have.property('active', EventType.MOVE); 47 | }); 48 | 49 | it('should toggle the default active if called with a boolean', () => { 50 | expect(AI).to.have.property('active', EventType.NONE); 51 | expect(AI).to.have.property('modificationKey', ModificationKeyType.NONE); 52 | 53 | AI._defaultActive = EventType.MOVE; 54 | AI.setActive(true); 55 | expect(AI).to.have.property('active', EventType.MOVE); 56 | AI.setModification(ModificationKeyType.CTRL); 57 | AI.setActive(false); 58 | expect(AI).to.have.property('active', EventType.NONE); 59 | expect(AI).to.have.property('modificationKey', ModificationKeyType.CTRL); 60 | }); 61 | 62 | it('should reset all defaults, if called without arguments', () => { 63 | AI.setModification(ModificationKeyType.CTRL); 64 | AI.setPointer(PointerKeyType.MIDDLE); 65 | AI.setActive(); 66 | expect(AI).to.have.property('active', EventType.NONE); 67 | expect(AI).to.have.property('modificationKey', ModificationKeyType.NONE); 68 | expect(AI).to.have.property('pointerKey', PointerKeyType.LEFT); 69 | }); 70 | }); 71 | }); 72 | --------------------------------------------------------------------------------