maplibre-gl 2.1.8 → 2.2.0-pre.2

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 (121) hide show
  1. package/build/generate-style-spec.ts +2 -0
  2. package/dist/maplibre-gl-csp-worker.js +1 -1
  3. package/dist/maplibre-gl-csp-worker.js.map +1 -1
  4. package/dist/maplibre-gl-csp.js +1 -1
  5. package/dist/maplibre-gl-csp.js.map +1 -1
  6. package/dist/maplibre-gl-dev.js +1517 -290
  7. package/dist/maplibre-gl.css +1 -1
  8. package/dist/maplibre-gl.d.ts +3476 -3112
  9. package/dist/maplibre-gl.js +4 -4
  10. package/dist/maplibre-gl.js.map +1 -1
  11. package/dist/package.json +1 -1
  12. package/package.json +15 -15
  13. package/src/css/maplibre-gl.css +20 -0
  14. package/src/css/svg/maplibregl-ctrl-terrain.svg +7 -0
  15. package/src/data/bucket/fill_extrusion_attributes.ts +4 -0
  16. package/src/data/bucket/fill_extrusion_bucket.ts +28 -4
  17. package/src/data/dem_data.test.ts +14 -1
  18. package/src/data/dem_data.ts +13 -0
  19. package/src/geo/transform.test.ts +56 -1
  20. package/src/geo/transform.ts +199 -47
  21. package/src/index.ts +2 -0
  22. package/src/render/draw_background.ts +6 -6
  23. package/src/render/draw_circle.ts +6 -2
  24. package/src/render/draw_collision_debug.ts +5 -1
  25. package/src/render/draw_debug.ts +5 -5
  26. package/src/render/draw_fill.ts +5 -2
  27. package/src/render/draw_fill_extrusion.ts +3 -2
  28. package/src/render/draw_heatmap.ts +2 -3
  29. package/src/render/draw_hillshade.ts +8 -7
  30. package/src/render/draw_line.ts +7 -5
  31. package/src/render/draw_raster.ts +8 -6
  32. package/src/render/draw_symbol.test.ts +34 -10
  33. package/src/render/draw_symbol.ts +23 -12
  34. package/src/render/draw_terrain.ts +123 -0
  35. package/src/render/painter.ts +52 -14
  36. package/src/render/program/hillshade_program.ts +7 -2
  37. package/src/render/program/line_program.ts +24 -10
  38. package/src/render/program/program_uniforms.ts +5 -1
  39. package/src/render/program/terrain_program.ts +83 -0
  40. package/src/render/program.ts +29 -5
  41. package/src/render/render_to_texture.test.ts +41 -0
  42. package/src/render/render_to_texture.ts +154 -0
  43. package/src/render/terrain.test.ts +53 -0
  44. package/src/render/terrain.ts +369 -0
  45. package/src/render/vertex_array_object.ts +21 -4
  46. package/src/shaders/_prelude.vertex.glsl +76 -0
  47. package/src/shaders/_prelude.vertex.glsl.g.ts +1 -1
  48. package/src/shaders/circle.fragment.glsl +2 -1
  49. package/src/shaders/circle.fragment.glsl.g.ts +1 -1
  50. package/src/shaders/circle.vertex.glsl +6 -2
  51. package/src/shaders/circle.vertex.glsl.g.ts +1 -1
  52. package/src/shaders/collision_box.vertex.glsl +1 -1
  53. package/src/shaders/collision_box.vertex.glsl.g.ts +1 -1
  54. package/src/shaders/debug.vertex.glsl +1 -1
  55. package/src/shaders/debug.vertex.glsl.g.ts +1 -1
  56. package/src/shaders/fill_extrusion.vertex.glsl +16 -2
  57. package/src/shaders/fill_extrusion.vertex.glsl.g.ts +1 -1
  58. package/src/shaders/fill_extrusion_pattern.vertex.glsl +15 -2
  59. package/src/shaders/fill_extrusion_pattern.vertex.glsl.g.ts +1 -1
  60. package/src/shaders/line.vertex.glsl +7 -3
  61. package/src/shaders/line.vertex.glsl.g.ts +1 -1
  62. package/src/shaders/line_gradient.vertex.glsl +7 -3
  63. package/src/shaders/line_gradient.vertex.glsl.g.ts +1 -1
  64. package/src/shaders/line_pattern.vertex.glsl +7 -3
  65. package/src/shaders/line_pattern.vertex.glsl.g.ts +1 -1
  66. package/src/shaders/line_sdf.vertex.glsl +7 -4
  67. package/src/shaders/line_sdf.vertex.glsl.g.ts +1 -1
  68. package/src/shaders/shaders.ts +11 -1
  69. package/src/shaders/symbol_icon.vertex.glsl +8 -8
  70. package/src/shaders/symbol_icon.vertex.glsl.g.ts +1 -1
  71. package/src/shaders/symbol_sdf.vertex.glsl +8 -5
  72. package/src/shaders/symbol_sdf.vertex.glsl.g.ts +1 -1
  73. package/src/shaders/symbol_text_and_icon.vertex.glsl +8 -5
  74. package/src/shaders/symbol_text_and_icon.vertex.glsl.g.ts +1 -1
  75. package/src/shaders/terrain.fragment.glsl +7 -0
  76. package/src/shaders/terrain.fragment.glsl.g.ts +2 -0
  77. package/src/shaders/terrain.vertex.glsl +12 -0
  78. package/src/shaders/terrain.vertex.glsl.g.ts +2 -0
  79. package/src/shaders/terrain_coords.fragment.glsl +11 -0
  80. package/src/shaders/terrain_coords.fragment.glsl.g.ts +2 -0
  81. package/src/shaders/terrain_depth.fragment.glsl +15 -0
  82. package/src/shaders/terrain_depth.fragment.glsl.g.ts +2 -0
  83. package/src/source/canvas_source.test.ts +1 -1
  84. package/src/source/geojson_source.test.ts +25 -0
  85. package/src/source/geojson_source.ts +1 -8
  86. package/src/source/geojson_worker_source.test.ts +19 -23
  87. package/src/source/geojson_worker_source.ts +19 -70
  88. package/src/source/raster_dem_tile_source.ts +4 -3
  89. package/src/source/raster_dem_tile_worker_source.ts +0 -1
  90. package/src/source/source_cache.test.ts +83 -0
  91. package/src/source/source_cache.ts +72 -11
  92. package/src/source/terrain_source_cache.test.ts +89 -0
  93. package/src/source/terrain_source_cache.ts +201 -0
  94. package/src/source/tile.ts +15 -0
  95. package/src/source/tile_id.ts +9 -0
  96. package/src/style/pauseable_placement.ts +3 -1
  97. package/src/style/style.test.ts +16 -0
  98. package/src/style/style.ts +57 -3
  99. package/src/style/validate_style.ts +2 -0
  100. package/src/style-spec/CHANGELOG.md +6 -0
  101. package/src/style-spec/error/validation_error.ts +1 -1
  102. package/src/style-spec/package.json +2 -2
  103. package/src/style-spec/reference/v8.json +42 -0
  104. package/src/style-spec/types.g.ts +7 -0
  105. package/src/style-spec/validate/validate.ts +2 -0
  106. package/src/style-spec/validate/validate_terrain.test.ts +46 -0
  107. package/src/style-spec/validate/validate_terrain.ts +41 -0
  108. package/src/style-spec/validate_style.min.ts +2 -0
  109. package/src/style-spec/validate_style.ts +1 -0
  110. package/src/symbol/collision_index.ts +28 -12
  111. package/src/symbol/placement.ts +24 -9
  112. package/src/symbol/projection.ts +42 -27
  113. package/src/ui/camera.ts +2 -0
  114. package/src/ui/control/terrain_control.ts +77 -0
  115. package/src/ui/default_locale.ts +3 -2
  116. package/src/ui/events.ts +18 -3
  117. package/src/ui/handler_manager.ts +33 -3
  118. package/src/ui/map.ts +36 -6
  119. package/src/ui/marker.test.ts +21 -0
  120. package/src/ui/marker.ts +14 -0
  121. package/src/util/primitives.ts +14 -11
