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

console.log('%cIMPORTANT GDPR INFO!', 'color: red; font-size: 20px; font-weight: bold;');
console.log('Please don\'t take data out of VeloViewer to use on other sites. This breaks GDPR privacy laws and the Strava API Terms and Conditions. VeloViewer users may opt-in to share their public data on veloviewer.com but that does not mean they give permission for their data to be taken, stored or displayed outside of VeloViewer. If you wish to get hold of Strava data for your own site you must use the Strava API and get individual athletes to authorise their accounts with your own application.');

var vv_category20 = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf", "#aec7e8", "#ffbb78", "#98df8a", "#ff9896", "#c5b0d5", "#c49c94", "#f7b6d2", "#c7c7c7", "#dbdb8d", "#9edae5"];
vv_category20 = d3.merge([vv_category20, vv_category20, vv_category20, vv_category20, vv_category20]);

var allTiles = {},
    explorerTiles = {};
var hasLocalStorage = false;
try {
    localStorage.setItem('test', 'test');
    localStorage.removeItem('test');
    hasLocalStorage = true;
} catch (e) {
    hasLocalStorage = false;
}

if (location.hash == '#clear' && hasLocalStorage) {
    localStorage.clear();
}

var tilesCalculated = false,
    explorerZoom = 14,
    gMapFullGradArr = [];
var orderBatch = 0;
var g_order, errorDisplayed, progressTotal = 0,
    progressComplete = 0;

if (typeof(isBeta) === 'undefined') {
    var isBeta = true;
}

var isRateBusy = false;
if (typeof(rateTime15) !== 'undefined') {
    var rateTime15Perc = rateTime15 / 900;
    var rateUse15Perc = rateUsage15 / rateLimit15;
    var rateTimeDayPerc = rateTimeDay / (24 * 60 * 60);
    var rateUseDayPerc = rateUsageDay / rateLimitDay;

    isRateBusy = (rateUse15Perc / rateTime15Perc > 0.8) || (rateTimeDayPerc > .2 && rateUseDayPerc / rateTimeDayPerc > 0.9);
}

function ieVersion(uaString) {
    uaString = uaString || navigator.userAgent;
    var match = /\b(MSIE |Trident.*?rv:|Edge\/)(\d+)/.exec(uaString);
    if (match) return parseInt(match[2])
}

var dows = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];

var zwiftIconSVG = '<svg id="logo-zwift-lockup" viewBox="0 0 130 130" width="100%" height="100%"><path fill="#fc6719" class="logo-z" d="M93.32,80H79.74l48.95-80H20C8.95,0,0,8.95,0,20s8.95,20,20,20h13.51L2.89,90.05c0,0-2.89,4.21-2.89,10.28  C0,111.19,8.81,120,19.67,120h73.65c11.05,0,20-8.95,20-20C113.32,88.95,104.36,80,93.32,80z"></path></svg>';

function throttled(wait, func, options) {
    var context, args, result, event, timeout = null,
        previous = 0;

    if (!options) options = {};
    var later = function() {
        d3.event = event;
        previous = options.leading === false ? 0 : Date.now();
        timeout = null;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
    };
    return function() {
        var now = Date.now();
        event = d3.event;
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            //d3.event = event;
            result = func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining);
        }
        return result;
    };
};

function removeItemsByIndexes(array, indexes) {
    // Sort indexes in descending order to avoid index shifting issues
    indexes.sort((a, b) => b - a);
    
    // Remove items from the array
    indexes.forEach(index => {
        if (index >= 0 && index < array.length) {
            array.splice(index, 1);
        }
    });
    
    return array;
}

function copyTextToClipboard(text) {
    var textArea = document.createElement("textarea");
  
    //
    // *** This styling is an extra step which is likely not required. ***
    //
    // Why is it here? To ensure:
    // 1. the element is able to have focus and selection.
    // 2. if the element was to flash render it has minimal visual impact.
    // 3. less flakyness with selection and copying which **might** occur if
    //    the textarea element is not visible.
    //
    // The likelihood is the element won't even render, not even a
    // flash, so some of these are just precautions. However in
    // Internet Explorer the element is visible whilst the popup
    // box asking the user for permission for the web page to
    // copy to the clipboard.
    //
  
    // Place in the top-left corner of screen regardless of scroll position.
    textArea.style.position = 'fixed';
    textArea.style.top = 0;
    textArea.style.left = 0;
  
    // Ensure it has a small width and height. Setting to 1px / 1em
    // doesn't work as this gives a negative w/h on some browsers.
    textArea.style.width = '2em';
    textArea.style.height = '2em';
  
    // We don't need padding, reducing the size if it does flash render.
    textArea.style.padding = 0;
  
    // Clean up any borders.
    textArea.style.border = 'none';
    textArea.style.outline = 'none';
    textArea.style.boxShadow = 'none';
  
    // Avoid flash of the white box if rendered for any reason.
    textArea.style.background = 'transparent';
  
  
    textArea.value = text;
  
    document.body.appendChild(textArea);
    textArea.focus();
    textArea.select();
  
    try {
      var successful = document.execCommand('copy');
      var msg = successful ? 'successful' : 'unsuccessful';
      console.log('Copying text command was ' + msg);
    } catch (err) {
      console.log('Oops, unable to copy');
    }
  
    document.body.removeChild(textArea);
  }
  
function pSBC(p,c0,c1,l) {
    var r,g,b,P,f,t,h,i=parseInt,m=Math.round,a=typeof(c1)=="string";
    if(typeof(p)!="number"||p<-1||p>1||typeof(c0)!="string"||(c0[0]!='r'&&c0[0]!='#')||(c1&&!a))return null;
    if(!this.pSBCr) {
      this.pSBCr= function(d) {
        var n=d.length,x={};
        if(n>9){
            [r,g,b,a]=d=d.split(","),n=d.length;
            if(n<3||n>4)return null;
            x.r=i(r[3]=="a"?r.slice(5):r.slice(4)),x.g=i(g),x.b=i(b),x.a=a?parseFloat(a):-1
        }else{
            if(n==8||n==6||n<4)return null;
            if(n<6)d="#"+d[1]+d[1]+d[2]+d[2]+d[3]+d[3]+(n>4?d[4]+d[4]:"");
            d=i(d.slice(1),16);
            if(n==9||n==5)x.r=d>>24&255,x.g=d>>16&255,x.b=d>>8&255,x.a=m((d&255)/0.255)/1000;
            else x.r=d>>16,x.g=d>>8&255,x.b=d&255,x.a=-1
        }return x};
      }
    h=c0.length>9,h=a?c1.length>9?true:c1=="c"?!h:false:h,f=this.pSBCr(c0),P=p<0,t=c1&&c1!="c"?this.pSBCr(c1):P?{r:0,g:0,b:0,a:-1}:{r:255,g:255,b:255,a:-1},p=P?p*-1:p,P=1-p;
    if(!f||!t)return null;
    if(l)r=m(P*f.r+p*t.r),g=m(P*f.g+p*t.g),b=m(P*f.b+p*t.b);
    else r=m(Math.pow((P*Math.pow(f.r,2)+p*Math.pow(t.r,2)),0.5)),g=m(Math.pow((P*Math.pow(f.g,2)+p*Math.pow(t.g,2)),0.5)),b=m(Math.pow((P*Math.pow(f.b,2)+p*Math.pow(t.b,2)),0.5));
    a=f.a,t=t.a,f=a>=0||t>=0,a=f?a<0?t:t<0?a:a*P+t*p:0;
    if(h)return"rgb"+(f?"a(":"(")+r+","+g+","+b+(f?","+m(a*1000)/1000:"")+")";
    else return"#"+(4294967296+r*16777216+g*65536+b*256+(f?m(a*255):0)).toString(16).slice(1,f?undefined:-2)
}

function copyToClipboard(str) {
  var el = document.createElement('textarea');
  el.value = str;
  el.setAttribute('readonly', '');
  el.style.position = 'absolute';
  el.style.left = '-9999px';
  document.body.appendChild(el);
  const selected =
    document.getSelection().rangeCount > 0
      ? document.getSelection().getRangeAt(0)
      : false;
  el.select();
  document.execCommand('copy');
  document.body.removeChild(el);
  if (selected) {
    document.getSelection().removeAllRanges();
    document.getSelection().addRange(selected);
  }
}

if (!String.prototype.encodeHTML) {
    String.prototype.encodeHTML = function () {
        return this.replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&apos;');
    };
}

function checkNestedObj(obj /*, level1, level2, ... levelN*/) {
  var args = Array.prototype.slice.call(arguments, 1);

  for (var i = 0; i < args.length; i++) {
    if (!obj || !obj.hasOwnProperty(args[i])) {
      return false;
    }
    obj = obj[args[i]];
  }
  return true;
}

function getValuesByKey(object, key) {
    var values = [];
    JSON.stringify(object, function(k, v) { 
    if (k === key) values.push(v);
        return v;
    });
    return values;
}

function getDateOfISOWeek(w, y) {
    var simple = new Date(y, 0, 1 + (w - 1) * 7);
    var dow = simple.getDay();
    var ISOweekStart = simple;
    if (dow <= 4)
        ISOweekStart.setDate(simple.getDate() - simple.getDay() + 1);
    else
        ISOweekStart.setDate(simple.getDate() + 8 - simple.getDay());
    return ISOweekStart;
}

var fInt = function(d) {
    var r = d3.format(',.0f')(d)
    return (r == '-0' ? '0' : r)
};
var f1dp = function(d) {
    var r = d3.format(',.1f')(d)
    return (r == '-0.0' ? '0' : r)
};
var f2dp = function(d) {
    var r = d3.format(',.2f')(d)
    return (r == '-0.00' ? '0' : r)
};
var f3dp = function(d) {
    var r = d3.format(',.3f')(d)
    return (r == '-0.000' ? '0' : r)
};
var f4dp = d3.format(',.4f');
var f5dp = d3.format(',.5f');
var f6dp = d3.format(',.6f');
var f02 = d3.format('02d');
var fTime = d3.time.format('%X');
var sTime = d3.time.format('%M:%S');
var lTime = d3.time.format('%H:%M');
var fDate = d3.time.format(typeof(datePreference) === 'undefined' ? '%d/%m/%Y' : datePreference);
var fDate2 = d3.time.format(typeof(datePreference) === 'undefined' ? '%d/%m/%y' : (datePreference.indexOf('y') > -1 ? datePreference.replace('y', 'Y') : datePreference.replace('Y', 'y')));
var fDate3 = d3.time.format(typeof(datePreference) === 'undefined' ? '%a %d/%m/%y' : '%a ' + (datePreference.indexOf('y') > -1 ? datePreference.replace('y', 'Y') : datePreference.replace('Y', 'y')));
var parseDate = d3.time.format('%Y-%m-%d').parse;
var fDateTime = d3.time.format('%Y-%m-%d %H:%M:%S');
var fDateTime2 = function(d) {
    return fDate(d) + ' ' + fTime(d);
}
var fTemp = function(d) {
    return f1dp(d) + ' °C';
};
var fSpeed1 = function(d) {
    return f1dp(d * speedMult) + ' ' + speedUnit
};
var fPercent0 = d3.format(',.0%');
var fPercent1 = d3.format(',.1%');
var fPercent2 = d3.format(',.2%');
var oneDay = 24 * 60 * 60 * 1000;
var parseDateTime = function(d, tz) {
    if (typeof(d) === 'undefined' || d == null) {
        return null
    }
    var retDate;
    if (d.toString().substr(4, 1) == '-') {
        retDate = d3.time.format('%Y-%m-%d %H:%M:%S').parse(d.replace('T', ' ').replace('Z', ''));
        if (tz != null) {
            retDate = new Date(retDate.getTime() + timeZoneDiff(tz));
        }
    } else {
        retDate = new Date(d);
    }
    return retDate;
}
var fDist = function(d, dp) {
    var val = d * lrgLenMult;
    if (typeof(dp) === 'undefined' || dp == null) {
        dp = 1;
    }
    switch (dp) {
        case 1:
            return f1dp(val) + ' ' + lrgLenUnit;
        case 0:
            return fInt(val) + ' ' + lrgLenUnit;
        case 2:
            return f2dp(val) + ' ' + lrgLenUnit;
        case 3:
            return f3dp(val) + ' ' + lrgLenUnit;
    }
    return d;
}
var fSmlDist = function(d, dp) {
    return fInt(d * smlLenMult) + ' ' + smlLenUnit
}
var fDistAuto = function(d) {
    var val = d * lrgLenMult;
    if (val == Math.round(val) || val > 100) {
        return fInt(val) + ' ' + lrgLenUnit;
    }
    if (val > 10) {
        return f1dp(val) + ' ' + lrgLenUnit;
    }
    if (val > 1) {
        return f2dp(val) + ' ' + lrgLenUnit;
    }
    return fInt(d * smlLenMult) + ' ' + smlLenUnit;
}

function convertCtoF(v) {
    return Math.round(v * 9 / 5 + 32);
}
var fETime = function(d) {
    if (d == null) {
        return 'n/a';
    }
    return d.toHrMinSec();
}

function displayTimezoneDatetime(dateTime, offsetHours, formatFn) {
    var utc = dateTime.getTime() + (dateTime.getTimezoneOffset() * 60000);
    var nd = new Date(utc + (3600000 * offsetHours));
    return formatFn(nd);
}

function timeZoneDiff(tzStr) {
    var mult = 1;
    if (tzStr.substr(0, 1) == '-') {
        mult = -1;
    }
    var val = tzStr.split(':');
    var hours = parseInt(val[0].replace(/[-+]/, ''));
    var mins = parseInt(val[1]);

    return mult * (hours * 1000 * 60 * 60 + mins * 1000 * 60);
}

Date.prototype.dayOfYear = function() {
    var j1 = new Date(this);
    j1.setMonth(0, 0);
    return Math.round((this - j1) / 8.64e7);
}

function daysBetween(firstDate, secondDate) {
    return Math.round(Math.abs((firstDate.getTime() - secondDate.getTime()) / (oneDay)))
}

Date.prototype.getWeekNumber = function() {
    var d = new Date(+this);
    d.setHours(0, 0, 0);
    d.setDate(d.getDate() + 4 - (d.getDay() || 7));
    var jan1 = new Date(d.getFullYear(), 0, 1);
    return Math.ceil((((d - jan1 - (jan1.getTimezoneOffset() < 0 ? 60 * 60 * 1000 : 0)) / 8.64e7) + 1) / 7);
};

Date.prototype.getFullWeek = function() {
    var jan1, w, d = new Date(this);
    d.setDate(d.getDate() + 4 - (d.getDay() || 7)); // Set to nearest Thursday: current date + 4 - current day number, make Sunday's day number 7
    jan1 = new Date(d.getFullYear(), 0, 1); // Get first day of year
    w = Math.floor((((d - jan1) / 86400000) + 1) / 7); // Calculate full weeks to nearest Thursday
    return {
        y: d.getFullYear(),
        w: w
    };
};
//Returns ISO 8601 week number
Date.prototype.getWeek = function() {
    return this.getFullWeek().w;
};
Date.prototype.getWeekYear = function() {
    var v = this.getFullWeek();
    return 'Week ' + v.w + ', ' + v.y
};

Array.prototype.last = function() {
    return this[this.length - 1];
}

Array.prototype.span = function() {
    return this[1] - this[0];
}

Array.prototype.intersect = function(b) {
    var a = this;
    var ai = 0,
        bi = 0;
    var result = new Array();

    while (ai < a.length && bi < b.length) {
        if (a[ai] < b[bi]) {
            ai++;
        } else if (a[ai] > b[bi]) {
            bi++;
        } else /* they're equal */ {
            result.push(a[ai]);
            ai++;
            bi++;
        }
    }

    return result;
}

function injectStyles(rule) {
    var div = $("<div />", {
        html: '&shy;<style>' + rule + '</style>'
    }).appendTo("body");
}

Array.prototype.removeIndexes = function(d) {
    for (var i = d.length - 1; i >= 0; i--) {
        this.splice(d[i], 1);
    }
    return this;
}

String.prototype.htmlEscape = function() {
    return String(this)
        .replace(/&/g, '&amp;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;');
}

String.prototype.escapeRegExp = function() {
    return this.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}

String.prototype.toSeconds = function() {
    var a = this.split(':');
    return (+a[0]) * 60 * 60 + (+a[1]) * 60 + (+a[2]);
}

