Skip to content

Commit 04931ea

Browse files
committed
Add TileJoin into the cli-vector
1 parent e74687d commit 04931ea

File tree

14 files changed

+205
-399
lines changed

14 files changed

+205
-399
lines changed

package-lock.json

Lines changed: 38 additions & 257 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"@linzjs/style": "^5.4.0",
2626
"@types/aws-lambda": "^8.10.75",
2727
"@types/geojson": "^7946.0.7",
28-
"@types/node": "^20.14.8",
28+
"@types/node": "^22.15.24",
2929
"@types/sinon": "^17.0.2",
3030
"conventional-github-releaser": "^3.1.5",
3131
"cross-env": "^7.0.3",

packages/cli-vector/package.json

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,9 @@
4343
"@types/mapbox__geojson-area": "^0.2.6",
4444
"@types/p-limit": "^2.1.0",
4545
"@types/polylabel": "^1.1.3",
46+
"@types/tar-stream": "^2.2.2",
4647
"cmd-ts": "^0.12.1",
47-
"stac-ts": "^1.0.0",
48-
"@types/better-sqlite3": "^7.6.9",
49-
"@types/tar-stream": "^2.2.2"
48+
"stac-ts": "^1.0.0"
5049
},
5150
"publishConfig": {
5251
"access": "public"
@@ -61,9 +60,9 @@
6160
"geojson": "^0.5.0",
6261
"p-limit": "^6.2.0",
6362
"polylabel": "^2.0.1",
63+
"sqlite": "^5.1.1",
64+
"tar-stream": "^2.2.0",
6465
"zod": "^3.24.4",
65-
"zod-geojson": "^0.0.3",
66-
"better-sqlite3": "^9.4.3",
67-
"tar-stream": "^2.2.0"
66+
"zod-geojson": "^0.0.3"
6867
}
6968
}