@@ -101,9 +101,15 @@ function getGlCoordMatrix(posMatrix: mat4,
101
101
  }
102
102
  }
103
103
 
104
- function project(point: Point, matrix: mat4) {
105
- const pos = [point.x, point.y, 0, 1] as vec4;
106
- xyTransformMat4(pos, pos, matrix);
104
+ function project(point: Point, matrix: mat4, getElevation: (x: number, y: number) => number) {
105
+ let pos;
106
+ if (getElevation) { // slow because of handle z-index
107
+ pos = [point.x, point.y, getElevation(point.x, point.y), 1] as vec4;
108
+ vec4.transformMat4(pos, pos, matrix);
109
+ } else { // fast because of ignore z-index
110
+ pos = [point.x, point.y, 0, 1] as vec4;
111
+ xyTransformMat4(pos, pos, matrix);
112
+ }
107
113
  const w = pos[3];
108
114
  return {
109
115
  point: new Point(pos[0] / w, pos[1] / w),
@@ -139,7 +145,8 @@ function updateLineLabels(bucket: SymbolBucket,
139
145
  glCoordMatrix: mat4,
140
146
  pitchWithMap: boolean,
141
147
  keepUpright: boolean,
142
- rotateToLine: boolean) {
148
+ rotateToLine: boolean,
149
+ getElevation: (x: number, y: number) => number) {
143
150
 
144
151
  const sizeData = isText ? bucket.textSizeData : bucket.iconSizeData;
145
152
  const partiallyEvaluatedSize = symbolSize.evaluateSizeForZoom(sizeData, painter.transform.zoom);
@@ -171,8 +178,14 @@ function updateLineLabels(bucket: SymbolBucket,
171
178
  // Awkward... but we're counting on the paired "vertical" symbol coming immediately after its horizontal counterpart
172
179
  useVertical = false;
173
180
 
174
- const anchorPos = [symbol.anchorX, symbol.anchorY, 0, 1] as vec4;
175
- vec4.transformMat4(anchorPos, anchorPos, posMatrix);
181
+ let anchorPos;
182
+ if (getElevation) { // slow because of handle z-index
183
+ anchorPos = [symbol.anchorX, symbol.anchorY, getElevation(symbol.anchorX, symbol.anchorY), 1] as vec4;
184
+ vec4.transformMat4(anchorPos, anchorPos, posMatrix);
185
+ } else { // fast because of ignore z-index
186
+ anchorPos = [symbol.anchorX, symbol.anchorY, 0, 1] as vec4;
187
+ xyTransformMat4(anchorPos, anchorPos, posMatrix);
188
+ }
176
189
 
177
190
  // Don't bother calculating the correct point for invisible labels.
178
191
  if (!isVisible(anchorPos, clippingBuffer)) {
@@ -187,18 +200,18 @@ function updateLineLabels(bucket: SymbolBucket,
187
200
  const pitchScaledFontSize = pitchWithMap ? fontSize / perspectiveRatio : fontSize * perspectiveRatio;
188
201
 
189
202
  const tileAnchorPoint = new Point(symbol.anchorX, symbol.anchorY);
190
- const anchorPoint = project(tileAnchorPoint, labelPlaneMatrix).point;
203
+ const anchorPoint = project(tileAnchorPoint, labelPlaneMatrix, getElevation).point;
191
204
  const projectionCache = {};
192
205
 
193
206
  const placeUnflipped: any = placeGlyphsAlongLine(symbol, pitchScaledFontSize, false /*unflipped*/, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix,
194
- bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio, rotateToLine);
207
+ bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio, rotateToLine, getElevation);
195
208
 
196
209
  useVertical = placeUnflipped.useVertical;
197
210
 
198
211
  if (placeUnflipped.notEnoughRoom || useVertical ||
199
212
  (placeUnflipped.needsFlipping &&
200
213
  (placeGlyphsAlongLine(symbol, pitchScaledFontSize, true /*flipped*/, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix,
201
- bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio, rotateToLine) as any).notEnoughRoom)) {
214
+ bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio, rotateToLine, getElevation) as any).notEnoughRoom)) {
202
215
  hideGlyphs(symbol.numGlyphs, dynamicLayoutVertexArray);
203
216
  }
204
217
  }
@@ -210,7 +223,7 @@ function updateLineLabels(bucket: SymbolBucket,
210
223
  }
211
224
  }
212
225
 
213
- function placeFirstAndLastGlyph(fontScale: number, glyphOffsetArray: GlyphOffsetArray, lineOffsetX: number, lineOffsetY: number, flip: boolean, anchorPoint: Point, tileAnchorPoint: Point, symbol: any, lineVertexArray: SymbolLineVertexArray, labelPlaneMatrix: mat4, projectionCache: any, rotateToLine: boolean) {
226
+ function placeFirstAndLastGlyph(fontScale: number, glyphOffsetArray: GlyphOffsetArray, lineOffsetX: number, lineOffsetY: number, flip: boolean, anchorPoint: Point, tileAnchorPoint: Point, symbol: any, lineVertexArray: SymbolLineVertexArray, labelPlaneMatrix: mat4, projectionCache: any, rotateToLine: boolean, getElevation: (x: number, y: number) => number) {
214
227
  const glyphEndIndex = symbol.glyphStartIndex + symbol.numGlyphs;
215
228
  const lineStartIndex = symbol.lineStartIndex;
216
229
  const lineEndIndex = symbol.lineStartIndex + symbol.lineLength;
@@ -219,12 +232,12 @@ function placeFirstAndLastGlyph(fontScale: number, glyphOffsetArray: GlyphOffset
219
232
  const lastGlyphOffset = glyphOffsetArray.getoffsetX(glyphEndIndex - 1);
220
233
 
221
234
  const firstPlacedGlyph = placeGlyphAlongLine(fontScale * firstGlyphOffset, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment,
222
- lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine);
235
+ lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine, getElevation);
223
236
  if (!firstPlacedGlyph)
224
237
  return null;
225
238
 
226
239
  const lastPlacedGlyph = placeGlyphAlongLine(fontScale * lastGlyphOffset, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment,
227
- lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine);
240
+ lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine, getElevation);
228
241
  if (!lastPlacedGlyph)
229
242
  return null;
230
243
 
@@ -252,7 +265,7 @@ function requiresOrientationChange(writingMode, firstPoint, lastPoint, aspectRat
252
265
  return null;
253
266
  }
254
267
 
255
- function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix, glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio, rotateToLine) {
268
+ function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix, glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio, rotateToLine, getElevation) {
256
269
  const fontScale = fontSize / 24;
257
270
  const lineOffsetX = symbol.lineOffsetX * fontScale;
258
271
  const lineOffsetY = symbol.lineOffsetY * fontScale;
@@ -265,12 +278,12 @@ function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, la
265
278
 
266
279
  // Place the first and the last glyph in the label first, so we can figure out
267
280
  // the overall orientation of the label and determine whether it needs to be flipped in keepUpright mode
268
- const firstAndLastGlyph = placeFirstAndLastGlyph(fontScale, glyphOffsetArray, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine);
281
+ const firstAndLastGlyph = placeFirstAndLastGlyph(fontScale, glyphOffsetArray, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine, getElevation);
269
282
  if (!firstAndLastGlyph) {
270
283
  return {notEnoughRoom: true};
271
284
  }
272
- const firstPoint = project(firstAndLastGlyph.first.point, glCoordMatrix).point;
273
- const lastPoint = project(firstAndLastGlyph.last.point, glCoordMatrix).point;
285
+ const firstPoint = project(firstAndLastGlyph.first.point, glCoordMatrix, getElevation).point;
286
+ const lastPoint = project(firstAndLastGlyph.last.point, glCoordMatrix, getElevation).point;
274
287
 
275
288
  if (keepUpright && !flip) {
276
289
  const orientationChange = requiresOrientationChange(symbol.writingMode, firstPoint, lastPoint, aspectRatio);
@@ -284,24 +297,24 @@ function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, la
284
297
  // Since first and last glyph fit on the line, we're sure that the rest of the glyphs can be placed
285
298
  // $FlowFixMe
286
299
  placedGlyphs.push(placeGlyphAlongLine(fontScale * glyphOffsetArray.getoffsetX(glyphIndex), lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment,
287
- lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine));
300
+ lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine, getElevation));
288
301
  }
