@tileflow/core 0.1.0-alpha.34 → 0.1.0-alpha.35

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.
@@ -295,10 +295,129 @@ opt-in.
295
295
 
296
296
  The vector palette uses thresholds −11 000, −8 000, −6 000, −4 000, −2 000, −1 000, −500, −200,
297
297
  −100, −50, −20, −10, and 0 metres. `bands`, `contours`, and `labels` remain separate controls.
298
- Until native contour fields are contracted, contours are a visual fallback drawn from band polygon
299
- edges, not surveyed isolines. The V1 schema exposes optional source-layer names
300
- `bathymetry_contour`, `seafloor_landform`, `water_name`, and `bathymetry_coverage`, but promises
301
- fields only for required `bathymetry.min_depth` and `bathymetry.sort_key`.
298
+ Without an optional-layer declaration, contours and depth labels retain the legacy polygon-band
299
+ behavior. `marine` shorthands and existing maps keep that behavior.
300
+
301
+ ### Declare contour and name layers
302
+
303
+ For a compatible source, `vectorLayers` explicitly declares optional layers. Core never fetches
304
+ TileJSON to detect them and never substitutes polygon edges for declared contour lines. A missing
305
+ declared layer therefore draws no features; validate the selected dataset before rendering.
306
+ `display: 'bands'` or `'hybrid'` loads the vector source.
307
+
308
+ | Declaration | Source layer | Geometry and fields |
309
+ | --------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------- |
310
+ | `contours: true` | `bathymetry_contour` | Lines with numeric `depth`, negative metres below the source vertical datum. Zero and positive depths are excluded. |
311
+ | `landforms: true` or a range inventory | `seafloor_landform` | Named points, lines, or polygons with stable source `id`, provider `class`, and `name`. |
312
+ | `waterNames: true` or a range inventory | `water_name` | Named points, lines, or polygons with the same name contract. |
313
+
314
+ Name geometry stays in its source representation. `placement: 'point'` (the default) lets the
315
+ renderer place names inside polygons or on points. Use `placement: 'line'` to label source lines.
316
+ Core creates no substitute point, centroid, or line feature.
317
+
318
+ Name layers preserve source properties; `id` is a source identifier, with no guarantee of a numeric
319
+ renderer feature ID. Optional `name:<language>` fields retain the source's language assignments.
320
+ `labels({language: 'es'})` selects a nonempty `name:es`, falling back to `name`; `auto` and `local`
321
+ use `name` directly. Core does not infer the language of an unqualified name or invent translations.
322
+ Optional `min_zoom` is inclusive and `max_zoom` exclusive, defaulting to 0 and 24. These nominal
323
+ ranges intersect the symbol style's range. With `true`, MapLibre evaluates feature zoom filters at
324
+ integer zooms: `min_zoom: 7.6` first becomes eligible at zoom 8. For exact fractional boundaries,
325
+ declare an exhaustive range inventory instead:
326
+
327
+ ```ts
328
+ vectorLayers: {
329
+ contours: true,
330
+ landforms: {zoomRanges: [{minZoom: 0, maxZoom: 24}]},
331
+ waterNames: {
332
+ zoomRanges: [{minZoom: 1, maxZoom: 6.5}, {minZoom: 7.6, maxZoom: 10}],
333
+ },
334
+ }
335
+ ```
336
+
337
+ Each declared pair becomes a separate symbol layer with fractional renderer bounds and equality
338
+ filters on the source's original range. A `7.6–10` feature appears at 7.6 and disappears at 10.
339
+ Unlisted pairs are omitted. Core never discovers ranges from tiles: validate the source inventory
340
+ against this exhaustive declaration before rendering, and retain that inventory with reproducible
341
+ captures. Numeric pairs must match the source's encoded values exactly.
342
+
343
+ Each name layer accepts 1–128 unique pairs with `0 <= minZoom < maxZoom <= 24`;
344
+ `tileflowBathymetryNameLayerLimits.maxZoomRanges` exposes the limit. Input order does not affect
345
+ output. Different pairs can overlap in zoom because equality filters keep their feature sets
346
+ disjoint. A style may narrow a pair's visibility; an empty intersection emits no layer.
347
+ Missing or null source bounds default to 0/24. Nonnumeric values use the same numeric-conversion
348
+ fallbacks; numeric strings are converted. Such features draw only if their normalized pair is
349
+ declared. Core preserves the original source properties.
350
+
351
+ Optional numeric `sort_key` defaults to 0; smaller values enter collision placement first within
352
+ each cohort. Cohorts follow ascending minimum then maximum zoom and sit below ordinary map labels.
353
+ Names use normal collision placement without overlap by default.
354
+
355
+ This example declares a source whose regional detail supports zoom 14. Its extended ranges are
356
+ explicit design choices, not a promise of worldwide detail:
357
+
358
+ <!-- docs:check -->
359
+
360
+ ```ts
361
+ import {bathymetry, defineMap, expr, field, fixed, labels, water} from '@tileflow/core';
362
+ import {soundings} from '@tileflow/maps';
363
+
364
+ export default defineMap({
365
+ id: 'regional-depth',
366
+ version: 1,
367
+ extends: soundings,
368
+ marine: {
369
+ bathymetry: bathymetry({
370
+ url: 'https://tiles.example.test/regional-depth/tiles.json',
371
+ vectorLayers: {
372
+ contours: true,
373
+ landforms: {zoomRanges: [{minZoom: 0, maxZoom: 24}]},
374
+ waterNames: {zoomRanges: [{minZoom: 7.6, maxZoom: 10}]},
375
+ },
376
+ contours: {},
377
+ labels: {},
378
+ }),
379
+ nautical: false,
380
+ },
381
+ modules: {
382
+ labels: labels({language: 'es'}),
383
+ water: water({
384
+ bathymetry: {
385
+ maxZoom: 15,
386
+ opacity: fixed(0.8, {reason: 'The selected regional source supplies detailed bands'}),
387
+ },
388
+ bathymetryContours: {maxZoom: 15},
389
+ bathymetryLabels: {
390
+ maxZoom: 15,
391
+ text: {
392
+ field: expr.concat(
393
+ expr.toString(expr.abs(expr.get(field('bathymetryContourDepth')))),
394
+ ' m',
395
+ ),
396
+ },
397
+ },
398
+ bathymetryLandforms: {maxZoom: 15},
399
+ bathymetryWaterNames: {maxZoom: 15},
400
+ }),
401
+ },
402
+ });
403
+ ```
404
+
405
+ `water.bathymetryContours` and `water.bathymetryLabels` now consume the declared lines and `depth`;
406
+ labels default to line placement. Band fills still consume `min_depth` and `sort_key`.
407
+ `water.bathymetryLandforms` and `water.bathymetryWaterNames` style the declared names; set
408
+ `visible: false` to hide either. Without their declaration, those styles emit no name layers.
409
+ Name styles may use `class` in expressions without changing the source classification.
410
+
411
+ Water-module styles override product-level `bands`, `contours`, and `labels`. Existing custom
412
+ `text.field` expressions remain authoritative: when refining a map from legacy polygons to native
413
+ contours, use `expr.get(field('bathymetryContourDepth'))` for its depth. That auxiliary binding
414
+ resolves to `depth` independently of the primary World schema; `bathymetryMinDepth` keeps its
415
+ legacy band meaning. Raw `expr.get('depth')` is rejected by authoring validation.
416
+ Increasing only `maxZoom` does not remove an opacity curve that fades out earlier. Extend ranges
417
+ and opacity only where the selected source supports the intended detail.
418
+
419
+ The V1 schema also reserves `bathymetry_coverage`; Core adds no coverage rendering by default.
420
+ Derived contours and names provide geographic context, not certified navigation data.
302
421
 