String.prototype.fullTextSearch = function(str) {
    var text = this;
    var excluded = false;
    if (str == null || str == '') {
        return true;
    }
    //Split on "or"
    var orArr = str.replaceAll(',', ' or ').split(' or ');
    var regStr = '';

    orArr.forEach(function(d, i) {
        var items = [];
        items = items.concat(d.match(/"[^"]*"/g));
        items = items.concat(d.replace(/"[^"]*"/g, '').replace(/\s+/g, ' ').trim().split(' '));

        if (i > 0) regStr += '|';

        regStr += '(';
        //
        items.forEach(function(e) {
            if (e != null && e != '') {
                if (e.substr(0, 1) == '-') {
                    if (text.toLowerCase().indexOf(e.substr(1).toLowerCase()) > -1) {
                        excluded = true;
                    }
                } else {
                    regStr += '(?=.*' + e.replace(/"/g, '').escapeRegExp() + ')';
                }
            }
        });
        regStr += ')';
        //
    });
    if (excluded) {
        return false;
    }
    return text.match(new RegExp(regStr, 'i')) != null;
}

String.prototype.replaceAll = function(a, b) {
    var retVal = this.replace(a, b);
    if (retVal.indexOf(a) > -1) {
        return retVal.replaceAll(a, b);
    }
    return retVal;
}
String.prototype.parseJSON = function() {
    return jQuery.parseJSON(this.replaceAll("\b", ""));
}

function ssGet(id, obj) {
    return JSON.parse(localStorage.getItem(id, obj));
}

function ssSet(id, obj) {
    localStorage.setItem(id, JSON.stringify(obj));
}

Number.prototype.between = function(a, b) {
    var min = Math.min(a, b),
        max = Math.max(a, b);

    return this >= min && this <= max;
};

Number.prototype.toDayHrMinSec = function() {
    var v = this;
    var a = v < 0;
    if (a) {
        v = Math.abs(v);
    }
    var days = Math.floor(v / (60 * 60 * 24));
    v = v - (days * 60 * 60 * 24);
    var hours = Math.floor(v / (60 * 60));
    v = v - (hours * 60 * 60);
    var mins = Math.floor(v / 60);
    var secs = Math.round(v - (mins * 60));

    if (secs >= 60) {
        secs -= 60;
        mins += 1;
    }
    if (mins >= 60) {
        mins -= 60;
        hours += 1;
    }
    if (hours >= 24) {
        hours -= 24;
        days += 1;
    }

    return (a ? '-' : '') + (days > 0 ? days + 'd ' : '') + f02(hours) + ':' + f02(mins) + ':' + f02(secs)
}

Number.prototype.toDayHrMinSec2 = function() {
    var v = this;
    var a = v < 0;
    if (a) {
        v = Math.abs(v);
    }
    var days = Math.floor(v / (60 * 60 * 24));
    v = v - (days * 60 * 60 * 24);
    var hours = Math.floor(v / (60 * 60));
    v = v - (hours * 60 * 60);
    var mins = Math.floor(v / 60);
    var secs = Math.round(v - (mins * 60));

    if (secs >= 60) {
        secs -= 60;
        mins += 1;
    }
    if (mins >= 60) {
        mins -= 60;
        hours += 1;
    }
    if (hours >= 24) {
        hours -= 24;
        days += 1;
    }

    return (a ? '-' : '') + (days > 0 ? days + 'd ' : '') + (hours > 0 ? f02(hours) + ':' : '') + f02(mins) + ':' + f02(secs)
}

Number.prototype.toHrMinSec = function() {
    var v = this;
    var posNeg = '';
    if (v < 0) {
        posNeg = '-';
        v *= -1;
    }
    var hours = Math.floor(v / (60 * 60));
    v = v - (hours * 60 * 60);
    var mins = Math.floor(v / 60);
    var secs = Math.round(v - (mins * 60));

    if (secs >= 60) {
        secs -= 60;
        mins += 1;
    }
    if (mins >= 60) {
        mins -= 60;
        hours += 1;
    }

    return posNeg + f02(hours) + ':' + f02(mins) + ':' + f02(secs)
}

Number.prototype.round = function(p_prec) {
    return Math.round(this * Math.pow(10, p_prec)) / Math.pow(10, p_prec);
}

Number.prototype.cleanFloat = function() {
    return (parseFloat(this.toPrecision(12)));
}


Number.prototype.toArray = function(p_num) {
    var arr = [];
    for (var i = 0; i < p_num; i++) {
        arr.push(+this);
    }
    return arr;
}

Date.prototype.yearDay = function(utc) {
    var start = new Date((utc ? this.getUTCFullYear() : this.getFullYear()), 0, 0);
    var diff = this - start;
    var oneDay = 1000 * 60 * 60 * 24;
    return Math.floor(diff / oneDay);
};

Date.prototype.isLeapYear = function(utc) {
    var y = utc ? this.getUTCFullYear() : this.getFullYear();
    return !(y % 4) && (y % 100) || !(y % 400);
};

function foreach(init, max, body, c) {
    doLoop(init);

    function doLoop(i) {
        if (i < max) {
            body(function() {
                doLoop(i + 1);
            });
        } else {
            c();
        }
    }
}

function isScrolledIntoView(el) {
    var rect = el.getBoundingClientRect();
    var elemTop = rect.top;
    var elemBottom = rect.bottom;

    // Only completely visible elements return true:
    var isVisible = (elemTop >= 0) && (elemBottom <= window.innerHeight);
    // Partially visible elements return true:
    //isVisible = elemTop < window.innerHeight && elemBottom >= 0;
    return isVisible;
}

function suf(i) {
    if ([11, 12, 13].indexOf(i % 100) == -1) {
        switch (i % 10) {
            case 1:
                return fInt(i) + 'st';
            case 2:
                return fInt(i) + 'nd';
            case 3:
                return fInt(i) + 'rd';
        }
    }
    return fInt(i) + 'th';
}

function decompress1(encoded, precision) {
    precision = Math.pow(10, -precision);
    var len = encoded.length,
        index = 0,
        val = 0,
        array = [];
    while (index < len) {
        var b, shift = 0,
            result = 0;
        do {
            b = encoded.charCodeAt(index++) - 63;
            result |= (b & 0x1f) << shift;
            shift += 5;
        } while (b >= 0x20);
        var dval = ((result & 1) ? ~(result >> 1) : (result >> 1));
        val += dval;
        array.push(val * precision);
    }
    return array;
}


function decompressN(encoded, precision, items) {
    if (typeof(encoded) === 'undefined' || encoded == null) {
        return [];
    }
    precision = Math.pow(10, -precision);
    var len = encoded.length,
        index = 0,
        oldVal = [],
        lat = 0,
        lng = 0,
        array = [];
    for (var i = 0; i < items; i++) {
        oldVal.push(0);
    }
    while (index < len) {
        for (var i = 0; i < items; i++) {
            var b, shift = 0,
                result = 0;
            do {
                b = encoded.charCodeAt(index++) - 63;
                result |= (b & 0x1f) << shift;
                shift += 5;
            } while (b >= 0x20);
            var dif = ((result & 1) ? ~(result >> 1) : (result >> 1));
            oldVal[i] += dif;

            array.push(oldVal[i] * precision);
        }
    }
    return array;
}

var encodeNumber2 = function(num) {
    var encodeString = '';
    var nextValue, finalValue;
    while (num >= 0x20) {
        nextValue = (0x20 | (num & 0x1f)) + 63;
        encodeString += (String.fromCharCode(nextValue));
        num >>= 5;
    }
    finalValue = num + 63;
    encodeString += (String.fromCharCode(finalValue));
    return encodeString;
};
var encodeSignedNumber = function(num) {
    var sgn_num = num << 1;
    if (num < 0) {
        sgn_num = ~(sgn_num);
    }

    return encodeNumber2(sgn_num);
};

function compress(points, precision, size) {
    size = typeof(size) === 'undefined' ? 2 : size;
    precision = typeof(precision) === 'undefined' ? 5 : precision;
    var i, dlat, dlng;
    var pval = [];
    var encoded_points = '';

    for (j = 0; j < size; j++) {
        pval[j] = 0;
    }
    precision = Math.pow(10, precision);

    for (i = 0; i < points.length; i++) {
        for (j = 0; j < size; j++) {
            var val = (size > 1 ? points[i][j] : points[i]);
            var valFloored = Math.floor(val * precision);
            dval = valFloored - pval[j];
            pval[j] = valFloored;
            encoded_points += encodeSignedNumber(dval);
        }
    }
    return encoded_points;
}

function jsonIndexOf(myArray, property, searchTerm) {
    for (var i = 0, len = myArray.length; i < len; i++) {
        if (myArray[i][property] === searchTerm) return i;
    }
    return -1;
}

var asyncCount = 0;

function asyncComplete(func) {
    asyncCount--;
    //if (asyncCount <= 0 || (typeof(ajaxBatchSize) !== 'undefined' && typeof(finalBatch) !== 'undefined' && !finalBatch && asyncCount <= ajaxBatchSize / 2)) {
        window.setTimeout(function() {
            func();
        }, 1);
    //}
}

var prgTimer;
var asyncPrgCount = 0;

function asyncWaitingPrg() {
    asyncPrgCount++;
    //var prgExtra = Math.min(9,Math.sqrt(asyncPrgCount+1))/10;
    var prgExtra = 1 / ((asyncPrgCount + 100) / 100);
    d3.select('#prgBar')
        .style('width', (progressTotal == 0 ? '0' : Math.round((progressComplete + prgExtra) * (100 / progressTotal))) + '%');
}

function asyncAdd2(asyncVar) {
    eval(asyncVar + '++');
    if (prgTimer == null) {
        //prgTimer = window.setTimeout(asyncWaitingPrg,10);
    }
}

var asyncComplete2Timer;

function asyncComplete2(func, asyncVar) {
    eval(asyncVar + '--');
    window.clearTimeout(prgTimer);
    progressComplete++;
    asyncPrgCount = 0;
    d3.select('#prgBar')
        .style('width', (progressTotal == 0 ? '0' : Math.round(progressComplete * (100 / progressTotal))) + '%')
    if (progressTotal - progressComplete > 0) {
        d3.select('#messageText')
            .text('Just getting the data, please wait... ' + (progressTotal - progressComplete) + ' remaining.');

        if (asyncComplete2Timer) clearTimeout(asyncComplete2Timer);
        asyncComplete2Timer = setTimeout(function() {
            asyncComplete2Timer = null;
            d3.select('#messageText')
                .text('Just getting the data, please wait... ' + (progressTotal - progressComplete) + ' remaining. Sorry for the wait, sometimes this can take a while!');
        }, 30000);
        //prgTimer = window.setTimeout(asyncWaitingPrg,100);
    } else {
        d3.select('#prgBar')
            .style('width', '100%')
        d3.select('#messageText')
            .text('All loaded. Processing...');
    }
    if (eval(asyncVar) == 0) {
        window.setTimeout(function() {
            func();
        }, 1);
    }
}

function handleAjaxError(xhr, textStatus, errorThrown, ajaxObj) {
    if (textStatus == 'timeout') {
        ajaxObj.tryCount++;
        if (ajaxObj.tryCount <= ajaxObj.retryLimit) {
            $.ajax(ajaxObj);
            return;
        }
        if (!errorDisplayed) {
            alert('Timeout');
            errorDisplayed = true;
        }
        return;
    }
    if (xhr.status == 500) {
        if (!errorDisplayed) {
            alert('Server error');
            errorDisplayed = true;
        }
    } else {
        if (!errorDisplayed) {
            //alert('Some other kind of error');
            errorDisplayed = true;
        }
    }
}

function handleAjaxError2(xhr, textStatus, errorThrown, ajaxObj, nextFunc) {
    ajaxObj.tryCount++;
    if (ajaxObj.tryCount <= ajaxObj.retryLimit) {
        $.ajax(ajaxObj);
        return;
    }
    if (!errorDisplayed) {
        alert(textStatus);
        errorDisplayed = true;
    }
    if (nextFunc != null) {
        asyncComplete(nextFunc);
    }
}

function setCookie(c_name, value, exdays) {
    if (hasLocalStorage) {
        localStorage.setItem('oc_'+c_name, value);
    }
    /*deleteCookie(c_name);
    var exdate = new Date();
    exdate.setDate(exdate.getDate() + exdays);
    var c_value = escape(value) + ((exdays == null) ? "" : "; expires=" + exdate.toUTCString()) + ';domain=.veloviewer.com;path=/;samesite=strict';
    document.cookie = c_name + "=" + c_value;*/
}

function getCookie(c_name) {
    let r = null;
    if (hasLocalStorage) {
        r = localStorage.getItem('oc_'+c_name);
    }
    return r == null ? undefined : r;
    /*var i, x, y, ARRcookies = document.cookie.split(";");
    for (i = 0; i < ARRcookies.length; i++) {
        x = ARRcookies[i].substr(0, ARRcookies[i].indexOf("="));
        y = ARRcookies[i].substr(ARRcookies[i].indexOf("=") + 1);
        x = x.replace(/^\s+|\s+$/g, "");
        if (x == c_name) {
            return unescape(y);
        }
    }*/
}

function deleteCookie(c_name) {
    document.cookie = c_name + '=;expires=Thu, 01 Jan 1970 00:00:01 GMT;domain=.veloviewer.com;path=/;samesite=strict';
};

function getParameterByName(name) {
    name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
    var regexS = "[\\?&]" + name + "=([^&#]*)";
    var regex = new RegExp(regexS);
    var results = regex.exec(window.location.search);
    if (results == null)
        return "";
    else
        return decodeURIComponent(results[1].replace(/\+/g, " "));
}

function getData(order, changeDir, batch) {
    orderBatch = batch;
    g_order = order;
    window.scrollTo(0, 0);
    setPagination();

    if ($(window).innerWidth() >= 768) {
        document.getElementById("tab1" + orderCol).className = document.getElementById("tab1" + orderCol).className.replace("headerSortUp", "").replace("headerSortDown", "");
        $('#' + pageType + 'Table > tbody').html("<p>Getting data... please wait.</p>");
        $('#' + pageType + 'List').html("<p>Refresh page to view content.</p>");
    } else {
        $('#' + pageType + 'List').html("<p>Getting data... please wait.</p>");
        $('#' + pageType + 'Table > tbody').html("<p>Refresh page to view content.</p>");
    }

    if (orderCol == order) {
        if (changeDir) {
            orderDir = (orderDir == "0") ? "1" : "0";
        }
    } else {
        orderCol = order;
        orderDir = 0;
    }

    rowCount = 100;
    if ($(window).innerWidth() >= 768) {
        $.get("/ApiAthlete" + pageType + "TableRows.php?v=1&athleteId=" + athleteId + "&order=" + order + "&direction=" + orderDir + "&rowCount=" + rowCount + "&batch=" + batch + ((nameFilter == '') ? '' : '&nameFilter=' + nameFilter), function(data) {
            $('#' + pageType + 'Table > tbody').html(data);

            $('.segmentLink').each(function() {
                var links = '';
                if (athleteId == '306128') {
                    links += '<a data-parentid="segment' + $(this).data("segmentid") + '" class="popoverLink" href="/segment/' + $(this).data("segmentid") + '/athlete/' + athleteId + '" title="View segment details">View segment details</a>';
                }
                links += '<a target="_blank" data-parentid="segment' + $(this).data("segmentid") + '" class="popoverLink" href="http://www.strava.com/segments/' + $(this).data("segmentid") + '" title="View segment on Strava (external link)">View on Strava</a>';
                links += '<a target="_blank" data-parentid="segment' + $(this).data("segmentid") + '" class="popoverLink" href="http://www.jonathanokeeffe.com/strava/segmentDetails.php?segmentId=' + $(this).data("segmentid") + '" title="View as much detail about this segment as you could possible imagine (external link)">O\'Keeffe\'s Segment History</a>';
                links += '<a target="_blank" data-parentid="segment' + $(this).data("segmentid") + '" class="popoverLink" href="http://raceshape.com/strava-segments/' + $(this).data("segmentid") + '?rides=' + $(this).data("effortid") + '-' + $(this).data("komid") + '" title="See where you lost time to the KOM (external link)">RaceShape your PR against KOM</a>';
                links += '<a target="_blank" data-parentid="segment' + $(this).data("segmentid") + '" class="popoverLink" href="http://strava-tools.raceshape.com/exporter/?url=http://app.strava.com/segments/' + $(this).data("segmentid") + '" title="Download the KOM\'s gpx/tcx/crs file for Virtual Partner action via RaceShape (external link)">Virtual Partner GPX download</a>';
                links += '<a data-parentid="segment' + $(this).data("segmentid") + '" class="popoverLink" href="NewLeaderboard.php?segmentId=' + $(this).data("segmentid") + '" title="View the alternative leaderboard for this segment">Alternative leaderboard</a>';
                links += '<a data-parentid="segment' + $(this).data("segmentid") + '" class="popoverLink" href="NewLeaderboard.php?segmentId=' + $(this).data("segmentid") + '&athleteId=' + athleteId + '" title="View the alternative leaderboard for this segment">Your alternative leaderboard</a>';
                links += '<a data-parentid="segment' + $(this).data("segmentid") + '" data-segmentid="' + $(this).data("segmentid") + '" class="updateSegmentDetails" href="#" title="Update this segments details (name, elevation, VAM and relative power)">Update segment details</a>';

                $(this).popover({
                    title: 'Segment links <button type="button" data-parentid="segment' + $(this).data("segmentid") + '" class="close popoverClose" data-dismiss="alert">x</button>',
                    content: links,
                    trigger: 'manual'
                });
            });

            $('.segmentLink').click(function() {
                $(this).popover('toggle');

                $('.popoverClose').click(function() {
                    $('#' + $(this).data('parentid')).popover('hide');
                });

                $('.popoverLink').click(function() {
                    $('#' + $(this).data('parentid')).popover('hide');
                });

                return false;
            });

            $('.segmentRideLink').each(function() {
                var links = '<a target="_blank" data-parentid="segmentride' + $(this).data("segmentid") + '" class="popoverLink" href="http://www.strava.com/activities/' + $(this).data("rideid") + '" title="View ride on Strava (external link)">View on Strava</a>';
                links += '<a target="_blank" data-parentid="segmentride' + $(this).data("segmentid") + '" class="popoverLink" href="http://www.jonathanokeeffe.com/strava/map.php?segment=' + $(this).data("segmentid") + '" title="View a map of all rides that have taken in this segment (external link)">O\'Keeffe\'s segment rides map</a>';
                links += '<a target="_blank" data-parentid="segmentride' + $(this).data("segmentid") + '" class="popoverLink" href="http://strava-tools.raceshape.com/exporter/?url=http://app.strava.com/rides/' + $(this).data("rideid") + '" title="Download the gpx/tcx/crs file via RaceShape (external link)">GPX download</a>';

                $(this).popover({
                    title: 'Ride links <button type="button" data-parentid="segmentride' + $(this).data("segmentid") + '" class="close popoverClose" data-dismiss="alert">x</button>',
                    content: links,
                    trigger: 'manual',
                    placement: 'left'
                });
            });

            $('.segmentRideLink').click(function() {
                $(this).popover('toggle');

                $('.popoverClose').click(function() {
                    $('#' + $(this).data('parentid')).popover('hide');
                });

                $('.popoverLink').click(function() {
                    $('#' + $(this).data('parentid')).popover('hide');
                });
                return false;
            });

        });

        if (orderDir == "1") {
            document.getElementById("tab1" + orderCol).className += " headerSortUp";
        } else {
            document.getElementById("tab1" + orderCol).className += " headerSortDown";
        }

    } else {
        rowCount = 50;

        $.get("/ApiAthlete" + pageType + "ListItems.php?athleteId=" + athleteId + "&order=" + order + "&direction=" + orderDir + "&rowCount=" + rowCount + "&batch=" + batch, function(data) {
            $('#' + pageType + 'List').html(data);
            $("#" + pageType + "List .collapse").collapse();
        });
    }
}

function setPagination() {
    var html = "";

    rowCount = 100;
    if ($(window).innerWidth() <= 768) {
        rowCount = 50;
    }

    var tooMany = false;
    var pageCount = Math.ceil(itemCount / rowCount);
    if (pageCount > 7) {
        tooMany = true;
    }

    for (i = 0; i < pageCount; i++) {
        if (!tooMany ||
            (i == 0) ||
            (i == pageCount - 1) ||
            (i == orderBatch - 1) ||
            (i == orderBatch) ||
            (i == orderBatch + 1) ||
            ((i <= 4) && (orderBatch <= 3)) ||
            ((i >= pageCount - 5) && (orderBatch >= pageCount - 4))) {
            html += '<li';
            if (i == orderBatch) {
                html += ' class="active"';
            }
            html += '><a href="javascript:getData(orderCol,false,' + i + ')">' + (i + 1) + '</a></li>';
        }

        if (tooMany &&
            (((orderBatch >= 4) &&
                    (i == 1)) ||
                ((orderBatch <= pageCount - 5) &&
                    (i == pageCount - 2)))) {
            html += '<li class="disabled"><a href="#">...</a></li>';
        }
    }
    $('#paginationUL').html(html);
}

if (!Array.prototype.indexOf) {
    Array.prototype.indexOf = function(elt /*, from*/ ) {
        var len = this.length >>> 0;

        var from = Number(arguments[1]) || 0;
        from = (from < 0) ? Math.ceil(from) : Math.floor(from);
        if (from < 0)
            from += len;

        for (; from < len; from++) {
            if (from in this &&
                this[from] === elt)
                return from;
        }
        return -1;
    };
}

var chartHueScale = d3.scale.linear()
    .domain([0, 100])
    .range([100, 0]);

function updateRateStats(request) {
    if (request.getResponseHeader('X-RateLimit-Usage') !== null && request.getResponseHeader('X-RateLimit-Usage') != ',') {
        var calls = request.getResponseHeader('X-RateLimit-Usage').split(',');
        var times = request.getResponseHeader('X-RateLimit-Time').split(',');

        rateUsage15 = parseInt(calls[0]);
        rateUsageDay = parseInt(calls[1]);
        rateTime15 = parseInt(times[0]);
        rateTimeDay = parseInt(times[1]);

        if (typeof(ajaxBatchSize) !== 'undefined') {
            if (typeof(maxAjaxBatchSize) === 'undefined') {
                maxAjaxBatchSize = ajaxBatchSize;
            }
            // Keep update concurrency fixed at the configured max batch size.
            ajaxBatchSize = maxAjaxBatchSize;
        }

        setRateInfo();
    }
}

function setRateInfo() {
    d3.select('#rateInfo')
        .style('margin-left', '10px')
        .selectAll('*')
        .remove();

    d3.select('#rateInfo')
        .append('div')
        .classed('progress', true)
        .style('margin-bottom', '1px')
        .style('height', '4px')
        .style('width', '30px')
        .attr('title', '15 minute rate usage: ' + rateUsage15 + ' of '+rateLimit15+' (' + f1dp(rateUsage15 * 100 / rateLimit15) + '%).')
        .append('div')
        .classed('bar', true)
        .style('background-image', 'none')
        .style('background-color', 'hsla(' + f1dp(chartHueScale(rateUsage15 * 100 / rateLimit15)) + ',100%,35%,1)')
        .style('width', (rateUsage15 * 100 / rateLimit15) + '%');

    d3.select('#rateInfo')
        .append('div')
        .classed('progress', true)
        .style('margin-bottom', '2px')
        .style('height', '4px')
        .style('width', '30px')
        .attr('title', '15 minute time progress: ' + rateTime15 + 's of 900s (' + f1dp(rateTime15 * 100 / 900) + '%).')
        .append('div')
        .classed('bar', true)
        .style('background-image', 'none')
        .style('width', (rateTime15 * 100 / 900) + '%');

    d3.select('#rateInfo')
        .append('div')
        .classed('progress', true)
        .style('margin-bottom', '1px')
        .style('height', '4px')
        .style('width', '30px')
        .attr('title', 'Day rate usage: ' + rateUsageDay + ' of '+rateLimitDay+' (' + f1dp(rateUsageDay * 100 / rateLimitDay) + '%).')
        .append('div')
        .classed('bar', true)
        .style('background-image', 'none')
        .style('background-color', 'hsla(' + f1dp(chartHueScale(rateUsageDay * 100 / rateLimitDay)) + ',100%,35%,1)')
        .style('width', (rateUsageDay * 100 / rateLimitDay) + '%');

    d3.select('#rateInfo')
        .append('div')
        .classed('progress', true)
        .style('margin-bottom', '2px')
        .style('height', '4px')
        .style('width', '30px')
        .attr('title', 'Day time progress: ' + rateTimeDay + 's of 86400s (' + f1dp(rateTimeDay * 100 / 86400) + '%).')
        .append('div')
        .classed('bar', true)
        .style('background-image', 'none')
        .style('width', (rateTimeDay * 100 / 86400) + '%');
    /*rateUsageDay
    rateTime15
    rateTimeDay*/
}

function isRateOk(response) {
    if (typeof(isPRO) === 'undefined' || !isPRO) {
        return true;
    }
    if (response.indexOf('{"error":"Rate limiting') == 0) {
        rateLimited = true;
        return false;
    }
    if (response.indexOf('{"curl error":"403"}') == 0) {
        rateLimited = true;
        return false;
    }
    return true;
}

if (typeof(rateUsage15) !== 'undefined') {
    setRateInfo();
}

d3.selectAll('.clearLocalStorageBtn')
    .style('display', (isBeta ? 'none' : null))
    .on('click', function(d) {
        alert('Please note. Pressing this button does not get newly added activities from Strava, you must go to your Update page to pull these into VeloViewer. This button just clears the local cache in your browser which can occasionally become out of sync with the VeloViewer server.');
        if (hasLocalStorage) {
            localStorage.clear();
        }
        location.reload();
    });

function clone(obj) {
    // Handle the 3 simple types, and null or undefined
    if (null == obj || "object" != typeof obj) return obj;

    // Handle Date
    if (obj instanceof Date) {
        var copy = new Date();
        copy.setTime(obj.getTime());
        return copy;
    }

    // Handle Array
    if (obj instanceof Array) {
        var copy = [];
        for (var i = 0, len = obj.length; i < len; i++) {
            copy[i] = clone(obj[i]);
        }
        return copy;
    }

    // Handle Object
    if (obj instanceof Object) {
        var copy = {};
        for (var attr in obj) {
            if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]);
        }
        return copy;
    }

    throw new Error("Unable to copy obj! Its type isn't supported.");
}

function setDlWidth() {
    d3.selectAll('dl').each(function(d) {
        var dl = d3.select(this);
        maxWidth = 0;
        dl.selectAll('dt').style('width', null);
        dl.selectAll('dt')
            .filter(function(e) {
                return d3.select(this).style('display') == 'block'
            })
            .each(function(e) {
                maxWidth = Math.max(maxWidth, Math.ceil(d3.select(this).node().getBoundingClientRect().width));
            })
        dl.selectAll('dt').style('width', maxWidth + 'px');
        dl.selectAll('dd').style('margin-left', (maxWidth + 6) + 'px');
    })
}
setDlWidth();
setTimeout(setDlWidth, 1000);
setTimeout(setDlWidth, 3000);
setTimeout(setDlWidth, 6000);

var scrollingTableResizeTimer;
var stBatchSize = 10000;
var stBatchNumber = 0;

function setScrollingTableSize(refresh) {
    d3.selectAll('.tableWrapper.st').each(function() {
        var tw = d3.select(this);


        var h = (typeof(this.desiredHeight) === 'undefined' ? '400px' : this.desiredHeight);
        tw.select('.st-body')
            .style('width', tw.style('width'))
            .style('height', h);

        tw.style('height', tw.select('.st-body').node().getBoundingClientRect().height + 'px');

        tw.select('.st-head').style('width', tw.select('.st-body').node().clientWidth + 'px');

        var bodyThs = tw.selectAll('.st-body th')[0];
        tw.selectAll('.st-head th')
            .each(function(d, i) {
                var item = d3.select(this);
                this.__data__ = bodyThs[i].__data__;
                item.style('min-width', d3.select(bodyThs[i]).style('width'));
            });

        if (tw.select('.st-head')[0][0] != null) {
            tw.select('.st-head').node().scrollLeft = tw.select('.st-body').node().scrollLeft;
        }

        // needs to recheck size in to cope with browser window scrollbar changes
        if (!refresh) {
            setTimeout(function() {
                setScrollingTableSize(true)
            }, 10);
        }

    });
}

function initScrollingTable(tableWrapper, height) {
    tableWrapper.select('.st-head').remove();

    tableWrapper.node().desiredHeight = height;

    tableWrapper.append('div')
        .classed('st-head', true)
        .append('table')
        .attr('class', tableWrapper.select('#dataTable, .dataTable').attr('class'))
        .each(function() {
            var item = d3.select(this);
            item.append('colgroup').html(tableWrapper.select('#dataTable colgroup, .dataTable colgroup').html());
            item.append('thead').html(tableWrapper.select('#dataTable thead, .dataTable thead').html());

            var orgThs = tableWrapper.selectAll('#dataTable thead th, #dataTable thead th');
            var newThs = tableWrapper.selectAll('.st-head thead th');
            for (var i = 0; i < orgThs[0].length; i++) {
                d3.select(newThs[0][i]).on('mouseover', d3.select(orgThs[0][i]).on('mouseover'));
                d3.select(newThs[0][i]).on('click', d3.select(orgThs[0][i]).on('click'));
            }
        });

    tableWrapper.select('.st-body')
        .on('scroll', function() {
            tableWrapper.select('.st-head').node().scrollLeft = tableWrapper.select('.st-body').node().scrollLeft;
        });

    if (scrollingTableResizeTimer == null) {
        $(window).resize(function() {
            window.clearTimeout(scrollingTableResizeTimer);
            scrollingTableResizeTimer = setTimeout(setScrollingTableSize, 200);
        });
        //scrollingTableResizeTimer = setTimeout(setScrollingTableSize, 10);
    } else {
        //setScrollingTableSize();
    }
    tableWrapper.style({
        'border-width': '1px'
    });
}

var arrayUnique = function(a) {
    return a.reduce(function(p, c) {
        if (p.indexOf(c) < 0) p.push(c);
        return p;
    }, []);
};

function detectIE() {
    return document.documentMode || /Edge/.test(navigator.userAgent);
    /*var ua = window.navigator.userAgent;
    var msie = ua.indexOf('MSIE ');
    var trident = ua.indexOf('Trident/');

    if (msie > 0) {
        // IE 10 or older => return version number
        return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
    }

    if (trident > 0) {
        // IE 11 (or newer) => return version number
        var rv = ua.indexOf('rv:');
        return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
    }

    // other browser
    return false;*/
}

if (detectIE()) {
    Object.defineProperty(SVGElement.prototype, 'outerHTML', {
        get: function() {
            var $node, $temp;
            $temp = document.createElement('div');
            $node = this.cloneNode(true);
            $temp.appendChild($node);
            return $temp.innerHTML;
        },
        enumerable: false,
        configurable: true
    });
}

function getBrowserInfo() {
    var userAgent = navigator.userAgent, browserName, fullVersion, majorVersion, minorVersion;

    // In case of Opera
    if ((verOffset = userAgent.indexOf("Opera")) != -1) {
        browserName = "Opera";
        fullVersion = userAgent.substring(verOffset + 6);
        if ((verOffset = userAgent.indexOf("Version")) != -1) {
            fullVersion = userAgent.substring(verOffset + 8);
        }
    }
    // In case of older versions of Microsoft Internet Explorer
    else if ((verOffset = userAgent.indexOf("MSIE")) != -1) {
        browserName = "Microsoft Internet Explorer";
        fullVersion = userAgent.substring(verOffset + 5);
    }
    // In case of Chrome
    else if ((verOffset = userAgent.indexOf("Chrome")) != -1) {
        browserName = "Chrome";
        fullVersion = userAgent.substring(verOffset + 7);
    }
    // In case of Safari
    else if ((verOffset = userAgent.indexOf("Safari")) != -1) {
        browserName = "Safari";
        fullVersion = userAgent.substring(verOffset + 7);
        if ((verOffset = userAgent.indexOf("Version")) != -1) {
            fullVersion = userAgent.substring(verOffset + 8);
        }
    }
    // In case of Firefox
    else if ((verOffset = userAgent.indexOf("Firefox")) != -1) {
        browserName = "Firefox";
        fullVersion = userAgent.substring(verOffset + 8);
    }
    // In case of newer versions of Microsoft Internet Explorer
    else if (userAgent.indexOf("Trident/") != -1) {
        browserName = "Microsoft Internet Explorer";
        fullVersion = userAgent.substring(userAgent.indexOf("rv:") + 3);
    }
    // In case of Edge
    else if ((verOffset = userAgent.indexOf("Edge/")) != -1) {
        browserName = "Microsoft Edge";
        fullVersion = userAgent.substring(verOffset + 5);
    }
    // For other browsers (or for unknown browser)
    else {
        browserName = "Unknown Browser";
        fullVersion = "Unknown Version";
    }

    // Trimming the fullVersion to get the major version number
    majorVersion = parseInt('' + fullVersion, 10);
    if (isNaN(majorVersion)) {
        fullVersion = '' + parseFloat(navigator.appVersion);
        majorVersion = parseInt(navigator.appVersion, 10);
    }
    minorVersion = parseInt('' + fullVersion.substr(fullVersion.indexOf('.')+1), 10);
    if (isNaN(minorVersion)) {
        fullVersion = '' + parseFloat(navigator.appVersion);
        majorVersion = parseInt(navigator.appVersion, 10);
    }

    return {
        browserName: browserName,
        fullVersion: fullVersion,
        majorVersion: majorVersion,
        minorVersion: minorVersion
    };
}

function getAge(dateString) {
    var today = new Date();
    var birthDate = new Date(dateString);
    var age = today.getFullYear() - birthDate.getFullYear();
    var m = today.getMonth() - birthDate.getMonth();
    if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
        age--;
    }
    return age;
}

