diff --git a/package-lock.json b/package-lock.json index 153efb1195..7eba4e18eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "copc": "^0.0.6", "earcut": "^3.0.0", "js-priority-queue": "^0.1.5", + "lru-cache": "^11.0.1", "pbf": "^4.0.1", "shpjs": "^6.1.0", "threads": "^1.7.0" @@ -239,6 +240,15 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "devOptional": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.25.4", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.4.tgz", @@ -4468,13 +4478,13 @@ } }, "node_modules/3d-tiles-renderer/node_modules/@types/node": { - "version": "22.7.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", - "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", + "version": "22.8.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.0.tgz", + "integrity": "sha512-84rafSBHC/z1i1E3p0cJwKA+CfYDNSXX9WSZBRopjIzLET8oNt6ht2tei4C7izwDeEiLLfdeSVBv1egOH916hg==", "optional": true, "peer": true, "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.19.8" } }, "node_modules/3d-tiles-renderer/node_modules/@vitejs/plugin-react": { @@ -8100,7 +8110,6 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -10370,13 +10379,11 @@ } }, "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "devOptional": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", + "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==", + "engines": { + "node": "20 || >=22" } }, "node_modules/make-dir": { @@ -15178,8 +15185,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "devOptional": true, - "license": "ISC" + "devOptional": true }, "node_modules/yargs": { "version": "17.7.2", diff --git a/package.json b/package.json index 3937c01f21..4cfdd4b4a1 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "copc": "^0.0.6", "earcut": "^3.0.0", "js-priority-queue": "^0.1.5", + "lru-cache": "^11.0.1", "pbf": "^4.0.1", "shpjs": "^6.1.0", "threads": "^1.7.0" diff --git a/src/Core/MainLoop.js b/src/Core/MainLoop.js index 1093eaff42..72e5ecb59e 100644 --- a/src/Core/MainLoop.js +++ b/src/Core/MainLoop.js @@ -53,7 +53,6 @@ function updateElements(context, geometryLayer, elements) { for (const attachedLayer of geometryLayer.attachedLayers) { if (attachedLayer.ready) { attachedLayer.update(context, attachedLayer, sub.element, sub.parent); - attachedLayer.cache.flush(); } } } else if (sub.elements) { @@ -67,7 +66,6 @@ function updateElements(context, geometryLayer, elements) { for (const attachedLayer of geometryLayer.attachedLayers) { if (attachedLayer.ready) { attachedLayer.update(context, attachedLayer, sub.elements[i], sub.parent); - attachedLayer.cache.flush(); } } } @@ -161,7 +159,6 @@ class MainLoop extends EventDispatcher { } // Clear the cache of expired resources - geometryLayer.cache.flush(); view.execFrameRequesters(MAIN_LOOP_EVENTS.AFTER_LAYER_UPDATE, dt, this.#updateLoopRestarted, geometryLayer); } diff --git a/src/Core/Prefab/TileBuilder.ts b/src/Core/Prefab/TileBuilder.ts index 7d1997767d..75f17e9929 100644 --- a/src/Core/Prefab/TileBuilder.ts +++ b/src/Core/Prefab/TileBuilder.ts @@ -1,6 +1,6 @@ import * as THREE from 'three'; import { TileGeometry } from 'Core/TileGeometry'; -import Cache from 'Core/Scheduler/Cache'; +import { LRUCache } from 'lru-cache'; import { computeBuffers } from 'Core/Prefab/computeBufferTileGeometry'; import OBB from 'Renderer/OBB'; import type Extent from 'Core/Geographic/Extent'; @@ -10,7 +10,7 @@ const cacheBuffer = new Map(); -const cacheTile = new Cache(); +const cacheTile = new LRUCache>({ max: 500 }); export type GpuBufferAttributes = { index: THREE.BufferAttribute | null; @@ -84,13 +84,14 @@ export function newTileGeometry( const bufferKey = `${builder.crs}_${params.disableSkirt ? 0 : 1}_${params.segments}`; - let promiseGeometry = cacheTile.get(south, params.level, bufferKey); + const key = `s${south}l${params.level}bK${bufferKey}`; + let promiseGeometry = cacheTile.get(key); // build geometry if doesn't exist if (!promiseGeometry) { let resolve; promiseGeometry = new Promise((r) => { resolve = r; }); - cacheTile.set(promiseGeometry, south, params.level, bufferKey); + cacheTile.set(key, promiseGeometry); params.extent = shareableExtent; params.center = builder.center(params.extent).clone(); @@ -145,7 +146,7 @@ export function newTileGeometry( const geometry = new TileGeometry(builder, params, gpuBuffers); geometry.OBB = new OBB(geometry.boundingBox!.min, geometry.boundingBox!.max); - geometry.initRefCount(cacheTile, [south, params.level, bufferKey]); + geometry.initRefCount(cacheTile, key); resolve!(geometry); return Promise.resolve({ geometry, quaternion, position }); diff --git a/src/Core/Scheduler/Cache.js b/src/Core/Scheduler/Cache.js index 1745226143..018ab72e6f 100644 --- a/src/Core/Scheduler/Cache.js +++ b/src/Core/Scheduler/Cache.js @@ -1,5 +1,3 @@ -let entry; - /** * Cache policies for flushing. Those policies can be used when something is * [set]{@link Cache.set} into the Cache, as the lifetime property. @@ -17,242 +15,3 @@ export const CACHE_POLICIES = { TEXTURE: 900000, GEOMETRY: 900000, }; - -/** - * This is a copy of the Map object, except that it also store a value for last - * time used. This value is used for cache expiration mechanism. - * - * @example - * import Cache, { CACHE_POLICIES } from 'Core/Scheduler/Cache'; - * - * const cache = new Cache(CACHE_POLICIES.TEXTURE) - * cache.set({ bar: 1 }, 'foo'); - * cache.set({ bar: 32 }, 'foo', 'toto'); - * - * cache.get('foo'); - * - * cache.delete('foo'); - * - * cache.clear(); - * - * cache.flush(); - */ -class Cache { - /** - * @param {number} [lifetime=CACHE_POLICIES.INFINITE] The cache expiration time for all values. - */ - constructor(lifetime = CACHE_POLICIES.INFINITE) { - this.lifeTime = lifetime; - this.lastTimeFlush = Date.now(); - this.data = new Map(); - } - - /** - * Returns the entry related to the specified key, content in array, from the cache. - * The array contents one to three key. - * The last time used property of the entry is updated to extend the longevity of the - * entry. - * - * @param {string[]|number[]} keyArray key array ([key0, key1, key3]) - * - * @return {Object} - */ - - getByArray(keyArray) { - return this.get(keyArray[0], keyArray[1], keyArray[2]); - } - - /** - * Adds or updates an entry with specified keys array ([key0, key1, key3]). - * Caution: it overrides any existing entry already set at this/those key/s. - * - * @param {Object} value to add in cache - * @param {string[]|number[]} keyArray key array ([key0, key1, key3]) - * - * @return {Object} the added value - */ - setByArray(value, keyArray) { - return this.set(value, keyArray[0], keyArray[1], keyArray[2]); - } - - /** - * Returns the entry related to the specified key from the cache. The last - * time used property of the entry is updated to extend the longevity of the - * entry. - * - * @param {string|number} key1 - * @param {string|number} [key2] - * @param {string|number} [key3] - * - * @return {Object} - */ - get(key1, key2, key3) { - const entry_1 = this.data.get(key1); - if (entry_1 == undefined) { return; } - - if (entry_1.lastTimeUsed != undefined) { - entry = entry_1; - } else { - const entry_2 = entry_1.get(key2); - if (entry_2 == undefined) { return; } - - if (entry_2.lastTimeUsed != undefined) { - entry = entry_2; - } else { - const entry_3 = entry_2.get(key3); - if (entry_3 == undefined) { return; } - entry = entry_3; - } - } - - if (entry.value) { - entry.lastTimeUsed = Date.now(); - return entry.value; - } - } - - /** - * Adds or updates an entry with specified keys (up to 3). - * Caution: it overrides any existing entry already set at this/those key/s. - * - * - * @param {Object} value to add in cache - * @param {string|number} key1 - * @param {string|number} [key2] - * @param {string|number} [key3] - * - * @return {Object} the added value - */ - set(value, key1, key2, key3) { - entry = { - value, - lastTimeUsed: Date.now(), - }; - - if (key2 == undefined) { - this.data.set(key1, entry); - return value; - } - - if (!this.data.get(key1)) { - this.data.set(key1, new Map()); - } - - const entry_map = this.data.get(key1); - - if (key3 == undefined) { - entry_map.set(key2, entry); - return value; - } - - if (!entry_map.get(key2)) { - entry_map.set(key2, new Map()); - } - - entry_map.get(key2).set(key3, entry); - - return value; - } - - /** - * Deletes the specified entry from the cache. - * - * @param {string|number} key1 - * @param {string|number} [key2] - * @param {string|number} [key3] - */ - delete(key1, key2, key3) { - const entry_1 = this.data.get(key1); - if (entry_1 === undefined) { return; } - - if (entry_1.lastTimeUsed != undefined) { - delete this.data.get(key1); - this.data.delete(key1); - } else { - const entry_2 = entry_1.get(key2); - if (entry_2 === undefined) { return; } - if (entry_2.lastTimeUsed != undefined) { - delete entry_1.get(key2); - entry_1.delete(key2); - if (entry_1.size == 0) { - this.data.delete(key1); - } - } else { - const entry_3 = entry_2.get(key3); - if (entry_3 === undefined) { return; } - delete entry_2.get(key3); - entry_2.delete(key3); - if (entry_2.size == 0) { - entry_1.delete(key2); - if (entry_1.size == 0) { - this.data.delete(key1); - } - } - } - } - } - - /** - * Removes all entries of the cache. - * - */ - clear() { - this.data.clear(); - } - - /** - * Flush the cache: entries that have been present for too long since the - * last time they were used, are removed from the cache. By default, the - * time is the current time, but the interval can be reduced by doing - * something like `Cache.flush(Date.now() - reductionTime)`. If you want to - * clear the whole cache, use {@link Cache.clear} instead. - * - * @param {number} [time=Date.now()] - */ - flush(time = Date.now()) { - if (this.lifeTime == CACHE_POLICIES.INFINITE || - this.lifeTime > time - this.lastTimeFlush || - !this.data.size) { - return; - } - - this.lastTimeFlush = Infinity; - this.data.forEach((v1, i) => { - if (this.lifeTime < time - v1.lastTimeUsed) { - delete this.data.get(i); - this.data.delete(i); - } else { - v1.forEach((v2, j) => { - if (this.lifeTime < time - v2.lastTimeUsed) { - delete v1.get(j); - v1.delete(j); - } else { - v2.forEach((v3, k) => { - if (this.lifeTime < time - v3.lastTimeUsed) { - delete v2.get(k); - v2.delete(k); - } else { - // Work for the moment because all flushed caches have 3 key! - this.lastTimeFlush = Math.min(this.lastTimeFlush, v3.lastTimeUsed); - } - }); - if (!v2.size) { - delete v1.get(j); - v1.delete(j); - } - } - }); - if (!v1.size) { - delete this.data.get(i); - this.data.delete(i); - } - } - }); - - if (this.data.size == 0) { - this.lastTimeFlush = Date.now(); - } - } -} - -export default Cache; diff --git a/src/Core/Style.js b/src/Core/Style.js index 58fa90cfde..d17bac1622 100644 --- a/src/Core/Style.js +++ b/src/Core/Style.js @@ -1,4 +1,4 @@ -import Cache from 'Core/Scheduler/Cache'; +import { LRUCache } from 'lru-cache'; import Fetcher from 'Provider/Fetcher'; import { Color } from 'three'; import { deltaE } from 'Renderer/Color'; @@ -6,7 +6,7 @@ import Coordinates from 'Core/Geographic/Coordinates'; import itowns_stroke_single_before from './StyleChunk/itowns_stroke_single_before.css'; -const cacheStyle = new Cache(); +const cachedImg = new LRUCache({ max: 500 }); const matrix = document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGMatrix(); const canvas = document.createElement('canvas'); @@ -43,11 +43,12 @@ export function readExpression(property, ctx) { return property; } -async function loadImage(source) { - let promise = cacheStyle.get(source, 'null'); +async function loadImage(url) { + const imgUrl = url.split('?')[0]; + let promise = cachedImg.get(imgUrl); if (!promise) { - promise = Fetcher.texture(source, { crossOrigin: 'anonymous' }); - cacheStyle.set(promise, source, 'null'); + promise = Fetcher.texture(url, { crossOrigin: 'anonymous' }); + cachedImg.set(imgUrl, promise); } return (await promise).image; } @@ -70,7 +71,7 @@ function replaceWhitePxl(imgd, color, id) { if (!color) { return imgd; } - const imgdColored = cacheStyle.get(id, color); + const imgdColored = cachedImg.get(`${id}_${color}`); if (!imgdColored) { const pix = imgd.data; const newColor = new Color(color); @@ -81,7 +82,7 @@ function replaceWhitePxl(imgd, color, id) { pix[i + 1] = (pix[i + 1] * d + newColor.g * 255 * (1 - d)); pix[i + 2] = (pix[i + 2] * d + newColor.b * 255 * (1 - d)); } - cacheStyle.set(imgd, id, color); + cachedImg.set(`${id}_${color}`, imgd); return imgd; } return imgdColored; diff --git a/src/Core/TileGeometry.ts b/src/Core/TileGeometry.ts index e63f5a1ac7..f50414333b 100644 --- a/src/Core/TileGeometry.ts +++ b/src/Core/TileGeometry.ts @@ -5,7 +5,7 @@ import { computeBuffers, getBufferIndexSize } import { GpuBufferAttributes, TileBuilder, TileBuilderParams } from 'Core/Prefab/TileBuilder'; import Extent from 'Core/Geographic/Extent'; -import Cache from 'Core/Scheduler/Cache'; +import { LRUCache } from 'lru-cache'; import OBB from 'Renderer/OBB'; import Coordinates from './Geographic/Coordinates'; @@ -112,8 +112,8 @@ export class TileGeometry extends THREE.BufferGeometry { * @param keys - The [south, level, epsg] key of this geometry. */ public initRefCount( - cacheTile: Cache, - keys: [string, number, string], + cacheTile: LRUCache>, + key: string, ): void { if (this._refCount !== null) { return; @@ -131,7 +131,7 @@ export class TileGeometry extends THREE.BufferGeometry { // (in THREE.WebGLBindingStates code). this.index = null; delete this.attributes.uv; - cacheTile.delete(...keys); + cacheTile.delete(key); super.dispose(); // THREE.BufferGeometry.prototype.dispose.call(this); } diff --git a/src/Layer/Layer.js b/src/Layer/Layer.js index 8eb67b9f22..03420f99cb 100644 --- a/src/Layer/Layer.js +++ b/src/Layer/Layer.js @@ -2,7 +2,7 @@ import * as THREE from 'three'; import { STRATEGY_MIN_NETWORK_TRAFFIC } from 'Layer/LayerUpdateStrategy'; import InfoLayer from 'Layer/InfoLayer'; import Source from 'Source/Source'; -import Cache from 'Core/Scheduler/Cache'; +import { LRUCache } from 'lru-cache'; import Style from 'Core/Style'; /** @@ -198,7 +198,10 @@ class Layer extends THREE.EventDispatcher { /** * @type {Cache} */ - this.cache = new Cache(cacheLifeTime); + this.cache = new LRUCache({ + max: 500, + ...(cacheLifeTime !== Infinity && { ttl: cacheLifeTime }), + }); this.mergeFeatures = mergeFeatures; } @@ -271,14 +274,14 @@ class Layer extends THREE.EventDispatcher { } getData(from, to) { - const key = this.source.requestToKey(this.source.isVectorSource ? to : from); - let data = this.cache.getByArray(key); + const key = this.source.getDataKey(this.source.isVectorSource ? to : from); + let data = this.cache.get(key); if (!data) { data = this.source.loadData(from, this) .then(feat => this.convert(feat, to), (err) => { throw err; }); - this.cache.setByArray(data, key); + this.cache.set(key, data); } return data; } diff --git a/src/Source/FileSource.js b/src/Source/FileSource.js index b10538b06e..c7af033b3b 100644 --- a/src/Source/FileSource.js +++ b/src/Source/FileSource.js @@ -1,5 +1,5 @@ import Source from 'Source/Source'; -import Cache from 'Core/Scheduler/Cache'; +import { LRUCache } from 'lru-cache'; /** * An object defining the source of a single resource to get from a direct @@ -136,8 +136,8 @@ class FileSource extends Source { this.fetchedData = f; }); } else if (source.features) { - this._featuresCaches[source.features.crs] = new Cache(); - this._featuresCaches[source.features.crs].setByArray(Promise.resolve(source.features), [0]); + this._featuresCaches[source.features.crs] = new LRUCache({ max: 500 }); + this._featuresCaches[source.features.crs].set(0, Promise.resolve(source.features)); } this.whenReady.then(() => this.fetchedData); @@ -152,14 +152,14 @@ class FileSource extends Source { onLayerAdded(options) { options.in = this; super.onLayerAdded(options); - let features = this._featuresCaches[options.out.crs].getByArray([0]); + let features = this._featuresCaches[options.out.crs].get(0); if (!features) { options.out.buildExtent = this.crs != 'EPSG:4978'; if (options.out.buildExtent) { options.out.forcedExtentCrs = options.out.crs != 'EPSG:4978' ? options.out.crs : this.crs; } features = this.parser(this.fetchedData, options); - this._featuresCaches[options.out.crs].setByArray(features, [0]); + this._featuresCaches[options.out.crs].set(0, features); } features.then((data) => { if (data.extent) { @@ -181,7 +181,7 @@ class FileSource extends Source { * @return {FeatureCollection|Texture} The parsed data. */ loadData(extent, out) { - return this._featuresCaches[out.crs].getByArray([0]); + return this._featuresCaches[out.crs].get(0); } extentInsideLimit(extent) { diff --git a/src/Source/OrientedImageSource.js b/src/Source/OrientedImageSource.js index 01de7b4b9e..50dcbc4f19 100644 --- a/src/Source/OrientedImageSource.js +++ b/src/Source/OrientedImageSource.js @@ -42,8 +42,8 @@ class OrientedImageSource extends Source { return this.imageUrl(imageInfo.cameraId, imageInfo.panoId); } - requestToKey(image) { - return [image.cameraId, image.panoId]; + getDataKey(image) { + return `c${image.cameraId}p${image.panoId}`; } /** diff --git a/src/Source/Source.js b/src/Source/Source.js index 1e8f986c98..e1f4fdd4de 100644 --- a/src/Source/Source.js +++ b/src/Source/Source.js @@ -8,7 +8,8 @@ import GTXParser from 'Parser/GTXParser'; import ISGParser from 'Parser/ISGParser'; import VectorTileParser from 'Parser/VectorTileParser'; import Fetcher from 'Provider/Fetcher'; -import Cache from 'Core/Scheduler/Cache'; +// import Cache from 'Core/Scheduler/Cache'; +import { LRUCache } from 'lru-cache'; /** @private */ export const supportedParsers = new Map([ @@ -22,30 +23,8 @@ export const supportedParsers = new Map([ ['application/gdf', GDFParser.parse], ]); -const noCache = { getByArray: () => { }, setByArray: a => a, clear: () => { } }; +const noCache = { get: () => {}, set: a => a, clear: () => {} }; -/** - * @property {string} crs - data crs projection. - * @property {boolean} isInverted - This option is to be set to the - * correct value, true or false (default being false), if the computation of - * the coordinates needs to be inverted to same scheme as OSM, Google Maps - * or other system. See [this link]( - * https://alastaira.wordpress.com/2011/07/06/converting-tms-tile-coordinates-to-googlebingosm-tile-coordinates) - * for more informations. - * - */ -class InformationsData { - constructor(options) { - if (options.projection) { - console.warn('Source projection parameter is deprecated, use crs instead.'); - options.crs = options.crs || options.projection; - } - if (options.crs) { - CRS.isValid(options.crs); - } - this.crs = options.crs; - } -} /** * This interface describes parsing options. * @typedef {Object} ParsingOptions @@ -99,13 +78,20 @@ let uid = 0; * depending on the current fetched tile for example * */ -class Source extends InformationsData { +class Source { /** * @param {Object} source - An object that can contain all properties of a * Source. Only the `url` property is mandatory. */ constructor(source) { - super(source); + if (source.projection) { + console.warn('Source projection parameter is deprecated, use crs instead.'); + source.crs = source.crs || source.projection; + } + if (source.crs) { + CRS.isValid(source.crs); + } + this.crs = source.crs; this.isSource = true; if (!source.url) { @@ -148,8 +134,8 @@ class Source extends InformationsData { throw new Error('In extended Source, you have to implement the method urlFromExtent!'); } - requestToKey(extent) { - return [extent.zoom, extent.row, extent.col]; + getDataKey(extent) { + return `z${extent.zoom}r${extent.row}c${extent.col}`; } /** @@ -162,24 +148,17 @@ class Source extends InformationsData { */ loadData(extent, out) { const cache = this._featuresCaches[out.crs]; - const key = this.requestToKey(extent); + const key = this.getDataKey(extent); + // console.log('Source.loadData', key); // try to get parsed data from cache - let features = cache.getByArray(key); + let features = cache.get(key); if (!features) { // otherwise fetch/parse the data - features = cache.setByArray( - this.fetcher(this.urlFromExtent(extent), this.networkOptions) - .then(file => this.parser(file, { out, in: this, extent })) - .catch(err => this.handlingError(err)), - key); - - if (this.onParsedFile) { - features.then((feat) => { - this.onParsedFile(feat); - console.warn('Source.onParsedFile was deprecated'); - return feat; - }); - } + features = this.fetcher(this.urlFromExtent(extent), this.networkOptions) + .then(file => this.parser(file, { out, in: this, extent })) + .catch(err => this.handlingError(err)); + + cache.set(key, features); } return features; } @@ -195,7 +174,7 @@ class Source extends InformationsData { // Cache feature only if it's vector data, the feature are cached in source. // It's not necessary to cache raster in Source, // because it's already cached on layer. - this._featuresCaches[options.out.crs] = this.isVectorSource ? new Cache() : noCache; + this._featuresCaches[options.out.crs] = this.isVectorSource ? new LRUCache({ max: 500 }) : noCache; } } diff --git a/src/Source/VectorTilesSource.js b/src/Source/VectorTilesSource.js index 067db89796..18a9732af1 100644 --- a/src/Source/VectorTilesSource.js +++ b/src/Source/VectorTilesSource.js @@ -171,26 +171,18 @@ class VectorTilesSource extends TMSSource { loadData(extent, out) { const cache = this._featuresCaches[out.crs]; - const key = this.requestToKey(extent); + const key = this.getDataKey(extent); // try to get parsed data from cache - let features = cache.getByArray(key); + let features = cache.get(key); if (!features) { // otherwise fetch/parse the data - features = cache.setByArray( - Promise.all(this.urls.map(url => - this.fetcher(this.urlFromExtent(extent, url), this.networkOptions) - .then(file => this.parser(file, { out, in: this, extent })))) - .then(collections => mergeCollections(collections)) - .catch(err => this.handlingError(err)), - key); + features = Promise.all(this.urls.map(url => + this.fetcher(this.urlFromExtent(extent, url), this.networkOptions) + .then(file => this.parser(file, { out, in: this, extent })))) + .then(collections => mergeCollections(collections)) + .catch(err => this.handlingError(err)); - if (this.onParsedFile) { - features.then((feat) => { - this.onParsedFile(feat); - console.warn('Source.onParsedFile was deprecated'); - return feat; - }); - } + cache.set(key, features); } return features; } diff --git a/src/Source/WFSSource.js b/src/Source/WFSSource.js index fa4154f051..3098e5ceaa 100644 --- a/src/Source/WFSSource.js +++ b/src/Source/WFSSource.js @@ -159,11 +159,11 @@ class WFSSource extends Source { return super.handlingError(err); } - requestToKey(extent) { + getDataKey(extent) { if (extent.isTile) { - return super.requestToKey(extent); + return super.getDataKey(extent); } else { - return [extent.zoom, extent.south, extent.west]; + return `z${extent.zoom}s${extent.south}w${extent.west}`; } } diff --git a/test/unit/cache.js b/test/unit/cache.js deleted file mode 100644 index fb58eecad4..0000000000 --- a/test/unit/cache.js +++ /dev/null @@ -1,64 +0,0 @@ -import assert from 'assert'; -import Cache, { CACHE_POLICIES } from 'Core/Scheduler/Cache'; - -describe('Cache', function () { - const cache = new Cache(); - it('Instance Cache', function () { - assert.equal(CACHE_POLICIES.INFINITE, cache.lifeTime); - }); - - it('Set/Get value in Cache', function () { - const tag = [2, 0, 0]; - cache.set('a', 0, 0, 0); - cache.set('b', 1, 0, 0); - cache.setByArray('c', tag); - cache.set('d', 3, 0); - cache.set('e', 4); - assert.equal('c', cache.getByArray(tag)); - assert.equal('d', cache.get(3, 0)); - assert.equal('e', cache.get(4)); - }); - - it('delete value in Cache', function () { - cache.delete(0, 0, 0); - cache.delete(1, 0, 0); - cache.delete(2, 0, 0); - cache.delete(3, 0); - cache.delete(4); - - assert.equal(undefined, cache.get(0, 0, 0)); - assert.equal(undefined, cache.get(1, 0, 0)); - assert.equal(undefined, cache.get(2, 0, 0)); - assert.equal(undefined, cache.get(3, 0)); - assert.equal(undefined, cache.get(4)); - }); - - it('delete empty Map', function () { - cache.set('a', 0, 0, 0); - cache.set('b', 0, 0, 1); - cache.set('c', 0, 0, 2); - cache.delete(0, 0, 0); - cache.delete(0, 0, 1); - cache.delete(0, 0, 2); - assert.equal(undefined, cache.get(0, 0)); - assert.equal(cache.data.size, 0); - }); - - it('Clear Cache', function () { - cache.set('a', 0, 0, 0); - cache.set('b', 0, 0, 1); - cache.set('c', 0, 0, 2); - cache.clear(); - assert.equal(0, cache.data.size); - }); - - it('flush Cache', function () { - cache.set('a', 0, 0, 0); - cache.lifeTime = 0; - cache.lastTimeFlush = 0; - cache.data.get(0).get(0).get(0).lastTimeUsed = 0; - assert.equal(cache.data.size, 1); - cache.flush(10); - assert.equal(cache.data.size, 0); - }); -}); diff --git a/test/unit/dataSourceProvider.js b/test/unit/dataSourceProvider.js index 6bbbbb52fa..702e0b040f 100644 --- a/test/unit/dataSourceProvider.js +++ b/test/unit/dataSourceProvider.js @@ -46,8 +46,6 @@ describe('Provide in Sources', function () { let nodeLayerElevation; let featureLayer; - let featureCountByCb = 0; - // Mock scheduler const context = { view: { @@ -105,10 +103,6 @@ describe('Provide in Sources', function () { crs: 'EPSG:3857', }); - featureLayer.source.onParsedFile = (fc) => { - featureCountByCb = fc.features.length; - }; - planarlayer.attach(featureLayer); context.elevationLayers = [elevationlayer]; @@ -263,7 +257,7 @@ describe('Provide in Sources', function () { tile.parent = { pendingSubdivision: false }; featureLayer.source.uid = 8; featureLayer.mergeFeatures = true; - featureLayer.cache.data.clear(); + featureLayer.cache.clear(); featureLayer.source._featuresCaches = {}; featureLayer.source.onLayerAdded({ out: featureLayer }); featureLayer.update(context, featureLayer, tile); @@ -273,7 +267,6 @@ describe('Provide in Sources', function () { assert.ok(features[0].meshes.children[1].isPoints); assert.equal(features[0].meshes.children[0].children.length, 0); assert.equal(features[0].meshes.children[1].children.length, 0); - assert.equal(featureCountByCb, 2); done(); }).catch(done); }); diff --git a/test/unit/source.js b/test/unit/source.js deleted file mode 100644 index 9e9d5a9b22..0000000000 --- a/test/unit/source.js +++ /dev/null @@ -1,347 +0,0 @@ -import { Matrix4 } from 'three'; -import assert from 'assert'; -import Source from 'Source/Source'; -import Layer from 'Layer/Layer'; -import WFSSource from 'Source/WFSSource'; -import WMTSSource from 'Source/WMTSSource'; -import WMSSource from 'Source/WMSSource'; -import TMSSource from 'Source/TMSSource'; -import FileSource from 'Source/FileSource'; -import OrientedImageSource from 'Source/OrientedImageSource'; -import C3DTilesSource from 'Source/C3DTilesSource'; -import C3DTilesIonSource from 'Source/C3DTilesIonSource'; -import Extent from 'Core/Geographic/Extent'; -import Tile from 'Core/Tile/Tile'; -import sinon from 'sinon'; -import Fetcher from 'Provider/Fetcher'; - -import fileSource from '../data/filesource/featCollec_Polygone.geojson'; - -const tileset = {}; - -describe('Sources', function () { - const vendorSpecific = { - buffer: 4096, - format_options: 'dpi:300;quantizer:octree', - tiled: true, - }; - - describe('Source', function () { - const paramsSource = { - url: 'http://', - }; - - it('should instance and throw error for Source', function () { - const source = new Source(paramsSource); - assert.throws(source.urlFromExtent, Error); - assert.throws(source.extentInsideLimit, Error); - }); - - it('should throw an error for having no url', function () { - assert.throws(() => new Source({}), Error); - }); - }); - - describe('WFSSource', function () { - const paramsWFS = { - url: 'http://domain.com', - typeName: 'test', - crs: 'EPSG:4326', - }; - - it('should instance and use WFSSource', function () { - const source = new WFSSource(paramsWFS); - assert.ok(source.isWFSSource); - }); - - it('should throw an error for having no required parameters', function () { - assert.throws(() => new WFSSource({}), Error); - assert.throws(() => new WFSSource({ typeName: 'test' }), Error); - }); - - it('should use vendor specific parameters for the creation of the WFS url', function () { - paramsWFS.vendorSpecific = vendorSpecific; - const source = new WFSSource(paramsWFS); - const extent = new Extent('EPSG:4326', 0, 10, 0, 10); - const url = source.urlFromExtent(extent); - const end = '&buffer=4096&format_options=dpi:300;quantizer:octree&tiled=true'; - assert.ok(url.endsWith(end)); - }); - - it('should handles errors', function () { - const bce = console.error; - // Mute console error - console.error = () => {}; - const source = new WFSSource(paramsWFS); - assert.throws(() => source.handlingError(new Error('error'))); - console.error = bce; - }); - - it('should return keys from request', function () { - const source = new WFSSource(paramsWFS); - const tile = new Tile('EPSG:4326', 5, 10, 15); - const keys = source.requestToKey(tile); - assert.equal(tile.zoom, keys[0]); - assert.equal(tile.row, keys[1]); - assert.equal(tile.col, keys[2]); - const extentepsg = new Extent('EPSG:4326', 5.5, 10, 22.3, 89.34); - const keysepsg = source.requestToKey(extentepsg); - assert.equal(extentepsg.south, keysepsg[1]); - assert.equal(extentepsg.west, keysepsg[2]); - }); - }); - - describe('WMTSSource', function () { - const paramsWMTS = { - url: 'http://domain.com', - name: 'name', - crs: 'EPSG:4326', - tileMatrixSet: 'PM', - }; - - it('should throw an error for having no name', function () { - assert.throws(() => new WMTSSource({}), Error); - }); - - it('should instance and use WMTSSource', function () { - const source = new WMTSSource(paramsWMTS); - const extent = new Tile('EPSG:3857', 5, 0, 0); - assert.ok(source.isWMTSSource); - assert.ok(source.urlFromExtent(extent)); - assert.ok(source.extentInsideLimit(extent, 5)); - }); - - it('should instance with tileMatrixSet', function () { - paramsWMTS.tileMatrixSet = 'PM'; - paramsWMTS.tileMatrixSetLimits = { - 0: { minTileRow: 0, maxTileRow: 1, minTileCol: 0, maxTileCol: 1 }, - 1: { minTileRow: 0, maxTileRow: 2, minTileCol: 0, maxTileCol: 2 }, - 2: { minTileRow: 0, maxTileRow: 4, minTileCol: 0, maxTileCol: 4 }, - 3: { minTileRow: 0, maxTileRow: 8, minTileCol: 0, maxTileCol: 8 }, - 4: { minTileRow: 0, maxTileRow: 16, minTileCol: 0, maxTileCol: 16 }, - 5: { minTileRow: 0, maxTileRow: 32, minTileCol: 0, maxTileCol: 32 }, - }; - const source = new WMTSSource(paramsWMTS); - const extent = new Tile('EPSG:3857', 5, 0, 0); - source.onLayerAdded({ out: { crs: 'EPSG:4326' } }); - assert.ok(source.isWMTSSource); - assert.ok(source.urlFromExtent(extent)); - assert.ok(source.extentInsideLimit(extent, 5)); - }); - - it('should use vendor specific parameters for the creation of the WMTS url', function () { - paramsWMTS.vendorSpecific = vendorSpecific; - const source = new WMTSSource(paramsWMTS); - const tile = new Tile('EPSG:4326', 0, 10, 0); - const url = source.urlFromExtent(tile); - const end = '&buffer=4096&format_options=dpi:300;quantizer:octree&tiled=true'; - assert.ok(url.endsWith(end)); - }); - }); - - describe('WMSSource', function () { - const paramsWMS = { - url: 'http://domain.com', - name: 'name', - extent: [-90, 90, -45, 45], - crs: 'EPSG:4326', - }; - - it('should instance and use WMSSource', function () { - const source = new WMSSource(paramsWMS); - const extent = new Extent('EPSG:4326', 0, 10, 0, 10); - assert.ok(source.isWMSSource); - assert.ok(source.urlFromExtent(extent)); - assert.ok(source.extentInsideLimit(extent)); - }); - - it('should set the correct axisOrder', function () { - paramsWMS.crs = 'EPSG:3857'; - const source = new WMSSource(paramsWMS); - assert.strictEqual(source.axisOrder, 'wsen'); - paramsWMS.crs = 'EPSG:4326'; - }); - - it('should use vendor specific parameters for the creation of the WMS url', function () { - paramsWMS.vendorSpecific = vendorSpecific; - const source = new WMSSource(paramsWMS); - const extent = new Extent('EPSG:4326', 0, 10, 0, 10); - const url = source.urlFromExtent(extent); - const end = '&buffer=4096&format_options=dpi:300;quantizer:octree&tiled=true'; - assert.ok(url.endsWith(end)); - }); - }); - - describe('OrientedImageSource', function () { - it('instance OrientedImageSource', function (done) { - const source = new OrientedImageSource({ url: 'http://source.test' }); - source.whenReady - .then((a) => { - assert.equal(Object.keys(a).length, 2); - done(); - }).catch(done); - }); - - it('should return keys OrientedImageSource from request', function () { - const source = new OrientedImageSource({ url: 'http://source.test' }); - const image = { cameraId: 5, panoId: 10 }; - const keys = source.requestToKey(image); - assert.equal(image.cameraId, keys[0]); - assert.equal(image.panoId, keys[1]); - }); - }); - - describe('TMSSource', function () { - const paramsTMS = { - url: 'http://', - crs: 'EPSG:3857', - tileMatrixSetLimits: { - 5: { minTileRow: 0, maxTileRow: 32, minTileCol: 0, maxTileCol: 32 }, - }, - }; - - it('should instance and use TMSSource', function () { - const source = new TMSSource(paramsTMS); - source.onLayerAdded({ out: { crs: 'EPSG:4326' } }); - const extent = new Tile('EPSG:3857', 5, 0, 0); - assert.ok(source.isTMSSource); - assert.ok(source.urlFromExtent(extent)); - assert.ok(source.extentInsideLimit(extent, extent.zoom)); - }); - }); - - let fetchedData; - - describe('FileSource', function () { - let stubFetcherJson; - before(function () { - stubFetcherJson = sinon.stub(Fetcher, 'json') - .callsFake(() => Promise.resolve(JSON.parse(fileSource))); - }); - - after(function () { - stubFetcherJson.restore(); - }); - - it('should instance FileSource with no source.fetchedData', function _it(done) { - const urlFilesource = 'https://raw.githubusercontent.com/gregoiredavid/france-geojson/master/departements/09-ariege/departement-09-ariege.geojson'; - const source = new FileSource({ - url: urlFilesource, - crs: 'EPSG:4326', - format: 'application/json', - extent: new Extent('EPSG:4326', 0, 20, 0, 20), - zoom: { min: 0, max: 21 }, - }); - - source.whenReady - .then(() => { - const extent = new Extent('EPSG:4326', 0, 10, 0, 10); - assert.ok(source.urlFromExtent()); - assert.ok(source.extentInsideLimit(extent)); - assert.ok(source.fetchedData); - assert.ok(!source.features); - assert.ok(source.isFileSource); - fetchedData = source.fetchedData; - assert.equal(fetchedData.features[0].properties.nom, 'Ariège_simplified'); - done(); - }).catch(done); - }); - - it('should instance FileSource with source.fetchedData and parse data with a layer', function (done) { - // TO DO need cleareance: what is this test for ? - // - testing instanceation Filesource when fetchedData and source.feature is already available ? - // - testing instanciate Layer ? - // - testing source.onLayerAdded ? - // - testing souce.loadData ? - const source = new FileSource({ - fetchedData, - format: 'application/json', - crs: 'EPSG:4326', - }); - - assert.ok(!source.features); - assert.equal(source.urlFromExtent(), 'none'); - assert.ok(source.fetchedData); - assert.ok(source.isFileSource); - - const layer = new Layer('09-ariege', { crs: 'EPSG:4326', source, structure: '2d' }); - layer.source.onLayerAdded({ out: layer }); - - layer.whenReady - .then(() => { - source.loadData([], layer) - .then((featureCollection) => { - assert.equal(featureCollection.features[0].vertices.length, 16); - done(); - }) - .catch((err) => { - done(err); - }); - }); - layer._resolve(); - }); - - it('should instance and use FileSource with features', function () { - const extent = new Extent('EPSG:4326', 0, 10, 0, 10); - const source = new FileSource({ - features: { foo: 'bar', crs: 'EPSG:4326', extent, matrixWorld: new Matrix4() }, - crs: 'EPSG:4326', - format: 'application/json', - }); - source.onLayerAdded({ out: { crs: source.crs } }); - assert.ok(source.urlFromExtent(extent).startsWith('none')); - assert.ok(!source.fetchedData); - - assert.ok(source.isFileSource); - }); - - it('should throw an error for having no required parameters', function () { - assert.throws(() => new FileSource({}), Error); - assert.throws(() => new FileSource({ crs: 'EPSG:4326' }), Error); - }); - - it('should set the crs projection from features', function () { - const source = new FileSource({ - features: { crs: 'EPSG:4326' }, - format: 'application/json', - }); - assert.strictEqual(source.crs, 'EPSG:4326'); - }); - }); - - describe('C3DTilesSource', function () { - let stubFetcherJson; - before(function () { - stubFetcherJson = sinon.stub(Fetcher, 'json') - .callsFake(() => Promise.resolve(tileset)); - }); - after(function () { - stubFetcherJson.restore(); - }); - - it('should throw an error for having no required parameters', function () { - assert.throws(() => new C3DTilesSource({}), Error); - }); - - it('should instance C3DTilesSource', function (done) { - const url3dTileset = 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/' + - '3DTiles/lyon_1_4978/tileset.json'; - const source = new C3DTilesSource({ url: url3dTileset }); - source.whenReady - .then(() => { - assert.ok(source.isC3DTilesSource); - assert.strictEqual(source.url, url3dTileset); - assert.strictEqual(source.baseUrl, url3dTileset.slice(0, url3dTileset.lastIndexOf('/') + 1)); - done(); - }).catch(done); - }); - }); - - describe('C3DTilesIonSource', function () { - it('should throw an error for having no required parameters', function () { - assert.throws(() => new C3DTilesIonSource({}), Error); - assert.throws(() => new C3DTilesIonSource({ accessToken: 'free-3d-tiles' }), Error); - assert.throws(() => new C3DTilesIonSource({ assetId: '66666' }), Error); - }); - }); -}); diff --git a/test/unit/source/C3DTilesSource.js b/test/unit/source/C3DTilesSource.js new file mode 100644 index 0000000000..8c551e2448 --- /dev/null +++ b/test/unit/source/C3DTilesSource.js @@ -0,0 +1,44 @@ +import assert from 'assert'; +import C3DTilesSource from 'Source/C3DTilesSource'; +import C3DTilesIonSource from 'Source/C3DTilesIonSource'; +import sinon from 'sinon'; +import Fetcher from 'Provider/Fetcher'; + +const tileset = {}; + +describe('C3DTilesSource', function () { + let stubFetcherJson; + before(function () { + stubFetcherJson = sinon.stub(Fetcher, 'json') + .callsFake(() => Promise.resolve(tileset)); + }); + after(function () { + stubFetcherJson.restore(); + }); + + it('should throw an error for having no required parameters', function () { + assert.throws(() => new C3DTilesSource({}), Error); + }); + + it('should instance C3DTilesSource', function (done) { + const url3dTileset = 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/' + + '3DTiles/lyon_1_4978/tileset.json'; + const source = new C3DTilesSource({ url: url3dTileset }); + source.whenReady + .then(() => { + assert.ok(source.isC3DTilesSource); + assert.strictEqual(source.url, url3dTileset); + assert.strictEqual(source.baseUrl, url3dTileset.slice(0, url3dTileset.lastIndexOf('/') + 1)); + done(); + }).catch(done); + }); + + describe('C3DTilesIonSource', function () { + it('should throw an error for having no required parameters', function () { + assert.throws(() => new C3DTilesIonSource({}), Error); + assert.throws(() => new C3DTilesIonSource({ accessToken: 'free-3d-tiles' }), Error); + assert.throws(() => new C3DTilesIonSource({ assetId: '66666' }), Error); + }); + }); +}); + diff --git a/test/unit/source/filesource.js b/test/unit/source/filesource.js new file mode 100644 index 0000000000..784cc34f28 --- /dev/null +++ b/test/unit/source/filesource.js @@ -0,0 +1,108 @@ +import { Matrix4 } from 'three'; +import assert from 'assert'; +import Layer from 'Layer/Layer'; +import FileSource from 'Source/FileSource'; +import Extent from 'Core/Geographic/Extent'; +import sinon from 'sinon'; +import Fetcher from 'Provider/Fetcher'; + +import fileSource from '../../data/filesource/featCollec_Polygone.geojson'; + +let fetchedData; + +describe('FileSource', function () { + let stubFetcherJson; + before(function () { + stubFetcherJson = sinon.stub(Fetcher, 'json') + .callsFake(() => Promise.resolve(JSON.parse(fileSource))); + }); + + after(function () { + stubFetcherJson.restore(); + }); + + it('should instance FileSource with no source.fetchedData', function _it(done) { + const urlFilesource = 'https://raw.githubusercontent.com/gregoiredavid/france-geojson/master/departements/09-ariege/departement-09-ariege.geojson'; + const source = new FileSource({ + url: urlFilesource, + crs: 'EPSG:4326', + format: 'application/json', + extent: new Extent('EPSG:4326', 0, 20, 0, 20), + zoom: { min: 0, max: 21 }, + }); + + source.whenReady + .then(() => { + const extent = new Extent('EPSG:4326', 0, 10, 0, 10); + assert.ok(source.urlFromExtent()); + assert.ok(source.extentInsideLimit(extent)); + assert.ok(source.fetchedData); + assert.ok(!source.features); + assert.ok(source.isFileSource); + fetchedData = source.fetchedData; + assert.equal(fetchedData.features[0].properties.nom, 'Ariège_simplified'); + done(); + }).catch(done); + }); + + it('should instance FileSource with source.fetchedData and parse data with a layer', function (done) { + // TO DO need cleareance: what is this test for ? + // - testing instanceation Filesource when fetchedData and source.feature is already available ? + // - testing instanciate Layer ? + // - testing source.onLayerAdded ? + // - testing souce.loadData ? + const source = new FileSource({ + fetchedData, + format: 'application/json', + crs: 'EPSG:4326', + }); + + assert.ok(!source.features); + assert.equal(source.urlFromExtent(), 'none'); + assert.ok(source.fetchedData); + assert.ok(source.isFileSource); + + const layer = new Layer('09-ariege', { crs: 'EPSG:4326', source, structure: '2d' }); + layer.source.onLayerAdded({ out: layer }); + + layer.whenReady + .then(() => { + source.loadData([], layer) + .then((featureCollection) => { + assert.equal(featureCollection.features[0].vertices.length, 16); + done(); + }) + .catch((err) => { + done(err); + }); + }); + layer._resolve(); + }); + + it('should instance and use FileSource with features', function () { + const extent = new Extent('EPSG:4326', 0, 10, 0, 10); + const source = new FileSource({ + features: { foo: 'bar', crs: 'EPSG:4326', extent, matrixWorld: new Matrix4() }, + crs: 'EPSG:4326', + format: 'application/json', + }); + source.onLayerAdded({ out: { crs: source.crs } }); + assert.ok(source.urlFromExtent(extent).startsWith('none')); + assert.ok(!source.fetchedData); + + assert.ok(source.isFileSource); + }); + + it('should throw an error for having no required parameters', function () { + assert.throws(() => new FileSource({}), Error); + assert.throws(() => new FileSource({ crs: 'EPSG:4326' }), Error); + }); + + it('should set the crs projection from features', function () { + const source = new FileSource({ + features: { crs: 'EPSG:4326' }, + format: 'application/json', + }); + assert.strictEqual(source.crs, 'EPSG:4326'); + }); +}); diff --git a/test/unit/source/orientedimagesource.js b/test/unit/source/orientedimagesource.js new file mode 100644 index 0000000000..696f9d83fc --- /dev/null +++ b/test/unit/source/orientedimagesource.js @@ -0,0 +1,21 @@ +import assert from 'assert'; +import OrientedImageSource from 'Source/OrientedImageSource'; + +describe('OrientedImageSource', function () { + it('instance OrientedImageSource', function (done) { + const source = new OrientedImageSource({ url: 'http://source.test' }); + source.whenReady + .then((a) => { + assert.equal(Object.keys(a).length, 2); + done(); + }).catch(done); + }); + + it('should return keys OrientedImageSource from request', function () { + const source = new OrientedImageSource({ url: 'http://source.test' }); + const image = { cameraId: 5, panoId: 10 }; + const key = source.getDataKey(image); + assert.equal(key, `c${image.cameraId}p${image.panoId}`); + }); +}); + diff --git a/test/unit/source/source.js b/test/unit/source/source.js new file mode 100644 index 0000000000..524ca807bb --- /dev/null +++ b/test/unit/source/source.js @@ -0,0 +1,37 @@ +import assert from 'assert'; +import Source from 'Source/Source'; + +describe('Abstract Source', function () { + const paramsSource = { + url: 'http://', + }; + describe('Instancing of a Source', function () { + let source; + it('should throw an error for having no url', function () { + assert.throws(() => new Source({}), Error); + }); + it('should succeed', function () { + source = new Source(paramsSource); + assert.ok(source.isSource); + }); + it('testing deprecated options', function () { + paramsSource.projection = 'EPSG:4326'; + const source = new Source(paramsSource); + assert.ok(source.isSource); + assert.equal(source.crs, paramsSource.projection); + }); + + it('testing abstract methods', function () { + assert.throws(source.urlFromExtent, Error); + assert.throws(source.extentInsideLimit, Error); + }); + + it("method 'onLayerRemoved'", function () { + const mockedCache = { get: () => {}, set: a => a, clear: () => {} }; + const unusedCrs = 'unusedCrs'; + source._featuresCaches[unusedCrs] = mockedCache; + source.onLayerRemoved({ unusedCrs }); + assert.equal(source._featuresCaches[unusedCrs], undefined); + }); + }); +}); diff --git a/test/unit/source/tmssource.js b/test/unit/source/tmssource.js new file mode 100644 index 0000000000..8803c8a309 --- /dev/null +++ b/test/unit/source/tmssource.js @@ -0,0 +1,23 @@ +import assert from 'assert'; +import TMSSource from 'Source/TMSSource'; +import Tile from 'Core/Tile/Tile'; + +describe('TMSSource', function () { + const paramsTMS = { + url: 'http://', + crs: 'EPSG:3857', + tileMatrixSetLimits: { + 5: { minTileRow: 0, maxTileRow: 32, minTileCol: 0, maxTileCol: 32 }, + }, + }; + + it('should instance and use TMSSource', function () { + const source = new TMSSource(paramsTMS); + source.onLayerAdded({ out: { crs: 'EPSG:4326' } }); + const extent = new Tile('TMS:3857', 5, 0, 0); + assert.ok(source.isTMSSource); + assert.ok(source.urlFromExtent(extent)); + assert.ok(source.extentInsideLimit(extent, extent.zoom)); + }); +}); + diff --git a/test/unit/source/wfssource.js b/test/unit/source/wfssource.js new file mode 100644 index 0000000000..f5f00464cf --- /dev/null +++ b/test/unit/source/wfssource.js @@ -0,0 +1,57 @@ +import assert from 'assert'; +import WFSSource from 'Source/WFSSource'; + +import Extent from 'Core/Geographic/Extent'; +import Tile from 'Core/Tile/Tile'; + +describe('WFSSource', function () { + const paramsWFS = { + url: 'http://domain.com', + typeName: 'test', + crs: 'EPSG:4326', + }; + const vendorSpecific = { + buffer: 4096, + format_options: 'dpi:300;quantizer:octree', + tiled: true, + }; + + it('should instance and use WFSSource', function () { + const source = new WFSSource(paramsWFS); + assert.ok(source.isWFSSource); + }); + + it('should throw an error for having no required parameters', function () { + assert.throws(() => new WFSSource({}), Error); + assert.throws(() => new WFSSource({ typeName: 'test' }), Error); + }); + + it('should use vendor specific parameters for the creation of the WFS url', function () { + paramsWFS.vendorSpecific = vendorSpecific; + const source = new WFSSource(paramsWFS); + const extent = new Extent('EPSG:4326', 0, 10, 0, 10); + const url = source.urlFromExtent(extent); + const end = '&buffer=4096&format_options=dpi:300;quantizer:octree&tiled=true'; + assert.ok(url.endsWith(end)); + }); + + it('should handles errors', function () { + const bce = console.error; + // Mute console error + console.error = () => {}; + const source = new WFSSource(paramsWFS); + assert.throws(() => source.handlingError(new Error('error'))); + console.error = bce; + }); + + it('should return keys from request', function () { + const source = new WFSSource(paramsWFS); + const tile = new Tile('TMS:4326', 5, 10, 15); + const key = source.getDataKey(tile); + assert.equal(key, `z${tile.zoom}r${tile.row}c${tile.col}`); + const extent = new Extent('EPSG:4326', 5.5, 10, 22.3, 89.34); + const keysepsg = source.getDataKey(extent); + assert.equal(keysepsg, `z${extent.zoom}s${extent.south}w${extent.west}`); + }); +}); + diff --git a/test/unit/source/wmssource.js b/test/unit/source/wmssource.js new file mode 100644 index 0000000000..b7ea44dc34 --- /dev/null +++ b/test/unit/source/wmssource.js @@ -0,0 +1,42 @@ +import assert from 'assert'; +import WMSSource from 'Source/WMSSource'; +import Extent from 'Core/Geographic/Extent'; + +describe('WMSSource', function () { + const paramsWMS = { + url: 'http://domain.com', + name: 'name', + extent: [-90, 90, -45, 45], + crs: 'EPSG:4326', + }; + const vendorSpecific = { + buffer: 4096, + format_options: 'dpi:300;quantizer:octree', + tiled: true, + }; + + it('should instance and use WMSSource', function () { + const source = new WMSSource(paramsWMS); + const extent = new Extent('EPSG:4326', 0, 10, 0, 10); + assert.ok(source.isWMSSource); + assert.ok(source.urlFromExtent(extent)); + assert.ok(source.extentInsideLimit(extent)); + }); + + it('should set the correct axisOrder', function () { + paramsWMS.crs = 'EPSG:3857'; + const source = new WMSSource(paramsWMS); + assert.strictEqual(source.axisOrder, 'wsen'); + paramsWMS.crs = 'EPSG:4326'; + }); + + it('should use vendor specific parameters for the creation of the WMS url', function () { + paramsWMS.vendorSpecific = vendorSpecific; + const source = new WMSSource(paramsWMS); + const extent = new Extent('EPSG:4326', 0, 10, 0, 10); + const url = source.urlFromExtent(extent); + const end = '&buffer=4096&format_options=dpi:300;quantizer:octree&tiled=true'; + assert.ok(url.endsWith(end)); + }); +}); + diff --git a/test/unit/source/wmtssource.js b/test/unit/source/wmtssource.js new file mode 100644 index 0000000000..5839ce6078 --- /dev/null +++ b/test/unit/source/wmtssource.js @@ -0,0 +1,58 @@ +import assert from 'assert'; +import WMTSSource from 'Source/WMTSSource'; + +import Tile from 'Core/Tile/Tile'; + +describe('WMTSSource', function () { + const paramsWMTS = { + url: 'http://domain.com', + name: 'name', + crs: 'EPSG:4326', + tileMatrixSet: 'PM', + }; + const vendorSpecific = { + buffer: 4096, + format_options: 'dpi:300;quantizer:octree', + tiled: true, + }; + + it('should throw an error for having no name', function () { + assert.throws(() => new WMTSSource({}), Error); + }); + + it('should instance and use WMTSSource', function () { + const source = new WMTSSource(paramsWMTS); + const extent = new Tile('TMS:3857', 5, 0, 0); + assert.ok(source.isWMTSSource); + assert.ok(source.urlFromExtent(extent)); + assert.ok(source.extentInsideLimit(extent, 5)); + }); + + it('should instance with tileMatrixSet', function () { + paramsWMTS.tileMatrixSet = 'PM'; + paramsWMTS.tileMatrixSetLimits = { + 0: { minTileRow: 0, maxTileRow: 1, minTileCol: 0, maxTileCol: 1 }, + 1: { minTileRow: 0, maxTileRow: 2, minTileCol: 0, maxTileCol: 2 }, + 2: { minTileRow: 0, maxTileRow: 4, minTileCol: 0, maxTileCol: 4 }, + 3: { minTileRow: 0, maxTileRow: 8, minTileCol: 0, maxTileCol: 8 }, + 4: { minTileRow: 0, maxTileRow: 16, minTileCol: 0, maxTileCol: 16 }, + 5: { minTileRow: 0, maxTileRow: 32, minTileCol: 0, maxTileCol: 32 }, + }; + const source = new WMTSSource(paramsWMTS); + const extent = new Tile('TMS:3857', 5, 0, 0); + source.onLayerAdded({ out: { crs: 'EPSG:4326' } }); + assert.ok(source.isWMTSSource); + assert.ok(source.urlFromExtent(extent)); + assert.ok(source.extentInsideLimit(extent, 5)); + }); + + it('should use vendor specific parameters for the creation of the WMTS url', function () { + paramsWMTS.vendorSpecific = vendorSpecific; + const source = new WMTSSource(paramsWMTS); + const tile = new Tile('TMS:4326', 0, 10, 0); + const url = source.urlFromExtent(tile); + const end = '&buffer=4096&format_options=dpi:300;quantizer:octree&tiled=true'; + assert.ok(url.endsWith(end)); + }); +}); + diff --git a/test/unit/sources.js b/test/unit/sources.js new file mode 100644 index 0000000000..a1dfe78492 --- /dev/null +++ b/test/unit/sources.js @@ -0,0 +1,11 @@ +import './source/source'; +import './source/filesource'; + +import './source/tmssource'; +import './source/wfssource'; +import './source/wmssource'; +import './source/wmtssource'; + +import './source/orientedimagesource'; + +import './source/C3DTilesSource'; diff --git a/tsconfig.json b/tsconfig.json index 63841f2d26..19374e5ca3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "*": [ "src/*" ] }, "module": "ESNext", + "moduleResolution": "bundler", /* Emit */ "declaration": true, "emitDeclarationOnly": true,