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

feat(config): Create an all tileset from imagery configs. BM-805 #2794

Merged
merged 16 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
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
91 changes: 64 additions & 27 deletions packages/config/src/memory/__tests__/memory.config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -64,28 +69,34 @@ 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();

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();
Expand All @@ -94,28 +105,36 @@ 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);

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_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();
Wentao-Kuang marked this conversation as resolved.
Show resolved Hide resolved
config.objects.clear();
config.put({
...baseTs,
Expand All @@ -126,33 +145,51 @@ 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);

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');
});
});
56 changes: 52 additions & 4 deletions packages/config/src/memory/memory.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface ConfigBundled {
style: ConfigVectorStyle[];
provider: ConfigProvider[];
tileSet: ConfigTileSet[];
duplicateImagery: ConfigTileSet[];
}

function isConfigImagery(i: BaseConfig): i is ConfigImagery {
Expand All @@ -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<string, unknown> type */
export function isObject(obj: unknown): obj is Record<string, unknown> {
if (typeof obj !== 'object') return false;
Expand Down Expand Up @@ -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);
}
Expand All @@ -74,6 +92,7 @@ export class ConfigProviderMemory extends BasemapsConfigProvider {
style: [],
provider: [],
tileSet: [],
duplicateImagery: this.duplicateImagery,
};

for (const val of this.objects.values()) {
Expand Down Expand Up @@ -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[] = [];
blacha marked this conversation as resolved.
Show resolved Hide resolved
for (const obj of this.objects.values()) {
// Limit child tileset generation to `aerial` layers only
if (isConfigTileSet(obj) && obj.name === 'aerial') {
Expand All @@ -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<string, ConfigLayer>();
// 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 */
Expand All @@ -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);
Wentao-Kuang marked this conversation as resolved.
Show resolved Hide resolved
} else {
existing.layers[0][i.projection] = i.id;
}

return existing;
}

Expand Down
Loading