/*
Copyright 2012-2026 VeloViewer. All Rights Reserved
###V17###
*/
L.Rectangle.Explorer = L.Rectangle.extend({
  options: {
		v: 0,
    a: 0
	}
});

L.GridLayer.Explorer = L.GridLayer.extend({
    createTile: function(coords, done) {
        return getExplorerTile(coords.x, coords.y, coords.z, done);
    }
});

L.gridLayer.explorer = function(opts) {
    return new L.GridLayer.Explorer(opts);
};

var exp, exp9, my9 = null;

var _allTiles = {};
var _msgShown = false;
var _myTiles = null;
var _myExpTiles = null;
var _cutoff = 8.9;
var expAsyncCount = 0;
var confCookie = 'acnf';
var cpref = 'a';
var mapsRequired = true;
var embed = false;
var hueScale = d3.scale.linear().domain([1, 100]).range([100, 0]);

function explorerConfigData(id, desc, defColor){
  this.id = id;
  this.desc = desc;
  this.hex = defColor;

  this.color = function(){
    return '#' + this.hex.substr(3, this.hex.length);
  }

  this.alpha = function(){
    return this.convertHexToDec(this.hex.substr(1, 2));
  }

  this.checkNewColor = function(c){
    //TODO expannd checks
    if(c.length == 9){
      return c;
    } else {
      return this.hex;
    }
  }

  this.setHex = function(x){
    let c = this.checkNewColor(x);
    this.hex = c;
    setCookie(this.id, c, 355);
  }

  this.convertDecToHex = function(d){
    let x = Math.round(d * 255).toString(16);
    return (x.length == 1 ? '0' : '') + x;
  }

  this.convertHexToDec = function(x){
    return Math.round(((parseInt(x, 16) / 255) * 100) / 100);
  }

  //only on load
  if(typeof(getCookie(this.id)) !== 'undefined'){
    this.hex = this.checkNewColor(getCookie(this.id));
  }
}

var explorerConfigDataSet = [
    new explorerConfigData('explorerTileStroke', 'Tile border', '#ff7e7e7e'),
    new explorerConfigData('explorerTileFill', 'Visited fill', '#ffa8a8a8'),
    new explorerConfigData('explorerMaxSquareStroke', 'Max Square border', '#ff000000'),
    new explorerConfigData('explorerMaxClumpFill', 'Max Cluster fill', '#ff5b5bff'),
    new explorerConfigData('explorerMaxColumnRowStroke', 'Max Column/Row border', '#ff00dd00')
];

var explorerConfigInfoText = 'Customise your Explorer tile colours (only applies to this page)';

var _tilequeue = [];

function getExplorerTile(x, y, z, done) {
  var tile = document.createElement("canvas");
  var z2, x2, y2 = null;

  if(z > _cutoff){
    z2 = 9;
    x2 = Math.floor(x / Math.pow(2, (z - z2)));
    y2 = Math.floor(y / Math.pow(2, (z - z2)));

    if(_tilequeue.indexOf([x2, y2, z2].join(',')) == -1){
      _tilequeue.push([x2, y2, z2].join(','));

      $.ajax({
          url: '/api/getExplorerMapTiles/'+x2+'/'+y2+'/'+z2+'/'+(s_s ? 1 : 0),
          tryCount: 0,
          retryLimit: 1,
          success: function(result) {
              var j = result.parseJSON();
              if (j.status == 'success') {
                if(j.data.length > 1){
                  j.data.forEach(function(d){
                    _allTiles[d.x + '-' + d.y] = d;
                  });
                  drawExplorerTiles(j.data);
                  drawMyTiles(x2, y2, z2);
                  done(null, tile);
                }
              }
          },
          error: function(xhr, textStatus, errorThrown) {
              if (textStatus == 'timeout' || xhr.status == 0) {
                  this.tryCount++;
                  if (this.tryCount <= this.retryLimit) {
                      $.ajax(this);
                      return false;
                  }
              }
          }
      });
    }
  }
  return tile;
}

const _athleteBins = [20, 40, 80];
const _visitBins = [200, 400, 800];

function colorRamp(name, title, color9, color3){
  this.name = name;
  this.title = title;
  this._color9 = color9;
  this._color3 = color3;

  this.getColors = function(){
    if(s_s || choropleth.val == 'm'){
      return this._color9;
    } else {
      return this._color3;
    }
  }

  this.visitsColorRange = null;
  this.athletesColorRange = null;
  this.myVisitsColorRange = null

  this.visitsMax = 1;
  this.athletesMax = 1;
  this.myMax = 1;

  this.visitsSteps = (s_s) ? [2,4,8,16,32,64,128,256,512] : _visitBins;
  this.athletesSteps = (s_s) ? [2,4,8,16,32,64,128,256,512] : _athleteBins;
  this.mySteps = [2,4,8,16,32,64,128,256,512];

  this.setVisitsRange = function(v){
    this.visitsSteps = this.calcSteps(v,'v');
    this.visitsMax = this.visitsSteps[this.visitsSteps.length-1];
    this.visitsColorRange = d3.scale.threshold().domain(this.visitsSteps).range(this.getColors());
  }

  this.setAthletesRange = function(v){
    this.athletesSteps = this.calcSteps(v,'a');
    this.athletesMax = this.athletesSteps[this.athletesSteps.length-1];
    this.athletesColorRange = d3.scale.threshold().domain(this.athletesSteps).range(this.getColors());
  }

  this.setMyRange = function(v){
    this.mySteps = this.calcSteps(v,'m');
    this.myMax = this.mySteps[this.mySteps.length-1];
    this.myVisitsColorRange = d3.scale.threshold().domain(this.mySteps).range(this.getColors());
  }

  this.calcSteps = function(v, t){
    let steps = [];
    if(s_s || t == 'm'){
      //dynamic bins
      let step = 1;
      let c = v;
      for (let i=0; i<this.getColors().length; i++ ){
        c = c/2;
      }
      c = Math.round(c);
      let n = c.toString().length;
      if(c <= 1*n){
        step = 1*n;
      } else if(c > 1*n && c <= 2*n){
        step = 2*n;
      } else if(c > 2*n && c <= 5*n){
        step = 5*n;
      } else {
        step = 10*n;
      }
      for(let i=0; i<this.getColors().length; i++){
        step = step * 2;
        steps.push(step);
      }
    } else {
      //static bins
      if(t == 'v')
        steps = _visitBins;
      else
        steps = _athleteBins;
    }
    return steps;
  }

  this.getColor = function (x, v, a){
    if(!s_s && x!='m'){
      v--; a--;
    }
    if(x == 'a'){
      return this.athletesColorRange(a);
    }else{
        if(x == 'm')
          return this.myVisitsColorRange(v);
        else
          return this.visitsColorRange(v);
    }
  }
}

