Skip to content

Commit ce846fd

Browse files
authored
Downloads chart: Add helper function for determining point in polygon (#8602)
1 parent a2832ce commit ce846fd

File tree

2 files changed

+90
-0
lines changed

2 files changed

+90
-0
lines changed

pkg/web_app/lib/src/widget/downloads_chart/computations.dart

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,51 @@ bool isPointOnPathWithTolerance(
125125
}
126126
return false;
127127
}
128+
129+
/// Determines if a point is inside a polygon.
130+
///
131+
/// Uses the ray casting algorithm to determine if a given point lies inside
132+
/// a polygon defined by a list of vertices. The polygon is assumed to be
133+
/// closed and non-self-intersecting.
134+
///
135+
/// Returns `true` if the point is inside the polygon or exactly on a vertex or
136+
/// an edge, and `false` otherwise.
137+
bool isPointInPolygon(List<(double, double)> polygon, (double, double) point) {
138+
if (polygon.length < 3) {
139+
return false;
140+
}
141+
142+
int intersections = 0;
143+
final (px, py) = point;
144+
145+
// Check if the point is on an edge
146+
if (isPointOnPathWithTolerance(polygon, point, 0.001)) {
147+
return true;
148+
}
149+
150+
for (int i = 0; i < polygon.length; i++) {
151+
final (x1, y1) = polygon[i];
152+
final (x2, y2) = polygon[(i + 1) % polygon.length];
153+
154+
// Check if the point is on a vertex
155+
if ((px == x1 && py == y1) || (px == x2 && py == y2)) {
156+
return true;
157+
}
158+
159+
if (py > min(y1, y2) && py <= max(y1, y2)) {
160+
double intersectX;
161+
if (y1 == y2) {
162+
// horizontal edge
163+
continue;
164+
} else {
165+
intersectX = x1 + (py - y1) * (x2 - x1) / (y2 - y1);
166+
}
167+
168+
if (px < intersectX) {
169+
intersections++;
170+
}
171+
}
172+
}
173+
174+
return intersections % 2 == 1;
175+
}

pkg/web_app/test/widget/downloads_chart/downloads_chart_test.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,46 @@ void main() {
182182
expect(isPointOnPathWithTolerance(chart, point, 0.001), isTrue);
183183
});
184184
});
185+
186+
group('isPointInPolygon', () {
187+
test('Basic inside/outside, vertex, edge, and invalid input', () {
188+
final square = [(0.0, 0.0), (0.0, 2.0), (2.0, 2.0), (2.0, 0.0)];
189+
final invalidPolygon = [(0.0, 0.0)];
190+
191+
final insidePoint = (1.0, 1.0);
192+
final outsidePoint = (3.0, 1.0);
193+
final vertexPoint = (0.0, 0.0);
194+
final edgePoint = (1.0, 0.0);
195+
196+
expect(isPointInPolygon(square, insidePoint), isTrue);
197+
expect(isPointInPolygon(square, outsidePoint), isFalse);
198+
expect(isPointInPolygon(square, vertexPoint), isTrue);
199+
expect(isPointInPolygon(square, edgePoint), isFalse);
200+
expect(isPointInPolygon(invalidPolygon, insidePoint), isFalse);
201+
});
202+
203+
test('Complex polygon', () {
204+
final complexPolygon = [
205+
(0.0, 0.0),
206+
(0.0, 4.0),
207+
(2.0, 2.0),
208+
(4.0, 4.0),
209+
(4.0, 0.0)
210+
];
211+
final insidePoint = (1.0, 1.0);
212+
final outsidePoint = (2.0, 3.0);
213+
final vertexPoint = (1.0, 1.0);
214+
final edgePoint = (1.0, 3.0);
215+
final edgePoint2 = (3.0, 3.0);
216+
final outsidePointOnEdgeExtension = (3.0, 4.0);
217+
218+
expect(isPointInPolygon(complexPolygon, insidePoint), isTrue);
219+
expect(isPointInPolygon(complexPolygon, outsidePoint), isFalse);
220+
expect(isPointInPolygon(complexPolygon, vertexPoint), isTrue);
221+
expect(isPointInPolygon(complexPolygon, edgePoint), isTrue);
222+
expect(isPointInPolygon(complexPolygon, edgePoint2), isTrue);
223+
expect(isPointInPolygon(complexPolygon, outsidePointOnEdgeExtension),
224+
isFalse);
225+
});
226+
});
185227
}

0 commit comments

Comments
 (0)