maplibre-gl 6.2.0 → 6.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/maplibre-gl-dev.mjs +1108 -427
- package/dist/maplibre-gl-dev.mjs.map +1 -1
- package/dist/maplibre-gl-shared-dev.mjs +359 -291
- package/dist/maplibre-gl-shared-dev.mjs.map +1 -1
- package/dist/maplibre-gl-shared.mjs +2 -2
- package/dist/maplibre-gl-shared.mjs.map +1 -1
- package/dist/maplibre-gl-worker-dev.mjs +23 -49
- package/dist/maplibre-gl-worker-dev.mjs.map +1 -1
- package/dist/maplibre-gl-worker.mjs +2 -2
- package/dist/maplibre-gl-worker.mjs.map +1 -1
- package/dist/maplibre-gl.css +1 -1
- package/dist/maplibre-gl.d.ts +467 -151
- package/dist/maplibre-gl.mjs +49 -49
- package/dist/maplibre-gl.mjs.map +1 -1
- package/package.json +12 -22
- package/src/data/bucket/fill_extrusion_bucket.ts +1 -13
- package/src/data/bucket/round_polygon_corners.test.ts +84 -36
- package/src/data/bucket/round_polygon_corners.ts +90 -39
- package/src/data/extent_bounds.ts +28 -1
- package/src/data/feature_index.bench.ts +21 -0
- package/src/data/segment.test.ts +2 -2
- package/src/geo/edge_insets.test.ts +1 -1
- package/src/geo/lng_lat.test.ts +7 -7
- package/src/geo/lng_lat_bounds.test.ts +4 -4
- package/src/geo/mercator_coordinate.test.ts +2 -2
- package/src/geo/projection/camera_helper.ts +5 -0
- package/src/geo/projection/globe_transform.test.ts +154 -1
- package/src/geo/projection/globe_transform.ts +7 -3
- package/src/geo/projection/globe_utils.ts +189 -2
- package/src/geo/projection/mercator_camera_helper.ts +1 -1
- package/src/geo/projection/mercator_transform.ts +21 -5
- package/src/geo/projection/vertical_perspective_camera_helper.test.ts +24 -0
- package/src/geo/projection/vertical_perspective_camera_helper.ts +71 -41
- package/src/geo/projection/vertical_perspective_transform.ts +7 -1
- package/src/geo/transform_interface.ts +13 -1
- package/src/index.ts +7 -2
- package/src/render/image_atlas.test.ts +180 -0
- package/src/render/image_atlas.ts +52 -6
- package/src/render/image_manager.test.ts +111 -0
- package/src/render/image_manager.ts +143 -144
- package/src/render/painter.ts +6 -14
- package/src/render/pattern_atlas.test.ts +131 -0
- package/src/render/pattern_atlas.ts +144 -0
- package/src/render/update_pattern_positions_in_program.test.ts +1 -1
- package/src/shaders/glsl/_prelude.vertex.glsl +10 -11
- package/src/shaders/glsl/_prelude.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/circle.vertex.glsl +4 -4
- package/src/shaders/glsl/circle.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/color_relief.fragment.glsl +1 -2
- package/src/shaders/glsl/color_relief.fragment.glsl.g.ts +1 -1
- package/src/shaders/glsl/fill_extrusion.vertex.glsl +3 -3
- package/src/shaders/glsl/fill_extrusion.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl +5 -5
- package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/heatmap.vertex.glsl +4 -4
- package/src/shaders/glsl/heatmap.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/hillshade_prepare.fragment.glsl +12 -12
- package/src/shaders/glsl/hillshade_prepare.fragment.glsl.g.ts +1 -1
- package/src/shaders/glsl/line.vertex.glsl +7 -7
- package/src/shaders/glsl/line.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/line_gradient.vertex.glsl +6 -6
- package/src/shaders/glsl/line_gradient.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/line_gradient_sdf.vertex.glsl +7 -7
- package/src/shaders/glsl/line_gradient_sdf.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/line_pattern.vertex.glsl +7 -7
- package/src/shaders/glsl/line_pattern.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/line_sdf.vertex.glsl +7 -7
- package/src/shaders/glsl/line_sdf.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/raster.fragment.glsl +4 -4
- package/src/shaders/glsl/raster.fragment.glsl.g.ts +1 -1
- package/src/shaders/glsl/raster.vertex.glsl +11 -6
- package/src/shaders/glsl/raster.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/symbol_icon.vertex.glsl +5 -5
- package/src/shaders/glsl/symbol_icon.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/symbol_sdf.vertex.glsl +5 -5
- package/src/shaders/glsl/symbol_sdf.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/symbol_text_and_icon.vertex.glsl +6 -6
- package/src/shaders/glsl/symbol_text_and_icon.vertex.glsl.g.ts +1 -1
- package/src/source/canvas_source.test.ts +31 -0
- package/src/source/canvas_source.ts +2 -1
- package/src/source/geojson_worker_source.test.ts +88 -0
- package/src/source/geojson_worker_source.ts +22 -39
- package/src/source/image_source.test.ts +246 -38
- package/src/source/image_source.ts +132 -19
- package/src/source/raster_dem_tile_source.test.ts +37 -3
- package/src/source/raster_dem_tile_source.ts +1 -2
- package/src/source/raster_dem_tile_worker_source.test.ts +1 -1
- package/src/source/raster_tile_source.test.ts +5 -26
- package/src/source/raster_tile_source.ts +4 -3
- package/src/source/vector_tile_worker_source.test.ts +94 -2
- package/src/source/vector_tile_worker_source.ts +18 -27
- package/src/source/video_source.test.ts +12 -8
- package/src/source/worker_tile.bench.ts +10 -0
- package/src/source/worker_tile.ts +0 -3
- package/src/style/create_style_layer.bench.ts +15 -0
- package/src/style/properties.bench.ts +81 -0
- package/src/style/style.test.ts +75 -10
- package/src/style/style.ts +37 -64
- package/src/style/style_image.ts +67 -9
- package/src/style/style_layer.bench.ts +65 -0
- package/src/style/style_layer.test.ts +1 -1
- package/src/style/style_layer.ts +2 -2
- package/src/style/validate_style.bench.ts +11 -0
- package/src/style/validate_style.test.ts +2 -2
- package/src/symbol/anchor.test.ts +2 -2
- package/src/symbol/collision_index.bench.ts +116 -0
- package/src/symbol/cross_tile_symbol_index.bench.ts +49 -0
- package/src/symbol/cross_tile_symbol_index.test.ts +31 -0
- package/src/symbol/cross_tile_symbol_index.ts +67 -37
- package/src/symbol/shaping.test.ts +2 -1
- package/src/symbol/symbol_layout.bench.ts +30 -0
- package/src/tile/tile_id.test.ts +1 -1
- package/src/tile/tile_manager.test.ts +69 -1
- package/src/tile/tile_manager.ts +16 -11
- package/src/ui/control/fullscreen_control.test.ts +10 -10
- package/src/ui/control/geolocate_control.test.ts +3 -5
- package/src/ui/control/globe_control.test.ts +6 -6
- package/src/ui/events.ts +1 -1
- package/src/ui/handler/scroll_zoom.test.ts +50 -0
- package/src/ui/handler/two_fingers_touch.test.ts +17 -6
- package/src/ui/handler_inertia.test.ts +91 -0
- package/src/ui/handler_inertia.ts +65 -14
- package/src/ui/handler_manager.test.ts +243 -3
- package/src/ui/handler_manager.ts +79 -16
- package/src/ui/map.ts +17 -3
- package/src/ui/map_tests/map_cross_window.test.ts +2 -2
- package/src/ui/map_tests/map_events.test.ts +3 -3
- package/src/ui/map_tests/map_images.test.ts +55 -0
- package/src/ui/map_tests/map_style.test.ts +9 -8
- package/src/ui/map_tests/map_webgl.test.ts +30 -0
- package/src/ui/marker.test.ts +177 -42
- package/src/ui/marker.ts +120 -10
- package/src/ui/popup.test.ts +14 -14
- package/src/util/evented.test.ts +33 -28
- package/src/util/evented.ts +69 -43
- package/src/util/image_request.ts +31 -1
- package/src/util/test/null_gl.ts +10 -0
- package/src/util/test/util.ts +2 -1
- package/src/util/util.test.ts +1 -1
- package/src/util/web_worker_transfer.test.ts +5 -5
- package/src/webgl/context.ts +20 -0
- package/src/webgl/draw/draw_background.ts +1 -1
- package/src/webgl/draw/draw_raster.ts +11 -6
- package/src/webgl/program/pattern.ts +3 -3
- package/src/webgl/program/raster_program.ts +11 -0
- package/src/webgl/program.ts +31 -16
- package/src/webgl/texture.test.ts +16 -1
- package/src/webgl/texture.ts +3 -0
- package/src/webgl/vertex_buffer.test.ts +17 -2
- package/src/webgl/vertex_buffer.ts +24 -13
|
@@ -16,6 +16,11 @@ export type MapControlsDeltas = {
|
|
|
16
16
|
pitchDelta: number;
|
|
17
17
|
rollDelta: number;
|
|
18
18
|
around: Point;
|
|
19
|
+
/**
|
|
20
|
+
* Elevation in meters of the terrain under `around` at gesture start; when set,
|
|
21
|
+
* pan and zoom keep the terrain at this elevation under `around`.
|
|
22
|
+
*/
|
|
23
|
+
aroundElevation?: number;
|
|
19
24
|
};
|
|
20
25
|
|
|
21
26
|
export type CameraForBoxAndBearingHandlerResult = {
|
|
@@ -4,11 +4,12 @@ import Point from '@mapbox/point-geometry';
|
|
|
4
4
|
import {LngLat} from '../lng_lat.ts';
|
|
5
5
|
import {GlobeTransform} from './globe_transform.ts';
|
|
6
6
|
import {CanonicalTileID, OverscaledTileID, UnwrappedTileID} from '../../tile/tile_id.ts';
|
|
7
|
-
import {angularCoordinatesRadiansToVector, mercatorCoordinatesToAngularCoordinatesRadians, sphereSurfacePointToCoordinates} from './globe_utils.ts';
|
|
7
|
+
import {angularCoordinatesRadiansToVector, mercatorCoordinatesToAngularCoordinatesRadians, sphereSurfacePointToCoordinates, versorSetLocationAtPoint} from './globe_utils.ts';
|
|
8
8
|
import {expectToBeCloseToArray} from '../../util/test/util.ts';
|
|
9
9
|
import {MercatorCoordinate} from '../mercator_coordinate.ts';
|
|
10
10
|
import {tileCoordinatesToLocation} from './mercator_utils.ts';
|
|
11
11
|
import {MercatorTransform} from './mercator_transform.ts';
|
|
12
|
+
import {differenceOfAnglesDegrees, MAX_VALID_LATITUDE} from '../../util/util.ts';
|
|
12
13
|
|
|
13
14
|
function testPlaneAgainstLngLat(lngDegrees: number, latDegrees: number, plane: number[]) {
|
|
14
15
|
const lat = latDegrees / 180.0 * Math.PI;
|
|
@@ -400,6 +401,31 @@ describe('GlobeTransform', () => {
|
|
|
400
401
|
expect(globeTransform.center.lat).toBeCloseTo(20.659450722109348, precisionDigits);
|
|
401
402
|
});
|
|
402
403
|
});
|
|
404
|
+
|
|
405
|
+
test('ignores the elevation parameter when rendering the globe', () => {
|
|
406
|
+
const transform = createGlobeTransform();
|
|
407
|
+
transform.setZoom(1);
|
|
408
|
+
coords = new LngLat(5, 10);
|
|
409
|
+
point = new Point(320, 240);
|
|
410
|
+
transform.setLocationAtPoint(coords, point, 1000);
|
|
411
|
+
unprojected = transform.screenPointToLocationAtElevation(point, 1000);
|
|
412
|
+
expect(unprojected.lng).toBeCloseTo(coords.lng, precisionDigits);
|
|
413
|
+
expect(unprojected.lat).toBeCloseTo(coords.lat, precisionDigits);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test('solves on the plane at the given elevation when rendering mercator', () => {
|
|
417
|
+
const transform = createGlobeTransform();
|
|
418
|
+
transform.setZoom(5);
|
|
419
|
+
transform.setPitch(40);
|
|
420
|
+
transform.setTransitionState(0); // rendering mercator
|
|
421
|
+
coords = new LngLat(5, 10);
|
|
422
|
+
point = new Point(320, 200);
|
|
423
|
+
transform.setLocationAtPoint(coords, point, 500);
|
|
424
|
+
unprojected = transform.screenPointToLocationAtElevation(point, 500);
|
|
425
|
+
// exact up to the center-latitude change the solve itself causes (~1e-5 deg)
|
|
426
|
+
expect(unprojected.lng).toBeCloseTo(coords.lng, 3);
|
|
427
|
+
expect(unprojected.lat).toBeCloseTo(coords.lat, 3);
|
|
428
|
+
});
|
|
403
429
|
});
|
|
404
430
|
});
|
|
405
431
|
|
|
@@ -618,4 +644,131 @@ describe('GlobeTransform', () => {
|
|
|
618
644
|
expect(globeTransform.center.lng).toBeCloseTo(originalLng, 10);
|
|
619
645
|
expect(globeTransform.center.lat).toBeCloseTo(originalLat, 10);
|
|
620
646
|
});
|
|
647
|
+
|
|
648
|
+
describe('versorSetLocationAtPoint', () => {
|
|
649
|
+
const precisionDigits = 4;
|
|
650
|
+
const globeTransform = createGlobeTransform();
|
|
651
|
+
globeTransform.setZoom(1);
|
|
652
|
+
globeTransform.setTransitionState(1);
|
|
653
|
+
let coords: LngLat;
|
|
654
|
+
let point: Point;
|
|
655
|
+
let unprojected: LngLat;
|
|
656
|
+
|
|
657
|
+
test('round-trip accuracy', () => {
|
|
658
|
+
coords = new LngLat(20, 30);
|
|
659
|
+
point = new Point(280, 200);
|
|
660
|
+
versorSetLocationAtPoint(globeTransform, coords, point, undefined, false);
|
|
661
|
+
unprojected = globeTransform.screenPointToLocation(point);
|
|
662
|
+
expect(unprojected.lng).toBeCloseTo(coords.lng, precisionDigits);
|
|
663
|
+
expect(unprojected.lat).toBeCloseTo(coords.lat, precisionDigits);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test('round-trip accuracy at center', () => {
|
|
667
|
+
coords = new LngLat(10, 15);
|
|
668
|
+
point = new Point(320, 240);
|
|
669
|
+
versorSetLocationAtPoint(globeTransform, coords, point);
|
|
670
|
+
unprojected = globeTransform.screenPointToLocation(point);
|
|
671
|
+
expect(unprojected.lng).toBeCloseTo(coords.lng, precisionDigits);
|
|
672
|
+
expect(unprojected.lat).toBeCloseTo(coords.lat, precisionDigits);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
test('near-pole panning stays finite', () => {
|
|
676
|
+
globeTransform.setCenter(new LngLat(0, 80));
|
|
677
|
+
coords = new LngLat(10, 85);
|
|
678
|
+
point = new Point(300, 230);
|
|
679
|
+
versorSetLocationAtPoint(globeTransform, coords, point);
|
|
680
|
+
expect(isNaN(globeTransform.center.lng)).toBe(false);
|
|
681
|
+
expect(isNaN(globeTransform.center.lat)).toBe(false);
|
|
682
|
+
expect(isNaN(globeTransform.bearing)).toBe(false);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test('identical source and target points leave the center unchanged', () => {
|
|
686
|
+
const centerBefore = globeTransform.center;
|
|
687
|
+
point = new Point(320, 240);
|
|
688
|
+
coords = globeTransform.screenPointToLocation(point);
|
|
689
|
+
versorSetLocationAtPoint(globeTransform, coords, point);
|
|
690
|
+
expect(globeTransform.center.lng).toBeCloseTo(centerBefore.lng, precisionDigits);
|
|
691
|
+
expect(globeTransform.center.lat).toBeCloseTo(centerBefore.lat, precisionDigits);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
test('bearing changes when panning off-center without a fixed bearing', () => {
|
|
695
|
+
const bearingBefore = globeTransform.bearing;
|
|
696
|
+
coords = new LngLat(20, 30);
|
|
697
|
+
point = new Point(250, 180);
|
|
698
|
+
versorSetLocationAtPoint(globeTransform, coords, point, undefined, false);
|
|
699
|
+
expect(globeTransform.bearing).not.toBeCloseTo(bearingBefore, 1);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
test('target points that miss the globe are ignored without a pan delta', () => {
|
|
703
|
+
const freshTransform = createGlobeTransform();
|
|
704
|
+
freshTransform.setZoom(1);
|
|
705
|
+
freshTransform.setTransitionState(1);
|
|
706
|
+
freshTransform.setCenter(new LngLat(5, 10));
|
|
707
|
+
const lngBefore = freshTransform.center.lng;
|
|
708
|
+
const latBefore = freshTransform.center.lat;
|
|
709
|
+
const bearingBefore = freshTransform.bearing;
|
|
710
|
+
point = new Point(620, 240);
|
|
711
|
+
expect(freshTransform.isPointOnMapSurface(point)).toBe(false);
|
|
712
|
+
coords = freshTransform.screenPointToLocation(point);
|
|
713
|
+
versorSetLocationAtPoint(freshTransform, coords, point);
|
|
714
|
+
expect(freshTransform.center.lng).toBe(lngBefore);
|
|
715
|
+
expect(freshTransform.center.lat).toBe(latBefore);
|
|
716
|
+
expect(freshTransform.bearing).toBe(bearingBefore);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
test('panning continues once the cursor leaves the globe', () => {
|
|
720
|
+
const freshTransform = createGlobeTransform();
|
|
721
|
+
freshTransform.setZoom(1);
|
|
722
|
+
freshTransform.setTransitionState(1);
|
|
723
|
+
freshTransform.setCenter(new LngLat(5, 10));
|
|
724
|
+
const lngBefore = freshTransform.center.lng;
|
|
725
|
+
const bearingBefore = freshTransform.bearing;
|
|
726
|
+
point = new Point(620, 240);
|
|
727
|
+
expect(freshTransform.isPointOnMapSurface(point)).toBe(false);
|
|
728
|
+
coords = freshTransform.screenPointToLocation(point);
|
|
729
|
+
versorSetLocationAtPoint(freshTransform, coords, point, new Point(20, 0));
|
|
730
|
+
expect(freshTransform.center.lng).not.toBe(lngBefore);
|
|
731
|
+
expect(isNaN(freshTransform.center.lng)).toBe(false);
|
|
732
|
+
expect(isNaN(freshTransform.center.lat)).toBe(false);
|
|
733
|
+
expect(freshTransform.bearing).toBe(bearingBefore);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
test('panning does not freeze near a centred pole', () => {
|
|
737
|
+
// With the pole centred the dial supplies all of the longitude change, and the center
|
|
738
|
+
// latitude is already clamped, so if the dial gives up the drag stops moving entirely.
|
|
739
|
+
const tr = createGlobeTransform();
|
|
740
|
+
tr.setZoom(1);
|
|
741
|
+
tr.setTransitionState(1);
|
|
742
|
+
tr.setCenter(new LngLat(0, MAX_VALID_LATITUDE));
|
|
743
|
+
const pole = tr.locationToScreenPoint(new LngLat(0, 90));
|
|
744
|
+
const cursor = new Point(pole.x + 10, pole.y);
|
|
745
|
+
const panDelta = new Point(0, 8); // tangential, so it sweeps around the pole
|
|
746
|
+
const lngBefore = tr.center.lng;
|
|
747
|
+
const location = tr.screenPointToLocation(cursor.sub(panDelta));
|
|
748
|
+
versorSetLocationAtPoint(tr, location, cursor, panDelta);
|
|
749
|
+
expect(Math.abs(differenceOfAnglesDegrees(lngBefore, tr.center.lng))).toBeGreaterThan(1);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
test('panning off the globe is slower than on it', () => {
|
|
753
|
+
const travel = (screenPoint: Point) => {
|
|
754
|
+
const tr = createGlobeTransform();
|
|
755
|
+
tr.setZoom(1);
|
|
756
|
+
tr.setTransitionState(1);
|
|
757
|
+
tr.setCenter(new LngLat(0, 0));
|
|
758
|
+
const panDelta = new Point(20, 0);
|
|
759
|
+
const location = tr.screenPointToLocation(screenPoint.sub(panDelta));
|
|
760
|
+
versorSetLocationAtPoint(tr, location, screenPoint, panDelta);
|
|
761
|
+
return Math.abs(differenceOfAnglesDegrees(0, tr.center.lng));
|
|
762
|
+
};
|
|
763
|
+
const onGlobe = new Point(340, 240);
|
|
764
|
+
const offGlobe = new Point(620, 240);
|
|
765
|
+
const reference = createGlobeTransform();
|
|
766
|
+
reference.setZoom(1);
|
|
767
|
+
reference.setTransitionState(1);
|
|
768
|
+
reference.setCenter(new LngLat(0, 0));
|
|
769
|
+
expect(reference.isPointOnMapSurface(onGlobe)).toBe(true);
|
|
770
|
+
expect(reference.isPointOnMapSurface(offGlobe)).toBe(false);
|
|
771
|
+
expect(travel(offGlobe)).toBeLessThan(travel(onGlobe));
|
|
772
|
+
});
|
|
773
|
+
});
|
|
621
774
|
});
|
|
@@ -411,13 +411,13 @@ export class GlobeTransform implements ITransform {
|
|
|
411
411
|
* Note: automatically adjusts zoom to keep planet size consistent
|
|
412
412
|
* (same size before and after a {@link setLocationAtPoint} call).
|
|
413
413
|
*/
|
|
414
|
-
setLocationAtPoint(lnglat: LngLat, point: Point): void {
|
|
414
|
+
setLocationAtPoint(lnglat: LngLat, point: Point, elevation?: number): void {
|
|
415
415
|
if (!this.isGlobeRendering) {
|
|
416
|
-
this._mercatorTransform.setLocationAtPoint(lnglat, point);
|
|
416
|
+
this._mercatorTransform.setLocationAtPoint(lnglat, point, elevation);
|
|
417
417
|
this.apply(this._mercatorTransform, false);
|
|
418
418
|
return;
|
|
419
419
|
}
|
|
420
|
-
this._verticalPerspectiveTransform.setLocationAtPoint(lnglat, point);
|
|
420
|
+
this._verticalPerspectiveTransform.setLocationAtPoint(lnglat, point, elevation);
|
|
421
421
|
this.apply(this._verticalPerspectiveTransform, false);
|
|
422
422
|
return;
|
|
423
423
|
}
|
|
@@ -434,6 +434,10 @@ export class GlobeTransform implements ITransform {
|
|
|
434
434
|
return this.currentTransform.screenPointToLocation(p, terrain);
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
+
screenPointToLocationAtElevation(p: Point, elevation: number): LngLat {
|
|
438
|
+
return this.currentTransform.screenPointToLocationAtElevation(p, elevation);
|
|
439
|
+
}
|
|
440
|
+
|
|
437
441
|
isPointOnMapSurface(p: Point, terrain?: Terrain): boolean {
|
|
438
442
|
return this.currentTransform.isPointOnMapSurface(p, terrain);
|
|
439
443
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {type ReadonlyVec4, vec3} from 'gl-matrix';
|
|
2
|
-
import {clamp, createVec3f64, lerp, MAX_VALID_LATITUDE, mod, remapSaturate, scaleZoom, wrap} from '../../util/util.ts';
|
|
1
|
+
import {quat, type ReadonlyVec4, vec3} from 'gl-matrix';
|
|
2
|
+
import {clamp, createVec3f64, createVec4f64, lerp, MAX_VALID_LATITUDE, mod, remapSaturate, scaleZoom, wrap} from '../../util/util.ts';
|
|
3
3
|
import {LngLat} from '../lng_lat.ts';
|
|
4
4
|
import {EXTENT} from '../../data/extent.ts';
|
|
5
5
|
import type Point from '@mapbox/point-geometry';
|
|
6
|
+
import type {ITransform} from '../transform_interface.ts';
|
|
6
7
|
|
|
7
8
|
export function getGlobeCircumferencePixels(transform: {worldSize: number; center: {lat: number}}): number {
|
|
8
9
|
const radius = getGlobeRadiusPixels(transform.worldSize, transform.center.lat);
|
|
@@ -107,6 +108,192 @@ export function sphereSurfacePointToCoordinates(surface: vec3): LngLat {
|
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Returns the globe orientation quaternion for the given map center and bearing.
|
|
113
|
+
* The inverse of {@link lngLatBearingFromOrientation}.
|
|
114
|
+
*/
|
|
115
|
+
export function orientationFromLngLatBearing(lngLat: LngLat, bearing: number): quat {
|
|
116
|
+
return quat.fromEuler(createVec4f64(), -lngLat.lng, -lngLat.lat, bearing);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Given a globe orientation quaternion, returns the corresponding map center and bearing.
|
|
121
|
+
* The inverse of {@link orientationFromLngLatBearing}.
|
|
122
|
+
*/
|
|
123
|
+
export function lngLatBearingFromOrientation(q: quat): { lng: number; lat: number; bearing: number } {
|
|
124
|
+
const x = q[0], y = q[1], z = q[2], w = q[3];
|
|
125
|
+
const lng = -Math.atan2(2 * (w * x + y * z), 1 - 2 * (x * x + y * y)) * 180 / Math.PI;
|
|
126
|
+
const lat = -Math.asin(clamp(2 * (w * y - z * x), -1, 1)) * 180 / Math.PI;
|
|
127
|
+
const bearing = Math.atan2(2 * (w * z + x * y), 1 - 2 * (y * y + z * z)) * 180 / Math.PI;
|
|
128
|
+
return {lng, lat, bearing};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* How much of the angle between the view axis and the horizon is given over to easing off, measured
|
|
133
|
+
* inward from the horizon. Only the outermost sliver of the globe is affected, where a pixel is
|
|
134
|
+
* already worth degrees of arc.
|
|
135
|
+
*
|
|
136
|
+
* This is the one number here open to taste, and it sets two things at once: narrowing it keeps the
|
|
137
|
+
* drag exact over more of the globe, and, since the exact curve steepens towards tangency, hands
|
|
138
|
+
* over at a higher rate too. Bell has no such freedom, as a hyperbola of the fixed form `k/d` meets
|
|
139
|
+
* a sphere in both value and slope at exactly one radius.
|
|
140
|
+
*/
|
|
141
|
+
const PAN_FALLOFF_BAND = 0.1;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Radius around the pole's screen position, in pixels, within which the cursor's sweep around it
|
|
145
|
+
* stops being trusted at face value: the angle a given drag subtends grows without bound as the
|
|
146
|
+
* cursor closes on the pole, and at the pole itself there is no angle at all.
|
|
147
|
+
*/
|
|
148
|
+
const DIAL_MIN_RADIUS_PIXELS = 20;
|
|
149
|
+
|
|
150
|
+
/** Kept just below a half turn so the center cannot land exactly on the far side of the globe. */
|
|
151
|
+
const PAN_MAX_ANGLE = Math.PI * 0.98;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Returns the point on the globe that a drag towards `point` should aim at.
|
|
155
|
+
*
|
|
156
|
+
* Exact tracking breaks down at the silhouette: the ray meets the globe at `asin(D sin a) - a`,
|
|
157
|
+
* whose slope runs away to infinity as the ray goes tangent, and past it there is no intersection
|
|
158
|
+
* at all. So the exact curve is left {@link PAN_FALLOFF_BAND} early and continued with a hyperbola
|
|
159
|
+
* matching it in value and slope, easing off and saturating short of the far side. Bell's virtual
|
|
160
|
+
* trackball takes the same idea of easing a sphere into a hyperbola before the rim, though it fits
|
|
161
|
+
* the curve differently: SGI's `trackball.c`, described in Henriksen, Sporring and Hornbæk,
|
|
162
|
+
* "Virtual Trackballs Revisited", IEEE TVCG 10(2):206-216, 2004.
|
|
163
|
+
*
|
|
164
|
+
* The angle comes from atan2, so a ray pointing away from the globe is a wide angle the falloff
|
|
165
|
+
* saturates rather than a case to reject.
|
|
166
|
+
* @param tr - The transform being dragged.
|
|
167
|
+
* @param point - The cursor position.
|
|
168
|
+
*/
|
|
169
|
+
function panSurfaceLocation(tr: ITransform, point: Point): LngLat {
|
|
170
|
+
const origin = tr.cameraPosition;
|
|
171
|
+
const distance = vec3.length(origin);
|
|
172
|
+
if (distance <= 1) {
|
|
173
|
+
return tr.screenPointToLocation(point);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const u = createVec3f64();
|
|
177
|
+
vec3.normalize(u, origin);
|
|
178
|
+
const direction = tr.getRayDirectionFromPixel(point);
|
|
179
|
+
const c = -vec3.dot(direction, u);
|
|
180
|
+
const lateral = createVec3f64();
|
|
181
|
+
vec3.scaleAndAdd(lateral, direction, u, c);
|
|
182
|
+
const s = vec3.length(lateral);
|
|
183
|
+
if (s < 1e-9) {
|
|
184
|
+
return tr.screenPointToLocation(point);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const angle = Math.atan2(s, c);
|
|
188
|
+
const horizonAngle = Math.asin(1 / distance);
|
|
189
|
+
const handoverAngle = horizonAngle * (1 - PAN_FALLOFF_BAND);
|
|
190
|
+
if (angle < handoverAngle) {
|
|
191
|
+
return tr.screenPointToLocation(point);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const sinHandover = distance * Math.sin(handoverAngle);
|
|
195
|
+
const targetAtHandover = Math.asin(clamp(sinHandover, -1, 1)) - handoverAngle;
|
|
196
|
+
const slopeAtHandover = distance * Math.cos(handoverAngle) / Math.sqrt(Math.max(1 - sinHandover * sinHandover, 1e-12)) - 1;
|
|
197
|
+
const room = PAN_MAX_ANGLE - targetAtHandover;
|
|
198
|
+
const excess = angle - handoverAngle;
|
|
199
|
+
const target = targetAtHandover + room * (slopeAtHandover * excess) / (room + slopeAtHandover * excess);
|
|
200
|
+
|
|
201
|
+
const e = createVec3f64();
|
|
202
|
+
vec3.scale(e, lateral, 1 / s);
|
|
203
|
+
const surface = createVec3f64();
|
|
204
|
+
vec3.scale(surface, u, Math.cos(clamp(target, 0, PAN_MAX_ANGLE)));
|
|
205
|
+
vec3.scaleAndAdd(surface, surface, e, Math.sin(clamp(target, 0, PAN_MAX_ANGLE)));
|
|
206
|
+
vec3.normalize(surface, surface);
|
|
207
|
+
return sphereSurfacePointToCoordinates(surface);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Rotates the globe so that the given location appears at the given screen point, by composing
|
|
212
|
+
* versors, the unit quaternions that represent rotations. Unlike the bearing-preserving
|
|
213
|
+
* {@link ITransform.setLocationAtPoint}, this stays smooth near and across the poles, and keeps
|
|
214
|
+
* panning once the cursor leaves the globe.
|
|
215
|
+
*
|
|
216
|
+
* Note: the delta rotation's axis is in the surface-vector frame of
|
|
217
|
+
* {@link angularCoordinatesToSurfaceVector}, while the orientation quaternion uses the Euler frame
|
|
218
|
+
* of {@link orientationFromLngLatBearing}, hence the component swizzle where they combine. Zoom is
|
|
219
|
+
* adjusted to keep the planet the same size, as `setLocationAtPoint` does.
|
|
220
|
+
* @param tr - The transform to rotate.
|
|
221
|
+
* @param lnglat - The location to bring under `point`.
|
|
222
|
+
* @param point - The screen point that `lnglat` should appear at.
|
|
223
|
+
* @param panDelta - The drag's pixel delta. Used to re-derive the previous cursor location through
|
|
224
|
+
* {@link panSurfaceLocation}, since both ends of the rotation must come from the same mapping.
|
|
225
|
+
* @param fixedBearing - Applies the swing only, keeping the bearing fixed, which is what dragging
|
|
226
|
+
* the globe does. Pass `false` to apply the twist about the view axis as well, so that the grabbed
|
|
227
|
+
* location tracks the cursor exactly and the bearing drifts with it.
|
|
228
|
+
*/
|
|
229
|
+
export function versorSetLocationAtPoint(tr: ITransform, lnglat: LngLat, point: Point, panDelta?: Point, fixedBearing = true): void {
|
|
230
|
+
const pointLngLat = panSurfaceLocation(tr, point);
|
|
231
|
+
let sourceLngLat = lnglat;
|
|
232
|
+
if (panDelta) {
|
|
233
|
+
sourceLngLat = panSurfaceLocation(tr, point.sub(panDelta));
|
|
234
|
+
} else if (!tr.isPointOnMapSurface(point)) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const vecToPixelCurrent = angularCoordinatesToSurfaceVector(pointLngLat);
|
|
239
|
+
const vecToTarget = angularCoordinatesToSurfaceVector(sourceLngLat);
|
|
240
|
+
const centerQuat = orientationFromLngLatBearing(tr.center, tr.bearing);
|
|
241
|
+
const w = vec3.cross(createVec3f64(), vecToTarget, vecToPixelCurrent);
|
|
242
|
+
const l = Math.sqrt(vec3.dot(w, w));
|
|
243
|
+
const t = Math.acos(clamp(vec3.dot(vecToTarget, vecToPixelCurrent), -1, 1)) / 2;
|
|
244
|
+
const s = Math.sin(t);
|
|
245
|
+
const delta = l ? quat.fromValues((w[1] / l) * s, (-w[0] / l) * s, (w[2] / l) * s, Math.cos(t)) : quat.fromValues(0, 0, 0, 1);
|
|
246
|
+
const newCenterQuat = quat.multiply(createVec4f64(), centerQuat, delta);
|
|
247
|
+
const {lng: newCenterLng, lat: newCenterLat, bearing: newBearing} = lngLatBearingFromOrientation(newCenterQuat);
|
|
248
|
+
|
|
249
|
+
const oldLat = tr.center.lat;
|
|
250
|
+
const finalLat = clamp(newCenterLat, -90, 90);
|
|
251
|
+
const finalLng = fixedBearing ? fixedBearingLongitude(tr, point, panDelta, newCenterLng) : newCenterLng;
|
|
252
|
+
|
|
253
|
+
tr.setCenter(new LngLat(wrap(finalLng, -180, 180), finalLat));
|
|
254
|
+
if (!fixedBearing) {
|
|
255
|
+
tr.setBearing(newBearing);
|
|
256
|
+
}
|
|
257
|
+
tr.setZoom(tr.zoom + getZoomAdjustment(oldLat, tr.center.lat));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Returns the center longitude for a bearing-preserving drag.
|
|
262
|
+
*
|
|
263
|
+
* The swing longitude becomes ill-conditioned near the pole and the grabbed location slips away
|
|
264
|
+
* from the cursor, so within the last ~12 degrees of latitude the cursor is treated as turning a
|
|
265
|
+
* dial around the pole, blended in with a smoothstep anchored on {@link MAX_VALID_LATITUDE}, the
|
|
266
|
+
* highest latitude the center can reach. The sweep comes from the raw pixel delta rather than a
|
|
267
|
+
* round-tripped previous cursor position, which would lose its sign to cancellation at the pole,
|
|
268
|
+
* and is damped within {@link DIAL_MIN_RADIUS_PIXELS} so it eases to nothing there instead of
|
|
269
|
+
* being dropped, which would leave a spot where the drag could not move at all.
|
|
270
|
+
* @param tr - The transform being dragged.
|
|
271
|
+
* @param point - The cursor position.
|
|
272
|
+
* @param panDelta - The drag's pixel delta, or undefined to use the swing longitude only.
|
|
273
|
+
* @param newCenterLng - The swing target longitude.
|
|
274
|
+
* @returns the center longitude to apply.
|
|
275
|
+
*/
|
|
276
|
+
function fixedBearingLongitude(tr: ITransform, point: Point, panDelta: Point | undefined, newCenterLng: number): number {
|
|
277
|
+
const oldLng = tr.center.lng;
|
|
278
|
+
const oldLat = tr.center.lat;
|
|
279
|
+
const poleLat = oldLat >= 0 ? 90 : -90;
|
|
280
|
+
const polePoint = tr.locationToScreenPoint(new LngLat(0, poleLat));
|
|
281
|
+
const rx = point.x - polePoint.x, ry = point.y - polePoint.y;
|
|
282
|
+
const r2 = rx * rx + ry * ry;
|
|
283
|
+
|
|
284
|
+
const tRamp = clamp(1 - (MAX_VALID_LATITUDE - Math.abs(oldLat)) / 12, 0, 1);
|
|
285
|
+
const dial = tRamp * tRamp * (3 - 2 * tRamp);
|
|
286
|
+
const dLngSwing = mod(newCenterLng - oldLng + 180, 360) - 180;
|
|
287
|
+
|
|
288
|
+
let dLngDial = 0;
|
|
289
|
+
if (dial > 0 && panDelta) {
|
|
290
|
+
const dTheta = (rx * panDelta.y - ry * panDelta.x) / Math.max(r2, DIAL_MIN_RADIUS_PIXELS * DIAL_MIN_RADIUS_PIXELS);
|
|
291
|
+
dLngDial = (poleLat > 0 ? 1 : -1) * dTheta * 180 / Math.PI;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return oldLng + (1 - dial) * dLngSwing + dial * dLngDial;
|
|
295
|
+
}
|
|
296
|
+
|
|
110
297
|
/**
|
|
111
298
|
* Given a normalized horizon plane in Ax+By+Cz+D=0 format, compute the center and radius of
|
|
112
299
|
* the circle in that plain that contains the entire visible portion of the unit sphere from horizon
|
|
@@ -47,7 +47,7 @@ export class MercatorCameraHelper implements ICameraHelper {
|
|
|
47
47
|
if (deltas.around.distSqr(tr.centerPoint) < 1.0e-2) {
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
|
-
tr.setLocationAtPoint(preZoomAroundLoc, deltas.around);
|
|
50
|
+
tr.setLocationAtPoint(preZoomAroundLoc, deltas.around, deltas.aroundElevation);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
cameraForBoxAndBearing(options: CameraForBoundsOptions, padding: PaddingOptions, bounds: LngLatBounds, bearing: number, tr: IReadonlyTransform): CameraForBoxAndBearingHandlerResult {
|
|
@@ -308,10 +308,18 @@ export class MercatorTransform implements ITransform {
|
|
|
308
308
|
this._helper.recalculateZoomAndCenter(elevation);
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
/**
|
|
312
|
+
* Moves the center so that `lnglat`, on the ground at `elevation` meters, renders
|
|
313
|
+
* at the screen `point`. Both rays are cast through the same inverse pixel matrix
|
|
314
|
+
* so its inversion error cancels out of their difference; the current-center ray
|
|
315
|
+
* must be intersected at the center's own elevation (z=0) — intersecting it with
|
|
316
|
+
* the elevated plane would land `(elevation - centerElevation)·tan(pitch)` away
|
|
317
|
+
* from the center and make repeated calls drift.
|
|
318
|
+
*/
|
|
319
|
+
setLocationAtPoint(lnglat: LngLat, point: Point, elevation: number = this.elevation): void {
|
|
320
|
+
const z = elevation - this.elevation;
|
|
313
321
|
const a = this.screenPointToMercatorCoordinateAtZ(point, z);
|
|
314
|
-
const b = this.screenPointToMercatorCoordinateAtZ(this.centerPoint,
|
|
322
|
+
const b = this.screenPointToMercatorCoordinateAtZ(this.centerPoint, 0);
|
|
315
323
|
const loc = MercatorCoordinate.fromLngLat(lnglat);
|
|
316
324
|
const newCenter = new MercatorCoordinate(
|
|
317
325
|
loc.x - (a.x - b.x),
|
|
@@ -332,6 +340,10 @@ export class MercatorTransform implements ITransform {
|
|
|
332
340
|
return this.screenPointToMercatorCoordinate(p, terrain)?.toLngLat();
|
|
333
341
|
}
|
|
334
342
|
|
|
343
|
+
screenPointToLocationAtElevation(p: Point, elevation: number): LngLat {
|
|
344
|
+
return this.screenPointToMercatorCoordinateAtZ(p, elevation - this.elevation)?.toLngLat();
|
|
345
|
+
}
|
|
346
|
+
|
|
335
347
|
screenPointToMercatorCoordinate(p: Point, terrain?: Terrain): MercatorCoordinate {
|
|
336
348
|
// get point-coordinate from terrain coordinates framebuffer
|
|
337
349
|
if (terrain) {
|
|
@@ -343,10 +355,14 @@ export class MercatorTransform implements ITransform {
|
|
|
343
355
|
return this.screenPointToMercatorCoordinateAtZ(p);
|
|
344
356
|
}
|
|
345
357
|
|
|
346
|
-
|
|
358
|
+
/**
|
|
359
|
+
* Intersects the ray through a screen point with the horizontal plane at `z`,
|
|
360
|
+
* given in meters relative to the plane at the center's elevation (not mercator units).
|
|
361
|
+
*/
|
|
362
|
+
screenPointToMercatorCoordinateAtZ(p: Point, z?: number): MercatorCoordinate {
|
|
347
363
|
|
|
348
364
|
// calculate point-coordinate on flat earth
|
|
349
|
-
const targetZ =
|
|
365
|
+
const targetZ = z ? z : 0;
|
|
350
366
|
// since we don't know the correct projected z value for the point,
|
|
351
367
|
// unproject two points to get a line and then find the point on that
|
|
352
368
|
// line with z=0
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {describe, expect, test} from 'vitest';
|
|
2
|
+
import Point from '@mapbox/point-geometry';
|
|
3
|
+
import {LngLat} from '../lng_lat.ts';
|
|
4
|
+
import {GlobeTransform} from './globe_transform.ts';
|
|
5
|
+
import {VerticalPerspectiveCameraHelper} from './vertical_perspective_camera_helper.ts';
|
|
6
|
+
import {type MapControlsDeltas} from './camera_helper.ts';
|
|
7
|
+
|
|
8
|
+
describe('VerticalPerspectiveCameraHelper.handleMapControlsPan', () => {
|
|
9
|
+
test('preserves bearing away from the poles', () => {
|
|
10
|
+
const tr = new GlobeTransform();
|
|
11
|
+
tr.resize(512, 512);
|
|
12
|
+
tr.setZoom(4);
|
|
13
|
+
tr.setCenter(new LngLat(0, 20));
|
|
14
|
+
tr.setTransitionState(1);
|
|
15
|
+
const helper = new VerticalPerspectiveCameraHelper();
|
|
16
|
+
|
|
17
|
+
helper.handleMapControlsPan(
|
|
18
|
+
{panDelta: new Point(50, 30), around: tr.centerPoint} as MapControlsDeltas,
|
|
19
|
+
tr, tr.center);
|
|
20
|
+
|
|
21
|
+
expect(tr.bearing).toBe(0);
|
|
22
|
+
expect(tr.center.lat).not.toBe(20);
|
|
23
|
+
});
|
|
24
|
+
});
|