/*
Copyright 2012-2026 VeloViewer. All Rights Reserved
###V71###
*/

L.Control.Layers.prototype._checkDisabledLayers = function() {};

const osGbCrs = '27700';
const osGbLight = 'osLight';
const osGbBounds = L.latLngBounds(L.latLng(49.38802038505334, -10.8342841886), L.latLng(62.2662683043619, 7.551552493184295));

function osCrs() {
  return new L.Proj.CRS('EPSG:27700', '+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 +units=m +no_defs', {
        resolutions: [ 896.0, 448.0, 224.0, 112.0, 56.0, 28.0, 14.0, 7.0, 3.5, 1.75 ],
        origin: [ -238375.0, 1376256.0 ]
    });
}

function latForm(lat) {
    return (Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, 14)));//lat2Tile(lat, 14);
}

function longForm(lon) {
    return (Math.floor((lon + 180) / 360 * Math.pow(2, 14)));//long2Tile(lon, 14);
}

L.FunctionButtons = L.Control.extend({
    includes: L.Mixin.Events, //L.Evented

    initialize: function(buttons, options) {
        if (!('push' in buttons && 'splice' in buttons))
            buttons = [buttons];
        this._buttons = buttons;
        if (!options && buttons.length > 0 && 'position' in buttons[0])
            options = {
                position: buttons[0].position
            };
        L.Control.prototype.initialize.call(this, options);
    },

    onAdd: function(map) {
        this._map = map;
        this._links = [];

        var container = L.DomUtil.create('div', 'leaflet-bar');
        for (var i = 0; i < this._buttons.length; i++) {
            var button = this._buttons[i],
                link = L.DomUtil.create('a', '', container);
            link._buttonIndex = i; // todo: remove?
            link.href = button.href || '#';
            if (button.href)
                link.target = 'funcbtn';
            link.style.padding = '0 4px';
            link.style.width = 'auto';
            link.style.minWidth = '20px';
            if (button.bgColor)
                link.style.backgroundColor = button.bgColor;
            if (button.title)
                link.title = button.title;
            button.link = link;
            this._updateContent(i);

            var stop = L.DomEvent.stopPropagation;
            L.DomEvent
                .on(link, 'click', stop)
                .on(link, 'mousedown', stop)
                .on(link, 'dblclick', stop)
                .on(link, 'mouseover', function() {
                    map.dragging.disable();
                    map.scrollWheelZoom.disable();
                })
                .on(link, 'mouseout', function() {
                    map.dragging.enable();
                    map.scrollWheelZoom.enable();
                });
            if (!button.href)
                L.DomEvent
                .on(link, 'click', L.DomEvent.preventDefault)
                .on(link, 'click', this.clicked, this);
        }

        return container;
    },

    _updateContent: function(n) {
        if (n >= this._buttons.length)
            return;
        var button = this._buttons[n],
            link = button.link,
            content = button.content;
        if (!link)
            return;
        if (content === undefined || content === false || content === null || content === '')
            link.innerHTML = button.alt || '&nbsp;';
        else if (typeof content === 'string') {
            var ext = content.length < 4 ? '' : content.substring(content.length - 4),
                isData = content.substring(0, 11) === 'data:image/';
            if (ext === '.png' || ext === '.gif' || ext === '.jpg' || isData) {
                link.style.width = '' + (button.imageSize || 26) + 'px';
                link.style.height = '' + (button.imageSize || 26) + 'px';
                link.style.padding = '0';
                link.style.backgroundImage = 'url(' + content + ')';
                link.style.backgroundRepeat = 'no-repeat';
                link.style.backgroundPosition = button.bgPos ? (-button.bgPos[0]) + 'px ' + (-button.bgPos[1]) + 'px' : '0px 0px';
            } else
                link.innerHTML = content;
        } else {
            while (link.firstChild)
                link.removeChild(link.firstChild);
            link.appendChild(content);
        }
    },

    setContent: function(n, content) {
        if (content === undefined) {
            content = n;
            n = 0;
        }
        if (n < this._buttons.length) {
            this._buttons[n].content = content;
            this._updateContent(n);
        }
    },

    setBgColor: function(n, colour) {
        if (n >= 0 && n < this._buttons.length && this._buttons[n].link) {
            this._buttons[n].link.style.backgroundColor = colour;
            //this._updateContent(n);
        }
    },

    getBgColor: function(n) {
        if (n >= 0 && n < this._buttons.length && this._buttons[n].link) {
            return this._buttons[n].link.style.backgroundColor;
            //this._updateContent(n);
        }
        return '';
    },

    setTitle: function(n, title) {
        if (title === undefined) {
            title = n;
            n = 0;
        }
        if (n < this._buttons.length) {
            var button = this._buttons[n];
            button.title = title;
            if (button.link)
                button.link.title = title;
        }
    },

    setBgPos: function(n, bgPos) {
        if (bgPos === undefined) {
            bgPos = n;
            n = 0;
        }
        if (n < this._buttons.length) {
            var button = this._buttons[n];
            button.bgPos = bgPos;
            if (button.link)
                button.link.style.backgroundPosition = bgPos ? (-bgPos[0]) + 'px ' + (-bgPos[1]) + 'px' : '0px 0px';
        }
    },

    setHref: function(n, href) {
        if (href === undefined) {
            href = n;
            n = 0;
        }
        if (n < this._buttons.length) {
            var button = this._buttons[n];
            button.href = href;
            if (button.link)
                button.link.href = href;
        }
    },

    clicked: function(e) {
        var link = (window.event && window.event.srcElement) || e.target || e.srcElement;
        while (link && 'tagName' in link && link.tagName !== 'A' && !('_buttonIndex' in link))
            link = link.parentNode;
        if ('_buttonIndex' in link) {
            var button = this._buttons[link._buttonIndex];
            if (button) {
                if ('callback' in button)
                    button.callback.call(button.context);
                this.fire('clicked', {
                    idx: link._buttonIndex
                });
            }
        }
    }
});

L.functionButtons = function(buttons, options) {
    return new L.FunctionButtons(buttons, options);
};

var mapProxy = https + '://',
    lm_prevZoom, lm_optionButtons, lm_CurrentPositionButton, watchPositionMarker = null,
    watchPosition = null,
    watchPositionTimer = null,
    lm_tileLines, lm_tileLinesOs, lm_showtiles = false,
    lm_mapLineSmoothing = 0,
    lm_mapLine = {
        color: '#FF032E',
        weight: 3,
        opacity: 0.5,
        smoothFactor: 1
    },
    lm_mapLineHighlight = {
        color: '#3C90A8',
        opacity: 1,
        weight: 3
    },
    lm_mapLineBGDef = {
        opacity: 1,
        weight: 5,
        pointRadius: 0.5,
        color: '#000',
        smoothFactor: lm_mapLineSmoothing
    }, photoLayer, photoData = [],
    lm_mapLineStyles = {},
    lm_tiles = {},
    lm_squares = {},
    lm_clusters = {},
    lm_columnRows = {},
    lm_clumpSpans = {},
    lm_currentBaseLayer,
    mapInitiated = {},
    lm_initialLoad = true,
    retinaImg = L.Browser.retina ? '@2x' : '',
    z_london, z_watopia, z_newyork, z_richmond, z_innsbruck, z_yorkshire, z_critcity, z_france, z_italy, z_champs, z_makuri, z_scotland, z_overlay, zwiftMaps,
    lm_ZwiftMapsMarker = typeof(getCookie('lm_ZwiftMapsMarker')) !== 'undefined' && +getCookie('lm_ZwiftMapsMarker') == 1;
    lm_osAttribution = '<span class="os-api-branding"><span class="logo"></span><span class="copyright">Contains OS data &copy; Crown copyright and database rights YYYY</span></span>'.replace('YYYY', new Date().getFullYear());


// please create and use your own thunderforest key - https://www.thunderforest.com/
var lm_thunderforestKey = '3e8acaf16b3f4e36a80a1732e9e82c40',
    lm_tfa = 'Maps © <a href="http://www.thunderforest.com">Thunderforest</a>, Data © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>',
    lm_tfOptions = {
        maxZoom: 21,
        attribution: lm_tfa
    },
    lm_layerVeloviewer = new L.TileLayer('https://{s}.tile.thunderforest.com/veloviewer-oh4/{z}/{x}/{y}' + retinaImg + '.png?apikey=' + lm_thunderforestKey, lm_tfOptions);
lm_layerVeloviewer.id = 'veloviewer';


var lm_layerCustom, lm_layerLive, lm_baseLayers;

// please create and use your own Open Spaces key - https://www.ordnancesurvey.co.uk/
if (typeof(embed) === 'undefined' || !embed) {

    var lm_layerLandscape = new L.TileLayer('https://{s}.tile.thunderforest.com/landscape/{z}/{x}/{y}' + retinaImg + '.png?apikey=' + lm_thunderforestKey, lm_tfOptions);
    lm_layerLandscape.id = 'landscape';

    var lm_layerCycle = new L.TileLayer('https://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}' + retinaImg + '.png?apikey=' + lm_thunderforestKey, lm_tfOptions);
    lm_layerCycle.id = 'ocm';
    var lm_layerOutdoors = new L.TileLayer('https://{s}.tile.thunderforest.com/outdoors/{z}/{x}/{y}' + retinaImg + '.png?apikey=' + lm_thunderforestKey, lm_tfOptions);
    lm_layerOutdoors.id = 'outdoors';
    var lm_layerOSM = new L.TileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '© <a href="http://openstreetmap.org">OpenStreetMap</a> contributors',
        maxZoom: 19
    });
    lm_layerOSM.id = 'osm';
    // valid values are 'roadmap', 'satellite', 'terrain' and 'hybrid'
    var lm_layerGTerrain = L.tileLayer('https://{s}.google.com/vt/lyrs=p&x={x}&y={y}&z={z}', {
        maxZoom: 21,
        subdomains: ['mt0', 'mt1', 'mt2', 'mt3']
    });
    lm_layerGTerrain.id = 'terrain';
    var lm_layerGHybrid = L.tileLayer('https://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', {
        maxZoom: 21,
        subdomains: ['mt0', 'mt1', 'mt2', 'mt3']
    });
    lm_layerGHybrid.id = 'hybrid';
    var lm_layerGSatellite = L.tileLayer('https://{s}.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', {
        maxZoom: 21,
        subdomains: ['mt0', 'mt1', 'mt2', 'mt3']
    });
    lm_layerGSatellite.id = 'satellite';

    var lm_layerOsLight = new L.TileLayer('https://api.os.uk/maps/raster/v1/zxy/Light_3857/{z}/{x}/{y}.png?key=AVwbagMgtYmKAKKD8ghznJTA0DBTAgLN', {
      //minZoom: 6.5,
      maxZoom: 20,
      attribution: lm_osAttribution
    });
    lm_layerOsLight.id = osGbLight;

    var lm_layer27700 = new L.TileLayer('https://api.os.uk/maps/raster/v1/zxy/Leisure_27700/{z}/{x}/{y}.png?key=AVwbagMgtYmKAKKD8ghznJTA0DBTAgLN', {
      crs: osCrs(),
      minZoom: 0,
      maxZoom: 9,
      attribution: lm_osAttribution
    });
    lm_layer27700.id = osGbCrs;

    lm_baseLayers = {
        //"Live": {},
        "Custom": {},
        "VeloViewer": lm_layerVeloviewer,
        "Landscape": lm_layerLandscape,
        "Outdoors": lm_layerOutdoors,
        "Open Cycle": lm_layerCycle,
        "Open Street": lm_layerOSM,
        "OS Light (GB only)": lm_layerOsLight,
        "OS Leisure (GB only)": lm_layer27700,
        "Terrain": lm_layerGTerrain,
        "Hybrid": lm_layerGHybrid,
        "Satellite": lm_layerGSatellite
    };

    if (typeof(os_sh) !== 'undefined' && os_sh == 0) {
      delete lm_baseLayers["OS Leisure (UK only)"];
    }

    if (typeof(mbCustomToken) !== 'undefined' && mbCustomToken != '') {
        if (mbCustomToken.substr(0, 5) == 'https') {
            lm_layerCustom = new L.TileLayer(mbCustomToken /*.replace(/\{/g, '${')*/.replace('{y}', '{y}' + (mbCustomToken.indexOf('mapbox') > -1 ? retinaImg : '')), {
                attribution: '© OpenStreetMap'
            });
        } else {
            lm_layerCustom = new L.TileLayer("https://{s}.tiles.mapbox.com/v4/" + mbCustomDomain + "/{z}/{x}/{y}" + retinaImg + ".png?access_token=" + mbCustomToken, {
                attribution: '© MapBox, © OpenStreetMap'
            });
        }
        lm_layerCustom.id = 'custom';
        lm_baseLayers.Custom = lm_layerCustom;
    } else {
        delete lm_baseLayers.Custom;
    }
} else {
    lm_baseLayers = {
        "Landscape": lm_layerLandscape
    }
}

