diff --git a/Progress.md b/Progress.md index b30b7a5c..59482db0 100644 --- a/Progress.md +++ b/Progress.md @@ -176,5 +176,5 @@ Dart. This is an on going project and functions are being added once needed. If - [x] [lengthToDegrees](https://github.com/dartclub/turf_dart/blob/main/lib/src/helpers.dart) - [x] [radiansToLength](https://github.com/dartclub/turf_dart/blob/main/lib/src/helpers.dart) - [x] [radiansToDegrees](https://github.com/dartclub/turf_dart/blob/main/lib/src/helpers.dart) -- [ ] toMercator -- [ ] toWgs84 \ No newline at end of file +- [x] [toMercator](https://github.com/dartclub/turf_dart/blob/main/lib/src/helpers.dart) +- [x] [toWGS84](https://github.com/dartclub/turf_dart/blob/main/lib/src/helpers.dart) diff --git a/lib/src/helpers.dart b/lib/src/helpers.dart index e49eca8f..8b6150e5 100644 --- a/lib/src/helpers.dart +++ b/lib/src/helpers.dart @@ -1,4 +1,5 @@ import 'dart:math'; +import 'package:geotypes/geotypes.dart'; enum Unit { meters, @@ -46,6 +47,27 @@ enum DistanceGeometry { /// Earth Radius used with the Harvesine formula and approximates using a spherical (non-ellipsoid) Earth. const earthRadius = 6371008.8; +/// Maximum extent of the Web Mercator projection in meters +const double mercatorLimit = 20037508.34; + +/// Earth radius in meters used for coordinate system conversions +const double conversionEarthRadius = 6378137.0; + +/// Coordinate reference systems for spatial data +enum CoordinateSystem { + /// WGS84 geographic coordinates (longitude/latitude) + wgs84, + + /// Web Mercator projection (EPSG:3857) + mercator, +} + +/// Coordinate system conversion constants +const coordSystemConstants = { + 'mercatorLimit': mercatorLimit, + 'earthRadius': conversionEarthRadius, +}; + /// Unit of measurement factors using a spherical (non-ellipsoid) earth radius. /// Keys are the name of the unit, values are the number of that unit in a single radian const factors = { @@ -100,9 +122,10 @@ num round(num value, [num precision = 0]) { } /// Convert a distance measurement (assuming a spherical Earth) from radians to a more friendly unit. -/// Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet +/// Valid units: [Unit.miles], [Unit.nauticalmiles], [Unit.inches], [Unit.yards], [Unit.meters], +/// [Unit.kilometers], [Unit.centimeters], [Unit.feet] num radiansToLength(num radians, [Unit unit = Unit.kilometers]) { - var factor = factors[unit]; + final factor = factors[unit]; if (factor == null) { throw Exception("$unit units is invalid"); } @@ -110,7 +133,8 @@ num radiansToLength(num radians, [Unit unit = Unit.kilometers]) { } /// Convert a distance measurement (assuming a spherical Earth) from a real-world unit into radians -/// Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet +/// Valid units: [Unit.miles], [Unit.nauticalmiles], [Unit.inches], [Unit.yards], [Unit.meters], +/// [Unit.kilometers], [Unit.centimeters], [Unit.feet] num lengthToRadians(num distance, [Unit unit = Unit.kilometers]) { num? factor = factors[unit]; if (factor == null) { @@ -120,7 +144,8 @@ num lengthToRadians(num distance, [Unit unit = Unit.kilometers]) { } /// Convert a distance measurement (assuming a spherical Earth) from a real-world unit into degrees -/// Valid units: miles, nauticalmiles, inches, yards, meters, metres, centimeters, kilometres, feet +/// Valid units: [Unit.miles], [Unit.nauticalmiles], [Unit.inches], [Unit.yards], [Unit.meters], +/// [Unit.centimeters], [Unit.kilometers], [Unit.feet] num lengthToDegrees(num distance, [Unit unit = Unit.kilometers]) { return radiansToDegrees(lengthToRadians(distance, unit)); } @@ -148,7 +173,8 @@ num degreesToRadians(num degrees) { } /// Converts a length to the requested unit. -/// Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet +/// Valid units: [Unit.miles], [Unit.nauticalmiles], [Unit.inches], [Unit.yards], [Unit.meters], +/// [Unit.kilometers], [Unit.centimeters], [Unit.feet] num convertLength( num length, [ Unit originalUnit = Unit.kilometers, @@ -161,7 +187,8 @@ num convertLength( } /// Converts a area to the requested unit. -/// Valid units: kilometers, kilometres, meters, metres, centimetres, millimeters, acres, miles, yards, feet, inches, hectares +/// Valid units: [Unit.kilometers], [Unit.meters], [Unit.centimeters], [Unit.millimeters], [Unit.acres], +/// [Unit.miles], [Unit.yards], [Unit.feet], [Unit.inches] num convertArea(num area, [originalUnit = Unit.meters, finalUnit = Unit.kilometers]) { if (area < 0) { @@ -180,3 +207,81 @@ num convertArea(num area, return (area / startFactor) * finalFactor; } + + +/// Converts coordinates from one system to another. +/// +/// Valid systems: [CoordinateSystem.wgs84], [CoordinateSystem.mercator] +/// Returns: [Position] in the target system +Position convertCoordinates( + Position coord, + CoordinateSystem fromSystem, + CoordinateSystem toSystem +) { + if (fromSystem == toSystem) { + return coord; + } + + if (fromSystem == CoordinateSystem.wgs84 && toSystem == CoordinateSystem.mercator) { + return toMercator(coord); + } else if (fromSystem == CoordinateSystem.mercator && toSystem == CoordinateSystem.wgs84) { + return toWGS84(coord); + } else { + throw ArgumentError("Unsupported coordinate system conversion from ${fromSystem.runtimeType} to ${toSystem.runtimeType}"); + } +} + +/// Converts a WGS84 coordinate to Web Mercator. +/// +/// Valid inputs: [Position] with [longitude, latitude] +/// Returns: [Position] with [x, y] coordinates in meters +Position toMercator(Position coord) { + // Use the earth radius constant for consistency + + // Clamp latitude to avoid infinite values near the poles + final longitude = coord[0]?.toDouble() ?? 0.0; + final latitude = max(min(coord[1]?.toDouble() ?? 0.0, 89.99), -89.99); + + // Convert longitude to x coordinate + final x = longitude * (conversionEarthRadius * pi / 180.0); + + // Convert latitude to y coordinate + final latRad = latitude * (pi / 180.0); + final y = log(tan((pi / 4) + (latRad / 2))) * conversionEarthRadius; + + // Clamp to valid Mercator bounds + final clampedX = max(min(x, mercatorLimit), -mercatorLimit); + final clampedY = max(min(y, mercatorLimit), -mercatorLimit); + + // Preserve altitude if present + final alt = coord.length > 2 ? coord[2] : null; + + return Position.of(alt != null ? [clampedX, clampedY, alt] : [clampedX, clampedY]); +} + +/// Converts a Web Mercator coordinate to WGS84. +/// +/// Valid inputs: [Position] with [x, y] in meters +/// Returns: [Position] with [longitude, latitude] coordinates +Position toWGS84(Position coord) { + // Use the earth radius constant for consistency + + // Clamp inputs to valid range + final x = max(min(coord[0]?.toDouble() ?? 0.0, mercatorLimit), -mercatorLimit); + final y = max(min(coord[1]?.toDouble() ?? 0.0, mercatorLimit), -mercatorLimit); + + // Convert x to longitude + final longitude = x / (conversionEarthRadius * pi / 180.0); + + // Convert y to latitude + final latRad = 2 * atan(exp(y / conversionEarthRadius)) - (pi / 2); + final latitude = latRad * (180.0 / pi); + + // Clamp latitude to valid range + final clampedLatitude = max(min(latitude, 90.0), -90.0); + + // Preserve altitude if present + final alt = coord.length > 2 ? coord[2] : null; + + return Position.of(alt != null ? [longitude, clampedLatitude, alt] : [longitude, clampedLatitude]); +} diff --git a/lib/src/to_mercator.dart b/lib/src/to_mercator.dart new file mode 100644 index 00000000..83fd55cc --- /dev/null +++ b/lib/src/to_mercator.dart @@ -0,0 +1,120 @@ +import 'dart:math' as math; +import 'package:geotypes/geotypes.dart'; +import 'package:turf/helpers.dart'; + +/// Converts WGS84 GeoJSON object to Web Mercator projection. +/// +/// Accepts a [GeoJSONObject] or [Position] and returns a projected [GeoJSONObject] or [Position]. +/// This function handles all GeoJSON types including Point, LineString, Polygon, +/// MultiPoint, MultiLineString, MultiPolygon, Feature, and FeatureCollection. +/// +/// If [mutate] is true, the input object is mutated for performance. +/// +/// See: https://en.wikipedia.org/wiki/Web_Mercator_projection +dynamic geoToMercator(dynamic geojson, {bool mutate = false}) { + // For simple Position objects, use the direct conversion + if (geojson is Position) { + return _toMercatorPosition(geojson); + } + + // Check that input is a GeoJSONObject for all other cases + if (geojson is! GeoJSONObject) { + throw ArgumentError('Unsupported input type: ${geojson.runtimeType}'); + } + + // Clone geojson to avoid side effects if not mutating + final workingObject = !mutate ? (geojson as GeoJSONObject).clone() : geojson; + + // Handle different GeoJSON types + if (workingObject is Point) { + workingObject.coordinates = _toMercatorPosition(workingObject.coordinates); + } else if (workingObject is LineString) { + for (var i = 0; i < workingObject.coordinates.length; i++) { + workingObject.coordinates[i] = _toMercatorPosition(workingObject.coordinates[i]); + } + } else if (workingObject is Polygon) { + for (var i = 0; i < workingObject.coordinates.length; i++) { + for (var j = 0; j < workingObject.coordinates[i].length; j++) { + workingObject.coordinates[i][j] = _toMercatorPosition(workingObject.coordinates[i][j]); + } + } + } else if (workingObject is MultiPoint) { + for (var i = 0; i < workingObject.coordinates.length; i++) { + workingObject.coordinates[i] = _toMercatorPosition(workingObject.coordinates[i]); + } + } else if (workingObject is MultiLineString) { + for (var i = 0; i < workingObject.coordinates.length; i++) { + for (var j = 0; j < workingObject.coordinates[i].length; j++) { + workingObject.coordinates[i][j] = _toMercatorPosition(workingObject.coordinates[i][j]); + } + } + } else if (workingObject is MultiPolygon) { + for (var i = 0; i < workingObject.coordinates.length; i++) { + for (var j = 0; j < workingObject.coordinates[i].length; j++) { + for (var k = 0; k < workingObject.coordinates[i][j].length; k++) { + workingObject.coordinates[i][j][k] = _toMercatorPosition(workingObject.coordinates[i][j][k]); + } + } + } + } else if (workingObject is GeometryCollection) { + for (var i = 0; i < workingObject.geometries.length; i++) { + workingObject.geometries[i] = geoToMercator(workingObject.geometries[i], mutate: true); + } + } else if (workingObject is Feature) { + if (workingObject.geometry != null) { + workingObject.geometry = geoToMercator(workingObject.geometry!, mutate: true); + } + } else if (workingObject is FeatureCollection) { + for (var i = 0; i < workingObject.features.length; i++) { + workingObject.features[i] = geoToMercator(workingObject.features[i], mutate: true); + } + } else { + throw ArgumentError('Unsupported input type: ${workingObject.runtimeType}'); + } + + return workingObject; +} + +/// Converts a Position from WGS84 to Web Mercator. +/// +/// Implements the spherical Mercator projection formulas. +/// Valid inputs: [Position] with [longitude, latitude] +/// Returns: [Position] with [x, y] coordinates in meters +Position _toMercatorPosition(Position wgs84) { + // Constants for Web Mercator projection + const double earthRadius = 6378137.0; // in meters + const double originShift = 2.0 * math.pi * earthRadius / 2.0; + + // Extract coordinates + final longitude = wgs84[0]?.toDouble() ?? 0.0; + final latitude = wgs84[1]?.toDouble() ?? 0.0; + + // Clamp latitude to avoid infinity near poles + final clampedLat = math.min(math.max(latitude, -89.9999), 89.9999); + + // Convert longitude to x coordinate + final x = longitude * originShift / 180.0; + + // Convert latitude to y coordinate + final rad = clampedLat * math.pi / 180.0; + final y = earthRadius * math.log(math.tan(math.pi / 4.0 + rad / 2.0)); + + // Clamp to valid Mercator bounds + final mercatorLimit = 20037508.34; // Maximum extent of Web Mercator in meters + final clampedX = math.max(math.min(x, mercatorLimit), -mercatorLimit); + final clampedY = math.max(math.min(y, mercatorLimit), -mercatorLimit); + + // Preserve altitude if present + final alt = wgs84.length > 2 ? wgs84[2] : null; + + return Position.of(alt != null + ? [ + clampedX, + clampedY, + alt, + ] + : [ + clampedX, + clampedY, + ]); +} diff --git a/lib/src/to_wgs84.dart b/lib/src/to_wgs84.dart new file mode 100644 index 00000000..4ced3e24 --- /dev/null +++ b/lib/src/to_wgs84.dart @@ -0,0 +1,113 @@ +import 'dart:math' as math; +import 'package:geotypes/geotypes.dart'; +import 'package:turf/helpers.dart'; + +/// Converts a [GeoJSONObject] or [Position] from Web Mercator to WGS84 coordinates. +/// +/// Accepts Mercator projection coordinates and returns WGS84 coordinates. +/// +/// If [mutate] is true, the input object is mutated for performance. +/// +/// See: https://epsg.io/4326 +dynamic geoToWgs84(dynamic mercator, {bool mutate = false}) { + // For simple Position objects, use the direct conversion + if (mercator is Position) { + return _toWgs84Position(mercator); + } + + // Check that input is a GeoJSONObject for all other cases + if (mercator is! GeoJSONObject) { + throw ArgumentError('Unsupported input type: ${mercator.runtimeType}'); + } + + // Clone mercator to avoid side effects if not mutating + final workingObject = !mutate ? (mercator as GeoJSONObject).clone() : mercator; + + // Handle different GeoJSON types + if (workingObject is Point) { + workingObject.coordinates = _toWgs84Position(workingObject.coordinates); + } else if (workingObject is LineString) { + for (var i = 0; i < workingObject.coordinates.length; i++) { + workingObject.coordinates[i] = _toWgs84Position(workingObject.coordinates[i]); + } + } else if (workingObject is Polygon) { + for (var i = 0; i < workingObject.coordinates.length; i++) { + for (var j = 0; j < workingObject.coordinates[i].length; j++) { + workingObject.coordinates[i][j] = _toWgs84Position(workingObject.coordinates[i][j]); + } + } + } else if (workingObject is MultiPoint) { + for (var i = 0; i < workingObject.coordinates.length; i++) { + workingObject.coordinates[i] = _toWgs84Position(workingObject.coordinates[i]); + } + } else if (workingObject is MultiLineString) { + for (var i = 0; i < workingObject.coordinates.length; i++) { + for (var j = 0; j < workingObject.coordinates[i].length; j++) { + workingObject.coordinates[i][j] = _toWgs84Position(workingObject.coordinates[i][j]); + } + } + } else if (workingObject is MultiPolygon) { + for (var i = 0; i < workingObject.coordinates.length; i++) { + for (var j = 0; j < workingObject.coordinates[i].length; j++) { + for (var k = 0; k < workingObject.coordinates[i][j].length; k++) { + workingObject.coordinates[i][j][k] = _toWgs84Position(workingObject.coordinates[i][j][k]); + } + } + } + } else if (workingObject is GeometryCollection) { + for (var i = 0; i < workingObject.geometries.length; i++) { + workingObject.geometries[i] = geoToWgs84(workingObject.geometries[i], mutate: true); + } + } else if (workingObject is Feature) { + if (workingObject.geometry != null) { + workingObject.geometry = geoToWgs84(workingObject.geometry!, mutate: true); + } + } else if (workingObject is FeatureCollection) { + for (var i = 0; i < workingObject.features.length; i++) { + workingObject.features[i] = geoToWgs84(workingObject.features[i], mutate: true); + } + } else { + throw ArgumentError('Unsupported input type: ${workingObject.runtimeType}'); + } + + return workingObject; +} + +/// Converts a Position from Web Mercator to WGS84. +/// +/// Valid inputs: [Position] with [x, y] in meters +/// Returns: [Position] with [longitude, latitude] coordinates +Position _toWgs84Position(Position mercator) { + // Constants for Web Mercator projection + const double earthRadius = 6378137.0; // in meters + const double mercatorLimit = 20037508.34; // Maximum extent in meters + const double originShift = 2.0 * math.pi * earthRadius / 2.0; + + // Extract coordinates + final x = math.max(math.min(mercator[0]?.toDouble() ?? 0.0, mercatorLimit), -mercatorLimit); + final y = math.max(math.min(mercator[1]?.toDouble() ?? 0.0, mercatorLimit), -mercatorLimit); + + // Convert x to longitude + final longitude = x / (earthRadius * math.pi / 180.0); + + // Convert y to latitude + final latRad = 2 * math.atan(math.exp(y / earthRadius)) - (math.pi / 2); + final latitude = latRad * (180.0 / math.pi); + + // Clamp latitude to valid range + final clampedLatitude = math.max(math.min(latitude, 90.0), -90.0); + + // Preserve altitude if present + final alt = mercator.length > 2 ? mercator[2] : null; + + return Position.of(alt != null + ? [ + longitude, + clampedLatitude, + alt, + ] + : [ + longitude, + clampedLatitude, + ]); +} diff --git a/lib/transform.dart b/lib/transform.dart index bc389780..1c307533 100644 --- a/lib/transform.dart +++ b/lib/transform.dart @@ -1,4 +1,5 @@ library turf_transform; export 'package:geotypes/geotypes.dart'; -export 'src/transform_rotate.dart'; +export 'src/to_mercator.dart'; +export 'src/to_wgs84.dart'; diff --git a/lib/turf.dart b/lib/turf.dart index 482694bb..374467c8 100644 --- a/lib/turf.dart +++ b/lib/turf.dart @@ -29,6 +29,7 @@ export 'midpoint.dart'; export 'nearest_point_on_line.dart'; export 'nearest_point.dart'; export 'point_to_line_distance.dart'; +export 'point_on_feature.dart'; export 'polygon_smooth.dart'; export 'polygon_to_line.dart'; export 'polyline.dart'; diff --git a/test/components/helpers_test.dart b/test/components/helpers_test.dart index 3ad0cb6d..687e9ada 100644 --- a/test/components/helpers_test.dart +++ b/test/components/helpers_test.dart @@ -1,80 +1,47 @@ -import 'dart:math'; import 'package:test/test.dart'; import 'package:turf/helpers.dart'; void main() { - test('radiansToLength', () { - expect(radiansToLength(1, Unit.radians), equals(1)); - expect(radiansToLength(1, Unit.kilometers), equals(earthRadius / 1000)); - expect(radiansToLength(1, Unit.miles), equals(earthRadius / 1609.344)); - }); - - test('lengthToRadians', () { - expect(lengthToRadians(1, Unit.radians), equals(1)); - expect(lengthToRadians(earthRadius / 1000, Unit.kilometers), equals(1)); - expect(lengthToRadians(earthRadius / 1609.344, Unit.miles), equals(1)); - }); - - test('lengthToDegrees', () { - expect(lengthToDegrees(1, Unit.radians), equals(57.29577951308232)); - expect(lengthToDegrees(100, Unit.kilometers), equals(0.899320363724538)); - expect(lengthToDegrees(10, Unit.miles), equals(0.1447315831437903)); - }); - - test('radiansToDegrees', () { - expect(round(radiansToDegrees(pi / 3), 6), equals(60), - reason: 'radiance conversion pi/3'); - expect(radiansToDegrees(3.5 * pi), equals(270), - reason: 'radiance conversion 3.5pi'); - expect(radiansToDegrees(-pi), equals(-180), - reason: 'radiance conversion -pi'); - }); - - test('radiansToDegrees', () { - expect(degreesToRadians(60), equals(pi / 3), - reason: 'degrees conversion 60'); - expect(degreesToRadians(270), equals(1.5 * pi), - reason: 'degrees conversion 270'); - expect(degreesToRadians(-180), equals(-pi), - reason: 'degrees conversion -180'); - }); - - test('bearingToAzimuth', () { - expect(bearingToAzimuth(40), equals(40)); - expect(bearingToAzimuth(-105), equals(255)); - expect(bearingToAzimuth(410), equals(50)); - expect(bearingToAzimuth(-200), equals(160)); - expect(bearingToAzimuth(-395), equals(325)); - }); - - test('round', () { - expect(round(125.123), equals(125)); - expect(round(123.123, 1), equals(123.1)); - expect(round(123.5), equals(124)); - - expect(() => round(34.5, -5), throwsA(isException)); - }); - - test('convertLength', () { - expect(convertLength(1000, Unit.meters), equals(1)); - expect(convertLength(1, Unit.kilometers, Unit.miles), - equals(0.621371192237334)); - expect(convertLength(1, Unit.miles, Unit.kilometers), equals(1.609344)); - expect(convertLength(1, Unit.nauticalmiles), equals(1.852)); - expect(convertLength(1, Unit.meters, Unit.centimeters), - equals(100.00000000000001)); - }); - - test('convertArea', () { - expect(convertArea(1000), equals(0.001)); - expect(convertArea(1, Unit.kilometers, Unit.miles), equals(0.386)); - expect(convertArea(1, Unit.miles, Unit.kilometers), - equals(2.5906735751295336)); - expect(convertArea(1, Unit.meters, Unit.centimeters), equals(10000)); - expect(convertArea(100, Unit.meters, Unit.acres), equals(0.0247105)); - expect( - convertArea(100, Unit.meters, Unit.yards), equals(119.59900459999999)); - expect(convertArea(100, Unit.meters, Unit.feet), equals(1076.3910417)); - expect(convertArea(100000, Unit.feet), equals(0.009290303999749462)); + group('Coordinate System Conversions', () { + test('convertCoordinates should convert between systems', () { + final wgs84Point = Position.of([10.0, 20.0, 100.0]); // longitude, latitude, altitude + + // WGS84 to Mercator + final mercatorPoint = convertCoordinates( + wgs84Point, + CoordinateSystem.wgs84, + CoordinateSystem.mercator + ); + + // Mercator to WGS84 (should get back close to the original) + final reconvertedPoint = convertCoordinates( + mercatorPoint, + CoordinateSystem.mercator, + CoordinateSystem.wgs84 + ); + + // Verify values are close to the originals + expect(reconvertedPoint[0]?.toDouble() ?? 0.0, closeTo(wgs84Point[0]?.toDouble() ?? 0.0, 0.001)); // longitude + expect(reconvertedPoint[1]?.toDouble() ?? 0.0, closeTo(wgs84Point[1]?.toDouble() ?? 0.0, 0.001)); // latitude + expect(reconvertedPoint[2], equals(wgs84Point[2])); // altitude should be preserved + }); + + test('toMercator should preserve altitude', () { + final wgs84Point = Position.of([10.0, 20.0, 100.0]); // longitude, latitude, altitude + final mercatorPoint = toMercator(wgs84Point); + + // Check that altitude is preserved + expect(mercatorPoint.length, equals(3)); + expect(mercatorPoint[2], equals(100.0)); + }); + + test('toWGS84 should preserve altitude', () { + final mercatorPoint = Position.of([1113194.9079327357, 2273030.92688923, 100.0]); // x, y, altitude + final wgs84Point = toWGS84(mercatorPoint); + + // Check that altitude is preserved + expect(wgs84Point.length, equals(3)); + expect(wgs84Point[2], equals(100.0)); + }); }); } diff --git a/test/components/to_mercator_test.dart b/test/components/to_mercator_test.dart new file mode 100644 index 00000000..2f82fee7 --- /dev/null +++ b/test/components/to_mercator_test.dart @@ -0,0 +1,135 @@ +import 'package:test/test.dart'; +import 'package:turf/helpers.dart'; +import 'package:turf/transform.dart'; + +void main() { + group('Mercator Projection', () { + test('should convert a Position from WGS84 to Web Mercator', () { + final wgs84 = Position.of([0, 0]); // Null Island + final mercator = geoToMercator(wgs84) as Position; + + expect(mercator[0], closeTo(0, 1e-9)); // At null island, x should be very close to 0 + expect(mercator[1], closeTo(0, 1e-9)); // At null island, y should be very close to 0 + }); + + test('should preserve altitude when converting Position', () { + final wgs84 = Position.of([0, 0, 100]); // Null Island with altitude + final mercator = geoToMercator(wgs84) as Position; + + expect(mercator.length, equals(3)); + expect(mercator[2], equals(100)); + }); + + test('should convert a Point from WGS84 to Web Mercator', () { + final point = Point(coordinates: Position.of([10, 20])); + final mercatorPoint = geoToMercator(point, mutate: false) as Point; + + // Compare with direct conversion + final expectedCoords = geoToMercator(point.coordinates) as Position; + + expect(mercatorPoint.coordinates[0], closeTo(expectedCoords[0]?.toDouble() ?? 0.0, 0.001)); + expect(mercatorPoint.coordinates[1], closeTo(expectedCoords[1]?.toDouble() ?? 0.0, 0.001)); + }); + + test('should convert a LineString from WGS84 to Web Mercator', () { + final lineString = LineString(coordinates: [ + Position.of([0, 0]), + Position.of([10, 10]) + ]); + + final mercatorLineString = geoToMercator(lineString, mutate: false) as LineString; + + // Check first point + final expectedFirstCoords = geoToMercator(lineString.coordinates[0]) as Position; + expect(mercatorLineString.coordinates[0][0], closeTo(expectedFirstCoords[0]?.toDouble() ?? 0.0, 0.001)); + expect(mercatorLineString.coordinates[0][1], closeTo(expectedFirstCoords[1]?.toDouble() ?? 0.0, 0.001)); + + // Check second point + final expectedSecondCoords = geoToMercator(lineString.coordinates[1]) as Position; + expect(mercatorLineString.coordinates[1][0], closeTo(expectedSecondCoords[0]?.toDouble() ?? 0.0, 0.001)); + expect(mercatorLineString.coordinates[1][1], closeTo(expectedSecondCoords[1]?.toDouble() ?? 0.0, 0.001)); + }); + + test('should convert a Polygon from WGS84 to Web Mercator', () { + final polygon = Polygon(coordinates: [ + [ + Position.of([0, 0]), + Position.of([10, 0]), + Position.of([10, 10]), + Position.of([0, 10]), + Position.of([0, 0]) // Closing point + ] + ]); + + final mercatorPolygon = geoToMercator(polygon, mutate: false) as Polygon; + + // Check a sample point + final expectedCoords = geoToMercator(polygon.coordinates[0][1]) as Position; + expect(mercatorPolygon.coordinates[0][1][0], closeTo(expectedCoords[0]?.toDouble() ?? 0.0, 0.001)); + expect(mercatorPolygon.coordinates[0][1][1], closeTo(expectedCoords[1]?.toDouble() ?? 0.0, 0.001)); + }); + + test('should convert a Feature from WGS84 to Web Mercator', () { + final feature = Feature( + geometry: Point(coordinates: Position.of([10, 20])), + properties: {'name': 'Test Point'} + ); + + final mercatorFeature = geoToMercator(feature, mutate: false) as Feature; + + // Verify geometry was converted + final expectedCoords = geoToMercator(feature.geometry!.coordinates) as Position; + expect(mercatorFeature.geometry!.coordinates[0], closeTo(expectedCoords[0]?.toDouble() ?? 0.0, 0.001)); + expect(mercatorFeature.geometry!.coordinates[1], closeTo(expectedCoords[1]?.toDouble() ?? 0.0, 0.001)); + + // Verify properties were preserved + expect(mercatorFeature.properties!['name'], equals('Test Point')); + }); + + test('should convert a FeatureCollection from WGS84 to Web Mercator', () { + final fc = FeatureCollection(features: [ + Feature( + geometry: Point(coordinates: Position.of([10, 20])), + properties: {'name': 'Point 1'} + ), + Feature( + geometry: Point(coordinates: Position.of([20, 30])), + properties: {'name': 'Point 2'} + ) + ]); + + final mercatorFc = geoToMercator(fc, mutate: false) as FeatureCollection; + + // Verify both features were converted + expect(mercatorFc.features.length, equals(2)); + + // Check first feature + final expectedCoords1 = geoToMercator(fc.features[0].geometry!.coordinates) as Position; + final point1 = mercatorFc.features[0].geometry as Point; + expect(point1.coordinates[0], closeTo(expectedCoords1[0]?.toDouble() ?? 0.0, 0.001)); + expect(point1.coordinates[1], closeTo(expectedCoords1[1]?.toDouble() ?? 0.0, 0.001)); + + // Verify properties were preserved + expect(mercatorFc.features[0].properties!['name'], equals('Point 1')); + expect(mercatorFc.features[1].properties!['name'], equals('Point 2')); + }); + + test('should throw for unsupported input types', () { + expect(() => geoToMercator("not a GeoJSON object"), throwsA(isA())); + }); + + test('should respect mutate option for performance', () { + final original = Point(coordinates: Position.of([10, 20])); + final clone = original.clone(); + + // With mutate: false + final withoutMutate = geoToMercator(original, mutate: false) as Point; + expect(original.coordinates[0], equals(clone.coordinates[0])); // Original unchanged + + // With mutate: true + final withMutate = geoToMercator(original, mutate: true) as Point; + expect(original.coordinates[0], equals(withMutate.coordinates[0])); // Original changed + expect(original.coordinates[0], isNot(equals(clone.coordinates[0]))); // Original different from original clone + }); + }); +} diff --git a/test/components/to_wgs84_test.dart b/test/components/to_wgs84_test.dart new file mode 100644 index 00000000..728b31ca --- /dev/null +++ b/test/components/to_wgs84_test.dart @@ -0,0 +1,153 @@ +import 'package:test/test.dart'; +import 'package:turf/helpers.dart'; +import 'package:turf/transform.dart'; + +void main() { + group('Web Mercator to WGS84 Conversion', () { + test('should convert a Position from Web Mercator to WGS84', () { + // A point in Web Mercator (Null Island) + final mercator = Position.of([0, 0]); + final wgs84 = geoToWgs84(mercator) as Position; + + expect(wgs84[0], closeTo(0, 1e-9)); // At null island, longitude should be very close to 0 + expect(wgs84[1], closeTo(0, 1e-9)); // At null island, latitude should be very close to 0 + }); + + test('should preserve altitude when converting Position', () { + final mercator = Position.of([0, 0, 100]); // Null Island with altitude + final wgs84 = geoToWgs84(mercator) as Position; + + expect(wgs84.length, equals(3)); + expect(wgs84[2], equals(100)); + }); + + test('should convert a Point from Web Mercator to WGS84', () { + // New York in Web Mercator approximately + final mercatorPoint = Point(coordinates: Position.of([-8237642.31, 4970241.32])); + final wgs84Point = geoToWgs84(mercatorPoint, mutate: false) as Point; + + // Verify coordinates are in expected WGS84 range + expect(wgs84Point.coordinates[0], closeTo(-74.0, 0.5)); // Approx. longitude of New York + expect(wgs84Point.coordinates[1], closeTo(40.7, 0.5)); // Approx. latitude of New York + }); + + test('should convert a LineString from Web Mercator to WGS84', () { + final mercatorLineString = LineString(coordinates: [ + // Points in Web Mercator + Position.of([0, 0]), + Position.of([1113195, 1118890]) + ]); + + final wgs84LineString = geoToWgs84(mercatorLineString, mutate: false) as LineString; + + // Check first point at Null Island + expect(wgs84LineString.coordinates[0][0], closeTo(0, 1e-9)); + expect(wgs84LineString.coordinates[0][1], closeTo(0, 1e-9)); + + // Check second point is in expected WGS84 range + expect(wgs84LineString.coordinates[1][0], closeTo(10.0, 0.5)); + expect(wgs84LineString.coordinates[1][1], closeTo(10.0, 0.5)); + }); + + test('should convert a Polygon from Web Mercator to WGS84', () { + final mercatorPolygon = Polygon(coordinates: [ + [ + Position.of([0, 0]), + Position.of([1113195, 0]), + Position.of([1113195, 1118890]), + Position.of([0, 1118890]), + Position.of([0, 0]) // Closing point + ] + ]); + + final wgs84Polygon = geoToWgs84(mercatorPolygon, mutate: false) as Polygon; + + // Check corners of polygon + expect(wgs84Polygon.coordinates[0][0][0], closeTo(0, 1e-9)); + expect(wgs84Polygon.coordinates[0][0][1], closeTo(0, 1e-9)); + + expect(wgs84Polygon.coordinates[0][2][0], closeTo(10.0, 0.5)); + expect(wgs84Polygon.coordinates[0][2][1], closeTo(10.0, 0.5)); + }); + + test('should convert a Feature from Web Mercator to WGS84', () { + final mercatorFeature = Feature( + geometry: Point(coordinates: Position.of([15550408.91, 4257980.73, 5.0])), // Tokyo in Web Mercator + properties: {'name': 'Tokyo', 'country': 'Japan'} + ); + + final wgs84Feature = geoToWgs84(mercatorFeature, mutate: false) as Feature; + + // Verify geometry was converted to approximately Tokyo's WGS84 coordinates + expect(wgs84Feature.geometry!.coordinates[0], closeTo(139.69, 0.5)); // Tokyo longitude + expect(wgs84Feature.geometry!.coordinates[1], closeTo(35.68, 0.5)); // Tokyo latitude + expect(wgs84Feature.geometry!.coordinates[2], equals(5.0)); // Preserve altitude + + // Verify properties were preserved + expect(wgs84Feature.properties!['name'], equals('Tokyo')); + expect(wgs84Feature.properties!['country'], equals('Japan')); + }); + + test('should convert a FeatureCollection from Web Mercator to WGS84', () { + final mercatorFc = FeatureCollection(features: [ + // Paris in Web Mercator + Feature( + geometry: Point(coordinates: Position.of([261865.42, 6250566.48])), + properties: {'name': 'Paris'} + ), + // Moscow in Web Mercator + Feature( + geometry: Point(coordinates: Position.of([4187399.59, 7509720.48])), + properties: {'name': 'Moscow'} + ) + ]); + + final wgs84Fc = geoToWgs84(mercatorFc, mutate: false) as FeatureCollection; + + // Verify both features were converted + expect(wgs84Fc.features.length, equals(2)); + + // Check first feature (Paris) + final point1 = wgs84Fc.features[0].geometry as Point; + expect(point1.coordinates[0], closeTo(2.35, 0.5)); // Paris longitude + expect(point1.coordinates[1], closeTo(48.85, 0.5)); // Paris latitude + + // Verify properties were preserved + expect(wgs84Fc.features[0].properties!['name'], equals('Paris')); + expect(wgs84Fc.features[1].properties!['name'], equals('Moscow')); + }); + + test('should throw for unsupported input types', () { + expect(() => geoToWgs84("not a GeoJSON object"), throwsA(isA())); + }); + + test('should respect mutate option for performance', () { + final original = Point(coordinates: Position.of([-8237642.31, 4970241.32])); + final clone = original.clone(); + + // With mutate: false + final withoutMutate = geoToWgs84(original, mutate: false) as Point; + expect(original.coordinates[0], equals(clone.coordinates[0])); // Original unchanged + + // With mutate: true + final withMutate = geoToWgs84(original, mutate: true) as Point; + expect(original.coordinates[0], equals(withMutate.coordinates[0])); // Original changed + expect(original.coordinates[0], isNot(equals(clone.coordinates[0]))); // Original different from original clone + }); + + test('round-trip conversion should approximately recover original coordinates', () { + // Start with WGS84 + final startWgs84 = Position.of([10, 20]); + + // Convert to Mercator + final mercator = geoToMercator(startWgs84) as Position; + + // Convert back to WGS84 + final endWgs84 = geoToWgs84(mercator) as Position; + + // Should be close to original + expect(endWgs84[0], closeTo(startWgs84[0]?.toDouble() ?? 0.0, 0.001)); + expect(endWgs84[1], closeTo(startWgs84[1]?.toDouble() ?? 0.0, 0.001)); + }); + }); +} diff --git a/test/examples/helpers/README.md b/test/examples/helpers/README.md new file mode 100644 index 00000000..34286885 --- /dev/null +++ b/test/examples/helpers/README.md @@ -0,0 +1,57 @@ +# Turf Helpers Examples + +This directory contains examples demonstrating the utility functions in the `helpers.dart` file of the turf_dart library. + +## Files in this Directory + +1. **helpers_example.dart**: Practical examples of using the helpers functions with explanations and output +2. **helpers_visualization.geojson**: Visual demonstration of the helpers functions for viewing in a GeoJSON viewer +3. **test_helpers.dart**: Simple test functions that exercise each major function in the helpers.dart file + +## Functionality Demonstrated + +### Unit Conversions +- Convert between different length units (kilometers, miles, meters, etc.) +- Convert between different area units +- Convert between radians, degrees, and real-world units + +### Angle Conversions +- Convert between degrees and radians +- Convert bearings to azimuths (normalized angles) + +### Rounding Functions +- Round numbers to specific precision levels + +## Running the Examples + +To run the example code and see the output: + +```bash +dart test/examples/helpers/helpers_example.dart +``` + +## Visualization + +The `helpers_visualization.geojson` file can be viewed in any GeoJSON viewer to see visual examples of: + +1. **Distance Conversion**: Circle with 10km radius showing conversion between degrees and kilometers +2. **Bearing Example**: Line and points showing bearing/azimuth concepts +3. **Angle Conversion**: Points at different angles (0°, 90°, 180°, 270°) around a circle +4. **Area Conversion**: Square with 10km² area + +## Helper Functions Reference + +| Function | Description | +|----------|-------------| +| `radiansToLength(radians, unit)` | Convert radians to real-world distance units | +| `lengthToRadians(distance, unit)` | Convert real-world distance to radians | +| `lengthToDegrees(distance, unit)` | Convert real-world distance to degrees | +| `bearingToAzimuth(bearing)` | Convert any bearing angle to standard azimuth (0-360°) | +| `radiansToDegrees(radians)` | Convert radians to degrees | +| `degreesToRadians(degrees)` | Convert degrees to radians | +| `convertLength(length, fromUnit, toUnit)` | Convert length between different units | +| `convertArea(area, fromUnit, toUnit)` | Convert area between different units | +| `round(value, precision)` | Round number to specified precision | +| `toMercator(coord)` | Convert WGS84 coordinates [lon, lat] to Web Mercator [x, y] | +| `toWGS84(coord)` | Convert Web Mercator coordinates [x, y] to WGS84 [lon, lat] | +| `convertCoordinates(coord, fromSystem, toSystem)` | Convert coordinates between different systems | diff --git a/test/examples/helpers/helpers_example.dart b/test/examples/helpers/helpers_example.dart new file mode 100644 index 00000000..6a069cce --- /dev/null +++ b/test/examples/helpers/helpers_example.dart @@ -0,0 +1,196 @@ +import 'dart:math'; +import 'package:turf/turf.dart'; + +/// This example demonstrates the helper functions available in turf_dart +void main() { + // Unit conversions + unitConversionExamples(); + + // Angle conversions + angleConversionExamples(); + + // Bearing to azimuth + bearingToAzimuthExamples(); + + // Rounding + roundingExamples(); + + // Coordinate system conversions + coordinateSystemConversionExamples(); +} + +void unitConversionExamples() { + print('\n=== Unit Conversion Examples ===\n'); + + // Example 1: Convert distances from one unit to another + print('Distance Conversions:'); + print(' 10 kilometers = ${convertLength(10, Unit.kilometers, Unit.miles).toStringAsFixed(2)} miles'); + print(' 26.2 miles = ${convertLength(26.2, Unit.miles, Unit.kilometers).toStringAsFixed(2)} kilometers'); + print(' 100 meters = ${convertLength(100, Unit.meters, Unit.feet).toStringAsFixed(2)} feet'); + print(' 1 nautical mile = ${convertLength(1, Unit.nauticalmiles, Unit.kilometers).toStringAsFixed(2)} kilometers'); + + // Example 2: Convert from radians to real-world units + print('\nRadians to Length:'); + final oneRadian = 1.0; + print(' 1 radian = ${radiansToLength(oneRadian, Unit.kilometers).toStringAsFixed(2)} kilometers'); + print(' 1 radian = ${radiansToLength(oneRadian, Unit.miles).toStringAsFixed(2)} miles'); + print(' 1 radian = ${radiansToLength(oneRadian, Unit.meters).toStringAsFixed(2)} meters'); + + // Example 3: Convert from real-world units to radians + print('\nLength to Radians:'); + print(' 100 kilometers = ${lengthToRadians(100, Unit.kilometers).toStringAsFixed(6)} radians'); + print(' 100 miles = ${lengthToRadians(100, Unit.miles).toStringAsFixed(6)} radians'); + + // Example 4: Convert from real-world units to degrees + print('\nLength to Degrees:'); + print(' 100 kilometers = ${lengthToDegrees(100, Unit.kilometers).toStringAsFixed(2)} degrees'); + print(' 100 miles = ${lengthToDegrees(100, Unit.miles).toStringAsFixed(2)} degrees'); + + // Example 5: Convert areas from one unit to another + print('\nArea Conversions:'); + print(' 1 square kilometer = ${convertArea(1, Unit.kilometers, Unit.meters).toStringAsFixed(0)} square meters'); + print(' 1 square mile = ${convertArea(1, Unit.miles, Unit.acres).toStringAsFixed(2)} acres'); + print(' 10000 square meters = ${convertArea(10000, Unit.meters, Unit.kilometers).toStringAsFixed(2)} square kilometers'); + print(' 5 acres = ${convertArea(5, Unit.acres, Unit.meters).toStringAsFixed(0)} square meters'); +} + +void angleConversionExamples() { + print('\n=== Angle Conversion Examples ===\n'); + + // Example 1: Convert degrees to radians + print('Degrees to Radians:'); + print(' 0° = ${degreesToRadians(0).toStringAsFixed(6)} radians'); + print(' 90° = ${degreesToRadians(90).toStringAsFixed(6)} radians'); + print(' 180° = ${degreesToRadians(180).toStringAsFixed(6)} radians'); + print(' 360° = ${degreesToRadians(360).toStringAsFixed(6)} radians'); + + // Example 2: Convert radians to degrees + print('\nRadians to Degrees:'); + print(' 0 radians = ${radiansToDegrees(0).toStringAsFixed(2)}°'); + print(' π/2 radians = ${radiansToDegrees(pi/2).toStringAsFixed(2)}°'); + print(' π radians = ${radiansToDegrees(pi).toStringAsFixed(2)}°'); + print(' 2π radians = ${radiansToDegrees(2*pi).toStringAsFixed(2)}°'); +} + +void bearingToAzimuthExamples() { + print('\n=== Bearing to Azimuth Examples ===\n'); + print('The bearingToAzimuth function converts any bearing angle to a standard azimuth:'); + print('- Azimuth is the angle between 0° and 360° in clockwise direction from north'); + print('- Negative bearings are converted to their positive equivalent'); + + // Converting various bearings to azimuth + final bearings = [-45.0, 0.0, 90.0, 180.0, 270.0, 360.0, 395.0, -170.0]; + + print('\nBearing → Azimuth conversions:'); + for (final bearing in bearings) { + final azimuth = bearingToAzimuth(bearing); + print(' $bearing° → $azimuth°'); + } +} + +void roundingExamples() { + print('\n=== Rounding Function Examples ===\n'); + + // The round function allows precise control over number rounding + final numbers = [3.14159265359, 0.123456789, 42.999999, -8.54321]; + + print('Rounding to different precision levels:'); + for (final num in numbers) { + print('\nOriginal: $num'); + print(' 0 decimals: ${round(num, 0)}'); + print(' 2 decimals: ${round(num, 2)}'); + print(' 4 decimals: ${round(num, 4)}'); + print(' 6 decimals: ${round(num, 6)}'); + } + + // Practical usage examples + print('\nPractical usage examples:'); + + // Example 1: Rounding coordinates for display + final coordinate = [151.2093, -33.8688]; // Sydney coordinates + print(' Original coordinate: ${coordinate[0]}, ${coordinate[1]}'); + print(' Rounded coordinate: ${round(coordinate[0], 4)}, ${round(coordinate[1], 4)}'); + + // Example 2: Rounding distances for human-readable output + final distance = 1234.5678; + print(' Original distance: $distance meters'); + print(' Rounded distance: ${round(distance, 0)} meters'); + + // Example 3: Rounding angles + final angle = 42.87654321; + print(' Original angle: $angle degrees'); + print(' Rounded angle: ${round(angle, 1)} degrees'); +} + +void coordinateSystemConversionExamples() { + print('\n=== Coordinate System Conversion Examples ===\n'); + print('The turf_dart library provides functions to convert between WGS84 (lon/lat) and'); + print('Web Mercator (EPSG:3857) coordinate systems.'); + + // Example 1: Convert specific locations + print('\nConverting well-known locations:'); + + // Define some well-known locations + final locations = [ + {'name': 'New York City', 'wgs84': [-74.006, 40.7128]}, + {'name': 'Sydney', 'wgs84': [151.2093, -33.8688]}, + {'name': 'Tokyo', 'wgs84': [139.6917, 35.6895]}, + {'name': 'London', 'wgs84': [-0.1278, 51.5074]} + ]; + + for (final location in locations) { + final wgs84 = location['wgs84'] as List; + final mercator = toMercator(wgs84); + + print('\n ${location['name']}:'); + print(' • WGS84 (lon/lat): [${wgs84[0]}, ${wgs84[1]}]'); + print(' • Mercator (x,y): [${round(mercator[0], 2)}, ${round(mercator[1], 2)}]'); + } + + // Example 2: Round-trip conversion demonstration + print('\nRound-trip conversion demonstration:'); + + final sydney = [151.2093, -33.8688]; + print('\n Original WGS84: $sydney'); + + // Convert to Web Mercator + final mercator = toMercator(sydney); + print(' → Web Mercator: [${round(mercator[0], 2)}, ${round(mercator[1], 2)}]'); + + // Convert back to WGS84 + final backToWgs84 = toWGS84(mercator); + print(' → Back to WGS84: [${round(backToWgs84[0], 6)}, ${round(backToWgs84[1], 6)}]'); + + // Show precision loss + final lonDiff = (sydney[0] - backToWgs84[0]).abs(); + final latDiff = (sydney[1] - backToWgs84[1]).abs(); + print(' Difference: [${lonDiff.toStringAsFixed(10)}°, ${latDiff.toStringAsFixed(10)}°]'); + + // Example 3: Using the unified convertCoordinates function + print('\nUsing convertCoordinates function:'); + + final london = [-0.1278, 51.5074]; + print(' Original WGS84: $london'); + + // Convert to Mercator using the convertCoordinates function + final londonMercator = convertCoordinates( + london, + CoordinateSystem.wgs84, + CoordinateSystem.mercator + ); + print(' → Web Mercator: [${round(londonMercator[0], 2)}, ${round(londonMercator[1], 2)}]'); + + // Convert back to WGS84 + final londonBackToWgs84 = convertCoordinates( + londonMercator, + CoordinateSystem.mercator, + CoordinateSystem.wgs84 + ); + print(' → Back to WGS84: [${round(londonBackToWgs84[0], 6)}, ${round(londonBackToWgs84[1], 6)}]'); + + // Example 4: Useful applications of coordinate conversions + print('\nPractical uses of coordinate conversions:'); + print(' • Web mapping: Converting between geographic (WGS84) and projection coordinates (Mercator)'); + print(' • Distance calculations: Mercator is useful for small areas where distance preservation is important'); + print(' • Visual representation: Web Mercator is the standard for most web mapping applications'); +} diff --git a/test/examples/helpers/helpers_visualization.geojson b/test/examples/helpers/helpers_visualization.geojson new file mode 100644 index 00000000..61aa3cbf --- /dev/null +++ b/test/examples/helpers/helpers_visualization.geojson @@ -0,0 +1,340 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "Coordinate System Conversions", + "description": "Comparison of WGS84 and Web Mercator projections", + "fill": "#6600cc", + "fill-opacity": 0.2, + "stroke": "#6600cc", + "stroke-width": 2 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-180, -80], + [180, -80], + [180, 80], + [-180, 80], + [-180, -80] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "name": "WGS84 Equator", + "description": "Line at latitude 0° in WGS84", + "stroke": "#6600cc", + "stroke-width": 2, + "stroke-dasharray": "5,5" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [-180, 0], + [180, 0] + ] + } + }, + { + "type": "Feature", + "properties": { + "name": "New York (WGS84)", + "description": "New York City in WGS84 coordinates", + "marker-color": "#6600cc", + "marker-symbol": "circle" + }, + "geometry": { + "type": "Point", + "coordinates": [-74.006, 40.7128] + } + }, + { + "type": "Feature", + "properties": { + "name": "Sydney (WGS84)", + "description": "Sydney in WGS84 coordinates", + "marker-color": "#6600cc", + "marker-symbol": "circle" + }, + "geometry": { + "type": "Point", + "coordinates": [151.2093, -33.8688] + } + }, + { + "type": "Feature", + "properties": { + "name": "Tokyo (WGS84)", + "description": "Tokyo in WGS84 coordinates", + "marker-color": "#6600cc", + "marker-symbol": "circle" + }, + "geometry": { + "type": "Point", + "coordinates": [139.6917, 35.6895] + } + }, + { + "type": "Feature", + "properties": { + "name": "London (WGS84)", + "description": "London in WGS84 coordinates", + "marker-color": "#6600cc", + "marker-symbol": "circle" + }, + "geometry": { + "type": "Point", + "coordinates": [-0.1278, 51.5074] + } + }, + { + "type": "Feature", + "properties": { + "name": "Distance Conversion Example", + "description": "Circle with 10km radius converted to different units", + "fill": "#ff9900", + "fill-opacity": 0.2, + "stroke": "#ff9900", + "stroke-width": 2 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [0, 0], + [0.09, 0], + [0.089, 0.012], + [0.087, 0.023], + [0.084, 0.035], + [0.08, 0.046], + [0.074, 0.056], + [0.068, 0.066], + [0.06, 0.076], + [0.051, 0.084], + [0.042, 0.092], + [0.032, 0.098], + [0.021, 0.098], + [0.011, 0.099], + [0, 0.1], + [-0.011, 0.099], + [-0.021, 0.098], + [-0.032, 0.098], + [-0.042, 0.092], + [-0.051, 0.084], + [-0.06, 0.076], + [-0.068, 0.066], + [-0.074, 0.056], + [-0.08, 0.046], + [-0.084, 0.035], + [-0.087, 0.023], + [-0.089, 0.012], + [-0.09, 0], + [-0.089, -0.012], + [-0.087, -0.023], + [-0.084, -0.035], + [-0.08, -0.046], + [-0.074, -0.056], + [-0.068, -0.066], + [-0.06, -0.076], + [-0.051, -0.084], + [-0.042, -0.092], + [-0.032, -0.098], + [-0.021, -0.098], + [-0.011, -0.099], + [0, -0.1], + [0.011, -0.099], + [0.021, -0.098], + [0.032, -0.098], + [0.042, -0.092], + [0.051, -0.084], + [0.06, -0.076], + [0.068, -0.066], + [0.074, -0.056], + [0.08, -0.046], + [0.084, -0.035], + [0.087, -0.023], + [0.089, -0.012], + [0.09, 0] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "name": "10km Circle", + "description": "Circle with radius of 10km (0.09 degrees)", + "marker-color": "#ff9900", + "marker-symbol": "circle" + }, + "geometry": { + "type": "Point", + "coordinates": [0, 0] + } + }, + { + "type": "Feature", + "properties": { + "name": "Bearing Example", + "description": "Demonstrates bearing to azimuth conversion", + "stroke": "#0066ff", + "stroke-width": 2 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [0, 0], + [0.1, 0.1] + ] + } + }, + { + "type": "Feature", + "properties": { + "name": "Origin Point", + "description": "Starting point for bearing calculations", + "marker-color": "#0066ff", + "marker-symbol": "circle" + }, + "geometry": { + "type": "Point", + "coordinates": [0, 0] + } + }, + { + "type": "Feature", + "properties": { + "name": "45° Bearing", + "description": "Point at 45° bearing from origin", + "marker-color": "#0066ff", + "marker-symbol": "triangle" + }, + "geometry": { + "type": "Point", + "coordinates": [0.1, 0.1] + } + }, + { + "type": "Feature", + "properties": { + "name": "Angle Conversion Example", + "description": "Points positioned at different angles around a circle", + "stroke": "#00cc99", + "stroke-width": 2, + "stroke-opacity": 0.7, + "fill": "#00cc99", + "fill-opacity": 0.1 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [0.05, 0], + [0.035, 0.035], + [0, 0.05], + [-0.035, 0.035], + [-0.05, 0], + [-0.035, -0.035], + [0, -0.05], + [0.035, -0.035], + [0.05, 0] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "name": "0° Point", + "description": "Point at 0° (East)", + "marker-color": "#00cc99", + "marker-symbol": "circle" + }, + "geometry": { + "type": "Point", + "coordinates": [0.05, 0] + } + }, + { + "type": "Feature", + "properties": { + "name": "90° Point", + "description": "Point at 90° (North)", + "marker-color": "#00cc99", + "marker-symbol": "circle" + }, + "geometry": { + "type": "Point", + "coordinates": [0, 0.05] + } + }, + { + "type": "Feature", + "properties": { + "name": "180° Point", + "description": "Point at 180° (West)", + "marker-color": "#00cc99", + "marker-symbol": "circle" + }, + "geometry": { + "type": "Point", + "coordinates": [-0.05, 0] + } + }, + { + "type": "Feature", + "properties": { + "name": "270° Point", + "description": "Point at 270° (South)", + "marker-color": "#00cc99", + "marker-symbol": "circle" + }, + "geometry": { + "type": "Point", + "coordinates": [0, -0.05] + } + }, + { + "type": "Feature", + "properties": { + "name": "Area Conversion Example", + "description": "Square with 10km² area (0.1° × 0.1°)", + "stroke": "#cc3300", + "stroke-width": 2, + "fill": "#cc3300", + "fill-opacity": 0.2 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-0.05, -0.05], + [0.05, -0.05], + [0.05, 0.05], + [-0.05, 0.05], + [-0.05, -0.05] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "name": "Area Center", + "description": "Center of area conversion example", + "marker-color": "#cc3300", + "marker-symbol": "square" + }, + "geometry": { + "type": "Point", + "coordinates": [0, 0] + } + } + ] +} diff --git a/test/examples/helpers/test_helpers.dart b/test/examples/helpers/test_helpers.dart new file mode 100644 index 00000000..1376c997 --- /dev/null +++ b/test/examples/helpers/test_helpers.dart @@ -0,0 +1,150 @@ +import 'dart:math'; +import 'package:turf/turf.dart'; + +void main() { + testUnitConversions(); + testAngleConversions(); + testBearingToAzimuth(); + testRounding(); + testCoordinateSystemConversions(); +} + +void testUnitConversions() { + print('\n=== Testing Unit Conversions ===\n'); + + // Test radiansToLength + final testRadians = 1.0; // 1 radian + print('1 radian equals:'); + print(' ${radiansToLength(testRadians, Unit.kilometers)} kilometers'); + print(' ${radiansToLength(testRadians, Unit.miles)} miles'); + print(' ${radiansToLength(testRadians, Unit.meters)} meters'); + + // Test lengthToRadians + final testDistance = 100.0; // 100 units + print('\n100 units equals:'); + print(' ${lengthToRadians(testDistance, Unit.kilometers)} radians from kilometers'); + print(' ${lengthToRadians(testDistance, Unit.miles)} radians from miles'); + print(' ${lengthToRadians(testDistance, Unit.meters)} radians from meters'); + + // Test lengthToDegrees + print('\n100 units equals:'); + print(' ${lengthToDegrees(testDistance, Unit.kilometers)} degrees from kilometers'); + print(' ${lengthToDegrees(testDistance, Unit.miles)} degrees from miles'); + + // Test convertLength + print('\nLength Conversions:'); + print(' 10 km = ${convertLength(10, Unit.kilometers, Unit.miles)} miles'); + print(' 10 miles = ${convertLength(10, Unit.miles, Unit.kilometers)} kilometers'); + print(' 5000 meters = ${convertLength(5000, Unit.meters, Unit.kilometers)} kilometers'); + + // Test convertArea + print('\nArea Conversions:'); + print(' 1 square km = ${convertArea(1, Unit.kilometers, Unit.meters)} square meters'); + print(' 5000 square meters = ${convertArea(5000, Unit.meters, Unit.kilometers)} square kilometers'); + print(' 1 square mile = ${convertArea(1, Unit.miles, Unit.acres)} acres'); +} + +void testAngleConversions() { + print('\n=== Testing Angle Conversions ===\n'); + + // Test degreesToRadians + print('Degrees to Radians:'); + print(' 0° = ${degreesToRadians(0)} radians'); + print(' 90° = ${degreesToRadians(90)} radians'); + print(' 180° = ${degreesToRadians(180)} radians'); + print(' 360° = ${degreesToRadians(360)} radians'); + + // Test radiansToDegrees + print('\nRadians to Degrees:'); + print(' 0 radians = ${radiansToDegrees(0)}°'); + print(' π/2 radians = ${radiansToDegrees(pi/2)}°'); + print(' π radians = ${radiansToDegrees(pi)}°'); + print(' 2π radians = ${radiansToDegrees(2*pi)}°'); + + // Test conversions back and forth + final testDegrees = 45.0; + final radians = degreesToRadians(testDegrees); + final backToDegrees = radiansToDegrees(radians); + print('\nRoundtrip Conversion:'); + print(' $testDegrees° → $radians radians → $backToDegrees°'); +} + +void testBearingToAzimuth() { + print('\n=== Testing Bearing to Azimuth Conversion ===\n'); + + final bearings = [-45.0, 0.0, 45.0, 90.0, 180.0, 270.0, 360.0, 405.0]; + + print('Bearing → Azimuth conversions:'); + for (final bearing in bearings) { + final azimuth = bearingToAzimuth(bearing); + print(' $bearing° → $azimuth°'); + } +} + +void testRounding() { + print('\n=== Testing Rounding Function ===\n'); + + final numbers = [3.14159265359, 0.123456789, 42.999999, -8.54321]; + + print('Rounding to different precision levels:'); + for (final num in numbers) { + print(' Original: $num'); + print(' 0 decimals: ${round(num, 0)}'); + print(' 2 decimals: ${round(num, 2)}'); + print(' 4 decimals: ${round(num, 4)}'); + } +} + +void testCoordinateSystemConversions() { + print('\n=== Testing Coordinate System Conversions ===\n'); + + // Test locations for conversion + final testLocations = [ + {'name': 'Null Island', 'wgs84': [0.0, 0.0]}, + {'name': 'New York', 'wgs84': [-74.006, 40.7128]}, + {'name': 'Sydney', 'wgs84': [151.2093, -33.8688]}, + {'name': 'London', 'wgs84': [-0.1278, 51.5074]}, + ]; + + print('WGS84 (lon/lat) ⟺ Web Mercator conversions:'); + for (final location in testLocations) { + final wgs84 = location['wgs84'] as List; + final mercator = toMercator(wgs84); + final backToWgs84 = toWGS84(mercator); + + print('\n ${location['name']}:'); + print(' WGS84: [${wgs84[0]}, ${wgs84[1]}]'); + print(' Mercator: [${mercator[0]}, ${mercator[1]}]'); + print(' Back to WGS84: [${backToWgs84[0]}, ${backToWgs84[1]}]'); + + // Check for roundtrip conversion accuracy + final lonDiff = (wgs84[0] - backToWgs84[0]).abs(); + final latDiff = (wgs84[1] - backToWgs84[1]).abs(); + print(' Roundtrip difference: [${lonDiff.toStringAsFixed(8)}°, ${latDiff.toStringAsFixed(8)}°]'); + } + + // Test using the convertCoordinates function + print('\nUsing convertCoordinates function:'); + for (final location in testLocations) { + final wgs84 = location['wgs84'] as List; + + // Convert WGS84 to Mercator + final mercator = convertCoordinates( + wgs84, + CoordinateSystem.wgs84, + CoordinateSystem.mercator + ); + + // Convert back to WGS84 + final backToWgs84 = convertCoordinates( + mercator, + CoordinateSystem.mercator, + CoordinateSystem.wgs84 + ); + + print('\n ${location['name']}:'); + print(' WGS84: [${wgs84[0]}, ${wgs84[1]}]'); + print(' Mercator: [${mercator[0]}, ${mercator[1]}]'); + print(' Back to WGS84: [${backToWgs84[0]}, ${backToWgs84[1]}]'); + } +} diff --git a/test/examples/transform/to_mercator_example.dart b/test/examples/transform/to_mercator_example.dart new file mode 100644 index 00000000..06a8bde1 --- /dev/null +++ b/test/examples/transform/to_mercator_example.dart @@ -0,0 +1,83 @@ +import 'package:turf/helpers.dart'; +import 'package:turf/transform.dart'; + +void main() { + print('=== Web Mercator Transformations Examples ===\n'); + + // Position conversion example + final wgs84Position = Position.of([-74.006, 40.7128, 10.0]); // New York City + print('WGS84 Position: $wgs84Position'); + + final mercatorPosition = geoToMercator(wgs84Position) as Position; + print('Mercator Position: $mercatorPosition'); + + final backToWgs84 = geoToWgs84(mercatorPosition) as Position; + print('Back to WGS84: $backToWgs84\n'); + + // Point geometry conversion + final point = Point(coordinates: Position.of([151.2093, -33.8688, 20.0])); // Sydney + print('WGS84 Point: $point'); + + final mercatorPoint = geoToMercator(point) as Point; + print('Mercator Point: $mercatorPoint'); + + final wgs84Point = geoToWgs84(mercatorPoint) as Point; + print('Back to WGS84 Point: $wgs84Point\n'); + + // LineString example + final lineString = LineString(coordinates: [ + Position.of([0, 0]), + Position.of([10, 10]), + Position.of([20, 0]) + ]); + print('WGS84 LineString: $lineString'); + + final mercatorLineString = geoToMercator(lineString) as LineString; + print('Mercator LineString: $mercatorLineString'); + + final wgs84LineString = geoToWgs84(mercatorLineString) as LineString; + print('Back to WGS84 LineString: $wgs84LineString\n'); + + // Feature example + final feature = Feature( + geometry: Point(coordinates: Position.of([139.6917, 35.6895, 5.0])), // Tokyo + properties: {'name': 'Tokyo', 'country': 'Japan'} + ); + print('WGS84 Feature: $feature'); + + // Use mutate: false to keep the original object unchanged + final mercatorFeature = geoToMercator(feature, mutate: false) as Feature; + print('Mercator Feature: $mercatorFeature'); + print('Original feature unchanged: ${feature.geometry!.coordinates != mercatorFeature.geometry!.coordinates}'); + + // Use mutate: true to modify the original object (better performance) + final clonedFeature = feature.clone(); + final mutatedFeature = geoToMercator(clonedFeature, mutate: true) as Feature; + print('Mutated Feature: $mutatedFeature'); + print('Original feature modified (when mutate=true): ${clonedFeature.geometry!.coordinates == mutatedFeature.geometry!.coordinates}\n'); + + // FeatureCollection example + final featureCollection = FeatureCollection(features: [ + Feature( + geometry: Point(coordinates: Position.of([2.3522, 48.8566])), // Paris + properties: {'name': 'Paris'} + ), + Feature( + geometry: Point(coordinates: Position.of([37.6173, 55.7558])), // Moscow + properties: {'name': 'Moscow'} + ) + ]); + print('WGS84 FeatureCollection: $featureCollection'); + + final mercatorFeatureCollection = geoToMercator(featureCollection) as FeatureCollection; + print('Mercator FeatureCollection: $mercatorFeatureCollection'); + + final wgs84FeatureCollection = geoToWgs84(mercatorFeatureCollection) as FeatureCollection; + print('Back to WGS84 FeatureCollection: $wgs84FeatureCollection\n'); + + print('=== Usage Tips ==='); + print('1. Use geoToMercator() to convert any GeoJSON object from WGS84 to Web Mercator'); + print('2. Use geoToWgs84() to convert any GeoJSON object from Web Mercator back to WGS84'); + print('3. Set mutate=true for better performance when you don\'t need to preserve the original object'); + print('4. Use as Position/Point/etc. to get the correct type back'); +} diff --git a/test/examples/transform/to_wgs84_example.dart b/test/examples/transform/to_wgs84_example.dart new file mode 100644 index 00000000..080bb220 --- /dev/null +++ b/test/examples/transform/to_wgs84_example.dart @@ -0,0 +1,103 @@ +import 'package:turf/helpers.dart'; +import 'package:turf/transform.dart'; + +void main() { + print('=== Web Mercator to WGS84 Transformations Examples ===\n'); + + // Position conversion example + final mercatorPosition = Position.of([-8237642.31, 4970241.32, 10.0]); // New York City in Mercator + print('Mercator Position: $mercatorPosition'); + + final wgs84Position = geoToWgs84(mercatorPosition) as Position; + print('WGS84 Position: $wgs84Position'); + + final backToMercator = geoToMercator(wgs84Position) as Position; + print('Back to Mercator: $backToMercator\n'); + + // Point geometry conversion + final mercatorPoint = Point(coordinates: Position.of([16830163.94, -3995519.76, 20.0])); // Sydney in Mercator + print('Mercator Point: $mercatorPoint'); + + final wgs84Point = geoToWgs84(mercatorPoint) as Point; + print('WGS84 Point: $wgs84Point'); + + final mercatorPointAgain = geoToMercator(wgs84Point) as Point; + print('Back to Mercator Point: $mercatorPointAgain\n'); + + // LineString example + final mercatorLineString = LineString(coordinates: [ + Position.of([0, 0]), + Position.of([1113195, 1118890]), + Position.of([2226390, 0]) + ]); + print('Mercator LineString: $mercatorLineString'); + + final wgs84LineString = geoToWgs84(mercatorLineString) as LineString; + print('WGS84 LineString: $wgs84LineString'); + + final mercatorLineStringAgain = geoToMercator(wgs84LineString) as LineString; + print('Back to Mercator LineString: $mercatorLineStringAgain\n'); + + // Feature example + final mercatorFeature = Feature( + geometry: Point(coordinates: Position.of([15550408.91, 4257980.73, 5.0])), // Tokyo in Mercator + properties: {'name': 'Tokyo', 'country': 'Japan'} + ); + print('Mercator Feature: $mercatorFeature'); + + // Use mutate: false to keep the original object unchanged + final wgs84Feature = geoToWgs84(mercatorFeature, mutate: false) as Feature; + print('WGS84 Feature: $wgs84Feature'); + print('Original feature unchanged: ${mercatorFeature.geometry!.coordinates != wgs84Feature.geometry!.coordinates}'); + + // Use mutate: true to modify the original object (better performance) + final clonedFeature = mercatorFeature.clone(); + final mutatedFeature = geoToWgs84(clonedFeature, mutate: true) as Feature; + print('Mutated Feature: $mutatedFeature'); + print('Original feature modified (when mutate=true): ${clonedFeature.geometry!.coordinates == mutatedFeature.geometry!.coordinates}\n'); + + // FeatureCollection example + final mercatorFeatureCollection = FeatureCollection(features: [ + Feature( + geometry: Point(coordinates: Position.of([261865.42, 6250566.48])), // Paris in Mercator + properties: {'name': 'Paris'} + ), + Feature( + geometry: Point(coordinates: Position.of([4187399.59, 7509720.48])), // Moscow in Mercator + properties: {'name': 'Moscow'} + ) + ]); + print('Mercator FeatureCollection: $mercatorFeatureCollection'); + + final wgs84FeatureCollection = geoToWgs84(mercatorFeatureCollection) as FeatureCollection; + print('WGS84 FeatureCollection: $wgs84FeatureCollection'); + + final mercatorFeatureCollectionAgain = geoToMercator(wgs84FeatureCollection) as FeatureCollection; + print('Back to Mercator FeatureCollection: $mercatorFeatureCollectionAgain\n'); + + print('=== Round-trip Conversion Accuracy ==='); + // Start with some WGS84 coordinates + final originalWgs84 = Position.of([10.0, 20.0, 30.0]); + print('Original WGS84: $originalWgs84'); + + // Convert to Mercator + final converted = geoToMercator(originalWgs84) as Position; + print('Converted to Mercator: $converted'); + + // Convert back to WGS84 + final roundTrip = geoToWgs84(converted) as Position; + print('Converted back to WGS84: $roundTrip'); + + // Calculate the difference (should be very small) + final lonDiff = (originalWgs84[0]?.toDouble() ?? 0.0) - (roundTrip[0]?.toDouble() ?? 0.0); + final latDiff = (originalWgs84[1]?.toDouble() ?? 0.0) - (roundTrip[1]?.toDouble() ?? 0.0); + print('Longitude difference: $lonDiff°'); + print('Latitude difference: $latDiff°\n'); + + print('=== Usage Tips ==='); + print('1. Use geoToWgs84() to convert any GeoJSON object from Web Mercator to WGS84'); + print('2. Use geoToMercator() to convert any GeoJSON object from WGS84 to Web Mercator'); + print('3. Set mutate=true for better performance when you don\'t need to preserve the original object'); + print('4. Use as Position/Point/etc. to get the correct type back'); + print('5. Round-trip conversions maintain high accuracy, but tiny numeric differences may occur'); +}