var colorRamps = [
    //Based on colorbrewer2 and done this way cos of d3v5 not being an option
    new colorRamp("YlOrBr", "Red Orange Yellow", ["#800026", "#bd0026", "#e31a1c", "#fc4e2a", "#fd8d3c", "#feb24c", "#fed976", "#ffeda0", "#ffffcc"], ["#f03b20", "#feb24c", "#ffeda0"]),
    new colorRamp("BuPu", "Purple Blue", ["#4d004b", "#810f7c", "#88419d", "#8c6bb1", "#8c96c6", "#9ebcda", "#bfd3e6", "#e0ecf4", "#f7fcfd"], ["#8856a7", "#9ebcda", "#e0ecf4"]),
    new colorRamp("YlGnBu", "Blue Green Yellow", ["#081d58", "#253494", "#225ea8", "#1d91c0", "#41b6c4", "#7fcdbb", "#c7e9b4", "#edf8b1", "#ffffd9"], ["#2c7fb8", "#7fcdbb", "#edf8b1"]),
    new colorRamp("YlGr", "Green Yellow", ["#004529", "#006837", "#238443", "#41ab5d", "#78c679", "#addd8e", "#d9f0a3", "#f7fcb9", "#ffffe5"], ["#31a354", "#addd8e", "#f7fcb9"]),
    new colorRamp("PuRd", "Red Purple", ["#67001f", "#980043", "#ce1256", "#e7298a", "#df65b0", "#c994c7", "#d4b9da", "#e7e1ef", "#f7f4f9"], ["#dd1c77", "#c994c7", "#e7e1ef"]),
    new colorRamp("WhBk", "Black", ["#000000", "#252525", "#525252", "#737373", "#969696", "#bdbdbd", "#d9d9d9", "#f0f0f0", "#ffffff"], ["#636363", "#bdbdbd", "#f0f0f0"]),
    new colorRamp("RdYlGr", "Green Yellow Red", ["#1a9850", "#66bd63", "#a6d96a", "#d9ef8b", "#ffffbf", "#fee08b", "#fdae61", "#f46d43", "#d73027"], ["#91cf60", "#ffffbf", "#fc8d59"])
];

let choropleth = {
  _val : null,
  get val() {
    if(this._val === null){
      if(_mode == 's'){
        this._val = 'm';
      } else {
        this._val = 'v';
        if(typeof(getCookie('expChoro')) !== 'undefined'){
          let x = getCookie('expChoro');
          if(x == 'm' && loggedInAthleteId <= 0) x = 'v';
          this._val = x;
          $('#datum').val(x);
        }
      }
    }
    return this._val;
  },
  set val(x) {
    this._val = x;
    setCookie('expChoro', x, 355);
  }
};

let selColors = {
  _val : null,
  get val() {
    if(this._val === null){
      let _defColor = 'YlOrBr';
      let _choice = _defColor;
      if(typeof(getCookie('expColor')) !== 'undefined'){
        _choice = getCookie('expColor');
        this._val = colorRamps.find(function(d){
          return d.name == _choice;
        });
      }
      if(this._val === null){
        this._val = colorRamps.find(function(d){
          return d.name == _defColor;
        });
      }
    }
    return this._val;
  },
  set val(x) {
    this._val = x;
    setCookie('expColor', x.name, 355);
  }
};

let invColors = {
  _val : null,
  get val() {
    if(this._val === null){
      if(typeof(getCookie('invColor')) !== 'undefined'){
        invColors.val = (getCookie('invColor') == 'true');
        $('#invertColors').prop('checked', invColors.val);
        if(invColors.val) flipColors();
      } else {
        this._val = false;
      }
    }
    return this._val;
  },
  set val(x) {
    this._val = x;
    setCookie('invColor', x, 355);
  }
};

let showGrid = {
  _val : null,
  get val() {
    if(this._val === null){
      if(typeof(getCookie('expShowGrid')) !== 'undefined'){
        this._val = (getCookie('expShowGrid') == 'true');
      }else{
        this._val = false;
      }
      lm_showtiles = this._val;
      lm_showHideTileLines();
    }
    return this._val;
  },
  set val(x) {
    this._val = x;
    lm_showtiles = x;
    setCookie('expShowGrid', x, 355);
  }
};

let maxVisits = {
  _val : 1,
  get val() {
    return this._val;
  },
  set val(x) {
    if(this._val < x){
      this._val = x;
      colorRamps.forEach(function(d){
        d.setVisitsRange(x);
      });
      changeColor(selColors.val.name);
    }
  }
};

let maxAthletes = {
  _val : 1,
  get val() {
    return this._val;
  },
  set val(x) {
    if(this._val < x){
      this._val = x;
      colorRamps.forEach(function(d){
        d.setAthletesRange(x);
      });
      changeColor(selColors.val.name);
    }
  }
};

let maxMyVisits = {
  _val : 1,
  get val() {
    return this._val;
  },
  set val(x) {
    if(this._val < x){
      this._val = x;
      colorRamps.forEach(function(d){
        d.setMyRange(x);
      });
      changeColor(selColors.val.name);
    }
  }
};

