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.
- package/build/generate-style-spec.ts +2 -0
- package/dist/maplibre-gl-csp-worker.js +1 -1
- package/dist/maplibre-gl-csp-worker.js.map +1 -1
- package/dist/maplibre-gl-csp.js +1 -1
- package/dist/maplibre-gl-csp.js.map +1 -1
- package/dist/maplibre-gl-dev.js +1517 -290
- package/dist/maplibre-gl.css +1 -1
- package/dist/maplibre-gl.d.ts +3476 -3112
- package/dist/maplibre-gl.js +4 -4
- package/dist/maplibre-gl.js.map +1 -1
- package/dist/package.json +1 -1
- package/package.json +15 -15
- package/src/css/maplibre-gl.css +20 -0
- package/src/css/svg/maplibregl-ctrl-terrain.svg +7 -0
- package/src/data/bucket/fill_extrusion_attributes.ts +4 -0
- package/src/data/bucket/fill_extrusion_bucket.ts +28 -4
- package/src/data/dem_data.test.ts +14 -1
- package/src/data/dem_data.ts +13 -0
- package/src/geo/transform.test.ts +56 -1
- package/src/geo/transform.ts +199 -47
- package/src/index.ts +2 -0
- package/src/render/draw_background.ts +6 -6
- package/src/render/draw_circle.ts +6 -2
- package/src/render/draw_collision_debug.ts +5 -1
- package/src/render/draw_debug.ts +5 -5
- package/src/render/draw_fill.ts +5 -2
- package/src/render/draw_fill_extrusion.ts +3 -2
- package/src/render/draw_heatmap.ts +2 -3
- package/src/render/draw_hillshade.ts +8 -7
- package/src/render/draw_line.ts +7 -5
- package/src/render/draw_raster.ts +8 -6
- package/src/render/draw_symbol.test.ts +34 -10
- package/src/render/draw_symbol.ts +23 -12
- package/src/render/draw_terrain.ts +123 -0
- package/src/render/painter.ts +52 -14
- package/src/render/program/hillshade_program.ts +7 -2
- package/src/render/program/line_program.ts +24 -10
- package/src/render/program/program_uniforms.ts +5 -1
- package/src/render/program/terrain_program.ts +83 -0
- package/src/render/program.ts +29 -5
- package/src/render/render_to_texture.test.ts +41 -0
- package/src/render/render_to_texture.ts +154 -0
- package/src/render/terrain.test.ts +53 -0
- package/src/render/terrain.ts +369 -0
- package/src/render/vertex_array_object.ts +21 -4
- package/src/shaders/_prelude.vertex.glsl +76 -0
- package/src/shaders/_prelude.vertex.glsl.g.ts +1 -1
- package/src/shaders/circle.fragment.glsl +2 -1
- package/src/shaders/circle.fragment.glsl.g.ts +1 -1
- package/src/shaders/circle.vertex.glsl +6 -2
- package/src/shaders/circle.vertex.glsl.g.ts +1 -1
- package/src/shaders/collision_box.vertex.glsl +1 -1
- package/src/shaders/collision_box.vertex.glsl.g.ts +1 -1
- package/src/shaders/debug.vertex.glsl +1 -1
- package/src/shaders/debug.vertex.glsl.g.ts +1 -1
- package/src/shaders/fill_extrusion.vertex.glsl +16 -2
- package/src/shaders/fill_extrusion.vertex.glsl.g.ts +1 -1
- package/src/shaders/fill_extrusion_pattern.vertex.glsl +15 -2
- package/src/shaders/fill_extrusion_pattern.vertex.glsl.g.ts +1 -1
- package/src/shaders/line.vertex.glsl +7 -3
- package/src/shaders/line.vertex.glsl.g.ts +1 -1
- package/src/shaders/line_gradient.vertex.glsl +7 -3
- package/src/shaders/line_gradient.vertex.glsl.g.ts +1 -1
- package/src/shaders/line_pattern.vertex.glsl +7 -3
- package/src/shaders/line_pattern.vertex.glsl.g.ts +1 -1
- package/src/shaders/line_sdf.vertex.glsl +7 -4
- package/src/shaders/line_sdf.vertex.glsl.g.ts +1 -1
- package/src/shaders/shaders.ts +11 -1
- package/src/shaders/symbol_icon.vertex.glsl +8 -8
- package/src/shaders/symbol_icon.vertex.glsl.g.ts +1 -1
- package/src/shaders/symbol_sdf.vertex.glsl +8 -5
- package/src/shaders/symbol_sdf.vertex.glsl.g.ts +1 -1
- package/src/shaders/symbol_text_and_icon.vertex.glsl +8 -5
- package/src/shaders/symbol_text_and_icon.vertex.glsl.g.ts +1 -1
- package/src/shaders/terrain.fragment.glsl +7 -0
- package/src/shaders/terrain.fragment.glsl.g.ts +2 -0
- package/src/shaders/terrain.vertex.glsl +12 -0
- package/src/shaders/terrain.vertex.glsl.g.ts +2 -0
- package/src/shaders/terrain_coords.fragment.glsl +11 -0
- package/src/shaders/terrain_coords.fragment.glsl.g.ts +2 -0
- package/src/shaders/terrain_depth.fragment.glsl +15 -0
- package/src/shaders/terrain_depth.fragment.glsl.g.ts +2 -0
- package/src/source/canvas_source.test.ts +1 -1
- package/src/source/geojson_source.test.ts +25 -0
- package/src/source/geojson_source.ts +1 -8
- package/src/source/geojson_worker_source.test.ts +19 -23
- package/src/source/geojson_worker_source.ts +19 -70
- package/src/source/raster_dem_tile_source.ts +4 -3
- package/src/source/raster_dem_tile_worker_source.ts +0 -1
- package/src/source/source_cache.test.ts +83 -0
- package/src/source/source_cache.ts +72 -11
- package/src/source/terrain_source_cache.test.ts +89 -0
- package/src/source/terrain_source_cache.ts +201 -0
- package/src/source/tile.ts +15 -0
- package/src/source/tile_id.ts +9 -0
- package/src/style/pauseable_placement.ts +3 -1
- package/src/style/style.test.ts +16 -0
- package/src/style/style.ts +57 -3
- package/src/style/validate_style.ts +2 -0
- package/src/style-spec/CHANGELOG.md +6 -0
- package/src/style-spec/error/validation_error.ts +1 -1
- package/src/style-spec/package.json +2 -2
- package/src/style-spec/reference/v8.json +42 -0
- package/src/style-spec/types.g.ts +7 -0
- package/src/style-spec/validate/validate.ts +2 -0
- package/src/style-spec/validate/validate_terrain.test.ts +46 -0
- package/src/style-spec/validate/validate_terrain.ts +41 -0
- package/src/style-spec/validate_style.min.ts +2 -0
- package/src/style-spec/validate_style.ts +1 -0
- package/src/symbol/collision_index.ts +28 -12
- package/src/symbol/placement.ts +24 -9
- package/src/symbol/projection.ts +42 -27
- package/src/ui/camera.ts +2 -0
- package/src/ui/control/terrain_control.ts +77 -0
- package/src/ui/default_locale.ts +3 -2
- package/src/ui/events.ts +18 -3
- package/src/ui/handler_manager.ts +33 -3
- package/src/ui/map.ts +36 -6
- package/src/ui/marker.test.ts +21 -0
- package/src/ui/marker.ts +14 -0
- package/src/util/primitives.ts +14 -11
package/src/style/style.ts
CHANGED
|
@@ -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;
|
|
@@ -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": "
|
|
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.
|
|
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
|
-
|
|
359
|
-
|
|
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
|
package/src/symbol/placement.ts
CHANGED
|
@@ -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) {
|