Skip to content

Commit

Permalink
fix: prevent crash when zooming far into Polygons (#1854)
Browse files Browse the repository at this point in the history
Co-authored-by: Luka S <github@jaffaketchup.dev>
  • Loading branch information
ReinisSprogis and JaffaKetchup authored Mar 29, 2024
1 parent 8d106d2 commit 149f847
Showing 1 changed file with 149 additions and 30 deletions.
179 changes: 149 additions & 30 deletions lib/src/layer/polygon_layer/painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ class _PolygonPainter<R extends Object> extends CustomPainter {

final _hits = <R>[]; // Avoids repetitive memory reallocation

// OutCodes for the Cohen-Sutherland algorithm
static const _csInside = 0; // 0000
static const _csLeft = 1; // 0001
static const _csRight = 2; // 0010
static const _csBottom = 4; // 0100
static const _csTop = 8; // 1000

/// Create a new [_PolygonPainter] instance.
_PolygonPainter({
required this.polygons,
Expand Down Expand Up @@ -212,6 +219,9 @@ class _PolygonPainter<R extends Object> extends CustomPainter {
origin: origin,
points: projectedPolygon.points,
),
size,
_getBorderPaint(polygon),
canvas,
);
}

Expand All @@ -234,7 +244,8 @@ class _PolygonPainter<R extends Object> extends CustomPainter {
}

if (!polygon.disableHolesBorder && polygon.borderStrokeWidth > 0.0) {
_addHoleBordersToPath(borderPath, polygon, holeOffsetsList);
_addHoleBordersToPath(borderPath, polygon, holeOffsetsList, size,
canvas, _getBorderPaint(polygon));
}
}

Expand All @@ -246,7 +257,7 @@ class _PolygonPainter<R extends Object> extends CustomPainter {
// ensure polygons and labels are stacked correctly, i.e.:
// p1, p1_label, p2, p2_label, ... .

// The painter will be null if the layouting algorithm determined that
// The painter will be null if the layOuting algorithm determined that
// there isn't enough space.
final painter = _buildLabelTextPainter(
mapSize: camera.size,
Expand Down Expand Up @@ -307,11 +318,15 @@ class _PolygonPainter<R extends Object> extends CustomPainter {
Path path,
Polygon polygon,
List<Offset> offsets,
Size canvasSize,
Paint paint,
Canvas canvas,
) {
if (polygon.isDotted) {
final borderRadius = polygon.borderStrokeWidth / 2;
final spacing = polygon.borderStrokeWidth * 1.5;
_addDottedLineToPath(path, offsets, borderRadius, spacing);
_addDottedLineToPath(
canvas, paint, offsets, borderRadius, spacing, canvasSize);
} else {
_addLineToPath(path, offsets);
}
Expand All @@ -321,12 +336,16 @@ class _PolygonPainter<R extends Object> extends CustomPainter {
Path path,
Polygon polygon,
List<List<Offset>> holeOffsetsList,
Size canvasSize,
Canvas canvas,
Paint paint,
) {
if (polygon.isDotted) {
final borderRadius = polygon.borderStrokeWidth / 2;
final spacing = polygon.borderStrokeWidth * 1.5;
for (final offsets in holeOffsetsList) {
_addDottedLineToPath(path, offsets, borderRadius, spacing);
_addDottedLineToPath(
canvas, paint, offsets, borderRadius, spacing, canvasSize);
}
} else {
for (final offsets in holeOffsetsList) {
Expand All @@ -335,52 +354,152 @@ class _PolygonPainter<R extends Object> extends CustomPainter {
}
}

// Function to clip a line segment to a rectangular area (canvas)
List<Offset>? _getVisibleSegment(Offset p0, Offset p1, Size canvasSize) {
// Function to compute the outCode for a point relative to the canvas
int computeOutCode(
double x,
double y,
double xMin,
double yMin,
double xMax,
double yMax,
) {
int code = _csInside;

if (x < xMin) {
code |= _csLeft;
} else if (x > xMax) {
code |= _csRight;
}
if (y < yMin) {
code |= _csBottom;
} else if (y > yMax) {
code |= _csTop;
}

return code;
}

const double xMin = 0;
const double yMin = 0;
final double xMax = canvasSize.width;
final double yMax = canvasSize.height;

double x0 = p0.dx;
double y0 = p0.dy;
double x1 = p1.dx;
double y1 = p1.dy;

int outCode0 = computeOutCode(x0, y0, xMin, yMin, xMax, yMax);
int outCode1 = computeOutCode(x1, y1, xMin, yMin, xMax, yMax);
bool accept = false;

while (true) {
if ((outCode0 | outCode1) == 0) {
// Both points inside; trivially accept
accept = true;
break;
} else if ((outCode0 & outCode1) != 0) {
// Both points share an outside zone; trivially reject
break;
} else {
// Could be partially inside; calculate intersection
double x;
double y;
final int outCodeOut = outCode0 != 0 ? outCode0 : outCode1;

if ((outCodeOut & _csTop) != 0) {
x = x0 + (x1 - x0) * (yMax - y0) / (y1 - y0);
y = yMax;
} else if ((outCodeOut & _csBottom) != 0) {
x = x0 + (x1 - x0) * (yMin - y0) / (y1 - y0);
y = yMin;
} else if ((outCodeOut & _csRight) != 0) {
y = y0 + (y1 - y0) * (xMax - x0) / (x1 - x0);
x = xMax;
} else if ((outCodeOut & _csLeft) != 0) {
y = y0 + (y1 - y0) * (xMin - x0) / (x1 - x0);
x = xMin;
} else {
// This else block should never be reached.
break;
}

// Update the point and outCode
if (outCodeOut == outCode0) {
x0 = x;
y0 = y;
outCode0 = computeOutCode(x0, y0, xMin, yMin, xMax, yMax);
} else {
x1 = x;
y1 = y;
outCode1 = computeOutCode(x1, y1, xMin, yMin, xMax, yMax);
}
}
}

if (accept) {
// Make sure we return the points within the canvas
return [Offset(x0, y0), Offset(x1, y1)];
}
return null;
}

void _addDottedLineToPath(
Path path,
Canvas canvas,
Paint paint,
List<Offset> offsets,
double radius,
double stepLength,
Size canvasSize,
) {
if (offsets.isEmpty) {
return;
}

double startDistance = 0;
for (int i = 0; i < offsets.length; i++) {
final o0 = offsets[i % offsets.length];
final o1 = offsets[(i + 1) % offsets.length];
final totalDistance = (o0 - o1).distance;

double distance = startDistance;
while (distance < totalDistance) {
final done = distance / totalDistance;
final remain = 1.0 - done;
final offset = Offset(
o0.dx * remain + o1.dx * done,
o0.dy * remain + o1.dy * done,
);
path.addOval(Rect.fromCircle(center: offset, radius: radius));

distance += stepLength;
// Calculate for all segments, including closing the loop from the last to the first point
final int totalOffsets = offsets.length;
for (int i = 0; i < totalOffsets; i++) {
final Offset start = offsets[i % totalOffsets];
final Offset end =
offsets[(i + 1) % totalOffsets]; // Wrap around to the first point

// Attempt to adjust the segment to the visible part of the canvas
final List<Offset>? visibleSegment =
_getVisibleSegment(start, end, canvasSize);
if (visibleSegment == null) {
continue; // Skip if the segment is completely outside
}

startDistance = distance < totalDistance
? stepLength - (totalDistance - distance)
: distance - totalDistance;
final Offset adjustedStart = visibleSegment[0];
final Offset adjustedEnd = visibleSegment[1];
final double lineLength = (adjustedStart - adjustedEnd).distance;
final Offset stepVector =
(adjustedEnd - adjustedStart) / lineLength * stepLength;
double traveledDistance = 0;

Offset currentPoint = adjustedStart;
while (traveledDistance < lineLength) {
// Draw the circle if within the canvas bounds (additional check now redundant)
canvas.drawCircle(currentPoint, radius, paint);

// Move to the next point
currentPoint = currentPoint + stepVector;
traveledDistance += stepLength;
}
}

path.addOval(Rect.fromCircle(center: offsets.last, radius: radius));
}

void _addLineToPath(Path path, List<Offset> offsets) {
path.addPolygon(offsets, true);
}

({Offset min, Offset max}) _getBounds(Offset origin, Polygon polygon) {
final bbox = polygon.boundingBox;
final bBox = polygon.boundingBox;
return (
min: getOffset(camera, origin, bbox.southWest),
max: getOffset(camera, origin, bbox.northEast),
min: getOffset(camera, origin, bBox.southWest),
max: getOffset(camera, origin, bBox.northEast),
);
}

Expand Down

0 comments on commit 149f847

Please sign in to comment.