289
302
  placedGlyphs.push(firstAndLastGlyph.last);
290
303
  } else {
291
304
  // Only a single glyph to place
292
305
  // So, determine whether to flip based on projected angle of the line segment it's on
293
306
  if (keepUpright && !flip) {
294
- const a = project(tileAnchorPoint, posMatrix).point;
307
+ const a = project(tileAnchorPoint, posMatrix, getElevation).point;
295
308
  const tileVertexIndex = (symbol.lineStartIndex + symbol.segment + 1);
296
309
  // $FlowFixMe
297
310
  const tileSegmentEnd = new Point(lineVertexArray.getx(tileVertexIndex), lineVertexArray.gety(tileVertexIndex));
298
- const projectedVertex = project(tileSegmentEnd, posMatrix);
311
+ const projectedVertex = project(tileSegmentEnd, posMatrix, getElevation);
299
312
  // We know the anchor will be in the viewport, but the end of the line segment may be
300
313
  // behind the plane of the camera, in which case we can use a point at any arbitrary (closer)
301
314
  // point on the segment.
302
315
  const b = (projectedVertex.signedDistanceFromCamera > 0) ?
303
316
  projectedVertex.point :
304
- projectTruncatedLineSegment(tileAnchorPoint, tileSegmentEnd, a, 1, posMatrix);
317
+ projectTruncatedLineSegment(tileAnchorPoint, tileSegmentEnd, a, 1, posMatrix, getElevation);
305
318
 
306
319
  const orientationChange = requiresOrientationChange(symbol.writingMode, a, b, aspectRatio);
307
320
  if (orientationChange) {
@@ -310,7 +323,7 @@ function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, la
310
323
  }
311
324
  // $FlowFixMe
312
325
  const singleGlyph = placeGlyphAlongLine(fontScale * glyphOffsetArray.getoffsetX(symbol.glyphStartIndex), lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment,
313
- symbol.lineStartIndex, symbol.lineStartIndex + symbol.lineLength, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine);
326
+ symbol.lineStartIndex, symbol.lineStartIndex + symbol.lineLength, lineVertexArray, labelPlaneMatrix, projectionCache, rotateToLine, getElevation);
314
327
  if (!singleGlyph)
315
328
  return {notEnoughRoom: true};
316
329
 
@@ -323,18 +336,19 @@ function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, la
323
336
  return {};
324
337
  }