packages/cli-vector/src/cli/cli.create.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export const CreateCommand = command({
6565

6666
// if applicable, combine all mbtiles files into a single mbtiles file
6767
if (args.join === true) {
68-
const mbtileFiles = items.map((item) => item.tmpPaths.mbtiles.pathname);
68+
const mbtileFiles = items.map((item) => item.tmpPaths.mbtiles);
6969
logger.info({ joining: mbtileFiles.length }, 'JoinMbtiles:Start');
7070

7171
const joinedFile = new URL(`joined.mbtiles`, TmpPath);
@@ -130,7 +130,7 @@ async function downloadSourceFile(
130130

131131
const layer = options.layer;
132132

133-
logger.info({ source: layer.source }, 'DownloadSourceFile: Start');
133+
logger.info({ source: layer.source, id: layer.id, name: layer.name }, 'DownloadSourceFile: Start');
134134

135135
if (!(await fsa.exists(tmpPaths.source.path))) {
136136
// TODO: We don't acturally need to head file from lds-cache here.
@@ -147,7 +147,7 @@ async function downloadSourceFile(
147147
});
148148
}
149149

150-
logger.info({ destination: tmpPaths.source.path }, 'DownloadSourceFile: End');
150+
logger.info({ destination: tmpPaths.source.path, id: layer.id, name: layer.name }, 'DownloadSourceFile: End');
151151
}
152152

153153
/**
@@ -178,36 +178,42 @@ async function createMbtilesFile(
178178
/**
179179
* Convert the source file into an ndjson
180180
*/
181-
logger.info({ source: tmpPaths.source.path }, '[1/5] Convert source file to ndjson: Start');
181+
logger.info({ source: tmpPaths.source.path, dataset: layer.name }, '[1/5] Convert source file to ndjson: Start');
182182
if (!(await fsa.exists(tmpPaths.ndjson))) {
183183
await ogr2ogrNDJson(tmpPaths.source.path, tmpPaths.ndjson, logger);
184184
}
185-
logger.info({ destination: tmpPaths.ndjson }, '[1/5] Convert source file to ndjson: End');
185+
logger.info({ destination: tmpPaths.ndjson, dataset: layer.name }, '[1/5] Convert source file to ndjson: End');
186186

187187
/**
188188
* Parse the ndjson file and apply the generalization options
189189
*/
190-
logger.info({ source: tmpPaths.ndjson }, '[2/5] Generalise ndjson features: Start');
190+
logger.info({ source: tmpPaths.ndjson, dataset: layer.name }, '[2/5] Generalise ndjson features: Start');
191191
let metrics: Metrics | null = null;
192192
if (!(await fsa.exists(tmpPaths.genNdjson))) {
193193
metrics = await generalize(tmpPaths.ndjson, tmpPaths.genNdjson, options, logger);
194194
if (metrics.output === 0) throw new Error(`Failed to generalize ndjson file ${tmpPaths.ndjson.href}`);
195195
}
196-
logger.info({ destination: tmpPaths.genNdjson }, '[2/5] Generalise ndjson features: End');
196+
logger.info({ destination: tmpPaths.genNdjson, dataset: layer.name }, '[2/5] Generalise ndjson features: End');
197197

198198
/**
199199
* Transform the generalized ndjson file to an mbtiles file
200200
*/
201-
logger.info({ source: tmpPaths.genNdjson }, '[3/5] Transform generalised ndjson into mbtiles: Start');
201+
logger.info(
202+
{ source: tmpPaths.genNdjson, dataset: layer.name },
203+
'[3/5] Transform generalised ndjson into mbtiles: Start',
204+
);
202205
if (!(await fsa.exists(tmpPaths.mbtiles))) {
203206
await tippecanoe(tmpPaths.genNdjson, tmpPaths.mbtiles, layer, logger);
204207
}
205-
logger.info({ destination: tmpPaths.mbtiles }, '[3/5] Transform generalised ndjson into mbtiles: End');
208+
logger.info(
209+
{ destination: tmpPaths.mbtiles, dataset: layer.name },
210+
'[3/5] Transform generalised ndjson into mbtiles: End',
211+
);
206212

207213
/**
208214
* Copy the mbtiles file to the same directory as the Vector Stac Item file
209215
*/
210-
logger.info({ source: tmpPaths.mbtiles }, '[4/5] Copy mbtiles to stac location: Start');
216+
logger.info({ source: tmpPaths.mbtiles, dataset: layer.name }, '[4/5] Copy mbtiles to stac location: Start');
211217
if (!(await fsa.exists(tmpPaths.mbtilesCopy))) {
212218
await fsa.write(tmpPaths.mbtilesCopy, fsa.readStream(tmpPaths.mbtiles));
213219

@@ -216,12 +222,12 @@ async function createMbtilesFile(
216222
throw new Error(`Failed to write the mbtiles file to ${tmpPaths.mbtilesCopy.href}`);
217223
}
218224
}
219-
logger.info({ destination: tmpPaths.mbtilesCopy }, '[4/5] Copy mbtiles to stac location: End');
225+
logger.info({ destination: tmpPaths.mbtilesCopy, dataset: layer.name }, '[4/5] Copy mbtiles to stac location: End');
220226

221227
/**
222228
* Update the Vector Stac Item file
223229
*/
224-
logger.info({ source: tmpPaths.origin }, '[5/5] Update stac: Start');
230+
logger.info({ source: tmpPaths.origin, dataset: layer.name }, '[5/5] Update stac: Start');
225231

226232
// Update 'cache' flag to 'true' now that the mbtiles file exists
227233
layer.cache!.exists = true;

packages/cli-vector/src/cli/cli.extract.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@ export const ExtractCommand = command({
5656
const smallLayers = [];
5757
const largeLayers = [];
5858
let total = 0;
59-
const allFiles: string[] = [];
59+
const allFiles = [];
6060
const vectorStac = new VectorStac(logger);
6161
for (const schema of schemas) {
6262
for (const layer of schema.layers) {
6363
if (layer.cache == null) throw new Error(`Fail to prepare cache path for layer ${schema.name}:${layer.id}`);
64-
allFiles.push(layer.cache.path.href);
64+
allFiles.push({ path: layer.cache.path.href });
6565

6666
// Skip if the layer is already processed in cache
6767
if (layer.cache.exists) {
Lines changed: 68 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,35 @@
1-
import { Epsg, TileMatrixSets } from '@basemaps/geo';
2-
import { fsa, LogType, Url } from '@basemaps/shared';
1+
import { TileMatrixSets } from '@basemaps/geo';
2+
import { fsa, isArgo, LogType, Url, UrlArrayJsonFile } from '@basemaps/shared';
33
import { CliId, CliInfo } from '@basemaps/shared/build/cli/info.js';
44
import { getLogger, logArguments } from '@basemaps/shared/build/cli/log.js';
5-
import { command, option, string } from 'cmd-ts';
6-
import { basename } from 'path';
5+
import { command, option, optional, string } from 'cmd-ts';
6+
import { mkdir } from 'fs/promises';
7+
import path, { basename, dirname } from 'path';
78
import { createGzip } from 'zlib';
89

910
import { createStacFiles } from '../stac.js';
1011
import { toTarIndex } from '../transform/covt.js';
1112
import { toTarTiles } from '../transform/mbtiles.to.ttiles.js';
1213
import { tileJoin } from '../transform/tippecanoe.js';
1314

14-
async function fromFile(path: URL): Promise<string[]> {
15-
const toProcess = await fsa.readJson(path);
16-
const paths: string[] = [];
17-
if (!Array.isArray(toProcess)) throw new Error(`File ${path.href} is not an array`);
18-
for (const task of toProcess) {
19-
if (typeof task !== 'string') throw new Error(`File ${path.href} is not an array of strings`);
20-
const sourceFile = new URL(task);
21-
if (sourceFile.protocol === 'file:') {
22-
paths.push(sourceFile.pathname);
15+
async function download(filePaths: URL[], outputPath: string, logger: LogType): Promise<URL[]> {
16+
const paths: URL[] = [];
17+
for (const file of filePaths) {
18+
if (file.protocol === 'file:') {
19+
paths.push(file);
2320
} else {
24-
const fileName = basename(sourceFile.pathname);
25-
if (fileName == null) throw new Error(`Unsupported source pathname ${sourceFile.pathname}`);
26-
const localFile = `tmp/join/${fileName}`;
27-
const stream = fsa.readStream(sourceFile);
28-
await fsa.write(fsa.toUrl(localFile), stream);
21+
const fileName = basename(file.pathname);
22+
if (fileName == null) throw new Error(`Unsupported source pathname ${file.pathname}`);
23+
const localFile = fsa.toUrl(`${outputPath}/downloads/${fileName}`);
24+
if (await fsa.exists(localFile)) {
25+
logger.info({ file: file, localFile, fileName }, 'Download:FileExists');
26+
} else {
27+
const stats = await fsa.head(file);
28+
logger.info({ file: file, localFile, fileName, size: stats?.size }, 'Download:Start');
29+
const stream = fsa.readStream(file);
30+
await fsa.write(localFile, stream);
31+
logger.info({ file: file, localFile, fileName }, 'Download:End');
32+
}
2933
paths.push(localFile);
3034
}
3135
}
@@ -36,8 +40,8 @@ async function fromFile(path: URL): Promise<string[]> {
3640
* Upload output file into s3 bucket
3741
*
3842
*/
39-
async function upload(file: URL, bucketPath: string, logger: LogType): Promise<URL> {
40-
logger.info({ file: file }, 'Load:Start');
43+
async function upload(file: URL, bucketPath: URL, logger: LogType): Promise<URL> {
44+
logger.info({ file: file.href }, 'Load:Start');
4145
let filename = basename(file.pathname);
4246
let stream = fsa.readStream(file);
4347

@@ -47,9 +51,11 @@ async function upload(file: URL, bucketPath: string, logger: LogType): Promise<U
4751
stream = stream.pipe(createGzip());
4852
}
4953

50-
// Upload to s3
51-
let path = fsa.toUrl(`${bucketPath}/${CliId}/${filename}`);
52-
if (filename.endsWith('catalog.json')) path = fsa.toUrl(`${bucketPath}/${filename}`); // Upload catalog to root directory
54+
// Upload to s3 or copy to local
55+
let path = new URL(`topographic/${CliId}/${filename}`, bucketPath);
56+
logger.info({ file: file, path: path }, 'Load:Path');
57+
if (path.protocol === 'file:') await mkdir(dirname(path.pathname), { recursive: true });
58+
if (filename.endsWith('catalog.json')) path = new URL(filename, bucketPath); // Upload catalog to root directory
5359
await fsa.write(path, stream);
5460
logger.info({ file: file, path: path }, 'Load:Finish');
5561
return path;
@@ -58,7 +64,7 @@ async function upload(file: URL, bucketPath: string, logger: LogType): Promise<U
5864
export const JoinArgs = {
5965
...logArguments,
6066
fromFile: option({
61-
type: Url,
67+
type: UrlArrayJsonFile,
6268
long: 'from-file',
6369
description: 'Path to JSON file containing array of paths to mbtiles.',
6470
}),
@@ -80,13 +86,13 @@ export const JoinArgs = {
8086
type: string,
8187
long: 'title',
8288
description: 'Title for the output etl data in the STAC file',
83-
defaultValue: () => 'Topographic',
89+
defaultValue: () => 'Topographic V2',
8490
defaultValueIsSerializable: true,
8591
}),
8692
target: option({
87-
type: string,
93+
type: optional(Url),
8894
long: 'target',
89-
description: 'Path of target location, could be local or s3',
95+
description: 'Path of target location to upload the processed file, could be local or s3',
9096
}),
9197
};
9298

@@ -97,32 +103,49 @@ export const JoinCommand = command({
97103
args: JoinArgs,
98104
async handler(args) {
99105
const logger = getLogger(this, args, 'cli-vector');
100-
const filePaths = await fromFile(args.fromFile);
101-
102-
logger.info({ files: filePaths.length }, 'JoinMbtiles: Start');
103-
104-
const outputMbtiles = fsa.toUrl(`tmp/${args.filename}.mbtiles`).pathname;
106+
const outputPath = path.resolve('tmp/join/');
107+
await mkdir(outputPath, { recursive: true });
108+
const tileMatrix = TileMatrixSets.find(args.tileMatrix);
109+
if (tileMatrix == null) throw new Error(`Tile matrix ${args.tileMatrix} is not supported`);
110+
const bucketPath = new URL(`vector/${tileMatrix.projection.code}/`, args.target ?? outputPath);
111+
const filePaths = await download(args.fromFile, outputPath, logger);
105112

113+
const outputMbtiles = path.join(outputPath, `${args.filename}.mbtiles`);
114+
logger.info({ files: filePaths.length, outputMbtiles }, 'JoinMbtiles: Start');
106115
await tileJoin(filePaths, outputMbtiles, logger);
116+
logger.info({ files: filePaths.length, outputMbtiles }, 'JoinMbtiles: End');
107117

108-
const outputCotar = fsa.toUrl(`tmp/${args.filename}.covt`);
109-
118+
const outputCotar = path.join(outputPath, `${args.filename}.tar.co`);
119+
logger.info({ mbtiles: outputMbtiles, outputCotar }, 'ToTartTiles: Start');
110120
await toTarTiles(outputMbtiles, outputCotar, logger);
121+
logger.info({ mbtiles: outputMbtiles, outputCotar }, 'ToTartTiles: End');
111122

112-
const [outputIndex, outputTar] = await toTarIndex(outputCotar, 'tmp/', args.filename, logger);
123+
const outputIndex = path.join(outputPath, `${args.filename}.tar.index`);
124+
logger.info({ cotar: outputCotar, outputIndex }, 'toTarIndex: Start');
125+
await toTarIndex(outputCotar, outputIndex, logger);
126+
logger.info({ cotar: outputCotar, outputIndex }, 'toTarIndex: End');
113127

114-
const tileMatrix = TileMatrixSets.find(args.tileMatrix);
115-
if (tileMatrix == null) throw new Error(`Tile matrix ${args.tileMatrix} is not supported`);
116-
const stacFiles = await createStacFiles(filePaths, args.target, args.filename, tileMatrix, args.title, logger);
128+
logger.info({ target: bucketPath, tileMatrix: tileMatrix.identifier }, 'CreateStac: Start');
129+
const stacFiles = await createStacFiles(args.fromFile, bucketPath, args.filename, tileMatrix, args.title, logger);
130+
logger.info({ cotar: outputCotar, outputIndex }, 'CreateStac: End');
117131

118132
// Upload output to s3
119-
const bucketPath = `${args.target}/vector/${Epsg.Google.code.toString()}/${args.filename}`;
120-
await upload(fsa.toUrl(outputMbtiles), bucketPath, logger);
121-
await upload(fsa.toUrl(outputTar), bucketPath, logger);
122-
await upload(fsa.toUrl(outputIndex), bucketPath, logger);
123-
// Upload stac Files
124-
for (const file of stacFiles) {
125-
await upload(file, bucketPath, logger);
133+
logger.info({ target: bucketPath, tileMatrix: tileMatrix.identifier }, 'Upload: Start');
134+
if (args.target) {
135+
await upload(fsa.toUrl(outputMbtiles), bucketPath, logger);
136+
await upload(fsa.toUrl(outputCotar), bucketPath, logger);
137+
await upload(fsa.toUrl(outputIndex), bucketPath, logger);
138+
// Upload stac Files
139+
for (const file of stacFiles) {
140+
await upload(file, bucketPath, logger);
141+
}
142+
logger.info({ target: bucketPath, tileMatrix: tileMatrix.identifier }, 'Upload: End');
143+
}
144+
145+
// Write output target for argo tasks to create pull request
146+
if (isArgo()) {
147+
const target = new URL(`topographic/${CliId}/${args.filename}.tar.co`, bucketPath);
148+
await fsa.write(fsa.toUrl('/tmp/target'), JSON.stringify([target]));
126149
}
127150
},
128151
});

packages/cli-vector/src/schema-loader/parser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const zSimplify = z.object({
2323
export const zLayer = z.object({
2424
id: z.string(),
2525
name: z.string(),
26+
version: z.number().optional(),
2627
source: z.string(),
2728
tags: zTags,
2829
attributes: zAttributes.optional(),

0 commit comments

Comments
 (0)