veni, vidi, fietsie

gpx tracks on OSM-maps with openlayers

This tutorial is a result of me growing tired of the privacynightmare that Google has become over the years. So, apart from rooting my lovely new Nokia 1 and deleting my Gmail account, I also wanted to get rid of googlemaps as a provider for the graphical illustrations of my biketravels.

If you google search on duckduckgo for "how to show gpx track on OSM", you will eventually end up at the openlayers website. There is an alternative that seems a bit more userfriendly called Leafletjs. But openlayers is totally into Node.js and I've been unthreaded-curious since I first heard about Node.js. So after ages of PHP-development I decided to seize the opportunity to get acquainted with Node for this map-gpx-project.

For this website I need a "slippy map" (a draggable map with zoomoptions) to show my GPX-tracks using different mapproviders. There's a tutorial at the openlayers site that assumes you do your developing on a linuxbox. I have one of those (as a filebackup/musicplayer) but for sentimental reasons I prefer my hacktool UltraEdit on windows 7 for development. There's a FreeBSD-testserver running on my homenetwork, so that's where I installed node and npm, the Node PackageManager, from the portscollection. The Libuv library is needed as a dependency and that's in the ports as well.

Stuff here was made with node v11.10.1, npm v6.8.0 and openlayers (OL) v5.3.0. I also assume you have something resembling a linuxbox at home and that you are a bit savvy concerning things *nix related.

I started out with this tutorial and followed up with the examples on openlayers.org. There have been some less- or undocumented pitfalls during this project, so I hope this may help other people on track leaving the alphabet holding.

the tutorial

On a *nix machine you need to create a directory with the name of your project. I used /home/osmgpx/public_html. When the source was built all files get prefixed with "public_html_", so you basically should mkdir /home/yourprojectname and cd to that directory and run all npm-commands from there to get a more sensible prefix.

When you run npm init in this directory you get a load of questions. Fill out some sensible values and just hit enter when asked for the test-script. Next run npm install ol to get the openlayer stuff and npm install --save-dev parcel-bundler to get the parcel-bundler to automagically install dependencies needed when developing. If you're not running all this as root you should sudo every here and there.

Edit package.json in your workingdirectory and make sure the scripts section looks like

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "start": "parcel index.html",
  "build": "parcel build --public-url . index.html"
},

Mind the comma after the exit 1" -bit and at the end!
You also need an index.html and index.js in this directory. Here's the sourcecode for index.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Tutorial GPX on OSM using OpenLayers</title>
    <style>
      #map {
        width: 400px;
        height: 250px;
      }
    </style>
  </head>
  <body>
    <div id="map"></div>
    <script src="./index.js"></script>
  </body>
</html>

For now an empty index.js is okay.
Next run npm start from the prompt in the directory. Npm starts a webserver running at localhost on port 1234. Localhost is a definition for the machine that the browser is running on. If you're on Windows you can trick FiFo (but not Chrome) into thinking that localhost is running on some other server by editing the hosts file which typically sits at C:\Windows\System32\drivers\etc\hosts.

To refresh the lot after editing the index-files I did a Ctrl-C and a npm start to restart the server to see the changes in the browser with F5. Npm is able to detect changes in the code by itself, but in my setup this did not work flawless. Npm checks for syntax errors when started, so check at the prompt when nothing is showing up in your browser.
When things go astray, there's clues in the console log of your browser (F12), but you probably knew that already.

In your index.html there's basically just a div with id=map and a link to the js-stuff. Not a lot is showing when you open http://localhost:1234, but in the sourcecode should be npm's index.html. The JS-file has a different name with the dirname as prefix in the source, but the (currently non-existemt) contents are read from index.js.
So let's do something about that. Open index.js in your favorite editor and add:

import 'ol/ol.css';
import {Map, View} from 'ol';
import OSM from 'ol/source/OSM.js';
import XYZ from 'ol/source/XYZ.js';
import GPX from 'ol/format/GPX.js';
import {defaults as defaultControls, FullScreen} from 'ol/control.js';
import {Tile as TileLayer, Vector as VectorLayer} from 'ol/layer.js';
import VectorSource from 'ol/source/Vector.js';
import {Stroke, Style} from 'ol/style.js';

