maplibre-gl 6.2.0 → 6.3.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.
Files changed (124) hide show
  1. package/dist/maplibre-gl-dev.mjs +522 -223
  2. package/dist/maplibre-gl-dev.mjs.map +1 -1
  3. package/dist/maplibre-gl-shared-dev.mjs +103 -58
  4. package/dist/maplibre-gl-shared-dev.mjs.map +1 -1
  5. package/dist/maplibre-gl-shared.mjs +2 -2
  6. package/dist/maplibre-gl-shared.mjs.map +1 -1
  7. package/dist/maplibre-gl-worker-dev.mjs +23 -49
  8. package/dist/maplibre-gl-worker-dev.mjs.map +1 -1
  9. package/dist/maplibre-gl-worker.mjs +2 -2
  10. package/dist/maplibre-gl-worker.mjs.map +1 -1
  11. package/dist/maplibre-gl.css +1 -1
  12. package/dist/maplibre-gl.d.ts +214 -59
  13. package/dist/maplibre-gl.mjs +44 -44
  14. package/dist/maplibre-gl.mjs.map +1 -1
  15. package/package.json +10 -20
  16. package/src/data/feature_index.bench.ts +21 -0
  17. package/src/data/segment.test.ts +2 -2
  18. package/src/geo/edge_insets.test.ts +1 -1
  19. package/src/geo/lng_lat.test.ts +7 -7
  20. package/src/geo/lng_lat_bounds.test.ts +4 -4
  21. package/src/geo/mercator_coordinate.test.ts +2 -2
  22. package/src/geo/projection/camera_helper.ts +5 -0
  23. package/src/geo/projection/globe_transform.test.ts +25 -0
  24. package/src/geo/projection/globe_transform.ts +7 -3
  25. package/src/geo/projection/mercator_camera_helper.ts +1 -1
  26. package/src/geo/projection/mercator_transform.ts +21 -5
  27. package/src/geo/projection/vertical_perspective_camera_helper.ts +60 -31
  28. package/src/geo/projection/vertical_perspective_transform.ts +7 -1
  29. package/src/geo/transform_interface.ts +13 -1
  30. package/src/index.ts +7 -2
  31. package/src/render/image_atlas.test.ts +72 -0
  32. package/src/render/image_atlas.ts +17 -2
  33. package/src/render/image_manager.test.ts +46 -0
  34. package/src/render/image_manager.ts +9 -2
  35. package/src/render/painter.ts +1 -12
  36. package/src/render/update_pattern_positions_in_program.test.ts +1 -1
  37. package/src/shaders/glsl/_prelude.vertex.glsl +2 -3
  38. package/src/shaders/glsl/_prelude.vertex.glsl.g.ts +1 -1
  39. package/src/shaders/glsl/circle.vertex.glsl +4 -4
  40. package/src/shaders/glsl/circle.vertex.glsl.g.ts +1 -1
  41. package/src/shaders/glsl/fill_extrusion.vertex.glsl +3 -3
  42. package/src/shaders/glsl/fill_extrusion.vertex.glsl.g.ts +1 -1
  43. package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl +5 -5
  44. package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl.g.ts +1 -1
  45. package/src/shaders/glsl/heatmap.vertex.glsl +4 -4
  46. package/src/shaders/glsl/heatmap.vertex.glsl.g.ts +1 -1
  47. package/src/shaders/glsl/line.vertex.glsl +7 -7
  48. package/src/shaders/glsl/line.vertex.glsl.g.ts +1 -1
  49. package/src/shaders/glsl/line_gradient.vertex.glsl +6 -6
  50. package/src/shaders/glsl/line_gradient.vertex.glsl.g.ts +1 -1
  51. package/src/shaders/glsl/line_gradient_sdf.vertex.glsl +7 -7
  52. package/src/shaders/glsl/line_gradient_sdf.vertex.glsl.g.ts +1 -1
  53. package/src/shaders/glsl/line_pattern.vertex.glsl +7 -7
  54. package/src/shaders/glsl/line_pattern.vertex.glsl.g.ts +1 -1
  55. package/src/shaders/glsl/line_sdf.vertex.glsl +7 -7
  56. package/src/shaders/glsl/line_sdf.vertex.glsl.g.ts +1 -1
  57. package/src/shaders/glsl/raster.fragment.glsl +4 -4
  58. package/src/shaders/glsl/raster.fragment.glsl.g.ts +1 -1
  59. package/src/shaders/glsl/raster.vertex.glsl +11 -6
  60. package/src/shaders/glsl/raster.vertex.glsl.g.ts +1 -1
  61. package/src/shaders/glsl/symbol_icon.vertex.glsl +5 -5
  62. package/src/shaders/glsl/symbol_icon.vertex.glsl.g.ts +1 -1
  63. package/src/shaders/glsl/symbol_sdf.vertex.glsl +5 -5
  64. package/src/shaders/glsl/symbol_sdf.vertex.glsl.g.ts +1 -1
  65. package/src/shaders/glsl/symbol_text_and_icon.vertex.glsl +6 -6
  66. package/src/shaders/glsl/symbol_text_and_icon.vertex.glsl.g.ts +1 -1
  67. package/src/source/canvas_source.test.ts +31 -0
  68. package/src/source/canvas_source.ts +2 -1
  69. package/src/source/geojson_worker_source.test.ts +88 -0
  70. package/src/source/geojson_worker_source.ts +22 -39
  71. package/src/source/image_source.test.ts +205 -7
  72. package/src/source/image_source.ts +118 -4
  73. package/src/source/raster_dem_tile_worker_source.test.ts +1 -1
  74. package/src/source/vector_tile_worker_source.test.ts +94 -2
  75. package/src/source/vector_tile_worker_source.ts +18 -27
  76. package/src/source/video_source.test.ts +12 -8
  77. package/src/source/worker_tile.bench.ts +10 -0
  78. package/src/source/worker_tile.ts +0 -3
  79. package/src/style/create_style_layer.bench.ts +15 -0
  80. package/src/style/properties.bench.ts +81 -0
  81. package/src/style/style.test.ts +11 -10
  82. package/src/style/style_image.ts +67 -9
  83. package/src/style/style_layer.bench.ts +65 -0
  84. package/src/style/style_layer.test.ts +1 -1
  85. package/src/style/style_layer.ts +2 -2
  86. package/src/style/validate_style.bench.ts +11 -0
  87. package/src/style/validate_style.test.ts +2 -2
  88. package/src/symbol/anchor.test.ts +2 -2
  89. package/src/symbol/collision_index.bench.ts +116 -0
  90. package/src/symbol/cross_tile_symbol_index.bench.ts +49 -0
  91. package/src/symbol/shaping.test.ts +2 -1
  92. package/src/symbol/symbol_layout.bench.ts +30 -0
  93. package/src/tile/tile_id.test.ts +1 -1
  94. package/src/tile/tile_manager.ts +2 -2
  95. package/src/ui/control/fullscreen_control.test.ts +10 -10
  96. package/src/ui/control/geolocate_control.test.ts +3 -5
  97. package/src/ui/control/globe_control.test.ts +6 -6
  98. package/src/ui/events.ts +1 -1
  99. package/src/ui/handler/scroll_zoom.test.ts +50 -0
  100. package/src/ui/handler/two_fingers_touch.test.ts +3 -3
  101. package/src/ui/handler_manager.test.ts +243 -3
  102. package/src/ui/handler_manager.ts +79 -16
  103. package/src/ui/map.ts +17 -3
  104. package/src/ui/map_tests/map_cross_window.test.ts +2 -2
  105. package/src/ui/map_tests/map_events.test.ts +3 -3
  106. package/src/ui/map_tests/map_images.test.ts +29 -0
  107. package/src/ui/map_tests/map_style.test.ts +9 -8
  108. package/src/ui/map_tests/map_webgl.test.ts +29 -0
  109. package/src/ui/marker.test.ts +26 -42
  110. package/src/ui/popup.test.ts +14 -14
  111. package/src/util/evented.test.ts +33 -28
  112. package/src/util/evented.ts +69 -43
  113. package/src/util/test/null_gl.ts +10 -0
  114. package/src/util/test/util.ts +2 -1
  115. package/src/util/util.test.ts +1 -1
  116. package/src/util/web_worker_transfer.test.ts +5 -5
  117. package/src/webgl/context.ts +20 -0
  118. package/src/webgl/draw/draw_raster.ts +11 -6
  119. package/src/webgl/program/raster_program.ts +11 -0
  120. package/src/webgl/program.ts +31 -16
  121. package/src/webgl/texture.test.ts +16 -1
  122. package/src/webgl/texture.ts +3 -0
  123. package/src/webgl/vertex_buffer.test.ts +17 -2
  124. package/src/webgl/vertex_buffer.ts +24 -13
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "maplibre-gl",
3
3
  "description": "BSD licensed community fork of mapbox-gl, a WebGL interactive maps library",
