@sakuzu/maplibre-gl-draw 2.0.0 → 2.1.1

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +119 -1
  2. package/README.ja.md +48 -35
  3. package/README.md +52 -36
  4. package/THIRD_PARTY_NOTICES.md +29 -0
  5. package/dist/api/datasets.d.ts +5 -0
  6. package/dist/api/draw.d.ts +6 -4
  7. package/dist/api/draw.js +6 -4
  8. package/dist/api/events.d.ts +10 -0
  9. package/dist/api/impl/create-draw.js +1 -1
  10. package/dist/api/impl/datasets.js +1 -0
  11. package/dist/api/impl/events.js +2 -1
  12. package/dist/api/impl/extension-host.js +4 -1
  13. package/dist/api/impl/options.js +58 -29
  14. package/dist/dataset/dataset.js +3 -0
  15. package/dist/dataset/retained.js +19 -18
  16. package/dist/dataset/selection.d.ts +3 -3
  17. package/dist/dataset/selection.js +3 -7
  18. package/dist/dataset/table-source.js +13 -11
  19. package/dist/dataset/types.d.ts +2 -0
  20. package/dist/dispatcher/input-router.js +2 -1
  21. package/dist/dispatcher/normalizer.js +19 -2
  22. package/dist/geometry/geojson.d.ts +4 -0
  23. package/dist/geometry/geojson.js +20 -11
  24. package/dist/index.d.ts +14 -0
  25. package/dist/view/layer/attach.d.ts +11 -5
  26. package/dist/view/layer/attach.js +85 -9
  27. package/dist/view/layer/custom-layer.js +2 -1
  28. package/dist/view/layer/drape-planner.d.ts +3 -0
  29. package/dist/view/layer/drape-planner.js +9 -7
  30. package/dist/view/layer/frame-render.js +2 -3
  31. package/dist/view/layer/frame-state.d.ts +3 -0
  32. package/dist/view/layer/frame-state.js +2 -1
  33. package/dist/view/layer/gl-state.d.ts +2 -3
  34. package/dist/view/layer/gl-state.js +2 -3
  35. package/dist/view/renderers/line/dash.d.ts +27 -0
  36. package/dist/view/renderers/line/dash.js +23 -12
  37. package/dist/view/terrain/drape/bin-store.d.ts +11 -2
  38. package/dist/view/terrain/drape/bin-store.js +26 -2
  39. package/dist/view/terrain/drape/binning.d.ts +32 -0
  40. package/dist/view/terrain/drape/binning.js +15 -7
  41. package/dist/view/terrain/drape/geometry.d.ts +27 -0
  42. package/dist/view/terrain/drape/geometry.js +57 -6
  43. package/dist/view/terrain/drape/pass.d.ts +8 -3
  44. package/dist/view/terrain/drape/pass.js +39 -28
  45. package/dist/view/terrain/drape/renderer.d.ts +13 -0
  46. package/dist/view/terrain/drape/renderer.js +157 -30
  47. package/package.json +22 -10
@@ -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
  }
