Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions pkg/web_app/lib/src/widget/downloads_chart/computations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,51 @@ bool isPointOnPathWithTolerance(
}
return false;
}

/// Determines if a point is inside a polygon.
///
/// Uses the ray casting algorithm to determine if a given point lies inside
/// a polygon defined by a list of vertices. The polygon is assumed to be
/// closed and non-self-intersecting.
///
/// Returns `true` if the point is inside the polygon or exactly on a vertex or
/// an edge, and `false` otherwise.
bool isPointInPolygon(List<(double, double)> polygon, (double, double) point) {
if (polygon.length < 3) {
return false;
}

int intersections = 0;
final (px, py) = point;

// Check if the point is on an edge
if (isPointOnPathWithTolerance(polygon, point, 0.001)) {
return true;
}

for (int i = 0; i < polygon.length; i++) {
final (x1, y1) = polygon[i];
final (x2, y2) = polygon[(i + 1) % polygon.length];

// Check if the point is on a vertex
if ((px == x1 && py == y1) || (px == x2 && py == y2)) {
return true;
}

if (py > min(y1, y2) && py <= max(y1, y2)) {
double intersectX;
if (y1 == y2) {
// horizontal edge
continue;
} else {
intersectX = x1 + (py - y1) * (x2 - x1) / (y2 - y1);
}

if (px < intersectX) {
intersections++;
}
}
}

return intersections % 2 == 1;
}
42 changes: 42 additions & 0 deletions pkg/web_app/test/widget/downloads_chart/downloads_chart_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,46 @@ void main() {
expect(isPointOnPathWithTolerance(chart, point, 0.001), isTrue);
});
});

group('isPointInPolygon', () {
test('Basic inside/outside, vertex, edge, and invalid input', () {
final square = [(0.0, 0.0), (0.0, 2.0), (2.0, 2.0), (2.0, 0.0)];
final invalidPolygon = [(0.0, 0.0)];

final insidePoint = (1.0, 1.0);
final outsidePoint = (3.0, 1.0);
final vertexPoint = (0.0, 0.0);
final edgePoint = (1.0, 0.0);

expect(isPointInPolygon(square, insidePoint), isTrue);
expect(isPointInPolygon(square, outsidePoint), isFalse);
expect(isPointInPolygon(square, vertexPoint), isTrue);
expect(isPointInPolygon(square, edgePoint), isFalse);
expect(isPointInPolygon(invalidPolygon, insidePoint), isFalse);
});

test('Complex polygon', () {
final complexPolygon = [
(0.0, 0.0),
(0.0, 4.0),
(2.0, 2.0),
(4.0, 4.0),
(4.0, 0.0)
];
final insidePoint = (1.0, 1.0);
final outsidePoint = (2.0, 3.0);
final vertexPoint = (1.0, 1.0);
final edgePoint = (1.0, 3.0);
final edgePoint2 = (3.0, 3.0);
final outsidePointOnEdgeExtension = (3.0, 4.0);

expect(isPointInPolygon(complexPolygon, insidePoint), isTrue);
expect(isPointInPolygon(complexPolygon, outsidePoint), isFalse);
expect(isPointInPolygon(complexPolygon, vertexPoint), isTrue);
expect(isPointInPolygon(complexPolygon, edgePoint), isTrue);
expect(isPointInPolygon(complexPolygon, edgePoint2), isTrue);
expect(isPointInPolygon(complexPolygon, outsidePointOnEdgeExtension),
isFalse);
});
});
}