Read this article here: https://blocks.roadtolarissa.com/oliverheilig/29e494c33ef58c6d5839
-
-
Save oliverheilig/29e494c33ef58c6d5839 to your computer and use it in GitHub Desktop.
Note: I'm well aware that i am using "Web Mercator" in this article. Web-Mercator is not exactly conformal and thus not even Mercator. For more infos about this particular issue and a brief history of Web-Mercator read this document. This page explains the general implications different projections have, with focus on software development for logistics applications.
From time to time i am involved in discussions about the pitfalls of the Mercator map projection. Yes, the Mercator projection was invented 1569 for crossing an ocean and not for internet maps. And yes, on a global scale the Mercator projection grotesquely distorts the sizes of nations and continents. You can also argue that this is a discrimination against the southern hemisphere.
<iframe width="950" height="540" src="https://www.youtube.com/embed/vVX-PrBRtTY?start=61" frameborder="0" allowfullscreen></iframe>Sometimes developers claim they don't make any projection to avoid distortions. What they actually mean is that they render the latitude and longitude angles of WGS84 directly to the screen. But this "unprojected" projection implies you are handling angles on a sphere like they were points on a plane, and this is also a projection, called equirectangular projection (or in french "plate carrée" and german "Plattkarte"). Like any other map projection the equirectangular projection has specific properties, but it lacks one important property: the conformality.
“Many of the most common and most important map projections are conformal or orthomorphic ... in that normally the shape of every small feature of the map is shown correctly... An important result of conformality is that relative angles at each point are correct, and the local scale in every direction around any one point is constant.” John P. Snyder, Map Projections Used by the U. S. Geological Survey, 1983
This is why i always prefer the mercator projection over any non-conformal projection for visualization and computations. The conformality is much more important for logistics applications than comparing the relative size of nations and continents. I also recommend transforming your points to Mercator before doing any geometric computations, because then:
- Angles are correct
- Shapes of structures are preserved
- You can compare distances locally with pythagoras
- You can approximate the geographic distance when multiplying the pythagoras-distance by
cos(lat)
This implies that you can render a geographic circle as a circle on the map canvas. Just divide the mercator radius by cos(center.lat)
. You see the benefit in the sample on top: The circle of the Karlsruhe "fan" is a circle in the Mercator map, but not in the "unprojected" map.
Plus, you can use 2D geometric algorithms on mercator points, like the computation of voronoi regions.
<iframe width="950" height="540" src="https://oliverheilig.github.io/voronoi-territories/" frameborder="0" allowfullscreen></iframe>These are the formulas to project a WGS (lng/lat) point to and from a Mercator point. This is the "Web" version, and we neglect the inaccuracy of Web Mercator, as i mentioned before. The earth radius (6378137.0)
is the major axis of WGS84. There's also a variation that uses the mean between major and minor axis (6371000.0)
, as used by my company's web services.
public static Point Wgs2SphereMercator(Point point)
{
return new Point {
X = 6378137.0 * point.X * Math.PI / 180.0,
Y = 6378137.0 * Math.Log(Math.Tan(Math.PI / 4.0 + point.Y * Math.PI / 360.0))
};
}
public static Point SphereMercator2Wgs(Point point)
{
return new Point {
X = (180.0 / Math.PI) * (point.X / 6378137.0),
Y = (360 / Math.PI) * (Math.Atan(Math.Exp(point.Y / 6378137.0)) - (Math.PI / 4))
};
}
- PTV xServer internet - Developer components hosted in the cloud to enhance your application
- Leaflet - an open-source JavaScript library for mobile-friendly interactive maps
- Leaflet.Sync - Synchronized view of two maps
- Voronoi-Territories - with D3 and Leaflet
- YouTube - "The West Wing" Season 2 Episode 16
- Noel Zinn - Web Mercator: Non-Conformal, Non-Mercator
- Sarah E. Battersby - Implications of Web Mercator and Its use in Online Mapping
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> | |
<html> | |
<head> | |
<title>Mercator vs Equirectangular</title> | |
<meta content="yes" name="apple-mobile-web-app-capable"> | |
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.0-beta.2.rc.2/leaflet.css" /> | |
<style> | |
body { | |
padding: 0; | |
margin: 0; | |
} | |
html, body, table, | |
#map, #map2 { | |
height: 100%; | |
} | |
</style> | |
</head> | |
<body> | |
<table width="100%" border="0"> | |
<tr> | |
<td width="50%" align="center"> | |
<h3>Mercator</h3> | |
</td> | |
<td colspan="2" align="center"> | |
<input id="radiusInput" type="range" name="points" min="0" max="1000"> | |
<div id="radiusText"></div> | |
</td> | |
<td width="50%" align="center"> | |
<h3>Equirectangular</h3> | |
</td> | |
</tr> | |
<tr> | |
<td colspan="2" id="map1" width="50" /> | |
<td colspan="2" id="map2" width="50" /> | |
</tr> | |
</table> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.0-beta.2.rc.2/leaflet.js"></script> | |
<script src="./L.Map.Sync.js"></script> | |
<script> | |
// using the xserver-internet WMS adapter | |
var xMapWmsUrl = 'https://api-test.cloud.ptvgroup.com/WMS/WMS'; | |
var xMapAttribution = '<a target="_blank" href="https://www.ptvgroup.com">PTV<\/a>, TOMTOM'; | |
var layerOptions = { | |
maxZoom: 19, minZoom: 0, opacity: 1.0, noWrap: false, | |
layers: 'xmap-gravelpit-bg', format: 'image/png', transparent: false, attribution: xMapAttribution | |
}; | |
// center and radius for our geographic circle | |
var center = new L.LatLng(49.01405, 8.4044); | |
var radius = 435.0; | |
// init map2 - mercator | |
var map1 = new L.Map('map1', { | |
crs:L.CRS.EPSG3857 | |
}).setView(center, 14); | |
new L.TileLayer.WMS(xMapWmsUrl, layerOptions).addTo(map1); | |
// init map2 - equirectangular | |
var map2 = new L.Map('map2', { | |
crs:L.CRS.EPSG4326 | |
}).setView(center, 14); | |
new L.TileLayer.WMS(xMapWmsUrl, layerOptions).addTo(map2); | |
// add the geographic radius | |
var c1 = L.circle( center, radius).addTo(map1); | |
var c2 = L.circle( center, radius).addTo(map2); | |
// sync the map viewports | |
map1.sync(map2); | |
map2.sync(map1); | |
// setting the radius slider | |
var ri = document.getElementById("radiusInput"); | |
var rt = document.getElementById("radiusText"); | |
ri.value = Math.sqrt(radius); | |
rt.innerHTML = radius + " m"; | |
ri['onchange'] = ri['oninput'] = function() { | |
radius = ri.value * ri.value; | |
rt.innerHTML = radius + " m"; | |
c1.setRadius(radius); | |
c2.setRadius(radius); | |
}; | |
// reset center on click | |
map1.on('click', function(e) {centerCircles(e.latlng);}); | |
map2.on('click', function(e) {centerCircles(e.latlng);}); | |
function centerCircles(latlng) | |
{ | |
c1.setLatLng(latlng); | |
c2.setLatLng(latlng); | |
} | |
</script> | |
</body> | |
</html> |
/* | |
* Extends L.Map to synchronize the interaction on one map to one or more other maps. | |
* oliverheilig: had to modify it heavily to work for maps with different projections. | |
*/ | |
(function () { | |
'use strict'; | |
L.Map = L.Map.extend({ | |
sync: function (map, options) { | |
this._initSync(); | |
options = options || {}; | |
// prevent double-syncing the map: | |
var present = false; | |
this._syncMaps.forEach(function (other) { | |
if (map === other) { | |
present = true; | |
} | |
}); | |
if (!present) { | |
this._syncMaps.push(map); | |
} | |
if (!options.noInitialSync) { | |
map.setView(this.getCenter(), this.getZoom(), { | |
animate: false, | |
reset: true | |
}); | |
} | |
return this; | |
}, | |
// unsync maps from each other | |
unsync: function (map) { | |
var self = this; | |
if (this._syncMaps) { | |
this._syncMaps.forEach(function (synced, id) { | |
if (map === synced) { | |
self._syncMaps.splice(id, 1); | |
} | |
}); | |
} | |
return this; | |
}, | |
// Checks if the maps is synced with anything | |
isSynced: function () { | |
return (this.hasOwnProperty('_syncMaps') && Object.keys(this._syncMaps).length > 0); | |
}, | |
// overload methods on originalMap to replay on _syncMaps; | |
_initSync: function () { | |
if (this._syncMaps) { | |
return; | |
} | |
var originalMap = this; | |
this._syncMaps = []; | |
L.extend(originalMap, { | |
setView: function (center, zoom, options, sync) { | |
if (!sync) { | |
originalMap._syncMaps.forEach(function (toSync) { | |
toSync.setView(center, zoom, options, true); | |
}); | |
} | |
return L.Map.prototype.setView.call(this, center, zoom, options); | |
} | |
}); | |
originalMap.on('zoomend', function () { | |
originalMap._syncMaps.forEach(function (toSync) { | |
toSync.setView(originalMap.getCenter(), originalMap.getZoom(), { | |
animate: false, | |
reset: false | |
}, true); | |
}); | |
}, this); | |
originalMap.dragging._draggable._updatePosition = function () { | |
L.Draggable.prototype._updatePosition.call(this); | |
var self = this; | |
originalMap._syncMaps.forEach(function (toSync) { | |
toSync.setView(originalMap.getCenter(), originalMap.getZoom(), { | |
animate: false, | |
reset: false | |
}, true); | |
}); | |
}; | |
} | |
}); | |
})(); |