@@ -324,8 +324,8 @@ export function isEmptyChunkBatches(batches) {
324
324
  * @param isExternallyRenderedPoint Predicate that returns whether a point is drawn by an external
325
325
  * renderer (every point is pushed when omitted). A point for which it is true goes neither onto
326
326
  * a batch nor into fallback
327
- * @param skipDrapedFills Do not push the solid polygons and lines (a frame where the analytic
328
- * drape draws them as ground pixels)
327
+ * @param skipDrapedFills Do not push the polygons and lines, solid or dashed (a frame where the
328
+ * analytic drape draws them as ground pixels)
329
329
  * @returns The batches that were built. When the GPU resources could not be created (the shaders
330
330
  * are not initialized) null is returned and the caller rebuilds them on the next frame
331
331
  *
@@ -739,7 +739,7 @@ export function isDrapedGeometry(feature) {
739
739
  * @internal
740
740
  */
741
741
  export function collectFeature(feature, draft, styles, isExternallyRenderedPoint, skipDrapedFills = false) {
742
- const skipSolid = skipDrapedFills && isDrapedGeometry(feature);
742
+ const skipDraped = skipDrapedFills && isDrapedGeometry(feature);
743
743
  switch (feature.type) {
744
744
  case 'Point':
745
745
  collectPoint(feature, coordinatesOf(feature), draft, styles, isExternallyRenderedPoint);
@@ -753,20 +753,20 @@ export function collectFeature(feature, draft, styles, isExternallyRenderedPoint
753
753
  return;
754
754
  case 'LineString':
755
755
  case 'Freehand':
756
- collectLine(feature, coordinatesOf(feature), draft, styles, skipSolid);
756
+ collectLine(feature, coordinatesOf(feature), draft, styles, skipDraped);
757
757
  return;
758
758
  case 'MultiLineString':
759
759
  for (const coords of coordinatesOf(feature)) {
760
- collectLine(feature, coords, draft, styles, skipSolid);
760
+ collectLine(feature, coords, draft, styles, skipDraped);
761
761
  }
762
762
  return;
763
763
  case 'Polygon':
764
- collectPolygon(feature, coordinatesOf(feature), draft, styles, 0, skipSolid);
764
+ collectPolygon(feature, coordinatesOf(feature), draft, styles, 0, skipDraped);
765
765
  return;
766
766
  case 'MultiPolygon': {
767
767
  const parts = coordinatesOf(feature);
768
768
  for (let i = 0; i < parts.length; i++) {
769
- collectPolygon(feature, parts[i], draft, styles, i, skipSolid);
769
+ collectPolygon(feature, parts[i], draft, styles, i, skipDraped);
770
770
  }
771
771
  return;
772
772
  }
@@ -827,18 +827,19 @@ function collectPoint(feature, coord, draft, styles, isExternallyRenderedPoint)
827
827
  * Pushes a line (a dashed line is sent to immediate mode because its CPU-side splitting depends
828
828
  * on the zoom)
829
829
  */
830
- function collectLine(feature, coords, draft, styles, skipSolid = false) {
830
+ function collectLine(feature, coords, draft, styles, skipDraped = false) {
831
831
  if (coords.length < 2)
832
832
  return;
833
833
  const strokeStyle = styles.getLineStringStrokeStyle(feature);
834
+ // A line, solid or dashed, is drawn by the analytic drape as ground pixels (in this frame
835
+ // only)
836
+ if (skipDraped && strokeStyle.opacity > 0 && strokeStyle.width > 0)
837
+ return;
834
838
  if (strokeStyle.lineStyle !== 'solid') {
835
839
  // The path that goes to immediate mode is not multiplied by the factors of zoomScale
836
840
  pushFallback(draft, feature);
837
841
  return;
838
842
  }
839
- // A solid line is drawn by the analytic drape as ground pixels (in this frame only)
840
- if (skipSolid && strokeStyle.opacity > 0 && strokeStyle.width > 0)
841
- return;
842
843
  const createdZoom = getCreatedZoom(feature);
843
844
  const target = createdZoom === undefined ? draft.fixedLines : draft.scaledLines;
844
845
  const item = toLineBatchItem(coords, feature.id, strokeStyle, createdZoom ?? FIXED_WIDTH_ZOOM);
@@ -856,23 +857,23 @@ function collectLine(feature, coords, draft, styles, skipSolid = false) {
856
857
  * A fill-only polygon is also pushed as an SDFPolygon without an outline (immediate mode uses
857
858
  * PolygonBatchRenderer, but the appearance of the fill is the same).
858
859
  */
859
- function collectPolygon(feature, rings, draft, styles, partIndex = 0, skipSolid = false) {
860
+ function collectPolygon(feature, rings, draft, styles, partIndex = 0, skipDraped = false) {
860
861
  const outerRing = rings[0];
861
862
  if (!outerRing || outerRing.length < 3)
862
863
  return;
863
864
  const { fillColor, strokeStyle } = styles.getPolygonStyles(feature);
864
865
  const hasStroke = strokeStyle.opacity > 0 && strokeStyle.width > 0;
866
+ if (!hasStroke && fillColor[3] <= 0)
867
+ return;
868
+ // The fill and the outline, solid or dashed, are drawn by the analytic drape as ground pixels
869
+ // (in this frame only)
870
+ if (skipDraped)
871
+ return;
865
872
  if (hasStroke && strokeStyle.lineStyle !== 'solid') {
866
873
  // The path that goes to immediate mode is not multiplied by the factors of zoomScale
867
874
  pushFallback(draft, feature);
868
875
  return;
869
876
  }
870
- if (!hasStroke && fillColor[3] <= 0)
871
- return;
872
- // The solid fill and outline are drawn by the analytic drape as ground pixels (in this frame
873
- // only)
874
- if (skipSolid)
875
- return;
876
877
  const createdZoom = getCreatedZoom(feature);
877
878
  const target = createdZoom === undefined ? draft.fixedPolygons : draft.scaledPolygons;
878
879
  // A fixed line width is declared by the negative convention (the same reason as collectLine).
@@ -6,8 +6,8 @@ import type { RetainedStyleResolver } from '../view/renderers/retained.js';
6
6
  * It is paired with `collectDataset` in `view/terrain/drape/pass.ts`. Changing only one of
7
7
  * them would show the selection highlight twice or in neither of them.
8
8
  *
9
- * A dashed outline does not go onto the drape (solving the accumulated distance along the outline
10
- * from the pixels is not implemented, so drawing it as a solid line would change the appearance).
11
- * For what does not go onto it, the immediate-mode path draws the highlight as before.
9
+ * Dashed lines and dashed outlines go onto the drape like solid ones (the drape lays the pattern
10
+ * out along the path). For what does not go onto it, the immediate-mode path draws the highlight
11
+ * as before.
12
12
  */
13
13
  export declare function isHandedToDrape(feature: Feature, styles: RetainedStyleResolver): boolean;
@@ -150,24 +150,20 @@ export function sameFeatureIds(a, b) {
150
150
  * It is paired with `collectDataset` in `view/terrain/drape/pass.ts`. Changing only one of
151
151
  * them would show the selection highlight twice or in neither of them.
152
152
  *
153
- * A dashed outline does not go onto the drape (solving the accumulated distance along the outline
154
- * from the pixels is not implemented, so drawing it as a solid line would change the appearance).
155
- * For what does not go onto it, the immediate-mode path draws the highlight as before.
153
+ * Dashed lines and dashed outlines go onto the drape like solid ones (the drape lays the pattern
154
+ * out along the path). For what does not go onto it, the immediate-mode path draws the highlight
155
+ * as before.
156
156
  */
157
157
  export function isHandedToDrape(feature, styles) {
158
158
  if (feature.type === 'Polygon' || feature.type === 'MultiPolygon') {
159
159
  const { fillColor, strokeStyle } = styles.getPolygonStyles(feature);
160
160
  const hasStroke = strokeStyle.opacity > 0 && strokeStyle.width > 0;
161
- if (hasStroke && strokeStyle.lineStyle !== 'solid')
162
- return false;
163
161
  return hasStroke || fillColor[3] > 0;
164
162
  }
165
163
  if (feature.type === 'LineString' ||
166
164
  feature.type === 'MultiLineString' ||
167
165
  feature.type === 'Freehand') {
168
166
  const strokeStyle = styles.getLineStringStrokeStyle(feature);
169
- if (strokeStyle.lineStyle !== 'solid')
170
- return false;
171
167
  return strokeStyle.opacity > 0 && strokeStyle.width > 0;
172
168
  }
173
169
  return false;
@@ -329,7 +329,8 @@ export class TableSource {
329
329
  }
330
330
  collector(rows, context) {
331
331
  const table = this.table;
332
- // Whether the solid lines and polygons of each child are left to the analytic drape
332
+ // Whether the lines and polygons (solid or dashed) of each child are left to the analytic
333
+ // drape
333
334
  const skipDraped = table.children.map((column) => context.skipDrapedFills === true && isDraped(column.type));
334
335
  const createdZoom = table.columns[CREATED_ZOOM_COLUMN];
335
336
  let next = 0;
@@ -497,18 +498,19 @@ class RowCollector {
497
498
  * A line over the coordinates `[start, end)` of the geometry column of the row (a dashed line
498
499
  * goes to immediate mode)
499
500
  */
500
- line(row, column, start, end, createdZoom, skipSolid) {
501
+ line(row, column, start, end, createdZoom, skipDraped) {
501
502
  if (end - start < 2)
502
503
  return;
503
504
  const slot = this.slots.line(column.type, this.slots.ruleColorOf(row));
504
505
  const stroke = slot.stroke;
506
+ // A line, solid or dashed, is drawn by the analytic drape as ground pixels (in this frame
507
+ // only)
508
+ if (skipDraped && stroke.opacity > 0 && stroke.width > 0)
509
+ return;
505
510
  if (stroke.lineStyle !== 'solid') {
506
511
  this.fallback(row);
507
512
  return;
508
513
  }
509
- // A solid line is drawn by the analytic drape as ground pixels (in this frame only)
510
- if (skipSolid && stroke.opacity > 0 && stroke.width > 0)
511
- return;
512
514
  const draft = this.draft;
513
515
  const fixed = createdZoom === undefined;
514
516
  let builder = fixed ? draft.packedFixedLines : draft.packedScaledLines;
@@ -528,21 +530,21 @@ class RowCollector {
528
530
  *
529
531
  * @param ringOffsets The offsets from a ring to its coordinates
530
532
  */
531
- polygon(row, column, ringStart, ringEnd, ringOffsets, partIndex, createdZoom, skipSolid) {
533
+ polygon(row, column, ringStart, ringEnd, ringOffsets, partIndex, createdZoom, skipDraped) {
532
534
  if (ringEnd <= ringStart)
533
535
  return;
534
536
  if (ringOffsets[ringStart + 1] - ringOffsets[ringStart] < 3)
535
537
  return;
536
538
  const slot = this.slots.polygon(column.type, this.slots.ruleColorOf(row));
539
+ if (!slot.hasStroke && slot.fillColor[3] <= 0)
540
+ return;
541
+ // The fill and the outline, solid or dashed, are drawn by the analytic drape as ground pixels
542
+ if (skipDraped)
543
+ return;
537
544
  if (slot.hasStroke && slot.strokeStyle.lineStyle !== 'solid') {
538
545
  this.fallback(row);
539
546
  return;
540
547
  }
541
- if (!slot.hasStroke && slot.fillColor[3] <= 0)
542
- return;
543
- // The solid fill and outline are drawn by the analytic drape as ground pixels
544
- if (skipSolid)
545
- return;
546
548
  const rings = new Array(ringEnd - ringStart);
547
549
  for (let r = ringStart; r < ringEnd; r++) {
548
550
  rings[r - ringStart] = column.positions(ringOffsets[r], ringOffsets[r + 1]);
@@ -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
  }
@@ -1,6 +1,6 @@
1
1
  // SPDX-FileCopyrightText: 2026 SAKAIDA Atsushi
2
2
  // SPDX-License-Identifier: AGPL-3.0-only
3
- import { resolvePixelRatio } from '../../shared/utils/pixel-ratio.js';
3
+ import { resolveContentPixelRatio, resolvePixelRatio } from '../../shared/utils/pixel-ratio.js';
4
4
  import { updateGlobeSubdivision } from '../globe-subdivision.js';
5
5
  import { beginRenderFrame } from '../shaders/frame.js';
6
6
  import { installAnchorProjector, uninstallAnchorProjector } from '../terrain/anchor.js';
@@ -350,6 +350,7 @@ export function createCustomLayer(deps) {
350
350
  zoom,
351
351
  rawZoom,
352
352
  dpr,
353
+ contentDpr: resolveContentPixelRatio(pixelRatio),
353
354
  terrainState,
354
355
  retainedAvailable: engine.storeRetainedCache !== null,
355
356
  overlayRenderers: dynamicOverlayRenderers,