4
- "version": "6.2.0",
4
+ "version": "6.3.0",
5
5
  "module": "dist/maplibre-gl.mjs",
6
6
  "style": "dist/maplibre-gl.css",
7
7
  "license": "BSD-3-Clause",
@@ -51,7 +51,6 @@
51
51
  "@mapbox/mapbox-gl-rtl-text": "^0.4.0",
52
52
  "@mapbox/mvt-fixtures": "^4.0.0",
53
53
  "@stylistic/eslint-plugin": "^5.10.0",
54
- "@types/benchmark": "^2.1.5",
55
54
  "@types/d3": "^7.4.3",
56
55
  "@types/earcut": "^3.0.0",
57
56
  "@types/eslint": "^9.6.1",
@@ -64,35 +63,32 @@
64
63
  "@types/offscreencanvas": "^2019.7.3",
65
64
  "@types/pixelmatch": "^5.2.6",
66
65
  "@types/pngjs": "^6.0.5",
67
- "@types/react": "^19.2.18",
68
- "@types/react-dom": "^19.2.4",
69
66
  "@types/request": "^2.48.13",
70
67
  "@types/shuffle-seed": "^1.1.3",
71
68
  "@types/window-or-global": "^1.0.6",
72
- "@typescript-eslint/eslint-plugin": "^8.65.0",
69
+ "@typescript-eslint/eslint-plugin": "^8.66.0",
73
70
  "@typescript-eslint/parser": "^8.59.3",