var lm_distanceMarkers = [];

function showDistanceMarker(ll, text, lmap) {
    if (!lm_distanceMarkers[ll.join('-') + '-' + text]) {
        var marker = new L.circleMarker(ll, {
            fillColor: '#000',
            fillOpacity: 1,
            radius: 2,
            stroke: false
        });
        marker.bindTooltip(text + ' ' + lrgLenUnit, {
            permanent: true,
            className: "leafletDistanceMarker"
        });
        //lm_distanceMarkers[ll.join('-') + '-' + text] = marker;
    }
    marker.addTo(lmap);
    //lm_distanceMarkers[ll.join('-') + '-' + text].addTo(map);
    marker.shownOnMap = true;
    return marker;
}

function hideDistanceMarker(marker, lmap) {
    lmap.removeLayer(marker);
    marker.shownOnMap = false;
}

function setLMapDistMarkers(lmap, distA, llA) {
    if (getParameterByName('hideDistMarker') == '1') return;
    if (lmap.getBounds() != null) {
        if (lmap.distanceMarkers && lmap.distanceMarkers.length) {
            lmap.distanceMarkers.forEach(function(d) {
                lmap.removeLayer(d);
            });
        }
        lmap.distanceMarkers = [];
        var lDistanceMarkers = [];
        if ((typeof(embed) === 'undefined' && embed) || getParameterByName('p') != '' || (document.getElementById('map' + lmap.suffix + 'DistanceMarkerCheckBox') != null && !document.getElementById('map' + lmap.suffix + 'DistanceMarkerCheckBox').checked)) return;
        var dist = 1;
        var z = lmap.getZoom();
        if (z >= 17) dist = 0.1;
        if (z < 17 && z >= 16) dist = 0.25;
        if (z < 16 && z >= 15) dist = 0.5;
        if (z < 15 && z >= 14) dist = 1;
        if (z < 14 && z >= 13) dist = 2;
        if (z < 13 && z >= 12) dist = 5;
        if (z < 12 && z >= 11) dist = 10;
        if (z < 11) dist = 25;

        var lMapB = lmap.getBounds();

        lDistanceMarkers = d3.range(distA[0], distA.last() + 1, dist / lrgLenMult).map(function(d, i) {
            return {
                d: d,
                t: i * dist,
                i: d3.bisectLeft(distA, d)
            }
        }).filter(function(d) {
            return d.i < distA.length && d.d != 0;
        }).map(function(d) {
            var ll = llA[d.i]
            if (distA[d.i] != d.d) {
                var dd = distA[d.i] - distA[d.i - 1];
                var de = d.d - distA[d.i - 1];
                ll = {
                    lat: llA[d.i - 1].lat + (de / dd) * (llA[d.i].lat - llA[d.i - 1].lat),
                    lng: llA[d.i - 1].lng + (de / dd) * (llA[d.i].lng - llA[d.i - 1].lng)
                };
            }
            return {
                i: d.i,
                t: (Math.round(100 * d.t) / 100).toString(),
                ll: ll
            };
        }).filter(function(d) {
            return (typeof(filtIndExtent) === 'undefined' || (d.i >= filtIndExtent[0] && d.i <= filtIndExtent[1])) &&
                d.ll.lat >= lMapB._southWest.lat && d.ll.lat <= lMapB._northEast.lat && d.ll.lng >= lMapB._southWest.lng && d.ll.lng <= lMapB._northEast.lng
        });

        lmap.distanceMarkers = lDistanceMarkers.map(function(d) {
            return showDistanceMarker([d.ll.lat, d.ll.lng], d.t, lmap);
        });
    }
}

function showExplorerTilesLoaded(obj, result) {
    tilesCalculated = true;
    routeExplorerTiles = uncompressTiles(result.t, 14);
    var sum;
    routeExplorerTiles = setExplorer(sum, routeExplorerTiles, true);
    drawTiles(routeExplorerTiles);
    if (lm_initialLoad) {
        if (+getCookie('ExplorerColumnRowShown')) {
            lm_showColumnRowBounds();
        } else {
            if (+getCookie('ExplorerClusterShown')) {
                lm_showClusterBounds();
            } else {
                if (+getCookie('ExplorerMaxSquareShown')) {
                    lm_showMaxSquareBounds();
                }
            }
        }
        lm_initialLoad = false;
    }
    lm_showHideTileLines();
    if (obj.func) {
        obj.func();
    }
}

function removeTiles(){
  for (var att in lm_tiles) {
      map.removeLayer(lm_tiles[att]);
      delete lm_tiles[att];
  }
}

function removeSquares(){
  for (var att in lm_squares) {
      map.removeLayer(lm_squares[att]);
      delete lm_squares[att];
  }
}

function removeClusters(){
  for (var att in lm_clusters) {
      map.removeLayer(lm_clusters[att]);
      delete lm_clusters[att];
  }
}

function removeRowColumns(){
    for (var att in lm_columnRows) {
      map.removeLayer(lm_columnRows[att]);
      delete lm_columnRows[att];
  }
}

function removeClumpSpans(){
    for (var att in lm_clumpSpans) {
      map.removeLayer(lm_clumpSpans[att]);
      delete lm_clumpSpans[att];
  }
}

function showExplorerTiles(f) {
    if (typeof(lm_optionButtons) === 'undefined') return;
    if (typeof(map) !== 'undefined' && map != null) {
        var msi = lm_optionButtons._buttons.map(function(d){return d.type}).indexOf('maxSqaure');
        var mci = lm_optionButtons._buttons.map(function(d){return d.type}).indexOf('maxCluster');
        var cci = lm_optionButtons._buttons.map(function(d){return d.type}).indexOf('maxColumnRow');
        if (lm_optionButtons.getBgColor(msi) == '' && lm_optionButtons.getBgColor(mci) == '') {
            removeTiles();
        }
        if (msi != -1 && lm_optionButtons.getBgColor(msi) == '') {
            removeSquares();
        }
        if (mci != -1 && lm_optionButtons.getBgColor(mci) == '') {
            removeClusters();
            removeClumpSpans();
        }
        if (cci != -1 && lm_optionButtons.getBgColor(cci) == '') {
            removeRowColumns();
        }
        if (lm_optionButtons.getBgColor(msi) != '' || lm_optionButtons.getBgColor(mci) != '' || (cci != -1 && lm_optionButtons.getBgColor(cci) != '')) {
            if (cpref == 'r') {
                if (!tilesCalculated) {
                    if (explorerTilesSrc.match(/\.[0-9]*\.js/) == null) {
                        alert('Explorer data not yet generated. Please visit your Summary page.');
                        return;
                    }
                    $.ajax({
                        url: explorerTilesSrc,
                        func: f,
                        async: true,
                        tryCount: 0,
                        success: function(result) {
                            showExplorerTilesLoaded(this, result);
                        },
                        error: function(result) {
                            this.tryCount++;
                            if (this.tryCount == 1) {
                                $.ajax({
                                    parentCall: this,
                                    async: true,
                                    url: '/api/checkName.php?n=' + this.url.substr(this.url.indexOf('athletes2')),
                                    success: function(d) {
                                        if (d.status == 'success') {
                                            var n = d.name.replace(/^.*\.([0-9]*)\.js$/, '$1')
                                            this.parentCall.url = this.parentCall.url.replace(/\.[0-9]*\./, '.' + n + '.');
                                            $.ajax(this.parentCall);
                                        } else {
                                            alert('Sorry, there has been a problem geting your explored tiles (1).');
                                        }
                                    }
                                });
                                return false;
                            } else {
                                alert('Sorry, there has been a problem geting your explored tiles (2).');
                            }
                        }
                    });
                    return;
                }
                drawTiles(routeExplorerTiles);
            } else {
                if (!tilesCalculated) {
                    var fAct = a.filter(function(d) {
                        return (typeof(d.map) !== 'undefined' && d.map != null) || typeof(texplorer[d.i]) !== 'undefined'
                    });
                    fAct.forEach(function(d) {
                        getActMapTiles(d, explorerZoom, 100);
                    });
                    tilesCalculated = true;
                }
                var sum;
                var tiles = setExplorer(sum, liveData.filter(function(d) {
                    return (typeof(d.map) !== 'undefined' && d.map != null) || typeof(texplorer[d.i]) !== 'undefined'
                }), true);
                drawTiles(tiles);
            }
        }
        //}
        if (lm_initialLoad) {
            if (getExplorerColumnRowShown()) {
                lm_showColumnRowBounds();
            } else {
                if (getExplorerClusterShown()) {
                    lm_showClusterBounds();
                } else {
                    if (getExplorerMaxSquareShown()) {
                        lm_showMaxSquareBounds();
                    }
                }
            }
            lm_initialLoad = false;
        }
        lm_showHideTileLines();
        if (f) f();
    }
}

function getExplorerColumnRowShown() {
    switch (lm_type) {
        case "explorer":
            return false; //maxColumnRowShown.val;
        default:
            return +getCookie('ExplorerColumnRowShown');
    }
}

function getExplorerClusterShown(){
  switch(lm_type){
    case "explorer":
      return maxClusterShown.val;
    default:
      return +getCookie('ExplorerClusterShown');
  }
}

function getExplorerMaxSquareShown(){
  switch(lm_type){
    case "explorer":
          return maxSquareShown.val;
    default:
      return +getCookie('ExplorerMaxSquareShown');
  }
}

function showExplorerColour() {
    d3.select('#explorerConfigModalInfo').html(explorerConfigInfoText);
}