let gridOpacity = {
  _val : null,
  get val() {
    if(this._val === null){
      if(typeof(getCookie('expGridO')) !== 'undefined'){
        this._val = +getCookie('expGridO');
      } else {
        this._val = 0.2;
      }
      explorerGridLineStyle.opacity = this._val;
      d3.select(".lm_opacitySlider").attr("value", this._val);
      if(typeof(lm_tileLines) !== 'undefined') lm_tileLines.setStyle(explorerGridLineStyle);
    }
    return this._val;
  },
  set val(x) {
    this._val = x;
    explorerGridLineStyle.opacity = x;
    lm_tileLines.setStyle(explorerGridLineStyle);
    setCookie('expGridO', x, 355);
  }
};

let maxSquareShown = {
  _val : null,
  get val(){
    if(this._val === null){
      if(typeof(getCookie('expMaxSquareShown')) !== 'undefined'){
        this._val = (getCookie('expMaxSquareShown')== 'true');
      } else {
        this._val = false;
      }
      if(this._val){
          showExplorerTiles(lm_showMaxSquareBounds);
      } else {
          showExplorerTiles();
      }
    }
    return this._val;
  },
  set val(x){
    if(x){
        showExplorerTiles(lm_showMaxSquareBounds);
        setCookie('expMaxSquareShown', 1, 355);
    } else {
        setCookie('expMaxSquareShown', 0, 355);
        showExplorerTiles();
    }
  }
}

let maxClusterShown = {
  _val : null,
  get val(){
    if(this._val === null){
      if(typeof(getCookie('expClusterShown')) !== 'undefined'){
        this._val = (getCookie('expClusterShown')== 'true');
      } else {
        this._val = false;
      }
      if(this._val){
          showExplorerTiles(lm_showClusterBounds);
      } else {
          showExplorerTiles();
      }
    }
    return this._val;
  },
  set val(x){
    if(x){
        showExplorerTiles(lm_showClusterBounds);
        setCookie('expClusterShown', 1, 355);
    } else {
        setCookie('expClusterShown', 0, 355);
        showExplorerTiles();
    }
  }
}

function getOpacity(v, a){
  return 0.55; //Math.round(opacityRange( choropleth.val == 'v' ? v : a ) * 100) / 100;
}

function getTip(x, y, v, a){
  let t = 'Tile <strong>{0}-{1}</strong><br />visited <strong>{2}</strong> time{3},<br />by <strong>{4}</strong> athlete{5}';
  if(s_s){
    t = t.replace('{0}', x.toString());
    t = t.replace('{1}', y.toString());
    t = t.replace('{2}', v.toString());
    t = t.replace('{4}', a.toString());

    t = t.replace('{3}', (v > 1) ? 's' : '');
    t = t.replace('{5}', (a > 1) ? 's' : '');
  } else {
    t = 'Tile <strong>{0}-{1}</strong><br />visited <strong>{2} to {3}</strong> times,<br />by <strong>{4} to {5}</strong> athletes';
    t = t.replace('{0}', x.toString());
    t = t.replace('{1}', y.toString());

    let min = '1';
    let max = v.toString();
    switch(v){
      case 400:
        min = '201';
        break;
      case 800:
        min = '401';
        max = max + '+';
        break;
    }
    t = t.replace('{2}', min);
    t = t.replace('{3}', max);

    min = '1';
    max = a.toString();
    switch(a){
      case 40:
        min = '21';
        break;
      case 80:
        min = '41';
        max = max + '+';
        break;
    }
    t = t.replace('{4}', min);
    t = t.replace('{5}', max);
  }
  return t;
}

function getMyTip(x, y, v, a){
  var t = 'Tile <strong>{0}-{1}</strong><br />visited <strong>{2}</strong> time{3},<br />by me';
  t = t.replace('{0}', x.toString());
  t = t.replace('{1}', y.toString());
  t = t.replace('{2}', v.toString());

  t = t.replace('{3}', (v > 1) ? 's' : '');

  return t;
}

function drawExplorerTiles(tiles){
  //set max value for my tiles as they arrive in one request
  maxVisits.val = Math.max.apply(Math, tiles.map(function(o) { return o.v; }));
  maxAthletes.val = Math.max.apply(Math, tiles.map(function(o) { return o.a; }));

  for(var i=0; i<tiles.length; i++){
    var d = tiles[i];
    var p = new L.Rectangle.Explorer([[+tile2lat(d.y, 14), +tile2long(d.x, 14)], [+tile2lat(d.y + 1, 14), +tile2long(d.x + 1, 14)]], {color: selColors.val.getColor((choropleth.val == 'm') ? 'v' : choropleth.val, d.v, d.a), weight: 1.0, fillOpacity: getOpacity(d.v, d.a), v: d.v, a: d.a});
    //p.bindPopup(getTip(d.x, d.y, d.v, d.a), {permanent: false});
    exp9.addLayer(p);
  }

  exp9.bringToBack();
}

function drawMyTiles(x, y, z){
  if(_myTiles != null){
    //tiles stored at z14, translate
    var xMin = x * Math.pow(2, (14 - z));
    var yMin = y * Math.pow(2, (14 - z));
    var xMax = xMin + Math.pow(2, (14 - z)) -1;
    var yMax = yMin + Math.pow(2, (14 - z)) -1;

    var tiles = _myTiles.filter(function(d){
      return (d.x >= xMin && d.x <= xMax && d.y >= yMin && d.y <= yMax);
    });

    maxMyVisits.val = Math.max.apply(Math, tiles.map(function(o) { return o.v; }));

    for(var i=0; i<tiles.length; i++){
      var d = tiles[i];
      var p = new L.Rectangle.Explorer([[+tile2lat(d.y, 14), +tile2long(d.x, 14)], [+tile2lat(d.y + 1, 14), +tile2long(d.x + 1, 14)]], {color: selColors.val.getColor('m', d.v, d.a), weight: 1.0, fillOpacity: getOpacity(d.v, d.a), v: d.v, a: d.a});
      p.bindPopup(getMyTip(d.x, d.y, d.v, d.a), {permanent: false});
      my9.addLayer(p);
      //console.debug("[["+(tile2lat(d.y, 14))+", "+(tile2long(d.x, 14))+"], ["+(tile2lat(d.y + 1, 14))+", "+(tile2long(d.x + 1, 14))+"]] color:"+(selColors.val.getColor('m', d.v, d.a))+", fillOpacity:"+(getOpacity(d.v, d.a)));
    }

    my9.bringToBack();
  }
}

