@orbat-mapper/tactical-draw-adapter-maplibre 0.2.0-alpha.19 → 0.2.0-alpha.20

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/dist/index.d.mts CHANGED
@@ -1,14 +1,25 @@
1
- import { BaseMapAdapter, EditPointerDriver, FeatureOps, IconResource, LayerId, MapCursor, NativeEventEntry, PickHandler, PickOptions, PixelCoordinate, StyleOptions } from "@orbat-mapper/tactical-draw";
1
+ import { BaseMapAdapter, EditPointerDriver, FeatureGroup, IconResource, LayerId, MapCursor, NativeEventEntry, PickHandler, PickOptions, PixelCoordinate, StyleOptions } from "@orbat-mapper/tactical-draw";
2
2
  import maplibregl from "maplibre-gl";
3
- import { Position } from "geojson";
3
+ import { Feature, Position } from "geojson";
4
4
 
5
5
  //#region src/ml-adapter.d.ts
6
6
  interface MapLibreLayerHandle {
7
7
  sourceId: string;
8
8
  /** MapLibre layer ids created for this logical layer (fill, line, circle, symbol) */
9
9
  mlLayerIds: string[];
10
+ groupBundles: Map<string, MapLibreGroupBundle>;
11
+ groupOrder: string[];
12
+ nextGroupBundleId: number;
10
13
  style?: StyleOptions;
11
14
  }
15
+ type PaintChannel = "fill" | "fill-pattern" | "line" | "circle" | "icon" | "text";
16
+ interface MapLibreGroupBundle {
17
+ readonly sourceId: string;
18
+ readonly layerIds: Map<PaintChannel, string>;
19
+ features: readonly Feature[];
20
+ featureSignature: string;
21
+ requiredIconIds: ReadonlySet<string>;
22
+ }
12
23
  type MapLibreEventToken = (e: any) => void;
