From 12982f0d0de4768db1a4df47ac5587c75a8d7f98 Mon Sep 17 00:00:00 2001 From: tiinsy Date: Tue, 1 Apr 2025 16:41:28 +1100 Subject: [PATCH 1/8] Implement Great Circle --- lib/great_circle.dart | 3 ++ lib/src/great_circle.dart | 72 +++++++++++++++++++++++++++++++ test/components/great_circle.dart | 10 +++++ 3 files changed, 85 insertions(+) create mode 100644 lib/great_circle.dart create mode 100644 lib/src/great_circle.dart create mode 100644 test/components/great_circle.dart diff --git a/lib/great_circle.dart b/lib/great_circle.dart new file mode 100644 index 00000000..4d82af0b --- /dev/null +++ b/lib/great_circle.dart @@ -0,0 +1,3 @@ +library turf_great_circle; + +export 'src/great_circle.dart'; \ No newline at end of file diff --git a/lib/src/great_circle.dart b/lib/src/great_circle.dart new file mode 100644 index 00000000..fca544cf --- /dev/null +++ b/lib/src/great_circle.dart @@ -0,0 +1,72 @@ +import 'dart:math' as math; +import 'package:turf/turf.dart'; +import 'helpers.dart'; + +/// Calculates the great circle route between two points on a sphere +/// +/// Useful link: https://en.wikipedia.org/wiki/Great-circle_distance + +List> greatCircle( + dynamic start, + dynamic end, + { + Map properties = const {}, + int npoints = 100, + int offset = 10 + }) { + if (start.length != 2 || end.length != 2) { + /// Coordinate checking + throw ArgumentError("Both start and end coordinates should have two values - a latitude and longitude"); + } + + // If start and end points are the same, + if (start[0] == end[0] && start[1] == end[1]) { + return List.generate(npoints, (_) => [start[0], start[1]]); + } + + + List> line = []; + + num lon1 = degreesToRadians(start[0]); + num lat1 = degreesToRadians(start[1]); + num lon2 = degreesToRadians(end[0]); + num lat2 = degreesToRadians(end[1]); + + // Harvesine formula + for (int i = 0; i <= npoints; i++) { + double f = i / npoints; + double delta = 2 * + math.asin(math.sqrt(math.pow(math.sin((lat2 - lat1) / 2), 2) + + math.cos(lat1) * math.cos(lat2) * math.pow(math.sin((lon2 - lon1) / 2), 2))); + double A = math.sin((1 - f) * delta) / math.sin(delta); + double B = math.sin(f * delta) / math.sin(delta); + double x = A * math.cos(lat1) * math.cos(lon1) + B * math.cos(lat2) * math.cos(lon2); + double y = A * math.cos(lat1) * math.sin(lon1) + B * math.cos(lat2) * math.sin(lon2); + double z = A * math.sin(lat1) + B * math.sin(lat2); + + double lat = math.atan2(z, math.sqrt(x * x + y * y)); + double lon = math.atan2(y, x); + + List point = [radiansToDegrees(lon).toDouble(), radiansToDegrees(lat).toDouble()]; + line.add(point); + } + /// Check for multilinestring if path crosses anti-meridian + bool crossAntiMeridian = (start[0] - end[0]).abs() > 180; + + /// If it crossed antimeridian, we need to split our lines + if (crossAntiMeridian) { + List> multiLine = []; + List> currentLine = []; + + for (var point in line) { + if ((point[0] - line[0][0]).abs() > 180) { + multiLine.addAll(currentLine); + currentLine = []; + } + currentLine.add(point); + } + multiLine.addAll(currentLine); + return multiLine; + } + return line; +} \ No newline at end of file diff --git a/test/components/great_circle.dart b/test/components/great_circle.dart new file mode 100644 index 00000000..e656dde1 --- /dev/null +++ b/test/components/great_circle.dart @@ -0,0 +1,10 @@ +import 'package:turf/great_circle.dart'; + +void main() { + + List start = [-122, 48]; + List end = [-77, 39]; + + List> result = greatCircle(start, end, npoints: 10); + print(result); +} \ No newline at end of file From 6faa302614d93ab8921178da9c3a5eb48a37ea88 Mon Sep 17 00:00:00 2001 From: tiinsy Date: Wed, 2 Apr 2025 13:04:24 +1100 Subject: [PATCH 2/8] Added more test features - complex tests not working --- lib/src/great_circle.dart | 18 +++++++------- test/components/great_circle.dart | 10 -------- test/components/great_circle_test.dart | 33 ++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 19 deletions(-) delete mode 100644 test/components/great_circle.dart create mode 100644 test/components/great_circle_test.dart diff --git a/lib/src/great_circle.dart b/lib/src/great_circle.dart index fca544cf..42d1c734 100644 --- a/lib/src/great_circle.dart +++ b/lib/src/great_circle.dart @@ -11,12 +11,12 @@ List> greatCircle( dynamic end, { Map properties = const {}, - int npoints = 100, + int npoints = 100, // npoints = number of intermediate points less one (e.g if you want 5 intermediate points, set npoints = 6) int offset = 10 }) { if (start.length != 2 || end.length != 2) { /// Coordinate checking - throw ArgumentError("Both start and end coordinates should have two values - a latitude and longitude"); + throw ArgumentError("Both start and end coordinates should have two values - a latitude and lnggitude"); } // If start and end points are the same, @@ -27,9 +27,9 @@ List> greatCircle( List> line = []; - num lon1 = degreesToRadians(start[0]); + num lng1 = degreesToRadians(start[0]); num lat1 = degreesToRadians(start[1]); - num lon2 = degreesToRadians(end[0]); + num lng2 = degreesToRadians(end[0]); num lat2 = degreesToRadians(end[1]); // Harvesine formula @@ -37,17 +37,17 @@ List> greatCircle( double f = i / npoints; double delta = 2 * math.asin(math.sqrt(math.pow(math.sin((lat2 - lat1) / 2), 2) + - math.cos(lat1) * math.cos(lat2) * math.pow(math.sin((lon2 - lon1) / 2), 2))); + math.cos(lat1) * math.cos(lat2) * math.pow(math.sin((lng2 - lng1) / 2), 2))); double A = math.sin((1 - f) * delta) / math.sin(delta); double B = math.sin(f * delta) / math.sin(delta); - double x = A * math.cos(lat1) * math.cos(lon1) + B * math.cos(lat2) * math.cos(lon2); - double y = A * math.cos(lat1) * math.sin(lon1) + B * math.cos(lat2) * math.sin(lon2); + double x = A * math.cos(lat1) * math.cos(lng1) + B * math.cos(lat2) * math.cos(lng2); + double y = A * math.cos(lat1) * math.sin(lng1) + B * math.cos(lat2) * math.sin(lng2); double z = A * math.sin(lat1) + B * math.sin(lat2); double lat = math.atan2(z, math.sqrt(x * x + y * y)); - double lon = math.atan2(y, x); + double lng = math.atan2(y, x); - List point = [radiansToDegrees(lon).toDouble(), radiansToDegrees(lat).toDouble()]; + List point = [radiansToDegrees(lng).toDouble().roundToDouble(), radiansToDegrees(lat).toDouble().roundToDouble()]; line.add(point); } /// Check for multilinestring if path crosses anti-meridian diff --git a/test/components/great_circle.dart b/test/components/great_circle.dart deleted file mode 100644 index e656dde1..00000000 --- a/test/components/great_circle.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:turf/great_circle.dart'; - -void main() { - - List start = [-122, 48]; - List end = [-77, 39]; - - List> result = greatCircle(start, end, npoints: 10); - print(result); -} \ No newline at end of file diff --git a/test/components/great_circle_test.dart b/test/components/great_circle_test.dart new file mode 100644 index 00000000..05825359 --- /dev/null +++ b/test/components/great_circle_test.dart @@ -0,0 +1,33 @@ +import 'package:turf/great_circle.dart'; +import 'package:test/test.dart'; + +void main() { + //First test - simple coordinates + + List start = [-90, 0]; + List end = [-80,0]; + + List> resultsFirstTest1 = [[-90.0, 0.0], [-88.0,0.0], [-86.0,0.0], [-84.0, 0.0], [-82.0, 0.0],[-80.0, 0.0]]; + List> resultsFirstTest2 = [[-90.0, 0.0], [-89.0,0.0], [-88.0,0.0], [-87.0, 0.0], [-86.0, 0.0],[-85.0,0.0], [-84.0,0.0], [-83.0, 0.0], [-82.0, 0.0],[-81.0, 0.0], [-80.0, 0.0]]; + + // Second test - intermediate coordiantes (non-straight lines) + List start2 = [48, -122]; + List end2 = [39, -77]; + + List> resultsSecondTest1 = [[48.0, -122.0], [43.5, -99.5], [39.0, -77.0]]; + List> resultsSecondTest2 = [[]]; + + + + // Third test - complex coordinates (crossing anti-meridian) + + List start3 = [-21, 143]; + List end3 = [41, -140]; + + test('Great circle simple tests:', () { + expect(greatCircle(start, end, npoints: 5), resultsFirstTest1); + expect(greatCircle(start, end, npoints: 10), resultsFirstTest2); + }); + + +} \ No newline at end of file From 386254f01b4d18d83da674b21cc3ab4694263641 Mon Sep 17 00:00:00 2001 From: tiinsy Date: Wed, 2 Apr 2025 14:54:23 +1100 Subject: [PATCH 3/8] changed test features --- lib/src/great_circle.dart | 45 ++++++++++++++++++++++---- test/components/great_circle_test.dart | 18 +++++++---- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/lib/src/great_circle.dart b/lib/src/great_circle.dart index 42d1c734..8bf7b8ec 100644 --- a/lib/src/great_circle.dart +++ b/lib/src/great_circle.dart @@ -16,21 +16,52 @@ List> greatCircle( }) { if (start.length != 2 || end.length != 2) { /// Coordinate checking - throw ArgumentError("Both start and end coordinates should have two values - a latitude and lnggitude"); + throw ArgumentError("Both start and end coordinates should have two values - a latitude and longitude"); } // If start and end points are the same, if (start[0] == end[0] && start[1] == end[1]) { return List.generate(npoints, (_) => [start[0], start[1]]); } - + // Coordinate checking for valid values + if (start[0] < -90) { + throw ArgumentError("Starting latitude (vertical) coordinate is less than -90. This is not a valid coordinate."); + } + + if (start[0] > 90) { + throw ArgumentError("Starting latitude (vertical) coordinate is greater than 90. This is not a valid coordinate."); + } + + if (start[1] < -180) { + throw ArgumentError('Starting longitude (horizontal) coordinate is less than -180. This is not a valid coordinate.'); + } + + if (start[1] > 180) { + throw ArgumentError('Starting longitude (horizontal) coordinate is greater than 180. This is not a valid coordinate.'); + } + + if (end[0] < -90) { + throw ArgumentError("Ending latitude (vertical) coordinate is less than -90. This is not a valid coordinate."); + } + + if (end[0] > 90) { + throw ArgumentError("Ending latitude (vertical) coordinate is greater than 90. This is not a valid coordinate."); + } + + if (end[1] < -180) { + throw ArgumentError('Ending longitude (horizontal) coordinate is less than -180. This is not a valid coordinate.'); + } + + if (end[1] > 180) { + throw ArgumentError('Ending longitude (horizontal) coordinate is greater than 180. This is not a valid coordinate.'); + } List> line = []; - num lng1 = degreesToRadians(start[0]); - num lat1 = degreesToRadians(start[1]); - num lng2 = degreesToRadians(end[0]); - num lat2 = degreesToRadians(end[1]); + num lat1 = degreesToRadians(start[0]); + num lng1 = degreesToRadians(start[1]); + num lat2 = degreesToRadians(end[0]); + num lng2 = degreesToRadians(end[1]); // Harvesine formula for (int i = 0; i <= npoints; i++) { @@ -47,7 +78,7 @@ List> greatCircle( double lat = math.atan2(z, math.sqrt(x * x + y * y)); double lng = math.atan2(y, x); - List point = [radiansToDegrees(lng).toDouble().roundToDouble(), radiansToDegrees(lat).toDouble().roundToDouble()]; + List point = [double.parse(radiansToDegrees(lat).toStringAsFixed(2)), double.parse(radiansToDegrees(lng).toStringAsFixed(2))]; line.add(point); } /// Check for multilinestring if path crosses anti-meridian diff --git a/test/components/great_circle_test.dart b/test/components/great_circle_test.dart index 05825359..43d6b5ed 100644 --- a/test/components/great_circle_test.dart +++ b/test/components/great_circle_test.dart @@ -10,24 +10,28 @@ void main() { List> resultsFirstTest1 = [[-90.0, 0.0], [-88.0,0.0], [-86.0,0.0], [-84.0, 0.0], [-82.0, 0.0],[-80.0, 0.0]]; List> resultsFirstTest2 = [[-90.0, 0.0], [-89.0,0.0], [-88.0,0.0], [-87.0, 0.0], [-86.0, 0.0],[-85.0,0.0], [-84.0,0.0], [-83.0, 0.0], [-82.0, 0.0],[-81.0, 0.0], [-80.0, 0.0]]; + test('Great circle simple tests:', () { + expect(greatCircle(start, end, npoints: 5), resultsFirstTest1); + expect(greatCircle(start, end, npoints: 10), resultsFirstTest2); + }); + // Second test - intermediate coordiantes (non-straight lines) List start2 = [48, -122]; List end2 = [39, -77]; - List> resultsSecondTest1 = [[48.0, -122.0], [43.5, -99.5], [39.0, -77.0]]; - List> resultsSecondTest2 = [[]]; + List> resultsSecondTest1 = [[48.0, -122.0], [45.75, -97.73], [39.0, -77.0]]; + List> resultsSecondTest2 = [[48.0, -122.0], [47.52, -109.61], [45.75, -97.73], [42.84, -86.80], [39.0, -77.0]]; + test('Great circle intermediate tests:', () { + expect(greatCircle(start2, end2, npoints: 2), resultsSecondTest1); + expect(greatCircle(start2, end2, npoints: 4), resultsSecondTest2); + }); // Third test - complex coordinates (crossing anti-meridian) List start3 = [-21, 143]; List end3 = [41, -140]; - - test('Great circle simple tests:', () { - expect(greatCircle(start, end, npoints: 5), resultsFirstTest1); - expect(greatCircle(start, end, npoints: 10), resultsFirstTest2); - }); } \ No newline at end of file From 4d667ce91a4fec7c79938376159311d7361c7e86 Mon Sep 17 00:00:00 2001 From: tiinsy Date: Thu, 3 Apr 2025 14:07:55 +1100 Subject: [PATCH 4/8] updated unit testing for GreatCircle - contains simple, intermediate and complex tests --- test/components/great_circle_test.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/components/great_circle_test.dart b/test/components/great_circle_test.dart index 43d6b5ed..f263ef5a 100644 --- a/test/components/great_circle_test.dart +++ b/test/components/great_circle_test.dart @@ -20,7 +20,7 @@ void main() { List end2 = [39, -77]; List> resultsSecondTest1 = [[48.0, -122.0], [45.75, -97.73], [39.0, -77.0]]; - List> resultsSecondTest2 = [[48.0, -122.0], [47.52, -109.61], [45.75, -97.73], [42.84, -86.80], [39.0, -77.0]]; + List> resultsSecondTest2 = [[48.0, -122.0], [47.52, -109.61], [45.75, -97.73], [42.85, -86.80], [39.0, -77.0]]; test('Great circle intermediate tests:', () { @@ -33,5 +33,10 @@ void main() { List start3 = [-21, 143]; List end3 = [41, -140]; - + List> resultsThirdTest1 = [[-21.0, 143.0], [12.65, 176.68], [41, -140]]; + List> resultsThirdTest2 = [[-21.0, 143.0], [-4.36, 160.22], [12.65, 176.68], [28.52, -164.56], [41, -140]]; + test('Great circle complex tests:', () { + expect(greatCircle(start3, end3, npoints: 2), resultsThirdTest1); + expect(greatCircle(start3, end3, npoints: 4), resultsThirdTest2); + }); } \ No newline at end of file From b32e1ab2a141864e15afa9e37e5029d953b245bf Mon Sep 17 00:00:00 2001 From: tiinsy Date: Fri, 2 May 2025 14:49:05 +1000 Subject: [PATCH 5/8] temporary saved changes --- lib/src/great_circle.dart | 47 +++++++++++++------------- test/components/great_circle_test.dart | 11 +++--- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/lib/src/great_circle.dart b/lib/src/great_circle.dart index 8bf7b8ec..f0b21f27 100644 --- a/lib/src/great_circle.dart +++ b/lib/src/great_circle.dart @@ -1,14 +1,15 @@ import 'dart:math' as math; import 'package:turf/turf.dart'; import 'helpers.dart'; +import 'package:turf/meta.dart'; /// Calculates the great circle route between two points on a sphere /// /// Useful link: https://en.wikipedia.org/wiki/Great-circle_distance -List> greatCircle( - dynamic start, - dynamic end, +Feature greatCircle( + Position start, + Position end, { Map properties = const {}, int npoints = 100, // npoints = number of intermediate points less one (e.g if you want 5 intermediate points, set npoints = 6) @@ -21,47 +22,47 @@ List> greatCircle( // If start and end points are the same, if (start[0] == end[0] && start[1] == end[1]) { - return List.generate(npoints, (_) => [start[0], start[1]]); + return Feature(geometry: LineString(coordinates: [])); } // Coordinate checking for valid values - if (start[0] < -90) { + if (start[0]! < -90) { throw ArgumentError("Starting latitude (vertical) coordinate is less than -90. This is not a valid coordinate."); } - if (start[0] > 90) { + if (start[0]! > 90) { throw ArgumentError("Starting latitude (vertical) coordinate is greater than 90. This is not a valid coordinate."); } - if (start[1] < -180) { + if (start[1]! < -180) { throw ArgumentError('Starting longitude (horizontal) coordinate is less than -180. This is not a valid coordinate.'); } - if (start[1] > 180) { + if (start[1]! > 180) { throw ArgumentError('Starting longitude (horizontal) coordinate is greater than 180. This is not a valid coordinate.'); } - if (end[0] < -90) { + if (end[0]! < -90) { throw ArgumentError("Ending latitude (vertical) coordinate is less than -90. This is not a valid coordinate."); } - if (end[0] > 90) { + if (end[0]! > 90) { throw ArgumentError("Ending latitude (vertical) coordinate is greater than 90. This is not a valid coordinate."); } - if (end[1] < -180) { + if (end[1]! < -180) { throw ArgumentError('Ending longitude (horizontal) coordinate is less than -180. This is not a valid coordinate.'); } - if (end[1] > 180) { + if (end[1]! > 180) { throw ArgumentError('Ending longitude (horizontal) coordinate is greater than 180. This is not a valid coordinate.'); } - List> line = []; + List line = []; - num lat1 = degreesToRadians(start[0]); - num lng1 = degreesToRadians(start[1]); - num lat2 = degreesToRadians(end[0]); - num lng2 = degreesToRadians(end[1]); + num lng1 = degreesToRadians(start[0]); + num lat1 = degreesToRadians(start[1]); + num lng2 = degreesToRadians(end[0]); + num lat2 = degreesToRadians(end[1]); // Harvesine formula for (int i = 0; i <= npoints; i++) { @@ -78,16 +79,16 @@ List> greatCircle( double lat = math.atan2(z, math.sqrt(x * x + y * y)); double lng = math.atan2(y, x); - List point = [double.parse(radiansToDegrees(lat).toStringAsFixed(2)), double.parse(radiansToDegrees(lng).toStringAsFixed(2))]; + Position point = Position(lng, lat); line.add(point); } /// Check for multilinestring if path crosses anti-meridian - bool crossAntiMeridian = (start[0] - end[0]).abs() > 180; + bool crossAntiMeridian = (start[1]! - end[1]!).abs() > 180; /// If it crossed antimeridian, we need to split our lines if (crossAntiMeridian) { - List> multiLine = []; - List> currentLine = []; + List> multiLine = []; + List currentLine = []; for (var point in line) { if ((point[0] - line[0][0]).abs() > 180) { @@ -97,7 +98,7 @@ List> greatCircle( currentLine.add(point); } multiLine.addAll(currentLine); - return multiLine; + return Feature(geometry: MultiLineString(coordinates: multiLine)); } - return line; + return Feature(geometry: LineString(coordinates: line)); } \ No newline at end of file diff --git a/test/components/great_circle_test.dart b/test/components/great_circle_test.dart index f263ef5a..24a5cde2 100644 --- a/test/components/great_circle_test.dart +++ b/test/components/great_circle_test.dart @@ -1,13 +1,14 @@ import 'package:turf/great_circle.dart'; import 'package:test/test.dart'; +import 'package:turf/meta.dart' void main() { //First test - simple coordinates - List start = [-90, 0]; - List end = [-80,0]; + final start = Position(-90, 0); + final end = Position(-80,0); - List> resultsFirstTest1 = [[-90.0, 0.0], [-88.0,0.0], [-86.0,0.0], [-84.0, 0.0], [-82.0, 0.0],[-80.0, 0.0]]; + List> resultsFirstTest1 = [[-89.0, 0.0], [-88.0,0.0], [-86.0,0.0], [-84.0, 0.0], [-82.0, 0.0],[-80.0, 0.0]]; List> resultsFirstTest2 = [[-90.0, 0.0], [-89.0,0.0], [-88.0,0.0], [-87.0, 0.0], [-86.0, 0.0],[-85.0,0.0], [-84.0,0.0], [-83.0, 0.0], [-82.0, 0.0],[-81.0, 0.0], [-80.0, 0.0]]; test('Great circle simple tests:', () { @@ -16,8 +17,8 @@ void main() { }); // Second test - intermediate coordiantes (non-straight lines) - List start2 = [48, -122]; - List end2 = [39, -77]; + final start2 = Position(48, -122); + final end2 = Position(39, -77); List> resultsSecondTest1 = [[48.0, -122.0], [45.75, -97.73], [39.0, -77.0]]; List> resultsSecondTest2 = [[48.0, -122.0], [47.52, -109.61], [45.75, -97.73], [42.85, -86.80], [39.0, -77.0]]; From 513ad1ec011e5bd39ef15c9daec54875d8cd7dbe Mon Sep 17 00:00:00 2001 From: tiinsy Date: Tue, 6 May 2025 12:31:35 +1000 Subject: [PATCH 6/8] testing overhaul changes --- lib/src/great_circle.dart | 18 ++++----- test/components/great_circle_test.dart | 51 ++++++++++++++++++++------ 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/lib/src/great_circle.dart b/lib/src/great_circle.dart index f0b21f27..4d9af9a0 100644 --- a/lib/src/great_circle.dart +++ b/lib/src/great_circle.dart @@ -1,7 +1,5 @@ import 'dart:math' as math; import 'package:turf/turf.dart'; -import 'helpers.dart'; -import 'package:turf/meta.dart'; /// Calculates the great circle route between two points on a sphere /// @@ -59,10 +57,10 @@ Feature greatCircle( List line = []; - num lng1 = degreesToRadians(start[0]); - num lat1 = degreesToRadians(start[1]); - num lng2 = degreesToRadians(end[0]); - num lat2 = degreesToRadians(end[1]); + num lng1 = degreesToRadians(start[0]!); + num lat1 = degreesToRadians(start[1]!); + num lng2 = degreesToRadians(end[0]!); + num lat2 = degreesToRadians(end[1]!); // Harvesine formula for (int i = 0; i <= npoints; i++) { @@ -83,7 +81,7 @@ Feature greatCircle( line.add(point); } /// Check for multilinestring if path crosses anti-meridian - bool crossAntiMeridian = (start[1]! - end[1]!).abs() > 180; + bool crossAntiMeridian = (lng1 -lng2).abs() > 180; /// If it crossed antimeridian, we need to split our lines if (crossAntiMeridian) { @@ -91,13 +89,13 @@ Feature greatCircle( List currentLine = []; for (var point in line) { - if ((point[0] - line[0][0]).abs() > 180) { - multiLine.addAll(currentLine); + if ((point[0]! - line[0][0]!).abs() > 180) { + multiLine.addAll([currentLine]); currentLine = []; } currentLine.add(point); } - multiLine.addAll(currentLine); + multiLine.addAll([currentLine]); return Feature(geometry: MultiLineString(coordinates: multiLine)); } return Feature(geometry: LineString(coordinates: line)); diff --git a/test/components/great_circle_test.dart b/test/components/great_circle_test.dart index 24a5cde2..05edfbd4 100644 --- a/test/components/great_circle_test.dart +++ b/test/components/great_circle_test.dart @@ -1,6 +1,8 @@ import 'package:turf/great_circle.dart'; import 'package:test/test.dart'; -import 'package:turf/meta.dart' +import 'package:turf/helpers.dart'; +import 'package:turf/meta.dart'; +import 'dart:math'; void main() { //First test - simple coordinates @@ -8,12 +10,19 @@ void main() { final start = Position(-90, 0); final end = Position(-80,0); - List> resultsFirstTest1 = [[-89.0, 0.0], [-88.0,0.0], [-86.0,0.0], [-84.0, 0.0], [-82.0, 0.0],[-80.0, 0.0]]; + List> resultsFirstTest1 = [[-90.0, 0.0], [-88.0,0.0], [-86.0,0.0], [-84.0, 0.0], [-82.0, 0.0],[-80.0, 0.0]]; List> resultsFirstTest2 = [[-90.0, 0.0], [-89.0,0.0], [-88.0,0.0], [-87.0, 0.0], [-86.0, 0.0],[-85.0,0.0], [-84.0,0.0], [-83.0, 0.0], [-82.0, 0.0],[-81.0, 0.0], [-80.0, 0.0]]; test('Great circle simple tests:', () { - expect(greatCircle(start, end, npoints: 5), resultsFirstTest1); - expect(greatCircle(start, end, npoints: 10), resultsFirstTest2); + var resultFirst1 = greatCircle(start, end, npoints: 5); + var convertedResultFirst1 = resultFirst1.geometry?.coordinates.map((pos) => + [double.parse(radiansToDegrees(pos[0]).toStringAsFixed(1)), double.parse(radiansToDegrees(pos[1]).toStringAsFixed(1))]).toList(); + expect(convertedResultFirst1, resultsFirstTest1); + + var resultFirst2 = greatCircle(start, end, npoints: 10); + var convertedResultFirst2 = resultFirst2.geometry?.coordinates.map((pos) => + [double.parse(radiansToDegrees(pos[0]).toStringAsFixed(1)), double.parse(radiansToDegrees(pos[1]).toStringAsFixed(1))]).toList(); + expect(convertedResultFirst2, resultsFirstTest2); }); // Second test - intermediate coordiantes (non-straight lines) @@ -25,19 +34,39 @@ void main() { test('Great circle intermediate tests:', () { - expect(greatCircle(start2, end2, npoints: 2), resultsSecondTest1); - expect(greatCircle(start2, end2, npoints: 4), resultsSecondTest2); + + var resultSecond1 = greatCircle(start2, end2, npoints: 2); + var convertedResultSecond1 = resultSecond1.geometry?.coordinates.map((pos) => + [double.parse(radiansToDegrees(pos[0]).toStringAsFixed(1)), double.parse(radiansToDegrees(pos[1]).toStringAsFixed(1))]).toList(); + expect(convertedResultSecond1, resultsSecondTest1); + + var resultSecond2 = greatCircle(start2, end2, npoints: 4); + print(resultSecond2.geometry?.coordinates); + var convertedResultSecond2 = resultSecond2.geometry?.coordinates.map((pos) => + [double.parse((pos[0]).toStringAsFixed(2)), double.parse((pos[1]).toStringAsFixed(2))]).toList(); + + print(convertedResultSecond2); + expect(convertedResultSecond2, resultsSecondTest2); }); // Third test - complex coordinates (crossing anti-meridian) - List start3 = [-21, 143]; - List end3 = [41, -140]; + final start3 = Position(-21, 143); + final end3 = Position(41, -140); List> resultsThirdTest1 = [[-21.0, 143.0], [12.65, 176.68], [41, -140]]; List> resultsThirdTest2 = [[-21.0, 143.0], [-4.36, 160.22], [12.65, 176.68], [28.52, -164.56], [41, -140]]; test('Great circle complex tests:', () { - expect(greatCircle(start3, end3, npoints: 2), resultsThirdTest1); - expect(greatCircle(start3, end3, npoints: 4), resultsThirdTest2); + + var resultThird1 = greatCircle(start3, end3, npoints: 2); + var convertedResultThird1 = resultThird1.geometry?.coordinates.map((pos) => + [double.parse(radiansToDegrees(pos[0]).toStringAsFixed(1)), double.parse(radiansToDegrees(pos[1]).toStringAsFixed(1))]).toList(); + expect(convertedResultThird1, resultsThirdTest1); + + var resultThird2 = greatCircle(start3, end3, npoints: 5); + var convertedResultThird2 = resultThird2.geometry?.coordinates.map((pos) => + [double.parse(radiansToDegrees(pos[0]).toStringAsFixed(1)), double.parse(radiansToDegrees(pos[1]).toStringAsFixed(1))]).toList(); + expect(convertedResultThird2, resultsThirdTest2); }); -} \ No newline at end of file +} + From 15d9b236fd182a78e5284473bc02789a1d3e3a8c Mon Sep 17 00:00:00 2001 From: tiinsy Date: Mon, 2 Jun 2025 12:20:37 +1000 Subject: [PATCH 7/8] added comments to feature greatCircle --- lib/src/great_circle.dart | 167 ++++++++++++++++++++------------------ 1 file changed, 90 insertions(+), 77 deletions(-) diff --git a/lib/src/great_circle.dart b/lib/src/great_circle.dart index 4d9af9a0..b71c2657 100644 --- a/lib/src/great_circle.dart +++ b/lib/src/great_circle.dart @@ -1,102 +1,115 @@ import 'dart:math' as math; import 'package:turf/turf.dart'; -/// Calculates the great circle route between two points on a sphere -/// +/// Calculates the great circle route between two points on a sphere +/// /// Useful link: https://en.wikipedia.org/wiki/Great-circle_distance - -Feature greatCircle( - Position start, - Position end, - { - Map properties = const {}, - int npoints = 100, // npoints = number of intermediate points less one (e.g if you want 5 intermediate points, set npoints = 6) - int offset = 10 - }) { - if (start.length != 2 || end.length != 2) { - /// Coordinate checking - throw ArgumentError("Both start and end coordinates should have two values - a latitude and longitude"); - } - // If start and end points are the same, - if (start[0] == end[0] && start[1] == end[1]) { - return Feature(geometry: LineString(coordinates: [])); - } - // Coordinate checking for valid values - if (start[0]! < -90) { - throw ArgumentError("Starting latitude (vertical) coordinate is less than -90. This is not a valid coordinate."); - } +Feature greatCircle(Position start, Position end, + {Map properties = const {}, + int npoints = + 100, // npoints = number of intermediate points less one (e.g if you want 5 intermediate points, set npoints = 6) + int offset = 10}) { + if (start.length != 2 || end.length != 2) { + /// Coordinate checking + throw ArgumentError( + "Both start and end coordinates should have two values - a latitude and longitude"); + } - if (start[0]! > 90) { - throw ArgumentError("Starting latitude (vertical) coordinate is greater than 90. This is not a valid coordinate."); - } + // If start and end points are the same, + if (start[0] == end[0] && start[1] == end[1]) { + return Feature(geometry: LineString(coordinates: [])); + } + // Coordinate checking for valid values + if (start[0]! < -90) { + throw ArgumentError( + "Starting latitude (vertical) coordinate is less than -90. This is not a valid coordinate."); + } - if (start[1]! < -180) { - throw ArgumentError('Starting longitude (horizontal) coordinate is less than -180. This is not a valid coordinate.'); - } + if (start[0]! > 90) { + throw ArgumentError( + "Starting latitude (vertical) coordinate is greater than 90. This is not a valid coordinate."); + } - if (start[1]! > 180) { - throw ArgumentError('Starting longitude (horizontal) coordinate is greater than 180. This is not a valid coordinate.'); - } + if (start[1]! < -180) { + throw ArgumentError( + 'Starting longitude (horizontal) coordinate is less than -180. This is not a valid coordinate.'); + } - if (end[0]! < -90) { - throw ArgumentError("Ending latitude (vertical) coordinate is less than -90. This is not a valid coordinate."); - } + if (start[1]! > 180) { + throw ArgumentError( + 'Starting longitude (horizontal) coordinate is greater than 180. This is not a valid coordinate.'); + } - if (end[0]! > 90) { - throw ArgumentError("Ending latitude (vertical) coordinate is greater than 90. This is not a valid coordinate."); - } + if (end[0]! < -90) { + throw ArgumentError( + "Ending latitude (vertical) coordinate is less than -90. This is not a valid coordinate."); + } - if (end[1]! < -180) { - throw ArgumentError('Ending longitude (horizontal) coordinate is less than -180. This is not a valid coordinate.'); - } + if (end[0]! > 90) { + throw ArgumentError( + "Ending latitude (vertical) coordinate is greater than 90. This is not a valid coordinate."); + } - if (end[1]! > 180) { - throw ArgumentError('Ending longitude (horizontal) coordinate is greater than 180. This is not a valid coordinate.'); - } - - List line = []; - - num lng1 = degreesToRadians(start[0]!); - num lat1 = degreesToRadians(start[1]!); - num lng2 = degreesToRadians(end[0]!); - num lat2 = degreesToRadians(end[1]!); - - // Harvesine formula - for (int i = 0; i <= npoints; i++) { + if (end[1]! < -180) { + throw ArgumentError( + 'Ending longitude (horizontal) coordinate is less than -180. This is not a valid coordinate.'); + } + + if (end[1]! > 180) { + throw ArgumentError( + 'Ending longitude (horizontal) coordinate is greater than 180. This is not a valid coordinate.'); + } + + // Creates a list to store points for the great circle arc + List line = []; + + num lng1 = degreesToRadians(start[0]!); + num lat1 = degreesToRadians(start[1]!); + num lng2 = degreesToRadians(end[0]!); + num lat2 = degreesToRadians(end[1]!); + + // Harvesine formula + for (int i = 0; i <= npoints; i++) { double f = i / npoints; double delta = 2 * math.asin(math.sqrt(math.pow(math.sin((lat2 - lat1) / 2), 2) + - math.cos(lat1) * math.cos(lat2) * math.pow(math.sin((lng2 - lng1) / 2), 2))); + math.cos(lat1) * + math.cos(lat2) * + math.pow(math.sin((lng2 - lng1) / 2), 2))); double A = math.sin((1 - f) * delta) / math.sin(delta); double B = math.sin(f * delta) / math.sin(delta); - double x = A * math.cos(lat1) * math.cos(lng1) + B * math.cos(lat2) * math.cos(lng2); - double y = A * math.cos(lat1) * math.sin(lng1) + B * math.cos(lat2) * math.sin(lng2); + double x = A * math.cos(lat1) * math.cos(lng1) + + B * math.cos(lat2) * math.cos(lng2); + double y = A * math.cos(lat1) * math.sin(lng1) + + B * math.cos(lat2) * math.sin(lng2); double z = A * math.sin(lat1) + B * math.sin(lat2); double lat = math.atan2(z, math.sqrt(x * x + y * y)); double lng = math.atan2(y, x); - Position point = Position(lng, lat); + Position point = Position(lng, lat); line.add(point); - } - /// Check for multilinestring if path crosses anti-meridian - bool crossAntiMeridian = (lng1 -lng2).abs() > 180; - - /// If it crossed antimeridian, we need to split our lines - if (crossAntiMeridian) { - List> multiLine = []; - List currentLine = []; - - for (var point in line) { - if ((point[0]! - line[0][0]!).abs() > 180) { - multiLine.addAll([currentLine]); - currentLine = []; - } - currentLine.add(point); + } + + /// Check for multilinestring if path crosses anti-meridian + bool crossAntiMeridian = (lng1 - lng2).abs() > 180; + + /// If it crossed antimeridian, we need to split our lines + if (crossAntiMeridian) { + List> multiLine = []; + List currentLine = []; + + for (var point in line) { + if ((point[0]! - line[0][0]!).abs() > 180) { + multiLine.addAll([currentLine]); + currentLine = []; } - multiLine.addAll([currentLine]); - return Feature(geometry: MultiLineString(coordinates: multiLine)); + currentLine.add(point); } - return Feature(geometry: LineString(coordinates: line)); -} \ No newline at end of file + multiLine.addAll([currentLine]); + return Feature( + geometry: MultiLineString(coordinates: multiLine)); + } + return Feature(geometry: LineString(coordinates: line)); +} From c162904a9e7868e5e0398f8861f65aeb83b64aa0 Mon Sep 17 00:00:00 2001 From: tiinsy Date: Thu, 5 Jun 2025 12:31:53 +1000 Subject: [PATCH 8/8] fixed test erros, added test comments --- lib/src/great_circle.dart | 57 +++++++++++- test/components/great_circle_test.dart | 123 ++++++++++++++++++------- 2 files changed, 144 insertions(+), 36 deletions(-) diff --git a/lib/src/great_circle.dart b/lib/src/great_circle.dart index b71c2657..fc9ae50a 100644 --- a/lib/src/great_circle.dart +++ b/lib/src/great_circle.dart @@ -64,10 +64,10 @@ Feature greatCircle(Position start, Position end, // Creates a list to store points for the great circle arc List line = []; - num lng1 = degreesToRadians(start[0]!); - num lat1 = degreesToRadians(start[1]!); - num lng2 = degreesToRadians(end[0]!); - num lat2 = degreesToRadians(end[1]!); + num lat1 = degreesToRadians(start[0]!); + num lng1 = degreesToRadians(start[1]!); + num lat2 = degreesToRadians(end[0]!); + num lng2 = degreesToRadians(end[1]!); // Harvesine formula for (int i = 0; i <= npoints; i++) { @@ -113,3 +113,52 @@ Feature greatCircle(Position start, Position end, } return Feature(geometry: LineString(coordinates: line)); } + +Feature debugGreatCircle(Position start, Position end, + {int npoints = 2}) { + print("Input start: Position(${start[0]}, ${start[1]})"); + print("Input end: Position(${end[0]}, ${end[1]})"); + + // Current assignment (what you have) + num lng1 = degreesToRadians(start[0]!); // longitude + num lat1 = degreesToRadians(start[1]!); // latitude + num lng2 = degreesToRadians(end[0]!); // longitude + num lat2 = degreesToRadians(end[1]!); // latitude + + print("After assignment:"); + print("lng1 (from start[0]): ${radiansToDegrees(lng1)}"); + print("lat1 (from start[1]): ${radiansToDegrees(lat1)}"); + print("lng2 (from end[0]): ${radiansToDegrees(lng2)}"); + print("lat2 (from end[1]): ${radiansToDegrees(lat2)}"); + + List line = []; + + // Just add the start and end points to see what happens + for (int i = 0; i <= npoints; i++) { + double f = i / npoints; + + if (f == 0) { + // Start point + Position point = Position(lng1, lat1); + line.add(point); + print( + "Start point created: Position(${lng1}, ${lat1}) = [${radiansToDegrees(lng1)}, ${radiansToDegrees(lat1)}]"); + } else if (f == 1) { + // End point + Position point = Position(lng2, lat2); + line.add(point); + print( + "End point created: Position(${lng2}, ${lat2}) = [${radiansToDegrees(lng2)}, ${radiansToDegrees(lat2)}]"); + } else { + // For simplicity, just add a midpoint + double midLng = (lng1 + lng2) / 2; + double midLat = (lat1 + lat2) / 2; + Position point = Position(midLng, midLat); + line.add(point); + print( + "Mid point created: Position(${midLng}, ${midLat}) = [${radiansToDegrees(midLng)}, ${radiansToDegrees(midLat)}]"); + } + } + + return Feature(geometry: LineString(coordinates: line)); +} diff --git a/test/components/great_circle_test.dart b/test/components/great_circle_test.dart index 05edfbd4..9f7a03ec 100644 --- a/test/components/great_circle_test.dart +++ b/test/components/great_circle_test.dart @@ -1,51 +1,94 @@ import 'package:turf/great_circle.dart'; import 'package:test/test.dart'; import 'package:turf/helpers.dart'; -import 'package:turf/meta.dart'; -import 'dart:math'; +/* Note: This test function could be worked on further especially when dealing with precision. +Due to the general nature of greatCircle as a visual linestring, this should not be a big issue. +However, having further precision (testing changes from 1 decimal place to 4-6 can make it more useful for when npoints is large OR for shorter distances) +A +*/ void main() { //First test - simple coordinates - final start = Position(-90, 0); - final end = Position(-80,0); + final start = Position(0, -90); + final end = Position(0, -80); + + List> resultsFirstTest1 = [ + [-90.0, 0.0], + [-88.0, 0.0], + [-86.0, 0.0], + [-84.0, 0.0], + [-82.0, 0.0], + [-80.0, 0.0] + ]; + List> resultsFirstTest2 = [ + [-90.0, 0.0], + [-89.0, 0.0], + [-88.0, 0.0], + [-87.0, 0.0], + [-86.0, 0.0], + [-85.0, 0.0], + [-84.0, 0.0], + [-83.0, 0.0], + [-82.0, 0.0], + [-81.0, 0.0], + [-80.0, 0.0] + ]; - List> resultsFirstTest1 = [[-90.0, 0.0], [-88.0,0.0], [-86.0,0.0], [-84.0, 0.0], [-82.0, 0.0],[-80.0, 0.0]]; - List> resultsFirstTest2 = [[-90.0, 0.0], [-89.0,0.0], [-88.0,0.0], [-87.0, 0.0], [-86.0, 0.0],[-85.0,0.0], [-84.0,0.0], [-83.0, 0.0], [-82.0, 0.0],[-81.0, 0.0], [-80.0, 0.0]]; - test('Great circle simple tests:', () { var resultFirst1 = greatCircle(start, end, npoints: 5); - var convertedResultFirst1 = resultFirst1.geometry?.coordinates.map((pos) => - [double.parse(radiansToDegrees(pos[0]).toStringAsFixed(1)), double.parse(radiansToDegrees(pos[1]).toStringAsFixed(1))]).toList(); + var convertedResultFirst1 = resultFirst1.geometry?.coordinates + .map((pos) => [ + double.parse(radiansToDegrees(pos[0]).toStringAsFixed(1)), + double.parse(radiansToDegrees(pos[1]).toStringAsFixed(1)) + ]) + .toList(); expect(convertedResultFirst1, resultsFirstTest1); var resultFirst2 = greatCircle(start, end, npoints: 10); - var convertedResultFirst2 = resultFirst2.geometry?.coordinates.map((pos) => - [double.parse(radiansToDegrees(pos[0]).toStringAsFixed(1)), double.parse(radiansToDegrees(pos[1]).toStringAsFixed(1))]).toList(); + var convertedResultFirst2 = resultFirst2.geometry?.coordinates + .map((pos) => [ + double.parse(radiansToDegrees(pos[0]).toStringAsFixed(1)), + double.parse(radiansToDegrees(pos[1]).toStringAsFixed(1)) + ]) + .toList(); expect(convertedResultFirst2, resultsFirstTest2); }); - + // Second test - intermediate coordiantes (non-straight lines) final start2 = Position(48, -122); final end2 = Position(39, -77); - List> resultsSecondTest1 = [[48.0, -122.0], [45.75, -97.73], [39.0, -77.0]]; - List> resultsSecondTest2 = [[48.0, -122.0], [47.52, -109.61], [45.75, -97.73], [42.85, -86.80], [39.0, -77.0]]; - + List> resultsSecondTest1 = [ + [-122.0, 48.0], + [-97.7, 45.8], + [-77.0, 39.0] + ]; + List> resultsSecondTest2 = [ + [-122.0, 48.0], + [-109.6, 47.5], + [-97.7, 45.8], + [-86.8, 42.8], + [-77.0, 39.0] + ]; test('Great circle intermediate tests:', () { - var resultSecond1 = greatCircle(start2, end2, npoints: 2); - var convertedResultSecond1 = resultSecond1.geometry?.coordinates.map((pos) => - [double.parse(radiansToDegrees(pos[0]).toStringAsFixed(1)), double.parse(radiansToDegrees(pos[1]).toStringAsFixed(1))]).toList(); + var convertedResultSecond1 = resultSecond1.geometry?.coordinates + .map((pos) => [ + double.parse(radiansToDegrees(pos[0]).toStringAsFixed(1)), + double.parse(radiansToDegrees(pos[1]).toStringAsFixed(1)) + ]) + .toList(); expect(convertedResultSecond1, resultsSecondTest1); var resultSecond2 = greatCircle(start2, end2, npoints: 4); - print(resultSecond2.geometry?.coordinates); - var convertedResultSecond2 = resultSecond2.geometry?.coordinates.map((pos) => - [double.parse((pos[0]).toStringAsFixed(2)), double.parse((pos[1]).toStringAsFixed(2))]).toList(); - - print(convertedResultSecond2); + var convertedResultSecond2 = resultSecond2.geometry?.coordinates + .map((pos) => [ + double.parse(radiansToDegrees(pos[0]).toStringAsFixed(1)), + double.parse(radiansToDegrees(pos[1]).toStringAsFixed(1)) + ]) + .toList(); expect(convertedResultSecond2, resultsSecondTest2); }); @@ -54,19 +97,35 @@ void main() { final start3 = Position(-21, 143); final end3 = Position(41, -140); - List> resultsThirdTest1 = [[-21.0, 143.0], [12.65, 176.68], [41, -140]]; - List> resultsThirdTest2 = [[-21.0, 143.0], [-4.36, 160.22], [12.65, 176.68], [28.52, -164.56], [41, -140]]; + List> resultsThirdTest1 = [ + [143.0, -21.0], + [176.7, 12.7], + [-140, 41] + ]; + List> resultsThirdTest2 = [ + [143.0, -21.0], + [160.2, -4.4], + [176.7, 12.7], + [-164.6, 28.5], + [-140, 41] + ]; test('Great circle complex tests:', () { - var resultThird1 = greatCircle(start3, end3, npoints: 2); - var convertedResultThird1 = resultThird1.geometry?.coordinates.map((pos) => - [double.parse(radiansToDegrees(pos[0]).toStringAsFixed(1)), double.parse(radiansToDegrees(pos[1]).toStringAsFixed(1))]).toList(); + var convertedResultThird1 = resultThird1.geometry?.coordinates + .map((pos) => [ + double.parse(radiansToDegrees(pos[0]).toStringAsFixed(1)), + double.parse(radiansToDegrees(pos[1]).toStringAsFixed(1)) + ]) + .toList(); expect(convertedResultThird1, resultsThirdTest1); - var resultThird2 = greatCircle(start3, end3, npoints: 5); - var convertedResultThird2 = resultThird2.geometry?.coordinates.map((pos) => - [double.parse(radiansToDegrees(pos[0]).toStringAsFixed(1)), double.parse(radiansToDegrees(pos[1]).toStringAsFixed(1))]).toList(); + var resultThird2 = greatCircle(start3, end3, npoints: 4); + var convertedResultThird2 = resultThird2.geometry?.coordinates + .map((pos) => [ + double.parse(radiansToDegrees(pos[0]).toStringAsFixed(1)), + double.parse(radiansToDegrees(pos[1]).toStringAsFixed(1)) + ]) + .toList(); expect(convertedResultThird2, resultsThirdTest2); }); } -