function changeDatum(){
  choropleth.val = $('#datum').val();
  if(localMap.hasLayer(exp9))
    localMap.removeLayer(exp9);
  if(localMap.hasLayer(my9))
    localMap.removeLayer(my9);
  showTiles();
  //due to GDPR the length of the ranges for the 3 datums may not be the same
  colorRamps.forEach(function(d){
    d.setVisitsRange(maxVisits.val);
    d.setAthletesRange(maxAthletes.val);
    d.setMyRange(maxMyVisits.val);
  });
  buildRamps();
  changeRamp();
  //end GDPR
  setTimeout(changeColor(selColors.val.name), 50);
}

function changeColor(col){
  selColors.val = colorRamps.find(function(d){
    return d.name == col;
  });

  changeLegend();

  if(localMap.hasLayer(exp9)){
    exp9.eachLayer(function(d){
      d.setStyle({color: selColors.val.getColor(choropleth.val, d.options.v, d.options.a), weight: 1.0, fillOpacity: getOpacity(d.options.v, d.options.a)});
    });
  }

  if(localMap.hasLayer(my9) && choropleth.val == 'm'){
    my9.eachLayer(function(d){
      d.setStyle({color: selColors.val.getColor('m', d.options.v, d.options.a), weight: 1.0, fillOpacity: getOpacity(d.options.v, d.options.a)});
    });
  }
}

function changeLegend(){
  let steps = selColors.val.visitsSteps;

  if(localMap.hasLayer(exp9)){
    if(choropleth.val == 'v')
      steps = selColors.val.visitsSteps.map(function(d){ return d});
    else
      steps = selColors.val.athletesSteps.map(function(d){ return d});
  }
  if(localMap.hasLayer(my9) && choropleth.val == 'm'){
    steps = selColors.val.mySteps.map(function(d){ return d});
  }

  if(steps[0]!=1)
    steps.unshift(1);

  setScaleUI();
  d3.select("#selColorLegend").selectAll("*").remove();

  d3.select("#selColorLegend")
      .selectAll("div")
      .data(steps)
      .enter()
      .append("div")
      .append("div") //.attr("id", function(d, i){ return 'selColorLegend' + i.toString(); })
      .html(function(d){ return d});
}

function changeRamp(col){
  d3.select("#selColorRamp").selectAll("*").remove();

  d3.select("#selColorRamp")
      .selectAll("div")
      .data(function(d){ return selColors.val.getColors(); })
      .enter()
      .append("div")
      .attr("style", function(d){ return "float:left;height:16px;width:16px;background-color:" + d; });
}

function flipColors(){
  colorRamps.forEach(function(d){
    d._color9 = d._color9.reverse();
    d._color3 = d._color3.reverse();
  });
}

function invertColors(){
  invColors.val = $('#invertColors').prop('checked');

  flipColors();

  bindColors();
  changeColor(selColors.val.name);
}

function buildRamps(){
  d3.selectAll(".dropdown-menu").filter(".color-ranges")
      .selectAll("*")
      .remove();

  d3.selectAll(".dropdown-menu").filter(".color-ranges")
      .selectAll("ul")
      .data(colorRamps)
      .enter()
      .append("li")
      .attr("name", function(d) { return d.name; })
      .append("a")
      .attr("href", "#")
      .attr("title", function(d){ return d.title; })
      .attr("onclick", function(d){ return "changeColor('" + d.name + "'); changeRamp('" + d.name + "')"; })
      .append("div")
      .attr("id", function(d) { return d.name; })
      .attr()
      .selectAll("div")
      .data(function(d){ return d.getColors(); })
      .enter()
      .append("div")
      .attr("style", function(d){ return "float:left;height:16px;width:16px;background-color:" + d; });

  changeRamp(selColors.val.name);
}

function bindColors(){
  //run onload...
  let isInv = invColors.val;
  buildRamps();
  //picking up the base colors, override onload
  for(let i=0; i<explorerConfigDataSet.length; i++){
    updateExplorerConfig(explorerConfigDataSet[i].id, explorerConfigDataSet[i].color(), explorerConfigDataSet[i].alpha());
  }
  lm_updateLayerStyles();
}

var bi = null;

function lm_onzoomend() {
  if(localMap.getZoom() <= _cutoff){
    hideTiles();
    if (bi == null) {
      bi = window.setInterval(function() {
        d3.select(document.getElementById('resetZoomBtnImg').parentElement).transition().duration(100).style('background','#ddd').transition().delay(400).duration(100).style('background','#fff');
      },1000);
    }
    if(!_msgShown){
      window.setTimeout(function() {
        alert('Please zoom in to see tiles');
      }, 10);
      _msgShown = true;
    }
  } else {
    if (bi != null) {
      window.clearInterval(bi);
      bi == null;
      d3.select(document.getElementById('resetZoomBtnImg').parentElement).style('background','#fff');
    }
    showTiles();
  }
}

function lm_onmoveend(){
  getViewableTilesMax(false);
  setTimeout(forceOpacity, 50);
  setTimeout(forceOpacity, 250);
}

