@tileflow/core 0.1.0-alpha.4 → 0.1.0-alpha.41
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/LICENSE +201 -0
- package/README.md +318 -26
- package/THIRD_PARTY_NOTICES.md +444 -0
- package/dist/3d-CzLTD0RK.d.ts +1092 -0
- package/dist/3d.d.ts +1 -0
- package/dist/3d.js +12660 -0
- package/dist/browser.d.ts +2278 -0
- package/dist/browser.js +21826 -0
- package/dist/build.d.ts +595 -0
- package/dist/build.js +29737 -0
- package/dist/capture-shot-ChPQvPyi.d.ts +291 -0
- package/dist/capture.d.ts +9 -0
- package/dist/capture.js +510 -0
- package/dist/config-reference.json +40527 -0
- package/dist/icon-composition-HUh_Lj8Z.d.ts +2516 -0
- package/dist/index.d.ts +1805 -162
- package/dist/index.js +26400 -3488
- package/dist/manifest-types-DnhSJjCM.d.ts +902 -0
- package/dist/manifest.d.ts +9 -0
- package/dist/manifest.js +449 -0
- package/dist/native-profile.d.ts +206 -0
- package/dist/native-profile.js +9339 -0
- package/dist/native.d.ts +365 -0
- package/dist/native.js +5970 -0
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +1445 -0
- package/dist/worker/camera-tile-worker.global.js +20 -0
- package/docs/application-tile-reading.md +86 -0
- package/docs/authoring-model.md +269 -0
- package/docs/boundary-areas-and-choropleths.md +217 -0
- package/docs/buildings-3d.md +608 -0
- package/docs/camera-moves-and-tile-cache.md +112 -0
- package/docs/capture-scenes.md +40 -0
- package/docs/capture-shots.md +173 -0
- package/docs/cartographic-defaults.md +210 -0
- package/docs/compiled-style-performance.md +286 -0
- package/docs/data-is-separate-from-design.md +610 -0
- package/docs/depth-markers.md +65 -0
- package/docs/dynamic-style-images.md +100 -0
- package/docs/globe-atmosphere.md +113 -0
- package/docs/hosted-session-authorization.md +106 -0
- package/docs/map-and-terrain-examples.md +156 -0
- package/docs/map-emphasis.md +89 -0
- package/docs/native-artifact-profile.md +152 -0
- package/docs/native-resource-urls.md +369 -0
- package/docs/nautical-marks.md +308 -0
- package/docs/public-api-and-browser-subpath.md +101 -0
- package/docs/route-drawing.md +191 -0
- package/docs/shared-visual-primitives.md +101 -0
- package/docs/street-lights.md +127 -0
- package/docs/street-sky.md +156 -0
- package/docs/style-states.md +73 -0
- package/docs/sun-and-light.md +167 -0
- package/docs/theme-transitions-and-blends.md +317 -0
- package/docs/themes-and-module-styles.md +156 -0
- package/docs/trees-3d.md +295 -0
- package/package.json +67 -8
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Camera moves and the tile cache
|
|
2
|
+
|
|
3
|
+
A long camera move outruns the network: the new view's tiles take longer to arrive than the move
|
|
4
|
+
takes. Some examples are a flight from a street to a region and back, a guided tour, or a scripted
|
|
5
|
+
opening. MapLibre keeps the tiles it no longer shows in an out-of-view cache, so a camera that
|
|
6
|
+
comes back finds them ready. This guide covers what keeps that cache useful and what empties it,
|
|
7
|
+
as MapLibre GL JS 6 behaves.
|
|
8
|
+
|
|
9
|
+
## What the cache keeps
|
|
10
|
+
|
|
11
|
+
The out-of-view cache holds about five views' worth of tiles per source:
|
|
12
|
+
|
|
13
|
+
- **Size.** It is the tiles across the map times `maxTileCacheZoomLevels`, 5 by default.
|
|
14
|
+
`maxTileCacheSize` caps it.
|
|
15
|
+
- **Too small for a flight.** A move that crosses many zooms (a street, the region, the street
|
|
16
|
+
again) needs more. Raise `maxTileCacheZoomLevels` on the map, for example to 12, so tiles
|
|
17
|
+
loaded early in a sequence are still there at its end.
|
|
18
|
+
- **Exact tiles only.** While a tile the camera needs is loading, MapLibre falls back on its loaded
|
|
19
|
+
parents or children only among the tiles it shows. It does not fall back on tiles it keeps out
|
|
20
|
+
of view. A cached tile helps only when it is the very tile a frame needs.
|
|
21
|
+
|
|
22
|
+
## What empties it
|
|
23
|
+
|
|
24
|
+
Changes that make MapLibre lay a source's tiles out again also empty that source's out-of-view
|
|
25
|
+
cache. The tiles in view are parsed again; the cached ones are dropped and come back from the
|
|
26
|
+
network when the camera returns.
|
|
27
|
+
|
|
28
|
+
**Changes that lay tiles out:**
|
|
29
|
+
|
|
30
|
+
- Any layout property, `visibility` included, through `setLayoutProperty`.
|
|
31
|
+
- A filter, through `setFilter`.
|
|
32
|
+
- A new value, through `setPaintProperty`, for a paint property whose value reads feature data,
|
|
33
|
+
such as `['get', ...]` or `['feature-state', ...]`.
|
|
34
|
+
- `setStyle` with layers that differ. A style that is not diffed starts every source afresh.
|
|
35
|
+
- In a theme blend, a change of the nearest theme over values like these. `setBlend({hold: true})`
|
|
36
|
+
keeps them back during a move; see
|
|
37
|
+
[theme transitions and blends](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/theme-transitions-and-blends.md).
|
|
38
|
+
|
|
39
|
+
**Changes that keep the cache:**
|
|
40
|
+
|
|
41
|
+
- A constant paint value, or one that reads only the zoom.
|
|
42
|
+
- `setFeatureState`, which rewrites the paint of the features it names.
|
|
43
|
+
- Global state read by values that read no feature data.
|
|
44
|
+
|
|
45
|
+
During a camera move, change only what keeps the cache. Make layout changes, filters and theme
|
|
46
|
+
crossings before the move starts or after it ends. To mark features during a move, prefer feature
|
|
47
|
+
state over a filter.
|
|
48
|
+
|
|
49
|
+
## Warm cameras before a move
|
|
50
|
+
|
|
51
|
+
MapLibre GL JS 6 has no public way to load a camera's tiles without showing it. Until it does,
|
|
52
|
+
warm cameras while something covers the map, such as a title card or a loading screen. Visit each
|
|
53
|
+
camera with `jumpTo`, wait until its tiles are in, then go on to the next:
|
|
54
|
+
|
|
55
|
+
<!-- docs:check -->
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
type WarmableMap = {
|
|
59
|
+
jumpTo(options: {
|
|
60
|
+
center: [number, number];
|
|
61
|
+
zoom: number;
|
|
62
|
+
bearing?: number;
|
|
63
|
+
pitch?: number;
|
|
64
|
+
}): unknown;
|
|
65
|
+
loaded(): boolean;
|
|
66
|
+
on(event: 'render', listener: () => void): unknown;
|
|
67
|
+
off(event: 'render', listener: () => void): unknown;
|
|
68
|
+
triggerRepaint(): void;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type Camera = {center: [number, number]; zoom: number; bearing?: number; pitch?: number};
|
|
72
|
+
|
|
73
|
+
/** Resolves once the tiles of the camera on screen are in, or false after `ms`. */
|
|
74
|
+
function tilesIn(map: WarmableMap, ms: number): Promise<boolean> {
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
const check = () => {
|
|
77
|
+
if (map.loaded()) done(true);
|
|
78
|
+
};
|
|
79
|
+
const done = (ok: boolean) => {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
map.off('render', check);
|
|
82
|
+
resolve(ok);
|
|
83
|
+
};
|
|
84
|
+
const timer = setTimeout(() => done(false), ms);
|
|
85
|
+
map.on('render', check);
|
|
86
|
+
map.triggerRepaint();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function warmCameras(map: WarmableMap, cameras: readonly Camera[], rest: Camera) {
|
|
91
|
+
for (const camera of cameras) {
|
|
92
|
+
map.jumpTo(camera);
|
|
93
|
+
await tilesIn(map, 1500);
|
|
94
|
+
}
|
|
95
|
+
map.jumpTo(rest);
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Four details matter:
|
|
100
|
+
|
|
101
|
+
- **Check after a frame.** `map.loaded()` can read false on the frame in which the last tile
|
|
102
|
+
arrived. So check it after a frame, on `render`, not once right after `jumpTo`.
|
|
103
|
+
- **Do not wait for `idle`.** `idle` also waits for labels to fade in, which adds time and loads
|
|
104
|
+
nothing.
|
|
105
|
+
- **Visit every zoom the move crosses.** A cached tile helps only when it is the exact tile a frame
|
|
106
|
+
needs, so warm each zoom, not just the destination.
|
|
107
|
+
- **Make cache-emptying changes first.** Make the changes listed above before the first visit.
|
|
108
|
+
Otherwise the warm-up is lost.
|
|
109
|
+
|
|
110
|
+
Mapbox GL JS offers `preloadOnly` on `jumpTo`, `easeTo` and `flyTo`, which loads a move's tiles
|
|
111
|
+
without showing it. A MapLibre release with an equivalent would replace the visits above with one
|
|
112
|
+
call per move.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Capture scenes
|
|
2
|
+
|
|
3
|
+
Start with the [@tileflow/core guide](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/README.md) for installation and a complete first example.
|
|
4
|
+
|
|
5
|
+
Commit named `scenes` on the singular exported map. A scene implicitly targets that map, so it does
|
|
6
|
+
not repeat a `map` selector. Scene metadata does not inherit and is removed before cartographic
|
|
7
|
+
compilation; it affects visual evidence, not style or manifest identity.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {defineMap} from '@tileflow/core';
|
|
11
|
+
import {streets} from '@tileflow/maps';
|
|
12
|
+
|
|
13
|
+
export default defineMap({
|
|
14
|
+
id: 'madrid',
|
|
15
|
+
name: 'Madrid',
|
|
16
|
+
version: 1,
|
|
17
|
+
extends: streets,
|
|
18
|
+
scenes: {
|
|
19
|
+
'madrid-desktop': {
|
|
20
|
+
theme: 'dark',
|
|
21
|
+
camera: {type: 'center', center: [-3.7038, 40.4168], zoom: 12},
|
|
22
|
+
viewport: {width: 1280, height: 800, dpr: 1},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Use `tileflow preview --scene madrid-desktop` for live review and
|
|
29
|
+
`tileflow capture madrid-desktop` for exact pixels and a schema-version-4 receipt containing the
|
|
30
|
+
concrete theme plus Streets, data, style, renderer, and image identities. World capture resolves the selected TileJSON
|
|
31
|
+
once and records the exact `world-v1` release plus descriptor/archive/data-contract hashes;
|
|
32
|
+
external fixtures may identify their explicit revision.
|
|
33
|
+
|
|
34
|
+
Every exact World V1 release ID is already canonical at the producer boundary: 12–128 characters
|
|
35
|
+
matching `^world-v1-[a-z0-9][a-z0-9._-]*[a-z0-9]$`. Core validates the literal value and never trims,
|
|
36
|
+
lowercases, or upgrades it to another World generation.
|
|
37
|
+
|
|
38
|
+
To record a timed camera move instead of one image, commit a
|
|
39
|
+
[capture shot](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/capture-shots.md)
|
|
40
|
+
and render it with `tileflow capture --shot <name>`.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Capture shots
|
|
2
|
+
|
|
3
|
+
Start with the [@tileflow/core guide](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/README.md) for installation and a complete first example.
|
|
4
|
+
|
|
5
|
+
A shot is a short, timed camera move committed on the singular exported map. Capture renders it
|
|
6
|
+
frame by frame into a WebM video or a directory of numbered PNG frames. Like
|
|
7
|
+
[capture scenes](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/capture-scenes.md),
|
|
8
|
+
shots implicitly target their map, do not inherit, and are removed before cartographic compilation;
|
|
9
|
+
they affect visual evidence, not style or manifest identity.
|
|
10
|
+
|
|
11
|
+
## Commit a shot
|
|
12
|
+
|
|
13
|
+
This shot moves from central Madrid toward the east over four seconds while the map passes from
|
|
14
|
+
the `light` theme to the `dark` theme:
|
|
15
|
+
|
|
16
|
+
<!-- docs:check -->
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import {defineMap} from '@tileflow/core';
|
|
20
|
+
import {streets} from '@tileflow/maps';
|
|
21
|
+
|
|
22
|
+
export default defineMap({
|
|
23
|
+
id: 'madrid',
|
|
24
|
+
version: 1,
|
|
25
|
+
extends: streets,
|
|
26
|
+
shots: {
|
|
27
|
+
'dusk-flyover': {
|
|
28
|
+
theme: 'light',
|
|
29
|
+
viewport: {width: 1280, height: 720, dpr: 1},
|
|
30
|
+
fps: 30,
|
|
31
|
+
keyframes: [
|
|
32
|
+
{
|
|
33
|
+
at: 0,
|
|
34
|
+
camera: {type: 'center', center: [-3.7038, 40.4168], zoom: 12},
|
|
35
|
+
blend: {themes: ['light', 'dark'], position: 0},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
at: 4000,
|
|
39
|
+
camera: {type: 'center', center: [-3.68, 40.42], zoom: 14, bearing: 30, pitch: 45},
|
|
40
|
+
blend: {themes: ['light', 'dark'], position: 1},
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
A shot has these fields:
|
|
49
|
+
|
|
50
|
+
- `theme`: a concrete theme of the map; `system` is invalid. The map loads this theme first, and a
|
|
51
|
+
shot without a blend shows it throughout.
|
|
52
|
+
- `viewport`: CSS pixels and a `dpr` of 1 or 2, with the same limits as a scene viewport.
|
|
53
|
+
- `fps`: 24, 25, 30, 50, or 60 frames per second. The default is 30.
|
|
54
|
+
- `easing`: `smooth` (the default) eases into and out of each keyframe; `linear` moves at a constant
|
|
55
|
+
rate between keyframes.
|
|
56
|
+
- `keyframes`: 2 to 64 entries. `at` is the whole number of milliseconds from the start of the
|
|
57
|
+
shot. The first keyframe is at 0, every later keyframe is later than the one before it, and the
|
|
58
|
+
last is at most 60000.
|
|
59
|
+
|
|
60
|
+
Keyframe cameras use the scene `center` form: `[longitude, latitude]`, zoom, and optional bearing
|
|
61
|
+
and pitch. The `bounds` form is not accepted, because the camera that fits bounds depends on the
|
|
62
|
+
renderer's viewport and projection, and a shot interpolates between exact cameras.
|
|
63
|
+
|
|
64
|
+
## Timing and interpolation
|
|
65
|
+
|
|
66
|
+
A shot renders one frame at the start and one every `1 / fps` seconds up to its last keyframe:
|
|
67
|
+
`floor(lastKeyframe.at × fps / 1000) + 1` frames, at most 3600. Frame `i` shows the shot at
|
|
68
|
+
`i × 1000 / fps` milliseconds, so the last keyframe is itself a frame when its time is a whole
|
|
69
|
+
number of frame intervals, as 4000 ms is at 30 frames per second.
|
|
70
|
+
|
|
71
|
+
Between two keyframes the center moves in Web Mercator coordinates, along the shorter way around
|
|
72
|
+
the world when it crosses the antimeridian. Zoom and pitch change linearly, and bearing turns along
|
|
73
|
+
the shorter arc. Easing applies to every value in the same way. A frame exactly at a keyframe shows
|
|
74
|
+
that keyframe's camera.
|
|
75
|
+
|
|
76
|
+
`tileflowCaptureShotFrames` returns the camera and blend position of every frame without rendering
|
|
77
|
+
anything, so tools can inspect a shot before capture:
|
|
78
|
+
|
|
79
|
+
<!-- docs:check -->
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import {tileflowCaptureShotFrames, type TileflowMapShot} from '@tileflow/core';
|
|
83
|
+
|
|
84
|
+
const shot = {
|
|
85
|
+
theme: 'light',
|
|
86
|
+
viewport: {width: 640, height: 360},
|
|
87
|
+
keyframes: [
|
|
88
|
+
{at: 0, camera: {type: 'center', center: [-3.7038, 40.4168], zoom: 12}},
|
|
89
|
+
{at: 1000, camera: {type: 'center', center: [-3.68, 40.42], zoom: 13}},
|
|
90
|
+
],
|
|
91
|
+
} satisfies TileflowMapShot;
|
|
92
|
+
|
|
93
|
+
const frames = tileflowCaptureShotFrames(shot);
|
|
94
|
+
console.log(frames.length); // 31
|
|
95
|
+
console.log(frames[15]?.camera.zoom); // 12.5
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Blend themes along a shot
|
|
99
|
+
|
|
100
|
+
Add `blend` to every keyframe, or to none. Each blend lists the same two to eight concrete themes in
|
|
101
|
+
the same order, and a `position` from 0 to one less than the number of themes, as in a
|
|
102
|
+
[theme blend](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/theme-transitions-and-blends.md).
|
|
103
|
+
The position moves between keyframes with the same easing as the camera.
|
|
104
|
+
|
|
105
|
+
Capture prepares the blend once with Core's theme controller and moves its position on every frame.
|
|
106
|
+
The controller applies a new position once it differs by at least a four-hundredth of a theme, so a
|
|
107
|
+
very slow blend changes the ground colours every few frames. Labels and icons change at the nearest
|
|
108
|
+
theme, without a cross-fade, because capture renders with reduced motion.
|
|
109
|
+
|
|
110
|
+
## Render a shot
|
|
111
|
+
|
|
112
|
+
Render committed shots with the CLI. The first command writes
|
|
113
|
+
`.tileflow/captures/shots/dusk-flyover.webm` and its receipt; the second writes numbered PNG frames
|
|
114
|
+
instead:
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
npx tileflow capture --shot dusk-flyover --json
|
|
118
|
+
npx tileflow capture --shot dusk-flyover --format frames --json
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Each frame waits until MapLibre has loaded the tiles and resources it needs, so a slow machine or
|
|
122
|
+
network makes the render take longer but never drops or repeats frames. See the
|
|
123
|
+
[capture guide](https://github.com/tileflow/tileflow-sdk/blob/main/packages/capture/README.md) for
|
|
124
|
+
the Node API and the
|
|
125
|
+
[capture contract](https://github.com/tileflow/tileflow-sdk/blob/main/docs/contracts/local-visual-capture.md)
|
|
126
|
+
for output paths, encoding, and the shot receipt.
|
|
127
|
+
|
|
128
|
+
## Play a shot in the browser
|
|
129
|
+
|
|
130
|
+
`playTileflowShot(map, shot, options)` from `@tileflow/core/browser` plays the same shot live. The
|
|
131
|
+
camera follows the keyframes frame by frame, with the same interpolation Capture renders, so the
|
|
132
|
+
page and the video show the same film. This example assumes an existing MapLibre map named `map`,
|
|
133
|
+
a theme controller, and the runtime styles of the blended themes:
|
|
134
|
+
|
|
135
|
+
<!-- docs:check -->
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import {
|
|
139
|
+
playTileflowShot,
|
|
140
|
+
type TileflowShotMotion,
|
|
141
|
+
type TileflowShotPlayerMap,
|
|
142
|
+
type TileflowThemeController,
|
|
143
|
+
} from '@tileflow/core/browser';
|
|
144
|
+
import type {TileflowRuntimeStyle} from '@tileflow/core/runtime';
|
|
145
|
+
|
|
146
|
+
export function playDuskFlyover(
|
|
147
|
+
map: TileflowShotPlayerMap,
|
|
148
|
+
controller: TileflowThemeController,
|
|
149
|
+
themes: readonly TileflowRuntimeStyle[],
|
|
150
|
+
shot: TileflowShotMotion,
|
|
151
|
+
showTitle: () => void,
|
|
152
|
+
) {
|
|
153
|
+
return playTileflowShot(map, shot, {
|
|
154
|
+
blend: (position) => void controller.setBlend({themes, position}),
|
|
155
|
+
cues: [{at: 2500, run: showTitle}],
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- **Blend.** `blend` receives the blend position at each frame of a shot that blends themes.
|
|
161
|
+
- **Cues.** `cues` run once each, in time order, as the shot reaches them, for application events
|
|
162
|
+
such as drawing a route or showing a title.
|
|
163
|
+
- **Stopping.** `stop()` or an aborted `signal` stops the shot where it is; `finished` resolves
|
|
164
|
+
`true` once the shot reaches its last keyframe and `false` when it was stopped first.
|
|
165
|
+
- **Reduced motion.** With reduced motion requested, the shot shows its last keyframe at once and
|
|
166
|
+
runs every cue in order.
|
|
167
|
+
|
|
168
|
+
`tileflowShotStateAt(shot, at)` returns the camera and blend position at any time, for players of
|
|
169
|
+
one's own.
|
|
170
|
+
|
|
171
|
+
Live playback runs at the browser's pace and does not wait for tiles, unlike a render. Load the
|
|
172
|
+
tiles of the cameras a shot crosses before playing it; see
|
|
173
|
+
[camera moves and the tile cache](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/camera-moves-and-tile-cache.md).
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Cartographic defaults
|
|
2
|
+
|
|
3
|
+
Start with the [Core guide](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/README.md)
|
|
4
|
+
for installation and a complete map. Semantic modules supply a coordinated starting appearance;
|
|
5
|
+
module styles and owner-local render targets can replace those decisions.
|
|
6
|
+
|
|
7
|
+
## Roads and roundabouts
|
|
8
|
+
|
|
9
|
+
`roads({rampOrder: 'below-roads'})` draws ramp fills before receiving carriageways in the same
|
|
10
|
+
surface, bridge or tunnel band. This prevents a rounded ramp endpoint covering the receiving
|
|
11
|
+
road. Choose `'by-class'` to retain class-based fill order instead. Explicit render-stack moves
|
|
12
|
+
remain available; ramp fills have targets `roads.classes.<class>.<structure>.rampFill` when the
|
|
13
|
+
separate ramp order is active. A selector refinement replaces that target's complete filter, so
|
|
14
|
+
retain its ramp eligibility when writing a custom selector. Module class fill styles apply to
|
|
15
|
+
both passes; a refinement of a specific `fill` or `rampFill` target changes only that pass.
|
|
16
|
+
|
|
17
|
+
Core owns circular-road geometry. Physical inner and outer radii take precedence when provided
|
|
18
|
+
by the source. Otherwise the ring follows the enabled class's effective surface width, including
|
|
19
|
+
width treatments. The default border follows the actual casing width and gap. Hidden classes and
|
|
20
|
+
surface fills do not leave orphan rings. Ring colors remain semantic palette choices, independently
|
|
21
|
+
replaceable through `roundabouts.fill.strokeColor` and `roundabouts.casing.strokeColor`; they do
|
|
22
|
+
not copy arbitrary per-class color curves.
|
|
23
|
+
|
|
24
|
+
Metric scaling and base-2 width curves retain their native interpolation. Other width curves are
|
|
25
|
+
sampled into the shared geometry curve: numeric widths target 0.1-pixel error, stopping refinement
|
|
26
|
+
once intervals reach 1/32 zoom; very large widths can exceed that target. Feature-dependent width
|
|
27
|
+
expressions retain authored and integer zoom stops without adaptive refinement, keeping expression
|
|
28
|
+
size bounded. Their interpolation between stops is approximate, with no universal pixel-error bound.
|
|
29
|
+
Step changes complete at the authored zoom, with the transition confined to the preceding 0.000001
|
|
30
|
+
zoom interval. Explicit circle radius and stroke-width expressions bypass this sampling when exact
|
|
31
|
+
independent behavior is required.
|
|
32
|
+
|
|
33
|
+
`roundabouts.fallbackWidth` replaces only the fallback carriageway width in pixels;
|
|
34
|
+
`roundabouts.borderWidth` replaces the total extra width around the ring. Existing circle styles
|
|
35
|
+
can independently replace radius, stroke width, color, opacity and zoom range. For example, this
|
|
36
|
+
module fragment retains geometric defaults but gives rings a deliberate fixed border:
|
|
37
|
+
|
|
38
|
+
<!-- docs:check -->
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import {roads} from '@tileflow/core';
|
|
42
|
+
|
|
43
|
+
export const customRoads = roads({
|
|
44
|
+
rampOrder: 'by-class',
|
|
45
|
+
roundabouts: {borderWidth: 0},
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Ground surfaces
|
|
50
|
+
|
|
51
|
+
The land module draws broad cover and park surfaces before mapped grass, meadow, scrub and
|
|
52
|
+
flowerbed details. Parking, sports pitches, tracks, playgrounds and zoos follow those details so
|
|
53
|
+
they remain visible inside broad green polygons. Both Tileflow World and OpenMapTiles' combined
|
|
54
|
+
park source follow this order. These defaults do not change the map's colors, opacity or textures.
|
|
55
|
+
|
|
56
|
+
A thematic map can give a park tint more prominence through the existing render stack. This
|
|
57
|
+
module fragment is ready to use as `modules.land` in a map:
|
|
58
|
+
|
|
59
|
+
<!-- docs:check -->
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import {land, refineRenderTarget, withRenderStack} from '@tileflow/core';
|
|
63
|
+
|
|
64
|
+
export const thematicLand = withRenderStack(land(), {
|
|
65
|
+
parkTint: refineRenderTarget({
|
|
66
|
+
target: 'land.landcover.urbanPark.fill',
|
|
67
|
+
placement: 'above-land',
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Refinements target contributions that the map actually draws. Disabling a target and retaining
|
|
73
|
+
a refinement for it is an authoring error. Disabling the whole module removes its contributions
|
|
74
|
+
and its render stack. The built-in surface order needs no target anchors and works when individual
|
|
75
|
+
surfaces are hidden.
|
|
76
|
+
|
|
77
|
+
## Street names on the ground or in the foreground
|
|
78
|
+
|
|
79
|
+
`labels({roadPlacement: 'foreground'})` is the default. Names draw above buildings and vegetation.
|
|
80
|
+
Use `roadPlacement: 'ground'` to draw them above flat building footprints and below raised
|
|
81
|
+
buildings, with text aligned to the map surface. Flat footprints must not clip the glyphs.
|
|
82
|
+
Shields and junction numbers remain in the foreground. Ground placement does not require the
|
|
83
|
+
buildings module: street names still draw when buildings are disabled.
|
|
84
|
+
|
|
85
|
+
This module fragment keeps the normal language selection and road-class filters while opting
|
|
86
|
+
into ground names. Individual symbol styles can override the default text alignment:
|
|
87
|
+
|
|
88
|
+
<!-- docs:check -->
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import {labels} from '@tileflow/core';
|
|
92
|
+
|
|
93
|
+
export const groundLabels = labels({
|
|
94
|
+
roadPlacement: 'ground',
|
|
95
|
+
collisionPriority: 'navigation',
|
|
96
|
+
language: 'local',
|
|
97
|
+
roads: 'all',
|
|
98
|
+
styles: {
|
|
99
|
+
roads: {
|
|
100
|
+
primary: {text: {pitchAlignment: 'viewport'}},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
MapLibre offers foreground symbols first when resolving collisions. Ground names therefore yield
|
|
107
|
+
to POIs and other foreground symbols, even with `collisionPriority: 'navigation'`. Navigation
|
|
108
|
+
priority still orders the ground road classes relative to one another; it does not make their
|
|
109
|
+
collision priority independent of drawing order. Foreground placement retains the existing
|
|
110
|
+
navigation priority over ordinary POIs. Neither mode turns collision detection off.
|
|
111
|
+
|
|
112
|
+
When compatible road-name layers share a physical bucket, their top-to-bottom collision priority
|
|
113
|
+
and each class's authored feature priority are retained. Overlap-enabled names retain drawing order.
|
|
114
|
+
|
|
115
|
+
`labels({roadGrouping: 'network'})` instead shares placement and repeated-name suppression across
|
|
116
|
+
compatible classes, in source feature order (or their common authored priority). It retains each
|
|
117
|
+
class's zoom thresholds and size curve. Different fonts, spacing, colors or priorities stay separate,
|
|
118
|
+
as do explicit ordered passes and fractional zoom thresholds. Streets uses this for its ordinary
|
|
119
|
+
roads and a separate pedestrian/path group. The default `'class'` retains road-class priority.
|
|
120
|
+
|
|
121
|
+
Use a refinement on `labels.roads.<class>` to move an individual class after this default has been
|
|
122
|
+
applied. Symbol size, zoom range, spacing, priority, fonts and halos remain available in
|
|
123
|
+
`styles.roads`. The same names, language fallbacks and class selectors are used in both modes.
|
|
124
|
+
|
|
125
|
+
## Compact road names
|
|
126
|
+
|
|
127
|
+
`labels({roadNameFormat: 'compact'})` abbreviates recognized Spanish and French road-type
|
|
128
|
+
prefixes and English suffixes after language selection: `Calle de Alcalá` becomes `C. de Alcalá`,
|
|
129
|
+
`Avenue Paul Doumer` becomes `Av. Paul Doumer`, and `West 34th Street` becomes `W 34th St`.
|
|
130
|
+
The default, `full`, preserves source names. Proper names, unknown types and route shields stay
|
|
131
|
+
unchanged. An explicit `styles.roads.<class>.text.field` takes precedence over this formatting.
|
|
132
|
+
|
|
133
|
+
## Label density and perspective
|
|
134
|
+
|
|
135
|
+
The SDK starts with local street names (`labels.roads: 'streets'`), POI density 2 and a
|
|
136
|
+
3-pixel text collision margin. Street names follow enabled road geometry and their zoom thresholds;
|
|
137
|
+
set `roads: 'major'` to retain a map with only arterial names, and `density: 3` for more POIs.
|
|
138
|
+
|
|
139
|
+
`poi({density: 2, densityByCategory: {landmark: 3, lodging: 1}})` changes category eligibility
|
|
140
|
+
while retaining one importance order across categories. Unspecified categories inherit `density`.
|
|
141
|
+
Each value is 1–5; eligible rank increases by one at z16 and two at `detailZoom` (default 17;
|
|
142
|
+
Streets uses 18). `detailZoom` must exceed 16 and cannot exceed 24. Source rank meanings
|
|
143
|
+
and category coverage differ between datasets, so equal settings need not produce equal counts.
|
|
144
|
+
|
|
145
|
+
MapLibre owns perspective scaling, distant-symbol rejection, collision placement and fading.
|
|
146
|
+
These native behaviors apply without a module switch. The SDK does not change feature filters
|
|
147
|
+
when panning, rotating, tilting or ending a gesture. Category and zoom eligibility remain compiled
|
|
148
|
+
into the style; explicit application filter edits remain application-owned. The experimental
|
|
149
|
+
`perspective` module option and `attachTileflowLabelPerspective` helper have been removed.
|
|
150
|
+
|
|
151
|
+
Default POIs place their pictogram and name together, with four alternative text anchors and
|
|
152
|
+
native collision detection. Parking and public toilets retain their icon-only representation.
|
|
153
|
+
Set `placement.coupleIconAndLabel: false` to place pictograms and names independently. Maps refining
|
|
154
|
+
separate `poi.icon` or `poi.label` targets should set this explicitly; coupled POIs use `poi`.
|
|
155
|
+
Explicit anchors, offsets and variable anchors replace the SDK's automatic text clearance.
|
|
156
|
+
|
|
157
|
+
Balanced foreground priority offers place names and route references first, followed by major
|
|
158
|
+
road names, ordinary POIs, minor road names and house numbers. `collisionPriority: 'navigation'`
|
|
159
|
+
also promotes minor road names above ordinary POIs. `placement.prominentRank` can promote selected
|
|
160
|
+
POI ranks above the orientation labels. Eligibility still depends on zoom, enabled classes and
|
|
161
|
+
source data. Feature priority sorts within a layer; it does not override the order between layers.
|
|
162
|
+
Explicit render-stack placements take precedence over these defaults.
|
|
163
|
+
|
|
164
|
+
With `roadPlacement: 'ground'`, the browser keeps road names above the 3D scene below 30° pitch,
|
|
165
|
+
so roofs cannot cut letters in narrow streets viewed from above. At 30° and above, names return
|
|
166
|
+
below solid raised geometry. Tileflow trees mount below these names so foliage cannot cut
|
|
167
|
+
individual letters. This policy follows `roadPlacement` alone and does not affect camera tilt.
|
|
168
|
+
Explicit render-stack moves opt a target out of automatic ordering; explicit tree `beforeId`
|
|
169
|
+
placement remains application-owned.
|
|
170
|
+
|
|
171
|
+
React, Vue, Svelte, preview and standalone capture attach the shared placement controller.
|
|
172
|
+
Applications constructing MapLibre maps directly call `attachTileflowLabelPlacement(map)` from
|
|
173
|
+
`@tileflow/core/browser` once after map construction. The returned function detaches its listeners
|
|
174
|
+
and restores ground ordering; map removal detaches without further map writes. Replacement styles
|
|
175
|
+
and late-mounted 3D layers are discovered automatically. Static style JSON and native renderers
|
|
176
|
+
retain compiled eligibility and hierarchy, but do not execute this browser scene-order controller.
|
|
177
|
+
|
|
178
|
+
Native perspective rejection is not a configurable per-category distance curve. The SDK does not
|
|
179
|
+
emulate Mapbox's `pitch` or `distance-from-center` symbol filters, which the supported MapLibre
|
|
180
|
+
style specification does not provide. Runtime fade duration and cross-source collisions continue
|
|
181
|
+
to use MapLibre defaults unless overridden through the adapter's `mapOptions`.
|
|
182
|
+
|
|
183
|
+
## Discover and override decisions
|
|
184
|
+
|
|
185
|
+
`tileflow describe labels --json` exposes the installed options, and `tileflow inspect --json`
|
|
186
|
+
shows resolved inheritance and merge provenance. `tileflow explain --inspection --json` adds
|
|
187
|
+
compiled semantic targets and physical provenance. These commands describe the authored map;
|
|
188
|
+
they do not rewrite explicit style choices.
|
|
189
|
+
|
|
190
|
+
`tileflow describe themes --json` lists the twelve required base color roles and composition
|
|
191
|
+
rules. A [single independent theme](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/themes-and-module-styles.md)
|
|
192
|
+
needs no time-of-day catalog. Optional color roles derive from that theme and accept explicit
|
|
193
|
+
replacements. Removing inherited lighting uses `reset()` inside `defineTheme`; it restores the
|
|
194
|
+
SDK fallback, while building shadows and occlusion have separate on/off controls.
|
|
195
|
+
|
|
196
|
+
`createStyleResult()` and `tileflow inspect compiled --json` include `report.decisions`: actual
|
|
197
|
+
color roles and selected road, label and 3D-building policies, each with its value, source,
|
|
198
|
+
reason and override guidance. Query `--path /report/decisions --limit 80` to bound this output.
|
|
199
|
+
`default` means an SDK fallback, `map` a value in the resolved map, `theme` a role defined by the
|
|
200
|
+
selected theme and `derived` an optional role computed by Core. Use config inspection's merge
|
|
201
|
+
provenance to distinguish inherited map values from leaf overrides. These decisions describe
|
|
202
|
+
module policies and the theme's color context; later module styles and render-stack refinements
|
|
203
|
+
can override them. They are not a trace of every final paint property or a visual-quality score.
|
|
204
|
+
Simple `createStyle()` and browser drawing do not collect this report.
|
|
205
|
+
|
|
206
|
+
Replace an inherited module with its constructor, patch it with `refine()`, restore an inherited
|
|
207
|
+
value with `reset()`, or remove the whole domain with `disable()`. See the
|
|
208
|
+
[authoring model](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/authoring-model.md)
|
|
209
|
+
for these composition rules. Capture contrasting designs and compare them at matching cameras to
|
|
210
|
+
check both the default result and intentional departures from it.
|