303
422
  The DEM enables future client-side sampling and depth profiles, but Core intentionally exposes no
304
423
  profile or remote sampling API yet. Rendered relief must not be interpreted as navigation-safe
@@ -318,6 +437,15 @@ explicit because the data contract cannot assume a sprite name. They require pos
318
437
  `markings` evidence and reject `crossing=no`; both fields are semantic bindings, so custom schemas
319
438
  can remap them without weakening that rule.
320
439
 
440
+ Metric road rings scale continuously with the map from zoom 15 through 24, including fractional
441
+ zooms. Their casing is below surface road fills so approaching streets cover the outline at each
442
+ junction. The ring fill stays below bridges and road markings.
443
+
444
+ Road treatment `widthScale` scales both the stroke and an authored `gapWidth`, keeping hollow
445
+ casings aligned with the carriageway at fractional zoom levels.
446
+ When every dash treatment is continuous (`[1, 0]`), road layers omit the dash pattern so thin
447
+ casings render as solid strokes. Actual construction and tunnel dashes remain patterned.
448
+
321
449
  Datasets may also publish the optional, decorative `canopy` layer: points inside urban parks and
322
450
  gardens, at most one per cell of a grid of about 20 m that measured tree canopy covers. Each point
323
451
  tells how much room a drawn grove of tree crowns has and how dense the canopy around it is. The
