@tileflow/core 0.1.0-alpha.31 → 0.1.0-alpha.33
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/README.md +41 -2
- package/THIRD_PARTY_NOTICES.md +25 -2
- package/dist/browser.d.ts +1008 -16
- package/dist/browser.js +6661 -1392
- package/dist/build.d.ts +64 -56
- package/dist/build.js +702 -253
- package/dist/capture-shot-ChPQvPyi.d.ts +291 -0
- package/dist/capture.d.ts +1 -1
- package/dist/capture.js +230 -0
- package/dist/config-reference.json +1710 -1370
- package/dist/{icon-composition-BiOTwrvD.d.ts → icon-composition-CSnN2-Xt.d.ts} +169 -33
- package/dist/index.d.ts +57 -13
- package/dist/index.js +1684 -800
- package/dist/{manifest-types-CTT2eLbp.d.ts → manifest-types-E7m23eXL.d.ts} +18 -1
- package/dist/manifest.d.ts +2 -2
- package/dist/native-profile.d.ts +25 -11
- package/dist/native-profile.js +21 -1
- package/dist/native.js +1893 -1884
- package/dist/runtime.d.ts +1 -1
- package/docs/application-tile-reading.md +80 -0
- package/docs/authoring-model.md +16 -6
- package/docs/buildings-3d.md +168 -0
- package/docs/camera-moves-and-tile-cache.md +112 -0
- package/docs/capture-scenes.md +5 -1
- package/docs/capture-shots.md +173 -0
- package/docs/compiled-style-performance.md +4 -1
- package/docs/data-is-separate-from-design.md +103 -9
- package/docs/depth-markers.md +65 -0
- package/docs/dynamic-style-images.md +100 -0
- package/docs/native-artifact-profile.md +5 -3
- package/docs/public-api-and-browser-subpath.md +3 -3
- package/docs/route-drawing.md +191 -0
- package/docs/shared-visual-primitives.md +20 -0
- package/docs/street-lights.md +80 -0
- package/docs/style-states.md +73 -0
- package/docs/sun-and-light.md +125 -0
- package/docs/theme-transitions-and-blends.md +41 -2
- package/docs/themes-and-module-styles.md +7 -0
- package/package.json +4 -1
- package/dist/capture-scene-D6soWztE.d.ts +0 -161
|
@@ -128,9 +128,7 @@ published, immutable glyph collection independent of World data. The older `/fon
|
|
|
128
128
|
remains a mutable compatibility URL and does not make a resolved map byte-reproducible. The global
|
|
129
129
|
glyph `assetSetSha256` is not the same-domain value as the per-map `assetSetSha256` in
|
|
130
130
|
`build-manifest.json`.
|
|
131
|
-
|
|
132
|
-
reference the exact local faces `Oxanium Medium` and `Oxanium SemiBold`. Baedeker and Siegfried
|
|
133
|
-
each own a packaged directory containing Cormorant Garamond Regular, SemiBold, and Italic.
|
|
131
|
+
Baedeker and Siegfried each own a packaged directory containing Cormorant Garamond Regular, SemiBold, and Italic.
|
|
134
132
|
|
|
135
133
|
Name the official generation deliberately, or use another OpenMapTiles-compatible vector source:
|
|
136
134
|
|
|
@@ -320,21 +318,117 @@ explicit because the data contract cannot assume a sprite name. They require pos
|
|
|
320
318
|
`markings` evidence and reject `crossing=no`; both fields are semantic bindings, so custom schemas
|
|
321
319
|
can remap them without weakening that rule.
|
|
322
320
|
|
|
321
|
+
Datasets may also publish the optional, decorative `canopy` layer: points inside urban parks and
|
|
322
|
+
gardens, at most one per cell of a grid of about 20 m that measured tree canopy covers. Each point
|
|
323
|
+
tells how much room a drawn grove of tree crowns has and how dense the canopy around it is. The
|
|
324
|
+
points are not surveyed trees, are unrelated to the optional `tree` layer, and are not search or
|
|
325
|
+
interaction targets. `openMapTiles()` and `tileflowWorldV1Schema()` bind the layer as `canopy` and
|
|
326
|
+
its numeric fields as follows:
|
|
327
|
+
|
|
328
|
+
- `canopyRank` (`rank`): a stable integer from 0 through 15. Lower ranks appear at lower zooms, and
|
|
329
|
+
the rank can vary artwork.
|
|
330
|
+
- `canopyClearanceAtZoom15` (`clearance_px_z15`): 4, 6, 8, 12, 16, or 24. This is the lower bound
|
|
331
|
+
of the distance from the point to the nearest edge a crown must not cover, such as a park edge,
|
|
332
|
+
water, a building, a plaza, a pitch, or a roadway, in pixels of a 512-pixel tile at zoom 15. A
|
|
333
|
+
grove fits at zoom `z` when its drawn reach in screen pixels is at most
|
|
334
|
+
`clearance_px_z15 × 2^(z − 15)`.
|
|
335
|
+
- `canopyCoverPercent` (`cover_pct`): 3, 30, or 60, the lower bound of the share, in percent, of
|
|
336
|
+
canopy taller than 3 m within 10 m of the point. The field is absent where no measurement exists;
|
|
337
|
+
treat absence as a middle density.
|
|
338
|
+
|
|
339
|
+
Features are MultiPoints, one per identical set of attributes. Native zoom 15 holds every point,
|
|
340
|
+
zoom 14 holds the points whose rank is below 8 at the same positions, and clients overzoom zoom 15.
|
|
341
|
+
`validateTileflowWorldV1Tilejson(...)` accepts a TileJSON without `canopy`; once declared, the layer
|
|
342
|
+
must occur exactly once, declare native z14–z15, and advertise Number `rank`, `clearance_px_z15`, and
|
|
343
|
+
`cover_pct`. No module draws the layer. A source without it remains valid, and render passes bound
|
|
344
|
+
to it draw nothing. `capabilities: {canopy: false}` removes the binding, after which a pass with
|
|
345
|
+
`feature: 'canopy'` fails with a missing-binding error.
|
|
346
|
+
|
|
347
|
+
Draw groves with symbol render passes, one grove of whole crowns per point, sized to its room:
|
|
348
|
+
|
|
349
|
+
<!-- docs:check -->
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import {expr, field, fixed, renderPass, token, vegetation, withRenderStack} from '@tileflow/core';
|
|
353
|
+
|
|
354
|
+
// The grove artwork reaches 32 pixels from its center at icon size 1.
|
|
355
|
+
const clearance = expr.get(field('canopyClearanceAtZoom15'));
|
|
356
|
+
// The size that fills the room at one zoom, never larger than the artwork itself.
|
|
357
|
+
const fit = (divisor: number) => expr.min(expr.divide(clearance, divisor), 1);
|
|
358
|
+
|
|
359
|
+
const groves = (minZoom: number, fromRank: number, toRank: number) =>
|
|
360
|
+
renderPass({
|
|
361
|
+
placement: 'above-vegetation',
|
|
362
|
+
feature: 'canopy',
|
|
363
|
+
renderer: 'symbol',
|
|
364
|
+
selector: {
|
|
365
|
+
kind: 'all',
|
|
366
|
+
selectors: [
|
|
367
|
+
{field: 'canopyRank', kind: 'compare', operator: 'gte', value: fromRank},
|
|
368
|
+
{field: 'canopyRank', kind: 'compare', operator: 'lt', value: toRank},
|
|
369
|
+
],
|
|
370
|
+
},
|
|
371
|
+
style: {
|
|
372
|
+
minZoom,
|
|
373
|
+
icon: {
|
|
374
|
+
ignorePlacement: true,
|
|
375
|
+
image: token.image('vegetation.canopy.grove'),
|
|
376
|
+
overlap: 'always',
|
|
377
|
+
size: expr.interpolate({base: 2, kind: 'exponential'}, expr.zoom(), [
|
|
378
|
+
[14, fit(64)],
|
|
379
|
+
[15, fit(32)],
|
|
380
|
+
[16, fit(16)],
|
|
381
|
+
[17, fit(8)],
|
|
382
|
+
[18, fit(4)],
|
|
383
|
+
]),
|
|
384
|
+
},
|
|
385
|
+
text: {
|
|
386
|
+
field: 'o',
|
|
387
|
+
font: token.font('default'),
|
|
388
|
+
opacity: fixed(0, {reason: 'The core text only decides placement'}),
|
|
389
|
+
overlap: 'cooperative',
|
|
390
|
+
size: fixed(8, {reason: 'Matches the solid core of the grove artwork'}),
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
export const canopyGroves = withRenderStack(vegetation(), {
|
|
396
|
+
groves14: groves(14, 0, 4),
|
|
397
|
+
groves15: groves(15, 4, 8),
|
|
398
|
+
groves16: groves(16, 8, 16),
|
|
399
|
+
});
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
- Give each rank range its own pass with `minZoom`, so its points appear whole at their entry zoom.
|
|
403
|
+
MapLibre evaluates expressions that combine zoom and feature data at whole zoom levels and
|
|
404
|
+
interpolates between them, so a fade per feature would leave symbols half transparent. Zoom 14
|
|
405
|
+
tiles carry the ranks below 8, so the first pass may cover ranks 0–7.
|
|
406
|
+
- Size each grove from `canopyClearanceAtZoom15`. Here the size is `clearance / 32` at zoom 15 and
|
|
407
|
+
doubles with each zoom until the grove reaches the artwork's size, which keeps the reach within
|
|
408
|
+
the room.
|
|
409
|
+
- Collision tests an icon's whole image, including the transparent margin around the crowns. A small
|
|
410
|
+
invisible text the size of the grove's core therefore decides placement with
|
|
411
|
+
`overlap: 'cooperative'`, while the icon draws with `overlap: 'always'` and `ignorePlacement: true`.
|
|
412
|
+
Groves may overlap each other but give way to labels, which draw above the vegetation band and are
|
|
413
|
+
therefore placed first, with the default `never`.
|
|
414
|
+
|
|
415
|
+
Every theme of the map must define the image token, and the map's icon directories must provide the
|
|
416
|
+
artwork. Native artifacts replace `cooperative` with `allowOverlap: true`, so there groves can cover
|
|
417
|
+
labels; see [symbol overlap modes](shared-visual-primitives.md#symbol-overlap-modes).
|
|
418
|
+
|
|
323
419
|
The optional `business_corridor` extension contains activity-selected source footprints below
|
|
324
420
|
buildings. Its `activity_score`, `rank`, `min_zoom`, and `confidence` fields control a quiet local
|
|
325
421
|
warm tint without drawing POI-radius circles. Current building footprints may carry sparse
|
|
326
422
|
`building_tone=commercial|destination|active`; absence means the neutral base building. The three
|
|
327
423
|
values preserve why the warm tone was selected even when a theme maps them to the same color.
|
|
328
|
-
Building visibility is fixed by the layer zoom contract and never by optional semantics.
|
|
329
|
-
|
|
330
|
-
compatibility path, but new candidates do not emit those fields. The land module also exposes
|
|
424
|
+
Building visibility is fixed by the layer zoom contract and never by optional semantics. The land
|
|
425
|
+
module also exposes
|
|
331
426
|
`medical`, `education`, and `government` independently instead of collapsing their already distinct
|
|
332
427
|
source classes into one civic color.
|
|
333
428
|
|
|
334
429
|
World and text assets are independent contracts. `tileflowWorld()` selects `world-v1/current` or an
|
|
335
|
-
exact `releaseId + descriptorSha256`; a `glyphs` declaration contains its own complete URL.
|
|
336
|
-
|
|
337
|
-
remain usable because each official map owns or inherits a URL or packaged-font provider.
|
|
430
|
+
exact `releaseId + descriptorSha256`; a `glyphs` declaration contains its own complete URL. Every
|
|
431
|
+
official map compiles as imported because it owns or inherits a URL or packaged-font provider.
|
|
338
432
|
URL-backed maps become exact-byte reproducible
|
|
339
433
|
when the immutable global base-asset set is published and their explicit URL is updated to its
|
|
340
434
|
`assetSetSha256` path; no World or compiler fallback participates in that rollout. The global
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Depth markers
|
|
2
|
+
|
|
3
|
+
DOM markers float above the map: in a 3D scene a pin behind a tower still shows in front of it.
|
|
4
|
+
`createTileflowDepthMarkers` draws markers in the map's 3D scene instead. A MapLibre custom layer
|
|
5
|
+
draws them against the depth the map's extrusions write, so nearer buildings hide them as the camera
|
|
6
|
+
moves.
|
|
7
|
+
|
|
8
|
+
## Add markers
|
|
9
|
+
|
|
10
|
+
This example assumes an existing MapLibre map named `map` with 3D buildings, and two pictures drawn
|
|
11
|
+
by the application:
|
|
12
|
+
|
|
13
|
+
<!-- docs:check -->
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import {createTileflowDepthMarkers, type TileflowDepthMarkerMap} from '@tileflow/core/browser';
|
|
17
|
+
|
|
18
|
+
export function pinDestination(
|
|
19
|
+
map: TileflowDepthMarkerMap,
|
|
20
|
+
at: [number, number],
|
|
21
|
+
pin: HTMLCanvasElement,
|
|
22
|
+
ring: HTMLCanvasElement,
|
|
23
|
+
) {
|
|
24
|
+
const markers = createTileflowDepthMarkers(map);
|
|
25
|
+
// A pin facing the viewer, 28 × 40 CSS pixels, standing on the position.
|
|
26
|
+
markers.add({lngLat: at, image: pin, size: [28, 40], anchor: 'bottom'});
|
|
27
|
+
// A ring lying on the ground, 60 metres across.
|
|
28
|
+
const halo = markers.add({lngLat: at, image: ring, size: [60, 60], alignment: 'ground'});
|
|
29
|
+
return {
|
|
30
|
+
pulse: (opacity: number) => halo.update({opacity}),
|
|
31
|
+
dispose: () => markers.dispose(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
A marker has:
|
|
37
|
+
|
|
38
|
+
- **Position.** `lngLat`, and `altitude` in metres above the ground.
|
|
39
|
+
- **Picture.** `image`: an image, an image bitmap, a canvas or image data. After drawing into a
|
|
40
|
+
canvas again, pass it to `update({image})` so the marker uploads it once more. Markers that share
|
|
41
|
+
a picture share its upload.
|
|
42
|
+
- **Alignment.** `screen` (the default) faces the viewer at `size` in CSS pixels, like a pin.
|
|
43
|
+
`ground` lies flat on the ground at `size` in metres, in the camera's perspective, like a ring or
|
|
44
|
+
an arrow painted on the street.
|
|
45
|
+
- **Anchor and placement.** `anchor` (`center` or `bottom`), `offset` in CSS pixels for screen
|
|
46
|
+
markers, and `rotation` in degrees clockwise: from the screen's up for screen markers, from north
|
|
47
|
+
for ground markers.
|
|
48
|
+
- **Opacity.** From 0 to 1.
|
|
49
|
+
- **`occlude`.** Defaults to true: nearer 3D content hides the marker. Set it to false for a marker
|
|
50
|
+
that must always show.
|
|
51
|
+
|
|
52
|
+
Markers test against the scene's depth without writing to it, so they never hide one another. A
|
|
53
|
+
screen marker takes the depth of its position: a tower in front of the position hides the whole
|
|
54
|
+
pin, a tower in front of part of it hides that part.
|
|
55
|
+
|
|
56
|
+
## Layer and lifecycle
|
|
57
|
+
|
|
58
|
+
The markers share one custom layer, `tileflow-depth-markers` unless `id` names another, added with
|
|
59
|
+
the first marker. `beforeId` places it under a layer, given as an id or as a function that finds the
|
|
60
|
+
layer again after a style change. After a style change the layer puts itself back, and pictures
|
|
61
|
+
are uploaded again. `dispose()` removes the layer and releases its GPU resources.
|
|
62
|
+
|
|
63
|
+
Depth markers need MapLibre GL JS on WebGL 2 (GL JS 5 or later). They work on globe and Mercator
|
|
64
|
+
projections. They draw pictures only: draw text and effects into the picture, or keep DOM markers
|
|
65
|
+
for interactive content.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Dynamic style images
|
|
2
|
+
|
|
3
|
+
Use `registerImage` on an existing `TileflowThemeController` from `@tileflow/core/browser` when an
|
|
4
|
+
application draws RGBA pixels that depend on a theme or style state. The controller owns the image's
|
|
5
|
+
registration, refresh and restoration. The application owns its drawing and prepares asynchronous
|
|
6
|
+
assets before registration. This browser API is tested with MapLibre GL JS 6.13; it is not a Native
|
|
7
|
+
or Static Maps image contract.
|
|
8
|
+
|
|
9
|
+
Start with the [Core guide](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/README.md)
|
|
10
|
+
and [theme controller setup](theme-transitions-and-blends.md#use-the-core-controller).
|
|
11
|
+
The map's symbol layer must already refer to the same image ID in every theme. Register before its
|
|
12
|
+
first placement. Choose an ID that does not exist in the sprite or belong to another application
|
|
13
|
+
resource. Registering an image does not create a layer or add points to a source.
|
|
14
|
+
|
|
15
|
+
## Draw an alert from state
|
|
16
|
+
|
|
17
|
+
This helper runs after browser mounting, with the controller for the existing map. The image is
|
|
18
|
+
64 physical pixels at pixel ratio 2, so it occupies 32 logical pixels before the layer's icon scale.
|
|
19
|
+
|
|
20
|
+
<!-- docs:check -->
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import type {TileflowThemeController} from '@tileflow/core/browser';
|
|
24
|
+
|
|
25
|
+
export function registerAlert(controller: TileflowThemeController) {
|
|
26
|
+
const canvas = document.createElement('canvas');
|
|
27
|
+
canvas.width = canvas.height = 64;
|
|
28
|
+
const context = canvas.getContext('2d', {willReadFrequently: true});
|
|
29
|
+
if (!context) throw new Error('Canvas 2D is unavailable.');
|
|
30
|
+
|
|
31
|
+
return controller.registerImage('live-alert', {
|
|
32
|
+
width: 64,
|
|
33
|
+
height: 64,
|
|
34
|
+
pixelRatio: 2,
|
|
35
|
+
states: ['alert'],
|
|
36
|
+
render({theme, states}) {
|
|
37
|
+
context.clearRect(0, 0, 64, 64);
|
|
38
|
+
context.beginPath();
|
|
39
|
+
context.arc(32, 32, 24, 0, Math.PI * 2);
|
|
40
|
+
context.fillStyle = states.alert ? '#ee624e' : '#edbd53';
|
|
41
|
+
context.fill();
|
|
42
|
+
context.strokeStyle = theme === 'night' ? '#ffffff' : '#253b42';
|
|
43
|
+
context.lineWidth = 4;
|
|
44
|
+
context.stroke();
|
|
45
|
+
return context.getImageData(0, 0, 64, 64).data;
|
|
46
|
+
},
|
|
47
|
+
onError(error) {
|
|
48
|
+
console.error('Alert image could not refresh:', error);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function activateAlert(controller: TileflowThemeController) {
|
|
54
|
+
await controller.setState('alert', true);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The resource receives the states listed in `states`, including intermediate values during numeric
|
|
59
|
+
state transitions. `theme` is the committed theme, or the nearest theme of a blend. `blend` exposes
|
|
60
|
+
its position and ordered theme names for continuous artwork interpolation. States not set through
|
|
61
|
+
this controller remain undefined; an explicit null is distinct from an unset state.
|
|
62
|
+
|
|
63
|
+
## Update and release
|
|
64
|
+
|
|
65
|
+
Theme changes and changes to declared states schedule a refresh. Identical inputs do no work;
|
|
66
|
+
several changes before the next animation frame coalesce. The `setState` promise describes the
|
|
67
|
+
state transition, not completion of the next image frame. There is no independent animation clock.
|
|
68
|
+
For camera values, new application data or another external input, call the returned handle's
|
|
69
|
+
`refresh()`. Read those values inside `render`, and return `undefined` when the existing pixels are
|
|
70
|
+
still correct. Undefined output on the first render creates a transparent image.
|
|
71
|
+
|
|
72
|
+
`render` is synchronous and returns exactly `width * height * 4` RGBA bytes as a `Uint8Array` or
|
|
73
|
+
`Uint8ClampedArray`. The controller copies the buffer. Width and height must be integers from 1
|
|
74
|
+
through 2048; pixel ratio must be positive and finite. All three remain fixed until disposal.
|
|
75
|
+
Changing pixel ratio while keeping physical size would change logical size and collision geometry,
|
|
76
|
+
so choose it at registration rather than following device pixel ratio changes automatically.
|
|
77
|
+
|
|
78
|
+
A style reload restores the last valid image before placement. WebGL context loss suspends image
|
|
79
|
+
work; restoration replays pixels and processes pending inputs. `handle.dispose()` removes only
|
|
80
|
+
that resource's image and cancels its pending work. `controller.dispose()` releases all of its
|
|
81
|
+
images. Map removal cancels work and releases listeners. Disposal is idempotent.
|
|
82
|
+
|
|
83
|
+
Invalid registration throws before claiming a resource. Later render failures keep the last valid
|
|
84
|
+
pixels and call `onError`, or `console.error` when omitted. A later refresh can retry. Another image
|
|
85
|
+
owner is never silently overwritten. The application must not call `addImage`, `updateImage` or
|
|
86
|
+
`removeImage` for a registered ID. Dynamic IDs must stay identical across blended themes; mixing
|
|
87
|
+
them with theme-specific atlas artwork is rejected before the blend replaces the style.
|
|
88
|
+
|
|
89
|
+
## Placement and scope
|
|
90
|
+
|
|
91
|
+
This API changes image pixels only. It does not resize icons, split layers, change padding,
|
|
92
|
+
anchors, sort order, overlap, filters or source features. Native symbol placement and collisions
|
|
93
|
+
remain governed by the authored style. Transparent pixels still occupy the image's existing
|
|
94
|
+
geometry: this API does not define separate visual and collision bounds.
|
|
95
|
+
|
|
96
|
+
All features using an ID share its pixels and state. Independent per-feature animation, SDF
|
|
97
|
+
generation, image loading, GPU composition and automatic framework props are outside this API.
|
|
98
|
+
Keep every selectable icon in the same symbol layer RGBA, including fallbacks; mixing these images
|
|
99
|
+
with SDF icons does not become supported through registration.
|
|
100
|
+
Profile the application's drawing cost and visible symbol rendering on its target devices.
|
|
@@ -72,8 +72,10 @@ Dev's `native-lowering-v1` preparation policy explicitly converts a fixed `{type
|
|
|
72
72
|
projection to Native's implicit Mercator before this validator runs. This is a recorded projection
|
|
73
73
|
change, not globe support or visual equivalence. One authored map serves web and native preparation;
|
|
74
74
|
no native-specific Streets definition is used. Preparation adapts only finite `line-cap` and
|
|
75
|
-
`line-dasharray` decisions into constant-property layers
|
|
76
|
-
|
|
75
|
+
`line-dasharray` decisions into constant-property layers, and replaces constant `icon-overlap` and
|
|
76
|
+
`text-overlap` modes with their `*-allow-overlap` equivalent. Other unsupported properties have no
|
|
77
|
+
implicit fallback. The raw validator continues to reject unlowered unsupported expressions and
|
|
78
|
+
overlap modes. See the [preparation contract](https://github.com/tileflow/tileflow-sdk/blob/main/packages/dev/docs/native-artifacts.md)
|
|
77
79
|
for the grammar, expansion limits and pending native visual checks. The lowering checks establish
|
|
78
80
|
per-feature selection and cap/dash values, not global feature draw order. A copied `line-sort-key`
|
|
79
81
|
cannot preserve ordering between simultaneous physical branches; overlapping features may produce
|
|
@@ -81,7 +83,7 @@ a different composition even when every property value is retained.
|
|
|
81
83
|
|
|
82
84
|
`native-build.json` uses strict schema version 2 and `preparationVersion: 'native-lowering-v1'`.
|
|
83
85
|
Its `transformations` array is ordered by map/theme and binds the input/lowered style hashes,
|
|
84
|
-
projection policy
|
|
86
|
+
projection policy, physical layer spans and symbol overlap replacements. The runtime manifest remains version 1. An old v1
|
|
85
87
|
build-record decoder must reject v2 rather than silently ignore the preparation evidence.
|
|
86
88
|
|
|
87
89
|
Text must use a valid glyph template containing `{fontstack}` and `{range}`, or a complete static
|
|
@@ -16,9 +16,9 @@ inside a named `maplibreOverlay()` bound to a named Team source.
|
|
|
16
16
|
|
|
17
17
|
Advanced cartography remains in the same public language: `withRenderStack()`, `renderPass()`, and
|
|
18
18
|
`refineRenderTarget()` address owner-local semantic targets without exposing physical layer IDs,
|
|
19
|
-
sources, source layers, raw filters, or before/after anchors. Typed `field()`
|
|
20
|
-
`expr.*` builders stay semantic until the compiler's single lowering boundary. There
|
|
21
|
-
subpath or alternate compiler integration API.
|
|
19
|
+
sources, source layers, raw filters, or physical before/after layer anchors. Typed `field()`
|
|
20
|
+
references and `expr.*` builders stay semantic until the compiler's single lowering boundary. There
|
|
21
|
+
is no recipe subpath or alternate compiler integration API.
|
|
22
22
|
|
|
23
23
|
`mapRevisionSha256` hashes only resolved cartography: the effective design after `extends`, the
|
|
24
24
|
semantic language, compiler-owned effective contributions, and exact source icon/font identities.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Route drawing
|
|
2
|
+
|
|
3
|
+
An application that draws a route on a Tileflow map can take the route's colours from the theme and
|
|
4
|
+
draw the line on with a moving head. Themes set route inks; the theme controller returns them for
|
|
5
|
+
the theme on screen; `createTileflowLineProgress` animates a group of line layers along their
|
|
6
|
+
length; `createTileflowGroundLayers` keeps them on the ground, under the map's 3D buildings.
|
|
7
|
+
|
|
8
|
+
## Set route inks in a theme
|
|
9
|
+
|
|
10
|
+
Route inks are optional theme tokens. A theme that sets none uses defaults for its colour scheme:
|
|
11
|
+
a blue line on a dark edge for light themes, a lighter blue with a glow and a bright core for dark
|
|
12
|
+
themes.
|
|
13
|
+
|
|
14
|
+
| Token | Kind | Use |
|
|
15
|
+
| -------------------- | ------ | ----------------------------------------------------- |
|
|
16
|
+
| `route.line` | colour | The chosen route. |
|
|
17
|
+
| `route.casing` | colour | The edge each side of the line, apart from the roads. |
|
|
18
|
+
| `route.core` | colour | A thin line along the middle. |
|
|
19
|
+
| `route.glow` | colour | A soft halo around the route. |
|
|
20
|
+
| `route.alternative` | colour | Other routes offered beside the chosen one. |
|
|
21
|
+
| `route.glow.opacity` | number | The halo's opacity, from 0 (none) to 1. |
|
|
22
|
+
| `route.core.opacity` | number | The core's opacity, from 0 (none) to 1. |
|
|
23
|
+
|
|
24
|
+
Every theme of a map carries the same tokens, so a map that sets route inks sets them in each of
|
|
25
|
+
its themes:
|
|
26
|
+
|
|
27
|
+
<!-- docs:check -->
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import {defineTheme, type TileflowTheme} from '@tileflow/core';
|
|
31
|
+
|
|
32
|
+
export function withNeonRoute(base: TileflowTheme, id: string): TileflowTheme {
|
|
33
|
+
return defineTheme(base, {
|
|
34
|
+
id,
|
|
35
|
+
version: 1,
|
|
36
|
+
colorScheme: base.colorScheme,
|
|
37
|
+
tokens: {
|
|
38
|
+
color: {
|
|
39
|
+
'route.line': '#ff4fae',
|
|
40
|
+
'route.casing': '#0b0f12',
|
|
41
|
+
'route.core': '#ffe0f0',
|
|
42
|
+
'route.glow': '#ff4fae',
|
|
43
|
+
},
|
|
44
|
+
number: {'route.glow.opacity': 0.55, 'route.core.opacity': 0.9},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The compiler publishes the resolved inks in the style's `tileflow:route` metadata.
|
|
51
|
+
`readTileflowRouteInks(style.metadata)` reads them from a style document;
|
|
52
|
+
`tileflowStyleRouteInks(style)` also falls back on the defaults for a style compiled without them.
|
|
53
|
+
|
|
54
|
+
## Draw a route in the theme's inks
|
|
55
|
+
|
|
56
|
+
The theme controller's `getRouteInks()` returns the inks of the theme on screen. During a blend it
|
|
57
|
+
mixes them between the neighbouring themes at the blend's position, in the same colour space as
|
|
58
|
+
the map's own colours. Read them again after `setTheme` or `setBlend`.
|
|
59
|
+
|
|
60
|
+
`createTileflowLineProgress(map, {layers})` owns the `line-gradient` and `line-opacity` of a group of
|
|
61
|
+
line layers. Their source must be GeoJSON with `lineMetrics: true`. The controller shows the line
|
|
62
|
+
from `start` to `end` (shares of its length), a highlight at the head while the line is being drawn,
|
|
63
|
+
and an optional light at `light`. It writes only what changed, paints a layer again when a style
|
|
64
|
+
change re-creates it, and skips layers that are not on the map.
|
|
65
|
+
|
|
66
|
+
This example assumes an existing MapLibre map named `map` whose style already holds four line
|
|
67
|
+
layers over a GeoJSON source with `lineMetrics: true`:
|
|
68
|
+
|
|
69
|
+
<!-- docs:check -->
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import {
|
|
73
|
+
createTileflowLineProgress,
|
|
74
|
+
type TileflowLineProgressMap,
|
|
75
|
+
type TileflowRouteInks,
|
|
76
|
+
} from '@tileflow/core/browser';
|
|
77
|
+
|
|
78
|
+
const layers = (inks: TileflowRouteInks) => [
|
|
79
|
+
{id: 'route-glow', color: inks.glow, highlight: '#ffffff', opacity: inks.glowOpacity},
|
|
80
|
+
{id: 'route-casing', color: inks.casing},
|
|
81
|
+
{id: 'route-line', color: inks.line, highlight: inks.core},
|
|
82
|
+
{id: 'route-core', color: inks.core, highlight: '#ffffff', opacity: inks.coreOpacity},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
export async function drawRoute(
|
|
86
|
+
map: TileflowLineProgressMap,
|
|
87
|
+
inks: TileflowRouteInks,
|
|
88
|
+
lengthInMetres: number,
|
|
89
|
+
) {
|
|
90
|
+
const route = createTileflowLineProgress(map, {
|
|
91
|
+
layers: layers(inks),
|
|
92
|
+
// A 90 m highlight, whatever the route's length.
|
|
93
|
+
headLength: Math.min(0.2, 90 / lengthInMetres),
|
|
94
|
+
initial: {end: 0},
|
|
95
|
+
});
|
|
96
|
+
await route.animate({to: {end: 1}, duration: 2500});
|
|
97
|
+
// A light runs along the drawn route.
|
|
98
|
+
await route.animate({to: {light: 1}, duration: 1500});
|
|
99
|
+
route.set({light: undefined});
|
|
100
|
+
return {
|
|
101
|
+
recolour: (next: TileflowRouteInks) => route.configure({layers: layers(next)}),
|
|
102
|
+
fadeOut: () => route.animate({to: {opacity: 0}, duration: 400}),
|
|
103
|
+
dispose: () => route.dispose(),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`animate` resolves `true` when the line gets where it was sent, and `false` when `set`, another
|
|
109
|
+
animation, or `dispose` stops it first; the line stays where it was stopped. A browser preference
|
|
110
|
+
for reduced motion shows the target at once. `tileflowLineProgressGradient(state, options)` returns
|
|
111
|
+
the same gradient for a single state, for styles built ahead of time.
|
|
112
|
+
|
|
113
|
+
## Keep a route under 3D buildings
|
|
114
|
+
|
|
115
|
+
MapLibre draws a 2D layer that comes after a 3D layer, such as extruded buildings or a custom layer
|
|
116
|
+
drawn in 3D, without testing depth. A route added under the labels therefore crosses in front of
|
|
117
|
+
the buildings. A route that lies on the ground goes before the first 3D layer: every building then
|
|
118
|
+
hides it exactly where it stands in front of it.
|
|
119
|
+
|
|
120
|
+
`tileflowGroundBeforeId(map)` returns that layer: the first 3D layer above the roads, or the start
|
|
121
|
+
of the vegetation when the buildings are flat, so a ground layer still covers the roads and the
|
|
122
|
+
flat buildings. Pass it as `beforeId` to `map.addLayer` for a single layer.
|
|
123
|
+
|
|
124
|
+
`createTileflowGroundLayers(map, {layers, occludedOpacity})` keeps a group of layers there, bottom
|
|
125
|
+
to top as given:
|
|
126
|
+
|
|
127
|
+
- It adds the layers, or moves those already on the map. A layer whose source is not on the map yet
|
|
128
|
+
is added when the source is.
|
|
129
|
+
- After a style change, it brings back the layers the change dropped, with the paint and layout
|
|
130
|
+
last written through the group.
|
|
131
|
+
- When a 3D layer lands above the group, such as a custom layer added later, the group moves under
|
|
132
|
+
it.
|
|
133
|
+
- `occludedOpacity` lets a line show through 3D content: by layer id, how much of the line shows
|
|
134
|
+
where buildings hide it, from 0 (nothing, the default) to 1. The group draws a copy of the line
|
|
135
|
+
over the 3D content at the line's opacity times that share, as a navigation app shows a route
|
|
136
|
+
through buildings. The copy lies over the group's other layers too, so where the line shows, it
|
|
137
|
+
tints the layers drawn above the line slightly; a line at the top of the group shows no change.
|
|
138
|
+
Only line layers whose `line-opacity` is a number can show through.
|
|
139
|
+
|
|
140
|
+
Paint the group's layers through the group, so the copies follow them. A helper that paints layers
|
|
141
|
+
takes the group in place of the map:
|
|
142
|
+
|
|
143
|
+
<!-- docs:check -->
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import {
|
|
147
|
+
createTileflowGroundLayers,
|
|
148
|
+
createTileflowLineProgress,
|
|
149
|
+
type TileflowGroundLayersMap,
|
|
150
|
+
type TileflowRouteInks,
|
|
151
|
+
} from '@tileflow/core/browser';
|
|
152
|
+
|
|
153
|
+
export function addRoute(map: TileflowGroundLayersMap, inks: TileflowRouteInks) {
|
|
154
|
+
// The map already holds a GeoJSON source named 'route' with `lineMetrics: true`.
|
|
155
|
+
const line = (id: string, width: number) => ({
|
|
156
|
+
id,
|
|
157
|
+
type: 'line',
|
|
158
|
+
source: 'route',
|
|
159
|
+
layout: {'line-cap': 'round', 'line-join': 'round'},
|
|
160
|
+
paint: {'line-width': width},
|
|
161
|
+
});
|
|
162
|
+
const ground = createTileflowGroundLayers(map, {
|
|
163
|
+
layers: [line('route-casing', 12), line('route-line', 8), line('route-core', 2)],
|
|
164
|
+
occludedOpacity: {'route-line': 0.35},
|
|
165
|
+
});
|
|
166
|
+
const route = createTileflowLineProgress(ground, {
|
|
167
|
+
layers: [
|
|
168
|
+
{id: 'route-casing', color: inks.casing},
|
|
169
|
+
{id: 'route-line', color: inks.line, highlight: inks.core},
|
|
170
|
+
{id: 'route-core', color: inks.core, opacity: inks.coreOpacity},
|
|
171
|
+
],
|
|
172
|
+
initial: {end: 0},
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
draw: () => route.animate({to: {end: 1}, duration: 2500}),
|
|
176
|
+
remove: () => {
|
|
177
|
+
route.dispose();
|
|
178
|
+
ground.dispose();
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`dispose` takes the group's layers and copies off the map. The copies keep the layer's id with
|
|
185
|
+
`:occluded` after it.
|
|
186
|
+
|
|
187
|
+
## Cost
|
|
188
|
+
|
|
189
|
+
Each frame of an animation writes one `line-gradient` per layer, and MapLibre rebuilds that layer's
|
|
190
|
+
gradient texture. A route of a few layers costs little; avoid animating dozens of long lines at
|
|
191
|
+
once.
|
|
@@ -22,6 +22,26 @@ Compounds express common cartographic structures: `AreaStyle` has `fill` and `ou
|
|
|
22
22
|
optional `text`, `icon`, and `marker`. Geographic selection remains owned by semantic modules;
|
|
23
23
|
ordinary visual styles intentionally do not accept raw source filters.
|
|
24
24
|
|
|
25
|
+
## Symbol overlap modes
|
|
26
|
+
|
|
27
|
+
`text.allowOverlap` and `icon.allowOverlap` either hide a part that collides with a symbol placed
|
|
28
|
+
before it or always show it. `overlap` selects one of three modes instead:
|
|
29
|
+
|
|
30
|
+
- `never`: hide the part when it collides.
|
|
31
|
+
- `always`: show the part even when it collides.
|
|
32
|
+
- `cooperative`: show the part unless it collides with a symbol placed with `never`. It may overlap
|
|
33
|
+
symbols placed with `always` or `cooperative`.
|
|
34
|
+
|
|
35
|
+
MapLibre places symbols from the top layer down, so a symbol placed before another is usually higher
|
|
36
|
+
in the stack. `overlap` takes precedence over `allowOverlap` on the same part, including an
|
|
37
|
+
`allowOverlap` default supplied by a module or already present on a refined target, so the compiled
|
|
38
|
+
layer carries only `text-overlap` or `icon-overlap`. MapLibre Native has no overlap modes, so native
|
|
39
|
+
artifact preparation replaces the mode with the matching `text-allow-overlap` or
|
|
40
|
+
`icon-allow-overlap`: `false` for `never` and `true` for `always` and `cooperative`. There a
|
|
41
|
+
cooperative part no longer gives way to symbols placed with `never`. The
|
|
42
|
+
[canopy grove recipe](data-is-separate-from-design.md) uses a cooperative text to decide where
|
|
43
|
+
icons draw.
|
|
44
|
+
|
|
25
45
|
## Line-fitted text backgrounds
|
|
26
46
|
|
|
27
47
|
`text.background` draws one opaque strip behind each shaped line of a label, fitted to that line's
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Street lights
|
|
2
|
+
|
|
3
|
+
`createTileflowStreetLights` lights a Tileflow map's streets at night from its own vector tiles. The
|
|
4
|
+
lamps the map's data holds shine where they stand, each with a pool of light on the ground and a
|
|
5
|
+
glowing head. Streets and pavements the data maps as lit, but whose lamps it does not hold, take a
|
|
6
|
+
soft light along them that claims no lamp's place. As the theme controller's sun darkens the map,
|
|
7
|
+
the lights switch on one by one; they go out as day comes.
|
|
8
|
+
|
|
9
|
+
It is a custom WebGL2 layer for MapLibre GL JS, drawn above the map's buildings and below its
|
|
10
|
+
labels. When `createTileflowBuildings3D` raises the buildings, they hide the lamps behind them. It
|
|
11
|
+
needs WebGL2 and the Mercator projection, and shines from zoom 14 by default.
|
|
12
|
+
|
|
13
|
+
## Light the streets
|
|
14
|
+
|
|
15
|
+
This example assumes an existing MapLibre map showing a Tileflow map whose vector source is called
|
|
16
|
+
`tileflow`, as Tileflow's maps call World, and a theme controller for it:
|
|
17
|
+
|
|
18
|
+
<!-- docs:check -->
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import {
|
|
22
|
+
createTileflowStreetLights,
|
|
23
|
+
type TileflowStreetLightsMap,
|
|
24
|
+
type TileflowThemeController,
|
|
25
|
+
} from '@tileflow/core/browser';
|
|
26
|
+
|
|
27
|
+
export function lightTheStreets(map: TileflowStreetLightsMap, controller: TileflowThemeController) {
|
|
28
|
+
// The lights switch on as the theme's or the application's sun darkens the map.
|
|
29
|
+
return createTileflowStreetLights({map, controller});
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The lights follow `controller.getSun()`: they switch on as its `brightness` falls through dusk, each
|
|
34
|
+
lamp at its own moment, and are all on at night. A theme whose night has no `lighting.sun` keeps
|
|
35
|
+
the default daylight sun, so its lights stay off; see
|
|
36
|
+
[sun and light](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/sun-and-light.md).
|
|
37
|
+
`on` reports how far they have switched on, from 0 to 1, `stats` the lamps on screen, and
|
|
38
|
+
`dispose()` takes the layer off the map.
|
|
39
|
+
|
|
40
|
+
## What the light comes from
|
|
41
|
+
|
|
42
|
+
The layer reads the source's z15 tiles round the camera, the zoom at which World carries its street
|
|
43
|
+
detail:
|
|
44
|
+
|
|
45
|
+
- Lamps from World's `street_furniture` points whose `subclass` is `street_lamp`. Where the data
|
|
46
|
+
gives them, a lamp's `height`, `lamp_mount`, `light_count`, `lamp_type` and `light_colour` set how
|
|
47
|
+
high it stands, how far its light reaches and its colour: sodium lamps shine orange, LED and
|
|
48
|
+
metal-halide lamps white. Other lamps take `color`, a warm white by default.
|
|
49
|
+
- Streets in World's `transportation` layer, and pavements in its `sidewalk` layer, that the data
|
|
50
|
+
maps as lit (`lit`). A street mapped as unlit never takes light.
|
|
51
|
+
|
|
52
|
+
How much of this a city's data holds varies widely: some map most of their lamps or most of their
|
|
53
|
+
lit streets, others almost none. Where the data says nothing, the streets stay dark.
|
|
54
|
+
|
|
55
|
+
`estimate` chooses where the layer adds light the data does not map as lamps:
|
|
56
|
+
|
|
57
|
+
- `lit` (the default): along the streets and pavements mapped as lit.
|
|
58
|
+
- `streets`: also along streets the data says nothing about, in tiles where the data maps few
|
|
59
|
+
lamps. This invents light, as night windows do, for maps that want every street lit; a tile whose
|
|
60
|
+
data maps its lamps keeps to them.
|
|
61
|
+
- `none`: only mapped lamps shine.
|
|
62
|
+
|
|
63
|
+
Estimated light keeps to the middle of its street and shows no lamp head, so it reads as a lit
|
|
64
|
+
street rather than as lamps that may not stand there.
|
|
65
|
+
|
|
66
|
+
## Options
|
|
67
|
+
|
|
68
|
+
`intensity` scales the light from 0 to 2 (1 by default). `minzoom` (14 by default) and `maxTiles`
|
|
69
|
+
(25 by default, at most 64) bound where and how far the lights shine. `beforeId` places the layer
|
|
70
|
+
before another one; `source` names a vector source other than `tileflow`; `transformRequest`
|
|
71
|
+
applies the map's request rules to the tiles the layer reads.
|
|
72
|
+
|
|
73
|
+
Layers that read the same source share each tile's download, so the street lights and the 3D
|
|
74
|
+
buildings fetch a tile once between them.
|
|
75
|
+
|
|
76
|
+
## Limits
|
|
77
|
+
|
|
78
|
+
- Mercator only; the layer draws nothing below its `minzoom`.
|
|
79
|
+
- The light falls on the ground; walls the lamps face are not lit by them.
|
|
80
|
+
- No terrain: lamps stand on flat ground.
|