tileflow 0.1.0-alpha.21 → 0.1.0-alpha.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ // src/cli-version.ts
2
+ import { readFileSync } from "fs";
3
+ function isRootCliVersionRequest(arguments_) {
4
+ return arguments_.length === 1 && (arguments_[0] === "--version" || arguments_[0] === "-V");
5
+ }
6
+ function getCliVersion() {
7
+ const document = JSON.parse(
8
+ readFileSync(new URL("../package.json", import.meta.url), "utf8")
9
+ );
10
+ return document.version;
11
+ }
12
+
13
+ export {
14
+ isRootCliVersionRequest,
15
+ getCliVersion
16
+ };
@@ -0,0 +1,382 @@
1
+ import {
2
+ TilesetBuildError
3
+ } from "./chunk-43VQMAQO.js";
4
+
5
+ // src/tileset-build/recipe.ts
6
+ import { createHash } from "crypto";
7
+ import { readFileSync as readFileSync2 } from "fs";
8
+ import { z } from "zod";
9
+ import { serializeCanonicalJson } from "@tileflow/core";
10
+
11
+ // src/tileset-build/crs.ts
12
+ import { existsSync, readFileSync } from "fs";
13
+ import proj4 from "proj4";
14
+ var wgs84Definition = "+proj=longlat +datum=WGS84 +no_defs";
15
+ var radiansPerDegree = Math.PI / 180;
16
+ var coincidentDatumPattern = /etrs|european_terrestrial_reference_system|rgf_?(19)?93|reseau_geodesique_francais_1993|nad_?(19)?83|north_american_(datum_)?1983|gda_?(19)?94|gda_?2020|geocentric_datum_of_australia|nzgd_?2000|new_zealand_geodetic_datum_2000|sirgas|jgd_?20(00|11)|japanese_geodetic_datum_20(00|11)/u;
17
+ function resolveCrs(statement, reproject) {
18
+ const { authority, definition, label } = statement;
19
+ if (definition === null) {
20
+ throw new TilesetBuildError(
21
+ "TF_TILESET_BUILD_CRS_UNKNOWN",
22
+ "crs",
23
+ `${label} does not state its coordinate reference system. Pass --source-crs EPSG:4326 if the coordinates are WGS84 longitude and latitude, or --source-crs <crs> --reproject if they are not. The build never guesses.`
24
+ );
25
+ }
26
+ const parsed = parseDefinition(definition, label);
27
+ const datum = String(parsed.datumCode ?? "none").toLowerCase();
28
+ const angularUnit = parsed.projName === "longlat" ? parsed.UNIT?.convert : void 0;
29
+ if (angularUnit !== void 0 && Math.abs(angularUnit - radiansPerDegree) > 1e-9) {
30
+ throw new TilesetBuildError(
31
+ "TF_TILESET_BUILD_CRS_UNSUPPORTED",
32
+ "crs",
33
+ `${label} measures coordinates in "${parsed.UNIT?.name}"; only degrees can be converted here. Reproject the data to WGS84 with another tool first.`
34
+ );
35
+ }
36
+ const greenwich = Math.abs(parsed.from_greenwich ?? 0) < 1e-12;
37
+ if (parsed.projName === "longlat" && datum === "wgs84" && greenwich) {
38
+ return { authority, label, reprojected: false, transform: null };
39
+ }
40
+ if (!reproject) {
41
+ throw new TilesetBuildError(
42
+ "TF_TILESET_BUILD_CRS_NOT_WGS84",
43
+ "crs",
44
+ `${label} is not WGS84 longitude and latitude. Add --reproject to convert it, or reproject the data yourself first. The build never reprojects without being asked.`
45
+ );
46
+ }
47
+ const datumType = parsed.datum?.datum_type;
48
+ const hasShift = datumType === 1 || datumType === 2 || datumType === 4 || definition.includes("+nadgrids=@null");
49
+ const coincident = (!hasShift || datumType === 4) && coincidentDatumPattern.test(datum);
50
+ if (!hasShift && !coincident) {
51
+ throw new TilesetBuildError(
52
+ "TF_TILESET_BUILD_DATUM_UNSUPPORTED",
53
+ "crs",
54
+ `${label} uses datum "${datum}", which has no known shift to WGS84 here; converting it would misplace features by up to hundreds of metres. Reproject with a tool that supports the datum, or pass a --source-crs proj string with +towgs84=.`
55
+ );
56
+ }
57
+ const converter = proj4(definition, wgs84Definition);
58
+ return {
59
+ authority,
60
+ label,
61
+ reprojected: true,
62
+ transform: (x, y) => converter.forward([x, y]),
63
+ ...coincident ? {
64
+ warning: `${label} uses datum "${datum}", treated as coincident with WGS84 (typically within 2 m).`
65
+ } : {}
66
+ };
67
+ }
68
+ function isInlineCrs(value) {
69
+ const trimmed = value.trim();
70
+ return /^epsg:\d{4,6}$/iu.test(trimmed) || isProjOrWkt(trimmed);
71
+ }
72
+ function isProjOrWkt(trimmed) {
73
+ return trimmed.startsWith("+") || /^[A-Z_]+\[/u.test(trimmed);
74
+ }
75
+ function portableCrs(value) {
76
+ if (isInlineCrs(value)) return value.trim();
77
+ return parseSourceCrsOption(value).definition;
78
+ }
79
+ function parseSourceCrsOption(value) {
80
+ const trimmed = value.trim();
81
+ const epsg = /^epsg:(\d{4,6})$/iu.exec(trimmed)?.[1];
82
+ if (epsg) {
83
+ const definition = epsgDefinition(Number(epsg));
84
+ if (!definition) {
85
+ throw new TilesetBuildError(
86
+ "TF_TILESET_BUILD_CRS_UNREADABLE",
87
+ "crs",
88
+ `EPSG:${epsg} has no built-in definition. Pass a proj string, WKT, or the path to a .prj file instead.`
89
+ );
90
+ }
91
+ return { authority: "option", definition, label: `EPSG:${epsg}` };
92
+ }
93
+ if (isProjOrWkt(trimmed)) {
94
+ return { authority: "option", definition: trimmed, label: "--source-crs" };
95
+ }
96
+ if (existsSync(trimmed)) {
97
+ return {
98
+ authority: "option",
99
+ definition: readFileSync(trimmed, "utf8").trim(),
100
+ label: "--source-crs"
101
+ };
102
+ }
103
+ throw new TilesetBuildError(
104
+ "TF_TILESET_BUILD_CRS_UNREADABLE",
105
+ "crs",
106
+ `Could not read --source-crs "${trimmed}". Expected EPSG:<code>, a proj string, WKT, or a .prj file path.`
107
+ );
108
+ }
109
+ function statementFromGeojsonCrs(member) {
110
+ const name = member?.properties?.name;
111
+ if (typeof name !== "string") {
112
+ throw new TilesetBuildError(
113
+ "TF_TILESET_BUILD_CRS_UNREADABLE",
114
+ "crs",
115
+ "The GeoJSON crs member is not a named CRS. Pass --source-crs to state it."
116
+ );
117
+ }
118
+ const code = /(?:epsg:(?::?[\d.]*:)?|ogc:(?:[\d.]*:)?crs)(\d+)/iu.exec(name)?.[1];
119
+ if (/crs:?84/iu.test(name) || code === "84" || code === "4326") {
120
+ return { authority: "declared", definition: wgs84Definition, label: `GeoJSON crs ${name}` };
121
+ }
122
+ const definition = code ? epsgDefinition(Number(code)) : null;
123
+ if (!definition) {
124
+ throw new TilesetBuildError(
125
+ "TF_TILESET_BUILD_CRS_UNREADABLE",
126
+ "crs",
127
+ `The GeoJSON crs "${name}" has no built-in definition. Pass --source-crs with a proj string, WKT, or .prj path, plus --reproject.`
128
+ );
129
+ }
130
+ return { authority: "declared", definition, label: `GeoJSON crs ${name}` };
131
+ }
132
+ function statementFromGeopackageSrs(layer, srs) {
133
+ if (!srs || srs.srsId === -1 || srs.srsId === 0) {
134
+ return {
135
+ authority: "declared",
136
+ definition: null,
137
+ label: `GeoPackage layer "${layer}"`
138
+ };
139
+ }
140
+ const epsg = srs.organization?.toUpperCase() === "EPSG" ? srs.organizationCode : null;
141
+ const label = epsg === null ? `GeoPackage layer "${layer}" (srs_id ${srs.srsId})` : `GeoPackage layer "${layer}" (EPSG:${epsg})`;
142
+ const pseudoMercator = epsg === 3857 || epsg === 3785 || epsg === 900913 || /^PROJCS\["[^"]*pseudo[ _-]?mercator/iu.test(srs.definition ?? "");
143
+ const exact = pseudoMercator ? epsgDefinition(3857) : epsg === 4326 ? wgs84Definition : null;
144
+ const known = epsg === null ? null : epsgDefinition(epsg);
145
+ return {
146
+ authority: "declared",
147
+ definition: exact ?? statedWkt(srs.definition) ?? known ?? statedWkt(srs.definition12063),
148
+ label
149
+ };
150
+ }
151
+ function statedWkt(value) {
152
+ const trimmed = value?.trim();
153
+ return trimmed && trimmed.toLowerCase() !== "undefined" ? trimmed : null;
154
+ }
155
+ var wgs84Statement = (authority, label) => ({ authority, definition: wgs84Definition, label });
156
+ function epsgDefinition(code) {
157
+ if (code === 4326) return wgs84Definition;
158
+ if (code === 3857 || code === 900913) {
159
+ return "+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +no_defs";
160
+ }
161
+ if (code === 4258)
162
+ return "+proj=longlat +ellps=GRS80 +datum=ETRS89 +towgs84=0,0,0,0,0,0,0 +no_defs";
163
+ if (code === 4269)
164
+ return "+proj=longlat +ellps=GRS80 +datum=NAD83 +towgs84=0,0,0,0,0,0,0 +no_defs";
165
+ const zone = code % 100;
166
+ if (code >= 32601 && code <= 32660) return `+proj=utm +zone=${zone} +datum=WGS84 +no_defs`;
167
+ if (code >= 32701 && code <= 32760) return `+proj=utm +zone=${zone} +south +datum=WGS84 +no_defs`;
168
+ if (code >= 25801 && code <= 25860 || code >= 26901 && code <= 26923) {
169
+ return `+proj=utm +zone=${zone} +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs`;
170
+ }
171
+ return null;
172
+ }
173
+ function parseDefinition(definition, label) {
174
+ try {
175
+ return new proj4.Proj(definition);
176
+ } catch {
177
+ throw new TilesetBuildError(
178
+ "TF_TILESET_BUILD_CRS_UNREADABLE",
179
+ "crs",
180
+ `${label} could not be parsed as a coordinate reference system. Pass --source-crs with a proj string, WKT, or .prj path.`
181
+ );
182
+ }
183
+ }
184
+
185
+ // src/tileset-build/recipe.ts
186
+ var tilesetRecipeSchemaUrl = "https://tileflow.dev/schemas/tileset-recipe/v1.json";
187
+ var maximumZoom = 16;
188
+ var layerNamePattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,191}$/u;
189
+ var maximumRecipeBytes = 1024 * 1024;
190
+ var nonEmpty = z.string().min(1);
191
+ var zoom = z.number().int().min(0).max(maximumZoom);
192
+ var entryName = z.string().regex(layerNamePattern);
193
+ var sourceSchema = z.strictObject({
194
+ crs: z.strictObject({
195
+ from: nonEmpty.refine(isInlineCrs, {
196
+ message: "must be EPSG:<code>, a proj string or WKT; a recipe cannot name a file"
197
+ }).optional(),
198
+ reproject: z.boolean().optional()
199
+ }).optional(),
200
+ csv: z.strictObject({ lat: nonEmpty.optional(), lon: nonEmpty.optional() }).optional(),
201
+ format: z.enum(["csv", "geojson", "geojsonseq", "geopackage", "gpx", "kml", "shapefile"]).optional(),
202
+ layer: nonEmpty.optional(),
203
+ zipEntry: nonEmpty.optional()
204
+ });
205
+ var layerSchema = z.strictObject({
206
+ featureId: z.strictObject({ field: nonEmpty, hash: z.boolean().optional() }).optional(),
207
+ fields: z.strictObject({ exclude: z.array(nonEmpty).optional(), include: z.array(nonEmpty).optional() }).refine((fields) => !(fields.exclude && fields.include), {
208
+ message: "include and exclude cannot be combined"
209
+ }).optional(),
210
+ maxzoom: z.union([z.literal("auto"), zoom]).optional(),
211
+ minzoom: zoom.optional(),
212
+ rank: z.strictObject({ by: nonEmpty, order: z.enum(["ascending", "descending"]).optional() }).optional(),
213
+ source: nonEmpty
214
+ });
215
+ var recipeObject = z.strictObject({
216
+ $schema: z.string().optional(),
217
+ layers: z.record(entryName, layerSchema),
218
+ metadata: z.strictObject({
219
+ attribution: nonEmpty.optional(),
220
+ description: nonEmpty.optional(),
221
+ name: nonEmpty.optional()
222
+ }).optional(),
223
+ sources: z.record(entryName, sourceSchema),
224
+ version: z.literal(1)
225
+ });
226
+ var recipeSchema = recipeObject.superRefine((recipe, context) => {
227
+ for (const key of ["sources", "layers"]) {
228
+ if (Object.keys(recipe[key]).length !== 1) {
229
+ context.addIssue({ code: "custom", message: "needs exactly one entry", path: [key] });
230
+ }
231
+ }
232
+ for (const [name, layer] of Object.entries(recipe.layers)) {
233
+ if (!Object.hasOwn(recipe.sources, layer.source)) {
234
+ context.addIssue({
235
+ code: "custom",
236
+ message: `names no source in "sources"`,
237
+ path: ["layers", name, "source"]
238
+ });
239
+ }
240
+ if (typeof layer.maxzoom === "number" && layer.maxzoom < (layer.minzoom ?? 0)) {
241
+ context.addIssue({
242
+ code: "custom",
243
+ message: "is below minzoom",
244
+ path: ["layers", name, "maxzoom"]
245
+ });
246
+ }
247
+ }
248
+ });
249
+ var present = (key, value) => value === void 0 ? {} : { [key]: value };
250
+ function recipeFromSettings(settings, layer) {
251
+ const csv = { ...present("lat", settings.latColumn), ...present("lon", settings.lonColumn) };
252
+ const crs = {
253
+ ...settings.sourceCrs === void 0 ? {} : { from: portableCrs(settings.sourceCrs) },
254
+ ...settings.reproject ? { reproject: true } : {}
255
+ };
256
+ const fields = { ...present("exclude", settings.exclude), ...present("include", settings.include) };
257
+ const metadata = {
258
+ ...present("attribution", settings.attribution),
259
+ ...present("description", settings.description),
260
+ ...present("name", settings.name)
261
+ };
262
+ return {
263
+ $schema: tilesetRecipeSchemaUrl,
264
+ layers: {
265
+ [layer]: {
266
+ ...present("featureId", settings.featureId),
267
+ ...Object.keys(fields).length > 0 ? { fields } : {},
268
+ maxzoom: settings.maxzoom ?? "auto",
269
+ minzoom: settings.minzoom ?? 0,
270
+ ...settings.rankBy === void 0 ? {} : {
271
+ rank: {
272
+ by: settings.rankBy,
273
+ order: settings.rankAscending ? "ascending" : "descending"
274
+ }
275
+ },
276
+ source: layer
277
+ }
278
+ },
279
+ ...Object.keys(metadata).length > 0 ? { metadata } : {},
280
+ sources: {
281
+ [layer]: {
282
+ ...Object.keys(crs).length > 0 ? { crs } : {},
283
+ ...Object.keys(csv).length > 0 ? { csv } : {},
284
+ ...present("format", settings.format),
285
+ ...present("layer", settings.inputLayer),
286
+ ...present("zipEntry", settings.zipEntry)
287
+ }
288
+ },
289
+ version: 1
290
+ };
291
+ }
292
+ function settingsFromRecipe(recipe) {
293
+ const [layerName, layer] = Object.entries(recipe.layers)[0];
294
+ const source = recipe.sources[layer.source];
295
+ return {
296
+ ...present("attribution", recipe.metadata?.attribution),
297
+ ...present("description", recipe.metadata?.description),
298
+ ...present("exclude", layer.fields?.exclude),
299
+ ...present("featureId", layer.featureId),
300
+ ...present("format", source.format),
301
+ ...present("include", layer.fields?.include),
302
+ ...present("inputLayer", source.layer),
303
+ ...present("latColumn", source.csv?.lat),
304
+ layer: layerName,
305
+ ...present("lonColumn", source.csv?.lon),
306
+ ...typeof layer.maxzoom === "number" ? { maxzoom: layer.maxzoom } : {},
307
+ ...present("minzoom", layer.minzoom),
308
+ ...present("name", recipe.metadata?.name),
309
+ ...layer.rank?.order === "ascending" ? { rankAscending: true } : {},
310
+ ...present("rankBy", layer.rank?.by),
311
+ ...source.crs?.reproject ? { reproject: true } : {},
312
+ ...present("sourceCrs", source.crs?.from),
313
+ ...present("zipEntry", source.zipEntry)
314
+ };
315
+ }
316
+ function parseTilesetRecipe(text) {
317
+ let document;
318
+ try {
319
+ document = JSON.parse(text);
320
+ } catch {
321
+ throw new TilesetBuildError(
322
+ "TF_TILESET_BUILD_RECIPE_INVALID",
323
+ "recipe",
324
+ "The recipe is not valid JSON."
325
+ );
326
+ }
327
+ const parsed = recipeSchema.safeParse(document);
328
+ if (parsed.success) return parsed.data;
329
+ const issue = parsed.error.issues[0];
330
+ const location = issue.path.join(".");
331
+ throw new TilesetBuildError(
332
+ "TF_TILESET_BUILD_RECIPE_INVALID",
333
+ location ? `recipe.${location}` : "recipe",
334
+ `Invalid recipe${location ? ` at ${location}` : ""}: ${issue.message}.`
335
+ );
336
+ }
337
+ function readTilesetRecipe(path) {
338
+ let bytes;
339
+ try {
340
+ bytes = readFileSync2(path);
341
+ } catch {
342
+ throw new TilesetBuildError(
343
+ "TF_TILESET_BUILD_RECIPE_UNREADABLE",
344
+ "recipe",
345
+ `Could not read the recipe ${path}.`
346
+ );
347
+ }
348
+ if (bytes.length > maximumRecipeBytes) {
349
+ throw new TilesetBuildError(
350
+ "TF_TILESET_BUILD_RECIPE_INVALID",
351
+ "recipe",
352
+ `The recipe is larger than ${maximumRecipeBytes} bytes.`
353
+ );
354
+ }
355
+ return parseTilesetRecipe(bytes.toString("utf8"));
356
+ }
357
+ function tilesetRecipeJsonSchema() {
358
+ return {
359
+ $id: tilesetRecipeSchemaUrl,
360
+ ...z.toJSONSchema(recipeObject, { target: "draft-2020-12" })
361
+ };
362
+ }
363
+ function recipeHash(recipe) {
364
+ return `sha256:${createHash("sha256").update(serializeCanonicalJson(recipe)).digest("hex")}`;
365
+ }
366
+
367
+ export {
368
+ resolveCrs,
369
+ parseSourceCrsOption,
370
+ statementFromGeojsonCrs,
371
+ statementFromGeopackageSrs,
372
+ wgs84Statement,
373
+ tilesetRecipeSchemaUrl,
374
+ maximumZoom,
375
+ layerNamePattern,
376
+ recipeFromSettings,
377
+ settingsFromRecipe,
378
+ parseTilesetRecipe,
379
+ readTilesetRecipe,
380
+ tilesetRecipeJsonSchema,
381
+ recipeHash
382
+ };