function forceOpacity(){
  let v = localMap.getBounds();
  let eL = exp9.getLayers();
  if(eL.length > 0){
    for(let i = 0; i < eL.length; i++){
      if(v.overlaps(eL[i]._bounds))
        eL[i].setStyle({color: selColors.val.getColor(choropleth.val, eL[i].options.v, eL[i].options.a), weight: 1.0, fillOpacity: getOpacity(eL[i].options.v, eL[i].options.a)});
    }
  }

  let mL = my9.getLayers();
  if(mL.length > 0){
    for(let i = 0; i < mL.length; i++){
      if(v.overlaps(mL[i]._bounds))
        mL[i].setStyle({color: selColors.val.getColor('m', mL[i].options.v, mL[i].options.a), weight: 1.0, fillOpacity: getOpacity(mL[i].options.v, mL[i].options.a)});
    }
  }
}

function hideTiles(){
  console.info('Hide tiles');
  if(localMap.hasLayer(exp9))
    localMap.removeLayer(exp9);
  if(localMap.hasLayer(my9))
    localMap.removeLayer(my9);
}

function showTiles(){
  console.info('Show tiles');
  if(!localMap.hasLayer(exp9) && choropleth.val != 'm'){
    localMap.addLayer(exp9);
    exp9.bringToBack();
  }
  if(!localMap.hasLayer(my9) && choropleth.val == 'm'){
    localMap.addLayer(my9);
    my9.bringToBack();
  }
}

function lm_addOptionButtons(){

  if(loggedInAthleteId > 0){
    //add explorer specific mapOptions
    var lButtons = [];

    /*lButtons.push({
        content: '&#9634',
        title: 'View Explorer Max Square',
        bgColor: +getCookie('expGrid') ? '#bbb' : null,
        type: 'expGrid'
    });*/

    lButtons.push({
        content: '&#9634',
        title: 'View Explorer Max Square',
        bgColor: +getCookie('expMaxSquareShown') ? '#bbb' : null,
        type: 'maxSqaure'
    });
    lButtons.push({
        title: 'View Explorer Max Cluster',
        bgColor: +getCookie('expClusterShown') ? '#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({
      content: '<i class="icon-cog"></i>',
      title: 'Map settings',
      type: 'expSettings'
    });

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

    lm_optionButtons.on('clicked', function(data) {
      switch (lButtons[data.idx].type) {
        case 'maxSqaure':
            if (lm_optionButtons.getBgColor(data.idx) == '') {
                lm_optionButtons.setBgColor(data.idx, '#bbb');
                maxSquareShown.val = true;
            } else {
                lm_optionButtons.setBgColor(data.idx, null);
                maxSquareShown.val = false;
            }
            break;
        case 'maxCluster':
          if (lm_optionButtons.getBgColor(data.idx) == '') {
              lm_optionButtons.setBgColor(data.idx, '#bbb');
              maxClusterShown.val = true;
          } else {
              lm_optionButtons.setBgColor(data.idx, null);
              maxClusterShown.val = false;
          }
          break;
        case 'expSettings':
          $('#explorerInfoModal').modal('hide');
          $('#map' + localMap.suffix + 'ConfigModal').modal({
              backdrop: false
          });
          break;
      }
    exp9.bringToBack();
    my9.bringToBack();
    });
    localMap.addControl(lm_optionButtons);
  }
}

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

    var cg = f.append('div').classed('control-group', true).style('margin-bottom', '0px').attr('title', 'The opacity/transparency of the explorer grid. 0 for transparent, 1 for opaque or anything in between.');
    cg.append('label').classed('control-label', true).text('Explorer grid opacity:');
    cg.append('div').classed('controls', true).append('input')
        .attr({
            'class': 'lm_opacitySlider',
            'type': 'range',
            'min': 0,
            'max': 1,
            'step': 0.1,
            'value': gridOpacity.val
        })
        .on('change', function() { gridOpacity.val = +this.value; });

    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': showGrid.val ? 'checked' : null
        })
        .on('change', function() {
            showGrid.val = this.checked  ? true : false;
            lm_showHideTileLines();
        });

    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(explorerConfigDataSet)
        .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,
                allowEmpty: false,
                showButtons: false,
                preferredFormat: 'hex',
                show: showExplorerColour,
                appendTo: d3.select('#map' + suffix + 'ConfigModal .modal-body').node()
            });
            $('#' + d.id).spectrum('set', d.hex);
        });
  }
}

function updateExplorerColour(a, noSave) {
    var id = this.id;
    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;

    //update object
    let x = explorerConfigDataSet.find(function(x){
      return x.id == id;
    });
    x.setHex(h1);

    //update UI
    updateExplorerConfig(id, h, a._a);
    lm_updateLayerStyles();
}

function updateExplorerConfig(id, h, a){
  switch (id) {
    case 'explorerTileStroke':
        explorerTileStyle.color = h;
        explorerTileStyle.opacity = a;
        explorerClumpStyle.color = h;
        explorerClumpStyle.opacity = a;
        break;
    case 'explorerTileFill':
        explorerTileStyle.fillColor = h;
        explorerTileStyle.fillOpacity = a;
        break;
    case 'explorerMaxSquareStroke':
        maxSquareStyle.color = h;
        maxSquareStyle.opacity = a;
        break;
    case 'explorerMaxClumpFill':
        explorerClumpStyle.fillColor = h;
        explorerClumpStyle.fillOpacity = a;
        break;
  }
}

function removeInvalidLayers(){
  if(typeof(photoLayer) !== 'undefined' && localMap.hasLayer(photoLayer))
    localMap.removeLayer(photoLayer);
  if(typeof(zwiftMaps) !== 'undefined' && localMap.hasLayer(zwiftMaps))
    localMap.removeLayer(zwiftMaps);
}

function getSelectedBaseLayer(options){
  switch (getCookie('expmap')) {
      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 "baw":
          options.layers = [lm_layerBaW];
          break;*/
      default:
          if (!options.layers) {
              options.layers = [lm_layerVeloviewerGrey];
          }
  }
}

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

