@orbat-mapper/tactical-draw-adapter-maplibre 0.7.3 → 0.8.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 +53 -0
- package/dist/index.mjs +214 -10
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -78,6 +78,18 @@ declare class PinLayer implements CustomLayerInterface {
|
|
|
78
78
|
private sortDirty;
|
|
79
79
|
/** View-wide far-to-near paint order; `false` restores canonical layer order. */
|
|
80
80
|
private depthSorted;
|
|
81
|
+
/** View-wide presentation while zoom or distance scaling is on, else undefined. */
|
|
82
|
+
private scaling;
|
|
83
|
+
/**
|
|
84
|
+
* Pin scaling reference, derived from the camera pose rather than MapLibre's center
|
|
85
|
+
* point: after a terrain gesture MapLibre re-seats the center on the terrain and
|
|
86
|
+
* recomputes zoom with the camera fixed, which would otherwise snap pin sizes.
|
|
87
|
+
*/
|
|
88
|
+
private reference;
|
|
89
|
+
/** Per-frame scratch, so the reference update allocates nothing in flat mode. */
|
|
90
|
+
private cameraScratch;
|
|
91
|
+
private rayScratch;
|
|
92
|
+
private positionScratch;
|
|
81
93
|
/** Hoisted out of the per-frame sort so no comparator is allocated while panning. */
|
|
82
94
|
private comparePaintOrder;
|
|
83
95
|
private version;
|
|
@@ -103,6 +115,8 @@ declare class PinLayer implements CustomLayerInterface {
|
|
|
103
115
|
private status;
|
|
104
116
|
private refreshTerrain;
|
|
105
117
|
private elevation;
|
|
118
|
+
/** Uncached DEM read; NaN while the DEM tile is missing or changing. */
|
|
119
|
+
private sampleElevation;
|
|
106
120
|
has(id: string): boolean;
|
|
107
121
|
constructor(id: string, resources: PinResources, onStatus?: ((event: PinPresentationStatusEvent) => void) | undefined);
|
|
108
122
|
private restoreContext;
|
|
@@ -127,6 +141,45 @@ declare class PinLayer implements CustomLayerInterface {
|
|
|
127
141
|
* — those stay at the call site, as they aren't part of the shared matrix math.
|
|
128
142
|
*/
|
|
129
143
|
private projectToPixel;
|
|
144
|
+
/** View-wide, so any pin's value describes the layer (like depthSort). */
|
|
145
|
+
private zoomScale;
|
|
146
|
+
/**
|
|
147
|
+
* Per frame: move the reference elevation toward the terrain under the view-center
|
|
148
|
+
* ray, then take the clip w where that ray crosses it. Pin sizes then depend only
|
|
149
|
+
* on the camera and that plane. The plane adapts only in proportion to camera
|
|
150
|
+
* motion, never over time: MapLibre re-seats its center (and zoom) on the terrain
|
|
151
|
+
* after a gesture with the camera fixed, and a stationary camera must not resize
|
|
152
|
+
* pins. The effective zoom is the zoom that plane would give, invariant under that
|
|
153
|
+
* re-seat (camera fixed, zoom and pixel units rescaled).
|
|
154
|
+
*/
|
|
155
|
+
private updateScaleReference;
|
|
156
|
+
/**
|
|
157
|
+
* Camera position as [Mercator x, Mercator y, meters up] written into `out`,
|
|
158
|
+
* independent of zoom and of the layer origin, so it is unchanged when MapLibre
|
|
159
|
+
* re-seats its center. It is the point where clip x, y and w all vanish: rows 0, 1
|
|
160
|
+
* and 3 of the column-major matrix, solved by Cramer's rule. False if singular.
|
|
161
|
+
*/
|
|
162
|
+
private cameraPosition;
|
|
163
|
+
private rayDepth;
|
|
164
|
+
/**
|
|
165
|
+
* Where the view-center ray meets elevation `z`: world x, y (layer pixels from the
|
|
166
|
+
* origin) and clip w, in a reused scratch object. Flat projection only; the globe
|
|
167
|
+
* falls back to the origin.
|
|
168
|
+
*/
|
|
169
|
+
private rayHit;
|
|
170
|
+
/**
|
|
171
|
+
* Terrain height where the view-center ray meets the current reference plane. Unlike
|
|
172
|
+
* MapLibre's center elevation, which is frozen during gestures and re-seated after
|
|
173
|
+
* them, this follows the camera continuously and is unchanged while it is still.
|
|
174
|
+
*/
|
|
175
|
+
private rayTerrainElevation;
|
|
176
|
+
/** Elevation of the distance-scaling reference plane; sea level until one is set. */
|
|
177
|
+
private referenceElevation;
|
|
178
|
+
/**
|
|
179
|
+
* Combined zoom and distance scale for a pin anchored at world (x, y), mirroring the
|
|
180
|
+
* shader: distance is horizontal, the anchor's clip w at the reference elevation.
|
|
181
|
+
*/
|
|
182
|
+
private pinScale;
|
|
130
183
|
footprint(id: string): PinFootprint | null;
|
|
131
184
|
moveFromPixel(id: string, attachment: PixelCoordinate): number[] | null;
|
|
132
185
|
onRemove(_map: Map$1, context: WebGLRenderingContext | WebGL2RenderingContext): void;
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as maplibregl from "maplibre-gl";
|
|
2
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";
|
|
3
|
+
import { BaseMapAdapter, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, HATCH_PATTERNS, PointerCallbackHub, TEXT_HALO_COLOR, TOUCH_HIT_TOLERANCE_PX, bindAbortable, combinePinScale, evaluatePickCandidates, fillPatternImageId, makeFillPatternImage, mergeNativeAndCachedPickFeatures, nearestWorldCopyLng, parseColor, pickHitTolerance, pickPointerType, pinDistanceScale, pinZoomScale, resolveTextHaloWidth } from "@orbat-mapper/tactical-draw";
|
|
4
4
|
import { EARTH_RADIUS, project, unproject } from "@orbat-mapper/control-measures";
|
|
5
5
|
//#region src/pin-atlas.ts
|
|
6
6
|
/**
|
|
@@ -219,6 +219,16 @@ vec4 projectPin(vec3 position, out float horizon) {
|
|
|
219
219
|
return mix(flatClip,globe,u_globeTransition);
|
|
220
220
|
}
|
|
221
221
|
uniform vec2 u_view;
|
|
222
|
+
// Zoom-out shrink for screen-space pixel dimensions (lift, dot, plate, symbol, halos).
|
|
223
|
+
// Stem width is left alone; fragment shading stays in unscaled local units.
|
|
224
|
+
uniform float u_scale;
|
|
225
|
+
// Perspective sizing, mirroring pinDistanceScale(): the exponent, 0 when off;
|
|
226
|
+
// u_depthRef is the reference clip w at u_refElevation.
|
|
227
|
+
uniform float u_distanceStrength;
|
|
228
|
+
uniform float u_depthRef;
|
|
229
|
+
uniform float u_refElevation;
|
|
230
|
+
// Bounds on zoom × distance (sizeLimits in combinePinScale); (0, huge) when both are off.
|
|
231
|
+
uniform vec2 u_sizeLimits;
|
|
222
232
|
out vec2 v_local;
|
|
223
233
|
out vec2 v_uv;
|
|
224
234
|
flat out vec4 v_bounds;
|
|
@@ -232,14 +242,26 @@ void main() {
|
|
|
232
242
|
float groundHorizon, targetHorizon;
|
|
233
243
|
vec4 ground = projectPin(a_ground.xyz, groundHorizon);
|
|
234
244
|
vec4 target = projectPin(a_target.xyz, targetHorizon);
|
|
235
|
-
|
|
245
|
+
float scale = u_scale;
|
|
246
|
+
if(u_distanceStrength > 0.) {
|
|
247
|
+
// Horizontal distance: measure the anchor at the reference elevation, so relief
|
|
248
|
+
// (a unit in a valley below the reference) never resizes pins while zooming.
|
|
249
|
+
// Flat clip w is affine in z, so only the globe needs another projection.
|
|
250
|
+
float refHorizon, depth;
|
|
251
|
+
if(u_globeTransition == 0.) depth = ground.w + (u_refElevation - a_ground.z) * u_matrix[2][3];
|
|
252
|
+
else depth = projectPin(vec3(a_ground.xy, u_refElevation), refHorizon).w;
|
|
253
|
+
if(depth > 0.)
|
|
254
|
+
scale *= pow(u_depthRef / depth, u_distanceStrength);
|
|
255
|
+
}
|
|
256
|
+
scale = clamp(scale, u_sizeLimits.x, u_sizeLimits.y);
|
|
257
|
+
if(a_flags.y < .5) { target = ground; targetHorizon = groundHorizon; target.y += a_ground.w * scale * 2. / u_view.y * target.w; }
|
|
236
258
|
v_horizon = a_flags.x < .5 ? groundHorizon : (a_flags.x < 1.5 ? mix(groundHorizon,targetHorizon,c.y) : targetHorizon);
|
|
237
259
|
vec2 gp = ground.xy / ground.w * u_view * .5;
|
|
238
260
|
vec2 tp = target.xy / target.w * u_view * .5;
|
|
239
261
|
float kind = a_flags.x;
|
|
240
262
|
vec4 clip = target;
|
|
241
263
|
vec2 offset;
|
|
242
|
-
if(kind < .5) { clip = ground; offset = (c - .5) * a_shape.xy; }
|
|
264
|
+
if(kind < .5) { clip = ground; offset = (c - .5) * a_shape.xy * scale; }
|
|
243
265
|
else if(kind < 1.5) {
|
|
244
266
|
vec2 delta = tp-gp; float len = length(delta);
|
|
245
267
|
vec2 dir = len > .0001 ? delta/len : vec2(0,1);
|
|
@@ -247,7 +269,7 @@ void main() {
|
|
|
247
269
|
vec2 p = mix(gp,tp,c.y) + vec2(-dir.y,dir.x) * (c.x-.5)*(a_shape.x+1.);
|
|
248
270
|
clip = vec4(p/u_view*2., mix(ground.z/ground.w,target.z/target.w,c.y), 1.);
|
|
249
271
|
offset = vec2(0);
|
|
250
|
-
} else { offset = (c - vec2(.5,0)) * a_shape.xy + a_shape.zw; }
|
|
272
|
+
} else { offset = ((c - vec2(.5,0)) * a_shape.xy + a_shape.zw) * scale; }
|
|
251
273
|
clip.xy += offset * 2. / u_view * clip.w;
|
|
252
274
|
if (ground.w <= 0. || target.w <= 0.) clip = vec4(2,2,2,1);
|
|
253
275
|
gl_Position=clip;
|
|
@@ -336,6 +358,10 @@ const PIN_GLOBE_RADIUS = EARTH_RADIUS;
|
|
|
336
358
|
function pinMercator(position) {
|
|
337
359
|
return MercatorCoordinate.fromLngLat([position[0], Math.max(-89.999999, Math.min(89.999999, position[1]))]);
|
|
338
360
|
}
|
|
361
|
+
/** Latitude in radians of Mercator `y`; allocation-free, unlike MercatorCoordinate.toLngLat. */
|
|
362
|
+
function mercatorLatitude(y) {
|
|
363
|
+
return Math.atan(Math.sinh(Math.PI * (1 - 2 * y)));
|
|
364
|
+
}
|
|
339
365
|
/** Scratch sphere position reused by the per-pin, per-frame depth() path. */
|
|
340
366
|
const depthSphere = [
|
|
341
367
|
0,
|
|
@@ -555,6 +581,42 @@ var PinLayer = class {
|
|
|
555
581
|
sortDirty = true;
|
|
556
582
|
/** View-wide far-to-near paint order; `false` restores canonical layer order. */
|
|
557
583
|
depthSorted = false;
|
|
584
|
+
/** View-wide presentation while zoom or distance scaling is on, else undefined. */
|
|
585
|
+
scaling;
|
|
586
|
+
/**
|
|
587
|
+
* Pin scaling reference, derived from the camera pose rather than MapLibre's center
|
|
588
|
+
* point: after a terrain gesture MapLibre re-seats the center on the terrain and
|
|
589
|
+
* recomputes zoom with the camera fixed, which would otherwise snap pin sizes.
|
|
590
|
+
*/
|
|
591
|
+
reference = {
|
|
592
|
+
elevation: NaN,
|
|
593
|
+
/**
|
|
594
|
+
* Clip w where distance scaling is 1. It tracks the terrain height at the center
|
|
595
|
+
* (see updateScaleReference): a sea-level reference drifts from the terrain by an
|
|
596
|
+
* offset that doubles in pixels each zoom level, making pins pulse while zooming.
|
|
597
|
+
*/
|
|
598
|
+
depth: NaN,
|
|
599
|
+
zoom: NaN,
|
|
600
|
+
camera: [
|
|
601
|
+
NaN,
|
|
602
|
+
NaN,
|
|
603
|
+
NaN
|
|
604
|
+
],
|
|
605
|
+
bearing: 0,
|
|
606
|
+
pitch: 0
|
|
607
|
+
};
|
|
608
|
+
/** Per-frame scratch, so the reference update allocates nothing in flat mode. */
|
|
609
|
+
cameraScratch = [
|
|
610
|
+
NaN,
|
|
611
|
+
NaN,
|
|
612
|
+
NaN
|
|
613
|
+
];
|
|
614
|
+
rayScratch = {
|
|
615
|
+
x: 0,
|
|
616
|
+
y: 0,
|
|
617
|
+
depth: NaN
|
|
618
|
+
};
|
|
619
|
+
positionScratch = [0, 0];
|
|
558
620
|
/** Hoisted out of the per-frame sort so no comparator is allocated while panning. */
|
|
559
621
|
comparePaintOrder = (a, b) => (this.depthSorted ? this.depths[b] - this.depths[a] : 0) || a - b;
|
|
560
622
|
version = 0;
|
|
@@ -589,6 +651,13 @@ var PinLayer = class {
|
|
|
589
651
|
const key = `${position[0]}/${position[1]}`;
|
|
590
652
|
const cached = this.elevationCache.get(key);
|
|
591
653
|
if (cached !== void 0) return cached;
|
|
654
|
+
const value = this.sampleElevation(position);
|
|
655
|
+
if (Number.isFinite(value)) this.elevationCache.set(key, value);
|
|
656
|
+
return value;
|
|
657
|
+
}
|
|
658
|
+
/** Uncached DEM read; NaN while the DEM tile is missing or changing. */
|
|
659
|
+
sampleElevation(position) {
|
|
660
|
+
if (!this.map.getTerrain() || Math.abs(position[1]) >= 85.051129) return 0;
|
|
592
661
|
let value;
|
|
593
662
|
try {
|
|
594
663
|
const terrain = this.map.terrain;
|
|
@@ -609,7 +678,6 @@ var PinLayer = class {
|
|
|
609
678
|
} catch {
|
|
610
679
|
return NaN;
|
|
611
680
|
}
|
|
612
|
-
this.elevationCache.set(key, value);
|
|
613
681
|
return value;
|
|
614
682
|
}
|
|
615
683
|
has(id) {
|
|
@@ -679,6 +747,11 @@ var PinLayer = class {
|
|
|
679
747
|
globeTransition: gl.getUniformLocation(program, "u_globeTransition"),
|
|
680
748
|
world: gl.getUniformLocation(program, "u_world"),
|
|
681
749
|
view: gl.getUniformLocation(program, "u_view"),
|
|
750
|
+
scale: gl.getUniformLocation(program, "u_scale"),
|
|
751
|
+
distanceStrength: gl.getUniformLocation(program, "u_distanceStrength"),
|
|
752
|
+
depthRef: gl.getUniformLocation(program, "u_depthRef"),
|
|
753
|
+
refElevation: gl.getUniformLocation(program, "u_refElevation"),
|
|
754
|
+
sizeLimits: gl.getUniformLocation(program, "u_sizeLimits"),
|
|
682
755
|
buffer: gl.getUniformLocation(program, "u_buffer"),
|
|
683
756
|
pick: gl.getUniformLocation(program, "u_pick"),
|
|
684
757
|
atlas: gl.getUniformLocation(program, "u_atlas"),
|
|
@@ -711,6 +784,8 @@ var PinLayer = class {
|
|
|
711
784
|
this.pins = pins;
|
|
712
785
|
this.prioritizesSelection = pins[0]?.presentation.prioritizePins ?? false;
|
|
713
786
|
this.depthSorted = pins[0]?.presentation.depthSort ?? false;
|
|
787
|
+
const view = pins[0]?.presentation;
|
|
788
|
+
this.scaling = view && (view.zoomScale.enabled || view.distanceScale.enabled) ? view : void 0;
|
|
714
789
|
if (this.elevationCache.size > pins.length * 60 + 1e3) this.elevationCache.clear();
|
|
715
790
|
if (!this.program) {
|
|
716
791
|
this.onStatus?.({
|
|
@@ -965,6 +1040,8 @@ var PinLayer = class {
|
|
|
965
1040
|
if (this.status.status !== "supported") return;
|
|
966
1041
|
this.frame = input;
|
|
967
1042
|
this.projection.update(input, this.origin, this.map.getCenter(), 512 * 2 ** this.map.getZoom(), this.map.getProjection()?.type === "globe");
|
|
1043
|
+
if (this.scaling) this.updateScaleReference();
|
|
1044
|
+
else this.reference.elevation = this.reference.depth = this.reference.zoom = NaN;
|
|
968
1045
|
this.updatePaintOrder();
|
|
969
1046
|
this.draw(context, false);
|
|
970
1047
|
}
|
|
@@ -1034,6 +1111,12 @@ var PinLayer = class {
|
|
|
1034
1111
|
gl.uniform1f(u.globeTransition, this.projection.transition);
|
|
1035
1112
|
gl.uniform1f(u.world, 512 * 2 ** this.map.getZoom());
|
|
1036
1113
|
gl.uniform2f(u.view, width, height);
|
|
1114
|
+
gl.uniform1f(u.scale, this.zoomScale());
|
|
1115
|
+
const distance = this.scaling?.distanceScale, limits = this.scaling?.sizeLimits;
|
|
1116
|
+
gl.uniform1f(u.distanceStrength, distance?.enabled ? distance.strength : 0);
|
|
1117
|
+
gl.uniform2f(u.sizeLimits, limits ? Math.min(limits.minScale, limits.maxScale) : 0, limits?.maxScale ?? 1e30);
|
|
1118
|
+
gl.uniform1f(u.depthRef, this.reference.depth);
|
|
1119
|
+
gl.uniform1f(u.refElevation, this.referenceElevation());
|
|
1037
1120
|
gl.uniform2f(u.buffer, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
1038
1121
|
gl.uniform1i(u.pick, pick ? 1 : 0);
|
|
1039
1122
|
gl.uniform1i(u.atlas, 0);
|
|
@@ -1123,6 +1206,123 @@ var PinLayer = class {
|
|
|
1123
1206
|
const width = this.map.getContainer().clientWidth, height = this.map.getContainer().clientHeight;
|
|
1124
1207
|
return [(c[0] / c[3] + 1) * width / 2, (1 - c[1] / c[3]) * height / 2];
|
|
1125
1208
|
}
|
|
1209
|
+
/** View-wide, so any pin's value describes the layer (like depthSort). */
|
|
1210
|
+
zoomScale() {
|
|
1211
|
+
if (!this.scaling) return 1;
|
|
1212
|
+
const zoom = Number.isFinite(this.reference.zoom) ? this.reference.zoom : this.map.getZoom();
|
|
1213
|
+
return pinZoomScale(this.scaling, zoom);
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Per frame: move the reference elevation toward the terrain under the view-center
|
|
1217
|
+
* ray, then take the clip w where that ray crosses it. Pin sizes then depend only
|
|
1218
|
+
* on the camera and that plane. The plane adapts only in proportion to camera
|
|
1219
|
+
* motion, never over time: MapLibre re-seats its center (and zoom) on the terrain
|
|
1220
|
+
* after a gesture with the camera fixed, and a stationary camera must not resize
|
|
1221
|
+
* pins. The effective zoom is the zoom that plane would give, invariant under that
|
|
1222
|
+
* re-seat (camera fixed, zoom and pixel units rescaled).
|
|
1223
|
+
*/
|
|
1224
|
+
updateScaleReference() {
|
|
1225
|
+
const target = this.rayTerrainElevation();
|
|
1226
|
+
const ref = this.reference;
|
|
1227
|
+
const camera = this.cameraPosition(this.cameraScratch) ? this.cameraScratch : ref.camera;
|
|
1228
|
+
const bearing = this.map.getBearing(), pitch = this.map.getPitch();
|
|
1229
|
+
if (!Number.isFinite(ref.elevation)) ref.elevation = target;
|
|
1230
|
+
else {
|
|
1231
|
+
const height = camera[2] - ref.elevation;
|
|
1232
|
+
const lat = mercatorLatitude(camera[1]);
|
|
1233
|
+
const metersPerMercator = 2 * Math.PI * PIN_GLOBE_RADIUS * Math.cos(lat);
|
|
1234
|
+
const travel = Math.hypot((camera[0] - ref.camera[0]) * metersPerMercator, (camera[1] - ref.camera[1]) * metersPerMercator, camera[2] - ref.camera[2]);
|
|
1235
|
+
const turn = Math.abs((bearing - ref.bearing + 540) % 360 - 180) / 90;
|
|
1236
|
+
const motion = (height > 0 ? travel / height : 1) + turn + Math.abs(pitch - ref.pitch) / 30;
|
|
1237
|
+
if (Number.isFinite(motion)) ref.elevation += (target - ref.elevation) * Math.min(1, motion * 2);
|
|
1238
|
+
}
|
|
1239
|
+
if (camera !== ref.camera) {
|
|
1240
|
+
ref.camera[0] = camera[0];
|
|
1241
|
+
ref.camera[1] = camera[1];
|
|
1242
|
+
ref.camera[2] = camera[2];
|
|
1243
|
+
}
|
|
1244
|
+
ref.bearing = bearing;
|
|
1245
|
+
ref.pitch = pitch;
|
|
1246
|
+
const centerDepth = this.rayDepth(this.map.getCenterElevation());
|
|
1247
|
+
const depth = this.rayDepth(ref.elevation);
|
|
1248
|
+
ref.depth = depth > 0 ? depth : centerDepth;
|
|
1249
|
+
ref.zoom = this.map.getZoom() + Math.log2(centerDepth / ref.depth);
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Camera position as [Mercator x, Mercator y, meters up] written into `out`,
|
|
1253
|
+
* independent of zoom and of the layer origin, so it is unchanged when MapLibre
|
|
1254
|
+
* re-seats its center. It is the point where clip x, y and w all vanish: rows 0, 1
|
|
1255
|
+
* and 3 of the column-major matrix, solved by Cramer's rule. False if singular.
|
|
1256
|
+
*/
|
|
1257
|
+
cameraPosition(out) {
|
|
1258
|
+
const m = this.projection.matrix;
|
|
1259
|
+
const a0 = m[0], a1 = m[4], a2 = m[8], t0 = -m[12];
|
|
1260
|
+
const b0 = m[1], b1 = m[5], b2 = m[9], t1 = -m[13];
|
|
1261
|
+
const c0 = m[3], c1 = m[7], c2 = m[11], t2 = -m[15];
|
|
1262
|
+
const det = a0 * (b1 * c2 - b2 * c1) - a1 * (b0 * c2 - b2 * c0) + a2 * (b0 * c1 - b1 * c0);
|
|
1263
|
+
if (!det || !Number.isFinite(det)) return false;
|
|
1264
|
+
const x = (t0 * (b1 * c2 - b2 * c1) - a1 * (t1 * c2 - b2 * t2) + a2 * (t1 * c1 - b1 * t2)) / det;
|
|
1265
|
+
const y = (a0 * (t1 * c2 - b2 * t2) - t0 * (b0 * c2 - b2 * c0) + a2 * (b0 * t2 - t1 * c0)) / det;
|
|
1266
|
+
const z = (a0 * (b1 * t2 - t1 * c1) - a1 * (b0 * t2 - t1 * c0) + t0 * (b0 * c1 - b1 * c0)) / det;
|
|
1267
|
+
out[0] = x / this.projection.world + this.origin[0];
|
|
1268
|
+
out[1] = y / this.projection.world + this.origin[1];
|
|
1269
|
+
out[2] = z;
|
|
1270
|
+
return true;
|
|
1271
|
+
}
|
|
1272
|
+
rayDepth(z) {
|
|
1273
|
+
return this.rayHit(z)?.depth ?? NaN;
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Where the view-center ray meets elevation `z`: world x, y (layer pixels from the
|
|
1277
|
+
* origin) and clip w, in a reused scratch object. Flat projection only; the globe
|
|
1278
|
+
* falls back to the origin.
|
|
1279
|
+
*/
|
|
1280
|
+
rayHit(z) {
|
|
1281
|
+
const hit = this.rayScratch;
|
|
1282
|
+
if (this.projection.transition !== 0) {
|
|
1283
|
+
hit.x = hit.y = 0;
|
|
1284
|
+
hit.depth = this.projection.project(0, 0, z).clip[3];
|
|
1285
|
+
return hit;
|
|
1286
|
+
}
|
|
1287
|
+
const m = this.projection.matrix;
|
|
1288
|
+
const a = m[0], b = m[4], c = m[1], d = m[5];
|
|
1289
|
+
const e = -(m[8] * z + m[12]), f = -(m[9] * z + m[13]);
|
|
1290
|
+
const det = a * d - b * c;
|
|
1291
|
+
if (Math.abs(det) < 1e-12) return null;
|
|
1292
|
+
hit.x = (e * d - b * f) / det;
|
|
1293
|
+
hit.y = (a * f - e * c) / det;
|
|
1294
|
+
hit.depth = m[3] * hit.x + m[7] * hit.y + m[11] * z + m[15];
|
|
1295
|
+
return hit;
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Terrain height where the view-center ray meets the current reference plane. Unlike
|
|
1299
|
+
* MapLibre's center elevation, which is frozen during gestures and re-seated after
|
|
1300
|
+
* them, this follows the camera continuously and is unchanged while it is still.
|
|
1301
|
+
*/
|
|
1302
|
+
rayTerrainElevation() {
|
|
1303
|
+
const ref = this.reference.elevation;
|
|
1304
|
+
const fallback = this.map.getCenterElevation();
|
|
1305
|
+
if (!this.map.getTerrain() || !Number.isFinite(ref)) return fallback;
|
|
1306
|
+
const hit = this.rayHit(ref);
|
|
1307
|
+
if (!hit || !(hit.depth > 0)) return fallback;
|
|
1308
|
+
const world = this.projection.world, position = this.positionScratch;
|
|
1309
|
+
position[0] = (hit.x / world + this.origin[0]) * 360 - 180;
|
|
1310
|
+
position[1] = mercatorLatitude(hit.y / world + this.origin[1]) * 180 / Math.PI;
|
|
1311
|
+
const value = this.sampleElevation(position);
|
|
1312
|
+
return Number.isFinite(value) ? value : fallback;
|
|
1313
|
+
}
|
|
1314
|
+
/** Elevation of the distance-scaling reference plane; sea level until one is set. */
|
|
1315
|
+
referenceElevation() {
|
|
1316
|
+
return Number.isFinite(this.reference.elevation) ? this.reference.elevation : 0;
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Combined zoom and distance scale for a pin anchored at world (x, y), mirroring the
|
|
1320
|
+
* shader: distance is horizontal, the anchor's clip w at the reference elevation.
|
|
1321
|
+
*/
|
|
1322
|
+
pinScale(presentation, worldX, worldY) {
|
|
1323
|
+
const depth = this.projection.project(worldX, worldY, this.referenceElevation()).clip[3];
|
|
1324
|
+
return combinePinScale(presentation, this.zoomScale(), pinDistanceScale(presentation, depth, this.reference.depth));
|
|
1325
|
+
}
|
|
1126
1326
|
footprint(id) {
|
|
1127
1327
|
const i = this.pins.findIndex((p) => p.id === id);
|
|
1128
1328
|
const pin = this.pins[i];
|
|
@@ -1130,10 +1330,13 @@ var PinLayer = class {
|
|
|
1130
1330
|
const p = pin.presentation, coord = pinMercator(pin.position);
|
|
1131
1331
|
coord.x += Math.round(this.origin[0] - coord.x);
|
|
1132
1332
|
const world = 512 * 2 ** this.map.getZoom();
|
|
1133
|
-
const
|
|
1134
|
-
const
|
|
1333
|
+
const x = (coord.x - this.origin[0]) * world, y = (coord.y - this.origin[1]) * world;
|
|
1334
|
+
const project = (z) => this.projectToPixel(x, y, z);
|
|
1335
|
+
const ground = project(this.elevations[i] ?? 0);
|
|
1336
|
+
const pinScale = this.pinScale(p, x, y);
|
|
1337
|
+
const attachment = p.mode === "world" ? project((this.elevations[i] ?? 0) + p.heightMeters) : [ground[0], ground[1] - p.liftPixels * pinScale];
|
|
1135
1338
|
if (!attachment.every(Number.isFinite)) return null;
|
|
1136
|
-
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);
|
|
1339
|
+
const scale = p.sizePixels / pin.octagonSize * pinScale, padding = (p.plate.enabled ? p.plate.padding : 0) * pinScale, halo = (p.symbolHalo.opacity > 0 ? p.symbolHalo.width : 0) * pinScale, overhang = Math.max(0, halo - padding);
|
|
1137
1340
|
return {
|
|
1138
1341
|
ground,
|
|
1139
1342
|
attachment,
|
|
@@ -1149,8 +1352,9 @@ var PinLayer = class {
|
|
|
1149
1352
|
const c = pinMercator(ll);
|
|
1150
1353
|
c.x += Math.round(this.origin[0] - c.x);
|
|
1151
1354
|
const world = 512 * 2 ** this.map.getZoom();
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1355
|
+
const x = (c.x - this.origin[0]) * world, y = (c.y - this.origin[1]) * world;
|
|
1356
|
+
const [px, py] = this.projectToPixel(x, y, elevationOrSeaLevel(this.elevation(ll)) + (p.mode === "world" ? p.heightMeters : 0));
|
|
1357
|
+
return [px, py - (p.mode === "screen" ? p.liftPixels * this.pinScale(p, x, y) : 0)];
|
|
1154
1358
|
};
|
|
1155
1359
|
return invertPinProjection(project, pin.position, attachment, this.map.getProjection()?.type === "globe" ? 90 : void 0);
|
|
1156
1360
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orbat-mapper/tactical-draw-adapter-maplibre",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "MapLibre GL JS engine adapter for @orbat-mapper/tactical-draw. Draw and edit tactical control measures (MIL-STD-2525 / APP-6) on a MapLibre map.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Orbat Mapper",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@orbat-mapper/control-measures": "^0.30.
|
|
41
|
-
"@orbat-mapper/tactical-draw": "^0.
|
|
40
|
+
"@orbat-mapper/control-measures": "^0.30.1",
|
|
41
|
+
"@orbat-mapper/tactical-draw": "^0.12.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"maplibre-gl": ">=5.8.0 <7.0.0"
|