function getTextWidth(text, font) {
    // re-use canvas object for better performance
    var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas"));
    var context = canvas.getContext("2d");
    context.font = font;
    var metrics = context.measureText(text);
    return metrics.width;
};

function deg2rad(degree_in) {
    return ((Math.PI / 180) * degree_in);
}
if (typeof Number.prototype.toRad == 'undefined') {
    Number.prototype.toRad = function() {
        return this * Math.PI / 180;
    }
}
if (typeof Number.prototype.toDeg == 'undefined') {
    Number.prototype.toDeg = function() {
        return this * 180 / Math.PI;
    }
}

function bearing(lat1, lon1, lat2, lon2) {
    var dLon = deg2rad(lon2 - lon1);
    lat1 = lat1.toRad();
    lat2 = lat2.toRad();
    var y = Math.sin(dLon) * Math.cos(lat2);
    var x = Math.cos(lat1) * Math.sin(lat2) -
        Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon);
    return Math.atan2(y, x).toDeg();
}

function distLatLon(lat1, lon1, lat2, lon2) {
    var R = 6377160; //6371000;
    var dLat = deg2rad(lat2 - lat1);
    var dLon = deg2rad(lon2 - lon1);
    var a =
        Math.sin(dLat / 2) * Math.sin(dLat / 2) +
        Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
        Math.sin(dLon / 2) * Math.sin(dLon / 2);
    var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    var d = R * c;
    return d;
}

if (typeof(athleteId) !== 'undefined') {
    d3.selectAll(d3.selectAll('form[action="https://www.paypal.com/cgi-bin/webscr"]')[0]
            .filter(function(d) {
                return d3.select(d).selectAll('input')[0].length == 3
            }))
        .append('input')
        .attr('type', 'hidden')
        .attr('name', 'custom')
        .attr('value', athleteId);
}

if (typeof(isFree) !== 'undefined' && typeof(isPRO) !== 'undefined' && typeof(warnDate) !== 'undefined') {
    if (!isFree && !isPRO && warnDate != '') {
        var tDate = parseDateTime(warnDate);
        d3.select('#tourInfo')
            .style('display', null)
            .classed('alert alert-info', true)
            .html('<a class="close" data-dismiss="alert" href="#">&times;</a><b>Your data will be truncated and frozen to your most recent 25 activities and their segments on or soon after ' + fDate(new Date(tDate.setDate(tDate.getDate() + 7))) + ' (a week from your first seeing of this message). <a href="/update">Upgrade to PRO</a> before that date to continue using VeloViewer on all of your data.</b>');
    }
}

if (!isBeta) {
    d3.select('#tourInfo *').remove();
    d3.select('#tourInfo').style('display', null)
        .classed('alert alert-info', true)
        .style('font-size', '1.8em')
        .style('line-height', '1.2')
        .html('This is the old site, you probably shouldn\'t be seeing this and if you are then your data is most likely not complete. Try logging out and back in again and if you are still seeing this message then please <a href="mailto:veloviewer@gmail.com?subject=Seeing old site - ' + athleteId + '">get in touch</a>!');
}

window.setTimeout(function() {
    d3.selectAll('.normalLink').each(function() {
        d3.select(this).attr('href', d3.select(this).attr('href') + '?referrer=Embed_' + document.referrer.replace(/http[s]*:\/\//, '').replace(/\/.*/, ''));
    })
}, 500);

function addExtraPoints(llArr, maxGap) {
    llArr = llArr.slice();
    maxGap = typeof(maxGap) === 'undefined' ? 100 : maxGap;
    for (var i = 1; i < llArr.length; i++) {
        var distFromLast = distLatLon(llArr[i - 1].lat, llArr[i - 1].lng, llArr[i].lat, llArr[i].lng);
        if (distFromLast > maxGap) {
            var lats = d3.range(0, Math.floor(distFromLast / maxGap)).map(function(j) {
                return llArr[i - 1].lat + (j + 1) * (llArr[i].lat - llArr[i - 1].lat) / (1 + Math.floor(distFromLast / maxGap))
            });
            var lngs = d3.range(0, Math.floor(distFromLast / maxGap)).map(function(j) {
                return llArr[i - 1].lng + (j + 1) * (llArr[i].lng - llArr[i - 1].lng) / (1 + Math.floor(distFromLast / maxGap))
            });

            var args = [i, 0].concat(lats.map(function(d, j) {
                return {
                    lat: d,
                    lng: lngs[j]
                }
            }));
            Array.prototype.splice.apply(llArr, args);
        }
    }
    return llArr;
}

function long2tile(lon, zoom) {
    var r = (Math.floor((lon + 180) / 360 * Math.pow(2, zoom)));
    if (r < 0) {
        r = 0;
    }
    if (r > Math.pow(2, zoom) - 1) {
        r = Math.pow(2, zoom) - 1;
    }
    return r;
}

function lat2tile(lat, zoom) {
    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, zoom)));
}

function tile2long(x, z) {
    return (x / Math.pow(2, z) * 360 - 180);
}

function tile2lat(y, z) {
    var n = Math.PI - 2 * Math.PI * y / Math.pow(2, z);
    return (180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))));
}

function uncompressTiles(tileStr, zoomLevel) {
    var t = decompressN(tileStr, 0, 2);
    var r = [];
    for (var n = 0; n < t.length / 2; n++) {
        var i = t[n * 2];
        var j = t[n * 2 + 1];
        var tl = {
            x: tile2long(i, zoomLevel),
            y: tile2lat(j, zoomLevel)
        };
        var br = {
            x: tile2long(i + 1, zoomLevel),
            y: tile2lat(j + 1, zoomLevel)
        };
        r.push({
            x: i,
            y: j,
            tl: tl,
            br: br,
            z: zoomLevel,
            deleted: false
        });
    }
    return r;
}

function getActMapTiles(act, zoomLevel, bufferDist, extraPoints) {
    /* Asked by user to keep explorer tiles on the Activities map
    if (['VirtualRide','VirtualRun'].indexOf(act.t) > -1) {
      act.tiles = [];
      act.tilesLength = 0;
      return;
    }*/
    extraPoints = typeof(extraPoints) === 'undefined' ? true : extraPoints;
    if (extraPoints && texplorer[act.i ? act.i : act.id]) {
        act.tiles = uncompressTiles(texplorer[act.i ? act.i : act.id], zoomLevel);
        act.tilesLength = act.tiles.length;
        return;
    }

    if (typeof(act.map2) === 'undefined') {
        if ((typeof(activity) !== 'undefined' || typeof(route) !== 'undefined') && act.latLngArr2Copy) {
            act.map2 = act.latLngArr2Copy;
        } else {
            act.map2 = decompressN(act.map, 5, 2);
        }
        /*act.mapll = act.map2.filter(function(d, j) {
            return j % 2 == 0
        }).map(function(d, j) {
            return {
                lat: d,
                lng: act.map2[j * 2 + 1]
            };
        });*/
    }

    act.tiles = {};
    bufferDist = 0;

    if (!extraPoints) {
        let lastTileX = null, lastTileY = null;
        act.map2.forEach(function(d, i) {
            let x = long2tile(d.lng, 14);
            let y = lat2tile(d.lat, 14);
            let t = getTileLL(x, y, zoomLevel);
            act.tiles[x + '_' + y + '_' + zoomLevel] = t;
            if (i > 0 && typeof(timeObj) !== 'undefined' && timeObj.data[i] - timeObj.data[i-1] <= 10 && typeof(distObj) !== 'undefined' && distObj.data[i] - distObj.data[i-1] < 100 && Math.abs(lastTileX - x) == 1 && Math.abs(lastTileY - y) == 1) {
                let x0 = x > lastTileX ? t.left : t.right;
                let y0 = y > lastTileY ? t.top : t.bottom;
                let x1=d.lng-x0, y1=d.lat-y0, x2=act.map2[i-1].lng-x0, y2=act.map2[i-1].lat-y0;
                let m = (y2-y1)/(x2-x1);
                let c = y1 - m * x1;
                let ty = c > 0 ? Math.min(y, lastTileY) : Math.max(y, lastTileY);
                let tx = ty == y ? lastTileX : x;
                act.tiles[tx + '_' + ty + '_' + zoomLevel] = getTileLL(tx, ty, zoomLevel);
            }
            lastTileX = x;
            lastTileY = y;
        });
        act.tiles = d3.values(act.tiles);
        act.tilesLength = act.tiles.length;
        return;
    }

    // from routes page so always fill all gaps
    if (typeof(cpref) !== 'undefined' && cpref.substring(0, 1) == 'r') {
        let lastTileX = null, lastTileY = null;
        let llArr = addExtraPoints(act.map2, 100);
        llArr.forEach(function (d, i) {
            let x = long2tile(d.lng, 14);
            let y = lat2tile(d.lat, 14);
            let t = getTileLL(x, y, zoomLevel);
            act.tiles[x + '_' + y + '_' + zoomLevel] = t;
            if (i > 0 && Math.abs(lastTileX - x) == 1 && Math.abs(lastTileY - y) == 1) {
                let x0 = x > lastTileX ? t.left : t.right;
                let y0 = y > lastTileY ? t.top : t.bottom;
                let x1 = d.lng - x0, y1 = d.lat - y0, x2 = llArr[i - 1].lng - x0, y2 = llArr[i - 1].lat - y0;
                let m = (y2 - y1) / (x2 - x1);
                let c = y1 - m * x1;
                let ty = c > 0 ? Math.min(y, lastTileY) : Math.max(y, lastTileY);
                let tx = ty == y ? lastTileX : x;
                act.tiles[tx + '_' + ty + '_' + zoomLevel] = getTileLL(tx, ty, zoomLevel);
            }
            lastTileX = x;
            lastTileY = y;
        });
        act.tiles = d3.values(act.tiles);
        act.tilesLength = act.tiles.length;
        return;
    }

    var maxDist = Math.max(1000, act.d / 10); //500;

    function getTileLL(x, y, z) {
        if (!allTiles[x + '-' + y]) {
            var o = {
                top: tile2lat(y, z),
                left: tile2long(x, z),
                bottom: tile2lat(y + 1, z),
                right: tile2long(x + 1, z)
            };
            o.x = x;
            o.y = y;
            o.tl = {
                x: o.left,
                y: o.top
            };
            o.br = {
                x: o.right,
                y: o.bottom
            };
            o.z = z;
            o.deleted = false;

            allTiles[x + '-' + y] = o;
        }
        return allTiles[x + '-' + y];
    }
    var ll = d3.range(0, act.map2.length / 2, 1).map(function(d) {
        var lat = act.map2[d * 2];
        var lng = act.map2[d * 2 + 1];
        return {
            lat: lat,
            lng: lng,
            x: long2tile(lng, 14),
            y: lat2tile(lat, 14)
        }
    });
    ll.slice(1).forEach(function(d, i) {
        var e = ll[i];
        d.d = distLatLon(d.lat, d.lng, e.lat, e.lng);
        if (i == 0) {
            act.tiles[e.x + '_' + e.y + '_' + zoomLevel] = getTileLL(e.x, e.y, zoomLevel);
        }
        act.tiles[d.x + '_' + d.y + '_' + zoomLevel] = getTileLL(d.x, d.y, zoomLevel);
        if (extraPoints && d.d <= maxDist && ((d.x != e.x && !((d.x == 0 && e.x == Math.pow(2, zoomLevel) - 1) || (e.x == 0 && d.x == Math.pow(2, zoomLevel) - 1))) || d.y != e.y)) {
            if (d.x == e.x) {
                for (var jy = Math.min(d.y, e.y) + 1; jy < Math.max(d.y, e.y); jy++) {
                    act.tiles[d.x + '_' + jy + '_' + zoomLevel] = getTileLL(d.x, jy, zoomLevel);
                    //d.extraTiles.push({ x: d.x, y: j });
                }
            }
            if (d.y == e.y) {
                for (var jx = Math.min(d.x, e.x) + 1; jx < Math.max(d.x, e.x); jx++) {
                    act.tiles[jx + '_' + d.y + '_' + zoomLevel] = getTileLL(jx, d.y, zoomLevel);
                    //d.extraTiles.push({ x: j, y: d.y });
                }
            }
            if (d.x != e.x && d.y != e.y) {
                var m = (d.lat - e.lat) / (d.lng - e.lng);
                var lng = function(y) {
                    return (y - e.lat) / m + e.lng
                };
                var lat = function(x) {
                    return m * (x - e.lng) + e.lat
                };
                for (var jy = Math.min(d.y, e.y); jy <= Math.max(d.y, e.y); jy++) {
                    for (var jx = Math.min(d.x, e.x); jx <= Math.max(d.x, e.x); jx++) {
                        var t = getTileLL(jx, jy, zoomLevel);
                        if ((d.x == jx && d.y == jy) || (e.x == jx && e.y == jy)) {
                            act.tiles[jx + '_' + jy + '_' + zoomLevel] = t;
                        } else {
                            var lngTop = lng(t.top),
                                lngBottom = lng(t.bottom),
                                latLeft = lat(t.left),
                                latRight = lat(t.right),
                                lngTopB = lngTop.between(t.left, t.right),
                                lngBottomB = lngBottom.between(t.left, t.right),
                                latLeftB = latLeft.between(t.top, t.bottom),
                                latRightB = latRight.between(t.top, t.bottom);
                            if (lngTopB + lngBottomB + latLeftB + latRightB > 1) {
                                act.tiles[jx + '_' + jy + '_' + zoomLevel] = t;
                                //d.extraTiles.push({ x: k, y: j });
                            }
                        }
                    }
                }
            }
        }
    });

    act.tiles = d3.values(act.tiles);
    act.tilesLength = act.tiles.length;
    return;
}

function setExplorer(sum, fAct, retTiles) {
    if (tilesCalculated) {
        var z = -1;
        explorerTiles = {};
        (fAct.length > 0 && typeof(fAct[0].an) !== 'undefined' ? d3.merge(fAct.filter(function(d) {
            return typeof(d.tiles) !== 'undefined' //((typeof(d.map) !== 'undefined' && d.map != null) || typeof(texplorer[d.i]) !== 'undefined') && d.t.substring(0, 7) != 'Virtual'
        }).map(function(d) {
            return d.tiles
        })) : fAct).forEach(function(d) {
            z = d.z;
            if (typeof(explorerTiles[d.x + '-' + d.y]) === 'undefined') {
                explorerTiles[d.x + '-' + d.y] = d;
                explorerTiles[d.x + '-' + d.y].count = 0;
            }
            explorerTiles[d.x + '-' + d.y].count++;
        });

        var tl = d3.values(explorerTiles);
        tl.sort(function(a, b) {
            return a.x == b.x ? b.y - a.y : b.x - a.x
        });

        for (i = 0; i < tl.length; i++) {
            tl[i].size = 1 + Math.min(
                typeof(explorerTiles[(tl[i].x + 0) + '-' + (tl[i].y + 1)]) === 'undefined' ? 0 : explorerTiles[(tl[i].x + 0) + '-' + (tl[i].y + 1)].size,
                typeof(explorerTiles[(tl[i].x + 1) + '-' + (tl[i].y + 0)]) === 'undefined' ? 0 : explorerTiles[(tl[i].x + 1) + '-' + (tl[i].y + 0)].size,
                typeof(explorerTiles[(tl[i].x + 1) + '-' + (tl[i].y + 1)]) === 'undefined' ? 0 : explorerTiles[(tl[i].x + 1) + '-' + (tl[i].y + 1)].size
            )
        }

        var em = d3.max(tl, function(d) {
            return d.size
        });

        explorerMaxs = tl.filter(function(d) {
            return d.size == em
        });

        if (typeof(sum) === 'undefined') {
            sum = {};
        }
        sum.explorer = tl.length;
        sum.explorerMax = em;

        calcClump();

        if (typeof (sum.year) !== 'undefined' && sum.year.length == 0 && sum.type.length == 0 && sum.gear.length == 0) {
            sum.explorerTilesCompressed = compress(tl.map(function (d) { return [d.x, d.y] }), 0, 2);
            sum.explorerClumpCompressed = compress(d3.keys(maxClump).sort().map(function (d) { return d.split('-').map(function (e) { return +e }) }), 0, 2);
        }

        sum.explorerClumpMax = d3.keys(maxClump).length;
        maxClumpSpan = {};
        maxClumpSpan.minX = 10000000, maxClumpSpan.maxX = -1, maxClumpSpan.minY = 10000000, maxClumpSpan.maxY = -1;
        Object.keys(maxClump).forEach(function (d) {
            var xy = d.split('-');
            maxClumpSpan.minX = Math.min(maxClumpSpan.minX, xy[0]);
            maxClumpSpan.maxX = Math.max(maxClumpSpan.maxX, xy[0]);
            maxClumpSpan.minY = Math.min(maxClumpSpan.minY, xy[1]);
            maxClumpSpan.maxY = Math.max(maxClumpSpan.maxY, xy[1]);
        });
        if (maxClumpSpan.minY == 10000000) {
            maxClumpSpan.minX = 0;
            maxClumpSpan.maxX = 0;
            maxClumpSpan.minY = 0;
            maxClumpSpan.maxY = 0;
            maxClumpSpan.span = 0;
            maxClumpSpan.text = '(no Explorer tiles)';
        } else {
            maxClumpSpan.span = (maxClumpSpan.maxX - maxClumpSpan.minX + 1) + (maxClumpSpan.maxY - maxClumpSpan.minY + 1);
            maxClumpSpan.text = '(' + maxClumpSpan.maxX + ' - ' + maxClumpSpan.minX + ' + 1) + (' + maxClumpSpan.maxY + ' - ' + maxClumpSpan.minY + ' + 1)';
        }
        sum.explorerClumpMaxSpan = maxClumpSpan;

        if (typeof(retTiles) !== 'undefined') {
            calcAllConnectedTiles();
            sum.tileConnections = tileConnections;
            var maxConnectedTiles = Object.values(tileConnections).length > 0 ? Object.values(tileConnections).filter(function (d) { return d.order == 1 })[0].tiles : [];
            // work out max connected span
            maxConnectedSpan = {};
            maxConnectedSpan.minX = 10000000, maxConnectedSpan.maxX = -1, maxConnectedSpan.minY = 10000000, maxConnectedSpan.maxY = -1;
            maxConnectedTiles.forEach(function (t) {
                var xy = t.split('-');
                maxConnectedSpan.minX = Math.min(maxConnectedSpan.minX, xy[0]);
                maxConnectedSpan.maxX = Math.max(maxConnectedSpan.maxX, xy[0]);
                maxConnectedSpan.minY = Math.min(maxConnectedSpan.minY, xy[1]);
                maxConnectedSpan.maxY = Math.max(maxConnectedSpan.maxY, xy[1]);
            });
            if (maxConnectedSpan.minX == 10000000) {
                maxConnectedSpan.minX = 0;
                maxConnectedSpan.maxX = 0;
                maxConnectedSpan.minY = 0;
                maxConnectedSpan.maxY = 0;
                maxConnectedSpan.span = 0;
                maxConnectedSpan.text = '(no connected tiles)';
            } else {
                maxConnectedSpan.span = (maxConnectedSpan.maxX - maxConnectedSpan.minX + 1) + (maxConnectedSpan.maxY - maxConnectedSpan.minY + 1);
                maxConnectedSpan.text = '(' + maxConnectedSpan.maxX + ' - ' + maxConnectedSpan.minX + ' + 1) + (' + maxConnectedSpan.maxY + ' - ' + maxConnectedSpan.minY + ' + 1)';
            }
            sum.explorerMaxConnectedSpan = maxConnectedSpan;

            sum.tilesMaxConnected = maxConnectedTiles.length;
            sum.tileXs = tileXs;
            tilesMaxConnectedX = Object.values(tileXs).length > 0 ? Object.values(tileXs).filter(function (d) { return d.maxOrder == 1 }) : [];
            sum.tilesMaxConnectedX = tilesMaxConnectedX;
            sum.tilesTotalX = Object.values(tileXs).length > 0 ? Object.values(tileXs).filter(function (d) { return d.totalOrder == 1 }) : [];
            sum.tileYs = tileYs;
            tilesMaxConnectedY = Object.values(tileYs).length > 0 ? Object.values(tileYs).filter(function (d) { return d.maxOrder == 1 }) : [];
            sum.tilesMaxConnectedY = tilesMaxConnectedY;
            sum.tilesTotalY = Object.values(tileYs).length > 0 ? Object.values(tileYs).filter(function (d) { return d.totalOrder == 1 }) : [];
        }

        if (retTiles != null) {
            return tl;
        }
    }
}

