diff --git a/pkg/web_app/lib/src/widget/downloads_chart/computations.dart b/pkg/web_app/lib/src/widget/downloads_chart/computations.dart index 2dfe7ae95b..60b3450308 100644 --- a/pkg/web_app/lib/src/widget/downloads_chart/computations.dart +++ b/pkg/web_app/lib/src/widget/downloads_chart/computations.dart @@ -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; +} diff --git a/pkg/web_app/test/widget/downloads_chart/downloads_chart_test.dart b/pkg/web_app/test/widget/downloads_chart/downloads_chart_test.dart index a9ac960280..0240f0d3d9 100644 --- a/pkg/web_app/test/widget/downloads_chart/downloads_chart_test.dart +++ b/pkg/web_app/test/widget/downloads_chart/downloads_chart_test.dart @@ -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); + }); + }); }