Skip to content

Commit a763e8c

Browse files
authored
Add option viewport-glyph to text-rotation-alignment (#716)
1 parent 5adea88 commit a763e8c

16 files changed

Lines changed: 408 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
### Features and improvements
44

5+
- Add option `viewport-glyph` to `text-rotation-alignment` which places glyphs along a linestring and rotates them to the x-axis of the viewport (#716).
6+
57
- *...Add new stuff here...*
68

79
### 🐞 Bug fixes
@@ -10,6 +12,8 @@
1012
- Add FeatureIdentifier type to define feature parameter in setFeatureState, removeFeatureState, and getFeatureState methods. Change FeatureIdentifier.id from `id: string | number;` to `id?: string | number | undefined;` (#1095)
1113
- Change map.on, map.off, and map.once type parameter from "type: MapEvent" to "type: MapEvent | string" (#1094)
1214

15+
- *...Add new stuff here...*
16+
1317
## 2.1.7
1418

1519
### 🐞 Bug fixes

debug/shield-rotation.html

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<title>MapLibre GL JS debug page</title>
6+
<meta charset='utf-8'>
7+
<meta name="viewport" content="width=device-width, initial-scale=1">
8+
<link rel='stylesheet' href='../dist/maplibre-gl.css' />
9+
<style>
10+
body {
11+
margin: 0;
12+
padding: 0;
13+
}
14+
15+
html,
16+
body,
17+
#map {
18+
height: 100%;
19+
}
20+
</style>
21+
</head>
22+
23+
<body>
24+
<style>
25+
.map-overlay {
26+
font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
27+
position: absolute;
28+
width: 200px;
29+
top: 0;
30+
left: 0;
31+
padding: 10px;
32+
}
33+
34+
.map-overlay .map-overlay-inner {
35+
background-color: #fff;
36+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
37+
border-radius: 3px;
38+
padding: 10px;
39+
margin-bottom: 10px;
40+
}
41+
42+
.map-overlay-inner fieldset {
43+
border: none;
44+
padding: 0;
45+
margin: 0 0 10px;
46+
}
47+
48+
.map-overlay-inner fieldset:last-child {
49+
margin: 0;
50+
}
51+
52+
.map-overlay-inner select {
53+
width: 100%;
54+
}
55+
56+
.map-overlay-inner label {
57+
display: block;
58+
font-weight: bold;
59+
margin: 0 0 5px;
60+
}
61+
</style>
62+
63+
<div id="map"></div>
64+
<div class="map-overlay top">
65+
<div class="map-overlay-inner">
66+
<fieldset>
67+
<label>text-rotation-alignment</label>
68+
<select id="rotation" name="rotation">
69+
<option value="map">map</option>
70+
<option value="viewport">viewport</option>
71+
<option value="viewport-glyph">viewport-glyph</option>
72+
</select>
73+
</fieldset>
74+
<fieldset>
75+
<label>text-pitch-alignment</label>
76+
<select id="pitch" name="pitch">
77+
<option value="map">map</option>
78+
<option value="viewport">viewport</option>
79+
</select>
80+
</fieldset>
81+
</div>
82+
</div>
83+
84+
<script src='../dist/maplibre-gl-dev.js'></script>
85+
<script>
86+
var map = window.map = new maplibregl.Map({
87+
container: 'map',
88+
zoom: 6,
89+
center: [-69.482, 45.42],
90+
style: 'https://demotiles.maplibre.org/style.json',
91+
hash: true
92+
});
93+
94+
map.addControl(new maplibregl.NavigationControl());
95+
map.addControl(new maplibregl.GeolocateControl());
96+
97+
map.on('load', function () {
98+
map.addSource('maine', {
99+
'type': 'geojson',
100+
'data': {
101+
'type': 'Feature',
102+
'geometry': {
103+
'type': 'Polygon',
104+
'coordinates': [
105+
[
106+
[-67.13734351262877, 45.137451890638886],
107+
[-66.96466, 44.8097],
108+
[-68.03252, 44.3252],
109+
[-69.06, 43.98],
110+
[-70.11617, 43.68405],
111+
[-70.64573401557249, 43.090083319667144],
112+
[-70.75102474636725, 43.08003225358635],
113+
[-70.79761105007827, 43.21973948828747],
114+
[-70.98176001655037, 43.36789581966826],
115+
[-70.94416541205806, 43.46633942318431],
116+
[-71.08482, 45.3052400000002],
117+
[-70.6600225491012, 45.46022288673396],
118+
[-70.30495378282376, 45.914794623389355],
119+
[-70.00014034695016, 46.69317088478567],
120+
[-69.23708614772835, 47.44777598732787],
121+
[-68.90478084987546, 47.184794623394396],
122+
[-68.23430497910454, 47.35462921812177],
123+
[-67.79035274928509, 47.066248887716995],
124+
[-67.79141211614706, 45.702585354182816],
125+
[-67.13734351262877, 45.137451890638886]
126+
]
127+
]
128+
}
129+
}
130+
});
131+
map.addLayer({
132+
'id': 'maine-line',
133+
'type': 'line',
134+
'source': 'maine'
135+
});
136+
map.addLayer({
137+
'id': 'maine-text',
138+
'type': 'symbol',
139+
'source': 'maine',
140+
'layout': {
141+
'text-field': 'maine',
142+
'symbol-placement': 'line',
143+
'text-pitch-alignment': 'map',
144+
'text-rotation-alignment': 'map',
145+
'text-size': 36
146+
}
147+
});
148+
var rotation = document.getElementById('rotation');
149+
rotation.addEventListener('change', function() {
150+
map.setLayoutProperty('maine-text', 'text-rotation-alignment', rotation.value);
151+
});
152+
var pitch = document.getElementById('pitch');
153+
pitch.addEventListener('change', function() {
154+
map.setLayoutProperty('maine-text', 'text-pitch-alignment', pitch.value);
155+
});
156+
157+
});
158+
159+
</script>
160+
</body>
161+
162+
</html>

src/data/bucket/symbol_bucket.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ class SymbolBucket implements Bucket {
524524

525525
if (text) {
526526
const fontStack = textFont.evaluate(evaluationFeature, {}, canonical).join(',');
527-
const textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point';
527+
const textAlongLine = layout.get('text-rotation-alignment') !== 'viewport' && layout.get('symbol-placement') !== 'point';
528528
this.allowVerticalPlacement = this.writingModes && this.writingModes.indexOf(WritingMode.vertical) >= 0;
529529
for (const section of text.sections) {
530530
if (!section.image) {

src/render/draw_symbol.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import SymbolStyleLayer from '../style/style_layer/symbol_style_layer';
77
import Painter from './painter';
88
import Program from './program';
99
import drawSymbol from './draw_symbol';
10+
import * as symbolProjection from '../symbol/projection';
1011
import type ZoomHistory from '../style/zoom_history';
1112
import type Map from '../ui/map';
1213
import type Transform from '../geo/transform';
@@ -18,6 +19,7 @@ jest.mock('./program');
1819
jest.mock('../source/source_cache');
1920
jest.mock('../source/tile');
2021
jest.mock('../data/bucket/symbol_bucket');
22+
jest.mock('../symbol/projection');
2123

2224
describe('drawSymbol', () => {
2325
test('should not do anything', () => {
@@ -85,4 +87,66 @@ describe('drawSymbol', () => {
8587

8688
expect(programMock.draw).toHaveBeenCalledTimes(1);
8789
});
90+
91+
test('should call updateLineLabels with rotateToLine === false if text-rotation-alignment is viewport-glyph', () => {
92+
93+
const painterMock = new Painter(null, null);
94+
painterMock.context = {
95+
gl: {},
96+
activeTexture: {
97+
set: () => {}
98+
}
99+
} as any;
100+
painterMock.renderPass = 'translucent';
101+
painterMock.transform = {pitch: 0, labelPlaneMatrix: mat4.create()} as any as Transform;
102+
painterMock.options = {} as any;
103+
104+
const layerSpec = {
105+
id: 'mock-layer',
106+
source: 'empty-source',
107+
type: 'symbol',
108+
layout: {
109+
'text-rotation-alignment': 'viewport-glyph',
110+
'text-field': 'ABC',
111+
'symbol-placement': 'line',
112+
},
113+
paint: {
114+
'text-opacity': 1
115+
}
116+
} as SymbolLayerSpecification;
117+
const layer = new SymbolStyleLayer(layerSpec);
118+
layer.recalculate({zoom: 0, zoomHistory: {} as ZoomHistory} as EvaluationParameters, []);
119+
120+
const tileId = new OverscaledTileID(1, 0, 1, 0, 0);
121+
tileId.posMatrix = mat4.create();
122+
const programMock = new Program(null, null, null, null, null, null);
123+
(painterMock.useProgram as jest.Mock).mockReturnValue(programMock);
124+
const bucketMock = new SymbolBucket(null);
125+
bucketMock.icon = {
126+
programConfigurations: {
127+
get: () => {}
128+
},
129+
segments: {
130+
get: () => [1]
131+
}
132+
} as any;
133+
bucketMock.iconSizeData = {
134+
kind: 'constant',
135+
layoutSize: 1
136+
};
137+
const tile = new Tile(tileId, 256);
138+
tile.tileID = tileId;
139+
tile.imageAtlasTexture = {
140+
bind: () => {}
141+
} as any;
142+
(tile.getBucket as jest.Mock).mockReturnValue(bucketMock);
143+
const sourceCacheMock = new SourceCache(null, null, null);
144+
(sourceCacheMock.getTile as jest.Mock).mockReturnValue(tile);
145+
sourceCacheMock.map = {showCollisionBoxes: false} as any as Map;
146+
147+
const spy = jest.spyOn(symbolProjection, 'updateLineLabels');
148+
drawSymbol(painterMock, sourceCacheMock, layer, [tileId], null);
149+
150+
expect(spy.mock.calls[0][8]).toBeFalsy(); // rotateToLine === false
151+
});
88152
});

src/render/draw_symbol.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ function drawLayerSymbols(
265265

266266
const rotateWithMap = rotationAlignment === 'map';
267267
const pitchWithMap = pitchAlignment === 'map';
268-
const alongLine = rotateWithMap && layer.layout.get('symbol-placement') !== 'point';
268+
const alongLine = rotationAlignment !== 'viewport' && layer.layout.get('symbol-placement') !== 'point';
269269
// Line label rotation happens in `updateLineLabels`
270270
// Pitched point labels are automatically rotated by the labelPlaneMatrix projection
271271
// Unpitched point labels need to have their rotation applied after projection
@@ -331,7 +331,8 @@ function drawLayerSymbols(
331331
bucket.hasIconData();
332332

333333
if (alongLine) {
334-
symbolProjection.updateLineLabels(bucket, coord.posMatrix, painter, isText, labelPlaneMatrix, glCoordMatrix, pitchWithMap, keepUpright);
334+
const rotateToLine = layer.layout.get('text-rotation-alignment') === 'map';
335+
symbolProjection.updateLineLabels(bucket, coord.posMatrix, painter, isText, labelPlaneMatrix, glCoordMatrix, pitchWithMap, keepUpright, rotateToLine);
335336
}
336337

337338
const matrix = painter.translatePosMatrix(coord.posMatrix, tile, translate, translateAnchor),

src/style-spec/reference/v8.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1690,6 +1690,9 @@
16901690
"viewport": {
16911691
"doc": "Produces glyphs whose x-axes are aligned with the x-axis of the viewport, regardless of the value of `symbol-placement`."
16921692
},
1693+
"viewport-glyph": {
1694+
"doc": "When `symbol-placement` is set to `point`, aligns text to the x-axis of the viewport. When `symbol-placement` is set to `line` or `line-center`, aligns glyphs to the x-axis of the viewport and places them along the line."
1695+
},
16931696
"auto": {
16941697
"doc": "When `symbol-placement` is set to `point`, this is equivalent to `viewport`. When `symbol-placement` is set to `line` or `line-center`, this is equivalent to `map`."
16951698
}
@@ -1711,6 +1714,9 @@
17111714
"android": "4.2.0",
17121715
"ios": "3.4.0",
17131716
"macos": "0.3.0"
1717+
},
1718+
"`viewport-glyph` value": {
1719+
"js": "2.1.8"
17141720
}
17151721
},
17161722
"expression": {

src/style/style_layer/symbol_style_layer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ class SymbolStyleLayer extends StyleLayer {
6868

6969
// If unspecified, `*-pitch-alignment` inherits `*-rotation-alignment`
7070
if (this.layout.get('text-pitch-alignment') === 'auto') {
71-
this.layout._values['text-pitch-alignment'] = this.layout.get('text-rotation-alignment');
71+
this.layout._values['text-pitch-alignment'] = this.layout.get('text-rotation-alignment') === 'map' ? 'map' : 'viewport';
7272
}
7373
if (this.layout.get('icon-pitch-alignment') === 'auto') {
7474
this.layout._values['icon-pitch-alignment'] = this.layout.get('icon-rotation-alignment');

src/symbol/collision_index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ class CollisionIndex {
147147
symbol,
148148
lineVertexArray,
149149
labelPlaneMatrix,
150-
projectionCache);
150+
projectionCache,
151+
false);
151152

152153
let collisionDetected = false;
153154
let inGrid = false;

0 commit comments

Comments
 (0)