var explorerMaxs = [];
var explorerTiles = {};
var explorerTileStyle = {
    color: "#FF0000",
    opacity: 0.5,
    weight: 0.5,
    fillColor: "#FF0000",
    fillOpacity: 0.2
};
/*var explorerTileFillUnvisited = {
    fillColor: "#CCCCCC",
    fillOpacity: 0.25
};*/
var explorerTileStyleSat = {
    color: "#FFFFFF",
    opacity: 0.5,
    weight: 0.5,
    fillColor: "#FFFFFF",
    fillOpacity: 0.2
};
var explorerNewTileStyle = {
    color: "#FF0000",
    opacity: 0.5,
    weight: 0.5,
    fillColor: "#eaff00",
    fillOpacity: 0.2
};
var explorerNewTileStyleSat = {
    color: "#FFFFFF",
    opacity: 0.5,
    weight: 0.5,
    fillColor: "#FFFFFF",
    fillOpacity: 0.3
};
var maxSquareStyle = {
    color: "#0000FF",
    opacity: 0.5,
    weight: 2,
    fillOpacity: 0
};
var maxSquareStyleSat = {
    color: "#FFFFFF",
    opacity: 0.5,
    weight: 2,
    fillOpacity: 0
};
var explorerClumpStyle = {
    color: "#FF0000",
    opacity: 0.5,
    weight: 0.5,
    fillColor: "#0000FF",
    fillOpacity: 0.15
};
var explorerClumpStyleSat = {
    color: "#FFFFFF",
    opacity: 0.5,
    weight: 0.5,
    fillColor: "#FFFFFF",
    fillOpacity: 0.3
};
var maxColumnRowStyle = {
    color: "#00a1a1",
    opacity: 0.7,
    weight: 2,
    fillOpacity: 0
};
var maxColumnRowStyleSat = {
    color: "#FFFFFF",
    opacity: 0.5,
    weight: 2,
    fillOpacity: 0
};
var maxClumpSpanStyle = {
    color: "#DD00DD",
    opacity: 0.5,
    weight: 2,
    fillOpacity: 0
};
var maxClumpSpanStyleSat = {
    color: "#FFFFFF",
    opacity: 0.5,
    weight: 2,
    fillOpacity: 0
};
var explorerGridLineStyle = {
    color: "#000000",
    opacity: 0.7,
    weight: 0.5
};
var maxClump, clumps, maxClumpSpan, maxConnectedSpan;

function trampoline(fn) {
    var op = fn;
    while (op != null && typeof op === 'function') {
        op = op();
    }
}

function getClump(ta) {
    function cal(ta) {
        var ta2 = {};
        for (var i = 0; i < ta.length; i++) {
            var t = explorerTiles[ta[i]];
            if (typeof(explorerTiles[(t.x - 1) + '-' + t.y]) !== 'undefined' && explorerTiles[(t.x - 1) + '-' + t.y].clumped && typeof(explorerTiles[(t.x - 1) + '-' + t.y].clump) === 'undefined') ta2[(t.x - 1) + '-' + t.y] = 1;
            if (typeof(explorerTiles[(t.x + 1) + '-' + t.y]) !== 'undefined' && explorerTiles[(t.x + 1) + '-' + t.y].clumped && typeof(explorerTiles[(t.x + 1) + '-' + t.y].clump) === 'undefined') ta2[(t.x + 1) + '-' + t.y] = 1;
            if (typeof(explorerTiles[t.x + '-' + (t.y - 1)]) !== 'undefined' && explorerTiles[t.x + '-' + (t.y - 1)].clumped && typeof(explorerTiles[t.x + '-' + (t.y - 1)].clump) === 'undefined') ta2[t.x + '-' + (t.y - 1)] = 1;
            if (typeof(explorerTiles[t.x + '-' + (t.y + 1)]) !== 'undefined' && explorerTiles[t.x + '-' + (t.y + 1)].clumped && typeof(explorerTiles[t.x + '-' + (t.y + 1)].clump) === 'undefined') ta2[t.x + '-' + (t.y + 1)] = 1;
        }
        ta2 = d3.keys(ta2);
        if (ta2.length > 0) {
            for (var i = 0; i < ta2.length; i++) {
                explorerTiles[ta2[i]].clump = 1;
                clumps[clumps.length - 1][ta2[i]] = 1;
            }
            return cal.bind(this, ta2);
        }
        return null;
    }
    return trampoline(cal.bind(this, ta));
}

function calcClump() {
    clumps = [];
    for (var t in explorerTiles) {
        var x = explorerTiles[t].x,
            y = explorerTiles[t].y;
        delete explorerTiles[t].clump;
        explorerTiles[t].clumped = (typeof(explorerTiles[(x - 1) + '-' + y]) !== 'undefined' && typeof(explorerTiles[(x + 1) + '-' + y]) !== 'undefined' && typeof(explorerTiles[x + '-' + (y - 1)]) !== 'undefined' && typeof(explorerTiles[x + '-' + (y + 1)]) !== 'undefined');
    }

    d3.values(explorerTiles).filter(function(d) {
        return d.clumped
    }).forEach(function(d) {
        if (typeof(d.clump) === 'undefined') {
            var c = {};
            clumps.push(c);
            d.clump = 1;
            c[d.x + '-' + d.y] = 1;
            getClump([d.x + '-' + d.y]);
        }
    });
    clumps.sort(function(a, b) {
        return Object.keys(b).length - Object.keys(a).length
    });

    maxClump = clumps.length > 0 ? clumps[0] : {};

    clumps = clumps.map(function (d, i) { return { id: i, tiles: Object.keys(d) } });
    clumps.forEach(function (d, i) {
        d.order = i > 0 && d.tiles.length == clumps[i - 1].tiles.length ? clumps[i - 1].order : i + 1;
        d.tiles.forEach(function (d) {
            explorerTiles[d].clumpId = i;
        });
    });
}

var tileConnections = [], tileXs = {}, tileYs = {}, tileConnectionsMaxId = -1, tilesMaxConnectedX = [], tilesMaxConnectedY = [];

function calcConnectedTilesNonRec(srcTileX, srcTileY, connectedId) {
    var ret = [];
    if (typeof (explorerTiles[(srcTileX - 1) + '-' + srcTileY]) !== 'undefined' && typeof (explorerTiles[(srcTileX - 1) + '-' + srcTileY].connectedId) === 'undefined')
        ret.push([srcTileX - 1, srcTileY]);
    if (typeof (explorerTiles[(srcTileX + 1) + '-' + srcTileY]) !== 'undefined' && typeof (explorerTiles[(srcTileX + 1) + '-' + srcTileY].connectedId) === 'undefined')
        ret.push([srcTileX + 1, srcTileY]);
    if (typeof (explorerTiles[srcTileX + '-' + (srcTileY - 1)]) !== 'undefined' && typeof (explorerTiles[srcTileX + '-' + (srcTileY - 1)].connectedId) === 'undefined')
        ret.push([srcTileX, srcTileY - 1]);
    if (typeof (explorerTiles[srcTileX + '-' + (srcTileY + 1)]) !== 'undefined' && typeof (explorerTiles[srcTileX + '-' + (srcTileY + 1)].connectedId) === 'undefined')
        ret.push([srcTileX, srcTileY + 1]);

    // also check diagonal tiles
    if (typeof (explorerTiles[(srcTileX - 1) + '-' + (srcTileY - 1)]) !== 'undefined' && typeof (explorerTiles[(srcTileX - 1) + '-' + (srcTileY - 1)].connectedId) === 'undefined')
        ret.push([srcTileX - 1, srcTileY - 1]);
    if (typeof (explorerTiles[(srcTileX + 1) + '-' + (srcTileY - 1)]) !== 'undefined' && typeof (explorerTiles[(srcTileX + 1) + '-' + (srcTileY - 1)].connectedId) === 'undefined')
        ret.push([srcTileX + 1, srcTileY - 1]);
    if (typeof (explorerTiles[(srcTileX - 1) + '-' + (srcTileY + 1)]) !== 'undefined' && typeof (explorerTiles[(srcTileX - 1) + '-' + (srcTileY + 1)].connectedId) === 'undefined')
        ret.push([srcTileX - 1, srcTileY + 1]);
    if (typeof (explorerTiles[(srcTileX + 1) + '-' + (srcTileY + 1)]) !== 'undefined' && typeof (explorerTiles[(srcTileX + 1) + '-' + (srcTileY + 1)].connectedId) === 'undefined')
        ret.push([srcTileX + 1, srcTileY + 1]);
    
    ret.forEach(function (d) {
        explorerTiles[d[0] + '-' + d[1]].connectedId = connectedId;
        tileConnections[connectedId].tiles.push(d[0] + '-' + d[1]);
    });
    return ret;
}

function calcAllConnectedTiles() {
    tileConnections = {}, tileXs = {}, tileYs = {}, tcid = 0, tileConnectionsMaxId = -1;
    for (var t in explorerTiles) {
        delete explorerTiles[t].connectedId;
    }
    for (var t in explorerTiles) {
        var x = explorerTiles[t].x,
            y = explorerTiles[t].y;
        if (typeof (explorerTiles[x + '-' + y].connectedId) === 'undefined') {
            tileConnections[tcid] = {
                id: tcid,
                tiles: [t]
            };
            var tilesToCheck = [[x, y]];
            explorerTiles[t].connectedId = tcid;
            while (tilesToCheck.length > 0) {
                var t = tilesToCheck.shift();
                Array.prototype.push.apply(tilesToCheck, calcConnectedTilesNonRec(t[0], t[1], tcid));
            }
            //calcConnectedTiles(x, y, tcid);
            tcid++;
        }
        if (typeof(tileXs[x]) === 'undefined') {
            tileXs[x] = {id: x, ys: []};
        }
        tileXs[x].ys.push(y);
        if (typeof(tileYs[y]) === 'undefined') {
            tileYs[y] = {id: y, xs: []};
        }
        tileYs[y].xs.push(x);
    }
    var tileConnectionsArr = Object.values(tileConnections);
    tileConnectionsArr.sort(function (a, b) { return b.tiles.length - a.tiles.length }).forEach(function (d,i) {
        d.order = i > 0 && d.tiles.length == tileConnectionsArr[i - 1].tiles.length ? tileConnectionsArr[i-1].order : i+1;
        tileConnections[d.id].order = d.order;
    });
    tileConnectionsMaxId = tileConnectionsArr.length > 0 ? tileConnectionsArr[0].id : -1;
    delete tileConnectionsArr;

    for (var x in tileXs) {
        tileXs[x].ys.sort();
        tileXs[x].max = 0;
        lastX = -10;
        curMax = [];
        tileXs[x].ys.forEach(function (d, i) {
            if (d == lastX + 1) {
                curMax.push(d);
            } else {
                curMax = [d];
            }
            if (tileXs[x].max < curMax.length) {
                tileXs[x].max = curMax.length;
                tileXs[x].maxYs = curMax;
            }
            lastX = d;
        });
    }
    var tileXsArr = Object.values(tileXs);
    tileXsArr.sort(function (a, b) { return b.max - a.max }).forEach(function (d, i) {
        d.maxOrder = i > 0 && d.max == tileXsArr[i - 1].max ? tileXsArr[i - 1].maxOrder : i + 1;
    });
    tileXsArr.sort(function (a, b) { return b.ys.length - a.ys.length }).forEach(function (d, i) {
        d.totalOrder = i > 0 && d.ys.length == tileXsArr[i - 1].ys.length ? tileXsArr[i - 1].totalOrder : i + 1;
    });
    delete tileXsArr;
    for (var y in tileYs) {
        tileYs[y].xs.sort();
        tileYs[y].max = 0;
        lastY = -10;
        curMax = [];
        tileYs[y].xs.forEach(function (d, i) {
            if (d == lastY + 1) {
                curMax.push(d);
            } else {
                curMax = [d];
            }
            if (tileYs[y].max < curMax.length) {
                tileYs[y].max = curMax.length;
                tileYs[y].maxXs = curMax;
            }
            lastY = d;
        });
    }
    var tileYsArr = Object.values(tileYs);
    tileYsArr.sort(function (a, b) { return b.max - a.max }).forEach(function (d, i) {
        d.maxOrder = i > 0 && d.max == tileYsArr[i - 1].max ? tileYsArr[i - 1].maxOrder : i + 1;
    });
    tileYsArr.sort(function (a, b) { return b.xs.length - a.xs.length }).forEach(function (d, i) {
        d.totalOrder = i > 0 && d.xs.length == tileYsArr[i - 1].xs.length ? tileYsArr[i - 1].totalOrder : i + 1;
    });
    delete tileYsArr;
}

function showTileActivities(tileX, tileY) {
    console.info('showTileActivities', tileX, tileY);
    d3.select('#tileActivitiesConfigModalLabel').html('Explorer Tile ' + tileX + '-' + tileY + ' Activities');
    d3.select('#tileActivitiesConfigModal .modal-body').html('<ul></ul>');

    var data = liveData.filter(function (d) {
        return typeof(d.tiles) !== 'undefined' && d.tiles.filter(function (e) {
            return e.x == tileX && e.y == tileY;
        }).length > 0
    });

    d3.select('#tileActivitiesConfigModal ul').selectAll('li').data(data).enter().append('li').html(function (d) {
        return '<span style="min-width:80px;display:inline-block;">' + fDate(parseDateTime(d.ls)) + '</span> <span style="min-width:60px;display:inline-block;">' + d.t + '</span> <span style="display:inline-block;"><a title="View activity on Strava" target="_blank" href="http://www.strava.com/activities/'+d.i+'"><img src="https://cf.veloviewer.com/img/strava_icon.svg" style="height:16px"></a> <a href="/athletes/' + contextAthleteId + '/activities/' + d.i + '" target="_blank">' + d.an + '</a></span>';
    });

    $('#tileActivitiesConfigModal').modal('show');
}

function tilePopupFunction(e) {
    var popLocation = e.latlng;
    var tileX = long2tile(popLocation.lng, 14);
    var tileY = lat2tile(popLocation.lat, 14);
    if (document.getElementById('tileActivitiesConfigModal') == null) {
        addModalPopup('tileActivitiesConfig', '', undefined, 0);
    }
    if (typeof(explorerTiles[tileX + '-' + tileY]) === 'undefined') {
        return;
    }
    var html = '<p style="font-weight: bold;">Explorer Tile ' + tileX + '-' + tileY + '</p>';
    var tile = explorerTiles[tileX + '-' + tileY];
    if (cpref == 'a') {
        html += '<p>Visited <button class="btn btn-link" style="margin: 0px;padding: 0px;font-size: 13px;border-width: 0px;font-weight: inherit;" onclick="showTileActivities(' + tileX + ',' + tileY + ')">' + fInt(tile.count) + ' time' + (tile.count > 1 ? 's' : '') + '</button>.</p>';
    }
    var isInMaxSquare = explorerMaxs.filter(function (d) { return tile.x >= d.x && tile.x < d.x + d.size && tile.y >= d.y && tile.y < d.y + d.size });
    html += '<p>' + (isInMaxSquare.length > 0 ? 'Is part of max square (' + explorerMaxs[0].size + 'x' + explorerMaxs[0].size + ')' : 'Is not part of max square') + '.</p>';
    html += '<p>Is ' + (tile.clumped ? 'part of a ' + fInt(clumps[tile.clumpId].tiles.length) + ' tile cluster (' + (tile.clumpId > 0 ? suf(clumps[tile.clumpId].order) + ' ' : '') + 'largest)' : 'not in a cluster') + '.</p>';
    if (typeof (tile.connectedId) !== 'undefined' && tileConnections[tile.connectedId]) {
        var tc = tileConnections[tile.connectedId];
        html += '<p>Part of ' + fInt(tc.tiles.length) + ' connected tiles (' + (tc.order > 1 ? suf(tc.order) + ' ' : '' ) + 'largest).</p>';
    }
    // find stats from column (x)
    var si = tileXs[tileX].ys.indexOf(+tileY);
    var adjCountX = 1;
    var checkId = si - 1;
    while (checkId > -1 && tileXs[tileX].ys[checkId] == +tileY - (si - checkId)) {
        adjCountX++;
        checkId--;
    }
    checkId = si + 1;
    while (checkId < tileXs[tileX].ys.length && tileXs[tileX].ys[checkId] == +tileY - (si - checkId)) {
        adjCountX++;
        checkId++;
    }
    // find stats from row (y)
    var si = tileYs[tileY].xs.indexOf(+tileX);
    var adjCountY = 1;
    var checkId = si - 1;
    while (checkId > -1 && tileYs[tileY].xs[checkId] == +tileX - (si - checkId)) {
        adjCountY++;
        checkId--;
    }
    checkId = si + 1;
    while (checkId < tileYs[tileY].xs.length && tileYs[tileY].xs[checkId] == +tileX - (si - checkId)) {
        adjCountY++;
        checkId++;
    }
    html += '<p><span style="font-weight: bold;">Column "' + tileX + '"</span></br>Part of ' + fInt(adjCountX) + ' vertically connected tiles.</br>'+
        (adjCountX == tileXs[tileX].max ? 'Part of max in this column' : 'Max connected: ' + tileXs[tileX].max) + ' (' + (tileXs[tileX].maxOrder > 1 ? suf(tileXs[tileX].maxOrder) + ' ' : '') + 'largest).<br/>'+
        'Total ticked: ' + tileXs[tileX].ys.length + ' (' + (tileXs[tileX].totalOrder > 1 ? suf(tileXs[tileX].totalOrder) + ' ' : '' ) + 'largest).</p>';
    html += '<p><span style="font-weight: bold;">Row "' + tileY + '"</span></br>Part of ' + fInt(adjCountY) + ' horizontally connected tiles.</br>'+
        (adjCountY == tileYs[tileY].max ? 'Part of max in this row' : 'Max connected: ' + tileYs[tileY].max) + ' (' + (tileYs[tileY].maxOrder > 1 ? suf(tileYs[tileY].maxOrder) + ' ' : '' ) + 'largest).<br/>'+
        'Total ticked: ' + tileYs[tileY].xs.length + ' (' + (tileYs[tileY].totalOrder > 1 ? suf(tileYs[tileY].totalOrder) + ' ' : '' ) + 'largest).</p>';

    var popup = L.popup()
        .setLatLng(popLocation)
        .setContent(html)
        .openOn(localMap);
}

