OpenStreetMaps helpers

Code snippet

var map;
var map_styles;
var map_overlay;
var map_interaction;
var map_feature_array;
var map_default_zoomlevel;
var map_txt_start;
var map_txt_direction;
var map_marker;
var map_dash_spacer;
var $map_popup;

//=== initialize the map ===========================================================================
function initMap(mapid) {
    setMapStyles();

    //mapid cannot be null
    if (mapid == null) {
        mapid = '';
    }

    //check if the map exists
    var $htmlmap = $('#map' + mapid);
    if ($htmlmap.length === 0) {
        return;
    }

    //declare some variables
    map_default_zoomlevel = 7;
    map_txt_start = 'Start';
    map_txt_direction = 'Direction';
    map_dash_spacer = ' - ';
    $map_popup = $('#map-popup' + mapid);
    var map_zoomlevel = map_default_zoomlevel;
    var map_mapcenter = cmsScriptVariables.dutch_map_center;
    var controls = [new ol.control.Zoom()];
    var tooltips = true;

    //check for data attribute values
    if ($htmlmap.data('zoom') === false) {
        controls = [];
    }
    if ($htmlmap.data('nott') === true) {
        tooltips = false;
    }
    if ($htmlmap.data('center') != null) {
        map_mapcenter = $htmlmap.data('center');
    }
    if ($htmlmap.data('zoomlevel') != null) {
        map_zoomlevel = parseInt($htmlmap.data('zoomlevel'));
    }

    //create the map object
    map = new ol.Map({
        target: 'map' + mapid,
        layers: [
            new ol.layer.Tile({
                source: new ol.source.OSM(),
                name: 'baselayer'
            })
        ],
        view: new ol.View({
            center: ol.proj.fromLonLat(map_mapcenter),
            zoom: map_zoomlevel,
            maxZoom: 18,
            constrainResolution: true
        }),
        controls: controls,
        interactions: ol.interaction.defaults({
            mouseWheelZoom: false
        })
    });

    //set the opacity of the tooltip to normal (otherwise it is visible before map init)
    $('.ol-popup').css('opacity', '1');

    //create an overlay to anchor the hover popup to the map
    if (tooltips) {
        addMapTooltips();
    }

    //bind keyboard to control the map if there is only one
    if ($('.ol-map-container').length === 1) {
        addMapKeyboardControls();
    }

    //svg images in zoom buttons
    $('.ol-zoom-in').html(getSvgIcon('plus'));
    $('.ol-zoom-out').html(getSvgIcon('minus'));

    //add the context menu
    createContextMenu();

    //if you want to enable zoom with scroll wheel and/or remove zoom controls from the map, use:
    //map = new ol.Map({
    //    controls: [],
    //    interactions: ol.interaction.defaults({ mouseWheelZoom: true })
    //});

    //add a custom style layer to the map
    //olms.apply(map, 'https://api.maptiler.com/maps/basic/style.json?key=xxxxx');
}


//=== set the map styles for gpx tracks ============================================================
function setMapStyles() {

    //available colors: black, blue, green, grey, magenta, orange, purple, red, white, yellow
    map_styles = {
        'track-black': new ol.style.Style({
            stroke: new ol.style.Stroke({
                color: '#000000',
                width: 3
            }),
        }),
        'track-blue': new ol.style.Style({
            stroke: new ol.style.Stroke({
                color: '#0f75bc',
                width: 3
            }),
        }),
        'track-green': new ol.style.Style({
            stroke: new ol.style.Stroke({
                color: '#308c07',
                width: 3
            }),
        }),
        'track-grey': new ol.style.Style({
            stroke: new ol.style.Stroke({
                color: '#999999',
                width: 3
            }),
        }),
        'track-magenta': new ol.style.Style({
            stroke: new ol.style.Stroke({
                color: '#c5007e',
                width: 3
            }),
        }),
        'track-orange': new ol.style.Style({
            stroke: new ol.style.Stroke({
                color: '#f49b00',
                width: 3
            }),
        }),
        'track-purple': new ol.style.Style({
            stroke: new ol.style.Stroke({
                color: '#566895',
                width: 3
            }),
        }),
        'track-red': new ol.style.Style({
            stroke: new ol.style.Stroke({
                color: '#bf1616',
                width: 3
            }),
        }),
        'track-white': new ol.style.Style({
            stroke: new ol.style.Stroke({
                color: '#ffffff',
                width: 3
            }),
        }),
        'track-yellow': new ol.style.Style({
            stroke: new ol.style.Stroke({
                color: '#f4e600',
                width: 3
            }),
        }),
    };
}