These declarations tell which modules we'll be using in this project. We'll encounter them along the way. Note that at the prompt bundler.js is doing stuff if you save index.js.
The map instance goes next into index.js:

const map = new Map({
	controls: defaultControls().extend([
		new FullScreen()
	]),
	target: 'map',
	layers: [],
	view: new View({
		center: [0, 0],
		zoom: 0
	})
});

There's some mapattributes here.
The default mapcontrols (zoom-in, zoom-out and information buttons) are extended by adding an icon to view the map in fullscreen, which is a really awesome feature (though not working properly in IE).
"Target" states de id of the div you want to show your map in.
"Layers" is an array that will be filled later.
The "view" sets the initial center and zoomlevel of the map. These values will be changed dynamically after loading the GPX file so the defaults will do. The total map of choice will be shown using these settings, until the trackdata has fully loaded. I didn't bother to change them.
If you change the centercoordinates, keep in mind that you may need the fromLonLat functionality (currently not imported at the top of this file) to transform the coordinates to prevent the map from centering at the coast of west Africa instead of Switzerland.
Zoomlevel 0 means totally zoomed out: you get a world map.

Next the layers need to be filled. There's 2 layers needed: one for the tiles of the map and another for the track. We start with some instances for the tiled maps:


var tileSource_oSTREETm = new OSM();
var tileSource_ArcGIS = new XYZ({//==>; esri sat
	attributions: '&copy; <a href="https://server.arcgisonline.com/ArcGIS/rest/services/">ArcGIS</a>',
	url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'
});

There're two maps involved in our project, now stored in the vars tileSource_oSTREETm and tileSource_ArcGIS. OpenStreetMap is easy to get: just an instance of OSM.
Satellite-tiles from ArcGIS need the X, Y(- coordinates) and Z(oom) properties in the URL to return the appropriate piece of the puzzle, so that makes it an instance of XYZ. The url info states the location where the tiles originate from.
The attributions-option states the copyrights that are shown on the right lower corner of the map when the I-icon is clicked.

There is a ton of possible maps; have a look at the nice overview here. Just adjust the mentioned Leafletcodes to the OL format as demonstrated here with ArcGIS to use them. Thunderforest has some real nice maps, but you need a (free) apikey to show them in your hobbyproject.
That said and/or done we need to load this tile-data into the first map-layer:


map.addLayer(new TileLayer({ //==>; osm = default
	source: tileSource_oSTREETm
}));

Save your index.js. If you restart the webserver with Ctrl-C and a npm run. At the commandprompt npm imports all the necessary modules. Ignore the Cannot read property 'type' of undefined warning and do an F5 in your browser and you should see the OSM worldmap at http://localhost:1234.
If not: reread or repeat the above steps!

ol tutorial using OSM
When the map shows, it's time to add the GPX-track. Add this to index.js


var vectorSource = new VectorSource({
	url: 'https://biketory.nl/media/content/ol_tutorial.gpx',
	format: new GPX()
});

to get the GPX data transformed into a vectorsource.
There's a caveat here. I put my gpx-file in the working directory with the index-files. In the examples at openlayers.org there's relative links in the url, so mine read url: 'test.gpx'. That didn't work, and nor does ./test.gpx. So I put my GPX-track on the productionserver.
Then the next problem showed up. Loading external files in JS gives you a warning in the console (F12 in your browser): Cross-Origin Request Blocked. It points to this page about Cross-Origin requests. To allow external files being read into JS applications elsewhere you need to adjust the .htaccess file on the productionserver by adding Header set Access-Control-Allow-Origin "*". This basically tells that it's okay to send my tutorial GPX-file to JS-scripts hosted elsewhere requesting it.
Once the mapcode is on your productionserver, you can use relative url's again.

Apart from a URL there is also a style attached to the track. It has a name (Stroke),a color (RGB/HexDec/Named), a width (in pixels) and an opacity (float). We need two instances to pimp the maptrack: of Style and Stroke.