@@ -421,7 +549,11 @@ buildings. Its `activity_score`, `rank`, `min_zoom`, and `confidence` fields con
421
549
  warm tint without drawing POI-radius circles. Current building footprints may carry sparse
422
550
  `building_tone=commercial|destination|active`; absence means the neutral base building. The three
423
551
  values preserve why the warm tone was selected even when a theme maps them to the same color.
424
- Building visibility is fixed by the layer zoom contract and never by optional semantics. The land
552
+ Buildings may also carry World's `class`, their use in Overture's building subtype values; without
553
+ a tone, the buildings module colours `residential`, `commercial`, public buildings (`civic`,
554
+ `education`, `medical`, `religious` and the like) and works (`industrial`, `agricultural`,
555
+ `service`) with the theme's `residential`, `commercial`, `civic` and `industrial` building colours,
556
+ and every other building with `generic`. Building visibility is fixed by the layer zoom contract and never by optional semantics. The land
425
557
  module also exposes
426
558
  `medical`, `education`, and `government` independently instead of collapsing their already distinct
427
559
  source classes into one civic color.
@@ -39,6 +39,11 @@ preparing the referenced resources.
39
39
 
40
40
  The exported `tileflowNativeProfile` targets MapLibre React Native 11.3.10 with Native Android 13.2.0
41
41
  and iOS 6.26.0. These are static compatibility targets, not a supported application dependency matrix.
42
+ The application integration uses MapLibre React Native 11.5.0 with Android 13.6.1 and iOS 6.31.0.
43
+ The artifact profile retains its earlier validation targets and build-record engine values, keeping
44
+ prepared artifacts compatible with existing Hosted validators. New application dependencies do not
45
+ expand the accepted style vocabulary.
46
+
42
47
  The wrapper's Style Spec version is 26.2.1. Tileflow retains its existing compiler and parser at
43
48
  24.8.5: `native-v1` checks that compiler's vocabulary and the intersection of its recorded Android
44
49
  and iOS support. It does not expose every feature of Style Spec 26.2.1 or upgrade the web compiler.
@@ -121,7 +121,7 @@ network request. Do not attach a credential merely because this helper accepted
121
121
 
122
122
  ## Private parser and source builds
123
123
 