function getViewableTilesMax(reset){
  let v = localMap.getBounds();
  let eL = exp9.getLayers();
  if(eL.length > 0){
    var tiles = [];
    for(let i = 0; i < eL.length; i++){
      if(v.overlaps(eL[i]._bounds))
        tiles.push(eL[i].options);
    }
    if(tiles.length > 0){
      if(reset) maxVisits._val = 0;
      maxVisits.val = Math.max.apply(Math, tiles.map(function(o) { return o.v; }));
      if(reset) maxAthletes._val = 0;
      maxAthletes.val = Math.max.apply(Math, tiles.map(function(o) { return o.a; }));
    }
  }

  let mL = my9.getLayers();
  if(mL.length > 0){
    let myTiles = [];
    for(let i = 0; i < mL.length; i++){
      if(v.overlaps(mL[i]._bounds))
        myTiles.push(mL[i].options);
    }
    if(myTiles.length > 0){
      if(reset) maxMyVisits._val = 0;
      maxMyVisits.val = Math.max.apply(Math, myTiles.map(function(o) { return o.v; }));
    }
  }
}

function showModalInfo(){
  $('#map' + localMap.suffix + 'ConfigModal').modal('hide');
  $('#explorerInfoModal').modal({
      backdrop: false
  });
}

function setModalInfo(){
  addModalPopup('explorerInfo', 'About the tiles', undefined, 0, d3.select('body'));

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

  var f = d3.select('#explorerInfoModal .modal-body').append('div').classed('form-horizontal', true);
  f.html('<p>The tiles are based on the Google Maps flat rectangular (Mercator) view of the Earth at the zoom level 14. Because of the rectangular view, tiles closer to the poles are actually smaller than those closer to the equator, but on average each tile is approximately 1-mile square and there are almost 276 million of them.</p><p>Each tile that has been visited by a VeloViewer users with a publicly viewable activity is displayed in this “heat” map. The number of visits and the number of visiting athletes is shown for each tile. Individual athletes or activities cannot be viewed. You can use the map to determine popular, unpopular tiles, or unvisited tiles (at least, not by anyone publicly claiming a visit).</p><p>Due to certain restrictions (aka GDPR) we can only show members of the public three levels on the heat map, with the data grouped into the three categories.</p><p>If you log in to your VeloViewer account you can also view your personal heat map, which only you can see, and which includes activities you have flagged as private. This data has nine levels to show greater detail than the public visits.</p><p>If you’ve done some exploration in places other athletes have not been, you may wish to claim some bragging rights by changing your activity to be publicly viewable (though if you do, it may take 24 hours for this to be visible as the data is cached).</p><p>If you are a logged in VeloViewer user with a premium Strava account there are nine categories for the public data, allowing the “heat” in the map to stand out better. Also, as you scroll around the map the scale will alter to accommodate more popular areas and the map is re-coloured accordingly. If you pan to areas much less popular you can use the “reset” button to limit the scale and map colours to only the tiles that are visible. This allows you to bring out the heat in any area.</p><p>Finally, due to the large number of tiles which make up the planet (did we say there are almost 276 million of them?) they can’t all be displayed at once, otherwise your computer might melt. If you zoom out too far on the map the tiles will be hidden. Zoom in to view the tiles or use the “Reset zoom” button on the bottom right of the map.</p>');
}

function setScaleUI(){
  if(s_s || choropleth.val == 'm') {
    d3.select("#selColorLegend").style("width", "160px");
    d3.selectAll(".dropdown-menu").filter(".color-ranges").style("min-width", "144px");
    d3.select("#resetScale").style("visibility", "visible");
  } else {
    d3.select("#selColorLegend").style("width", "64px");
    d3.selectAll(".dropdown-menu").filter(".color-ranges").style("min-width", "48px");
    d3.select("#resetScale").style("visibility", "hidden");
  }
}

function regionStats(entries){

  let e = entries.sort(function(a, b){
      return (b.visitedCount / b.tileCount) - (a.visitedCount / a.tileCount);
  }).slice(0, 5);


  let dl = d3.select('#expBestsList');
  dl.append('dt')
    .attr('title', "Regional bests are the areas you have explored the most. The percentage shown represents the amount of tiles you have ticked out of the number of tiles available in that region.")
    .text('Regional Bests:');
  let dd = dl.append('dd')
    .selectAll('div')
    .data(e)
    .enter()
    .append('div');

  dd.append('div')
    .attr('style', 'float: left; clear: left')
    .html(function(d) { return '<a href="/explorerRegions?r='+ d.countryCode + '">'+ d.name + '</a>&nbsp;'; });

  dd.append('div')
    .attr('style', 'float: right; clear: right')
    .html(function(d) { return ((d.visitedCount / d.tileCount) * 100).toFixed(2) + ' %'; });

  dd.node().childNodes[0].setAttribute('style', 'float: left');
}

function explorerOptInBtnClick() {
  $.ajax({
    url: '/api/setting/0/1',
    success: function(result) {
      console.info('Explorer opt-in saved');
    },
    error: function(xhr, textStatus, errorThrown) {
        console.info(errorThrown);
    }
  });
  d3.select('#explorerOptInSpan').remove();
}