74
71
  "@typescript/native-preview": "beta",
75
72
  "@unicode/unicode-17.0.0": "^1.6.17",
76
73
  "@vitest/coverage-v8": "4.1.10",
77
- "@vitest/eslint-plugin": "^1.6.24",
74
+ "@vitest/eslint-plugin": "^1.6.26",
78
75
  "@vitest/ui": "4.1.10",
79
76
  "address": "^2.0.3",
80
77
  "autoprefixer": "^10.5.4",
81
- "benchmark": "^2.1.4",
82
78
  "canvas": "^3.2.3",
83
79
  "cross-env": "^10.1.0",
84
80
  "cspell": "^10.0.1",
85
- "cssnano": "^8.0.2",
81
+ "cssnano": "^8.0.4",
86
82
  "d3": "^7.9.0",
87
83
  "d3-queue": "^3.0.7",
88
- "devtools-protocol": "^0.0.1672245",
84
+ "devtools-protocol": "^0.0.1674729",
89
85
  "diff": "^9.0.0",
90
86
  "eslint": "^10.8.0",
91
87
  "eslint-plugin-html": "^8.1.4",
92
88
  "eslint-plugin-tsdoc": "0.5.2",
93
89
  "expect": "^30.4.1",
94
90
  "glob": "^13.0.6",
95
- "globals": "^17.8.0",
91
+ "globals": "^17.9.0",
96
92
  "is-builtin-module": "^5.0.0",
97
93
  "jsdom": "^30.0.1",
98
94
  "minimist": "^1.2.8",
@@ -108,11 +104,9 @@
108
104
  "postcss-cli": "^11.0.1",
109
105
  "postcss-inline-svg": "^6.0.0",
110
106
  "pretty-bytes": "^7.1.1",
111
- "puppeteer": "^25.4.0",
112
- "react": "^19.2.8",
113
- "react-dom": "^19.2.8",
107
+ "puppeteer": "^25.5.0",
114
108
  "regenerate": "^1.4.2",
115
- "rolldown": "^1.2.1",
109
+ "rolldown": "^1.2.3",
116
110
  "rolldown-plugin-dts": "^0.27.8",
117
111
  "rw": "^1.3.3",
118
112
  "semver": "^7.8.5",
@@ -125,7 +119,7 @@
125
119
  "typedoc": "^0.28.20",
126
120
  "typedoc-plugin-markdown": "^4.11.0",
127
121
  "typescript": "^6.0.3",
128
- "vite": "^8.2.0",
122
+ "vite": "^8.2.1",
129
123
  "vitest": "^4.1.5",
130
124
  "vitest-webgl-canvas-mock": "^1.1.0"
131
125
  },
@@ -143,14 +137,11 @@
143
137
  "build-prod": "cross-env BUILD=production rolldown -c rolldown.config.ts",
144
138
  "build-css": "postcss -o dist/maplibre-gl.css src/css/maplibre-gl.css",
145
139
  "watch-css": "postcss --watch -o dist/maplibre-gl.css src/css/maplibre-gl.css",
146
- "build-benchmarks": "npm run build-dev && rolldown -c test/bench/rolldown_config_benchmarks.ts",
147
- "watch-benchmarks": "rolldown -c test/bench/rolldown_config_benchmarks.ts --watch",
148
140
  "spellcheck": "cspell",
149
141
  "docs": "npm run generate-docs && docker compose run --rm docs",
150
142
  "start-server": "st --no-cache -H localhost --port 9966 .",
151
143
  "start-docs": "docker compose run --rm -it --service-ports docs-serve",
152
144
  "start": "run-p watch-css watch-dev start-server",
153
- "start-bench": "run-p watch-css watch-benchmarks start-server",
154
145
  "lint": "eslint",
155
146
  "lint-css": "stylelint **/*.css verbose",
156
147
  "test": "run-p lint lint-css test-render test-unit test-integration test-build",
@@ -164,8 +155,7 @@
164
155
  "test-render": "vitest test/integration/render/render.test.ts",
165
156
  "codegen": "run-p --print-label generate-style-code generate-unicode-data generate-struct-arrays generate-shaders && npm run generate-typings",
166
157
  "bench": "vitest bench --run --config vitest.config.bench.ts",
167
- "benchmark": "node test/bench/run-benchmarks.ts",
168
- "gl-stats": "node test/bench/gl-stats.ts",
158
+ "bench-e2e": "node test/bench/e2e/run.ts",
169
159
  "prepare": "npm run codegen",
170
160
  "typecheck": "tsgo --noEmit && tsgo --project tsconfig.dist.json"
171
161
  },
