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 025dedf726..d73426e9f6 100644 --- a/pkg/web_app/lib/src/widget/downloads_chart/computations.dart +++ b/pkg/web_app/lib/src/widget/downloads_chart/computations.dart @@ -41,3 +41,41 @@ Iterable prepareRanges(List rangeDownloads) { return (ranges: ranges, weekLists: result.reversed.toList()); } + +/// Calculates the closest point on the line segment between [startPoint] +/// and [endPoint] to a given [point]. +/// +/// If [startPoint] and [endPoint] are the same, [startPoint] is returned. +/// +/// If [point] is outside the line segment, that is the closest point would not +/// be within the thwo endpoints, `(double.maxFinite, double.maxFinite)` +/// is returned. +(num, num) closestPointOnLine( + (num, num) startPoint, (num, num) endPoint, (num, num) point) { + final directionVector = + (endPoint.$1 - startPoint.$1, endPoint.$2 - startPoint.$2); + + if (directionVector.$1 == 0 && directionVector.$2 == 0) { + return startPoint; + } + + final v = (point.$1 - startPoint.$1, point.$2 - startPoint.$2); + + // The dot product ((v · d) / (d · d)) + final t = ((v.$1 * directionVector.$1 + v.$2 * directionVector.$2) / + (directionVector.$1 * directionVector.$1 + + directionVector.$2 * directionVector.$2)); + + if (t < 0 || t > 1) { + // Closest point is before or after the line. + return (double.maxFinite, double.maxFinite); + } + + // t * d + final projectionVOntoD = (t * directionVector.$1, t * directionVector.$2); + final closestPoint = ( + startPoint.$1 + projectionVOntoD.$1, + startPoint.$2 + projectionVOntoD.$2 + ); + return (closestPoint.$1, closestPoint.$2); +} 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 6ffd9a6466..549215d578 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 @@ -78,4 +78,78 @@ void main() { expect(w3[i], [0, 0, 0, 0, 0, 0]); } }); + + group('closestPointOnLine tests', () { + test('point on the line', () { + final lineStart = (0, 0); + final lineEnd = (10, 10); + final point = (5, 5); + final closest = closestPointOnLine(lineStart, lineEnd, point); + expect(closest, (5.0, 5.0)); + }); + + test('point before the line', () { + final lineStart = (0, 0); + final lineEnd = (10, 10); + final point = (-2, -5); + final closest = closestPointOnLine(lineStart, lineEnd, point); + expect(closest, (double.maxFinite, double.maxFinite)); + }); + + test('point after the line', () { + final lineStart = (0, 0); + final lineEnd = (10, 10); + final point = (15, 15); + final closest = closestPointOnLine(lineStart, lineEnd, point); + expect(closest, (double.maxFinite, double.maxFinite)); + }); + + test('point off the line', () { + final lineStart = (0, 0); + final lineEnd = (10, 10); + final point = (5, 3); + final closest = closestPointOnLine(lineStart, lineEnd, point); + expect(closest, (4.0, 4.0)); + }); + + test('vertical line', () { + final lineStart = (1, 2); + final lineEnd = (1, 10); + final point = (5, 5); + final closest = closestPointOnLine(lineStart, lineEnd, point); + expect(closest, (1.0, 5.0)); + }); + + test('horizontal line', () { + final lineStart = (2, 1); + final lineEnd = (10, 1); + final point = (5, 5); + final closest = closestPointOnLine(lineStart, lineEnd, point); + expect(closest, (5.0, 1.0)); + }); + + test('same start and end points', () { + final lineStart = (5, 5); + final lineEnd = (5, 5); + final point = (10, 10); + final closest = closestPointOnLine(lineStart, lineEnd, point); + expect(closest, (5.0, 5.0)); + }); + + test('line with negative coordinates', () { + final lineStart = (-5, -5); + final lineEnd = (5, 5); + final point = (0, 10); + final closest = closestPointOnLine(lineStart, lineEnd, point); + expect(closest, (5.0, 5.0)); + }); + + test('line with negative and positive coordinates', () { + final lineStart = (-5, 5); + final lineEnd = (5, -5); + final point = (0, 0); + final closest = closestPointOnLine(lineStart, lineEnd, point); + expect(closest, (0.0, 0.0)); + }); + }); }