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
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 112 additions & 6 deletions lib/src/helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,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 +121,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 +143,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 +172,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 +186,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 +206,83 @@ num convertArea(num area,

return (area / startFactor) * finalFactor;
}


/// Converts coordinates from one system to another.
///
/// Valid systems: [CoordinateSystem.wgs84], [CoordinateSystem.mercator]
/// Returns: [List] of coordinates in the target system
List<double> convertCoordinates(
List<num> coord,
CoordinateSystem fromSystem,
CoordinateSystem toSystem
) {
if (fromSystem == toSystem) {
return coord.map((e) => e.toDouble()).toList();
}

if (fromSystem == CoordinateSystem.wgs84 && toSystem == CoordinateSystem.mercator) {
return toMercator(coord);
} else if (fromSystem == CoordinateSystem.mercator && toSystem == CoordinateSystem.wgs84) {
return toWGS84(coord);
} else {
throw Exception("Unsupported coordinate system conversion: $fromSystem to $toSystem");
}
}

/// Converts a WGS84 coordinate to Web Mercator.
///
/// Valid inputs: [List] of [longitude, latitude]
/// Returns: [List] of [x, y] coordinates in meters
List<double> toMercator(List<num> coord) {
if (coord.length < 2) {
throw Exception("coordinates must contain at least 2 values");
}

// Use the earth radius constant for consistency

// Clamp latitude to avoid infinite values near the poles
final longitude = coord[0].toDouble();
final latitude = max(min(coord[1].toDouble(), 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);

return [clampedX, clampedY];
}

/// Converts a Web Mercator coordinate to WGS84.
///
/// Valid inputs: [List] of [x, y] in meters
/// Returns: [List] of [longitude, latitude] coordinates
List<double> toWGS84(List<num> coord) {
if (coord.length < 2) {
throw Exception("coordinates must contain at least 2 values");
}

// Use the earth radius constant for consistency

// Clamp inputs to valid range
final x = max(min(coord[0].toDouble(), mercatorLimit), -mercatorLimit);
final y = max(min(coord[1].toDouble(), 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);

return [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
95 changes: 95 additions & 0 deletions test/components/helpers_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:math';
import 'package:test/test.dart';
import 'package:turf/helpers.dart';
import 'package:geotypes/geotypes.dart';

void main() {
test('radiansToLength', () {
Expand Down Expand Up @@ -77,4 +78,98 @@ void main() {
expect(convertArea(100, Unit.meters, Unit.feet), equals(1076.3910417));
expect(convertArea(100000, Unit.feet), equals(0.009290303999749462));
});

test('toMercator', () {
// Test with San Francisco coordinates
final wgs84 = [-122.4194, 37.7749];
final mercator = toMercator(wgs84);

// Expected values (approximate)
final expectedX = -13627665.0;
final expectedY = 4547675.0;

// Check conversion produces results within an acceptable range
expect(mercator[0], closeTo(expectedX, 50.0));
expect(mercator[1], closeTo(expectedY, 50.0));

// Test with error case
expect(() => toMercator([]), throwsException);
});

test('toWGS84', () {
// Test with San Francisco Mercator coordinates
final mercator = [-13627695.092862014, 4547675.345836067];
final wgs84 = toWGS84(mercator);

// Expected values (approximate)
final expectedLon = -122.42;
final expectedLat = 37.77;

// Check conversion produces results within an acceptable range
expect(wgs84[0], closeTo(expectedLon, 0.01));
expect(wgs84[1], closeTo(expectedLat, 0.01));

// Test with error case
expect(() => toWGS84([]), throwsException);
});

test('Round-trip conversion WGS84-Mercator-WGS84', () {
// Test coordinates for various cities
final cities = [
[-122.4194, 37.7749], // San Francisco
[139.6917, 35.6895], // Tokyo
[151.2093, -33.8688], // Sydney
[-0.1278, 51.5074], // London
];

for (final original in cities) {
final mercator = toMercator(original);
final roundTrip = toWGS84(mercator);

// Round-trip should return to the original value within a small delta
expect(roundTrip[0], closeTo(original[0], 0.00001));
expect(roundTrip[1], closeTo(original[1], 0.00001));
}
});

test('convertCoordinates', () {
// Test WGS84 to Mercator conversion
final wgs84 = [-122.4194, 37.7749]; // San Francisco
final mercator = convertCoordinates(
wgs84,
CoordinateSystem.wgs84,
CoordinateSystem.mercator
);

// Should match toMercator result
final directMercator = toMercator(wgs84);
expect(mercator[0], equals(directMercator[0]));
expect(mercator[1], equals(directMercator[1]));

// Test Mercator to WGS84 conversion
final backToWgs84 = convertCoordinates(
mercator,
CoordinateSystem.mercator,
CoordinateSystem.wgs84
);

// Should match toWGS84 result and be close to original
expect(backToWgs84[0], closeTo(wgs84[0], 0.00001));
expect(backToWgs84[1], closeTo(wgs84[1], 0.00001));

// Test same system conversion (should return same values)
final sameSystem = convertCoordinates(
wgs84,
CoordinateSystem.wgs84,
CoordinateSystem.wgs84
);
expect(sameSystem[0], equals(wgs84[0]));
expect(sameSystem[1], equals(wgs84[1]));

// Test error case
expect(
() => convertCoordinates([], CoordinateSystem.wgs84, CoordinateSystem.mercator),
throwsException
);
});
}
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