maplibre-gl 3.2.2 → 3.3.1
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/LICENSE.txt +1 -1
- package/README.md +2 -1
- package/build/generate-dist-package.js +7 -2
- package/build/generate-struct-arrays.ts +3 -1
- package/build/generate-style-code.ts +7 -8
- package/build/generate-typings.ts +1 -1
- package/dist/LICENSE.txt +116 -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 +472 -195
- package/dist/maplibre-gl-dev.js.map +1 -1
- package/dist/maplibre-gl.d.ts +58 -16
- package/dist/maplibre-gl.js +3 -3
- package/dist/maplibre-gl.js.map +1 -1
- package/dist/package.json +1 -1
- package/package.json +24 -23
- package/src/data/array_types.g.ts +78 -14
- package/src/data/bucket/symbol_attributes.ts +7 -1
- package/src/data/bucket/symbol_bucket.ts +4 -1
- package/src/render/draw_symbol.ts +8 -9
- package/src/render/program.ts +15 -0
- package/src/source/vector_tile_source.ts +0 -1
- package/src/source/video_source.ts +4 -0
- package/src/style/properties.ts +4 -0
- package/src/style/style.ts +14 -8
- package/src/style/style_layer/background_style_layer_properties.g.ts +1 -6
- package/src/style/style_layer/circle_style_layer_properties.g.ts +1 -6
- package/src/style/style_layer/fill_extrusion_style_layer_properties.g.ts +1 -6
- package/src/style/style_layer/fill_style_layer_properties.g.ts +1 -6
- package/src/style/style_layer/heatmap_style_layer_properties.g.ts +1 -6
- package/src/style/style_layer/hillshade_style_layer_properties.g.ts +1 -6
- package/src/style/style_layer/line_style_layer_properties.g.ts +1 -6
- package/src/style/style_layer/raster_style_layer_properties.g.ts +1 -6
- package/src/style/style_layer/symbol_style_layer_properties.g.ts +4 -6
- package/src/style/style_layer/variable_text_anchor.test.ts +117 -0
- package/src/style/style_layer/variable_text_anchor.ts +163 -0
- package/src/symbol/placement.ts +52 -40
- package/src/symbol/symbol_layout.ts +42 -116
- package/src/ui/control/navigation_control.ts +0 -1
- package/src/ui/map.test.ts +37 -8
- package/src/ui/map.ts +14 -13
- package/src/ui/marker.ts +1 -1
- package/src/ui/popup.ts +1 -1
- package/src/util/throttle.ts +7 -3
|
@@ -14,12 +14,7 @@ import {
|
|
|
14
14
|
CrossFaded
|
|
15
15
|
} from '../properties';
|
|
16
16
|
|
|
17
|
-
import type {Color} from '@maplibre/maplibre-gl-style-spec';
|
|
18
|
-
import type {Padding} from '@maplibre/maplibre-gl-style-spec';
|
|
19
|
-
|
|
20
|
-
import type {Formatted} from '@maplibre/maplibre-gl-style-spec';
|
|
21
|
-
|
|
22
|
-
import type {ResolvedImage} from '@maplibre/maplibre-gl-style-spec';
|
|
17
|
+
import type {Color, Formatted, Padding, ResolvedImage, VariableAnchorOffsetCollection} from '@maplibre/maplibre-gl-style-spec';
|
|
23
18
|
import {StylePropertySpecification} from '@maplibre/maplibre-gl-style-spec';
|
|
24
19
|
|
|
25
20
|
|
|
@@ -14,12 +14,7 @@ import {
|
|
|
14
14
|
CrossFaded
|
|
15
15
|
} from '../properties';
|
|
16
16
|
|
|
17
|
-
import type {Color} from '@maplibre/maplibre-gl-style-spec';
|
|
18
|
-
import type {Padding} from '@maplibre/maplibre-gl-style-spec';
|
|
19
|
-
|
|
20
|
-
import type {Formatted} from '@maplibre/maplibre-gl-style-spec';
|
|
21
|
-
|
|
22
|
-
import type {ResolvedImage} from '@maplibre/maplibre-gl-style-spec';
|
|
17
|
+
import type {Color, Formatted, Padding, ResolvedImage, VariableAnchorOffsetCollection} from '@maplibre/maplibre-gl-style-spec';
|
|
23
18
|
import {StylePropertySpecification} from '@maplibre/maplibre-gl-style-spec';
|
|
24
19
|
|
|
25
20
|
import {
|
|
@@ -58,6 +53,7 @@ export type SymbolLayoutProps = {
|
|
|
58
53
|
"text-justify": DataDrivenProperty<"auto" | "left" | "center" | "right">,
|
|
59
54
|
"text-radial-offset": DataDrivenProperty<number>,
|
|
60
55
|
"text-variable-anchor": DataConstantProperty<Array<"center" | "left" | "right" | "top" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right">>,
|
|
56
|
+
"text-variable-anchor-offset": DataDrivenProperty<VariableAnchorOffsetCollection>,
|
|
61
57
|
"text-anchor": DataDrivenProperty<"center" | "left" | "right" | "top" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right">,
|
|
62
58
|
"text-max-angle": DataConstantProperty<number>,
|
|
63
59
|
"text-writing-mode": DataConstantProperty<Array<"horizontal" | "vertical">>,
|
|
@@ -104,6 +100,7 @@ export type SymbolLayoutPropsPossiblyEvaluated = {
|
|
|
104
100
|
"text-justify": PossiblyEvaluatedPropertyValue<"auto" | "left" | "center" | "right">,
|
|
105
101
|
"text-radial-offset": PossiblyEvaluatedPropertyValue<number>,
|
|
106
102
|
"text-variable-anchor": Array<"center" | "left" | "right" | "top" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right">,
|
|
103
|
+
"text-variable-anchor-offset": PossiblyEvaluatedPropertyValue<VariableAnchorOffsetCollection>,
|
|
107
104
|
"text-anchor": PossiblyEvaluatedPropertyValue<"center" | "left" | "right" | "top" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right">,
|
|
108
105
|
"text-max-angle": number,
|
|
109
106
|
"text-writing-mode": Array<"horizontal" | "vertical">,
|
|
@@ -151,6 +148,7 @@ const getLayout = () => layout = layout || new Properties({
|
|
|
151
148
|
"text-justify": new DataDrivenProperty(styleSpec["layout_symbol"]["text-justify"] as any as StylePropertySpecification),
|
|
152
149
|
"text-radial-offset": new DataDrivenProperty(styleSpec["layout_symbol"]["text-radial-offset"] as any as StylePropertySpecification),
|
|
153
150
|
"text-variable-anchor": new DataConstantProperty(styleSpec["layout_symbol"]["text-variable-anchor"] as any as StylePropertySpecification),
|
|
151
|
+
"text-variable-anchor-offset": new DataDrivenProperty(styleSpec["layout_symbol"]["text-variable-anchor-offset"] as any as StylePropertySpecification),
|
|
154
152
|
"text-anchor": new DataDrivenProperty(styleSpec["layout_symbol"]["text-anchor"] as any as StylePropertySpecification),
|
|
155
153
|
"text-max-angle": new DataConstantProperty(styleSpec["layout_symbol"]["text-max-angle"] as any as StylePropertySpecification),
|
|
156
154
|
"text-writing-mode": new DataConstantProperty(styleSpec["layout_symbol"]["text-writing-mode"] as any as StylePropertySpecification),
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {EvaluationParameters} from '../evaluation_parameters';
|
|
2
|
+
import {ZoomHistory} from '../zoom_history';
|
|
3
|
+
import {SymbolStyleLayer} from './symbol_style_layer';
|
|
4
|
+
import {INVALID_TEXT_OFFSET, evaluateVariableOffset, getTextVariableAnchorOffset} from './variable_text_anchor';
|
|
5
|
+
|
|
6
|
+
describe('evaluateVariableOffset', () => {
|
|
7
|
+
test('fromRadialOffset', () => {
|
|
8
|
+
// Radial offset mode is invoked by using INVALID_TEXT_OFFSET as the Y value
|
|
9
|
+
const srcOffset = [10, INVALID_TEXT_OFFSET] as [number, number];
|
|
10
|
+
|
|
11
|
+
expect(evaluateVariableOffset('center', srcOffset)).toEqual([0, 0]);
|
|
12
|
+
|
|
13
|
+
// Top/bottom offsets are shifted by the default baseline (7)
|
|
14
|
+
expect(evaluateVariableOffset('top', srcOffset)).toEqual([0, 3]);
|
|
15
|
+
expect(evaluateVariableOffset('bottom', srcOffset)).toEqual([0, -3]);
|
|
16
|
+
expect(evaluateVariableOffset('left', srcOffset)).toEqual([10, 0]);
|
|
17
|
+
expect(evaluateVariableOffset('right', srcOffset)).toEqual([-10, 0]);
|
|
18
|
+
|
|
19
|
+
const hypotenuse = 10 / Math.SQRT2;
|
|
20
|
+
expect(evaluateVariableOffset('top-left', srcOffset)).toEqual([expect.closeTo(hypotenuse), expect.closeTo(hypotenuse - 7)]);
|
|
21
|
+
expect(evaluateVariableOffset('top-right', srcOffset)).toEqual([expect.closeTo(-hypotenuse), expect.closeTo(hypotenuse - 7)]);
|
|
22
|
+
expect(evaluateVariableOffset('bottom-left', srcOffset)).toEqual([expect.closeTo(hypotenuse), expect.closeTo(-hypotenuse + 7)]);
|
|
23
|
+
expect(evaluateVariableOffset('bottom-right', srcOffset)).toEqual([expect.closeTo(-hypotenuse), expect.closeTo(-hypotenuse + 7)]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('fromTextOffset', () => {
|
|
27
|
+
const srcOffset = [10, -10] as [number, number];
|
|
28
|
+
|
|
29
|
+
expect(evaluateVariableOffset('center', srcOffset)).toEqual([0, 0]);
|
|
30
|
+
|
|
31
|
+
// Top/bottom offsets are shifted by the default baseline (7)
|
|
32
|
+
expect(evaluateVariableOffset('top', srcOffset)).toEqual([0, 3]);
|
|
33
|
+
expect(evaluateVariableOffset('bottom', srcOffset)).toEqual([0, -3]);
|
|
34
|
+
expect(evaluateVariableOffset('left', srcOffset)).toEqual([10, 0]);
|
|
35
|
+
expect(evaluateVariableOffset('right', srcOffset)).toEqual([-10, 0]);
|
|
36
|
+
expect(evaluateVariableOffset('top-left', srcOffset)).toEqual([10, 3]);
|
|
37
|
+
expect(evaluateVariableOffset('top-right', srcOffset)).toEqual([-10, 3]);
|
|
38
|
+
expect(evaluateVariableOffset('bottom-left', srcOffset)).toEqual([10, -3]);
|
|
39
|
+
expect(evaluateVariableOffset('bottom-right', srcOffset)).toEqual([-10, -3]);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function createSymbolLayer(layerProperties) {
|
|
44
|
+
const layer = new SymbolStyleLayer(layerProperties);
|
|
45
|
+
layer.recalculate({zoom: 0, zoomHistory: {} as ZoomHistory} as EvaluationParameters, []);
|
|
46
|
+
return layer;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('getTextVariableAnchorOffset', () => {
|
|
50
|
+
test('defaults - no props set', () => {
|
|
51
|
+
const props = {};
|
|
52
|
+
const layer = createSymbolLayer(props);
|
|
53
|
+
|
|
54
|
+
expect(getTextVariableAnchorOffset(layer, null, null)).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('text-variable-anchor-offset set', () => {
|
|
58
|
+
const props = {layout: {'text-variable-anchor-offset': ['top', [1, 1], 'bottom', [2, 2]]}};
|
|
59
|
+
const layer = createSymbolLayer(props);
|
|
60
|
+
|
|
61
|
+
const offset = getTextVariableAnchorOffset(layer, null, null);
|
|
62
|
+
expect(offset).toBeDefined();
|
|
63
|
+
// Offset converted to EMs, accounting for baseline shift on Y axis
|
|
64
|
+
expect(offset.toString()).toBe('["top",[24,17],"bottom",[48,55]]');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('text-variable-anchor set', () => {
|
|
68
|
+
const props = {layout: {'text-variable-anchor': ['top']}};
|
|
69
|
+
const layer = createSymbolLayer(props);
|
|
70
|
+
|
|
71
|
+
const offset = getTextVariableAnchorOffset(layer, null, null);
|
|
72
|
+
expect(offset).toBeDefined();
|
|
73
|
+
// Default offset (0, 0) converted to EMs, accounting for baseline shift on Y axis
|
|
74
|
+
expect(offset.toString()).toBe('["top",[0,-7]]');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('text-variable-anchor and text-offset set', () => {
|
|
78
|
+
const props = {layout: {'text-variable-anchor': ['top'], 'text-offset': [1, 1]}};
|
|
79
|
+
const layer = createSymbolLayer(props);
|
|
80
|
+
|
|
81
|
+
const offset = getTextVariableAnchorOffset(layer, null, null);
|
|
82
|
+
expect(offset).toBeDefined();
|
|
83
|
+
// Offset converted to EMs, accounting for baseline shift on Y axis
|
|
84
|
+
expect(offset.toString()).toBe('["top",[0,17]]');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('text-variable-anchor and text-radial-offset set', () => {
|
|
88
|
+
const props = {layout: {'text-variable-anchor': ['top'], 'text-radial-offset': 2}};
|
|
89
|
+
const layer = createSymbolLayer(props);
|
|
90
|
+
|
|
91
|
+
const offset = getTextVariableAnchorOffset(layer, null, null);
|
|
92
|
+
expect(offset).toBeDefined();
|
|
93
|
+
// Offset converted to EMs, accounting for baseline shift on Y axis
|
|
94
|
+
expect(offset.toString()).toBe('["top",[0,41]]');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('text-variable-anchor, text-offset, and text-radial-offset set', () => {
|
|
98
|
+
const props = {layout: {'text-variable-anchor': ['top'], 'text-offset': [1, 1], 'text-radial-offset': 2}};
|
|
99
|
+
const layer = createSymbolLayer(props);
|
|
100
|
+
|
|
101
|
+
const offset = getTextVariableAnchorOffset(layer, null, null);
|
|
102
|
+
expect(offset).toBeDefined();
|
|
103
|
+
// Offset converted to EMs, accounting for baseline shift on Y axis
|
|
104
|
+
expect(offset.toString()).toBe('["top",[0,41]]');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('text-variable-anchor and text-variable-anchor-offset set', () => {
|
|
108
|
+
const props = {layout: {'text-variable-anchor-offset': ['top', [1, 1]], 'text-variable-anchor': ['bottom']}};
|
|
109
|
+
const layer = createSymbolLayer(props);
|
|
110
|
+
|
|
111
|
+
const offset = getTextVariableAnchorOffset(layer, null, null);
|
|
112
|
+
expect(offset).toBeDefined();
|
|
113
|
+
// Offset converted to EMs, accounting for baseline shift on Y axis
|
|
114
|
+
expect(offset.toString()).toBe('["top",[24,17]]');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {VariableAnchorOffsetCollection, VariableAnchorOffsetCollectionSpecification} from '@maplibre/maplibre-gl-style-spec';
|
|
2
|
+
import {SymbolFeature} from '../../data/bucket/symbol_bucket';
|
|
3
|
+
import {CanonicalTileID} from '../../source/tile_id';
|
|
4
|
+
import ONE_EM from '../../symbol/one_em';
|
|
5
|
+
import {SymbolStyleLayer} from './symbol_style_layer';
|
|
6
|
+
|
|
7
|
+
export enum TextAnchorEnum {
|
|
8
|
+
'center' = 1,
|
|
9
|
+
'left' = 2,
|
|
10
|
+
'right' = 3,
|
|
11
|
+
'top' = 4,
|
|
12
|
+
'bottom' = 5,
|
|
13
|
+
'top-left' = 6,
|
|
14
|
+
'top-right' = 7,
|
|
15
|
+
'bottom-left' = 8,
|
|
16
|
+
'bottom-right' = 9
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type TextAnchor = keyof typeof TextAnchorEnum;
|
|
20
|
+
|
|
21
|
+
// The radial offset is to the edge of the text box
|
|
22
|
+
// In the horizontal direction, the edge of the text box is where glyphs start
|
|
23
|
+
// But in the vertical direction, the glyphs appear to "start" at the baseline
|
|
24
|
+
// We don't actually load baseline data, but we assume an offset of ONE_EM - 17
|
|
25
|
+
// (see "yOffset" in shaping.js)
|
|
26
|
+
const baselineOffset = 7;
|
|
27
|
+
export const INVALID_TEXT_OFFSET = Number.POSITIVE_INFINITY;
|
|
28
|
+
|
|
29
|
+
export function evaluateVariableOffset(anchor: TextAnchor, offset: [number, number]): [number, number] {
|
|
30
|
+
|
|
31
|
+
function fromRadialOffset(anchor: TextAnchor, radialOffset: number): [number, number] {
|
|
32
|
+
let x = 0, y = 0;
|
|
33
|
+
if (radialOffset < 0) radialOffset = 0; // Ignore negative offset.
|
|
34
|
+
// solve for r where r^2 + r^2 = radialOffset^2
|
|
35
|
+
const hypotenuse = radialOffset / Math.SQRT2;
|
|
36
|
+
switch (anchor) {
|
|
37
|
+
case 'top-right':
|
|
38
|
+
case 'top-left':
|
|
39
|
+
y = hypotenuse - baselineOffset;
|
|
40
|
+
break;
|
|
41
|
+
case 'bottom-right':
|
|
42
|
+
case 'bottom-left':
|
|
43
|
+
y = -hypotenuse + baselineOffset;
|
|
44
|
+
break;
|
|
45
|
+
case 'bottom':
|
|
46
|
+
y = -radialOffset + baselineOffset;
|
|
47
|
+
break;
|
|
48
|
+
case 'top':
|
|
49
|
+
y = radialOffset - baselineOffset;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
switch (anchor) {
|
|
54
|
+
case 'top-right':
|
|
55
|
+
case 'bottom-right':
|
|
56
|
+
x = -hypotenuse;
|
|
57
|
+
break;
|
|
58
|
+
case 'top-left':
|
|
59
|
+
case 'bottom-left':
|
|
60
|
+
x = hypotenuse;
|
|
61
|
+
break;
|
|
62
|
+
case 'left':
|
|
63
|
+
x = radialOffset;
|
|
64
|
+
break;
|
|
65
|
+
case 'right':
|
|
66
|
+
x = -radialOffset;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return [x, y];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function fromTextOffset(anchor: TextAnchor, offsetX: number, offsetY: number): [number, number] {
|
|
74
|
+
let x = 0, y = 0;
|
|
75
|
+
// Use absolute offset values.
|
|
76
|
+
offsetX = Math.abs(offsetX);
|
|
77
|
+
offsetY = Math.abs(offsetY);
|
|
78
|
+
|
|
79
|
+
switch (anchor) {
|
|
80
|
+
case 'top-right':
|
|
81
|
+
case 'top-left':
|
|
82
|
+
case 'top':
|
|
83
|
+
y = offsetY - baselineOffset;
|
|
84
|
+
break;
|
|
85
|
+
case 'bottom-right':
|
|
86
|
+
case 'bottom-left':
|
|
87
|
+
case 'bottom':
|
|
88
|
+
y = -offsetY + baselineOffset;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
switch (anchor) {
|
|
93
|
+
case 'top-right':
|
|
94
|
+
case 'bottom-right':
|
|
95
|
+
case 'right':
|
|
96
|
+
x = -offsetX;
|
|
97
|
+
break;
|
|
98
|
+
case 'top-left':
|
|
99
|
+
case 'bottom-left':
|
|
100
|
+
case 'left':
|
|
101
|
+
x = offsetX;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return [x, y];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (offset[1] !== INVALID_TEXT_OFFSET) ? fromTextOffset(anchor, offset[0], offset[1]) : fromRadialOffset(anchor, offset[0]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Helper to support both text-variable-anchor and text-variable-anchor-offset. Offset values converted from EMs to PXs
|
|
112
|
+
export function getTextVariableAnchorOffset(layer: SymbolStyleLayer, feature: SymbolFeature, canonical: CanonicalTileID): VariableAnchorOffsetCollection | null {
|
|
113
|
+
const layout = layer.layout;
|
|
114
|
+
// If style specifies text-variable-anchor-offset, just return it
|
|
115
|
+
const variableAnchorOffset = layout.get('text-variable-anchor-offset')?.evaluate(feature, {}, canonical);
|
|
116
|
+
|
|
117
|
+
if (variableAnchorOffset) {
|
|
118
|
+
const sourceValues = variableAnchorOffset.values;
|
|
119
|
+
const destValues: VariableAnchorOffsetCollectionSpecification = [];
|
|
120
|
+
|
|
121
|
+
// Convert offsets from EM to PX, and apply baseline shift
|
|
122
|
+
for (let i = 0; i < sourceValues.length; i += 2) {
|
|
123
|
+
const anchor = destValues[i] = sourceValues[i] as TextAnchor;
|
|
124
|
+
const offset = (sourceValues[i + 1] as [number, number]).map(t => t * ONE_EM) as [number, number];
|
|
125
|
+
|
|
126
|
+
if (anchor.startsWith('top')) {
|
|
127
|
+
offset[1] -= baselineOffset;
|
|
128
|
+
} else if (anchor.startsWith('bottom')) {
|
|
129
|
+
offset[1] += baselineOffset;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
destValues[i + 1] = offset;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return new VariableAnchorOffsetCollection(destValues);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// If style specifies text-variable-anchor, convert to the new format
|
|
139
|
+
const variableAnchor = layout.get('text-variable-anchor');
|
|
140
|
+
|
|
141
|
+
if (variableAnchor) {
|
|
142
|
+
let textOffset: [number, number];
|
|
143
|
+
const unevaluatedLayout = layer._unevaluatedLayout;
|
|
144
|
+
|
|
145
|
+
// The style spec says don't use `text-offset` and `text-radial-offset` together
|
|
146
|
+
// but doesn't actually specify what happens if you use both. We go with the radial offset.
|
|
147
|
+
if (unevaluatedLayout.getValue('text-radial-offset') !== undefined) {
|
|
148
|
+
textOffset = [layout.get('text-radial-offset').evaluate(feature, {}, canonical) * ONE_EM, INVALID_TEXT_OFFSET];
|
|
149
|
+
} else {
|
|
150
|
+
textOffset = layout.get('text-offset').evaluate(feature, {}, canonical).map(t => t * ONE_EM) as [number, number];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const anchorOffsets: VariableAnchorOffsetCollectionSpecification = [];
|
|
154
|
+
|
|
155
|
+
for (const anchor of variableAnchor) {
|
|
156
|
+
anchorOffsets.push(anchor, evaluateVariableOffset(anchor, textOffset));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return new VariableAnchorOffsetCollection(anchorOffsets);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return null;
|
|
163
|
+
}
|
package/src/symbol/placement.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type {FeatureKey} from './collision_index';
|
|
|
3
3
|
import {EXTENT} from '../data/extent';
|
|
4
4
|
import * as symbolSize from './symbol_size';
|
|
5
5
|
import * as projection from './projection';
|
|
6
|
-
import {getAnchorJustification
|
|
6
|
+
import {getAnchorJustification} from './symbol_layout';
|
|
7
7
|
import {getAnchorAlignment, WritingMode} from './shaping';
|
|
8
8
|
import {mat4} from 'gl-matrix';
|
|
9
9
|
import {pixelsToTileUnits} from '../source/pixels_to_tile_units';
|
|
@@ -17,12 +17,12 @@ import {getOverlapMode, OverlapMode} from '../style/style_layer/overlap_mode';
|
|
|
17
17
|
import type {Tile} from '../source/tile';
|
|
18
18
|
import {SymbolBucket, CollisionArrays, SingleCollisionBox} from '../data/bucket/symbol_bucket';
|
|
19
19
|
|
|
20
|
-
import type {CollisionBoxArray, CollisionVertexArray, SymbolInstance} from '../data/array_types.g';
|
|
20
|
+
import type {CollisionBoxArray, CollisionVertexArray, SymbolInstance, TextAnchorOffset} from '../data/array_types.g';
|
|
21
21
|
import type {FeatureIndex} from '../data/feature_index';
|
|
22
22
|
import type {OverscaledTileID} from '../source/tile_id';
|
|
23
|
-
import type {TextAnchor} from './symbol_layout';
|
|
24
23
|
import {Terrain} from '../render/terrain';
|
|
25
24
|
import {warnOnce} from '../util/util';
|
|
25
|
+
import {TextAnchor, TextAnchorEnum} from '../style/style_layer/variable_text_anchor';
|
|
26
26
|
|
|
27
27
|
class OpacityState {
|
|
28
28
|
opacity: number;
|
|
@@ -147,10 +147,9 @@ function calculateVariableLayoutShift(
|
|
|
147
147
|
const {horizontalAlign, verticalAlign} = getAnchorAlignment(anchor);
|
|
148
148
|
const shiftX = -(horizontalAlign - 0.5) * width;
|
|
149
149
|
const shiftY = -(verticalAlign - 0.5) * height;
|
|
150
|
-
const offset = evaluateVariableOffset(anchor, textOffset);
|
|
151
150
|
return new Point(
|
|
152
|
-
shiftX +
|
|
153
|
-
shiftY +
|
|
151
|
+
shiftX + textOffset[0] * textBoxScale,
|
|
152
|
+
shiftY + textOffset[1] * textBoxScale
|
|
154
153
|
);
|
|
155
154
|
}
|
|
156
155
|
|
|
@@ -339,7 +338,7 @@ export class Placement {
|
|
|
339
338
|
}
|
|
340
339
|
|
|
341
340
|
attemptAnchorPlacement(
|
|
342
|
-
|
|
341
|
+
textAnchorOffset: TextAnchorOffset,
|
|
343
342
|
textBox: SingleCollisionBox,
|
|
344
343
|
width: number,
|
|
345
344
|
height: number,
|
|
@@ -363,7 +362,8 @@ export class Placement {
|
|
|
363
362
|
};
|
|
364
363
|
} {
|
|
365
364
|
|
|
366
|
-
const
|
|
365
|
+
const anchor = TextAnchorEnum[textAnchorOffset.textAnchor] as TextAnchor;
|
|
366
|
+
const textOffset = [textAnchorOffset.textOffset0, textAnchorOffset.textOffset1] as [number, number];
|
|
367
367
|
const shift = calculateVariableLayoutShift(anchor, width, height, textOffset, textBoxScale);
|
|
368
368
|
|
|
369
369
|
const placedGlyphBoxes = this.collisionIndex.placeCollisionBox(
|
|
@@ -528,7 +528,11 @@ export class Placement {
|
|
|
528
528
|
}
|
|
529
529
|
};
|
|
530
530
|
|
|
531
|
-
|
|
531
|
+
const textAnchorOffsetStart = symbolInstance.textAnchorOffsetStartIndex;
|
|
532
|
+
const textAnchorOffsetEnd = symbolInstance.textAnchorOffsetEndIndex;
|
|
533
|
+
|
|
534
|
+
// If start+end indices match, text-variable-anchor is not in play.
|
|
535
|
+
if (textAnchorOffsetEnd === textAnchorOffsetStart) {
|
|
532
536
|
const placeBox = (collisionTextBox, orientation) => {
|
|
533
537
|
const placedFeature = this.collisionIndex.placeCollisionBox(
|
|
534
538
|
collisionTextBox,
|
|
@@ -561,47 +565,54 @@ export class Placement {
|
|
|
561
565
|
updatePreviousOrientationIfNotPlaced(placed && placed.box && placed.box.length);
|
|
562
566
|
|
|
563
567
|
} else {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
// If this symbol was in the last placement, shift the previously used
|
|
567
|
-
// anchor to the front of the anchor list, only if the previous anchor
|
|
568
|
-
// is still in the anchor list
|
|
569
|
-
if (this.prevPlacement && this.prevPlacement.variableOffsets[symbolInstance.crossTileID]) {
|
|
570
|
-
const prevOffsets = this.prevPlacement.variableOffsets[symbolInstance.crossTileID];
|
|
571
|
-
if (anchors.indexOf(prevOffsets.anchor) > 0) {
|
|
572
|
-
anchors = anchors.filter(anchor => anchor !== prevOffsets.anchor);
|
|
573
|
-
anchors.unshift(prevOffsets.anchor);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
568
|
+
// If this symbol was in the last placement, prefer placement using same anchor, if it's still available
|
|
569
|
+
let prevAnchor = TextAnchorEnum[this.prevPlacement?.variableOffsets[symbolInstance.crossTileID]?.anchor];
|
|
576
570
|
|
|
577
571
|
const placeBoxForVariableAnchors = (collisionTextBox, collisionIconBox, orientation) => {
|
|
578
572
|
const width = collisionTextBox.x2 - collisionTextBox.x1;
|
|
579
573
|
const height = collisionTextBox.y2 - collisionTextBox.y1;
|
|
580
574
|
const textBoxScale = symbolInstance.textBoxScale;
|
|
581
|
-
|
|
582
575
|
const variableIconBox = hasIconTextFit && (iconOverlapMode === 'never') ? collisionIconBox : null;
|
|
583
576
|
|
|
584
577
|
let placedBox: {
|
|
585
578
|
box: Array<number>;
|
|
586
579
|
offscreen: boolean;
|
|
587
580
|
} = {box: [], offscreen: false};
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
if (
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
581
|
+
let placementPasses = (textOverlapMode === 'never') ? 1 : 2;
|
|
582
|
+
let overlapMode: OverlapMode = 'never';
|
|
583
|
+
|
|
584
|
+
if (prevAnchor) {
|
|
585
|
+
placementPasses++;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
for (let pass = 0; pass < placementPasses; pass++) {
|
|
589
|
+
for (let i = textAnchorOffsetStart; i < textAnchorOffsetEnd; i++) {
|
|
590
|
+
const textAnchorOffset = bucket.textAnchorOffsets.get(i);
|
|
591
|
+
|
|
592
|
+
if (prevAnchor && textAnchorOffset.textAnchor !== prevAnchor) {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const result = this.attemptAnchorPlacement(
|
|
597
|
+
textAnchorOffset, collisionTextBox, width, height,
|
|
598
|
+
textBoxScale, rotateWithMap, pitchWithMap, textPixelRatio, posMatrix,
|
|
599
|
+
collisionGroup, overlapMode, symbolInstance, bucket, orientation, variableIconBox, getElevation);
|
|
600
|
+
|
|
601
|
+
if (result) {
|
|
602
|
+
placedBox = result.placedGlyphBoxes;
|
|
603
|
+
if (placedBox && placedBox.box && placedBox.box.length) {
|
|
604
|
+
placeText = true;
|
|
605
|
+
shift = result.shift;
|
|
606
|
+
return placedBox;
|
|
607
|
+
}
|
|
603
608
|
}
|
|
604
609
|
}
|
|
610
|
+
|
|
611
|
+
if (prevAnchor) {
|
|
612
|
+
prevAnchor = null;
|
|
613
|
+
} else {
|
|
614
|
+
overlapMode = textOverlapMode;
|
|
615
|
+
}
|
|
605
616
|
}
|
|
606
617
|
|
|
607
618
|
return placedBox;
|
|
@@ -952,11 +963,12 @@ export class Placement {
|
|
|
952
963
|
if (bucket.hasIconCollisionBoxData()) bucket.iconCollisionBox.collisionVertexArray.clear();
|
|
953
964
|
if (bucket.hasTextCollisionBoxData()) bucket.textCollisionBox.collisionVertexArray.clear();
|
|
954
965
|
|
|
955
|
-
const
|
|
966
|
+
const layer = bucket.layers[0];
|
|
967
|
+
const layout = layer.layout;
|
|
956
968
|
const duplicateOpacityState = new JointOpacityState(null, 0, false, false, true);
|
|
957
969
|
const textAllowOverlap = layout.get('text-allow-overlap');
|
|
958
970
|
const iconAllowOverlap = layout.get('icon-allow-overlap');
|
|
959
|
-
const
|
|
971
|
+
const hasVariablePlacement = layer._unevaluatedLayout.hasValue('text-variable-anchor') || layer._unevaluatedLayout.hasValue('text-variable-anchor-offset');
|
|
960
972
|
const rotateWithMap = layout.get('text-rotation-alignment') === 'map';
|
|
961
973
|
const pitchWithMap = layout.get('text-pitch-alignment') === 'map';
|
|
962
974
|
const hasIconTextFit = layout.get('icon-text-fit') !== 'none';
|
|
@@ -1074,7 +1086,7 @@ export class Placement {
|
|
|
1074
1086
|
let shift = new Point(0, 0);
|
|
1075
1087
|
if (collisionArrays.textBox || collisionArrays.verticalTextBox) {
|
|
1076
1088
|
let used = true;
|
|
1077
|
-
if (
|
|
1089
|
+
if (hasVariablePlacement) {
|
|
1078
1090
|
const variableOffset = this.variableOffsets[crossTileID];
|
|
1079
1091
|
if (variableOffset) {
|
|
1080
1092
|
// This will show either the currently placed position or the last
|