function updateExplorerColour(a, noSave) {
    var ah = Math.round(Math.round(a._a * 255)).toString(16);
    var r = Math.round(a._r).toString(16);
    var g = Math.round(a._g).toString(16);
    var b = Math.round(a._b).toString(16);
    var h = '#' + (r.length == 1 ? '0' : '') + r + (g.length == 1 ? '0' : '') + g + (b.length == 1 ? '0' : '') + b;
    var h1 = '#' + (ah.length == 1 ? '0' : '') + ah + (r.length == 1 ? '0' : '') + r + (g.length == 1 ? '0' : '') + g + (b.length == 1 ? '0' : '') + b;
    switch (this.id) {
        case 'explorerTileStroke':
            explorerTileStyle.color = h;
            explorerTileStyle.opacity = a._a;
            explorerClumpStyle.color = h;
            explorerClumpStyle.opacity = a._a;
            break;
        case 'explorerTileFill':
            explorerTileStyle.fillColor = h;
            explorerTileStyle.fillOpacity = a._a;
            break;
        case 'explorerNewTileFill':
            explorerNewTileStyle.fillColor = h;
            explorerNewTileStyle.fillOpacity = a._a;
            break;
        case 'explorerMaxSquareStroke':
            maxSquareStyle.color = h;
            maxSquareStyle.opacity = a._a;
            break;
        case 'explorerMaxClumpFill':
            explorerClumpStyle.fillColor = h;
            explorerClumpStyle.fillOpacity = a._a;
            break;
        case 'explorerMaxColumnRowStroke':
            maxColumnRowStyle.color = h;
            maxColumnRowStyle.opacity = a._a;
            break;
        case 'explorerMaxClumpSpanStroke':
            maxClumpSpanStyle.color = h;
            maxClumpSpanStyle.opacity = a._a;
            break;
        case 'explorerTileStrokeSat':
            explorerTileStyleSat.color = h;
            explorerTileStyleSat.opacity = a._a;
            explorerClumpStyleSat.color = h;
            explorerClumpStyleSat.opacity = a._a;
            break;
        case 'explorerTileFillSat':
            explorerTileStyleSat.fillColor = h;
            explorerTileStyleSat.fillOpacity = a._a;
            break;
        case 'explorerNewTileFillSat':
            explorerNewTileStyleSat.fillColor = h;
            explorerNewTileStyleSat.fillOpacity = a._a;
            break;
        case 'explorerMaxColumnRowStrokeSat':
            maxColumnRowStyleSat.color = h;
            maxColumnRowStyleSat.opacity = a._a;
            break;
        case 'explorerMaxSquareStrokeSat':
            maxSquareStyleSat.color = h;
            maxSquareStyleSat.opacity = a._a;
            break;
        case 'explorerMaxClumpFillSat':
            explorerClumpStyleSat.fillColor = h;
            explorerClumpStyleSat.fillOpacity = a._a;
            break;
        case 'explorerMaxClumpSpanStrokeSat':
            maxClumpSpanStyleSat.color = h;
            maxClumpSpanStyleSat.opacity = a._a;
            break;
    }
    explorerConfig[this.id] = h1;
    if (typeof(noSave) === 'undefined') {
        d3.select('#explorerConfigModalInfo').html('Saving...');
        $.ajax({
            url: '/api/saveExplorerConfig.php',
            type: "POST",
            data: {
                d: JSON.stringify(explorerConfig)
            },
            async: true,
            success: function(result) {
                if (result.status == 'success') {
                    d3.select('#explorerConfigModalInfo').html('Changes saved.');
                } else {
                    d3.select('#explorerConfigModalInfo').html('Did not save!');
                }
                setTimeout(showExplorerColour, 5000);
            },
            error: function(result) {
                d3.select('#explorerConfigModalInfo').html('Did not save!');
                setTimeout(showExplorerColour, 5000);
            }
        });
    }
    lm_updateLayerStyles()
}
if (explorerConfig == '') {
    explorerConfig = {};
} else {
    explorerConfig = JSON.parse(explorerConfig);
    if (explorerConfig.explorerTileStroke) {
        explorerTileStyle.color = '#' + explorerConfig.explorerTileStroke.substr(3);
        explorerTileStyle.opacity = parseInt(explorerConfig.explorerTileStroke.substr(1, 2), 16) / 255;
        explorerClumpStyle.color = '#' + explorerConfig.explorerTileStroke.substr(3);
        explorerClumpStyle.opacity = parseInt(explorerConfig.explorerTileStroke.substr(1, 2), 16) / 255;
    }
    if (explorerConfig.explorerTileFill) {
        explorerTileStyle.fillColor = '#' + explorerConfig.explorerTileFill.substr(3);
        explorerTileStyle.fillOpacity = parseInt(explorerConfig.explorerTileFill.substr(1, 2), 16) / 255;
    }
    if (explorerConfig.explorerMaxSquareStroke) {
        maxSquareStyle.color = '#' + explorerConfig.explorerMaxSquareStroke.substr(3);
        maxSquareStyle.opacity = parseInt(explorerConfig.explorerMaxSquareStroke.substr(1, 2), 16) / 255;
    }
    if (explorerConfig.explorerMaxClumpFill) {
        explorerClumpStyle.fillColor = '#' + explorerConfig.explorerMaxClumpFill.substr(3);
        explorerClumpStyle.fillOpacity = parseInt(explorerConfig.explorerMaxClumpFill.substr(1, 2), 16) / 255;
    }
    if (explorerConfig.explorerMaxColumnRowStroke) {
        maxColumnRowStyle.color = '#' + explorerConfig.explorerMaxColumnRowStroke.substr(3);
        maxColumnRowStyle.opacity = parseInt(explorerConfig.explorerMaxColumnRowStroke.substr(1, 2), 16) / 255;
    }
    if (explorerConfig.explorerMaxClumpSpanStroke) {
        maxClumpSpanStyle.color = '#' + explorerConfig.explorerMaxClumpSpanStroke.substr(3);
        maxClumpSpanStyle.opacity = parseInt(explorerConfig.explorerMaxClumpSpanStroke.substr(1, 2), 16) / 255;
    }
    if (explorerConfig.explorerNewTileFill) {
        explorerNewTileStyle.fillColor = '#' + explorerConfig.explorerNewTileFill.substr(3);
        explorerNewTileStyle.fillOpacity = parseInt(explorerConfig.explorerNewTileFill.substr(1, 2), 16) / 255;
    }
    if (explorerConfig.explorerTileStrokeSat) {
        explorerTileStyleSat.color = '#' + explorerConfig.explorerTileStrokeSat.substr(3);
        explorerTileStyleSat.opacity = parseInt(explorerConfig.explorerTileStrokeSat.substr(1, 2), 16) / 255;
        explorerClumpStyleSat.color = '#' + explorerConfig.explorerTileStrokeSat.substr(3);
        explorerClumpStyleSat.opacity = parseInt(explorerConfig.explorerTileStrokeSat.substr(1, 2), 16) / 255;
    }
    if (explorerConfig.explorerTileFillSat) {
        explorerTileStyleSat.fillColor = '#' + explorerConfig.explorerTileFillSat.substr(3);
        explorerTileStyleSat.fillOpacity = parseInt(explorerConfig.explorerTileFillSat.substr(1, 2), 16) / 255;
    }
    if (explorerConfig.explorerNewTileFillSat) {
        explorerNewTileStyleSat.fillColor = '#' + explorerConfig.explorerNewTileFillSat.substr(3);
        explorerNewTileStyleSat.fillOpacity = parseInt(explorerConfig.explorerNewTileFillSat.substr(1, 2), 16) / 255;
    }
    if (explorerConfig.explorerMaxSquareStrokeSat) {
        maxSquareStyleSat.color = '#' + explorerConfig.explorerMaxSquareStrokeSat.substr(3);
        maxSquareStyleSat.opacity = parseInt(explorerConfig.explorerMaxSquareStrokeSat.substr(1, 2), 16) / 255;
    }
    if (explorerConfig.explorerMaxClumpFillSat) {
        explorerClumpStyleSat.fillColor = '#' + explorerConfig.explorerMaxClumpFillSat.substr(3);
        explorerClumpStyleSat.fillOpacity = parseInt(explorerConfig.explorerMaxClumpFillSat.substr(1, 2), 16) / 255;
    }
    if (explorerConfig.explorerMaxColumnRowStrokeSat) {
        maxColumnRowStyleSat.color = '#' + explorerConfig.explorerMaxColumnRowStrokeSat.substr(3);
        maxColumnRowStyleSat.opacity = parseInt(explorerConfig.explorerMaxColumnRowStrokeSat.substr(1, 2), 16) / 255;
    }
    if (explorerConfig.explorerMaxClumpSpanStrokeSat) {
        maxClumpSpanStyleSat.color = '#' + explorerConfig.explorerMaxClumpSpanStrokeSat.substr(3);
        maxClumpSpanStyleSat.opacity = parseInt(explorerConfig.explorerMaxClumpSpanStrokeSat.substr(1, 2), 16) / 255;
    }
}
var explorerConfigInfoText = '<button style="float:right" class="btn btn-primary btn-mini" onclick="resetExplorerConfig()">Reset</button>Customise your Explorer tile colours (also apply to <a href="http://blog.veloviewer.com/veloviewer-chrome-extension-for-strava-website/" target="_blank">Google Chrome extension</a>).';
var explorerConfigData = [{
    id: 'explorerTileStroke',
    desc: 'Tile border',
    default: '#7FFF0000'
}, {
    id: 'explorerTileFill',
    desc: 'Visited fill',
    default: '#70FF0000'
}, {
    id: 'explorerMaxSquareStroke',
    desc: 'Max Square border',
    default: '#7F0000FF'
}, {
    id: 'explorerMaxClumpFill',
    desc: 'Max Cluster fill',
    default: '#700000FF'
}, {
    id: 'explorerMaxClumpSpanStroke',
    desc: 'Max Cluster span border',
    default: '#7FDD00DD'
}, {
    id: 'explorerMaxColumnRowStroke',
    desc: 'Max Connected Span/Column/Row border',
    default: '#7F00A1A1'
}, {
    id: 'explorerNewTileFill',
    desc: 'New tile fill',
    default: '#70eaff00'
}, {
    id: 'explorerTileStrokeSat',
    desc: 'Tile border',
    default: '#7FFFFFFF'
}, {
    id: 'explorerTileFillSat',
    desc: 'Visited fill',
    default: '#36FFFFFF'
}, {
    id: 'explorerMaxSquareStrokeSat',
    desc: 'Max Square border',
    default: '#7FFFFFFF'
}, {
    id: 'explorerMaxClumpFillSat',
    desc: 'Max Cluster fill',
    default: '#4CFFFFFF'
}, {
    id: 'explorerMaxClumpSpanStrokeSat',
    desc: 'Max Cluster span border',
    default: '#7FFFFFFF'
}, {
    id: 'explorerMaxColumnRowStrokeSat',
    desc: 'Max Connected Span/Column/Row border',
    default: '#7FFFFFFF'
}, {
    id: 'explorerNewTileFillSat',
    desc: 'New tile fill',
    default: '#4CFFFFFF'
}];

function resetExplorerConfig() {
    explorerConfigData.forEach(function(d) {
        $('#' + d.id).spectrum('set', d.default);
        updateExplorerColour.bind(d3.select('#' + d.id).node(), $('#' + d.id).spectrum('get'))();
    });
    d3.select('#explorerConfigModalInfo').html('Saving...');
    $.ajax({
        url: '/api/saveExplorerConfig.php',
        type: "POST",
        data: {
            d: JSON.stringify(explorerConfig)
        },
        async: true,
        success: function(result) {
            if (result.status == 'success') {
                d3.select('#explorerConfigModalInfo').html('Changes saved.');
            } else {
                d3.select('#explorerConfigModalInfo').html('Did not save!');
            }
            setTimeout(showExplorerColour, 5000);
        },
        error: function(result) {
            d3.select('#explorerConfigModalInfo').html('Did not save!');
            setTimeout(showExplorerColour, 5000);
        }
    });
    lm_updateLayerStyles();
}

function lm_updateLayerStyles() {
    var tileStyle = eval('explorerTileStyle' + (lm_currentBaseLayer.id == 'hybrid' ? 'Sat' : ''));
    var tileMinorConnectionStyle = clone(tileStyle);
    tileMinorConnectionStyle.fillOpacity = tileMinorConnectionStyle.fillOpacity / 2;

    for (var att in lm_tiles) {
        lm_tiles[att].setStyle(explorerTiles[att].connectedId == tileConnectionsMaxId ? tileStyle : tileMinorConnectionStyle)
    }
    for (var att in lm_squares) {
        lm_squares[att].setStyle(lm_currentBaseLayer.id == 'hybrid' ? maxSquareStyleSat : maxSquareStyle)
    }
    for (var att in lm_columnRows) {
        lm_columnRows[att].setStyle(lm_currentBaseLayer.id == 'hybrid' ? maxColumnRowStyleSat : maxColumnRowStyle)
    }
    for (var att in lm_clumpSpans) {
        lm_clumpSpans[att].setStyle(lm_currentBaseLayer.id == 'hybrid' ? maxClumpSpanStyleSat : maxClumpSpanStyle)
    }
    var tileClumpStyle = eval('explorerClumpStyle' + (lm_currentBaseLayer.id == 'hybrid' ? 'Sat' : ''));
    //tileClumpStyle.clickable = false;
    var tileMinorClumpStyle = clone(tileClumpStyle);
    tileMinorClumpStyle.fillOpacity = tileMinorClumpStyle.fillOpacity / 2;

    for (var att in lm_clusters) {
        if (lm_clusters[att].maxClump) {
            lm_clusters[att].setStyle(tileClumpStyle)
        } else {
            lm_clusters[att].setStyle(tileMinorClumpStyle);
        }
    }
    lm_tileLines.setStyle(getGridTileLineStyle());
    if (typeof(routeNewTiles) !== 'undefined') {
        routeNewTiles.forEach(function (t) {
            map.removeLayer(t);
        });
        routeNewTiles = [];
    }
}

function lm_showMaxSquareBounds() {
  if(explorerMaxs.length > 0){
    var br = explorerTiles[(explorerMaxs[0].x + explorerMaxs[0].size - 1) + '-' + (explorerMaxs[0].y + explorerMaxs[0].size - 1)];
    if (typeof(br) === 'undefined') {
        br = explorerMaxs[0];
    }
    br = br.br;
    var tl = explorerMaxs[0].tl;
    map.fitBounds([
        [br.y, tl.x],
        [tl.y, br.x]
    ], { padding: [40, 40] });
  }
}

function lm_showClusterBounds() {
    var ca = d3.keys(maxClump).map(function(d) {
        return d.split('-').map(function(e, i) {
            return +e
        })
    });
    if (ca.length == 0) return;
    var cXExt = d3.extent(ca, function(d) {
        return d[0]
    });
    var cYExt = d3.extent(ca, function(d) {
        return d[1]
    });
    var tl = {
        x: tile2long(cXExt[0], 14),
        y: tile2lat(cYExt[0], 14)
    };
    var br = {
        x: tile2long(cXExt[1] + 1, 14),
        y: tile2lat(cYExt[1] + 1, 14)
    };
    //var expCExt = new OpenLayers.Bounds(tl.x - (br.x - tl.x) * 0.25, tl.y - (br.y - tl.y) * 0.25, br.x + (br.x - tl.x) * 0.25, br.y + (br.y - tl.y) * 0.25).transform(fromProjection, toProjection);
    map.fitBounds([
        [br.y, tl.x],
        [tl.y, br.x]
    ], { padding: [40, 40] });

}

