@orbat-mapper/tactical-draw-adapter-maplibre 0.3.22 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +141 -3
- package/dist/index.mjs +1201 -7
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,1046 @@
|
|
|
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 manager = terrain.tileManager ?? terrain.sourceCache;
|
|
527
|
+
const source = terrain.tileManager ? terrain.tileManager.getSource() : terrain.sourceCache.sourceCache.getSource();
|
|
528
|
+
const { tileID, mercatorX, mercatorY } = terrain._getOverscaledTileIDFromLngLatZoom(new LngLat(position[0], position[1]), Math.min(manager.maxzoom, source.maxzoom + manager.deltaZoom));
|
|
529
|
+
const x = mercatorX % 8192;
|
|
530
|
+
const y = mercatorY % 8192;
|
|
531
|
+
const getSampler = terrain.getElevationSampler ?? terrain._getElevationSampler;
|
|
532
|
+
let value;
|
|
533
|
+
if (getSampler) {
|
|
534
|
+
const sampler = getSampler.call(terrain, tileID);
|
|
535
|
+
if (!sampler) return NaN;
|
|
536
|
+
value = sampler(x, y, 8192) * terrain.exaggeration;
|
|
537
|
+
} else {
|
|
538
|
+
if (!manager.getSourceTile(tileID, true)?.dem) return NaN;
|
|
539
|
+
value = terrain.getElevation(tileID, x, y, 8192);
|
|
540
|
+
}
|
|
541
|
+
this.elevationCache.set(key, value);
|
|
542
|
+
return value;
|
|
543
|
+
}
|
|
544
|
+
has(id) {
|
|
545
|
+
return this.pins.some((pin) => pin.id === id);
|
|
546
|
+
}
|
|
547
|
+
constructor(id, resources, onStatus) {
|
|
548
|
+
this.id = id;
|
|
549
|
+
this.resources = resources;
|
|
550
|
+
this.onStatus = onStatus;
|
|
551
|
+
}
|
|
552
|
+
restoreContext = () => {
|
|
553
|
+
this.ready = false;
|
|
554
|
+
this.resourceRevision = -1;
|
|
555
|
+
this.textures = [];
|
|
556
|
+
this.pickSize = "";
|
|
557
|
+
this.pickTexture = void 0;
|
|
558
|
+
this.pickFramebuffer = void 0;
|
|
559
|
+
this.onRemoveListeners();
|
|
560
|
+
this.onAdd(this.map, this.gl);
|
|
561
|
+
this.setPins(this.pins);
|
|
562
|
+
};
|
|
563
|
+
onRemoveListeners() {
|
|
564
|
+
this.map.off("sourcedata", this.refreshTerrain);
|
|
565
|
+
this.map.off("terrain", this.refreshTerrain);
|
|
566
|
+
this.map.off("resize", this.refreshTerrain);
|
|
567
|
+
this.map.off("moveend", this.refreshTerrain);
|
|
568
|
+
this.map.off("webglcontextrestored", this.restoreContext);
|
|
569
|
+
}
|
|
570
|
+
onAdd(map, context) {
|
|
571
|
+
this.removed = false;
|
|
572
|
+
this.map = map;
|
|
573
|
+
this.gl = context;
|
|
574
|
+
const gl = this.gl;
|
|
575
|
+
try {
|
|
576
|
+
const program = gl.createProgram();
|
|
577
|
+
for (const [type, source] of [[gl.VERTEX_SHADER, PIN_VERTEX], [gl.FRAGMENT_SHADER, PIN_FRAGMENT]]) {
|
|
578
|
+
const s = shader(gl, type, source);
|
|
579
|
+
gl.attachShader(program, s);
|
|
580
|
+
gl.deleteShader(s);
|
|
581
|
+
}
|
|
582
|
+
gl.linkProgram(program);
|
|
583
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program) ?? "Pin link failed");
|
|
584
|
+
this.program = program;
|
|
585
|
+
this.uniforms = this.resolveUniforms(gl);
|
|
586
|
+
this.buffer = gl.createBuffer();
|
|
587
|
+
this.vao = gl.createVertexArray();
|
|
588
|
+
map.on("webglcontextrestored", this.restoreContext);
|
|
589
|
+
map.on("sourcedata", this.refreshTerrain);
|
|
590
|
+
map.on("terrain", this.refreshTerrain);
|
|
591
|
+
map.on("resize", this.refreshTerrain);
|
|
592
|
+
map.on("moveend", this.refreshTerrain);
|
|
593
|
+
} catch (error) {
|
|
594
|
+
this.setStatus({
|
|
595
|
+
status: "unavailable",
|
|
596
|
+
reason: String(error)
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/** Uniform locations are fixed once a program is linked; resolved here, not per draw. */
|
|
601
|
+
resolveUniforms(gl) {
|
|
602
|
+
const program = this.program;
|
|
603
|
+
const uniforms = {
|
|
604
|
+
matrix: gl.getUniformLocation(program, "u_matrix"),
|
|
605
|
+
globeMatrix: gl.getUniformLocation(program, "u_globeMatrix"),
|
|
606
|
+
clippingPlane: gl.getUniformLocation(program, "u_clippingPlane"),
|
|
607
|
+
origin: gl.getUniformLocation(program, "u_origin"),
|
|
608
|
+
globeTransition: gl.getUniformLocation(program, "u_globeTransition"),
|
|
609
|
+
world: gl.getUniformLocation(program, "u_world"),
|
|
610
|
+
view: gl.getUniformLocation(program, "u_view"),
|
|
611
|
+
buffer: gl.getUniformLocation(program, "u_buffer"),
|
|
612
|
+
pick: gl.getUniformLocation(program, "u_pick"),
|
|
613
|
+
atlas: gl.getUniformLocation(program, "u_atlas"),
|
|
614
|
+
terrain: gl.getUniformLocation(program, "u_terrain"),
|
|
615
|
+
hasTerrain: gl.getUniformLocation(program, "u_hasTerrain")
|
|
616
|
+
};
|
|
617
|
+
this.uniforms = uniforms;
|
|
618
|
+
return uniforms;
|
|
619
|
+
}
|
|
620
|
+
setStatus(status) {
|
|
621
|
+
if (status.status === this.status.status && status.reason === this.status.reason) return;
|
|
622
|
+
this.status = status;
|
|
623
|
+
this.onStatus?.({
|
|
624
|
+
...status,
|
|
625
|
+
graphicIds: this.pins.filter((p) => !p.highlightOnly).map((p) => p.id)
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
getStatus(id) {
|
|
629
|
+
return id && this.pendingElevation.has(id) ? { status: "waiting-for-elevation" } : { ...this.status };
|
|
630
|
+
}
|
|
631
|
+
async setPins(pins, changedIndices) {
|
|
632
|
+
if (pins.length !== this.pins.length) this.dirty = true;
|
|
633
|
+
else {
|
|
634
|
+
const changed = changedIndices ?? pins.flatMap((pin, i) => pin !== this.pins[i] ? [i] : []);
|
|
635
|
+
for (const i of changed) {
|
|
636
|
+
if (pins[i].id !== this.pins[i].id) this.dirty = true;
|
|
637
|
+
this.changedPins.add(i);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
this.pins = pins;
|
|
641
|
+
if (this.elevationCache.size > pins.length * 60 + 1e3) this.elevationCache.clear();
|
|
642
|
+
if (!this.program) {
|
|
643
|
+
this.onStatus?.({
|
|
644
|
+
...this.status,
|
|
645
|
+
graphicIds: pins.map((pin) => pin.id)
|
|
646
|
+
});
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
this.needsDepth = pins.some((p) => p.presentation.occlusion === "terrain");
|
|
650
|
+
if (this.resourceRevision === this.resources.revision && this.ready) {
|
|
651
|
+
this.map.triggerRepaint();
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
this.dirty = true;
|
|
655
|
+
this.resourceRevision = this.resources.revision;
|
|
656
|
+
this.ready = false;
|
|
657
|
+
this.setStatus({ status: "loading" });
|
|
658
|
+
const version = ++this.version;
|
|
659
|
+
try {
|
|
660
|
+
const snapshot = await this.resources.load(this.gl);
|
|
661
|
+
if (!snapshot || version !== this.version || this.removed) return;
|
|
662
|
+
this.atlas = snapshot.atlas;
|
|
663
|
+
this.textures = snapshot.textures;
|
|
664
|
+
this.ready = true;
|
|
665
|
+
this.map.triggerRepaint();
|
|
666
|
+
} catch (error) {
|
|
667
|
+
this.setStatus({
|
|
668
|
+
status: "unavailable",
|
|
669
|
+
reason: `Pin resources: ${error}`
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
buildPin(i, center, data, ranges) {
|
|
674
|
+
const pin = this.pins[i], p = pin.presentation;
|
|
675
|
+
const anchor = pinMercator(pin.position);
|
|
676
|
+
anchor.x += Math.round(center.x - anchor.x);
|
|
677
|
+
const elevation = this.elevation(pin.position);
|
|
678
|
+
this.elevations[i] = elevation;
|
|
679
|
+
if (!Number.isFinite(elevation)) {
|
|
680
|
+
this.pendingElevation.add(pin.id);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
this.pendingElevation.delete(pin.id);
|
|
684
|
+
const a = [
|
|
685
|
+
anchor.x - center.x,
|
|
686
|
+
anchor.y - center.y,
|
|
687
|
+
elevation,
|
|
688
|
+
p.liftPixels
|
|
689
|
+
];
|
|
690
|
+
const b = [
|
|
691
|
+
a[0],
|
|
692
|
+
a[1],
|
|
693
|
+
elevation + (p.mode === "world" ? p.heightMeters : 0),
|
|
694
|
+
p.plate.radius
|
|
695
|
+
];
|
|
696
|
+
const image = this.atlas.entries.get(pin.resource.id);
|
|
697
|
+
const opacity = pin.opacity;
|
|
698
|
+
const highlightOnly = pin.highlightOnly;
|
|
699
|
+
const instanceId = i + 1;
|
|
700
|
+
const occlusionFlag = p.occlusion === "terrain" ? 1 : 0;
|
|
701
|
+
const worldMode = p.mode === "world" ? 1 : 0;
|
|
702
|
+
const haloColor = rgba(p.halo.color, p.halo.opacity * opacity);
|
|
703
|
+
const stemColor = rgba(p.stem.color, p.stem.opacity * opacity);
|
|
704
|
+
if (pin.direction !== void 0 && !pin.highlightOnly) {
|
|
705
|
+
const elevationAt = (ll) => this.elevation(ll);
|
|
706
|
+
const angle = pin.direction, length = 180;
|
|
707
|
+
const tip = [Math.sin(angle) * length, Math.cos(angle) * length];
|
|
708
|
+
const paths = [
|
|
709
|
+
[[0, 0], tip],
|
|
710
|
+
[[tip[0] - Math.sin(angle - .5) * length * .25, tip[1] - Math.cos(angle - .5) * length * .25], tip],
|
|
711
|
+
[[tip[0] - Math.sin(angle + .5) * length * .25, tip[1] - Math.cos(angle + .5) * length * .25], tip]
|
|
712
|
+
];
|
|
713
|
+
const saved = [...a];
|
|
714
|
+
for (const [from, to] of paths) for (let segment = 0; segment < 8; segment++) {
|
|
715
|
+
const ground = arrowSegmentPoint(center, pin.position, elevationAt, from, to, segment / 8);
|
|
716
|
+
const target = arrowSegmentPoint(center, pin.position, elevationAt, from, to, (segment + 1) / 8);
|
|
717
|
+
if (!Number.isFinite(ground[2]) || !Number.isFinite(target[2])) continue;
|
|
718
|
+
a.splice(0, 4, ...ground);
|
|
719
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.STEM, [
|
|
720
|
+
p.stem.width + 2 * p.halo.width,
|
|
721
|
+
0,
|
|
722
|
+
0,
|
|
723
|
+
0
|
|
724
|
+
], haloColor, a, target, 1);
|
|
725
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.STEM, [
|
|
726
|
+
p.stem.width,
|
|
727
|
+
0,
|
|
728
|
+
0,
|
|
729
|
+
0
|
|
730
|
+
], stemColor, a, target, 1);
|
|
731
|
+
}
|
|
732
|
+
a.splice(0, 4, ...saved);
|
|
733
|
+
}
|
|
734
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.STEM, [
|
|
735
|
+
p.stem.width + 2 * p.halo.width,
|
|
736
|
+
0,
|
|
737
|
+
0,
|
|
738
|
+
0
|
|
739
|
+
], haloColor, a, b, worldMode);
|
|
740
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.STEM, [
|
|
741
|
+
p.stem.width,
|
|
742
|
+
0,
|
|
743
|
+
0,
|
|
744
|
+
0
|
|
745
|
+
], stemColor, a, b, worldMode);
|
|
746
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.DOT, [
|
|
747
|
+
2 * (p.dot.radius + p.halo.width),
|
|
748
|
+
2 * (p.dot.radius + p.halo.width),
|
|
749
|
+
0,
|
|
750
|
+
0
|
|
751
|
+
], haloColor, a, b, worldMode);
|
|
752
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.DOT, [
|
|
753
|
+
2 * p.dot.radius,
|
|
754
|
+
2 * p.dot.radius,
|
|
755
|
+
0,
|
|
756
|
+
0
|
|
757
|
+
], rgba(p.dot.color, p.dot.opacity * opacity), a, b, worldMode);
|
|
758
|
+
const scale = p.sizePixels / pin.octagonSize;
|
|
759
|
+
const width = pin.resource.width * scale, height = pin.resource.height * scale;
|
|
760
|
+
const offsetX = (pin.resource.width / 2 - pin.resource.anchor[0]) * scale;
|
|
761
|
+
const padding = p.plate.enabled ? p.plate.padding : 0;
|
|
762
|
+
if (p.plate.enabled) {
|
|
763
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.PLATE, [
|
|
764
|
+
width + padding * 2,
|
|
765
|
+
height + padding * 2,
|
|
766
|
+
offsetX,
|
|
767
|
+
0
|
|
768
|
+
], rgba(p.plate.borderColor, p.plate.opacity * opacity), a, b, worldMode);
|
|
769
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.PLATE, [
|
|
770
|
+
Math.max(0, width + padding * 2 - p.plate.borderWidth * 2),
|
|
771
|
+
Math.max(0, height + padding * 2 - p.plate.borderWidth * 2),
|
|
772
|
+
offsetX,
|
|
773
|
+
p.plate.borderWidth
|
|
774
|
+
], rgba(p.plate.color, p.plate.opacity * opacity), a, b, worldMode);
|
|
775
|
+
}
|
|
776
|
+
if (p.symbolHalo.width > 0 && p.symbolHalo.opacity > 0) {
|
|
777
|
+
const radius = p.symbolHalo.width;
|
|
778
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.SYMBOL_HALO, [
|
|
779
|
+
width + radius * 2,
|
|
780
|
+
height + radius * 2,
|
|
781
|
+
offsetX,
|
|
782
|
+
padding - radius
|
|
783
|
+
], rgba(p.symbolHalo.color, p.symbolHalo.opacity * opacity), a, [
|
|
784
|
+
b[0],
|
|
785
|
+
b[1],
|
|
786
|
+
b[2],
|
|
787
|
+
radius
|
|
788
|
+
], worldMode);
|
|
789
|
+
}
|
|
790
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.SYMBOL, [
|
|
791
|
+
width,
|
|
792
|
+
height,
|
|
793
|
+
offsetX,
|
|
794
|
+
padding
|
|
795
|
+
], [
|
|
796
|
+
1,
|
|
797
|
+
1,
|
|
798
|
+
1,
|
|
799
|
+
opacity
|
|
800
|
+
], a, b, worldMode);
|
|
801
|
+
if (pin.highlightOnly) {
|
|
802
|
+
const margin = Math.max(padding, p.symbolHalo.opacity > 0 ? p.symbolHalo.width : 0);
|
|
803
|
+
appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionFlag, PIN_KIND.HIGHLIGHT, [
|
|
804
|
+
width + margin * 2 + 6,
|
|
805
|
+
height + margin * 2 + 6,
|
|
806
|
+
offsetX,
|
|
807
|
+
padding - margin - 3
|
|
808
|
+
], rgba("#0284c7", 1), a, b, worldMode);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
rebuild() {
|
|
812
|
+
if (!this.ready) return;
|
|
813
|
+
if (!this.dirty) {
|
|
814
|
+
const previousPending = new Set(this.pendingElevation);
|
|
815
|
+
const center = {
|
|
816
|
+
x: this.origin[0],
|
|
817
|
+
y: this.origin[1]
|
|
818
|
+
};
|
|
819
|
+
for (const i of this.changedPins) {
|
|
820
|
+
const data = [];
|
|
821
|
+
this.buildPin(i, center, data, []);
|
|
822
|
+
const slice = this.slices[i];
|
|
823
|
+
const page = this.atlas.entries.get(this.pins[i].resource.id)?.page;
|
|
824
|
+
if (data.length !== slice.length || page !== slice.page) {
|
|
825
|
+
this.pendingElevation = previousPending;
|
|
826
|
+
this.dirty = true;
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
|
|
830
|
+
this.gl.bufferSubData(this.gl.ARRAY_BUFFER, slice.offset * Float32Array.BYTES_PER_ELEMENT, new Float32Array(data));
|
|
831
|
+
}
|
|
832
|
+
if (!this.dirty) {
|
|
833
|
+
this.changedPins.clear();
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const map = this.map, gl = this.gl;
|
|
838
|
+
if (!this.ready) return;
|
|
839
|
+
const viewCenter = map.getCenter();
|
|
840
|
+
const center = pinMercator([viewCenter.lng, viewCenter.lat]);
|
|
841
|
+
this.origin = [center.x, center.y];
|
|
842
|
+
const data = [];
|
|
843
|
+
const ranges = [];
|
|
844
|
+
this.elevations = [];
|
|
845
|
+
const previousPending = this.pendingElevation;
|
|
846
|
+
this.pendingElevation = /* @__PURE__ */ new Set();
|
|
847
|
+
this.setStatus({ status: "supported" });
|
|
848
|
+
this.slices = [];
|
|
849
|
+
for (let i = 0; i < this.pins.length; i++) {
|
|
850
|
+
const offset = data.length;
|
|
851
|
+
this.buildPin(i, center, data, ranges);
|
|
852
|
+
this.slices.push({
|
|
853
|
+
offset,
|
|
854
|
+
length: data.length - offset,
|
|
855
|
+
page: this.atlas.entries.get(this.pins[i].resource.id)?.page
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
const waiting = [...this.pendingElevation].filter((id) => !previousPending.has(id));
|
|
859
|
+
const available = [...previousPending].filter((id) => !this.pendingElevation.has(id));
|
|
860
|
+
if (waiting.length) this.onStatus?.({
|
|
861
|
+
status: "waiting-for-elevation",
|
|
862
|
+
graphicIds: waiting
|
|
863
|
+
});
|
|
864
|
+
if (available.length) this.onStatus?.({
|
|
865
|
+
status: "supported",
|
|
866
|
+
graphicIds: available
|
|
867
|
+
});
|
|
868
|
+
this.ranges = ranges;
|
|
869
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
870
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.DYNAMIC_DRAW);
|
|
871
|
+
this.dirty = false;
|
|
872
|
+
this.changedPins.clear();
|
|
873
|
+
}
|
|
874
|
+
render(context, input) {
|
|
875
|
+
if (!this.program || !this.ready || this.removed) return;
|
|
876
|
+
if (this.dirty || this.changedPins.size) try {
|
|
877
|
+
this.rebuild();
|
|
878
|
+
} catch (error) {
|
|
879
|
+
this.setStatus({
|
|
880
|
+
status: "unavailable",
|
|
881
|
+
reason: `Pin terrain: ${error}`
|
|
882
|
+
});
|
|
883
|
+
this.dirty = false;
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (this.status.status !== "supported") return;
|
|
887
|
+
this.frame = input;
|
|
888
|
+
this.projection.update(input, this.origin, this.map.getCenter(), 512 * 2 ** this.map.getZoom(), this.map.getProjection()?.type === "globe");
|
|
889
|
+
this.draw(context, false);
|
|
890
|
+
}
|
|
891
|
+
draw(gl, pick) {
|
|
892
|
+
const u = this.uniforms ?? this.resolveUniforms(gl);
|
|
893
|
+
gl.useProgram(this.program);
|
|
894
|
+
gl.bindVertexArray(this.vao);
|
|
895
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
896
|
+
gl.disable(gl.DEPTH_TEST);
|
|
897
|
+
gl.depthRange(0, 1);
|
|
898
|
+
gl.depthMask(false);
|
|
899
|
+
gl.disable(gl.CULL_FACE);
|
|
900
|
+
gl.disable(gl.STENCIL_TEST);
|
|
901
|
+
if (pick) gl.disable(gl.BLEND);
|
|
902
|
+
else {
|
|
903
|
+
gl.enable(gl.BLEND);
|
|
904
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
905
|
+
}
|
|
906
|
+
const width = this.map.getContainer().clientWidth, height = this.map.getContainer().clientHeight;
|
|
907
|
+
gl.uniformMatrix4fv(u.matrix, false, this.matrix);
|
|
908
|
+
gl.uniformMatrix4fv(u.globeMatrix, false, this.projection.globeMatrix);
|
|
909
|
+
gl.uniform4fv(u.clippingPlane, this.projection.clippingPlane);
|
|
910
|
+
gl.uniform2f(u.origin, this.origin[0], this.origin[1]);
|
|
911
|
+
gl.uniform1f(u.globeTransition, this.projection.transition);
|
|
912
|
+
gl.uniform1f(u.world, 512 * 2 ** this.map.getZoom());
|
|
913
|
+
gl.uniform2f(u.view, width, height);
|
|
914
|
+
gl.uniform2f(u.buffer, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
915
|
+
gl.uniform1i(u.pick, pick ? 1 : 0);
|
|
916
|
+
gl.uniform1i(u.atlas, 0);
|
|
917
|
+
gl.uniform1i(u.terrain, 1);
|
|
918
|
+
const terrain = this.map.terrain;
|
|
919
|
+
const needsDepth = this.needsDepth && !!this.map.getTerrain();
|
|
920
|
+
const texture = needsDepth ? terrain?._fboDepthTexture?.texture : null;
|
|
921
|
+
if (needsDepth && !texture) {
|
|
922
|
+
this.setStatus({
|
|
923
|
+
status: "unavailable",
|
|
924
|
+
reason: "MapLibre terrain depth texture is unavailable"
|
|
925
|
+
});
|
|
926
|
+
this.dirty = true;
|
|
927
|
+
gl.bindVertexArray(null);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
gl.uniform1i(u.hasTerrain, texture ? 1 : 0);
|
|
931
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
932
|
+
gl.bindTexture(gl.TEXTURE_2D, texture ?? this.textures[0]);
|
|
933
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
934
|
+
for (const range of this.ranges) {
|
|
935
|
+
gl.bindTexture(gl.TEXTURE_2D, this.textures[range.page]);
|
|
936
|
+
for (let attr = 0; attr < 6; attr++) {
|
|
937
|
+
gl.enableVertexAttribArray(attr);
|
|
938
|
+
gl.vertexAttribPointer(attr, 4, gl.FLOAT, false, STRIDE * 4, (range.first * STRIDE + attr * 4) * 4);
|
|
939
|
+
gl.vertexAttribDivisor(attr, 1);
|
|
940
|
+
}
|
|
941
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, range.count);
|
|
942
|
+
}
|
|
943
|
+
gl.bindVertexArray(null);
|
|
944
|
+
}
|
|
945
|
+
/** On-demand GPU pick shares the visual shader including alpha and terrain mask. */
|
|
946
|
+
pick(pixel, tolerance = 0) {
|
|
947
|
+
if (!this.frame || this.status.status !== "supported") return null;
|
|
948
|
+
const gl = this.gl, w = gl.drawingBufferWidth, h = gl.drawingBufferHeight;
|
|
949
|
+
const previous = gl.getParameter(gl.FRAMEBUFFER_BINDING);
|
|
950
|
+
if (this.pickSize !== `${w}/${h}`) {
|
|
951
|
+
if (this.pickTexture) gl.deleteTexture(this.pickTexture);
|
|
952
|
+
if (this.pickFramebuffer) gl.deleteFramebuffer(this.pickFramebuffer);
|
|
953
|
+
this.pickTexture = gl.createTexture();
|
|
954
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
955
|
+
gl.bindTexture(gl.TEXTURE_2D, this.pickTexture);
|
|
956
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
957
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
958
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
959
|
+
this.pickFramebuffer = gl.createFramebuffer();
|
|
960
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.pickFramebuffer);
|
|
961
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.pickTexture, 0);
|
|
962
|
+
this.pickSize = `${w}/${h}`;
|
|
963
|
+
}
|
|
964
|
+
const dpr = w / this.map.getContainer().clientWidth;
|
|
965
|
+
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);
|
|
966
|
+
if (rw <= 0 || rh <= 0) {
|
|
967
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, previous);
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.pickFramebuffer);
|
|
971
|
+
gl.viewport(0, 0, w, h);
|
|
972
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
973
|
+
gl.scissor(x, y, rw, rh);
|
|
974
|
+
gl.colorMask(true, true, true, true);
|
|
975
|
+
gl.clearColor(0, 0, 0, 0);
|
|
976
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
977
|
+
this.draw(gl, true);
|
|
978
|
+
const pixels = new Uint8Array(rw * rh * 4);
|
|
979
|
+
gl.readPixels(x, y, rw, rh, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
980
|
+
let id = 0;
|
|
981
|
+
for (let i = 0; i < pixels.length; i += 4) id = Math.max(id, pixels[i] + pixels[i + 1] * 256 + pixels[i + 2] * 65536);
|
|
982
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
983
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, previous);
|
|
984
|
+
this.map.painter.context.setDirty();
|
|
985
|
+
this.map.painter.setBaseState();
|
|
986
|
+
this.map.triggerRepaint();
|
|
987
|
+
return id ? this.pins[id - 1]?.id ?? null : null;
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Shared Mercator/globe clip-space projection converted to container pixels. Callers
|
|
991
|
+
* differ in elevation source and in whether/where a screen-mode lift offset applies
|
|
992
|
+
* — those stay at the call site, as they aren't part of the shared matrix math.
|
|
993
|
+
*/
|
|
994
|
+
projectToPixel(worldX, worldY, z) {
|
|
995
|
+
const { clip: c, horizon } = this.projection.project(worldX, worldY, z);
|
|
996
|
+
if (horizon < 0 || c[3] <= 0) return [NaN, NaN];
|
|
997
|
+
const width = this.map.getContainer().clientWidth, height = this.map.getContainer().clientHeight;
|
|
998
|
+
return [(c[0] / c[3] + 1) * width / 2, (1 - c[1] / c[3]) * height / 2];
|
|
999
|
+
}
|
|
1000
|
+
footprint(id) {
|
|
1001
|
+
const i = this.pins.findIndex((p) => p.id === id);
|
|
1002
|
+
const pin = this.pins[i];
|
|
1003
|
+
if (!pin || !this.ready || !this.frame || !Number.isFinite(this.elevations[i])) return null;
|
|
1004
|
+
const p = pin.presentation, coord = pinMercator(pin.position);
|
|
1005
|
+
coord.x += Math.round(this.origin[0] - coord.x);
|
|
1006
|
+
const world = 512 * 2 ** this.map.getZoom();
|
|
1007
|
+
const project = (z) => this.projectToPixel((coord.x - this.origin[0]) * world, (coord.y - this.origin[1]) * world, z);
|
|
1008
|
+
const ground = project(this.elevations[i] ?? 0), attachment = p.mode === "world" ? project((this.elevations[i] ?? 0) + p.heightMeters) : [ground[0], ground[1] - p.liftPixels];
|
|
1009
|
+
if (!attachment.every(Number.isFinite)) return null;
|
|
1010
|
+
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);
|
|
1011
|
+
return {
|
|
1012
|
+
ground,
|
|
1013
|
+
attachment,
|
|
1014
|
+
min: [attachment[0] - pin.resource.anchor[0] * scale - padding - overhang, attachment[1] - pin.resource.height * scale - padding * 2 - overhang],
|
|
1015
|
+
max: [attachment[0] + (pin.resource.width - pin.resource.anchor[0]) * scale + padding + overhang, attachment[1] + overhang]
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
moveFromPixel(id, attachment) {
|
|
1019
|
+
const pin = this.pins.find((pin) => pin.id === id);
|
|
1020
|
+
if (!pin || !this.frame) return null;
|
|
1021
|
+
const p = pin.presentation;
|
|
1022
|
+
const project = (ll) => {
|
|
1023
|
+
const c = pinMercator(ll);
|
|
1024
|
+
c.x += Math.round(this.origin[0] - c.x);
|
|
1025
|
+
const world = 512 * 2 ** this.map.getZoom();
|
|
1026
|
+
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));
|
|
1027
|
+
return [px, py - (p.mode === "screen" ? p.liftPixels : 0)];
|
|
1028
|
+
};
|
|
1029
|
+
return invertPinProjection(project, pin.position, attachment, this.map.getProjection()?.type === "globe" ? 90 : void 0);
|
|
1030
|
+
}
|
|
1031
|
+
onRemove(_map, context) {
|
|
1032
|
+
this.removed = true;
|
|
1033
|
+
this.version++;
|
|
1034
|
+
const gl = context;
|
|
1035
|
+
this.onRemoveListeners();
|
|
1036
|
+
if (this.pickTexture) gl.deleteTexture(this.pickTexture);
|
|
1037
|
+
if (this.pickFramebuffer) gl.deleteFramebuffer(this.pickFramebuffer);
|
|
1038
|
+
if (this.vao) gl.deleteVertexArray(this.vao);
|
|
1039
|
+
if (this.buffer) gl.deleteBuffer(this.buffer);
|
|
1040
|
+
if (this.program) gl.deleteProgram(this.program);
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
1043
|
+
//#endregion
|
|
3
1044
|
//#region src/mercator.ts
|
|
4
1045
|
/**
|
|
5
1046
|
* Web Mercator world circumference in meters (2π × 6378137). The single source
|
|
@@ -255,6 +1296,25 @@ function emptyFC() {
|
|
|
255
1296
|
};
|
|
256
1297
|
}
|
|
257
1298
|
/**
|
|
1299
|
+
* Builds the pin-status emitter passed to each `PinLayer`. Bound only to the
|
|
1300
|
+
* `listeners` set (never to the adapter itself, via `this`), so a `PinLayer`
|
|
1301
|
+
* holding onto this callback for its lifetime does not transitively retain
|
|
1302
|
+
* the whole `MapLibreAdapter` object graph (map, all layer handles, etc.).
|
|
1303
|
+
*/
|
|
1304
|
+
function createPinStatusDispatcher(listeners) {
|
|
1305
|
+
return (event) => {
|
|
1306
|
+
queueMicrotask(() => {
|
|
1307
|
+
const errors = [];
|
|
1308
|
+
for (const listener of listeners) try {
|
|
1309
|
+
listener(event);
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
errors.push(error);
|
|
1312
|
+
}
|
|
1313
|
+
if (errors.length) throw new AggregateError(errors, "Pin presentation status listener errors");
|
|
1314
|
+
});
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
258
1318
|
* Build the paint expression for a circle layer with per-feature
|
|
259
1319
|
* `properties.style` overlay (ADR-0006). Each property prefers a value
|
|
260
1320
|
* pulled from `properties.style.<field>`, falling back to the layer default.
|
|
@@ -825,12 +1885,36 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
825
1885
|
}
|
|
826
1886
|
destroyLayer(handle) {
|
|
827
1887
|
this.clearGroupBundles(handle);
|
|
1888
|
+
handle.pinResources?.dispose();
|
|
828
1889
|
for (const mlId of handle.mlLayerIds) if (this.map.getLayer(mlId)) this.map.removeLayer(mlId);
|
|
829
1890
|
if (this.map.getSource(handle.sourceId)) this.map.removeSource(handle.sourceId);
|
|
830
1891
|
}
|
|
831
|
-
applyFeatureGroups(layerId,
|
|
1892
|
+
applyFeatureGroups(layerId, input) {
|
|
1893
|
+
const groups = [];
|
|
1894
|
+
for (const group of input) if (group.pin && this.getPinPresentationCapability().status !== "unsupported") {
|
|
1895
|
+
const previous = groups[groups.length - 1];
|
|
1896
|
+
if (previous?.pinBatch) previous.pinBatch.push(group.pin);
|
|
1897
|
+
else groups.push({
|
|
1898
|
+
id: `pin-run:${group.id}`,
|
|
1899
|
+
features: [],
|
|
1900
|
+
iconResources: [],
|
|
1901
|
+
pinBatch: [group.pin]
|
|
1902
|
+
});
|
|
1903
|
+
} else groups.push({
|
|
1904
|
+
...group,
|
|
1905
|
+
id: `ordinary:${group.id}`
|
|
1906
|
+
});
|
|
832
1907
|
const handle = this.layers.get(layerId);
|
|
833
1908
|
if (!handle) return;
|
|
1909
|
+
const allPins = groups.flatMap((group) => group.pinBatch ?? []);
|
|
1910
|
+
if (allPins.length) {
|
|
1911
|
+
handle.pinResources ??= new PinResources(this.map);
|
|
1912
|
+
const previousBatch = handle.lastPinBatch;
|
|
1913
|
+
if (!(previousBatch !== void 0 && previousBatch.length === allPins.length && allPins.every((pin, index) => pin === previousBatch[index]))) {
|
|
1914
|
+
handle.pinResources.setRoster(allPins);
|
|
1915
|
+
handle.lastPinBatch = allPins;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
834
1918
|
const desiredGroupIds = new Set(groups.map((group) => group.id));
|
|
835
1919
|
const previousGroupOrder = handle.groupOrder.filter((groupId) => desiredGroupIds.has(groupId));
|
|
836
1920
|
const bundlesNeedingOrder = /* @__PURE__ */ new Set();
|
|
@@ -846,6 +1930,26 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
846
1930
|
bundle = this.createGroupBundle(layerId, handle);
|
|
847
1931
|
handle.groupBundles.set(group.id, bundle);
|
|
848
1932
|
}
|
|
1933
|
+
if (group.pinBatch) {
|
|
1934
|
+
if (!bundle.pinLayer) {
|
|
1935
|
+
const custom = new PinLayer(`${bundle.sourceId}-pins`, handle.pinResources, this.emitPinStatus);
|
|
1936
|
+
this.map.addLayer(custom, handle.mlLayerIds[0]);
|
|
1937
|
+
bundle.pinLayer = custom;
|
|
1938
|
+
bundlesNeedingOrder.add(group.id);
|
|
1939
|
+
}
|
|
1940
|
+
const previous = bundle.pinSignature;
|
|
1941
|
+
let changedIndices;
|
|
1942
|
+
if (previous?.length === group.pinBatch.length) {
|
|
1943
|
+
changedIndices = [];
|
|
1944
|
+
for (let i = 0; i < group.pinBatch.length; i++) if (group.pinBatch[i] !== previous[i]) changedIndices.push(i);
|
|
1945
|
+
}
|
|
1946
|
+
if (!previous || bundle.pinResourceRevision !== handle.pinResources?.revision || previous.length !== group.pinBatch.length || !!changedIndices?.length) {
|
|
1947
|
+
bundle.pinSignature = group.pinBatch;
|
|
1948
|
+
bundle.pinResourceRevision = handle.pinResources?.revision;
|
|
1949
|
+
bundle.pinLayer.setPins(group.pinBatch, changedIndices);
|
|
1950
|
+
}
|
|
1951
|
+
continue;
|
|
1952
|
+
}
|
|
849
1953
|
let contentChanged;
|
|
850
1954
|
if (!isNewBundle && bundle.revision !== void 0 && group.revision !== void 0) {
|
|
851
1955
|
contentChanged = bundle.revision !== group.revision;
|
|
@@ -864,6 +1968,11 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
864
1968
|
if (this.reconcileGroupPaintSurfaces(handle, bundle, group.features)) bundlesNeedingOrder.add(group.id);
|
|
865
1969
|
if (contentChanged || resourcesChanged) this.updateGroupBundleSource(bundle);
|
|
866
1970
|
}
|
|
1971
|
+
if (!allPins.length) {
|
|
1972
|
+
handle.pinResources?.dispose();
|
|
1973
|
+
handle.pinResources = void 0;
|
|
1974
|
+
handle.lastPinBatch = void 0;
|
|
1975
|
+
}
|
|
867
1976
|
handle.groupOrder = groups.map((group) => group.id);
|
|
868
1977
|
this.orderGroupBundles(handle, previousGroupOrder, handle.groupOrder, bundlesNeedingOrder);
|
|
869
1978
|
}
|
|
@@ -888,6 +1997,7 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
888
1997
|
};
|
|
889
1998
|
}
|
|
890
1999
|
destroyGroupBundle(bundle) {
|
|
2000
|
+
if (bundle.pinLayer && this.map.getLayer(bundle.pinLayer.id)) this.map.removeLayer(bundle.pinLayer.id);
|
|
891
2001
|
for (const overlay of bundle.overlays.values()) {
|
|
892
2002
|
if (this.map.getLayer(overlay.layerId)) this.map.removeLayer(overlay.layerId);
|
|
893
2003
|
if (this.map.getSource(overlay.sourceId)) this.map.removeSource(overlay.sourceId);
|
|
@@ -954,6 +2064,9 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
954
2064
|
for (const bundle of handle.groupBundles.values()) this.destroyGroupBundle(bundle);
|
|
955
2065
|
handle.groupBundles.clear();
|
|
956
2066
|
handle.groupOrder = [];
|
|
2067
|
+
handle.pinResources?.dispose();
|
|
2068
|
+
handle.pinResources = void 0;
|
|
2069
|
+
handle.lastPinBatch = void 0;
|
|
957
2070
|
}
|
|
958
2071
|
requiredIconIds(group) {
|
|
959
2072
|
const referenced = new Set(group.features.flatMap((feature) => {
|
|
@@ -1099,13 +2212,14 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
1099
2212
|
const bundle = handle.groupBundles.get(groupId);
|
|
1100
2213
|
if (!bundle) continue;
|
|
1101
2214
|
if (forcedGroupIds.has(groupId) || stableGroupIds !== null && !stableGroupIds.has(groupId)) {
|
|
2215
|
+
if (bundle.pinLayer && this.map.getLayer(bundle.pinLayer.id)) this.map.moveLayer(bundle.pinLayer.id, beforeId);
|
|
1102
2216
|
for (const overlay of bundle.overlays.values()) if (this.map.getLayer(overlay.layerId)) this.map.moveLayer(overlay.layerId, beforeId);
|
|
1103
2217
|
for (const key of bundle.paintOrder) {
|
|
1104
2218
|
const surface = bundle.paintSurfaces.get(key);
|
|
1105
2219
|
if (surface && this.map.getLayer(surface.layerId)) this.map.moveLayer(surface.layerId, beforeId);
|
|
1106
2220
|
}
|
|
1107
2221
|
}
|
|
1108
|
-
beforeId = firstBundleLayerId(bundle) ?? beforeId;
|
|
2222
|
+
beforeId = bundle.pinLayer?.id ?? firstBundleLayerId(bundle) ?? beforeId;
|
|
1109
2223
|
}
|
|
1110
2224
|
}
|
|
1111
2225
|
nativeLayerIds(handle) {
|
|
@@ -1200,10 +2314,11 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
1200
2314
|
for (const event of this.viewChangeEvents()) this.map.off(event, token.fn);
|
|
1201
2315
|
}
|
|
1202
2316
|
viewChangeEvents() {
|
|
1203
|
-
return this.viewChangeMode === "settle" ? ["moveend"] : [
|
|
2317
|
+
return this.viewChangeMode === "settle" ? ["moveend", "projectiontransition"] : [
|
|
1204
2318
|
"move",
|
|
1205
2319
|
"resize",
|
|
1206
|
-
"moveend"
|
|
2320
|
+
"moveend",
|
|
2321
|
+
"projectiontransition"
|
|
1207
2322
|
];
|
|
1208
2323
|
}
|
|
1209
2324
|
createEditPointerDriver() {
|
|
@@ -1464,7 +2579,7 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
1464
2579
|
id: feature.id,
|
|
1465
2580
|
geometry: feature.geometry,
|
|
1466
2581
|
properties: feature.properties
|
|
1467
|
-
})), this.featureCache.get(layerId) ?? []);
|
|
2582
|
+
})), (this.featureCache.get(layerId) ?? []).filter((f) => !f.properties?.pinPresentation));
|
|
1468
2583
|
const size = this.getViewportSize();
|
|
1469
2584
|
const candidate = evaluatePickCandidates(relevantFeatures.map(({ feature, nativeHit, visualOrder }) => {
|
|
1470
2585
|
const iconId = feature.properties?.iconId;
|
|
@@ -1495,6 +2610,17 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
1495
2610
|
},
|
|
1496
2611
|
bearing: this.getBearing()
|
|
1497
2612
|
});
|
|
2613
|
+
const pinHit = this.pinHit(layerId, [x, y], tol);
|
|
2614
|
+
if (pinHit && (!candidate || pinHit.order >= (this.getFeatureGroupOrder(layerId, candidate.feature.id) ?? -1))) {
|
|
2615
|
+
handler({
|
|
2616
|
+
id: pinHit.id,
|
|
2617
|
+
feature: pinHit.feature,
|
|
2618
|
+
pixel: [x, y],
|
|
2619
|
+
coordinate: [e.lngLat.lng, e.lngLat.lat],
|
|
2620
|
+
originalEvent
|
|
2621
|
+
});
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
1498
2624
|
if (!candidate) return;
|
|
1499
2625
|
const cmId = candidate.graphicId;
|
|
1500
2626
|
if (cmId == null) return;
|
|
@@ -1511,6 +2637,74 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
1511
2637
|
return () => this.map.off("click", onMlClick);
|
|
1512
2638
|
});
|
|
1513
2639
|
}
|
|
2640
|
+
pinStatusListeners = /* @__PURE__ */ new Set();
|
|
2641
|
+
emitPinStatus = createPinStatusDispatcher(this.pinStatusListeners);
|
|
2642
|
+
onPinPresentationStatusChange(handler) {
|
|
2643
|
+
this.pinStatusListeners.add(handler);
|
|
2644
|
+
return () => this.pinStatusListeners.delete(handler);
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* Pin layers across this adapter's group bundles.
|
|
2648
|
+
* - With no `handle`: every layer's bundles, in bundle-map (creation)
|
|
2649
|
+
* order — fine for id-based lookups where only "does one match" matters.
|
|
2650
|
+
* - With `handle`: only that layer's bundles, walked topmost-first (reverse
|
|
2651
|
+
* paint order) — required for hit-testing, where the first hit must be
|
|
2652
|
+
* the visually topmost pin.
|
|
2653
|
+
*/
|
|
2654
|
+
*pinLayers(handle) {
|
|
2655
|
+
if (handle) {
|
|
2656
|
+
for (const groupId of [...handle.groupOrder].reverse()) {
|
|
2657
|
+
const pinLayer = handle.groupBundles.get(groupId)?.pinLayer;
|
|
2658
|
+
if (pinLayer) yield pinLayer;
|
|
2659
|
+
}
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
for (const h of this.layers.values()) for (const bundle of h.groupBundles.values()) if (bundle.pinLayer) yield bundle.pinLayer;
|
|
2663
|
+
}
|
|
2664
|
+
/** First non-null/undefined result of `fn` across all pin layers, else null. */
|
|
2665
|
+
findPinLayer(fn) {
|
|
2666
|
+
for (const pinLayer of this.pinLayers()) {
|
|
2667
|
+
const result = fn(pinLayer);
|
|
2668
|
+
if (result != null) return result;
|
|
2669
|
+
}
|
|
2670
|
+
return null;
|
|
2671
|
+
}
|
|
2672
|
+
getPinPresentationCapability(graphicId) {
|
|
2673
|
+
if (!(maplibregl.getVersion?.() ?? "").startsWith("6.")) return {
|
|
2674
|
+
status: "unsupported",
|
|
2675
|
+
reason: "Pins require MapLibre 6"
|
|
2676
|
+
};
|
|
2677
|
+
if (graphicId) {
|
|
2678
|
+
const status = this.findPinLayer((pinLayer) => pinLayer.has(graphicId) ? pinLayer.getStatus(graphicId) : null);
|
|
2679
|
+
if (status) return status;
|
|
2680
|
+
}
|
|
2681
|
+
return { status: "supported" };
|
|
2682
|
+
}
|
|
2683
|
+
getPinFootprint(id) {
|
|
2684
|
+
return this.findPinLayer((pinLayer) => pinLayer.footprint(id));
|
|
2685
|
+
}
|
|
2686
|
+
movePinFromPixel(id, attachment) {
|
|
2687
|
+
return this.findPinLayer((pinLayer) => pinLayer.moveFromPixel(id, attachment));
|
|
2688
|
+
}
|
|
2689
|
+
pinHit(layerId, pixel, tolerance) {
|
|
2690
|
+
const handle = this.layers.get(layerId);
|
|
2691
|
+
if (!handle) return null;
|
|
2692
|
+
for (const pinLayer of this.pinLayers(handle)) {
|
|
2693
|
+
const id = pinLayer.pick(pixel, tolerance);
|
|
2694
|
+
if (!id) continue;
|
|
2695
|
+
const feature = this.featureCache.get(layerId)?.find((f) => f.properties?.graphicId === id);
|
|
2696
|
+
if (feature) return {
|
|
2697
|
+
id,
|
|
2698
|
+
feature,
|
|
2699
|
+
order: this.getFeatureGroupOrder(layerId, feature.id) ?? 0
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
return null;
|
|
2703
|
+
}
|
|
2704
|
+
pickPin(id, pixel, tolerance) {
|
|
2705
|
+
for (const [layerId] of this.layers) if (this.pinHit(layerId, pixel, tolerance)?.id === id) return true;
|
|
2706
|
+
return false;
|
|
2707
|
+
}
|
|
1514
2708
|
setGlobeMode(enabled) {
|
|
1515
2709
|
if (enabled) this.map.setProjection({ type: "globe" });
|
|
1516
2710
|
else this.map.setProjection({ type: "mercator" });
|