@sakuzu/maplibre-gl-draw 2.0.0 → 2.1.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 CHANGED
@@ -6,6 +6,100 @@ the project follows semantic versioning.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.1.0] - 2026-10-01
10
+
11
+ The standard UI package, the documentation site with twenty examples and
12
+ the playground on it, the event `options.changed`,
13
+ `Dataset.getStyleRule()`, and fixes to the order of the layers after a
14
+ diffed `setStyle`, to a drag across the antimeridian on the globe, to the
15
+ `/geometry` functions given a feature, and to `scaleWithZoom` changed at
16
+ runtime. Nothing changes incompatibly.
17
+
18
+ ### Added in 2.1.0
19
+
20
+ - The event `options.changed`, with `{ options, previous }`: what
21
+ `draw.options.get()` returns after and before a `draw.options.update`
22
+ that changed a value. An update that changes nothing fires nothing,
23
+ and the event is not a change of the document, so it fires no
24
+ `document.changed`.
25
+ - Twenty examples built with the standard UI, each with its page
26
+ on the site: `get-started`, `style-features`, `feature-properties`,
27
+ `layers-and-groups`, `style-rules-and-legend`, `snapping-and-tracing`,
28
+ `geometry-operations`, `images`, `editing-shapes`, `zoom-and-scale`,
29
+ `save-and-load`, `terrain`, `globe`, `200000-features`, `datasets`,
30
+ `columnar-data-in-a-worker`, `read-only-viewer`, `plugins`,
31
+ `custom-feature-types` and `custom-ui`. Their basemap menu offers the
32
+ OpenFreeMap styles.
33
+ - A sample of Overture Maps data for the examples, in
34
+ `examples/public/data/` (not in the package): the 10,477 buildings and
35
+ the 17,558 places of central Tokyo east of the station, as GeoJSON and
36
+ GeoParquet. `npm run data:overture` (`scripts/fetch-overture-sample.mjs`)
37
+ writes them again. The buildings are under the ODbL and the places
38
+ under the CDLA Permissive 2.0;
39
+ `THIRD_PARTY_NOTICES.md` and `examples/public/data/README.md` record
40
+ the attribution and the licenses of every source. The `datasets`,
41
+ `columnar-data-in-a-worker` and `style-rules-and-legend` examples show
42
+ them.
43
+ - `Dataset.getStyleRule()`, the style rule of a dataset: the
44
+ one last given to `setStyleRule`, or the one given to `datasets.add`,
45
+ or `undefined` when there is none. A legend of the datasets reads it.
46
+
47
+ ### Changed in 2.1.0
48
+
49
+ - The site (<https://sakuzu.github.io/maplibre-gl-draw/>) is
50
+ the documentation site: getting started and the guides in English and
51
+ Japanese, a gallery of the examples, each on a page that runs it
52
+ beside its code, the playground under `/playground/` and the API
53
+ reference under `/api/`, which opens with where each resource of
54
+ `Draw` is. The examples with buttons of their own (`basic`,
55
+ `save-load`, `style-rules`, `snapping-and-geometry`, `read-only`,
56
+ `plugin`, `custom-feature-type`, `large-data` and `table-worker`) are
57
+ replaced by the examples of the gallery, and their addresses, like
58
+ those of the HTML pages of the API reference of 1.0 and 2.0, redirect
59
+ to the pages that took their place. `npm run site:dev` serves the site
60
+ with the examples and the playground for a local check.
61
+ - The playground is built on the standard UI, with the tools
62
+ and the inspector sections of a plugin and of a custom feature type,
63
+ and the switches that are not tools on Shift and a letter. It opens on
64
+ an overview of every look one layer can hold; `?plain` opens it empty.
65
+ - The dev server of the examples (`npm run dev`) moves from
66
+ port 3000 to 3200, and that of the playground
67
+ (`npm run dev:playground`) from 3001 to 3300. They and
68
+ `npm run test:e2e` need the standard UI built first
69
+ (`npm run ui:build`).
70
+ - No sample geometry of the examples, the scenes of the
71
+ playground or the development page of the standard UI lies over the
72
+ Imperial Palace, its East Gardens or Kitanomaru Park. Each page keeps
73
+ its data in a `data.ts` of its own, and a test walks all of it and
74
+ fails when anything reaches into that area.
75
+
76
+ ### Fixed in 2.1.0
77
+
78
+ - After `map.setStyle` in its default diff mode, the drawing is
79
+ on top of the map again. maplibre leaves custom layers out of the
80
+ style it diffs, so the layers of the new style were added above the
81
+ drawing and hid it. The layers of the instance now move back on top on
82
+ every `style.load`, in their order, with the map layers placed between
83
+ them; a full `setStyle` (`diff: false`) works as before.
84
+ - `draw.options.update({ scaleWithZoom })` reaches the drawing
85
+ modes at once. They kept the value given to `createDraw`, so a feature
86
+ drawn after the option was turned off still got a reference zoom
87
+ (`maplibre-gl-draw:createdZoom`), and one drawn after it was turned on
88
+ got none.
89
+ - The functions of `/geometry` take a feature of the drawing as it
90
+ is, as documented. A feature whose `type` names a geometry type
91
+ (`Polygon`, `LineString` and so on) was read as a geometry and threw a
92
+ `GeometryError` (`invalid-input`); an object with a `geometry` member
93
+ that is a geometry is now read as a feature first, whatever its own
94
+ `type`.
95
+ - On the globe, a freehand stroke drawn across the antimeridian is
96
+ drawn as the stroke alone. The globe gives the pointer longitudes in
97
+ [-180, 180], so where the pointer crossed the line (or slid along the
98
+ edge of the sphere past it) the next point of the stroke was 360
99
+ degrees from the one before, and a line went once round the globe
100
+ along the parallel. The coordinates of a press now run on past 180 as
101
+ they do on the flat map.
102
+
9
103
  ## [2.0.0] - 2026-09-30
10
104
 
11
105
  2.0.0 redesigns the public API around resources and their collections.
@@ -1052,6 +1146,5 @@ available from Kasika, Inc.
1052
1146
  - A read-only mode and an interaction lock.
1053
1147
  - Plugins, custom modes and custom feature types.
1054
1148
 
1055
- [Unreleased]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.0.0...HEAD
1056
1149
  [2.0.0]: https://github.com/sakuzu/maplibre-gl-draw/compare/v1.0.0...v2.0.0
1057
1150
  [1.0.0]: https://github.com/sakuzu/maplibre-gl-draw/releases/tag/v1.0.0
package/README.ja.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  このページの正は英語版 ([README.md](README.md)) です。
12
12
 
13
- ![東京駅周辺の playground。半透明で重なる円、細い線から太い線、破線と点線、枠と頂点ハンドルが付いた選択中の穴あき多角形、マルチポリゴン、円と四角と三角と星のマーカー、画像を囲むフリーハンドの線、不透明度を下げたレイヤー、カテゴリーのスタイル規則で塗り分けた区画、段階区分の規則で塗り分けた細かい六角形と凡例、レイヤーとグループの階層を示すレイヤーパネル](docs/images/overview.jpg)
13
+ ![東京駅周辺の playground。半透明で重なる円、細い線から太い線、破線と点線、輪郭が実線と破線と点線の面、枠と頂点ハンドルが付いた選択中の穴あき多角形、マルチポリゴン、円と四角と三角と星のマーカー、画像を囲むフリーハンドの線、不透明度を下げたレイヤー、カテゴリーのスタイル規則で塗り分けた区画、段階区分の規則で塗り分けた細かい六角形、レイヤーとグループと六角形のデータセットを並べたレイヤーのタブ (隣の凡例のタブに 2 つの規則が出ます)、選んだ多角形のインスペクター](docs/images/overview.jpg)
14
14
 
15
15
  ## 機能
16
16
 
@@ -122,28 +122,36 @@ GeoJSON の properties を持つので、ファイルはコードで読む地物
122
122
  ブラウザーですぐに試せます。インストールは要りません。
123
123
 
124
124
  - [プレイグラウンド][demo]
125
- - すべての機能を 1 つの画面で。レイヤーとプロパティのパネル付き
126
- - [基本][ex-basic]
127
- - 面の描画、変更のイベント、保存と復元
128
- - [書き出しと読み込み][ex-save-load]
129
- - GeoJSON と独自形式の書き出し、地図に落としたファイルの読み込み
130
- - [スタイル規則][ex-style-rules]
125
+ - すべての機能を 1 つの画面で。標準の UI 付き
126
+ - [例][examples]
127
+ - 20 の例。どれも、例を動かすページにそのコードを添えています
128
+
129
+ 例の一部を挙げます。
130
+
131
+ - [Get started][ex-get-started]
132
+ - 地物を描き、選び、パネルで直す
133
+ - [Save and load][ex-save-and-load]
134
+ - GeoJSON と独自形式、地図に落としたファイル、読み込みで外された地物
135
+ - [Style rules and legend][ex-style-rules-and-legend]
131
136
  - 属性の値で色を分ける 4 種類の規則と凡例
132
- - [吸着と幾何演算][ex-snapping-and-geometry]
133
- - 吸着、境界のなぞり、共有する頂点の同時移動、結合、切り抜き、
134
- バッファ、分割
135
- - [3D 地形][ex-terrain]
136
- - 3D 地形の上での描画と選択
137
- - [閲覧専用][ex-read-only]
138
- - 閲覧専用モード、操作のロック、ロックしたレイヤー
139
- - [プラグイン][ex-plugin]
140
- - イベント、API、独自のモードを持つプラグイン
141
- - [独自の地物型][ex-custom-feature-type]
137
+ - [Snapping and tracing][ex-snapping-and-tracing] と
138
+ [Geometry operations][ex-geometry-operations]
139
+ - 吸着、境界のなぞり、結合、交差、差、分割、バッファー
140
+ - [Terrain][ex-terrain]
141
+ - 3D 地形の上での描画と編集
142
+ - [Read-only viewer][ex-read-only-viewer]
143
+ - 見るための描画。閲覧専用、操作のロック、クリックした地物の属性
144
+ - [Plugins][ex-plugins]
145
+ - 独自のモードを持つプラグインと、標準の UI に足すその道具
146
+ - [Custom feature types][ex-custom-feature-types]
142
147
  - 独自の描き方、当たり判定、範囲選択を持つ地物の型
143
- - [データセット][ex-large-data]
144
- - 5 万のマス目の色分けと、見えている範囲の点の取り寄せ
145
- - [100 万の点][ex-table-worker]
146
- - Worker で読み込む 100 万の点
148
+ - [Datasets][ex-datasets]
149
+ - Overture Maps の東京都心の建物と場所を描画の下と上に。場所は
150
+ 見えている範囲の分を取り寄せる
151
+ - [Columnar data in a Worker][ex-columnar-data-in-a-worker]
152
+ - 同じ建物を Worker で GeoParquet から読み込み、列のまま描く
153
+ - [Build your own UI][ex-custom-ui]
154
+ - 標準の UI を使わない、自分の道具のバーとパネル
147
155
 
148
156
  ## インストール
149
157
 
@@ -191,8 +199,9 @@ document.querySelector('#save')?.addEventListener('click', () => {
191
199
  });
192
200
  ```
193
201
 
194
- ページ全体は [examples/basic/](examples/basic/) にあります。
195
- [はじめかた](docs/getting-started.ja.md) で順を追って説明しています。
202
+ [はじめかた](docs/getting-started.ja.md) では、このページを順を追って
203
+ 作ります。[Get started][ex-get-started] の例は、自分のボタンの代わりに
204
+ 標準の UI を地図に重ねます。
196
205
 
197
206
  ## 入口
198
207
 
@@ -231,7 +240,8 @@ document.querySelector('#save')?.addEventListener('click', () => {
231
240
  [docs/README.ja.md](docs/README.ja.md) に、はじめかた、手引き、
232
241
  リファレンス、内部の文書を読む順に並べています。1.0、mapbox-gl-draw、
233
242
  terra-draw から移る場合は [移行](docs/guides/migrating.ja.md) を参照して
234
- ください。
243
+ ください。[API リファレンス][api] のメインのエントリーのページは、
244
+ インスタンスの資源とそれぞれのメソッドの一覧から始まります。
235
245
 
236
246
  ## 開発に参加する
237
247
 
@@ -254,15 +264,18 @@ AGPL が製品に合わない場合は、Kasika, Inc. (可視化技研株式会
254
264
 
255
265
  [maplibre]: https://maplibre.org/maplibre-gl-js/docs/
256
266
  [custom-layer]: https://maplibre.org/maplibre-gl-js/docs/API/interfaces/CustomLayerInterface/
257
- [demo]: https://sakuzu.github.io/maplibre-gl-draw/
267
+ [demo]: https://sakuzu.github.io/maplibre-gl-draw/playground/
258
268
  [api]: https://sakuzu.github.io/maplibre-gl-draw/api/
259
- [ex-basic]: https://sakuzu.github.io/maplibre-gl-draw/examples/basic/
260
- [ex-save-load]: https://sakuzu.github.io/maplibre-gl-draw/examples/save-load/
261
- [ex-style-rules]: https://sakuzu.github.io/maplibre-gl-draw/examples/style-rules/
262
- [ex-snapping-and-geometry]: https://sakuzu.github.io/maplibre-gl-draw/examples/snapping-and-geometry/
263
- [ex-terrain]: https://sakuzu.github.io/maplibre-gl-draw/examples/terrain/
264
- [ex-read-only]: https://sakuzu.github.io/maplibre-gl-draw/examples/read-only/
265
- [ex-plugin]: https://sakuzu.github.io/maplibre-gl-draw/examples/plugin/
266
- [ex-custom-feature-type]: https://sakuzu.github.io/maplibre-gl-draw/examples/custom-feature-type/
267
- [ex-large-data]: https://sakuzu.github.io/maplibre-gl-draw/examples/large-data/
268
- [ex-table-worker]: https://sakuzu.github.io/maplibre-gl-draw/examples/table-worker/
269
+ [examples]: https://sakuzu.github.io/maplibre-gl-draw/ja/examples/
270
+ [ex-get-started]: https://sakuzu.github.io/maplibre-gl-draw/ja/examples/get-started.html
271
+ [ex-save-and-load]: https://sakuzu.github.io/maplibre-gl-draw/ja/examples/save-and-load.html
272
+ [ex-style-rules-and-legend]: https://sakuzu.github.io/maplibre-gl-draw/ja/examples/style-rules-and-legend.html
273
+ [ex-snapping-and-tracing]: https://sakuzu.github.io/maplibre-gl-draw/ja/examples/snapping-and-tracing.html
274
+ [ex-geometry-operations]: https://sakuzu.github.io/maplibre-gl-draw/ja/examples/geometry-operations.html
275
+ [ex-terrain]: https://sakuzu.github.io/maplibre-gl-draw/ja/examples/terrain.html
276
+ [ex-read-only-viewer]: https://sakuzu.github.io/maplibre-gl-draw/ja/examples/read-only-viewer.html
277
+ [ex-plugins]: https://sakuzu.github.io/maplibre-gl-draw/ja/examples/plugins.html
278
+ [ex-custom-feature-types]: https://sakuzu.github.io/maplibre-gl-draw/ja/examples/custom-feature-types.html
279
+ [ex-datasets]: https://sakuzu.github.io/maplibre-gl-draw/ja/examples/datasets.html
280
+ [ex-columnar-data-in-a-worker]: https://sakuzu.github.io/maplibre-gl-draw/ja/examples/columnar-data-in-a-worker.html
281
+ [ex-custom-ui]: https://sakuzu.github.io/maplibre-gl-draw/ja/examples/custom-ui.html
package/README.md CHANGED
@@ -8,7 +8,7 @@ it does on a flat map.
8
8
  [Demos][demo] | [Documentation](docs/README.md) | [API reference][api] |
9
9
  [日本語](README.ja.md)
10
10
 
11
- ![The playground over central Tokyo: overlapping translucent circles, lines from thin to thick, dashed and dotted lines, a polygon with a hole selected with its frame and vertex handles, a multipolygon, circle, square, triangle and star markers, a freehand loop around an image, a faded layer, parcels colored by a categorical rule, a fine hexagon grid colored by a graduated rule, a legend, and the Layers panel showing layers and groups](docs/images/overview.jpg)
11
+ ![The playground over central Tokyo: overlapping translucent circles, lines from thin to thick, dashed and dotted lines, areas with a solid, a dashed and a dotted outline, a polygon with a hole selected with its frame and vertex handles, a multipolygon, circle, square, triangle and star markers, a freehand loop around an image, a faded layer, parcels colored by a categorical rule, a fine hexagon grid colored by a graduated rule, the Layers tab listing the layers, the groups and the hexagon dataset (the Legend tab beside it lists both rules), and the inspector of the selected polygon](docs/images/overview.jpg)
12
12
 
13
13
  ## Features
14
14
 
@@ -126,31 +126,41 @@ returns ([plugins](docs/guides/plugins.md),
126
126
  Try them in the browser, with nothing to install.
127
127
 
128
128
  - [Playground][demo]
129
- - Every feature in one editor, with layer and property panels
130
- - [Basic][ex-basic]
131
- - Drawing polygons, the change events, saving and restoring
132
- - [Export and load][ex-save-load]
133
- - Export as GeoJSON and in the native format, loading files dropped on
134
- the map
135
- - [Style rules][ex-style-rules]
129
+ - Every feature in one editor, with the standard UI
130
+ - [Examples][examples]
131
+ - Twenty examples, each on a page that runs it beside its code
132
+
133
+ Among the examples:
134
+
135
+ - [Get started][ex-get-started]
136
+ - Drawing, selecting and changing features in the panel
137
+ - [Save and load][ex-save-and-load]
138
+ - GeoJSON and the native format, files dropped on the map, the
139
+ features a load leaves out
140
+ - [Style rules and legend][ex-style-rules-and-legend]
136
141
  - The four kinds of style rule that color features by a property, with
137
142
  a legend
138
- - [Snapping and geometry][ex-snapping-and-geometry]
139
- - Snapping, tracing a boundary, moving shared vertices, union,
140
- subtract, buffer and split
141
- - [3D terrain][ex-terrain]
142
- - Drawing and selecting on 3D terrain
143
- - [Read-only][ex-read-only]
144
- - The read-only mode, the interaction lock and locked layers
145
- - [Plugin][ex-plugin]
146
- - A plugin with an event, an API and a mode of its own
147
- - [Custom feature type][ex-custom-feature-type]
143
+ - [Snapping and tracing][ex-snapping-and-tracing] and
144
+ [geometry operations][ex-geometry-operations]
145
+ - Snapping, tracing a boundary, union, intersection, difference, split
146
+ and buffer
147
+ - [Terrain][ex-terrain]
148
+ - Drawing and editing on 3D terrain
149
+ - [Read-only viewer][ex-read-only-viewer]
150
+ - A drawing to look at: read-only, the interaction lock, the attributes
151
+ of the feature clicked
152
+ - [Plugins][ex-plugins]
153
+ - A plugin with a mode of its own, and its tool in the standard UI
154
+ - [Custom feature types][ex-custom-feature-types]
148
155
  - A kind of feature with its own drawing, hit test and box selection
149
- - [Datasets][ex-large-data]
150
- - 50,000 cells colored by a property, and points fetched for the part
151
- of the map in view
152
- - [A million points][ex-table-worker]
153
- - A million points read in a Worker
156
+ - [Datasets][ex-datasets]
157
+ - The buildings and places of central Tokyo from Overture Maps, under
158
+ and over the drawing, with places fetched for the part in view
159
+ - [Columnar data in a Worker][ex-columnar-data-in-a-worker]
160
+ - The same buildings read from GeoParquet in a Worker and drawn from
161
+ their columns
162
+ - [Build your own UI][ex-custom-ui]
163
+ - A toolbar and a panel of your own, without the standard UI
154
164
 
155
165
  ## Installation
156
166
 
@@ -198,8 +208,9 @@ document.querySelector('#save')?.addEventListener('click', () => {
198
208
  });
199
209
  ```
200
210
 
201
- The complete page is [examples/basic/](examples/basic/), and
202
- [getting started](docs/getting-started.md) walks through it step by step.
211
+ [Getting started](docs/getting-started.md) builds this page step by
212
+ step, and the [Get started][ex-get-started] example lays the standard UI
213
+ over the map in place of your own buttons.
203
214
 
204
215
  ## Entry points
205
216
 
@@ -239,6 +250,8 @@ Import from the main entry unless you need one of the others.
239
250
  [docs/README.md](docs/README.md) lists every document in reading order:
240
251
  getting started, the guides, the reference and the internals. Moving from
241
252
  1.0, mapbox-gl-draw or terra-draw? See [migrating](docs/guides/migrating.md).
253
+ The page of the main entry in the [API reference][api] opens with a list
254
+ of the resources of the instance and the methods each one has.
242
255
 
243
256
  ## Contributing
244
257
 
@@ -261,15 +274,18 @@ The notices of the third-party code this package contains are in
261
274
 
262
275
  [maplibre]: https://maplibre.org/maplibre-gl-js/docs/
263
276
  [custom-layer]: https://maplibre.org/maplibre-gl-js/docs/API/interfaces/CustomLayerInterface/
264
- [demo]: https://sakuzu.github.io/maplibre-gl-draw/
277
+ [demo]: https://sakuzu.github.io/maplibre-gl-draw/playground/
265
278
  [api]: https://sakuzu.github.io/maplibre-gl-draw/api/
266
- [ex-basic]: https://sakuzu.github.io/maplibre-gl-draw/examples/basic/
267
- [ex-save-load]: https://sakuzu.github.io/maplibre-gl-draw/examples/save-load/
268
- [ex-style-rules]: https://sakuzu.github.io/maplibre-gl-draw/examples/style-rules/
269
- [ex-snapping-and-geometry]: https://sakuzu.github.io/maplibre-gl-draw/examples/snapping-and-geometry/
270
- [ex-terrain]: https://sakuzu.github.io/maplibre-gl-draw/examples/terrain/
271
- [ex-read-only]: https://sakuzu.github.io/maplibre-gl-draw/examples/read-only/
272
- [ex-plugin]: https://sakuzu.github.io/maplibre-gl-draw/examples/plugin/
273
- [ex-custom-feature-type]: https://sakuzu.github.io/maplibre-gl-draw/examples/custom-feature-type/
274
- [ex-large-data]: https://sakuzu.github.io/maplibre-gl-draw/examples/large-data/
275
- [ex-table-worker]: https://sakuzu.github.io/maplibre-gl-draw/examples/table-worker/
279
+ [examples]: https://sakuzu.github.io/maplibre-gl-draw/examples/
280
+ [ex-get-started]: https://sakuzu.github.io/maplibre-gl-draw/examples/get-started.html
281
+ [ex-save-and-load]: https://sakuzu.github.io/maplibre-gl-draw/examples/save-and-load.html
282
+ [ex-style-rules-and-legend]: https://sakuzu.github.io/maplibre-gl-draw/examples/style-rules-and-legend.html
283
+ [ex-snapping-and-tracing]: https://sakuzu.github.io/maplibre-gl-draw/examples/snapping-and-tracing.html
284
+ [ex-geometry-operations]: https://sakuzu.github.io/maplibre-gl-draw/examples/geometry-operations.html
285
+ [ex-terrain]: https://sakuzu.github.io/maplibre-gl-draw/examples/terrain.html
286
+ [ex-read-only-viewer]: https://sakuzu.github.io/maplibre-gl-draw/examples/read-only-viewer.html
287
+ [ex-plugins]: https://sakuzu.github.io/maplibre-gl-draw/examples/plugins.html
288
+ [ex-custom-feature-types]: https://sakuzu.github.io/maplibre-gl-draw/examples/custom-feature-types.html
289
+ [ex-datasets]: https://sakuzu.github.io/maplibre-gl-draw/examples/datasets.html
290
+ [ex-columnar-data-in-a-worker]: https://sakuzu.github.io/maplibre-gl-draw/examples/columnar-data-in-a-worker.html
291
+ [ex-custom-ui]: https://sakuzu.github.io/maplibre-gl-draw/examples/custom-ui.html
@@ -273,3 +273,32 @@ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
273
273
  TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
274
274
  THIS SOFTWARE.
275
275
  ```
276
+
277
+ ---
278
+
279
+ ## Sample data of the examples (Overture Maps)
280
+
281
+ The examples of the repository (`examples/`, not part of the package)
282
+ show buildings and places of central Tokyo from Overture Maps, in
283
+ `examples/public/data/`. `scripts/fetch-overture-sample.mjs` extracts
284
+ them, and `examples/public/data/README.md` records the release, the
285
+ extent, the counts and the sources of the rows.
286
+
287
+ Attribution: © OpenStreetMap contributors, Overture Maps Foundation.
288
+
289
+ - The buildings (`tokyo-buildings.geojson` and `tokyo-buildings.parquet`)
290
+ are a database derived from Overture's buildings theme, under the Open
291
+ Database License 1.0 (ODbL): <https://opendatacommons.org/licenses/odbl/1-0/>.
292
+ Its rows come from OpenStreetMap and Microsoft's Global ML Building
293
+ Footprints (ODbL), and from Qian Shi, et al., A First High-quality
294
+ Vector Data of Buildings in East Asian Countries Based on a
295
+ Comprehensive Large-scale Mapping Framework, Zenodo,
296
+ doi:10.5281/zenodo.8174931 (CC BY 4.0)
297
+ - The places (`tokyo-places.geojson`) come from Overture's places theme,
298
+ under the Community Data License Agreement Permissive 2.0:
299
+ <https://cdla.dev/permissive-2-0/>. Rows from Foursquare are under the
300
+ Apache License 2.0 (Copyright 2024 Foursquare Labs, Inc.), and rows from
301
+ AllThePlaces under CC0 1.0
302
+
303
+ The terms of every source are on
304
+ <https://docs.overturemaps.org/attribution/>.
@@ -154,6 +154,11 @@ export interface Dataset {
154
154
  setTable(table: Table | PreparedTable): void;
155
155
  /** Replaces the style rule; `undefined` removes it. */
156
156
  setStyleRule(rule: StyleRule | undefined): void;
157
+ /**
158
+ * The style rule, or `undefined` when none is set: the rule last given to `setStyleRule`, or
159
+ * the one given to `datasets.add` when it was not replaced.
160
+ */
161
+ getStyleRule(): StyleRule | undefined;
157
162
  /** Replaces the scale factor and the opacity by zoom; `null` removes them. */
158
163
  setZoomScale(zoomScale: DatasetZoomScale | null): void;
159
164
  /** The scale factor and the opacity by zoom, or `null` when none is set. */
@@ -123,10 +123,12 @@ export interface Draw {
123
123
  /**
124
124
  * Puts a draw instance on a map and returns it.
125
125
  *
126
- * It can be called before, while or after the map loads. The layers are added as soon as the
127
- * style accepts them, and added again after `setStyle`. Unless `initDefaultLayer: false` is
128
- * given, the document starts with one empty layer. Several instances can share a page, each on
129
- * its own map.
126
+ * It can be called before, while or after the map loads. The layers are added on top of the map
127
+ * as soon as the style accepts them. After `setStyle`, with or without `diff`, they are on top
128
+ * again: added again after a full replacement, and moved above the layers of the new style after
129
+ * a diff. A layer added to the map later goes above them and stays there until the style changes.
130
+ * Unless `initDefaultLayer: false` is given, the document starts with one empty layer. Several
131
+ * instances can share a page, each on its own map.
130
132
  *
131
133
  * @param map - The map to draw on
132
134
  * @param options - The options; every one can be omitted
package/dist/api/draw.js CHANGED
@@ -4,10 +4,12 @@ import { createDraw as createDrawInstance } from './impl/create-draw.js';
4
4
  /**
5
5
  * Puts a draw instance on a map and returns it.
6
6
  *
7
- * It can be called before, while or after the map loads. The layers are added as soon as the
8
- * style accepts them, and added again after `setStyle`. Unless `initDefaultLayer: false` is
9
- * given, the document starts with one empty layer. Several instances can share a page, each on
10
- * its own map.
7
+ * It can be called before, while or after the map loads. The layers are added on top of the map
8
+ * as soon as the style accepts them. After `setStyle`, with or without `diff`, they are on top
9
+ * again: added again after a full replacement, and moved above the layers of the new style after
10
+ * a diff. A layer added to the map later goes above them and stays there until the style changes.
11
+ * Unless `initDefaultLayer: false` is given, the document starts with one empty layer. Several
12
+ * instances can share a page, each on its own map.
11
13
  *
12
14
  * @param map - The map to draw on
13
15
  * @param options - The options; every one can be omitted
@@ -10,6 +10,7 @@ import type { DrawError } from './errors.js';
10
10
  import type { Hit } from './extension/provider.js';
11
11
  import type { UpdateSource } from './extension/store.js';
12
12
  import type { Feature, FeatureInput, FileData, Group, Layer, LoadResult, Metadata, MoveTarget } from './model.js';
13
+ import type { RuntimeOptions } from './options.js';
13
14
  import type { LayerStackEntry, Mode, Selection, SelectionType, SnapResult, VertexSelection } from './state.js';
14
15
  /** A point on the screen, as `[x, y]` in CSS pixels. */
15
16
  export type ScreenPoint = [number, number];
@@ -231,6 +232,15 @@ export interface DrawEvents {
231
232
  'interactionLock.changed': {
232
233
  locked: boolean;
233
234
  };
235
+ /**
236
+ * `draw.options.update` changed the options; `options` and `previous` are what
237
+ * `draw.options.get()` returns after and before the change. An update that leaves every
238
+ * value as it was does not fire it
239
+ */
240
+ 'options.changed': {
241
+ options: Readonly<RuntimeOptions>;
242
+ previous: Readonly<RuntimeOptions>;
243
+ };
234
244
  /** The snapping target changed */
235
245
  'snap.changed': {
236
246
  result: SnapResult | null;
@@ -24,7 +24,7 @@ const ASYNC_MEMBERS = new Set(['document.load', 'document.loadMany']);
24
24
  * @internal
25
25
  */
26
26
  export function createDrawOnEngine(engine, options = {}, setExternalEntry = () => { }) {
27
- const drawOptions = createOptions(engine, options, setExternalEntry);
27
+ const drawOptions = createOptions(engine, options, setExternalEntry, (payload) => engine.events.emit('options.changed', payload));
28
28
  drawOptions.applyCreation();
29
29
  const { context, modeManager, events, map } = engine;
30
30
  const { store } = context;
@@ -258,6 +258,7 @@ export function wrapDataset(dataset) {
258
258
  throw invalidInput('The rule must be an object');
259
259
  dataset.setStyleRule(rule);
260
260
  },
261
+ getStyleRule: () => dataset.getStyleRule(),
261
262
  setZoomScale(zoomScale) {
262
263
  if (zoomScale !== null)
263
264
  checkFunction(zoomScale, 'zoomScale');
@@ -10,7 +10,8 @@
10
10
  * `document.changed` with the whole
11
11
  * change when the transaction changed the document. The engine announces the rest
12
12
  * on its internal emitter (snapping, clicks, images, the stacking order, failed loads,
13
- * drags), and each is passed on under its name here.
13
+ * drags), and each is passed on under its name here. `options.changed` comes from
14
+ * `draw.options`, which emits it on the same hub.
14
15
  */
15
16
  import { DRAW_PROPERTY_KEYS } from '../../shared/properties.js';
16
17
  import { geometryFromCoordinates } from '../../shared/utils/coordinates.js';
@@ -164,7 +164,10 @@ export function createExtensionHost(deps) {
164
164
  },
165
165
  getWritableLayerId: context.getWritableLayerId,
166
166
  generateId: context.generateFeatureId,
167
- scaleWithZoom: context.options.scaleWithZoom,
167
+ // Read when a feature is committed, so a change of the option applies at once
168
+ get scaleWithZoom() {
169
+ return context.options.scaleWithZoom;
170
+ },
168
171
  selectionStyle: context.selectionStyle,
169
172
  boxSelectionStyle: context.renderingConfig.boxSelectionStyle,
170
173
  notifyDrawCommit: (feature) => announceDrawCommit(feature),
@@ -656,9 +656,10 @@ function assignInPlace(target, source) {
656
656
  * @param options - The options the instance was created with
657
657
  * @param setExternalEntry - Replaces the function that tells the entries from outside the
658
658
  * document
659
+ * @param onChanged - Receives the options after and before an update that changed them
659
660
  * @internal
660
661
  */
661
- export function createOptions(engine, options, setExternalEntry) {
662
+ export function createOptions(engine, options, setExternalEntry, onChanged = () => { }) {
662
663
  const { context, map, customLayer } = engine;
663
664
  let current = runtimePart(options);
664
665
  const apply = (patch) => {
@@ -758,39 +759,44 @@ export function createOptions(engine, options, setExternalEntry) {
758
759
  else if (redraw)
759
760
  map.triggerRepaint();
760
761
  };
762
+ const get = () => {
763
+ const { snapService, trace, topology, pixelRatioSource, renderingConfig } = context;
764
+ const snap = snapService.getOptions();
765
+ const values = {
766
+ ...current,
767
+ scaleWithZoom: context.options.scaleWithZoom,
768
+ clickTolerance: context.options.clickTolerance,
769
+ dragThreshold: context.options.dragThreshold,
770
+ snapping: {
771
+ ...current.snapping,
772
+ enabled: snap.enabled,
773
+ tolerancePx: snap.tolerancePx,
774
+ disableKey: snap.disableKey,
775
+ kinds: { ...snap.kinds },
776
+ datasets: snap.datasets,
777
+ guideStepDegrees: snap.guideStepDegrees,
778
+ },
779
+ tracing: { enabled: trace.enabled },
780
+ topology: { sharedVertexDrag: topology.sharedVertexDrag },
781
+ rendering: {
782
+ ...current.rendering,
783
+ renderScale: pixelRatioSource.getScaleFactor(),
784
+ cacheGeometry: renderingConfig.storeRetained !== false,
785
+ timeSlicing: renderingConfig.timeSlicing !== false,
786
+ },
787
+ };
788
+ return structuredCloneOptions(values);
789
+ };
761
790
  return {
762
- get() {
763
- const { snapService, trace, topology, pixelRatioSource, renderingConfig } = context;
764
- const snap = snapService.getOptions();
765
- const values = {
766
- ...current,
767
- scaleWithZoom: context.options.scaleWithZoom,
768
- clickTolerance: context.options.clickTolerance,
769
- dragThreshold: context.options.dragThreshold,
770
- snapping: {
771
- ...current.snapping,
772
- enabled: snap.enabled,
773
- tolerancePx: snap.tolerancePx,
774
- disableKey: snap.disableKey,
775
- kinds: { ...snap.kinds },
776
- datasets: snap.datasets,
777
- guideStepDegrees: snap.guideStepDegrees,
778
- },
779
- tracing: { enabled: trace.enabled },
780
- topology: { sharedVertexDrag: topology.sharedVertexDrag },
781
- rendering: {
782
- ...current.rendering,
783
- renderScale: pixelRatioSource.getScaleFactor(),
784
- cacheGeometry: renderingConfig.storeRetained !== false,
785
- timeSlicing: renderingConfig.timeSlicing !== false,
786
- },
787
- };
788
- return structuredCloneOptions(values);
789
- },
791
+ get,
790
792
  update(patch) {
791
793
  checkPatch(patch);
794
+ const previous = get();
792
795
  current = mergeOptions(current, patch);
793
796
  apply(patch);
797
+ const next = get();
798
+ if (!sameOptions(previous, next))
799
+ onChanged({ options: next, previous });
794
800
  },
795
801
  getStyle() {
796
802
  return current.style;
@@ -809,6 +815,29 @@ export function createOptions(engine, options, setExternalEntry) {
809
815
  },
810
816
  };
811
817
  }
818
+ /**
819
+ * Whether two copies of the options hold the same values: plain objects and arrays are
820
+ * compared item by item, a function by identity, and a key given as `undefined` is the same
821
+ * as a key left out
822
+ */
823
+ function sameOptions(a, b) {
824
+ if (Object.is(a, b))
825
+ return true;
826
+ if (Array.isArray(a) || Array.isArray(b)) {
827
+ return (Array.isArray(a) &&
828
+ Array.isArray(b) &&
829
+ a.length === b.length &&
830
+ a.every((item, i) => sameOptions(item, b[i])));
831
+ }
832
+ if (!isRecord(a) || !isRecord(b))
833
+ return false;
834
+ const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
835
+ for (const key of keys) {
836
+ if (!sameOptions(a[key], b[key]))
837
+ return false;
838
+ }
839
+ return true;
840
+ }
812
841
  /** A copy of the options that shares the functions and nothing else */
813
842
  function structuredCloneOptions(value) {
814
843
  if (Array.isArray(value))
@@ -347,6 +347,9 @@ export class DatasetImpl {
347
347
  getBaseStyle() {
348
348
  return this.styler.base;
349
349
  }
350
+ getStyleRule() {
351
+ return this.styler.rule;
352
+ }
350
353
  getFeatures() {
351
354
  return this.source.features();
352
355
  }
@@ -306,6 +306,8 @@ export interface Dataset {
306
306
  setTable(table: Table | PreparedTable): void;
307
307
  /** Replaces the style rule (undefined clears it) */
308
308
  setStyleRule(rule: StyleRule | undefined): void;
309
+ /** The style rule in effect (undefined when not set) */
310
+ getStyleRule(): StyleRule | undefined;
309
311
  /**
310
312
  * Replaces the zoom-dependent drawing factors (null clears them)
311
313
  *
@@ -39,7 +39,8 @@ function firstTentativeCoordinate(tentative) {
39
39
  * little beyond ±180; the rendering and the export deal with that
40
40
  * - A drag moves every coordinate of its events by the one shift decided from where it started
41
41
  * (`dragStartLngLat`), so the distance dragged never jumps by 360 degrees when the pointer
42
- * crosses the antimeridian during the drag
42
+ * crosses the antimeridian during the drag. The normalizer gives the positions of a press
43
+ * continuously (the globe's jump from 180 to -180 included), so one shift keeps them so
43
44
  *
44
45
  * Away from the antimeridian, on the main copy of the world, nothing changes and the event is
45
46
  * returned as it is.
@@ -1,5 +1,6 @@
1
1
  // SPDX-FileCopyrightText: 2026 SAKAIDA Atsushi
2
2
  // SPDX-License-Identifier: AGPL-3.0-only
3
+ import { nearestLongitude } from '../shared/math/longitude.js';
3
4
  import { getModifiers } from './types.js';
4
5
  const DEFAULT_OPTIONS = {
5
6
  dragThreshold: 3,
@@ -51,6 +52,20 @@ function createProfiles(opts) {
51
52
  },
52
53
  };
53
54
  }
55
+ /**
56
+ * The position of the pointer, on the copy of the world nearest to where it was last
57
+ *
58
+ * The flat map gives the pointer unwrapped longitudes, which run on past 180 as it crosses the
59
+ * antimeridian. The globe gives them in [-180, 180], so they jump by 360 degrees there: a
60
+ * freehand stroke drawn from them went once round the world along the parallel, and the shift
61
+ * of a feature dragged across jumped by a turn. Within a press each position is therefore
62
+ * taken on the copy nearest to the last one, so the pointer moves on continuously, as on the
63
+ * flat map. A position within 180 degrees of the last one is returned unchanged.
64
+ */
65
+ function continuing(lngLat, last) {
66
+ const lng = nearestLongitude(lngLat.lng, last.lng);
67
+ return lng === lngLat.lng ? lngLat : { lng, lat: lngLat.lat };
68
+ }
54
69
  /**
55
70
  * Whether a mouse event is the compatibility event a browser emits for a touch
56
71
  */
@@ -184,7 +199,8 @@ export function createInputNormalizer(map, options = {}) {
184
199
  *
185
200
  * Once the press has become a drag it stays one until it is released.
186
201
  */
187
- function movePress(current, point, lngLat, originalEvent) {
202
+ function movePress(current, point, rawLngLat, originalEvent) {
203
+ const lngLat = continuing(rawLngLat, current.lastLngLat);
188
204
  current.lastPoint = point;
189
205
  current.lastLngLat = lngLat;
190
206
  current.lastEvent = originalEvent;
@@ -204,7 +220,8 @@ export function createInputNormalizer(map, options = {}) {
204
220
  * Releases the press: dragend (when dragging) and mouseup. Returns whether the release
205
221
  * is a click candidate.
206
222
  */
207
- function releasePress(current, point, lngLat, originalEvent) {
223
+ function releasePress(current, point, rawLngLat, originalEvent) {
224
+ const lngLat = continuing(rawLngLat, current.lastLngLat);
208
225
  clearLongPress(current);
209
226
  press = null;
210
227
  if (current.dragging) {
@@ -23,6 +23,10 @@ export type LineInput = LineString | MultiLineString | {
23
23
  * Returns the geometry of a geometry, or of anything that carries one in a `geometry` field
24
24
  * (a GeoJSON feature, a feature of the drawing)
25
25
  *
26
+ * A value whose `geometry` member is a geometry is read as a feature first, whatever its own
27
+ * `type` says: a feature of the drawing names its feature type there (`'Polygon'`,
28
+ * `'Circle'`, `'Freehand'` and so on), which is not the type of a geometry it is.
29
+ *
26
30
  * @throws GeometryError (`invalid-input`) for a value that is neither, and for a feature
27
31
  * without a geometry
28
32
  */
@@ -24,6 +24,10 @@ function isPosition(value) {
24
24
  * Returns the geometry of a geometry, or of anything that carries one in a `geometry` field
25
25
  * (a GeoJSON feature, a feature of the drawing)
26
26
  *
27
+ * A value whose `geometry` member is a geometry is read as a feature first, whatever its own
28
+ * `type` says: a feature of the drawing names its feature type there (`'Polygon'`,
29
+ * `'Circle'`, `'Freehand'` and so on), which is not the type of a geometry it is.
30
+ *
27
31
  * @throws GeometryError (`invalid-input`) for a value that is neither, and for a feature
28
32
  * without a geometry
29
33
  */
@@ -32,26 +36,31 @@ export function geometryOf(input, operation) {
32
36
  if (!isObject(value)) {
33
37
  throw invalidInput(operation, 'the input is not a geometry or a feature');
34
38
  }
35
- if (!isGeometryType(value.type)) {
36
- if (!('geometry' in value) && value.type !== 'Feature') {
37
- throw invalidInput(operation, 'the input is not a geometry or a feature');
38
- }
39
+ if ('geometry' in value || value.type === 'Feature') {
39
40
  const geometry = value.geometry;
40
- if (!isObject(geometry) || !isGeometryType(geometry.type)) {
41
+ if (isObject(geometry) && isGeometryType(geometry.type)) {
42
+ return geometryOf(geometry, operation);
43
+ }
44
+ if (!isGeometry(value)) {
41
45
  throw invalidInput(operation, 'the feature has no geometry');
42
46
  }
43
- return geometryOf(geometry, operation);
44
47
  }
45
- if (value.type === 'GeometryCollection') {
46
- if (!Array.isArray(value.geometries)) {
47
- throw invalidInput(operation, 'the geometry collection has no geometries');
48
- }
48
+ if (value.type === 'GeometryCollection' && !Array.isArray(value.geometries)) {
49
+ throw invalidInput(operation, 'the geometry collection has no geometries');
49
50
  }
50
- else if (!Array.isArray(value.coordinates)) {
51
+ if (!isGeometry(value)) {
51
52
  throw invalidInput(operation, 'the input is not a geometry or a feature');
52
53
  }
53
54
  return value;
54
55
  }
56
+ /** Whether an object has the type of a geometry and the member that type carries */
57
+ function isGeometry(value) {
58
+ if (!isGeometryType(value.type))
59
+ return false;
60
+ if (value.type === 'GeometryCollection')
61
+ return Array.isArray(value.geometries);
62
+ return Array.isArray(value.coordinates);
63
+ }
55
64
  /** Whether a value names a GeoJSON geometry type */
56
65
  function isGeometryType(type) {
57
66
  return typeof type === 'string' && (COORDINATE_TYPES.has(type) || type === 'GeometryCollection');
package/dist/index.d.ts CHANGED
@@ -17,6 +17,20 @@
17
17
  * });
18
18
  * ```
19
19
  *
20
+ * Where to find things: every resource of {@link Draw} is a field with its own methods.
21
+ *
22
+ * - `draw.features` (create, update, delete, move, union, split): {@link FeaturesCollection}
23
+ * - `draw.layers` (create, update, reorder, setActive): {@link LayersCollection}
24
+ * - `draw.groups` (create, update, move): {@link GroupsCollection}
25
+ * - `draw.datasets` (add, remove, move): {@link DatasetsCollection}
26
+ * - `draw.selection` (set, add, clear, delete, group): {@link SelectionResource}
27
+ * - `draw.vertexSelection` (set, clear, delete): {@link VertexSelectionResource}
28
+ * - `draw.metadata` and `draw.options` (get, update): {@link MetadataResource}, {@link OptionsResource}
29
+ * - `draw.document` (load, toJSON, toGeoJSON): {@link DocumentResource}
30
+ * - `draw.drawing` (addVertex, finish, cancel): {@link DrawingResource}
31
+ * - `draw.extensions` (plugins, modes, feature types): {@link ExtensionsCollections}
32
+ * - events with `draw.on`: {@link DrawEvents}; errors: {@link DrawError}
33
+ *
20
34
  * Read next: [getting started](https://sakuzu.github.io/maplibre-gl-draw/getting-started),
21
35
  * then the guides for [drawing](https://sakuzu.github.io/maplibre-gl-draw/guides/drawing) and
22
36
  * [saving and loading](https://sakuzu.github.io/maplibre-gl-draw/guides/save-load). The
@@ -5,13 +5,19 @@ import type { CustomLayerInterface, Map as MapLibreMap } from 'maplibre-gl';
5
5
  *
6
6
  * One rule covers every timing: the slots are added whenever the style accepts layers and a slot
7
7
  * is missing. It is checked once now (for a map whose style is already parsed, even while tiles
8
- * are still loading) and again on every `styledata` event. maplibre fires `styledata` when a
9
- * style finishes loading (before `style.load` and `load`) and after changes to the style, so the
10
- * slots are also restored after `setStyle`, including a diffed `setStyle` that drops layers it
11
- * does not know. A slot that is already on the map is never added twice.
8
+ * are still loading) and again on every `styledata` and `style.load` event. maplibre fires
9
+ * `styledata` when a style finishes loading (before `style.load` and `load`) and after changes to
10
+ * the style, so the slots are also restored after a full `setStyle`. A slot that is already on
11
+ * the map is never added twice.
12
12
  *
13
13
  * The slots are added in order from the first (the backmost), which lines them up in the same
14
- * order in maplibre (layers added later are on top).
14
+ * order in maplibre (layers added later are on top). A slot missing while a later one is on the
15
+ * map is added just behind it.
16
+ *
17
+ * On `style.load` the slots are also put back on top of the map (see `placeSlotsOnTop`). maplibre
18
+ * fires it when a style is loaded and at the end of a diffed `setStyle` that changed something,
19
+ * and only then: a layer the host adds or moves itself fires `styledata` alone, so a layer the
20
+ * host put above the drawing stays there until the style is changed.
15
21
  *
16
22
  * @param map The map
17
23
  * @param getSlotLayers Returns the current list of slots (first = backmost)
@@ -11,19 +11,82 @@
11
11
  function styleAcceptsLayers(map) {
12
12
  return map.style?._loaded === true;
13
13
  }
14
+ /**
15
+ * Puts the slots back on top of the map after a style change
16
+ *
17
+ * The slots, and the native layers the host placed between them (the separators), form one
18
+ * block that belongs at the top of the map, with the slots in their order (the first is the
19
+ * backmost). A full `setStyle` gives that by itself: the old layers are gone and the slots are
20
+ * added again on top. A diffed `setStyle` does not: maplibre's `Style.serialize()` leaves custom
21
+ * layers out, so the diff neither removes nor adds the slots, and the layers of the new style are
22
+ * added above them, which hides the drawing under the basemap.
23
+ *
24
+ * Nothing is moved when the block is already in place, so the `styledata` that follows a move
25
+ * finds nothing to do. When it is not, the block is moved to the top: each slot, followed by the
26
+ * native layers that sat just above it (up to the next slot), keeps its neighbours. The layers
27
+ * above the frontmost slot are taken as part of the new style and stay below the block.
28
+ *
29
+ * @param map The map
30
+ * @param slotLayers The slots (first = backmost)
31
+ */
32
+ function placeSlotsOnTop(map, slotLayers) {
33
+ const order = map.getLayersOrder();
34
+ const slotIds = slotLayers.map((layer) => layer.id).filter((id) => order.includes(id));
35
+ if (slotIds.length === 0)
36
+ return;
37
+ const isSlot = new Set(slotIds);
38
+ // The block runs from the backmost slot on the map to the frontmost one
39
+ let first = -1;
40
+ let last = -1;
41
+ order.forEach((id, index) => {
42
+ if (!isSlot.has(id))
43
+ return;
44
+ if (first < 0)
45
+ first = index;
46
+ last = index;
47
+ });
48
+ const block = order.slice(first, last + 1);
49
+ const slotsInBlock = block.filter((id) => isSlot.has(id));
50
+ const inOrder = slotsInBlock.every((id, index) => id === slotIds[index]);
51
+ if (inOrder && last === order.length - 1)
52
+ return;
53
+ // Each slot keeps the native layers that followed it
54
+ const following = new Map();
55
+ let current = '';
56
+ for (const id of block) {
57
+ if (isSlot.has(id)) {
58
+ current = id;
59
+ following.set(id, []);
60
+ }
61
+ else {
62
+ following.get(current)?.push(id);
63
+ }
64
+ }
65
+ for (const slotId of slotIds) {
66
+ map.moveLayer(slotId);
67
+ for (const id of following.get(slotId) ?? [])
68
+ map.moveLayer(id);
69
+ }
70
+ }
14
71
  /**
15
72
  * Keeps the frames of the stacking order on the map for as long as the returned detach function has not
16
73
  * been called
17
74
  *
18
75
  * One rule covers every timing: the slots are added whenever the style accepts layers and a slot
19
76
  * is missing. It is checked once now (for a map whose style is already parsed, even while tiles
20
- * are still loading) and again on every `styledata` event. maplibre fires `styledata` when a
21
- * style finishes loading (before `style.load` and `load`) and after changes to the style, so the
22
- * slots are also restored after `setStyle`, including a diffed `setStyle` that drops layers it
23
- * does not know. A slot that is already on the map is never added twice.
77
+ * are still loading) and again on every `styledata` and `style.load` event. maplibre fires
78
+ * `styledata` when a style finishes loading (before `style.load` and `load`) and after changes to
79
+ * the style, so the slots are also restored after a full `setStyle`. A slot that is already on
80
+ * the map is never added twice.
24
81
  *
25
82
  * The slots are added in order from the first (the backmost), which lines them up in the same
26
- * order in maplibre (layers added later are on top).
83
+ * order in maplibre (layers added later are on top). A slot missing while a later one is on the
84
+ * map is added just behind it.
85
+ *
86
+ * On `style.load` the slots are also put back on top of the map (see `placeSlotsOnTop`). maplibre
87
+ * fires it when a style is loaded and at the end of a diffed `setStyle` that changed something,
88
+ * and only then: a layer the host adds or moves itself fires `styledata` alone, so a layer the
89
+ * host put above the drawing stays there until the style is changed.
27
90
  *
28
91
  * @param map The map
29
92
  * @param getSlotLayers Returns the current list of slots (first = backmost)
@@ -33,15 +96,28 @@ export function attachSlotLayers(map, getSlotLayers) {
33
96
  const ensureSlots = () => {
34
97
  if (!styleAcceptsLayers(map))
35
98
  return;
36
- for (const slotLayer of getSlotLayers()) {
37
- if (!map.getLayer(slotLayer.id)) {
99
+ const slotLayers = getSlotLayers();
100
+ slotLayers.forEach((slotLayer, index) => {
101
+ if (map.getLayer(slotLayer.id))
102
+ return;
103
+ // Behind the next slot that is on the map, so that the slots stay in order
104
+ const next = slotLayers.slice(index + 1).find((later) => map.getLayer(later.id));
105
+ if (next)
106
+ map.addLayer(slotLayer, next.id);
107
+ else
38
108
  map.addLayer(slotLayer);
39
- }
40
- }
109
+ });
110
+ };
111
+ const restoreSlots = () => {
112
+ ensureSlots();
113
+ if (styleAcceptsLayers(map))
114
+ placeSlotsOnTop(map, getSlotLayers());
41
115
  };
42
116
  map.on('styledata', ensureSlots);
117
+ map.on('style.load', restoreSlots);
43
118
  ensureSlots();
44
119
  return () => {
45
120
  map.off('styledata', ensureSlots);
121
+ map.off('style.load', restoreSlots);
46
122
  };
47
123
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakuzu/maplibre-gl-draw",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "WebGL2 drawing and editing engine for MapLibre GL JS: points, lines, and polygons rendered in a custom layer, with selection, hit testing, and geometry utilities.",
5
5
  "keywords": [
6
6
  "maplibre",
@@ -66,32 +66,44 @@
66
66
  "workspaces": [
67
67
  "examples",
68
68
  "playground",
69
- "bench"
69
+ "bench",
70
+ "ui"
70
71
  ],
71
72
  "scripts": {
72
73
  "build": "rm -rf dist tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
73
74
  "typecheck": "tsc --noEmit",
74
75
  "check:spdx": "node scripts/spdx.mjs",
75
- "lint": "biome check src examples playground bench scripts docs/.vitepress/config.mts && npm run check:layers && npm run check:terms && npm run check:spdx",
76
+ "lint": "biome check src examples playground bench ui scripts docs/.vitepress/config.mts docs/.vitepress/theme && npm run check:layers && npm run check:terms && npm run check:spdx && node ui/scripts/check-imports.mjs",
76
77
  "check:layers": "node scripts/check-layers.mjs",
77
78
  "check:terms": "node scripts/check-terms.mjs",
78
79
  "docs:api": "typedoc --options typedoc.site.json",
79
80
  "docs:check": "node scripts/docs-check.mjs",
80
81
  "docs:image": "node scripts/capture-readme-image.mjs",
81
- "build:site": "node scripts/build-site.mjs",
82
+ "data:overture": "node scripts/fetch-overture-sample.mjs",
83
+ "site:thumbnails": "node scripts/need-ui.mjs && node scripts/capture-example-thumbnails.mjs",
84
+ "build:site": "npm run build && npm run ui:build && node scripts/build-site.mjs",
82
85
  "deploy:pages": "node scripts/deploy-pages.mjs",
83
- "lint:fix": "biome check --write src examples playground bench",
84
- "format": "biome format --write src examples playground bench",
86
+ "lint:fix": "biome check --write src examples playground bench ui",
87
+ "format": "biome format --write src examples playground bench ui",
85
88
  "test": "vitest run",
86
- "test:e2e": "vitest run --config src/e2e/vitest.config.ts",
89
+ "test:e2e": "node scripts/need-ui.mjs && vitest run --config src/e2e/vitest.config.ts",
87
90
  "test:watch": "vitest",
88
- "dev": "npm run dev -w examples",
91
+ "dev": "node scripts/need-ui.mjs && npm run dev -w examples",
89
92
  "dev:playground": "npm run dev -w playground",
90
93
  "dev:bench": "npm run dev -w bench",
94
+ "ui:build": "npm run build -w ui",
95
+ "ui:typecheck": "npm run typecheck -w ui",
96
+ "ui:test": "npm run test -w ui",
97
+ "ui:test:e2e": "npm run test:e2e -w ui",
98
+ "ui:lint": "npm run lint -w ui",
99
+ "ui:dev": "npm run dev -w ui",
100
+ "ui:check": "npm run ui:typecheck && npm run ui:test && npm run ui:test:e2e && npm run ui:build",
101
+ "ui:check:package": "publint ui && rm -rf ui/.pack && mkdir -p ui/.pack && npm pack -w ui --ignore-scripts --pack-destination ui/.pack >/dev/null && attw ui/.pack/*.tgz --profile esm-only --exclude-entrypoints ./style.css && rm -rf ui/.pack",
91
102
  "check:package": "publint && rm -rf .pack && mkdir -p .pack && npm pack --ignore-scripts --pack-destination .pack >/dev/null && attw .pack/*.tgz --profile esm-only && node --input-type=module -e \"await import('./dist/index.js'); await import('./dist/geometry/index.js'); await import('./dist/table/index.js'); await import('./dist/webgl/index.js'); console.log('dist imports ok')\" && rm -rf .pack",
92
103
  "prepack": "npm run build",
93
- "site:dev": "npm run docs:api && vitepress dev docs",
94
- "site:build": "npm run docs:api && vitepress build docs",
104
+ "site:dev": "node scripts/site-dev.mjs",
105
+ "site:dev:docs": "npm run docs:api && vitepress dev docs --port 5173 --strictPort",
106
+ "site:build": "node scripts/need-ui.mjs && npm run docs:api && vitepress build docs",
95
107
  "site:preview": "vitepress preview docs"
96
108
  },
97
109
  "dependencies": {