Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

replace Cache by Lru cache #2451

Merged
merged 1 commit into from
Jan 31, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
refactor(cache): use LRUCache instead of own cache
BREAKING CHANGE:
- remove Source#onParsedFile callback
ftoromanoff authored and Desplandis committed Jan 31, 2025
commit cf0fba22b0ee34caf2a95ec1265a72d61b6173a1
34 changes: 20 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
3 changes: 0 additions & 3 deletions src/Core/MainLoop.js
Original file line number Diff line number Diff line change
@@ -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);
}
11 changes: 6 additions & 5 deletions src/Core/Prefab/TileBuilder.ts
Original file line number Diff line number Diff line change
@@ -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<string, {
index: THREE.BufferAttribute,
uv: THREE.BufferAttribute,
}>();
const cacheTile = new Cache();
const cacheTile = new LRUCache<string, Promise<TileGeometry>>({ 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 });
241 changes: 0 additions & 241 deletions src/Core/Scheduler/Cache.js
Original file line number Diff line number Diff line change
@@ -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;
17 changes: 9 additions & 8 deletions src/Core/Style.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
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';
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;
8 changes: 4 additions & 4 deletions src/Core/TileGeometry.ts
Original file line number Diff line number Diff line change
@@ -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<string, Promise<TileGeometry>>,
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);
}
13 changes: 8 additions & 5 deletions src/Layer/Layer.js
Original file line number Diff line number Diff line change
@@ -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;
}
12 changes: 6 additions & 6 deletions src/Source/FileSource.js
Original file line number Diff line number Diff line change
@@ -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) {
4 changes: 2 additions & 2 deletions src/Source/OrientedImageSource.js
Original file line number Diff line number Diff line change
@@ -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}`;
}

/**
67 changes: 23 additions & 44 deletions src/Source/Source.js
Original file line number Diff line number Diff line change
@@ -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</li>
* </ul>
*/
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;
}
}

24 changes: 8 additions & 16 deletions src/Source/VectorTilesSource.js
Original file line number Diff line number Diff line change
@@ -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;
}
6 changes: 3 additions & 3 deletions src/Source/WFSSource.js
Original file line number Diff line number Diff line change
@@ -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}`;
}
}

64 changes: 0 additions & 64 deletions test/unit/cache.js

This file was deleted.

9 changes: 1 addition & 8 deletions test/unit/dataSourceProvider.js
Original file line number Diff line number Diff line change
@@ -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);
});
347 changes: 0 additions & 347 deletions test/unit/source.js

This file was deleted.

44 changes: 44 additions & 0 deletions test/unit/source/C3DTilesSource.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});

108 changes: 108 additions & 0 deletions test/unit/source/filesource.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
21 changes: 21 additions & 0 deletions test/unit/source/orientedimagesource.js
Original file line number Diff line number Diff line change
@@ -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}`);
});
});

37 changes: 37 additions & 0 deletions test/unit/source/source.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
23 changes: 23 additions & 0 deletions test/unit/source/tmssource.js
Original file line number Diff line number Diff line change
@@ -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));
});
});

57 changes: 57 additions & 0 deletions test/unit/source/wfssource.js
Original file line number Diff line number Diff line change
@@ -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}`);
});
});

42 changes: 42 additions & 0 deletions test/unit/source/wmssource.js
Original file line number Diff line number Diff line change
@@ -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));
});
});

58 changes: 58 additions & 0 deletions test/unit/source/wmtssource.js
Original file line number Diff line number Diff line change
@@ -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));
});
});

11 changes: 11 additions & 0 deletions test/unit/sources.js
Original file line number Diff line number Diff line change
@@ -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';
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@
"*": [ "src/*" ]
},
"module": "ESNext",
"moduleResolution": "bundler",
/* Emit */
"declaration": true,
"emitDeclarationOnly": true,