//=== create an overlay to anchor the hover popup to the map =======================================
function addMapTooltips() {
    //create a mew overlay
    map_overlay = new ol.Overlay({
        element: document.getElementById($map_popup.prop('id')),
        autoPan: {
            animation: {
                duration: 250,
            }
        }
    });

    //add the overlay to the map
    map.addOverlay(map_overlay);

    //add a mousemove function to the map to detect a marker and if found show popup
    map.on('pointermove', function (e) {
        var feature = map.forEachFeatureAtPixel(e.pixel, function (feat, layer) {
            return feat;
        });

        //get the current position of the mouse in pixels relative to the map div container
        //console.log(e.pixel)

        //show the marker info if there is a name property and it has contents
        if (feature && typeof feature.get('name') != 'undefined' && feature.get('name') != null && feature.get('name') !== '') {
            $map_popup.html(feature.get('name'));
            map_overlay.setPosition(e.coordinate);
        } else {
            map_overlay.setPosition(null);
        }
    });
}


//=== bind keyboard to control the map =============================================================
function addMapKeyboardControls() {
    document.body.onkeydown = function (e) {

        //minzoom = 3, maxzoom = 18
        var factor = 80;
        var increment = 5;
        var minzoom = 3;
        var view = map.getView();
        var center = view.getCenter();
        var zoomlevel = view.getZoom();
        var step = view.getResolution() * (factor + (zoomlevel - minzoom) * increment);

        try {
            switch (e.key) {

                //left
                case 'ArrowLeft':
                    setMapCenter([center[0] - step, center[1]], 250);
                    break;

                //up
                case 'ArrowUp':
                    e.preventDefault();
                    setMapCenter([center[0], center[1] + step], 250);
                    break;

                //right
                case 'ArrowRight':

                    setMapCenter([center[0] + step, center[1]], 250);
                    break;

                //down
                case 'ArrowDown':
                    e.preventDefault();
                    setMapCenter([center[0], center[1] - step], 250);
                    break;

                //zoom in
                case '+':
                case '=':
                    setMapZoom(map.getView().getZoom() + 1);
                    break;

                //zoom out
                case '-':
                    setMapZoom(map.getView().getZoom() - 1);
                    break;
            }
        } catch (e) {
        }
    };
}


