@mapwhit/tilerenderer 0.47.2 → 0.49.0
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/min/package.json +1 -1
- package/package.json +3 -2
- package/src/data/array_types.js +169 -0
- package/src/data/bucket/symbol_attributes.js +18 -0
- package/src/data/bucket/symbol_bucket.js +116 -73
- package/src/data/program_configuration.js +18 -10
- package/src/geo/transform.js +17 -7
- package/src/index.js +1 -1
- package/src/render/glyph_atlas.js +3 -6
- package/src/render/image_atlas.js +3 -6
- package/src/render/image_manager.js +41 -41
- package/src/source/rtl_text_plugin.js +1 -0
- package/src/source/source_cache.js +1 -1
- package/src/source/tile.js +6 -5
- package/src/source/worker.js +1 -10
- package/src/style/style.js +4 -19
- package/src/style/style_layer/symbol_style_layer_properties.js +7 -1
- package/src/style-spec/expression/compound_expression.js +30 -16
- package/src/style-spec/expression/definitions/assertion.js +52 -5
- package/src/style-spec/expression/definitions/coercion.js +13 -0
- package/src/style-spec/expression/definitions/comparison.js +193 -0
- package/src/style-spec/expression/definitions/formatted.js +123 -0
- package/src/style-spec/expression/definitions/index.js +11 -62
- package/src/style-spec/expression/definitions/interpolate.js +17 -7
- package/src/style-spec/expression/definitions/literal.js +5 -0
- package/src/style-spec/expression/parsing_context.js +6 -7
- package/src/style-spec/expression/types.js +12 -1
- package/src/style-spec/feature_filter/convert.js +197 -0
- package/src/style-spec/feature_filter/index.js +5 -2
- package/src/style-spec/function/convert.js +78 -100
- package/src/style-spec/reference/v8.json +160 -52
- package/src/symbol/collision_index.js +0 -1
- package/src/symbol/cross_tile_symbol_index.js +12 -7
- package/src/symbol/get_anchors.js +11 -22
- package/src/symbol/mergelines.js +4 -1
- package/src/symbol/placement.js +58 -54
- package/src/symbol/quads.js +7 -6
- package/src/symbol/shaping.js +185 -40
- package/src/symbol/symbol_layout.js +40 -37
- package/src/symbol/transform_text.js +12 -1
- package/src/ui/map.js +8 -25
- package/src/style-spec/expression/definitions/array.js +0 -82
- package/src/style-spec/expression/definitions/equals.js +0 -93
|
@@ -17,7 +17,7 @@ function getAngleWindowSize(shapedText, glyphSize, boxScale) {
|
|
|
17
17
|
return shapedText ? (3 / 5) * glyphSize * boxScale : 0;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function
|
|
20
|
+
function getShapedLabelLength(shapedText, shapedIcon) {
|
|
21
21
|
return Math.max(
|
|
22
22
|
shapedText ? shapedText.right - shapedText.left : 0,
|
|
23
23
|
shapedIcon ? shapedIcon.right - shapedIcon.left : 0
|
|
@@ -26,7 +26,7 @@ function getLabelLength(shapedText, shapedIcon) {
|
|
|
26
26
|
|
|
27
27
|
function getCenterAnchor(line, maxAngle, shapedText, shapedIcon, glyphSize, boxScale) {
|
|
28
28
|
const angleWindowSize = getAngleWindowSize(shapedText, glyphSize, boxScale);
|
|
29
|
-
const labelLength =
|
|
29
|
+
const labelLength = getShapedLabelLength(shapedText, shapedIcon) * boxScale;
|
|
30
30
|
|
|
31
31
|
let prevDistance = 0;
|
|
32
32
|
const centerDistance = getLineLength(line) / 2;
|
|
@@ -45,11 +45,10 @@ function getCenterAnchor(line, maxAngle, shapedText, shapedIcon, glyphSize, boxS
|
|
|
45
45
|
|
|
46
46
|
const anchor = new Anchor(x, y, b.angleTo(a), i);
|
|
47
47
|
anchor._round();
|
|
48
|
-
if (angleWindowSize
|
|
49
|
-
return;
|
|
48
|
+
if (!angleWindowSize || checkMaxAngle(line, anchor, labelLength, angleWindowSize, maxAngle)) {
|
|
49
|
+
return anchor;
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
return anchor;
|
|
51
|
+
return;
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
prevDistance += segmentDistance;
|
|
@@ -62,16 +61,16 @@ function getAnchors(line, spacing, maxAngle, shapedText, shapedIcon, glyphSize,
|
|
|
62
61
|
// on the line.
|
|
63
62
|
|
|
64
63
|
const angleWindowSize = getAngleWindowSize(shapedText, glyphSize, boxScale);
|
|
65
|
-
|
|
66
|
-
const labelLength =
|
|
64
|
+
const shapedLabelLength = getShapedLabelLength(shapedText, shapedIcon);
|
|
65
|
+
const labelLength = shapedLabelLength * boxScale;
|
|
67
66
|
|
|
68
67
|
// Is the line continued from outside the tile boundary?
|
|
69
68
|
const isLineContinued = line[0].x === 0 || line[0].x === tileExtent || line[0].y === 0 || line[0].y === tileExtent;
|
|
70
69
|
|
|
71
70
|
// Is the label long, relative to the spacing?
|
|
72
71
|
// If so, adjust the spacing so there is always a minimum space of `spacing / 4` between label edges.
|
|
73
|
-
if (spacing - labelLength
|
|
74
|
-
spacing = labelLength
|
|
72
|
+
if (spacing - labelLength < spacing / 4) {
|
|
73
|
+
spacing = labelLength + spacing / 4;
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
// Offset the first anchor by:
|
|
@@ -82,20 +81,10 @@ function getAnchors(line, spacing, maxAngle, shapedText, shapedIcon, glyphSize,
|
|
|
82
81
|
const fixedExtraOffset = glyphSize * 2;
|
|
83
82
|
|
|
84
83
|
const offset = !isLineContinued
|
|
85
|
-
? ((
|
|
84
|
+
? ((shapedLabelLength / 2 + fixedExtraOffset) * boxScale * overscaling) % spacing
|
|
86
85
|
: ((spacing / 2) * overscaling) % spacing;
|
|
87
86
|
|
|
88
|
-
return resample(
|
|
89
|
-
line,
|
|
90
|
-
offset,
|
|
91
|
-
spacing,
|
|
92
|
-
angleWindowSize,
|
|
93
|
-
maxAngle,
|
|
94
|
-
labelLength * boxScale,
|
|
95
|
-
isLineContinued,
|
|
96
|
-
false,
|
|
97
|
-
tileExtent
|
|
98
|
-
);
|
|
87
|
+
return resample(line, offset, spacing, angleWindowSize, maxAngle, labelLength, isLineContinued, false, tileExtent);
|
|
99
88
|
}
|
|
100
89
|
|
|
101
90
|
function resample(
|
package/src/symbol/mergelines.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { Formatted } = require('../style-spec/expression/definitions/formatted');
|
|
2
|
+
|
|
1
3
|
module.exports = function (features) {
|
|
2
4
|
const leftIndex = new Map();
|
|
3
5
|
const rightIndex = new Map();
|
|
@@ -5,7 +7,8 @@ module.exports = function (features) {
|
|
|
5
7
|
let mergedIndex = 0;
|
|
6
8
|
|
|
7
9
|
for (let k = 0; k < features.length; k++) {
|
|
8
|
-
const { geometry, text } = features[k];
|
|
10
|
+
const { geometry, text: featureText } = features[k];
|
|
11
|
+
const text = featureText instanceof Formatted ? featureText.toString() : featureText;
|
|
9
12
|
|
|
10
13
|
if (!text) {
|
|
11
14
|
add(k);
|
package/src/symbol/placement.js
CHANGED
|
@@ -172,7 +172,12 @@ class Placement {
|
|
|
172
172
|
|
|
173
173
|
const collisionGroup = this.collisionGroups.get(bucket.sourceID);
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
if (!bucket.collisionArrays && collisionBoxArray) {
|
|
176
|
+
bucket.deserializeCollisionBoxes(collisionBoxArray);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < bucket.symbolInstances.length; i++) {
|
|
180
|
+
const symbolInstance = bucket.symbolInstances.get(i);
|
|
176
181
|
if (!seenCrossTileIDs[symbolInstance.crossTileID]) {
|
|
177
182
|
if (holdingForFade) {
|
|
178
183
|
// Mark all symbols from this tile as "not placed", but don't add to seenCrossTileIDs, because we don't
|
|
@@ -192,22 +197,14 @@ class Placement {
|
|
|
192
197
|
let textFeatureIndex = 0;
|
|
193
198
|
let iconFeatureIndex = 0;
|
|
194
199
|
|
|
195
|
-
|
|
196
|
-
symbolInstance.collisionArrays = bucket.deserializeCollisionBoxes(
|
|
197
|
-
collisionBoxArray,
|
|
198
|
-
symbolInstance.textBoxStartIndex,
|
|
199
|
-
symbolInstance.textBoxEndIndex,
|
|
200
|
-
symbolInstance.iconBoxStartIndex,
|
|
201
|
-
symbolInstance.iconBoxEndIndex
|
|
202
|
-
);
|
|
203
|
-
}
|
|
200
|
+
const collisionArrays = bucket.collisionArrays[i];
|
|
204
201
|
|
|
205
|
-
if (
|
|
206
|
-
textFeatureIndex =
|
|
202
|
+
if (collisionArrays.textFeatureIndex) {
|
|
203
|
+
textFeatureIndex = collisionArrays.textFeatureIndex;
|
|
207
204
|
}
|
|
208
|
-
if (
|
|
205
|
+
if (collisionArrays.textBox) {
|
|
209
206
|
placedGlyphBoxes = this.collisionIndex.placeCollisionBox(
|
|
210
|
-
|
|
207
|
+
collisionArrays.textBox,
|
|
211
208
|
layout.get('text-allow-overlap'),
|
|
212
209
|
textPixelRatio,
|
|
213
210
|
posMatrix,
|
|
@@ -216,9 +213,9 @@ class Placement {
|
|
|
216
213
|
placeText = placedGlyphBoxes.box.length > 0;
|
|
217
214
|
offscreen = offscreen && placedGlyphBoxes.offscreen;
|
|
218
215
|
}
|
|
219
|
-
const textCircles =
|
|
216
|
+
const textCircles = collisionArrays.textCircles;
|
|
220
217
|
if (textCircles) {
|
|
221
|
-
const placedSymbol = bucket.text.placedSymbolArray.get(symbolInstance.
|
|
218
|
+
const placedSymbol = bucket.text.placedSymbolArray.get(symbolInstance.horizontalPlacedTextSymbolIndex);
|
|
222
219
|
const fontSize = symbolSize.evaluateSizeForFeature(
|
|
223
220
|
bucket.textSizeData,
|
|
224
221
|
partiallyEvaluatedTextSize,
|
|
@@ -229,7 +226,6 @@ class Placement {
|
|
|
229
226
|
layout.get('text-allow-overlap'),
|
|
230
227
|
scale,
|
|
231
228
|
textPixelRatio,
|
|
232
|
-
symbolInstance.key,
|
|
233
229
|
placedSymbol,
|
|
234
230
|
bucket.lineVertexArray,
|
|
235
231
|
bucket.glyphOffsetArray,
|
|
@@ -248,12 +244,12 @@ class Placement {
|
|
|
248
244
|
offscreen = offscreen && placedGlyphCircles.offscreen;
|
|
249
245
|
}
|
|
250
246
|
|
|
251
|
-
if (
|
|
252
|
-
iconFeatureIndex =
|
|
247
|
+
if (collisionArrays.iconFeatureIndex) {
|
|
248
|
+
iconFeatureIndex = collisionArrays.iconFeatureIndex;
|
|
253
249
|
}
|
|
254
|
-
if (
|
|
250
|
+
if (collisionArrays.iconBox) {
|
|
255
251
|
placedIconBoxes = this.collisionIndex.placeCollisionBox(
|
|
256
|
-
|
|
252
|
+
collisionArrays.iconBox,
|
|
257
253
|
layout.get('icon-allow-overlap'),
|
|
258
254
|
textPixelRatio,
|
|
259
255
|
posMatrix,
|
|
@@ -398,16 +394,30 @@ class Placement {
|
|
|
398
394
|
|
|
399
395
|
const layout = bucket.layers[0].layout;
|
|
400
396
|
const duplicateOpacityState = new JointOpacityState(null, 0, false, false, true);
|
|
397
|
+
const textAllowOverlap = layout.get('text-allow-overlap');
|
|
398
|
+
const iconAllowOverlap = layout.get('icon-allow-overlap');
|
|
399
|
+
// If allow-overlap is true, we can show symbols before placement runs on them
|
|
400
|
+
// But we have to wait for placement if we potentially depend on a paired icon/text
|
|
401
|
+
// with allow-overlap: false.
|
|
402
|
+
// See https://github.com/mapbox/mapbox-gl-js/issues/7032
|
|
401
403
|
const defaultOpacityState = new JointOpacityState(
|
|
402
404
|
null,
|
|
403
405
|
0,
|
|
404
|
-
layout.get('
|
|
405
|
-
layout.get('
|
|
406
|
+
textAllowOverlap && (iconAllowOverlap || !bucket.hasIconData() || layout.get('icon-optional')),
|
|
407
|
+
iconAllowOverlap && (textAllowOverlap || !bucket.hasTextData() || layout.get('text-optional')),
|
|
406
408
|
true
|
|
407
409
|
);
|
|
408
410
|
|
|
411
|
+
if (
|
|
412
|
+
!bucket.collisionArrays &&
|
|
413
|
+
collisionBoxArray &&
|
|
414
|
+
(bucket.hasCollisionBoxData() || bucket.hasCollisionCircleData())
|
|
415
|
+
) {
|
|
416
|
+
bucket.deserializeCollisionBoxes(collisionBoxArray);
|
|
417
|
+
}
|
|
418
|
+
|
|
409
419
|
for (let s = 0; s < bucket.symbolInstances.length; s++) {
|
|
410
|
-
const symbolInstance = bucket.symbolInstances
|
|
420
|
+
const symbolInstance = bucket.symbolInstances.get(s);
|
|
411
421
|
const isDuplicate = seenCrossTileIDs[symbolInstance.crossTileID];
|
|
412
422
|
|
|
413
423
|
let opacityState = this.opacities[symbolInstance.crossTileID];
|
|
@@ -432,11 +442,14 @@ class Placement {
|
|
|
432
442
|
for (let i = 0; i < opacityEntryCount; i++) {
|
|
433
443
|
bucket.text.opacityVertexArray.emplaceBack(packedOpacity);
|
|
434
444
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
445
|
+
// If this label is completely faded, mark it so that we don't have to calculate
|
|
446
|
+
// its position at render time
|
|
447
|
+
bucket.text.placedSymbolArray.get(symbolInstance.horizontalPlacedTextSymbolIndex).hidden =
|
|
448
|
+
opacityState.text.isHidden();
|
|
449
|
+
|
|
450
|
+
if (symbolInstance.verticalPlacedTextSymbolIndex >= 0) {
|
|
451
|
+
bucket.text.placedSymbolArray.get(symbolInstance.verticalPlacedTextSymbolIndex).hidden =
|
|
452
|
+
opacityState.text.isHidden();
|
|
440
453
|
}
|
|
441
454
|
}
|
|
442
455
|
|
|
@@ -445,35 +458,26 @@ class Placement {
|
|
|
445
458
|
for (let i = 0; i < symbolInstance.numIconVertices / 4; i++) {
|
|
446
459
|
bucket.icon.opacityVertexArray.emplaceBack(packedOpacity);
|
|
447
460
|
}
|
|
448
|
-
|
|
449
|
-
placedSymbol.hidden = opacityState.icon.isHidden();
|
|
461
|
+
bucket.icon.placedSymbolArray.get(s).hidden = opacityState.icon.isHidden();
|
|
450
462
|
}
|
|
451
463
|
|
|
452
|
-
if (
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
symbolInstance.iconBoxEndIndex
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
const collisionArrays = symbolInstance.collisionArrays;
|
|
463
|
-
if (collisionArrays) {
|
|
464
|
-
if (collisionArrays.textBox && bucket.hasCollisionBoxData()) {
|
|
465
|
-
updateCollisionVertices(bucket.collisionBox.collisionVertexArray, opacityState.text.placed, false);
|
|
466
|
-
}
|
|
464
|
+
if (bucket.hasCollisionBoxData() || bucket.hasCollisionCircleData()) {
|
|
465
|
+
const collisionArrays = bucket.collisionArrays[s];
|
|
466
|
+
if (collisionArrays) {
|
|
467
|
+
if (collisionArrays.textBox) {
|
|
468
|
+
updateCollisionVertices(bucket.collisionBox.collisionVertexArray, opacityState.text.placed, false);
|
|
469
|
+
}
|
|
467
470
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
+
if (collisionArrays.iconBox) {
|
|
472
|
+
updateCollisionVertices(bucket.collisionBox.collisionVertexArray, opacityState.icon.placed, false);
|
|
473
|
+
}
|
|
471
474
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
475
|
+
const textCircles = collisionArrays.textCircles;
|
|
476
|
+
if (textCircles && bucket.hasCollisionCircleData()) {
|
|
477
|
+
for (let k = 0; k < textCircles.length; k += 5) {
|
|
478
|
+
const notUsed = isDuplicate || textCircles[k + 4] === 0;
|
|
479
|
+
updateCollisionVertices(bucket.collisionCircle.collisionVertexArray, opacityState.text.placed, notUsed);
|
|
480
|
+
}
|
|
477
481
|
}
|
|
478
482
|
}
|
|
479
483
|
}
|
package/src/symbol/quads.js
CHANGED
|
@@ -105,7 +105,8 @@ function getGlyphQuads(anchor, shaping, layer, alongLine, feature, positions) {
|
|
|
105
105
|
|
|
106
106
|
for (let k = 0; k < positionedGlyphs.length; k++) {
|
|
107
107
|
const positionedGlyph = positionedGlyphs[k];
|
|
108
|
-
const
|
|
108
|
+
const glyphPositions = positions[positionedGlyph.fontStack];
|
|
109
|
+
const glyph = glyphPositions?.[positionedGlyph.glyph];
|
|
109
110
|
if (!glyph) continue;
|
|
110
111
|
|
|
111
112
|
const rect = glyph.rect;
|
|
@@ -115,7 +116,7 @@ function getGlyphQuads(anchor, shaping, layer, alongLine, feature, positions) {
|
|
|
115
116
|
const glyphPadding = 1.0;
|
|
116
117
|
const rectBuffer = GLYPH_PBF_BORDER + glyphPadding;
|
|
117
118
|
|
|
118
|
-
const halfAdvance = glyph.metrics.advance / 2;
|
|
119
|
+
const halfAdvance = (glyph.metrics.advance * positionedGlyph.scale) / 2;
|
|
119
120
|
|
|
120
121
|
const glyphOffset = alongLine ? [positionedGlyph.x + halfAdvance, positionedGlyph.y] : [0, 0];
|
|
121
122
|
|
|
@@ -123,10 +124,10 @@ function getGlyphQuads(anchor, shaping, layer, alongLine, feature, positions) {
|
|
|
123
124
|
? [0, 0]
|
|
124
125
|
: [positionedGlyph.x + halfAdvance + textOffset[0], positionedGlyph.y + textOffset[1]];
|
|
125
126
|
|
|
126
|
-
const x1 = glyph.metrics.left - rectBuffer - halfAdvance + builtInOffset[0];
|
|
127
|
-
const y1 = -glyph.metrics.top - rectBuffer + builtInOffset[1];
|
|
128
|
-
const x2 = x1 + rect.w;
|
|
129
|
-
const y2 = y1 + rect.h;
|
|
127
|
+
const x1 = (glyph.metrics.left - rectBuffer) * positionedGlyph.scale - halfAdvance + builtInOffset[0];
|
|
128
|
+
const y1 = (-glyph.metrics.top - rectBuffer) * positionedGlyph.scale + builtInOffset[1];
|
|
129
|
+
const x2 = x1 + rect.w * positionedGlyph.scale;
|
|
130
|
+
const y2 = y1 + rect.h * positionedGlyph.scale;
|
|
130
131
|
|
|
131
132
|
const tl = new Point(x1, y1);
|
|
132
133
|
const tr = new Point(x2, y1);
|
package/src/symbol/shaping.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { charHasUprightVerticalOrientation, charAllowsIdeographicBreaking } = require('../util/script_detection');
|
|
2
2
|
const verticalizePunctuation = require('../util/verticalize_punctuation');
|
|
3
3
|
const { plugin: rtlTextPlugin } = require('../source/rtl_text_plugin');
|
|
4
|
+
const { Formatted } = require('../style-spec/expression/definitions/formatted');
|
|
4
5
|
|
|
5
6
|
const WritingMode = {
|
|
6
7
|
horizontal: 1,
|
|
@@ -14,20 +15,94 @@ module.exports = {
|
|
|
14
15
|
WritingMode
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
class TaggedString {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.text = '';
|
|
21
|
+
this.sectionIndex = []; // maps each character in 'text' to its corresponding entry in 'sections'
|
|
22
|
+
this.sections = [];
|
|
23
|
+
}
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
static fromFeature(text, defaultFontStack) {
|
|
26
|
+
const result = new TaggedString();
|
|
27
|
+
if (text instanceof Formatted) {
|
|
28
|
+
for (let i = 0; i < text.sections.length; i++) {
|
|
29
|
+
const section = text.sections[i];
|
|
30
|
+
result.sections.push({
|
|
31
|
+
scale: section.scale || 1,
|
|
32
|
+
fontStack: section.fontStack || defaultFontStack
|
|
33
|
+
});
|
|
34
|
+
result.text += section.text;
|
|
35
|
+
for (let j = 0; j < section.text.length; j++) {
|
|
36
|
+
result.sectionIndex.push(i);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
result.text = text;
|
|
41
|
+
result.sections.push({ scale: 1, fontStack: defaultFontStack });
|
|
42
|
+
for (let i = 0; i < text.length; i++) {
|
|
43
|
+
result.sectionIndex.push(0);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
length() {
|
|
50
|
+
return this.text.length;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getSection(index) {
|
|
54
|
+
return this.sections[this.sectionIndex[index]];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getCharCode(index) {
|
|
58
|
+
return this.text.charCodeAt(index);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
verticalizePunctuation() {
|
|
62
|
+
this.text = verticalizePunctuation(this.text);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
trim() {
|
|
66
|
+
let beginningWhitespace = 0;
|
|
67
|
+
for (let i = 0; i < this.text.length && whitespace[this.text.charCodeAt(i)]; i++) {
|
|
68
|
+
beginningWhitespace++;
|
|
69
|
+
}
|
|
70
|
+
let trailingWhitespace = this.text.length;
|
|
71
|
+
for (let i = this.text.length - 1; i >= 0 && i >= beginningWhitespace && whitespace[this.text.charCodeAt(i)]; i--) {
|
|
72
|
+
trailingWhitespace--;
|
|
73
|
+
}
|
|
74
|
+
this.text = this.text.substring(beginningWhitespace, trailingWhitespace);
|
|
75
|
+
this.sectionIndex = this.sectionIndex.slice(beginningWhitespace, trailingWhitespace);
|
|
76
|
+
}
|
|
20
77
|
|
|
21
|
-
|
|
78
|
+
substring(start, end) {
|
|
79
|
+
const substring = new TaggedString();
|
|
80
|
+
substring.text = this.text.substring(start, end);
|
|
81
|
+
substring.sectionIndex = this.sectionIndex.slice(start, end);
|
|
82
|
+
substring.sections = this.sections;
|
|
83
|
+
return substring;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
toString() {
|
|
87
|
+
return this.text;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getMaxScale() {
|
|
91
|
+
return this.sectionIndex.reduce((max, index) => Math.max(max, this.sections[index].scale), 0);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function breakLines(input, lineBreakPoints) {
|
|
22
96
|
const lines = [];
|
|
97
|
+
const text = input.text;
|
|
23
98
|
let start = 0;
|
|
24
99
|
for (const lineBreak of lineBreakPoints) {
|
|
25
|
-
lines.push(
|
|
100
|
+
lines.push(input.substring(start, lineBreak));
|
|
26
101
|
start = lineBreak;
|
|
27
102
|
}
|
|
28
103
|
|
|
29
104
|
if (start < text.length) {
|
|
30
|
-
lines.push(
|
|
105
|
+
lines.push(input.substring(start, text.length));
|
|
31
106
|
}
|
|
32
107
|
return lines;
|
|
33
108
|
}
|
|
@@ -35,6 +110,7 @@ function breakLines(text, lineBreakPoints) {
|
|
|
35
110
|
function shapeText(
|
|
36
111
|
text,
|
|
37
112
|
glyphs,
|
|
113
|
+
defaultFontStack,
|
|
38
114
|
maxWidth,
|
|
39
115
|
lineHeight,
|
|
40
116
|
textAnchor,
|
|
@@ -44,9 +120,9 @@ function shapeText(
|
|
|
44
120
|
verticalHeight,
|
|
45
121
|
writingMode
|
|
46
122
|
) {
|
|
47
|
-
|
|
123
|
+
const logicalInput = TaggedString.fromFeature(text, defaultFontStack);
|
|
48
124
|
if (writingMode === WritingMode.vertical) {
|
|
49
|
-
logicalInput
|
|
125
|
+
logicalInput.verticalizePunctuation();
|
|
50
126
|
}
|
|
51
127
|
|
|
52
128
|
const positionedGlyphs = [];
|
|
@@ -62,9 +138,39 @@ function shapeText(
|
|
|
62
138
|
|
|
63
139
|
let lines;
|
|
64
140
|
|
|
65
|
-
const { processBidirectionalText } = rtlTextPlugin;
|
|
66
|
-
if (processBidirectionalText) {
|
|
67
|
-
|
|
141
|
+
const { processBidirectionalText, processStyledBidirectionalText } = rtlTextPlugin;
|
|
142
|
+
if (processBidirectionalText && logicalInput.sections.length === 1) {
|
|
143
|
+
// Bidi doesn't have to be style-aware
|
|
144
|
+
lines = [];
|
|
145
|
+
const untaggedLines = processBidirectionalText(
|
|
146
|
+
logicalInput.toString(),
|
|
147
|
+
determineLineBreaks(logicalInput, spacing, maxWidth, glyphs)
|
|
148
|
+
);
|
|
149
|
+
for (const line of untaggedLines) {
|
|
150
|
+
const taggedLine = new TaggedString();
|
|
151
|
+
taggedLine.text = line;
|
|
152
|
+
taggedLine.sections = logicalInput.sections;
|
|
153
|
+
for (let i = 0; i < line.length; i++) {
|
|
154
|
+
taggedLine.sectionIndex.push(0);
|
|
155
|
+
}
|
|
156
|
+
lines.push(taggedLine);
|
|
157
|
+
}
|
|
158
|
+
} else if (processStyledBidirectionalText) {
|
|
159
|
+
// Need version of mapbox-gl-rtl-text with style support for combining RTL text
|
|
160
|
+
// with formatting
|
|
161
|
+
lines = [];
|
|
162
|
+
const processedLines = processStyledBidirectionalText(
|
|
163
|
+
logicalInput.text,
|
|
164
|
+
logicalInput.sectionIndex,
|
|
165
|
+
determineLineBreaks(logicalInput, spacing, maxWidth, glyphs)
|
|
166
|
+
);
|
|
167
|
+
for (const line of processedLines) {
|
|
168
|
+
const taggedLine = new TaggedString();
|
|
169
|
+
taggedLine.text = line[0];
|
|
170
|
+
taggedLine.sectionIndex = line[1];
|
|
171
|
+
taggedLine.sections = logicalInput.sections;
|
|
172
|
+
lines.push(taggedLine);
|
|
173
|
+
}
|
|
68
174
|
} else {
|
|
69
175
|
lines = breakLines(logicalInput, determineLineBreaks(logicalInput, spacing, maxWidth, glyphs));
|
|
70
176
|
}
|
|
@@ -73,6 +179,7 @@ function shapeText(
|
|
|
73
179
|
|
|
74
180
|
if (!positionedGlyphs.length) return false;
|
|
75
181
|
|
|
182
|
+
shaping.text = shaping.text.toString();
|
|
76
183
|
return shaping;
|
|
77
184
|
}
|
|
78
185
|
|
|
@@ -105,13 +212,15 @@ const breakable = {
|
|
|
105
212
|
// See https://github.com/mapbox/mapbox-gl-js/issues/3658
|
|
106
213
|
};
|
|
107
214
|
|
|
108
|
-
function determineAverageLineWidth(logicalInput, spacing, maxWidth,
|
|
215
|
+
function determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphMap) {
|
|
109
216
|
let totalWidth = 0;
|
|
110
217
|
|
|
111
|
-
for (let index = 0; index < logicalInput.length; index++) {
|
|
112
|
-
const
|
|
218
|
+
for (let index = 0; index < logicalInput.length(); index++) {
|
|
219
|
+
const section = logicalInput.getSection(index);
|
|
220
|
+
const positions = glyphMap[section.fontStack];
|
|
221
|
+
const glyph = positions?.[logicalInput.getCharCode(index)];
|
|
113
222
|
if (!glyph) continue;
|
|
114
|
-
totalWidth += glyph.metrics.advance + spacing;
|
|
223
|
+
totalWidth += glyph.metrics.advance * section.scale + spacing;
|
|
115
224
|
}
|
|
116
225
|
|
|
117
226
|
const lineCount = Math.max(1, Math.ceil(totalWidth / maxWidth));
|
|
@@ -182,39 +291,41 @@ function leastBadBreaks(lastLineBreak) {
|
|
|
182
291
|
return leastBadBreaks(lastLineBreak.priorBreak).concat(lastLineBreak.index);
|
|
183
292
|
}
|
|
184
293
|
|
|
185
|
-
function determineLineBreaks(logicalInput, spacing, maxWidth,
|
|
294
|
+
function determineLineBreaks(logicalInput, spacing, maxWidth, glyphMap) {
|
|
186
295
|
if (!maxWidth) return [];
|
|
187
296
|
|
|
188
297
|
if (!logicalInput) return [];
|
|
189
298
|
|
|
190
299
|
const potentialLineBreaks = [];
|
|
191
|
-
const targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth,
|
|
300
|
+
const targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphMap);
|
|
192
301
|
|
|
193
302
|
let currentX = 0;
|
|
194
303
|
|
|
195
|
-
for (let i = 0; i < logicalInput.length; i++) {
|
|
196
|
-
const
|
|
197
|
-
const
|
|
304
|
+
for (let i = 0; i < logicalInput.length(); i++) {
|
|
305
|
+
const section = logicalInput.getSection(i);
|
|
306
|
+
const codePoint = logicalInput.getCharCode(i);
|
|
307
|
+
const positions = glyphMap[section.fontStack];
|
|
308
|
+
const glyph = positions?.[codePoint];
|
|
198
309
|
|
|
199
|
-
if (glyph && !whitespace[codePoint]) currentX += glyph.metrics.advance + spacing;
|
|
310
|
+
if (glyph && !whitespace[codePoint]) currentX += glyph.metrics.advance * section.scale + spacing;
|
|
200
311
|
|
|
201
312
|
// Ideographic characters, spaces, and word-breaking punctuation that often appear without
|
|
202
313
|
// surrounding spaces.
|
|
203
|
-
if (i < logicalInput.length - 1 && (breakable[codePoint] || charAllowsIdeographicBreaking(codePoint))) {
|
|
314
|
+
if (i < logicalInput.length() - 1 && (breakable[codePoint] || charAllowsIdeographicBreaking(codePoint))) {
|
|
204
315
|
potentialLineBreaks.push(
|
|
205
316
|
evaluateBreak(
|
|
206
317
|
i + 1,
|
|
207
318
|
currentX,
|
|
208
319
|
targetWidth,
|
|
209
320
|
potentialLineBreaks,
|
|
210
|
-
calculatePenalty(codePoint, logicalInput.
|
|
321
|
+
calculatePenalty(codePoint, logicalInput.getCharCode(i + 1)),
|
|
211
322
|
false
|
|
212
323
|
)
|
|
213
324
|
);
|
|
214
325
|
}
|
|
215
326
|
}
|
|
216
327
|
|
|
217
|
-
return leastBadBreaks(evaluateBreak(logicalInput.length, currentX, targetWidth, potentialLineBreaks, 0, true));
|
|
328
|
+
return leastBadBreaks(evaluateBreak(logicalInput.length(), currentX, targetWidth, potentialLineBreaks, 0, true));
|
|
218
329
|
}
|
|
219
330
|
|
|
220
331
|
function getAnchorAlignment(anchor) {
|
|
@@ -250,7 +361,17 @@ function getAnchorAlignment(anchor) {
|
|
|
250
361
|
return { horizontalAlign, verticalAlign };
|
|
251
362
|
}
|
|
252
363
|
|
|
253
|
-
function shapeLines(
|
|
364
|
+
function shapeLines(
|
|
365
|
+
shaping,
|
|
366
|
+
glyphMap,
|
|
367
|
+
lines,
|
|
368
|
+
lineHeight,
|
|
369
|
+
textAnchor,
|
|
370
|
+
textJustify,
|
|
371
|
+
writingMode,
|
|
372
|
+
spacing,
|
|
373
|
+
verticalHeight
|
|
374
|
+
) {
|
|
254
375
|
// the y offset *should* be part of the font metadata
|
|
255
376
|
const yOffset = -17;
|
|
256
377
|
|
|
@@ -262,27 +383,49 @@ function shapeLines(shaping, glyphs, lines, lineHeight, textAnchor, textJustify,
|
|
|
262
383
|
|
|
263
384
|
const justify = textJustify === 'right' ? 1 : textJustify === 'left' ? 0 : 0.5;
|
|
264
385
|
|
|
265
|
-
for (
|
|
266
|
-
line
|
|
386
|
+
for (const line of lines) {
|
|
387
|
+
line.trim();
|
|
388
|
+
|
|
389
|
+
const lineMaxScale = line.getMaxScale();
|
|
267
390
|
|
|
268
|
-
if (!line.length) {
|
|
391
|
+
if (!line.length()) {
|
|
269
392
|
y += lineHeight; // Still need a line feed after empty line
|
|
270
393
|
continue;
|
|
271
394
|
}
|
|
272
395
|
|
|
273
396
|
const lineStartIndex = positionedGlyphs.length;
|
|
274
|
-
for (let i = 0; i < line.length; i++) {
|
|
275
|
-
const
|
|
276
|
-
const
|
|
397
|
+
for (let i = 0; i < line.length(); i++) {
|
|
398
|
+
const section = line.getSection(i);
|
|
399
|
+
const codePoint = line.getCharCode(i);
|
|
400
|
+
// We don't know the baseline, but since we're laying out
|
|
401
|
+
// at 24 points, we can calculate how much it will move when
|
|
402
|
+
// we scale up or down.
|
|
403
|
+
const baselineOffset = (lineMaxScale - section.scale) * 24;
|
|
404
|
+
const positions = glyphMap[section.fontStack];
|
|
405
|
+
const glyph = positions?.[codePoint];
|
|
277
406
|
|
|
278
407
|
if (!glyph) continue;
|
|
279
408
|
|
|
280
409
|
if (!charHasUprightVerticalOrientation(codePoint) || writingMode === WritingMode.horizontal) {
|
|
281
|
-
positionedGlyphs.push({
|
|
282
|
-
|
|
410
|
+
positionedGlyphs.push({
|
|
411
|
+
glyph: codePoint,
|
|
412
|
+
x,
|
|
413
|
+
y: y + baselineOffset,
|
|
414
|
+
vertical: false,
|
|
415
|
+
scale: section.scale,
|
|
416
|
+
fontStack: section.fontStack
|
|
417
|
+
});
|
|
418
|
+
x += glyph.metrics.advance * section.scale + spacing;
|
|
283
419
|
} else {
|
|
284
|
-
positionedGlyphs.push({
|
|
285
|
-
|
|
420
|
+
positionedGlyphs.push({
|
|
421
|
+
glyph: codePoint,
|
|
422
|
+
x,
|
|
423
|
+
y: baselineOffset,
|
|
424
|
+
vertical: true,
|
|
425
|
+
scale: section.scale,
|
|
426
|
+
fontStack: section.fontStack
|
|
427
|
+
});
|
|
428
|
+
x += verticalHeight * section.scale + spacing;
|
|
286
429
|
}
|
|
287
430
|
}
|
|
288
431
|
|
|
@@ -291,18 +434,18 @@ function shapeLines(shaping, glyphs, lines, lineHeight, textAnchor, textJustify,
|
|
|
291
434
|
const lineLength = x - spacing;
|
|
292
435
|
maxLineLength = Math.max(lineLength, maxLineLength);
|
|
293
436
|
|
|
294
|
-
justifyLine(positionedGlyphs,
|
|
437
|
+
justifyLine(positionedGlyphs, glyphMap, lineStartIndex, positionedGlyphs.length - 1, justify);
|
|
295
438
|
}
|
|
296
439
|
|
|
297
440
|
x = 0;
|
|
298
|
-
y += lineHeight;
|
|
441
|
+
y += lineHeight * lineMaxScale;
|
|
299
442
|
}
|
|
300
443
|
|
|
301
444
|
const { horizontalAlign, verticalAlign } = getAnchorAlignment(textAnchor);
|
|
302
445
|
align(positionedGlyphs, justify, horizontalAlign, verticalAlign, maxLineLength, lineHeight, lines.length);
|
|
303
446
|
|
|
304
447
|
// Calculate the bounding box
|
|
305
|
-
const height =
|
|
448
|
+
const height = y - yOffset;
|
|
306
449
|
|
|
307
450
|
shaping.top += -verticalAlign * height;
|
|
308
451
|
shaping.bottom = shaping.top + height;
|
|
@@ -311,12 +454,14 @@ function shapeLines(shaping, glyphs, lines, lineHeight, textAnchor, textJustify,
|
|
|
311
454
|
}
|
|
312
455
|
|
|
313
456
|
// justify right = 1, left = 0, center = 0.5
|
|
314
|
-
function justifyLine(positionedGlyphs,
|
|
457
|
+
function justifyLine(positionedGlyphs, glyphMap, start, end, justify) {
|
|
315
458
|
if (!justify) return;
|
|
316
459
|
|
|
317
|
-
const
|
|
460
|
+
const lastPositionedGlyph = positionedGlyphs[end];
|
|
461
|
+
const positions = glyphMap[lastPositionedGlyph.fontStack];
|
|
462
|
+
const glyph = positions?.[lastPositionedGlyph.glyph];
|
|
318
463
|
if (glyph) {
|
|
319
|
-
const lastAdvance = glyph.metrics.advance;
|
|
464
|
+
const lastAdvance = glyph.metrics.advance * lastPositionedGlyph.scale;
|
|
320
465
|
const lineIndent = (positionedGlyphs[end].x + lastAdvance) * justify;
|
|
321
466
|
|
|
322
467
|
for (let j = start; j <= end; j++) {
|