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
@@ -57,11 +57,13 @@ import type {
57
57
  FilterSpecification,
58
58
  StyleSpecification,
59
59
  LightSpecification,
60
- SourceSpecification
60
+ SourceSpecification,
61
+ TerrainSpecification
61
62
  } from '../style-spec/types.g';
62
63
  import type {CustomLayerInterface} from './style_layer/custom_style_layer';
63
64
  import type {Validator} from './validate_style';
64
65
  import type {OverscaledTileID} from '../source/tile_id';
66
+ import Terrain from '../render/terrain';
65
67
 
66
68
  const supportedDiffOperations = pick(diffOperations, [
67
69
  'addLayer',
@@ -102,6 +104,7 @@ export type StyleOptions = {
102
104
  export type StyleSetterOptions = {
103
105
  validate?: boolean;
104
106
  };
107
+
105
108
  /**
106
109
  * @private
107
110
  */
@@ -113,6 +116,7 @@ class Style extends Evented {
113
116
  glyphManager: GlyphManager;
114
117
  lineAtlas: LineAtlas;
115
118
  light: Light;
119
+ terrain: Terrain;
116
120
 
117
121
  _request: Cancelable;
118
122
  _spriteRequest: Cancelable;
@@ -123,6 +127,8 @@ class Style extends Evented {
123
127
  zoomHistory: ZoomHistory;
124
128
  _loaded: boolean;
125
129
  _rtlTextPluginCallback: (a: any) => any;
130
+ _terrainDataCallback: (e: any) => any;
131
+ _terrainfreezeElevationCallback: (e: any) => any;
126
132
  _changed: boolean;
127
133
  _updatedSources: {[_: string]: 'clear' | 'reload'};
128
134
  _updatedLayers: {[_: string]: true};
@@ -278,6 +284,8 @@ class Style extends Evented {
278
284
 
279
285
  this.light = new Light(this.stylesheet.light);
280
286
 
287
+ this.setTerrain(this.stylesheet.terrain);
288
+
281
289
  this.fire(new Event('data', {dataType: 'style'}));
282
290
  this.fire(new Event('style.load'));
283
291
  }
@@ -478,6 +486,52 @@ class Style extends Evented {
478
486
  this._changedImages = {};
479
487
  }
480
488
 
489
+ /**
490
+ * Loads a 3D terrain mesh, based on a "raster-dem" source.
491
+ * @param {TerrainSpecification} [options] Options object.
492
+ */
493
+ setTerrain(options?: TerrainSpecification) {
494
+ this._checkLoaded();
495
+
496
+ // clear event handlers
497
+ if (this._terrainDataCallback) this.off('data', this._terrainDataCallback);
498
+ if (this._terrainfreezeElevationCallback) this.map.off('freezeElevation', this._terrainfreezeElevationCallback);
499
+
500
+ // remove terrain
501
+ if (!options) {
502
+ this.terrain = null;
503
+ this.map.transform.updateElevation(this.terrain);
504
+
505
+ // add terrain
506
+ } else {
507
+ const sourceCache = this.sourceCaches[options.source];
508
+ if (!sourceCache) throw new Error(`cannot load terrain, because there exists no source with ID: ${options.source}`);
509
+ this.terrain = new Terrain(this, sourceCache, options);
510
+ this.map.transform.updateElevation(this.terrain);
511
+ this._terrainfreezeElevationCallback = (e: any) => {
512
+ if (e.freeze) {
513
+ this.map.transform.freezeElevation = true;
514
+ } else {
515
+ this.map.transform.freezeElevation = false;
516
+ this.map.transform.recalculateZoom(this.terrain);
517
+ }
518
+ };
519
+ this._terrainDataCallback = e => {
520
+ if (!e.tile) return;
521
+ if (e.sourceId === options.source) {
522
+ this.map.transform.updateElevation(this.terrain);
523
+ this.terrain.rememberForRerender(e.sourceId, e.tile.tileID);
524
+ } else if (e.source.type === 'geojson') {
525
+ this.terrain.rememberForRerender(e.sourceId, e.tile.tileID);
526
+ }
527
+ };
528
+ this.on('data', this._terrainDataCallback);
529
+ this.map.on('freezeElevation', this._terrainfreezeElevationCallback);
530
+ }
531
+
532
+ this.map.fire(new Event('terrain', {terrain: options}));
533
+ }
534
+
481
535
  /**
482
536
  * Update this style's state to match the given style JSON, performing only
483
537
  * the necessary mutations.
@@ -1259,7 +1313,7 @@ class Style extends Evented {
1259
1313
 
1260
1314
  _updateSources(transform: Transform) {
1261
1315
  for (const id in this.sourceCaches) {
1262
- this.sourceCaches[id].update(transform);
1316
+ this.sourceCaches[id].update(transform, this.terrain);
1263
1317
  }
1264
1318
  }
1265
1319
 
@@ -1300,7 +1354,7 @@ class Style extends Evented {
1300
1354
  forceFullPlacement = forceFullPlacement || this._layerOrderChanged || fadeDuration === 0;
1301
1355
 
1302
1356
  if (forceFullPlacement || !this.pauseablePlacement || (this.pauseablePlacement.isDone() && !this.placement.stillRecent(browser.now(), transform.zoom))) {
1303
- this.pauseablePlacement = new PauseablePlacement(transform, this._order, forceFullPlacement, showCollisionBoxes, fadeDuration, crossSourceCollisions, this.placement);
1357
+ this.pauseablePlacement = new PauseablePlacement(transform, this.terrain, this._order, forceFullPlacement, showCollisionBoxes, fadeDuration, crossSourceCollisions, this.placement);
1304
1358
  this._layerOrderChanged = false;
1305
1359
  }
1306
1360
 
@@ -15,6 +15,7 @@ type ValidateStyle = {
15
15
  source: Validator;
16
16
  layer: Validator;
17
17
  light: Validator;
18
+ terrain: Validator;
18
19
  filter: Validator;
19
20
  paintProperty: Validator;
20
21
  layoutProperty: Validator;
@@ -25,6 +26,7 @@ export const validateStyle = (validateStyleMin as ValidateStyle);
25
26
 
26
27
  export const validateSource = validateStyle.source;
27
28
  export const validateLight = validateStyle.light;
29
+ export const validateTerrain = validateStyle.terrain;
28
30
  export const validateFilter = validateStyle.filter;
29
31
  export const validatePaintProperty = validateStyle.paintProperty;
30
32
  export const validateLayoutProperty = validateStyle.layoutProperty;
@@ -1,3 +1,9 @@
1
+ ## 16.0.0
2
+
3
+ ### Breaking changes
4
+
5
+ * index.js has been renamed to index.cjs to fix the 'is treated as an ES module file' error in node 12+ [#1223](https://github.com/maplibre/maplibre-gl-js/pull/1223)
6
+
1
7
  ## 15.1.0
2
8
 
3
9
  ### ✨ Features and improvements
@@ -5,7 +5,7 @@ export default class ValidationError {
5
5
  identifier: string;
6
6
  line: number;
7
7
 
8
- constructor(key: string, value: {
8
+ constructor(key: string, value: any & {
9
9
  __line__: number;
10
10
  }, message: string, identifier?: string | null) {
11
11
  this.message = (key ? `${key}: ` : '') + message;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@maplibre/maplibre-gl-style-spec",
3
3
  "description": "a specification for maplibre gl styles",
4
- "version": "15.1.0",
4
+ "version": "16.0.0",
5
5
  "author": "MapLibre",
6
6
  "keywords": [
7
7
  "mapbox",
@@ -12,7 +12,7 @@
12
12
  "maplibre-gl-js"
13
13
  ],
14
14
  "license": "ISC",
15
- "main": "./dist/index.js",
15
+ "main": "./dist/index.cjs",
16
16
  "module": "./dist/index.mjs",
17
17
  "type": "module",
18
18
  "scripts": {
@@ -57,6 +57,15 @@
57
57
  "intensity": 0.4
58
58
  }
59
59
  },
60
+ "terrain": {
61
+ "type": "terrain",
62
+ "doc": "The terrain configuration.",
63
+ "example": {
64
+ "source": "raster-dem-source",
65
+ "exaggeration": 0.5,
66
+ "elevationOffset": 100
67
+ }
68
+ },
60
69
  "sources": {
61
70
  "required": true,
62
71
  "type": "sources",
@@ -3810,6 +3819,39 @@
3810
3819
  }
3811
3820
  }
3812
3821
  },
3822
+ "terrain": {
3823
+ "source": {
3824
+ "type": "string",
3825
+ "doc": "The source for the terrain data.",
3826
+ "required": true,
3827
+ "sdk-support": {
3828
+ "basic functionality": {
3829
+ "js": "2.2.0"
3830
+ }
3831
+ }
3832
+ },
3833
+ "exaggeration": {
3834
+ "type": "number",
3835
+ "minimum": 0,
3836
+ "doc": "The exaggeration of the terrain - how high it will look.",
3837
+ "default": 1.0,
3838
+ "sdk-support": {
3839
+ "basic functionality": {
3840
+ "js": "2.2.0"
3841
+ }
3842
+ }
3843
+ },
3844
+ "elevationOffset": {
3845
+ "type": "number",
3846
+ "doc": "The elevation offset.",
3847
+ "default": 450,
3848
+ "sdk-support": {
3849
+ "basic functionality": {
3850
+ "js": "2.2.0"
3851
+ }
3852
+ }
3853
+ }
3854
+ },
3813
3855
  "paint": [
3814
3856
  "paint_fill",
3815
3857
  "paint_line",
@@ -85,6 +85,7 @@ export type StyleSpecification = {
85
85
  "bearing"?: number,
86
86
  "pitch"?: number,
87
87
  "light"?: LightSpecification,
88
+ "terrain"?: TerrainSpecification,
88
89
  "sources": {[_: string]: SourceSpecification},
89
90
  "sprite"?: string,
90
91
  "glyphs"?: string,
@@ -99,6 +100,12 @@ export type LightSpecification = {
99
100
  "intensity"?: PropertyValueSpecification<number>
100
101
  };
101
102
 
103
+ export type TerrainSpecification = {
104
+ "source": string,
105
+ "exaggeration"?: number,
106
+ "elevationOffset"?: number
107
+ };
108
+
102
109
  export type VectorSourceSpecification = {
103
110
  "type": "vector",
104
111
  "url"?: string,
@@ -17,6 +17,7 @@ import validateFilter from './validate_filter';
17
17
  import validateLayer from './validate_layer';
18
18
  import validateSource from './validate_source';
19
19
  import validateLight from './validate_light';
20
+ import validateTerrain from './validate_terrain';
20
21
  import validateString from './validate_string';
21
22
  import validateFormatted from './validate_formatted';
22
23
  import validateImage from './validate_image';
@@ -37,6 +38,7 @@ const VALIDATORS = {
37
38
  'object': validateObject,
38
39
  'source': validateSource,
39
40
  'light': validateLight,
41
+ 'terrain': validateTerrain,
40
42
  'string': validateString,
41
43
  'formatted': validateFormatted,
42
44
  'resolvedImage': validateImage
@@ -0,0 +1,46 @@
1
+ import validateTerrain from './validate_terrain';
2
+ import v8 from '../reference/v8.json';
3
+
4
+ describe('Validate Terrain', () => {
5
+ test('Should return error in case terrain is not an object', () => {
6
+ const errors = validateTerrain({value: 1 as any, styleSpec: v8, style: {} as any});
7
+ expect(errors).toHaveLength(1);
8
+ expect(errors[0].message).toContain('number');
9
+ expect(errors[0].message).toContain('object');
10
+ expect(errors[0].message).toContain('terrain');
11
+ });
12
+
13
+ test('Should return error in case terrain source is not a string', () => {
14
+ const errors = validateTerrain({value: {source: 1 as any}, styleSpec: v8, style: {} as any});
15
+ expect(errors).toHaveLength(1);
16
+ expect(errors[0].message).toContain('number');
17
+ expect(errors[0].message).toContain('string');
18
+ expect(errors[0].message).toContain('source');
19
+ });
20
+
21
+ test('Should return error in case of unknown property', () => {
22
+ const errors = validateTerrain({value: {a: 1} as any, styleSpec: v8, style: {} as any});
23
+ expect(errors).toHaveLength(1);
24
+ expect(errors[0].message).toContain('a');
25
+ expect(errors[0].message).toContain('unknown');
26
+ });
27
+
28
+ test('Should return errors according to spec violations', () => {
29
+ const errors = validateTerrain({value: {source: 1 as any, exaggeration: {} as any, elevationOffset: 'ex2' as any}, styleSpec: v8, style: {} as any});
30
+ expect(errors).toHaveLength(3);
31
+ expect(errors[0].message).toContain('number');
32
+ expect(errors[0].message).toContain('string');
33
+ expect(errors[0].message).toContain('source');
34
+ expect(errors[1].message).toContain('number');
35
+ expect(errors[1].message).toContain('object');
36
+ expect(errors[1].message).toContain('exaggeration');
37
+ expect(errors[2].message).toContain('number');
38
+ expect(errors[2].message).toContain('string');
39
+ expect(errors[2].message).toContain('elevationOffset');
40
+ });
41
+
42
+ test('Should pass if everything is according to spec', () => {
43
+ const errors = validateTerrain({value: {source: 'source-id', elevationOffset: 1, exaggeration: 0.2}, styleSpec: v8, style: {} as any});
44
+ expect(errors).toHaveLength(0);
45
+ });
46
+ });
@@ -0,0 +1,41 @@
1
+ import ValidationError from '../error/validation_error';
2
+ import getType from '../util/get_type';
3
+ import validate from './validate';
4
+ import type {StyleSpecification, TerrainSpecification} from '../types.g';
5
+ import type v8 from '../reference/v8.json';
6
+
7
+ export default function validateTerrain(
8
+ options: {value: TerrainSpecification; styleSpec: typeof v8; style: StyleSpecification}
9
+ ): ValidationError[] {
10
+
11
+ const terrain = options.value;
12
+ const styleSpec = options.styleSpec;
13
+ const terrainSpec = styleSpec.terrain;
14
+ const style = options.style;
15
+
16
+ let errors = [];
17
+
18
+ const rootType = getType(terrain);
19
+ if (terrain === undefined) {
20
+ return errors;
21
+ } else if (rootType !== 'object') {
22
+ errors = errors.concat([new ValidationError('terrain', terrain, `object expected, ${rootType} found`)]);
23
+ return errors;
24
+ }
25
+
26
+ for (const key in terrain) {
27
+ if (terrainSpec[key]) {
28
+ errors = errors.concat(validate({
29
+ key,
30
+ value: terrain[key],
31
+ valueSpec: terrainSpec[key],
32
+ style,
33
+ styleSpec
34
+ }));
35
+ } else {
36
+ errors = errors.concat([new ValidationError(key, terrain[key], `unknown property "${key}"`)]);
37
+ }
38
+ }
39
+
40
+ return errors;
41
+ }
@@ -6,6 +6,7 @@ import validateGlyphsURL from './validate/validate_glyphs_url';
6
6
 
7
7
  import validateSource from './validate/validate_source';
8
8
  import validateLight from './validate/validate_light';
9
+ import validateTerrain from './validate/validate_terrain';
9
10
  import validateLayer from './validate/validate_layer';
10
11
  import validateFilter from './validate/validate_filter';
11
12
  import validatePaintProperty from './validate/validate_paint_property';
@@ -58,6 +59,7 @@ function validateStyleMin(style, styleSpec = latestStyleSpec) {
58
59
 
59
60
  validateStyleMin.source = wrapCleanErrors(validateSource);
60
61
  validateStyleMin.light = wrapCleanErrors(validateLight);
62
+ validateStyleMin.terrain = wrapCleanErrors(validateTerrain);
61
63
  validateStyleMin.layer = wrapCleanErrors(validateLayer);
62
64
  validateStyleMin.filter = wrapCleanErrors(validateFilter);
63
65
  validateStyleMin.paintProperty = wrapCleanErrors(validatePaintProperty);
@@ -33,6 +33,7 @@ export default function validateStyle(style, styleSpec = v8) {
33
33
 
34
34
  export const source = validateStyleMin.source;
35
35
  export const light = validateStyleMin.light;
36
+ export const terrain = validateStyleMin.terrain;
36
37
  export const layer = validateStyleMin.layer;
37
38
  export const filter = validateStyleMin.filter;
38
39
  export const paintProperty = validateStyleMin.paintProperty;
@@ -55,6 +55,10 @@ class CollisionIndex {
55
55
  gridRightBoundary: number;
56
56
  gridBottomBoundary: number;
57
57
 
58
+ // With perspectiveRatio the fontsize is calculated for tilted maps (near = bigger, far = smaller).
59
+ // The cutoff defines a threshold to no longer render labels near the horizon.
60
+ perspectiveRatioCutoff: number;
61
+
58
62
  constructor(
59
63
  transform: Transform,
60
64
  grid = new GridIndex<FeatureKey>(transform.width + 2 * viewportPadding, transform.height + 2 * viewportPadding, 25),
@@ -70,6 +74,8 @@ class CollisionIndex {
70
74
  this.screenBottomBoundary = transform.height + viewportPadding;
71
75
  this.gridRightBoundary = transform.width + 2 * viewportPadding;
72
76
  this.gridBottomBoundary = transform.height + 2 * viewportPadding;
77
+
78
+ this.perspectiveRatioCutoff = 0.6;
73
79
  }
74
80
 
75
81
  placeCollisionBox(
@@ -77,12 +83,13 @@ class CollisionIndex {
77
83
  overlapMode: OverlapMode,
78
84
  textPixelRatio: number,
79
85
  posMatrix: mat4,
80
- collisionGroupPredicate?: (key: FeatureKey) => boolean
86
+ collisionGroupPredicate?: (key: FeatureKey) => boolean,
87
+ getElevation?: (x: number, y: number) => number
81
88
  ): {
82
89
  box: Array<number>;
83
90
  offscreen: boolean;
84
91
  } {
85
- const projectedPoint = this.projectAndGetPerspectiveRatio(posMatrix, collisionBox.anchorPointX, collisionBox.anchorPointY);
92
+ const projectedPoint = this.projectAndGetPerspectiveRatio(posMatrix, collisionBox.anchorPointX, collisionBox.anchorPointY, getElevation);
86
93
  const tileToViewport = textPixelRatio * projectedPoint.perspectiveRatio;
87
94
  const tlX = collisionBox.x1 * tileToViewport + projectedPoint.point.x;
88
95
  const tlY = collisionBox.y1 * tileToViewport + projectedPoint.point.y;
@@ -90,7 +97,8 @@ class CollisionIndex {
90
97
  const brY = collisionBox.y2 * tileToViewport + projectedPoint.point.y;
91
98
 
92
99
  if (!this.isInsideGrid(tlX, tlY, brX, brY) ||
93
- (overlapMode !== 'always' && this.grid.hitTest(tlX, tlY, brX, brY, overlapMode, collisionGroupPredicate))) {
100
+ (overlapMode !== 'always' && this.grid.hitTest(tlX, tlY, brX, brY, overlapMode, collisionGroupPredicate)) ||
101
+ projectedPoint.perspectiveRatio < this.perspectiveRatioCutoff) {
94
102
  return {
95
103
  box: [],
96
104
  offscreen: false
@@ -116,7 +124,8 @@ class CollisionIndex {
116
124
  pitchWithMap: boolean,
117
125
  collisionGroupPredicate: (key: FeatureKey) => boolean,
118
126
  circlePixelDiameter: number,
119
- textPixelPadding: number
127
+ textPixelPadding: number,
128
+ getElevation: (x: number, y: number) => number
120
129
  ): {
121
130
  circles: Array<number>;
122
131
  offscreen: boolean;
@@ -125,12 +134,12 @@ class CollisionIndex {
125
134
  const placedCollisionCircles = [];
126
135
 
127
136
  const tileUnitAnchorPoint = new Point(symbol.anchorX, symbol.anchorY);
128
- const screenAnchorPoint = projection.project(tileUnitAnchorPoint, posMatrix);
137
+ const screenAnchorPoint = projection.project(tileUnitAnchorPoint, posMatrix, getElevation);
129
138
  const perspectiveRatio = projection.getPerspectiveRatio(this.transform.cameraToCenterDistance, screenAnchorPoint.signedDistanceFromCamera);
130
139
  const labelPlaneFontSize = pitchWithMap ? fontSize / perspectiveRatio : fontSize * perspectiveRatio;
131
140
  const labelPlaneFontScale = labelPlaneFontSize / ONE_EM;
132
141
 
133
- const labelPlaneAnchorPoint = projection.project(tileUnitAnchorPoint, labelPlaneMatrix).point;
142
+ const labelPlaneAnchorPoint = projection.project(tileUnitAnchorPoint, labelPlaneMatrix, getElevation).point;
134
143
 
135
144
  const projectionCache = {};
136
145
  const lineOffsetX = symbol.lineOffsetX * labelPlaneFontScale;
@@ -148,7 +157,8 @@ class CollisionIndex {
148
157
  lineVertexArray,
149
158
  labelPlaneMatrix,
150
159
  projectionCache,
151
- false);
160
+ false,
161
+ getElevation);
152
162
 
153
163
  let collisionDetected = false;
154
164
  let inGrid = false;
@@ -178,7 +188,7 @@ class CollisionIndex {
178
188
 
179
189
  // The path might need to be converted into screen space if a pitched map is used as the label space
180
190
  if (labelToScreenMatrix) {
181
- const screenSpacePath = projectedPath.map(p => projection.project(p, labelToScreenMatrix));
191
+ const screenSpacePath = projectedPath.map(p => projection.project(p, labelToScreenMatrix, getElevation));
182
192
 
183
193
  // Do not try to place collision circles if even of the points is behind the camera.
184
194
  // This is a plausible scenario with big camera pitch angles
@@ -265,7 +275,7 @@ class CollisionIndex {
265
275
  }
266
276
 
267
277
  return {
268
- circles: ((!showCollisionCircles && collisionDetected) || !inGrid) ? [] : placedCollisionCircles,
278
+ circles: ((!showCollisionCircles && collisionDetected) || !inGrid || perspectiveRatio < this.perspectiveRatioCutoff) ? [] : placedCollisionCircles,
269
279
  offscreen: entirelyOffscreen,
270
280
  collisionDetected
271
281
  };
@@ -354,9 +364,15 @@ class CollisionIndex {
354
364
  }
355
365
  }
356
366
 
357
- projectAndGetPerspectiveRatio(posMatrix: mat4, x: number, y: number) {
358
- const p = [x, y, 0, 1] as vec4;
359
- projection.xyTransformMat4(p, p, posMatrix);
367
+ projectAndGetPerspectiveRatio(posMatrix: mat4, x: number, y: number, getElevation: (x: number, y: number) => number) {
368
+ let p;
369
+ if (getElevation) { // slow because of handle z-index
370
+ p = [x, y, getElevation(x, y), 1] as vec4;
371
+ vec4.transformMat4(p, p, posMatrix);
372
+ } else { // fast because of ignore z-index
373
+ p = [x, y, 0, 1] as vec4;
374
+ projection.xyTransformMat4(p, p, posMatrix);
375
+ }
360
376
  const a = new Point(
361
377
  (((p[0] / p[3] + 1) / 2) * this.transform.width) + viewportPadding,
362
378
  (((-p[1] / p[3] + 1) / 2) * this.transform.height) + viewportPadding
@@ -22,6 +22,7 @@ import type {CollisionBoxArray, CollisionVertexArray, SymbolInstance} from '../d
22
22
  import type FeatureIndex from '../data/feature_index';
23
23
  import type {OverscaledTileID} from '../source/tile_id';
24
24
  import type {TextAnchor} from './symbol_layout';
25
+ import Terrain from '../render/terrain';
25
26
 
26
27
  class OpacityState {
27
28
  opacity: number;
@@ -210,6 +211,7 @@ export type CrossTileID = string | number;
210
211
 
211
212
  export class Placement {
212
213
  transform: Transform;
214
+ terrain: Terrain;
213
215
  collisionIndex: CollisionIndex;
214
216
  placements: {
215
217
  [_ in CrossTileID]: JointPlacement;
@@ -238,8 +240,9 @@ export class Placement {
238
240
  [k in any]: CollisionCircleArray;
239
241
  };
240
242
 
241
- constructor(transform: Transform, fadeDuration: number, crossSourceCollisions: boolean, prevPlacement?: Placement) {
243
+ constructor(transform: Transform, terrain: Terrain, fadeDuration: number, crossSourceCollisions: boolean, prevPlacement?: Placement) {
242
244
  this.transform = transform.clone();
245
+ this.terrain = terrain;
243
246
  this.collisionIndex = new CollisionIndex(this.transform);
244
247
  this.placements = {};
245
248
  this.opacities = {};
@@ -350,7 +353,8 @@ export class Placement {
350
353
  symbolInstance: SymbolInstance,
351
354
  bucket: SymbolBucket,
352
355
  orientation: number,
353
- iconBox?: SingleCollisionBox | null
356
+ iconBox?: SingleCollisionBox | null,
357
+ getElevation?: (x: number, y: number) => number
354
358
  ): {
355
359
  shift: Point;
356
360
  placedGlyphBoxes: {
@@ -366,14 +370,14 @@ export class Placement {
366
370
  shiftVariableCollisionBox(
367
371
  textBox, shift.x, shift.y,
368
372
  rotateWithMap, pitchWithMap, this.transform.angle),
369
- textOverlapMode, textPixelRatio, posMatrix, collisionGroup.predicate);
373
+ textOverlapMode, textPixelRatio, posMatrix, collisionGroup.predicate, getElevation);
370
374
 
371
375
  if (iconBox) {
372
376
  const placedIconBoxes = this.collisionIndex.placeCollisionBox(
373
377
  shiftVariableCollisionBox(
374
378
  iconBox, shift.x, shift.y,
375
379
  rotateWithMap, pitchWithMap, this.transform.angle),
376
- textOverlapMode, textPixelRatio, posMatrix, collisionGroup.predicate);
380
+ textOverlapMode, textPixelRatio, posMatrix, collisionGroup.predicate, getElevation);
377
381
  if (placedIconBoxes.box.length === 0) return;
378
382
  }
379
383
 
@@ -489,6 +493,14 @@ export class Placement {
489
493
  verticalTextFeatureIndex = collisionArrays.verticalTextFeatureIndex;
490
494
  }
491
495
 
496
+ // update elevation of collisionArrays
497
+ const tileID = this.retainedQueryData[bucket.bucketInstanceId].tileID;
498
+ const getElevation = this.terrain ? (x: number, y: number) => this.terrain.getElevation(tileID, x, y) : null;
499
+ for (const boxType of ['textBox', 'verticalTextBox', 'iconBox', 'verticalIconBox']) {
500
+ const box = collisionArrays[boxType];
501
+ if (box) box.elevation = getElevation ? getElevation(box.anchorPointX, box.anchorPointY) : 0;
502
+ }
503
+
492
504
  const textBox = collisionArrays.textBox;
493
505
  if (textBox) {
494
506
 
@@ -528,7 +540,9 @@ export class Placement {
528
540
  textOverlapMode,
529
541
  textPixelRatio,
530
542
  posMatrix,
531
- collisionGroup.predicate);
543
+ collisionGroup.predicate,
544
+ getElevation
545
+ );
532
546
  if (placedFeature && placedFeature.box && placedFeature.box.length) {
533
547
  this.markUsedOrientation(bucket, orientation, symbolInstance);
534
548
  this.placedOrientations[symbolInstance.crossTileID] = orientation;
@@ -583,7 +597,7 @@ export class Placement {
583
597
  const result = this.attemptAnchorPlacement(
584
598
  anchor, collisionTextBox, width, height,
585
599
  textBoxScale, rotateWithMap, pitchWithMap, textPixelRatio, posMatrix,
586
- collisionGroup, overlapMode, symbolInstance, bucket, orientation, variableIconBox);
600
+ collisionGroup, overlapMode, symbolInstance, bucket, orientation, variableIconBox, getElevation);
587
601
 
588
602
  if (result) {
589
603
  placedBox = result.placedGlyphBoxes;
@@ -658,7 +672,9 @@ export class Placement {
658
672
  pitchWithMap,
659
673
  collisionGroup.predicate,
660
674
  circlePixelDiameter,
661
- textPixelPadding);
675
+ textPixelPadding,
676
+ getElevation
677
+ );
662
678
 
663
679
  assert(!placedGlyphCircles.circles.length || (!placedGlyphCircles.collisionDetected || showCollisionBoxes));
664
680
  // If text-overlap is set to 'always', force "placedCircles" to true
@@ -674,7 +690,6 @@ export class Placement {
674
690
  }
675
691
 
676
692
  if (collisionArrays.iconBox) {
677
-
678
693
  const placeIconFeature = iconBox => {
679
694
  const shiftedIconBox = hasIconTextFit && shift ?
680
695
  shiftVariableCollisionBox(
@@ -682,7 +697,7 @@ export class Placement {
682
697
  rotateWithMap, pitchWithMap, this.transform.angle) :
683
698
  iconBox;
684
699
  return this.collisionIndex.placeCollisionBox(shiftedIconBox,
685
- iconOverlapMode, textPixelRatio, posMatrix, collisionGroup.predicate);
700
+ iconOverlapMode, textPixelRatio, posMatrix, collisionGroup.predicate, getElevation);
686
701
  };
687
702
 
688
703
  if (placedVerticalText && placedVerticalText.box && placedVerticalText.box.length && collisionArrays.verticalIconBox) {