@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.
- package/CHANGELOG.md +119 -1
- package/README.ja.md +48 -35
- package/README.md +52 -36
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/api/datasets.d.ts +5 -0
- package/dist/api/draw.d.ts +6 -4
- package/dist/api/draw.js +6 -4
- package/dist/api/events.d.ts +10 -0
- package/dist/api/impl/create-draw.js +1 -1
- package/dist/api/impl/datasets.js +1 -0
- package/dist/api/impl/events.js +2 -1
- package/dist/api/impl/extension-host.js +4 -1
- package/dist/api/impl/options.js +58 -29
- package/dist/dataset/dataset.js +3 -0
- package/dist/dataset/retained.js +19 -18
- package/dist/dataset/selection.d.ts +3 -3
- package/dist/dataset/selection.js +3 -7
- package/dist/dataset/table-source.js +13 -11
- package/dist/dataset/types.d.ts +2 -0
- package/dist/dispatcher/input-router.js +2 -1
- package/dist/dispatcher/normalizer.js +19 -2
- package/dist/geometry/geojson.d.ts +4 -0
- package/dist/geometry/geojson.js +20 -11
- package/dist/index.d.ts +14 -0
- package/dist/view/layer/attach.d.ts +11 -5
- package/dist/view/layer/attach.js +85 -9
- package/dist/view/layer/custom-layer.js +2 -1
- package/dist/view/layer/drape-planner.d.ts +3 -0
- package/dist/view/layer/drape-planner.js +9 -7
- package/dist/view/layer/frame-render.js +2 -3
- package/dist/view/layer/frame-state.d.ts +3 -0
- package/dist/view/layer/frame-state.js +2 -1
- package/dist/view/layer/gl-state.d.ts +2 -3
- package/dist/view/layer/gl-state.js +2 -3
- package/dist/view/renderers/line/dash.d.ts +27 -0
- package/dist/view/renderers/line/dash.js +23 -12
- package/dist/view/terrain/drape/bin-store.d.ts +11 -2
- package/dist/view/terrain/drape/bin-store.js +26 -2
- package/dist/view/terrain/drape/binning.d.ts +32 -0
- package/dist/view/terrain/drape/binning.js +15 -7
- package/dist/view/terrain/drape/geometry.d.ts +27 -0
- package/dist/view/terrain/drape/geometry.js +57 -6
- package/dist/view/terrain/drape/pass.d.ts +8 -3
- package/dist/view/terrain/drape/pass.js +39 -28
- package/dist/view/terrain/drape/renderer.d.ts +13 -0
- package/dist/view/terrain/drape/renderer.js +157 -30
- package/package.json +22 -10
package/dist/api/impl/options.js
CHANGED
|
@@ -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))
|
package/dist/dataset/dataset.js
CHANGED
package/dist/dataset/retained.js
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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,
|
|
760
|
+
collectLine(feature, coords, draft, styles, skipDraped);
|
|
761
761
|
}
|
|
762
762
|
return;
|
|
763
763
|
case 'Polygon':
|
|
764
|
-
collectPolygon(feature, coordinatesOf(feature), draft, styles, 0,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
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
|
|
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,
|
|
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,
|
|
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]);
|
package/dist/dataset/types.d.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
*/
|
package/dist/geometry/geojson.js
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
9
|
-
* style finishes loading (before `style.load` and `load`) and after changes to
|
|
10
|
-
* slots are also restored after
|
|
11
|
-
*
|
|
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
|
|
21
|
-
* style finishes loading (before `style.load` and `load`) and after changes to
|
|
22
|
-
* slots are also restored after
|
|
23
|
-
*
|
|
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
|
-
|
|
37
|
-
|
|
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,
|