function drawTiles(tiles) {
    var retArr = [];
    var lTiles = tiles.filter(function(d) { return !d.deleted; });
    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 (tileConnectionsMaxId != -1) {
        lm_optionButtons.setTitle(msi, 'View Explorer Max Square.\nMax Square size: ' + (explorerMaxs.length ? explorerMaxs[0].size : 0) + ' tiles\nTotal tiles: ' + lTiles.length + ' titles');
        lm_optionButtons.setTitle(mci, 'View Explorer Max Cluster.\nMax Cluster size: ' + Object.keys(maxClump).length + ' tiles\nMax Cluster Span: ' + maxClumpSpan.span + ' tiles\n(' + maxClumpSpan.text + ')' + '\nTotal tiles: ' + lTiles.length + ' titles');
        if (cci != -1) lm_optionButtons.setTitle(cci, 'View Explorer Max Span/Column/Row.\nMax connected: ' + tileConnections[tileConnectionsMaxId].tiles.length + ' tiles\nMax Connected Span : ' + (maxConnectedSpan.span) + ' tiles\n(' + maxConnectedSpan.text + ')' + '\nMax Connected Column : ' + (tilesMaxConnectedX.length > 0 ? tilesMaxConnectedX[0].max : 0) + 'tiles (X: ' + tilesMaxConnectedX.map(function (d) { return d.id }).join() + ')\nMax Connected Row : ' + (tilesMaxConnectedY.length > 0 ? tilesMaxConnectedY[0].max : 0) + 'tiles (Y: ' + tilesMaxConnectedY.map(function (d) { return d.id }).join() + ')' + '\nTotal tiles: ' + lTiles.length + ' titles');
    }
    else {
        lm_optionButtons.setTitle(msi, 'View Explorer Max Square.\nNo max square found.\nTotal tiles: ' + lTiles.length);
        lm_optionButtons.setTitle(mci, 'View Explorer Max Cluster.\nNo max cluster found.\nTotal tiles: ' + lTiles.length);
        if (cci != -1) lm_optionButtons.setTitle(cci, 'View Explorer Max Span/Column/Row.\nNo max column/row found.\nTotal tiles: ' + lTiles.length);
    }
    var tempTiles = {};
    var tempSquares = {};
    var tempClusters = {};
    var tempColumnRows = {};
    var tempClumpSpans = {};
    var sat = lm_currentBaseLayer.id == 'hybrid' ? 'Sat' : '';

    var tileStyle = eval('explorerTileStyle' + sat);
    var tileMinorConnectionStyle = clone(tileStyle);
    tileMinorConnectionStyle.fillOpacity = tileMinorConnectionStyle.fillOpacity / 2;
    var tileClumpStyle = eval('explorerClumpStyle' + sat);
    var tileMinorClumpStyle = clone(tileClumpStyle);
    tileMinorClumpStyle.fillOpacity = tileMinorClumpStyle.fillOpacity / 2;

    lTiles.forEach(function(d) {
        var coords = [
            [d.tl.y, d.tl.x],
            [d.tl.y, d.br.x],
            [d.br.y, d.br.x],
            [d.br.y, d.tl.x]
        ];

        if (!explorerTiles[d.x + '-' + d.y].clumped || lm_optionButtons.getBgColor(mci) == '') {
            tempTiles[d.x + '-' + d.y] = L.polygon(coords, explorerTiles[d.x + '-' + d.y].connectedId == tileConnectionsMaxId ? tileStyle : tileMinorConnectionStyle);
        } else {
            tempClusters[d.x + '-' + d.y] = L.polygon(coords, typeof(maxClump[d.x + '-' + d.y]) !== 'undefined' ? tileClumpStyle : tileMinorClumpStyle);
            tempClusters[d.x + '-' + d.y].maxClump = typeof(maxClump[d.x + '-' + d.y]) !== 'undefined';
        }
    });
    if (lm_optionButtons.getBgColor(mci) != '') {
        var options = eval('maxClumpSpanStyle' + sat);
        var coords = [
            [tile2lat(maxClumpSpan.minY, 14), tile2long(maxClumpSpan.minX, 14)],
            [tile2lat(maxClumpSpan.minY, 14), tile2long(maxClumpSpan.maxX + 1, 14)],
            [tile2lat(maxClumpSpan.maxY + 1, 14), tile2long(maxClumpSpan.maxX + 1, 14)],
            [tile2lat(maxClumpSpan.maxY + 1, 14), tile2long(maxClumpSpan.minX, 14)]
        ];
        tempClumpSpans[maxClumpSpan.minX + '-' + maxClumpSpan.minY] = L.polygon(coords, options);
    }
    if (lm_optionButtons.getBgColor(msi) != '') {
        explorerMaxs.forEach(function(d) {
            var coords = [
                [d.tl.y, d.tl.x],
                [d.tl.y, explorerTiles[(d.x + d.size - 1) + '-' + (d.y + d.size - 1)].br.x],
                [explorerTiles[(d.x + d.size - 1) + '-' + (d.y + d.size - 1)].br.y, explorerTiles[(d.x + d.size - 1) + '-' + (d.y + d.size - 1)].br.x],
                [explorerTiles[(d.x + d.size - 1) + '-' + (d.y + d.size - 1)].br.y, d.tl.x]
            ];

            var options = eval('maxSquareStyle' + sat);
            tempSquares[d.x + '-' + d.y] = L.polygon(coords, options);
        });
    }

    if (cci != -1 && lm_optionButtons.getBgColor(cci) != '') {
        Object.values(tileXs).filter(function (d) { return d.maxOrder == 1 }).forEach(function(d) {
            var tl = explorerTiles[d.id + '-' + d.maxYs[0]];
            var br = explorerTiles[d.id + '-' + d.maxYs.last()];
            var coords = [
                [tl.tl.y, tl.tl.x],
                [tl.tl.y, br.br.x],
                [br.br.y, br.br.x],
                [br.br.y, tl.tl.x]
            ];

            var options = eval('maxColumnRowStyle' + sat);
            tempColumnRows['C'+tl.x + '-' + tl.y] = L.polygon(coords, options);
        });
        Object.values(tileYs).filter(function (d) { return d.maxOrder == 1 }).forEach(function(d) {
            var tl = explorerTiles[d.maxXs[0] + '-' + d.id];
            var br = explorerTiles[d.maxXs.last() + '-' + d.id];
            var coords = [
                [tl.tl.y, tl.tl.x],
                [tl.tl.y, br.br.x],
                [br.br.y, br.br.x],
                [br.br.y, tl.tl.x]
            ];

            var options = eval('maxColumnRowStyle' + sat);
            tempColumnRows['R'+tl.x + '-' + tl.y] = L.polygon(coords, options);
        });
        var coords = [
            [tile2lat(maxConnectedSpan.minY, 14), tile2long(maxConnectedSpan.minX, 14)],
            [tile2lat(maxConnectedSpan.minY, 14), tile2long(maxConnectedSpan.maxX + 1, 14)],
            [tile2lat(maxConnectedSpan.maxY + 1, 14), tile2long(maxConnectedSpan.maxX + 1, 14)],
            [tile2lat(maxConnectedSpan.maxY + 1, 14), tile2long(maxConnectedSpan.minX, 14)]
        ];
        var options = eval('maxColumnRowStyle' + sat);
        tempColumnRows[maxConnectedSpan.minX + '-' + maxConnectedSpan.minY] = L.polygon(coords, options);
    }

    for (var att in lm_squares) {
        if (!tempSquares[att]) {
            map.removeLayer(lm_squares[att]);
            delete lm_squares[att];
        } else {
            lm_squares[att].bringToBack();
        }
    }
    for (var att in tempSquares) {
        if (!lm_squares[att]) {
            map.addLayer(tempSquares[att]);
            lm_squares[att] = tempSquares[att];
            tempSquares[att].bringToBack();
        }
    }
    for (var att in tempClumpSpans) {
        if (!lm_clumpSpans[att]) {
            map.addLayer(tempClumpSpans[att]);
            lm_clumpSpans[att] = tempClumpSpans[att];
            tempClumpSpans[att].bringToBack();
        }
    }
    for (var att in lm_columnRows) {
        if (!tempColumnRows[att]) {
            map.removeLayer(lm_columnRows[att]);
            delete lm_columnRows[att];
        } else {
            lm_columnRows[att].bringToBack();
        }
    }
    for (var att in tempColumnRows) {
        if (!lm_columnRows[att]) {
            map.addLayer(tempColumnRows[att]);
            lm_columnRows[att] = tempColumnRows[att];
            tempColumnRows[att].bringToBack();
        }
    }

    for (var att in lm_clusters) {
        if (!tempClusters[att]) {
            map.removeLayer(lm_clusters[att]);
            delete lm_clusters[att];
        } else {
            lm_clusters[att].bringToBack();
        }
    }
    for (var att in tempClusters) {
        if (!lm_clusters[att]) {
            map.addLayer(tempClusters[att]);
            lm_clusters[att] = tempClusters[att];
            tempClusters[att].bringToBack();
        }
    }
    for (var att in lm_tiles) {
        if (!tempTiles[att]) {
            map.removeLayer(lm_tiles[att]);
            delete lm_tiles[att];
        } else {
            lm_tiles[att].bringToBack();
        }
    }
    for (var att in tempTiles) {
        if (!lm_tiles[att]) {
            map.addLayer(tempTiles[att]);
            lm_tiles[att] = tempTiles[att];
            tempTiles[att].bringToBack();
        }
    }
    if (typeof(liveData) !== 'undefined') {
        window.setTimeout(function() {
            liveData.forEach(function(d) {
                if (d.ll) {
                    map.removeLayer(d.ll);
                    map.addLayer(d.ll);
                }
            });
        }, 2000);
    }
}

var addEvent = function(object, type, callback) {
    if (object == null || typeof(object) == 'undefined') return;
    if (object.addEventListener) {
        object.addEventListener(type, callback, false);
    } else if (object.attachEvent) {
        object.attachEvent("on" + type, callback);
    } else {
        object["on" + type] = callback;
    }
};

var map3ddrawn = false;

function setup3dmap() {
    d3.selectAll('#myTabs').append('li').classed('hidden-phone', true).append('a').attr({
        'href': '#map3d',
        'data-toggle': 'tab'
    }).html('3D Map');
    var map3dTab = d3.selectAll('.tab-content')
        .append('div')
        .style({
            'height': '550px'
        })
        .attr({
            'class': 'tab-pane',
            'id': 'map3d'
        })
    if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) {
        map3dTab.append('div').classed('alert alert-error', true).text('Firefox currently has a bug resulting in the 3D elements not stacking correctly in the view as you rotate.');
    }
    if (detectIE()) {
        map3dTab.append('div').classed('alert alert-error', true).text('So far I\'ve not managed to get this working in IE (and some issues in Firefox). Try Chrome where it looks lovely.');
    }
    map3dTab.style('position', 'relative')
        .append('div')
        .attr('id', 'map3dScale')
        .style('position', 'absolute')
        .append('div')
        .attr('id', 'map3dWrapper')
        .style({
            '-webkit-perspective': '5000px',
            '-moz-perspective': '5000px',
            'perspective': '5000px',
            'position': 'absolute',
            'top': '0px',
            'left': '0px'
        })
        .append('div')
        .attr('id', 'map3dContent')
        .style({
            '-webkit-transform': 'rotateX(' + spinYVal2 + 'deg)',
            '-moz-transform': 'rotateX(' + spinYVal2 + 'deg)',
            'transform': 'rotateX(' + spinYVal2 + 'deg)',
            '-webkit-transform-style': 'preserve-3d',
            '-moz-transform-style': 'preserve-3d',
            'transform-style': 'preserve-3d',
            'position': 'relative',
            'pointer-events': 'none'
        });
    d3.select('#map3d')
        .style('background', 'white')
        .style('overflow', 'hidden');

    $('a[data-toggle="tab"][href="#map3d"]').on('shown', function(e) {
        curTab = 'map3d';
        d3.selectAll('.floatingVVLogo').style('display', null);
        if (typeof(brushExtent) !== 'undefined') {
            if (brushExtent[0] == brushExtent[1]) {
                filtIndExtent = [0, segment.latLngArr2Copy.length - 1];
            } else {
                filtIndExtent = [d3.bisect(segment.distanceArr2Copy, brushExtent[0]) - 1, Math.min(segment.distanceArr2Copy.length - 1, d3.bisectLeft(segment.distanceArr2Copy, brushExtent[1]))];
            }
            segment.latLngArr2 = segment.latLngArr2Copy.slice(Math.max(0, filtIndExtent[0]), filtIndExtent[1] + 1);
            segment.elevationArr2 = segment.elevationArr2Copy.slice(Math.max(0, filtIndExtent[0]), filtIndExtent[1] + 1);
            segment.distanceArr2 = segment.distanceArr2Copy.slice(Math.max(0, filtIndExtent[0]), filtIndExtent[1] + 1);
        }
        draw3dmap(!map3ddrawn);
    });
}

var xxExt, yyExt, mtxExt, mtyExt, tilePadding = 4,
    map3dzoom = 1,
    map3dpo, draw3dmapTimer = null;

function draw3dmap(isFirst) {
    if (isFirst) {
        draw3dmap2(true);
    } else {
        window.clearTimeout(draw3dmapTimer);
        draw3dmapTimer = window.setTimeout(function() {
            window.clearTimeout(draw3dmapTimer);
            draw3dmapTimer = null;
            draw3dmap2(false)
        }, 500);

    }
}

function draw3dmap2(isFirst) {
    if (segment.latLngArr2.length <= 1) {
        return;
    }
    if (isFirst) {
        d3.select('body').append('div').attr('class', 'floatingVVLogo')
            .attr('style', 'position:fixed;bottom:10px;left:40px')
            .html('<img src="' + https + '://cf.veloviewer.com/img/logo30blackred.png"><span style="vertical-align: bottom;margin: 0px 0px 5px 10px;">powered by</span><div class="poweredbystrava" style="vertical-align: bottom;"></div>');

        if (getCookie("map") == 'Ordnance Survey (UK)') {
            alert('Sorry but Ordnance Survey maps not yet supportede in this view for technical reasons. Change map type and try again.')
            return;
        }
    }

    //var map3dzoom = map3.zoom;
    var map3dtiles = Math.max(2, Math.floor(parseInt(d3.select('#map3d').style('width')) / 256) + 1);
    var latExt1 = d3.extent(segment.latLngArr2, function(d) {
        return d.lat
    });
    var lngExt1 = d3.extent(segment.latLngArr2, function(d) {
        return d.lng
    })
    for (var i = 18; i > 1; i--) {
        if (map3dtiles > Math.max(Math.abs(long2tile(lngExt1[0], i) - long2tile(lngExt1[1], i)), Math.abs(lat2tile(latExt1[0], i) - lat2tile(latExt1[1], i)))) {
            map3dzoom = i - 0; //1
            i = 0;
        }
    }
    map3ddrawn = true;
    var latExt = d3.extent(segment.latLngArr2, function(d) {
        return d.lat
    }).map(function(d) {
        return lat2tile(d, map3dzoom)
    }).sort();
    var lngExt = d3.extent(segment.latLngArr2, function(d) {
        return d.lng
    }).map(function(d) {
        return long2tile(d, map3dzoom)
    }).sort();

    var tileTemplate2;
    if (typeof(lm_currentBaseLayer) !== 'undefined') {
        if (lm_currentBaseLayer._url) {
            tileTemplate2 = lm_currentBaseLayer._url;
        } else {
            tileTemplate2 = d3.select('#mapContainer3 .leaflet-tile-loaded img').node().src.replace(/x=[0-9]*&y=[0-9]*&z=[0-9]*/, 'x={x}&y={y}&z={z}');
        }
    } else {
        tileTemplate2 = d3.select('#mapContainer3 .olTileImage, #mapContainer3 .gm-style img')[0].map(function(d) {
            return d.src.replace(/\/\/[abc]\./, '//{s}.')
                .replace(/[0-9]*\/[0-9]*\/[0-9]*([@\.])/, '{z}/{x}/{y}$1')
                .replace(/x=[0-9]*&y=[0-9]*&z=[0-9]*&/, 'x={x}&y={y}&z={z}&');
        })[0];
    }
    var tileTemplate = function(x, y, z) {
        return tileTemplate2.replace('{s}', ['a', 'b', 'c'][(x + y) % 3]).replace('{x}', x).replace('{y}', y).replace('{z}', z);
    };
    /*var mapTilePrefix = ['a', 'b', 'c'];
    if (tileTemplate2.indexOf('//a.') > -1 || tileTemplate2.indexOf('//b.') > -1 || tileTemplate2.indexOf('//c.') > -1) {
        tileTemplate = function(x, y, z) {
            return tileTemplate2.replace(/\/\/[abc]\./, '//' + mapTilePrefix[Math.floor(Math.random() * mapTilePrefix.length)] + '.')
        }
    }*/
    var mapTiles = [];
    var mtd = [];

    for (var i = lngExt[0] - tilePadding; i <= lngExt[1] + tilePadding; i++) {
        for (var j = latExt[0] - tilePadding; j <= latExt[1] + tilePadding; j++) {
            mapTiles.push(tileTemplate(i, j, map3dzoom));
            //.replace(/[0-9]*\/[0-9]*\/[0-9]*([@\.])/, map3dzoom + '/' + i + '/' + j + '$1')
            //.replace(/x=[0-9]*&y=[0-9]*&z=[0-9]*&/, 'x=' + i + '&y=' + j + '&z=' + map3dzoom + '&'));
            mtd.push([map3dzoom, i, j]);
        }
    }
    /*    var mapTiles = d3.selectAll('#mapContainer3 .olTileImage')[0].map(function(d) {
          return d.src
        }).sort();*/
    /*var mtd = mapTiles.map(function(d) {
      var myRe = /[0-9]*\/[0-9]*\/[0-9]*\./;
      return myRe.exec(d)[0].split('/').map(function(d) {
        return +d
      })
    });*/
    mtxExt = d3.extent(mtd, function(d) {
        return d[1]
    });
    mtyExt = d3.extent(mtd, function(d) {
        return d[2]
    });
    var xScale = d3.scale.linear().domain(d3.range(mtxExt[0], mtxExt[1] + 2).map(function(d) {
        return tile2long(d, mtd[0][0])
    })).range(d3.range(0, (mtxExt[1] - mtxExt[0] + 2) * 250, 250));
    var yScale = d3.scale.linear().domain(d3.range(mtyExt[0], mtyExt[1] + 2).map(function(d) {
        return tile2lat(d, mtd[0][0])
    })).range(d3.range(0, (mtyExt[1] - mtyExt[0] + 2) * 250, 250));

    var ld = segment.latLngArr2.map(function(d, i) {
        var r = {};
        r.i = i;
        r.lng = d.lng;
        r.lat = d.lat;
        r.x = xScale(d.lng);
        r.y = yScale(d.lat);
        r.z = segment.elevationArr2[i];
        r.d = segment.distanceArr2[i];
        return r;
    });

    var zExt = d3.extent(ld, function(d) {
        return d.z
    });

    var tileLen = distLatLon(xScale.domain()[0], yScale.domain()[0], xScale.domain()[1], yScale.domain()[1]);

    var zScale = d3.scale.linear().domain(zExt).range([10, 10 + ((zExt[1] - zExt[0]) * (250 * elevMult * 1.5) / tileLen)]);

    var smoothed = filterData(ld.map(function(d) {
        return {
            x: d.d,
            y: d.z
        }
    }));
    ld.forEach(function(d, i) {
        d.z = smoothed[i].ySmooth;
    });
    var ld2 = simplify(ld, Math.sqrt(segment.distanceArr2[segment.distanceArr2.length - 1] - segment.distanceArr2[0]) / 1000, false);
    ld2 = ld2.map(function(d, i) {
        var r = d;
        r.dd = i == ld2.length - 1 ? 0 : segment.distanceArr2[ld2[i + 1].i] - segment.distanceArr2[r.i];
        r.dx = i == ld2.length - 1 ? 0 : ld2[i + 1].x - r.x;
        r.dy = i == ld2.length - 1 ? 0 : ld2[i + 1].y - r.y;
        r.z1 = zScale(r.z);
        r.z2 = i == ld2.length - 1 ? 0 : zScale(ld2[i + 1].z);
        r.zd = i == ld2.length - 1 ? 0 : ld2[i + 1].z - r.z;
        r.td = Math.sqrt(r.dx * r.dx + r.dy * r.dy) + 1;
        r.b = i == ld2.length - 1 ? 0 : Math.atan2(d.dy, d.dx) * 180 / Math.PI;
        r.b = (r.b + (10 * 360)) % 360;
        r.g = r.zd / r.dd;
        r.c = colScale2(r.g);
        r.c2 = d3.rgb(d.c);
        return r;
    });

    xxExt = d3.extent(ld2, function(d) {
        return d.x
    });
    yyExt = d3.extent(ld2, function(d) {
        return d.y
    });

    d3.select('#map3d')
        .style('height', (window.innerHeight - document.getElementById('map3d').getBoundingClientRect().top) + 'px');

    var to = (xxExt[0] + (xxExt[1] - xxExt[0]) / 2) + 'px ' + (yyExt[0] + (yyExt[1] - yyExt[0]) / 2) + 'px';

    d3.select('#map3dWrapper')
        .style('-webkit-perspective-origin', to)
        .style('-moz-perspective-origin', to)
        .style('perspective-origin', to);

    d3.select('#map3dContent').selectAll('*').remove();
    d3.select('#map3dContent')
        .style({
            'position': 'absolute',
            'background-color': 'transparent',
            'height': ((mtyExt[1] - mtyExt[0] + 1) * 250) + 'px',
            '-webkit-transform-origin': to,
            '-moz-transform-origin': to,
            'transform-origin': to
        })
        .append('svg')
        /*    .attr({
               'xmlns': 'http://www.w3.org/2000/svg',
               'xmlns:xlink': 'http://www.w3.org/1999/xlink'
            })*/
        .style({
            'position': 'obsolute',
            'background-color': 'transparent',
            'width': ((mtxExt[1] - mtxExt[0] + 1) * 250) + 'px',
            'height': ((mtyExt[1] - mtyExt[0] + 1) * 250) + 'px'
        })
        .selectAll('image')
        .data(mapTiles)
        .enter()
        .append('image')
        .attr({
            'xlink:href': function(d) {
                return d
            },
            'x': function(d, i) {
                return 250 * (mtd[i][1] - mtxExt[0])
            },
            'y': function(d, i) {
                return 250 * (mtd[i][2] - mtyExt[0])
            },
            'width': '250px',
            'height': '250px'
        })
        .style({
            'position': 'absolute',
            'width': '250px',
            'height': '250px'
        })

    d3.select('#map3dContent')
        .selectAll('svg.p').remove();

    var svgp = d3.select('#map3dContent')
        .selectAll('svg.p')
        .data(ld2.slice(0, ld2.length - 1))
        .enter()
        .append('svg')
        .classed('p', true)
        .attr('title', 'hello')
        .style({
            'position': 'absolute',
            'top': function(d) {
                return d.y + 'px'
            },
            'left': function(d) {
                return d.x + 'px'
            },
            'height': function(d) {
                return (Math.max(d.z1, d.z2) + 2) + 'px'
            },
            'width': function(d) {
                return d.td + 'px'
            },
            '-webkit-transform': function(d) {
                return 'rotateX(90deg) rotateY(' + d.b + 'deg)'
            },
            '-moz-transform': function(d) {
                return 'rotateX(90deg) rotateY(' + d.b + 'deg)'
            },
            'transform': function(d) {
                return 'rotateX(90deg) rotateY(' + d.b + 'deg)'
            },
            'background': 'transparent',
            '-webkit-transform-origin': '0px 0px',
            '-moz-transform-origin': '0px 0px',
            'transform-origin': '0px 0px'
        });

    svgp.append('polygon')
        .classed('grad', true)
        .attr('points', function(d) {
            return '0,0 ' + d.td + ',0 ' + d.td + ',' + d.z2 + ' 0,' + d.z1
        })
        .attr('style', function(d) {
            return 'opacity:0.8;fill:' + d.c + ';stroke-width:0px;stroke:' + d.c2.darker().toString();
        });
    /*.append("svg:title")
        .text(function(d, i) { return "Gradient " + f1dp(100*d.g)+'%'; });;*/
    svgp.append('polygon')
        .classed('road', true)
        .attr('points', function(d) {
            return d.td + ',' + (d.z2 + 1) + ' 0,' + (d.z1 + 1)
        })
        .attr('style', function(d) {
            return 'opacity:0.8;fill:transparent;stroke-width:2px;stroke:#444';
        });

    if (isFirst) {
        d3.select('#map3d')
            .on('mousedown', function(e) {
                d3.select('#map3d')
                    .on('mousemove', function(f) {
                        if (d3.event.which == 1) {
                            handleMove3d2(this, d3.mouse(this), 'mouse');
                        } else {
                            d3.select('#map3d')
                                .on('mousemove', null);
                            dragX2 = null;
                            dragY2 = null;
                        }
                        d3.event.preventDefault();
                    });
            })
            .on('mouseup', function() {
                d3.select('#map3d')
                    .on('mousemove', null);
                dragX2 = null;
                dragY2 = null;
                d3.event.preventDefault();
            })
            .on('touchstart', function(e) {
                d3.select('#map3d')
                    .on('touchmove', function(f) {
                        handleMove3d2(this, d3.touches(this), 'touch');
                        d3.event.preventDefault();
                    });
                d3.event.preventDefault();
            })
            .on('touchend', function() {
                d3.select('#map3d')
                    .on('mousemove', null);
                dragX2 = null;
                dragY2 = null;

                d3.event.preventDefault();
            });
        addEvent(window, "resize", function(event) {
            map3dcentre();
        });
    }
    map3dcentre();
    map3dshade();
}

var spinXVal2 = 0,
    spinYVal2 = 45,
    dragX2, dragY2, map3dMoveTimer;

function map3dcentre() {
    var padding = {
        "top": 40,
        "bottom": 30,
        "left": 20,
        "right": 20
    };
    d3.select('#map3dScale').style('transform', null);
    //var marginLeft = (2 * (xxExt[0] + (xxExt[1] - xxExt[0]) / 2) - parseInt(d3.select('#map3d').style('width'))) / 2; //(aw - (xxExt[1]-xxExt[0]))/2; //tilePadding * 250 - 250;
    var map3doffsetTop = document.getElementById('map3d').getBoundingClientRect().top;
    var map3doffsetLeft = document.getElementById('map3d').getBoundingClientRect().left;
    var map3doffsetRight = document.getElementById('map3d').getBoundingClientRect().right;
    var rects = d3.selectAll('#map3d .p')[0].map(function(d) {
        return d.getBoundingClientRect()
    });
    var minTop = d3.min(rects, function(d) {
        return d.top
    });
    var maxBottom = d3.max(rects, function(d) {
        return d.bottom
    });
    var minLeft = d3.min(rects, function(d) {
        return d.left
    });
    var maxRight = d3.max(rects, function(d) {
        return d.right
    });
    var vScale = ((window.innerHeight - map3doffsetTop - padding.top - padding.bottom) / (maxBottom - minTop));
    var hScale = ((parseInt(d3.select('#map3d').style('width')) - padding.left - padding.right) / (maxRight - minLeft));
    d3.select('#map3dScale').style('transform', 'scale(' + Math.min(vScale, hScale) + ')').style('top', '0px').style('left', '0px');
    var minTop = d3.min(d3.selectAll('#map3d .p')[0].map(function(d) {
        return d.getBoundingClientRect().top
    }));
    var maxBottom = d3.max(d3.selectAll('#map3d .p')[0].map(function(d) {
        return d.getBoundingClientRect().bottom
    }));
    //  var marginTop = parseInt(d3.select('#map3dWrapper').style('top')) - ((minTop - map3doffsetTop) - ((minTop - map3doffsetTop) + (window.innerHeight - maxBottom)) / 2);
    //  var marginTop = -1 * (minTop - map3doffsetTop) + parseInt(d3.select('#map3dWrapper').style('top')) + padding.top;
    var marginTop = (-1 * (minTop - ((window.innerHeight - document.getElementById('map3d').getBoundingClientRect().top) - (maxBottom - minTop)) / 2 - document.getElementById('map3d').getBoundingClientRect().top));
    var minLeft = d3.min(d3.selectAll('#map3d .p')[0].map(function(d) {
        return d.getBoundingClientRect().left
    }));
    var maxRight = d3.max(d3.selectAll('#map3d .p')[0].map(function(d) {
        return d.getBoundingClientRect().right
    }));
    var marginLeft = parseInt(d3.select('#map3dWrapper').style('left')) - ((minLeft - map3doffsetLeft) - ((minLeft - map3doffsetLeft) + (map3doffsetRight - maxRight)) / 2);
    d3.select('#map3dScale').style('top', marginTop + 'px').style('left', marginLeft + 'px');
    //d3.select('#map3dWrapper').style('left', marginLeft + 'px');
}