function lm_showColumnRowBounds() {
    var maxXArr= Object.values(tileXs).filter(function (d) { return d.maxOrder == 1 });
    var maxYArr = Object.values(tileYs).filter(function (d) { return d.maxOrder == 1 });
    if (maxXArr.length > 0 && maxYArr.length > 0) {
        var minY = Math.min(d3.min(maxYArr, function (d) { return d.id }), d3.min(maxXArr, function (d) { return d.maxYs[0] }));
        var minX = Math.min(d3.min(maxXArr, function (d) { return d.id }), d3.min(maxYArr, function (d) { return d.maxXs[0] }));
        var maxY = Math.max(d3.max(maxYArr, function (d) { return d.id }), d3.max(maxXArr, function (d) { return d.maxYs.last() }));
        var maxX = Math.max(d3.max(maxXArr, function (d) { return d.id }), d3.max(maxYArr, function (d) { return d.maxXs.last() }));
        map.fitBounds([
            [tile2lat(minY, 14), tile2long(minX, 14)],
            [tile2lat(maxY, 14), tile2long(maxX, 14)]
        ], { padding: [40, 40] });
    }
}

var lm_type = 'base';

function getGridTileLineStyle(){
  switch(lm_type){
    case 'explorer':
      //explorer page uses a different style to the user tiles
      return explorerGridLineStyle;
    default:
      return lm_currentBaseLayer.id == 'hybrid' ? explorerTileStyleSat : explorerTileStyle;
  }
}

function lm_showHideTileLines() {
    if (typeof(lm_optionButtons) === 'undefined') return;
    var msi = lm_optionButtons._buttons.map(function(d){return d.type}).indexOf('maxSqaure');
    var mci = lm_optionButtons._buttons.map(function(d){return d.type}).indexOf('maxCluster');
    var cci = lm_optionButtons._buttons.map(function(d){return d.type}).indexOf('maxColumnRow');

    var currentZoom = getZoomForCheck();
    if (currentZoom < 9 || !lm_showtiles || (lm_optionButtons.getBgColor(msi) == '' && lm_optionButtons.getBgColor(mci) == '' && lm_optionButtons.getBgColor(cci) == '')) {
        if (lm_tileLines && localMap.hasLayer(lm_tileLines)) {
            localMap.removeLayer(lm_tileLines);
        }
        if (lm_tileLinesOs && localMap.hasLayer(lm_tileLinesOs)) {
            localMap.removeLayer(lm_tileLinesOs);
        }
    }
    if (currentZoom >= 9 && lm_showtiles && (lm_optionButtons.getBgColor(msi) != '' || lm_optionButtons.getBgColor(mci) != '' || lm_optionButtons.getBgColor(cci) != '')) {
        if(lm_currentBaseLayer.id != osGbCrs){
          if (lm_tileLinesOs && localMap.hasLayer(lm_tileLinesOs)) {
              localMap.removeLayer(lm_tileLinesOs);
          }
          if (lm_tileLines && !localMap.hasLayer(lm_tileLines)) {
            window.setTimeout(function() {
                localMap.addLayer(lm_tileLines);
                lm_tileLines.setStyle(getGridTileLineStyle());
            }, 250);
          }
        }else{
          if (lm_tileLines && localMap.hasLayer(lm_tileLines)) {
              localMap.removeLayer(lm_tileLines);
          }
          window.setTimeout(function() {
            localMap.addLayer(lm_tileLinesOs);
            lm_tileLines.setStyle(getGridTileLineStyle());
          }, 250);
        }
    }
}

