@tileflow/core 0.1.0-alpha.47 → 0.1.0-alpha.48

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/dist/runtime.js CHANGED
@@ -488,6 +488,18 @@ function planTileflowThemeBlend(styles, options = {}) {
488
488
  }
489
489
  }
490
490
  }
491
+ function mixTileflowColours(from, to, t) {
492
+ const a = oklab(from);
493
+ const b = oklab(to);
494
+ if (!a || !b) return void 0;
495
+ const alpha = a[3] + (b[3] - a[3]) * t;
496
+ if (alpha <= 0) return "rgba(0, 0, 0, 0)";
497
+ const channels = a.slice(0, 3).map((value, channel) => {
498
+ const from2 = value * a[3];
499
+ return (from2 + (b[channel] * b[3] - from2) * t) / alpha;
500
+ });
501
+ return colourFromOklab([...channels, alpha]);
502
+ }
491
503
  function fillLayer(layer, position, dominant, layerSwitches) {
492
504
  const filled = { ...layer };
493
505
  for (const group of ["layout", "paint"]) {
@@ -531,10 +543,7 @@ function mixLeaf(values, position) {
531
543
  if (t <= 0 || from === to) return from;
532
544
  if (t >= 1) return to;
533
545
  if (typeof from === "number" && typeof to === "number") return from + (to - from) * t;
534
- const a = oklab(String(from));
535
- const b = oklab(String(to));
536
- if (!a || !b) return t < 0.5 ? from : to;
537
- return colourFromOklab(a.map((value, channel) => value + (b[channel] - value) * t));
546
+ return mixTileflowColours(String(from), String(to), t) ?? (t < 0.5 ? from : to);
538
547
  }
539
548
  function clampPosition(position, themes) {
540
549
  if (!Number.isFinite(position)) return 0;
@@ -3253,49 +3262,6 @@ function fail(message) {
3253
3262
  throw new TypeError(`Tileflow style family: ${message}`);
3254
3263
  }
3255
3264
 
3256
- // src/style-family-loader.ts
3257
- async function loadTileflowStyleFamily(url, options = {}) {
3258
- const request = options.fetch ?? globalThis.fetch.bind(globalThis);
3259
- const response = await request(url, { credentials: "same-origin", signal: options.signal });
3260
- if (!response.ok) throw new Error(`Tileflow style family request failed: ${response.status}`);
3261
- const maximum = tileflowStyleFamilyLimits.documentBytes;
3262
- const length = response.headers.get("content-length");
3263
- if (length && Number(length) > maximum) {
3264
- await response.body?.cancel();
3265
- throw new TypeError("Tileflow style family is too large.");
3266
- }
3267
- const reader = response.body?.getReader();
3268
- if (!reader) throw new TypeError("Tileflow style family response has no body.");
3269
- const chunks = [];
3270
- let total = 0;
3271
- try {
3272
- for (; ; ) {
3273
- const { done, value } = await reader.read();
3274
- if (done) break;
3275
- total += value.byteLength;
3276
- if (total > maximum) {
3277
- await reader.cancel();
3278
- throw new TypeError("Tileflow style family is too large.");
3279
- }
3280
- chunks.push(value);
3281
- }
3282
- } finally {
3283
- reader.releaseLock();
3284
- }
3285
- const bytes2 = new Uint8Array(total);
3286
- let offset = 0;
3287
- for (const chunk of chunks) {
3288
- bytes2.set(chunk, offset);
3289
- offset += chunk.length;
3290
- }
3291
- return parseTileflowStyleFamily(
3292
- JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes2))
3293
- );
3294
- }
3295
-
3296
- // src/color-program-runtime.ts
3297
- init_color_program();
3298
-
3299
3265
  // src/themes/index.ts
3300
3266
  init_color_program();
3301
3267
  init_model();
@@ -3362,7 +3328,271 @@ function rgbToHex(r, g, b) {
3362
3328
  return `#${[r, g, b].map((value) => value.toString(16).padStart(2, "0")).join("")}`;
3363
3329
  }
3364
3330
 
3331
+ // src/overlays.ts
3332
+ init_portable_identity_rules();
3333
+ var tileflowOverlayPlacements = [
3334
+ "above-water",
3335
+ "below-roads",
3336
+ "above-roads",
3337
+ "above-buildings",
3338
+ "below-labels",
3339
+ "above-labels"
3340
+ ];
3341
+ var tileflowClusterMetadataKey = "tileflow:cluster";
3342
+ function geojsonSource(options = {}) {
3343
+ const data = options.data ?? { features: [], type: "FeatureCollection" };
3344
+ if (typeof data === "string") {
3345
+ if (!isHttpsUrl(data)) {
3346
+ throw new Error("Tileflow GeoJSON source URLs must be absolute https:// URLs.");
3347
+ }
3348
+ } else if (!isPlainRecord3(data) || data.type !== "FeatureCollection" || !Array.isArray(data.features)) {
3349
+ throw new Error("Tileflow GeoJSON source data must be a FeatureCollection or an https:// URL.");
3350
+ }
3351
+ assertClusterOptions(options);
3352
+ const attribution = options.attribution?.trim();
3353
+ return Object.freeze({
3354
+ ...attribution ? { attribution } : {},
3355
+ ...pickClusterOptions(options),
3356
+ data: typeof data === "string" ? data : Object.freeze(cloneJson2(data)),
3357
+ kind: "geojson"
3358
+ });
3359
+ }
3360
+ function maplibreOverlay(options) {
3361
+ if (!isTileflowPortableId(options.source)) {
3362
+ throw new Error(`Invalid Tileflow overlay source ID: ${options.source}.`);
3363
+ }
3364
+ if (!tileflowOverlayPlacements.includes(options.placement)) {
3365
+ throw new Error(`Invalid Tileflow overlay placement: ${String(options.placement)}.`);
3366
+ }
3367
+ if (!Array.isArray(options.layers) || options.layers.length < 1) {
3368
+ throw new Error("Tileflow MapLibre overlays require at least one layer.");
3369
+ }
3370
+ const layers = options.layers.map((layer, index) => {
3371
+ if (!isPlainRecord3(layer)) {
3372
+ throw new Error(`Tileflow overlay layer ${index} must be a plain object.`);
3373
+ }
3374
+ if (!isTileflowPortableId(layer.id)) {
3375
+ throw new Error(`Invalid Tileflow overlay layer ID: ${String(layer.id)}.`);
3376
+ }
3377
+ if (layer.id === "tileflow" || layer.id.startsWith("tileflow-")) {
3378
+ throw new Error(`Tileflow overlay layer ID is reserved: ${layer.id}.`);
3379
+ }
3380
+ if (typeof layer.type !== "string" || !layer.type) {
3381
+ throw new Error(`Tileflow overlay layer ${layer.id} requires a MapLibre type.`);
3382
+ }
3383
+ if (Object.hasOwn(layer, "source")) {
3384
+ throw new Error(
3385
+ `Tileflow overlay layer ${layer.id} inherits its source from the overlay and must not declare source.`
3386
+ );
3387
+ }
3388
+ return Object.freeze(cloneJson2(layer));
3389
+ });
3390
+ return Object.freeze({
3391
+ kind: "maplibre-overlay",
3392
+ layers: Object.freeze(layers),
3393
+ placement: options.placement,
3394
+ source: options.source
3395
+ });
3396
+ }
3397
+ function clusterOverlay(options) {
3398
+ const colors = options.colors ?? ["#2563eb", "#7c3aed", "#db2777"];
3399
+ const radii = options.radii ?? [16, 22, 30];
3400
+ const [low, high] = options.steps ?? [100, 750];
3401
+ if (!(low > 0 && high > low) || !radii.every((radius) => radius > 0)) {
3402
+ throw new Error("Tileflow cluster steps must increase and radii must be positive.");
3403
+ }
3404
+ const metadata = (role) => ({
3405
+ [tileflowClusterMetadataKey]: {
3406
+ expandOnClick: options.expandOnClick ?? true,
3407
+ role
3408
+ }
3409
+ });
3410
+ const step = (values) => [
3411
+ "step",
3412
+ ["get", "point_count"],
3413
+ values[0],
3414
+ low,
3415
+ values[1],
3416
+ high,
3417
+ values[2]
3418
+ ];
3419
+ return maplibreOverlay({
3420
+ layers: [
3421
+ {
3422
+ filter: ["has", "point_count"],
3423
+ id: `${options.id}-clusters`,
3424
+ metadata: metadata("cluster"),
3425
+ paint: {
3426
+ "circle-color": step(colors),
3427
+ "circle-radius": step(radii),
3428
+ "circle-stroke-color": "#ffffff",
3429
+ "circle-stroke-width": 2
3430
+ },
3431
+ type: "circle"
3432
+ },
3433
+ {
3434
+ filter: ["has", "point_count"],
3435
+ id: `${options.id}-cluster-count`,
3436
+ layout: {
3437
+ "text-allow-overlap": true,
3438
+ "text-field": ["get", "point_count_abbreviated"],
3439
+ ...options.font ? { "text-font": [...options.font] } : {},
3440
+ "text-ignore-placement": true,
3441
+ "text-size": 13
3442
+ },
3443
+ metadata: metadata("count"),
3444
+ paint: { "text-color": options.textColor ?? "#ffffff" },
3445
+ type: "symbol"
3446
+ },
3447
+ {
3448
+ filter: ["!", ["has", "point_count"]],
3449
+ id: `${options.id}-points`,
3450
+ metadata: metadata("point"),
3451
+ paint: {
3452
+ "circle-color": options.pointColor ?? colors[0],
3453
+ "circle-radius": 6,
3454
+ "circle-stroke-color": "#ffffff",
3455
+ "circle-stroke-width": 1.5
3456
+ },
3457
+ type: "circle"
3458
+ }
3459
+ ],
3460
+ placement: options.placement,
3461
+ source: options.source
3462
+ });
3463
+ }
3464
+ function heatmapOverlay(options) {
3465
+ const colors = options.colors ?? [
3466
+ [0, "rgba(33, 102, 172, 0)"],
3467
+ [0.2, "#67a9cf"],
3468
+ [0.4, "#d1e5f0"],
3469
+ [0.6, "#fddbc7"],
3470
+ [0.8, "#ef8a62"],
3471
+ [1, "#b2182b"]
3472
+ ];
3473
+ if (colors.length < 2 || colors.some(([density], index) => {
3474
+ const previous = colors[index - 1]?.[0] ?? -1;
3475
+ return !(density >= 0 && density <= 1 && density > previous);
3476
+ })) {
3477
+ throw new Error("Tileflow heatmap colors need at least two increasing densities from 0 to 1.");
3478
+ }
3479
+ const paint = {
3480
+ "heatmap-color": ["interpolate", ["linear"], ["heatmap-density"], ...colors.flat()],
3481
+ "heatmap-intensity": options.intensity ?? 1,
3482
+ "heatmap-opacity": options.opacity ?? 0.8,
3483
+ "heatmap-radius": options.radius ?? 30,
3484
+ "heatmap-weight": options.weight ?? 1
3485
+ };
3486
+ return maplibreOverlay({
3487
+ layers: [
3488
+ {
3489
+ id: options.id,
3490
+ ...options.maxZoom === void 0 ? {} : { maxzoom: options.maxZoom },
3491
+ paint,
3492
+ ...options.sourceLayer === void 0 ? {} : { "source-layer": options.sourceLayer },
3493
+ type: "heatmap"
3494
+ }
3495
+ ],
3496
+ placement: options.placement,
3497
+ source: options.source
3498
+ });
3499
+ }
3500
+ function pickClusterOptions(source) {
3501
+ return {
3502
+ ...source.cluster === void 0 ? {} : { cluster: source.cluster },
3503
+ ...source.clusterMaxZoom === void 0 ? {} : { clusterMaxZoom: source.clusterMaxZoom },
3504
+ ...source.clusterMinPoints === void 0 ? {} : { clusterMinPoints: source.clusterMinPoints },
3505
+ ...source.clusterProperties === void 0 ? {} : { clusterProperties: cloneJson2(source.clusterProperties) },
3506
+ ...source.clusterRadius === void 0 ? {} : { clusterRadius: source.clusterRadius }
3507
+ };
3508
+ }
3509
+ function assertClusterOptions(options) {
3510
+ const { cluster, clusterMaxZoom, clusterMinPoints, clusterProperties, clusterRadius } = options;
3511
+ if (cluster !== void 0 && typeof cluster !== "boolean") {
3512
+ throw new Error("Tileflow GeoJSON cluster must be a boolean.");
3513
+ }
3514
+ if (clusterRadius !== void 0 && !(Number.isFinite(clusterRadius) && clusterRadius > 0)) {
3515
+ throw new Error("Tileflow clusterRadius must be a positive number of pixels.");
3516
+ }
3517
+ if (clusterMaxZoom !== void 0 && !(Number.isFinite(clusterMaxZoom) && clusterMaxZoom >= 0 && clusterMaxZoom <= 24)) {
3518
+ throw new Error("Tileflow clusterMaxZoom must be a zoom from 0 to 24.");
3519
+ }
3520
+ if (clusterMinPoints !== void 0 && !(Number.isInteger(clusterMinPoints) && clusterMinPoints >= 2)) {
3521
+ throw new Error("Tileflow clusterMinPoints must be an integer of at least 2.");
3522
+ }
3523
+ if (!cluster && [clusterMaxZoom, clusterMinPoints, clusterProperties, clusterRadius].some(
3524
+ (value) => value !== void 0
3525
+ )) {
3526
+ throw new Error("Tileflow cluster options require cluster: true.");
3527
+ }
3528
+ for (const [name, value] of Object.entries(clusterProperties ?? {})) {
3529
+ if (!Array.isArray(value) || value.length !== 2) {
3530
+ throw new Error(`Tileflow clusterProperties.${name} must be [operator, mapExpression].`);
3531
+ }
3532
+ }
3533
+ }
3534
+ function isHttpsUrl(value) {
3535
+ try {
3536
+ return new URL(value).protocol === "https:";
3537
+ } catch {
3538
+ return false;
3539
+ }
3540
+ }
3541
+ function cloneJson2(value) {
3542
+ if (Array.isArray(value)) return value.map(cloneJson2);
3543
+ if (!isPlainRecord3(value)) return value;
3544
+ return Object.fromEntries(
3545
+ Object.entries(value).map(([key, child]) => [key, cloneJson2(child)])
3546
+ );
3547
+ }
3548
+ function isPlainRecord3(value) {
3549
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
3550
+ const prototype = Object.getPrototypeOf(value);
3551
+ return prototype === Object.prototype || prototype === null;
3552
+ }
3553
+
3554
+ // src/style-family-loader.ts
3555
+ async function loadTileflowStyleFamily(url, options = {}) {
3556
+ const request = options.fetch ?? globalThis.fetch.bind(globalThis);
3557
+ const response = await request(url, { credentials: "same-origin", signal: options.signal });
3558
+ if (!response.ok) throw new Error(`Tileflow style family request failed: ${response.status}`);
3559
+ const maximum = tileflowStyleFamilyLimits.documentBytes;
3560
+ const length = response.headers.get("content-length");
3561
+ if (length && Number(length) > maximum) {
3562
+ await response.body?.cancel();
3563
+ throw new TypeError("Tileflow style family is too large.");
3564
+ }
3565
+ const reader = response.body?.getReader();
3566
+ if (!reader) throw new TypeError("Tileflow style family response has no body.");
3567
+ const chunks = [];
3568
+ let total = 0;
3569
+ try {
3570
+ for (; ; ) {
3571
+ const { done, value } = await reader.read();
3572
+ if (done) break;
3573
+ total += value.byteLength;
3574
+ if (total > maximum) {
3575
+ await reader.cancel();
3576
+ throw new TypeError("Tileflow style family is too large.");
3577
+ }
3578
+ chunks.push(value);
3579
+ }
3580
+ } finally {
3581
+ reader.releaseLock();
3582
+ }
3583
+ const bytes2 = new Uint8Array(total);
3584
+ let offset = 0;
3585
+ for (const chunk of chunks) {
3586
+ bytes2.set(chunk, offset);
3587
+ offset += chunk.length;
3588
+ }
3589
+ return parseTileflowStyleFamily(
3590
+ JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes2))
3591
+ );
3592
+ }
3593
+
3365
3594
  // src/color-program-runtime.ts
