@orbat-mapper/tactical-draw-adapter-maplibre 0.8.3 → 0.8.4

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
@@ -31,6 +31,12 @@ declare class PinResources {
31
31
  revision: number;
32
32
  private key;
33
33
  private resources;
34
+ /**
35
+ * Decoded SVGs by resource id. Decoding does not depend on scale or pixel
36
+ * ratio (the atlas scales on draw), so a roster change re-decodes only new
37
+ * icons. Pruned to the current roster in `setRoster`.
38
+ */
39
+ private readonly decoded;
34
40
  private pending?;
35
41
  private textures;
36
42
  private context?;
@@ -45,6 +51,7 @@ declare class PinResources {
45
51
  */
46
52
  setRoster(pins: readonly PinRender[], pixelRatio?: number): void;
47
53
  load(gl: WebGL2RenderingContext): Promise<PinResourceSnapshot | undefined>;
54
+ private decode;
48
55
  dispose(): void;
49
56
  }
50
57
  //#endregion
@@ -299,6 +306,8 @@ declare class MapLibreAdapter extends BaseMapAdapter<MapLibreLayerHandle, MapLib
299
306
  private map;
300
307
  private readonly viewChangeMode;
301
308
  private readonly pendingIconImages;
309
+ /** `icon-offset` per registered icon resource id, computed once per resource. */
310
+ private readonly iconOffsets;
302
311
  readonly scalesGroundPointSymbolsNatively = true;
303
312
  private terrainActive;
304
313
  private terrainBanding;
@@ -354,11 +363,12 @@ declare class MapLibreAdapter extends BaseMapAdapter<MapLibreLayerHandle, MapLib
354
363
  /**
355
364
  * Promote a source feature for MapLibre: stamp `_fid` (so data-driven paint
356
365
  * and picks can key on the caller's feature id) and, for a hatch-patterned
357
- * Polygon, `_fillPatternImageId` (registering the tile image on demand). At
358
- * most one object spread — a feature needing neither passes through
359
- * untouched.
366
+ * Polygon, `_fillPatternImageId` (registering the tile image on demand), and
367
+ * for an icon point, `_iconOffset` (cached per resource). At most one object
368
+ * spread — a feature needing none of these passes through untouched.
360
369
  */
361
370
  private promoteFeature;
371
+ private resolveIconOffset;
362
372
  /**
363
373
  * Resolve a hatch-patterned Polygon's `style.fillPattern`/`fillColor` to its
364
374
  * hatch-tile image id, registering the image on demand. Returns `undefined`
package/dist/index.mjs CHANGED
@@ -63,6 +63,12 @@ var PinResources = class {
63
63
  revision = 0;
64
64
  key = "";
65
65
  resources = /* @__PURE__ */ new Map();
66
+ /**
67
+ * Decoded SVGs by resource id. Decoding does not depend on scale or pixel
68
+ * ratio (the atlas scales on draw), so a roster change re-decodes only new
69
+ * icons. Pruned to the current roster in `setRoster`.
70
+ */
71
+ decoded = /* @__PURE__ */ new Map();
66
72
  pending;
67
73
  textures = [];
68
74
  context;
@@ -97,6 +103,7 @@ var PinResources = class {
97
103
  }
98
104
  const key = JSON.stringify([...resources].map(([id, value]) => [id, value.scale]).sort(([a], [b]) => a.localeCompare(b)));
99
105
  if (key === this.key) return;
106
+ for (const id of this.decoded.keys()) if (!resources.has(id)) this.decoded.delete(id);
100
107
  this.key = key;
101
108
  this.resources = resources;
102
109
  this.revision++;
@@ -114,7 +121,7 @@ var PinResources = class {
114
121
  const side = Math.max(256, Math.sqrt(area) * 1.3, ...sizes.flatMap((s) => [s.w, s.h]));
115
122
  const atlas = new PinAtlas(Math.min(2048, gl.getParameter(gl.MAX_TEXTURE_SIZE), 2 ** Math.ceil(Math.log2(side))));
116
123
  const icons = [...this.resources.values()];
117
- const images = await Promise.all(icons.map(({ pin }) => decodeIcon(pin.resource)));
124
+ const images = await Promise.all(icons.map(({ pin }) => this.decode(pin.resource)));
118
125
  if (this.disposed || revision !== this.revision) return;
119
126
  icons.forEach(({ pin, scale }, i) => atlas.add(pin.resource, scale, images[i]));
120
127
  const textures = [];
@@ -146,6 +153,17 @@ var PinResources = class {
146
153
  };
147
154
  })();
