Skip to content

To mercator and to wgs84 function #230

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 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
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]);
}
120 changes: 120 additions & 0 deletions lib/src/to_mercator.dart
Original file line number Diff line number Diff line change
@@ -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,
]);
}
Loading