@@ -0,0 +1,21 @@
1
+ import {bench} from 'vitest';
2
+ import {FeatureIndex} from './feature_index.ts';
3
+ import {OverscaledTileID} from '../tile/tile_id.ts';
4
+
5
+ import type {FeatureFilter} from '@maplibre/maplibre-gl-style-spec';
6
+ import type {VectorTileFeatureLike, VectorTileLayerLike} from '@maplibre/vt-pbf';
7
+
8
+ const layerCount = 100;
9
+ const filter = {needGeometry: false} as FeatureFilter;
10
+
11
+ const featureIndex = new FeatureIndex(new OverscaledTileID(0, 0, 0, 0, 0));
12
+ featureIndex.bucketLayerIDs = [Array.from({length: layerCount}, (_, i) => `layer-${i}`)];
13
+ featureIndex.vtLayers = {
14
+ '0': {feature: () => ({} as VectorTileFeatureLike)} as unknown as VectorTileLayerLike
15
+ };
16
+
17
+ const layerIdsToTest = new Set(Array.from({length: layerCount}, (_, i) => `non-existing-layer-${i}`));
18
+
19
+ bench('FeatureIndex.loadMatchingFeature', () => {
20
+ featureIndex.loadMatchingFeature({}, 0, 0, 0, filter, layerIdsToTest, [], {}, {});
21
+ });
@@ -4,13 +4,13 @@ import {SegmentVector} from './segment.ts';
4
4
 