//=== add a marker to the map ======================================================================
function addMarker(id, name, color, lat, lng, draggable, custom_marker, custom_layer) {
    //create a feature
    var feature = new ol.Feature({
        geometry: new ol.geom.Point(ol.proj.fromLonLat([lng, lat])),
        name: name,
        id: id,
        color: color,
        type: 'marker'
    });

    var markerimg = `/images/marker-${color}.png`;

    if (custom_marker != null && custom_marker !== '') {
        markerimg = custom_marker;
    }

    //create an icon style with an image as marker
    var style = new ol.style.Style({
        image: new ol.style.Icon(({
            anchor: [0.5, 36],
            anchorXUnits: 'fraction',
            anchorYUnits: 'pixels',
            src: markerimg
        }))
    });

    //or create an icon style with a shape as marker
    //var style = new ol.style.Style({
    //    image: new ol.style.Circle({
    //        radius: 10,
    //        stroke: new ol.style.Stroke({
    //            color: 'black',
    //            width: 3
    //        }),
    //        fill: new ol.style.Fill({
    //            color: 'white'
    //        }),
    //    })
    //});

    //add the style to the feature
    feature.setStyle(style);

    //create a layer and add the feature
    var layer = new ol.layer.Vector({
        source: new ol.source.Vector({
            features: [feature]
        }),
        name: name,
        type: 'marker',
        zIndex: 100
    });

    //is there a custom layer
    if (custom_layer != null) {
        custom_layer.getSource().addFeature(feature);
        layer = custom_layer;
    } else {
        //add the layer to the map
        map.addLayer(layer);
    }

    //no draggable marker
    if (!draggable) {
        return;
    }

    //add a modifier
    var modify = new ol.interaction.Modify({
        hitDetection: layer,
        source: layer.getSource()
    });

    //needed variables
    var container = document.getElementById('map-container');
    var overlay = modify.getOverlay().getSource();
    var $cmslat = $('.cms_lat');
    var $cmslng = $('.cms_lng');

    //the container does not have the required id
    if (container == null) {
        alert('The map container "<div class="ol-map-container">" does not have the id "map-container" required for draggable markers.');
        return;
    }

    //modify handlers
    modify.on(['modifystart', 'modifyend'], function (e) {
        container.style.cursor = e.type === 'modifystart' ? 'grabbing' : 'pointer';
    });

    overlay.on(['addfeature', 'removefeature'], function (e) {
        container.style.cursor = e.type === 'addfeature' ? 'pointer' : '';
    });

    //get the coordinates during drag
    feature.on('change', function () {
        var newcoords = ol.proj.toLonLat(this.getGeometry().getCoordinates());
        //console.log(ol.proj.toLonLat(this.getGeometry().getCoordinates()));

        $cmslat.val(newcoords[1]);
        $cmslng.val(newcoords[0]);
        $cmslat.html(newcoords[1]);
        $cmslng.html(newcoords[0]);

        $cmslat.show();
        $cmslng.show();
        $cmslat.closest('div').show();
    }, feature);

    //add the modifier to the map
    map.addInteraction(modify);

    //if it is the first marker make it a global variable
    if (map_marker == null) {
        map_marker = feature;
    }
}


//=== load a gpx file from an url ==================================================================
function addGpxFromUrl(url, name, color, show_marker) {

    //create a layer
    var layer = new ol.layer.Vector({
        source: new ol.source.Vector({
            url: url,
            format: new ol.format.GPX()
        }),
        style: function () {
            return map_styles['track-' + color];
        },
        name: name,
        type: 'track',
        zIndex: 100
    });

    //add the layer to the map
    map.addLayer(layer);

    //bind a listeret to the layer (needed becaue loading the source from an url is async)
    layer.getSource().on('addfeature', function (e) {
        //get the coordinates from the layer
        var coords = e.feature.getGeometry().clone().transform('EPSG:3857', 'EPSG:4326');

        //put the coordinates in a html element (to use elsewhere or post it to the server)
        var json = saveGpxToHtmlElement(e.feature);

        //add a marker to the first coordinate
        if (show_marker) {
            addMarker(1, name + map_dash_spacer + map_txt_start, color, coords.getFirstCoordinate()[1], coords.getFirstCoordinate()[0], false);
        }

        //zoom and fit bounds
        zoomAndFitBounds(layer);
    });
}


//=== load a gpx file from a string variable =======================================================
function addGpxFromString(gpx, name, color, show_marker, animate) {

    //remove the existing layer
    removeLayerFromMap(name);

    //create a layer
    var layer = new ol.layer.Vector({
        source: new ol.source.Vector({}),
        style: function () {
            return map_styles['track-' + color];
        },
        name: name,
        type: 'track',
        zIndex: 100
    });

    //read the features from the gpx
    var features = new ol.format.GPX().readFeatures(gpx, {
        dataProjection: 'EPSG:4326',
        featureProjection: 'EPSG:3857'
    });

    //add the features to the layer
    layer.getSource().addFeatures(features);

    //add the layer to the map
    map.addLayer(layer);

    //get the coordinates from the layer
    var coords = features[0].getGeometry().clone().transform('EPSG:3857', 'EPSG:4326');

    //put the coordinates in a html element (to use elsewhere or post it to the server)
    var json = saveGpxToHtmlElement(features);

    //to get the distance in km
    var distance = ol.sphere.getLength(layer.getSource().getFeatures()[0].getGeometry()) / 1000;

    //add a marker to the first coordinate
    if (show_marker) {
        addMarker(1, name + map_dash_spacer + map_txt_start + map_dash_spacer + distance.toFixed(1) + ' km', color, coords.getFirstCoordinate()[1], coords.getFirstCoordinate()[0], false);
    }

    //zoom and fit bounds
    zoomAndFitBounds(layer);

    //add a line animation
    if (animate) {
        animateLine(new ol.geom.LineString(json).transform('EPSG:4326', 'EPSG:3857'), name + map_dash_spacer + map_txt_direction);
    }
}


