Skip to content

Commit bd12357

Browse files
authored
Implement Web Mercator (#67)
1 parent 2f45a9e commit bd12357

File tree

4 files changed

+115
-0
lines changed

4 files changed

+115
-0
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,18 @@ maximal efficiency when performing multiple `transform`s. The transformation can
419419
be inverted with `inv` to perform the reverse transformation with respect to the
420420
same origin.
421421

422+
#### Web Mercator support
423+
424+
We support the Web Mercator / Pseudo Mercator projection with the
425+
`WebMercatorfromLLA` and `LLAfromWebMercator` transformations for
426+
interoperability with many web mapping systems. The scaling of the
427+
northing and easting is defined to be meters at the Equator, the same as how
428+
proj handles this (see https://proj.org/operations/projections/webmerc.html ).
429+
430+
If you need to deal with web mapping tile coordinate systems (zoom levels and
431+
pixel coordinates, etc) these could be added by composing another
432+
transformation on top of the web mercator projection defined in this package.
433+
422434
#### Composed transformations
423435

424436
Many other methods are defined as convenience constructors for composed

src/Geodesy.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export
4242
UTMfromLLA, LLAfromUTM, UTMfromECEF, ECEFfromUTM, ENUfromUTM, UTMfromENU,
4343
UTMZfromLLA, LLAfromUTMZ, UTMZfromECEF, ECEFfromUTMZ, ENUfromUTMZ, UTMZfromENU,
4444
UTMZfromUTM, UTMfromUTMZ,
45+
WebMercatorfromLLA, LLAfromWebMercator,
4546

4647
# Datum transformations
4748
datum_shift_ECEF,
@@ -55,6 +56,7 @@ include("datums.jl")
5556
include("points.jl")
5657
include("transverse_mercator.jl")
5758
include("polar_stereographic.jl")
59+
include("web_mercator.jl")
5860
include("transformations.jl")
5961
include("conversion.jl")
6062
include("distances.jl")

src/web_mercator.jl

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""
2+
WebMercatorfromLLA(datum=wgs84)
3+
4+
Convert from LLA to Web Mercator / Pseudo Mercator, following the convention of
5+
proj, which uses a scaling factor of the semi-major axis of the ellipsoid
6+
https://proj.org/operations/projections/webmerc.html.
7+
8+
!!! warning
9+
For web mapping applications this projection is ubiquitous due to its
10+
simplicity of implementation, but this simplicity gives rise to poor
11+
mathematical properties: it doesn't preserve angles away from the equator.
12+
Other projections should be preferred if possible, and especially when
13+
measurement is important. See
14+
https://earth-info.nga.mil/GandG/wgs84/web_mercator/(U)%20NGA_SIG_0011_1.0.0_WEBMERC.pdf
15+
for an extended discussion.
16+
"""
17+
struct WebMercatorfromLLA <: Transformation
18+
el::Ellipsoid
19+
end
20+
21+
WebMercatorfromLLA(d::Datum=wgs84) = WebMercatorfromLLA(ellipsoid(d))
22+
23+
"""
24+
LLAfromWebMercator
25+
26+
Inverse of WebMercatorfromLLA — see the docs for that transformation.
27+
"""
28+
struct LLAfromWebMercator <: Transformation
29+
el::Ellipsoid
30+
end
31+
32+
LLAfromWebMercator(d::Datum=wgs84) = LLAfromWebMercator(ellipsoid(d))
33+
34+
35+
function (trans::WebMercatorfromLLA)(lla::LLA)
36+
xy = trans(LatLon(lla.lat, lla.lon))
37+
SA[xy[1], xy[2], lla.alt]
38+
end
39+
40+
function (trans::WebMercatorfromLLA)(ll::LatLon)
41+
lat_lim = 85.06 # according to https://epsg.io/3857
42+
if abs(ll.lat) > lat_lim
43+
throw(ArgumentError("Exceeded Web Mercator latitude bounds"))
44+
end
45+
x, y = web_mercator_forward(ll.lat, ll.lon, trans.el.a)
46+
SA[x, y]
47+
end
48+
49+
function (trans::LLAfromWebMercator)(point::AbstractVector)
50+
x = point[1]
51+
y = point[2]
52+
lat, lon = web_mercator_reverse(x, y, trans.el.a)
53+
if length(point) == 2
54+
LatLon(lat, lon)
55+
elseif length(point) == 3
56+
LLA(lat, lon, point[3])
57+
else
58+
throw(ArgumentError("Expected input vector of length 2 or 3"))
59+
end
60+
end
61+
62+
Base.inv(trans::WebMercatorfromLLA) = LLAfromWebMercator(trans.el)
63+
Base.inv(trans::LLAfromWebMercator) = WebMercatorfromLLA(trans.el)
64+
65+
# Web / Psuedo Mercator. Following the scaling convention of proj, the scaling
66+
# will be set to the ellipsoid semi-major axis.
67+
# https://proj.org/operations/projections/webmerc.html
68+
function web_mercator_forward(lat, lon, scaling)
69+
x = scaling * deg2rad(lon)
70+
y = scaling * log(tand((90 + lat)/2))
71+
(x,y)
72+
end
73+
74+
function web_mercator_reverse(x, y, scaling)
75+
lon = rad2deg(x / scaling)
76+
lat = 2 * atand(exp(y / scaling)) - 90
77+
(lat,lon)
78+
end

test/transformations.jl

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,29 @@
7373
@test inv(enu_ecef) == ecef_enu
7474
@test inv(ecef_enu) == enu_ecef
7575

76+
# Web Mercator
77+
points = [LLA(0, 0, 0),
78+
LLA(49, 2, 0),
79+
LLA(-19, -128, 4),
80+
LLA(36, -77, 6)]
81+
# Reference generated using Proj4
82+
# [Proj4.transform(Proj4.Projection("+proj=longlat +datum=WGS84"),
83+
# Proj4.Projection("+proj=webmerc +datum=WGS84"),
84+
# [point.lon, point.lat, point.alt]) for point in points]
85+
points_wm = [[0.0, 0.0, 0.0],
86+
[222638.98158654713, 6.274861394006577e6, 0.0],
87+
[-1.4248894821539015e7, -2.1549359150858945e6, 4.0],
88+
[-8.571600791082064e6, 4.300621372044271e6, 6.0]]
89+
wm_lla = WebMercatorfromLLA()
90+
lla_wm = LLAfromWebMercator()
91+
@testset "WebMercator" for (lla,wm) in zip(points, points_wm)
92+
@test wm_lla(lla) wm
93+
@test lla_wm(wm) lla
94+
ll = LatLon(lla.lat, lla.lon)
95+
wm_2d = wm[1:2]
96+
@test wm_lla(ll) wm_2d
97+
@test lla_wm(wm_2d) ll
98+
end
7699

77100
# Composed transformation functions
78101
@test LLAfromENU(ecef, wgs84) == LLAfromECEF(wgs84) ECEFfromENU(ecef, wgs84)

0 commit comments

Comments
 (0)