Dirty Reprojectors
#map-virtue {
height: 600px;
margin-bottom: 2rem;
}
.mapboxgl-ctrl-top-right .mapboxgl-ctrl {
margin: 10px 2rem 0 0;
}
The Robinson projection in Mapbox GL
We’re open sourcing a way to pre-process geometries for use in Mapbox GL, to produce maps that render something besides the standard Web Mercator projection.
Almost all web mapping libraries render maps using Web Mercator, making an assumption that you generally can’t change out-of-the-box. This has advantages, but it posed a real challenge for us when we set out to build the Washington Post’s live election results map, where using the Albers USA projection was an important requirement. To meet that requirement, we built a pipeline to pre-process geometries.
We started with our standard WGS 84 (longitude/latitude) coordinate geojson files and projected those files into our desired projection. Next, we scaled the result to fit within the Web Mercator coordinate system.
We took the scaled files and reverse-projected them back into WGS 84, before converting them to mbtiles and uploading the whole thing to Mapbox Studio.
We used Mike Bostock’s d3-geo and d3-geo-projection libraries to do the heavy lifting on the rather tricky projection math.
To show some of the projections you can achieve with this method, we built the Dirty Reprojectors App. The site includes the long list of projections d3 comes bundled with, and will also accept and project your own geojson files.
For greater control over projection variables like parallels and center points, use the Dirty Reprojectors CLI instead. There you can tweak the projection code as needed.
Caveats
After reprojecting geometries, the actual longitude/latitude coordinates will not be accurate. This complicates tasks like reverse geocoding.
Bounds are also tricky. Depending on the projection, a bounding box (rectangle) in longitutde/latitude coordinates may no longer look like one once it’s reprojected. Functions like map.fitBounds() where the map fills the space of its bounds can take more work to get right. Our work-around was to calculate and save the post-projection bounding box for each US state, instead of using the “real” bounding box coordinates.
We’re always looking for novel ways to solve problems. If this helps you solve a problem, we’d love to hear about it. You can always reach out to us on Twitter or Github.
mapboxgl.accessToken = 'pk.eyJ1IjoiZGV2c2VlZCIsImEiOiJnUi1mbkVvIn0.018aLhX0Mb0tdtaT2QNe2Q';
var map = new mapboxgl.Map({
container: 'map-virtue',
style: 'mapbox://styles/devseed/civlnnvyf000y2imxc4xqimcx',
center: [0, -10],
zoom: 1,
minZoom: 1
});
map.addControl(new mapboxgl.NavigationControl());
map.scrollZoom.disable();
var hoverSource = 'composite:hover';
// duplicate the source as a hover source
function onStyleLoad (e) {
var style = map.getStyle()
if (style.sources && style.sources.composite) {
map.addSource(hoverSource, Object.assign({}, style.sources.composite));
map.off('style.load', onStyleLoad);
}
}
// once the newly duped hover source is loaded,
// reset the map style but alter the hover layer to point to new source
// then assign the mousemove listeners
function onSourceLoad (e) {
if (e.source.id === hoverSource) {
var style = map.getStyle()
var hover = style.layers.find(function (d) {
return d.id === 'Country-Fill-Hover';
});
hover.source = 'composite:hover';
map.setStyle(style);
map.on('mousemove', function(e) {
var features = map.queryRenderedFeatures(e.point, { layers: ['Country-Fill'] });
if (features.length) {
map.setFilter('Country-Fill-Hover', ['==', 'admin', features[0].properties.admin]);
} else {
map.setFilter('Country-Fill-Hover', ['==', 'admin', '']);
}
});
map.on('mouseout', function() {
map.setFilter('Country-Fill-Hover', ['==', 'admin', '']);
});
map.off('source.load', onSourceLoad);
}
}
map.on('style.load', onStyleLoad);
map.on('source.load', onSourceLoad);