summaryrefslogtreecommitdiff
path: root/src/mbgl/text/quads.cpp
blob: ab10c5a6b7517452ee444ef5a206637865a20dab (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
#include <mbgl/text/quads.hpp>
#include <mbgl/text/shaping.hpp>
#include <mbgl/tile/geometry_tile_data.hpp>
#include <mbgl/geometry/anchor.hpp>
#include <mbgl/style/layers/symbol_layer_properties.hpp>
#include <mbgl/util/math.hpp>
#include <mbgl/util/constants.hpp>
#include <mbgl/util/optional.hpp>

#include <cassert>

namespace mbgl {

using namespace style;

const float globalMinScale = 0.5f; // underscale by 1 zoom level

SymbolQuad getIconQuad(const Anchor& anchor,
                       const PositionedIcon& shapedIcon,
                       const GeometryCoordinates& line,
                       const SymbolLayoutProperties::Evaluated& layout,
                       const float layoutTextSize,
                       const style::SymbolPlacementType placement, 
                       const Shaping& shapedText) {
    const ImagePosition& image = shapedIcon.image();

    // If you have a 10px icon that isn't perfectly aligned to the pixel grid it will cover 11 actual
    // pixels. The quad needs to be padded to account for this, otherwise they'll look slightly clipped
    // on one edge in some cases.
    const float border = 1.0;

    float top = shapedIcon.top() - border / image.pixelRatio;
    float left = shapedIcon.left() - border / image.pixelRatio;
    float bottom = shapedIcon.bottom() + border / image.pixelRatio;
    float right = shapedIcon.right() + border / image.pixelRatio;
    Point<float> tl;
    Point<float> tr;
    Point<float> br;
    Point<float> bl;

    if (layout.get<IconTextFit>() != IconTextFitType::None && shapedText) {
        auto iconWidth = right - left;
        auto iconHeight = bottom - top;
        auto size = layoutTextSize / 24.0f;
        auto textLeft = shapedText.left * size;
        auto textRight = shapedText.right * size;
        auto textTop = shapedText.top * size;
        auto textBottom = shapedText.bottom * size;
        auto textWidth = textRight - textLeft;
        auto textHeight = textBottom - textTop;
        auto padT = layout.get<IconTextFitPadding>()[0];
        auto padR = layout.get<IconTextFitPadding>()[1];
        auto padB = layout.get<IconTextFitPadding>()[2];
        auto padL = layout.get<IconTextFitPadding>()[3];
        auto offsetY = layout.get<IconTextFit>() == IconTextFitType::Width ? (textHeight - iconHeight) * 0.5 : 0;
        auto offsetX = layout.get<IconTextFit>() == IconTextFitType::Height ? (textWidth - iconWidth) * 0.5 : 0;
        auto width = layout.get<IconTextFit>() == IconTextFitType::Width || layout.get<IconTextFit>() == IconTextFitType::Both ? textWidth : iconWidth;
        auto height = layout.get<IconTextFit>() == IconTextFitType::Height || layout.get<IconTextFit>() == IconTextFitType::Both ? textHeight : iconHeight;
        left = textLeft + offsetX - padL;
        top = textTop + offsetY - padT;
        right = textLeft + offsetX + padR + width;
        bottom = textTop + offsetY + padB + height;
        tl = {left, top};
        tr = {right, top};
        br = {right, bottom};
        bl = {left, bottom};
    } else {
        tl = {left, top};
        tr = {right, top};
        br = {right, bottom};
        bl = {left, bottom};
    }

    float angle = shapedIcon.angle();
    if (placement == style::SymbolPlacementType::Line) {
        assert(static_cast<unsigned int>(anchor.segment) < line.size());
        const GeometryCoordinate &prev= line[anchor.segment];
        if (anchor.point.y == prev.y && anchor.point.x == prev.x &&
            static_cast<unsigned int>(anchor.segment + 1) < line.size()) {
            const GeometryCoordinate &next= line[anchor.segment + 1];
            angle += std::atan2(anchor.point.y - next.y, anchor.point.x - next.x) + M_PI;
        } else {
            angle += std::atan2(anchor.point.y - prev.y, anchor.point.x - prev.x);
        }
    }

    if (angle) {
        // Compute the transformation matrix.
        float angle_sin = std::sin(angle);
        float angle_cos = std::cos(angle);
        std::array<float, 4> matrix = {{angle_cos, -angle_sin, angle_sin, angle_cos}};

        tl = util::matrixMultiply(matrix, tl);
        tr = util::matrixMultiply(matrix, tr);
        bl = util::matrixMultiply(matrix, bl);
        br = util::matrixMultiply(matrix, br);
    }

    // Icon quad is padded, so texture coordinates also need to be padded.
    Rect<uint16_t> textureRect {
        static_cast<uint16_t>(image.textureRect.x - border),
        static_cast<uint16_t>(image.textureRect.y - border),
        static_cast<uint16_t>(image.textureRect.w + border * 2),
        static_cast<uint16_t>(image.textureRect.h + border * 2)
    };

    return SymbolQuad { tl, tr, bl, br, textureRect, 0, 0, anchor.point, globalMinScale, std::numeric_limits<float>::infinity(), shapedText.writingMode };
}

struct GlyphInstance {
    explicit GlyphInstance(Point<float> anchorPoint_) : anchorPoint(std::move(anchorPoint_)) {}
    explicit GlyphInstance(Point<float> anchorPoint_, bool upsideDown_, float minScale_, float maxScale_,
            float angle_)
        : anchorPoint(std::move(anchorPoint_)), upsideDown(upsideDown_), minScale(minScale_), maxScale(maxScale_), angle(angle_) {}

    const Point<float> anchorPoint;
    const bool upsideDown = false;
    const float minScale = globalMinScale;
    const float maxScale = std::numeric_limits<float>::infinity();
    const float angle = 0.0f;
};

using GlyphInstances = std::vector<GlyphInstance>;
    
struct VirtualSegment {
    Point<float> anchor;
    Point<float> end;
    size_t index;
    float minScale;
    float maxScale;
};
    
inline void insertSegmentGlyph(std::back_insert_iterator<GlyphInstances> glyphs,
                        const VirtualSegment& virtualSegment,
                        const bool glyphIsLogicallyForward,
                        const bool upsideDown) {
    float segmentAngle = std::atan2(virtualSegment.end.y - virtualSegment.anchor.y, virtualSegment.end.x - virtualSegment.anchor.x);
    // If !glyphIsLogicallyForward, we're iterating through the segments in reverse logical order as well, so we need to flip the segment angle
    float glyphAngle = glyphIsLogicallyForward ? segmentAngle : segmentAngle + M_PI;
    
    // Insert a glyph rotated at this angle for display in the range from [scale, previous(larger) scale].
    glyphs = GlyphInstance{
        /* anchor */ virtualSegment.anchor,
        /* upsideDown */ upsideDown,
        /* minScale */ virtualSegment.minScale,
        /* maxScale */ virtualSegment.maxScale,
        /* angle */ static_cast<float>(std::fmod((glyphAngle + 2.0 * M_PI), (2.0 * M_PI)))};
}

/**
 Given the distance along the line from the label anchor to the beginning of the current segment,
 project a "virtual anchor" point at the same distance along the line extending out from this segment.
 
                B <-- beginning of current segment
* . . . . . . . *--------* E <-- end of current segment
VA              |      
                /        VA = "virtual segment anchor"
               /
     ---*-----`
        A = label anchor
 
 Distance _along line_ from A to B == straight-line distance from VA to B.
 */
inline Point<float> getVirtualSegmentAnchor(const Point<float>& segmentBegin, const Point<float>& segmentEnd, float distanceFromAnchorToSegmentBegin) {
    Point<float> segmentDirectionUnitVector = util::normal<float>(segmentBegin, segmentEnd);
    return segmentBegin - (segmentDirectionUnitVector * distanceFromAnchorToSegmentBegin);
}

/*
 Given the segment joining `segmentAnchor` and `segmentEnd` and a desired offset
 `glyphDistanceFromAnchor` at which a glyph is to be placed, calculate the minimum
 "scale" at which the glyph will fall on the segment (i.e., not past the end)

 "Scale" here refers to the ratio between the *rendered* zoom level and the text-layout
 zoom level, which is 1 + (source tile's zoom level).  `glyphDistanceFromAnchor`, although
 passed in units consistent with the text-layout zoom level, is based on text size.  So
 when the tile is being rendered at z < text-layout zoom, the glyph's actual distance from
 the anchor is larger relative to the segment's length than at layout time:
 
 
                                                                   GLYPH
 z == layout-zoom, scale == 1:        segmentAnchor *--------------^-------------* segmentEnd
 z == layout-zoom - 1, scale == 0.5:  segmentAnchor *--------------^* segmentEnd
 
                                                    <-------------->
                                                    Anchor-to-glyph distance stays visually fixed,
                                                    so it changes relative to the segment.
*/
inline float getMinScaleForSegment(const float glyphDistanceFromAnchor,
                         const Point<float>& segmentAnchor,
                         const Point<float>& segmentEnd) {
    const auto distanceFromAnchorToEnd = util::dist<float>(segmentAnchor, segmentEnd);
    return glyphDistanceFromAnchor / distanceFromAnchorToEnd;
}

inline Point<float> getSegmentEnd(const bool glyphIsLogicallyForward,
                           const GeometryCoordinates& line,
                           const size_t segmentIndex) {
    return convertPoint<float>(glyphIsLogicallyForward ? line[segmentIndex+1] : line[segmentIndex]);
}

optional<VirtualSegment> getNextVirtualSegment(const VirtualSegment& previousVirtualSegment,
                                               const GeometryCoordinates& line,
                                               const float glyphDistanceFromAnchor,
                                               const bool glyphIsLogicallyForward) {
    auto nextSegmentBegin = previousVirtualSegment.end;
    
    auto end = nextSegmentBegin;
    size_t index = previousVirtualSegment.index;

    // skip duplicate nodes
    while (end == nextSegmentBegin) {
        // look ahead by 2 points in the line because the segment index refers to the beginning
        // of the segment, and we need an endpoint too
        if (glyphIsLogicallyForward && (index + 2 < line.size())) {
            index += 1;
        } else if (!glyphIsLogicallyForward && index != 0) {
            index -= 1;
        } else {
            return {};
        }
        
        end = getSegmentEnd(glyphIsLogicallyForward, line, index);
    }
    
    const auto anchor = getVirtualSegmentAnchor(nextSegmentBegin, end,
                                                util::dist<float>(previousVirtualSegment.anchor,
                                                                  previousVirtualSegment.end));
    return VirtualSegment {
        anchor,
        end,
        index,
        getMinScaleForSegment(glyphDistanceFromAnchor, anchor, end),
        previousVirtualSegment.minScale
    };
}
    
/*
 Given (1) a glyph positioned relative to an anchor point and (2) a line to follow,
 calculates which segment of the line the glyph will fall on for each possible
 scale range, and for each range produces a "virtual" anchor point and an angle that will
 place the glyph on the right segment and rotated to the correct angle.
 
 Because one glyph quad is made ahead of time for each possible orientation, the
 symbol_sdf shader can quickly handle changing layout as we zoom in and out
 
 If the "keepUpright" property is set, we call getLineGlyphs twice (once upright and 
 once "upside down"). This will generate two sets of glyphs following the line in opposite
 directions. Later, SymbolLayout::place will look at the glyphs and based on the placement
 angle determine if their original anchor was "upright" or not -- based on that, it throws
 away one set of glyphs or the other (this work has to be done in the CPU, but it's just a
 filter so it's fast)
 */
void getLineGlyphs(std::back_insert_iterator<GlyphInstances> glyphs,
                      Anchor& anchor,
                      float glyphHorizontalOffsetFromAnchor,
                      const GeometryCoordinates& line,
                      size_t anchorSegment,
                      bool upsideDown) {
    assert(line.size() > anchorSegment+1);
    
    // This is true if the glyph is "logically forward" of the anchor point, based on the ordering of line segments
    //  The actual angle of the line is irrelevant
    //  If "upsideDown" is set, everything is flipped
    const bool glyphIsLogicallyForward = (glyphHorizontalOffsetFromAnchor >= 0) ^ upsideDown;
    const float glyphDistanceFromAnchor = std::fabs(glyphHorizontalOffsetFromAnchor);
    
    const auto initialSegmentEnd = getSegmentEnd(glyphIsLogicallyForward, line, anchorSegment);
    VirtualSegment virtualSegment = {
        anchor.point,
        initialSegmentEnd,
        anchorSegment,
        getMinScaleForSegment(glyphDistanceFromAnchor, anchor.point, initialSegmentEnd),
        std::numeric_limits<float>::infinity()
    };
    
    while (true) {
        insertSegmentGlyph(glyphs,
                           virtualSegment,
                           glyphIsLogicallyForward,
                           upsideDown);
        
        if (virtualSegment.minScale <= anchor.scale) {
            // No need to calculate below the scale where the label starts showing
            return;
        }
        
        optional<VirtualSegment> nextVirtualSegment = getNextVirtualSegment(virtualSegment,
                                                                            line,
                                                                            glyphDistanceFromAnchor,
                                                                            glyphIsLogicallyForward);
        if (!nextVirtualSegment) {
            // There are no more segments, so we can't fit this glyph on the line at a lower scale
            // This implies we can't show the label at all at lower scale, so we update the anchor's min scale
            anchor.scale = virtualSegment.minScale;
            return;
        } else {
            virtualSegment = *nextVirtualSegment;
        }
    }
    
}

SymbolQuads getGlyphQuads(Anchor& anchor,
                          const Shaping& shapedText,
                          const float boxScale,
                          const GeometryCoordinates& line,
                          const SymbolLayoutProperties::Evaluated& layout,
                          const style::SymbolPlacementType placement,
                          const GlyphPositionMap& positions) {
    const float textRotate = layout.get<TextRotate>() * util::DEG2RAD;
    const bool keepUpright = layout.get<TextKeepUpright>();

    SymbolQuads quads;

    for (const PositionedGlyph &positionedGlyph: shapedText.positionedGlyphs) {
        auto positionsIt = positions.find(positionedGlyph.glyph);
        if (positionsIt == positions.end())
            continue;

        const GlyphPosition& glyph = positionsIt->second;
        const Rect<uint16_t>& rect = glyph.rect;
        const float centerX = (positionedGlyph.x + glyph.metrics.advance / 2.0f) * boxScale;

        GlyphInstances glyphInstances;
        if (placement == style::SymbolPlacementType::Line) {
            getLineGlyphs(std::back_inserter(glyphInstances), anchor, centerX, line, anchor.segment, false);
            if (keepUpright)
                getLineGlyphs(std::back_inserter(glyphInstances), anchor, centerX, line, anchor.segment, true);
        } else {
            glyphInstances.emplace_back(GlyphInstance{anchor.point});
        }

        // The rects have an addditional buffer that is not included in their size;
        const float glyphPadding = 1.0f;
        const float rectBuffer = 3.0f + glyphPadding;

        const float x1 = positionedGlyph.x + glyph.metrics.left - rectBuffer;
        const float y1 = positionedGlyph.y - glyph.metrics.top - rectBuffer;
        const float x2 = x1 + rect.w;
        const float y2 = y1 + rect.h;

        const Point<float> center{positionedGlyph.x, static_cast<float>(static_cast<float>(glyph.metrics.advance) / 2.0)};

        Point<float> otl{x1, y1};
        Point<float> otr{x2, y1};
        Point<float> obl{x1, y2};
        Point<float> obr{x2, y2};

        if (positionedGlyph.angle != 0) {
            otl = util::rotate(otl - center, positionedGlyph.angle) + center;
            otr = util::rotate(otr - center, positionedGlyph.angle) + center;
            obl = util::rotate(obl - center, positionedGlyph.angle) + center;
            obr = util::rotate(obr - center, positionedGlyph.angle) + center;
        }

        for (const GlyphInstance &instance : glyphInstances) {
            Point<float> tl = otl;
            Point<float> tr = otr;
            Point<float> bl = obl;
            Point<float> br = obr;

            if (textRotate) {
                // Compute the transformation matrix.
                float angle_sin = std::sin(textRotate);
                float angle_cos = std::cos(textRotate);
                std::array<float, 4> matrix = {{angle_cos, -angle_sin, angle_sin, angle_cos}};

                tl = util::matrixMultiply(matrix, tl);
                tr = util::matrixMultiply(matrix, tr);
                bl = util::matrixMultiply(matrix, bl);
                br = util::matrixMultiply(matrix, br);
            }

            // Prevent label from extending past the end of the line
            const float glyphMinScale = std::max(instance.minScale, anchor.scale);

            // All the glyphs for a label are tagged with either the "right side up" or "upside down" anchor angle,
            //  which is used at placement time to determine which set to show
            const float anchorAngle = std::fmod((anchor.angle + (instance.upsideDown ? M_PI : 0.0) + 2 * M_PI), (2 * M_PI));
            const float glyphAngle = std::fmod((instance.angle + (instance.upsideDown ? M_PI : 0.0) + 2 * M_PI), (2 * M_PI));
            quads.emplace_back(tl, tr, bl, br, rect, anchorAngle, glyphAngle, instance.anchorPoint, glyphMinScale, instance.maxScale, shapedText.writingMode);
        }
    }

    return quads;
}
} // namespace mbgl