Skip to content

Commit 68013d9

Browse files
committed
Downloads chart: Add helper function for determining point in polygon
1 parent 818f473 commit 68013d9

File tree

2 files changed

+81
-0
lines changed

2 files changed

+81
-0
lines changed

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,46 @@ 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 and
136+
/// `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+
for (int i = 0; i < polygon.length; i++) {
146+
final (x1, y1) = polygon[i];
147+
final (x2, y2) = polygon[(i + 1) % polygon.length];
148+
149+
// Check if the point is on a vertex
150+
if ((px == x1 && py == y1) || (px == x2 && py == y2)) {
151+
return true;
152+
}
153+
154+
if (py > min(y1, y2) && py <= max(y1, y2)) {
155+
double intersectX;
156+
// vertical edge
157+
if (x1 == x2) {
158+
intersectX = x1;
159+
} else {
160+
intersectX = x1 + (py - y1) * (x2 - x1) / (y2 - y1);
161+
}
162+
163+
if (px < intersectX) {
164+
intersections++;
165+
}
166+
}
167+
}
168+
169+
return intersections % 2 == 1;
170+
}

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,42 @@ 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 = (3.0, 3.0);
213+
final vertexPoint = (1.0, 1.0);
214+
final outsidePointOnEdgeExtension = (3.0, 4.0);
215+
216+
expect(isPointInPolygon(complexPolygon, insidePoint), isTrue);
217+
expect(isPointInPolygon(complexPolygon, outsidePoint), isFalse);
218+
expect(isPointInPolygon(complexPolygon, vertexPoint), isTrue);
219+
expect(isPointInPolygon(complexPolygon, outsidePointOnEdgeExtension),
220+
isFalse);
221+
});
222+
});
185223
}

0 commit comments

Comments
 (0)