5
5
  describe('SegmentVector', () => {
6
6
  test('constructor', () => {
7
- expect(new SegmentVector() instanceof SegmentVector).toBeTruthy();
7
+ expect(new SegmentVector()).toBeInstanceOf(SegmentVector);
8
8
  });
9
9
 
10
10
  test('simpleSegment', () => {
11
11
  SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16;
12
12
  const segmentVector = SegmentVector.simpleSegment(0, 0, 10, 0);
13
- expect(segmentVector instanceof SegmentVector).toBeTruthy();
13
+ expect(segmentVector).toBeInstanceOf(SegmentVector);
14
14
  expect(segmentVector.segments).toHaveLength(1);
15
15
  expect(segmentVector.segments[0].vertexLength).toBe(10);
16
16
  });
@@ -4,7 +4,7 @@ import {EdgeInsets} from '../geo/edge_insets.ts';
4
4
  describe('EdgeInsets', () => {
5
5
  describe('constructor', () => {
6
6
  test('creates an object with default values', () => {
7
- expect(new EdgeInsets() instanceof EdgeInsets).toBeTruthy();
7
+ expect(new EdgeInsets()).toBeInstanceOf(EdgeInsets);
8
8
  });
9
9
 
10
10
  test('invalid initialization', () => {
@@ -3,7 +3,7 @@ import {LngLat} from '../geo/lng_lat.ts';
3
3
 
4
4
  describe('LngLat', () => {
5
5
  test('constructor', () => {
6
- expect(new LngLat(0, 0) instanceof LngLat).toBeTruthy();
6
+ expect(new LngLat(0, 0)).toBeInstanceOf(LngLat);
7
7
 
8
8
  expect(() => {
9
9
  new LngLat(0, -91);
@@ -15,12 +15,12 @@ describe('LngLat', () => {
15
15
  });
16
16
 
17
17
  test('convert', () => {
18
- expect(LngLat.convert([0, 10]) instanceof LngLat).toBeTruthy();
19
- expect(LngLat.convert({lng: 0, lat: 10}) instanceof LngLat).toBeTruthy();
20
- expect(LngLat.convert({lng: 0, lat: 0}) instanceof LngLat).toBeTruthy();
21
- expect(LngLat.convert({lon: 0, lat: 10}) instanceof LngLat).toBeTruthy();
22
- expect(LngLat.convert({lon: 0, lat: 0}) instanceof LngLat).toBeTruthy();
23
- expect(LngLat.convert(new LngLat(0, 0)) instanceof LngLat).toBeTruthy();
18
+ expect(LngLat.convert([0, 10])).toBeInstanceOf(LngLat);
19
+ expect(LngLat.convert({lng: 0, lat: 10})).toBeInstanceOf(LngLat);
20
+ expect(LngLat.convert({lng: 0, lat: 0})).toBeInstanceOf(LngLat);
21
+ expect(LngLat.convert({lon: 0, lat: 10})).toBeInstanceOf(LngLat);
22
+ expect(LngLat.convert({lon: 0, lat: 0})).toBeInstanceOf(LngLat);
23
+ expect(LngLat.convert(new LngLat(0, 0))).toBeInstanceOf(LngLat);
24
24
  });
25
25
 
26
26
  test('wrap', () => {
@@ -336,25 +336,25 @@ describe('LngLatBounds', () => {
336
336
  test('point is in bounds', () => {
337
337
  const llb = new LngLatBounds([-1, -1], [1, 1]);
338
338
  const ll = {lng: 0, lat: 0};
339
- expect(llb.contains(ll)).toBeTruthy();
339
+ expect(llb.contains(ll)).toBe(true);
340
340
  });
341
341
 
342
342
  test('point is not in bounds', () => {
343
343
  const llb = new LngLatBounds([-1, -1], [1, 1]);
344
344
  const ll = {lng: 3, lat: 3};
345
- expect(llb.contains(ll)).toBeFalsy();
345
+ expect(llb.contains(ll)).toBe(false);
346
346
  });
347
347
 
348
348
  test('point is in bounds that spans dateline', () => {
349
349
  const llb = new LngLatBounds([190, -10], [170, 10]);
350
350
  const ll = {lng: 180, lat: 0};
351
- expect(llb.contains(ll)).toBeTruthy();
351
+ expect(llb.contains(ll)).toBe(true);
352
352
  });
353
353
 
354
354
  test('point is not in bounds that spans dateline', () => {
355
355
  const llb = new LngLatBounds([190, -10], [170, 10]);
356
356
  const ll = {lng: 0, lat: 0};
357
- expect(llb.contains(ll)).toBeFalsy();
357
+ expect(llb.contains(ll)).toBe(false);
358
358
  });
359
359
  });
360
360
  });
@@ -4,8 +4,8 @@ import {MercatorCoordinate, mercatorScale} from './mercator_coordinate.ts';
4
4
 
5
5
  describe('LngLat', () => {
6
6
  test('constructor', () => {
7
- expect(new MercatorCoordinate(0, 0) instanceof MercatorCoordinate).toBeTruthy();
8
- expect(new MercatorCoordinate(0, 0, 0) instanceof MercatorCoordinate).toBeTruthy();
7
+ expect(new MercatorCoordinate(0, 0)).toBeInstanceOf(MercatorCoordinate);
8
+ expect(new MercatorCoordinate(0, 0, 0)).toBeInstanceOf(MercatorCoordinate);
9
9
  });
10
10
 
11
11
  test('fromLngLat', () => {
@@ -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 = {
@@ -400,6 +400,31 @@ describe('GlobeTransform', () => {
400
400
  expect(globeTransform.center.lat).toBeCloseTo(20.659450722109348, precisionDigits);
401
401
  });
402
402
  });
403
+
404
+ test('ignores the elevation parameter when rendering the globe', () => {
405
+ const transform = createGlobeTransform();
406
+ transform.setZoom(1);
407
+ coords = new LngLat(5, 10);
408
+ point = new Point(320, 240);
409
+ transform.setLocationAtPoint(coords, point, 1000);
410
+ unprojected = transform.screenPointToLocationAtElevation(point, 1000);
411
+ expect(unprojected.lng).toBeCloseTo(coords.lng, precisionDigits);
412
+ expect(unprojected.lat).toBeCloseTo(coords.lat, precisionDigits);
413
+ });
414
+
415
+ test('solves on the plane at the given elevation when rendering mercator', () => {
416
+ const transform = createGlobeTransform();
417
+ transform.setZoom(5);
418
+ transform.setPitch(40);
419
+ transform.setTransitionState(0); // rendering mercator
420
+ coords = new LngLat(5, 10);
421
+ point = new Point(320, 200);
422
+ transform.setLocationAtPoint(coords, point, 500);
423
+ unprojected = transform.screenPointToLocationAtElevation(point, 500);
424
+ // exact up to the center-latitude change the solve itself causes (~1e-5 deg)
425
+ expect(unprojected.lng).toBeCloseTo(coords.lng, 3);
426
+ expect(unprojected.lat).toBeCloseTo(coords.lat, 3);
427
+ });
403
428
  });
404
429
  });
405
430
 
@@ -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
  }
@@ -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
- setLocationAtPoint(lnglat: LngLat, point: Point): void {
312
- const z = mercatorZfromAltitude(this.elevation, this.center.lat);
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, z);
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
- screenPointToMercatorCoordinateAtZ(p: Point, mercatorZ?: number): MercatorCoordinate {
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 = mercatorZ ? mercatorZ : 0;
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
@@ -2,7 +2,7 @@ import Point from '@mapbox/point-geometry';
2
2
  import {cameraBoundsWarning, type CameraForBoxAndBearingHandlerResult, type EaseToHandlerResult, type EaseToHandlerOptions, type FlyToHandlerResult, type FlyToHandlerOptions, type ICameraHelper, type MapControlsDeltas, updateRotation, cameraForBoxAndBearing} from './camera_helper.ts';
3
3
  import {LngLat, type LngLatLike} from '../lng_lat.ts';
4
4
  import {angularCoordinatesToSurfaceVector, computeGlobePanCenter, getGlobeRadiusPixels, getZoomAdjustment, globeDistanceOfLocationsPixels, interpolateLngLatForGlobe} from './globe_utils.ts';
5
- import {clamp, createVec3f64, differenceOfAnglesDegrees, MAX_VALID_LATITUDE, remapSaturate, rollPitchBearingEqual, scaleZoom, warnOnce, zoomScale} from '../../util/util.ts';
5
+ import {clamp, createVec3f64, differenceOfAnglesDegrees, lerp, MAX_VALID_LATITUDE, remapSaturate, rollPitchBearingEqual, scaleZoom, warnOnce, zoomScale} from '../../util/util.ts';
6
6
  import {type mat4, vec3} from 'gl-matrix';
7
7
  import {normalizeCenter} from '../transform_helper.ts';
8
8
  import {interpolates} from '@maplibre/maplibre-gl-style-spec';
@@ -12,6 +12,45 @@ import type {CameraForBoundsOptions} from '../../ui/camera.ts';
12
12
  import type {LngLatBounds} from '../lng_lat_bounds.ts';
13
13
  import type {PaddingOptions} from '../edge_insets.ts';
14
14
 
15
+ /**
16
+ * Zoom movement slowing starts when the mouse ray passes further than this above the planet surface,
17
+ * so a cursor off the globe does not move the map unnaturally. Globe radius is 1, so 0.3 is ~2000 km.
18
+ */
19
+ const RAY_SURFACE_DISTANCE_FOR_SLOWING_START = 0.3;
20
+
21
+ /** How sharply zoom movement slows as the mouse ray rises above the planet surface. Lower is more gradual. */
22
+ const SLOWING_MULTIPLIER = 0.5;
23
+
24
+ /** Longitude difference between zoom location and map center at which the blend from exact to heuristic zooming starts, in degrees. */
25
+ const INTERPOLATE_TO_HEURISTIC_START_LNG = 45;
26
+
27
+ /** Longitude difference at which the blend to heuristic zooming is complete, in degrees. */
28
+ const INTERPOLATE_TO_HEURISTIC_END_LNG = 85;
29
+
30
+ /** Exponent applied to the blend factor: below 1, so the blend leans towards heuristic zooming and flattens as it completes. */
31
+ const INTERPOLATE_TO_HEURISTIC_EXPONENT = 0.25;
32
+
33
+ /**
34
+ * Distance of the mouse ray from the globe center at which the blend from exact to heuristic zooming
35
+ * starts. Globe radius is 1, so 1 is a ray grazing the horizon.
36
+ */
37
+ const INTERPOLATE_TO_HEURISTIC_START_HORIZON = 0.95;
38
+
39
+ /** Ray distance at which the blend to heuristic zooming is complete. */
40
+ const INTERPOLATE_TO_HEURISTIC_END_HORIZON = 0.999;
41
+
42
+ /**
43
+ * Globe radius relative to the smaller viewport dimension below which zoom movement near the horizon
44
+ * starts being inhibited, avoiding unnatural movements when the map is zoomed out a lot.
45
+ */
46
+ const SLOWING_RADIUS_START = 0.9;
47
+
48
+ /** Globe radius relative to the smaller viewport dimension at which that inhibition is at full strength. */
49
+ const SLOWING_RADIUS_STOP = 0.5;
50
+
51
+ /** Fraction of the zoom movement that remains once the globe has shrunk to `SLOWING_RADIUS_STOP`. */
52
+ const SLOWING_RADIUS_SLOW_FACTOR = 0.25;
53
+
15
54
  /**
16
55
  * @internal
17
56
  */
@@ -36,6 +75,13 @@ export class VerticalPerspectiveCameraHelper implements ICameraHelper {
36
75
  };
37
76
  }
38
77
 
78
+ /**
79
+ * Zooms around the pointer.
80
+ *
81
+ * `setLocationAtPoint` is exact but degenerates when called repeatedly for a
82
+ * location whose longitude is far from the center's, or one near the horizon,
83
+ * so those cases blend towards a heuristic.
84
+ */
39
85
  handleMapControlsRollPitchBearingZoom(deltas: MapControlsDeltas, tr: ITransform): void {
40
86
  const zoomPixel = deltas.around;
41
87
  const zoomLoc = tr.screenPointToLocation(zoomPixel);
@@ -51,28 +97,6 @@ export class VerticalPerspectiveCameraHelper implements ICameraHelper {
51
97
  return;
52
98
  }
53
99
 
54
- // Problem: `setLocationAtPoint` for globe works when it is called a single time, but is a little glitchy in practice when used repeatedly for zooming.
55
- // - `setLocationAtPoint` repeatedly called at a location behind a pole will eventually glitch out
56
- // - `setLocationAtPoint` at location the longitude of which is more than 90° different from current center will eventually glitch out
57
- // But otherwise works fine at higher zooms, or when the target is somewhat near the current map center.
58
- // Solution: use a heuristic zooming in the problematic cases and interpolate to `setLocationAtPoint` when possible.
59
-
60
- // Magic numbers that control:
61
- // - when zoom movement slowing starts for cursor not on globe (avoid unnatural map movements)
62
- // - when we interpolate from exact zooming to heuristic zooming based on longitude difference of target location to current center
63
- // - when we interpolate from exact zooming to heuristic zooming based on globe being too small on screen
64
- // - when zoom movement slowing starts for globe being too small on viewport (avoids unnatural/unwanted map movements when map is zoomed out a lot)
65
- const raySurfaceDistanceForSlowingStart = 0.3; // Zoom movement slowing will start when the planet surface to ray distance is greater than this number (globe radius is 1, so 0.3 is ~2000km form the surface).
66
- const slowingMultiplier = 0.5; // The lower this value, the slower will the "zoom movement slowing" occur.
67
- const interpolateToHeuristicStartLng = 45; // When zoom location longitude is this many degrees away from map center, we start interpolating from exact zooming to heuristic zooming.
68
- const interpolateToHeuristicEndLng = 85; // Longitude difference at which interpolation to heuristic zooming ends.
69
- const interpolateToHeuristicExponent = 0.25; // Makes interpolation smoother.
70
- const interpolateToHeuristicStartRadius = 0.75; // When globe is this many times larger than the smaller viewport dimension, we start interpolating from exact zooming to heuristic zooming.
71
- const interpolateToHeuristicEndRadius = 0.35; // Globe size at which interpolation to heuristic zooming ends.
72
- const slowingRadiusStart = 0.9; // If globe is this many times larger than the smaller viewport dimension, start inhibiting map movement while zooming
73
- const slowingRadiusStop = 0.5;
74
- const slowingRadiusSlowFactor = 0.25; // How much is movement slowed when globe is too small
75
-
76
100
  const dLngRaw = differenceOfAnglesDegrees(tr.center.lng, zoomLoc.lng);
77
101
  const dLng = dLngRaw / (Math.abs(dLngRaw / 180) + 1.0); // This gradually reduces the amount of longitude change if the zoom location is very far, eg. on the other side of the pole (possible when looking at a pole).
78
102
  const dLat = differenceOfAnglesDegrees(tr.center.lat, zoomLoc.lat);
@@ -87,15 +111,21 @@ export class VerticalPerspectiveCameraHelper implements ICameraHelper {
87
111
  rayDirection[1] * distanceToClosestPoint,
88
112
  rayDirection[2] * distanceToClosestPoint
89
113
  ]);
90
- const distanceFromSurface = vec3.length(closestPoint) - 1;
91
- const distanceFactor = Math.exp(-Math.max(distanceFromSurface - raySurfaceDistanceForSlowingStart, 0) * slowingMultiplier);
114
+ const rayDistanceFromGlobeCenter = vec3.length(closestPoint); // 0 for a ray straight under the camera, 1 for one grazing the horizon.
115
+ const distanceFromSurface = rayDistanceFromGlobeCenter - 1;
116
+ const distanceFactor = Math.exp(-Math.max(distanceFromSurface - RAY_SURFACE_DISTANCE_FOR_SLOWING_START, 0) * SLOWING_MULTIPLIER);
117
+
118
+ // Near the horizon a pixel is worth degrees of arc and `setLocationAtPoint` can have no solution at all.
119
+ const interpolationFactorHorizon = remapSaturate(rayDistanceFromGlobeCenter, INTERPOLATE_TO_HEURISTIC_START_HORIZON, INTERPOLATE_TO_HEURISTIC_END_HORIZON, 0, 1);
92
120
 
93
121
  // Slow zoom movement down if the globe is too small on viewport
94
- const radius = getGlobeRadiusPixels(tr.worldSize, tr.center.lat) / Math.min(tr.width, tr.height); // Radius relative to larger viewport dimension
95
- const radiusFactor = remapSaturate(radius, slowingRadiusStart, slowingRadiusStop, 1.0, slowingRadiusSlowFactor);
122
+ const radius = getGlobeRadiusPixels(tr.worldSize, tr.center.lat) / Math.min(tr.width, tr.height); // Radius relative to smaller viewport dimension
123
+ const radiusFactor = remapSaturate(radius, SLOWING_RADIUS_START, SLOWING_RADIUS_STOP, 1.0, SLOWING_RADIUS_SLOW_FACTOR);
124
+ // Globe size only stands in for how close the zoom location is to the horizon, so apply the slowdown where that is actually the case.
125
+ const slowingFactor = Math.min(distanceFactor, lerp(1.0, radiusFactor, interpolationFactorHorizon));
96
126
 
97
127
  // Compute how much to move towards the zoom location
98
- const factor = (1.0 - zoomScale(-actualZoomDelta)) * Math.min(distanceFactor, radiusFactor);
128
+ const factor = (1.0 - zoomScale(-actualZoomDelta)) * slowingFactor;
99
129
 
100
130
  const oldCenterLat = tr.center.lat;
101
131
  const oldZoom = tr.zoom;
@@ -109,9 +139,8 @@ export class VerticalPerspectiveCameraHelper implements ICameraHelper {
109
139
  const exactCenter = tr.center;
110
140
 
111
141
  // Interpolate between exact zooming and heuristic zooming depending on the longitude difference between current center and zoom location.
112
- const interpolationFactorLongitude = remapSaturate(Math.abs(dLngRaw), interpolateToHeuristicStartLng, interpolateToHeuristicEndLng, 0, 1);
113
- const interpolationFactorRadius = remapSaturate(radius, interpolateToHeuristicStartRadius, interpolateToHeuristicEndRadius, 0, 1);
114
- const heuristicFactor = Math.pow(Math.max(interpolationFactorLongitude, interpolationFactorRadius), interpolateToHeuristicExponent);
142
+ const interpolationFactorLongitude = remapSaturate(Math.abs(dLngRaw), INTERPOLATE_TO_HEURISTIC_START_LNG, INTERPOLATE_TO_HEURISTIC_END_LNG, 0, 1);
143
+ const heuristicFactor = Math.pow(Math.max(interpolationFactorLongitude, interpolationFactorHorizon), INTERPOLATE_TO_HEURISTIC_EXPONENT);
115
144
 
116
145
  const lngExactToHeuristic = differenceOfAnglesDegrees(exactCenter.lng, heuristicCenter.lng);
117
146
  const latExactToHeuristic = differenceOfAnglesDegrees(exactCenter.lat, heuristicCenter.lat);
@@ -662,7 +662,8 @@ export class VerticalPerspectiveTransform implements ITransform {
662
662
  * Note: automatically adjusts zoom to keep planet size consistent
663
663
  * (same size before and after a {@link setLocationAtPoint} call).
664
664
  */
665
- setLocationAtPoint(lnglat: LngLat, point: Point): void {
665
+ setLocationAtPoint(lnglat: LngLat, point: Point, _elevation?: number): void {
666
+ // The elevation is ignored: this transform solves on the planet's surface.
666
667
  // This returns some fake coordinates for pixels that do not lie on the planet.
667
668
  // Whatever uses this `setLocationAtPoint` function will need to account for that.
668
669
  const pointLngLat = this.unprojectScreenPoint(point);
@@ -801,6 +802,11 @@ export class VerticalPerspectiveTransform implements ITransform {
801
802
  return this.screenPointToMercatorCoordinate(p, terrain)?.toLngLat();
802
803
  }
803
804
 
805
+ screenPointToLocationAtElevation(p: Point, _elevation: number): LngLat {
806
+ // No flat ground plane to intersect at an elevation: use the planet surface.
807
+ return this.screenPointToLocation(p);
808
+ }
809
+
804
810
  isPointOnMapSurface(p: Point, _terrain?: Terrain): boolean {
805
811
  const rayOrigin = this._cameraPosition;
806
812
  const rayDirection = this.getRayDirectionFromPixel(p);
@@ -204,8 +204,10 @@ interface ITransformMutators {
204
204
  * Set's the transform's center so that the given point on screen is at the given world coordinates.
205
205
  * @param lnglat - Desired world coordinates of the point.
206
206
  * @param point - The screen point that should lie at the given coordinates.
207
+ * @param elevation - Optional ground elevation in meters above sea level at `lnglat`,
208
+ * defaults to the elevation at the map's center. Ignored when rendering the globe.
207
209
  */
208
- setLocationAtPoint(lnglat: LngLat, point: Point): void;
210
+ setLocationAtPoint(lnglat: LngLat, point: Point, elevation?: number): void;
209
211
 
210
212
  /**
211
213
  * Sets or clears the map's geographical constraints.
@@ -331,6 +333,16 @@ export interface IReadonlyTransform extends ITransformGetters {
331
333
  */
332
334
  screenPointToLocation(p: Point, terrain?: Terrain): LngLat;
333
335
 
336
+ /**
337
+ * @internal
338
+ * Given a point on screen, return its LngLat location assuming the ground there
339
+ * lies at the given elevation. When rendering the globe the elevation is ignored.
340
+ * @param p - screen point
341
+ * @param elevation - ground elevation in meters above sea level
342
+ * @returns lnglat location
343
+ */
344
+ screenPointToLocationAtElevation(p: Point, elevation: number): LngLat;
345
+
334
346
  /**
335
347
  * @internal
336
348
  * Given a point on screen, return its mercator coordinate.
package/src/index.ts CHANGED
@@ -15,7 +15,7 @@ import {LngLat, type LngLatLike} from './geo/lng_lat.ts';
15
15
  import {LngLatBounds, type LngLatBoundsLike} from './geo/lng_lat_bounds.ts';
16
16
  import Point from '@mapbox/point-geometry';
17
17
  import {MercatorCoordinate} from './geo/mercator_coordinate.ts';
18
- import {Evented, ErrorEvent, Event, type Listener} from './util/evented.ts';
18
+ import {Evented, ErrorEvent, Event, type ErrorEventType, type EventedParentData, type EventTypeMap, type Listener} from './util/evented.ts';
19
19
  import {type AddProtocolAction, config} from './util/config.ts';
20
20
  import {rtlMainThreadPluginFactory} from './source/rtl_text_plugin_main_thread.ts';
21
21
  import {now, setNow, restoreNow, isTimeFrozen} from './util/time_control.ts';
@@ -57,7 +57,7 @@ import type {Handler, HandlerResult} from './ui/handler_manager.ts';
57
57
  import type {Complete, Mat4f32, Mat4f64, RequireAtLeastOne, Subscription} from './util/util.ts';
58
58
  import type {CalculateTileZoomFunction, CoveringTilesOptions} from './geo/projection/covering_tiles.ts';
59
59
  import type {TransformConstrainFunction} from './geo/transform_interface.ts';
60
- import type {StyleImage, StyleImageData, StyleImageInterface, StyleImageMetadata, TextFit} from './style/style_image.ts';
60
+ import type {StyleImage, StyleImageData, StyleImageInterface, StyleImageMetadata, StyleImageWebGLData, StyleImageWebGLTarget, TextFit} from './style/style_image.ts';
61
61
  import type {StyleLayer, PaintPropertyEntry} from './style/style_layer.ts';
62
62
  import type {Tile} from './tile/tile.ts';
63
63
  import type {GeoJSONFeatureDiff, GeoJSONFeatureId, GeoJSONSourceDiff} from './source/geojson_source_diff.ts';
@@ -297,6 +297,9 @@ export {
297
297
  type CanonicalTileRange,
298
298
  type Tile,
299
299
  type Listener,
300
+ type EventTypeMap,
301
+ type ErrorEventType,
302
+ type EventedParentData,
300
303
  type Coordinates,
301
304
  type ImageSourceImage,
302
305
  type UpdateImageOptions,
@@ -308,6 +311,8 @@ export {
308
311
  type GeolocateControlOptions,
309
312
  type LogoControlOptions,
310
313
  type StyleImageInterface,
314
+ type StyleImageWebGLData,
315
+ type StyleImageWebGLTarget,
311
316
  type AddLayerObject,
312
317
  type StyleSetterOptions,
313
318
  type CameraForBoundsOptions,