@orbat-mapper/tactical-draw-adapter-maplibre 0.3.22 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +141 -3
- package/dist/index.mjs +1190 -7
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,1035 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import * as maplibregl from "maplibre-gl";
|
|
2
|
+
import { LngLat, MercatorCoordinate } from "maplibre-gl";
|
|
3
|
+
import { BaseMapAdapter, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, HATCH_PATTERNS, PointerCallbackHub, TEXT_HALO_COLOR, TOUCH_HIT_TOLERANCE_PX, bindAbortable, evaluatePickCandidates, fillPatternImageId, makeFillPatternImage, mergeNativeAndCachedPickFeatures, nearestWorldCopyLng, parseColor, pickHitTolerance, pickPointerType, resolveTextHaloWidth } from "@orbat-mapper/tactical-draw";
|
|
4
|
+
import { EARTH_RADIUS, project, unproject } from "@orbat-mapper/control-measures";
|
|
5
|
+
//#region src/pin-atlas.ts
|
|
6
|
+
/**
|
|
7
|
+
* Rasterize one icon's SVG. Kept separate from packing so a whole roster can
|
|
8
|
+
* decode in parallel — shelf placement stays order-dependent, decoding is not.
|
|
9
|
+
*/
|
|
10
|
+
async function decodeIcon(resource) {
|
|
11
|
+
const image = new Image();
|
|
12
|
+
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(resource.svg)}`;
|
|
13
|
+
await image.decode();
|
|
14
|
+
return image;
|
|
15
|
+
}
|
|
16
|
+
/** Paged shelves avoid one texture per symbol and do not reorder draw instances. */
|
|
17
|
+
var PinAtlas = class {
|
|
18
|
+
size;
|
|
19
|
+
entries = /* @__PURE__ */ new Map();
|
|
20
|
+
pages = [];
|
|
21
|
+
x = 0;
|
|
22
|
+
y = 0;
|
|
23
|
+
rowHeight = 0;
|
|
24
|
+
constructor(size = 2048) {
|
|
25
|
+
this.size = size;
|
|
26
|
+
}
|
|
27
|
+
add(resource, scale, image) {
|
|
28
|
+
if (this.entries.has(resource.id)) return;
|
|
29
|
+
const width = Math.min(this.size - 4, Math.max(1, Math.ceil(resource.width * scale)));
|
|
30
|
+
const height = Math.min(this.size - 4, Math.max(1, Math.ceil(resource.height * scale)));
|
|
31
|
+
if (this.x + width + 4 > this.size) {
|
|
32
|
+
this.x = 0;
|
|
33
|
+
this.y += this.rowHeight;
|
|
34
|
+
this.rowHeight = 0;
|
|
35
|
+
}
|
|
36
|
+
if (!this.pages.length || this.y + height + 4 > this.size) {
|
|
37
|
+
const canvas = document.createElement("canvas");
|
|
38
|
+
canvas.width = canvas.height = this.size;
|
|
39
|
+
this.pages.push(canvas);
|
|
40
|
+
this.x = this.y = this.rowHeight = 0;
|
|
41
|
+
}
|
|
42
|
+
const canvas = this.pages[this.pages.length - 1];
|
|
43
|
+
const x = this.x + 2, y = this.y + 2;
|
|
44
|
+
canvas.getContext("2d").drawImage(image, x, y, width, height);
|
|
45
|
+
this.entries.set(resource.id, {
|
|
46
|
+
page: this.pages.length - 1,
|
|
47
|
+
uv: [
|
|
48
|
+
x / this.size,
|
|
49
|
+
y / this.size,
|
|
50
|
+
(x + width) / this.size,
|
|
51
|
+
(y + height) / this.size
|
|
52
|
+
]
|
|
53
|
+
});
|
|
54
|
+
this.x += width + 4;
|
|
55
|
+
this.rowHeight = Math.max(this.rowHeight, height + 4);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/pin-resources.ts
|
|
60
|
+
/** One atlas roster per logical layer, shared across ordinary-graphic barriers. */
|
|
61
|
+
var PinResources = class {
|
|
62
|
+
map;
|
|
63
|
+
revision = 0;
|
|
64
|
+
key = "";
|
|
65
|
+
resources = /* @__PURE__ */ new Map();
|
|
66
|
+
pending;
|
|
67
|
+
textures = [];
|
|
68
|
+
context;
|
|
69
|
+
disposed = false;
|
|
70
|
+
needsDepth = false;
|
|
71
|
+
invalidateDepth = () => {
|
|
72
|
+
if (this.needsDepth && this.map.getTerrain() && this.map.getProjection()?.type === "globe") this.map.painter.terrainFacilitator.depthDirty = true;
|
|
73
|
+
};
|
|
74
|
+
restore = () => {
|
|
75
|
+
this.revision++;
|
|
76
|
+
this.pending = void 0;
|
|
77
|
+
this.textures = [];
|
|
78
|
+
};
|
|
79
|
+
constructor(map) {
|
|
80
|
+
this.map = map;
|
|
81
|
+
map.on("webglcontextrestored", this.restore);
|
|
82
|
+
map.on("move", this.invalidateDepth);
|
|
83
|
+
}
|
|
84
|
+
setRoster(pins) {
|
|
85
|
+
this.needsDepth = pins.some((pin) => pin.presentation.occlusion === "terrain");
|
|
86
|
+
const resources = /* @__PURE__ */ new Map();
|
|
87
|
+
for (const pin of pins) {
|
|
88
|
+
const scale = pin.presentation.sizePixels / pin.octagonSize * Math.min(devicePixelRatio, 2);
|
|
89
|
+
if (scale > (resources.get(pin.resource.id)?.scale ?? 0)) resources.set(pin.resource.id, {
|
|
90
|
+
pin,
|
|
91
|
+
scale
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const key = JSON.stringify([...resources].map(([id, value]) => [id, value.scale]).sort(([a], [b]) => a.localeCompare(b)));
|
|
95
|
+
if (key === this.key) return;
|
|
96
|
+
this.key = key;
|
|
97
|
+
this.resources = resources;
|
|
98
|
+
this.revision++;
|
|
99
|
+
this.pending = void 0;
|
|
100
|
+
}
|
|
101
|
+
load(gl) {
|
|
102
|
+
this.context = gl;
|
|
103
|
+
return this.pending ??= (async () => {
|
|
104
|
+
const revision = this.revision;
|
|
105
|
+
const sizes = [...this.resources.values()].map(({ pin, scale }) => ({
|
|
106
|
+
w: pin.resource.width * scale + 4,
|
|
107
|
+
h: pin.resource.height * scale + 4
|
|
108
|
+
}));
|
|
109
|
+
const area = sizes.reduce((sum, s) => sum + s.w * s.h, 0);
|
|
110
|
+
const side = Math.max(256, Math.sqrt(area) * 1.3, ...sizes.flatMap((s) => [s.w, s.h]));
|
|
111
|
+
const atlas = new PinAtlas(Math.min(2048, gl.getParameter(gl.MAX_TEXTURE_SIZE), 2 ** Math.ceil(Math.log2(side))));
|
|
112
|
+
const icons = [...this.resources.values()];
|
|
113
|
+
const images = await Promise.all(icons.map(({ pin }) => decodeIcon(pin.resource)));
|
|
114
|
+
if (this.disposed || revision !== this.revision) return;
|
|
115
|
+
icons.forEach(({ pin, scale }, i) => atlas.add(pin.resource, scale, images[i]));
|
|
116
|
+
const textures = [];
|
|
117
|
+
try {
|
|
118
|
+
for (const page of atlas.pages) {
|
|
119
|
+
const texture = gl.createTexture();
|
|
120
|
+
if (!texture) throw new Error("Could not allocate pin texture");
|
|
121
|
+
textures.push(texture);
|
|
122
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
123
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
124
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
125
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
126
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, page);
|
|
127
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
128
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
129
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
130
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
for (const texture of textures) gl.deleteTexture(texture);
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
for (const texture of this.textures) gl.deleteTexture(texture);
|
|
137
|
+
this.textures = textures;
|
|
138
|
+
this.map.painter.context.setDirty();
|
|
139
|
+
return {
|
|
140
|
+
atlas,
|
|
141
|
+
textures
|
|
142
|
+
};
|
|
143
|
+
})();
|
|
144
|
+
}
|
|
145
|
+
dispose() {
|
|
146
|
+
this.disposed = true;
|
|
147
|
+
this.revision++;
|
|
148
|
+
this.map.off("webglcontextrestored", this.restore);
|
|
149
|
+
this.map.off("move", this.invalidateDepth);
|
|
150
|
+
for (const texture of this.textures) this.context?.deleteTexture(texture);
|
|
151
|
+
this.textures = [];
|
|
152
|
+
this.resources.clear();
|
|
153
|
+
this.pending = void 0;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/pin-inversion.ts
|
|
158
|
+
/** Follow the local projection branch from the unit, never a foreground terrain ray hit. */
|
|
159
|
+
function invertPinProjection(project, initial, target, latitudeLimit = 85.051129) {
|
|
160
|
+
let position = [initial[0], initial[1]];
|
|
161
|
+
const valid = (p) => p.every(Number.isFinite) && Math.abs(p[1]) < latitudeLimit;
|
|
162
|
+
if (!valid(position) || !target.every(Number.isFinite)) return null;
|
|
163
|
+
for (let n = 0; n < 12; n++) {
|
|
164
|
+
const a = project(position), ex = target[0] - a[0], ey = target[1] - a[1], error = Math.hypot(ex, ey);
|
|
165
|
+
if (!Number.isFinite(error)) return null;
|
|
166
|
+
if (error < .05) return position;
|
|
167
|
+
const step = 1e-5, b = project([position[0] + step, position[1]]), c = project([position[0], position[1] + step]);
|
|
168
|
+
const jx = (b[0] - a[0]) / step, jy = (b[1] - a[1]) / step, kx = (c[0] - a[0]) / step, ky = (c[1] - a[1]) / step, det = jx * ky - jy * kx;
|
|
169
|
+
if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return null;
|
|
170
|
+
const dx = (ex * ky - ey * kx) / det, dy = (ey * jx - ex * jy) / det;
|
|
171
|
+
let improved = false;
|
|
172
|
+
for (let damping = 1; damping >= 1 / 128; damping /= 2) {
|
|
173
|
+
const candidate = [position[0] + dx * damping, position[1] + dy * damping];
|
|
174
|
+
if (!valid(candidate)) continue;
|
|
175
|
+
const pixel = project(candidate);
|
|
176
|
+
if (Math.hypot(target[0] - pixel[0], target[1] - pixel[1]) < error) {
|
|
177
|
+
position = candidate;
|
|
178
|
+
improved = true;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!improved) return null;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
//#region src/pin-shaders.ts
|
|
188
|
+
const PIN_VERTEX = `#version 300 es
|
|
189
|
+
precision highp float;
|
|
190
|
+
layout(location=0) in vec4 a_ground;
|
|
191
|
+
layout(location=1) in vec4 a_target;
|
|
192
|
+
layout(location=2) in vec4 a_shape;
|
|
193
|
+
layout(location=3) in vec4 a_color;
|
|
194
|
+
layout(location=4) in vec4 a_uv;
|
|
195
|
+
layout(location=5) in vec4 a_flags;
|
|
196
|
+
uniform mat4 u_matrix;
|
|
197
|
+
uniform float u_world;
|
|
198
|
+
uniform mat4 u_globeMatrix;
|
|
199
|
+
uniform vec4 u_clippingPlane;
|
|
200
|
+
uniform vec2 u_origin;
|
|
201
|
+
uniform float u_globeTransition;
|
|
202
|
+
out float v_horizon;
|
|
203
|
+
vec4 projectPin(vec3 position, out float horizon) {
|
|
204
|
+
vec4 flatClip = u_matrix * vec4(position.xy * u_world, position.z, 1.);
|
|
205
|
+
horizon = 1.;
|
|
206
|
+
if(u_globeTransition == 0.) return flatClip;
|
|
207
|
+
vec2 mercator = position.xy + u_origin;
|
|
208
|
+
float angle = mercator.x * 6.283185307179586 + 3.141592653589793;
|
|
209
|
+
float t = exp(3.141592653589793 - mercator.y * 6.283185307179586);
|
|
210
|
+
vec3 sphere = vec3(sin(angle)*2.*t, t*t-1., cos(angle)*2.*t)/(t*t+1.);
|
|
211
|
+
sphere *= 1. + position.z / 6371008.8;
|
|
212
|
+
vec4 globe = u_globeMatrix * vec4(sphere, 1.);
|
|
213
|
+
float distance = dot(sphere,u_clippingPlane.xyz) + u_clippingPlane.w;
|
|
214
|
+
horizon = 1. - (1.-distance) * max(0.,(u_globeTransition-.2)/.8);
|
|
215
|
+
return mix(flatClip,globe,u_globeTransition);
|
|
216
|
+
}
|
|
217
|
+
uniform vec2 u_view;
|
|
218
|
+
out vec2 v_local;
|
|
219
|
+
out vec2 v_uv;
|
|
220
|
+
flat out vec4 v_bounds;
|
|
221
|
+
flat out vec4 v_color;
|
|
222
|
+
flat out vec4 v_shape;
|
|
223
|
+
flat out vec4 v_flags;
|
|
224
|
+
flat out float v_radius;
|
|
225
|
+
const vec2 corners[6] = vec2[6](vec2(0,0),vec2(1,0),vec2(0,1),vec2(0,1),vec2(1,0),vec2(1,1));
|
|
226
|
+
void main() {
|
|
227
|
+
vec2 c = corners[gl_VertexID];
|
|
228
|
+
float groundHorizon, targetHorizon;
|
|
229
|
+
vec4 ground = projectPin(a_ground.xyz, groundHorizon);
|
|
230
|
+
vec4 target = projectPin(a_target.xyz, targetHorizon);
|
|
231
|
+
if(a_flags.y < .5) { target = ground; targetHorizon = groundHorizon; target.y += a_ground.w * 2. / u_view.y * target.w; }
|
|
232
|
+
v_horizon = a_flags.x < .5 ? groundHorizon : (a_flags.x < 1.5 ? mix(groundHorizon,targetHorizon,c.y) : targetHorizon);
|
|
233
|
+
vec2 gp = ground.xy / ground.w * u_view * .5;
|
|
234
|
+
vec2 tp = target.xy / target.w * u_view * .5;
|
|
235
|
+
float kind = a_flags.x;
|
|
236
|
+
vec4 clip = target;
|
|
237
|
+
vec2 offset;
|
|
238
|
+
if(kind < .5) { clip = ground; offset = (c - .5) * a_shape.xy; }
|
|
239
|
+
else if(kind < 1.5) {
|
|
240
|
+
vec2 delta = tp-gp; float len = length(delta);
|
|
241
|
+
vec2 dir = len > .0001 ? delta/len : vec2(0,1);
|
|
242
|
+
vec2 p = mix(gp,tp,c.y) + vec2(-dir.y,dir.x) * (c.x-.5)*a_shape.x;
|
|
243
|
+
clip = vec4(p/u_view*2., mix(ground.z/ground.w,target.z/target.w,c.y), 1.);
|
|
244
|
+
offset = vec2(0);
|
|
245
|
+
} else { offset = (c - vec2(.5,0)) * a_shape.xy + a_shape.zw; }
|
|
246
|
+
clip.xy += offset * 2. / u_view * clip.w;
|
|
247
|
+
if (ground.w <= 0. || target.w <= 0.) clip = vec4(2,2,2,1);
|
|
248
|
+
gl_Position=clip;
|
|
249
|
+
v_local=(c-.5)*a_shape.xy;
|
|
250
|
+
v_uv=mix(a_uv.xy,a_uv.zw,vec2(c.x,1.-c.y));
|
|
251
|
+
v_bounds=a_uv; v_color=a_color; v_shape=a_shape; v_flags=a_flags; v_radius=a_target.w;
|
|
252
|
+
}`;
|
|
253
|
+
const PIN_FRAGMENT = `#version 300 es
|
|
254
|
+
precision highp float;
|
|
255
|
+
in float v_horizon;
|
|
256
|
+
in vec2 v_local;
|
|
257
|
+
in vec2 v_uv;
|
|
258
|
+
flat in vec4 v_bounds;
|
|
259
|
+
flat in vec4 v_color;
|
|
260
|
+
flat in vec4 v_shape;
|
|
261
|
+
flat in vec4 v_flags;
|
|
262
|
+
flat in float v_radius;
|
|
263
|
+
uniform sampler2D u_atlas;
|
|
264
|
+
// Texture return precision follows the sampler, not the default float precision.
|
|
265
|
+
// Preserve the packed depth channels on GPUs that implement lowp separately.
|
|
266
|
+
uniform highp sampler2D u_terrain;
|
|
267
|
+
uniform vec2 u_buffer;
|
|
268
|
+
uniform bool u_hasTerrain;
|
|
269
|
+
uniform bool u_pick;
|
|
270
|
+
|
|
271
|
+
out vec4 color;
|
|
272
|
+
float symbolAlpha(vec2 pixel, vec2 size) {
|
|
273
|
+
vec2 uv = pixel / size + .5;
|
|
274
|
+
if(any(lessThan(uv,vec2(0))) || any(greaterThan(uv,vec2(1)))) return 0.;
|
|
275
|
+
return texture(u_atlas,mix(v_bounds.xy,v_bounds.zw,vec2(uv.x,1.-uv.y))).a;
|
|
276
|
+
}
|
|
277
|
+
void main() {
|
|
278
|
+
if(v_horizon < 0.) discard;
|
|
279
|
+
if(u_hasTerrain && v_flags.z > .5) {
|
|
280
|
+
vec4 packed=texture(u_terrain,gl_FragCoord.xy/u_buffer);
|
|
281
|
+
float depth=dot(packed,vec4(1./16777216.,1./65536.,1./256.,1.));
|
|
282
|
+
float ours=gl_FragCoord.z*2.-1.;
|
|
283
|
+
// A transparent depth texel is sky (no terrain), not a near-plane occluder.
|
|
284
|
+
if(depth > 0. && ours > depth + .00012) discard;
|
|
285
|
+
}
|
|
286
|
+
vec4 paint=v_color;
|
|
287
|
+
float kind=v_flags.x;
|
|
288
|
+
if(kind < .5) {
|
|
289
|
+
float edge=length(v_local)-v_shape.x*.5;
|
|
290
|
+
paint.a *= 1.-smoothstep(-1.,0.,edge);
|
|
291
|
+
} else if(kind > 4.5) {
|
|
292
|
+
// One optional halo instance; reuse the canonical atlas, including text and HQ staff.
|
|
293
|
+
vec2 size = v_shape.xy - vec2(2. * v_radius);
|
|
294
|
+
float alpha = symbolAlpha(v_local,size);
|
|
295
|
+
for(int i=0;i<16;i++) {
|
|
296
|
+
float angle = float(i) * 6.28318530718 / 16.;
|
|
297
|
+
vec2 direction = vec2(cos(angle),sin(angle));
|
|
298
|
+
alpha = max(alpha,symbolAlpha(v_local + direction * v_radius,size));
|
|
299
|
+
alpha = max(alpha,symbolAlpha(v_local + direction * v_radius * .5,size));
|
|
300
|
+
}
|
|
301
|
+
paint.a *= alpha;
|
|
302
|
+
} else if(kind > 3.5) {
|
|
303
|
+
if(min(v_shape.x*.5-abs(v_local.x),v_shape.y*.5-abs(v_local.y)) > 2.) discard;
|
|
304
|
+
} else if(kind > 2.5) {
|
|
305
|
+
paint *= texture(u_atlas,v_uv);
|
|
306
|
+
} else if(kind > 1.5) {
|
|
307
|
+
float radius=min(v_radius,min(v_shape.x,v_shape.y)*.5);
|
|
308
|
+
vec2 q=abs(v_local)-v_shape.xy*.5+radius;
|
|
309
|
+
float edge=length(max(q,0.))+min(max(q.x,q.y),0.)-radius;
|
|
310
|
+
paint.a *= 1.-smoothstep(-1.,0.,edge);
|
|
311
|
+
}
|
|
312
|
+
if(paint.a < .01) discard;
|
|
313
|
+
if(u_pick) {
|
|
314
|
+
float id=v_flags.w;
|
|
315
|
+
color=vec4(mod(id,256.),mod(floor(id/256.),256.),floor(id/65536.),255.)/255.;
|
|
316
|
+
} else color=vec4(paint.rgb*paint.a,paint.a);
|
|
317
|
+
}`;
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/pin-projection.ts
|
|
320
|
+
/**
|
|
321
|
+
* Single source of truth for the globe radius the pin pipeline works in. The
|
|
322
|
+
* GLSL in `pin-shaders.ts` repeats the literal because a shader cannot import;
|
|
323
|
+
* this export is what it must stay in step with.
|
|
324
|
+
*/
|
|
325
|
+
const PIN_GLOBE_RADIUS = EARTH_RADIUS;
|
|
326
|
+
/** Mercator is singular at the poles; this sub-metre inset keeps GPU inputs finite. */
|
|
327
|
+
function pinMercator(position) {
|
|
328
|
+
return MercatorCoordinate.fromLngLat([position[0], Math.max(-89.999999, Math.min(89.999999, position[1]))]);
|
|
329
|
+
}
|
|
330
|
+
const multiply = (m, v) => [
|
|
331
|
+
0,
|
|
332
|
+
1,
|
|
333
|
+
2,
|
|
334
|
+
3
|
|
335
|
+
].map((row) => v.reduce((sum, n, col) => sum + n * m[col * 4 + row], 0));
|
|
336
|
+
/** CPU counterpart of projectPin in pin-shaders.ts, also used by drag inversion. */
|
|
337
|
+
var PinProjection = class {
|
|
338
|
+
matrix = new Float32Array(16);
|
|
339
|
+
globeMatrix = new Float32Array(16);
|
|
340
|
+
clippingPlane = new Float32Array(4);
|
|
341
|
+
transition = 0;
|
|
342
|
+
origin = [0, 0];
|
|
343
|
+
world = 512;
|
|
344
|
+
update(input, origin, center, world, globe) {
|
|
345
|
+
this.origin = origin;
|
|
346
|
+
this.world = world;
|
|
347
|
+
const data = globe ? input.getProjectionData({
|
|
348
|
+
tileID: { canonical: {
|
|
349
|
+
x: 0,
|
|
350
|
+
y: 0,
|
|
351
|
+
z: 0
|
|
352
|
+
} },
|
|
353
|
+
applyGlobeMatrix: true
|
|
354
|
+
}) : void 0;
|
|
355
|
+
this.transition = data?.projectionTransition ?? 0;
|
|
356
|
+
if (data) {
|
|
357
|
+
this.globeMatrix.set(data.mainMatrix);
|
|
358
|
+
this.clippingPlane.set(data.clippingPlane);
|
|
359
|
+
}
|
|
360
|
+
const fallback = globe ? input.defaultProjectionData.fallbackMatrix : input.modelViewProjectionMatrix;
|
|
361
|
+
const xyScale = globe ? 1 / world : 1;
|
|
362
|
+
const zScale = globe ? MercatorCoordinate.fromLngLat(center).meterInMercatorCoordinateUnits() : 1;
|
|
363
|
+
for (let row = 0; row < 4; row++) {
|
|
364
|
+
this.matrix[row] = fallback[row] * xyScale;
|
|
365
|
+
this.matrix[4 + row] = fallback[4 + row] * xyScale;
|
|
366
|
+
this.matrix[8 + row] = fallback[8 + row] * zScale;
|
|
367
|
+
this.matrix[12 + row] = fallback[12 + row] + fallback[row] * xyScale * origin[0] * world + fallback[4 + row] * xyScale * origin[1] * world;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
project(worldX, worldY, elevation) {
|
|
371
|
+
const flat = multiply(this.matrix, [
|
|
372
|
+
worldX,
|
|
373
|
+
worldY,
|
|
374
|
+
elevation,
|
|
375
|
+
1
|
|
376
|
+
]);
|
|
377
|
+
if (this.transition === 0) return {
|
|
378
|
+
clip: flat,
|
|
379
|
+
horizon: 1
|
|
380
|
+
};
|
|
381
|
+
const x = worldX / this.world + this.origin[0];
|
|
382
|
+
const y = worldY / this.world + this.origin[1];
|
|
383
|
+
const angle = x * Math.PI * 2 + Math.PI;
|
|
384
|
+
const t = Math.exp(Math.PI - y * Math.PI * 2);
|
|
385
|
+
const scale = 1 + elevation / PIN_GLOBE_RADIUS;
|
|
386
|
+
const sphere = [
|
|
387
|
+
Math.sin(angle) * 2 * t / (t * t + 1),
|
|
388
|
+
(t * t - 1) / (t * t + 1),
|
|
389
|
+
Math.cos(angle) * 2 * t / (t * t + 1)
|
|
390
|
+
].map((n) => n * scale);
|
|
391
|
+
const globe = multiply(this.globeMatrix, [...sphere, 1]);
|
|
392
|
+
const horizon = 1 - (1 - sphere.reduce((sum, n, i) => sum + n * this.clippingPlane[i], this.clippingPlane[3])) * Math.max(0, (this.transition - .2) / .8);
|
|
393
|
+
return {
|
|
394
|
+
clip: flat.map((n, i) => n * (1 - this.transition) + globe[i] * this.transition),
|
|
395
|
+
horizon
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/pin-kind.ts
|
|
401
|
+
/**
|
|
402
|
+
* Draw-kind tags shared between the pin instance data (pin-layer.ts) and the
|
|
403
|
+
* WebGL shaders (pin-shaders.ts).
|
|
404
|
+
*
|
|
405
|
+
* The GLSL side branches on numeric thresholds (`kind < .5`, `kind < 1.5`,
|
|
406
|
+
* `kind > 4.5`, ...) rather than importing this module — see the comment
|
|
407
|
+
* near those branches in pin-shaders.ts. If you renumber a value here, you
|
|
408
|
+
* must update the shader thresholds by hand or the two will silently drift.
|
|
409
|
+
*/
|
|
410
|
+
const PIN_KIND = {
|
|
411
|
+
DOT: 0,
|
|
412
|
+
STEM: 1,
|
|
413
|
+
PLATE: 2,
|
|
414
|
+
SYMBOL: 3,
|
|
415
|
+
HIGHLIGHT: 4,
|
|
416
|
+
SYMBOL_HALO: 5
|
|
417
|
+
};
|
|
418
|
+
//#endregion
|
|
419
|
+
//#region src/pin-layer.ts
|
|
420
|
+
const STRIDE = 24;
|
|
421
|
+
/** Shader-ready `[r, g, b, a]` in 0-1, with the color's own alpha folded into `opacity`. */
|
|
422
|
+
function rgba(color, opacity) {
|
|
423
|
+
const { r, g, b, a } = parseColor(color);
|
|
424
|
+
return [
|
|
425
|
+
r / 255,
|
|
426
|
+
g / 255,
|
|
427
|
+
b / 255,
|
|
428
|
+
a * opacity
|
|
429
|
+
];
|
|
430
|
+
}
|
|
431
|
+
function shader(gl, type, source) {
|
|
432
|
+
const s = gl.createShader(type);
|
|
433
|
+
gl.shaderSource(s, source);
|
|
434
|
+
gl.compileShader(s);
|
|
435
|
+
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
|
|
436
|
+
const message = gl.getShaderInfoLog(s);
|
|
437
|
+
gl.deleteShader(s);
|
|
438
|
+
throw new Error(`Pin shader: ${message}`);
|
|
439
|
+
}
|
|
440
|
+
return s;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Appends one instance's worth of vertex-shader input to `data` and extends `ranges`
|
|
444
|
+
* accordingly. Hoisted out of rebuild()'s per-pin loop (ADR n/a) so pushing 10k+ pins
|
|
445
|
+
* never allocates a fresh closure per pin — every varying value is an explicit param.
|
|
446
|
+
*/
|
|
447
|
+
function appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionTerrain, kind, shape, color, a, target, mode) {
|
|
448
|
+
if (highlightOnly && kind !== PIN_KIND.HIGHLIGHT) return;
|
|
449
|
+
const first = data.length / STRIDE;
|
|
450
|
+
const previous = ranges[ranges.length - 1];
|
|
451
|
+
if (previous?.page === image.page) previous.count++;
|
|
452
|
+
else ranges.push({
|
|
453
|
+
page: image.page,
|
|
454
|
+
first,
|
|
455
|
+
count: 1
|
|
456
|
+
});
|
|
457
|
+
data.push(...a, ...target, ...shape, ...color, ...image.uv, kind, mode, occlusionTerrain, instanceId);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Ground-following point along a direction-arrow segment, `t` of the way from `from`
|
|
461
|
+
* to `to` (both east/north metre offsets from the pin). Hoisted out of rebuild()'s
|
|
462
|
+
* per-pin, per-segment loops — at 10k pins that loop can define this closure 240k+
|
|
463
|
+
* times per rebuild otherwise.
|
|
464
|
+
*/
|
|
465
|
+
function arrowSegmentPoint(center, position, elevationAt, from, to, t) {
|
|
466
|
+
const east = from[0] + (to[0] - from[0]) * t;
|
|
467
|
+
const north = from[1] + (to[1] - from[1]) * t;
|
|
468
|
+
const ll = [position[0] + east / (111320 * Math.cos(position[1] * Math.PI / 180)), position[1] + north / 111320];
|
|
469
|
+
const mc = pinMercator(ll);
|
|
470
|
+
mc.x += Math.round(center.x - mc.x);
|
|
471
|
+
return [
|
|
472
|
+
mc.x - center.x,
|
|
473
|
+
mc.y - center.y,
|
|
474
|
+
elevationAt(ll) + 1,
|
|
475
|
+
0
|
|
476
|
+
];
|
|
477
|
+
}
|
|
478
|
+
/** Batched GPU pin rendering. Adjacent groups only; instance order is paint order. */
|
|
479
|
+
var PinLayer = class {
|
|
480
|
+
id;
|
|
481
|
+
resources;
|
|
482
|
+
onStatus;
|
|
483
|
+
type = "custom";
|
|
484
|
+
renderingMode = "2d";
|
|
485
|
+
map;
|
|
486
|
+
gl;
|
|
487
|
+
program;
|
|
488
|
+
uniforms;
|
|
489
|
+
buffer;
|
|
490
|
+
vao;
|
|
491
|
+
textures = [];
|
|
492
|
+
atlas = new PinAtlas();
|
|
493
|
+
pins = [];
|
|
494
|
+
ranges = [];
|
|
495
|
+
version = 0;
|
|
496
|
+
resourceRevision = -1;
|
|
497
|
+
needsDepth = false;
|
|
498
|
+
elevationCache = /* @__PURE__ */ new Map();
|
|
499
|
+
pendingElevation = /* @__PURE__ */ new Set();
|
|
500
|
+
removed = false;
|
|
501
|
+
ready = false;
|
|
502
|
+
dirty = true;
|
|
503
|
+
changedPins = /* @__PURE__ */ new Set();
|
|
504
|
+
slices = [];
|
|
505
|
+
origin = [0, 0];
|
|
506
|
+
elevations = [];
|
|
507
|
+
projection = new PinProjection();
|
|
508
|
+
matrix = this.projection.matrix;
|
|
509
|
+
frame;
|
|
510
|
+
pickFramebuffer;
|
|
511
|
+
pickTexture;
|
|
512
|
+
pickSize = "";
|
|
513
|
+
status = { status: "supported" };
|
|
514
|
+
refreshTerrain = (event) => {
|
|
515
|
+
if (event.sourceId && event.sourceId !== this.map.getTerrain()?.source) return;
|
|
516
|
+
if (event.type === "sourcedata" || event.type === "terrain") this.elevationCache.clear();
|
|
517
|
+
this.dirty = true;
|
|
518
|
+
this.map.triggerRepaint();
|
|
519
|
+
};
|
|
520
|
+
elevation(position) {
|
|
521
|
+
if (!this.map.getTerrain() || Math.abs(position[1]) >= 85.051129) return 0;
|
|
522
|
+
const key = `${position[0]}/${position[1]}`;
|
|
523
|
+
const cached = this.elevationCache.get(key);
|
|
524
|
+
if (cached !== void 0) return cached;
|
|
525
|
+
const terrain = this.map.terrain;
|
|
526
|
+
const { tileID, mercatorX, mercatorY } = terrain._getOverscaledTileIDFromLngLatZoom(new LngLat(position[0], position[1]), Math.min(terrain.tileManager.maxzoom, terrain.tileManager.getSource().maxzoom + terrain.tileManager.deltaZoom));
|
|
527
|
+
const sampler = terrain._getElevationSampler(tileID);
|
|
528
|
+
if (!sampler) return NaN;
|
|
529
|
+
const value = sampler(mercatorX % 8192, mercatorY % 8192, 8192) * terrain.exaggeration;
|
|
530
|
+
this.elevationCache.set(key, value);
|
|
531
|
+
return value;
|
|
532
|
+
}
|
|
533
|
+
has(id) {
|
|
534
|
+
return this.pins.some((pin) => pin.id === id);
|
|
535
|
+
}
|
|
536
|
+
constructor(id, resources, onStatus) {
|
|
537
|
+
this.id = id;
|
|
538
|
+
this.resources = resources;
|
|
539
|
+
this.onStatus = onStatus;
|
|
540
|
+
}
|
|
541
|
+
restoreContext = () => {
|
|
542
|
+
this.ready = false;
|
|
543
|
+
this.resourceRevision = -1;
|
|
544
|
+
this.textures = [];
|
|
545
|
+
this.pickSize = "";
|
|
546
|
+
this.pickTexture = void 0;
|
|
547
|
+
this.pickFramebuffer = void 0;
|
|
548
|
+
this.onRemoveListeners();
|
|
549
|
+
this.onAdd(this.map, this.gl);
|
|
550
|
+
this.setPins(this.pins);
|
|
551
|
+
};
|
|
552
|
+
onRemoveListeners() {
|
|
553
|
+
this.map.off("sourcedata", this.refreshTerrain);
|
|
554
|
+
this.map.off("terrain", this.refreshTerrain);
|
|
555
|
+
this.map.off("resize", this.refreshTerrain);
|
|
556
|
+
this.map.off("moveend", this.refreshTerrain);
|
|
557
|
+
this.map.off("webglcontextrestored", this.restoreContext);
|
|
558
|
+
}
|
|
559
|
+
onAdd(map, context) {
|
|
560
|
+
this.removed = false;
|
|
561
|
+
this.map = map;
|
|
562
|
+
this.gl = context;
|
|
563
|
+
const gl = this.gl;
|
|
564
|
+
try {
|
|
565
|
+
const program = gl.createProgram();
|
|
566
|
+
for (const [type, source] of [[gl.VERTEX_SHADER, PIN_VERTEX], [gl.FRAGMENT_SHADER, PIN_FRAGMENT]]) {
|
|
567
|
+
const s = shader(gl, type, source);
|
|
568
|
+
gl.attachShader(program, s);
|
|
569
|
+
gl.deleteShader(s);
|
|
570
|
+
}
|
|
571
|
+
gl.linkProgram(program);
|
|
572
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program) ?? "Pin link failed");
|
|
573
|
+
this.program = program;
|
|
574
|
+
this.uniforms = this.resolveUniforms(gl);
|
|
575
|
+
this.buffer = gl.createBuffer();
|
|
576
|
+
this.vao = gl.createVertexArray();
|
|
577
|
+
map.on("webglcontextrestored", this.restoreContext);
|
|
578
|
+
map.on("sourcedata", this.refreshTerrain);
|
|
579
|
+
map.on("terrain", this.refreshTerrain);
|
|
580
|
+
map.on("resize", this.refreshTerrain);
|
|
581
|
+
map.on("moveend", this.refreshTerrain);
|
|
582
|
+
} catch (error) {
|
|
583
|
+
this.setStatus({
|
|
584
|
+
status: "unavailable",
|
|
585
|
+
reason: String(error)
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
/** Uniform locations are fixed once a program is linked; resolved here, not per draw. */
|
|
590
|
+
resolveUniforms(gl) {
|
|
591
|
+
const program = this.program;
|
|
592
|
+
const uniforms = {
|
|
593
|
+
matrix: gl.getUniformLocation(program, "u_matrix"),
|
|
594
|
+
globeMatrix: gl.getUniformLocation(program, "u_globeMatrix"),
|
|
595
|
+
clippingPlane: gl.getUniformLocation(program, "u_clippingPlane"),
|
|
596
|
+
origin: gl.getUniformLocation(program, "u_origin"),
|
|
597
|
+
globeTransition: gl.getUniformLocation(program, "u_globeTransition"),
|
|
598
|
+
world: gl.getUniformLocation(program, "u_world"),
|
|
599
|
+
view: gl.getUniformLocation(program, "u_view"),
|
|
600
|
+
buffer: gl.getUniformLocation(program, "u_buffer"),
|
|
601
|
+
pick: gl.getUniformLocation(program, "u_pick"),
|
|
602
|
+
atlas: gl.getUniformLocation(program, "u_atlas"),
|
|
603
|
+
terrain: gl.getUniformLocation(program, "u_terrain"),
|
|
604
|
+
hasTerrain: gl.getUniformLocation(program, "u_hasTerrain")
|
|
605
|
+
};
|
|
606
|
+
this.uniforms = uniforms;
|
|
607
|
+
return uniforms;
|
|
608
|
+
}
|
|
609
|
+
setStatus(status) {
|
|
610
|
+
if (status.status === this.status.status && status.reason === this.status.reason) return;
|
|
611
|
+
this.status = status;
|
|
612
|
+
this.onStatus?.({
|
|
613
|
+
...status,
|
|
614
|
+
graphicIds: this.pins.filter((p) => !p.highlightOnly).map((p) => p.id)
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
getStatus(id) {
|
|
618
|
+
return id && this.pendingElevation.has(id) ? { status: "waiting-for-elevation" } : { ...this.status };
|
|
619
|
+
}
|
|
620
|
+
async setPins(pins, changedIndices) {
|
|
621
|
+
if (pins.length !== this.pins.length) this.dirty = true;
|
|
622
|
+
else {
|
|
623
|
+
const changed = changedIndices ?? pins.flatMap((pin, i) => pin !== this.pins[i] ? [i] : []);
|
|
624
|
+
for (const i of changed) {
|
|
625
|
+
if (pins[i].id !== this.pins[i].id) this.dirty = true;
|
|
626
|
+
this.changedPins.add(i);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
this.pins = pins;
|
|
630
|
+
if (this.elevationCache.size > pins.length * 60 + 1e3) this.elevationCache.clear();
|
|
631
|
+
if (!this.program) {
|
|
632
|
+
this.onStatus?.({
|
|
633
|
+
...this.status,
|
|
634
|
+
graphicIds: pins.map((pin) => pin.id)
|
|
635
|
+
});
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
this.needsDepth = pins.some((p) => p.presentation.occlusion === "terrain");
|
|
639
|
+
if (this.resourceRevision === this.resources.revision && this.ready) {
|
|
640
|
+
this.map.triggerRepaint();
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
this.dirty = true;
|
|
644
|
+
this.resourceRevision = this.resources.revision;
|
|
645
|
+
this.ready = false;
|
|
646
|
+
this.setStatus({ status: "loading" });
|
|
647
|
+
const version = ++this.version;
|
|
648
|
+
try {
|
|
649
|
+
const snapshot = await this.resources.load(this.gl);
|
|
650
|
+
if (!snapshot || version !== this.version || this.removed) return;
|
|
651
|
+
this.atlas = snapshot.atlas;
|
|
652
|
+
this.textures = snapshot.textures;
|
|
653
|
+
this.ready = true;
|
|
654
|
+
this.map.triggerRepaint();
|
|
655
|
+
} catch (error) {
|
|
656
|
+
this.setStatus({
|
|
657
|
+
status: "unavailable",
|
|
658
|
+
reason: `Pin resources: ${error}`
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
buildPin(i, center, data, ranges) {
|
|
663
|
+
const pin = this.pins[i], p = pin.presentation;
|
|
664
|
+
const anchor = pinMercator(pin.position);
|
|
665
|
+
anchor.x += Math.round(center.x - anchor.x);
|
|
666
|
+
const elevation = this.elevation(pin.position);
|
|
667
|
+
this.elevations[i] = elevation;
|
|
668
|
+
if (!Number.isFinite(elevation)) {
|
|
669
|
+
this.pendingElevation.add(pin.id);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
this.pendingElevation.delete(pin.id);
|
|
673
|
+
const a = [
|
|
674
|
+
anchor.x - center.x,
|
|
675
|
+
anchor.y - center.y,
|
|
676
|
+
elevation,
|
|
677
|
+
p.liftPixels
|
|
678
|
+
];
|
|
679
|
+
const b = [
|
|
680
|
+
a[0],
|
|
681
|
+
a[1],
|
|
682
|
+
elevation + (p.mode === "world" ? p.heightMeters : 0),
|
|
683
|
+
p.plate.radius
|
|
684
|
+
];
|
|
685
|
+
const image = this.atlas.entries.get(pin.resource.id);
|
|
686
|
+
const opacity = pin.opacity;
|
|
687
|
+
const highlightOnly = pin.highlightOnly;
|
|
688
|
+
const instanceId = i + 1;
|
|
689
|
+
const occlusionFlag = p.occlusion === "terrain" ? 1 : 0;
|
|
690
|
+
const worldMode = p.mode === "world" ? 1 : 0;
|
|
691
|
+
const haloColor = rgba(p.halo.color, p.halo.opacity * opacity);
|
|
692
|
+
const stemColor = rgba(p.stem.color, p.stem.opacity * opacity);
|
|
693
|
+
if (pin.direction !== void 0 && !pin.highlightOnly) {
|
|
694
|
+
const elevationAt = (ll) => this.elevation(ll);
|
|
695
|
+
const angle = pin.direction, length = 180;
|
|
696
|
+
const tip = [Math.sin(angle) * length, Math.cos(angle) * length];
|
|
697
|
+
const paths = [
|
|
698
|
+
[[0, 0], tip],
|
|
699
|
+
[[tip[0] - Math.sin(angle - .5) * length * .25, tip[1] - Math.cos(angle - .5) * length * .25], tip],
|
|
700
|
+
[[tip[0] - Math.sin(angle + .5) * length * .25, tip[1] - Math.cos(angle + .5) * length * .25], tip]
|
|
701
|
+
];
|
|
702
|
+
const saved = [...a];
|
|
703
|
+
for (const [from, to] of paths) for (let segment = 0; segment < 8; segment++) {
|
|
704
|
+
const ground = arrowSegmentPoint(center, pin.position, elevationAt, from, to, segment / 8);
|
|
705
|
+
const target = arrowSegmentPoint(center, pin.position, elevationAt, from, to, (segment + 1) / 8);
|
|
706
|
+
if (!Number.isFinite(ground[2]) || !Number.isFinite(target[2])) continue;
|
|
707
|
+
a.splice(0, 4, ...ground);
|
|
708
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.STEM, [
|
|
709
|
+
p.stem.width + 2 * p.halo.width,
|
|
710
|
+
0,
|
|
711
|
+
0,
|
|
712
|
+
0
|
|
713
|
+
], haloColor, a, target, 1);
|
|
714
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.STEM, [
|
|
715
|
+
p.stem.width,
|
|
716
|
+
0,
|
|
717
|
+
0,
|
|
718
|
+
0
|
|
719
|
+
], stemColor, a, target, 1);
|
|
720
|
+
}
|
|
721
|
+
a.splice(0, 4, ...saved);
|
|
722
|
+
}
|
|
723
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.STEM, [
|
|
724
|
+
p.stem.width + 2 * p.halo.width,
|
|
725
|
+
0,
|
|
726
|
+
0,
|
|
727
|
+
0
|
|
728
|
+
], haloColor, a, b, worldMode);
|
|
729
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.STEM, [
|
|
730
|
+
p.stem.width,
|
|
731
|
+
0,
|
|
732
|
+
0,
|
|
733
|
+
0
|
|
734
|
+
], stemColor, a, b, worldMode);
|
|
735
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.DOT, [
|
|
736
|
+
2 * (p.dot.radius + p.halo.width),
|
|
737
|
+
2 * (p.dot.radius + p.halo.width),
|
|
738
|
+
0,
|
|
739
|
+
0
|
|
740
|
+
], haloColor, a, b, worldMode);
|
|
741
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.DOT, [
|
|
742
|
+
2 * p.dot.radius,
|
|
743
|
+
2 * p.dot.radius,
|
|
744
|
+
0,
|
|
745
|
+
0
|
|
746
|
+
], rgba(p.dot.color, p.dot.opacity * opacity), a, b, worldMode);
|
|
747
|
+
const scale = p.sizePixels / pin.octagonSize;
|
|
748
|
+
const width = pin.resource.width * scale, height = pin.resource.height * scale;
|
|
749
|
+
const offsetX = (pin.resource.width / 2 - pin.resource.anchor[0]) * scale;
|
|
750
|
+
const padding = p.plate.enabled ? p.plate.padding : 0;
|
|
751
|
+
if (p.plate.enabled) {
|
|
752
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.PLATE, [
|
|
753
|
+
width + padding * 2,
|
|
754
|
+
height + padding * 2,
|
|
755
|
+
offsetX,
|
|
756
|
+
0
|
|
757
|
+
], rgba(p.plate.borderColor, p.plate.opacity * opacity), a, b, worldMode);
|
|
758
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.PLATE, [
|
|
759
|
+
Math.max(0, width + padding * 2 - p.plate.borderWidth * 2),
|
|
760
|
+
Math.max(0, height + padding * 2 - p.plate.borderWidth * 2),
|
|
761
|
+
offsetX,
|
|
762
|
+
p.plate.borderWidth
|
|
763
|
+
], rgba(p.plate.color, p.plate.opacity * opacity), a, b, worldMode);
|
|
764
|
+
}
|
|
765
|
+
if (p.symbolHalo.width > 0 && p.symbolHalo.opacity > 0) {
|
|
766
|
+
const radius = p.symbolHalo.width;
|
|
767
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.SYMBOL_HALO, [
|
|
768
|
+
width + radius * 2,
|
|
769
|
+
height + radius * 2,
|
|
770
|
+
offsetX,
|
|
771
|
+
padding - radius
|
|
772
|
+
], rgba(p.symbolHalo.color, p.symbolHalo.opacity * opacity), a, [
|
|
773
|
+
b[0],
|
|
774
|
+
b[1],
|
|
775
|
+
b[2],
|
|
776
|
+
radius
|
|
777
|
+
], worldMode);
|
|
778
|
+
}
|
|
779
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.SYMBOL, [
|
|
780
|
+
width,
|
|
781
|
+
height,
|
|
782
|
+
offsetX,
|
|
783
|
+
padding
|
|
784
|
+
], [
|
|
785
|
+
1,
|
|
786
|
+
1,
|
|
787
|
+
1,
|
|
788
|
+
opacity
|
|
789
|
+
], a, b, worldMode);
|
|
790
|
+
if (pin.highlightOnly) {
|
|
791
|
+
const margin = Math.max(padding, p.symbolHalo.opacity > 0 ? p.symbolHalo.width : 0);
|
|
792
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.HIGHLIGHT, [
|
|
793
|
+
width + margin * 2 + 6,
|
|
794
|
+
height + margin * 2 + 6,
|
|
795
|
+
offsetX,
|
|
796
|
+
padding - margin - 3
|
|
797
|
+
], rgba("#0284c7", 1), a, b, worldMode);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
rebuild() {
|
|
801
|
+
if (!this.ready) return;
|
|
802
|
+
if (!this.dirty) {
|
|
803
|
+
const previousPending = new Set(this.pendingElevation);
|
|
804
|
+
const center = {
|
|
805
|
+
x: this.origin[0],
|
|
806
|
+
y: this.origin[1]
|
|
807
|
+
};
|
|
808
|
+
for (const i of this.changedPins) {
|
|
809
|
+
const data = [];
|
|
810
|
+
this.buildPin(i, center, data, []);
|
|
811
|
+
const slice = this.slices[i];
|
|
812
|
+
const page = this.atlas.entries.get(this.pins[i].resource.id)?.page;
|
|
813
|
+
if (data.length !== slice.length || page !== slice.page) {
|
|
814
|
+
this.pendingElevation = previousPending;
|
|
815
|
+
this.dirty = true;
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
|
|
819
|
+
this.gl.bufferSubData(this.gl.ARRAY_BUFFER, slice.offset * Float32Array.BYTES_PER_ELEMENT, new Float32Array(data));
|
|
820
|
+
}
|
|
821
|
+
if (!this.dirty) {
|
|
822
|
+
this.changedPins.clear();
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const map = this.map, gl = this.gl;
|
|
827
|
+
if (!this.ready) return;
|
|
828
|
+
const viewCenter = map.getCenter();
|
|
829
|
+
const center = pinMercator([viewCenter.lng, viewCenter.lat]);
|
|
830
|
+
this.origin = [center.x, center.y];
|
|
831
|
+
const data = [];
|
|
832
|
+
const ranges = [];
|
|
833
|
+
this.elevations = [];
|
|
834
|
+
const previousPending = this.pendingElevation;
|
|
835
|
+
this.pendingElevation = /* @__PURE__ */ new Set();
|
|
836
|
+
this.setStatus({ status: "supported" });
|
|
837
|
+
this.slices = [];
|
|
838
|
+
for (let i = 0; i < this.pins.length; i++) {
|
|
839
|
+
const offset = data.length;
|
|
840
|
+
this.buildPin(i, center, data, ranges);
|
|
841
|
+
this.slices.push({
|
|
842
|
+
offset,
|
|
843
|
+
length: data.length - offset,
|
|
844
|
+
page: this.atlas.entries.get(this.pins[i].resource.id)?.page
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
const waiting = [...this.pendingElevation].filter((id) => !previousPending.has(id));
|
|
848
|
+
const available = [...previousPending].filter((id) => !this.pendingElevation.has(id));
|
|
849
|
+
if (waiting.length) this.onStatus?.({
|
|
850
|
+
status: "waiting-for-elevation",
|
|
851
|
+
graphicIds: waiting
|
|
852
|
+
});
|
|
853
|
+
if (available.length) this.onStatus?.({
|
|
854
|
+
status: "supported",
|
|
855
|
+
graphicIds: available
|
|
856
|
+
});
|
|
857
|
+
this.ranges = ranges;
|
|
858
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
859
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.DYNAMIC_DRAW);
|
|
860
|
+
this.dirty = false;
|
|
861
|
+
this.changedPins.clear();
|
|
862
|
+
}
|
|
863
|
+
render(context, input) {
|
|
864
|
+
if (!this.program || !this.ready || this.removed) return;
|
|
865
|
+
if (this.dirty || this.changedPins.size) try {
|
|
866
|
+
this.rebuild();
|
|
867
|
+
} catch (error) {
|
|
868
|
+
this.setStatus({
|
|
869
|
+
status: "unavailable",
|
|
870
|
+
reason: `Pin terrain: ${error}`
|
|
871
|
+
});
|
|
872
|
+
this.dirty = false;
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (this.status.status !== "supported") return;
|
|
876
|
+
this.frame = input;
|
|
877
|
+
this.projection.update(input, this.origin, this.map.getCenter(), 512 * 2 ** this.map.getZoom(), this.map.getProjection()?.type === "globe");
|
|
878
|
+
this.draw(context, false);
|
|
879
|
+
}
|
|
880
|
+
draw(gl, pick) {
|
|
881
|
+
const u = this.uniforms ?? this.resolveUniforms(gl);
|
|
882
|
+
gl.useProgram(this.program);
|
|
883
|
+
gl.bindVertexArray(this.vao);
|
|
884
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
885
|
+
gl.disable(gl.DEPTH_TEST);
|
|
886
|
+
gl.depthRange(0, 1);
|
|
887
|
+
gl.depthMask(false);
|
|
888
|
+
gl.disable(gl.CULL_FACE);
|
|
889
|
+
gl.disable(gl.STENCIL_TEST);
|
|
890
|
+
if (pick) gl.disable(gl.BLEND);
|
|
891
|
+
else {
|
|
892
|
+
gl.enable(gl.BLEND);
|
|
893
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
894
|
+
}
|
|
895
|
+
const width = this.map.getContainer().clientWidth, height = this.map.getContainer().clientHeight;
|
|
896
|
+
gl.uniformMatrix4fv(u.matrix, false, this.matrix);
|
|
897
|
+
gl.uniformMatrix4fv(u.globeMatrix, false, this.projection.globeMatrix);
|
|
898
|
+
gl.uniform4fv(u.clippingPlane, this.projection.clippingPlane);
|
|
899
|
+
gl.uniform2f(u.origin, this.origin[0], this.origin[1]);
|
|
900
|
+
gl.uniform1f(u.globeTransition, this.projection.transition);
|
|
901
|
+
gl.uniform1f(u.world, 512 * 2 ** this.map.getZoom());
|
|
902
|
+
gl.uniform2f(u.view, width, height);
|
|
903
|
+
gl.uniform2f(u.buffer, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
904
|
+
gl.uniform1i(u.pick, pick ? 1 : 0);
|
|
905
|
+
gl.uniform1i(u.atlas, 0);
|
|
906
|
+
gl.uniform1i(u.terrain, 1);
|
|
907
|
+
const terrain = this.map.terrain;
|
|
908
|
+
const needsDepth = this.needsDepth && !!this.map.getTerrain();
|
|
909
|
+
const texture = needsDepth ? terrain?._fboDepthTexture?.texture : null;
|
|
910
|
+
if (needsDepth && !texture) {
|
|
911
|
+
this.setStatus({
|
|
912
|
+
status: "unavailable",
|
|
913
|
+
reason: "MapLibre terrain depth texture is unavailable"
|
|
914
|
+
});
|
|
915
|
+
this.dirty = true;
|
|
916
|
+
gl.bindVertexArray(null);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
gl.uniform1i(u.hasTerrain, texture ? 1 : 0);
|
|
920
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
921
|
+
gl.bindTexture(gl.TEXTURE_2D, texture ?? this.textures[0]);
|
|
922
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
923
|
+
for (const range of this.ranges) {
|
|
924
|
+
gl.bindTexture(gl.TEXTURE_2D, this.textures[range.page]);
|
|
925
|
+
for (let attr = 0; attr < 6; attr++) {
|
|
926
|
+
gl.enableVertexAttribArray(attr);
|
|
927
|
+
gl.vertexAttribPointer(attr, 4, gl.FLOAT, false, STRIDE * 4, (range.first * STRIDE + attr * 4) * 4);
|
|
928
|
+
gl.vertexAttribDivisor(attr, 1);
|
|
929
|
+
}
|
|
930
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, range.count);
|
|
931
|
+
}
|
|
932
|
+
gl.bindVertexArray(null);
|
|
933
|
+
}
|
|
934
|
+
/** On-demand GPU pick shares the visual shader including alpha and terrain mask. */
|
|
935
|
+
pick(pixel, tolerance = 0) {
|
|
936
|
+
if (!this.frame || this.status.status !== "supported") return null;
|
|
937
|
+
const gl = this.gl, w = gl.drawingBufferWidth, h = gl.drawingBufferHeight;
|
|
938
|
+
const previous = gl.getParameter(gl.FRAMEBUFFER_BINDING);
|
|
939
|
+
if (this.pickSize !== `${w}/${h}`) {
|
|
940
|
+
if (this.pickTexture) gl.deleteTexture(this.pickTexture);
|
|
941
|
+
if (this.pickFramebuffer) gl.deleteFramebuffer(this.pickFramebuffer);
|
|
942
|
+
this.pickTexture = gl.createTexture();
|
|
943
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
944
|
+
gl.bindTexture(gl.TEXTURE_2D, this.pickTexture);
|
|
945
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
946
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
947
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
948
|
+
this.pickFramebuffer = gl.createFramebuffer();
|
|
949
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.pickFramebuffer);
|
|
950
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.pickTexture, 0);
|
|
951
|
+
this.pickSize = `${w}/${h}`;
|
|
952
|
+
}
|
|
953
|
+
const dpr = w / this.map.getContainer().clientWidth;
|
|
954
|
+
const radius = Math.ceil(tolerance * dpr), x = Math.max(0, Math.floor(pixel[0] * dpr) - radius), y = Math.max(0, h - 1 - Math.floor(pixel[1] * dpr) - radius), rw = Math.min(w - x, 2 * radius + 1), rh = Math.min(h - y, 2 * radius + 1);
|
|
955
|
+
if (rw <= 0 || rh <= 0) {
|
|
956
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, previous);
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.pickFramebuffer);
|
|
960
|
+
gl.viewport(0, 0, w, h);
|
|
961
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
962
|
+
gl.scissor(x, y, rw, rh);
|
|
963
|
+
gl.colorMask(true, true, true, true);
|
|
964
|
+
gl.clearColor(0, 0, 0, 0);
|
|
965
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
966
|
+
this.draw(gl, true);
|
|
967
|
+
const pixels = new Uint8Array(rw * rh * 4);
|
|
968
|
+
gl.readPixels(x, y, rw, rh, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
969
|
+
let id = 0;
|
|
970
|
+
for (let i = 0; i < pixels.length; i += 4) id = Math.max(id, pixels[i] + pixels[i + 1] * 256 + pixels[i + 2] * 65536);
|
|
971
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
972
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, previous);
|
|
973
|
+
this.map.painter.context.setDirty();
|
|
974
|
+
this.map.painter.setBaseState();
|
|
975
|
+
this.map.triggerRepaint();
|
|
976
|
+
return id ? this.pins[id - 1]?.id ?? null : null;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Shared Mercator/globe clip-space projection converted to container pixels. Callers
|
|
980
|
+
* differ in elevation source and in whether/where a screen-mode lift offset applies
|
|
981
|
+
* — those stay at the call site, as they aren't part of the shared matrix math.
|
|
982
|
+
*/
|
|
983
|
+
projectToPixel(worldX, worldY, z) {
|
|
984
|
+
const { clip: c, horizon } = this.projection.project(worldX, worldY, z);
|
|
985
|
+
if (horizon < 0 || c[3] <= 0) return [NaN, NaN];
|
|
986
|
+
const width = this.map.getContainer().clientWidth, height = this.map.getContainer().clientHeight;
|
|
987
|
+
return [(c[0] / c[3] + 1) * width / 2, (1 - c[1] / c[3]) * height / 2];
|
|
988
|
+
}
|
|
989
|
+
footprint(id) {
|
|
990
|
+
const i = this.pins.findIndex((p) => p.id === id);
|
|
991
|
+
const pin = this.pins[i];
|
|
992
|
+
if (!pin || !this.ready || !this.frame || !Number.isFinite(this.elevations[i])) return null;
|
|
993
|
+
const p = pin.presentation, coord = pinMercator(pin.position);
|
|
994
|
+
coord.x += Math.round(this.origin[0] - coord.x);
|
|
995
|
+
const world = 512 * 2 ** this.map.getZoom();
|
|
996
|
+
const project = (z) => this.projectToPixel((coord.x - this.origin[0]) * world, (coord.y - this.origin[1]) * world, z);
|
|
997
|
+
const ground = project(this.elevations[i] ?? 0), attachment = p.mode === "world" ? project((this.elevations[i] ?? 0) + p.heightMeters) : [ground[0], ground[1] - p.liftPixels];
|
|
998
|
+
if (!attachment.every(Number.isFinite)) return null;
|
|
999
|
+
const scale = p.sizePixels / pin.octagonSize, padding = p.plate.enabled ? p.plate.padding : 0, halo = p.symbolHalo.opacity > 0 ? p.symbolHalo.width : 0, overhang = Math.max(0, halo - padding);
|
|
1000
|
+
return {
|
|
1001
|
+
ground,
|
|
1002
|
+
attachment,
|
|
1003
|
+
min: [attachment[0] - pin.resource.anchor[0] * scale - padding - overhang, attachment[1] - pin.resource.height * scale - padding * 2 - overhang],
|
|
1004
|
+
max: [attachment[0] + (pin.resource.width - pin.resource.anchor[0]) * scale + padding + overhang, attachment[1] + overhang]
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
moveFromPixel(id, attachment) {
|
|
1008
|
+
const pin = this.pins.find((pin) => pin.id === id);
|
|
1009
|
+
if (!pin || !this.frame) return null;
|
|
1010
|
+
const p = pin.presentation;
|
|
1011
|
+
const project = (ll) => {
|
|
1012
|
+
const c = pinMercator(ll);
|
|
1013
|
+
c.x += Math.round(this.origin[0] - c.x);
|
|
1014
|
+
const world = 512 * 2 ** this.map.getZoom();
|
|
1015
|
+
const [px, py] = this.projectToPixel((c.x - this.origin[0]) * world, (c.y - this.origin[1]) * world, this.elevation(ll) + (p.mode === "world" ? p.heightMeters : 0));
|
|
1016
|
+
return [px, py - (p.mode === "screen" ? p.liftPixels : 0)];
|
|
1017
|
+
};
|
|
1018
|
+
return invertPinProjection(project, pin.position, attachment, this.map.getProjection()?.type === "globe" ? 90 : void 0);
|
|
1019
|
+
}
|
|
1020
|
+
onRemove(_map, context) {
|
|
1021
|
+
this.removed = true;
|
|
1022
|
+
this.version++;
|
|
1023
|
+
const gl = context;
|
|
1024
|
+
this.onRemoveListeners();
|
|
1025
|
+
if (this.pickTexture) gl.deleteTexture(this.pickTexture);
|
|
1026
|
+
if (this.pickFramebuffer) gl.deleteFramebuffer(this.pickFramebuffer);
|
|
1027
|
+
if (this.vao) gl.deleteVertexArray(this.vao);
|
|
1028
|
+
if (this.buffer) gl.deleteBuffer(this.buffer);
|
|
1029
|
+
if (this.program) gl.deleteProgram(this.program);
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
//#endregion
|
|
3
1033
|
//#region src/mercator.ts
|
|
4
1034
|
/**
|
|
5
1035
|
* Web Mercator world circumference in meters (2π × 6378137). The single source
|
|
@@ -255,6 +1285,25 @@ function emptyFC() {
|
|
|
255
1285
|
};
|
|
256
1286
|
}
|
|
257
1287
|
/**
|
|
1288
|
+
* Builds the pin-status emitter passed to each `PinLayer`. Bound only to the
|
|
1289
|
+
* `listeners` set (never to the adapter itself, via `this`), so a `PinLayer`
|
|
1290
|
+
* holding onto this callback for its lifetime does not transitively retain
|
|
1291
|
+
* the whole `MapLibreAdapter` object graph (map, all layer handles, etc.).
|
|
1292
|
+
*/
|
|
1293
|
+
function createPinStatusDispatcher(listeners) {
|
|
1294
|
+
return (event) => {
|
|
1295
|
+
queueMicrotask(() => {
|
|
1296
|
+
const errors = [];
|
|
1297
|
+
for (const listener of listeners) try {
|
|
1298
|
+
listener(event);
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
errors.push(error);
|
|
1301
|
+
}
|
|
1302
|
+
if (errors.length) throw new AggregateError(errors, "Pin presentation status listener errors");
|
|
1303
|
+
});
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
258
1307
|
* Build the paint expression for a circle layer with per-feature
|
|
259
1308
|
* `properties.style` overlay (ADR-0006). Each property prefers a value
|
|
260
1309
|
* pulled from `properties.style.<field>`, falling back to the layer default.
|
|
@@ -825,12 +1874,36 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
825
1874
|
}
|
|
826
1875
|
destroyLayer(handle) {
|
|
827
1876
|
this.clearGroupBundles(handle);
|
|
1877
|
+
handle.pinResources?.dispose();
|
|
828
1878
|
for (const mlId of handle.mlLayerIds) if (this.map.getLayer(mlId)) this.map.removeLayer(mlId);
|
|
829
1879
|
if (this.map.getSource(handle.sourceId)) this.map.removeSource(handle.sourceId);
|
|
830
1880
|
}
|
|
831
|
-
applyFeatureGroups(layerId,
|
|
1881
|
+
applyFeatureGroups(layerId, input) {
|
|
1882
|
+
const groups = [];
|
|
1883
|
+
for (const group of input) if (group.pin && this.getPinPresentationCapability().status !== "unsupported") {
|
|
1884
|
+
const previous = groups[groups.length - 1];
|
|
1885
|
+
if (previous?.pinBatch) previous.pinBatch.push(group.pin);
|
|
1886
|
+
else groups.push({
|
|
1887
|
+
id: `pin-run:${group.id}`,
|
|
1888
|
+
features: [],
|
|
1889
|
+
iconResources: [],
|
|
1890
|
+
pinBatch: [group.pin]
|
|
1891
|
+
});
|
|
1892
|
+
} else groups.push({
|
|
1893
|
+
...group,
|
|
1894
|
+
id: `ordinary:${group.id}`
|
|
1895
|
+
});
|
|
832
1896
|
const handle = this.layers.get(layerId);
|
|
833
1897
|
if (!handle) return;
|
|
1898
|
+
const allPins = groups.flatMap((group) => group.pinBatch ?? []);
|
|
1899
|
+
if (allPins.length) {
|
|
1900
|
+
handle.pinResources ??= new PinResources(this.map);
|
|
1901
|
+
const previousBatch = handle.lastPinBatch;
|
|
1902
|
+
if (!(previousBatch !== void 0 && previousBatch.length === allPins.length && allPins.every((pin, index) => pin === previousBatch[index]))) {
|
|
1903
|
+
handle.pinResources.setRoster(allPins);
|
|
1904
|
+
handle.lastPinBatch = allPins;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
834
1907
|
const desiredGroupIds = new Set(groups.map((group) => group.id));
|
|
835
1908
|
const previousGroupOrder = handle.groupOrder.filter((groupId) => desiredGroupIds.has(groupId));
|
|
836
1909
|
const bundlesNeedingOrder = /* @__PURE__ */ new Set();
|
|
@@ -846,6 +1919,26 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
846
1919
|
bundle = this.createGroupBundle(layerId, handle);
|
|
847
1920
|
handle.groupBundles.set(group.id, bundle);
|
|
848
1921
|
}
|
|
1922
|
+
if (group.pinBatch) {
|
|
1923
|
+
if (!bundle.pinLayer) {
|
|
1924
|
+
const custom = new PinLayer(`${bundle.sourceId}-pins`, handle.pinResources, this.emitPinStatus);
|
|
1925
|
+
this.map.addLayer(custom, handle.mlLayerIds[0]);
|
|
1926
|
+
bundle.pinLayer = custom;
|
|
1927
|
+
bundlesNeedingOrder.add(group.id);
|
|
1928
|
+
}
|
|
1929
|
+
const previous = bundle.pinSignature;
|
|
1930
|
+
let changedIndices;
|
|
1931
|
+
if (previous?.length === group.pinBatch.length) {
|
|
1932
|
+
changedIndices = [];
|
|
1933
|
+
for (let i = 0; i < group.pinBatch.length; i++) if (group.pinBatch[i] !== previous[i]) changedIndices.push(i);
|
|
1934
|
+
}
|
|
1935
|
+
if (!previous || bundle.pinResourceRevision !== handle.pinResources?.revision || previous.length !== group.pinBatch.length || !!changedIndices?.length) {
|
|
1936
|
+
bundle.pinSignature = group.pinBatch;
|
|
1937
|
+
bundle.pinResourceRevision = handle.pinResources?.revision;
|
|
1938
|
+
bundle.pinLayer.setPins(group.pinBatch, changedIndices);
|
|
1939
|
+
}
|
|
1940
|
+
continue;
|
|
1941
|
+
}
|
|
849
1942
|
let contentChanged;
|
|
850
1943
|
if (!isNewBundle && bundle.revision !== void 0 && group.revision !== void 0) {
|
|
851
1944
|
contentChanged = bundle.revision !== group.revision;
|
|
@@ -864,6 +1957,11 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
864
1957
|
if (this.reconcileGroupPaintSurfaces(handle, bundle, group.features)) bundlesNeedingOrder.add(group.id);
|
|
865
1958
|
if (contentChanged || resourcesChanged) this.updateGroupBundleSource(bundle);
|
|
866
1959
|
}
|
|
1960
|
+
if (!allPins.length) {
|
|
1961
|
+
handle.pinResources?.dispose();
|
|
1962
|
+
handle.pinResources = void 0;
|
|
1963
|
+
handle.lastPinBatch = void 0;
|
|
1964
|
+
}
|
|
867
1965
|
handle.groupOrder = groups.map((group) => group.id);
|
|
868
1966
|
this.orderGroupBundles(handle, previousGroupOrder, handle.groupOrder, bundlesNeedingOrder);
|
|
869
1967
|
}
|
|
@@ -888,6 +1986,7 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
888
1986
|
};
|
|
889
1987
|
}
|
|
890
1988
|
destroyGroupBundle(bundle) {
|
|
1989
|
+
if (bundle.pinLayer && this.map.getLayer(bundle.pinLayer.id)) this.map.removeLayer(bundle.pinLayer.id);
|
|
891
1990
|
for (const overlay of bundle.overlays.values()) {
|
|
892
1991
|
if (this.map.getLayer(overlay.layerId)) this.map.removeLayer(overlay.layerId);
|
|
893
1992
|
if (this.map.getSource(overlay.sourceId)) this.map.removeSource(overlay.sourceId);
|
|
@@ -954,6 +2053,9 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
954
2053
|
for (const bundle of handle.groupBundles.values()) this.destroyGroupBundle(bundle);
|
|
955
2054
|
handle.groupBundles.clear();
|
|
956
2055
|
handle.groupOrder = [];
|
|
2056
|
+
handle.pinResources?.dispose();
|
|
2057
|
+
handle.pinResources = void 0;
|
|
2058
|
+
handle.lastPinBatch = void 0;
|
|
957
2059
|
}
|
|
958
2060
|
requiredIconIds(group) {
|
|
959
2061
|
const referenced = new Set(group.features.flatMap((feature) => {
|
|
@@ -1099,13 +2201,14 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
1099
2201
|
const bundle = handle.groupBundles.get(groupId);
|
|
1100
2202
|
if (!bundle) continue;
|
|
1101
2203
|
if (forcedGroupIds.has(groupId) || stableGroupIds !== null && !stableGroupIds.has(groupId)) {
|
|
2204
|
+
if (bundle.pinLayer && this.map.getLayer(bundle.pinLayer.id)) this.map.moveLayer(bundle.pinLayer.id, beforeId);
|
|
1102
2205
|
for (const overlay of bundle.overlays.values()) if (this.map.getLayer(overlay.layerId)) this.map.moveLayer(overlay.layerId, beforeId);
|
|
1103
2206
|
for (const key of bundle.paintOrder) {
|
|
1104
2207
|
const surface = bundle.paintSurfaces.get(key);
|
|
1105
2208
|
if (surface && this.map.getLayer(surface.layerId)) this.map.moveLayer(surface.layerId, beforeId);
|
|
1106
2209
|
}
|
|
1107
2210
|
}
|
|
1108
|
-
beforeId = firstBundleLayerId(bundle) ?? beforeId;
|
|
2211
|
+
beforeId = bundle.pinLayer?.id ?? firstBundleLayerId(bundle) ?? beforeId;
|
|
1109
2212
|
}
|
|
1110
2213
|
}
|
|
1111
2214
|
nativeLayerIds(handle) {
|
|
@@ -1200,10 +2303,11 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
1200
2303
|
for (const event of this.viewChangeEvents()) this.map.off(event, token.fn);
|
|
1201
2304
|
}
|
|
1202
2305
|
viewChangeEvents() {
|
|
1203
|
-
return this.viewChangeMode === "settle" ? ["moveend"] : [
|
|
2306
|
+
return this.viewChangeMode === "settle" ? ["moveend", "projectiontransition"] : [
|
|
1204
2307
|
"move",
|
|
1205
2308
|
"resize",
|
|
1206
|
-
"moveend"
|
|
2309
|
+
"moveend",
|
|
2310
|
+
"projectiontransition"
|
|
1207
2311
|
];
|
|
1208
2312
|
}
|
|
1209
2313
|
createEditPointerDriver() {
|
|
@@ -1464,7 +2568,7 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
1464
2568
|
id: feature.id,
|
|
1465
2569
|
geometry: feature.geometry,
|
|
1466
2570
|
properties: feature.properties
|
|
1467
|
-
})), this.featureCache.get(layerId) ?? []);
|
|
2571
|
+
})), (this.featureCache.get(layerId) ?? []).filter((f) => !f.properties?.pinPresentation));
|
|
1468
2572
|
const size = this.getViewportSize();
|
|
1469
2573
|
const candidate = evaluatePickCandidates(relevantFeatures.map(({ feature, nativeHit, visualOrder }) => {
|
|
1470
2574
|
const iconId = feature.properties?.iconId;
|
|
@@ -1495,6 +2599,17 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
1495
2599
|
},
|
|
1496
2600
|
bearing: this.getBearing()
|
|
1497
2601
|
});
|
|
2602
|
+
const pinHit = this.pinHit(layerId, [x, y], tol);
|
|
2603
|
+
if (pinHit && (!candidate || pinHit.order >= (this.getFeatureGroupOrder(layerId, candidate.feature.id) ?? -1))) {
|
|
2604
|
+
handler({
|
|
2605
|
+
id: pinHit.id,
|
|
2606
|
+
feature: pinHit.feature,
|
|
2607
|
+
pixel: [x, y],
|
|
2608
|
+
coordinate: [e.lngLat.lng, e.lngLat.lat],
|
|
2609
|
+
originalEvent
|
|
2610
|
+
});
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
1498
2613
|
if (!candidate) return;
|
|
1499
2614
|
const cmId = candidate.graphicId;
|
|
1500
2615
|
if (cmId == null) return;
|
|
@@ -1511,6 +2626,74 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
1511
2626
|
return () => this.map.off("click", onMlClick);
|
|
1512
2627
|
});
|
|
1513
2628
|
}
|
|
2629
|
+
pinStatusListeners = /* @__PURE__ */ new Set();
|
|
2630
|
+
emitPinStatus = createPinStatusDispatcher(this.pinStatusListeners);
|
|
2631
|
+
onPinPresentationStatusChange(handler) {
|
|
2632
|
+
this.pinStatusListeners.add(handler);
|
|
2633
|
+
return () => this.pinStatusListeners.delete(handler);
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Pin layers across this adapter's group bundles.
|
|
2637
|
+
* - With no `handle`: every layer's bundles, in bundle-map (creation)
|
|
2638
|
+
* order — fine for id-based lookups where only "does one match" matters.
|
|
2639
|
+
* - With `handle`: only that layer's bundles, walked topmost-first (reverse
|
|
2640
|
+
* paint order) — required for hit-testing, where the first hit must be
|
|
2641
|
+
* the visually topmost pin.
|
|
2642
|
+
*/
|
|
2643
|
+
*pinLayers(handle) {
|
|
2644
|
+
if (handle) {
|
|
2645
|
+
for (const groupId of [...handle.groupOrder].reverse()) {
|
|
2646
|
+
const pinLayer = handle.groupBundles.get(groupId)?.pinLayer;
|
|
2647
|
+
if (pinLayer) yield pinLayer;
|
|
2648
|
+
}
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
for (const h of this.layers.values()) for (const bundle of h.groupBundles.values()) if (bundle.pinLayer) yield bundle.pinLayer;
|
|
2652
|
+
}
|
|
2653
|
+
/** First non-null/undefined result of `fn` across all pin layers, else null. */
|
|
2654
|
+
findPinLayer(fn) {
|
|
2655
|
+
for (const pinLayer of this.pinLayers()) {
|
|
2656
|
+
const result = fn(pinLayer);
|
|
2657
|
+
if (result != null) return result;
|
|
2658
|
+
}
|
|
2659
|
+
return null;
|
|
2660
|
+
}
|
|
2661
|
+
getPinPresentationCapability(graphicId) {
|
|
2662
|
+
if (!(maplibregl.getVersion?.() ?? "").startsWith("6.")) return {
|
|
2663
|
+
status: "unsupported",
|
|
2664
|
+
reason: "Pins require MapLibre 6"
|
|
2665
|
+
};
|
|
2666
|
+
if (graphicId) {
|
|
2667
|
+
const status = this.findPinLayer((pinLayer) => pinLayer.has(graphicId) ? pinLayer.getStatus(graphicId) : null);
|
|
2668
|
+
if (status) return status;
|
|
2669
|
+
}
|
|
2670
|
+
return { status: "supported" };
|
|
2671
|
+
}
|
|
2672
|
+
getPinFootprint(id) {
|
|
2673
|
+
return this.findPinLayer((pinLayer) => pinLayer.footprint(id));
|
|
2674
|
+
}
|
|
2675
|
+
movePinFromPixel(id, attachment) {
|
|
2676
|
+
return this.findPinLayer((pinLayer) => pinLayer.moveFromPixel(id, attachment));
|
|
2677
|
+
}
|
|
2678
|
+
pinHit(layerId, pixel, tolerance) {
|
|
2679
|
+
const handle = this.layers.get(layerId);
|
|
2680
|
+
if (!handle) return null;
|
|
2681
|
+
for (const pinLayer of this.pinLayers(handle)) {
|
|
2682
|
+
const id = pinLayer.pick(pixel, tolerance);
|
|
2683
|
+
if (!id) continue;
|
|
2684
|
+
const feature = this.featureCache.get(layerId)?.find((f) => f.properties?.graphicId === id);
|
|
2685
|
+
if (feature) return {
|
|
2686
|
+
id,
|
|
2687
|
+
feature,
|
|
2688
|
+
order: this.getFeatureGroupOrder(layerId, feature.id) ?? 0
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
return null;
|
|
2692
|
+
}
|
|
2693
|
+
pickPin(id, pixel, tolerance) {
|
|
2694
|
+
for (const [layerId] of this.layers) if (this.pinHit(layerId, pixel, tolerance)?.id === id) return true;
|
|
2695
|
+
return false;
|
|
2696
|
+
}
|
|
1514
2697
|
setGlobeMode(enabled) {
|
|
1515
2698
|
if (enabled) this.map.setProjection({ type: "globe" });
|
|
1516
2699
|
else this.map.setProjection({ type: "mercator" });
|