//=== add a gpx track from an array of single points [[52.8058, 6.7958], [52.8056, 6.7957], ... ] ==
function addGpxFromPoints(gpx, name, color, show_marker, animate) {
    if (gpx.length === 0) {
        return;
    }

    //openlayers uses [lon, lat], not [lat, lon]
    if (gpx[0][0] > 20 && gpx[0][1] < 20) {
        gpx.map(function (l) {
            return l.reverse();
        });
    }

    //create a geometry from the coordinates
    var geometry = new ol.geom.LineString(gpx).transform('EPSG:4326', 'EPSG:3857');

    //create a layer
    var layer = new ol.layer.Vector({
        source: new ol.source.Vector({
            features: [
                new ol.Feature({
                    geometry: geometry,
                    name: name
                })
            ]
        }),
        style: function () {
            return map_styles['track-' + color];
        },
        name: name,
        type: 'track',
        zIndex: 100
    });

    //add the layer to the map
    map.addLayer(layer);

    //add a marker to the first coordinate
    if (show_marker) {
        addMarker(2, name + map_dash_spacer + map_txt_start, color, gpx[0][1], gpx[0][0], false);
    }

    //zoom and fit bounds
    zoomAndFitBounds(layer);

    //add a line animation
    if (animate) {
        animateLine(geometry, name + map_dash_spacer + map_txt_direction);
    }
}


//=== enable dragging and dropping a gpx track on the map ==========================================
function addGpxFromDragDrop() {

    //if an interaction exists remove it first
    if (map_interaction) {
        map.removeInteraction(map_interaction);
    }

    //create the interaction
    map_interaction = new ol.interaction.DragAndDrop({
        formatConstructors: [
            ol.format.GPX
        ],
    });

    //on drag event
    map_interaction.on('addfeatures', function (e) {
        //reset the map
        resetMap();

        //create a layer
        var layer = new ol.layer.Vector({
            source: new ol.source.Vector({
                features: e.features,
            }),
            style: function () {
                return map_styles['track-red'];
            },
            name: 'GpxFromDragDrop',
            type: 'track',
            zIndex: 100
        });

        //add the layer to the map
        map.addLayer(layer);

        //get the coordinates from the layer
        var coords = e.features[0].getGeometry().clone().transform('EPSG:3857', 'EPSG:4326');

        //put the coordinates in a html element (to use elsewhere or post it to the server)
        var json = saveGpxToHtmlElement(e.features);

        //add a marker to the first coordinate
        addMarker(1, map_txt_start, 'blue', coords.getFirstCoordinate()[1], coords.getFirstCoordinate()[0], false);

        //zoom and fit bounds
        zoomAndFitBounds(layer);
    });

    //add the interaction to the map
    map.addInteraction(map_interaction);
}


//=== add a gpx with a html file upload control ====================================================
function addGpxFromFileUpload() {
    var color = 'red';
    var show_marker = false;
    var animate = true;

    //bind a change event to the control
    $('#map-upload').on('change', function () {
        var $this = $(this);
        var gpxfile = document.getElementById($this.prop('id')).files[0];

        //reset the map
        resetMap();

        //create a reader
        var reader = new FileReader();
        reader.onload = function (e) {

            //if there a color defined
            if ($this.data('color') != null) {
                color = $this.data('color');
            }

            //start marker to be shown
            if ($this.data('marker') != null) {
                show_marker = $this.data('marker');
            }

            //should the line be animated
            if ($this.data('animate') != null) {
                animate = $this.data('animate');
            }

            //add it to the map
            addGpxFromString(e.target.result, 'GpxFromFileUpload', color, show_marker, animate);
        };

        reader.readAsText(gpxfile);
        $this.val('');
    });
}


