Skip to content

Commit 0ea552e

Browse files
authored
fix(config): allow initializing config from URLs (#2830)
1 parent f1ed481 commit 0ea552e

File tree

8 files changed

+127
-31
lines changed

8 files changed

+127
-31
lines changed

packages/cogify/src/cogify/cli/cli.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ConfigProviderMemory, base58 } from '@basemaps/config';
2-
import { ConfigImageryTiff, initConfigFromPaths } from '@basemaps/config/build/json/tiff.config.js';
2+
import { ConfigImageryTiff, initConfigFromUrls } from '@basemaps/config/build/json/tiff.config.js';
33
import { Projection, TileMatrixSets } from '@basemaps/geo';
44
import { fsa } from '@basemaps/shared';
55
import { CliInfo } from '@basemaps/shared/build/cli/info.js';
@@ -30,7 +30,7 @@ export const BasemapsCogifyConfigCommand = command({
3030

3131
const mem = new ConfigProviderMemory();
3232
metrics.start('imagery:load');
33-
const cfg = await initConfigFromPaths(mem, [urlToString(args.path)]);
33+
const cfg = await initConfigFromUrls(mem, [args.path]);
3434
metrics.end('imagery:load');
3535
logger.info({ imagery: cfg.imagery.length, titles: cfg.imagery.map((f) => f.title) }, 'ImageryConfig:Loaded');
3636

packages/cogify/src/cogify/cli/cli.cover.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ConfigProviderMemory } from '@basemaps/config';
2-
import { initConfigFromPaths } from '@basemaps/config/build/json/tiff.config.js';
2+
import { initConfigFromUrls } from '@basemaps/config/build/json/tiff.config.js';
33
import { GoogleTms, Nztm2000QuadTms, TileId } from '@basemaps/geo';
44
import { fsa } from '@basemaps/shared';
55
import { CliId, CliInfo } from '@basemaps/shared/build/cli/info.js';
@@ -10,6 +10,7 @@ import { CutlineOptimizer } from '../../cutline.js';
1010
import { getLogger, logArguments } from '../../log.js';
1111
import { TileCoverContext, createTileCover } from '../../tile.cover.js';
1212
import { createFileStats } from '../stac.js';
13+
import { Url } from '../parsers.js';
1314

1415
const SupportedTileMatrix = [GoogleTms, Nztm2000QuadTms];
1516

@@ -27,7 +28,7 @@ export const BasemapsCogifyCoverCommand = command({
2728
description: 'Cutline blend amount see GDAL_TRANSLATE -cblend',
2829
defaultValue: () => 20,
2930
}),
30-
paths: restPositionals({ type: string, displayName: 'path', description: 'Path to source imagery' }),
31+
paths: restPositionals({ type: Url, displayName: 'path', description: 'Path to source imagery' }),
3132
tileMatrix: option({
3233
type: string,
3334
long: 'tile-matrix',
@@ -40,7 +41,7 @@ export const BasemapsCogifyCoverCommand = command({
4041

4142
const mem = new ConfigProviderMemory();
4243
metrics.start('imagery:load');
43-
const cfg = await initConfigFromPaths(mem, args.paths);
44+
const cfg = await initConfigFromUrls(mem, args.paths);
4445
const imageryLoadTime = metrics.end('imagery:load');
4546
if (cfg.imagery.length === 0) throw new Error('No imagery found');
4647
const im = cfg.imagery[0];

packages/cogify/src/cogify/parsers.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ export const Url: Type<string, URL> = {
1010
try {
1111
return new URL(str);
1212
} catch (e) {
13-
// Possibly already a URL
14-
if (str.includes(':')) throw e;
1513
return pathToFileURL(str);
1614
}
1715
},
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { fsa } from '@chunkd/fs';
2+
import { FsMemory, SourceMemory } from '@chunkd/source-memory';
3+
import { fileURLToPath } from 'node:url';
4+
import o from 'ospec';
5+
import { ConfigProviderMemory } from '../../memory/memory.config.js';
6+
import { initConfigFromUrls } from '../tiff.config.js';
7+
8+
const simpleTiff = new URL('../../../../__tests__/static/rgba8_tiled.tiff', import.meta.url);
9+
10+
o.spec('config import', () => {
11+
const fsMemory = new FsMemory();
12+
13+
// TODO SourceMemory adds `memory://` to every url even if it already has a `memory://` prefix
14+
fsMemory.source = (filePath): SourceMemory => {
15+
const bytes = fsMemory.files.get(filePath);
16+
if (bytes == null) throw new Error('Failed to load file: ' + filePath);
17+
return new SourceMemory(filePath.replace('memory://', ''), bytes);
18+
};
19+
20+
o.before(() => fsa.register('memory://', fsMemory));
21+
o.beforeEach(() => fsMemory.files.clear());
22+
23+
o('should load tiff from filesystem', async () => {
24+
const buf = await fsa.read(fileURLToPath(simpleTiff));
25+
await fsa.write('memory://tiffs/tile-tiff-name/tiff-a.tiff', buf);
26+
27+
const cfg = new ConfigProviderMemory();
28+
const ret = await initConfigFromUrls(cfg, [new URL('memory://tiffs/tile-tiff-name')]);
29+
30+
o(ret.imagery.length).equals(1);
31+
const imagery = ret.imagery[0];
32+
o(imagery.name).equals('tile-tiff-name');
33+
o(imagery.files).deepEquals([{ name: 'tiff-a.tiff', x: 0, y: -64, width: 64, height: 64 }]);
34+
});
35+
36+
o('should create multiple imagery layers from multiple folders', async () => {
37+
const buf = await fsa.read(fileURLToPath(simpleTiff));
38+
await fsa.write('memory://tiffs/tile-tiff-a/tiff-a.tiff', buf);
39+
await fsa.write('memory://tiffs/tile-tiff-b/tiff-b.tiff', buf);
40+
41+
const cfg = new ConfigProviderMemory();
42+
const ret = await initConfigFromUrls(cfg, [
43+
new URL('memory://tiffs/tile-tiff-a'),
44+
new URL('memory://tiffs/tile-tiff-b/'),
45+
]);
46+
47+
o(ret.imagery.length).equals(2);
48+
o(ret.imagery[0].name).equals('tile-tiff-a');
49+
o(ret.imagery[0].files).deepEquals([{ name: 'tiff-a.tiff', x: 0, y: -64, width: 64, height: 64 }]);
50+
51+
o(ret.imagery[1].name).equals('tile-tiff-b');
52+
o(ret.imagery[1].files).deepEquals([{ name: 'tiff-b.tiff', x: 0, y: -64, width: 64, height: 64 }]);
53+
54+
o(ret.tileSet.layers.length).equals(2);
55+
o(ret.tileSet.layers[0][3857]).equals(ret.imagery[0].id);
56+
o(ret.tileSet.layers[0].name).equals(ret.imagery[0].name);
57+
o(ret.tileSet.layers[1][3857]).equals(ret.imagery[1].id);
58+
o(ret.tileSet.layers[1].name).equals(ret.imagery[1].name);
59+
});
60+
});

packages/config/src/json/tiff.config.ts

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import {
1010
import { fsa } from '@chunkd/fs';
1111
import { CogTiff } from '@cogeotiff/core';
1212
import pLimit, { LimitFunction } from 'p-limit';
13-
import { basename, resolve } from 'path';
13+
import { basename } from 'path';
1414
import { StacCollection } from 'stac-ts';
15+
import { fileURLToPath } from 'url';
1516
import { sha256base58 } from '../base58.node.js';
1617
import { ConfigImagery } from '../config/imagery.js';
1718
import { ConfigTileSetRaster, TileSetType } from '../config/tile.set.js';
@@ -47,9 +48,10 @@ export type ConfigImageryTiff = ConfigImagery & TiffSummary;
4748
*
4849
* @throws if any of the tiffs have differing EPSG or GSD
4950
**/
50-
function computeTiffSummary(target: string, tiffs: CogTiff[]): TiffSummary {
51+
function computeTiffSummary(target: URL, tiffs: CogTiff[]): TiffSummary {
5152
const res: Partial<TiffSummary> = { files: [] };
5253

54+
const targetPath = urlToString(target);
5355
let bounds: Bounds | undefined;
5456
for (const tiff of tiffs) {
5557
const firstImage = tiff.getImage(0);
@@ -78,7 +80,9 @@ function computeTiffSummary(target: string, tiffs: CogTiff[]): TiffSummary {
7880
else bounds = bounds.union(imgBounds);
7981

8082
if (res.files == null) res.files = [];
81-
res.files.push({ name: tiff.source.uri, ...imgBounds });
83+
84+
const relativePath = toRelative(targetPath, tiff.source.uri);
85+
res.files.push({ name: relativePath, ...imgBounds });
8286
}
8387
res.bounds = bounds?.toJson();
8488
if (res.bounds == null) throw new Error('Failed to extract imagery bounds from:' + target);
@@ -87,11 +91,28 @@ function computeTiffSummary(target: string, tiffs: CogTiff[]): TiffSummary {
8791
return res as TiffSummary;
8892
}
8993

94+
/** Convert a path to a relative path
95+
* @param base the path to be relative to
96+
* @param other the path to convert
97+
*/
98+
function toRelative(base: string, other: string): string {
99+
if (!other.startsWith(base)) throw new Error('Paths are not relative');
100+
const part = other.slice(base.length);
101+
if (part.startsWith('/') || part.startsWith('\\')) return part.slice(1);
102+
return part;
103+
}
104+
105+
/** Convert a URL to a string using fileUrlToPath if the URL is a file:// */
106+
function urlToString(u: URL): string {
107+
if (u.protocol === 'file:') return fileURLToPath(u);
108+
return u.href;
109+
}
110+
90111
/** Attempt to read a stac collection.json from the target path if it exists or return null if anything goes wrong. */
91-
async function loadStacFromPath(target: string): Promise<StacCollection | null> {
92-
const collectionPath = fsa.join(target, 'collection.json');
112+
async function loadStacFromURL(target: URL): Promise<StacCollection | null> {
113+
const collectionPath = new URL('collection.json', target);
93114
try {
94-
return await fsa.readJson(collectionPath);
115+
return await fsa.readJson(urlToString(collectionPath));
95116
} catch (e) {
96117
return null;
97118
}
@@ -104,30 +125,31 @@ async function loadStacFromPath(target: string): Promise<StacCollection | null>
104125
*
105126
* @returns Imagery configuration generated from the path
106127
*/
107-
export async function imageryFromTiffPath(target: string, Q: LimitFunction, log?: LogType): Promise<ConfigImageryTiff> {
108-
const sourceFiles = await fsa.toArray(fsa.list(target));
128+
export async function imageryFromTiffUrl(target: URL, Q: LimitFunction, log?: LogType): Promise<ConfigImageryTiff> {
129+
const targetPath = urlToString(target);
130+
const sourceFiles = await fsa.toArray(fsa.list(targetPath));
109131
const tiffs = await Promise.all(
110132
sourceFiles.filter(isTiff).map((c) => Q(() => new CogTiff(fsa.source(c)).init(true))),
111133
);
112134

113135
try {
114-
const stac = await loadStacFromPath(target);
136+
const stac = await loadStacFromURL(target);
115137
const params = computeTiffSummary(target, tiffs);
116138

117-
const folderName = basename(target);
139+
const folderName = basename(targetPath);
118140
const title = stac?.title ?? folderName;
119141
const tileMatrix =
120142
params.projection === EpsgCode.Nztm2000 ? Nztm2000QuadTms : TileMatrixSets.tryGet(params.projection);
121143

122144
const imagery: ConfigImageryTiff = {
123-
id: sha256base58(target),
145+
id: sha256base58(target.href),
124146
name: folderName,
125147
title,
126148
updatedAt: Date.now(),
127149
projection: params.projection,
128150
tileMatrix: tileMatrix?.identifier ?? 'none',
129151
gsd: params.gsd,
130-
uri: resolve(target),
152+
uri: targetPath,
131153
bounds: params.bounds,
132154
files: params.files,
133155
collection: stac ?? undefined,
@@ -177,16 +199,16 @@ export async function imageryFromTiffPath(target: string, Q: LimitFunction, log?
177199
* @param concurrency number of tiff files to load at a time
178200
* @returns
179201
*/
180-
export async function initConfigFromPaths(
202+
export async function initConfigFromUrls(
181203
provider: ConfigProviderMemory,
182-
targets: string[],
204+
targets: URL[],
183205
concurrency = 25,
184206
log?: LogType,
185207
): Promise<{ tileSet: ConfigTileSetRaster; imagery: ConfigImageryTiff[] }> {
186208
const q = pLimit(concurrency);
187209

188210
const imageryConfig: Promise<ConfigImageryTiff>[] = [];
189-
for (const target of targets) imageryConfig.push(imageryFromTiffPath(target, q, log));
211+
for (const target of targets) imageryConfig.push(imageryFromTiffUrl(target, q, log));
190212

191213
const aerialTileSet: ConfigTileSetRaster = {
192214
id: 'ts_aerial',

packages/lambda-tiler/src/cli/render.tile.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { ConfigProviderMemory } from '@basemaps/config';
2-
import { initConfigFromPaths } from '@basemaps/config/build/json/tiff.config.js';
2+
import { initConfigFromUrls } from '@basemaps/config/build/json/tiff.config.js';
33
import { GoogleTms, ImageFormat } from '@basemaps/geo';
44
import { LogConfig, setDefaultConfig } from '@basemaps/shared';
55
import { fsa } from '@chunkd/fs';
66
import { LambdaHttpRequest, LambdaUrlRequest, UrlEvent } from '@linzjs/lambda';
77
import { Context } from 'aws-lambda';
88
import { TileXyzRaster } from '../routes/tile.xyz.raster.js';
9+
import { pathToFileURL } from 'url';
910

10-
const target = `/home/blacha/tmp/basemaps/white-lines/nz-0.5m/`;
11+
const target = pathToFileURL(`/home/blacha/tmp/basemaps/white-lines/nz-0.5m/`);
1112
const tile = { z: 10, x: 1013, y: 633 };
1213
const tileMatrix = GoogleTms;
1314
const imageFormat = ImageFormat.Webp;
@@ -16,12 +17,12 @@ async function main(): Promise<void> {
1617
const log = LogConfig.get();
1718
const provider = new ConfigProviderMemory();
1819
setDefaultConfig(provider);
19-
const { tileSet, imagery } = await initConfigFromPaths(provider, [target]);
20+
const { tileSet, imagery } = await initConfigFromUrls(provider, [target]);
2021

2122
if (tileSet.layers.length === 0) throw new Error('No imagery found in path: ' + target);
2223
log.info({ tileSet: tileSet.name, layers: tileSet.layers.length }, 'TileSet:Loaded');
2324
for (const im of imagery) {
24-
log.info({ imagery: im.uri, title: im.title, tileMatrix: im.tileMatrix, files: im.files.length }, 'Imagery:Loaded');
25+
log.info({ url: im.uri, title: im.title, tileMatrix: im.tileMatrix, files: im.files.length }, 'Imagery:Loaded');
2526
}
2627
const request = new LambdaUrlRequest({ headers: {} } as UrlEvent, {} as Context, log) as LambdaHttpRequest;
2728

packages/server/src/cli.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
import { Env, LogConfig } from '@basemaps/shared';
22
import { CliInfo } from '@basemaps/shared/build/cli/info.js';
3-
import { command, flag, number, option, optional, restPositionals, string } from 'cmd-ts';
3+
import { Type, command, flag, number, option, optional, restPositionals, string } from 'cmd-ts';
4+
import { pathToFileURL } from 'node:url';
45
import { createServer } from './server.js';
56

67
CliInfo.package = 'basemaps/server';
78

89
const DefaultPort = 5000;
10+
/**
11+
* Parse a input parameter as a URL,
12+
* if it looks like a file path convert it using `pathToFileURL`
13+
**/
14+
export const Url: Type<string, URL> = {
15+
async from(str) {
16+
try {
17+
return new URL(str);
18+
} catch (e) {
19+
return pathToFileURL(str);
20+
}
21+
},
22+
};
923

1024
export const BasemapsServerCommand = command({
1125
name: 'basemaps-server',
@@ -26,7 +40,7 @@ export const BasemapsServerCommand = command({
2640
long: 'assets',
2741
description: 'Where the assets (sprites, fonts) are located',
2842
}),
29-
paths: restPositionals({ type: string, displayName: 'path', description: 'Path to imagery' }),
43+
paths: restPositionals({ type: Url, displayName: 'path', description: 'Path to imagery' }),
3044
},
3145
handler: async (args) => {
3246
const logger = LogConfig.get();

packages/server/src/config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ import {
66
ConfigProviderDynamo,
77
ConfigProviderMemory,
88
} from '@basemaps/config';
9-
import { initConfigFromPaths } from '@basemaps/config/build/json/tiff.config.js';
9+
import { initConfigFromUrls } from '@basemaps/config/build/json/tiff.config.js';
1010
import { fsa, getDefaultConfig, LogType } from '@basemaps/shared';
1111

1212
export type ServerOptions = ServerOptionsTiffs | ServerOptionsConfig;
1313

1414
/** Load configuration from folders */
1515
export interface ServerOptionsTiffs {
1616
assets?: string;
17-
paths: string[];
17+
paths: URL[];
1818
}
1919

2020
/** Load configuration from a config file/dynamodb */
@@ -34,7 +34,7 @@ export async function loadConfig(opts: ServerOptions, logger: LogType): Promise<
3434
// Load the config directly from the source tiff files
3535
if ('paths' in opts) {
3636
const mem = new ConfigProviderMemory();
37-
const ret = await initConfigFromPaths(mem, opts.paths);
37+
const ret = await initConfigFromUrls(mem, opts.paths);
3838
logger.info({ tileSet: ret.tileSet.name, layers: ret.tileSet.layers.length }, 'TileSet:Loaded');
3939
for (const im of ret.imagery) {
4040
logger.info(

0 commit comments

Comments
 (0)