var vectorStyle = new Style({
	stroke: new Stroke({
		color: 'red',
		width: 3,
		opacity: 0.5
	})
});

Next we have to add the source and style of the track to the layers:


map.addLayer(new VectorLayer({
	source: vectorSource,
	style: vectorStyle
}));

That's all we need to get the track on the map.
Save index.js and do a Ctrl-C and a npm run at the command prompt and F5 your browser. You should see a worldmap with a tiny red blob somewhere in the Netherlands. Zoom in to find out where me, my bike and some other individuals (and their bikes) were on a sunday afternoon in 2014.

openlayers tutorial gpx osm

Next we like to scale the map to the gpx-vectorstroke. OL can do the calculating for you, but only after the track is loaded. I started out with a 3 Mb track that took some time to load. OL vectorsource has a method called getExtent(), but it returns some awkward values when called when the GPX-data is not or not yet present.
To get a fool-proof map, we're gonna check if the layers are alive and kicking. To find out we need this in index.js:

var layers = map.getLayers().getArray();

OL has a method once, which basically listens for a certain type of event. We use "change" to find the state of the vectorSource.
Since we have all layers in an array we also check if layer[1] is actually filled.
Then we can call the getExtent-method to fit the trackpart of the map into the div:

vectorSource.once('change',function(e){
	if(vectorSource.getState() === 'ready') {
		if(layers[1].getSource().getFeatures().length > 0) {
			map.getView().fit(vectorSource.getExtent());
		}
	}
});

Save your index.js file and do a Ctrl-C and a npm run at the command prompt and a F5 in your browser. You should see this:

ol-tutorial gpx track on osm

Since google has a satellite-option we wanna have one too! Add the following selectbox to index.html below the map-div and above the scriptsource:

<select name="tilesource" id="tilesource">
   <option value='oSTREETm' selected="selected">OpenStreetMap</option>
   <option value='ArcGIS'>Satellite</option>
</select>

Then we need to tell index.js to listen to what happens in the html-file. So let's get the element and add an event-listener. In between is the onChange-method that replaces the mapsource in layer[0] when another map gets chosen. Add this to your index.js:

var select = document.getElementById('tilesource');

function onChange() {
	var newMap = select.value;
	if(newMap) {
		var tileSource	='tileSource_' + newMap;
		var mapSource	= eval(tileSource);
		var mapLayer0	= layers[0];
		mapLayer0.setSource(mapSource);
	}
}

select.addEventListener('change', onChange);

Save your index.js file and do a Ctrl-C and a npm run at the command prompt and a F5 in your browser and try the ArcGIS satellite-source. Things should look like this:

osm gpx track arcGIS satellite view openlayers tutorial

If you zoom in on your project you may see the the touristtrap called Kinderdijk where they deliberately forgot to remove some windmills so Japanese tourists can block the adjoining bikepaths.

If things look okay you can build a production bundle of your application by running npm run build.
Npm creates a /dist directory with an index.html. Check to which css- and js file it links. You can change the name of those linked files to yourprojectname.js and yourprojectname.css and just change the links in index.html.
If you move just those three files (index.html. yourprojectname.js and yourprojectname.css) to your old-fashioned regular threaded webserver, you should have a working setup. The other files in the /dist dir are not needed.

Since <link>-tags are preferred in the <head>-section, but are tolerated elsewhere according to the HTML-specs, it's possible to just insert the necessary code in the body of another webpage. This means that there's no longer any need to use iframes showing complete html-pages as I used to do with Googlemaps.

Well. You've got the basics. I turned this into a module for use in my CMS by providing the tracksettings (url, size, color) for the JS in the html-bit. You'll probably be able to figure that part out for yourself.

Just one final remark: if the zoomout- and fullscreen-icon are garbled, your webserver is not serving UTF-8. Check your php.ini (there was a legacy charset settting on 1 of my servers).

I sincerely hope this page saves some time for other privacy-aware googledeniers, opensource-enthousiasts, cyclists and developers and especially those few readers who fit in all those categories. :-)

Zipped source is here. Demo here.