//=== add multiple markers from an array ===========================================================
function addMultipleMarkers(markerArr, type) {
    var markerimg = `/images/site/marker-${type}.png`;
    var markerimg_hover = `/images/site/marker-${type}-red.png`;
    var url = '/toertochten/';

    if (type === 'club') {
        url = '/clubs/';
    } else if (type == 'track') {
        url = '/routes/';
    }

    var normalstyle = new ol.style.Style({
        image: new ol.style.Icon(({
            anchor: [0.5, 0.5],
            src: markerimg
        })),
        zIndex: 100
    });

    var hoverstyle = new ol.style.Style({
        image: new ol.style.Icon(({
            anchor: [0.5, 0.5],
            src: markerimg_hover
        })),
        zIndex: 1000
    });

    //create a new layer
    var layer = new ol.layer.Vector({
        source: new ol.source.Vector(),
        style: normalstyle
    });

    //loop all the markers to add popover
    for (var i = 0; i < markerArr.length; i++) {
        var item = markerArr[i];

        layer.getSource().addFeature(
            new ol.Feature({
                geometry: new ol.geom.Point(ol.proj.fromLonLat([parseFloat(item.lng), parseFloat(item.lat)])),
                id: item.id,
                type: 'icon',
                name: `<strong>${item.naam}</strong><br>${item.regel2}`
            })
        );
    }

    map_feature_array = layer.getSource().getFeatures();

    //make the marker red on hover
    var hover = null;
    map.on('pointermove', function (evt) {
        map.getTargetElement().style.cursor = map.hasFeatureAtPixel(evt.pixel) ? 'pointer' : '';

        if (hover != null) {
            hover.setStyle(normalstyle);
            hover = null;
        }

        map.forEachFeatureAtPixel(evt.pixel, function (f) {
            if (f.get('type') === 'icon') {
                hover = f;
                return true;
            } else {
                return false;
            }
        });

        if (hover) {
            hover.setStyle(hoverstyle);
        }
    });

    //add the layer to the map
    map.addLayer(layer);

    //make the marker clickable
    map.on('click', (e) => {
        map.forEachFeatureAtPixel(e.pixel, function (f) {
            if (f.get('type') === 'icon') {
                location.href = url + hover.get('id');
                return true;
            } else {
                return false;
            }
        });
    });

    //zoom and fit bounds
    //zoomAndFitBounds(layer);
}


//=== move the map marker to a new position ========================================================
function moveMarker(lat, lng) {
    var coordinates = ol.proj.fromLonLat([lng, lat]);
    map_marker.getGeometry().setCoordinates(coordinates);
}


//=== add a click event to the map to get the clicked coordinate ===================================
function addClickEvent() {
    var $popupclick = $('#map-popup-click');

    //check if the click element exists
    if ($popupclick.length === 0) {
        alert('The element "<div id="map-popup-click" class="ol-popup"></div>" was not found.')
        return;
    }

    //create an overlay to anchor the hover popup to the map
    var clickOverlay = new ol.Overlay({
        element: document.getElementById($popupclick.prop('id')),
        autoPan: {
            animation: {
                duration: 250,
            }
        }
    });

    //add the overlay to the map
    map.addOverlay(clickOverlay);

    //add the click event and show the popup
    map.on('singleclick', function (e) {

        //if a popup is open then close it on map click
        if (clickOverlay.getPosition()) {
            clickOverlay.setPosition(null);
        } else {
            var location = ol.proj.toLonLat(e.coordinate).toString().split(',');

            $popupclick.html(`<p>You clicked here:</p><code>${location[1]}<br>${location[0]}</code>`);
            clickOverlay.setPosition(e.coordinate);
        }
    });
}


