One really quick thing I want to write about today is OpenLayers and a common pitfall I come across when writing mapping code. I originally came across this when writing code for a GovHack project, but I keep forgetting and wasting many hours of my life to it, so hopefully this time now that I’ve got it down, I’ll be good.

Inevitably, when I plot data on a map, I want it in a web browser, and I’ll want a street map for context (generally OpenStreetMaps), as well as a layer on top with whatever I want to render - maybe a heatmap of population density, or whatever. So I use OpenLayers, a neat Javascript library for layering multiple different location data types with a plethora of features.

When you start plotting your vector layer, you’ll struggle to well…see what you’re plotting. Invariably, everything will end up in the ocean, somewhere near Africa.

This is down to different data sources using different map projections - since we’re trying to coerce fundamentally three-dimensional information (the Earth is a three-dimensional object!) into a two-dimensional space, we use map projections to manage the loss of information in a consistent, predictable way.

Many data sources I’ve used for location-based statistics (mostly Australian government datasources) use a different map projection format (EPSG:4326) to the one used by OpenStreetMaps (EPSG:3857). Note that the GTFS transit specification also uses EPSG:4326 for public transit route maps - a detail that was hard to find since the GTFS docs refer to it as “WGS 84 latitude / longitude”.

OpenLayers does not automatically reconcile these projections for you, and I’m pretty sure it uses the bottom-most layer as the base projection, so any other layers need to be following the same projection. This is why we end up with data points in the middle of the ocean, and not overlaid correctly onto OpenStreetMaps.

The solution is to transform your latitudes and longitudes (depending on how you’re pulling them in) from the map projection your data source used, into the one that is used by the street map provider.

There’s a few different ways to do this but I find the best way is creating your OpenLayers geometry objects, and then using the built-in transform method.

Here’s a quick example of how you could do it:

// Example imports features from a GeoJSON blob.

const features = new ol.format.GeoJSON().readFeatures(geoJsonObj);

features.forEach(el => {
  el.getGeometry().transform('EPSG:4326', 'EPSG:3857')
})

const vectorSource = new ol.source.Vector({ features });

const vectorLayer = new ol.layer.Vector({
  source: vectorSource,
});

// Now you can do whatever you want with your vector layer...like put it on a map

const map = new ol.Map({
  layers: [
    new ol.layer.Tile({
      source: new ol.source.OSM()
    }),
    vectorLayer
  ],
  target: 'map',
  controls: ol.control.defaults({
    attributionOptions: {
      collapsible: false
    }
  }),
  view: new ol.View({
    center: ol.proj.fromLonLat([151.1021473, -33.9663161]),
    zoom: 12
  })
});

Once you’ve got the projection transform working, it all should fall into place.

For further reading on map projections, GeospatialPython did a really good writeup that I found whilst researching some different GIS conversion tools - check it out here.

You could also take a look at the OpenLayers code for QuestionTime, the project that I co-wrote which makes extensive use of OpenLayers and where I originally worked out how to do correct map projections from disparate data sources.