148
155
  }
156
+ decode(resource) {
157
+ let image = this.decoded.get(resource.id);
158
+ if (!image) {
159
+ image = decodeIcon(resource);
160
+ image.catch(() => {
161
+ if (this.decoded.get(resource.id) === image) this.decoded.delete(resource.id);
162
+ });
163
+ this.decoded.set(resource.id, image);
164
+ }
165
+ return image;
166
+ }
149
167
  dispose() {
150
168
  this.disposed = true;
151
169
  this.revision++;
@@ -154,6 +172,7 @@ var PinResources = class {
154
172
  for (const texture of this.textures) this.context?.deleteTexture(texture);
155
173
  this.textures = [];
156
174
  this.resources.clear();
175
+ this.decoded.clear();
157
176
  this.pending = void 0;
158
177
  }
159
178
  };
@@ -1484,6 +1503,8 @@ function buildIconSizing(props) {
1484
1503
  //#region src/ml-adapter.ts
1485
1504
  /** Feature property key for the resolved hatch-pattern image id (see `promoteFeature`). */
1486
1505
  const FILL_PATTERN_IMAGE_ID_PROPERTY = "_fillPatternImageId";
1506
+ /** Feature property key for a point symbol's `icon-offset` (see `promoteFeature`). */
1507
+ const ICON_OFFSET_PROPERTY = "_iconOffset";
1487
1508
  const DEFAULT_LINE_COLOR = "#000";
1488
1509
  const DEFAULT_LINE_WIDTH = 2;
1489
1510
  /**
@@ -1765,6 +1786,11 @@ function buildIconLayer(id, source, iconSize = buildIconSizeExpression()) {
1765
1786
  ],
1766
1787
  180 / Math.PI
1767
1788
  ],
1789
+ "icon-offset": [
1790
+ "coalesce",
1791
+ ["get", ICON_OFFSET_PROPERTY],
1792
+ ["literal", [0, 0]]
1793
+ ],
1768
1794
  "icon-rotation-alignment": "viewport",
1769
1795
  "icon-pitch-alignment": "viewport",
1770
1796
  "icon-allow-overlap": true,
@@ -1778,12 +1804,14 @@ function buildIconLayer(id, source, iconSize = buildIconSizeExpression()) {
1778
1804
  ] }
1779
1805
  };
1780
1806
  }
1781
- function centeredSvg(resource) {
1782
- const halfWidth = Math.max(resource.anchor[0], resource.width - resource.anchor[0]);
1783
- const halfHeight = Math.max(resource.anchor[1], resource.height - resource.anchor[1]);
1784
- const width = halfWidth * 2;
1785
- const height = halfHeight * 2;
1786
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><g transform="translate(${halfWidth - resource.anchor[0]} ${halfHeight - resource.anchor[1]})">${resource.svg}</g></svg>`;
1807
+ /** The icon image: the resource's own canvas, no padding (see `iconOffset`). */
1808
+ function iconSvg(resource) {
1809
+ const { width, height } = resource;
1810
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${resource.svg}</svg>`;
1811
+ }
1812
+ /** `icon-offset` placing a cropped icon's `resource.anchor` on the point (image px). */
1813
+ function iconOffset(resource) {
1814
+ return [resource.width / 2 - resource.anchor[0], resource.height / 2 - resource.anchor[1]];
1787
1815
  }
1788
1816
  /**
1789
1817
  * Per-feature icon size, using integer zoom stops:
@@ -2164,6 +2192,8 @@ var MapLibreAdapter = class extends BaseMapAdapter {
2164
2192
  map;
2165
2193
  viewChangeMode;
2166
2194
  pendingIconImages = /* @__PURE__ */ new Set();
2195
+ /** `icon-offset` per registered icon resource id, computed once per resource. */
2196
+ iconOffsets = /* @__PURE__ */ new Map();
2167
2197
  scalesGroundPointSymbolsNatively = true;
2168
2198
  terrainActive = false;
2169
2199
  terrainBanding;
@@ -2696,15 +2726,17 @@ var MapLibreAdapter = class extends BaseMapAdapter {
2696
2726
  /**
2697
2727
  * Promote a source feature for MapLibre: stamp `_fid` (so data-driven paint
2698
2728
  * and picks can key on the caller's feature id) and, for a hatch-patterned
2699
- * Polygon, `_fillPatternImageId` (registering the tile image on demand). At
2700
- * most one object spread — a feature needing neither passes through
2701
- * untouched.
2729
+ * Polygon, `_fillPatternImageId` (registering the tile image on demand), and
2730
+ * for an icon point, `_iconOffset` (cached per resource). At most one object
2731
+ * spread — a feature needing none of these passes through untouched.
2702
2732
  */
2703
2733
  promoteFeature(feature) {
2704
2734
  const extra = {};
2705
2735
  if (feature.id !== void 0) extra._fid = feature.id;
2706
2736
  const imageId = this.resolveFillPatternImage(feature);
2707
2737
  if (imageId !== void 0) extra[FILL_PATTERN_IMAGE_ID_PROPERTY] = imageId;
2738
+ const offset = this.resolveIconOffset(feature);
2739
+ if (offset !== void 0) extra[ICON_OFFSET_PROPERTY] = offset;
2708
2740
  if (Object.keys(extra).length === 0) return feature;
2709
2741
  return {
2710
2742
  ...feature,
@@ -2714,6 +2746,18 @@ var MapLibreAdapter = class extends BaseMapAdapter {
2714
2746
  }
2715
2747
  };
2716
2748
  }
2749
+ resolveIconOffset(feature) {
2750
+ const iconId = feature.properties?.iconId;
2751
+ if (typeof iconId !== "string") return void 0;
2752
+ let offset = this.iconOffsets.get(iconId);
2753
+ if (offset === void 0) {
2754
+ const resource = this.getIconResource(iconId);
2755
+ if (!resource) return void 0;
2756
+ offset = iconOffset(resource);
2757
+ this.iconOffsets.set(iconId, offset);
2758
+ }
2759
+ return offset;
2760
+ }
2717
2761
  /**
2718
2762
  * Resolve a hatch-patterned Polygon's `style.fillPattern`/`fillColor` to its
2719
2763
  * hatch-tile image id, registering the image on demand. Returns `undefined`
@@ -2748,11 +2792,12 @@ var MapLibreAdapter = class extends BaseMapAdapter {
2748
2792
  for (const handle of this.layers.values()) for (const bundle of handle.groupBundles.values()) if (bundle.requiredIconIds.has(resource.id)) this.updateGroupBundleSource(bundle);
2749
2793
  };
2750
2794
  image.onerror = () => this.pendingIconImages.delete(resource.id);
2751
- image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(centeredSvg(resource))}`;
2795
+ image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(iconSvg(resource))}`;
2752
2796
  }
2753
2797
  }
2754
2798
  removeIconResource(id) {
2755
2799
  this.pendingIconImages.delete(id);
2800
+ this.iconOffsets.delete(id);
2756
2801
  if (this.map.hasImage(id)) this.map.removeImage(id);
2757
2802
  }
2758
2803
  attachNativeEvent(type, onEvent) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbat-mapper/tactical-draw-adapter-maplibre",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
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",
@@ -37,8 +37,8 @@
37
37
  "access": "public"
38
38
  },
39
39
  "dependencies": {
40
- "@orbat-mapper/control-measures": "^0.32.0",
41
- "@orbat-mapper/tactical-draw": "^0.14.0"
40
+ "@orbat-mapper/tactical-draw": "^0.14.0",
41
+ "@orbat-mapper/control-measures": "^0.32.0"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "maplibre-gl": ">=5.8.0 <7.0.0"