diff --git a/packages/config/src/memory/__tests__/memory.config.test.ts b/packages/config/src/memory/__tests__/memory.config.test.ts index e07d54389..06f4b9711 100644 --- a/packages/config/src/memory/__tests__/memory.config.test.ts +++ b/packages/config/src/memory/__tests__/memory.config.test.ts @@ -3,31 +3,36 @@ import { BaseConfig } from '../../config/base.js'; import { ConfigImagery } from '../../config/imagery.js'; import { ConfigTileSetRaster } from '../../config/tile.set.js'; import { ConfigProviderMemory } from '../memory.config.js'; +import { ulid } from 'ulid'; +import timers from 'node:timers/promises'; o.spec('MemoryConfig', () => { const config = new ConfigProviderMemory(); o.beforeEach(() => config.objects.clear()); + const id = ulid(); + const imId = `im_${id}`; + const tsId = `ts_${id}`; - const baseImg = { id: 'im_Image123', name: 'ōtorohanga_urban_2021_0-1m_RGB', projection: 3857 } as ConfigImagery; - const baseTs = { id: 'ts_TileSet123', description: 'tileset' } as ConfigTileSetRaster; + const baseImg = { id: imId, name: 'ōtorohanga_urban_2021_0-1m_RGB', projection: 3857 } as ConfigImagery; + const baseTs = { id: tsId, description: 'tileset' } as ConfigTileSetRaster; o('should load correct objects from memory', async () => { config.put(baseTs); config.put(baseImg); - const img = await config.Imagery.get('Image123'); - o(img?.id).equals('im_Image123'); + const img = await config.Imagery.get(imId); + o(img?.id).equals(imId); - const ts = await config.TileSet.get('TileSet123'); - o(ts?.id).equals('ts_TileSet123'); + const ts = await config.TileSet.get(tsId); + o(ts?.id).equals(tsId); o(ts?.description).equals('tileset'); }); o('should support prefixed keys', async () => { config.put(baseImg); - const img = await config.Imagery.get('im_Image123'); - o(img?.id).equals('im_Image123'); + const img = await config.Imagery.get(imId); + o(img?.id).equals(imId); }); o('should not find objects', async () => { @@ -64,14 +69,20 @@ o.spec('MemoryConfig', () => { config.createVirtualTileSets(); const cfg = config.toJson(); - o(cfg.tileSet.length).equals(2); - o(cfg.tileSet[0].id).equals('ts_Image123'); + o(cfg.tileSet.length).equals(3); + o(cfg.tileSet[0].id).equals(tsId); o(cfg.tileSet[1].id).equals('ts_ōtorohanga-urban-2021-0.1m'); + o(cfg.tileSet[2].id).equals('ts_all'); + const allTileSet = cfg.tileSet[2]; + o(allTileSet.layers.length).equals(1); + o(allTileSet.layers[0].name).equals('ōtorohanga-urban-2021-0.1m'); }); + const newId = ulid(); + const newImId = `im_${newId}`; o('should create virtual tilesets by name', async () => { config.put(baseImg); - config.put({ ...baseImg, projection: 2193, id: 'im_Image234' } as ConfigImagery); + config.put({ ...baseImg, projection: 2193, id: newImId } as ConfigImagery); o(config.toJson().tileSet.length).equals(0); config.createVirtualTileSets(); @@ -79,13 +90,13 @@ o.spec('MemoryConfig', () => { const target = await config.TileSet.get('ōtorohanga-urban-2021-0.1m'); o(target?.layers.length).equals(1); o(target?.layers[0][3857]).equals(baseImg.id); - o(target?.layers[0][2193]).equals('im_Image234'); + o(target?.layers[0][2193]).equals(newImId); o(target?.name).equals('ōtorohanga-urban-2021-0.1m'); }); o('virtual tilesets can be called multiple times', () => { config.put(baseImg); - config.put({ ...baseImg, projection: 2193, id: 'im_Image234' } as ConfigImagery); + config.put({ ...baseImg, projection: 2193, id: newImId } as ConfigImagery); config.createVirtualTileSets(); config.createVirtualTileSets(); @@ -94,15 +105,21 @@ o.spec('MemoryConfig', () => { const cfg = config.toJson(); // 1 tileset per imagery id (2x) // 1 tileset per imagery name (1x) - o(cfg.tileSet.length).equals(3); - o(cfg.tileSet[0].id).equals('ts_Image123'); + o(cfg.tileSet.length).equals(4); + o(cfg.tileSet[0].id).equals(tsId); o(cfg.tileSet[1].id).equals('ts_ōtorohanga-urban-2021-0.1m'); - o(cfg.tileSet[2].id).equals('ts_Image234'); + o(cfg.tileSet[2].id).equals(`ts_${newId}`); + o(cfg.tileSet[3].id).equals('ts_all'); + o(cfg.tileSet[3].layers.length).equals(1); + o(cfg.tileSet[3].layers[0][2193]).equals(newImId); + o(cfg.tileSet[3].layers[0][3857]).equals(imId); + o(cfg.tileSet[3].layers[0].maxZoom).equals(undefined); + o(cfg.tileSet[3].layers[0].minZoom).equals(32); }); o('virtual tilesets should overwrite existing projections', async () => { config.put(baseImg); - config.put({ ...baseImg, id: 'im_Image234' } as ConfigImagery); + config.put({ ...baseImg, id: newImId } as ConfigImagery); o(config.toJson().tileSet.length).equals(0); @@ -110,12 +127,14 @@ o.spec('MemoryConfig', () => { const target = await config.TileSet.get('ts_ōtorohanga-urban-2021-0.1m'); o(target?.layers.length).equals(1); - o(target?.layers[0][3857]).equals('im_Image234'); + o(target?.layers[0][3857]).equals(newImId); o(target?.layers[0][2193]).equals(undefined); o(target?.name).equals('ōtorohanga-urban-2021-0.1m'); }); o('virtual tilesets should be created with `:`', async () => { + const idA = ulid(); + const idB = ulid(); config.objects.clear(); config.put({ ...baseTs, @@ -126,16 +145,15 @@ o.spec('MemoryConfig', () => { name: baseImg.name, title: '', category: '', - 2193: 'im_image-2193', - 3857: 'im_image-3857', + 2193: `im_${idA}`, + 3857: `im_${idB}`, }, ], } as BaseConfig); - config.put({ ...baseImg, id: 'im_image-2193', projection: 2193 } as ConfigImagery); - config.put({ ...baseImg, id: 'im_image-3857', projection: 3857 } as ConfigImagery); + config.put({ ...baseImg, id: `im_${idA}`, projection: 2193 } as ConfigImagery); + config.put({ ...baseImg, id: `im_${idB}`, projection: 3857 } as ConfigImagery); o(config.toJson().tileSet.length).equals(1); - config.createVirtualTileSets(); const tileSets = config.toJson().tileSet.map((c) => c.id); @@ -143,16 +161,35 @@ o.spec('MemoryConfig', () => { o(tileSets).deepEquals([ 'ts_aerial', 'ts_aerial:ōtorohanga_urban_2021_0-1m_RGB', // deprecated by child `:` - 'ts_image-2193', // By image id + `ts_${idA}`, // By image id 'ts_ōtorohanga-urban-2021-0.1m', // By name - 'ts_image-3857', // By image id + `ts_${idB}`, // By image id + 'ts_all', ]); const target = await config.TileSet.get('ts_aerial:ōtorohanga_urban_2021_0-1m_RGB'); o(target?.layers.length).equals(1); - o(target?.layers[0][3857]).equals('im_image-3857'); - o(target?.layers[0][2193]).equals('im_image-2193'); + o(target?.layers[0][3857]).equals(`im_${idB}`); + o(target?.layers[0][2193]).equals(`im_${idA}`); // the name should be mapped back to the expected name so tiles will be served via the same endpoints as by name o(target?.name).equals('ōtorohanga-urban-2021-0.1m'); }); + + o('The latest imagery should overwrite the old ones', async () => { + const idLater = ulid(); + await timers.setTimeout(5); + const idLatest = ulid(); + config.put(baseImg); + config.put({ ...baseImg, id: `im_${idLater}` } as ConfigImagery); + config.put({ ...baseImg, id: `im_${idLatest}` } as ConfigImagery); + + o(config.toJson().imagery.length).equals(3); + + config.createVirtualTileSets(); + const target = await config.TileSet.get('ts_ōtorohanga-urban-2021-0.1m'); + o(target?.layers.length).equals(1); + o(target?.layers[0][3857]).equals(`im_${idLatest}`); + o(target?.layers[0][2193]).equals(undefined); + o(target?.name).equals('ōtorohanga-urban-2021-0.1m'); + }); }); diff --git a/packages/config/src/memory/memory.config.ts b/packages/config/src/memory/memory.config.ts index a38c34d0a..e791b7434 100644 --- a/packages/config/src/memory/memory.config.ts +++ b/packages/config/src/memory/memory.config.ts @@ -22,6 +22,7 @@ export interface ConfigBundled { style: ConfigVectorStyle[]; provider: ConfigProvider[]; tileSet: ConfigTileSet[]; + duplicateImagery: ConfigTileSet[]; } function isConfigImagery(i: BaseConfig): i is ConfigImagery { @@ -31,6 +32,20 @@ function isConfigTileSet(i: BaseConfig): i is ConfigTileSet { return ConfigId.getPrefix(i.id) === ConfigPrefix.TileSet; } +/** Get the last id from the s3 path and compare to get the latest id based on the timestamp */ +function findLatestId(idA: string, idB: string): string { + const ulidA = ConfigId.unprefix(ConfigPrefix.Imagery, idA); + const ulidB = ConfigId.unprefix(ConfigPrefix.Imagery, idB); + try { + const timeA = decodeTime(ulidA); + const timeB = decodeTime(ulidB); + if (timeA >= timeB) return idA; + } finally { + //If not ulid return the return id alphabetically. + return idA.localeCompare(idB) > 0 ? idA : idB; + } +} + /** Force a unknown object into a Record type */ export function isObject(obj: unknown): obj is Record { if (typeof obj !== 'object') return false; @@ -61,6 +76,9 @@ export class ConfigProviderMemory extends BasemapsConfigProvider { /** Asset path from the config bundle */ assets: string; + /** Catch configs with the same imagery that using the different imagery ids. */ + duplicateImagery: ConfigTileSet[] = []; + put(obj: BaseConfig): void { this.objects.set(obj.id, obj); } @@ -74,6 +92,7 @@ export class ConfigProviderMemory extends BasemapsConfigProvider { style: [], provider: [], tileSet: [], + duplicateImagery: this.duplicateImagery, }; for (const val of this.objects.values()) { @@ -105,6 +124,7 @@ export class ConfigProviderMemory extends BasemapsConfigProvider { /** Find all imagery inside this configuration and create a virtual tile set for it */ createVirtualTileSets(): void { + const allLayers: ConfigLayer[] = []; for (const obj of this.objects.values()) { // Limit child tileset generation to `aerial` layers only if (isConfigTileSet(obj) && obj.name === 'aerial') { @@ -113,11 +133,32 @@ export class ConfigProviderMemory extends BasemapsConfigProvider { this.imageryToChildTileSet(obj, layer, EpsgCode.Google); } } else if (isConfigImagery(obj)) { - // TODO should this really overwrite existing tilesets this.put(ConfigProviderMemory.imageryToTileSet(obj)); - this.imageryToTileSetByName(obj); + const tileSet = this.imageryToTileSetByName(obj); + allLayers.push(tileSet.layers[0]); } } + // Create an all tileset contains all raster layers + if (allLayers.length) this.createVirtualAllTileSet(allLayers); + } + + createVirtualAllTileSet(layers: ConfigLayer[]): void { + const layerByName = new Map(); + // Set all layers as minZoom:32 + for (const l of layers) { + const newLayer = { ...l, maxZoom: undefined, minZoom: 32 }; + layerByName.set(newLayer.name, { ...layerByName.get(l.name), ...newLayer }); + } + const allTileset: ConfigTileSet = { + type: TileSetType.Raster, + id: 'ts_all', + name: 'all_imagery', + title: 'All Imagery Basemaps', + category: 'Basemaps', + format: ImageFormat.Webp, + layers: Array.from(layerByName.values()), + }; + this.put(allTileset); } /** Create a tileset by the standardized name */ @@ -140,8 +181,15 @@ export class ConfigProviderMemory extends BasemapsConfigProvider { removeUndefined(existing); this.put(existing); } - // TODO this overwrites existing layers - existing.layers[0][i.projection] = i.id; + // The latest imagery overwrite the earlier ones. + const existingImageryId = existing.layers[0][i.projection]; + if (existingImageryId) { + existing.layers[0][i.projection] = findLatestId(i.id, existingImageryId); + this.duplicateImagery.push(existing); + } else { + existing.layers[0][i.projection] = i.id; + } + return existing; }