//=== add a moving marker to a route ===============================================================
function animateLine(route, name) {
    var speed = 30;
    var distance = 0;
    var lasttime;
    var position = new ol.geom.Point(route.clone().getFirstCoordinate());

    var feature = new ol.Feature({
        geometry: position,
    });
    var startMarker = new ol.Feature({
        geometry: position,
    });
    var endMarker = new ol.Feature({
        geometry: position
    });

    //create an icon style with a shape as marker
    var style = new ol.style.Style({
        image: new ol.style.Circle({
            radius: 10,
            stroke: new ol.style.Stroke({
                color: 'black',
                width: 2
            }),
            fill: new ol.style.Fill({
                color: 'red'
            })
        })
    });

    //create a layer with the marker that will move
    var layer = new ol.layer.Vector({
        source: new ol.source.Vector({
            features: [feature, startMarker, endMarker],
        }),
        name: name,
        zIndex: 200
    });

    //add the layer to the map
    map.addLayer(layer);

    //function to move the marker
    function moveFeature(e) {
        var time = e.frameState.time;
        distance = (distance + (speed * (time - lasttime)) / 1e6) % 2;
        lasttime = time;

        //if the distance > 1 then the end has been reached so restart
        if (distance > 1) {
            stopAnimation();
            startAnimation();
        }

        //set the new coordinate for the marker
        position.setCoordinates(route.getCoordinateAt(distance));

        //draw it on the map
        var context = ol.render.getVectorContext(e);
        context.setStyle(style);
        context.drawGeometry(position);

        map.render();
    }

    //start the animation
    function startAnimation() {
        distance = 0;
        lasttime = Date.now();
        layer.on('postrender', moveFeature);
        feature.setGeometry(null);
    }


    //stop the animation
    function stopAnimation() {
        feature.setGeometry(position);
        layer.un('postrender', moveFeature);
    }

    //start
    startAnimation();
}


