void getSegmentGlyphs(std::back_insert_iterator<GlyphInstances> glyphs, Anchor &anchor, float offset, const GeometryCoordinates &line, int segment, bool forward) { const bool upsideDown = !forward; if (offset < 0) forward = !forward; if (forward) segment++; assert((int)line.size() > segment); vec2<float> end = line[segment]; vec2<float> newAnchorPoint = anchor; float prevscale = std::numeric_limits<float>::infinity(); offset = std::fabs(offset); const float placementScale = anchor.scale; while (true) { const float dist = util::dist<float>(newAnchorPoint, end); const float scale = offset / dist; float angle = std::atan2(end.y - newAnchorPoint.y, end.x - newAnchorPoint.x); if (!forward) angle += M_PI; if (upsideDown) angle += M_PI; glyphs = GlyphInstance{ /* anchor */ newAnchorPoint, /* offset */ static_cast<float>(upsideDown ? M_PI : 0.0), /* minScale */ scale, /* maxScale */ prevscale, /* angle */ static_cast<float>(std::fmod((angle + 2.0 * M_PI), (2.0 * M_PI)))}; if (scale <= placementScale) break; newAnchorPoint = end; // skip duplicate nodes while (newAnchorPoint == end) { segment += forward ? 1 : -1; if ((int)line.size() <= segment || segment < 0) { anchor.scale = scale; return; } end = line[segment]; } vec2<float> normal = util::normal<float>(newAnchorPoint, end) * dist; newAnchorPoint = newAnchorPoint - normal; prevscale = scale; } }
bool lineIntersectsLine(const GeometryCoordinates& lineA, const GeometryCoordinates& lineB) { if (lineA.size() == 0 || lineB.size() == 0) return false; for (auto i = lineA.begin(); i != lineA.end() - 1; i++) { auto& a0 = *i; auto& a1 = *(i + 1); for (auto j = lineB.begin(); j != lineB.end() - 1; j++) { auto& b0 = *j; auto& b1 = *(j + 1); if (lineSegmentIntersectsLineSegment(a0, a1, b0, b1)) return true; } } return false; }
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 }; }
void CollisionFeature::bboxifyLabel(const GeometryCoordinates &line, GeometryCoordinate &anchorPoint, const int segment, const float labelLength, const float boxSize) { const float step = boxSize / 2; const unsigned int nBoxes = std::floor(labelLength / step); // offset the center of the first box by half a box so that the edge of the // box is at the edge of the label. const float firstBoxOffset = -boxSize / 2; GeometryCoordinate &p = anchorPoint; int index = segment + 1; float anchorDistance = firstBoxOffset; // move backwards along the line to the first segment the label appears on do { index--; // there isn't enough room for the label after the beginning of the line // checkMaxAngle should have already caught this if (index < 0) return; anchorDistance -= util::dist<float>(line[index], p); p = line[index]; } while (anchorDistance > -labelLength / 2); float segmentLength = util::dist<float>(line[index], line[index + 1]); for (unsigned int i = 0; i < nBoxes; i++) { // the distance the box will be from the anchor const float boxDistanceToAnchor = -labelLength / 2 + i * step; // the box is not on the current segment. Move to the next segment. while (anchorDistance + segmentLength < boxDistanceToAnchor) { anchorDistance += segmentLength; index++; // There isn't enough room before the end of the line. if (index + 1 >= (int)line.size()) return; segmentLength = util::dist<float>(line[index], line[index + 1]); } // the distance the box will be from the beginning of the segment const float segmentBoxDistance = boxDistanceToAnchor - anchorDistance; const auto& p0 = line[index]; const auto& p1 = line[index + 1]; Point<float> boxAnchor = { p0.x + segmentBoxDistance / segmentLength * (p1.x - p0.x), p0.y + segmentBoxDistance / segmentLength * (p1.y - p0.y) }; const float distanceToInnerEdge = std::max(std::fabs(boxDistanceToAnchor - firstBoxOffset) - step / 2, 0.0f); const float maxScale = labelLength / 2 / distanceToInnerEdge; boxes.emplace_back(boxAnchor, -boxSize / 2, -boxSize / 2, boxSize / 2, boxSize / 2, maxScale); } }
SymbolQuads getIconQuads(Anchor& anchor, const PositionedIcon& shapedIcon, const GeometryCoordinates& line, const SymbolLayoutProperties& layout, const bool alongLine) { auto image = *(shapedIcon.image); const float border = 1.0; auto left = shapedIcon.left - border; auto right = left + image.pos.w / image.relativePixelRatio; auto top = shapedIcon.top - border; auto bottom = top + image.pos.h / image.relativePixelRatio; vec2<float> tl{left, top}; vec2<float> tr{right, top}; vec2<float> br{right, bottom}; vec2<float> bl{left, bottom}; float angle = layout.icon.rotate * util::DEG2RAD; if (alongLine) { assert(static_cast<unsigned int>(anchor.segment) < line.size()); const GeometryCoordinate &prev= line[anchor.segment]; if (anchor.y == prev.y && anchor.x == prev.x && static_cast<unsigned int>(anchor.segment + 1) < line.size()) { const GeometryCoordinate &next= line[anchor.segment + 1]; angle += std::atan2(anchor.y - next.y, anchor.x - next.x) + M_PI; } else { angle += std::atan2(anchor.y - prev.y, anchor.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 = tl.matMul(matrix); tr = tr.matMul(matrix); bl = bl.matMul(matrix); br = br.matMul(matrix); } SymbolQuads quads; quads.emplace_back(tl, tr, bl, br, image.pos, 0, anchor, globalMinScale, std::numeric_limits<float>::infinity()); return quads; }
static ClipperLib::Path toClipperPath(const GeometryCoordinates& ring) { ClipperLib::Path result; result.reserve(ring.size()); for (const auto& p : ring) { result.emplace_back(p.x, p.y); } return result; }
static double signedArea(const GeometryCoordinates& ring) { double sum = 0; for (std::size_t i = 0, len = ring.size(), j = len - 1; i < len; j = i++) { const GeometryCoordinate& p1 = ring[i]; const GeometryCoordinate& p2 = ring[j]; sum += (p2.x - p1.x) * (p1.y + p2.y); } return sum; }
bool polygonIntersectsBufferedMultiLine(const GeometryCoordinates& polygon, const GeometryCollection& multiLine, float radius) { for (auto& line : multiLine) { if (polygon.size() >= 3) { for (auto& p : line) { if (polygonContainsPoint(polygon, p)) return true; } } if (lineIntersectsBufferedLine(polygon, line, radius)) return true; } return false; }
/* 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; } } }
bool pointIntersectsBufferedLine(const GeometryCoordinate& p, const GeometryCoordinates& line, const float radius) { const float radiusSquared = radius * radius; if (line.size() == 1) return util::distSqr<float>(p, line.at(0)) < radiusSquared; if (line.size() == 0) return false; for (auto i = line.begin() + 1; i != line.end(); i++) { // Find line segments that have a distance <= radius^2 to p // In that case, we treat the line as "containing point p". auto& v = *(i - 1); auto& w = *i; if (distToSegmentSquared(p, v, w) < radiusSquared) return true; } return false; }
bool lineIntersectsBufferedLine(const GeometryCoordinates& lineA, const GeometryCoordinates& lineB, float radius) { if (lineA.size() > 1) { if (lineIntersectsLine(lineA, lineB)) return true; // Check whether any point in either line is within radius of the other line for (auto& p : lineB) { if (pointIntersectsBufferedLine(p, lineA, radius)) return true; } } for (auto& p : lineA) { if (pointIntersectsBufferedLine(p, lineB, radius)) return true; } return false; }
void LineBucket::addGeometry(const GeometryCoordinates& vertices) { const GLsizei len = [&vertices] { GLsizei l = static_cast<GLsizei>(vertices.size()); // If the line has duplicate vertices at the end, adjust length to remove them. while (l > 2 && vertices[l - 1] == vertices[l - 2]) { l--; } return l; }(); if (len < 2) { // fprintf(stderr, "a line must have at least two vertices\n"); return; } const float miterLimit = layout.join == JoinType::Bevel ? 1.05f : float(layout.miterLimit); const double sharpCornerOffset = SHARP_CORNER_OFFSET * (util::EXTENT / (util::tileSize * overscaling)); const GeometryCoordinate firstVertex = vertices.front(); const GeometryCoordinate lastVertex = vertices[len - 1]; const bool closed = firstVertex == lastVertex; if (len == 2 && closed) { // fprintf(stderr, "a line may not have coincident points\n"); return; } const CapType beginCap = layout.cap; const CapType endCap = closed ? CapType::Butt : CapType(layout.cap); int8_t flip = 1; double distance = 0; bool startOfLine = true; GeometryCoordinate currentVertex = GeometryCoordinate::null(), prevVertex = GeometryCoordinate::null(), nextVertex = GeometryCoordinate::null(); vec2<double> prevNormal = vec2<double>::null(), nextNormal = vec2<double>::null(); // the last three vertices added e1 = e2 = e3 = -1; if (closed) { currentVertex = vertices[len - 2]; nextNormal = util::perp(util::unit(vec2<double>(firstVertex - currentVertex))); } const GLint startVertex = vertexBuffer.index(); std::vector<TriangleElement> triangleStore; for (GLsizei i = 0; i < len; ++i) { if (closed && i == len - 1) { // if the line is closed, we treat the last vertex like the first nextVertex = vertices[1]; } else if (i + 1 < len) { // just the next vertex nextVertex = vertices[i + 1]; } else { // there is no next vertex nextVertex = GeometryCoordinate::null(); } // if two consecutive vertices exist, skip the current one if (nextVertex && vertices[i] == nextVertex) { continue; } if (nextNormal) { prevNormal = nextNormal; } if (currentVertex) { prevVertex = currentVertex; } currentVertex = vertices[i]; // Calculate the normal towards the next vertex in this line. In case // there is no next vertex, pretend that the line is continuing straight, // meaning that we are just using the previous normal. nextNormal = nextVertex ? util::perp(util::unit(vec2<double>(nextVertex - currentVertex))) : prevNormal; // If we still don't have a previous normal, this is the beginning of a // non-closed line, so we're doing a straight "join". if (!prevNormal) { prevNormal = nextNormal; } // Determine the normal of the join extrusion. It is the angle bisector // of the segments between the previous line and the next line. vec2<double> joinNormal = util::unit(prevNormal + nextNormal); /* joinNormal prevNormal * ↖ ↑ * .________. prevVertex * | * nextNormal ← | currentVertex * | * nextVertex ! * */ // Calculate the length of the miter (the ratio of the miter to the width). // Find the cosine of the angle between the next and join normals // using dot product. The inverse of that is the miter length. const float cosHalfAngle = joinNormal.x * nextNormal.x + joinNormal.y * nextNormal.y; const float miterLength = cosHalfAngle != 0 ? 1 / cosHalfAngle: 1; const bool isSharpCorner = cosHalfAngle < COS_HALF_SHARP_CORNER && prevVertex && nextVertex; if (isSharpCorner && i > 0) { const double prevSegmentLength = util::dist<double>(currentVertex, prevVertex); if (prevSegmentLength > 2.0 * sharpCornerOffset) { GeometryCoordinate newPrevVertex = currentVertex - (util::round(vec2<double>(currentVertex - prevVertex) * (sharpCornerOffset / prevSegmentLength))); distance += util::dist<double>(newPrevVertex, prevVertex); addCurrentVertex(newPrevVertex, flip, distance, prevNormal, 0, 0, false, startVertex, triangleStore); prevVertex = newPrevVertex; } } // The join if a middle vertex, otherwise the cap const bool middleVertex = prevVertex && nextVertex; JoinType currentJoin = layout.join; const CapType currentCap = nextVertex ? beginCap : endCap; if (middleVertex) { if (currentJoin == JoinType::Round) { if (miterLength < layout.roundLimit) { currentJoin = JoinType::Miter; } else if (miterLength <= 2) { currentJoin = JoinType::FakeRound; } } if (currentJoin == JoinType::Miter && miterLength > miterLimit) { currentJoin = JoinType::Bevel; } if (currentJoin == JoinType::Bevel) { // The maximum extrude length is 128 / 63 = 2 times the width of the line // so if miterLength >= 2 we need to draw a different type of bevel where. if (miterLength > 2) { currentJoin = JoinType::FlipBevel; } // If the miterLength is really small and the line bevel wouldn't be visible, // just draw a miter join to save a triangle. if (miterLength < miterLimit) { currentJoin = JoinType::Miter; } } } // Calculate how far along the line the currentVertex is if (prevVertex) distance += util::dist<double>(currentVertex, prevVertex); if (middleVertex && currentJoin == JoinType::Miter) { joinNormal = joinNormal * miterLength; addCurrentVertex(currentVertex, flip, distance, joinNormal, 0, 0, false, startVertex, triangleStore); } else if (middleVertex && currentJoin == JoinType::FlipBevel) { // miter is too big, flip the direction to make a beveled join if (miterLength > 100) { // Almost parallel lines joinNormal = nextNormal; } else { const float direction = prevNormal.x * nextNormal.y - prevNormal.y * nextNormal.x > 0 ? -1 : 1; const float bevelLength = miterLength * util::mag(prevNormal + nextNormal) / util::mag(prevNormal - nextNormal); joinNormal = util::perp(joinNormal) * bevelLength * direction; } addCurrentVertex(currentVertex, flip, distance, joinNormal, 0, 0, false, startVertex, triangleStore); addCurrentVertex(currentVertex, -flip, distance, joinNormal, 0, 0, false, startVertex, triangleStore); } else if (middleVertex && (currentJoin == JoinType::Bevel || currentJoin == JoinType::FakeRound)) { const bool lineTurnsLeft = flip * (prevNormal.x * nextNormal.y - prevNormal.y * nextNormal.x) > 0; const float offset = -std::sqrt(miterLength * miterLength - 1); float offsetA; float offsetB; if (lineTurnsLeft) { offsetB = 0; offsetA = offset; } else { offsetA = 0; offsetB = offset; } // Close previous segement with bevel if (!startOfLine) { addCurrentVertex(currentVertex, flip, distance, prevNormal, offsetA, offsetB, false, startVertex, triangleStore); } if (currentJoin == JoinType::FakeRound) { // The join angle is sharp enough that a round join would be visible. // Bevel joins fill the gap between segments with a single pie slice triangle. // Create a round join by adding multiple pie slices. The join isn't actually round, but // it looks like it is at the sizes we render lines at. // Add more triangles for sharper angles. // This math is just a good enough approximation. It isn't "correct". const int n = std::floor((0.5 - (cosHalfAngle - 0.5)) * 8); for (int m = 0; m < n; m++) { auto approxFractionalJoinNormal = util::unit(nextNormal * ((m + 1.0f) / (n + 1.0f)) + prevNormal); addPieSliceVertex(currentVertex, flip, distance, approxFractionalJoinNormal, lineTurnsLeft, startVertex, triangleStore); } addPieSliceVertex(currentVertex, flip, distance, joinNormal, lineTurnsLeft, startVertex, triangleStore); for (int k = n - 1; k >= 0; k--) { auto approxFractionalJoinNormal = util::unit(prevNormal * ((k + 1.0f) / (n + 1.0f)) + nextNormal); addPieSliceVertex(currentVertex, flip, distance, approxFractionalJoinNormal, lineTurnsLeft, startVertex, triangleStore); } } // Start next segment if (nextVertex) { addCurrentVertex(currentVertex, flip, distance, nextNormal, -offsetA, -offsetB, false, startVertex, triangleStore); } } else if (!middleVertex && currentCap == CapType::Butt) { if (!startOfLine) { // Close previous segment with a butt addCurrentVertex(currentVertex, flip, distance, prevNormal, 0, 0, false, startVertex, triangleStore); } // Start next segment with a butt if (nextVertex) { addCurrentVertex(currentVertex, flip, distance, nextNormal, 0, 0, false, startVertex, triangleStore); } } else if (!middleVertex && currentCap == CapType::Square) { if (!startOfLine) { // Close previous segment with a square cap addCurrentVertex(currentVertex, flip, distance, prevNormal, 1, 1, false, startVertex, triangleStore); // The segment is done. Unset vertices to disconnect segments. e1 = e2 = -1; flip = 1; } // Start next segment if (nextVertex) { addCurrentVertex(currentVertex, flip, distance, nextNormal, -1, -1, false, startVertex, triangleStore); } } else if (middleVertex ? currentJoin == JoinType::Round : currentCap == CapType::Round) { if (!startOfLine) { // Close previous segment with a butt addCurrentVertex(currentVertex, flip, distance, prevNormal, 0, 0, false, startVertex, triangleStore); // Add round cap or linejoin at end of segment addCurrentVertex(currentVertex, flip, distance, prevNormal, 1, 1, true, startVertex, triangleStore); // The segment is done. Unset vertices to disconnect segments. e1 = e2 = -1; flip = 1; } // Start next segment with a butt if (nextVertex) { // Add round cap before first segment addCurrentVertex(currentVertex, flip, distance, nextNormal, -1, -1, true, startVertex, triangleStore); addCurrentVertex(currentVertex, flip, distance, nextNormal, 0, 0, false, startVertex, triangleStore); } } if (isSharpCorner && i < len - 1) { const double nextSegmentLength = util::dist<double>(currentVertex, nextVertex); if (nextSegmentLength > 2 * sharpCornerOffset) { GeometryCoordinate newCurrentVertex = currentVertex + util::round(vec2<double>(nextVertex - currentVertex) * (sharpCornerOffset / nextSegmentLength)); distance += util::dist<double>(newCurrentVertex, currentVertex); addCurrentVertex(newCurrentVertex, flip, distance, nextNormal, 0, 0, false, startVertex, triangleStore); currentVertex = newCurrentVertex; } } startOfLine = false; } const GLsizei endVertex = vertexBuffer.index(); const GLsizei vertexCount = endVertex - startVertex; // Store the triangle/line groups. { if (triangleGroups.empty() || (triangleGroups.back()->vertex_length + vertexCount > 65535)) { // Move to a new group because the old one can't hold the geometry. triangleGroups.emplace_back(std::make_unique<TriangleGroup>()); } assert(triangleGroups.back()); auto& group = *triangleGroups.back(); for (const auto& triangle : triangleStore) { triangleElementsBuffer.add(group.vertex_length + triangle.a, group.vertex_length + triangle.b, group.vertex_length + triangle.c); } group.vertex_length += vertexCount; group.elements_length += triangleStore.size(); } }
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 }; }
optional<PlacedGlyph> placeGlyphAlongLine(const float offsetX, const float lineOffsetX, const float lineOffsetY, const bool flip, const Point<float>& projectedAnchorPoint, const Point<float>& tileAnchorPoint, const uint16_t anchorSegment, const GeometryCoordinates& line, const std::vector<float>& tileDistances, const mat4& labelPlaneMatrix, const bool returnTileDistance) { const float combinedOffsetX = flip ? offsetX - lineOffsetX : offsetX + lineOffsetX; int16_t dir = combinedOffsetX > 0 ? 1 : -1; float angle = 0.0; if (flip) { // The label needs to be flipped to keep text upright. // Iterate in the reverse direction. dir *= -1; angle = M_PI; } if (dir < 0) angle += M_PI; int32_t currentIndex = dir > 0 ? anchorSegment : anchorSegment + 1; const int32_t initialIndex = currentIndex; Point<float> current = projectedAnchorPoint; Point<float> prev = projectedAnchorPoint; float distanceToPrev = 0.0; float currentSegmentDistance = 0.0; const float absOffsetX = std::abs(combinedOffsetX); while (distanceToPrev + currentSegmentDistance <= absOffsetX) { currentIndex += dir; // offset does not fit on the projected line if (currentIndex < 0 || currentIndex >= static_cast<int32_t>(line.size())) { return {}; } prev = current; PointAndCameraDistance projection = project(convertPoint<float>(line.at(currentIndex)), labelPlaneMatrix); if (projection.second > 0) { current = projection.first; } else { // The vertex is behind the plane of the camera, so we can't project it // Instead, we'll create a vertex along the line that's far enough to include the glyph const Point<float> previousTilePoint = distanceToPrev == 0 ? tileAnchorPoint : convertPoint<float>(line.at(currentIndex - dir)); const Point<float> currentTilePoint = convertPoint<float>(line.at(currentIndex)); current = projectTruncatedLineSegment(previousTilePoint, currentTilePoint, prev, absOffsetX - distanceToPrev + 1, labelPlaneMatrix); } distanceToPrev += currentSegmentDistance; currentSegmentDistance = util::dist<float>(prev, current); } // The point is on the current segment. Interpolate to find it. const float segmentInterpolationT = (absOffsetX - distanceToPrev) / currentSegmentDistance; const Point<float> prevToCurrent = current - prev; Point<float> p = (prevToCurrent * segmentInterpolationT) + prev; // offset the point from the line to text-offset and icon-offset p += util::perp(prevToCurrent) * static_cast<float>(lineOffsetY * dir / util::mag(prevToCurrent)); const float segmentAngle = angle + std::atan2(current.y - prev.y, current.x - prev.x); return {{ p, segmentAngle, returnTileDistance ? TileDistance( (currentIndex - dir) == initialIndex ? 0 : tileDistances[currentIndex - dir], absOffsetX - distanceToPrev ) : optional<TileDistance>() }}; }
bool checkMaxAngle(const GeometryCoordinates &line, Anchor &anchor, const float labelLength, const float windowSize, const float maxAngle) { // horizontal labels always pass if (anchor.segment < 0) return true; GeometryCoordinate anchorPoint = convertPoint<int16_t>(anchor.point); GeometryCoordinate &p = anchorPoint; int index = anchor.segment + 1; float anchorDistance = 0; // move backwards along the line to the first segment the label appears on while (anchorDistance > -labelLength / 2) { index--; // there isn't enough room for the label after the beginning of the line if (index < 0) return false; anchorDistance -= util::dist<float>(line[index], p); p = line[index]; } anchorDistance += util::dist<float>(line[index], line[index + 1]); index++; // store recent corners and their total angle difference std::queue<Corner> recentCorners; float recentAngleDelta = 0; // move forwards by the length of the label and check angles along the way while (anchorDistance < labelLength / 2) { // there isn't enough room for the label before the end of the line if (index + 1 >= (int)line.size()) return false; auto& prev = line[index - 1]; auto& current = line[index]; auto& next = line[index + 1]; float angleDelta = util::angle_to(prev, current) - util::angle_to(current, next); // restrict angle to -pi..pi range angleDelta = std::fabs(std::fmod(angleDelta + 3 * M_PI, M_PI * 2) - M_PI); recentCorners.emplace(anchorDistance, angleDelta); recentAngleDelta += angleDelta; // remove corners that are far enough away from the list of recent anchors while (anchorDistance - recentCorners.front().distance > windowSize) { recentAngleDelta -= recentCorners.front().angleDelta; recentCorners.pop(); } // the sum of angles within the window area exceeds the maximum allowed value. check fails. if (recentAngleDelta > maxAngle) return false; index++; anchorDistance += util::dist<float>(current, next); } // no part of the line had an angle greater than the maximum allowed. check passes. return true; }
void CollisionFeature::bboxifyLabel(const GeometryCoordinates& line, GeometryCoordinate& anchorPoint, const int segment, const float labelLength, const float boxSize, const float overscaling) { const float step = boxSize / 2; const int nBoxes = std::floor(labelLength / step); // We calculate line collision circles out to 300% of what would normally be our // max size, to allow collision detection to work on labels that expand as // they move into the distance // Vertically oriented labels in the distant field can extend past this padding // This is a noticeable problem in overscaled tiles where the pitch 0-based // symbol spacing will put labels very close together in a pitched map. // To reduce the cost of adding extra collision circles, we slowly increase // them for overscaled tiles. const float overscalingPaddingFactor = 1 + .4 * ::log2(static_cast<double>(overscaling)); const int nPitchPaddingBoxes = std::floor(nBoxes * overscalingPaddingFactor / 2); // offset the center of the first box by half a box so that the edge of the // box is at the edge of the label. const float firstBoxOffset = -boxSize / 2; GeometryCoordinate &p = anchorPoint; int index = segment + 1; float anchorDistance = firstBoxOffset; const float labelStartDistance = -labelLength / 2; const float paddingStartDistance = labelStartDistance - labelLength / 8; // move backwards along the line to the first segment the label appears on do { index--; if (index < 0) { if (anchorDistance > labelStartDistance) { // there isn't enough room for the label after the beginning of the line // checkMaxAngle should have already caught this return; } else { // The line doesn't extend far enough back for all of our padding, // but we got far enough to show the label under most conditions. index = 0; break; } } anchorDistance -= util::dist<float>(line[index], p); p = line[index]; } while (anchorDistance > paddingStartDistance); auto segmentLength = util::dist<float>(line[index], line[index + 1]); for (int i = -nPitchPaddingBoxes; i < nBoxes + nPitchPaddingBoxes; i++) { // the distance the box will be from the anchor const float boxOffset = i * step; float boxDistanceToAnchor = labelStartDistance + boxOffset; // make the distance between pitch padding boxes bigger if (boxOffset < 0) boxDistanceToAnchor += boxOffset; if (boxOffset > labelLength) boxDistanceToAnchor += boxOffset - labelLength; if (boxDistanceToAnchor < anchorDistance) { // The line doesn't extend far enough back for this box, skip it // (This could allow for line collisions on distant tiles) continue; } // the box is not on the current segment. Move to the next segment. while (anchorDistance + segmentLength < boxDistanceToAnchor) { anchorDistance += segmentLength; index++; // There isn't enough room before the end of the line. if (index + 1 >= (int)line.size()) return; segmentLength = util::dist<float>(line[index], line[index + 1]); } // the distance the box will be from the beginning of the segment const float segmentBoxDistance = boxDistanceToAnchor - anchorDistance; const auto& p0 = line[index]; const auto& p1 = line[index + 1]; Point<float> boxAnchor = { p0.x + segmentBoxDistance / segmentLength * (p1.x - p0.x), p0.y + segmentBoxDistance / segmentLength * (p1.y - p0.y) }; // If the box is within boxSize of the anchor, force the box to be used // (so even 0-width labels use at least one box) // Otherwise, the .8 multiplication gives us a little bit of conservative // padding in choosing which boxes to use (see CollisionIndex#placedCollisionCircles) const float paddedAnchorDistance = std::abs(boxDistanceToAnchor - firstBoxOffset) < step ? 0 : (boxDistanceToAnchor - firstBoxOffset) * 0.8; boxes.emplace_back(boxAnchor, boxAnchor - convertPoint<float>(anchorPoint), -boxSize / 2, -boxSize / 2, boxSize / 2, boxSize / 2, paddedAnchorDistance, boxSize / 2); } }