13
24
  interface MapLibreViewToken {
14
25
  fn: () => void;
@@ -25,17 +36,21 @@ declare class MapLibreAdapter extends BaseMapAdapter<MapLibreLayerHandle, MapLib
25
36
  private map;
26
37
  private readonly viewChangeMode;
27
38
  private readonly pendingIconImages;
28
- private readonly pendingLayerUpdates;
29
- private readonly deferredLayerRemovals;
30
39
  readonly scalesGroundPointSymbolsNatively = true;
31
40
  constructor(map: maplibregl.Map, options?: MapLibreAdapterOptions);
32
41
  private toContinuousLng;
33
42
  protected createLayer(layerId: LayerId, style?: StyleOptions): MapLibreLayerHandle;
34
43
  protected destroyLayer(handle: MapLibreLayerHandle): void;
35
- protected applyFeatureOps(layerId: LayerId, ops: FeatureOps): void;
36
- private updateSourceData;
37
- private flushDeferredLayerRemoval;
38
- deferLayerRemovalUntilLayerUpdate(layerId: LayerId, updatedLayerId: LayerId): void;
44
+ protected applyFeatureGroups(layerId: LayerId, groups: readonly FeatureGroup[]): void;
45
+ private createGroupBundle;
46
+ private destroyGroupBundle;
47
+ private clearGroupBundles;
48
+ private requiredIconIds;
49
+ private reconcileGroupChannels;
50
+ private buildGroupLayer;
51
+ private updateGroupBundleSource;
52
+ private orderGroupBundles;
53
+ private nativeLayerIds;
39
54
  /**
40
55
  * Promote a source feature for MapLibre: stamp `_fid` (so data-driven paint
41
56
  * and picks can key on the caller's feature id) and, for a hatch-patterned
@@ -62,6 +77,8 @@ declare class MapLibreAdapter extends BaseMapAdapter<MapLibreLayerHandle, MapLib
62
77
  private viewChangeEvents;
63
78
  createEditPointerDriver(): EditPointerDriver;
64
79
  setLayerStyle(layerId: LayerId, style: StyleOptions): void;
80
+ private legacyPaintChannel;
81
+ private applyChannelStyle;
65
82
  toLonLat(coordinate: number[]): Position;
66
83
  fromLonLat(position: Position): number[];
67
84
  getPixelFromCoordinate(position: Position): PixelCoordinate | null;
package/dist/index.mjs CHANGED
@@ -8,6 +8,57 @@ const DEFAULT_LINE_WIDTH = 2;
8
8
  function isHatchPattern(value) {
9
9
  return HATCH_PATTERNS.includes(value);
10
10
  }
11
+ const PAINT_CHANNELS = [
12
+ "fill",
13
+ "fill-pattern",
14
+ "line",
15
+ "circle",
16
+ "icon",
17
+ "text"
18
+ ];
19
+ function firstBundleLayerId(bundle) {
20
+ for (const channel of PAINT_CHANNELS) {
21
+ const layerId = bundle.layerIds.get(channel);
22
+ if (layerId) return layerId;
23
+ }
24
+ }
25
+ /**
26
+ * Find the largest subset of bundles whose relative order already matches the
27
+ * desired stack. Reorders then move only the remaining bundles.
28
+ */
29
+ function longestStableGroupIds(previousGroupIds, desiredGroupIds) {
30
+ const previousIndices = new Map(previousGroupIds.map((groupId, index) => [groupId, index]));
31
+ const sequence = desiredGroupIds.flatMap((groupId) => {
32
+ const previousIndex = previousIndices.get(groupId);
33
+ return previousIndex === void 0 ? [] : [{
34
+ groupId,
35
+ previousIndex
36
+ }];
37
+ });
38
+ const tails = [];
39
+ const tailSequenceIndices = [];
40
+ const predecessors = new Array(sequence.length).fill(-1);
41
+ for (let sequenceIndex = 0; sequenceIndex < sequence.length; sequenceIndex++) {
42
+ const previousIndex = sequence[sequenceIndex].previousIndex;
43
+ let low = 0;
44
+ let high = tails.length;
45
+ while (low < high) {
46
+ const middle = low + high >>> 1;
47
+ if (tails[middle] < previousIndex) low = middle + 1;
48
+ else high = middle;
49
+ }
50
+ if (low > 0) predecessors[sequenceIndex] = tailSequenceIndices[low - 1];
51
+ tails[low] = previousIndex;
52
+ tailSequenceIndices[low] = sequenceIndex;
53
+ }
54
+ const stableGroupIds = /* @__PURE__ */ new Set();
55
+ let sequenceIndex = tailSequenceIndices.at(-1);
56
+ while (sequenceIndex !== void 0 && sequenceIndex >= 0) {
57
+ stableGroupIds.add(sequence[sequenceIndex].groupId);
58
+ sequenceIndex = predecessors[sequenceIndex];
59
+ }
60
+ return stableGroupIds;
61
+ }
11
62
  function isPromiseLike(value) {
12
63
  return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
13
64
  }
@@ -595,12 +646,46 @@ function buildLinePaint(style) {
595
646
  ]
596
647
  };
597
648
  }