3595
+ init_color_program();
3366
3596
  init_model();
3367
3597
  function applyTileflowColorOverrides(style, overrides) {
3368
3598
  const keys = Object.keys(overrides);
@@ -4452,18 +4682,22 @@ export {
4452
4682
  applyTileflowColorOverrides,
4453
4683
  assertValidTileflowRuntimeSource,
4454
4684
  clearTileflowManifestCache,
4685
+ clusterOverlay,
4455
4686
  createTileflowSessionController,
4456
4687
  createTileflowSessionId,
4457
4688
  defaultTileflowManifestUrl,
4458
4689
  defaultTileflowRuntimeView,
4459
4690
  expandTileflowFamilyTemplate,
4691
+ geojsonSource,
4460
4692
  getTileflowAnalyticsApiUrl,
4461
4693
  getTileflowStyleFontFaces,
4694
+ heatmapOverlay,
4462
4695
  inferTileflowAnalyticsFromStyleUrl,
4463
4696
  isTileflowTimeOfDay,
4464
4697
  loadTileflowManifest,
4465
4698
  loadTileflowRuntimeAppearance,
4466
4699
  loadTileflowStyleFamily,
4700
+ maplibreOverlay,
4467
4701
  materializeTileflowStyleFamily,
4468
4702
  mergeTileflowAnalytics,
4469
4703
  nearestTileflowBlendTheme,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tileflow/core",
3
- "version": "0.1.0-alpha.47",
3
+ "version": "0.1.0-alpha.48",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Typed Tileflow config and MapLibre style compiler.",
6
6
  "type": "module",
@@ -25,6 +25,7 @@
25
25
  "types": "./dist/index.d.ts",
26
26
  "files": [
27
27
  "dist",
28
+ "REFERENCE.md",
28
29
  "LICENSE",
29
30
  "THIRD_PARTY_NOTICES.md"
30
31
  ],