Skip to content

towsg84 and tomercator unit [Improve helpers.dart with better documentation and code style] #219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
4 changes: 2 additions & 2 deletions Progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- [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)
117 changes: 111 additions & 6 deletions lib/src/helpers.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:math';
import 'package:geotypes/geotypes.dart';

enum Unit {
meters,
Expand Down Expand Up @@ -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 = <Unit, num>{
Expand Down Expand Up @@ -100,17 +122,19 @@ 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");
}
return radians * factor;
}

/// 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) {
Expand All @@ -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));
}
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand All @@ -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]);
}
1 change: 1 addition & 0 deletions lib/turf.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
115 changes: 41 additions & 74 deletions test/components/helpers_test.dart
Original file line number Diff line number Diff line change
@@ -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));
});
});
}
57 changes: 57 additions & 0 deletions test/examples/helpers/README.md
Original file line number Diff line number Diff line change
@@ -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 |
Loading