//=== add multiple markers from an array and make them clustered ===================================
function addMultipleMarkersClustered(markerArr) {
    if (markerArr.length === 0) {
        return;
    }

    var url = '';

    //sort the array
    markerArr.sort((a, b) => a.lat - b.lat);

    //create the features
    map_feature_array = new Array(markerArr.length);
    var prevmarker;
    var offsetdefault = 0.0015;
    var offsettotal = offsetdefault;

    //loop the markers
    for (let i = 0; i < markerArr.length; ++i) {
        var item = markerArr[i];
        var point = new ol.geom.Point(ol.proj.fromLonLat([parseFloat(item.lng), parseFloat(item.lat)]));
        var markercompare = item.lng.toFixed(3) + '_' + item.lat.toFixed(3);

        //if the features have the same coordinates move them slightly so they appear next to each other
        if (markercompare === prevmarker) {
            point = new ol.geom.Point(ol.proj.fromLonLat([parseFloat(item.lng) + offsettotal, parseFloat(item.lat)]));
            offsettotal += offsetdefault;
        } else {
            offsettotal = offsetdefault;
        }

        map_feature_array[i] = new ol.Feature({
            geometry: point,
            id: item.id,
            type: 'icon',
            name: `<strong>${item.naam}</strong><br>${item.regel2}`
        });

        prevmarker = markercompare;
    }

    //create the source
    var clusterSource = new ol.source.Cluster({
        distance: 40,
        minDistance: 10,
        source: new ol.source.Vector({
            features: map_feature_array,
        })
    });

    //clustered style
    var zindex = 100;
    var clusters = new ol.layer.Vector({
        zIndex: 1,
        source: clusterSource,
        style: function (feature) {
            const size = feature.get('features').length;

            //if there are no more clusters you can return a different icon
            if (size === 1) {
                //var style = new ol.style.Style();
                //return style;
            }

            var radius = 12;
            var textsize = 1;
            var text = size.toString();

            //dynamic cluster marker size calculations
            if (size > 100) {
                radius = (size * 0.2);
                textsize = size / 60;
            } else if (size > 50) {
                radius = (size * 0.4);
                textsize = size / 25;
            } else if (size > 25) {
                radius = (size * 0.6);
                textsize = size / 20;
            } else if (size > 1) {
                radius = 15;
                textsize = 1.3;
            }

            //max markercluster size
            if (radius > 75) {
                radius = 75;
                textsize = 5;
            }

            var style = new ol.style.Style({
                image: new ol.style.Circle({
                    radius: radius,
                    fill: new ol.style.Fill({
                        color: 'black'
                    }),
                    stroke: new ol.style.Stroke({
                        color: 'white',
                        width: 1
                    }),
                }),
                text: new ol.style.Text({
                    text: text,
                    fill: new ol.style.Fill({
                        color: '#fff',
                    }),
                    scale: textsize,
                    offsetY: size > 50 ? 2 : 1
                }),
                zIndex: zindex
            });

            zindex++;
            return style;
        },
    });

    //create a tile layer
    var raster = new ol.layer.Tile({
        source: new ol.source.OSM()
    });

    //add the layers to the map
    map.addLayer(raster);
    map.addLayer(clusters);

    //make the marker clickable
    map.on('click', (e) => {
        clusters.getFeatures(e.pixel).then((clickedFeatures) => {
            if (clickedFeatures.length) {

                //get the clustered coordinates
                const features = clickedFeatures[0].get('features');

                //when there are multiple features do a zoom otherwise handle the click
                if (features.length > 1) {
                    const extent = ol.extent.boundingExtent(
                        features.map((r) => r.getGeometry().getCoordinates())
                    );

                    map.getView().fit(extent, { duration: 1000, padding: [50, 50, 50, 50] });
                } else {
                    location.href = url + features[0].get('id');
                }
            }
        });
    });

    //show a mouse pointer and popup on hover
    map.on('pointermove', function (e) {
        map.getTargetElement().style.cursor = map.hasFeatureAtPixel(e.pixel) ? 'pointer' : '';

        clusters.getFeatures(e.pixel).then((hoveredFeatures) => {
            if (hoveredFeatures.length) {

                //get the clustered coordinates
                const features = hoveredFeatures[0].get('features');

                //when there is a single features show the popup
                if (features.length === 1) {
                    if (features[0] && typeof features[0].get('name') != 'undefined' && features[0].get('name') != null && features[0].get('name') !== '') {
                        $map_popup.html(features[0].get('name'));
                        map_overlay.setPosition(e.coordinate);
                    } else {
                        map_overlay.setPosition(null);
                    }
                }
            }
        });
    });

    //zoom and fit bounds
    zoomAndFitBounds(clusterSource);
}


//=== put the coordinates in a html element (to use elsewhere or post it to the server) ============
function saveGpxToHtmlElement(features) {
    //get the coordinates from the layer
    var json = '';

    for (var i = 0; i < features.length; i++) {
        var coords = features[i].getGeometry().clone().transform('EPSG:3857', 'EPSG:4326');

        try {
            var str = JSON.stringify(coords.getCoordinates()[0]);

            if (str.length > json.length) {
                json = str;
            }
        } catch (e) {
            console.log(e.message);
        }
    }

    //put the coordinates in a html element
    $('#map-gpx').val(json);

    return $.parseJSON(json);
}


//=== set the map zoom level =======================================================================
function setMapZoom(zoom) {
    map.getView().animate({
        duration: 250,
        zoom: zoom
    });
}


//=== center the map on a coordinate ===============================================================
function setMapCenter(center, duration = 0) {
    map.getView().animate({
        duration: duration,
        center: center
    });
}


//=== make all the items on the map fit on the screen ==============================================
function zoomAndFitBounds(layer) {
    var geom = ol.geom.Polygon.fromExtent(layer.getSource().getExtent())
    geom.scale(1.1);
    map.getView().fit(geom, { size: map.getSize() });
}


//=== add a layer to the map =======================================================================
function addLayerToMap(name) {
    var layer = new ol.layer.Vector({
        name: name,
        zIndex: 600,
        source: new ol.source.Vector()
    });

    //add it to the map
    map.addLayer(layer);

    return layer;
}