function localInit(){
  let _n = '';
  let _m = '';

  if (+loggedInAthleteId <= 0) {
    // Not logged in
    _n = '<a href="https://www.strava.com/oauth/authorize?client_id=36&scope=read,read_all,profile:read_all,profile:write,activity:read,activity:read_all,activity:write&redirect_uri=' + document.location.origin + document.location.pathname + '&response_type=code&approval_prompt=auto&state=private"><img style="height:40px" src="https://cf.veloviewer.com/img/btn_strava_connectwith_orange.svg"></a> <div style="display: inline-block;vertical-align: middle;">to view your own <br class="visible-phone">explorations.</div>';
  } else {
    if (!isPRO) {
      if (pdNoData) {
        // Logged in not PRO not yeet sync'ed
        _n = 'You\'ve not yet sync\'ed your free data. Please head to <a href="/update">your Update page</a> to bring it in to VeloViewer.';
      } else {
        // Logged in not PRO
        _n = 'As a free user your own exploration is restricted to 25 activities. <a href="/update">Upgrade to PRO</a> to bring your entire Strava history into VeloViewer.';
      }
    } else {
      if (pdNoData) {
        // Logged in not PRO not yeet sync'ed
        _n = 'You\'ve not yet sync\'ed your free data. Please head to <a href="/update">your Update page</a> to bring it in to VeloViewer.';
      } else {
        if (isLoggedInUserExplorerPublic != 1) {
          // Logged in PRO not public
          _m = '<span id="explorerOptInSpan">You need to opt-in to make your public visited Explorer tiles visible to other users in the heatmap: <button class="btn btn-primary btn-mini" onclick="explorerOptInBtnClick()">Opt-in</button></span>';
        } else {
            // Logged in PRO public - no message
        }
      }
    }
  }
  if (s_s) {
    // logged in Premium
  } else {
    // logged in not Premium
    _m += (_m=='' ? '' : '</p><p>') + 'The <a target="_blank" href="https://blog.veloviewer.com/veloviewer-explorer-tiles-global-heatmap#Premium">heatmap\'s detail is restricted</a> for non Strava Premium users. <span class=\'hidden-phone\'>VeloViewer users can g</span><span class=\'visible-phone\'>G</span>et a <a href="https://promo.strava.com/veloviewer/">60 day free Strava Premium trial here</a>.';
  }


  //if (+loggedInAthleteId > 0 && !s_s) _n = 'Want to see more detail on the public heat maps? VeloViewer users can get a <a href="https://promo.strava.com/veloviewer/">60 day free Strava Premium trial here</a>. ';
  //if(isFree) _n = _n + 'As a free user your own exploration is restricted to 25 activities. <a href="/update">Upgrade to PRO</b></a> to see your entire exploration.</p> ';

// Discover the popular, less known and unvisited tiles according to VeloViewer users\' public data.

  if (_n != '') {
    d3.select('#tourInfo')
      .classed('alert alert-info', true)
      .style({
        "display": null,
        "margin": "-5px 0px 5px 0px"
      })
      .html(_n);
  }

  if (_m != '') {
    d3.select('#mapText')
      .style({
        "display": null,
        "margin": (_mode == 's') ? null : "-10px 0px 5px 5px"
      })
      .html(_m);
  }

  if (_mode != 's' || +loggedInAthleteId > 0){
    removeInvalidLayers();

    lm_ZoomResetButton = L.functionButtons([{
        content: '<img id="resetZoomBtnImg" src="images/resetzoom.png" />',
        title: 'Zoom to show VeloViewer Explorer Tiles'
    }], {
        position: 'bottomright'
    });

    lm_ZoomResetButton.on('clicked', function(data) {
        localMap.setView(localMap.getBounds().getCenter(), 9);
        /*if (bi != null) {
          window.clearInterval(bi);
          d3.select(document.getElementById('resetZoomBtnImg').parentElement).style('background','#fff');
        }*/
    });
    localMap.addControl(lm_ZoomResetButton);

    if(typeof(cacheAct) !== 'undefined'){
        let a = cacheAct;
        delete cacheAct;

        for (var i = 0; i < a.length; i++) {
            if (((typeof(a[i].map) !== 'undefined' && a[i].map != null) || typeof(texplorer[a[i].i]) !== 'undefined') && a[i].t.substring(0, 7) != 'Virtual') {
                getActMapTiles(a[i], explorerZoom, 100);
            }
        }
        tilesCalculated = true;

        expSyncTiles(a);
        /*if(_mode == 's')
          expSyncCountries(a, regionStats);
        else
          expSyncCountries(a);*/

        //store a reduced list of tiles and counts for logged in user
        let tmp = a.filter(function(d){
          return d.tiles && d.tiles.length > 0 && !d.deleted
        }).flatMap(function(d){
          return d.tiles.map(function(e){return {x: e.x, y: e.y, v: 1, a: 1}})
        });

        _myTiles = [];

        tmp.forEach((d) => {
          let x = _myTiles.find(function (e){
            return d.x == e.x && d.y == e.y;
          });

          if(x !== undefined){
            x.v++;
          } else {
            _myTiles.push(d);
          }
        });
        //set max value for my tiles as they arrive in one request
        //maxMyVisits.val = Math.max.apply(Math, _myTiles.map(function(o) { return o.v; }));

        //get center of max explorer clump for focus
        let su = {};
        _myExpTiles = setExplorer(su, a, true);
        liveData = a;

        su.totalDist2 = d3.sum(a.filter(function(d) {
            return d.m == 0 && d.trn == 0 && d.map && d.t != 'VirtualRide' && d.t != 'VirtualRun'
        }).map(function(d) {
            return d.d
        }));

        let x = [];
        let y = [];
        for(n in maxClump){
          let t = n.split('-');
          x.push(+t[0]);
          y.push(+t[1]);
        }

        x.sort(function(a, b){return a - b});
        y.sort(function(a, b){return a - b});

        //onload show the explorer tiles?
        let mq = maxSquareShown.val;
        let mc = maxClusterShown.val;

        let dl = d3.select('#expBests')
          .append('dl')
          .attr('id', 'expBestsList')
          .classed('dl-horizontal', true);

        dl.append('dt')
          .attr('title', "The Explorer score is the total number of zoom level " + explorerZoom + " map tiles you've passed through divided by the distance traveled rewarding those that like to visit new places, roads and trails. To see your the tiles you've covered go to your Activities page and select the Explorer box below the map.")
          .text('Explorer score:');
        dl.append('dd')
          .html('<span title="You have passed through ' + su.explorer + ' zoom level ' + explorerZoom + ' map tiles averaging ' + f3dp(lrgLenMult * su.totalDist2 / su.explorer) + ' ' + lrgLenUnit + ' per tile">' + su.explorer + ' tiles, average of ' + f3dp(lrgLenMult * su.totalDist2 / su.explorer) + ' ' + lrgLenUnit + ' per tile</span>');

        dl.append('dt')
          //.attr('title', "The Explorer score is the total number of zoom level " + explorerZoom + " map tiles you've passed through divided by the distance traveled rewarding those that like to visit new places, roads and trails. To see your the tiles you've covered go to your Activities page and select the Explorer box below the map.")
          .text('Max square:');
        dl.append('dd')
          .html((su.explorerMax ? su.explorerMax + 'x' + su.explorerMax : '0x0'));

        dl.append('dt')
          //.attr('title', "The Explorer score is the total number of zoom level " + explorerZoom + " map tiles you've passed through divided by the distance traveled rewarding those that like to visit new places, roads and trails. To see your the tiles you've covered go to your Activities page and select the Explorer box below the map.")
          .text('Max cluster:');
        dl.append('dd')
          .html(su.explorerClumpMax);

        /*let textnode = document.createTextNode("");
        let explorerDT = document.createElement('dt');
        explorerDT.title = "The Explorer score is the total number of zoom level " + explorerZoom + " map tiles you've passed through divided by the distance traveled rewarding those that like to visit new places, roads and trails. To see your the tiles you've covered go to your Activities page and select the Explorer box below the map.";
        explorerDT.innerHTML = 'Explorer score:';
        explorerDT.appendChild(textnode);

        let explorerDD = document.createElement('dd');
        explorerDD.appendChild(textnode);
        explorerDD.id = 'explorerDD';
        dl.node().appendChild(explorerDT);
        dl.node().appendChild(explorerDD);*/

        //explorerKMLDisplayLink();

        if(x.length > 0 && y.length > 0){

          let c = new L.LatLngBounds([tile2lat(y[y.length-1], 14), tile2long(x[0], 14)], [tile2lat(y[0], 14), tile2long(x[x.length-1], 14)]);
          localMap.setView(c.getCenter(), 9);

          return;
        }
    }

    //fallback to London
    localMap.setView(new L.LatLng(51.509865, -0.118092), 9);
  }
  return;
}