function map3dshade() {
    var mapAngle = (spinXVal2 + 100 * 360) % 360;
    d3.selectAll('#map3dContent .p .grad').each(function(d) {
        var displayAngle = (d.b + mapAngle + 2 * 360) % 360;
        displayAngle = (displayAngle > 90 && displayAngle <= 270 ? displayAngle - 180 : displayAngle);
        var e = Math.sin(deg2rad(displayAngle));
        d3.select(this).style({
            'fill': d.c2.darker(e),
            'stroke': d.c2.darker(0.5 + e)
        })
    });
}

function handleMove3d2(obj, mouseTouch, type) {
    if (type == 'touch') {
        mouseTouch = mouseTouch[0];
    }

    var l_dragX = mouseTouch[0];
    var l_dragY = mouseTouch[1];

    l_dragX = 2 * 360 * l_dragX / parseInt(d3.select('#map3dContent').style('width'));
    l_dragY = 2 * 360 * l_dragY / parseInt(d3.select('#map3dContent').style('height'));

    if (dragX2 != null && spinYVal + dragY2 - l_dragY <= 0) {
        spinXVal2 += dragX2 - l_dragX;
        spinYVal2 += dragY2 - l_dragY;
        spinYVal2 = Math.max(Math.min(spinYVal2, 90), 0);
    }

    dragX2 = l_dragX;
    dragY2 = l_dragY;
    //console.debug('x: ' + mouseTouch[0] + ', y: ' + mouseTouch[1] + ', xChange: ' + xChange + ', yChange: ' + yChange + 'spinXVal: ' + spinXVal2 + ', spinYVal: ' + spinYVal2);

    d3.select('#map3dContent')
        .style('-webkit-transform', 'rotateX(' + spinYVal2 + 'deg) rotateZ(' + spinXVal2 + 'deg)')
        .style('-moz-transform', 'rotateX(' + spinYVal2 + 'deg) rotateZ(' + spinXVal2 + 'deg)')
        .style('transform', 'rotateX(' + spinYVal2 + 'deg) rotateZ(' + spinXVal2 + 'deg)');

    map3dshade();

    window.clearTimeout(map3dMoveTimer);
    map3dMoveTimer = window.setTimeout(map3dcentre, 500);
}

var powerBrackets = ['0W', '0-50W', '50-100W', '100-150W', '150-200W', '200-250W', '250-300W', '300-350W', '350-400W', '400-450W', '450W+'];

function addModalPopup(name, title, maxHeight, widthPercent, parent) {
    parent = typeof(parent) === 'undefined' ? d3.select('body') : parent;
    var mh = ''
    widthPercent = typeof(widthPercent) === 'undefined' ? 90 : widthPercent;
    if (typeof(maxHeight) !== 'undefined' && maxHeight != null) {
        mh = ' style="max-height:' + maxHeight + 'px"';
    }
    parent.append('div')
        .attr({
            id: name + 'Modal',
            class: 'modal hide fade',
            tabindex: '-1',
            role: 'dialog',
            'aria-labelledby': name + 'ModalLabel',
            'aria-hidden': 'true',
            style: widthPercent == 0 ? '' : 'margin:0px;top:5%;left:' + ((100 - widthPercent) / 2) + '%;width:' + widthPercent + '%'
        }).html('<div class="modal-header"><button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button><h3 id="' + name + 'ModalLabel">' + title + '</h3></div><div class="modal-body"' + mh + '></div><div class="modal-footer"><button class="btn" data-dismiss="modal" aria-hidden="true">Close</button></div>');
}

function setBrushDash() {
    d3.selectAll('.filterDiv .brush .extent, #elevChart .brush .extent')
        .style('stroke-dasharray', function() {
            var cbr = (this.getBoundingClientRect());
            return '0,' + cbr.width + ',' + cbr.height + ',' + cbr.width + ',' + cbr.height
        })
}

var breakdownDrawn = false,
    bdSizeScale, pi = Math.PI,
    bgTooltip, bgdTooltip,
    bdsize = 400,
    bdmargin = 30,
    breakdownAngle = 45,
    breakdownGrad = 5,
    bdinnerSize = bdsize - 2 * bdmargin,
    gradSections,
    gradDist, bgTooltip,
    bdtn, bdts, bdte, bdtw, bdlo, bdc, bdc1, bdc2, bdl1, bdl2, bdsmel, bdGradDist = {};

function gbd(llArr, dArr, eArr, tArr) {
    return llArr.filter(function(d, i) {
        return i > 0
    }).map(function(d, i) {
        var a = llArr[i];
        var b = llArr[i + 1];
        var br = bearing(a.lat, a.lng, b.lat, b.lng);
        br = br < 0 ? br + 360 : br;
        return {
            d: dArr[i + 1] - dArr[i],
            t: typeof(tArr) !== 'undefined' ? tArr[i + 1] - tArr[i] : 0,
            b: br,
            g: Math.min(Math.max(-.25, (eArr[i + 1] - eArr[i]) / (dArr[i + 1] - dArr[i])), .25)
        }
    }).filter(function(d) {
        return d.d > 0
    })
}

function gbdd(deg, per, da) {
    return d3.values(d3.nest()
            .key(function(d) {
                var a = Math.round(d.b / deg) * deg;
                return a == 360 ? 0 : a
            })
            .rollup(function(d) {
                var o = {};
                o.b = Math.round(d[0].b / deg) * deg;
                o.b = o.b == 360 ? 0 : o.b
                o.d = d3.sum(d, function(e) {
                    return e.d
                });
                o.g = d3.values(d3.nest()
                        .key(function(d) {
                            return Math.round(100 * d.g / per) * per;
                        })
                        .rollup(function(d) {
                            var o = {};
                            o.g = Math.round(100 * d[0].g / per) * per;
                            o.d = d3.sum(d, function(e) {
                                return e.d
                            });
                            return o;
                        })
                        .map(d))
                    .sort(function(a, b) {
                        return a.g - b.g
                    });
                return o;
            })
            .map(da))
        .sort(function(a, b) {
            return a.b - b.b
        })
}

function gdd(da) {
    return d3.values(d3.nest()
            .key(function(d) {
                return Math.round(100 * d.g);
            })
            .rollup(function(d) {
                var o = {};
                o.g = Math.round(100 * d[0].g);
                o.d = d3.sum(d, function(e) {
                    return e.d
                });
                o.t = d3.sum(d, function(e) {
                    return e.t
                });
                return o;
            })
            .map(da))
        .sort(function(a, b) {
            return a.g - b.g
        });
}

function drawBreakdown(parent, llArr, dArr, eArr, fExt, tArr) {
    var g, th, tb, isFirstTime = false;
    if (typeof(eArr) === 'undefined') {
        var da = (typeof(segment.distanceArr2Copy) === 'undefined' ? segment.distanceArr2 : segment.distanceArr2Copy);
        var ea = (typeof(segment.elevationArr2Copy) === 'undefined' ? segment.elevationArr2 : segment.elevationArr2Copy);
        bdsmel = filterData(da.map(function(d, i) {
            return {
                x: d,
                y: ea[i]
            }
        })).map(function(d) {
            return d.ySmooth
        });
        eArr = bdsmel;
    }
    if (typeof(fExt) === 'undefined') {
        var gbda = gbd(llArr, dArr, eArr, tArr);
    } else {
        var gbda = gbd(llArr.slice(Math.max(0, filtIndExtent[0]), filtIndExtent[1] + 1),
            dArr.slice(Math.max(0, filtIndExtent[0]), filtIndExtent[1] + 1),
            eArr.slice(Math.max(0, filtIndExtent[0]), filtIndExtent[1] + 1),
            typeof(tArr) !== 'undefined' ? tArr.slice(Math.max(0, filtIndExtent[0]), filtIndExtent[1] + 1) : tArr);
    }
    rpg = gbdd(breakdownAngle, breakdownGrad, gbda);
    rg = gdd(gbda);

    if (!breakdownDrawn) {
        isFirstTime = true;
        var row = parent.append('div')
            .classed('breakdownWrapper', true);

        g = row.append('div')
            .style('display', 'inline')
            .classed('breakdownTotal', true)
            .append('svg')
            .append('g');

        var t = parent.append('div')
            .classed('tableWrapper breakdownTotalTable', true)
            .append('table')
            .classed('table table-striped table-condensed table-hover table-bordered', true);

        th = t.append('thead')
            .append('tr');
        tb = t.append('tbody');

        th.append('th').text('Direction');
        th.append('th').text('Total');

        bdSizeScale = d3.scale.linear();

        bdc = g.append('circle')
            .style('fill', '#eee')
            .style('stroke', 'black')
            .style('stroke-opacity', 0.2)
            .style('stroke-width', 0.75)
            .attr('cx', 0)
            .attr('cy', 0);

        bdc1 = g.append('circle')
            .style('stroke', 'black')
            .style('stroke-opacity', 0.2)
            .style('stroke-width', 0.5)
            .style('fill', 'transparent');

        bdc2 = g.append('circle')
            .style('fill', '#eee')
            .style('stroke', 'black')
            .style('stroke-opacity', 0.2)
            .style('stroke-width', 0.5)
            .style('fill', 'transparent');

        bdl1 = g.append('line')
            .style('stroke', 'black')
            .style('stroke-opacity', 0.2)
            .style('stroke-width', 0.75)
            .attr('x1', 0)
            .attr('x2', 0);

        bdl2 = g.append('line')
            .style('stroke', 'black')
            .style('stroke-opacity', 0.2)
            .style('stroke-width', 0.75)
            .attr('y1', 0)
            .attr('y2', 0);

        bdtn = g.append('text')
            .attr('text-anchor', 'middle')
            .style('font-size', '2em')
            .text('N');

        bdts = g.append('text')
            .attr('text-anchor', 'middle')
            .style('font-size', '2em')
            .text('S');

        bdte = g.append('text')
            .attr('text-anchor', 'start')
            .style('font-size', '2em')
            .text('E');

        bdtw = g.append('text')
            .attr('text-anchor', 'end')
            .style('font-size', '2em')
            .text('W');

        // setup distance/gradient chart
        bdGradDist.m = {};
        bdGradDist.m.l = 40;
        bdGradDist.m.r = 5;
        bdGradDist.m.t = 5;
        bdGradDist.m.b = 30;
        bdGradDist.svg = row.append('div')
            .style('display', 'inline')
            .classed('breakdownGradDist', true)
            .append('svg')
            .append('g')
            .attr("transform", "translate(" + bdGradDist.m.l + "," + bdGradDist.m.t + ")");

        bdGradDist.xscale = d3.scale.linear();
        bdGradDist.yscale = d3.scale.linear()
            .domain([-25.5, 25.5]);

        /*bdGradDist.xscale1 = d3.scale.linear()
            .domain([-30, 30]);
        bdGradDist.yscale1 = d3.scale.linear();*/

        bdGradDist.xAxis = d3.svg.axis();
        bdGradDist.xAxis
            .orient('bottom')
            .scale(bdGradDist.xscale)
            .tickFormat(function(d) {
                return fDistAuto(d);
            })
            .ticks(4);
        //.tickValues(tickVals)

        bdGradDist.yAxis = d3.svg.axis();
        bdGradDist.yAxis
            .orient('left')
            .scale(bdGradDist.yscale)
            .tickSize(0)
            .tickFormat(function(d) {
                return fInt(d) + ' %';
            })
            .tickValues(d3.range(-25, 26, 5));

        bdGradDist.y_axis = bdGradDist.svg.append('g');

        bdGradDist.x_axis = bdGradDist.svg.append('g');

        /*bdGradDist.area = d3.svg.area()
            .x(function(d) {
                return bdGradDist.xscale1(d.g);
            })
            .y1(function(d) {
                return bdGradDist.yscale1(d.d);
            });

        bdGradDist.areaPathG = d3.select('.breakdownGradDist svg').append('g');
        bdGradDist.areaPath = bdGradDist.areaPathG.append("path")
            .attr("class", "area");*/

        breakdownDrawn = true;
    } else {
        g = parent.select('.breakdownTotal svg g');
        th = parent.select('table thead tr');
        tb = parent.select('table tbody');
    }

    bdoutersize = parseFloat(parent.select('.breakdownWrapper').style('width'));
    bdsize = Math.min(400, bdoutersize * 2 / 3);
    bdinnerSize = bdsize - 2 * bdmargin;

    bdGradDist.xscale
        .domain([0, d3.max(rg, function(d) {
            return d.d
        })])
        .range([0, parseFloat(d3.select('.breakdownGradDist svg').style('width')) - bdGradDist.m.l - bdGradDist.m.r]);

    bdGradDist.yscale
        .range([bdsize - bdGradDist.m.t - bdGradDist.m.b, 0]);

    /*bdGradDist.yscale1
        .domain([d3.max(rg, function(d) {
            return d.d
        }), 0])
        .range([0, parseFloat(d3.select('.breakdownGradDist svg').style('width')) - bdGradDist.m.l - bdGradDist.m.r]);
    bdGradDist.xscale1
        .range([bdsize - bdGradDist.m.t - bdGradDist.m.b, 0]);*/

    d3.select('.breakdownGradDist svg')
        .style({
            'width': (bdoutersize - bdsize) + 'px',
            'height': bdsize + 'px'
        });

    bdGradDist.y_axis.call(bdGradDist.yAxis);

    bdGradDist.xAxis.tickSize(-bdsize);
    bdGradDist.x_axis
        .attr("transform", "translate(0," + (bdsize - bdGradDist.m.b - bdGradDist.m.t) + ")")
        .call(bdGradDist.xAxis);

    bdGradDist.svg.selectAll('.tick line')
        .style({
            stroke: 'black',
            'stroke-opacity': 0.5
        });
    bdGradDist.svg.selectAll('.domain')
        .style({
            display: 'none'
        });

    var rects = bdGradDist.svg.selectAll('rect.item')
        .data(rg, function(d) {
            return d.g
        });

    rects.exit().remove();

    rects.enter().append('rect')
        .attr({
            class: 'item',
            'fill-opacity': 0.8,
            fill: function(d) {
                return colScale2(d.g / 100)
            },
            stroke: function(d) {
                return colScale2(d.g / 100)
            }
        })
        .on('mouseover', function(d) {
            if (typeof(bgdTooltip) === 'undefined') {
                bgdTooltip = d3.select('body').append('div')
                    .classed('nvtooltip xy-tooltip nv-pointer-events-none', true)
                    .attr('id', 'nvtooltip-bgdTooltip')
                    .attr('style', 'top: 0px; left: 0px; opacity: 0');
            }
            bgdTooltip.selectAll('*').remove();

            bgdTooltip.append('div')
                .style('margin', '6px 6px 0px 6px')
                .append('strong')
                .classed('x-value', true)
                .text(fInt(d.g) + ' % for ' + fDistAuto(d.d) + (d.t > 0 ? ' ('+d.t.toDayHrMinSec2()+')' : ''));

            if (d.g != 0) {
                var total = d3.sum(rg.filter(function(e) {
                    return d.g > 0 ? e.g >= d.g : (d.g < 0 ? e.g <= d.g : d.g == e.g);
                }), function(e) {
                    return e.d;
                });
                var timeTotal = d3.sum(rg.filter(function(e) {
                    return d.g > 0 ? e.g >= d.g : (d.g < 0 ? e.g <= d.g : d.g == e.g);
                }), function(e) {
                    return e.t;
                });

                var desc = '<div>' + fInt(d.g) + ' % and ' + (d.g > 0 ? 'above' : 'below') + ': ' + fDistAuto(total) + (timeTotal > 0 ? ' ('+timeTotal.toDayHrMinSec2()+')' : '') + '</div>';

                bgdTooltip.append('div')
                    .attr('style', 'margin: 6px 6px 0px 6px;white-space:normal;font-weight:bold;width:200px;font-size:0.8em')
                    .html(desc);
            }

            bgdTooltip.style('opacity', null);
        })
        .on('mouseout', function() {
            bgdTooltip.style('opacity', 0)
        })
        .on('mousemove', function() {
            var x = d3.event.pageX + 10;
            var w = parseFloat(bgdTooltip.style('width'));
            if (x + w > window.innerWidth) {
                x = d3.event.pageX - w - 10;
            }
            var y = d3.event.pageY - parseFloat(bgdTooltip.style('height')) / 2;
            bgdTooltip.style('transform', 'translate(' + x + 'px, ' + y + 'px)')
        });

    rects.attr({
        x: 0,
        y: function(d) {
            return bdGradDist.yscale(d.g + 0.5)
        },
        height: bdGradDist.yscale(25 - 0.5),
        width: function(d) {
            return bdGradDist.xscale(d.d)
        }
    })
    /*.each(function(d) {
            var title = d3.select(this).selectAll('title').data([0]);
            title.enter().append('title');
            title.text(fInt(d.g) + ' % for ' + fDistAuto(d.d))
        })*/
    ;

    /*bdGradDist.area
        .y0(bdGradDist.yscale1(0));

    bdGradDist.areaPath.datum(rg)
        .attr("d", bdGradDist.area);

        bdGradDist.areaPathG.attr('transform', 'rotate(90) translate(0,'+(-1*bdGradDist.yscale1.range()[1] - bdGradDist.m.l)+')')*/


    bdSizeScale.domain([0, d3.max(rpg, function(d) {
            return d.d
        })])
        .range([0, bdinnerSize / 2]);

    parent.select('.breakdownTotal svg')
        .style('width', bdsize + 'px')
        .style('height', bdsize + 'px');

    g.attr("transform", function(d) {
        return 'translate(' + (bdsize / 2) + ',' + (bdsize / 2) + ')'
    });

    bdc.attr('r', bdinnerSize / 2);
    bdc1.attr('r', (2 / 3) * (bdinnerSize / 2));
    bdc2.attr('r', (1 / 3) * (bdinnerSize / 2));

    bdl1.attr('y1', -bdinnerSize / 2)
        .attr('y2', bdinnerSize / 2);

    bdl2.attr('x1', -bdinnerSize / 2)
        .attr('x2', bdinnerSize / 2);

    bdtn.attr("transform", function(d) {
        return 'translate(' + 0 + ',' + (-2 - bdinnerSize / 2) + ')'
    });

    bdts.attr("transform", function(d) {
        return 'translate(0,' + (18 + bdinnerSize / 2) + ')'
    });

    bdte.attr("transform", function(d) {
        return 'translate(' + (2 + bdinnerSize / 2) + ',8)'
    });

    bdtw.attr("transform", function(d) {
        return 'translate(' + (-2 - bdinnerSize / 2) + ',8)'
    });

    var gg = g.selectAll('g.gg')
        .data(rpg, function(d) {
            return d.b
        });
    gg.enter().append('g')
        .classed('gg', true);

    gg.exit().remove();

    gg.each(function(d) {
        d3.select(this)
            .selectAll('path.pg').remove();
        var ggi = d3.select(this)
            .selectAll('path.pg')
            .data(d.g, function(e) {
                return e.g
            });
        ggi.exit().remove();
        ggi.enter()
            .append('path')
            .classed('pg', true)
            .style('fill', function(e) {
                return colScale2(e.g / 100)
            })
            .style('stroke-width', '0')
            .style('fill-opacity', '1')
            .on('mouseover', function(e) {
                if (typeof(bgTooltip) === 'undefined') {
                    bgTooltip = d3.select('body').append('div')
                        .classed('nvtooltip xy-tooltip nv-pointer-events-none', true)
                        .attr('id', 'nvtooltip-bgTooltip')
                        .attr('style', 'top: 0px; left: 0px; opacity: 0');
                }
                bgTooltip.selectAll('*').remove();

                bgTooltip.append('div')
                    .style('margin', '6px 6px 0px 6px')
                    .append('strong')
                    .classed('x-value', true)
                    .text((d.b - breakdownAngle / 2) + ' to ' + (d.b + breakdownAngle / 2) + ' degrees');

                var desc = '<div>Total distance: ' + fDistAuto(d.d) + ' (' + fPercent0(d.d / d3.sum(rpg, function(f) {
                    return f.d
                })) + ')</div>';
                desc += '<div>' + (e.g - breakdownGrad / 2) + ' to ' + (e.g + breakdownGrad / 2) + ' % for ' + fDistAuto(e.d) + ' (' + fPercent0(e.d / d.d) + ')</div>';

                bgTooltip.append('div')
                    .attr('style', 'margin: 6px 6px 0px 6px;white-space:normal;font-weight:bold;width:200px;font-size:0.8em')
                    .html(desc);

                bgTooltip.style('opacity', null);
            })
            .on('mouseout', function() {
                bgTooltip.style('opacity', 0)
            })
            .on('mousemove', function() {
                var x = d3.event.pageX + 10;
                var w = parseFloat(bgTooltip.style('width'));
                if (x + w > window.innerWidth) {
                    x = d3.event.pageX - w - 10;
                }
                var y = d3.event.pageY - parseFloat(bgTooltip.style('height')) / 2;
                bgTooltip.style('transform', 'translate(' + x + 'px, ' + y + 'px)')
            });

        gradDist = 0;
        ggi.transition()
            .duration(0)
            .attr('d', function(e) {
                var ir = bdSizeScale(gradDist);
                gradDist += e.d
                var or = bdSizeScale(gradDist);
                return d3.svg.arc()
                    .innerRadius(ir)
                    .outerRadius(or)
                    .startAngle((d.b - breakdownAngle / 2) * (pi / 180))
                    .endAngle((d.b + breakdownAngle / 2) * (pi / 180))()
            });
    });

    var paths = g.selectAll('path.p')
        .data(rpg, function(d) {
            return d.b
        });

    paths.exit().remove();

    paths.enter()
        .append('path')
        .classed('p', true)
        //.style('fill', '#FF032E')
        .style('stroke-width', '1')
        .style('stroke', '#000')
        .style('fill-opacity', 0)
        .style('pointer-events', 'none');

    paths.transition()
        .duration(0)
        .attr('d', function(d) {
            return d3.svg.arc()
                .innerRadius(0)
                .outerRadius(bdSizeScale(d.d))
                .startAngle((d.b - breakdownAngle / 2) * (pi / 180))
                .endAngle((d.b + breakdownAngle / 2) * (pi / 180))()
        });

    var ge = d3.extent(d3.merge(rpg.map(function(d) {
        return d.g
    })), function(d) {
        return d.g
    });

    var ths = th.selectAll('th.gradth')
        .data(d3.range(ge[0], ge[1] + .1, breakdownGrad), function(d) {
            return d
        });

    ths.exit().remove();
    ths.enter().append('th')
        .classed('gradth', true)
        .text(function(d) {
            return (d - breakdownGrad / 2) + ' to ' + (d + breakdownGrad / 2) + ' %'
        });
    ths.order();

    tb.selectAll('tr').remove();
    var tr = tb.selectAll('tr')
        .data(rpg, function(d) {
            return d.b
        });

    tr.exit().remove();
    tr.enter().append('tr').each(function() {
        d3.select(this).append('td')
            .classed('tdgd', true)
            .html(function(d) {
                return '<span style="float:right;-ms-transform: rotate(' + d.b + 'deg);-webkit-transform: rotate(' + d.b + 'deg);transform: rotate(' + d.b + 'deg)">&uarr;</span><span style="margin-right:5px">' + (d.b - breakdownAngle / 2) + '° to ' + (d.b + breakdownAngle / 2) + '°</span>'
            });
        d3.select(this).append('td')
            .classed('tdgt', true);
    });

    tr.selectAll('td.tdgt')
        .text(function(d) {
            return fDistAuto(d.d)
        })

    tr.each(function(d) {
        var data = d3.range(ge[0], ge[1] + .1, breakdownGrad).map(function(e) {
            var a = d.g.filter(function(f) {
                return f.g == e
            });
            return {
                g: e,
                d: a.length > 0 ? a[0].d : 0
            }
        });
        var tri = d3.select(this)
            .selectAll('td.tdg')
            .data(data, function(e) {
                return e.g
            });

        tri.exit().remove();
        tri.enter()
            .append('td')
            .classed('tdg', true);

        tri.text(function(e) {
            return fDistAuto(e.d)
        });
        tri.order();
    });
    tr.order();

    if (isFirstTime) {
        drawBreakdown(parent, llArr, dArr, eArr, fExt, tArr);
    }
}