//=== remove a layer from the map ==================================================================
function removeLayerFromMap(name) {
    map.getLayers().forEach(layer => {
        if (layer && layer.get('name') === name) {
            map.removeLayer(layer);
        }
    });
}


//=== reset the map to the default state ===========================================================
function resetMap() {
    var layers = map.getLayers().getArray().slice();

    layers.forEach(layer => {

        //do not remove base and contexmenu layer
        if (layer && (layer.get('name') === 'baselayer' || layer.get('name') === 'contextmenu')) {
            return;
        }

        map.removeLayer(layer);
    });

    //reset zoom and map center
    setMapZoom(map_default_zoomlevel);
    setMapCenter(ol.proj.fromLonLat(cmsScriptVariables.dutch_map_center));

    $('#map-gpx').val('');
}


//=== create the right click context menu ==========================================================
function createContextMenu() {
    var icon_in = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItem9vbS1pbiI+PGNpcmNsZSBjeD0iMTEiIGN5PSIxMSIgcj0iOCI+PC9jaXJjbGU+PGxpbmUgeDE9IjIxIiB5MT0iMjEiIHgyPSIxNi42NSIgeTI9IjE2LjY1Ij48L2xpbmU+PGxpbmUgeDE9IjExIiB5MT0iOCIgeDI9IjExIiB5Mj0iMTQiPjwvbGluZT48bGluZSB4MT0iOCIgeTE9IjExIiB4Mj0iMTQiIHkyPSIxMSI+PC9saW5lPjwvc3ZnPg==';
    var icon_out = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItem9vbS1vdXQiPjxjaXJjbGUgY3g9IjExIiBjeT0iMTEiIHI9IjgiPjwvY2lyY2xlPjxsaW5lIHgxPSIyMSIgeTE9IjIxIiB4Mj0iMTYuNjUiIHkyPSIxNi42NSI+PC9saW5lPjxsaW5lIHgxPSI4IiB5MT0iMTEiIHgyPSIxNCIgeTI9IjExIj48L2xpbmU+PC9zdmc+';
    var icon_ctr = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItbWF4aW1pemUiPjxwYXRoIGQ9Ik04IDNINWEyIDIgMCAwIDAtMiAydjNtMTggMFY1YTIgMiAwIDAgMC0yLTJoLTNtMCAxOGgzYTIgMiAwIDAgMCAyLTJ2LTNNMyAxNnYzYTIgMiAwIDAgMCAyIDJoMyI+PC9wYXRoPjwvc3ZnPg==';

    //menu items
    var menuitems = [
        {
            text: 'Zoom in',
            icon: icon_in,
            callback: zoomIn
        },
        {
            text: 'Zoom out',
            icon: icon_out,
            callback: zoomOut
        },
        {
            text: 'Center map',
            icon: icon_ctr,
            callback: center
        }
    ];

    //create the context menu
    var contextmenu = new ContextMenu({
        items: menuitems
    });
    map.addControl(contextmenu);

    //on menu open
    contextmenu.on('open', function (evt) {
        contextmenu.clear();
        contextmenu.extend(menuitems);
        contextmenu.extend(contextmenu.getDefaultItems());
    });

    //create a layer
    var layer = new ol.layer.Vector({
        source: new ol.source.Vector(),
        name: 'contextmenu'
    });

    //add the layer to the map
    map.addLayer(layer);

    //on mouse move
    map.on('pointermove', function (e) {
        if (e.dragging) {
            return;
        }

        var pixel = map.getEventPixel(e.originalEvent);
        var hit = map.hasFeatureAtPixel(pixel);

        map.getTargetElement().style.cursor = hit ? 'pointer' : '';
    });

    //context menu functionms
    function center(obj) {
        setMapCenter(obj.coordinate, 500);
    }

    function zoomIn(obj) {
        setMapZoom(map.getView().getZoom() + 1);
    }

    function zoomOut(obj) {
        setMapZoom(map.getView().getZoom() - 1);
    }
}