function resizeMap() {
  $('#mapContainer').css('height', $(window).height() - document.getElementById('mapContainer').getBoundingClientRect().top - 20);
  if(typeof(map)!=="undefined"){
    if (map.invalidateSize) {
      map.invalidateSize();
    }
  }
}

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);
}

$(document).ready(function() {
  lm_type = 'explorer';

  let gray = ['contrast:100%', 'grayscale:100%'];

  lm_layerVeloviewerGrey = new L.tileLayer.colorFilter('https://{s}.tile.thunderforest.com/veloviewer-oh4/{z}/{x}/{y}' + retinaImg + '.png?apikey=' + lm_thunderforestKey, {attribution: lm_tfa, filter: gray});
  lm_layerVeloviewerGrey.id = 'veloviewerGrey';

  lm_baseLayers = {
      //"Live": {},
      "VeloViewer Grey": lm_layerVeloviewerGrey,
      "VeloViewer": lm_layerVeloviewer,
      "Landscape": lm_layerLandscape,
      "Outdoors": lm_layerOutdoors,
      "Open Cycle": lm_layerCycle,
      "Open Street": lm_layerOSM,
      //"Black & White": lm_layerBaW,
      "Terrain": lm_layerGTerrain,
      "Hybrid": lm_layerGHybrid,
      "Satellite": lm_layerGSatellite
  };

  if(_mode != 's' || +loggedInAthleteId > 0){
    initMap('', true, true);

    exp = L.gridLayer.explorer();
    localMap.addLayer(exp);
    exp9 = new L.FeatureGroup();
    my9 = new L.FeatureGroup();

    localMap.setMaxZoom(15);
    localMap.setMinZoom(3);

    bindColors();
    setModalInfo();
    setScaleUI();
  }

  d3.select("#selColorLegend")
      .selectAll("div")
      .data(function(d){ return (s_s || choropleth.val == 'm') ? ['1','2','4','8','16','32','64','128','256','512'] : ['1', '20', '40', '80']; })
      .enter()
      .append("div")
      .append("div")
      .html(function(d){ return d});

  // main description
  d3.select('#mainDescription').html('Over 268 million tiles across the world are ready to be explored. <a target="_blank" href="https://blog.veloviewer.com/veloviewer-explorer-overview/">VeloViewer Explorer Tiles explained.</a> Data from Apple, Wahoo, Garmin, Suunto, Coros, etc. devices.');

  if (+loggedInAthleteId <= 0) {
    // Not logged in
    _n = '<a href="https://www.strava.com/oauth/authorize?client_id=36&scope=read,read_all,profile:read_all,profile:write,activity:read,activity:read_all,activity:write&redirect_uri=' + document.location.origin + document.location.pathname + '&response_type=code&approval_prompt=auto&state=private"><img style="height:40px" src="https://cf.veloviewer.com/img/btn_strava_connectwith_orange.svg"></a> <div style="display: inline-block;vertical-align: middle;">to view your own <br class="visible-phone">explorations.</div>';
    localInit();
  }
  // isLoggedInUserExplorerPublic

  /*if(_n.length > 0){
    d3.select('#tourInfo')
      .style({
        "display": null,
        "margin": ""-5px 0px 5px 0px"
      })
      .append("div")
      .classed('alert alert-info', true)
      .style('padding-bottom', '0px')
      .html('<p><b>Important note:  </b>' + _n + '</p>');

    d3.select('#tourInfo p').style('margin-bottom', '0px');
  }*/

  window.addEventListener('resize', resizeMap, true);
  resizeMap();
});