var gmapDetailPolylines = [];

function setMapDetail(type, parent) {
    if (typeof(isOS) === 'undefined') {
        isOS = false;
    }
    if (type == 'G') {
        gmapDetailPolylines.forEach(function(d) {
            d.setMap(null);
        });
        gmapDetailPolylines = [];
    }
    var da = (typeof(segment.da) !== 'undefined' ? segment.da : (typeof(segment.distanceArr2Copy) !== 'undefined' ? segment.distanceArr2Copy : segment.distanceArr2));
    var lla = (typeof(segment.lla) !== 'undefined' ? segment.lla : (typeof(segment.latLngArr2Copy) !== 'undefined' ? segment.latLngArr2Copy : segment.latLngArr2)); //test
    if (typeof(bdsmel) === 'undefined') {
        var ea = (typeof(segment.ea) !== 'undefined' ? segment.ea : (typeof(segment.elevationArr2Copy) !== 'undefined' ? segment.elevationArr2Copy : segment.elevationArr2));
        bdsmel = filterData(da.map(function(d, i) {
            return {
                x: d,
                y: ea[i]
            }
        })).map(function(d) {
            return d.ySmooth
        });
    }

    var lData = bdsmel.map(function(d, i) {
        return {
            x: da[i],
            y: d,
            z: 0,
            i: i
        }
    });
    if (typeof(brushExtent) !== 'undefined' && brushExtent[0] != brushExtent[1] && brushExtent[1] > 0) {
        lData = lData.slice(filtIndExtent[0], filtIndExtent[1] + 1)
    }
    var mapDetailData = lData;
    if (lData.length >= 250) {
        var simpVal = 10;
        mapDetailData = simplify(lData, simpVal, true);
        while (mapDetailData.length < 250 && simpVal > 0.01) {
            mapDetailData = simplify(lData, simpVal, true);
            simpVal = simpVal / 2;
        }
    }

    if (gMapFullGradArr.length == 0) {
        gMapFullGradArr = mapDetailData.slice(1).map(function(d, i) {
            //console.log(i + ', ' + d.y + ', ' + mapDetailData[i].y + ', ' + d.x + ', ' + mapDetailData[i].x + ', ' + ((d.y - mapDetailData[i].y) / (d.x - mapDetailData[i].x)));
            return {
                c: colScale2((d.y - mapDetailData[i].y) / (d.x - mapDetailData[i].x)),
                x: d.x
            }
        });
    }

    var mapDetailArr = [];
    mapDetailData.forEach(function(d, i) {
        if (i > 0) {
            var lp = [];
            lla.slice(mapDetailData[i - 1].i, d.i + 1).forEach(function(d, i) {
                e = d;
                if (isOS) {
                    var OSll = getOSPos(d.lat, d.lng);
                    e.lat = OSll.lat;
                    e.lng = OSll.lng;
                }

                switch (type) {
                    case 'O':
                        nextTP = new OpenLayers.LonLat(e.lng, e.lat).transform((isOS ? toProjection : fromProjection), toProjection);
                        lp.push(new OpenLayers.Geometry.Point(nextTP.lon, nextTP.lat));
                        break;
                    case 'G':
                        lp.push(new google.maps.LatLng(d.lat, d.lng));
                        break;
                    case 'L':
                        lp.push([d.lat, d.lng]);
                }
            });
            switch (type) {
                case 'O':
                    var ls = new OpenLayers.Geometry.LineString(lp);
                    var lv = new OpenLayers.Feature.Vector(ls, null, clone(mapLineFGBase));
                    lv.style.strokeColor = colScale2((mapDetailData[i].y - mapDetailData[i - 1].y) / (mapDetailData[i].x - mapDetailData[i - 1].x));
                    mapDetailArr.push(lv);
                    break;
                case 'G':
                    var poly = new google.maps.Polyline({
                        path: lp,
                        geodesic: true,
                        strokeColor: colScale2((mapDetailData[i].y - mapDetailData[i - 1].y) / (mapDetailData[i].x - mapDetailData[i - 1].x)),
                        strokeOpacity: 1.0,
                        strokeWeight: 2,
                        zIndex: 100 + i
                    });
                    poly.setMap(parent);
                    gmapDetailPolylines.push(poly);
                    break;
                case 'L':
                    var col = colScale2((mapDetailData[i].y - mapDetailData[i - 1].y) / (mapDetailData[i].x - mapDetailData[i - 1].x));
                    if (!lm_mapLineStyles[col]) {
                        lm_mapLineStyles[col] = {
                            opacity: 1,
                            weight: 2,
                            color: col,
                            smoothFactor: lm_mapLineSmoothing
                        }
                    }
                    var layer = new L.Polyline(lp, lm_mapLineStyles[col]);
                    mapDetailArr.push(layer);
                    parent.addLayer(layer);
                    break;
            }
        }
    });
    if (type == 'O') {
        parent.addFeatures(mapDetailArr);
    }
    if (type == 'L') {
        return mapDetailArr;
    }
}

var isRetinaDisplayVal = null;
function isRetinaDisplay() {
    if (isRetinaDisplayVal != null) return isRetinaDisplayVal;
    isRetinaDisplayVal = false;
    if (window.matchMedia) {
        var mq = window.matchMedia("only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (-o-min-device-pixel-ratio: 2.6/2), only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen  and (min-device-pixel-ratio: 1.3), only screen and (min-resolution: 1.3dppx)");
        isRetinaDisplayVal = (mq && mq.matches || (window.devicePixelRatio > 1));
    }
    return isRetinaDisplayVal;
}

var setSVPanoTimer = null,
    streetviewShown = false,
    svMap, svPano, hlSVPoly, marker, isPanningSV = false,
    svMarker;
var retinaImg = isRetinaDisplay() ? '@2x' : '';

function getBearing(x1, y1, x2, y2) {
    return Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
}

function loadstreetview() {
    if (!streetviewShown) {

        var nlsmapG = new google.maps.ImageMapType({
                getTileUrl: function(coord, zoom) {
                    return "http://t" + ((coord.x + coord.y) % 5) + ".cz.tileserver.com/osnew/" + zoom + '/' + coord.x + '/' + coord.y + '.png';
                },
                tileSize: new google.maps.Size(256, 256),
                isPng: false,
                maxZoom: 16,
                minZoom: 1,
                name: 'OS (UK only)',
                alt: "Ordnance Survey (UK only)"
            }),
            osmmapG = new google.maps.ImageMapType({
                getTileUrl: function(coord, zoom) {
                    var tilesPerGlobe = 1 << zoom;
                    var x = coord.x % tilesPerGlobe;
                    if (x < 0) {
                        x = tilesPerGlobe + x;
                    }
                    return "http://tile.openstreetmap.org/" + zoom + "/" + x + "/" + coord.y + ".png";
                },
                tileSize: new google.maps.Size(256, 256),
                name: "OSM",
                maxZoom: 18
            }),
            cyclemapG = new google.maps.ImageMapType({
                getTileUrl: function(coord, zoom) {
                    return mapProxy.replace('https', 'http') + ['a', 'b', 'c'][(Math.floor(Math.random() * 3))] + '.tile.opencyclemap.org/cycle/' + zoom + '/' + coord.x + '/' + coord.y + retinaImg + '.png';
                },
                tileSize: new google.maps.Size(256, 256),
                name: "Cycle Map",
                maxZoom: 18
            });

        if (typeof(segment.latLngArr2Copy) === 'undefined') {
            segment.latLngArr2Copy = segment.latLngArr2;
            segment.distanceArr2Copy = segment.distanceArr2;
        }
        var la = (typeof(llObj) !== 'undefined' ? llObj.data2 : segment.latLngArr2Copy);
        var da = (typeof(distObj) !== 'undefined' ? distObj.data : segment.distanceArr2Copy);
        d3.select('#svTab').style('margin', '-10px -10px 0px -10px').append('div')
            .attr({
                id: 'svMap',
                style: 'float:left;height:100%;width:50%;'
            });
        d3.select('#svTab').append('div')
            .attr({
                id: 'svPano',
                style: 'float:left;height:100%;width:50%;'
            });

        var pos = new google.maps.LatLng(la[0].lat, la[0].lng);
        var bearing = getBearing(la[0].lat, la[0].lng, la[1].lat, la[1].lng);

        svMap = new google.maps.Map(document.getElementById('svMap'), {
            mapTypeControlOptions: {
                mapTypeIds: ['custom', 'OSM', 'osopendata', 'cyclemap', 'bandw', google.maps.MapTypeId.TERRAIN, google.maps.MapTypeId.SATELLITE, google.maps.MapTypeId.HYBRID],
                style: google.maps.MapTypeControlStyle.DROPDOWN_MENU
            },
            mapTypeControl: true
        });

        svMap.mapTypes.set('osopendata', nlsmapG);
        svMap.mapTypes.set('OSM', osmmapG);
        svMap.mapTypes.set('cyclemap', cyclemapG);

        if (typeof(mbCustomToken) !== 'undefined' && mbCustomToken != '') {
            svMap.mapTypes.set('custom', new google.maps.ImageMapType({
                getTileUrl: function(coord, zoom) {
                    return mapProxy + ['a', 'b', 'c'][(Math.floor(Math.random() * 3))] + '.tiles.mapbox.com/v4/' + mbCustomDomain + '/' + zoom + '/' + coord.x + '/' + coord.y + retinaImg + '.png?access_token=' + mbCustomToken;
                },
                tileSize: new google.maps.Size(256, 256),
                name: 'Custom',
                maxZoom: 18
            }));
            svMap.setMapTypeId('custom');
        }

        var bounds = new google.maps.LatLngBounds();
        bounds.extend(new google.maps.LatLng(segment.minLat, segment.minLng));
        bounds.extend(new google.maps.LatLng(segment.maxLat, segment.maxLng));
        svMap.fitBounds(bounds);

        svPano = new google.maps.StreetViewPanorama(
            document.getElementById('svPano'), {
                enableCloseButton: true,
                position: pos,
                pov: {
                    heading: bearing,
                    pitch: 0
                }
            });
        svMap.setStreetView(svPano);

        var bgSVPoly = new google.maps.Polyline({
            path: la.map(function(d) {
                return new google.maps.LatLng(d.lat, d.lng);
            }),
            geodesic: true,
            strokeColor: '#FFF',
            strokeOpacity: 1.0,
            strokeWeight: 5,
            zIndex: 1
        });
        bgSVPoly.setMap(svMap);
        hlSVPoly = new google.maps.Polyline({
            path: la.map(function(d) {
                return new google.maps.LatLng(d.lat, d.lng);
            }),
            geodesic: true,
            strokeColor: '#000',
            strokeOpacity: 1.0,
            strokeWeight: 5,
            zIndex: 2
        });
        hlSVPoly.setMap(svMap);
        var fgSVPoly = new google.maps.Polyline({
            path: la.map(function(d) {
                return new google.maps.LatLng(d.lat, d.lng);
            }),
            geodesic: true,
            strokeColor: '#FF032E',
            strokeOpacity: 1.0,
            strokeWeight: 2,
            zIndex: 3
        });
        fgSVPoly.setMap(svMap);

        setMapDetail('G', svMap);

        new google.maps.Marker({
            position: new google.maps.LatLng(la[0].lat, la[0].lng),
            map: svPano,
            icon: https + '://chart.googleapis.com/chart?chst=d_map_spin&chld=1.1|0|00FF00|13|b|Start'
        });
        new google.maps.Marker({
            position: new google.maps.LatLng(la[0].lat, la[0].lng),
            map: svMap,
            icon: https + '://' + assetPrefix + '/img/marker-green.png'
        });

        new google.maps.Marker({
            position: new google.maps.LatLng(la[la.length - 1].lat, la[la.length - 1].lng),
            map: svPano,
            icon: https + '://chart.googleapis.com/chart?chst=d_map_spin&chld=1.1|0|FF032E|13|b|End'
        });
        new google.maps.Marker({
            position: new google.maps.LatLng(la[la.length - 1].lat, la[la.length - 1].lng),
            map: svMap,
            icon: https + '://' + assetPrefix + '/img/marker-finish.png'
        });

        svMarker = new google.maps.Marker({
            position: pos,
            icon: {
                path: google.maps.SymbolPath.CIRCLE,
                fillOpacity: 0,
                fillColor: '#000',
                strokeOpacity: 0.8,
                strokeColor: '#000',
                strokeWeight: 3,
                scale: 4 //pixels
            }
        });
        svMarker.setMap(svMap);

        svMap.setZoom(svMap.getZoom() == 0 ? 14 : svMap.getZoom());

        svMap.addListener('maptypeid_changed', setGMapAttribution);
        svMap.addListener('resize', setGMapAttribution);

        svPano.addListener('position_changed', syncToPano);
        svPano.addListener('visible_changed', function() {
            if (svPano.getVisible()) {
                d3.select('#svMap').style('width', '50%');
                d3.select('#svPano').style('width', '50%');
            } else {
                d3.select('#svMap').style('width', '100%');
                d3.select('#svPano').style('width', '0%');
            }
            var c = svMap.getCenter();
            google.maps.event.trigger(svMap, "resize");
            svMap.setCenter(c);
            setGMapAttribution();
        });
        streetviewShown = true;
    }
}

function setGMapAttribution() {
    window.setTimeout(function() {
        switch (svMap.mapTypeId) {
            case 'custom':
                d3.select('.gm-style-cc span').each(function() {
                    d3.select(this).html('Maps © <a style="color: rgb(68, 68, 68)" href="http://www.mapbox.com">MapBox</a>, Data © <a style="color: rgb(68, 68, 68)" href="http://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>').style('display', null);
                    d3.select(this.parentElement.parentElement.parentElement).style('width', null);
                });
                break;
            case 'cyclemap':
                d3.select('.gm-style-cc span').each(function() {
                    d3.select(this).html('Maps © <a style="color: rgb(68, 68, 68)" href="http://www.thunderforest.com">Thunderforest</a>, Data © <a style="color: rgb(68, 68, 68)" href="http://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>').style('display', null);
                    d3.select(this.parentElement.parentElement.parentElement).style('width', null);
                });
                break;
            case 'OSM':
                d3.select('.gm-style-cc span').each(function() {
                    d3.select(this).html('© <a style="color: rgb(68, 68, 68)" href="http://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>').style('display', null);
                    d3.select(this.parentElement.parentElement.parentElement).style('width', null);
                });
                break;
            case 'osopendata':
                d3.select('.gm-style-cc span').each(function() {
                    d3.select(this).html('<a style="color: rgb(68, 68, 68)" href="http://www.ordnancesurvey.co.uk/oswebsite/opendata/">Ordnance Survey OpenData</a> © Crown copyright and database right 2010 <a style="color: rgb(68, 68, 68)" href="http://www.klokantech.com/" target="_blank">Klokan Technologies GmbH</a>').style('display', null);
                    d3.select(this.parentElement.parentElement.parentElement).style('width', null);
                });
                break;
        }
    }, 100);
}

function setSvExtent() {
    var la = (typeof(llObj) !== 'undefined' ? llObj.data2 : segment.latLngArr2Copy);
    var da = (typeof(distObj) !== 'undefined' ? distObj.data : segment.distanceArr2Copy);
    if (typeof(filtIndExtent) === 'undefined' || typeof(filtIndExtent[0]) === 'undefined') {
        filtIndExtent = [0, la.length-1];
    }
    var llArr = la.slice(Math.max(0, filtIndExtent[0]), filtIndExtent[1] + 1);
    hlSVPoly.setPath(llArr.map(function(d) {
        return new google.maps.LatLng(d.lat, d.lng);
    }));
    var bounds = new google.maps.LatLngBounds();
    var latExt = d3.extent(llArr, function(d) {
        return d.lat
    });
    var lngExt = d3.extent(llArr, function(d) {
        return d.lng
    });
    bounds.extend(new google.maps.LatLng(latExt[0], lngExt[0]));
    bounds.extend(new google.maps.LatLng(latExt[1], lngExt[1]));
    svMap.fitBounds(bounds);

    if (llArr.length != la.length) {
        setSVPano(llArr[0], llArr[1], false);
    }
}

function setSVPano(ll0, ll1, setCenter) {
    if (svPano.getVisible()) {
        if (setSVPanoTimer != null) {
            window.clearTimeout(setSVPanoTimer);
            setSVPanoTimer = null;
        }
        setSVPanoTimer = setTimeout(function() {
            svPano.setVisible(true);
            var pos = new google.maps.LatLng(ll0.lat, ll0.lng);
            svPano.setPosition(pos);
            var bearing = getBearing(ll0.lat, ll0.lng, ll1.lat, ll1.lng);
            svPano.setPov({
                heading: bearing,
                pitch: 0
            })
            if (setCenter) {
                svMap.setCenter({
                    lat: ll0.lat,
                    lng: ll0.lng
                });
            }
            setSVPanoTimer = null;
        }, 1000);
    }
}

function syncToPano() {
    //var positionCell = document.getElementById('position-cell');
    //positionCell.firstChild.nodeValue = panorama.getPosition() + '';
    //setMapMarker(elevChart.xScale().invert(d3.event.x - this.getBoundingClientRect().left), d3.event.x - this.getBoundingClientRect().left);
    if (!isPanningSV) {
        var la = (typeof(llObj) !== 'undefined' ? llObj.data2 : segment.latLngArr2Copy);
        var da = (typeof(distObj) !== 'undefined' ? distObj.data : segment.distanceArr2Copy);
        var lat = svPano.getPosition().lat();
        var lng = svPano.getPosition().lng();
        var rp = [];
        for (var i = 0; i < la.length; i++) {
            var e = la[i];
            var d = distLatLon(e.lat, e.lng, lat, lng);
            if (d < 75) {
                rp.push({
                    i: i,
                    d: d
                });
            }
        }
        if (rp.length > 0) {
            rp.sort(function(a, b) {
                return a.d - b.d
            });
            if (typeof(elevChart) !== 'undefined') {
                if (typeof(elevChart.xScale) !== 'undefined') {
                    setMapMarker(da[rp[0].i], elevChart.xScale()(da[rp[0].i]));
                } else {
                    setMapMarker(da[rp[0].i]);
                }
            }
        }
    }
}

function calculateVVOM(arr, eg) {
    var v = (typeof(activity) !== 'undefined' ? activity : segment).type.indexOf('Virtual') == 0;
    var xy = clone(arr);
    xy[0].eg = 0;
    xy[0].g = 0;
    xy[0].s = 0;
    for (var i = 1; i < xy.length; i++) {
        xy[i].eg = 0;
        xy[i].g = 0;
        if (xy[i].y - xy[i - 1].y > 0) {
            if (typeof(eg) !== 'undefined') {
                eg += xy[i].y - xy[i - 1].y;
            }
        }
        xy[i].eg = xy[i].y - xy[i - 1].y;
        xy[i].dd = xy[i].x - xy[i - 1].x;
        xy[i].g = xy[i].eg / xy[i].dd;
        xy[i].s = xy[i].eg < -0.05 ? 0 : (xy[i].g + 0.05) * xy[i].eg * (1 + (v ? 0 : xy[i].y) / 5000) * (1 / 8);
    }
    var sc = (xy.last().x / 5000) + d3.sum(xy, function(d) {
        return d.s;
    });
    return sc;
}

var grdspd = [
        [100, 1],
        [14, 2.4],
        [10.5, 4],
        [8, 5],
        [6.5, 6],
        [4.5, 7],
        [2.5, 9],
        [0, 12],
        [-7, 20],
        [-9.5, 25],
        [-50, 25]
    ],
    grdspdScale = d3.scale.linear().domain(grdspd.map(function(d) {
        return d[0]
    })).range(grdspd.map(function(d) {
        return d[1] * 0.7
    }));

function sendToPMR(xyz, name) {
    var vv_gpx = createGpxTcxKml(xyz, 'gpx', name);
    var form = d3.select('body').append('form').attr({
            'method': 'post',
            'id': 'pmrForm',
            //'target': '_blank',
            'action': 'https://store.printmyride.uk/veloviewer/veloviewer-affiliate.php',
            'accept-charset': 'UTF-8'
        })
        .attr('style', 'position:absolute;top:0px;left:330px;z-index:2000');
    form.append('textarea').attr({
        'id': 'vv_gpx1',
        'name': 'gpx',
    })
    document.getElementById('vv_gpx1').value = vv_gpx;
    document.getElementById('pmrForm').submit();
    form.remove();
}


