@tileflow/core 0.1.0-alpha.20 → 0.1.0-alpha.21
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.
- package/GENERATED_OUTPUT_LICENSE.md +21 -0
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +969 -142
- package/THIRD_PARTY_NOTICES.md +185 -0
- package/TRADEMARKS.md +10 -0
- package/dist/browser.d.ts +105 -2
- package/dist/browser.js +3135 -11
- package/dist/build.d.ts +195 -0
- package/dist/build.js +12254 -0
- package/dist/capture-scene-BK_0J2oR.d.ts +161 -0
- package/dist/capture.d.ts +9 -0
- package/dist/capture.js +206 -0
- package/dist/domains-CPSrdTik.d.ts +4 -0
- package/dist/index.d.ts +180 -182
- package/dist/index.js +12259 -4245
- package/dist/manifest.d.ts +3 -0
- package/dist/manifest.js +229 -0
- package/dist/recipe.d.ts +66 -0
- package/dist/recipe.js +577 -0
- package/dist/requirements-BV77ZVhR.d.ts +491 -0
- package/dist/runtime-C4MtSzkB.d.ts +223 -0
- package/dist/runtime.d.ts +3 -0
- package/dist/runtime.js +1117 -0
- package/dist/types-DlUGI6LC.d.ts +525 -0
- package/dist/visual-semantics-DTC-8Hsb.d.ts +479 -0
- package/package.json +40 -5
- package/dist/runtime-DroKDIuB.d.ts +0 -1069
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,1117 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res, err) => function __init() {
|
|
4
|
+
if (err) throw err[0];
|
|
5
|
+
try {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
} catch (e) {
|
|
8
|
+
throw err = [e], e;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/portable-identity-rules.ts
|
|
17
|
+
function isTileflowPortableId(value) {
|
|
18
|
+
return typeof value === "string" && value.length <= tileflowPortableIdMaximumLength && tileflowPortableIdPattern.test(value) && !["constructor", "prototype"].includes(value) && !/^(?:AUX|CON|NUL|PRN|COM[1-9]|LPT[1-9])$/i.test(value);
|
|
19
|
+
}
|
|
20
|
+
function isTileflowThemeName(value) {
|
|
21
|
+
return isTileflowPortableId(value) && value !== "system";
|
|
22
|
+
}
|
|
23
|
+
var tileflowPortableIdMaximumLength, tileflowPortableIdPattern;
|
|
24
|
+
var init_portable_identity_rules = __esm({
|
|
25
|
+
"src/portable-identity-rules.ts"() {
|
|
26
|
+
"use strict";
|
|
27
|
+
tileflowPortableIdMaximumLength = 64;
|
|
28
|
+
tileflowPortableIdPattern = /^[a-z][a-z0-9-]{0,63}$/u;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// src/portable-identity.ts
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
var tileflowPortableIdSchema, tileflowThemeNameSchema;
|
|
35
|
+
var init_portable_identity = __esm({
|
|
36
|
+
"src/portable-identity.ts"() {
|
|
37
|
+
"use strict";
|
|
38
|
+
init_portable_identity_rules();
|
|
39
|
+
tileflowPortableIdSchema = z.string().min(1).max(tileflowPortableIdMaximumLength).regex(tileflowPortableIdPattern, {
|
|
40
|
+
message: "Expected lowercase kebab-case beginning with a letter"
|
|
41
|
+
}).refine(
|
|
42
|
+
(name) => !["constructor", "prototype"].includes(name) && !/^(?:AUX|CON|NUL|PRN|COM[1-9]|LPT[1-9])$/i.test(name),
|
|
43
|
+
"Expected a portable identifier that is not a reserved filename or prototype key"
|
|
44
|
+
);
|
|
45
|
+
tileflowThemeNameSchema = tileflowPortableIdSchema.refine(
|
|
46
|
+
isTileflowThemeName,
|
|
47
|
+
"Expected a concrete theme name; system is browser-only"
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// src/manifest.ts
|
|
53
|
+
var manifest_exports = {};
|
|
54
|
+
__export(manifest_exports, {
|
|
55
|
+
parseTileflowRuntimeManifest: () => parseTileflowRuntimeManifest,
|
|
56
|
+
safeParseTileflowRuntimeManifest: () => safeParseTileflowRuntimeManifest,
|
|
57
|
+
tileflowRuntimeManifestLimits: () => tileflowRuntimeManifestLimits,
|
|
58
|
+
tileflowRuntimeManifestSchema: () => tileflowRuntimeManifestSchema,
|
|
59
|
+
tileflowRuntimeManifestVersion: () => tileflowRuntimeManifestVersion
|
|
60
|
+
});
|
|
61
|
+
import { z as z2 } from "zod";
|
|
62
|
+
function parseTileflowRuntimeManifest(input) {
|
|
63
|
+
return tileflowRuntimeManifestSchema.parse(input);
|
|
64
|
+
}
|
|
65
|
+
function safeParseTileflowRuntimeManifest(input) {
|
|
66
|
+
return tileflowRuntimeManifestSchema.safeParse(input);
|
|
67
|
+
}
|
|
68
|
+
function validateUniqueFontFaces(fontFaces, context) {
|
|
69
|
+
const seen = /* @__PURE__ */ new Map();
|
|
70
|
+
for (const [index, fontFace] of fontFaces.entries()) {
|
|
71
|
+
const identity = `${fontFace.family}\0${fontFace.style ?? "normal"}\0${fontFace.weight ?? "400"}`;
|
|
72
|
+
const first = seen.get(identity);
|
|
73
|
+
if (first !== void 0) {
|
|
74
|
+
context.addIssue({
|
|
75
|
+
code: "custom",
|
|
76
|
+
message: `Duplicate font face identity; first declared at index ${first}`,
|
|
77
|
+
path: [index]
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
seen.set(identity, index);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function isPublicResourceUrl(value) {
|
|
85
|
+
if (value.startsWith("//")) return false;
|
|
86
|
+
if (!value.startsWith("/") && !value.startsWith("./") && !value.startsWith("../") && !isAbsolutePublicUrl(value)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return isSafeResolvedPublicUrl(value);
|
|
90
|
+
}
|
|
91
|
+
function isAbsolutePublicUrl(value) {
|
|
92
|
+
try {
|
|
93
|
+
const url = new URL(value);
|
|
94
|
+
return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password && !url.hash && !hasEncodedPathSeparator(value);
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function isSafeResolvedPublicUrl(value) {
|
|
100
|
+
try {
|
|
101
|
+
const url = new URL(value, "https://manifest.invalid/owner/manifest.json");
|
|
102
|
+
return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password && !url.hash && !hasEncodedPathSeparator(value);
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function hasEncodedPathSeparator(value) {
|
|
108
|
+
return /%(?:2f|5c)/iu.test(value);
|
|
109
|
+
}
|
|
110
|
+
function isApiOrigin(value) {
|
|
111
|
+
try {
|
|
112
|
+
const url = new URL(value);
|
|
113
|
+
return isAbsolutePublicUrl(value) && url.pathname === "/" && !url.search && value.replace(/\/$/u, "") === url.origin;
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function findUnsafeManifestStructure(input) {
|
|
119
|
+
if (!input || typeof input !== "object") return void 0;
|
|
120
|
+
const pending = [{ path: [], value: input }];
|
|
121
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
122
|
+
while (pending.length > 0) {
|
|
123
|
+
const current = pending.pop();
|
|
124
|
+
if (visited.has(current.value)) continue;
|
|
125
|
+
visited.add(current.value);
|
|
126
|
+
if (!Array.isArray(current.value)) {
|
|
127
|
+
const prototype = Object.getPrototypeOf(current.value);
|
|
128
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
129
|
+
return { path: current.path, reason: "prototype" };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const key of Object.keys(current.value)) {
|
|
133
|
+
const path = [...current.path, Array.isArray(current.value) ? Number(key) : key];
|
|
134
|
+
if (key === "__proto__") return { path, reason: "key" };
|
|
135
|
+
const descriptor = Object.getOwnPropertyDescriptor(current.value, key);
|
|
136
|
+
if (descriptor && "value" in descriptor && descriptor.value && typeof descriptor.value === "object") {
|
|
137
|
+
pending.push({ path, value: descriptor.value });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
var tileflowRuntimeManifestVersion, tileflowRuntimeManifestLimits, safeTextSchema, publicResourceUrlSchema, apiOriginSchema, viewSchema, fontFaceSchema, fontFacesSchema, manifestThemeSchema, manifestMapSchema, canonicalManifestSchema, safeManifestInputSchema, tileflowRuntimeManifestSchema;
|
|
144
|
+
var init_manifest = __esm({
|
|
145
|
+
"src/manifest.ts"() {
|
|
146
|
+
"use strict";
|
|
147
|
+
init_portable_identity();
|
|
148
|
+
tileflowRuntimeManifestVersion = 1;
|
|
149
|
+
tileflowRuntimeManifestLimits = Object.freeze({ maximumBytes: 1024 * 1024 });
|
|
150
|
+
safeTextSchema = z2.string().min(1).max(2048).refine((value) => value === value.trim(), "Expected text without surrounding whitespace").refine((value) => !/[\p{Cc}\\]/u.test(value), "Expected safe text");
|
|
151
|
+
publicResourceUrlSchema = safeTextSchema.refine(isPublicResourceUrl, {
|
|
152
|
+
message: "Expected an HTTP(S), root-relative, or path-relative public URL"
|
|
153
|
+
});
|
|
154
|
+
apiOriginSchema = safeTextSchema.refine(isApiOrigin, {
|
|
155
|
+
message: "Expected an absolute HTTP(S) origin without path, credentials, query, or fragment"
|
|
156
|
+
});
|
|
157
|
+
viewSchema = z2.object({
|
|
158
|
+
bearing: z2.number().finite().min(-180).max(180).optional(),
|
|
159
|
+
center: z2.tuple([z2.number().finite().min(-180).max(180), z2.number().finite().min(-90).max(90)]).optional(),
|
|
160
|
+
pitch: z2.number().finite().min(0).max(85).optional(),
|
|
161
|
+
zoom: z2.number().finite().min(0).max(24).optional()
|
|
162
|
+
}).strict();
|
|
163
|
+
fontFaceSchema = z2.object({
|
|
164
|
+
family: safeTextSchema.max(100),
|
|
165
|
+
source: publicResourceUrlSchema,
|
|
166
|
+
style: z2.enum(["italic", "normal", "oblique"]).optional(),
|
|
167
|
+
weight: z2.enum(["100", "200", "300", "400", "500", "600", "700", "800", "900"]).optional()
|
|
168
|
+
}).strict();
|
|
169
|
+
fontFacesSchema = z2.array(fontFaceSchema).max(16).superRefine(validateUniqueFontFaces);
|
|
170
|
+
manifestThemeSchema = z2.object({
|
|
171
|
+
colorScheme: z2.enum(["dark", "light"]),
|
|
172
|
+
fontFaces: fontFacesSchema.optional(),
|
|
173
|
+
revision: safeTextSchema.max(128).optional(),
|
|
174
|
+
styleId: safeTextSchema.max(128).optional(),
|
|
175
|
+
styleUrl: publicResourceUrlSchema
|
|
176
|
+
}).strict();
|
|
177
|
+
manifestMapSchema = z2.object({
|
|
178
|
+
apiUrl: apiOriginSchema.optional(),
|
|
179
|
+
defaultTheme: tileflowThemeNameSchema,
|
|
180
|
+
environment: safeTextSchema.max(128).optional(),
|
|
181
|
+
mapId: safeTextSchema.max(128).optional(),
|
|
182
|
+
systemThemes: z2.object({ dark: tileflowThemeNameSchema, light: tileflowThemeNameSchema }).strict().optional(),
|
|
183
|
+
themes: z2.record(tileflowThemeNameSchema, manifestThemeSchema),
|
|
184
|
+
usageMode: z2.literal("session").optional(),
|
|
185
|
+
view: viewSchema.optional(),
|
|
186
|
+
worldGeneration: z2.literal("v1").optional()
|
|
187
|
+
}).strict().superRefine((entry, context) => {
|
|
188
|
+
const names = Object.keys(entry.themes);
|
|
189
|
+
if (names.length === 0) {
|
|
190
|
+
context.addIssue({ code: "custom", message: "Expected at least one theme", path: ["themes"] });
|
|
191
|
+
}
|
|
192
|
+
if (names.length > 64) {
|
|
193
|
+
context.addIssue({ code: "too_big", maximum: 64, origin: "object", path: ["themes"] });
|
|
194
|
+
}
|
|
195
|
+
if (!Object.hasOwn(entry.themes, entry.defaultTheme)) {
|
|
196
|
+
context.addIssue({
|
|
197
|
+
code: "custom",
|
|
198
|
+
message: "defaultTheme must name a declared theme",
|
|
199
|
+
path: ["defaultTheme"]
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (entry.systemThemes) {
|
|
203
|
+
for (const colorScheme of ["light", "dark"]) {
|
|
204
|
+
const name = entry.systemThemes[colorScheme];
|
|
205
|
+
const theme = entry.themes[name];
|
|
206
|
+
if (!theme) {
|
|
207
|
+
context.addIssue({
|
|
208
|
+
code: "custom",
|
|
209
|
+
message: `systemThemes.${colorScheme} must name a declared theme`,
|
|
210
|
+
path: ["systemThemes", colorScheme]
|
|
211
|
+
});
|
|
212
|
+
} else if (theme.colorScheme !== colorScheme) {
|
|
213
|
+
context.addIssue({
|
|
214
|
+
code: "custom",
|
|
215
|
+
message: `systemThemes.${colorScheme} must reference a ${colorScheme} theme`,
|
|
216
|
+
path: ["systemThemes", colorScheme]
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (entry.usageMode === void 0 !== (entry.worldGeneration === void 0)) {
|
|
222
|
+
context.addIssue({
|
|
223
|
+
code: "custom",
|
|
224
|
+
message: "usageMode and worldGeneration must be declared together"
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
canonicalManifestSchema = z2.object({
|
|
229
|
+
apiUrl: apiOriginSchema.optional(),
|
|
230
|
+
maps: z2.record(tileflowPortableIdSchema, manifestMapSchema),
|
|
231
|
+
version: z2.literal(tileflowRuntimeManifestVersion)
|
|
232
|
+
}).strict().superRefine((manifest, context) => {
|
|
233
|
+
const mapCount = Object.keys(manifest.maps).length;
|
|
234
|
+
if (mapCount === 0) {
|
|
235
|
+
context.addIssue({ code: "custom", message: "Expected at least one map", path: ["maps"] });
|
|
236
|
+
}
|
|
237
|
+
if (mapCount > 1e3) {
|
|
238
|
+
context.addIssue({ code: "too_big", maximum: 1e3, origin: "object", path: ["maps"] });
|
|
239
|
+
}
|
|
240
|
+
if (new TextEncoder().encode(JSON.stringify(manifest)).byteLength > tileflowRuntimeManifestLimits.maximumBytes) {
|
|
241
|
+
context.addIssue({
|
|
242
|
+
code: "too_big",
|
|
243
|
+
maximum: tileflowRuntimeManifestLimits.maximumBytes,
|
|
244
|
+
origin: "string",
|
|
245
|
+
message: "Manifest JSON exceeds the 1 MiB limit"
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
safeManifestInputSchema = z2.unknown().superRefine((input, context) => {
|
|
250
|
+
const unsafe = findUnsafeManifestStructure(input);
|
|
251
|
+
if (unsafe) {
|
|
252
|
+
context.addIssue({
|
|
253
|
+
code: "custom",
|
|
254
|
+
message: unsafe.reason === "prototype" ? "Expected plain manifest objects" : "Expected no prototype-mutating keys",
|
|
255
|
+
path: unsafe.path
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
tileflowRuntimeManifestSchema = safeManifestInputSchema.pipe(canonicalManifestSchema);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// src/runtime.ts
|
|
264
|
+
init_portable_identity_rules();
|
|
265
|
+
var defaultTileflowManifestUrl = "/tileflow/manifest.json";
|
|
266
|
+
var defaultTileflowRuntimeView = Object.freeze({
|
|
267
|
+
bearing: 0,
|
|
268
|
+
center: [0, 20],
|
|
269
|
+
pitch: 0,
|
|
270
|
+
zoom: 2
|
|
271
|
+
});
|
|
272
|
+
var tileflowStyleFontFacesMetadataKey = "tileflow:fontFaces";
|
|
273
|
+
var tileflowStyleFontFaceLimits = Object.freeze({
|
|
274
|
+
maximumCount: 16,
|
|
275
|
+
maximumSourceLength: 2048
|
|
276
|
+
});
|
|
277
|
+
var maxStaticImageDimension = 1280;
|
|
278
|
+
var maxStaticImagePixels = 1280 * 1280;
|
|
279
|
+
var defaultManifestCacheTtlMs = 3e4;
|
|
280
|
+
var maximumManifestBytes = 1024 * 1024;
|
|
281
|
+
var maximumSessionGrantBytes = 64 * 1024;
|
|
282
|
+
var manifestCache = new globalThis.Map();
|
|
283
|
+
var missingStaticMapIdWarnings = /* @__PURE__ */ new Set();
|
|
284
|
+
function shouldLoadTileflowManifest(options) {
|
|
285
|
+
return options.source.kind === "tileflow";
|
|
286
|
+
}
|
|
287
|
+
function validateTileflowRuntimeSource(source) {
|
|
288
|
+
if (source === void 0) {
|
|
289
|
+
return { code: "missing-source", error: "source is required", ok: false };
|
|
290
|
+
}
|
|
291
|
+
if (!isPlainRuntimeRecord(source)) {
|
|
292
|
+
return { code: "invalid-source", error: "source must be an object", ok: false };
|
|
293
|
+
}
|
|
294
|
+
if (source.kind === "tileflow") {
|
|
295
|
+
if (!isTileflowPortableId(source.map) || source.manifestUrl !== void 0 && (typeof source.manifestUrl !== "string" || source.manifestUrl.length === 0)) {
|
|
296
|
+
return {
|
|
297
|
+
code: "invalid-source",
|
|
298
|
+
error: "a tileflow source requires a portable map id and an optional non-empty manifestUrl",
|
|
299
|
+
ok: false
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return { ok: true };
|
|
303
|
+
}
|
|
304
|
+
if (source.kind === "maplibre") {
|
|
305
|
+
const style = source.style;
|
|
306
|
+
if ((typeof style !== "string" || style.length === 0) && (!style || typeof style !== "object" || Array.isArray(style))) {
|
|
307
|
+
return {
|
|
308
|
+
code: "invalid-source",
|
|
309
|
+
error: "a maplibre source requires a style object or non-empty style URL",
|
|
310
|
+
ok: false
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return { ok: true };
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
code: "invalid-source",
|
|
317
|
+
error: "source.kind must be 'tileflow' or 'maplibre'",
|
|
318
|
+
ok: false
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function validateTileflowThemeSelection(value) {
|
|
322
|
+
return value === "system" || isTileflowThemeName(value);
|
|
323
|
+
}
|
|
324
|
+
function assertValidTileflowRuntimeSource(source) {
|
|
325
|
+
const validation = validateTileflowRuntimeSource(source);
|
|
326
|
+
if (!validation.ok) {
|
|
327
|
+
throw new TypeError(`Invalid Tileflow runtime source: ${validation.error}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function resolveTileflowRuntimeStyle(options) {
|
|
331
|
+
assertValidTileflowRuntimeSource(options.source);
|
|
332
|
+
if (options.source.kind === "maplibre") {
|
|
333
|
+
if (options.theme !== void 0) {
|
|
334
|
+
throw new TypeError("The theme option is only valid for a Tileflow map source.");
|
|
335
|
+
}
|
|
336
|
+
if (typeof options.source.style !== "string") {
|
|
337
|
+
return {
|
|
338
|
+
fontFaces: getTileflowStyleFontFaces(options.source.style),
|
|
339
|
+
style: options.source.style
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
analytics: inferTileflowAnalyticsFromStyleUrl(options.source.style),
|
|
344
|
+
style: options.source.style
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
if (options.manifestMap) {
|
|
348
|
+
const theme = resolveTileflowRuntimeTheme(
|
|
349
|
+
options.manifestMap,
|
|
350
|
+
options.theme,
|
|
351
|
+
options.colorScheme
|
|
352
|
+
);
|
|
353
|
+
return {
|
|
354
|
+
analytics: {
|
|
355
|
+
apiUrl: options.manifestMap.apiUrl,
|
|
356
|
+
mapId: options.manifestMap.mapId,
|
|
357
|
+
styleId: theme.styleId
|
|
358
|
+
},
|
|
359
|
+
colorScheme: theme.colorScheme,
|
|
360
|
+
...theme.fontFaces === void 0 ? {} : { fontFaces: [...theme.fontFaces] },
|
|
361
|
+
revision: theme.revision,
|
|
362
|
+
style: theme.styleUrl,
|
|
363
|
+
theme: theme.name
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
function resolveTileflowRuntimeTheme(map, selection, colorScheme) {
|
|
369
|
+
let name;
|
|
370
|
+
if (selection === void 0) {
|
|
371
|
+
name = map.defaultTheme;
|
|
372
|
+
} else if (selection === "system") {
|
|
373
|
+
if (!map.systemThemes) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
`Tileflow map "${map.name}" does not declare systemThemes; select a concrete theme.`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
if (!colorScheme) {
|
|
379
|
+
throw new Error('Resolving theme="system" requires an explicit browser color scheme.');
|
|
380
|
+
}
|
|
381
|
+
name = map.systemThemes[colorScheme];
|
|
382
|
+
} else {
|
|
383
|
+
name = selection;
|
|
384
|
+
}
|
|
385
|
+
if (!isTileflowThemeName(name)) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
`Tileflow resolved invalid theme ${JSON.stringify(name)} for map "${map.name}"; expected a concrete portable theme name.`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
const theme = Object.hasOwn(map.themes, name) ? map.themes[name] : void 0;
|
|
391
|
+
if (!theme) {
|
|
392
|
+
const available = Object.keys(map.themes).sort().join(", ");
|
|
393
|
+
throw new Error(
|
|
394
|
+
`Unknown Tileflow theme "${name}" for map "${map.name}". Available themes: ${available}.`
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
return { ...theme, name };
|
|
398
|
+
}
|
|
399
|
+
function getTileflowStyleFontFaces(style) {
|
|
400
|
+
const input = style.metadata?.[tileflowStyleFontFacesMetadataKey];
|
|
401
|
+
if (input === void 0) return [];
|
|
402
|
+
if (!Array.isArray(input) || input.length > tileflowStyleFontFaceLimits.maximumCount) {
|
|
403
|
+
throw new TypeError("Invalid Tileflow style fontFaces metadata.");
|
|
404
|
+
}
|
|
405
|
+
const seen = /* @__PURE__ */ new Set();
|
|
406
|
+
return input.map((value) => {
|
|
407
|
+
if (!isPlainRuntimeRecord(value)) {
|
|
408
|
+
throw new TypeError("Invalid Tileflow style fontFaces metadata.");
|
|
409
|
+
}
|
|
410
|
+
const keys = Object.keys(value);
|
|
411
|
+
if (keys.some((key2) => !["family", "source", "style", "weight"].includes(key2))) {
|
|
412
|
+
throw new TypeError("Invalid Tileflow style fontFaces metadata.");
|
|
413
|
+
}
|
|
414
|
+
const family = value.family;
|
|
415
|
+
const source = value.source;
|
|
416
|
+
const fontStyle = value.style;
|
|
417
|
+
const weight = value.weight;
|
|
418
|
+
if (typeof family !== "string" || family.length === 0 || family.length > 100 || family !== family.trim() || /[\p{Cc}\\]/u.test(family) || typeof source !== "string" || source.length === 0 || source.length > tileflowStyleFontFaceLimits.maximumSourceLength || source !== source.trim() || /[\p{Cc}\\]/u.test(source) || !isTileflowPublicFontUrl(source) || fontStyle !== void 0 && !["italic", "normal", "oblique"].includes(String(fontStyle)) || weight !== void 0 && !/^[1-9]00$/u.test(String(weight))) {
|
|
419
|
+
throw new TypeError("Invalid Tileflow style fontFaces metadata.");
|
|
420
|
+
}
|
|
421
|
+
const face = {
|
|
422
|
+
family,
|
|
423
|
+
source,
|
|
424
|
+
...fontStyle === void 0 ? {} : { style: fontStyle },
|
|
425
|
+
...weight === void 0 ? {} : { weight }
|
|
426
|
+
};
|
|
427
|
+
const key = `${face.family}\0${face.style ?? "normal"}\0${face.weight ?? "400"}`;
|
|
428
|
+
if (seen.has(key)) throw new TypeError("Invalid duplicate Tileflow style fontFace metadata.");
|
|
429
|
+
seen.add(key);
|
|
430
|
+
return face;
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function normalizeTileflowUrl(value) {
|
|
434
|
+
return value.replace(/\/+$/, "");
|
|
435
|
+
}
|
|
436
|
+
function createTileflowSessionId() {
|
|
437
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
|
438
|
+
return `ses_${crypto.randomUUID()}`;
|
|
439
|
+
}
|
|
440
|
+
return `ses_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
|
|
441
|
+
}
|
|
442
|
+
function createTileflowSessionController(input) {
|
|
443
|
+
const fetchGrant = input.fetch ?? globalThis.fetch;
|
|
444
|
+
const grantTimeoutMs = validateSessionGrantTimeout(input.grantTimeoutMs);
|
|
445
|
+
const now = input.now ?? (() => /* @__PURE__ */ new Date());
|
|
446
|
+
const sessionIdFactory = input.sessionIdFactory ?? createTileflowSessionId;
|
|
447
|
+
const createSession = (startedAt) => ({
|
|
448
|
+
binding: null,
|
|
449
|
+
grant: null,
|
|
450
|
+
grantPromise: null,
|
|
451
|
+
pendingGrantRequests: 0,
|
|
452
|
+
requestCount: 0,
|
|
453
|
+
sessionId: sessionIdFactory(),
|
|
454
|
+
startedAt
|
|
455
|
+
});
|
|
456
|
+
let activeSession = createSession(now().getTime());
|
|
457
|
+
async function resolveGrant(session, analytics, at) {
|
|
458
|
+
if (session.grant && session.grant.expiresAt - at > 3e4) {
|
|
459
|
+
return { grant: session.grant, session };
|
|
460
|
+
}
|
|
461
|
+
session.pendingGrantRequests += 1;
|
|
462
|
+
try {
|
|
463
|
+
if (!session.grantPromise) {
|
|
464
|
+
const pending = (async () => {
|
|
465
|
+
try {
|
|
466
|
+
const nextGrant = await requestSessionGrant({
|
|
467
|
+
analytics,
|
|
468
|
+
at,
|
|
469
|
+
fetchGrant,
|
|
470
|
+
timeoutMs: grantTimeoutMs,
|
|
471
|
+
sessionId: session.sessionId,
|
|
472
|
+
source: input.source
|
|
473
|
+
});
|
|
474
|
+
session.grant = nextGrant;
|
|
475
|
+
return { grant: nextGrant, session };
|
|
476
|
+
} catch (error) {
|
|
477
|
+
if (!(error instanceof TileflowSessionRestartError)) throw error;
|
|
478
|
+
const replacement = createSession(at);
|
|
479
|
+
replacement.binding = session.binding;
|
|
480
|
+
replacement.requestCount = session.pendingGrantRequests;
|
|
481
|
+
if (activeSession === session) activeSession = replacement;
|
|
482
|
+
const replacementPending = requestSessionGrant({
|
|
483
|
+
analytics,
|
|
484
|
+
at,
|
|
485
|
+
fetchGrant,
|
|
486
|
+
timeoutMs: grantTimeoutMs,
|
|
487
|
+
sessionId: replacement.sessionId,
|
|
488
|
+
source: input.source
|
|
489
|
+
}).then((nextGrant) => {
|
|
490
|
+
replacement.grant = nextGrant;
|
|
491
|
+
return { grant: nextGrant, session: replacement };
|
|
492
|
+
});
|
|
493
|
+
replacement.grantPromise = replacementPending;
|
|
494
|
+
try {
|
|
495
|
+
return await replacementPending;
|
|
496
|
+
} finally {
|
|
497
|
+
if (replacement.grantPromise === replacementPending) {
|
|
498
|
+
replacement.grantPromise = null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
} finally {
|
|
502
|
+
session.grantPromise = null;
|
|
503
|
+
}
|
|
504
|
+
})();
|
|
505
|
+
session.grantPromise = pending;
|
|
506
|
+
}
|
|
507
|
+
return await session.grantPromise;
|
|
508
|
+
} finally {
|
|
509
|
+
session.pendingGrantRequests -= 1;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
get sessionId() {
|
|
514
|
+
return activeSession.sessionId;
|
|
515
|
+
},
|
|
516
|
+
async resolveRequestUrl(url, analytics) {
|
|
517
|
+
if (!analytics || !analytics.mapId) return void 0;
|
|
518
|
+
const at = now().getTime();
|
|
519
|
+
if (at - activeSession.startedAt >= 6 * 60 * 60 * 1e3 || activeSession.requestCount >= 1e4) {
|
|
520
|
+
activeSession = createSession(at);
|
|
521
|
+
}
|
|
522
|
+
const apiOrigin = originOf(getTileflowAnalyticsApiUrl(analytics));
|
|
523
|
+
const binding = JSON.stringify([getTileflowAnalyticsApiUrl(analytics), analytics.mapId]);
|
|
524
|
+
let session = activeSession;
|
|
525
|
+
const grantedOrigins = session.binding === binding ? session.grant?.resourceOrigins : void 0;
|
|
526
|
+
if (!isEligibleResource(url, analytics.mapId, apiOrigin, grantedOrigins)) {
|
|
527
|
+
return void 0;
|
|
528
|
+
}
|
|
529
|
+
if (session.binding !== null && session.binding !== binding) {
|
|
530
|
+
session = createSession(at);
|
|
531
|
+
activeSession = session;
|
|
532
|
+
}
|
|
533
|
+
session.binding = binding;
|
|
534
|
+
session.requestCount += 1;
|
|
535
|
+
const resolved = await resolveGrant(session, analytics, at);
|
|
536
|
+
const nextUrl = new URL(url);
|
|
537
|
+
nextUrl.searchParams.set("session", resolved.session.sessionId);
|
|
538
|
+
nextUrl.searchParams.set("map", analytics.mapId);
|
|
539
|
+
if (analytics.styleId) nextUrl.searchParams.set("styleId", analytics.styleId);
|
|
540
|
+
if (resolved.grant.token) nextUrl.searchParams.set("grant", resolved.grant.token);
|
|
541
|
+
return nextUrl.toString();
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
async function requestSessionGrant(input) {
|
|
546
|
+
return runWithinSessionGrantTimeout(input.timeoutMs, async (signal) => {
|
|
547
|
+
const apiUrl = getTileflowAnalyticsApiUrl(input.analytics);
|
|
548
|
+
const response = await input.fetchGrant(`${apiUrl}/v1/sessions/start`, {
|
|
549
|
+
body: JSON.stringify({
|
|
550
|
+
mapId: input.analytics.mapId,
|
|
551
|
+
sdkVersion: input.analytics.sdkVersion,
|
|
552
|
+
sessionId: input.sessionId,
|
|
553
|
+
source: input.analytics.source ?? input.source,
|
|
554
|
+
styleId: input.analytics.styleId
|
|
555
|
+
}),
|
|
556
|
+
credentials: "omit",
|
|
557
|
+
headers: { "Content-Type": "application/json" },
|
|
558
|
+
method: "POST",
|
|
559
|
+
signal
|
|
560
|
+
});
|
|
561
|
+
throwIfSessionGrantAborted(signal);
|
|
562
|
+
let body = null;
|
|
563
|
+
try {
|
|
564
|
+
const source = await readBoundedUtf8Response(response, maximumSessionGrantBytes, {
|
|
565
|
+
invalidUtf8: "Tileflow session grant response was not valid UTF-8.",
|
|
566
|
+
tooLarge: "Tileflow session grant response was too large."
|
|
567
|
+
});
|
|
568
|
+
body = JSON.parse(source);
|
|
569
|
+
} catch (error) {
|
|
570
|
+
if (error instanceof SyntaxError) body = null;
|
|
571
|
+
else throw error;
|
|
572
|
+
}
|
|
573
|
+
throwIfSessionGrantAborted(signal);
|
|
574
|
+
if (response.status === 409 && isRecord(body) && body.code === "COMMERCIAL_SESSION_RESTART_REQUIRED" && body.ok === false && body.retryWithNewSession === true && body.sessionId === input.sessionId) {
|
|
575
|
+
throw new TileflowSessionRestartError();
|
|
576
|
+
}
|
|
577
|
+
if (!response.ok) {
|
|
578
|
+
throw new Error(`Tileflow session grant failed: ${response.status}`);
|
|
579
|
+
}
|
|
580
|
+
const parsed = parseSessionGrantResponse(body, input.sessionId);
|
|
581
|
+
if (!parsed) {
|
|
582
|
+
throw new Error("Tileflow session grant response was invalid.");
|
|
583
|
+
}
|
|
584
|
+
const expiresAt = parsed.grant ? Date.parse(parsed.expiresAt) : input.at + 6e4;
|
|
585
|
+
if (response.status === 201 && (!parsed.grant || !parsed.expiresAt) || parsed.grant && (!Number.isFinite(expiresAt) || expiresAt <= input.at)) {
|
|
586
|
+
throw new Error("Tileflow session grant response was invalid.");
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
expiresAt,
|
|
590
|
+
resourceOrigins: new Set(
|
|
591
|
+
[originOf(apiUrl), ...(parsed.resourceOrigins ?? []).map(originOf)].filter(
|
|
592
|
+
(value) => Boolean(value)
|
|
593
|
+
)
|
|
594
|
+
),
|
|
595
|
+
token: parsed.grant ?? null
|
|
596
|
+
};
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
function validateSessionGrantTimeout(value) {
|
|
600
|
+
const resolved = value ?? 1e4;
|
|
601
|
+
if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > 12e4) {
|
|
602
|
+
throw new Error("Tileflow session grantTimeoutMs must be an integer from 1 to 120000");
|
|
603
|
+
}
|
|
604
|
+
return resolved;
|
|
605
|
+
}
|
|
606
|
+
async function runWithinSessionGrantTimeout(timeoutMs, operation) {
|
|
607
|
+
const controller = new AbortController();
|
|
608
|
+
const timeoutError = new Error(`Tileflow session grant timed out after ${timeoutMs}ms`);
|
|
609
|
+
const timeout = setTimeout(() => controller.abort(timeoutError), timeoutMs);
|
|
610
|
+
try {
|
|
611
|
+
return await raceSessionGrantWithAbort(operation(controller.signal), controller.signal);
|
|
612
|
+
} finally {
|
|
613
|
+
clearTimeout(timeout);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function raceSessionGrantWithAbort(operation, signal) {
|
|
617
|
+
if (signal.aborted) return Promise.reject(sessionGrantAbortReason(signal));
|
|
618
|
+
return new Promise((resolve, reject) => {
|
|
619
|
+
const onAbort = () => {
|
|
620
|
+
cleanup();
|
|
621
|
+
reject(sessionGrantAbortReason(signal));
|
|
622
|
+
};
|
|
623
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
624
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
625
|
+
operation.then(
|
|
626
|
+
(value) => {
|
|
627
|
+
cleanup();
|
|
628
|
+
resolve(value);
|
|
629
|
+
},
|
|
630
|
+
(error) => {
|
|
631
|
+
cleanup();
|
|
632
|
+
reject(error);
|
|
633
|
+
}
|
|
634
|
+
);
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
function throwIfSessionGrantAborted(signal) {
|
|
638
|
+
if (signal.aborted) throw sessionGrantAbortReason(signal);
|
|
639
|
+
}
|
|
640
|
+
function sessionGrantAbortReason(signal) {
|
|
641
|
+
if (signal.reason instanceof Error) return signal.reason;
|
|
642
|
+
const error = new Error("Tileflow session grant was aborted");
|
|
643
|
+
error.name = "AbortError";
|
|
644
|
+
return error;
|
|
645
|
+
}
|
|
646
|
+
function parseSessionGrantResponse(value, expectedSessionId) {
|
|
647
|
+
if (!isRecord(value) || value.ok !== true || value.sessionId !== expectedSessionId) return null;
|
|
648
|
+
if (value.usageMode !== void 0 && value.usageMode !== "session") return null;
|
|
649
|
+
if (value.billingEnabled !== void 0 && value.billingEnabled !== false) return null;
|
|
650
|
+
if (value.disposition !== void 0 && value.disposition !== "ordinary" && value.disposition !== "unbilled_fail_open") {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
if (value.meterMode !== void 0 && value.meterMode !== "disabled" && value.meterMode !== "shadow" && value.meterMode !== "enforced") {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
if (value.grant !== void 0 && (typeof value.grant !== "string" || value.grant.length === 0 || value.grant.length > 8192)) {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
if (value.expiresAt !== void 0 && typeof value.expiresAt !== "string") return null;
|
|
660
|
+
if (value.grant !== void 0 && value.expiresAt === void 0) return null;
|
|
661
|
+
if (value.resourceOrigins !== void 0 && (!Array.isArray(value.resourceOrigins) || value.resourceOrigins.length > 32 || value.resourceOrigins.some((origin) => !originOf(origin)))) {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
return value;
|
|
665
|
+
}
|
|
666
|
+
function isRecord(value) {
|
|
667
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
668
|
+
}
|
|
669
|
+
function isPlainRuntimeRecord(value) {
|
|
670
|
+
if (!isRecord(value)) return false;
|
|
671
|
+
const prototype = Object.getPrototypeOf(value);
|
|
672
|
+
return prototype === Object.prototype || prototype === null;
|
|
673
|
+
}
|
|
674
|
+
function isTileflowPublicFontUrl(value) {
|
|
675
|
+
if (value.startsWith("//")) return false;
|
|
676
|
+
if (value.startsWith("/") || value.startsWith("./") || value.startsWith("../")) return true;
|
|
677
|
+
try {
|
|
678
|
+
const url = new URL(value);
|
|
679
|
+
return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password && !url.hash;
|
|
680
|
+
} catch {
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
var TileflowSessionRestartError = class extends Error {
|
|
685
|
+
constructor() {
|
|
686
|
+
super("Tileflow session must restart");
|
|
687
|
+
this.name = "TileflowSessionRestartError";
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
function isEligibleResource(value, mapId, apiOrigin, grantedOrigins) {
|
|
691
|
+
try {
|
|
692
|
+
const url = new URL(value);
|
|
693
|
+
if (url.username || url.password) return false;
|
|
694
|
+
const originAllowed = url.origin === apiOrigin || grantedOrigins?.has(url.origin) || apiOrigin === "https://api.tileflow.dev" && url.origin === "https://cdn.tileflow.dev";
|
|
695
|
+
if (!originAllowed) return false;
|
|
696
|
+
return url.pathname === `/maps/${encodeURIComponent(mapId)}/style.json` || url.pathname.startsWith("/tiles/") || url.pathname.startsWith("/v1/tiles/") || url.pathname.startsWith("/fonts/") || url.pathname.startsWith("/sprites/");
|
|
697
|
+
} catch {
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function originOf(value) {
|
|
702
|
+
if (typeof value !== "string") return null;
|
|
703
|
+
try {
|
|
704
|
+
const url = new URL(value);
|
|
705
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) return null;
|
|
706
|
+
return url.origin;
|
|
707
|
+
} catch {
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
function resolveTileflowAnalyticsRequestUrl(url, analytics, sessionId) {
|
|
712
|
+
if (!analytics || analytics.enabled === false) {
|
|
713
|
+
return void 0;
|
|
714
|
+
}
|
|
715
|
+
const apiUrl = getTileflowAnalyticsApiUrl(analytics);
|
|
716
|
+
if (!apiUrl || !isTileflowTileUrl(url, apiUrl)) {
|
|
717
|
+
return void 0;
|
|
718
|
+
}
|
|
719
|
+
const nextUrl = new URL(url);
|
|
720
|
+
nextUrl.searchParams.set("session", sessionId);
|
|
721
|
+
if (analytics.mapId) {
|
|
722
|
+
nextUrl.searchParams.set("map", analytics.mapId);
|
|
723
|
+
}
|
|
724
|
+
if (analytics.styleId) {
|
|
725
|
+
nextUrl.searchParams.set("styleId", analytics.styleId);
|
|
726
|
+
}
|
|
727
|
+
return nextUrl.toString();
|
|
728
|
+
}
|
|
729
|
+
function startTileflowSession(analytics, input) {
|
|
730
|
+
if (!analytics || analytics.enabled === false || !input.mapId || typeof navigator === "undefined") {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const endpoint = analytics.endpoint ?? `${getTileflowAnalyticsApiUrl(analytics)}/v1/sessions/analytics`;
|
|
734
|
+
const payload = JSON.stringify({
|
|
735
|
+
mapId: input.mapId,
|
|
736
|
+
metadata: analytics.metadata,
|
|
737
|
+
referrer: typeof document === "undefined" ? void 0 : document.referrer || "",
|
|
738
|
+
sdkVersion: analytics.sdkVersion,
|
|
739
|
+
sessionId: input.sessionId,
|
|
740
|
+
source: analytics.source ?? input.source,
|
|
741
|
+
styleId: input.styleId,
|
|
742
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
743
|
+
});
|
|
744
|
+
const blob = new Blob([payload], { type: "application/json" });
|
|
745
|
+
if (navigator.sendBeacon?.(endpoint, blob)) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
void fetch(endpoint, {
|
|
749
|
+
body: payload,
|
|
750
|
+
credentials: "omit",
|
|
751
|
+
headers: {
|
|
752
|
+
"Content-Type": "application/json"
|
|
753
|
+
},
|
|
754
|
+
keepalive: true,
|
|
755
|
+
method: "POST"
|
|
756
|
+
}).catch(() => void 0);
|
|
757
|
+
}
|
|
758
|
+
function getTileflowAnalyticsApiUrl(analytics) {
|
|
759
|
+
if (analytics.apiUrl) {
|
|
760
|
+
return normalizeTileflowUrl(analytics.apiUrl);
|
|
761
|
+
}
|
|
762
|
+
if (analytics.endpoint) {
|
|
763
|
+
try {
|
|
764
|
+
const url = new URL(analytics.endpoint, getTileflowLocationOrigin());
|
|
765
|
+
return url.origin;
|
|
766
|
+
} catch {
|
|
767
|
+
return "https://api.tileflow.dev";
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return "https://api.tileflow.dev";
|
|
771
|
+
}
|
|
772
|
+
function mergeTileflowAnalytics(userAnalytics, runtimeAnalytics) {
|
|
773
|
+
if (!userAnalytics && !runtimeAnalytics?.mapId) {
|
|
774
|
+
return void 0;
|
|
775
|
+
}
|
|
776
|
+
return {
|
|
777
|
+
...runtimeAnalytics,
|
|
778
|
+
...userAnalytics,
|
|
779
|
+
apiUrl: userAnalytics?.apiUrl ?? runtimeAnalytics?.apiUrl,
|
|
780
|
+
mapId: userAnalytics?.mapId ?? runtimeAnalytics?.mapId,
|
|
781
|
+
styleId: userAnalytics?.styleId ?? runtimeAnalytics?.styleId
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
function resolveTileflowMapMode(options) {
|
|
785
|
+
return options.mode ?? "interactive";
|
|
786
|
+
}
|
|
787
|
+
function resolveTileflowRuntimeView(options) {
|
|
788
|
+
const manifestView = options.manifestMap?.view;
|
|
789
|
+
return {
|
|
790
|
+
...options.fallback ?? defaultTileflowRuntimeView,
|
|
791
|
+
...manifestView ?? {},
|
|
792
|
+
...options.bearing !== void 0 ? { bearing: options.bearing } : {},
|
|
793
|
+
...options.center !== void 0 ? { center: options.center } : {},
|
|
794
|
+
...options.pitch !== void 0 ? { pitch: options.pitch } : {},
|
|
795
|
+
...options.zoom !== void 0 ? { zoom: options.zoom } : {}
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
function normalizeTileflowRuntimeCenter(center, fallback = defaultTileflowRuntimeView.center) {
|
|
799
|
+
if (!center) return [fallback[0], fallback[1]];
|
|
800
|
+
let longitude;
|
|
801
|
+
let latitude;
|
|
802
|
+
if (Array.isArray(center)) {
|
|
803
|
+
[longitude, latitude] = center;
|
|
804
|
+
} else {
|
|
805
|
+
const objectCenter = center;
|
|
806
|
+
longitude = objectCenter.lng ?? objectCenter.lon;
|
|
807
|
+
latitude = objectCenter.lat;
|
|
808
|
+
}
|
|
809
|
+
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
|
|
810
|
+
return [fallback[0], fallback[1]];
|
|
811
|
+
}
|
|
812
|
+
return [longitude, latitude];
|
|
813
|
+
}
|
|
814
|
+
function loadTileflowManifest(url, options = {}) {
|
|
815
|
+
assertSafeManifestRequestUrl(url);
|
|
816
|
+
const ttl = normalizeManifestCacheTtl(options.cacheTtlMs);
|
|
817
|
+
const timeoutMs = normalizeManifestTimeout(options.timeoutMs);
|
|
818
|
+
const cacheable = ttl > 0 && !options.fetch && !options.signal;
|
|
819
|
+
const now = Date.now();
|
|
820
|
+
const cached = cacheable ? manifestCache.get(url) : void 0;
|
|
821
|
+
if (cached && cached.expiresAt > now) return cached.promise;
|
|
822
|
+
if (cached) manifestCache.delete(url);
|
|
823
|
+
const fetchManifest = options.fetch ?? globalThis.fetch;
|
|
824
|
+
const requestSignal = createManifestRequestSignal(options.signal, timeoutMs);
|
|
825
|
+
const promise = Promise.resolve().then(() => fetchManifest(url, { cache: "no-store", signal: requestSignal.signal })).then(async (response) => {
|
|
826
|
+
if (response.status === 404) {
|
|
827
|
+
manifestCache.delete(url);
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
if (!response.ok) throw new Error(`Tileflow manifest failed: ${response.status}`);
|
|
831
|
+
const source = await readBoundedManifestResponse(response);
|
|
832
|
+
let input;
|
|
833
|
+
try {
|
|
834
|
+
input = JSON.parse(source);
|
|
835
|
+
} catch {
|
|
836
|
+
throw new Error("Tileflow manifest is not valid JSON.");
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
const { parseTileflowRuntimeManifest: parseTileflowRuntimeManifest2 } = await Promise.resolve().then(() => (init_manifest(), manifest_exports));
|
|
840
|
+
return resolveLoadedTileflowManifestUrls(
|
|
841
|
+
parseTileflowRuntimeManifest2(input),
|
|
842
|
+
response.url || resolveTileflowLoadedResourceUrl(url, getTileflowLocationBaseUrl())
|
|
843
|
+
);
|
|
844
|
+
} catch (error) {
|
|
845
|
+
const message = error instanceof Error ? error.message : "schema validation failed";
|
|
846
|
+
throw new Error(`Invalid Tileflow manifest: ${message}`);
|
|
847
|
+
}
|
|
848
|
+
}).catch((error) => {
|
|
849
|
+
manifestCache.delete(url);
|
|
850
|
+
throw error;
|
|
851
|
+
}).finally(requestSignal.cleanup);
|
|
852
|
+
if (cacheable) manifestCache.set(url, { expiresAt: now + ttl, promise });
|
|
853
|
+
return promise;
|
|
854
|
+
}
|
|
855
|
+
function resolveLoadedTileflowManifestUrls(manifest, manifestUrl) {
|
|
856
|
+
return {
|
|
857
|
+
...manifest,
|
|
858
|
+
maps: Object.fromEntries(
|
|
859
|
+
Object.entries(manifest.maps).map(([mapName, entry]) => [
|
|
860
|
+
mapName,
|
|
861
|
+
{
|
|
862
|
+
...entry,
|
|
863
|
+
themes: Object.fromEntries(
|
|
864
|
+
Object.entries(entry.themes).map(([themeName, theme]) => {
|
|
865
|
+
const styleUrl = resolveTileflowLoadedResourceUrl(theme.styleUrl, manifestUrl);
|
|
866
|
+
return [
|
|
867
|
+
themeName,
|
|
868
|
+
{
|
|
869
|
+
...theme,
|
|
870
|
+
styleUrl,
|
|
871
|
+
...theme.fontFaces ? {
|
|
872
|
+
fontFaces: theme.fontFaces.map((definition) => ({
|
|
873
|
+
...definition,
|
|
874
|
+
source: resolveTileflowLoadedResourceUrl(definition.source, styleUrl)
|
|
875
|
+
}))
|
|
876
|
+
} : {}
|
|
877
|
+
}
|
|
878
|
+
];
|
|
879
|
+
})
|
|
880
|
+
)
|
|
881
|
+
}
|
|
882
|
+
])
|
|
883
|
+
)
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
function resolveTileflowLoadedResourceUrl(value, baseUrl) {
|
|
887
|
+
let url;
|
|
888
|
+
try {
|
|
889
|
+
url = new URL(value, baseUrl);
|
|
890
|
+
} catch {
|
|
891
|
+
throw new TypeError("Tileflow manifest resource URL is invalid.");
|
|
892
|
+
}
|
|
893
|
+
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password || url.hash || /%(?:2f|5c)/iu.test(value)) {
|
|
894
|
+
throw new TypeError("Tileflow manifest resource URL is invalid.");
|
|
895
|
+
}
|
|
896
|
+
return url.toString();
|
|
897
|
+
}
|
|
898
|
+
function clearTileflowManifestCache(url) {
|
|
899
|
+
if (url === void 0) manifestCache.clear();
|
|
900
|
+
else manifestCache.delete(url);
|
|
901
|
+
}
|
|
902
|
+
function resolveTileflowManifestMap(manifest, mapName) {
|
|
903
|
+
if (!Object.hasOwn(manifest.maps, mapName)) return null;
|
|
904
|
+
const entry = manifest.maps[mapName];
|
|
905
|
+
const defaultStyleUrl = entry.themes[entry.defaultTheme]?.styleUrl;
|
|
906
|
+
const analytics = defaultStyleUrl ? inferTileflowAnalyticsFromStyleUrl(defaultStyleUrl) : void 0;
|
|
907
|
+
return {
|
|
908
|
+
apiUrl: entry.apiUrl ?? manifest.apiUrl ?? analytics?.apiUrl,
|
|
909
|
+
mapId: entry.mapId ?? analytics?.mapId,
|
|
910
|
+
name: mapName,
|
|
911
|
+
defaultTheme: entry.defaultTheme,
|
|
912
|
+
...entry.environment ? { environment: entry.environment } : {},
|
|
913
|
+
...entry.systemThemes ? { systemThemes: { ...entry.systemThemes } } : {},
|
|
914
|
+
themes: entry.themes,
|
|
915
|
+
usageMode: entry.usageMode,
|
|
916
|
+
view: entry.view,
|
|
917
|
+
worldGeneration: entry.worldGeneration
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
function resolveTileflowStaticImageUrl(input) {
|
|
921
|
+
if (!input.manifestMap?.mapId) {
|
|
922
|
+
warnMissingStaticMapId(input.manifestMap);
|
|
923
|
+
return void 0;
|
|
924
|
+
}
|
|
925
|
+
if (!input.imageSize) {
|
|
926
|
+
return void 0;
|
|
927
|
+
}
|
|
928
|
+
const apiUrl = normalizeTileflowUrl(input.manifestMap.apiUrl ?? "https://api.tileflow.dev");
|
|
929
|
+
const mapId = encodeURIComponent(input.manifestMap.mapId);
|
|
930
|
+
const url = new URL(`/maps/${mapId}/static.png`, apiUrl);
|
|
931
|
+
const theme = resolveTileflowRuntimeTheme(input.manifestMap, input.theme, input.colorScheme);
|
|
932
|
+
url.searchParams.set("center", input.center.join(","));
|
|
933
|
+
url.searchParams.set("theme", theme.name);
|
|
934
|
+
url.searchParams.set("zoom", String(input.zoom));
|
|
935
|
+
url.searchParams.set("width", String(input.imageSize.width));
|
|
936
|
+
url.searchParams.set("height", String(input.imageSize.height));
|
|
937
|
+
return url.toString();
|
|
938
|
+
}
|
|
939
|
+
function warnMissingStaticMapId(manifestMap) {
|
|
940
|
+
const styleUrl = manifestMap ? manifestMap.themes[manifestMap.defaultTheme]?.styleUrl : void 0;
|
|
941
|
+
if (!styleUrl || missingStaticMapIdWarnings.has(styleUrl) || typeof console === "undefined") {
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
missingStaticMapIdWarnings.add(styleUrl);
|
|
945
|
+
console.warn(
|
|
946
|
+
`Tileflow image mode requires a deployed manifest entry with mapId. The manifest style URL "${styleUrl}" cannot be converted to a static image URL.`
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
function normalizeTileflowStaticImageSize(input) {
|
|
950
|
+
let width = clampTileflowImageDimension(input.width);
|
|
951
|
+
let height = clampTileflowImageDimension(input.height);
|
|
952
|
+
const pixelCount = width * height;
|
|
953
|
+
if (pixelCount > maxStaticImagePixels) {
|
|
954
|
+
const scale = Math.sqrt(maxStaticImagePixels / pixelCount);
|
|
955
|
+
width = clampTileflowImageDimension(width * scale);
|
|
956
|
+
height = clampTileflowImageDimension(height * scale);
|
|
957
|
+
}
|
|
958
|
+
return { height, width };
|
|
959
|
+
}
|
|
960
|
+
async function readBoundedManifestResponse(response) {
|
|
961
|
+
return readBoundedUtf8Response(response, maximumManifestBytes, {
|
|
962
|
+
invalidUtf8: "Tileflow manifest is not valid UTF-8.",
|
|
963
|
+
tooLarge: "Tileflow manifest is too large."
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
async function readBoundedUtf8Response(response, maximumBytes, errors) {
|
|
967
|
+
const contentLength = response.headers.get("content-length");
|
|
968
|
+
if (contentLength !== null) {
|
|
969
|
+
const declaredBytes = Number(contentLength);
|
|
970
|
+
if (Number.isFinite(declaredBytes) && declaredBytes > maximumBytes) {
|
|
971
|
+
throw new Error(errors.tooLarge);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
if (!response.body) return "";
|
|
975
|
+
const reader = response.body.getReader();
|
|
976
|
+
const chunks = [];
|
|
977
|
+
let byteLength = 0;
|
|
978
|
+
while (true) {
|
|
979
|
+
const next = await reader.read();
|
|
980
|
+
if (next.done) break;
|
|
981
|
+
byteLength += next.value.byteLength;
|
|
982
|
+
if (byteLength > maximumBytes) {
|
|
983
|
+
await reader.cancel();
|
|
984
|
+
throw new Error(errors.tooLarge);
|
|
985
|
+
}
|
|
986
|
+
chunks.push(next.value);
|
|
987
|
+
}
|
|
988
|
+
const bytes = new Uint8Array(byteLength);
|
|
989
|
+
let offset = 0;
|
|
990
|
+
for (const chunk of chunks) {
|
|
991
|
+
bytes.set(chunk, offset);
|
|
992
|
+
offset += chunk.byteLength;
|
|
993
|
+
}
|
|
994
|
+
try {
|
|
995
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
996
|
+
} catch {
|
|
997
|
+
throw new Error(errors.invalidUtf8);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function normalizeManifestCacheTtl(value) {
|
|
1001
|
+
if (value === void 0) return defaultManifestCacheTtlMs;
|
|
1002
|
+
if (!Number.isFinite(value) || value < 0 || value > 5 * 6e4) {
|
|
1003
|
+
throw new TypeError("cacheTtlMs must be between 0 and 300000 milliseconds.");
|
|
1004
|
+
}
|
|
1005
|
+
return value;
|
|
1006
|
+
}
|
|
1007
|
+
function normalizeManifestTimeout(value) {
|
|
1008
|
+
if (value === void 0) return 1e4;
|
|
1009
|
+
if (!Number.isFinite(value) || value < 1 || value > 6e4) {
|
|
1010
|
+
throw new TypeError("timeoutMs must be between 1 and 60000 milliseconds.");
|
|
1011
|
+
}
|
|
1012
|
+
return value;
|
|
1013
|
+
}
|
|
1014
|
+
function createManifestRequestSignal(external, timeoutMs) {
|
|
1015
|
+
const controller = new AbortController();
|
|
1016
|
+
const abortFromExternal = () => controller.abort(external?.reason);
|
|
1017
|
+
if (external?.aborted) abortFromExternal();
|
|
1018
|
+
else external?.addEventListener("abort", abortFromExternal, { once: true });
|
|
1019
|
+
const timeout = setTimeout(
|
|
1020
|
+
() => controller.abort(new DOMException("Timed out", "TimeoutError")),
|
|
1021
|
+
timeoutMs
|
|
1022
|
+
);
|
|
1023
|
+
return {
|
|
1024
|
+
cleanup() {
|
|
1025
|
+
clearTimeout(timeout);
|
|
1026
|
+
external?.removeEventListener("abort", abortFromExternal);
|
|
1027
|
+
},
|
|
1028
|
+
signal: controller.signal
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
function assertSafeManifestRequestUrl(value) {
|
|
1032
|
+
if (value.length < 1 || value.length > 2048 || /[\p{Cc}\\]/u.test(value) || value.startsWith("//") || /%(?:2f|5c)/iu.test(value)) {
|
|
1033
|
+
throw new TypeError("Tileflow manifest URL must be a safe HTTP(S) or relative URL.");
|
|
1034
|
+
}
|
|
1035
|
+
try {
|
|
1036
|
+
const url = new URL(value, getTileflowLocationOrigin());
|
|
1037
|
+
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password || url.hash) {
|
|
1038
|
+
throw new Error("unsafe");
|
|
1039
|
+
}
|
|
1040
|
+
} catch {
|
|
1041
|
+
throw new TypeError("Tileflow manifest URL must be a safe HTTP(S) or relative URL.");
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
function inferTileflowAnalyticsFromStyleUrl(styleUrl) {
|
|
1045
|
+
try {
|
|
1046
|
+
const url = new URL(styleUrl, getTileflowLocationOrigin());
|
|
1047
|
+
const match = url.pathname.match(/^\/maps\/([^/]+)\/style\.json$/);
|
|
1048
|
+
if (!match?.[1]) {
|
|
1049
|
+
return void 0;
|
|
1050
|
+
}
|
|
1051
|
+
return {
|
|
1052
|
+
apiUrl: url.origin,
|
|
1053
|
+
mapId: match[1],
|
|
1054
|
+
styleId: url.toString()
|
|
1055
|
+
};
|
|
1056
|
+
} catch {
|
|
1057
|
+
return void 0;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
function isTileflowTileUrl(url, apiUrl) {
|
|
1061
|
+
try {
|
|
1062
|
+
const parsedUrl = new URL(url);
|
|
1063
|
+
const parsedApiUrl = new URL(apiUrl);
|
|
1064
|
+
return parsedUrl.origin === parsedApiUrl.origin && isTilePath(parsedUrl);
|
|
1065
|
+
} catch {
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
function isTilePath(url) {
|
|
1070
|
+
return url.pathname.startsWith("/tiles/") || url.pathname.startsWith("/v1/tiles/");
|
|
1071
|
+
}
|
|
1072
|
+
function clampTileflowImageDimension(value) {
|
|
1073
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1074
|
+
return 640;
|
|
1075
|
+
}
|
|
1076
|
+
return Math.max(64, Math.min(maxStaticImageDimension, Math.round(value)));
|
|
1077
|
+
}
|
|
1078
|
+
function getTileflowLocationOrigin() {
|
|
1079
|
+
if (typeof window === "undefined") {
|
|
1080
|
+
return "http://localhost";
|
|
1081
|
+
}
|
|
1082
|
+
return window.location.origin;
|
|
1083
|
+
}
|
|
1084
|
+
function getTileflowLocationBaseUrl() {
|
|
1085
|
+
if (typeof document !== "undefined" && document.baseURI) return document.baseURI;
|
|
1086
|
+
if (typeof window !== "undefined" && window.location.href) return window.location.href;
|
|
1087
|
+
return `${getTileflowLocationOrigin().replace(/\/+$/u, "")}/`;
|
|
1088
|
+
}
|
|
1089
|
+
export {
|
|
1090
|
+
assertValidTileflowRuntimeSource,
|
|
1091
|
+
clearTileflowManifestCache,
|
|
1092
|
+
createTileflowSessionController,
|
|
1093
|
+
createTileflowSessionId,
|
|
1094
|
+
defaultTileflowManifestUrl,
|
|
1095
|
+
defaultTileflowRuntimeView,
|
|
1096
|
+
getTileflowAnalyticsApiUrl,
|
|
1097
|
+
getTileflowStyleFontFaces,
|
|
1098
|
+
inferTileflowAnalyticsFromStyleUrl,
|
|
1099
|
+
loadTileflowManifest,
|
|
1100
|
+
mergeTileflowAnalytics,
|
|
1101
|
+
normalizeTileflowRuntimeCenter,
|
|
1102
|
+
normalizeTileflowStaticImageSize,
|
|
1103
|
+
normalizeTileflowUrl,
|
|
1104
|
+
resolveTileflowAnalyticsRequestUrl,
|
|
1105
|
+
resolveTileflowManifestMap,
|
|
1106
|
+
resolveTileflowMapMode,
|
|
1107
|
+
resolveTileflowRuntimeStyle,
|
|
1108
|
+
resolveTileflowRuntimeTheme,
|
|
1109
|
+
resolveTileflowRuntimeView,
|
|
1110
|
+
resolveTileflowStaticImageUrl,
|
|
1111
|
+
shouldLoadTileflowManifest,
|
|
1112
|
+
startTileflowSession,
|
|
1113
|
+
tileflowStyleFontFaceLimits,
|
|
1114
|
+
tileflowStyleFontFacesMetadataKey,
|
|
1115
|
+
validateTileflowRuntimeSource,
|
|
1116
|
+
validateTileflowThemeSelection
|
|
1117
|
+
};
|