325
338
 
326
- function projectTruncatedLineSegment(previousTilePoint: Point, currentTilePoint: Point, previousProjectedPoint: Point, minimumLength: number, projectionMatrix: mat4) {
339
+ function projectTruncatedLineSegment(previousTilePoint: Point, currentTilePoint: Point, previousProjectedPoint: Point, minimumLength: number, projectionMatrix: mat4, getElevation: (x: number, y: number) => number) {
327
340
  // We are assuming "previousTilePoint" won't project to a point within one unit of the camera plane
328
341
  // If it did, that would mean our label extended all the way out from within the viewport to a (very distant)
329
342
  // point near the plane of the camera. We wouldn't be able to render the label anyway once it crossed the
330
343
  // plane of the camera.
331
- const projectedUnitVertex = project(previousTilePoint.add(previousTilePoint.sub(currentTilePoint)._unit()), projectionMatrix).point;
344
+ const projectedUnitVertex = project(previousTilePoint.add(previousTilePoint.sub(currentTilePoint)._unit()), projectionMatrix, getElevation).point;
332
345
  const projectedUnitSegment = previousProjectedPoint.sub(projectedUnitVertex);
333
346
 
334
347
  return previousProjectedPoint.add(projectedUnitSegment._mult(minimumLength / projectedUnitSegment.mag()));
335
348
  }
336
349
 
337
- function placeGlyphAlongLine(offsetX: number,
350
+ function placeGlyphAlongLine(
351
+ offsetX: number,
338
352
  lineOffsetX: number,
339
353
  lineOffsetY: number,
340
354
  flip: boolean,
@@ -348,7 +362,8 @@ function placeGlyphAlongLine(offsetX: number,
348
362
  projectionCache: {
349
363
  [_: number]: Point;
350
364
  },
351
- rotateToLine: boolean) {
365
+ rotateToLine: boolean,
366
+ getElevation: (x: number, y: number) => number) {
352
367
 
353
368
  const combinedOffsetX = flip ?
354
369
  offsetX - lineOffsetX :
@@ -390,7 +405,7 @@ function placeGlyphAlongLine(offsetX: number,
390
405
  current = projectionCache[currentIndex];
391
406
  if (current === undefined) {
392
407
  const currentVertex = new Point(lineVertexArray.getx(currentIndex), lineVertexArray.gety(currentIndex));
393
- const projection = project(currentVertex, labelPlaneMatrix);
408
+ const projection = project(currentVertex, labelPlaneMatrix, getElevation);
394
409
  if (projection.signedDistanceFromCamera > 0) {
395
410
  current = projectionCache[currentIndex] = projection.point;
396
411
  } else {
@@ -401,7 +416,7 @@ function placeGlyphAlongLine(offsetX: number,
401
416
  tileAnchorPoint :
402
417
  new Point(lineVertexArray.getx(previousLineVertexIndex), lineVertexArray.gety(previousLineVertexIndex));
403
418
  // Don't cache because the new vertex might not be far enough out for future glyphs on the same segment
404
- current = projectTruncatedLineSegment(previousTilePoint, currentVertex, prev, absOffsetX - distanceToPrev + 1, labelPlaneMatrix);
419
+ current = projectTruncatedLineSegment(previousTilePoint, currentVertex, prev, absOffsetX - distanceToPrev + 1, labelPlaneMatrix, getElevation);
405
420
  }
406
421
  }
407
422
 
package/src/ui/camera.ts CHANGED
@@ -898,6 +898,7 @@ abstract class Camera extends Evented {
898
898
 
899
899
  _prepareEase(eventData: any, noMoveStart: boolean, currently: any = {}) {
900
900
  this._moving = true;
901
+ this.fire(new Event('freezeElevation', {freeze: true}));
901
902
 
902
903
  if (!noMoveStart && !currently.moving) {
903
904
  this.fire(new Event('movestart', eventData));
@@ -933,6 +934,7 @@ abstract class Camera extends Evented {
933
934
  return;
934
935
  }
935
936
  delete this._easeId;
937
+ this.fire(new Event('freezeElevation', {freeze: false}));
936
938
 
937
939
  const wasZooming = this._zooming;
938
940
  const wasRotating = this._rotating;
@@ -0,0 +1,77 @@
1
+ import DOM from '../../util/dom';
2
+ import {bindAll} from '../../util/util';
3
+
4
+ import type Map from '../map';
5
+ import type {IControl} from './control';
6
+ import type {TerrainSpecification} from '../../style-spec/types.g';
7
+
8
+ /**
9
+ * An `TerrainControl` control adds a button to turn terrain on and off.
10
+ *
11
+ * @implements {IControl}
12
+ * @param {Object} [options]
13
+ * @param {string} [options.id] The ID of the raster-dem source to use.
14
+ * @param {exaggeration: number; elevationOffset: number} [options.options] Allowed options are exaggeration: number; elevationOffset: number
15
+ * @example
16
+ * var map = new maplibregl.Map({TerrainControl: false})
17
+ * .addControl(new maplibregl.TerrainControl({
18
+ * source: "terrain"
19
+ * }));
20
+ */
21
+ class TerrainControl implements IControl {
22
+ options: TerrainSpecification;
23
+ _map: Map;
24
+ _container: HTMLElement;
25
+ _terrainButton: HTMLButtonElement;
26
+
27
+ constructor(options: TerrainSpecification) {
28
+ this.options = options;
29
+
30
+ bindAll([
31
+ '_toggleTerrain',
32
+ '_updateTerrainIcon',
33
+ ], this);
34
+ }
35
+
36
+ onAdd(map: Map) {
37
+ this._map = map;
38
+ this._container = DOM.create('div', 'maplibregl-ctrl maplibregl-ctrl-group mapboxgl-ctrl mapboxgl-ctrl-group');
39
+ this._terrainButton = DOM.create('button', 'maplibregl-ctrl-terrain mapboxgl-ctrl-terrain', this._container);
40
+ DOM.create('span', 'maplibregl-ctrl-icon mapboxgl-ctrl-icon', this._terrainButton).setAttribute('aria-hidden', 'true');
41
+ this._terrainButton.type = 'button';
42
+ this._terrainButton.addEventListener('click', this._toggleTerrain);
43
+
44
+ this._updateTerrainIcon();
45
+ this._map.on('terrain', this._updateTerrainIcon);
46
+ return this._container;
47
+ }
48
+
49
+ onRemove() {
50
+ DOM.remove(this._container);
51
+ this._map.off('terrain', this._updateTerrainIcon);
52
+ this._map = undefined;
53
+ }
54
+
55
+ _toggleTerrain() {
56
+ if (this._map.getTerrain()) {
57
+ this._map.setTerrain(null);
58
+ } else {
59
+ this._map.setTerrain(this.options);
60
+ }
61
+ this._updateTerrainIcon();
62
+ }
63
+
64
+ _updateTerrainIcon() {
65
+ this._terrainButton.classList.remove('maplibregl-ctrl-terrain', 'mapboxgl-ctrl-terrain');
66
+ this._terrainButton.classList.remove('maplibregl-ctrl-terrain-enabled', 'mapboxgl-ctrl-terrain-enabled');
67
+ if (this._map.style.terrain) {
68
+ this._terrainButton.classList.add('maplibregl-ctrl-terrain-enabled', 'mapboxgl-ctrl-terrain-enabled');
69
+ this._terrainButton.title = this._map._getUIString('TerrainControl.disableTerrain');
70
+ } else {
71
+ this._terrainButton.classList.add('maplibregl-ctrl-terrain', 'mapboxgl-ctrl-terrain');
72
+ this._terrainButton.title = this._map._getUIString('TerrainControl.enableTerrain');
73
+ }
74
+ }
75
+ }
76
+
77
+ export default TerrainControl;
@@ -13,8 +13,9 @@ const defaultLocale = {
13
13
  'ScaleControl.Meters': 'm',
14
14
  'ScaleControl.Kilometers': 'km',
15
15
  'ScaleControl.Miles': 'mi',
16
- 'ScaleControl.NauticalMiles': 'nm'
17
-
16
+ 'ScaleControl.NauticalMiles': 'nm',
17
+ 'TerrainControl.enableTerrain': 'Enable terrain',
18
+ 'TerrainControl.disableTerrain': 'Disable terrain'
18
19
  };
19
20
 
20
21
  export default defaultLocale;
package/src/ui/events.ts CHANGED
@@ -311,6 +311,10 @@ export type MapDataEvent = {
311
311
  sourceDataType: MapSourceDataType;
312
312
  };
313
313
 
314
+ export type MapTerrainEvent = {
315
+ type: 'terrain';
316
+ };
317
+
314
318
  export type MapContextEvent = {
315
319
  type: 'webglcontextlost' | 'webglcontextrestored';
316
320
  originalEvent: WebGLContextEvent;
@@ -386,6 +390,8 @@ export type MapEventType = {
386
390
  pitchend: MapLibreEvent<MouseEvent | TouchEvent | undefined>;
387
391
 
388
392
  wheel: MapWheelEvent;
393
+
394
+ terrain: MapTerrainEvent;
389
395
  };
390
396
 
391
397
  export type MapEvent =
@@ -1394,8 +1400,17 @@ export type MapEvent =
1394
1400
  */
1395
1401
  | 'style.load'
1396
1402
 
1403
+ /**
1404
+ * @event terrain
1405
+ * @memberof Map
1406
+ * @instance
1407
+ * @private
1408
+ */
1409
+ | 'terrain'
1410
+
1397
1411
  /**
1398
1412
  * Fired when a request for one of the map's sources' tiles is aborted.
1413
+ * Fired when a request for one of the map's sources' data is aborted.
1399
1414
  * See {@link MapDataEvent} for more information.
1400
1415
  *
1401
1416
  * @event dataabort
@@ -1406,7 +1421,7 @@ export type MapEvent =
1406
1421
  * // Initialize the map
1407
1422
  * var map = new maplibregl.Map({ // map options });
1408
1423
  * // Set an event listener that fires
1409
- * // when a request for one of the map's sources' tiles is aborted.
1424
+ * // when a request for one of the map's sources' data is aborted.
1410
1425
  * map.on('dataabort', function() {
1411
1426
  * console.log('A dataabort event occurred.');
1412
1427
  * });
@@ -1414,7 +1429,7 @@ export type MapEvent =
1414
1429
  | 'dataabort'
1415
1430
 
1416
1431
  /**
1417
- * Fired when a request for one of the map's sources' tiles is aborted.
1432
+ * Fired when a request for one of the map's sources' data is aborted.
1418
1433
  * See {@link MapDataEvent} for more information.
1419
1434
  *
1420
1435
  * @event sourcedataabort
@@ -1425,7 +1440,7 @@ export type MapEvent =
1425
1440
  * // Initialize the map
1426
1441
  * var map = new maplibregl.Map({ // map options });
1427
1442
  * // Set an event listener that fires
1428
- * // when a request for one of the map's sources' tiles is aborted.
1443
+ * // when a request for one of the map's sources' data is aborted.
1429
1444
  * map.on('sourcedataabort', function() {
1430
1445
  * console.log('A sourcedataabort event occurred.');
1431
1446
  * });
@@ -18,6 +18,7 @@ import DragRotateHandler from './handler/shim/drag_rotate';
18
18
  import TouchZoomRotateHandler from './handler/shim/touch_zoom_rotate';
19
19
  import {bindAll, extend} from '../util/util';
20
20
  import Point from '@mapbox/point-geometry';
21
+ import LngLat from '../geo/lng_lat';
21
22
  import assert from 'assert';
22
23
 
23
24
  export type InputEvent = MouseEvent | TouchEvent | KeyboardEvent | WheelEvent;
@@ -100,6 +101,7 @@ class HandlerManager {
100
101
  _handlersById: {[x: string]: Handler};
101
102
  _updatingCamera: boolean;
102
103
  _changes: Array<[HandlerResult, any, any]>;
104
+ _drag: {center: Point; lngLat: LngLat; point: Point; handlerName: string};
103
105
  _previousActiveHandlers: {[x: string]: Handler};
104
106
  _listeners: Array<[Window | Document | HTMLElement, string, {
105
107
  passive?: boolean;
@@ -411,11 +413,11 @@ class HandlerManager {
411
413
  }
412
414
 
413
415
  _updateMapTransform(combinedResult: any, combinedEventsInProgress: any, deactivatedHandlers: any) {
414
-
415
416
  const map = this._map;
416
417
  const tr = map.transform;
418
+ const terrain = map.style && map.style.terrain;
417
419
 
418
- if (!hasChange(combinedResult)) {
420
+ if (!hasChange(combinedResult) && !(terrain && this._drag)) {
419
421
  return this._fireEvents(combinedEventsInProgress, deactivatedHandlers, true);
420
422
  }
421
423
 
@@ -433,7 +435,35 @@ class HandlerManager {
433
435
  if (bearingDelta) tr.bearing += bearingDelta;
434
436
  if (pitchDelta) tr.pitch += pitchDelta;
435
437
  if (zoomDelta) tr.zoom += zoomDelta;
436
- tr.setLocationAtPoint(loc, around);
438
+
439
+ if (!terrain) {
440
+ tr.setLocationAtPoint(loc, around);
441
+ } else {
442
+ // when 3d-terrain is enabled act a litte different:
443
+ // - draging do not drag the picked point itself, instead it drags the map by pixel-delta.
444
+ // With this approach it is no longer possible to pick a point from somewhere near
445
+ // the horizon to the center in one move.
446
+ // So this logic avoids the problem, that in such cases you easily loose orientation.
447
+ // - scrollzoom does not zoom into the mouse-point, instead it zooms into map-center
448
+ // this should be fixed in future-version
449
+ // when dragging starts, remember mousedown-location and panDelta from this point
450
+ if (combinedEventsInProgress.drag && !this._drag) {
451
+ this._drag = {
452
+ center: tr.centerPoint,
453
+ lngLat: tr.pointLocation(around),
454
+ point: around,
455
+ handlerName: combinedEventsInProgress.drag.handlerName
456
+ };
457
+ map.fire(new Event('freezeElevation', {freeze: true}));
458
+ // when dragging ends, recalcuate the zoomlevel for the new center coordinate
459
+ } else if (this._drag && deactivatedHandlers[this._drag.handlerName]) {
460
+ map.fire(new Event('freezeElevation', {freeze: false}));
461
+ this._drag = null;
462
+ // drag map
463
+ } else if (combinedEventsInProgress.drag && this._drag) {
464
+ tr.center = tr.pointLocation(tr.centerPoint.sub(panDelta));
465
+ }
466
+ }
437
467
 
438
468
  this._map._update();
439
469
  if (!combinedResult.noInertia) this._inertia.record(combinedResult);
package/src/ui/map.ts CHANGED
@@ -51,7 +51,8 @@ import type {
51
51
  FilterSpecification,
52
52
  StyleSpecification,
53
53
  LightSpecification,
54
- SourceSpecification
54
+ SourceSpecification,
55
+ TerrainSpecification
55
56
  } from '../style-spec/types.g';
56
57
  import {Callback} from '../types/callback';
57
58
  import type {ControlPosition, IControl} from './control/control';
@@ -432,6 +433,10 @@ class Map extends Camera {
432
433
  this.on('move', () => this._update(false));
433
434
  this.on('moveend', () => this._update(false));
434
435
  this.on('zoom', () => this._update(true));
436
+ this.on('terrain', () => {
437
+ this.painter.terrainFacilitator.dirty = true;
438
+ this._update(true);
439
+ });
435
440
 
436
441
  if (typeof window !== 'undefined') {
437
442
  addEventListener('online', this._onWindowOnline, false);
@@ -873,7 +878,7 @@ class Map extends Camera {
873
878
  * var point = map.project(coordinate);
874
879
  */
875
880
  project(lnglat: LngLatLike) {
876
- return this.transform.locationPoint(LngLat.convert(lnglat));
881
+ return this.transform.locationPoint(LngLat.convert(lnglat), this.style && this.style.terrain);
877
882
  }
878
883
 
879
884
  /**
@@ -889,7 +894,7 @@ class Map extends Camera {
889
894
  * });
890
895
  */
891
896
  unproject(point: PointLike) {
892
- return this.transform.pointLocation(Point.convert(point));
897
+ return this.transform.pointLocation(Point.convert(point), this.style && this.style.terrain);
893
898
  }
894
899
 
895
900
  /**
@@ -1571,6 +1576,28 @@ class Map extends Camera {
1571
1576
  return source.loaded();
1572
1577
  }
1573
1578
 
1579
+ /**
1580
+ * Loads a 3D terrain mesh, based on a "raster-dem" source.
1581
+ * @param {TerrainSpecification} [options] Options object.
1582
+ * @returns {Map} `this`
1583
+ * @example
1584
+ * map.setTerrain({ source: 'terrain' });
1585
+ */
1586
+ setTerrain(options: TerrainSpecification): Map {
1587
+ this.style.setTerrain(options);
1588
+ return this;
1589
+ }
1590
+
1591
+ /**
1592
+ * Get the terrain-options if terrain is loaded
1593
+ * @returns {TerrainSpecification} the TerrainSpecification passed to setTerrain
1594
+ * @example
1595
+ * map.getTerrain(); // { source: 'terrain' };
1596
+ */
1597
+ getTerrain(): TerrainSpecification {
1598
+ return this.style.terrain && this.style.terrain.options;
1599
+ }
1600
+
1574
1601
  /**
1575
1602
  * Returns a Boolean indicating whether all tiles in the viewport from all sources on
1576
1603
  * the style are loaded.
@@ -1579,8 +1606,7 @@ class Map extends Camera {
1579
1606
  * @example
1580
1607
  * var tilesLoaded = map.areTilesLoaded();
1581
1608
  */
1582
-
1583
- areTilesLoaded() {
1609
+ areTilesLoaded(): boolean {
1584
1610
  const sources = this.style && this.style.sourceCaches;
1585
1611
  for (const id in sources) {
1586
1612
  const source = sources[id];
@@ -1614,7 +1640,7 @@ class Map extends Camera {
1614
1640
  * @example
1615
1641
  * map.removeSource('bathymetry-data');
1616
1642
  */
1617
- removeSource(id: string) {
1643
+ removeSource(id: string): Map {
1618
1644
  this.style.removeSource(id);
1619
1645
  return this._update(true);
1620
1646
  }
@@ -2550,6 +2576,10 @@ class Map extends Camera {
2550
2576
  this.style._updateSources(this.transform);
2551
2577
  }
2552
2578
 
2579
+ // update terrain stuff
2580
+ if (this.style.terrain) this.style.terrain.sourceCache.update(this.transform, this.style.terrain);
2581
+ this.transform.updateElevation(this.style.terrain);
2582
+
2553
2583
  this._placementDirty = this.style && this.style._updatePlacement(this.painter.transform, this.showCollisionBoxes, this._fadeDuration, this._crossSourceCollisions);
2554
2584
 
2555
2585
  // Actually draw
@@ -4,6 +4,7 @@ import Popup from './popup';
4
4
  import LngLat from '../geo/lng_lat';
5
5
  import Point from '@mapbox/point-geometry';
6
6
  import simulate from '../../test/unit/lib/simulate_interaction';
7
+ import type Terrain from '../render/terrain';
7
8
 
8
9
  function createMap(options = {}) {
9
10
  const container = window.document.createElement('div');
@@ -771,4 +772,24 @@ describe('marker', () => {
771
772
 
772
773
  map.remove();
773
774
  });
775
+
776
+ test('Marker removed after update when terrain is on should clear timeout', () => {
777
+ jest.spyOn(global, 'setTimeout');
778
+ jest.spyOn(global, 'clearTimeout');
779
+ const map = createMap();
780
+ const marker = new Marker()
781
+ .setLngLat([0, 0])
782
+ .addTo(map);
783
+ map.style.terrain = {
784
+ getElevation: () => 0
785
+ } as any as Terrain;
786
+
787
+ marker.setOffset([10, 10]);
788
+
789
+ expect(setTimeout).toHaveBeenCalled();
790
+ marker.remove();
791
+ expect(clearTimeout).toHaveBeenCalled();
792
+
793
+ map.remove();
794
+ });
774
795
  });
package/src/ui/marker.ts CHANGED
@@ -74,6 +74,7 @@ export default class Marker extends Evented {
74
74
  _pitchAlignment: string;
75
75
  _rotationAlignment: string;
76
76
  _originalTabIndex: string; // original tabindex of _element
77
+ _opacityTimeout: ReturnType<typeof setTimeout>;
77
78
 
78
79
  constructor(options?: MarkerOptions, legacyOptions?: MarkerOptions) {
79
80
  super();
@@ -264,6 +265,10 @@ export default class Marker extends Evented {
264
265
  * @returns {Marker} `this`
265
266
  */
266
267
  remove() {
268
+ if (this._opacityTimeout) {
269
+ clearTimeout(this._opacityTimeout);
270
+ delete this._opacityTimeout;
271
+ }
267
272
  if (this._map) {
268
273
  this._map.off('click', this._onMapClick);
269
274
  this._map.off('move', this._update);
@@ -469,6 +474,15 @@ export default class Marker extends Evented {
469
474
  }
470
475
 
471
476
  DOM.setTransform(this._element, `${anchorTranslate[this._anchor]} translate(${this._pos.x}px, ${this._pos.y}px) ${pitch} ${rotation}`);
477
+
478
+ // in case of 3D, ask the terrain coords-framebuffer for this pos and check if the marker is visible
479
+ // call this logic in setTimeout with a timeout of 100ms to save performance in map-movement
480
+ if (this._map.style && this._map.style.terrain && !this._opacityTimeout) this._opacityTimeout = setTimeout(() => {
481
+ const lnglat = this._map.unproject(this._pos);
482
+ const metresPerPixel = 40075016.686 * Math.abs(Math.cos(this._lngLat.lat * Math.PI / 180)) / Math.pow(2, this._map.transform.tileZoom + 8);
483
+ this._element.style.opacity = lnglat.distanceTo(this._lngLat) > metresPerPixel * 20 ? '0.2' : '1.0';
484
+ this._opacityTimeout = null;
485
+ }, 100);
472
486
  }
473
487
 
474
488
  /**
@@ -1,5 +1,4 @@
1
1
  import {mat4, vec3, vec4} from 'gl-matrix';
2
- import assert from 'assert';
3
2
 
4
3
  class Frustum {
5
4
 
@@ -19,10 +18,12 @@ class Frustum {
19
18
 
20
19
  const scale = Math.pow(2, zoom);
21
20
 
22
- // Transform frustum corner points from clip space to tile space
23
- const frustumCoords = clipSpaceCorners
24
- .map(v => vec4.transformMat4([] as any, v as any, invProj))
25
- .map(v => vec4.scale([] as any, v, 1.0 / v[3] / worldSize * scale));
21
+ // Transform frustum corner points from clip space to tile space, Z to meters
22
+ const frustumCoords = clipSpaceCorners.map(v => {
23
+ v = vec4.transformMat4([] as any, v as any, invProj) as any;
24
+ const s = 1.0 / v[3] / worldSize * scale;
25
+ return vec4.mul(v as any, v as any, [s, s, 1.0 / v[3], s] as vec4);
26
+ });
26
27
 
27
28
  const frustumPlanePointIndices = [
28
29
  [0, 1, 2], // near
@@ -84,14 +85,16 @@ class Aabb {
84
85
  intersects(frustum: Frustum): number {
85
86
  // Execute separating axis test between two convex objects to find intersections
86
87
  // Each frustum plane together with 3 major axes define the separating axes
87
- // Note: test only 4 points as both min and max points have equal elevation
88
- assert(this.min[2] === 0 && this.max[2] === 0);
89
88
 
90
89
  const aabbPoints = [
91
- [this.min[0], this.min[1], 0.0, 1],
92
- [this.max[0], this.min[1], 0.0, 1],
93
- [this.max[0], this.max[1], 0.0, 1],
94
- [this.min[0], this.max[1], 0.0, 1]
90
+ [this.min[0], this.min[1], this.min[2], 1],
91
+ [this.max[0], this.min[1], this.min[2], 1],
92
+ [this.max[0], this.max[1], this.min[2], 1],
93
+ [this.min[0], this.max[1], this.min[2], 1],
94
+ [this.min[0], this.min[1], this.max[2], 1],
95
+ [this.max[0], this.min[1], this.max[2], 1],
96
+ [this.max[0], this.max[1], this.max[2], 1],
97
+ [this.min[0], this.max[1], this.max[2], 1]
95
98
  ];
96
99
 
97
100
  let fullyInside = true;