isard 1.2.0 → 1.3.0
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/CHANGELOG.md +88 -0
- package/README.md +44 -39
- package/dist/basemap/pmtileslayer.d.ts +33 -0
- package/dist/basemap/pmtileslayer.js +97 -0
- package/dist/basemap/pmtileslayer.js.map +1 -0
- package/dist/basemap/trees.d.ts +26 -0
- package/dist/basemap/trees.js +141 -0
- package/dist/basemap/trees.js.map +1 -0
- package/dist/basemap/vectorstyle.d.ts +10 -0
- package/dist/basemap/vectorstyle.js +35 -0
- package/dist/basemap/vectorstyle.js.map +1 -1
- package/dist/basemap/vectortiles.d.ts +67 -44
- package/dist/basemap/vectortiles.js +386 -302
- package/dist/basemap/vectortiles.js.map +1 -1
- package/dist/basemap/workerpool.d.ts +17 -0
- package/dist/basemap/workerpool.js +194 -0
- package/dist/basemap/workerpool.js.map +1 -0
- package/dist/basemap.worker.js +2 -2
- package/dist/basemap.worker.js.map +15 -8
- package/dist/decoder.worker.js +4 -4
- package/dist/decoder.worker.js.map +3 -3
- package/dist/engine-loader.d.ts +5 -1
- package/dist/engine-loader.js +11 -1
- package/dist/engine-loader.js.map +1 -1
- package/dist/geoplateforme.d.ts +16 -0
- package/dist/geoplateforme.js +23 -0
- package/dist/geoplateforme.js.map +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/isard_bg.wasm +0 -0
- package/dist/isard_core_bg.wasm +0 -0
- package/dist/isard_tess_bg.wasm +0 -0
- package/dist/layers/layers.js +9 -0
- package/dist/layers/layers.js.map +1 -1
- package/dist/layers/vectors.d.ts +60 -0
- package/dist/layers/vectors.js +130 -0
- package/dist/layers/vectors.js.map +1 -0
- package/dist/map/frame-loop.d.ts +3 -0
- package/dist/map/frame-loop.js +6 -0
- package/dist/map/frame-loop.js.map +1 -1
- package/dist/map/raster-source.js +1 -1
- package/dist/map/raster-source.js.map +1 -1
- package/dist/map/workers.d.ts +1 -1
- package/dist/map/workers.js +1 -1
- package/dist/map/workers.js.map +1 -1
- package/dist/map.d.ts +21 -6
- package/dist/map.js +65 -11
- package/dist/map.js.map +1 -1
- package/dist/react/IsardMap.js +1 -0
- package/dist/react/IsardMap.js.map +1 -1
- package/dist/react/Swipe.d.ts +11 -4
- package/dist/react/Swipe.js +26 -15
- package/dist/react/Swipe.js.map +1 -1
- package/dist/react/VectorGeometry.d.ts +35 -0
- package/dist/react/VectorGeometry.js +82 -0
- package/dist/react/VectorGeometry.js.map +1 -0
- package/dist/react/context.d.ts +2 -1
- package/dist/react/context.js.map +1 -1
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +2 -1
- package/dist/react/index.js.map +1 -1
- package/dist/types.d.ts +24 -1
- package/dist/types.js.map +1 -1
- package/dist/ui/minimap.d.ts +25 -0
- package/dist/ui/minimap.js +90 -0
- package/dist/ui/minimap.js.map +1 -0
- package/dist/ui/scale.d.ts +19 -0
- package/dist/ui/scale.js +44 -0
- package/dist/ui/scale.js.map +1 -0
- package/dist/ui/search.d.ts +25 -0
- package/dist/ui/search.js +119 -0
- package/dist/ui/search.js.map +1 -0
- package/dist/ui/style.js +17 -0
- package/dist/ui/style.js.map +1 -1
- package/dist/util/sun.d.ts +11 -0
- package/dist/util/sun.js +26 -0
- package/dist/util/sun.js.map +1 -0
- package/dist/wasm/isard.d.ts +82 -7
- package/dist/wasm/isard.js +229 -8
- package/dist/wasm/isard_core.d.ts +82 -7
- package/dist/wasm/isard_core.js +229 -8
- package/dist/wasm/isard_tess.d.ts +109 -0
- package/dist/wasm/isard_tess.js +398 -0
- package/dist/workers/basemap/geometry.d.ts +72 -0
- package/dist/workers/basemap/labels.d.ts +8 -0
- package/dist/workers/basemap/raster.d.ts +10 -0
- package/dist/workers/basemap/tiles.d.ts +65 -0
- package/dist/workers/basemap/trees.d.ts +19 -0
- package/dist/workers/basemap.worker.d.ts +126 -22
- package/dist/workers/basemap.worker.js +72 -551
- package/dist/workers/basemap.worker.js.map +1 -1
- package/package.json +5 -4
- package/vite.js +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,94 @@
|
|
|
3
3
|
Every notable change of `isard` (published as `@ign-junn/isard` up to 0.1.1). The format follows [Keep a Changelog](https://keepachangelog.com), the
|
|
4
4
|
versions [Semantic Versioning](https://semver.org) (before 1.0.0, a minor version may change an API).
|
|
5
5
|
|
|
6
|
+
## [1.3.0] - 2026-10-23
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- `vectorTiles().vectors()`: the fills and lines of a MapLibre style drawn as geometry on the globe and the relief,
|
|
11
|
+
tile by tile (cut in the basemap workers, `vectors` job), rather than into raster images: sharp at any zoom and
|
|
12
|
+
pitch, in the style's order with each layer's zoom range, dashes, widths following the zoom, restyled without
|
|
13
|
+
cutting the tiles again (`setPaintProperty`, `setLayoutProperty`), picked by feature with its style layer
|
|
14
|
+
(`VectorLayer`); `<VectorGeometry>` in React (`paint` by style layer or as a function of it, `visible`, `order`).
|
|
15
|
+
The tiles are made ready for the GPU in the basemap workers (their fills triangulated, their lines cut) by the
|
|
16
|
+
tessellation module, `isard_tess_bg.wasm` (37 KB gzipped, the engine's own code, loaded with the first tile; the
|
|
17
|
+
main thread does it without), the main thread only uploading them: on a scripted flight over Paris with the whole
|
|
18
|
+
Positron style, half the main thread's time of the engine doing it, about that of the raster path. They follow the
|
|
19
|
+
camera while it moves. Their geometry is the engine's only (the layer keeps what picking needs): an engine made
|
|
20
|
+
anew, after a context loss or with 3D Tiles, has them cut again by the workers, about 1 MB of JavaScript heap less per
|
|
21
|
+
tile of the whole Positron style. The example Vector basemaps draws the water so, in TypeScript and React. About 12 KB
|
|
22
|
+
gzipped more for the engine.
|
|
23
|
+
- The swipe's `other` layers, shown on the other side of the line only (`setSwipe({ layers, other })`, `<SwipeOther>`
|
|
24
|
+
in React): two meshes of the same place, each on its side, never drawn over each other. The example Swipe on a mesh
|
|
25
|
+
(Strasbourg's photomeshes of 2018 and 2022), in TypeScript and React.
|
|
26
|
+
- Controls: `ScaleControl`, `SearchControl` (the Géoplateforme's geocoding by default, `geoplateformeGeocode`) and
|
|
27
|
+
`MinimapControl`; the example Globe IGN (orthophotos, relief, PLAN IGN's vector tiles and labels over them, its buildings,
|
|
28
|
+
the four controls), in TypeScript and React.
|
|
29
|
+
- `vectorTiles().extrusions()`: the buildings of vector tiles (PLAN IGN's `bati_surf`, OpenMapTiles' `building`)
|
|
30
|
+
raised by the style given, their tiles following the view (decoded in the basemap workers, handed over as typed
|
|
31
|
+
arrays); the Globe IGN example's buildings everywhere the map goes.
|
|
32
|
+
- Without relief, the camera settles on the 3D Tiles mesh under its center rather than at sea level, on its lowest
|
|
33
|
+
surface around the center (the street, not a roof or a spire, nothing jumping as the map zooms): a mesh shown alone
|
|
34
|
+
(`basemap: 'none'`), as in the example Swipe on a mesh, which no longer loads relief nor imagery under its meshes.
|
|
35
|
+
The layers standing on the map's ground (blocks, models) leave the camera at sea level.
|
|
36
|
+
- 3D Tiles in the legacy `CESIUM_RTC` glTF extension (ContextCapture's exports), its center applied as `RTC_CENTER`.
|
|
37
|
+
|
|
38
|
+
- 3D trees: `vectorTiles().trees()` grows trees on the woods and grass of the vector tiles. The basemap workers
|
|
39
|
+
scatter them on a jittered grid, the same ones each time a tile is cut. They are one instanced draw, dense near the
|
|
40
|
+
view's center and thinned far away (`maxInstances`), with their colors as given (`TreeColors`, `treeModel`). Example:
|
|
41
|
+
Trees (the forest of Fontainebleau over the relief).
|
|
42
|
+
- Buildings in `vectors()`: the style's `fill-extrusion` layers are raised to their height and base height in the same
|
|
43
|
+
tiles, lit by the side they face.
|
|
44
|
+
- The sun's shadows: `map.setSun({ date } | { azimuth, altitude }, opacity, night)`, the `sun` option or
|
|
45
|
+
`<IsardMap sun>`, with `sunPosition(date, lonLat)`. The solid scene (buildings, trees, models, 3D Tiles, the relief) is
|
|
46
|
+
drawn from the sun into a 2048² depth map around the view, then a pass darkens what the sun does not reach, before
|
|
47
|
+
the labels.
|
|
48
|
+
- The night: as the sun sets (from 6° above the horizon to 6° under it, or `night` set by hand), the frame is tinted
|
|
49
|
+
in the blue of the night and the shadows fade. The windows of the buildings of `vectors()` and `extrusions()` light
|
|
50
|
+
up, floor by floor and bay by bay, the same ones from frame to frame and wherever the view or the tile is. The
|
|
51
|
+
streets glow, with `vectors({ streets })` or `<VectorGeometry streets>` choosing which ones. The labels turn light on
|
|
52
|
+
dark.
|
|
53
|
+
- `vectors()` drawn closer to MapLibre: the lines' ends as their `line-cap` says, the fills' outlines
|
|
54
|
+
(`fill-outline-color`) one pixel wide, and the colors following the camera's zoom across tiles of every level.
|
|
55
|
+
- Overzoom in `vectors()`: past the source's deepest level, its tiles are cut again into deeper ones down to level 18,
|
|
56
|
+
with the filters and styles of their level. A building cut at a tile's edge rises no wall there.
|
|
57
|
+
- Examples: A 3D vector map (Paris from OpenFreeMap's Liberty style, all of it as geometry, a summer evening), Paris
|
|
58
|
+
by night (a clock through the day), Inside a shopping mall (Westfield Les 4 Temps at La Défense floor by floor, from
|
|
59
|
+
OpenStreetMap's indoor data), in TypeScript and React; the examples grouped by subject, each on one page with its
|
|
60
|
+
TypeScript and React sources in tabs.
|
|
61
|
+
- `bun run perf`: the frames, long tasks and heap of the 3D vector examples while dragged, to run before and after a
|
|
62
|
+
change of the drawing.
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- With a sun, the labels are drawn after the shadows and the night: a layer stacked over a label layer is shaded and
|
|
67
|
+
lit too. The lights are drawn on both faces of the walls (whichever way their rings wind) and in their columns of
|
|
68
|
+
the swipe.
|
|
69
|
+
- `vectors()`: a tile's colors are judged translucent by their last zoom too. A tile stretched far past its level
|
|
70
|
+
mixes its colors over its first zooms. An outline added or taken away (`fill-outline-color`, `fill-antialias`) cuts
|
|
71
|
+
the tiles again instead of dropping their restyles.
|
|
72
|
+
- `trees()`: the cells are bounded per tile, so a small `spacing` no longer freezes a worker, and a ring of many
|
|
73
|
+
points no longer overflows the call stack.
|
|
74
|
+
- At night, no glare toward the horizon: the windows fade out under two pixels a bay, and the streets' glow narrows
|
|
75
|
+
and fades far away.
|
|
76
|
+
- `map.whenIdle()` on a map already idle, and the `ready` of a layer added to it that loads nothing (lines from
|
|
77
|
+
data, the geometry of vector tiles), resolved by the next frame: they waited for an 'idle' that no frame sent.
|
|
78
|
+
- The examples' indexes of the docs: one row per example, the React ones listed again.
|
|
79
|
+
|
|
80
|
+
### Changed
|
|
81
|
+
|
|
82
|
+
- The large files are split by what they do: the basemap worker's jobs in `src/workers/basemap/`, the worker pool
|
|
83
|
+
and the PMTiles layers out of `vectortiles.ts`, a vector layer's tile and its JavaScript API out of `render/vectors.rs`.
|
|
84
|
+
`geo`'s modules are grouped (`view/`, `body/`, `mesh/`), the RAF20 geoid grid moved to `crates/geo/data/`, and each
|
|
85
|
+
crate has a README.
|
|
86
|
+
- The models' relief altitudes are kept by position across instance sets. The tessellation's arrays are copied once.
|
|
87
|
+
`extrusions()` follows the view while it moves.
|
|
88
|
+
- The docs: the React section written as a library's (installation, quick start, guide), the home page's features,
|
|
89
|
+
the roadmap and the architecture (an interactive diagram) brought up to date.
|
|
90
|
+
- The repository that publishes and builds the docs chosen by its variables (`RELEASE`, `DOCS_BASE`); a contributing
|
|
91
|
+
guide, a security policy, a code of conduct and the templates of issues and pull requests; the PLAN IGN tiles of
|
|
92
|
+
two visual tests recorded, which no longer reach the network.
|
|
93
|
+
|
|
6
94
|
## [1.2.0] - 2026-10-22
|
|
7
95
|
|
|
8
96
|
### Added
|
package/README.md
CHANGED
|
@@ -10,13 +10,13 @@ A WebGL2 map engine for large, interactive data layers, labels and 3D Tiles.<br
|
|
|
10
10
|
Rust core in WebAssembly, a small TypeScript API.
|
|
11
11
|
|
|
12
12
|
[](https://www.npmjs.com/package/isard)
|
|
13
|
-
[](https://ideal-adventure-zqn6y6p.pages.github.io/)
|
|
14
|
+
[](https://github.com/IGNF/isard/actions/workflows/checks.yml)
|
|
15
15
|
[](LICENSE)
|
|
16
16
|
|
|
17
|
-
**[Documentation](https://
|
|
18
|
-
**[Examples](https://
|
|
19
|
-
**[API](https://
|
|
17
|
+
**[Documentation](https://ideal-adventure-zqn6y6p.pages.github.io/)** ·
|
|
18
|
+
**[Examples](https://ideal-adventure-zqn6y6p.pages.github.io/examples/)** ·
|
|
19
|
+
**[API](https://ideal-adventure-zqn6y6p.pages.github.io/api)** ·
|
|
20
20
|
**[Changelog](CHANGELOG.md)**
|
|
21
21
|
|
|
22
22
|
<img src="docs/public/screenshots/hero.jpg" alt="Isard: rings drawn by a shader over the relief of the Mont Blanc, buildings colored by height, communes colored by population, the globe" />
|
|
@@ -33,20 +33,24 @@ General-purpose map libraries spend their time on things these maps do not need.
|
|
|
33
33
|
- 🗺️ **Basemaps and relief, worldwide.** OpenFreeMap's vector basemaps by default (OpenStreetMap data, no key), any
|
|
34
34
|
MapLibre style drawn in workers with its labels above your data, raster tiles, a worldwide relief, on a globe that
|
|
35
35
|
turns into Web Mercator. The Géoplateforme's data for France in one option or call: PLAN.IGN, orthophotos,
|
|
36
|
-
hillshade, the IGN relief, any WMTS, WMS or WFS layer ([Géoplateforme (IGN) data](https://
|
|
36
|
+
hillshade, the IGN relief, any WMTS, WMS or WFS layer ([Géoplateforme (IGN) data](https://ideal-adventure-zqn6y6p.pages.github.io/guide/geoplateforme)).
|
|
37
37
|
- ⛰️ **Everything on the relief.** Lines, circles, labels and buildings stand on the ground; fills of any size are
|
|
38
38
|
exact to the pixel over hills and valleys; your own GLSL is drawn over the ground like the basemap.
|
|
39
39
|
- 🪐 **The Moon, Mars, any body.** `body: 'moon'`, `'mars'` or an ellipsoid of your own: the globe at its radius
|
|
40
40
|
with its own atmosphere halo (or none), its metres in the camera, the relief and 3D Tiles, NASA's planetary mosaics
|
|
41
41
|
reprojected in the page from longitudes and latitudes, and their relief: `terrain: true` raises Olympus Mons and
|
|
42
42
|
digs Valles Marineris and the lunar craters from NASA's LOLA and MOLA models.
|
|
43
|
+
- 🌃 **A 3D vector city.** Any MapLibre style as geometry (`vectors()`): its streets, water and buildings raised from
|
|
44
|
+
its `fill-extrusion` layers, sharp at any zoom (its tiles cut again past the source's deepest level), 3D trees in
|
|
45
|
+
its woods, the sun's shadows at any hour, the night with lit windows and glowing streets; indoor data floor by floor
|
|
46
|
+
(a shopping mall, its shops raised and named).
|
|
43
47
|
- 🏙️ **Buildings and 3D Tiles.** Buildings colored by a value or by a network's travel times, photomeshes and city
|
|
44
48
|
models in 3D Tiles (Draco, meshopt, KTX2).
|
|
45
49
|
- ☁️ **Point clouds.** LiDAR in COPC read by range requests (the IGN's LiDAR HD, reprojected from Lambert-93 onto the
|
|
46
50
|
relief), 3D Tiles points; colored by RGB, class, altitude or intensity, Eye-Dome Lighting, a point budget, picked.
|
|
47
51
|
- 🧊 **Your own 3D.** glTF models at positions, instanced and picked; custom layers for your WebGL code or a three.js
|
|
48
52
|
scene, in the map's frame and depth, with camera-relative matrices exact at any zoom.
|
|
49
|
-
- 🦀 **Small and exact.**
|
|
53
|
+
- 🦀 **Small and exact.** 272 KB of WebAssembly gzipped (446 KB once a 3D Tiles layer, a model or a point cloud is added), f64 on the CPU and f32 relative to the camera on the GPU
|
|
50
54
|
(centimetres at any zoom), frames on demand only, strict TypeScript types end to end.
|
|
51
55
|
|
|
52
56
|
## Quick start
|
|
@@ -64,32 +68,32 @@ map.on('hover', ({ lonLat }) => lonLat && network.setValues(travelTimes(lonLat))
|
|
|
64
68
|
```
|
|
65
69
|
|
|
66
70
|
That is the whole integration: the map fills its container, follows its size, handles mouse and touch and loads the
|
|
67
|
-
basemap. With a bundler, see [Getting started](https://
|
|
71
|
+
basemap. With a bundler, see [Getting started](https://ideal-adventure-zqn6y6p.pages.github.io/guide/getting-started#with-a-bundler).
|
|
68
72
|
|
|
69
73
|
## Examples
|
|
70
74
|
|
|
71
|
-
Live in the [documentation](https://
|
|
75
|
+
Live in the [documentation](https://ideal-adventure-zqn6y6p.pages.github.io/examples/), sources in [`examples/`](examples/).
|
|
72
76
|
|
|
73
77
|
<table>
|
|
74
78
|
<tr>
|
|
75
|
-
<td width="50%"><a href="https://
|
|
76
|
-
<td width="50%"><a href="https://
|
|
79
|
+
<td width="50%"><a href="https://ideal-adventure-zqn6y6p.pages.github.io/examples/globe"><img src="docs/public/screenshots/globe.jpg" alt="" /></a><br /><b>The globe</b><br />turning in its atmosphere, a flight to a click</td>
|
|
80
|
+
<td width="50%"><a href="https://ideal-adventure-zqn6y6p.pages.github.io/examples/basic"><img src="docs/public/screenshots/basic.jpg" alt="" /></a><br /><b>A map</b><br />the vector basemap, a marker and its popup</td>
|
|
77
81
|
</tr>
|
|
78
82
|
<tr>
|
|
79
|
-
<td><a href="https://
|
|
80
|
-
<td><a href="https://
|
|
83
|
+
<td><a href="https://ideal-adventure-zqn6y6p.pages.github.io/examples/geojson"><img src="docs/public/screenshots/geojson.jpg" alt="" /></a><br /><b>GeoJSON layers</b><br />fills by value, outlines, labels, picking</td>
|
|
84
|
+
<td><a href="https://ideal-adventure-zqn6y6p.pages.github.io/examples/buildings"><img src="docs/public/screenshots/buildings.jpg" alt="" /></a><br /><b>Buildings</b><br />raised to their height, colored by it</td>
|
|
81
85
|
</tr>
|
|
82
86
|
<tr>
|
|
83
|
-
<td><a href="https://
|
|
84
|
-
<td><a href="https://
|
|
87
|
+
<td><a href="https://ideal-adventure-zqn6y6p.pages.github.io/examples/relief"><img src="docs/public/screenshots/relief.jpg" alt="" /></a><br /><b>Relief and a ground shader</b><br />your GLSL over the mountains</td>
|
|
88
|
+
<td><a href="https://ideal-adventure-zqn6y6p.pages.github.io/examples/network"><img src="docs/public/screenshots/network.jpg" alt="" /></a><br /><b>Travel times on hover</b><br />a network recolored at every move</td>
|
|
85
89
|
</tr>
|
|
86
90
|
<tr>
|
|
87
|
-
<td><a href="https://
|
|
88
|
-
<td><a href="https://
|
|
91
|
+
<td><a href="https://ideal-adventure-zqn6y6p.pages.github.io/examples/vector"><img src="docs/public/screenshots/vector.jpg" alt="" /></a><br /><b>Vector basemaps</b><br />OpenFreeMap's styles, a restyled subset, the water drawn as geometry</td>
|
|
92
|
+
<td><a href="https://ideal-adventure-zqn6y6p.pages.github.io/examples/tiles3d"><img src="docs/public/screenshots/tiles3d.jpg" alt="" /></a><br /><b>3D Tiles</b><br />the Lille photomesh</td>
|
|
89
93
|
</tr>
|
|
90
94
|
<tr>
|
|
91
|
-
<td><a href="https://
|
|
92
|
-
<td><a href="https://
|
|
95
|
+
<td><a href="https://ideal-adventure-zqn6y6p.pages.github.io/examples/moon"><img src="docs/public/screenshots/moon.jpg" alt="" /></a><br /><b>The Moon</b><br />another body, the LRO mosaic over LOLA's relief, the Apollo sites</td>
|
|
96
|
+
<td><a href="https://ideal-adventure-zqn6y6p.pages.github.io/examples/mars"><img src="docs/public/screenshots/mars.jpg" alt="" /></a><br /><b>Mars</b><br />its thin dusty halo, the Viking mosaic over MOLA's relief, famous sites</td>
|
|
93
97
|
</tr>
|
|
94
98
|
</table>
|
|
95
99
|
|
|
@@ -97,32 +101,32 @@ Live in the [documentation](https://graffkevin.github.io/isard/examples/), sourc
|
|
|
97
101
|
|
|
98
102
|
| Layer | | |
|
|
99
103
|
|---|---|---|
|
|
100
|
-
| [Networks](https://
|
|
101
|
-
| [Lines, circles, fills](https://
|
|
102
|
-
| [Labels](https://
|
|
103
|
-
| [Buildings](https://
|
|
104
|
-
| [Ground shaders](https://
|
|
105
|
-
| [Rasters](https://
|
|
106
|
-
| [3D Tiles](https://
|
|
107
|
-
| [glTF models](https://
|
|
108
|
-
| [Point clouds](https://
|
|
109
|
-
| [Custom layers](https://
|
|
104
|
+
| [Networks](https://ideal-adventure-zqn6y6p.pages.github.io/layers/networks) | `addNetwork` | millions of edges colored by a value per node, worked out per pixel along the edges |
|
|
105
|
+
| [Lines, circles, fills](https://ideal-adventure-zqn6y6p.pages.github.io/layers/features) | `addLines`, `addCircles`, `addFills` | GeoJSON styled per feature: zoom stops, functions, values through a ramp, filters; in any CRS, KML, GPX and TopoJSON parsed |
|
|
106
|
+
| [Labels](https://ideal-adventure-zqn6y6p.pages.github.io/layers/labels) | `addSymbols` | SDF fonts (Noto Sans by default, any glyphs), halos, along lines, collisions shared by all layers |
|
|
107
|
+
| [Buildings](https://ideal-adventure-zqn6y6p.pages.github.io/layers/buildings) | `addExtrusions` | tiles or one set, colored by values or by a network's times |
|
|
108
|
+
| [Ground shaders](https://ideal-adventure-zqn6y6p.pages.github.io/layers/ground) | `addGround` | your fragment shader over the ground: water depth, heat maps |
|
|
109
|
+
| [Rasters](https://ideal-adventure-zqn6y6p.pages.github.io/layers/rasters) | `addRaster`, `addPmtiles` | XYZ, WMTS, WMS, PMTiles; in Web Mercator, or reprojected from another grid (longitudes and latitudes, Lambert-93, UTM, any CRS through proj4); or tiles drawn by your code |
|
|
110
|
+
| [3D Tiles](https://ideal-adventure-zqn6y6p.pages.github.io/layers/tiles3d) | `addTiles3d` | b3dm, pnts, glb, external tilesets, compressed or not |
|
|
111
|
+
| [glTF models](https://ideal-adventure-zqn6y6p.pages.github.io/layers/models) | `addModels` | a glTF model at many positions, instanced, on the relief, picked |
|
|
112
|
+
| [Point clouds](https://ideal-adventure-zqn6y6p.pages.github.io/layers/pointclouds) | `addPointCloud` | COPC (LiDAR HD) by range requests, pnts and glTF points; by class, altitude, intensity, EDL |
|
|
113
|
+
| [Custom layers](https://ideal-adventure-zqn6y6p.pages.github.io/layers/custom) | `addCustomLayer` | your WebGL code or a three.js scene in the map's frame, sharing its depth |
|
|
110
114
|
|
|
111
115
|
Plus markers, popups, controls, picking within a radius, `easeTo`, `flyTo`, a sky and fog for tilted views. All in
|
|
112
|
-
the [API reference](https://
|
|
116
|
+
the [API reference](https://ideal-adventure-zqn6y6p.pages.github.io/api).
|
|
113
117
|
|
|
114
118
|
Local files on a map: `bun scripts/viewer.ts data.geojson lidar.copc.laz tiles.pmtiles` serves the viewer
|
|
115
119
|
(`apps/viewer`: files dropped or given, a layer list, a bridge any app can drive) on 127.0.0.1
|
|
116
|
-
([The viewer](https://
|
|
120
|
+
([The viewer](https://ideal-adventure-zqn6y6p.pages.github.io/guide/viewer)).
|
|
117
121
|
|
|
118
122
|
In QGIS (3.28 and 4): the **isard 3D view** plugin shows the map canvas and its layers in 3D, in a panel or the
|
|
119
|
-
browser, the camera, layers and clicks in sync ([QGIS plugin](https://
|
|
123
|
+
browser, the camera, layers and clicks in sync ([QGIS plugin](https://ideal-adventure-zqn6y6p.pages.github.io/guide/qgis)).
|
|
120
124
|
|
|
121
125
|
## Fast by design
|
|
122
126
|
|
|
123
127
|
Geometry uploaded once and only the view drawn, values in textures, the ground composited in one pass, frames on
|
|
124
128
|
demand, decoding and vector tiles in workers. The details and the measures:
|
|
125
|
-
[Performance](https://
|
|
129
|
+
[Performance](https://ideal-adventure-zqn6y6p.pages.github.io/guide/performance).
|
|
126
130
|
|
|
127
131
|
## Develop
|
|
128
132
|
|
|
@@ -138,28 +142,29 @@ bun run docs:dev # the documentation, with the examples live
|
|
|
138
142
|
```
|
|
139
143
|
|
|
140
144
|
The root holds two workspaces side by side: Rust (`Cargo.toml`) and TypeScript (`package.json`), and `bun run build`
|
|
141
|
-
drives both.
|
|
145
|
+
drives both. Each crate has its README; [crates/README.md](crates/README.md) guides through them.
|
|
142
146
|
|
|
143
147
|
```
|
|
144
148
|
crates/geo/ camera, projections, geodesy and geoid, tile selection, label placement, polygons tested natively
|
|
145
149
|
crates/tiles3d/ 3D Tiles tree, traversal and decoding tested natively
|
|
146
150
|
crates/pointcloud/ COPC point clouds: header, octree hierarchy, traversal, LAZ chunks tested natively
|
|
147
151
|
crates/engine/ the WebAssembly core: the engine, the ground, the layers, picking
|
|
152
|
+
crates/tess/ the tessellation module: vector tiles made ready for the GPU in the basemap workers
|
|
148
153
|
packages/isard/ the npm package isard: GeoMap, layer handles, overlays, workers
|
|
149
154
|
apps/ the demo, the viewer (local files, a bridge for other apps) and the benchmark
|
|
150
155
|
examples/ one page per example
|
|
151
156
|
docs/ the documentation site (VitePress)
|
|
152
157
|
scripts/ build, dev server, docs, release
|
|
153
|
-
integrations/qgis/ the QGIS plugin (Python), its server tested with python3 -m unittest
|
|
158
|
+
integrations/qgis/ the QGIS plugin (Python), its server tested with python3 -m unittest; integrations/README.md: the bridge
|
|
154
159
|
```
|
|
155
160
|
|
|
156
|
-
More in [Develop and release](https://
|
|
161
|
+
More in [Develop and release](https://ideal-adventure-zqn6y6p.pages.github.io/guide/develop).
|
|
157
162
|
|
|
158
163
|
## Roadmap
|
|
159
164
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
-
|
|
165
|
+
The whole MapLibre style drawn as vectors, a 3D vector map to walk through, fluids on the globe (wind through the
|
|
166
|
+
streets, animated), several projections on the globe, and smaller items: see
|
|
167
|
+
[Roadmap](https://ideal-adventure-zqn6y6p.pages.github.io/guide/roadmap).
|
|
163
168
|
|
|
164
169
|
## Licence
|
|
165
170
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** PMTiles archives on the map (`GeoMap.addPmtiles`): their images as a raster layer, or their vector tiles drawn with
|
|
2
|
+
* the style given or a plain one made from their layers' names */
|
|
3
|
+
import type { Layer } from '../layers/layers.ts';
|
|
4
|
+
import type { RasterOptions } from '../types.ts';
|
|
5
|
+
import type { Style } from './vectorstyle.ts';
|
|
6
|
+
/**
|
|
7
|
+
* A plain style for vector tiles known only by their layers' names (a PMTiles archive's `vector_layers`, an MBTiles'
|
|
8
|
+
* metadata): each layer's polygons filled, its lines and outlines stroked and its points as circles, a color per
|
|
9
|
+
* layer, the `name` of points as labels. `source` is the style's vector source (`url`: TileJSON or `pmtiles://…`, or
|
|
10
|
+
* `tiles`)
|
|
11
|
+
*/
|
|
12
|
+
export declare const basicStyle: (source: {
|
|
13
|
+
url?: string;
|
|
14
|
+
tiles?: string[];
|
|
15
|
+
minzoom?: number;
|
|
16
|
+
maxzoom?: number;
|
|
17
|
+
}, layers: readonly string[]) => Style;
|
|
18
|
+
/** Options of `GeoMap.addPmtiles` */
|
|
19
|
+
export interface PmtilesOptions {
|
|
20
|
+
/** What the archive holds (default from its header: raster images or vector tiles) */
|
|
21
|
+
kind?: 'raster' | 'vector';
|
|
22
|
+
/** A MapLibre style for vector tiles (its vector source `pmtiles://…`); default `basicStyle` of the archive's layers */
|
|
23
|
+
style?: Style;
|
|
24
|
+
opacity?: number;
|
|
25
|
+
/** Texture size of raster tiles (default 256; 512 for the sharper tiles of a 512 px archive) */
|
|
26
|
+
size?: number;
|
|
27
|
+
/** Credits, HTML (default the archive's `attribution` metadata, as text) */
|
|
28
|
+
attribution?: string;
|
|
29
|
+
before?: Layer;
|
|
30
|
+
}
|
|
31
|
+
/** A raster loader reading the tiles of a `pmtiles://` archive URL (images of any size, drawn at `size`); undefined
|
|
32
|
+
* for another URL */
|
|
33
|
+
export declare const pmtilesRasterLoader: (url: string, size: number) => RasterOptions["loader"];
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** PMTiles archives on the map (`GeoMap.addPmtiles`): their images as a raster layer, or their vector tiles drawn with
|
|
2
|
+
* the style given or a plain one made from their layers' names */
|
|
3
|
+
import { isRecord } from "../util/guards.js";
|
|
4
|
+
import { pmtiles, pmtilesUrl } from "./pmtiles.js";
|
|
5
|
+
/** Colors of the layers of `basicStyle`, in turn */
|
|
6
|
+
const BASIC_COLORS = ['#2f6fd1', '#d1495b', '#2a9d8f', '#e9a03b', '#7b5ea7', '#5c7a29', '#c8553d', '#3d8eb9'];
|
|
7
|
+
/**
|
|
8
|
+
* A plain style for vector tiles known only by their layers' names (a PMTiles archive's `vector_layers`, an MBTiles'
|
|
9
|
+
* metadata): each layer's polygons filled, its lines and outlines stroked and its points as circles, a color per
|
|
10
|
+
* layer, the `name` of points as labels. `source` is the style's vector source (`url`: TileJSON or `pmtiles://…`, or
|
|
11
|
+
* `tiles`)
|
|
12
|
+
*/
|
|
13
|
+
export const basicStyle = (source, layers) => {
|
|
14
|
+
const out = [];
|
|
15
|
+
layers.forEach((name, i) => {
|
|
16
|
+
const color = BASIC_COLORS[i % BASIC_COLORS.length] ?? '#2f6fd1';
|
|
17
|
+
const base = { source: 'data', 'source-layer': name };
|
|
18
|
+
out.push({ id: `${name}-fill`, type: 'fill', ...base, paint: { 'fill-color': color, 'fill-opacity': 0.18 } }, { id: `${name}-line`, type: 'line', ...base, paint: { 'line-color': color, 'line-width': 1.2 } }, {
|
|
19
|
+
id: `${name}-circle`,
|
|
20
|
+
type: 'circle',
|
|
21
|
+
...base,
|
|
22
|
+
paint: { 'circle-color': color, 'circle-radius': 3.5, 'circle-stroke-color': '#fff', 'circle-stroke-width': 1 },
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
layers.forEach((name) => {
|
|
26
|
+
out.push({
|
|
27
|
+
id: `${name}-label`,
|
|
28
|
+
type: 'symbol',
|
|
29
|
+
source: 'data',
|
|
30
|
+
'source-layer': name,
|
|
31
|
+
filter: ['all', ['==', ['geometry-type'], 'Point'], ['has', 'name']],
|
|
32
|
+
layout: { 'text-field': ['get', 'name'], 'text-size': 12, 'text-offset': [0, 0.9], 'text-anchor': 'top' },
|
|
33
|
+
paint: { 'text-color': '#1d2433', 'text-halo-color': '#fff', 'text-halo-width': 1.5 },
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
return { version: 8, sources: { data: { type: 'vector', ...source } }, layers: out };
|
|
37
|
+
};
|
|
38
|
+
/** HTML of a file reduced to its text, escaped: credits from an archive's metadata cannot run in the page */
|
|
39
|
+
const plainText = (html) => {
|
|
40
|
+
return html
|
|
41
|
+
.replace(/<[^>]*>/g, '')
|
|
42
|
+
.replace(/&(?!(amp|lt|gt|quot|copy|#\d+);)/g, '&')
|
|
43
|
+
.replace(/</g, '<')
|
|
44
|
+
.replace(/>/g, '>');
|
|
45
|
+
};
|
|
46
|
+
/** @internal `GeoMap.addPmtiles`: a raster layer of the archive's images, or vector tiles with their raster layer and
|
|
47
|
+
* labels */
|
|
48
|
+
export const pmtilesLayer = async (map, url, options) => {
|
|
49
|
+
const archiveUrl = pmtilesUrl(url.startsWith('pmtiles://') ? url : `pmtiles://${url}`) ?? url;
|
|
50
|
+
const archive = pmtiles(archiveUrl);
|
|
51
|
+
const [header, metadata] = await Promise.all([
|
|
52
|
+
archive.header(),
|
|
53
|
+
archive.metadata().catch(() => ({})),
|
|
54
|
+
]);
|
|
55
|
+
const credit = metadata.attribution;
|
|
56
|
+
const attribution = options.attribution ?? (typeof credit === 'string' && credit ? plainText(credit) : undefined);
|
|
57
|
+
const kind = options.kind ?? (header.tileType === 'mvt' ? 'vector' : 'raster');
|
|
58
|
+
const credits = attribution ? { attribution } : {};
|
|
59
|
+
if (kind === 'raster') {
|
|
60
|
+
return map.addRaster({
|
|
61
|
+
url: `pmtiles://${archiveUrl}`,
|
|
62
|
+
minZoom: header.minZoom,
|
|
63
|
+
maxZoom: Math.max(header.maxZoom, 19),
|
|
64
|
+
...(options.opacity === undefined ? {} : { opacity: options.opacity }),
|
|
65
|
+
...(options.size === undefined ? {} : { size: options.size }),
|
|
66
|
+
...(options.before ? { before: options.before } : {}),
|
|
67
|
+
...credits,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const layers = metadata.vector_layers;
|
|
71
|
+
const names = Array.isArray(layers)
|
|
72
|
+
? layers.map((l) => (isRecord(l) ? l.id : undefined)).filter((id) => typeof id === 'string')
|
|
73
|
+
: [];
|
|
74
|
+
const style = options.style ?? basicStyle({ url: `pmtiles://${archiveUrl}` }, names);
|
|
75
|
+
const tiles = map.vectorTiles({ style, ...credits });
|
|
76
|
+
tiles.raster({
|
|
77
|
+
...(options.opacity === undefined ? {} : { opacity: options.opacity }),
|
|
78
|
+
...(options.before ? { before: options.before } : {}),
|
|
79
|
+
});
|
|
80
|
+
tiles.labels();
|
|
81
|
+
return tiles;
|
|
82
|
+
};
|
|
83
|
+
/** A raster loader reading the tiles of a `pmtiles://` archive URL (images of any size, drawn at `size`); undefined
|
|
84
|
+
* for another URL */
|
|
85
|
+
export const pmtilesRasterLoader = (url, size) => {
|
|
86
|
+
const archiveUrl = pmtilesUrl(url);
|
|
87
|
+
if (!archiveUrl)
|
|
88
|
+
return undefined;
|
|
89
|
+
const archive = pmtiles(archiveUrl);
|
|
90
|
+
return async (z, x, y, signal) => {
|
|
91
|
+
const bytes = await archive.tile(z, x, y, signal);
|
|
92
|
+
if (!bytes)
|
|
93
|
+
return null;
|
|
94
|
+
return createImageBitmap(new Blob([bytes]), { resizeWidth: size, resizeHeight: size });
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
//# sourceMappingURL=pmtileslayer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pmtileslayer.js","sourceRoot":"","sources":["../../src/basemap/pmtileslayer.ts"],"names":[],"mappings":"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","sourcesContent":["/** PMTiles archives on the map (`GeoMap.addPmtiles`): their images as a raster layer, or their vector tiles drawn with\n * the style given or a plain one made from their layers' names */\n\nimport type { Layer, RasterLayer } from '../layers/layers.ts';\nimport type { GeoMap } from '../map.ts';\nimport type { RasterOptions } from '../types.ts';\nimport { isRecord } from '../util/guards.ts';\nimport { pmtiles, pmtilesUrl } from './pmtiles.ts';\nimport type { Style, StyleLayer } from './vectorstyle.ts';\nimport type { VectorTiles } from './vectortiles.ts';\n\n/** Colors of the layers of `basicStyle`, in turn */\nconst BASIC_COLORS = ['#2f6fd1', '#d1495b', '#2a9d8f', '#e9a03b', '#7b5ea7', '#5c7a29', '#c8553d', '#3d8eb9'];\n\n/**\n * A plain style for vector tiles known only by their layers' names (a PMTiles archive's `vector_layers`, an MBTiles'\n * metadata): each layer's polygons filled, its lines and outlines stroked and its points as circles, a color per\n * layer, the `name` of points as labels. `source` is the style's vector source (`url`: TileJSON or `pmtiles://…`, or\n * `tiles`)\n */\nexport const basicStyle = (\n source: { url?: string; tiles?: string[]; minzoom?: number; maxzoom?: number },\n layers: readonly string[],\n): Style => {\n const out: StyleLayer[] = [];\n\n layers.forEach((name, i) => {\n const color = BASIC_COLORS[i % BASIC_COLORS.length] ?? '#2f6fd1';\n const base = { source: 'data', 'source-layer': name };\n\n out.push(\n { id: `${name}-fill`, type: 'fill', ...base, paint: { 'fill-color': color, 'fill-opacity': 0.18 } },\n { id: `${name}-line`, type: 'line', ...base, paint: { 'line-color': color, 'line-width': 1.2 } },\n {\n id: `${name}-circle`,\n type: 'circle',\n ...base,\n paint: { 'circle-color': color, 'circle-radius': 3.5, 'circle-stroke-color': '#fff', 'circle-stroke-width': 1 },\n },\n );\n });\n layers.forEach((name) => {\n out.push({\n id: `${name}-label`,\n type: 'symbol',\n source: 'data',\n 'source-layer': name,\n filter: ['all', ['==', ['geometry-type'], 'Point'], ['has', 'name']],\n layout: { 'text-field': ['get', 'name'], 'text-size': 12, 'text-offset': [0, 0.9], 'text-anchor': 'top' },\n paint: { 'text-color': '#1d2433', 'text-halo-color': '#fff', 'text-halo-width': 1.5 },\n });\n });\n\n return { version: 8, sources: { data: { type: 'vector', ...source } }, layers: out };\n};\n\n/** Options of `GeoMap.addPmtiles` */\nexport interface PmtilesOptions {\n /** What the archive holds (default from its header: raster images or vector tiles) */\n kind?: 'raster' | 'vector';\n /** A MapLibre style for vector tiles (its vector source `pmtiles://…`); default `basicStyle` of the archive's layers */\n style?: Style;\n opacity?: number;\n /** Texture size of raster tiles (default 256; 512 for the sharper tiles of a 512 px archive) */\n size?: number;\n /** Credits, HTML (default the archive's `attribution` metadata, as text) */\n attribution?: string;\n before?: Layer;\n}\n\n/** HTML of a file reduced to its text, escaped: credits from an archive's metadata cannot run in the page */\nconst plainText = (html: string): string => {\n return html\n .replace(/<[^>]*>/g, '')\n .replace(/&(?!(amp|lt|gt|quot|copy|#\\d+);)/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>');\n};\n\n/** @internal `GeoMap.addPmtiles`: a raster layer of the archive's images, or vector tiles with their raster layer and\n * labels */\nexport const pmtilesLayer = async (\n map: GeoMap,\n url: string,\n options: PmtilesOptions,\n): Promise<RasterLayer | VectorTiles> => {\n const archiveUrl = pmtilesUrl(url.startsWith('pmtiles://') ? url : `pmtiles://${url}`) ?? url;\n const archive = pmtiles(archiveUrl);\n const [header, metadata] = await Promise.all([\n archive.header(),\n archive.metadata().catch((): Record<string, unknown> => ({})),\n ]);\n const credit = metadata.attribution;\n const attribution = options.attribution ?? (typeof credit === 'string' && credit ? plainText(credit) : undefined);\n const kind = options.kind ?? (header.tileType === 'mvt' ? 'vector' : 'raster');\n const credits = attribution ? { attribution } : {};\n\n if (kind === 'raster') {\n return map.addRaster({\n url: `pmtiles://${archiveUrl}`,\n minZoom: header.minZoom,\n maxZoom: Math.max(header.maxZoom, 19),\n ...(options.opacity === undefined ? {} : { opacity: options.opacity }),\n ...(options.size === undefined ? {} : { size: options.size }),\n ...(options.before ? { before: options.before } : {}),\n ...credits,\n });\n }\n\n const layers = metadata.vector_layers;\n const names = Array.isArray(layers)\n ? layers.map((l) => (isRecord(l) ? l.id : undefined)).filter((id) => typeof id === 'string')\n : [];\n const style = options.style ?? basicStyle({ url: `pmtiles://${archiveUrl}` }, names);\n const tiles = map.vectorTiles({ style, ...credits });\n\n tiles.raster({\n ...(options.opacity === undefined ? {} : { opacity: options.opacity }),\n ...(options.before ? { before: options.before } : {}),\n });\n tiles.labels();\n\n return tiles;\n};\n\n/** A raster loader reading the tiles of a `pmtiles://` archive URL (images of any size, drawn at `size`); undefined\n * for another URL */\nexport const pmtilesRasterLoader = (url: string, size: number): RasterOptions['loader'] => {\n const archiveUrl = pmtilesUrl(url);\n\n if (!archiveUrl) return undefined;\n\n const archive = pmtiles(archiveUrl);\n\n return async (z, x, y, signal) => {\n const bytes = await archive.tile(z, x, y, signal);\n\n if (!bytes) return null;\n\n return createImageBitmap(new Blob([bytes]), { resizeWidth: size, resizeHeight: size });\n };\n};\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The trees of a vector map (`VectorTiles.trees`): a low-poly tree made here as a glTF binary (no file to download),
|
|
3
|
+
* its trunk and its crown flat-shaded in their colors, and the style layers they grow on by default.
|
|
4
|
+
*/
|
|
5
|
+
import type { StyleLayer } from './vectorstyle.ts';
|
|
6
|
+
/** Numbers of a tree in the workers' answer: longitude, latitude, scale, heading, rank */
|
|
7
|
+
export declare const TREE_NUMBERS = 5;
|
|
8
|
+
/** Colors of the trunk and of the crown (linear RGBA, as glTF's `baseColorFactor`) */
|
|
9
|
+
export interface TreeColors {
|
|
10
|
+
trunk: [number, number, number, number];
|
|
11
|
+
crown: [number, number, number, number];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A tree as a glTF binary, about 10 metres high: a trunk and a rounded crown in `colors`, one mesh of two primitives,
|
|
15
|
+
* its front +Z (`ModelInstance.heading` turns it), standing on its origin
|
|
16
|
+
*/
|
|
17
|
+
export declare const treeModel: (colors?: Partial<TreeColors>) => Uint8Array<ArrayBuffer>;
|
|
18
|
+
/**
|
|
19
|
+
* Whether trees grow on a style layer by default: the woods and forests of the style's fills (OpenMapTiles'
|
|
20
|
+
* `landcover` wood, PLAN IGN's forests) and the grass of its parks and gardens; not the wooded relief of PLAN IGN,
|
|
21
|
+
* which repeats its forests, nor OpenMapTiles' `park` layer (protected areas and historic sites: a river's banks
|
|
22
|
+
* among them)
|
|
23
|
+
*/
|
|
24
|
+
export declare const isWoodland: (layer: StyleLayer) => boolean;
|
|
25
|
+
/** Default spacing of the trees of a layer (metres): close in woods and forests, apart on grass */
|
|
26
|
+
export declare const treeSpacing: (layer: StyleLayer) => number;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The trees of a vector map (`VectorTiles.trees`): a low-poly tree made here as a glTF binary (no file to download),
|
|
3
|
+
* its trunk and its crown flat-shaded in their colors, and the style layers they grow on by default.
|
|
4
|
+
*/
|
|
5
|
+
/** Numbers of a tree in the workers' answer: longitude, latitude, scale, heading, rank */
|
|
6
|
+
export const TREE_NUMBERS = 5;
|
|
7
|
+
/** Sides of the trunk and of the crown */
|
|
8
|
+
const SIDES = 7;
|
|
9
|
+
const DEFAULT_COLORS = {
|
|
10
|
+
trunk: [0.2, 0.12, 0.07, 1],
|
|
11
|
+
crown: [0.12, 0.27, 0.08, 1],
|
|
12
|
+
};
|
|
13
|
+
/** A solid of revolution around the vertical (glTF +Y up), flat-shaded: rings of (height, radius), a point at the
|
|
14
|
+
* top when its radius is 0; the triangles' positions and normals, one normal per face */
|
|
15
|
+
const revolve = (rings) => {
|
|
16
|
+
const at = (k, [h, r]) => {
|
|
17
|
+
const a = (k / SIDES) * Math.PI * 2;
|
|
18
|
+
return [Math.cos(a) * r, h, Math.sin(a) * r];
|
|
19
|
+
};
|
|
20
|
+
const positions = [];
|
|
21
|
+
const normals = [];
|
|
22
|
+
const face = (a, b, c) => {
|
|
23
|
+
const u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
|
24
|
+
const v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
|
25
|
+
const n = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
|
|
26
|
+
const length = Math.hypot(...n);
|
|
27
|
+
// A degenerate face (two points at the apex) is left out
|
|
28
|
+
if (length === 0)
|
|
29
|
+
return;
|
|
30
|
+
positions.push(...a, ...b, ...c);
|
|
31
|
+
[0, 1, 2].forEach(() => {
|
|
32
|
+
normals.push(n[0] / length, n[1] / length, n[2] / length);
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
rings.slice(1).forEach((ring, i) => {
|
|
36
|
+
const below = rings[i] ?? ring;
|
|
37
|
+
Array.from({ length: SIDES }, (_, k) => k).forEach((k) => {
|
|
38
|
+
const [a, b] = [at(k, below), at(k + 1, below)];
|
|
39
|
+
const [c, d] = [at(k, ring), at(k + 1, ring)];
|
|
40
|
+
// Counter-clockwise seen from outside
|
|
41
|
+
face(a, c, b);
|
|
42
|
+
face(b, c, d);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
// The bottom closed: seen from below
|
|
46
|
+
const base = rings[0];
|
|
47
|
+
if (base && base[1] > 0) {
|
|
48
|
+
Array.from({ length: SIDES }, (_, k) => k).forEach((k) => {
|
|
49
|
+
face([0, base[0], 0], at(k, base), at(k + 1, base));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return { positions, normals };
|
|
53
|
+
};
|
|
54
|
+
/** Lowest and highest of interleaved x, y, z (glTF needs them on positions) */
|
|
55
|
+
const bounds = (xyz) => {
|
|
56
|
+
const axes = [0, 1, 2].map((a) => xyz.filter((_, i) => i % 3 === a));
|
|
57
|
+
return { min: axes.map((v) => Math.min(...v)), max: axes.map((v) => Math.max(...v)) };
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* A tree as a glTF binary, about 10 metres high: a trunk and a rounded crown in `colors`, one mesh of two primitives,
|
|
61
|
+
* its front +Z (`ModelInstance.heading` turns it), standing on its origin
|
|
62
|
+
*/
|
|
63
|
+
export const treeModel = (colors = {}) => {
|
|
64
|
+
const { trunk, crown } = { ...DEFAULT_COLORS, ...colors };
|
|
65
|
+
const parts = [
|
|
66
|
+
revolve([
|
|
67
|
+
[0, 0.28],
|
|
68
|
+
[3.2, 0.2],
|
|
69
|
+
]),
|
|
70
|
+
revolve([
|
|
71
|
+
[2.4, 0.6],
|
|
72
|
+
[3.6, 2.4],
|
|
73
|
+
[5.6, 2.9],
|
|
74
|
+
[7.8, 2.3],
|
|
75
|
+
[9.4, 1.1],
|
|
76
|
+
[10.2, 0],
|
|
77
|
+
]),
|
|
78
|
+
];
|
|
79
|
+
// The buffer: each part's positions then normals, 4-byte floats
|
|
80
|
+
const floats = parts.flatMap((p) => [...p.positions, ...p.normals]);
|
|
81
|
+
const bin = new Float32Array(floats);
|
|
82
|
+
const views = parts.flatMap((p, i) => {
|
|
83
|
+
const before = parts.slice(0, i).reduce((n, q) => n + (q.positions.length + q.normals.length) * 4, 0);
|
|
84
|
+
return [
|
|
85
|
+
{ buffer: 0, byteOffset: before, byteLength: p.positions.length * 4, target: 34962 },
|
|
86
|
+
{ buffer: 0, byteOffset: before + p.positions.length * 4, byteLength: p.normals.length * 4, target: 34962 },
|
|
87
|
+
];
|
|
88
|
+
});
|
|
89
|
+
const accessors = parts.flatMap((p, i) => [
|
|
90
|
+
{ bufferView: i * 2, componentType: 5126, count: p.positions.length / 3, type: 'VEC3', ...bounds(p.positions) },
|
|
91
|
+
{ bufferView: i * 2 + 1, componentType: 5126, count: p.normals.length / 3, type: 'VEC3' },
|
|
92
|
+
]);
|
|
93
|
+
const gltf = {
|
|
94
|
+
asset: { version: '2.0', generator: 'isard' },
|
|
95
|
+
scene: 0,
|
|
96
|
+
scenes: [{ nodes: [0] }],
|
|
97
|
+
nodes: [{ mesh: 0 }],
|
|
98
|
+
meshes: [
|
|
99
|
+
{
|
|
100
|
+
primitives: parts.map((_, i) => ({ attributes: { POSITION: i * 2, NORMAL: i * 2 + 1 }, material: i })),
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
materials: [trunk, crown].map((color) => ({
|
|
104
|
+
pbrMetallicRoughness: { baseColorFactor: color, metallicFactor: 0, roughnessFactor: 1 },
|
|
105
|
+
})),
|
|
106
|
+
accessors,
|
|
107
|
+
bufferViews: views,
|
|
108
|
+
buffers: [{ byteLength: bin.byteLength }],
|
|
109
|
+
};
|
|
110
|
+
// The chunks padded to 4 bytes: the JSON with spaces, the binary with zeros
|
|
111
|
+
const json = new TextEncoder().encode(JSON.stringify(gltf));
|
|
112
|
+
const jsonLength = Math.ceil(json.length / 4) * 4;
|
|
113
|
+
const binLength = Math.ceil(bin.byteLength / 4) * 4;
|
|
114
|
+
const out = new Uint8Array(12 + 8 + jsonLength + 8 + binLength);
|
|
115
|
+
const view = new DataView(out.buffer);
|
|
116
|
+
view.setUint32(0, 0x46546c67, true);
|
|
117
|
+
view.setUint32(4, 2, true);
|
|
118
|
+
view.setUint32(8, out.length, true);
|
|
119
|
+
view.setUint32(12, jsonLength, true);
|
|
120
|
+
view.setUint32(16, 0x4e4f534a, true);
|
|
121
|
+
out.fill(0x20, 20, 20 + jsonLength);
|
|
122
|
+
out.set(json, 20);
|
|
123
|
+
view.setUint32(20 + jsonLength, binLength, true);
|
|
124
|
+
view.setUint32(24 + jsonLength, 0x004e4942, true);
|
|
125
|
+
out.set(new Uint8Array(bin.buffer), 28 + jsonLength);
|
|
126
|
+
return out;
|
|
127
|
+
};
|
|
128
|
+
/** Whether a style layer is OpenMapTiles' grass (the lawns of parks and gardens among it) */
|
|
129
|
+
const isGrass = (layer) => layer['source-layer'] === 'landcover' && /grass/i.test(layer.id);
|
|
130
|
+
/**
|
|
131
|
+
* Whether trees grow on a style layer by default: the woods and forests of the style's fills (OpenMapTiles'
|
|
132
|
+
* `landcover` wood, PLAN IGN's forests) and the grass of its parks and gardens; not the wooded relief of PLAN IGN,
|
|
133
|
+
* which repeats its forests, nor OpenMapTiles' `park` layer (protected areas and historic sites: a river's banks
|
|
134
|
+
* among them)
|
|
135
|
+
*/
|
|
136
|
+
export const isWoodland = (layer) => layer.type === 'fill' &&
|
|
137
|
+
layer['source-layer'] !== 'oro_relief' &&
|
|
138
|
+
(/wood|forest|for[eê]t/i.test(layer.id) || isGrass(layer));
|
|
139
|
+
/** Default spacing of the trees of a layer (metres): close in woods and forests, apart on grass */
|
|
140
|
+
export const treeSpacing = (layer) => (isGrass(layer) ? 25 : 9);
|
|
141
|
+
//# sourceMappingURL=trees.js.map
|