function setUnexploredKML() {
    if (typeof(lm_optionButtons) === 'undefined') return;
    if (d3.select('#exportUnexploredKml').node() == null) return;

    d3.selectAll('.kmlExport')
        .attr('href', '#')
        .attr('download', null)
        .classed('disabled', true);

    if (!tilesCalculated) {
      var fAct = a.filter(function(d) {
         return (typeof(d.map) !== 'undefined' && d.map != null) || typeof(texplorer[d.i]) !== 'undefined'
      });
      fAct.forEach(function(d) {
         getActMapTiles(d, explorerZoom, 100);
      });
      tilesCalculated = true;
      var sum;
      var tiles = setExplorer(sum, liveData.filter(function(d) {
      return (typeof(d.map) !== 'undefined' && d.map != null) || typeof(texplorer[d.i]) !== 'undefined'
      }), true);
    }

    var currentZoom = getZoomForCheck();

    if(currentZoom >= 8) {
        var kml = '<?xml version="1.0" encoding="UTF-8"?><kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom"><Document><name>Unexplored Tiles - Veloviewer</name><open>1</open><Style id="s"><LineStyle><color>' + explorerTileStyle.color.replace(/#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})/, 'ff$3$2$1') + '</color><width>1</width></LineStyle></Style><Folder><name>Unexplored Tiles - Veloviewer</name><open>1</open>';

        var tc = 0;
        for (var x = long2tile(map.getBounds().getWest(), 14); x <= long2tile(map.getBounds().getEast(), 14); x++) {
            for (var y = lat2tile(map.getBounds().getNorth(), 14); y <= lat2tile(map.getBounds().getSouth(), 14); y++) {
                if (!explorerTiles[x + '-' + y]) {
                    kml += '<Placemark><styleUrl>#s</styleUrl><LineString><coordinates>' + (+tile2long(x, 14) + (+tile2long(x + 1, 14) - tile2long(x, 14)) / 10).toFixed(6) + ',' + (+tile2lat(y, 14) + (+tile2lat(y + 1, 14) - tile2lat(y, 14)) / 10).toFixed(6) + ' ' + tile2long(x, 14).toFixed(6) + ',' + tile2lat(y, 14).toFixed(6) + ' ' + tile2long(x + 1, 14).toFixed(6) + ',' + tile2lat(y, 14).toFixed(6) + ' ' + tile2long(x + 1, 14).toFixed(6) + ',' + tile2lat(y + 1, 14).toFixed(6) + ' ' + tile2long(x, 14).toFixed(6) + ',' + tile2lat(y + 1, 14).toFixed(6) + ' ' + tile2long(x, 14).toFixed(6) + ',' + tile2lat(y, 14).toFixed(6) + '</coordinates></LineString></Placemark>';
                    tc++;
                }
            }
        }
        kml += '</Folder></Document></kml>';

        var iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;

        window.URL = window.URL || window.webkitURL;

        var egtbu;
        if (iOS) {
            ekmlbu = "data:application/vnd.google-earth.kml+xml;charset=utf-8," + encodeURIComponent(kml);
        } else {
            var blob = new Blob([kml], { type: "application/vnd.google-earth.kml+xml;charset=utf-8" });
            ekmlbu = window.URL.createObjectURL(blob);
        }
        d3.select('#exportUnexploredKml')
            .attr('href', ekmlbu)
            .attr('download', 'unexplored_tiles.kml')
            .classed('disabled', false);

        d3.select('#exportUnexploredKmlText')
            .style('display', null)
            .html(fInt(tc) + ' unexplored tiles in current map view.');
    } else {
        d3.select('#exportUnexploredKmlText')
            .style('display', null)
            .html('Please zoom in.');
    }

    // also create explorered tile kml
    var kml = '<?xml version="1.0" encoding="UTF-8"?><kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom"><Document><name>Explored Tiles - Veloviewer</name><open>1</open><Style id="s"><LineStyle><color>' + explorerTileStyle.color.replace(/#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})/, 'ff$3$2$1') + '</color><width>1</width></LineStyle></Style><Folder><name>Explored Tiles - Veloviewer</name><open>1</open>';

    var tc = 0;
    d3.values(explorerTiles).forEach(function(d) {
        kml += '<Placemark><styleUrl>#s</styleUrl><LineString><coordinates>' + (+tile2long(d.x, 14) + (+tile2long(d.x + 1, 14) - tile2long(d.x, 14)) / 10).toFixed(6) + ',' + (+tile2lat(d.y, 14) + (+tile2lat(d.y + 1, 14) - tile2lat(d.y, 14)) / 10).toFixed(6) + ' ' + tile2long(d.x, 14).toFixed(6) + ',' + tile2lat(d.y, 14).toFixed(6) + ' ' + tile2long(d.x + 1, 14).toFixed(6) + ',' + tile2lat(d.y, 14).toFixed(6) + ' ' + tile2long(d.x + 1, 14).toFixed(6) + ',' + tile2lat(d.y + 1, 14).toFixed(6) + ' ' + tile2long(d.x, 14).toFixed(6) + ',' + tile2lat(d.y + 1, 14).toFixed(6) + ' ' + tile2long(d.x, 14).toFixed(6) + ',' + tile2lat(d.y, 14).toFixed(6) + '</coordinates></LineString></Placemark>';
        tc++;
    });
    kml += '</Folder></Document></kml>';

    var iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;

    window.URL = window.URL || window.webkitURL;

    var egtbu;
    if (iOS) {
        ekmlbu = "data:application/vnd.google-earth.kml+xml;charset=utf-8," + encodeURIComponent(kml);
    } else {
        var blob = new Blob([kml], { type: "application/vnd.google-earth.kml+xml;charset=utf-8" });
        ekmlbu = window.URL.createObjectURL(blob);
    }
    d3.select('#exportExploredKml')
        .attr('href', ekmlbu)
        .attr('download', 'explored_tiles.kml')
        .classed('disabled', false);

    d3.select('#exportExploredKmlText')
        .style('display', null)
        .html(fInt(tc) + ' ticked map tiles.');

    // also create explorered tile kml with 3d

    var maxTileCount = 0;
    for (var att in explorerTiles) {
        maxTileCount = Math.max(maxTileCount, explorerTiles[att].count)
    }
    var colourArray = ['#1a9850', '#66bd63', '#a6d96a', '#d9ef8b', '#ffffbf', '#fee08b', '#fdae61', '#f46d43', '#d73027']; //['#800026', '#bd0026', '#e31a1c', '#fc4e2a', '#fd8d3c', '#feb24c', '#fed976', '#ffeda0', '#ffffcc'];
    var ls = d3.scale.linear().range(colourArray).domain([1,maxTileCount]).domain(colourArray.map(function(d,i){
        return Math.log(maxTileCount) * (i)/(colourArray.length - 1)
    }));

    var kml = '<?xml version="1.0" encoding="UTF-8"?><kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom"><Document><name>Explored Tiles 3D - Veloviewer</name><open>1</open>';
    //kml += '<Style id="s"><LineStyle><color>' + explorerTileStyle.color.replace(/#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})/, 'ff$3$2$1') + '</color><width>1</width></LineStyle><PolyStyle><color>' + explorerTileStyle.color.replace(/#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})/, 'aa$3$2$1') + '</color></PolyStyle></Style>';
    kml += '<Folder><name>Explored Tiles 3D - Veloviewer</name><open>1</open>';

    var tc = 0;
    d3.values(explorerTiles).forEach(function(d) {
      var height = distLatLon(d.tl.y, d.tl.x, d.tl.y, d.br.x);
      var towerHeight = 1;
      if (document.getElementById('exportExploredKml3dFlattenCB').checked) {
        towerHeight = Math.round(height * Math.sqrt(d.count));
      } else {
        towerHeight = height * d.count;
      }
      kml += '<Placemark><Style><LineStyle><color>'+ls(Math.log(d.count)).replace(/#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})/, 'ff$3$2$1')+'</color></LineStyle><PolyStyle><color>'+ls(Math.log(d.count)).replace(/#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})/, 'ff$3$2$1')+'</color></PolyStyle></Style><LineString><extrude>1</extrude><tessellate>1</tessellate><altitudeMode>absolute</altitudeMode><coordinates>' + tile2long(d.x, 14).toFixed(6) + ',' + tile2lat(d.y, 14).toFixed(6) + ','+towerHeight + ' ' + tile2long(d.x + 1, 14).toFixed(6) + ',' + tile2lat(d.y, 14).toFixed(6) + ','+towerHeight+' ' + tile2long(d.x + 1, 14).toFixed(6) + ',' + tile2lat(d.y + 1, 14).toFixed(6) + ','+towerHeight+' ' + tile2long(d.x, 14).toFixed(6) + ',' + tile2lat(d.y + 1, 14).toFixed(6) + ','+towerHeight+' ' + tile2long(d.x, 14).toFixed(6) + ',' + tile2lat(d.y, 14).toFixed(6) + ','+towerHeight+'</coordinates></LineString></Placemark>';
        tc++;
    });
    kml += '</Folder></Document></kml>';

    var iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;

    window.URL = window.URL || window.webkitURL;

    var egtbu;
    if (iOS) {
        ekmlbu = "data:application/vnd.google-earth.kml+xml;charset=utf-8," + encodeURIComponent(kml);
    } else {
        var blob = new Blob([kml], { type: "application/vnd.google-earth.kml+xml;charset=utf-8" });
        ekmlbu = window.URL.createObjectURL(blob);
    }

    d3.select('#exportExploredKml3d')
        .attr('href', ekmlbu)
        .attr('download', 'explored_tiles_3d.kml')
        .classed('disabled', false);
}

function getSelectedBaseLayer(options){
  switch (getCookie('lmap')) {
      /*case "live":
          if (lm_baseLayers.Live) {
              options.layers = [lm_layerLive];
          }*/
      case "custom":
          if (!options.layers && lm_baseLayers.Custom) {
              options.layers = [lm_layerCustom];
          }
      case "landscape":
          if (!options.layers) {
              options.layers = [lm_layerLandscape];
          }
          break;
      case "veloviewer":
          options.layers = [lm_layerVeloviewer];
          break;
      case "ocm":
          options.layers = [lm_layerCycle];
          break;
      case "outdoors":
          options.layers = [lm_layerOutdoors];
          break;
      case "osm":
          options.layers = [lm_layerOSM];
          break;
      case "terrain":
          options.layers = [lm_layerGTerrain];
          break;
      case "hybrid":
          options.layers = [lm_layerGHybrid];
          break;
      case "satellite":
          options.layers = [lm_layerGSatellite];
          break;
      case osGbLight:
          options.layers = [lm_layerOsLight];
          break;
      case osGbCrs.toString():
          options.layers = [lm_layer27700];
          break;
      default:
          /*if (lm_baseLayers.Live) {
              options.layers = [lm_layerLive];
          }*/
          if (!options.layers && lm_baseLayers.Custom) {
              options.layers = [lm_layerCustom];
          }
          if (!options.layers) {
              options.layers = [lm_layerVeloviewer];
          }
  }
}

function initMap(suffix, useCanvas) {
    if (typeof(cpref) !== 'undefined') lm_mapLine.opacity = typeof(getCookie(cpref + 'o')) === 'undefined' ? 0.5 : getCookie(cpref + 'o');

    if (typeof(suffix) === 'undefined' || suffix == null) suffix = '';
    if (mapInitiated['map' + suffix] == 1) return;

    mapInitiated['map' + suffix] = 1;

    d3.selectAll('#mapDomObj' + suffix).style('border', 'solid 1px #bbb').html('');

    var options = {
        zoomSnap: 0.1,
        zoomControl: false,
        fullscreenControl: !embed,
        fullscreenControlOptions: {
            position: 'topright'
        }
    };

    if (typeof(embed) === 'undefined' || !embed) {
        getSelectedBaseLayer(options);
    } else {
        options.layers = [lm_layerVeloviewer];
    }

    lm_currentBaseLayer = options.layers[0];

    if (useCanvas) {
        options.renderer = L.canvas();
        //options.preferCanvas = true;
    }

    eval('map' + suffix + ' = new L.Map("mapDomObj' + suffix + '", options)');
    localMap = eval('map' + suffix);

    L.control.scale({
        metric: lrgLenUnit == 'km',
        imperial: lrgLenUnit != 'km',
        position: 'bottomleft'
    }).addTo(localMap);

    if (typeof (cpref) !== 'undefined' && cpref.substring(0, 1) != 's') {
        if (typeof(L.control.mousePosition) !== 'undefined') {
            L.control.mousePosition({ lngFormatter: longForm, latFormatter: latForm, separator: '-', emptyString: '', lngFirst: true, prefix: 'Tile: ' }).addTo(localMap);
        }

        /*L.Control.ExplorerInfo = L.Control.extend({
            _container: null,
            options: {
                position: 'bottomleft'
            },
            onAdd: function (map) {
                var div = L.DomUtil.create(
                    "div",
                    "leaflet-control-mouseposition"
                );
                div.title = 'VeloViewer Explorer Tile X-Y for mouse position';
                this._container = div;
                return div;
            },
            updateHTML: function (x, y) {
                this._container.innerHTML = 'Tile: ' + x + '-' + y;
            }
        });

        var explorerInfo = new L.Control.ExplorerInfo();
        localMap.addControl(explorerInfo);

        localMap.addEventListener('mousemove', (event) => {
            let x = long2tile(event.latlng.lng, 14);
            let y = lat2tile(event.latlng.lat, 14);
            explorerInfo.updateHTML(x, y);
        });*/
    }

    if (L.photo) {
      photoLayer = L.photo.cluster().on('click', function(evt) {
        var d = evt.layer.photo;
        var mw, mh = Math.min(localMap._container.getBoundingClientRect().height-150, 380);
        if (mh * d.x/d.y > 380 || mh * d.x/d.y > localMap._container.getBoundingClientRect().width-50) {
          mw = Math.min(localMap._container.getBoundingClientRect().width-50, 380);
          mh = mw * d.y/d.x;
        } else {
          mw = mh * d.x/d.y;
        }
          var template = '<a target="_blank" href="' + d.u2048 + '""><img src="' + d.url + '" width="' + mw + '" height="' + mh + '" style="max-width: 100%;height: auto;"/>' + (typeof (d.videoUrl) !== 'undefined' ? '<span style="position:absolute;left:10%;top:5%;font-size:5em;opacity:0.5;color: #FF032E;">▶</span>' : '') +'</a><p style="max-width:'+mw+'px">{caption}</p>';

        evt.layer.bindPopup(L.Util.template(template, d), {
          className: 'leaflet-popup-photo',
          minWidth: mw
        }).openPopup();
      });
    }

    new L.Control.Zoom({ position: 'bottomright' }).addTo(localMap);

    localMap.suffix = suffix;

    zwiftMaps = L.layerGroup();
    z_london = L.imageOverlay('https://cf.veloviewer.com/Zwift/MiniMap_London.jpg', [
        [51.5362, -0.1776],
        [51.4601, -0.0555]
    ]).addTo(zwiftMaps);
    z_london.v_north = 51.5362;
    z_london.v_south = 51.4601;
    z_london.v_east = -0.0555;
    z_london.v_west = -0.1776;
    z_london.v_url = 'https://cf.veloviewer.com/Zwift/MiniMap_London.jpg';
    z_watopia = L.imageOverlay('https://cf.veloviewer.com/Zwift/MiniMap_Watopia_V4.jpg', [
        [-11.626,166.87747],
        [-11.729,167.03255]
    ]).addTo(zwiftMaps);
    z_watopia.v_north = -11.626;
    z_watopia.v_south = -11.729;
    z_watopia.v_east = 167.03255;
    z_watopia.v_west = 166.87747;
    z_watopia.v_url = 'https://cf.veloviewer.com/Zwift/MiniMap_Watopia_V4.jpg';
    z_newyork = L.imageOverlay('https://cf.veloviewer.com/Zwift/MiniMap_NewYork_V2.jpg', [
        [[40.81725, -74.0227], [40.6385, -73.9222]]
    ]).addTo(zwiftMaps);
    z_newyork.v_north = 40.81725;
    z_newyork.v_south = 40.6385;
    z_newyork.v_east = -73.9222;
    z_newyork.v_west = -74.0227;
    z_newyork.v_url = 'https://cf.veloviewer.com/Zwift/MiniMap_NewYork_V2.jpg';
    z_richmond = L.imageOverlay('https://cf.veloviewer.com/Zwift/MiniMap_Richmond.jpg', [
        [37.5774, -77.48954],
        [37.5014, -77.394]
    ]).addTo(zwiftMaps);
    z_richmond.v_north = 37.5774;
    z_richmond.v_south = 37.5014;
    z_richmond.v_east = -77.394;
    z_richmond.v_west = -77.48954;
    z_richmond.v_url = 'https://cf.veloviewer.com/Zwift/MiniMap_Richmond.jpg';
    z_innsbruck = L.imageOverlay('https://cf.veloviewer.com/Zwift/MiniMap_Innsbruck.jpg', [
        [47.2947, 11.3501],
        [47.2055, 11.4822]
    ]).addTo(zwiftMaps);
    z_innsbruck.v_north = 47.2947;
    z_innsbruck.v_south = 47.2055;
    z_innsbruck.v_east = 11.4822;
    z_innsbruck.v_west = 11.3501;
    z_innsbruck.v_url = 'https://cf.veloviewer.com/Zwift/MiniMap_Innsbruck.jpg';
    z_yorkshire = L.imageOverlay('https://cf.veloviewer.com/Zwift/MiniMap_Yorkshire.jpg', [
        [54.0254, -1.6320],
        [53.9491, -1.5022]
    ]).addTo(zwiftMaps);
    z_yorkshire.v_north = 54.0254;
    z_yorkshire.v_south = 53.9491;
    z_yorkshire.v_east = -1.5022;
    z_yorkshire.v_west = -1.6320;
    z_yorkshire.v_url = 'https://cf.veloviewer.com/Zwift/MiniMap_Yorkshire.jpg';
    z_critcity = L.imageOverlay('https://cf.veloviewer.com/Zwift/MiniMap_CritCity.jpg', [
        [-10.3657, 165.7824],
        [-10.4038, 165.8207]
    ]).addTo(zwiftMaps);
    z_critcity.v_north = z_critcity.getBounds().getNorth();
    z_critcity.v_south = z_critcity.getBounds().getSouth();
    z_critcity.v_east = z_critcity.getBounds().getEast();
    z_critcity.v_west = z_critcity.getBounds().getWest();
    z_critcity.v_url = 'https://cf.veloviewer.com/Zwift/MiniMap_CritCity.jpg';

    z_france = L.imageOverlay('https://cf.veloviewer.com/Zwift/MiniMap_France.jpg', [
      [-21.64155, 166.1384],
      [-21.7564, 166.26125]
    ]).addTo(zwiftMaps);
    z_france.v_url = z_france._url;
    z_france.v_north = z_france.getBounds().getNorth();
    z_france.v_south = z_france.getBounds().getSouth();
    z_france.v_east = z_france.getBounds().getEast();
    z_france.v_west = z_france.getBounds().getWest();

    z_italy = L.imageOverlay('https://cf.veloviewer.com/Zwift/MiniMap_Italy.jpg', [
        [44.5308037, 11.26261748],
        [44.45463821, 11.36991729102076]
    ]).addTo(zwiftMaps);
    z_italy.v_url = z_italy._url;
    z_italy.v_north = z_italy.getBounds().getNorth();
    z_italy.v_south = z_italy.getBounds().getSouth();
    z_italy.v_east = z_italy.getBounds().getEast();
    z_italy.v_west = z_italy.getBounds().getWest();
    z_italy.v_url = 'https://cf.veloviewer.com/Zwift/MiniMap_Italy.jpg';

    z_champs = L.imageOverlay('https://cf.veloviewer.com/Zwift/MiniMap_Champs.jpg', [
        [48.9058, 2.2561],
        [48.82945, 2.3722]
    ]).addTo(zwiftMaps);
    z_champs.v_url = z_champs._url;
    z_champs.v_north = z_champs.getBounds().getNorth();
    z_champs.v_south = z_champs.getBounds().getSouth();
    z_champs.v_east = z_champs.getBounds().getEast();
    z_champs.v_west = z_champs.getBounds().getWest();

    z_makuri = L.imageOverlay('https://cf.veloviewer.com/Zwift/MiniMap_Makuri_v4.jpg', [
        [-10.7375, 165.7828],
        [-10.831, 165.8772]
    ]).addTo(zwiftMaps);
    z_makuri.v_url = z_makuri._url;
    z_makuri.v_north = z_makuri.getBounds().getNorth();
    z_makuri.v_south = z_makuri.getBounds().getSouth();
    z_makuri.v_east = z_makuri.getBounds().getEast();
    z_makuri.v_west = z_makuri.getBounds().getWest();

    z_scotland = L.imageOverlay('https://cf.veloviewer.com/Zwift/MiniMap_Scotland.jpg', [[55.675959999999996,-5.28053],[55.6185,-5.17753]]).addTo(zwiftMaps);
    z_scotland.v_url = z_scotland._url;
    z_scotland.v_north = z_scotland.getBounds().getNorth();
    z_scotland.v_south = z_scotland.getBounds().getSouth();
    z_scotland.v_east = z_scotland.getBounds().getEast();
    z_scotland.v_west = z_scotland.getBounds().getWest();

    z_overlay = {
        'Zwift maps': zwiftMaps
    };

    if (typeof(embed) === 'undefined' || !embed) {
        var layerControl = L.control.layers(lm_baseLayers /*, z_overlay*/ ).addTo(localMap);
        layerControl.setPosition('topleft');

        if (lm_ZwiftMapsMarker) {
            zwiftMaps.addTo(localMap);
        }

        localMap.on('moveend', function() {
            if (typeof(filterActiveColumns) !== 'undefined') {
                var filterId = jsonIndexOf(filterActiveColumns, 'field', 'mapFilter');
                if ((filterId > -1) &&
                    (filterActiveColumns[filterId].filterActive == 1)) {
                    var ex = localMap.getBounds();

                    filterActiveColumns[filterId].minLat = ex._southWest.lat;
                    filterActiveColumns[filterId].maxLat = ex._northEast.lat;
                    filterActiveColumns[filterId].minLng = ex._southWest.lng;
                    filterActiveColumns[filterId].maxLng = ex._northEast.lng;

                    setMapFilterText(filterActiveColumns[filterId]);

                    clearTimeout(refreshTimer);

                    refreshTimer = setTimeout(function() {
                        dataLoaded()
                    }, Math.min(updateTime * 1.1, 250));
                }
            }
        });

        localMap.on('popupopen', function(e) {
          var id = -1;
          if(typeof(e.popup._source) !== 'undefined' && typeof(e.popup._source.vv_type) !== 'undefined'){
            if (e.popup._source.vv_type && e.popup._source.vv_type == 'mapLine') {
              id =  e.popup._source.vv_id;
            }
            if (e.popup._source.photo) {
              id =  e.popup._source.photo.act.i;
            }
          }
            if (id > -1) {
                e.popup._source.isPopupOpen = true;
                var loffset = Math.floor(liveData.map(function(d) {
                    return d.i
                }).indexOf(id) / rowCount);
                if (loffset != offset) {
                    offset = loffset;
                    dataLoaded(true);
                }
                d3.select('#tr' + id).classed('info', true);
                if (photosShownChecked) {
                  drawPhotosScroll(id);
                  d3.selectAll('.photo' + id)
                      .style('background', '#FF032E');
                }
            }
        });
        localMap.on('popupclose', function(e) {
          if(typeof(e.popup._source) !== 'undefined' && typeof(e.popup._source.vv_type) !== 'undefined'){
            if (e.popup._source.vv_type && e.popup._source.vv_type == 'mapLine') {
                e.popup._source.isPopupOpen = false;
                e.popup._source.setStyle(lm_mapLine);
                d3.select('#tr' + e.popup._source.vv_id).classed('info', false);
                if (photosShownChecked) {
                  d3.selectAll('.photo' + e.popup._source.vv_id)
                      .style('background', null);
                }
            }
            if (e.popup._source.photo) {
                e.popup._source.isPopupOpen = false;
                d3.select('#tr' + e.popup._source.photo.act.i).classed('info', false);
                if (photosShownChecked) {
                  d3.selectAll('.photo' + e.popup._source.photo.act.i)
                      .style('background', null);
                }
            }
          }
        });

        if (typeof(cpref) !== 'undefined' && suffix != 2) {

            lm_addOptionButtons();

            addModalPopup('map' + suffix + 'Config', 'Map Settings', undefined, 0, d3.select(localMap._container));

            d3.select('#map' + suffix + 'ConfigModal').on('mouseover', function() {
                localMap.dragging.disable();
                localMap.scrollWheelZoom.disable();
            })
            d3.select('#map' + suffix + 'ConfigModal').on('mouseout', function() {
                localMap.dragging.enable();
                localMap.scrollWheelZoom.enable();
            })

            lm_addModalPopupContents(suffix);
        }

        if (typeof(tilePopupFunction) !== 'undefined') {
            localMap.on('click', tilePopupFunction);
        }
    } else {
        if (getParameterByName('zwift') == 1) {
            zwiftMaps.addTo(localMap);
        }
    }

    /*var btn = L.functionButtons([{ content: '<span style="color:red">Sqr</span>', bgColor: '#444' }, { content: 'Clu' }]);
    btn.on('clicked', function(data) {
        if (data.idx == 0) {
            localMap.setView([59.939, 30.315], 13);
            btn.setBgColor(0, null);
        } else {
            localMap.setView([53.3, -1.7], 9);
        }
    });
    localMap.addControl(btn);

    // start the map in South-East England
    localMap.setView(new L.LatLng(53.3, -1.7), 6);*/


    // draw explorer tiles
    var tileCoords = [], tileCoordsOs = []; //, tmpLat = [], tmpLng = [];
    var tmpCoord = 0;
    for (var i = 0; i <= 16384; i++) {
        tmpCoord = tile2lat(i, 14);
        if (i % 2 == 0) {
            tileCoords.push([tmpCoord, -180]);
            tileCoords.push([tmpCoord, 180]);
        } else {
            tileCoords.push([tmpCoord, 180]);
            tileCoords.push([tmpCoord, -180]);
        }
    }
    for (var i = 16384; i >= 0; i--) {
        tmpCoord = tile2long(i, 14);
        if (i % 2 == 0) {
            tileCoords.push([-90, tmpCoord]);
            tileCoords.push([90, tmpCoord]);
        } else {
            tileCoords.push([90, tmpCoord]);
            tileCoords.push([-90, tmpCoord]);
        }
    }


    //OS Bounds = [7699, -10.83251953125] [8535, 7.53662109375] [4545, 62.257696189351215] [5599, 49.39667507519397]
    for(var i=4545; i<=5599; i++){
      tmpCoord = tile2lat(i, 14);
      if (i % 2 == 0) {
        for(var j=7699; j<=8535; j++){
          tileCoordsOs.push([tmpCoord, tile2long(j, 14)]);
        }
      }else{
        for(var j=8535; j>=7699; j--){
          tileCoordsOs.push([tmpCoord, tile2long(j, 14)]);
        }
      }
    }
    for(var i=7699; i<=8535; i++){
      tmpCoord = tile2long(i, 14);
      if (i % 2 == 0) {
        for(var j=4545; j<=5599; j++){
          tileCoordsOs.push([tile2lat(j, 14), tmpCoord]);
        }
      }else{
        for(var j=5599; j>=4545; j--){
          tileCoordsOs.push([tile2lat(j, 14), tmpCoord]);
        }
      }
    }

    lm_tileLines = L.polyline(tileCoords, getGridTileLineStyle());
    lm_tileLinesOs = L.polyline(tileCoordsOs, getGridTileLineStyle());


    localMap.on('layerremove', function(e) {
      prevZoom(localMap.getZoom(), false);
      console.debug('layerremove = ' + localMap.getZoom());
    });

    localMap.on('zoomstart', function(e) {
        console.debug('zoomstart = ' + localMap.getZoom());
    });

    localMap.on("moveend", function (e) {
      if (typeof(lm_onmoveend) !== 'undefined') lm_onmoveend();
    });

    localMap.on('zoomend', function(e) {
        lm_showHideTileLines();

        console.debug('zoomend = ' + localMap.getZoom());
        if(_prevZoomLock > 0 && localMap.getZoom() == 9){
          localMap._resetView(localMap.getCenter(), prevZoom(), true);
        }

        if (typeof(lm_onzoomend) !== 'undefined') lm_onzoomend();
    });

    localMap.on('baselayerchange', function(e) {
        if(e.layer.id.startsWith(osGbCrs) == true && os_ag == 0){
          if(window.confirm('Changes to the Ordnance Survey Mapping API terms and conditions for their premium mapping services require VeloViewer to share your personal details with them upon request.  Do you consent?')){
            $.ajax({
                url: '/api/setting/101/1'
            });
            os_ag = 1;
          }else{
            //reset back to original
            var baseLayers = Object.values(lm_baseLayers), f = 0;
            for(var i=0; i < baseLayers.length; i++) {
              if(baseLayers[i].id == lm_currentBaseLayer.id){
                f = i;
                break;
              }
            }
            $('.leaflet-control-layers-selector')[f].click();
            e.layer = lm_currentBaseLayer;
            return;
          }
        }

        var crsChanged = false;
        var center = localMap.getCenter();
        var zoom = (prevZoom() != null) ? prevZoom() : localMap.getZoom();
        var bounds = localMap.getBounds();

        if(lm_currentBaseLayer.id == osGbCrs && e.layer.id != osGbCrs){ //||
          //(lm_currentBaseLayer.id.startsWith(osGbCrs) == true && e.layer.id.startsWith(osGbCrs) == true))//map init fires but based on the wrong crs zoom level
          zoom = getGZoomFromOSZoom(zoom);
          crsChanged = true;
          console.debug('baselayerchange = ' + lm_currentBaseLayer.id + ' -> ' + e.layer.id + '; zoom = ' + prevZoom() + ' -> ' + zoom);
        }

        if(lm_currentBaseLayer.id != osGbCrs && e.layer.id == osGbCrs){
          zoom = getOsZoomFromGZoom(zoom);
          crsChanged = true;
          console.debug('baselayerchange = ' + lm_currentBaseLayer.id + ' -> ' + e.layer.id + '; zoom = ' + prevZoom() + ' -> ' + zoom);
        }

        if(prevZoom() == null && lm_currentBaseLayer.id == osGbCrs && e.layer.id == osGbCrs){
          //page loaded witrh OS leisure maps...convert the zoom to OS equivalent
          zoom = getOsZoomFromGZoom(zoom);
        }

        if(prevZoom() == null && ((lm_currentBaseLayer.id == osGbCrs && e.layer.id == osGbCrs) || (lm_currentBaseLayer.id == osGbLight && e.layer.id == osGbLight))){
          //page loaded with OS maps, pan to view if current bounds do not intersect (not viewable)
          center = osGbBounds.getCenter();
        }

        if(prevZoom() != null && (e.layer.id == osGbCrs || e.layer.id == osGbLight)){
          if(osGbBounds.contains(center) == false)
            center = osGbBounds.getCenter();
        }

        if(e.layer.id == osGbLight && zoom < 6.5){
          zoom = 6.5;
        }

        //set cookie
        lm_currentBaseLayer = e.layer;
        setSelectedBaseLayer();

        d3.selectAll('.os-logo-control').remove();

        localMap.options.crs = e.layer.id == osGbCrs ? osCrs() : L.CRS.EPSG3857;
        if(crsChanged) preparePhotos();
        console.debug('baselayerchange = ' + lm_currentBaseLayer.id + ', center = ' + center.lat + ' ' + center.lng + ', zoom = ' + zoom);
        localMap._resetView(center, zoom, true);
        prevZoom(zoom, true);
        if(crsChanged) reloadPhotos();

    });

    if (typeof(setExtent) !== 'undefined') {
        if (cpref == 'sd') {
            if (suffix == '' || suffix == '3') {
                setExtent();
            }
        } else {
            setExtent(applyFilters(a));
        }
    }
}

function setSelectedBaseLayer(){
  setCookie('lmap', lm_currentBaseLayer.id, 365);
}

function lm_addOptionButtons(){

  var lButtons = [];

    if (cpref == 'a' || cpref == 'r') {
        if (cpref == 'a') {
            lButtons.push({
                content: '<i class="icon-camera"></i>',
                title: 'View photos',
                bgColor: +getCookie('PhotosShown') || typeof(getCookie('PhotosShown')) === 'undefined' ? '#bbb' : null,
                type: 'photos'
            });
        }
        lButtons.push({
            content: '&#9634',
            title: 'View Explorer Max Square',
            bgColor: +getCookie('ExplorerMaxSquareShown') ? '#bbb' : null,
            type: 'maxSqaure'
        });
        lButtons.push({
            title: 'View Explorer Max Cluster',
            bgColor: +getCookie('ExplorerClusterShown') ? '#bbb' : null,
            content: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" style="margin-left: -3px;margin-top: -2px;"><style type="text/css">.fg {fill:#333;stroke-width: 0px;}.bg {fill:white;stroke-width: 0px;}</style><g><rect class="fg" x="15" y="9" width="1" height="1"/><rect class="fg" x="14" y="9" width="1" height="1"/><rect class="fg" x="13" y="9" width="1" height="1"/><rect class="fg" x="14" y="8" width="1" height="1"/><rect class="fg" x="14" y="10" width="1" height="1"/><rect class="fg" x="12" y="9" width="1" height="1"/><rect class="fg" x="13" y="8" width="1" height="1"/><rect class="fg" x="13" y="10" width="1" height="1"/><rect class="fg" x="14" y="7" width="1" height="1"/><rect class="fg" x="11" y="9" width="1" height="1"/><rect class="fg" x="12" y="8" width="1" height="1"/><rect class="fg" x="12" y="10" width="1" height="1"/><rect class="fg" x="13" y="7" width="1" height="1"/><rect class="fg" x="14" y="6" width="1" height="1"/><rect class="fg" x="10" y="9" width="1" height="1"/><rect class="fg" x="11" y="8" width="1" height="1"/><rect class="fg" x="11" y="10" width="1" height="1"/><rect class="fg" x="12" y="7" width="1" height="1"/><rect class="fg" x="13" y="6" width="1" height="1"/><rect class="fg" x="14" y="5" width="1" height="1"/><rect class="fg" x="9" y="9" width="1" height="1"/><rect class="fg" x="10" y="8" width="1" height="1"/><rect class="fg" x="10" y="10" width="1" height="1"/><rect class="fg" x="11" y="7" width="1" height="1"/><rect class="fg" x="12" y="6" width="1" height="1"/><rect class="fg" x="13" y="5" width="1" height="1"/><rect class="fg" x="14" y="4" width="1" height="1"/><rect class="fg" x="8" y="9" width="1" height="1"/><rect class="fg" x="9" y="8" width="1" height="1"/><rect class="fg" x="9" y="10" width="1" height="1"/><rect class="fg" x="10" y="7" width="1" height="1"/><rect class="fg" x="10" y="11" width="1" height="1"/><rect class="fg" x="11" y="6" width="1" height="1"/><rect class="fg" x="12" y="5" width="1" height="1"/><rect class="fg" x="13" y="4" width="1" height="1"/><rect class="fg" x="14" y="3" width="1" height="1"/><rect class="fg" x="7" y="9" width="1" height="1"/><rect class="fg" x="8" y="8" width="1" height="1"/><rect class="fg" x="8" y="10" width="1" height="1"/><rect class="fg" x="9" y="7" width="1" height="1"/><rect class="fg" x="9" y="11" width="1" height="1"/><rect class="fg" x="10" y="6" width="1" height="1"/><rect class="fg" x="11" y="5" width="1" height="1"/><rect class="fg" x="12" y="4" width="1" height="1"/><rect class="fg" x="13" y="3" width="1" height="1"/><rect class="fg" x="15" y="3" width="1" height="1"/><rect class="fg" x="6" y="9" width="1" height="1"/><rect class="fg" x="7" y="8" width="1" height="1"/><rect class="fg" x="7" y="10" width="1" height="1"/><rect class="fg" x="8" y="7" width="1" height="1"/><rect class="fg" x="8" y="11" width="1" height="1"/><rect class="fg" x="9" y="6" width="1" height="1"/><rect class="fg" x="9" y="12" width="1" height="1"/><rect class="fg" x="10" y="5" width="1" height="1"/><rect class="fg" x="11" y="4" width="1" height="1"/><rect class="fg" x="5" y="9" width="1" height="1"/><rect class="fg" x="6" y="8" width="1" height="1"/><rect class="fg" x="6" y="10" width="1" height="1"/><rect class="fg" x="7" y="7" width="1" height="1"/><rect class="fg" x="7" y="11" width="1" height="1"/><rect class="fg" x="8" y="6" width="1" height="1"/><rect class="fg" x="8" y="12" width="1" height="1"/><rect class="fg" x="9" y="5" width="1" height="1"/><rect class="fg" x="9" y="13" width="1" height="1"/><rect class="fg" x="4" y="9" width="1" height="1"/><rect class="fg" x="5" y="8" width="1" height="1"/><rect class="fg" x="5" y="10" width="1" height="1"/><rect class="fg" x="6" y="7" width="1" height="1"/><rect class="fg" x="6" y="11" width="1" height="1"/><rect class="fg" x="7" y="6" width="1" height="1"/><rect class="fg" x="7" y="12" width="1" height="1"/><rect class="fg" x="8" y="5" width="1" height="1"/><rect class="fg" x="8" y="13" width="1" height="1"/><rect class="fg" x="9" y="4" width="1" height="1"/><rect class="fg" x="10" y="13" width="1" height="1"/><rect class="fg" x="9" y="14" width="1" height="1"/><rect class="fg" x="3" y="9" width="1" height="1"/><rect class="fg" x="4" y="8" width="1" height="1"/><rect class="fg" x="4" y="10" width="1" height="1"/><rect class="fg" x="5" y="7" width="1" height="1"/><rect class="fg" x="5" y="11" width="1" height="1"/><rect class="fg" x="6" y="12" width="1" height="1"/><rect class="fg" x="7" y="13" width="1" height="1"/><rect class="fg" x="8" y="4" width="1" height="1"/><rect class="fg" x="8" y="14" width="1" height="1"/><rect class="fg" x="10" y="14" width="1" height="1"/><rect class="fg" x="9" y="15" width="1" height="1"/><rect class="fg" x="2" y="9" width="1" height="1"/><rect class="fg" x="3" y="8" width="1" height="1"/><rect class="fg" x="3" y="10" width="1" height="1"/><rect class="fg" x="4" y="7" width="1" height="1"/><rect class="fg" x="4" y="11" width="1" height="1"/><rect class="fg" x="5" y="12" width="1" height="1"/><rect class="fg" x="6" y="13" width="1" height="1"/><rect class="fg" x="7" y="14" width="1" height="1"/><rect class="fg" x="7" y="4" width="1" height="1"/><rect class="fg" x="8" y="15" width="1" height="1"/><rect class="fg" x="11" y="14" width="1" height="1"/><rect class="fg" x="10" y="15" width="1" height="1"/><rect class="fg" x="2" y="10" width="1" height="1"/><rect class="fg" x="3" y="7" width="1" height="1"/><rect class="fg" x="3" y="11" width="1" height="1"/><rect class="fg" x="4" y="6" width="1" height="1"/><rect class="fg" x="2" y="11" width="1" height="1"/><rect class="fg" x="3" y="12" width="1" height="1"/></g></svg>',
            type: 'maxCluster'
        });
        lButtons.push({
            title: 'View Explorer Max Connected Span/Column/Row',
            bgColor: +getCookie('ExplorerColumnRowShown') ? '#bbb' : null,
            content: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" style="margin-top: 7px;"><style type="text/css">.fg {fill:#333;stroke-width: 0px;}.bg {fill:white;stroke-width: 0px;}</style><g><rect class="fg" x="0" y="7" width="16" height="2"/><rect class="fg" x="7" y="0" width="2" height="16"/></g></svg>',
            type: 'maxColumnRow'
        });
        lButtons.push({
            content: '<div style="padding: 5px 0px 0px 2px;width:16px" id="showZwiftMaps">' + zwiftIconSVG + '</div>',
            bgColor: lm_ZwiftMapsMarker ? '#bbb' : null,
            title: 'Show Zwift maps',
            type: 'zwift'
        });
        lButtons.push({
            content: '<i class="icon-cog"></i>',
            title: 'Map settings',
            type: 'settings'
        });

        lm_CurrentPositionButton = L.functionButtons([{
            content: '<i class="icon-screenshot"></i>',
            title: 'Show your location'
        }], {
            position: 'bottomright'
        });

        lm_CurrentPositionButton.on('clicked', function(data) {
            if (lm_CurrentPositionButton.getBgColor(data.idx) == '') {
                lm_CurrentPositionButton.setBgColor(data.idx, '#bbb');
                watchPosition = navigator.geolocation.watchPosition(function(pos) {
                    var firstTime = true;
                    if (watchPositionMarker != null) {
                        firstTime = false;
                        localMap.removeLayer(watchPositionMarker);
                    }
                    watchPositionMarker = new L.Circle([pos.coords.latitude, pos.coords.longitude], {
                        radius: Math.max(10, pos.coords.accuracy)
                    });
                    watchPositionMarker.addTo(localMap);
                    if (firstTime) {
                        if(getCookie('lmap')==osGbCrs){
                          localMap.setZoom(0);
                        }else{
                          localMap.setZoom(15);
                        }
                        if (watchPositionTimer != null) {
                            window.clearTimeout(watchPositionTimer);
                        }
                        watchPositionTimer = window.setTimeout(function() {
                            watchPositionTimer = null;
                        }, 5200);
                    }
                    if (watchPositionTimer != null) {
                        localMap.panTo(new L.LatLng(pos.coords.latitude, pos.coords.longitude));
                    }
                }, function(err) {
                    //lm_CurrentPositionButton.setBgColor(data.idx, null);
                    console.warn('ERROR(' + err.code + '): ' + err.message);
                }, {
                    enableHighAccuracy: true,
                    timeout: 1000,
                    maximumAge: 0
                });
            } else {
                lm_CurrentPositionButton.setBgColor(data.idx, null);
                if (watchPosition != null) {
                    navigator.geolocation.clearWatch(watchPosition);
                    watchPosition = null;
                }
                if (watchPositionMarker != null) {
                    localMap.removeLayer(watchPositionMarker);
                    watchPositionMarker = null;
                }
            }
        });
        localMap.addControl(lm_CurrentPositionButton);
    } else {
        lButtons = [{
            content: '<div style="padding: 5px 0px 0px 2px;width:16px" id="showZwiftMaps">' + zwiftIconSVG + '</div>',
            bgColor: lm_ZwiftMapsMarker ? '#bbb' : null,
            title: 'Show Zwift maps',
            type: 'zwift'
        }, {
            content: '<i class="icon-cog"></i>',
            title: 'Map settings',
            type: 'settings'
        }];
    }

  lm_optionButtons = L.functionButtons(lButtons, {
      position: 'topright'
  });

  lm_optionButtons.on('clicked', function(data) {
      if (cpref == 'a' || cpref == 'r') {
          switch (lButtons[data.idx].type) {
            case 'photos':
                if (lm_optionButtons.getBgColor(data.idx) == '') {
                    lm_optionButtons.setBgColor(data.idx, '#bbb');
                    photoLayer.addTo(localMap);
                    setCookie('PhotosShown', 1, 355);
                } else {
                    lm_optionButtons.setBgColor(data.idx, null);
                    localMap.removeLayer(photoLayer);
                    setCookie('PhotosShown', 0, 355);
                }
                break;
                case 'maxSqaure':
                    if (lm_optionButtons.getBgColor(data.idx) == '') {
                        lm_optionButtons.setBgColor(data.idx, '#bbb');
                        showExplorerTiles(lm_showMaxSquareBounds);
                        setCookie('ExplorerMaxSquareShown', 1, 355);
                    } else {
                        lm_optionButtons.setBgColor(data.idx, null);
                        setCookie('ExplorerMaxSquareShown', 0, 355);
                        showExplorerTiles();
                    }
                    break;
              case 'maxCluster':
                  if (lm_optionButtons.getBgColor(data.idx) == '') {
                      lm_optionButtons.setBgColor(data.idx, '#bbb');
                      showExplorerTiles(lm_showClusterBounds);
                      setCookie('ExplorerClusterShown', 1, 355);
                  } else {
                      lm_optionButtons.setBgColor(data.idx, null);
                      setCookie('ExplorerClusterShown', 0, 355);
                      showExplorerTiles();
                  }
                  break;
              case 'maxColumnRow':
                  if (lm_optionButtons.getBgColor(data.idx) == '') {
                      lm_optionButtons.setBgColor(data.idx, '#bbb');
                      showExplorerTiles(lm_showColumnRowBounds);
                      setCookie('ExplorerColumnRowShown', 1, 355);
                  } else {
                      lm_optionButtons.setBgColor(data.idx, null);
                      setCookie('ExplorerColumnRowShown', 0, 355);
                      showExplorerTiles();
                  }
                  break;
              case 'zwift':
                  if (lm_ZwiftMapsMarker) {
                      lm_optionButtons.setBgColor(data.idx, null);
                      localMap.removeLayer(zwiftMaps);
                      lm_ZwiftMapsMarker = 0;
                  } else {
                      lm_optionButtons.setBgColor(data.idx, '#bbb');
                      zwiftMaps.addTo(localMap);
                      lm_ZwiftMapsMarker = 1;
                  }
                  setCookie('lm_ZwiftMapsMarker', lm_ZwiftMapsMarker ? '1' : '0', 365);
                  break;
              case 'settings':
                  $('#map' + localMap.suffix + 'ConfigModal').modal({
                      backdrop: false
                  });
                  setUnexploredKML();
                  break;
          }
      } else {
          switch (data.idx) {
            case 0:
                  if (lm_ZwiftMapsMarker) {
                      lm_optionButtons.setBgColor(data.idx, null);
                      localMap.removeLayer(zwiftMaps);
                      lm_ZwiftMapsMarker = 0;
                  } else {
                      lm_optionButtons.setBgColor(data.idx, '#bbb');
                      zwiftMaps.addTo(localMap);
                      lm_ZwiftMapsMarker = 1;
                  }
                  setCookie('lm_ZwiftMapsMarker', lm_ZwiftMapsMarker ? '1' : '0', 365);
                  break;
              case 1:
                  $('#map' + localMap.suffix + 'ConfigModal').modal({
                      backdrop: false
                  });
                  break;
          }
      }
  });
  localMap.addControl(lm_optionButtons);
}

function lm_addModalPopupContents(suffix){
  var f = d3.select('#map' + suffix + 'ConfigModal .modal-body').append('div').classed('form-horizontal', true);

  if (cpref == 'sd') {
      cg = f.append('div').classed('control-group', true).style('margin-bottom', (cpref || cpref == 'r' == 'a' ? '' : '1') + '0px').attr('title', 'Show distance markers.');
      cg.append('label').classed('control-label', true).text('Distance markers:');
      cg.append('div').classed('controls', true).append('input')
          .attr({
              'id': 'map' + suffix + 'DistanceMarkerCheckBox',
              'class': 'map' + suffix + 'DistanceMarkerCheckBox',
              'type': 'checkbox',
              'checked': typeof(getCookie('lm_distanceMarker')) === 'undefined' || +getCookie('lm_distanceMarker') == 1 ? 'checked' : null
          })
          .on('change', function() {
              setCookie('lm_distanceMarker', this.checked ? 1 : 0, 365);
              if (typeof(lm_onzoomend) !== 'undefined') lm_onzoomend();
          });

  } else {
      var cg = f.append('div').classed('control-group', true).style('margin-bottom', '0px').attr('title', 'The opacity/transparency of your activitiy trails. 0 for transparent, 1 for opaque or anyone in between.');
      cg.append('label').classed('control-label', true).text('Line opacity:');
      cg.append('div').classed('controls', true).append('input')
          .attr({
              'class': 'lm_opacitySlider',
              'type': 'range',
              'min': 0,
              'max': 1,
              'step': 0.1,
              'value': lm_mapLine.opacity
          })
          .on('change', function() {
              lm_mapLine.opacity = +this.value;
              setCookie(cpref + 'o', +this.value, 355);
              a.forEach(function(d) {
                  if (d.mapLineStyle) {
                      d.mapLineStyle.opacity = +lm_mapLine.opacity;
                      if (d.ll) d.ll.setStyle(d.mapLineStyle);
                  } else {
                      if (d.ll) d.ll.setStyle(lm_mapLine);
                  }
              });
          });

      /*cg = f.append('div').classed('control-group', true).style('margin-bottom', '0px').attr('title', 'The colour for the lines on the map.');
      cg.append('label').classed('control-label', true).text('Line colour:');
      d3.select(this).append('label').classed('control-label', true).attr('for', 'lineColour').html(d.desc + ':');
      d3.select(this).append('div').classed('controls', true).each(function() {
          d3.select(this).append('input').attr('type', 'text').attr('id', 'lineColourPicker');
      });
      $('#lineColourPicker').spectrum({
          showAlpha: true,
          change: updateLineColour,
          //move: updateExplorerColour,
          allowEmpty: false,
          showButtons: false,
          preferredFormat: 'hex',
          show: showLineColour,
          appendTo: d3.select('#map' + suffix + 'ConfigModal .modal-body').node()
      });
      $('#' + d.id).spectrum('set', explorerConfig[d.id] ? explorerConfig[d.id] : d.default);*/

      cg = f.append('div').classed('control-group', true).style('margin-bottom', '0px').attr('title', 'Filter your list of activities to just those completely enclosed by the boundaries of the map.');
      cg.append('label').classed('control-label', true).text('Filter to map:');
      cg.append('div').classed('controls', true).append('input')
          .attr({
              'id': 'mapFilterCheckBox',
              'class': 'mapFilterCheckBox',
              'type': 'checkbox'
          })
          .on('change', function() {
              var filterId = jsonIndexOf(columns, 'field', 'mapFilter');
              if (filterId == -1) {
                  columns.push({
                      title: 'Map',
                      desc: 'Map filter',
                      field: 'mapFilter',
                      minLat: 0,
                      maxLat: 0,
                      minLng: 0,
                      maxLng: 0,
                      type: 'map',
                      display: 'map',
                      filterActive: 0,
                      hideZeros: 0,
                      displayOrder: -1
                  });
              }
              filterId = jsonIndexOf(columns, 'field', 'mapFilter');

              if (this.checked) {
                  var ex = localMap.getBounds();

                  columns[filterId].minLat = ex._southWest.lat;
                  columns[filterId].maxLat = ex._northEast.lat;
                  columns[filterId].minLng = ex._southWest.lng;
                  columns[filterId].maxLng = ex._northEast.lng;
                  columns[filterId].filterActive = 1;

                  setMapFilterText(columns[filterId]);
              } else {
                  columns[filterId].filterActive = 0;
              }

              offset = 0;
              setFilterActive();
              applyFilters(a);
              dataLoaded();
              updateActiveFilters();
              updateAvailableFilters();
          });

      cg = f.append('div').classed('control-group', true).style('margin-bottom', (cpref || cpref == 'r' == 'a' ? '' : '1') + '0px').attr('title', 'Auto-zoom the map to area of just the filtered activities.');
      cg.append('label').classed('control-label', true).text('Auto-zoom:');
      cg.append('div').classed('controls', true).append('input')
          .attr({
              'id': 'mapAutoZoomCheckBox',
              'class': 'mapAutoZoomCheckBox',
              'type': 'checkbox',
              'checked': typeof(getCookie('lm_autozoom')) === 'undefined' || +getCookie('lm_autozoom') == 1 ? 'checked' : null
          })
          .on('change', function() {
              setCookie('lm_autozoom', this.checked ? 1 : 0, 365);
              setExtent(liveData);
          });
  }
  cg = f.append('div').classed('control-group', true).style('margin-bottom', (cpref || cpref == 'r' == 'a' ? '' : '1') + '0px').attr('title', 'Show Zwift maps.');

  if (cpref == 'a' || cpref == 'r') {
      cg = f.append('div').classed('control-group', true).style('margin-bottom', '10px').attr('title', 'Show all Explorer tile lines when max square or cluster enabled and zoomed in to a certain level (9).');
      cg.append('label').classed('control-label', true).text('Explorer tile lines:');
      cg.append('div').classed('controls', true).append('input')
          .attr({
              'id': 'mapShowTilesCheckBox',
              'class': 'mapShowTilesCheckBox',
              'type': 'checkbox',
              'checked': typeof(getCookie('lm_showtiles')) !== 'undefined' && +getCookie('lm_showtiles') == 1 ? 'checked' : null
          })
          .on('change', function() {
              setCookie('lm_showtiles', this.checked ? 1 : 0, 365);
              lm_showtiles = this.checked;
              lm_showHideTileLines();
          });

      lm_showtiles = typeof(getCookie('lm_showtiles')) !== 'undefined' && +getCookie('lm_showtiles') == 1;

      cg = f.append('div').classed('control-group', true).style('margin-bottom', '10px').attr('title', 'Export KML file of all explored tiles based on the current set of filters.');
      cg.append('label').classed('control-label', true).text('Explorer tile export:');
      var cgd = cg.append('div').classed('controls', true);
      cgd.append('a')
          .attr({
              'id': 'exportExploredKml',
              'class': 'btn btn-primary btn-mini kmlExport',
              'style': 'color:white;margin-top:5px'
          }).html('KML');
      cgd.append('span')
          .attr({
              'id': 'exportExploredKmlText',
              'class': 'kmlExportText',
              'style': 'display:none;vertical-align:bottom;margin-left:5px;'
          });

      cg = f.append('div').classed('control-group', true).style('margin-bottom', '10px').attr('title', 'Export 3D KML file of all explored tiles based on the current set of filters.');
      cg.append('label').classed('control-label', true).text('Explorer tile 3D export:');
      var cgd = cg.append('div').classed('controls', true);
      cgd.append('a')
          .attr({
              'id': 'exportExploredKml3d',
              'class': 'btn btn-primary btn-mini kmlExport',
              'style': 'color:white;margin-top:5px'
          }).html('KML');
      cgd.append('label')
          .attr({
              'class': 'checkbox inline',
              'title': 'Typically people have a really high concentration of visits to tiles in the immediate vicinity to where they live resulting in huge towers if the 3D KML file is viewed 1 to 1 (i.e. each tile visit results in the addition of a full height tile cube being stacked each time). By default the KML file will flatten the towers by square-rooting the visit count which seems to result in a better view of the data. I tried logarithms first but they squashed the tall towers far too much. Uncheck the box to get an KML export with towers represented 1 to 1.',
              style: 'font-size: inherit;vertical-align:bottom;margin-left:5px;'
          }).html('<input type="checkbox" checked="checked" id="exportExploredKml3dFlattenCB">Flatten spikes');

      d3.select('#exportExploredKml3dFlattenCB').on('change', setUnexploredKML);

      cg = f.append('div').classed('control-group', true).style('margin-bottom', '10px').attr('title', 'Export KML file of all unexplored tiles in the current map view based on the current set of features.');
      cg.append('label').classed('control-label', true).text('Unexplored tile export:');
      var cgd = cg.append('div').classed('controls', true);
      cgd.append('a')
          .attr({
              'id': 'exportUnexploredKml',
              'class': 'btn btn-primary btn-mini kmlExport',
              'style': 'color:white;margin-top:5px'
          }).html('KML');
      cgd.append('span')
          .attr({
              'id': 'exportUnexploredKmlText',
              'class': 'kmlExportText',
              'style': 'display:none;vertical-align:bottom;margin-left:5px;'
          });

      d3.select('#map' + suffix + 'ConfigModal .modal-body')
          .append('div').attr('id', 'explorerConfigModalInfo').classed('alert alert-info', true)
          .html(explorerConfigInfoText);
      d3.select('#map' + suffix + 'ConfigModal .modal-body')
          .append('div').classed('form-horizontal explorerConfig', true)
          .selectAll('div.colourPickers').data(explorerConfigData)
          .enter().append('div').classed('control-group', true).style('margin-bottom', '5px').each(function(d) {
              if (d.id == 'explorerTileStrokeSat') d3.select(this).classed('satStart', true);
              d3.select(this).append('label').classed('control-label', true).attr('for', d.id).html(d.desc + ':');
              d3.select(this).append('div').classed('controls', true).each(function() {
                  d3.select(this).append('input').attr('type', 'text').attr('id', d.id);
              });
              $('#' + d.id).spectrum({
                  showAlpha: true,
                  change: updateExplorerColour,
                  //move: updateExplorerColour,  // saves every time so best not to use
                  allowEmpty: false,
                  showButtons: false,
                  preferredFormat: 'hex',
                  show: showExplorerColour,
                  appendTo: d3.select('#map' + suffix + 'ConfigModal .modal-body').node()
              });
              $('#' + d.id).spectrum('set', explorerConfig[d.id] ? explorerConfig[d.id] : d.default);
          });
      d3.select('#map' + suffix + 'ConfigModal .modal-body .explorerConfig').insert('h3', '.satStart').text('Satellite map colour options:');
  }
}

function getZoomForCheck(){
  return (lm_currentBaseLayer.id == osGbCrs) ? getGZoomFromOSZoom(localMap.getZoom()) : localMap.getZoom();
}

function getOsZoomFromGZoom(zoom){
  const osZoomFromGZoom = d3.scale.linear().domain([6.5, 15.9]).range([0, 9]);
  return Math.round(osZoomFromGZoom(zoom) * 10) / 10;
}

function getGZoomFromOSZoom(zoom){
  const gZoomFromOSZoom = d3.scale.linear().domain([0, 9]).range([6.5, 15.9]);
  return Math.round(gZoomFromOSZoom(zoom) * 10) / 10;
}

function preparePhotos(){
  if (photoLayer) {
    photoLayer.clear();
  }
}

function reloadPhotos(){
  if (photoLayer) {
    photoLayer.clear();
    photoLayer.add(photoData);
  }
}

var _prevZoom = null;
var _prevZoomLock = 0;

function prevZoom(val, overrideLock) {
  //Workaround for layerremove firing multiple times
  if (!arguments.length) return _prevZoom;
  if(_prevZoomLock == 0 || overrideLock == true){
    _prevZoom = val;
    _prevZoomLock = 1;
    setTimeout(function(){_prevZoomLock = 0; _prevZoom = null;}, 500);
  }
}
