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
@@ -5,12 +5,13 @@ import Point from '@mapbox/point-geometry';
5
5
  import {wrap, clamp} from '../util/util';
6
6
  import {number as interpolate} from '../style-spec/util/interpolate';
7
7
  import EXTENT from '../data/extent';
8
- import {vec4, mat4, mat2, vec2} from 'gl-matrix';
8
+ import {vec3, vec4, mat4, mat2, vec2} from 'gl-matrix';
9
9
  import {Aabb, Frustum} from '../util/primitives';
10
10
  import EdgeInsets from './edge_insets';
11
11
 
12
12
  import {UnwrappedTileID, OverscaledTileID, CanonicalTileID} from '../source/tile_id';
13
13
  import type {PaddingOptions} from './edge_insets';
14
+ import Terrain from '../render/terrain';
14
15
 
15
16
  /**
16
17
  * A single transform, generally used for a single tile to be
@@ -31,14 +32,17 @@ class Transform {
31
32
  zoomFraction: number;
32
33
  pixelsToGLUnits: [number, number];
33
34
  cameraToCenterDistance: number;
35
+ cameraToSeaLevelDistance: number;
34
36
  mercatorMatrix: mat4;
35
37
  projMatrix: mat4;
36
38
  invProjMatrix: mat4;
37
39
  alignedProjMatrix: mat4;
38
40
  pixelMatrix: mat4;
41
+ pixelMatrix3D: mat4;
39
42
  pixelMatrixInverse: mat4;
40
43
  glCoordMatrix: mat4;
41
44
  labelPlaneMatrix: mat4;
45
+ freezeElevation: boolean;
42
46
  _fov: number;
43
47
  _pitch: number;
44
48
  _zoom: number;
@@ -49,6 +53,8 @@ class Transform {
49
53
  _minPitch: number;
50
54
  _maxPitch: number;
51
55
  _center: LngLat;
56
+ _elevation: number;
57
+ _pixelPerMeter: number;
52
58
  _edgeInsets: EdgeInsets;
53
59
  _constraining: boolean;
54
60
  _posMatrixCache: {[_: string]: mat4};
@@ -57,6 +63,7 @@ class Transform {
57
63
  constructor(minZoom?: number, maxZoom?: number, minPitch?: number, maxPitch?: number, renderWorldCopies?: boolean) {
58
64
  this.tileSize = 512; // constant
59
65
  this.maxValidLatitude = 85.051129; // constant
66
+ this.freezeElevation = false;
60
67
 
61
68
  this._renderWorldCopies = renderWorldCopies === undefined ? true : !!renderWorldCopies;
62
69
  this._minZoom = minZoom || 0;
@@ -70,6 +77,7 @@ class Transform {
70
77
  this.width = 0;
71
78
  this.height = 0;
72
79
  this._center = new LngLat(0, 0);
80
+ this._elevation = 0;
73
81
  this.zoom = 0;
74
82
  this.angle = 0;
75
83
  this._fov = 0.6435011087932844;
@@ -87,6 +95,7 @@ class Transform {
87
95
  clone.width = this.width;
88
96
  clone.height = this.height;
89
97
  clone._center = this._center;
98
+ clone._elevation = this._elevation;
90
99
  clone.zoom = this.zoom;
91
100
  clone.angle = this.angle;
92
101
  clone._fov = this._fov;
@@ -207,6 +216,14 @@ class Transform {
207
216
  this._calcMatrices();
208
217
  }
209
218
 
219
+ get elevation(): number { return this._elevation; }
220
+ set elevation(elevation: number) {
221
+ if (elevation === this._elevation) return;
222
+ this._elevation = elevation;
223
+ this._constrain();
224
+ this._calcMatrices();
225
+ }
226
+
210
227
  get padding(): PaddingOptions { return this._edgeInsets.toJSON(); }
211
228
  set padding(padding: PaddingOptions) {
212
229
  if (this._edgeInsets.equals(padding)) return;
@@ -322,6 +339,7 @@ class Transform {
322
339
  roundZoom?: boolean;
323
340
  reparseOverscaled?: boolean;
324
341
  renderWorldCopies?: boolean;
342
+ terrain?: Terrain;
325
343
  }
326
344
  ): Array<OverscaledTileID> {
327
345
  let z = this.coveringZoomLevel(options);
@@ -330,23 +348,24 @@ class Transform {
330
348
  if (options.minzoom !== undefined && z < options.minzoom) return [];
331
349
  if (options.maxzoom !== undefined && z > options.maxzoom) z = options.maxzoom;
332
350
 
351
+ const cameraCoord = this.pointCoordinate(this.getCameraPoint());
333
352
  const centerCoord = MercatorCoordinate.fromLngLat(this.center);
334
353
  const numTiles = Math.pow(2, z);
354
+ const cameraPoint = [numTiles * cameraCoord.x, numTiles * cameraCoord.y, 0];
335
355
  const centerPoint = [numTiles * centerCoord.x, numTiles * centerCoord.y, 0];
336
356
  const cameraFrustum = Frustum.fromInvProjectionMatrix(this.invProjMatrix, this.worldSize, z);
337
357
 
338
358
  // No change of LOD behavior for pitch lower than 60 and when there is no top padding: return only tile ids from the requested zoom level
339
359
  let minZoom = options.minzoom || 0;
340
360
  // Use 0.1 as an epsilon to avoid for explicit == 0.0 floating point checks
341
- if (this.pitch <= 60.0 && this._edgeInsets.top < 0.1)
361
+ if (!options.terrain && this.pitch <= 60.0 && this._edgeInsets.top < 0.1)
342
362
  minZoom = z;
343
363
 
344
- // There should always be a certain number of maximum zoom level tiles surrounding the center location
345
- const radiusOfMaxLvlLodInTiles = 3;
364
+ // There should always be a certain number of maximum zoom level tiles surrounding the center location in 2D or in front of the camera in 3D
365
+ const radiusOfMaxLvlLodInTiles = options.terrain ? 2 / Math.min(this.tileSize, options.tileSize) * this.tileSize : 3;
346
366
 
347
367
  const newRootTile = (wrap: number): any => {
348
368
  return {
349
- // All tiles are on zero elevation plane => z difference is zero
350
369
  aabb: new Aabb([wrap * numTiles, 0, 0], [(wrap + 1) * numTiles, numTiles, 0]),
351
370
  zoom: 0,
352
371
  x: 0,
@@ -388,8 +407,9 @@ class Transform {
388
407
  fullyVisible = intersectResult === 2;
389
408
  }
390
409
 
391
- const distanceX = it.aabb.distanceX(centerPoint);
392
- const distanceY = it.aabb.distanceY(centerPoint);
410
+ const refPoint = options.terrain ? cameraPoint : centerPoint;
411
+ const distanceX = it.aabb.distanceX(refPoint);
412
+ const distanceY = it.aabb.distanceY(refPoint);
393
413
  const longestDim = Math.max(Math.abs(distanceX), Math.abs(distanceY));
394
414
 
395
415
  // We're using distance based heuristics to determine if a tile should be split into quadrants or not.
@@ -401,9 +421,12 @@ class Transform {
401
421
 
402
422
  // Have we reached the target depth or is the tile too far away to be any split further?
403
423
  if (it.zoom === maxZoom || (longestDim > distToSplit && it.zoom >= minZoom)) {
424
+ const dz = maxZoom - it.zoom, dx = cameraPoint[0] - 0.5 - (x << dz), dy = cameraPoint[1] - 0.5 - (y << dz);
404
425
  result.push({
405
426
  tileID: new OverscaledTileID(it.zoom === maxZoom ? overscaledZ : it.zoom, it.wrap, it.zoom, x, y),
406
- distanceSq: vec2.sqrLen([centerPoint[0] - 0.5 - x, centerPoint[1] - 0.5 - y])
427
+ distanceSq: vec2.sqrLen([centerPoint[0] - 0.5 - x, centerPoint[1] - 0.5 - y]),
428
+ // this variable is currently not used, but may be important to reduce the amount of loaded tiles
429
+ tileDistanceToCamera: Math.sqrt(dx * dx + dy * dy)
407
430
  });
408
431
  continue;
409
432
  }
@@ -411,8 +434,22 @@ class Transform {
411
434
  for (let i = 0; i < 4; i++) {
412
435
  const childX = (x << 1) + (i % 2);
413
436
  const childY = (y << 1) + (i >> 1);
414
-
415
- stack.push({aabb: it.aabb.quadrant(i), zoom: it.zoom + 1, x: childX, y: childY, wrap: it.wrap, fullyVisible});
437
+ const childZ = it.zoom + 1;
438
+ let quadrant = it.aabb.quadrant(i);
439
+ if (options.terrain) {
440
+ const tileID = new OverscaledTileID(childZ, it.wrap, childZ, childX, childY);
441
+ const tile = options.terrain.getTerrainData(tileID).tile;
442
+ let minElevation = this.elevation, maxElevation = this.elevation;
443
+ if (tile && tile.dem) {
444
+ minElevation = tile.dem.min * options.terrain.exaggeration;
445
+ maxElevation = tile.dem.max * options.terrain.exaggeration;
446
+ }
447
+ quadrant = new Aabb(
448
+ [quadrant.min[0], quadrant.min[1], minElevation] as vec3,
449
+ [quadrant.max[0], quadrant.max[1], maxElevation] as vec3
450
+ );
451
+ }
452
+ stack.push({aabb: quadrant, zoom: childZ, x: childX, y: childY, wrap: it.wrap, fullyVisible});
416
453
  }
417
454
  }
418
455
 
@@ -446,6 +483,72 @@ class Transform {
446
483
 
447
484
  get point(): Point { return this.project(this.center); }
448
485
 
486
+ /**
487
+ * Updates the center-elevation value unless freezeElevation is activated.
488
+ * @param terrain the terrain
489
+ */
490
+ updateElevation(terrain?: Terrain) {
491
+ if (this.freezeElevation) return;
492
+ this.elevation = terrain ? this.getElevation(this._center, terrain) : 0;
493
+ }
494
+
495
+ /**
496
+ * get the elevation from terrain for the current zoomlevel.
497
+ * @param lnglat the location
498
+ * @param terrain the terrain
499
+ * @returns {Number} elevation in meters
500
+ */
501
+ getElevation(lnglat: LngLat, terrain: Terrain) {
502
+ const merc = MercatorCoordinate.fromLngLat(lnglat);
503
+ const worldSize = (1 << this.tileZoom) * EXTENT;
504
+ const mercX = merc.x * worldSize, mercY = merc.y * worldSize;
505
+ const tileX = Math.floor(mercX / EXTENT), tileY = Math.floor(mercY / EXTENT);
506
+ const tileID = new OverscaledTileID(this.tileZoom, 0, this.tileZoom, tileX, tileY);
507
+ return terrain.getElevation(tileID, mercX % EXTENT, mercY % EXTENT, EXTENT);
508
+ }
509
+
510
+ /**
511
+ * get the camera position in LngLat and altitudes in meter
512
+ * @returns {Object} An object with lngLat & altitude.
513
+ */
514
+ getCameraPosition(): {
515
+ lngLat: LngLat;
516
+ altitude: number;
517
+ } {
518
+ const lngLat = this.pointLocation(this.getCameraPoint());
519
+ const altitude = Math.cos(this._pitch) * this.cameraToCenterDistance / this._pixelPerMeter;
520
+ return {lngLat, altitude: altitude + this.elevation};
521
+ }
522
+
523
+ /**
524
+ * This method works in combination with freezeElevation activated.
525
+ * freezeElevtion is enabled during map-panning because during this the camera should sit in constant height.
526
+ * After panning finished, call this method to recalculate the zoomlevel for the current camera-height in current terrain.
527
+ * @param {Terrain} terrain the terrain
528
+ */
529
+ recalculateZoom(terrain: Terrain) {
530
+ // find position the camera is looking on
531
+ const center = this.pointLocation(this.centerPoint, terrain);
532
+ const elevation = this.getElevation(center, terrain);
533
+ const deltaElevation = this.elevation - elevation;
534
+ if (!deltaElevation) return;
535
+
536
+ // calculate mercator distance between camera & target
537
+ const cameraPosition = this.getCameraPosition();
538
+ const camera = MercatorCoordinate.fromLngLat(cameraPosition.lngLat, cameraPosition.altitude);
539
+ const target = MercatorCoordinate.fromLngLat(center, elevation);
540
+ const dx = camera.x - target.x, dy = camera.y - target.y, dz = camera.z - target.z;
541
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
542
+
543
+ // from this distance we calculate the new zoomlevel
544
+ const zoom = this.scaleZoom(this.cameraToCenterDistance / distance / this.tileSize);
545
+
546
+ // update matrices
547
+ this._elevation = elevation;
548
+ this._center = center;
549
+ this.zoom = zoom;
550
+ }
551
+
449
552
  setLocationAtPoint(lnglat: LngLat, point: Point) {
450
553
  const a = this.pointCoordinate(point);
451
554
  const b = this.pointCoordinate(this.centerPoint);
@@ -462,31 +565,35 @@ class Transform {
462
565
  /**
463
566
  * Given a location, return the screen point that corresponds to it
464
567
  * @param {LngLat} lnglat location
568
+ * @param {Terrain} terrain optional terrain
465
569
  * @returns {Point} screen point
466
570
  * @private
467
571
  */
468
- locationPoint(lnglat: LngLat) {
469
- return this.coordinatePoint(this.locationCoordinate(lnglat));
572
+ locationPoint(lnglat: LngLat, terrain?: Terrain): Point {
573
+ return terrain ?
574
+ this.coordinatePoint(this.locationCoordinate(lnglat), this.getElevation(lnglat, terrain), this.pixelMatrix3D) :
575
+ this.coordinatePoint(this.locationCoordinate(lnglat));
470
576
  }
471
577
 
472
578
  /**
473
579
  * Given a point on screen, return its lnglat
474
580
  * @param {Point} p screen point
581
+ * @param {Terrain} terrain optional terrain
475
582
  * @returns {LngLat} lnglat location
476
583
  * @private
477
584
  */
478
- pointLocation(p: Point) {
479
- return this.coordinateLocation(this.pointCoordinate(p));
585
+ pointLocation(p: Point, terrain?: Terrain): LngLat {
586
+ return this.coordinateLocation(this.pointCoordinate(p, terrain));
480
587
  }
481
588
 
482
589
  /**
483
590
  * Given a geographical lnglat, return an unrounded
484
591
  * coordinate that represents it at this transform's zoom level.
485
592
  * @param {LngLat} lnglat
486
- * @returns {Coordinate}
593
+ * @returns {MercatorCoordinate}
487
594
  * @private
488
595
  */
489
- locationCoordinate(lnglat: LngLat) {
596
+ locationCoordinate(lnglat: LngLat): MercatorCoordinate {
490
597
  return MercatorCoordinate.fromLngLat(lnglat);
491
598
  }
492
599
 
@@ -496,11 +603,27 @@ class Transform {
496
603
  * @returns {LngLat} lnglat
497
604
  * @private
498
605
  */
499
- coordinateLocation(coord: MercatorCoordinate) {
500
- return coord.toLngLat();
606
+ coordinateLocation(coord: MercatorCoordinate): LngLat {
607
+ return coord && coord.toLngLat();
501
608
  }
502
609
 
503
- pointCoordinate(p: Point) {
610
+ /**
611
+ * Given a Point, return its mercator coordinate.
612
+ * @param {Point} p the point
613
+ * @param {Terrain} terrain optional terrain
614
+ * @returns {LngLat} lnglat
615
+ * @private
616
+ */
617
+ pointCoordinate(p: Point, terrain?: Terrain): MercatorCoordinate {
618
+ // get point-coordinate from terrain coordinates framebuffer
619
+ if (terrain) {
620
+ const coordinate = terrain.pointCoordinate(p);
621
+ if (coordinate != null) {
622
+ return coordinate;
623
+ }
624
+ }
625
+
626
+ // calcuate point-coordinate on flat earth
504
627
  const targetZ = 0;
505
628
  // since we don't know the correct projected z value for the point,
506
629
  // unproject two points to get a line and then find the point on that
@@ -531,12 +654,14 @@ class Transform {
531
654
  /**
532
655
  * Given a coordinate, return the screen point that corresponds to it
533
656
  * @param {Coordinate} coord
657
+ * @params {number} elevation default = 0
658
+ * @params {mat4} pixelMatrix, default = this.pixelMatrix
534
659
  * @returns {Point} screen point
535
660
  * @private
536
661
  */
537
- coordinatePoint(coord: MercatorCoordinate) {
538
- const p = [coord.x * this.worldSize, coord.y * this.worldSize, 0, 1] as any;
539
- vec4.transformMat4(p, p, this.pixelMatrix);
662
+ coordinatePoint(coord: MercatorCoordinate, elevation: number = 0, pixelMatrix = this.pixelMatrix): Point {
663
+ const p = [coord.x * this.worldSize, coord.y * this.worldSize, elevation, 1] as any;
664
+ vec4.transformMat4(p, p, pixelMatrix);
540
665
  return new Point(p[0] / p[3], p[1] / p[3]);
541
666
  }
542
667
 
@@ -546,9 +671,10 @@ class Transform {
546
671
  * @returns {LngLatBounds} Returns a {@link LngLatBounds} object describing the map's geographical bounds.
547
672
  */
548
673
  getBounds(): LngLatBounds {
674
+ const top = Math.max(0, this.height / 2 - this.getHorizon());
549
675
  return new LngLatBounds()
550
- .extend(this.pointLocation(new Point(0, 0)))
551
- .extend(this.pointLocation(new Point(this.width, 0)))
676
+ .extend(this.pointLocation(new Point(0, top)))
677
+ .extend(this.pointLocation(new Point(this.width, top)))
552
678
  .extend(this.pointLocation(new Point(this.width, this.height)))
553
679
  .extend(this.pointLocation(new Point(0, this.height)));
554
680
  }
@@ -564,6 +690,16 @@ class Transform {
564
690
  return new LngLatBounds([this.lngRange[0], this.latRange[0]], [this.lngRange[1], this.latRange[1]]);
565
691
  }
566
692
 
693
+ /**
694
+ * Calculate pixel height of the visible horizon in relation to map-center (e.g. height/2),
695
+ * multiplied by a static factor to simulate the earth-radius.
696
+ * The calculated value is the horizontal line from the camera-height to sea-level.
697
+ * @returns {number} Horizon above center in pixels.
698
+ */
699
+ getHorizon(): number {
700
+ return Math.tan(Math.PI / 2 - this._pitch) * this.cameraToCenterDistance * 0.85;
701
+ }
702
+
567
703
  /**
568
704
  * Sets or clears the map's geographical constraints.
569
705
  * @param {LngLatBounds} bounds A {@link LngLatBounds} object describing the new geographic boundaries of the map.
@@ -682,7 +818,23 @@ class Transform {
682
818
 
683
819
  const halfFov = this._fov / 2;
684
820
  const offset = this.centerOffset;
821
+ const x = this.point.x, y = this.point.y;
685
822
  this.cameraToCenterDistance = 0.5 / Math.tan(halfFov) * this.height;
823
+ this._pixelPerMeter = mercatorZfromAltitude(1, this.center.lat) * this.worldSize;
824
+
825
+ let m = mat4.identity(new Float64Array(16) as any);
826
+ mat4.scale(m, m, [this.width / 2, -this.height / 2, 1]);
827
+ mat4.translate(m, m, [1, -1, 0]);
828
+ this.labelPlaneMatrix = m;
829
+
830
+ m = mat4.identity(new Float64Array(16) as any);
831
+ mat4.scale(m, m, [1, -1, 1]);
832
+ mat4.translate(m, m, [-1, -1, 0]);
833
+ mat4.scale(m, m, [2 / this.width, 2 / this.height, 1]);
834
+ this.glCoordMatrix = m;
835
+
836
+ // calculate the camera to sea-level distance in pixel in respect of terrain
837
+ this.cameraToSeaLevelDistance = this.cameraToCenterDistance + this._elevation * this._pixelPerMeter / Math.cos(this._pitch);
686
838
 
687
839
  // Find the distance from the center point [width/2 + offset.x, height/2 + offset.y] to the
688
840
  // center top point [width/2 + offset.x, 0] in Z units, using the law of sines.
@@ -690,14 +842,20 @@ class Transform {
690
842
  // (the distance between[width/2, height/2] and [width/2 + 1, height/2])
691
843
  const groundAngle = Math.PI / 2 + this._pitch;
692
844
  const fovAboveCenter = this._fov * (0.5 + offset.y / this.height);
693
- const topHalfSurfaceDistance = Math.sin(fovAboveCenter) * this.cameraToCenterDistance / Math.sin(clamp(Math.PI - groundAngle - fovAboveCenter, 0.01, Math.PI - 0.01));
694
- const point = this.point;
695
- const x = point.x, y = point.y;
845
+ const topHalfSurfaceDistance = Math.sin(fovAboveCenter) * this.cameraToSeaLevelDistance / Math.sin(clamp(Math.PI - groundAngle - fovAboveCenter, 0.01, Math.PI - 0.01));
846
+
847
+ // Find the distance from the center point to the horizon
848
+ const horizon = this.getHorizon();
849
+ const horizonAngle = Math.atan(horizon / this.cameraToCenterDistance);
850
+ const fovCenterToHorizon = 2 * horizonAngle * (0.5 + offset.y / (horizon * 2));
851
+ const topHalfSurfaceDistanceHorizon = Math.sin(fovCenterToHorizon) * this.cameraToSeaLevelDistance / Math.sin(clamp(Math.PI - groundAngle - fovCenterToHorizon, 0.01, Math.PI - 0.01));
696
852
 
697
853
  // Calculate z distance of the farthest fragment that should be rendered.
698
- const furthestDistance = Math.cos(Math.PI / 2 - this._pitch) * topHalfSurfaceDistance + this.cameraToCenterDistance;
854
+ const furthestDistance = Math.cos(Math.PI / 2 - this._pitch) * topHalfSurfaceDistance + this.cameraToSeaLevelDistance;
855
+ const furthestDistanceHorizon = Math.cos(Math.PI / 2 - this._pitch) * topHalfSurfaceDistanceHorizon + this.cameraToSeaLevelDistance;
856
+
699
857
  // Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthestDistance`
700
- const farZ = furthestDistance * 1.01;
858
+ const farZ = Math.min(furthestDistance, furthestDistanceHorizon) * 1.01;
701
859
 
702
860
  // The larger the value of nearZ is
703
861
  // - the more depth precision is available for features (good)
@@ -709,10 +867,10 @@ class Transform {
709
867
  const nearZ = this.height / 50;
710
868
 
711
869
  // matrix for conversion from location to GL coordinates (-1 .. 1)
712
- let m = new Float64Array(16) as any;
870
+ m = new Float64Array(16) as any;
713
871
  mat4.perspective(m, this._fov, this.width / this.height, nearZ, farZ);
714
872
 
715
- //Apply center of perspective offset
873
+ // Apply center of perspective offset
716
874
  m[8] = -offset.x * 2 / this.width;
717
875
  m[9] = offset.y * 2 / this.height;
718
876
 
@@ -727,10 +885,18 @@ class Transform {
727
885
  this.mercatorMatrix = mat4.scale([] as any, m, [this.worldSize, this.worldSize, this.worldSize]);
728
886
 
729
887
  // scale vertically to meters per pixel (inverse of ground resolution):
730
- mat4.scale(m, m, [1, 1, mercatorZfromAltitude(1, this.center.lat) * this.worldSize]);
888
+ mat4.scale(m, m, [1, 1, this._pixelPerMeter]);
889
+
890
+ // matrix for conversion from location to screen coordinates in 2D
891
+ this.pixelMatrix = mat4.multiply(new Float64Array(16) as any, this.labelPlaneMatrix, m);
731
892
 
893
+ // matrix for conversion from location to GL coordinates (-1 .. 1)
894
+ mat4.translate(m, m, [0, 0, -this.elevation]); // elevate camera over terrain
732
895
  this.projMatrix = m;
733
- this.invProjMatrix = mat4.invert([] as any, this.projMatrix);
896
+ this.invProjMatrix = mat4.invert([] as any, m);
897
+
898
+ // matrix for conversion from location to screen coordinates in 2D
899
+ this.pixelMatrix3D = mat4.multiply(new Float64Array(16) as any, this.labelPlaneMatrix, m);
734
900
 
735
901
  // Make a second projection matrix that is aligned to a pixel grid for rendering raster tiles.
736
902
  // We're rounding the (floating point) x/y values to achieve to avoid rendering raster images to fractional
@@ -746,20 +912,6 @@ class Transform {
746
912
  mat4.translate(alignedM, alignedM, [ dx > 0.5 ? dx - 1 : dx, dy > 0.5 ? dy - 1 : dy, 0 ]);
747
913
  this.alignedProjMatrix = alignedM;
748
914
 
749
- m = mat4.create();
750
- mat4.scale(m, m, [this.width / 2, -this.height / 2, 1]);
751
- mat4.translate(m, m, [1, -1, 0]);
752
- this.labelPlaneMatrix = m;
753
-
754
- m = mat4.create();
755
- mat4.scale(m, m, [1, -1, 1]);
756
- mat4.translate(m, m, [-1, -1, 0]);
757
- mat4.scale(m, m, [2 / this.width, 2 / this.height, 1]);
758
- this.glCoordMatrix = m;
759
-
760
- // matrix for conversion from location to screen coordinates
761
- this.pixelMatrix = mat4.multiply(new Float64Array(16) as any, this.labelPlaneMatrix, this.projMatrix);
762
-
763
915
  // inverse matrix for conversion from screen coordinaes to location
764
916
  m = mat4.invert(new Float64Array(16) as any, this.pixelMatrix);
765
917
  if (!m) throw new Error('failed to invert matrix');
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ import AttributionControl from './ui/control/attribution_control';
8
8
  import LogoControl from './ui/control/logo_control';
9
9
  import ScaleControl from './ui/control/scale_control';
10
10
  import FullscreenControl from './ui/control/fullscreen_control';
11
+ import TerrainControl from './ui/control/terrain_control';
11
12
  import Popup from './ui/popup';
12
13
  import Marker from './ui/marker';
13
14
  import Style from './style/style';
@@ -46,6 +47,7 @@ const exported = {
46
47
  LogoControl,
47
48
  ScaleControl,
48
49
  FullscreenControl,
50
+ TerrainControl,
49
51
  Popup,
50
52
  Marker,
51
53
  Style,
@@ -9,10 +9,11 @@ import {
9
9
  import type Painter from './painter';
10
10
  import type SourceCache from '../source/source_cache';
11
11
  import type BackgroundStyleLayer from '../style/style_layer/background_style_layer';
12
+ import {OverscaledTileID} from '../source/tile_id';
12
13
 
13
14
  export default drawBackground;
14
15
 
15
- function drawBackground(painter: Painter, sourceCache: SourceCache, layer: BackgroundStyleLayer) {
16
+ function drawBackground(painter: Painter, sourceCache: SourceCache, layer: BackgroundStyleLayer, coords?: Array<OverscaledTileID>) {
16
17
  const color = layer.paint.get('background-color');
17
18
  const opacity = layer.paint.get('background-opacity');
18
19
 
@@ -31,10 +32,8 @@ function drawBackground(painter: Painter, sourceCache: SourceCache, layer: Backg
31
32
  const stencilMode = StencilMode.disabled;
32
33
  const depthMode = painter.depthModeForSublayer(0, pass === 'opaque' ? DepthMode.ReadWrite : DepthMode.ReadOnly);
33
34
  const colorMode = painter.colorModeForRenderPass();
34
-
35
35
  const program = painter.useProgram(image ? 'backgroundPattern' : 'background');
36
-
37
- const tileIDs = transform.coveringTiles({tileSize});
36
+ const tileIDs = coords ? coords : transform.coveringTiles({tileSize, terrain: painter.style.terrain});
38
37
 
39
38
  if (image) {
40
39
  context.activeTexture.set(gl.TEXTURE0);
@@ -43,13 +42,14 @@ function drawBackground(painter: Painter, sourceCache: SourceCache, layer: Backg
43
42
 
44
43
  const crossfade = layer.getCrossfadeParameters();
45
44
  for (const tileID of tileIDs) {
46
- const matrix = painter.transform.calculatePosMatrix(tileID.toUnwrapped());
45
+ const matrix = coords ? tileID.posMatrix : painter.transform.calculatePosMatrix(tileID.toUnwrapped());
47
46
  const uniformValues = image ?
48
47
  backgroundPatternUniformValues(matrix, opacity, painter, image, {tileID, tileSize}, crossfade) :
49
48
  backgroundUniformValues(matrix, opacity, color);
49
+ const terrainData = painter.style.terrain && painter.style.terrain.getTerrainData(tileID);
50
50
 
51
51
  program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled,
52
- uniformValues, layer.id, painter.tileExtentBuffer,
52
+ uniformValues, terrainData, layer.id, painter.tileExtentBuffer,
53
53
  painter.quadTriangleIndexBuffer, painter.tileExtentSegments);
54
54
  }
55
55
  }
@@ -15,6 +15,7 @@ import type VertexBuffer from '../gl/vertex_buffer';
15
15
  import type IndexBuffer from '../gl/index_buffer';
16
16
  import type {UniformValues} from './uniform_binding';
17
17
  import type {CircleUniformsType} from './program/circle_program';
18
+ import type {TerrainData} from '../render/terrain';
18
19
 
19
20
  export default drawCircles;
20
21
 
@@ -24,6 +25,7 @@ type TileRenderState = {
24
25
  layoutVertexBuffer: VertexBuffer;
25
26
  indexBuffer: IndexBuffer;
26
27
  uniformValues: UniformValues<CircleUniformsType>;
28
+ terrainData: TerrainData;
27
29
  };
28
30
 
29
31
  type SegmentsTileRenderState = {
@@ -66,6 +68,7 @@ function drawCircles(painter: Painter, sourceCache: SourceCache, layer: CircleSt
66
68
  const program = painter.useProgram('circle', programConfiguration);
67
69
  const layoutVertexBuffer = bucket.layoutVertexBuffer;
68
70
  const indexBuffer = bucket.indexBuffer;
71
+ const terrainData = painter.style.terrain && painter.style.terrain.getTerrainData(coord);
69
72
  const uniformValues = circleUniformValues(painter, coord, tile, layer);
70
73
 
71
74
  const state: TileRenderState = {
@@ -74,6 +77,7 @@ function drawCircles(painter: Painter, sourceCache: SourceCache, layer: CircleSt
74
77
  layoutVertexBuffer,
75
78
  indexBuffer,
76
79
  uniformValues,
80
+ terrainData
77
81
  };
78
82
 
79
83
  if (sortFeaturesByKey) {
@@ -100,11 +104,11 @@ function drawCircles(painter: Painter, sourceCache: SourceCache, layer: CircleSt
100
104
  }
101
105
 
102
106
  for (const segmentsState of segmentsRenderStates) {
103
- const {programConfiguration, program, layoutVertexBuffer, indexBuffer, uniformValues} = segmentsState.state;
107
+ const {programConfiguration, program, layoutVertexBuffer, indexBuffer, uniformValues, terrainData} = segmentsState.state;
104
108
  const segments = segmentsState.segments;
105
109
 
106
110
  program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled,
107
- uniformValues, layer.id,
111
+ uniformValues, terrainData, layer.id,
108
112
  layoutVertexBuffer, indexBuffer, segments,
109
113
  layer.paint, painter.transform.zoom, programConfiguration);
110
114
  }
@@ -22,6 +22,7 @@ type TileBatch = {
22
22
  circleOffset: number;
23
23
  transform: mat4;
24
24
  invTransform: mat4;
25
+ coord: OverscaledTileID;
25
26
  };
26
27
 
27
28
  let quadTriangles: QuadTriangleArray;
@@ -60,7 +61,8 @@ function drawCollisionDebug(painter: Painter, sourceCache: SourceCache, layer: S
60
61
  circleArray,
61
62
  circleOffset,
62
63
  transform,
63
- invTransform
64
+ invTransform,
65
+ coord
64
66
  });
65
67
 
66
68
  circleCount += circleArray.length / 4; // 4 values per circle
@@ -75,6 +77,7 @@ function drawCollisionDebug(painter: Painter, sourceCache: SourceCache, layer: S
75
77
  posMatrix,
76
78
  painter.transform,
77
79
  tile),
80
+ painter.style.terrain && painter.style.terrain.getTerrainData(coord),
78
81
  layer.id, buffers.layoutVertexBuffer, buffers.indexBuffer,
79
82
  buffers.segments, null, painter.transform.zoom, null, null,
80
83
  buffers.collisionVertexBuffer);
@@ -132,6 +135,7 @@ function drawCollisionDebug(painter: Painter, sourceCache: SourceCache, layer: S
132
135
  painter.colorModeForRenderPass(),
133
136
  CullFaceMode.disabled,
134
137
  uniforms,
138
+ painter.style.terrain && painter.style.terrain.getTerrainData(batch.coord),
135
139
  layer.id,
136
140
  vertexBuffer,
137
141
  indexBuffer,
@@ -77,15 +77,12 @@ function drawDebugTile(painter, sourceCache, coord: OverscaledTileID) {
77
77
  const stencilMode = StencilMode.disabled;
78
78
  const colorMode = painter.colorModeForRenderPass();
79
79
  const id = '$debug';
80
+ const terrainData = painter.style.terrain && painter.style.terrain.getTerrainData(coord);
80
81
 
81
82
  context.activeTexture.set(gl.TEXTURE0);
82
83
  // Bind the empty texture for drawing outlines
83
84
  painter.emptyTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
84
85
 
85
- program.draw(context, gl.LINE_STRIP, depthMode, stencilMode, colorMode, CullFaceMode.disabled,
86
- debugUniformValues(posMatrix, Color.red), id,
87
- painter.debugBuffer, painter.tileBorderIndexBuffer, painter.debugSegments);
88
-
89
86
  const tileRawData = sourceCache.getTileByID(coord.key).latestRawTileData;
90
87
  const tileByteLength = (tileRawData && tileRawData.byteLength) || 0;
91
88
  const tileSizeKb = Math.floor(tileByteLength / 1024);
@@ -99,8 +96,11 @@ function drawDebugTile(painter, sourceCache, coord: OverscaledTileID) {
99
96
  drawTextToOverlay(painter, tileLabel);
100
97
 
101
98
  program.draw(context, gl.TRIANGLES, depthMode, stencilMode, ColorMode.alphaBlended, CullFaceMode.disabled,
102
- debugUniformValues(posMatrix, Color.transparent, scaleRatio), id,
99
+ debugUniformValues(posMatrix, Color.transparent, scaleRatio), null, id,
103
100
  painter.debugBuffer, painter.quadTriangleIndexBuffer, painter.debugSegments);
101
+ program.draw(context, gl.LINE_STRIP, depthMode, stencilMode, colorMode, CullFaceMode.disabled,
102
+ debugUniformValues(posMatrix, Color.red), terrainData, id,
103
+ painter.debugBuffer, painter.tileBorderIndexBuffer, painter.debugSegments);
104
104
  }
105
105
 
106
106
  function drawTextToOverlay(painter: Painter, text: string) {
@@ -81,6 +81,7 @@ function drawFillTiles(painter, sourceCache, layer, coords, depthMode, colorMode
81
81
 
82
82
  const programConfiguration = bucket.programConfigurations.get(layer.id);
83
83
  const program = painter.useProgram(programName, programConfiguration);
84
+ const terrainData = painter.style.terrain && painter.style.terrain.getTerrainData(coord);
84
85
 
85
86
  if (image) {
86
87
  painter.context.activeTexture.set(gl.TEXTURE0);
@@ -96,7 +97,9 @@ function drawFillTiles(painter, sourceCache, layer, coords, depthMode, colorMode
96
97
  if (posTo && posFrom) programConfiguration.setConstantPatternPositions(posTo, posFrom);
97
98
  }
98
99
 
99
- const tileMatrix = painter.translatePosMatrix(coord.posMatrix, tile,
100
+ const terrainCoord = terrainData ? coord : null;
101
+ const posMatrix = terrainCoord ? terrainCoord.posMatrix : coord.posMatrix;
102
+ const tileMatrix = painter.translatePosMatrix(posMatrix, tile,
100
103
  layer.paint.get('fill-translate'), layer.paint.get('fill-translate-anchor'));
101
104
 
102
105
  if (!isOutline) {
@@ -115,7 +118,7 @@ function drawFillTiles(painter, sourceCache, layer, coords, depthMode, colorMode
115
118
  }
116
119
 
117
120
  program.draw(painter.context, drawMode, depthMode,
118
- painter.stencilModeForClipping(coord), colorMode, CullFaceMode.disabled, uniformValues,
121
+ painter.stencilModeForClipping(coord), colorMode, CullFaceMode.disabled, uniformValues, terrainData,
119
122
  layer.id, bucket.layoutVertexBuffer, indexBuffer, segments,
120
123
  layer.paint, painter.transform.zoom, programConfiguration);
121
124
  }