124
- The native entry embeds the low-level parser from `whatwg-url` 15.1.0, `tr46` 6.0.0 (Unicode 17
124
+ The native entry embeds the low-level parser from `whatwg-url` 17.2.0, `tr46` 7.0.0 (Unicode 18
125
125
  IDNA processing) and Punycode.js 2.3.1. It exposes no URL class or parser configuration. Package
126
126
  consumers only install Core; the published `dist/native.js` has no external runtime imports.
127
127
  The parser is a development/build dependency, not an additional runtime package to install.
@@ -136,8 +136,10 @@ policy with its existing host parser; it does not embed the mobile parser in its
136
136
 
137
137
  From an SDK source checkout, run the ordinary Core build, not a direct source import in a native
138
138
  application. `tsup.native.config.ts` builds only the native entry with `platform: 'browser'`,
139
- `target: 'es2020'`, ESM and no splitting. Its plugin redirects exactly the two relative
140
- `./encoding` imports in the pinned parser modules to Core's private UTF-8 codec. It rejects an
139
+ `target: 'es2020'`, ESM and no splitting. Its plugin redirects the two relative
140
+ `./encoding` imports and the parser's two `@exodus/bytes` encoding imports to Core's private UTF-8
141
+ codec. Query encoding accepts only UTF-8; the legacy encoding bootstrap has no side effects.
142
+ Exodus and its legacy encoding tables are not embedded. The build rejects an
141
143
  unexpected dependency version, an unbound codec import or external runtime imports. It does not
142
144
  patch packages on disk, configure a global alias or change another entry's build.
143
145
 
@@ -258,3 +258,53 @@ export async function duskWhileFlying(
258
258
  The React, Vue, and Svelte maps take the same flag in `themeBlend`: pass
259
259
  `themeBlend={{hold: true, position, themes}}` while the camera moves and drop `hold` when it rests.
260
260
  The React Native `Map` blends natively and does not take `hold`.
261
+
262
+ ## Time of day
263
+
264
+ `timeOfDay` is an optional map capability. It maps a theme and an artistic preset (`day`,
265
+ `dawn`, `dusk`, `night`) to a complete theme already in `themes`:
266
+
267
+ <!-- docs:check -->
268
+
269
+ ```ts
270
+ import {defineMap} from '@tileflow/core';
271
+ import {streets, streetsThemes} from '@tileflow/maps';
272
+
273
+ export default defineMap({
274
+ id: 'city',
275
+ version: 1,
276
+ extends: streets,
277
+ themes: {
278
+ paper: streetsThemes.light,
279
+ sunrise: streetsThemes.dawn,
280
+ evening: streetsThemes.dusk,
281
+ ink: streetsThemes.dark,
282
+ },
283
+ defaultTheme: 'paper',
284
+ systemThemes: {light: 'paper', dark: 'ink'},
285
+ timeOfDay: {
286
+ paper: {day: 'paper', dawn: 'sunrise', dusk: 'evening', night: 'ink'},
287
+ },
288
+ });
289
+ ```
290
+
291
+ Applications keep `theme` and optionally add `timeOfDay`: `theme="paper" timeOfDay="dusk"`
292
+ selects `evening`. Compilation supports `createStyle(map, {theme: 'paper', timeOfDay: 'dusk'})`;
293
+ `resolveTileflowRuntimeTheme(map, theme, colorScheme, timeOfDay)` uses the same resolution.
294
+ Omitting the preset preserves the existing selection. `system` resolves its light/dark theme
295
+ first. A concrete variant such as `ink` also identifies its family when a preset is supplied.
296
+ Maps or themes without a family ignore this option. Within a declared family, an unavailable
297
+ preset is an error; controls should offer only declared presets. Families must include their own
298
+ name in their presets, and a concrete theme can belong to only one family.
299
+
300
+ The catalog inherits atomically; `timeOfDay: {}` disables it on an extension. Replacing `themes`
301
+ clears an inherited catalog. `defaultTheme`, `systemThemes`, and `themeTransition` retain their
302
+ behavior. An explicit `themeBlend` owns its concrete theme sequence and takes precedence over
303
+ `timeOfDay`. Changing the preset does not start a clock or call `setSun`; lighting is whatever the
304
+ selected theme already declares, and an application's sun override retains its existing behavior.
305
+ Captures, style URLs, and transition events use the resolved concrete theme name.
306
+
307
+ Maps with a catalog publish runtime manifest version 2. Updated browser and native readers accept
308
+ both versions. Old readers need a version 1 manifest: `createManifest(project, {manifestVersion: 1})`
309
+ omits the catalog and keeps every concrete theme available. Maps without a catalog still emit
310
+ version 1. Update readers before delivering a version 2 manifest.
package/docs/trees-3d.md CHANGED
@@ -12,10 +12,68 @@ style it compiles, for every theme, and the renderer follows theme changes and b
12
12
 
13
13
  It is a custom WebGL2 layer for MapLibre GL JS, drawn above the map's buildings and below its
14
14
  labels. It needs WebGL2 and the Mercator projection, and the trees stand from zoom 15 by default.
15
- Elsewhere (Capture, Static Maps, Native, or a browser without WebGL2) the map draws the vegetation
16
- module's flat trees.
15
+ When model trees are requested, Capture, Static Maps, Native, and browsers without WebGL2 use
16
+ the module's `fallback`: `flat` by default, or `none` to omit trees. Static output uses the 2D view
17
+ for `auto` maps. Streets chooses `none`, so it never substitutes flat crowns
18
+ for its models unless an extension explicitly chooses `flat` or `auto`.
17
19
 
18
- ## Stand the trees
20
+ ## Visibility and representation
21
+
22
+ Use `createTileflowVegetation` for application controls. React, Vue and Svelte mount it automatically
23
+ in interactive maps. The map chooses `vegetation.mode` independently of visibility:
24
+
25
+ | Mode | 2D view | 3D view |
26
+ | -------------- | ------------------- | ------------------- |
27
+ | `3d` (default) | Model trees | Model trees |
28
+ | `flat` | Flat crowns/circles | Flat crowns/circles |
29
+ | `auto` | Flat crowns/circles | Model trees |
30
+
31
+ `viewMode` is explicit application state (`2d` by default), not a pitch threshold. Tilting the camera
32
+ does not change representation. Visibility defaults to `vegetation.visible ?? true`; OFF hides every
33
+ representation. Turning trees off releases model buffers and tile readers. Controls apply immediately,
34
+ without a grow/sink animation, and do not change building appearance or lighting.
35
+
36
+ <!-- docs:check -->
37
+
38
+ ```ts
39
+ import {createTileflowVegetation, type TileflowVegetationOptions} from '@tileflow/core/browser';
40
+
41
+ export function mountVegetation(options: TileflowVegetationOptions) {
42
+ const trees = createTileflowVegetation({...options, viewMode: '2d'});
43
+ trees.setVisible(false); // Hide all vegetation, including flat artwork.
44
+ trees.setVisible(undefined); // Restore the map's authored visibility.
45
+ trees.setViewMode('3d'); // Only auto maps change representation.
46
+ return trees; // Call dispose() before removing the map, or let its remove event clean up.
47
+ }
48
+ ```
49
+
50
+ Pass the application's HTTP tile `transformRequest` when it supplies credentials or request headers;
51
+ asynchronous transforms are supported. Use one vegetation controller per map and do not mount a second
52
+ low-level tree renderer alongside it. Style replacement and theme changes retain visibility and view mode.
53
+
54
+ To change an inherited map's policy while preserving its artwork:
55
+
56
+ <!-- docs:check -->
57
+
58
+ ```ts
59
+ import {defineMap, refine} from '@tileflow/core';
60
+ import {streets} from '@tileflow/maps';
61
+
62
+ export default defineMap({
63
+ id: 'adaptive-trees',
64
+ version: 1,
65
+ extends: streets,
66
+ modules: {vegetation: refine({mode: 'auto', fallback: 'flat'})},
67
+ });
68
+ ```
69
+
70
+ Use `refine({visible: false})` for initially hidden vegetation that can be enabled at runtime.
71
+ Use `vegetation: disable()` to remove the module entirely; a runtime toggle cannot restore a module
72
+ that is absent from the compiled map. Browser framework components accept `vegetation={false}` and
73
+ `viewMode="3d"` (Vue: `:vegetation="false"`, `view-mode="3d"`). Image mode uses the compiled static
74
+ fallback and does not apply these interactive controls.
75
+
76
+ ## Animate trees with the low-level renderer
19
77
 
20
78
  This example assumes an existing MapLibre map showing a Tileflow map whose vector source is called
21
79
  `tileflow`, as Tileflow's maps call World, and a theme controller for it:
@@ -42,7 +100,8 @@ a share of their size, from 0 to 1. Each returns a promise that resolves once th
42
100
  newer call takes over from where the trees stand. A browser preference for reduced motion changes at
43
101
  once. `dispose()` takes the layer off the map.
44
102
 
45
- While they stand, the 3D trees stand in for the map's flat trees: the layers the vegetation module
103
+ This low-level API retains its flat-layer restoration behavior; use the vegetation controller for
104
+ a visibility toggle. While they stand, the 3D trees stand in for the map's flat trees: the layers the vegetation module
46
105
  draws them with are hidden when the trees are shown, and come back as they were once the trees have
47
106
  sunk or the layer leaves the map.
48
107
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tileflow/core",
3
- "version": "0.1.0-alpha.34",
3
+ "version": "0.1.0-alpha.35",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Typed Tileflow config and MapLibre style compiler.",
6
6
  "type": "module",
@@ -84,9 +84,10 @@
84
84
  "@mapbox/vector-tile": "3.0.0",
85
85
  "earcut": "3.2.4",
86
86
  "maplibre-contour": "0.1.0",
87
+ "meshoptimizer": "1.3.0",
87
88
  "pbf": "5.1.2",
88
- "pmtiles": "4.4.0",
89
- "whatwg-url": "15.1.0"
89
+ "pmtiles": "4.5.0",
90
+ "whatwg-url": "17.2.0"
90
91
  },
91
92
  "scripts": {
92
93
  "build": "tsup --config tsup.build.config.ts && tsup --config tsup.native-profile.config.ts && tsup src/browser.ts --format esm --dts --no-splitting --platform browser && tsup --config tsup.native.config.ts && cp ../../docs/modules-api-reference.json dist/config-reference.json",