649
+ function geometryIncludes(feature, geometryTypes) {
650
+ const geometry = feature.geometry;
651
+ if (!geometry) return false;
652
+ if (geometry.type === "GeometryCollection") return geometry.geometries.some((candidate) => geometryTypes.has(candidate.type));
653
+ return geometryTypes.has(geometry.type);
654
+ }
655
+ const POLYGON_GEOMETRIES = new Set(["Polygon", "MultiPolygon"]);
656
+ const LINE_GEOMETRIES = new Set([
657
+ "LineString",
658
+ "MultiLineString",
659
+ "Polygon",
660
+ "MultiPolygon"
661
+ ]);
662
+ const POINT_GEOMETRIES = new Set(["Point", "MultiPoint"]);
663
+ function featureStyle(feature) {
664
+ const style = feature.properties?.style;
665
+ return style && typeof style === "object" ? style : {};
666
+ }
667
+ function groupPaintChannels(features, style) {
668
+ const channels = /* @__PURE__ */ new Set();
669
+ for (const feature of features) {
670
+ if (geometryIncludes(feature, POLYGON_GEOMETRIES)) {
671
+ const fillPattern = featureStyle(feature).fillPattern;
672
+ if (featureStyle(feature).fillColor !== void 0 || style?.fillColor !== void 0 || isHatchPattern(fillPattern)) channels.add(isHatchPattern(fillPattern) ? "fill-pattern" : "fill");
673
+ }
674
+ if (geometryIncludes(feature, LINE_GEOMETRIES)) channels.add("line");
675
+ if (!geometryIncludes(feature, POINT_GEOMETRIES)) continue;
676
+ const properties = feature.properties ?? {};
677
+ const hasIcon = properties.iconId != null;
678
+ const hasText = style?.text !== void 0 && (style.text.labelProperty ? properties[style.text.labelProperty] != null : style.text.text !== void 0);
679
+ if (hasIcon) channels.add("icon");
680
+ if (hasText) channels.add("text");
681
+ if (!hasIcon && !hasText) channels.add("circle");
682
+ }
683
+ return channels;
684
+ }
598
685
  var MapLibreAdapter = class extends BaseMapAdapter {
599
686
  map;
600
687
  viewChangeMode;
601
688
  pendingIconImages = /* @__PURE__ */ new Set();
602
- pendingLayerUpdates = /* @__PURE__ */ new Map();
603
- deferredLayerRemovals = /* @__PURE__ */ new Map();
604
689
  scalesGroundPointSymbolsNatively = true;
605
690
  constructor(map, options = {}) {
606
691
  super();
@@ -681,71 +766,176 @@ var MapLibreAdapter = class extends BaseMapAdapter {
681
766
  return {
682
767
  sourceId,
683
768
  mlLayerIds,
769
+ groupBundles: /* @__PURE__ */ new Map(),
770
+ groupOrder: [],
771
+ nextGroupBundleId: 0,
684
772
  style
685
773
  };
686
774
  }
687
775
  destroyLayer(handle) {
688
- for (const [layerId, layerHandle] of this.layers) {
689
- if (layerHandle !== handle) continue;
690
- this.pendingLayerUpdates.get(layerId)?.cancel();
691
- this.pendingLayerUpdates.delete(layerId);
692
- this.deferredLayerRemovals.delete(layerId);
693
- break;
694
- }
776
+ this.clearGroupBundles(handle);
695
777
  for (const mlId of handle.mlLayerIds) if (this.map.getLayer(mlId)) this.map.removeLayer(mlId);
696
778
  if (this.map.getSource(handle.sourceId)) this.map.removeSource(handle.sourceId);
697
779
  }
698
- applyFeatureOps(layerId, ops) {
699
- const deferred = this.deferredLayerRemovals.get(layerId);
700
- const isPureRemoval = ops.adds.length === 0 && ops.updates.length === 0 && (ops.removes?.length ?? 0) > 0;
701
- if (deferred && isPureRemoval) {
702
- deferred.after.then(() => this.flushDeferredLayerRemoval(layerId, deferred), () => this.flushDeferredLayerRemoval(layerId, deferred));
703
- return;
704
- }
705
- if (deferred) this.deferredLayerRemovals.delete(layerId);
706
- this.updateSourceData(layerId, ops.full);
707
- }
708
- updateSourceData(layerId, features) {
780
+ applyFeatureGroups(layerId, groups) {
709
781
  const handle = this.layers.get(layerId);
710
782
  if (!handle) return;
711
- const source = this.map.getSource(handle.sourceId);
712
- if (!source) return;
713
- const promotedFeatures = features.map((f) => this.promoteFeature(f));
714
- let resolveSourceData;
715
- const sourceDataCompletion = new Promise((resolve) => {
716
- resolveSourceData = resolve;
783
+ const desiredGroupIds = new Set(groups.map((group) => group.id));
784
+ const previousGroupOrder = handle.groupOrder.filter((groupId) => desiredGroupIds.has(groupId));
785
+ const bundlesNeedingOrder = /* @__PURE__ */ new Set();
786
+ for (const [groupId, bundle] of handle.groupBundles) {
787
+ if (desiredGroupIds.has(groupId)) continue;
788
+ this.destroyGroupBundle(bundle);
789
+ handle.groupBundles.delete(groupId);
790
+ }
791
+ for (const group of groups) {
792
+ let bundle = handle.groupBundles.get(group.id);
793
+ const isNewBundle = bundle === void 0;
794
+ if (!bundle) {
795
+ bundle = this.createGroupBundle(layerId, handle);
796
+ handle.groupBundles.set(group.id, bundle);
797
+ bundlesNeedingOrder.add(group.id);
798
+ }
799
+ const featureSignature = JSON.stringify(group.features);
800
+ const contentChanged = isNewBundle || bundle.featureSignature !== featureSignature;
801
+ const requiredIconIds = this.requiredIconIds(group);
802
+ const resourcesChanged = bundle.requiredIconIds.size !== requiredIconIds.size || [...bundle.requiredIconIds].some((id) => !requiredIconIds.has(id));
803
+ bundle.features = group.features;
804
+ bundle.featureSignature = featureSignature;
805
+ bundle.requiredIconIds = requiredIconIds;
806
+ if (this.reconcileGroupChannels(handle, bundle, groupPaintChannels(group.features, handle.style))) bundlesNeedingOrder.add(group.id);
807
+ if (contentChanged || resourcesChanged) this.updateGroupBundleSource(bundle);
808
+ }
809
+ handle.groupOrder = groups.map((group) => group.id);
810
+ this.orderGroupBundles(handle, previousGroupOrder, handle.groupOrder, bundlesNeedingOrder);
811
+ }
812
+ createGroupBundle(layerId, handle) {
813
+ const sourceId = `${layerId}-group-${++handle.nextGroupBundleId}-src`;
814
+ this.map.addSource(sourceId, {
815
+ type: "geojson",
816
+ data: emptyFC(),
817
+ promoteId: "_fid"
717
818
  });
718
- const onSourceData = (event) => {
719
- if (event.sourceDataType !== "content") return;
720
- source.off("data", onSourceData);
721
- resolveSourceData();
819
+ return {
820
+ sourceId,
821
+ layerIds: /* @__PURE__ */ new Map(),
822
+ features: [],
823
+ featureSignature: "[]",
824
+ requiredIconIds: /* @__PURE__ */ new Set()
722
825
  };
723
- source.on("data", onSourceData);
826
+ }
827
+ destroyGroupBundle(bundle) {
828
+ for (const layerId of bundle.layerIds.values()) if (this.map.getLayer(layerId)) this.map.removeLayer(layerId);
829
+ if (this.map.getSource(bundle.sourceId)) this.map.removeSource(bundle.sourceId);
830
+ }
831
+ clearGroupBundles(handle) {
832
+ for (const bundle of handle.groupBundles.values()) this.destroyGroupBundle(bundle);
833
+ handle.groupBundles.clear();
834
+ handle.groupOrder = [];
835
+ }
836
+ requiredIconIds(group) {
837
+ const referenced = new Set(group.features.flatMap((feature) => {
838
+ const iconId = feature.properties?.iconId;
839
+ return typeof iconId === "string" ? [iconId] : [];
840
+ }));
841
+ return new Set(group.iconResources.map((resource) => resource.id).filter((resourceId) => referenced.has(resourceId)));
842
+ }
843
+ reconcileGroupChannels(handle, bundle, desiredChannels) {
844
+ let changed = false;
845
+ for (const channel of PAINT_CHANNELS) {
846
+ const layerId = bundle.layerIds.get(channel);
847
+ if (layerId && !desiredChannels.has(channel)) {
848
+ if (this.map.getLayer(layerId)) this.map.removeLayer(layerId);
849
+ bundle.layerIds.delete(channel);
850
+ changed = true;
851
+ } else if (!layerId && desiredChannels.has(channel)) {
852
+ const nextLayerId = `${bundle.sourceId}-${channel}`;
853
+ this.map.addLayer(this.buildGroupLayer(nextLayerId, bundle.sourceId, channel, handle.style), handle.mlLayerIds[0]);
854
+ bundle.layerIds.set(channel, nextLayerId);
855
+ changed = true;
856
+ }
857
+ }
858
+ return changed;
859
+ }
860
+ buildGroupLayer(id, sourceId, channel, style) {
861
+ switch (channel) {
862
+ case "fill": return {
863
+ id,
864
+ type: "fill",
865
+ source: sourceId,
866
+ filter: buildFillFilter(false),
867
+ paint: buildFillPaint(style)
868
+ };
869
+ case "fill-pattern": return {
870
+ id,
871
+ type: "fill",
872
+ source: sourceId,
873
+ filter: buildFillFilter(true),
874
+ paint: FILL_PATTERN_PAINT
875
+ };
876
+ case "line": return {
877
+ id,
878
+ type: "line",
879
+ source: sourceId,
880
+ filter: [
881
+ "any",
882
+ [
883
+ "==",
884
+ "$type",
885
+ "LineString"
886
+ ],
887
+ [
888
+ "==",
889
+ "$type",
890
+ "Polygon"
891
+ ]
892
+ ],
893
+ paint: buildLinePaint(style),
894
+ layout: {
895
+ "line-cap": "round",
896
+ "line-join": "round"
897
+ }
898
+ };
899
+ case "circle": return {
900
+ id,
901
+ type: "circle",
902
+ source: sourceId,
903
+ filter: buildCirclePointFilter(style),
904
+ paint: buildCirclePaint(style)
905
+ };
906
+ case "icon": return buildIconLayer(id, sourceId);
907
+ case "text":
908
+ if (!style?.text) throw new Error("Cannot create a MapLibre text channel without text style");
909
+ return buildSymbolLayer(id, sourceId, style.text);
910
+ }
911
+ }
912
+ updateGroupBundleSource(bundle) {
913
+ const source = this.map.getSource(bundle.sourceId);
914
+ if (!source) return;
915
+ const features = [...bundle.requiredIconIds].every((id) => this.map.hasImage(id)) ? bundle.features.map((feature) => this.promoteFeature(feature)) : [];
724
916
  const result = source.setData({
725
917
  type: "FeatureCollection",
726
- features: promotedFeatures
918
+ features
727
919
  }, true);
728
- const supportsCompletionPromise = isPromiseLike(result);
729
- if (supportsCompletionPromise) source.off("data", onSourceData);
730
- const completion = supportsCompletionPromise ? Promise.resolve(result) : sourceDataCompletion;
731
- const pending = {
732
- completion,
733
- cancel() {
734
- source.off("data", onSourceData);
735
- }
736
- };
737
- this.pendingLayerUpdates.set(layerId, pending);
738
- completion.catch(() => {});
920
+ if (isPromiseLike(result)) Promise.resolve(result).catch(() => {});
739
921
  }
740
- flushDeferredLayerRemoval(layerId, deferred) {
741
- if (this.deferredLayerRemovals.get(layerId) !== deferred) return;
742
- this.deferredLayerRemovals.delete(layerId);
743
- this.updateSourceData(layerId, this.getLayerFeatures(layerId));
922
+ orderGroupBundles(handle, previousGroupIds, desiredGroupIds, forcedGroupIds) {
923
+ let beforeId = handle.mlLayerIds[0];
924
+ if (!beforeId) return;
925
+ const stableGroupIds = longestStableGroupIds(previousGroupIds, desiredGroupIds);
926
+ for (let index = desiredGroupIds.length - 1; index >= 0; index--) {
927
+ const groupId = desiredGroupIds[index];
928
+ const bundle = handle.groupBundles.get(groupId);
929
+ if (!bundle) continue;
930
+ if (!stableGroupIds.has(groupId) || forcedGroupIds.has(groupId)) for (const channel of PAINT_CHANNELS) {
931
+ const layerId = bundle.layerIds.get(channel);
932
+ if (layerId && this.map.getLayer(layerId)) this.map.moveLayer(layerId, beforeId);
933
+ }
934
+ beforeId = firstBundleLayerId(bundle) ?? beforeId;
935
+ }
744
936
  }
745
- deferLayerRemovalUntilLayerUpdate(layerId, updatedLayerId) {
746
- const pending = this.pendingLayerUpdates.get(updatedLayerId);
747
- if (!pending || !this.layers.has(layerId)) return;
748
- this.deferredLayerRemovals.set(layerId, { after: pending.completion });
937
+ nativeLayerIds(handle) {
938
+ return [...handle.mlLayerIds, ...[...handle.groupBundles.values()].flatMap((bundle) => [...bundle.layerIds.values()])];
749
939
  }
750
940
  /**
751
941
  * Promote a source feature for MapLibre: stamp `_fid` (so data-driven paint
@@ -799,6 +989,7 @@ var MapLibreAdapter = class extends BaseMapAdapter {
799
989
  if (!this.pendingIconImages.has(resource.id)) return;
800
990
  this.pendingIconImages.delete(resource.id);
801
991
  if (!this.map.hasImage(resource.id)) this.map.addImage(resource.id, image);
992
+ for (const handle of this.layers.values()) for (const bundle of handle.groupBundles.values()) if (bundle.requiredIconIds.has(resource.id)) this.updateGroupBundleSource(bundle);
802
993
  };
803
994
  image.onerror = () => this.pendingIconImages.delete(resource.id);
804
995
  image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(centeredSvg(resource))}`;
@@ -956,39 +1147,72 @@ var MapLibreAdapter = class extends BaseMapAdapter {
956
1147
  setLayerStyle(layerId, style) {
957
1148
  const handle = this.layers.get(layerId);
958
1149
  if (!handle) return;
959
- handle.style = style;
1150
+ const resolvedStyle = {
1151
+ ...handle.style,
1152
+ ...style,
1153
+ ...style.text ? { text: {
1154
+ ...handle.style?.text,
1155
+ ...style.text
1156
+ } } : {}
1157
+ };
1158
+ handle.style = resolvedStyle;
960
1159
  for (const mlId of handle.mlLayerIds) {
961
- const layer = this.map.getLayer(mlId);
962
- if (!layer) continue;
963
- const type = layer.type;
964
- if (type === "fill") {
965
- if (mlId.endsWith("-fill-pattern")) continue;
966
- this.map.setPaintProperty(mlId, "fill-color", buildFillPaint(style)["fill-color"]);
967
- } else if (type === "line") {
968
- const paint = buildLinePaint(style);
969
- if (style.strokeColor !== void 0) this.map.setPaintProperty(mlId, "line-color", paint["line-color"]);
970
- if (style.strokeWidth !== void 0) this.map.setPaintProperty(mlId, "line-width", paint["line-width"]);
971
- if (style.strokeDash !== void 0) this.map.setPaintProperty(mlId, "line-dasharray", paint["line-dasharray"]);
972
- } else if (type === "circle") {
973
- const paint = buildCirclePaint(style);
974
- this.map.setPaintProperty(mlId, "circle-radius", paint["circle-radius"]);
975
- this.map.setPaintProperty(mlId, "circle-color", paint["circle-color"]);
976
- this.map.setPaintProperty(mlId, "circle-stroke-color", paint["circle-stroke-color"]);
977
- this.map.setPaintProperty(mlId, "circle-stroke-width", paint["circle-stroke-width"]);
978
- this.map.setFilter(mlId, buildCirclePointFilter(style));
979
- } else if (type === "symbol" && style.text) {
980
- if (style.text.text || style.text.labelProperty) this.map.setLayoutProperty(mlId, "text-field", buildTextFieldExpression(style.text));
981
- if (style.text.fillColor) this.map.setPaintProperty(mlId, "text-color", style.text.fillColor);
982
- if (style.text.strokeColor) this.map.setPaintProperty(mlId, "text-halo-color", buildTextHaloColorExpression(style.text));
983
- if (style.text.strokeWidth !== void 0) this.map.setPaintProperty(mlId, "text-halo-width", buildTextHaloWidthExpression(style.text));
984
- }
1160
+ const channel = this.legacyPaintChannel(mlId);
1161
+ if (channel) this.applyChannelStyle(mlId, channel, style, resolvedStyle);
985
1162
  }
1163
+ const bundlesNeedingOrder = /* @__PURE__ */ new Set();
1164
+ for (const [groupId, bundle] of handle.groupBundles) if (this.reconcileGroupChannels(handle, bundle, groupPaintChannels(bundle.features, resolvedStyle))) bundlesNeedingOrder.add(groupId);
1165
+ this.orderGroupBundles(handle, handle.groupOrder, handle.groupOrder, bundlesNeedingOrder);
1166
+ for (const bundle of handle.groupBundles.values()) for (const [channel, mlId] of bundle.layerIds) this.applyChannelStyle(mlId, channel, style, resolvedStyle);
986
1167
  if (style.text && !handle.mlLayerIds.some((id) => id.endsWith("-symbol"))) {
987
1168
  const symbolId = `${layerId}-symbol`;
988
- this.map.addLayer(buildSymbolLayer(symbolId, handle.sourceId, style.text));
1169
+ this.map.addLayer(buildSymbolLayer(symbolId, handle.sourceId, resolvedStyle.text));
989
1170
  handle.mlLayerIds.push(symbolId);
990
1171
  }
991
1172
  }
1173
+ legacyPaintChannel(layerId) {
1174
+ if (layerId.endsWith("-fill-pattern")) return "fill-pattern";
1175
+ if (layerId.endsWith("-fill")) return "fill";
1176
+ if (layerId.endsWith("-line")) return "line";
1177
+ if (layerId.endsWith("-circle")) return "circle";
1178
+ if (layerId.endsWith("-icon")) return "icon";
1179
+ if (layerId.endsWith("-symbol")) return "text";
1180
+ }
1181
+ applyChannelStyle(layerId, channel, styleUpdate, resolvedStyle) {
1182
+ if (!this.map.getLayer(layerId)) return;
1183
+ switch (channel) {
1184
+ case "fill":
1185
+ if (styleUpdate.fillColor === void 0) break;
1186
+ this.map.setPaintProperty(layerId, "fill-color", buildFillPaint(resolvedStyle)["fill-color"]);
1187
+ break;
1188
+ case "fill-pattern": break;
1189
+ case "line": {
1190
+ const paint = buildLinePaint(resolvedStyle);
1191
+ if (styleUpdate.strokeColor !== void 0) this.map.setPaintProperty(layerId, "line-color", paint["line-color"]);
1192
+ if (styleUpdate.strokeWidth !== void 0) this.map.setPaintProperty(layerId, "line-width", paint["line-width"]);
1193
+ if (styleUpdate.strokeDash !== void 0) this.map.setPaintProperty(layerId, "line-dasharray", paint["line-dasharray"]);
1194
+ break;
1195
+ }
1196
+ case "circle": {
1197
+ if (!(styleUpdate.pointRadius !== void 0 || styleUpdate.pointFillColor !== void 0 || styleUpdate.fillColor !== void 0 || styleUpdate.strokeColor !== void 0 || styleUpdate.strokeWidth !== void 0 || styleUpdate.text !== void 0)) break;
1198
+ const paint = buildCirclePaint(resolvedStyle);
1199
+ this.map.setPaintProperty(layerId, "circle-radius", paint["circle-radius"]);
1200
+ this.map.setPaintProperty(layerId, "circle-color", paint["circle-color"]);
1201
+ this.map.setPaintProperty(layerId, "circle-stroke-color", paint["circle-stroke-color"]);
1202
+ this.map.setPaintProperty(layerId, "circle-stroke-width", paint["circle-stroke-width"]);
1203
+ this.map.setFilter(layerId, buildCirclePointFilter(resolvedStyle));
1204
+ break;
1205
+ }
1206
+ case "icon": break;
1207
+ case "text":
1208
+ if (!styleUpdate.text || !resolvedStyle.text) break;
1209
+ if (styleUpdate.text.text || styleUpdate.text.labelProperty) this.map.setLayoutProperty(layerId, "text-field", buildTextFieldExpression(resolvedStyle.text));
1210
+ if (styleUpdate.text.fillColor) this.map.setPaintProperty(layerId, "text-color", resolvedStyle.text.fillColor);
1211
+ if (styleUpdate.text.strokeColor) this.map.setPaintProperty(layerId, "text-halo-color", buildTextHaloColorExpression(resolvedStyle.text));
1212
+ if (styleUpdate.text.strokeWidth !== void 0) this.map.setPaintProperty(layerId, "text-halo-width", buildTextHaloWidthExpression(resolvedStyle.text));
1213
+ break;
1214
+ }
1215
+ }
992
1216
  toLonLat(coordinate) {
993
1217
  return unproject(coordinate[0], coordinate[1]);
994
1218
  }
@@ -1048,7 +1272,7 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1048
1272
  const tol = pickHitTolerance(originalEvent);
1049
1273
  const { x, y } = e.point;
1050
1274
  const bbox = [[x - tol, y - tol], [x + tol, y + tol]];
1051
- const relevantFeatures = mergeNativeAndCachedPickFeatures(this.map.queryRenderedFeatures(bbox, { layers: handle.mlLayerIds }).map((feature) => ({
1275
+ const relevantFeatures = mergeNativeAndCachedPickFeatures(this.map.queryRenderedFeatures(bbox, { layers: this.nativeLayerIds(handle) }).map((feature) => ({
1052
1276
  type: "Feature",
1053
1277
  id: feature.id,
1054
1278
  geometry: feature.geometry,
@@ -1058,10 +1282,12 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1058
1282
  const candidate = evaluatePickCandidates(relevantFeatures.map(({ feature, nativeHit, visualOrder }) => {
1059
1283
  const iconId = feature.properties?.iconId;
1060
1284
  const icon = typeof iconId === "string" ? this.getIconResource(iconId) : void 0;
1285
+ const featureGroupOrder = this.getFeatureGroupOrder(layerId, feature.id);
1061
1286
  return {
1062
1287
  feature,
1063
1288
  visualOrder,
1064
1289
  nativeHit,
1290
+ ...featureGroupOrder !== void 0 ? { featureGroupOrder } : {},
1065
1291
  ...icon ? { icon } : {},
1066
1292
  layerStyle: {
1067
1293
  ...handle.style,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbat-mapper/tactical-draw-adapter-maplibre",
3
- "version": "0.2.0-alpha.19",
3
+ "version": "0.2.0-alpha.20",
4
4
  "description": "MapLibre GL JS engine adapter for @orbat-mapper/tactical-draw. Draw and edit tactical control measures (MIL-STD-2525 / APP-6) on a MapLibre map.",
5
5
  "license": "MIT",
6
6
  "author": "Orbat Mapper",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@orbat-mapper/control-measures": "^0.2.0-alpha.25",
41
- "@orbat-mapper/tactical-draw": "^0.2.0-alpha.34"
41
+ "@orbat-mapper/tactical-draw": "^0.2.0-alpha.35"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "maplibre-gl": ">=5.8.0 <6.0.0"