function createGpxTcxKml(xyz, type, name, maxSize, fd) {
    var xml, xmlEnd, xmlSize, itemSize, sd, simpFactor = 0.0000001,
        tf = d3.time.format('%Y-%m-%dT%H:%M:%S.000Z'),
        fd = typeof(fd) === 'undefined' ? d3.time.format('%Y-%m-%d')(new Date()) : fd,
        maxSize = typeof(maxSize) === 'undefined' ? 500000 : maxSize;
    if (fd != -1) {
        fd = fd.split('-');
        sd = new Date(+fd[0], +fd[1] - 1, +fd[2], 6, 0, Math.floor(60 * 60 * Math.random()));
    }
    name = name.replace(/\&/g, '&#038;');

    switch (type) {
        case 'gpx':
            xml = '<?xml version="1.0" encoding="UTF-8"?><gpx version="1.1" creator="VeloViewer with Barometer" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><metadata><link href="veloviewer.com"><text>VeloViewer</text></link><referrer>VeloViewerPMR</referrer>' + (fd != -1 ? '<time>' + tf(sd) + '</time>' : '') + '</metadata><trk><name>' + name + '</name><trkseg>';
            xmlEnd = '</trkseg></trk></gpx>';
            itemSize = (fd != -1 ? 104 : 66);
            break;
        case 'kml':
            xml = '<?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"><Placemark><name>' + name + '</name><description>Created with VeloViewer.com</description><Style id="default"><LineStyle><color>FFCAB200</color><width>3</width></LineStyle></Style><LineString><coordinates>';
            xmlEnd = '</coordinates></LineString></Placemark></kml>';
            itemSize = 22;
            break;
    }
    xmlSize = xml.length + xmlEnd.length;

    while (xmlSize + xyz.length * itemSize > maxSize) {
        xyz = simplify(xyz, simpFactor, true);
        simpFactor *= 2;
    }

    for (var i = 0; i < xyz.length; i++) {
        if (fd != -1) {
            var td = 0;
            if (i > 0) {
                var dist = distLatLon(xyz[i - 1].y, xyz[i - 1].x, xyz[i].y, xyz[i].x);
                var grad = (xyz[i].z - xyz[i - 1].z) / dist;
                td = dist / grdspdScale(grad);
            }
            sd = new Date(sd.getTime() + td * 1000);
        }
        switch (type) {
            case 'gpx':
                xml += '<trkpt lon="' + xyz[i].x.toFixed(5) + '" lat="' + xyz[i].y.toFixed(5) + '"><ele>' + xyz[i].z.toFixed(1) + '</ele>' + (fd != -1 ? '<time>' + tf(sd) + '</time></trkpt>' : '');
                break;
            case 'kml':
                xml += xyz[i].x.toFixed(5) + ',' + xyz[i].y.toFixed(5) + ' ';
                break;
        }
    }

    return xml + xmlEnd;
}

if (typeof(starredNotPopulated) !== 'undefined') {
    var url = '/api/getStarredSegments.php';
    $.ajax({
        url: url,
        async: true,
        success: function(result) {
            if (typeof(result.length) !== 'undefined') {
                starred = result;
                if (typeof(setStarredData) !== 'undefined') setStarredData();
            }
        },
        error: function(result) {
            //alert('Oh dear, something has gone wrong :-(');
        }
    });
}

var leaderboardsHtml = '<li><a href="/leaderboard">Overall & Club Yearly Leaderboards</a></li><li class="divider"></li><li class="nav-header">Zwift Insider Leaderboards</li><li><a href="/zwift-insider">Overall</a></li><li><a href="/zwift-insider/watopia-short">Watopia (Short)</a></li><li><a href="/zwift-insider/watopia-medium">Watopia (Medium)</a></li></li><li><a href="/zwift-insider/watopia-long">Watopia (Long)</a></li><li><a href="/zwift-insider/london">London</a></li><li><a href="/zwift-insider/new-york">New York</a></li><li><a href="/zwift-insider/yorkshire">Yorkshire</a></li><li><a href="/zwift-insider/innsbruck">Innsbruck</a></li><li><a href="/zwift-insider/richmond">Richmond</a></li><li><a href="/zwift-insider/france">France + Paris</a></li><li><a href="/zwift-insider/makuri-short">Makuri Island (Short)</a></li><li><li><a href="/zwift-insider/makuri-long">Makuri Island (Long)</a></li><li><a href="/zwift-insider/scotland">Scotland</a></li><li><a href="/zwift-insider/rebel-routes">Rebel Routes</a></li><li><a href="/zwift-insider/kom-short">KOM (Short)</a></li><li><a href="/zwift-insider/kom-long">KOM (Long)</a></li><li><a href="/climb-portal/short">Climb Portal (Short)</a></li><li><a href="/climb-portal/long">Climb Portal (Long)</a></li><li class="nav-header">100 Climbs Leaderboards</li><li><a href="/100Climbs/Original">Original 100 Greatest Cycling Climbs</a></li><li><a href="/100Climbs/Another">Another 100 Greatest Cycling Climbs</a></li><li><a href="/100Climbs/X-List">100 Climbs X-List</a></li><li><a href="/100Climbs/South-East">Cycling Climbs of South-East England</a></li><li><a href="/100Climbs/South-West">Cycling Climbs of South-West England</a></li><li><a href="/100Climbs/Midlands">Cycling Climbs of the Midlands</a></li><li><a href="/100Climbs/Yorkshire">Cycling Climbs of Yorkshire</a></li><li><a href="/100Climbs/NorthWest">Cycling Climbs of North-West England</a></li><li><a href="/100Climbs/NorthEast">Cycling Climbs of North-East England</a></li><li><a href="/100Climbs/Wales">Cycling Climbs of Wales</a></li><li><a href="/100Climbs/Scotland">Cycling Climbs of Scotland</a></li><li><a href="/100Climbs/Hellingen">Hellingen: Cycling Climbs of Belgium</a></li><li><a href="/100Climbs/Tour-de-France">100 Greatest Cycling Climbs of the Tour de France</a></li><li><a href="/100Climbs/Italy">100 Greatest Cycling Climbs of Italy</a></li><li><a href="/100Climbs/Spain">100 Greatest Cycling Climbs of Spain</a></li>';

if (typeof(myHunterSeries) !== 'undefined' && myHunterSeries.length > 0) {
    leaderboardsHtml += '<li class="divider"></li>';
    myHunterSeries.forEach(function(d) {
        leaderboardsHtml += '<li><a href="/segmentHunterSeries/' + d.id + '">' + d.name + '</a></li>';
    });
}
d3.select('ul.leaderboards').html(leaderboardsHtml);
d3.select('ul.coolStuff').html('<li ><a href="/infographic">Year Infographic</a></li><li class="divider"></li><li><a href="https://blog.veloviewer.com/">Blog</a></li><li><a href="https://blog.veloviewer.com/about/">About</a></li><li><a href="https://blog.veloviewer.com/meet-the-team/">Meet The Team</a></li><li><a href="https://blog.veloviewer.com/veloviewer-free-vs-pro/">Free vs PRO</a></li><li><a href="https://blog.veloviewer.com/veloviewer-renewal-infomation/">Subscription/Renewal Info</a></li><li><a href="https://blog.veloviewer.com/faq/">FAQ</a></li><li><a href="https://blog.veloviewer.com/contact/">Contact</a></li>');

/*if (!hasLocalStorage || localStorage.getItem('hideSufferfest') == null) {
    d3.selectAll('.sufferfestAd').style('display', null).selectAll('a').attr('href','https://blog.veloviewer.com/60-days-of-the-sufferfest-for-free-with-veloviewer-pro/').attr('target','_blank');
}
d3.selectAll('.sufferfestAd a').html(function(){return d3.select(this).html().replace(' 60 DAYS FOR FREE!', '&nbsp;60&nbsp;DAYS&nbsp;FOR&nbsp;FREE!')});
*/
d3.select('#VVOSTTS').remove();
d3.select('#socialPlaceholder')
    .append('div')
    .attr('id', 'VVOSTTS')
    .style({
        'display': 'block',
        'margin-bottom': '15px',
        'border': '0.5px solid #ccc',
        'background': 'white',
        'padding': '5px 10px'
    })
    .append('h3')
    .html('<img src="https://cf.veloviewer.com/img/UCI_World_Tour_logo.svg" style="margin-bottom:5px"><br/>VeloViewer is used by all mens and womens WorldTour teams along with many ProTour, Continental and national teams. <a target="_blank" href="https://blog.veloviewer.com/about-the-veloviewer-worldtour-package/">More&nbsp;details...</a>');

window.setInterval(function() { if (window.self !== window.top) d3.selectAll('a:not([target]').attr('target', '_top') }, 1000);

var vvwrsvg = '<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="157px" height="28px" viewBox="0 0 157 28"><g transform="matrix(1.25,0,0,-1.25,0,29.999997)"><g transform="matrix(1.760792,0,0,1.760792,-12.151382,-8.963022)"><path transform="translate(30.6738,8.6094)" d="m 0,0 c -1.061,0 -1.82,0.887 -1.82,1.933 l 0,0.03 c 0,1.045 0.696,1.916 1.788,1.916 1.061,0 1.837,-0.887 1.837,-1.946 l 0,-0.033 C 1.805,0.871 1.092,0 0,0 m -0.032,-2.549 c 2.125,0 3.798,1.218 4.451,2.891 L 4.421,0.35 3.555,1.871 C 3.131,2.629 2.985,2.885 2.657,3.469 2.001,4.633 1.047,6.309 1.047,6.309 0.714,6.38 0.363,6.412 0,6.412 c -2.739,0 -4.733,-2.043 -4.733,-4.479 l 0,-0.033 c 0,-2.437 1.979,-4.449 4.701,-4.449 m -7.471,0.207 2.992,0 0,12.158 -2.992,0 0,-12.158 z m -5.588,4.987 c 0.158,0.949 0.696,1.55 1.487,1.55 0.824,0 1.394,-0.601 1.504,-1.55 l -2.991,0 z m 1.757,-5.194 c 1.694,0 2.881,0.697 3.704,1.742 L -9.292,0.57 c -0.618,-0.601 -1.171,-0.871 -1.9,-0.871 -0.933,0 -1.615,0.477 -1.852,1.41 l 5.731,0 c 0.016,0.205 0.016,0.428 0.016,0.618 0,2.517 -1.362,4.685 -4.307,4.685 -2.532,0 -4.321,-1.978 -4.321,-4.479 l 0,-0.033 c 0,-2.643 1.9,-4.449 4.591,-4.449" style="fill:#fff;stroke:none"></path><path transform="translate(6.9233,14.8223)" d="M 0,0 4.879,-8.619 9.742,0 6.852,0 4.879,-3.465 2.907,0 0,0 z" style="fill:#fff;stroke:none"></path><path transform="translate(32.9336,14.8223)" d="M 0,0 4.879,-8.619 9.742,0 6.852,0 4.879,-3.465 2.907,0 0,0 z" style="fill:#FF032E;stroke:none"></path><path transform="translate(72.4004,6.2676)" d="m 0,0 3.008,0 0,2.676 c 0,1.963 0.887,2.865 2.453,2.865 l 0.254,0 0,3.167 C 4.291,8.77 3.498,8.01 3.008,6.838 l 0,1.726 L 0,8.564 0,0 z m -5.508,4.986 c 0.158,0.95 0.696,1.551 1.488,1.551 0.823,0 1.393,-0.601 1.504,-1.551 l -2.992,0 z m 1.756,-5.193 c 1.695,0 2.883,0.697 3.705,1.742 l -1.662,1.377 c -0.617,-0.601 -1.172,-0.871 -1.9,-0.871 -0.934,0 -1.614,0.477 -1.852,1.41 l 5.731,0 c 0.015,0.205 0.015,0.428 0.015,0.617 0,2.518 -1.361,4.686 -4.305,4.686 -2.533,0 -4.322,-1.979 -4.322,-4.48 l 0,-0.032 c 0,-2.643 1.899,-4.449 4.59,-4.449 m -13.896,0.143 2.564,0 1.426,4.464 1.392,-4.464 2.596,0 2.645,8.628 -2.897,0 -1.156,-4.337 -1.33,4.369 -2.469,0 -1.33,-4.337 -1.125,4.305 -2.928,0 2.612,-8.628 z m -7.122,5.05 c 0.159,0.95 0.698,1.551 1.489,1.551 0.824,0 1.392,-0.601 1.504,-1.551 l -2.993,0 z m 1.758,-5.193 c 1.694,0 2.881,0.697 3.705,1.742 l -1.662,1.377 c -0.619,-0.601 -1.172,-0.871 -1.9,-0.871 -0.934,0 -1.615,0.477 -1.852,1.41 l 5.731,0 c 0.015,0.205 0.015,0.428 0.015,0.617 0,2.518 -1.361,4.686 -4.306,4.686 -2.533,0 -4.323,-1.979 -4.323,-4.48 l 0,-0.032 c 0,-2.643 1.901,-4.449 4.592,-4.449 M -30.373,0 l 2.992,0 0,8.564 -2.992,0 0,-8.564 z" style="fill:#FF032E;stroke:none"></path><path transform="translate(43.5332,15.4336)" d="M 0,0 C 0.826,0 1.496,0.67 1.496,1.496 1.496,2.322 0.826,2.992 0,2.992 -0.827,2.992 -1.496,2.322 -1.496,1.496 -1.496,0.67 -0.827,0 0,0" style="fill:#FF032E;stroke:none"></path></g></g></svg>';

function MD5(d) { var r = M(V(Y(X(d), 8 * d.length))); return r.toLowerCase() };

function M(d) { for (var _, m = "0123456789ABCDEF", f = "", r = 0; r < d.length; r++) _ = d.charCodeAt(r), f += m.charAt(_ >>> 4 & 15) + m.charAt(15 & _); return f }

function X(d) { for (var _ = Array(d.length >> 2), m = 0; m < _.length; m++) _[m] = 0; for (m = 0; m < 8 * d.length; m += 8) _[m >> 5] |= (255 & d.charCodeAt(m / 8)) << m % 32; return _ }

function V(d) { for (var _ = "", m = 0; m < 32 * d.length; m += 8) _ += String.fromCharCode(d[m >> 5] >>> m % 32 & 255); return _ }

function Y(d, _) {
    d[_ >> 5] |= 128 << _ % 32, d[14 + (_ + 64 >>> 9 << 4)] = _;
    for (var m = 1732584193, f = -271733879, r = -1732584194, i = 271733878, n = 0; n < d.length; n += 16) {
        var h = m,
            t = f,
            g = r,
            e = i;
        f = md5_ii(f = md5_ii(f = md5_ii(f = md5_ii(f = md5_hh(f = md5_hh(f = md5_hh(f = md5_hh(f = md5_gg(f = md5_gg(f = md5_gg(f = md5_gg(f = md5_ff(f = md5_ff(f = md5_ff(f = md5_ff(f, r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 0], 7, -680876936), f, r, d[n + 1], 12, -389564586), m, f, d[n + 2], 17, 606105819), i, m, d[n + 3], 22, -1044525330), r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 4], 7, -176418897), f, r, d[n + 5], 12, 1200080426), m, f, d[n + 6], 17, -1473231341), i, m, d[n + 7], 22, -45705983), r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 8], 7, 1770035416), f, r, d[n + 9], 12, -1958414417), m, f, d[n + 10], 17, -42063), i, m, d[n + 11], 22, -1990404162), r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 12], 7, 1804603682), f, r, d[n + 13], 12, -40341101), m, f, d[n + 14], 17, -1502002290), i, m, d[n + 15], 22, 1236535329), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 1], 5, -165796510), f, r, d[n + 6], 9, -1069501632), m, f, d[n + 11], 14, 643717713), i, m, d[n + 0], 20, -373897302), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 5], 5, -701558691), f, r, d[n + 10], 9, 38016083), m, f, d[n + 15], 14, -660478335), i, m, d[n + 4], 20, -405537848), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 9], 5, 568446438), f, r, d[n + 14], 9, -1019803690), m, f, d[n + 3], 14, -187363961), i, m, d[n + 8], 20, 1163531501), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 13], 5, -1444681467), f, r, d[n + 2], 9, -51403784), m, f, d[n + 7], 14, 1735328473), i, m, d[n + 12], 20, -1926607734), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 5], 4, -378558), f, r, d[n + 8], 11, -2022574463), m, f, d[n + 11], 16, 1839030562), i, m, d[n + 14], 23, -35309556), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 1], 4, -1530992060), f, r, d[n + 4], 11, 1272893353), m, f, d[n + 7], 16, -155497632), i, m, d[n + 10], 23, -1094730640), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 13], 4, 681279174), f, r, d[n + 0], 11, -358537222), m, f, d[n + 3], 16, -722521979), i, m, d[n + 6], 23, 76029189), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 9], 4, -640364487), f, r, d[n + 12], 11, -421815835), m, f, d[n + 15], 16, 530742520), i, m, d[n + 2], 23, -995338651), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 0], 6, -198630844), f, r, d[n + 7], 10, 1126891415), m, f, d[n + 14], 15, -1416354905), i, m, d[n + 5], 21, -57434055), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 12], 6, 1700485571), f, r, d[n + 3], 10, -1894986606), m, f, d[n + 10], 15, -1051523), i, m, d[n + 1], 21, -2054922799), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 8], 6, 1873313359), f, r, d[n + 15], 10, -30611744), m, f, d[n + 6], 15, -1560198380), i, m, d[n + 13], 21, 1309151649), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 4], 6, -145523070), f, r, d[n + 11], 10, -1120210379), m, f, d[n + 2], 15, 718787259), i, m, d[n + 9], 21, -343485551), m = safe_add(m, h), f = safe_add(f, t), r = safe_add(r, g), i = safe_add(i, e)
    }
    return Array(m, f, r, i)
}

function md5_cmn(d, _, m, f, r, i) { return safe_add(bit_rol(safe_add(safe_add(_, d), safe_add(f, i)), r), m) }

function md5_ff(d, _, m, f, r, i, n) { return md5_cmn(_ & m | ~_ & f, d, _, r, i, n) }

function md5_gg(d, _, m, f, r, i, n) { return md5_cmn(_ & f | m & ~f, d, _, r, i, n) }

function md5_hh(d, _, m, f, r, i, n) { return md5_cmn(_ ^ m ^ f, d, _, r, i, n) }

function md5_ii(d, _, m, f, r, i, n) { return md5_cmn(m ^ (_ | ~f), d, _, r, i, n) }

function safe_add(d, _) { var m = (65535 & d) + (65535 & _); return (d >> 16) + (_ >> 16) + (m >> 16) << 16 | 65535 & m }

function bit_rol(d, _) { return d << _ | d >>> 32 - _ }

function expSyncCountries(acts, callBack){
  $.ajax({
      type: "POST",
      url: '/api/setExplorerLeaderboards.php',
      data: {
        d: JSON.stringify(acts.filter(function(d){
            return d.tiles && d.tiles.length > 0 && !d.deleted
          }).map(function(d){
              return{ i: d.i, t: compress(d.tiles.map(function(e){ return [e.x, e.y]}), 0, 2)
            }
          })
        )
      },
      success: function(result) {
          var j = result.parseJSON();
          if (j.status == 'success' && typeof(callBack) !== 'undefined') {
              callBack(j.data);
          }
      }
  });
}

function expSyncTiles(acts){
  if(isPRO && isLoggedInUserExplorerPublic){
    var prospective = acts.filter(function(d){
      //only activities with spatial data flagged as not private or deleted
      return d.tiles && d.tiles.length > 0 && d.p == 0 && !d.deleted
    }).map(function(d){
      var s = {};
      setExplorer(s, [d]);
      return {
        i: d.i,
        t: d.t.split(' ')[0],
        y: d.y,
        s: s.explorerMax,
        c: s.explorerClumpMax,
        e: compress(d.tiles.map(function(e){return [e.x, e.y]}), 0, 2),
        m: expGetTilesHash(d.tiles)
      }
    });

    expGetAthleteTiles(prospective);
  }
}

function expProcessTiles(prospective, existing){
  var inserts = expGetDifferenceById(prospective, existing);
  if(inserts.length > 0)  expSaveAthleteTiles('INS', inserts);
  var updates = expGetDifferenceByMd5(prospective, existing);
  if(updates.length > 0)  expSaveAthleteTiles('UPD', updates);
  var deletes = expGetDifferenceById(existing, prospective);
  // delay deletion to allow for any updates to be saved first
  if(deletes.length > 0)  window.setTimeout(function() { expDelAthleteTiles(deletes); }, 1000);
}

function expGetAthleteTiles(prospective){
  $.ajax({
      url: '/api/getAthleteTiles.php',
      success: function(result) {
          var j = result.parseJSON();
          if (j.status == 'success') {
              expProcessTiles(prospective, j.data);
          } else {
              alert('Whoops, something has gone wrong.');
          }
      }
  });
}

function expSaveAthleteTiles(action, a){
  $.ajax({
      type: "POST",
      url: '/api/saveAthleteTiles.php',
      data: {
        act: action,
        d: JSON.stringify(a)
      }
  });
}

function expDelAthleteTiles(a){
  $.ajax({
      type: "POST",
      url: '/api/deleteAthleteTiles.php',
      data: {
        d: JSON.stringify(a.map(function(d){
          return {
            i: d.i
          }
        }))
      }
  });
}

function expGetDifferenceById(a,b){
  return a.filter(function(o1){
    return !b.some(function(o2){
        return o1.i == o2.i;
    });
  });
}

function expGetDifferenceByMd5(a,b){
  return a.filter(function(o1){
    return b.some(function(o2){
        return o1.i == o2.i && o1.m !== o2.m;
    });
  });
}

function expGetTilesHash(tiles){
  return MD5(tiles.sort(
      function(a, b) {
        return (a.x * 1000000 + a.y) - (b.x * 1000000 + b.y)
  }).map(
    function(e){
      return [e.x, + e.y]
    }
  ).toString());
}

var refreshSessionInterval = null,
refreshSessionModal = null,
refreshSessionModalAllowClose = false;
function refreshSessionCall() {
    $.ajax({
        url: '/api/refreshSession.php',
        success: function(result) {
            var j = result.parseJSON();
            if (+j.loggedInAthleteId == +loggedInAthleteId) {
                if (refreshSessionModal != null) {
                    $('#refreshSessionModal').modal('hide');
                }
                return;
            }
            if (refreshSessionModal == null) {
                refreshSessionModal = d3.select('body').append('div').attr('id', 'refreshSessionModal')
                    .attr('class', 'modal').attr('tabindex', '-1').attr('role', 'dialog').attr('aria-labelledby', 'refreshSessionModalLabel').attr('aria-hidden', 'true')
                    .html('<div class="modal-dialog" role="document"><div class="modal-content"><div class="modal-header"><h5 class="modal-title" id="refreshSessionModalLabel">Session has expired</h5></div><div class="modal-footer"><button id="refreshSessionModalRefreshButton" type="button" class="btn btn-primary">Reload</button></div></div></div>');
            }
            d3.select('#refreshSessionModalRefreshButton').on('click', function() {
                $('#refreshSessionModal').modal('hide');
                location.reload();
            });
            if (!refreshSessionModalAllowClose) {
                $('#refreshSessionModal').modal({'backdrop': 'static', 'keyboard': false});
            }
            $('#refreshSessionModal').modal('show'); 
        }
    });   
}
function startRefreshSession(allowClose) {
    refreshSessionModalAllowClose = typeof(allowClose) !== 'undefined' ? allowClose : false;
    if (refreshSessionInterval != null) {
        clearInterval(refreshSessionInterval);
    }
    refreshSessionInterval = setInterval(refreshSessionCall, 1000 * 60 * 5); // 5 minutes
    // when the document is focused, refreshSessionCall and restart the interval
    window.addEventListener('focus', function() {
        refreshSessionCall();
        //refreshSessionInterval = setInterval(refreshSessionCall, 60000);
    });
    // when the document is blurred, clear the interval
    window.addEventListener('blur', function() {
        //clearInterval(refreshSessionInterval);
        //refreshSessionInterval = null;
    });
}