Skip to content
Snippets Groups Projects
Select Git revision
  • l10n_master
  • master default protected
  • test
  • build
  • proxim
  • getdevices
  • patch-2
  • patch-1
  • v0.7.4
  • v0.7.3
  • v0.7.3-2-nightly
  • v0.7.3-1-nightly
  • v0.7.2
  • v0.7.0
  • v0.6.9
  • v0.6.8
  • v0.6.7
  • v0.6.5
  • v0.6.4
  • v0.6.3
  • v0.6.2
  • v0.6.1
  • v0.6.0
  • v0.5.11
  • v0.5.10
  • v0.5.8
  • v0.5.4
  • v0.5.3
28 results

phonetrack.js

Code owners
Assign users and groups as approvers for specific file changes. Learn more.
phonetrack.js 280.02 KiB
/*jshint esversion: 6 */
/**
 * Nextcloud - PhoneTrack
 *
 *
 * This file is licensed under the Affero General Public License version 3 or
 * later. See the COPYING file.
 *
 * @author Julien Veyssier <eneiluj@posteo.net>
 * @copyright Julien Veyssier 2017
 */
(function ($, OC) {
    'use strict';

    //////////////// VAR DEFINITION /////////////////////

    var colorCodeBright = [
        '#ff0000',
        '#00ffff',
        '#800080',
        '#00ff00',
        '#ffff00',
        '#ffa500',
        '#0000ff',
        '#a52a2a',
        '#7fff00',
        '#dc143c',
        '#ff1493',
        '#ffd700'
    ];
    var colorCodePastel = [
        '#ACD941',
        '#C5B4CC',
        '#FFB904',
        '#FF7679',
        '#FFBEAF',
        '#94C6F8',
        '#EF3F3D',
        '#6B8200',
        '#FFA100',
        '#979cf7',
        '#fca2ab',
        '#d8fca2',
        '#77AFFF',
        '#a2fcf3',
        '#857BA7',
        '#c6a2fc'
    ];
    var colorCodeDark = [
        '#004081',
        '#634733',
        '#6D2403',
        '#3A240A',
        '#293A2E',
        '#400D31',
        '#424437',
        '#1E0E15'
    ];


    var lastColorUsed = -1;

    var phonetrack = {
        map: {},
        baseLayers: null,
        overlayLayers: null,
        restoredTileLayer: null,
        // indexed by session name, contains dict indexed by deviceid
        sessionLineLayers: {},
        // just the positions (the displayed ones, filtered, with the cut : list of lists)
        sessionDisplayedLatlngs: {},
        // just the positions (non-filtered)
        sessionLatlngs: {},
        // the featureGroups of line points
        sessionPointsLayers: {},
        // the same line points but indexed by their ID
        sessionPointsLayersById: {},
        sessionPointsEntriesById: {},
        // the last position markers
        sessionMarkerLayers: {},
        sessionColors: {},
        sessionShapes: {},
        currentRefreshAjax: null,
        currentTimer: null,
        // remember the oldest and newest point of each device
        lastTime: {},
        firstTime: {},
        lastZindex: 1000,
        movepointSession: null,
        movepointDevice: null,
        movepointId: null,
        // to avoid checking the dom too many times
        isSessionShared: {},
        // indexed by token, then by deviceid
        deviceNames: {},
        deviceAliases: {},
        devicePointIcons: {},
        // indexed by token, then by devicename
        deviceIds: {},
        filtersEnabled: false,
        filterValues: {},
        NSEWClick: {},
    };

    var offset = L.point(-7, 0);

    var hoverStyle = {
        weight: 12,
        opacity: 0.7,
        color: 'black'
    };
    var defaultStyle = {
        weight: 5,
        opacity: 1
    };

    var symbolSelectClasses = {
        'Dot, White': 'dot-select',
        'Pin, Blue': 'pin-blue-select',
        'Pin, Green': 'pin-green-select',
        'Pin, Red': 'pin-red-select',
        'Flag, Green': 'flag-green-select',
        'Flag, Red': 'flag-red-select',
        'Flag, Blue': 'flag-blue-select',
        'Block, Blue': 'block-blue-select',
        'Block, Green': 'block-green-select',
        'Block, Red': 'block-red-select',
        'Blue Diamond': 'diamond-blue-select',
        'Green Diamond': 'diamond-green-select',
        'Red Diamond': 'diamond-red-select',
        'Residence': 'residence-select',
        'Drinking Water': 'drinking-water-select',
        'Trail Head': 'hike-select',
        'Bike Trail': 'bike-trail-select',
        'Campground': 'campground-select',
        'Bar': 'bar-select',
        'Skull and Crossbones': 'skullcross-select',
        'Geocache': 'geocache-select',
        'Geocache Found': 'geocache-open-select',
        'Medical Facility': 'medical-select',
        'Contact, Alien': 'contact-alien-select',
        'Contact, Big Ears': 'contact-bigears-select',
        'Contact, Female3': 'contact-female3-select',
        'Contact, Cat': 'contact-cat-select',
        'Contact, Dog': 'contact-dog-select',
    };

    var symbolIcons = {
        'Dot, White': L.divIcon({
                iconSize: L.point(7,7),
        }),
        'Pin, Blue': L.divIcon({
            className: 'pin-blue',
            iconAnchor: [5, 30]
        }),
        'Pin, Green': L.divIcon({
            className: 'pin-green',
            iconAnchor: [5, 30]
        }),
        'Pin, Red': L.divIcon({
            className: 'pin-red',
            iconAnchor: [5, 30]
        }),
        'Flag, Green': L.divIcon({
            className: 'flag-green',
            iconAnchor: [1, 25]
        }),
        'Flag, Red': L.divIcon({
            className: 'flag-red',
            iconAnchor: [1, 25]
        }),
        'Flag, Blue': L.divIcon({
            className: 'flag-blue',
            iconAnchor: [1, 25]
        }),
        'Block, Blue': L.divIcon({
            className: 'block-blue',
            iconAnchor: [8, 8]
        }),
        'Block, Green': L.divIcon({
            className: 'block-green',
            iconAnchor: [8, 8]
        }),
        'Block, Red': L.divIcon({
            className: 'block-red',
            iconAnchor: [8, 8]
        }),
        'Blue Diamond': L.divIcon({
            className: 'diamond-blue',
            iconAnchor: [9, 9]
        }),
        'Green Diamond': L.divIcon({
            className: 'diamond-green',
            iconAnchor: [9, 9]
        }),
        'Red Diamond': L.divIcon({
            className: 'diamond-red',
            iconAnchor: [9, 9]
        }),
        'Residence': L.divIcon({
            className: 'residence',
            iconAnchor: [12, 12]
        }),
        'Drinking Water': L.divIcon({
            className: 'drinking-water',
            iconAnchor: [12, 12]
        }),
        'Trail Head': L.divIcon({
            className: 'hike',
            iconAnchor: [12, 12]
        }),
        'Bike Trail': L.divIcon({
            className: 'bike-trail',
            iconAnchor: [12, 12]
        }),
        'Campground': L.divIcon({
            className: 'campground',
            iconAnchor: [12, 12]
        }),
        'Bar': L.divIcon({
            className: 'bar',
            iconAnchor: [10, 12]
        }),
        'Skull and Crossbones': L.divIcon({
            className: 'skullcross',
            iconAnchor: [12, 12]
        }),
        'Geocache': L.divIcon({
            className: 'geocache',
            iconAnchor: [11, 10]
        }),
        'Geocache Found': L.divIcon({
            className: 'geocache-open',
            iconAnchor: [11, 10]
        }),
        'Medical Facility': L.divIcon({
            className: 'medical',
            iconAnchor: [13, 11]
        }),
        'Contact, Alien': L.divIcon({
            className: 'contact-alien',
            iconAnchor: [12, 12]
        }),
        'Contact, Big Ears': L.divIcon({
            className: 'contact-bigears',
            iconAnchor: [12, 12]
        }),
        'Contact, Female3': L.divIcon({
            className: 'contact-female3',
            iconAnchor: [12, 12]
        }),
        'Contact, Cat': L.divIcon({
            className: 'contact-cat',
            iconAnchor: [12, 12]
        }),
        'Contact, Dog': L.divIcon({
            className: 'contact-dog',
            iconAnchor: [12, 12]
        }),
    };

    var METERSTOMILES = 0.0006213711;
    var METERSTOFOOT = 3.28084;
    var METERSTONAUTICALMILES = 0.000539957;

    //////////////// UTILS /////////////////////

    function pad(n) {
        return (n < 10) ? ('0' + n) : n;
    }

    function endsWith(str, suffix) {
        return str.indexOf(suffix, str.length - suffix.length) !== -1;
    }

    function basename(str) {
        var base = String(str).substring(str.lastIndexOf('/') + 1);
        if (base.lastIndexOf(".") !== -1) {
            base = base.substring(0, base.lastIndexOf("."));
        }
        return base;
    }

    function hexToRgb(hex) {
        var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
        return result ? {
            r: parseInt(result[1], 16),
            g: parseInt(result[2], 16),
            b: parseInt(result[3], 16)
        } : null;
    }

    function componentToHex(c) {
        var hex = c.toString(16);
        return hex.length == 1 ? "0" + hex : hex;
    }

    function rgbToHex(r, g, b) {
        return "#" + componentToHex(parseInt(r)) + componentToHex(parseInt(g)) + componentToHex(parseInt(b));
    }

    function hexToDarkerHex(hex) {
        var rgb = hexToRgb(hex);
        while (getColorBrightness(rgb) > 100) {
            if (rgb.r > 0) rgb.r--;
            if (rgb.g > 0) rgb.g--;
            if (rgb.b > 0) rgb.b--;
        }
        return rgbToHex(rgb.r, rgb.g, rgb.b);
    }

    // this formula was found here : https://stackoverflow.com/a/596243/7692836
    function getColorBrightness(rgb) {
        return 0.2126*rgb.r + 0.7152*rgb.g + 0.0722*rgb.b;
    }

    function brify(str, linesize) {
        var res = '';
        var words = str.split(' ');
        var cpt = 0;
        var toAdd = '';
        for (var i=0; i<words.length; i++) {
            if ((cpt + words[i].length) < linesize) {
                toAdd += words[i] + ' ';
                cpt += words[i].length + 1;
            }
            else{
                res += toAdd + '<br/>';
                toAdd = words[i] + ' ';
                cpt = words[i].length + 1;
            }
        }
        res += toAdd;
        return res;
    }

    function Timer(callback, delay) {
        var timerId, start, remaining = delay;

        this.pause = function() {
            window.clearTimeout(timerId);
            remaining -= new Date() - start;
        };

        this.resume = function() {
            start = new Date();
            window.clearTimeout(timerId);
            timerId = window.setTimeout(callback, remaining);
        };
        this.resume();
    }

    function toDegreesMinutesAndSeconds(coordinate) {
        var absolute = Math.abs(coordinate);
        var degrees = Math.floor(absolute);
        var minutesNotTruncated = (absolute - degrees) * 60;
        var minutes = Math.floor(minutesNotTruncated);
        var seconds = Math.floor((minutesNotTruncated - minutes) * 60);

        return degrees + "°" + minutes + "'" + seconds + escapeHTML('"');
    }

    function convertDMS(lat, lng) {
        var latitude = toDegreesMinutesAndSeconds(lat);
        var latitudeCardinal = Math.sign(lat) >= 0 ? 'N' : 'S';

        var longitude = toDegreesMinutesAndSeconds(lng);
        var longitudeCardinal = Math.sign(lng) >= 0 ? 'E' : 'W';

        return latitude + ' ' + latitudeCardinal + ' ' + longitude + ' ' + longitudeCardinal;
    }

    //////////////// MAP /////////////////////

    function load_map() {
        // change meta to send referrer
        // usefull for IGN tiles authentication !
        $('meta[name=referrer]').attr('content', 'origin');

        var layer = getUrlParameter('layer');
        var default_layer = 'OpenStreetMap';
        if (phonetrack.restoredTileLayer !== null) {
            default_layer = phonetrack.restoredTileLayer;
        }
        else if (typeof layer !== 'undefined') {
            default_layer = layer;
        }

        var baseLayers = {};

        // add base layers
        $('#basetileservers li[type=tile]').each(function() {
            var sname = $(this).attr('name');
            var surl = $(this).attr('url');
            var minz = parseInt($(this).attr('minzoom'));
            var maxz = parseInt($(this).attr('maxzoom'));
            var sattrib = $(this).attr('attribution');
            var stransparent = ($(this).attr('transparent') === 'true');
            var sopacity = $(this).attr('opacity');
            if (typeof sopacity !== typeof undefined && sopacity !== false && sopacity !== '') {
                sopacity = parseFloat(sopacity);
            }
            else {
                sopacity = 1;
            }
            baseLayers[sname] = new L.TileLayer(surl, {minZoom: minz, maxZoom: maxz, attribution: sattrib, opacity: sopacity, transparent: stransparent});
        });
        $('#basetileservers li[type=tilewms]').each(function() {
            var sname = $(this).attr('name');
            var surl = $(this).attr('url');
            var slayers = $(this).attr('layers') || '';
            var sversion = $(this).attr('version') || '1.1.1';
            var stransparent = ($(this).attr('transparent') === 'true');
            var sformat = $(this).attr('format') || 'image/png';
            var sopacity = $(this).attr('opacity');
            if (typeof sopacity !== typeof undefined && sopacity !== false && sopacity !== '') {
                sopacity = parseFloat(sopacity);
            }
            else {
                sopacity = 1;
            }
            var sattrib = $(this).attr('attribution') || '';
            baseLayers[sname] = new L.tileLayer.wms(surl, {layers: slayers, version: sversion, transparent: stransparent, opacity: sopacity, format: sformat, attribution: sattrib});
        });
        // add custom layers
        $('#tileserverlist li').each(function() {
            var sname = $(this).attr('servername');
            var surl = $(this).attr('url');
            var sminzoom = $(this).attr('minzoom') || '1';
            var smaxzoom = $(this).attr('maxzoom') || '20';
            var sattrib = $(this).attr('attribution') || '';
            baseLayers[sname] = new L.TileLayer(surl,
                    {minZoom: sminzoom, maxZoom: smaxzoom, attribution: sattrib});
        });
        $('#tilewmsserverlist li').each(function() {
            var sname = $(this).attr('servername');
            var surl = $(this).attr('url');
            var sminzoom = $(this).attr('minzoom') || '1';
            var smaxzoom = $(this).attr('maxzoom') || '20';
            var slayers = $(this).attr('layers') || '';
            var sversion = $(this).attr('version') || '1.1.1';
            var sformat = $(this).attr('format') || 'image/png';
            var sattrib = $(this).attr('attribution') || '';
            baseLayers[sname] = new L.tileLayer.wms(surl,
                    {format: sformat, version: sversion, layers: slayers, minZoom: sminzoom, maxZoom: smaxzoom, attribution: sattrib});
        });
        phonetrack.baseLayers = baseLayers;

        var baseOverlays = {};

        // add base overlays
        $('#basetileservers li[type=overlay]').each(function() {
            var sname = $(this).attr('name');
            var surl = $(this).attr('url');
            var minz = parseInt($(this).attr('minzoom'));
            var maxz = parseInt($(this).attr('maxzoom'));
            var sattrib = $(this).attr('attribution');
            var stransparent = ($(this).attr('transparent') === 'true');
            var sopacity = $(this).attr('opacity');
            if (typeof sopacity !== typeof undefined && sopacity !== false && sopacity !== '') {
                sopacity = parseFloat(sopacity);
            }
            else {
                sopacity = 0.4;
            }
            baseOverlays[sname] = new L.TileLayer(surl, {minZoom: minz, maxZoom: maxz, attribution: sattrib, opacity: sopacity, transparent: stransparent});
        });
        $('#basetileservers li[type=overlaywms]').each(function() {
            var sname = $(this).attr('name');
            var surl = $(this).attr('url');
            var slayers = $(this).attr('layers') || '';
            var sversion = $(this).attr('version') || '1.1.1';
            var stransparent = ($(this).attr('transparent') === 'true');
            var sopacity = $(this).attr('opacity');
            if (typeof sopacity !== typeof undefined && sopacity !== false && sopacity !== '') {
                sopacity = parseFloat(sopacity);
            }
            else {
                sopacity = 0.4;
            }
            var sformat = $(this).attr('format') || 'image/png';
            var sattrib = $(this).attr('attribution') || '';
            baseOverlays[sname] = new L.tileLayer.wms(surl, {layers: slayers, version: sversion, transparent: stransparent, opacity: sopacity, format: sformat, attribution: sattrib});
        });
        // add custom overlays
        $('#overlayserverlist li').each(function() {
            var sname = $(this).attr('servername');
            var surl = $(this).attr('url');
            var sminzoom = $(this).attr('minzoom') || '1';
            var smaxzoom = $(this).attr('maxzoom') || '20';
            var stransparent = ($(this).attr('transparent') === 'true');
            var sopacity = $(this).attr('opacity');
            if (typeof sopacity !== typeof undefined && sopacity !== false && sopacity !== '') {
                sopacity = parseFloat(sopacity);
            }
            else {
                sopacity = 0.4;
            }
            var sattrib = $(this).attr('attribution') || '';
            baseOverlays[sname] = new L.TileLayer(surl,
                    {minZoom: sminzoom, maxZoom: smaxzoom, transparent: stransparent, opcacity: sopacity, attribution: sattrib});
        });
        $('#overlaywmsserverlist li').each(function() {
            var sname = $(this).attr('servername');
            var surl = $(this).attr('url');
            var sminzoom = $(this).attr('minzoom') || '1';
            var smaxzoom = $(this).attr('maxzoom') || '20';
            var slayers = $(this).attr('layers') || '';
            var sversion = $(this).attr('version') || '1.1.1';
            var sformat = $(this).attr('format') || 'image/png';
            var stransparent = ($(this).attr('transparent') === 'true');
            var sopacity = $(this).attr('opacity');
            if (typeof sopacity !== typeof undefined && sopacity !== false && sopacity !== '') {
                sopacity = parseFloat(sopacity);
            }
            else {
                sopacity = 0.4;
            }
            var sattrib = $(this).attr('attribution') || '';
            baseOverlays[sname] = new L.tileLayer.wms(surl, {layers: slayers, version: sversion, transparent: stransparent, opacity: sopacity, format: sformat, attribution: sattrib, minZoom: sminzoom, maxZoom: smaxzoom});
        });
        phonetrack.overlayLayers = baseOverlays;

        phonetrack.map = new L.Map('map', {
            zoomControl: true
        });

        var notificationText = '<div id="loadingnotification"><i class="fa fa-spinner fa-pulse fa-3x fa-fw display"></i><b id="loadingpc"></b></div>';
        phonetrack.notificationDialog = L.control.dialog({
            anchor: [0, -65],
            position: 'topright',
            //minSize: [70, 70],
            //maxSize: [70, 70],
            size: [55, 55]
        })
        .setContent(notificationText);

        L.control.scale({metric: true, imperial: true, position: 'topleft'})
        .addTo(phonetrack.map);

        L.control.mousePosition().addTo(phonetrack.map);
        phonetrack.locateControl = L.control.locate({setView: false, locateOptions: {enableHighAccuracy: true}});
        phonetrack.locateControl.addTo(phonetrack.map);
        phonetrack.map.on('locationfound', locationFound);
        var linearcolor = '#FF0080';
        if (OCA.Theming) {
            linearcolor = OCA.Theming.color;
        }
        phonetrack.map.addControl(new L.Control.LinearMeasurement({
            unitSystem: 'metric',
            color: linearcolor,
            type: 'line'
        }));
        L.control.sidebar('sidebar').addTo(phonetrack.map);

        phonetrack.map.setView(new L.LatLng(27, 5), 3);

        if (! baseLayers.hasOwnProperty(default_layer)) {
            default_layer = 'OpenStreetMap';
        }
        phonetrack.map.addLayer(baseLayers[default_layer]);

        phonetrack.activeLayers = L.control.activeLayers(baseLayers, baseOverlays);
        phonetrack.activeLayers.addTo(phonetrack.map);

        //phonetrack.map.on('contextmenu',rightClick);
        //phonetrack.map.on('popupclose',function() {});
        //phonetrack.map.on('viewreset',updateTrackListFromBounds);
        //phonetrack.map.on('dragend',updateTrackListFromBounds);
        //phonetrack.map.on('moveend', updateTrackListFromBounds);
        //phonetrack.map.on('zoomend', updateTrackListFromBounds);
        //phonetrack.map.on('baselayerchange', updateTrackListFromBounds);
        if (! pageIsPublic()) {
            phonetrack.map.on('baselayerchange', saveOptionTileLayer);
        }

        phonetrack.moveButton = L.easyButton({
            position: 'bottomright',
            states: [{
                stateName: 'nomove',
                icon:      'fa networkicon',
                title:     t('phonetrack', 'Show lines'),
                onClick: function(btn, map) {
                    $('#viewmove').click();
                    btn.state('move');
                }
            },{
                stateName: 'move',
                icon:      'fa networkicon nc-theming-main-background',
                title:     t('phonetrack', 'Hide lines'),
                onClick: function(btn, map) {
                    $('#viewmove').click();
                    btn.state('nomove');
                }
            }]
        });
        phonetrack.moveButton.addTo(phonetrack.map);

        if ($('#viewmove').is(':checked')) {
            phonetrack.moveButton.state('move');
        }
        else {
            phonetrack.moveButton.state('nomove');
        }

        phonetrack.zoomButton = L.easyButton({
            position: 'bottomright',
            states: [{
                stateName: 'nozoom',
                icon:      'fa autozoomicon',
                title:     t('phonetrack', 'Activate automatic zoom'),
                onClick: function(btn, map) {
                    $('#autozoom').click();
                    btn.state('zoom');
                }
            },{
                stateName: 'zoom',
                icon:      'fa autozoomicon nc-theming-main-background',
                title:     t('phonetrack', 'Disable automatic zoom'),
                onClick: function(btn, map) {
                    $('#autozoom').click();
                    btn.state('nozoom');
                }
            }]
        });
        phonetrack.zoomButton.addTo(phonetrack.map);

        if ($('#autozoom').is(':checked')) {
            phonetrack.zoomButton.state('zoom');
        }
        else {
            phonetrack.zoomButton.state('nozoom');
        }

        phonetrack.timeButton = L.easyButton({
            position: 'bottomright',
            states: [{
                stateName: 'noshowtime',
                icon:      'fa pointtooltipicon',
                title:     t('phonetrack', 'Show last point tooltip'),
                onClick: function(btn, map) {
                    $('#showtime').click();
                    btn.state('showtime');
                }
            },{
                stateName: 'showtime',
                icon:      'fa pointtooltipicon nc-theming-main-background',
                title:     t('phonetrack', 'Hide last point tooltip'),
                onClick: function(btn, map) {
                    $('#showtime').click();
                    btn.state('noshowtime');
                }
            }]
        });
        phonetrack.timeButton.addTo(phonetrack.map);

        if ($('#showtime').is(':checked')) {
            phonetrack.timeButton.state('showtime');
        }
        else {
            phonetrack.timeButton.state('noshowtime');
        }

        phonetrack.doZoomButton = L.easyButton({
            position: 'bottomright',
            states: [{
                stateName: 'no-importa',
                icon:      'fa normalzoomicon',
                title:     t('phonetrack', 'Zoom on all devices'),
                onClick: function(btn, map) {
                    zoomOnDisplayedMarkers();
                }
            }]
        });
        phonetrack.doZoomButton.addTo(phonetrack.map);
        $(phonetrack.doZoomButton.button).addClass('easy-button-inactive');
    }

    function enterMovePointMode() {
        $('.leaflet-container').css('cursor','crosshair');
        phonetrack.map.on('click', movePoint);
        OC.Notification.showTemporary(t('phonetrack', 'Click on the map to move the point, press ESC to cancel'));
    }

    function leaveMovePointMode() {
        $('.leaflet-container').css('cursor','grab');
        phonetrack.map.off('click', movePoint);
        phonetrack.movepointSession = null;
        phonetrack.movepointDevice = null;
        phonetrack.movepointId = null;
    }

    function movePoint(e) {
        var lat = e.latlng.lat;
        var lon = e.latlng.lng;
        var token = phonetrack.movepointSession;
        var deviceid = phonetrack.movepointDevice;
        var pid = phonetrack.movepointId;
        var entry = phonetrack.sessionPointsEntriesById[token][deviceid][pid];
        editPointDB(
            token,
            deviceid,
            pid,
            lat,
            lon,
            entry.altitude,
            entry.accuracy,
            entry.satellites,
            entry.batterylevel,
            entry.timestamp,
            entry.useragent,
            entry.speed,
            entry.bearing
        );
        leaveMovePointMode();
    }

    function dragPointEnd(e) {
        var m = e.target;
        var entry = phonetrack.sessionPointsEntriesById[m.session][m.device][m.pid];
        editPointDB(
            m.session,
            m.device,
            m.pid,
            m.getLatLng().lat,
            m.getLatLng().lng,
            entry.altitude,
            entry.accuracy,
            entry.satellites,
            entry.batterylevel,
            entry.timestamp,
            entry.useragent,
            entry.speed,
            entry.bearing
        );
    }

    function enterNSEWMode(but) {
        $('.leaflet-container').css('cursor','crosshair');
        var s = but.parent().parent().parent().parent().attr('token');
        var d = but.parent().parent().parent().parent().attr('device');
        var ne = but.hasClass('geonortheastbutton');
        phonetrack.NSEWClick = {s: s, d: d, ne: ne};
        phonetrack.map.on('click', NSEWClickMap);
    }

    function leaveNSEWMode() {
        $('.leaflet-container').css('cursor','grab');
        phonetrack.map.off('click', NSEWClickMap);
    }

    function NSEWClickMap(e) {
        var lat = e.latlng.lat;
        var lon = e.latlng.lng;
        while (lon < -180) {
            lon = lon + 360;
        }
        lat = lat.toFixed(6);
        lon = lon.toFixed(6);
        var s = phonetrack.NSEWClick.s;
        var d = phonetrack.NSEWClick.d;
        var ne = phonetrack.NSEWClick.ne;
        var geodiv = $('.session[token='+s+'] .devicelist li[device='+d+'] .addgeofencediv');
        if (ne) {
            geodiv.find('.fencenorth').val(lat);
            geodiv.find('.fenceeast').val(lon);
        }
        else {
            geodiv.find('.fencesouth').val(lat);
            geodiv.find('.fencewest').val(lon);
        }
        leaveNSEWMode();
    }

    function enterAddPointMode() {
        $('.leaflet-container').css('cursor','crosshair');
        phonetrack.map.on('click', addPointClickMap);
        $('#canceladdpoint').show();
        $('#explainaddpoint').show();
    }

    function leaveAddPointMode() {
        $('.leaflet-container').css('cursor','grab');
        phonetrack.map.off('click', addPointClickMap);
        $('#canceladdpoint').hide();
        $('#explainaddpoint').hide();
    }

    function addPointClickMap(e) {
        addPointDB(e.latlng.lat.toFixed(6), e.latlng.lng.toFixed(6), null, null, null, null, moment());
        leaveAddPointMode();
    }

    function deleteMultiplePoints(bounds=null) {
        var pid, pidlist, pidsToDelete, cpt, did, dname, layers, l, i;
        var s = $('#deletePointSession option:selected').attr('token');
        dname = $('#deletePointDevice').val();
        did = getDeviceId(s, dname);
        // if session is watched, if device exists, for all displayed points
        if ($('.session[token=' + s + '] .watchbutton i').hasClass('fa-toggle-on')) {
            if (dname === '') {
                for (did in phonetrack.sessionPointsLayers[s]) {
                    pidlist = [];
                    layers = phonetrack.sessionPointsLayers[s][did].getLayers();
                    for (i = 0; i < layers.length; i++) {
                        l = layers[i];
                        if (bounds === null || bounds.contains(l.getLatLng())) {
                            pidlist.push(l.getLatLng().alt);
                        }
                    }
                    // split pidlist in smaller parts
                    cpt = 0;
                    while (cpt < pidlist.length) {
                        pidsToDelete = [];
                        pidsToDelete.push(pidlist[cpt]);
                        cpt++;
                        // make bunch of 500 points
                        while (cpt < pidlist.length && cpt%500 !== 0) {
                            pidsToDelete.push(pidlist[cpt]);
                            cpt++;
                        }
                        deletePointsDB(s, did, pidsToDelete);
                    }
                }
            }
            else{
                if (phonetrack.sessionLineLayers[s].hasOwnProperty(did)) {
                    pidlist = [];
                    layers = phonetrack.sessionPointsLayers[s][did].getLayers();
                    for (i = 0; i < layers.length; i++) {
                        l = layers[i];
                        if (bounds === null || bounds.contains(l.getLatLng())) {
                            pidlist.push(l.getLatLng().alt);
                        }
                    }
                    // split pidlist in smaller parts
                    cpt = 0;
                    while (cpt < pidlist.length) {
                        pidsToDelete = [];
                        pidsToDelete.push(pidlist[cpt]);
                        cpt++;
                        // make bunch of 500 points
                        while (cpt < pidlist.length && cpt%500 !== 0) {
                            pidsToDelete.push(pidlist[cpt]);
                            cpt++;
                        }
                        deletePointsDB(s, did, pidsToDelete);
                    }
                }
            }
        }
    }

    /*
     * get key events
     */
    function checkKey(e) {
        e = e || window.event;
        var kc = e.keyCode;
        //console.log(kc);

        if (kc === 60 || kc === 220) {
            e.preventDefault();
            $('#sidebar').toggleClass('collapsed');
        }

        if (e.key === 'Escape' && phonetrack.movepointSession !== null) {
            leaveMovePointMode();
        }
    }

    function getUrlParameter(sParam)
    {
        var sPageURL = window.location.search.substring(1);
        var sURLVariables = sPageURL.split('&');
        for (var i = 0; i < sURLVariables.length; i++) 
        {
            var sParameterName = sURLVariables[i].split('=');
            if (sParameterName[0] === sParam) 
            {
                return decodeURIComponent(sParameterName[1]);
            }
        }
    }

    //////////////// ANIMATIONS /////////////////////

    function showLoadingAnimation() {
        phonetrack.notificationDialog.addTo(phonetrack.map);
        $('#loadingpc').text('');
    }

    function hideLoadingAnimation() {
        $('#loadingpc').text('');
        phonetrack.notificationDialog.remove();
    }

    //////////////// PUBLIC DIR/FILE /////////////////////

    function pageIsPublicWebLog() {
        return phonetrack.pageIsPublicWebLog;
    }

    function pageIsPublicSessionWatch() {
        return phonetrack.pageIsPublicSessionWatch;
    }

    function pageIsPublic() {
        return (pageIsPublicWebLog() || pageIsPublicSessionWatch());
    }

    //////////////// USER TILE SERVERS /////////////////////

    function addTileServer(type) {
        var sname = $('#'+type+'servername').val();
        var surl = $('#'+type+'serverurl').val();
        var sminzoom = $('#'+type+'minzoom').val();
        var smaxzoom = $('#'+type+'maxzoom').val();
        var stransparent = $('#'+type+'transparent').is(':checked');
        var sopacity = $('#'+type+'opacity').val() || '';
        var sformat = $('#'+type+'format').val() || '';
        var sversion = $('#'+type+'version').val() || '';
        var slayers = $('#'+type+'layers').val() || '';
        if (sname === '' || surl === '') {
            OC.Notification.showTemporary(
                t('phonetrack', 'Server name or server url should not be empty')
            );
            OC.Notification.showTemporary(
                t('phonetrack', 'Impossible to add tile server')
            );
            return;
        }
        if ($('#'+type+'serverlist ul li[servername="' + sname + '"]').length > 0) {
            OC.Notification.showTemporary(
                t('phonetrack', 'A server with this name already exists')
            );
            OC.Notification.showTemporary(
                t('phonetrack', 'Impossible to add tile server')
            );
            return;
        }
        $('#'+type+'servername').val('');
        $('#'+type+'serverurl').val('');

        var req = {
            servername: sname,
            serverurl: surl,
            type: type,
            layers: slayers,
            version: sversion,
            tformat: sformat,
            opacity: sopacity,
            transparent: stransparent,
            minzoom: sminzoom,
            maxzoom: smaxzoom,
            attribution: ''
        };
        var url = OC.generateUrl('/apps/phonetrack/addTileServer');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done) {
                $('#'+type+'serverlist ul').prepend(
                    '<li style="display:none;" servername="' + escapeHTML(sname || '') +
                    '" title="' + escapeHTML(surl || '') + '">' +
                    escapeHTML(sname || '') + ' <button>' +
                    '<i class="fa fa-trash" aria-hidden="true" style="color:red;"></i> ' +
                    t('phonetrack', 'Delete') +
                    '</button></li>'
                );
                $('#'+type+'serverlist ul li[servername="' + sname + '"]').fadeIn('slow');

                var newlayer;
                if (type === 'tile') {
                    // add tile server in leaflet control
                    newlayer = new L.TileLayer(surl,
                        {minZoom: sminzoom, maxZoom: smaxzoom, attribution: ''});
                    phonetrack.activeLayers.addBaseLayer(newlayer, sname);
                    phonetrack.baseLayers[sname] = newlayer;
                }
                else if (type === 'tilewms'){
                    // add tile server in leaflet control
                    newlayer = new L.tileLayer.wms(surl,
                        {format: sformat, version: sversion, layers: slayers, minZoom: sminzoom, maxZoom: smaxzoom, attribution: ''});
                    phonetrack.activeLayers.addBaseLayer(newlayer, sname);
                    phonetrack.overlayLayers[sname] = newlayer;
                }
                if (type === 'overlay') {
                    // add tile server in leaflet control
                    newlayer = new L.TileLayer(surl,
                        {minZoom: sminzoom, maxZoom: smaxzoom, transparent: stransparent, opcacity: sopacity, attribution: ''});
                    phonetrack.activeLayers.addOverlay(newlayer, sname);
                    phonetrack.baseLayers[sname] = newlayer;
                }
                else if (type === 'overlaywms'){
                    // add tile server in leaflet control
                    newlayer = new L.tileLayer.wms(surl,
                        {layers: slayers, version: sversion, transparent: stransparent, opacity: sopacity, format: sformat, attribution: '', minZoom: sminzoom, maxZoom: smaxzoom});
                    phonetrack.activeLayers.addOverlay(newlayer, sname);
                    phonetrack.overlayLayers[sname] = newlayer;
                }
                OC.Notification.showTemporary(t('phonetrack', 'Tile server "{ts}" has been added', {ts: sname}));
            }
            else{
                OC.Notification.showTemporary(t('phonetrack', 'Failed to add tile server "{ts}"', {ts: sname}));
            }
        }).always(function() {
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to add tile server'));
        });
    }

    function deleteTileServer(li, type) {
        var sname = li.attr('servername');
        var req = {
            servername: sname,
            type: type
        };
        var url = OC.generateUrl('/apps/phonetrack/deleteTileServer');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done) {
                li.fadeOut('slow', function() {
                    li.remove();
                });
                if (type === 'tile') {
                    var activeLayerName = phonetrack.activeLayers.getActiveBaseLayer().name;
                    // if we delete the active layer, first select another
                    if (activeLayerName === sname) {
                        $('input.leaflet-control-layers-selector').first().click();
                    }
                    phonetrack.activeLayers.removeLayer(phonetrack.baseLayers[sname]);
                    delete phonetrack.baseLayers[sname];
                }
                else {
                    phonetrack.activeLayers.removeLayer(phonetrack.overlayLayers[sname]);
                    delete phonetrack.overlayLayers[sname];
                }
                OC.Notification.showTemporary(t('phonetrack', 'Tile server "{ts}" has been deleted', {ts: sname}));
            }
            else{
                OC.Notification.showTemporary(t('phonetrack', 'Failed to delete tile server "{ts}"', {ts: sname}));
            }
        }).always(function() {
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to delete tile server'));
        });
    }

    //////////////// SAVE/RESTORE OPTIONS /////////////////////

    function restoreOptions() {
        var mom;
        var url = OC.generateUrl('/apps/phonetrack/getOptionsValues');
        var req = {
        };
        var optionsValues = {};
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            optionsValues = response.values;
            if (optionsValues) {
                var elem, tag, type, k;
                for (k in optionsValues) {
                    elem = $('#'+k);
                    tag = elem.prop('tagName');
                    if (k === 'linewidth') {
                        $('#'+k).val(optionsValues[k]);
                        $('#linewidthlabel').text(optionsValues[k]+'px');
                    }
                    else if (k === 'pointlinealpha') {
                        $('#'+k).val(optionsValues[k]);
                        $('#pointlinealphalabel').text(optionsValues[k]);
                    }
                    else if (k === 'pointradius') {
                        $('#'+k).val(optionsValues[k]);
                        $('#pointradiuslabel').text(optionsValues[k]+'px');
                    }
                    else if (k === 'tilelayer') {
                        phonetrack.restoredTileLayer = optionsValues[k];
                    }
                    else if (k === 'activeSessions') {
                        phonetrack.sessionsFromSavedOptions = $.parseJSON(optionsValues[k]);
                    }
                    else if (k === 'showsidebar') {
                        if (optionsValues[k] !== 'true') {
                            $('#sidebar').addClass('collapsed');
                            $('#sidebar li.active').removeClass('active');
                        }
                    }
                    else if (tag === 'SELECT') {
                        elem.val(optionsValues[k]);
                    }
                    else if (tag === 'INPUT') {
                        type = elem.attr('type');
                        if (type === 'date') {
                            if (optionsValues[k] !== null &&
                                optionsValues[k] !== ''
                            ) {
                                if (String(optionsValues[k]).match(/\d\d\d\d-\d\d-\d\d/g) !== null) {
                                    elem.val(optionsValues[k]);
                                }
                                else {
                                    try {
                                        mom = moment.unix(parseInt(optionsValues[k]));
                                        elem.val(mom.format('YYYY-MM-DD'));
                                    }
                                    catch(err) {
                                        elem.val('');
                                    }
                                }
                            }
                            else {
                                elem.val('');
                            }
                        }
                        else if (type === 'checkbox') {
                            elem.prop('checked', optionsValues[k] !== 'false');
                        }
                        else if (type === 'text' || type === 'number' || type === 'range') {
                            elem.val(optionsValues[k]);
                        }
                    }
                }
            }
            // quite important ;-)
            main();
        }).fail(function() {
            OC.Notification.showTemporary(
                t('phonetrack', 'Failed to contact server to restore options values')
            );
            OC.Notification.showTemporary(
                t('phonetrack', 'Reload this page')
            );
        });
    }

    function saveOptionTileLayer(refreshAfter=false) {
        saveOptions('tilelayer', refreshAfter);
    }

    function saveOptions(keyParam, refreshAfter=false) {
        var keys = keyParam;
        if (keys.constructor !== Array) {
            keys = [keyParam];
        }
        var i, key, value;
        var options = {};
        for (i = 0; i < keys.length; i++) {
            key = keys[i];
            if (key === 'tilelayer') {
                value = phonetrack.activeLayers.getActiveBaseLayer().name;
            }
            else if (key === 'showsidebar') {
                value = !$('#sidebar').hasClass('collapsed');
            }
            else if (key === 'activeSessions') {
                value = {};
                var devs, s, d, zoom, line, point;
                $('.session').each(function() {
                    s = $(this).attr('token');
                    if (isSessionActive(s)) {
                        value[s] = {};
                        $(this).find('.devicelist li').each(function() {
                            d = $(this).attr('device');
                            zoom = $(this).find('.toggleAutoZoomDevice').hasClass('on');
                            line = $(this).find('.toggleLineDevice').hasClass('on');
                            point = $(this).find('.toggleDetail').hasClass('on');
                            value[s][d] = {
                                zoom: zoom,
                                line: line,
                                point: point
                            };
                        });
                    }
                });
                value = JSON.stringify(value);
            }
            else {
                var elem = $('#'+key);
                var tag = elem.prop('tagName');
                var type = elem.attr('type');
                if (tag === 'SELECT' || (tag === 'INPUT' && (type === 'text' || type === 'number' || type === 'range'))) {
                    value = elem.val();
                }
                else if (tag === 'INPUT' && type === 'checkbox') {
                    value = elem.is(':checked');
                }
                else if (tag === 'INPUT' && type === 'date') {
                    if (elem.val() === '') {
                        value = '';
                    }
                    else {
                        value = moment(elem.val()).unix();
                    }
                }
            }
            options[key] = value;
        }

        if (!pageIsPublic()) {
            var req = {
                options: options
            };
            var url = OC.generateUrl('/apps/phonetrack/saveOptionValue');
            $.ajax({
                type: 'POST',
                url: url,
                data: req,
                async: true
            }).done(function (response) {
                if (refreshAfter === true) {
                    if (phonetrack.currentTimer !== null) {
                        phonetrack.currentTimer.pause();
                        phonetrack.currentTimer = null;
                    }
                    refresh();
                }
            }).fail(function() {
                OC.Notification.showTemporary(
                    t('phonetrack', 'Failed to contact server to save options values')
                );
                OC.Notification.showTemporary(
                    t('phonetrack', 'Reload this page')
                );
            });
        }
    }

    function addFiltersBookmarkDb(e) {
        var name = $('#filtername').val();
        if (name === '') {
            t('phonetrack', 'Filter bookmark should have a name');
            return;
        }
        var filters = {};
        $('#filterPointsTable input[type=date], #filterPointsTable input[type=number]').each(function () {
            var val = $(this).val();
            var id = $(this).attr('id');
            if (val !== '') {
                filters[id] = val;
            }
        });

        var req = {
            name: name,
            filters: JSON.stringify(filters),
        };
        var url = OC.generateUrl('/apps/phonetrack/addFiltersBookmark');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                addFiltersBookmark(name, filters, response.bookid);
            }
        }).fail(function() {
            OC.Notification.showTemporary(
                t('phonetrack', 'Failed to contact server to save filters bookmark')
            );
            OC.Notification.showTemporary(
                t('phonetrack', 'Reload this page')
            );
        });
    }

    function addFiltersBookmark(name, filters, bookid) {
        var f = filters;

        var li = '<li bookid="' + bookid + '" name="' + escapeHTML(name || '') + '" title="';
        for (var fname in f) {
            li = li + fname + ' : ' + f[fname] + '\n';
        }
        li = li + '">' +
            '<label class="booklabel">'+escapeHTML(name || '')+'</label>' +
            '<button class="applybookbutton"><i class="fa fa-filter"></i></button>' +
            '<button class="deletebookbutton"><i class="fa fa-trash"></i></button>' +
            '<p class="filterstxt" style="display:none;">' + JSON.stringify(filters) + '</p>' +
            '</li>';
        $('#filterbookmarks').append(li);
    }

    function deleteFiltersBookmarkDb(elem) {
        var name =   elem.parent().attr('name');
        var bookid = elem.parent().attr('bookid');

        var req = {
            bookid: bookid
        };
        var url = OC.generateUrl('/apps/phonetrack/deleteFiltersBookmark');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                $('#filterbookmarks li[bookid='+bookid+']').remove();
            }
        }).fail(function() {
            OC.Notification.showTemporary(
                t('phonetrack', 'Failed to contact server to delete filters bookmark')
            );
        });
    }

    function applyFiltersBookmark(elem) {
        var filterKeys = [];
        // reset filters
        $('#filterPointsTable input[type=date], #filterPointsTable input[type=number]').each(function () {
            $(this).val('');
            filterKeys.push($(this).attr('id'));
        });

        // apply
        var bname = elem.parent().attr('name');
        var filterstxt = elem.parent().find('.filterstxt').text();
        var f = $.parseJSON(filterstxt);
        for (var id in f) {
            $('#'+id).val(f[id]);
        }

        changeApplyFilter();
        // save filters in options
        saveOptions(filterKeys, $('#applyfilters').is(':checked'));
    }

    //////////////// SYMBOLS /////////////////////

    function fillWaypointStyles() {
        for (var st in symbolIcons) {
            $('select#waypointstyleselect').append('<option value="' + st + '">' + st + '</option>');
        }
        $('select#waypointstyleselect').val('Pin, Blue');
        updateWaypointStyle('Pin, Blue');
    }

    //////////////// SESSIONS ///////////////////

    function createSession() {
        var sessionName = $('#sessionnameinput').val();
        $('#sessionnameinput').val('');
        if (!sessionName) {
            OC.Notification.showTemporary(t('phonetrack', 'Session name should not be empty'));
            return;
        }
        var req = {
            name: sessionName
        };
        var url = OC.generateUrl('/apps/phonetrack/createSession');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                addSession(response.token, sessionName, response.publicviewtoken, [], 1);
            }
            else if (response.done === 2) {
                OC.Notification.showTemporary(t('phonetrack', 'Session name already used'));
            }
        }).always(function() {
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to create session'));
        });
    }

    function getSessionName(token) {
        return $('div.session[token="' + token + '"] .sessionBar .sessionName').text();
    }

    function getDeviceName(sessionid, did) {
        return phonetrack.deviceNames[sessionid][parseInt(did)];
    }

    function getDeviceAlias(sessionid, did) {
        return phonetrack.deviceAliases[sessionid][parseInt(did)];
    }

    function getDeviceId(sessionid, devicename) {
        return phonetrack.deviceIds[sessionid][devicename];
    }

    function addSession(token, name, publicviewtoken, isPublic, devices=[], sharedWith=[],
                        selected=false, isFromShare=false, isSharedBy='',
                        reservedNames=[], publicFilteredShares=[], autoexport='no', autopurge='no') {
        var i;
        // init names/ids dict
        phonetrack.deviceNames[token] = {};
        phonetrack.deviceAliases[token] = {};
        phonetrack.deviceIds[token] = {};
        phonetrack.devicePointIcons[token] = {};
        phonetrack.lastTime[token] = {};
        phonetrack.firstTime[token] = {};
        // if session is not shared (we have write access)
        if (!isFromShare) {
            $('#addPointSession').append('<option value="' + name + '" token="' + token + '">' + name + '</option>');
            $('#deletePointSession').append('<option value="' + name + '" token="' + token + '">' + name + '</option>');
        }
        var gpsloggerUrl = OC.generateUrl('/apps/phonetrack/log/gpslogger/' + token + '/yourname?');
        var gpsloggerParams = 'lat=%LAT&' +
            'lon=%LON&' +
            'sat=%SAT&' +
            'alt=%ALT&' +
            'acc=%ACC&' +
            'speed=%SPD&' +
            'bearing=%DIR&' +
            'timestamp=%TIMESTAMP&' +
            'bat=%BATT';
        gpsloggerUrl = window.location.origin + gpsloggerUrl + gpsloggerParams;

        var owntracksurl = OC.generateUrl('/apps/phonetrack/log/owntracks/' + token + '/yourname');
        owntracksurl = window.location.origin + owntracksurl;

        var uloggerurl = OC.generateUrl('/apps/phonetrack/log/ulogger/' + token + '/yourname');
        uloggerurl = window.location.origin + uloggerurl;

        var traccarurl = OC.generateUrl('/apps/phonetrack/log/traccar/' + token + '/yourname');
        traccarurl = window.location.origin + traccarurl;

        var opengtsurl = OC.generateUrl('/apps/phonetrack/log/opengts/' + token + '/yourname');
        opengtsurl = window.location.origin + opengtsurl;

        var locusmapurl = OC.generateUrl('/apps/phonetrack/log/locusmap/' + token + '/yourname');
        locusmapurl =window.location.origin + locusmapurl;

        var osmandurl = OC.generateUrl('/apps/phonetrack/log/osmand/' + token + '/yourname?');
        osmandurl = osmandurl +
            'lat={0}&' +
            'lon={1}&' +
            'alt={4}&' +
            'acc={3}&' +
            'timestamp={2}&' +
            'speed={5}&' +
            'bearing={6}';
        osmandurl = window.location.origin + osmandurl;

        var geturl = OC.generateUrl('/apps/phonetrack/logGet/' + token + '/yourname?');
        geturl = geturl +
            'lat=LAT&' +
            'lon=LON&' +
            'alt=ALT&' +
            'acc=ACC&' +
            'bat=BAT&' +
            'sat=SAT&' +
            'speed=SPD&' +
            'bearing=DIR&' +
            'timestamp=TIME';
        geturl = window.location.origin + geturl;

        var pl = $('#pubviewline').is(':checked') ? '1' : '0';
        var pp = $('#pubviewpoint').is(':checked') ? '1' : '0';
        var linePointParams = $.param({lineToggle: pl, pointToggle: pp});

        var publicTrackUrl = OC.generateUrl('/apps/phonetrack/publicWebLog/' + token + '/yourname?');
        publicTrackUrl = window.location.origin + publicTrackUrl + linePointParams;

        var publicWatchUrl = OC.generateUrl('/apps/phonetrack/publicSessionWatch/' + publicviewtoken + '?');
        publicWatchUrl = window.location.origin + publicWatchUrl + linePointParams;

        var APIUrl = OC.generateUrl('/apps/phonetrack/api/getlastpositions/' + publicviewtoken);
        APIUrl = window.location.origin + APIUrl;

        var watchicon = 'fa-toggle-off';
        if (selected) {
            watchicon = 'fa-toggle-on';
        }
        var divtxt = '<div class="session" token="' + token + '"' +
           ' publicviewtoken="' + publicviewtoken + '"' +
           ' shared="' + (isFromShare?1:0) + '"' +
            '>';
        phonetrack.isSessionShared[token] = isFromShare;
        divtxt = divtxt + '<div class="sessionBar">';
        divtxt = divtxt + '<button class="watchbutton" title="' + t('phonetrack', 'Watch this session') + '">' +
            '<i class="fa ' + watchicon + '" aria-hidden="true"></i></button>';

        var sharedByText = '';
        if (isSharedBy !== '') {
            sharedByText = ' (' +
                t('phonetrack', 'shared by {u}', {u: isSharedBy}) +
                ')';
        }
        divtxt = divtxt + '<div class="sessionName" title="' + name + sharedByText + '">' + name + '</div><input class="renameSessionInput" type="text"/>';
        if (!pageIsPublic()) {
            divtxt = divtxt + '<button class="dropdownbutton" title="'+t('phonetrack', 'More actions')+'">' +
                '<i class="fa fa-bars" aria-hidden="true"></i></button>';
        }
        divtxt = divtxt + ' <button class="zoomsession" ' +
            'title="' + t('phonetrack', 'Zoom on this session') + '">' +
            '<i class="fa fa-search"></i></button>';
        if (!pageIsPublic() && !isFromShare) {
            divtxt = divtxt + '<button class="sharesession" title="'+t('phonetrack', 'URL to share session')+'">' +
                '<i class="fa fa-share-alt" aria-hidden="true"></i></button>';
        }
        if (!pageIsPublicSessionWatch() && !isFromShare) {
            divtxt = divtxt + '<button class="moreUrlsButton" title="' + t('phonetrack', 'URLs for logging apps') + '">' +
                '<i class="fa fa-link"></i></button>';
        }
        if (!pageIsPublic() && !isFromShare) {
            divtxt = divtxt + '<button class="reservNameButton" title="' + t('phonetrack', 'Reserve device names') + '">' +
                '<i class="fa fa-male"></i></button>';
        }
        divtxt = divtxt + '</div>';
        if (!pageIsPublic()) {
            divtxt = divtxt + '<div class="dropdown-content">';

            if (!isFromShare) {
                divtxt = divtxt + '<button class="removeSession">' +
                    '<i class="fa fa-trash" aria-hidden="true"></i> ' + t('phonetrack', 'Delete session') + '</button>';
                divtxt = divtxt + '<button class="editsessionbutton">' +
                    '<i class="fa fa-pencil-alt"></i> ' + t('phonetrack', 'Rename session') + '</button>';
            }
            divtxt = divtxt + '<div><button class="export">' +
                '<i class="fa fa-save" aria-hidden="true"></i> ' + t('phonetrack', 'Export to gpx') + '</button>';
            divtxt = divtxt + '<input role="exportname" type="text" value="' + escapeHTML(name) + '.gpx"/></div>';

            if (!isFromShare) {
                divtxt = divtxt + '<div class="autoexportdiv" title="' +
                    t('phonetrack', 'Files are created in \'{exdir}\'', {exdir: escapeHTML($('#autoexportpath').val())}) + '">' +
                    '<div><i class="fa fa-save" aria-hidden="true"></i> ' + t('phonetrack', 'Automatic export') + '</div>';
                divtxt = divtxt + '<select role="autoexport">';
                divtxt = divtxt + '<option value="no">' + t('phonetrack', 'never') + '</option>';
                divtxt = divtxt + '<option value="daily">' + t('phonetrack', 'daily') + '</option>';
                divtxt = divtxt + '<option value="weekly">' + t('phonetrack', 'weekly') + '</option>';
                divtxt = divtxt + '<option value="monthly">' + t('phonetrack', 'monthly') + '</option>';
                divtxt = divtxt + '</select>';
                divtxt = divtxt + '</div>';

                divtxt = divtxt + '<div class="autopurgediv" ' +
                    'title="' + t('phonetrack', 'Automatic purge is triggered daily and will delete points older than selected duration') + '">' +
                    '<div><i class="fa fa-trash" aria-hidden="true"></i> ' + t('phonetrack', 'Automatic purge') + '</div>';
                divtxt = divtxt + '<select role="autopurge">';
                divtxt = divtxt + '<option value="no">' + t('phonetrack', 'don\'t purge') + '</option>';
                divtxt = divtxt + '<option value="day">' + t('phonetrack', 'a day') + '</option>';
                divtxt = divtxt + '<option value="week">' + t('phonetrack', 'a week') + '</option>';
                divtxt = divtxt + '<option value="month">' + t('phonetrack', 'a month') + '</option>';
                divtxt = divtxt + '</select>';
                divtxt = divtxt + '</div>';
            }

            divtxt = divtxt + '</div>';
        }
        if (!pageIsPublic() && !isFromShare) {
            divtxt = divtxt + '<div class="namereservdiv">';
            divtxt = divtxt + '<p class="information">' + t('phonetrack', 'Name reservation is optional.') + '<br/>' +
                t('phonetrack', 'Name can be set directly in logging URL if it is not reserved.') + '<br/>' +
                t('phonetrack', 'To log with a reserved name, use its token in logging URL.') + '<br/>' +
                t('phonetrack', 'If a name is reserved, the only way to log with this name is with its token.') +
                '</p>';

            divtxt = divtxt + '<label class="addnamereservLabel">' + t('phonetrack', 'Reserve this device name') + ' :</label>';
            divtxt = divtxt + '<input class="addnamereserv" type="text" title="' +
                t('phonetrack', 'Type reserved name and press \'Enter\'') + '"></input>';
            divtxt = divtxt + '<ul class="namereservlist">';
            for (i = 0; i < reservedNames.length; i++) {
                divtxt = divtxt + '<li name="' + escapeHTML(reservedNames[i].name) + '"><label>' +
                    reservedNames[i].name + ' : ' + reservedNames[i].token + '</label>' +
                    '<button class="deletereservedname"><i class="fa fa-trash"></i></li>';
            }
            divtxt = divtxt + '</ul>';
            divtxt = divtxt + '<hr/></div>';

            divtxt = divtxt + '<div class="sharediv">';

            divtxt = divtxt + '<div class="usersharediv">';
            divtxt = divtxt + '<p class="addusershareLabel">' + t('phonetrack', 'Share with user') + ' :</p>';
            divtxt = divtxt + '<input class="addusershare" type="text" title="' +
                t('phonetrack', 'Type user name and press \'Enter\'') + '"></input>';
            divtxt = divtxt + '<ul class="usersharelist">';

            for (i = 0; i < sharedWith.length; i++) {
                divtxt = divtxt + '<li username="' + escapeHTML(sharedWith[i]) + '"><label>' +
                    t('phonetrack', 'Shared with {u}', {'u': sharedWith[i]}) + '</label>' +
                    '<button class="deleteusershare"><i class="fa fa-trash"></i></li>';
            }
            divtxt = divtxt + '</ul>';
            divtxt = divtxt + '</div><hr/>';

            var titlePublic = t('phonetrack', 'A private session is not visible on public browser logging page');
            var icon = 'fa-toggle-off';
            var pubtext = t('phonetrack', 'Make session public');
            if (parseInt(isPublic) === 1) {
                icon = 'fa-toggle-on';
                pubtext = t('phonetrack', 'Make session private');
            }
            divtxt = divtxt + '<button class="publicsessionbutton" title="' + titlePublic + '">';
            divtxt = divtxt + '<i class="fa ' + icon + '"></i> <b>' + pubtext + '</b></button>';
            divtxt = divtxt + '<div class="publicWatchUrlDiv">';
            divtxt = divtxt + '<p class="publicWatchUrlLabel">' + t('phonetrack', 'Public watch URL') + ' :</p>';
            divtxt = divtxt + '<input class="ro" role="publicWatchUrl" type="text" value="' + publicWatchUrl + '"></input>';
            divtxt = divtxt + '<p class="APIUrlLabel">' + t('phonetrack', 'API URL (JSON last positions)') + ' :</p>';
            divtxt = divtxt + '<input class="ro" role="APIUrl" type="text" value="' + APIUrl + '"></input>';
            divtxt = divtxt + '</div><hr/>';

            divtxt = divtxt + '<div class="publicfilteredsharediv">';
            divtxt = divtxt + '<button class="addpublicfilteredshareButton" ' +
                'title="' + t('phonetrack', 'Current active filters will be applied on shared view') + '">' +
                '<i class="fa fa-plus-circle" aria-hidden="true"></i> ' +
                t('phonetrack', 'Add public filtered share') + '</button>';
            divtxt = divtxt + '<ul class="publicfilteredsharelist">';
            divtxt = divtxt + '</ul>';
            divtxt = divtxt + '</div>';

            divtxt = divtxt + '<hr/></div>';
        }
        if (!pageIsPublicSessionWatch() && !isFromShare) {
            divtxt = divtxt + '<div class="moreUrls">';
            divtxt = divtxt + '<p class="urlhint information">' +
                t('phonetrack', 'List of server URLs to configure logging apps.') + '<br/>' +
                t('phonetrack', 'Replace \'yourname\' with the desired device name or with the name reservation token') +
                '</p>';
            divtxt = divtxt + '<p><label>' + t('phonetrack', 'Public browser logging URL') + ' : </label>' +
                '<button class="urlhelpbutton" logger="publicTrack"><i class="fa fa-question"></i> <i class="fa fa-qrcode"></i></button>' +
                '</p>';
            divtxt = divtxt + '<input class="ro" role="publicTrackurl" type="text" value="' + publicTrackUrl + '"></input>';

            divtxt = divtxt + '<p><label>' + t('phonetrack', 'OsmAnd URL') + ' : </label>' +
                '<button class="urlhelpbutton" logger="osmand"><i class="fa fa-question"></i> <i class="fa fa-qrcode"></i></button>' +
                '</p>';
            divtxt = divtxt + '<input class="ro" role="osmandurl" type="text" value="' + osmandurl + '"></input>';

            divtxt = divtxt + '<p>' + t('phonetrack', 'GpsLogger GET and POST URL') + ' : ' +
                '<button class="urlhelpbutton" logger="gpslogger"><i class="fa fa-question"></i> <i class="fa fa-qrcode"></i></button>' +
                '</p>';
            divtxt = divtxt + '<input class="ro" role="gpsloggerurl" type="text" value="' + gpsloggerUrl + '"></input>';
            divtxt = divtxt + '<p>' + t('phonetrack', 'Owntracks (HTTP mode) URL') + ' : ' +
                '<button class="urlhelpbutton" logger="owntracks"><i class="fa fa-question"></i> <i class="fa fa-qrcode"></i></button>' +
                '</p>';
            divtxt = divtxt + '<input class="ro" role="owntracksurl" type="text" value="' + owntracksurl + '"></input>';
            divtxt = divtxt + '<p>' + t('phonetrack', 'Ulogger URL') + ' : ' +
                '<button class="urlhelpbutton" logger="ulogger"><i class="fa fa-question"></i> <i class="fa fa-qrcode"></i></button>' +
                '</p>';
            divtxt = divtxt + '<input class="ro" role="uloggerurl" type="text" value="' + uloggerurl + '"></input>';
            divtxt = divtxt + '<p>' + t('phonetrack', 'Traccar URL') + ' : ' +
                '<button class="urlhelpbutton" logger="traccar"><i class="fa fa-question"></i> <i class="fa fa-qrcode"></i></button>' +
                '</p>';
            divtxt = divtxt + '<input class="ro" role="traccarurl" type="text" value="' + traccarurl + '"></input>';
            divtxt = divtxt + '<p>' + t('phonetrack', 'OpenGTS URL') + ' : ' +
                '<button class="urlhelpbutton" logger="opengts"><i class="fa fa-question"></i> <i class="fa fa-qrcode"></i></button>' +
                '</p>';
            divtxt = divtxt + '<input class="ro" role="opengtsurl" type="text" value="' + opengtsurl + '"></input>';
            divtxt = divtxt + '<p>' + t('phonetrack', 'Locus Map URL') + ' : ' +
                '<button class="urlhelpbutton" logger="locusmap"><i class="fa fa-question"></i> <i class="fa fa-qrcode"></i></button>' +
                '</p>';
            divtxt = divtxt + '<input class="ro" role="locusmapurl" type="text" value="' + locusmapurl + '"></input>';
            divtxt = divtxt + '<p>' + t('phonetrack', 'HTTP GET URL') + ' : ' +
                '<button class="urlhelpbutton" logger="get"><i class="fa fa-question"></i> <i class="fa fa-qrcode"></i></button>' +
                '</p>';
            divtxt = divtxt + '<input class="ro" role="geturl" type="text" value="' + geturl + '"></input>';
            divtxt = divtxt + '<hr/></div>';
        }
        divtxt = divtxt + '<ul class="devicelist" token="' + token + '"></ul></div>';

        $('div#sessions').append($(divtxt).fadeIn('slow')).find('input.ro[type=text]').prop('readonly', true);
        if (!selected) {
            $('.session[token="' + token + '"]').find('.devicelist').hide();
        }
        $('.session[token="' + token + '"]').find('.sharediv').hide();
        $('.session[token="' + token + '"]').find('.moreUrls').hide();
        $('.session[token="' + token + '"]').find('.namereservdiv').hide();
        $('.session[token="' + token + '"]').find('select[role=autoexport]').val(autoexport);
        $('.session[token="' + token + '"]').find('select[role=autopurge]').val(autopurge);
        if (parseInt(isPublic) === 0) {
            $('.session[token="' + token + '"]').find('.publicWatchUrlDiv').hide();
        }
            //.find('input[type=text]').prop('readonly', false);
        for (i = 0; i < publicFilteredShares.length; i++) {
            addPublicSessionShare(
                token,
                publicFilteredShares[i].token,
                publicFilteredShares[i].filters,
                publicFilteredShares[i].devicename,
                publicFilteredShares[i].lastposonly,
                publicFilteredShares[i].geofencify
            );
        }
        ///////////////////////////////////////////////////////////
        if (! phonetrack.sessionLineLayers.hasOwnProperty(token)) {
            phonetrack.sessionLineLayers[token] = {};
            phonetrack.sessionDisplayedLatlngs[token] = {};
            phonetrack.sessionLatlngs[token] = {};
            phonetrack.sessionPointsLayers[token] = {};
            phonetrack.sessionPointsLayersById[token] = {};
            phonetrack.sessionPointsEntriesById[token] = {};
        }
        if (! phonetrack.sessionMarkerLayers.hasOwnProperty(token)) {
            phonetrack.sessionMarkerLayers[token] = {};
        }
        ////////////////////////////////////////////////////////////
        // Manage devices from given list
        var ii, dev, devid, devname, devalias, devcolor, devnametoken, devgeofences, devproxims, devshape;
        for (ii=0; ii < devices.length; ii++) {
            dev = devices[ii];
            devid = dev[0];
            devname = dev[1];
            devalias = dev[2];
            devcolor = dev[3];
            devnametoken = dev[4];
            devgeofences = dev[5];
            devproxims = dev[6];
            devshape = dev[7];

            if (phonetrack.sessionsFromSavedOptions &&
                phonetrack.sessionsFromSavedOptions.hasOwnProperty(token) &&
                phonetrack.sessionsFromSavedOptions[token].hasOwnProperty(devid)) {
                addDevice(
                    token, devid, name, devcolor, devname, devgeofences,
                    phonetrack.sessionsFromSavedOptions[token][devid].zoom,
                    phonetrack.sessionsFromSavedOptions[token][devid].line,
                    phonetrack.sessionsFromSavedOptions[token][devid].point,
                    devalias,
                    devproxims,
                    devshape
                );
                // once restored, get rid of the data
                delete phonetrack.sessionsFromSavedOptions[token][devid];
            }
            else {
                addDevice(token, devid, name, devcolor, devname, devgeofences, false, false, false, devalias, devproxims, devshape);
            }
        }
    }

    function deleteSession(token) {
        var div = $('div.session[token='+token+']');

        var req = {
            token: token
        };
        var url = OC.generateUrl('/apps/phonetrack/deleteSession');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                removeSession(div);
            }
            else if (response.done === 2) {
                OC.Notification.showTemporary(t('phonetrack', 'The session you want to delete does not exist'));
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to delete session'));
            }
        }).always(function() {
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to delete session'));
        });
    }

    function deleteDevice(token, deviceid) {
        var sessionName = getSessionName(token);
        var req = {
            token: token,
            deviceid: deviceid
        };
        var url = OC.generateUrl('/apps/phonetrack/deleteDevice');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            var devicename = getDeviceName(token, deviceid);
            if (response.done === 1) {
                removeDevice(token, deviceid);
                OC.Notification.showTemporary(t('phonetrack', 'Device \'{d}\' of session \'{s}\' has been deleted', {d: devicename, s: sessionName}));
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to delete device \'{d}\' of session \'{s}\'', {d: devicename, s: sessionName}));
            }
        }).always(function() {
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to delete device'));
        });
    }

    function removeDevice(token, device) {
        // remove devicelist line
        $('.devicelist li[token="' + token + '"][device="' + device + '"]').fadeOut('slow', function() {
            $(this).remove();
        });
        // remove marker, line and tooltips
        phonetrack.sessionMarkerLayers[token][device].unbindTooltip().remove();
        delete phonetrack.sessionMarkerLayers[token][device];
        phonetrack.sessionLineLayers[token][device].unbindTooltip().remove();
        delete phonetrack.sessionLineLayers[token][device];
        delete phonetrack.sessionDisplayedLatlngs[token][device];
        delete phonetrack.sessionLatlngs[token][device];
        phonetrack.sessionPointsLayers[token][device].unbindTooltip().remove();
        delete phonetrack.sessionPointsLayers[token][device];
        delete phonetrack.lastTime[token][device];
        delete phonetrack.firstTime[token][device];
    }

    function removeSession(div) {
        var d;
        var token = div.attr('token');
        // remove all devices
        for (d in phonetrack.sessionMarkerLayers[token]) {
            removeDevice(token, d);
        }
        // remove things in sidebar
        $('#addPointSession option[token=' + token + ']').remove();
        $('#deletePointSession option[token=' + token + ']').remove();
        div.fadeOut('slow', function() {
            div.remove();
        });
    }

    function renameSession(token, oldname, newname) {
        var req = {
            token: token,
            newname: newname
        };
        var url = OC.generateUrl('/apps/phonetrack/renameSession');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                renameSessionSuccess(token, oldname, newname);
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'Impossible to rename session') + ' ' + oldname);
            }
        }).always(function() {
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to rename session'));
        });
    }

    function renameSessionSuccess(token, oldname, newname) {
        $('#addPointSession option[token=' + token + ']').attr('value', newname);
        $('#addPointSession option[token=' + token + ']').text(newname);
        $('#deletePointSession option[token=' + token + ']').attr('value', newname);
        $('#deletePointSession option[token=' + token + ']').text(newname);
        var perm = $('#showtime').is(':checked');
        var d, to, p, l, id;
        $('.session[token='+token+'] .sessionBar .sessionName').text(newname);
        for (d in phonetrack.sessionMarkerLayers[token]) {
            // line tooltip
            to = phonetrack.sessionLineLayers[token][d].getTooltip()._content;
            to = to.replace(
                oldname + ' | ',
                newname + ' | '
            );
            phonetrack.sessionLineLayers[token][d].unbindTooltip();
            phonetrack.sessionLineLayers[token][d].bindTooltip(
                to,
                {
                    permanent: false,
                    sticky: true,
                    className: 'tooltip' + token + d
                }
            );
        }
    }

    function renameDevice(token, deviceid, oldname, newname) {
        var req = {
            token: token,
            deviceid: deviceid,
            newname: newname
        };
        var url = OC.generateUrl('/apps/phonetrack/renameDevice');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                renameDeviceSuccess(token, deviceid, oldname, newname);
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'Impossible to rename device') + ' ' + escapeHTML(oldname));
            }
        }).always(function() {
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to rename device'));
        });
    }

    function renameDeviceSuccess(token, d, oldname, newname) {
        var perm = $('#showtime').is(':checked');
        var to, p, l, id;
        var sessionName = getSessionName(token);
        var alias = getDeviceAlias(token, d);
        var nameLabelTxt;
        if (alias !== '') {
            nameLabelTxt = alias + ' (' + newname + ')';
        }
        else {
            nameLabelTxt = newname;
        }
        $('.session[token=' + token + '] .devicelist li[device="' + d + '"] .deviceLabel').text(nameLabelTxt);

        // manage names/ids
        var intDid = parseInt(d);
        phonetrack.deviceNames[token][intDid] = newname;
        delete phonetrack.deviceIds[token][oldname];
        phonetrack.deviceIds[token][newname] = intDid;

        // line tooltip
        phonetrack.sessionLineLayers[token][d].unbindTooltip();
        phonetrack.sessionLineLayers[token][d].bindTooltip(
            sessionName + ' | ' + nameLabelTxt,
            {
                permanent: false,
                sticky: true,
                className: 'tooltip' + token + d
            }
        );
        // update main marker letter
        var mletter = $('#markerletter').is(':checked');
        var letter = '';
        if (mletter) {
            if (alias !== '') {
                letter = alias[0];
            }
            else {
                letter = newname[0];
            }
        }
        var radius = parseInt($('#pointradius').val());
        var shape = phonetrack.sessionShapes[token+d];
        var iconMarker = L.divIcon({
            iconAnchor: [radius, radius],
            className: shape + 'marker color' + token + d,
            html: '<b>' + letter + '</b>'
        });
        phonetrack.sessionMarkerLayers[token][d].setIcon(iconMarker);
    }

    function setDeviceAlias(token, deviceid, newalias) {
        var req = {
            token: token,
            deviceid: deviceid,
            newalias: newalias
        };
        var url = OC.generateUrl('/apps/phonetrack/setDeviceAlias');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                setDeviceAliasSuccess(token, deviceid, newalias);
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'Impossible to set device alias for {n}'), {'n': getDeviceName(token, deviceid)});
            }
        }).always(function() {
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to set device alias'));
        });
    }

    function setDeviceAliasSuccess(token, d, newalias) {
        var perm = $('#showtime').is(':checked');
        var to, p, l, id;
        var sessionName = getSessionName(token);
        var devname = getDeviceName(token, d);
        var nameLabelTxt;
        if (newalias !== '') {
            nameLabelTxt = newalias + ' (' + devname + ')';
        }
        else {
            nameLabelTxt = devname;
        }
        $('.session[token=' + token + '] .devicelist li[device="' + d + '"] .deviceLabel').text(nameLabelTxt);

        // manage names/ids
        var intDid = parseInt(d);
        phonetrack.deviceAliases[token][intDid] = newalias;

        // line tooltip
        phonetrack.sessionLineLayers[token][d].unbindTooltip();
        phonetrack.sessionLineLayers[token][d].bindTooltip(
            sessionName + ' | ' + nameLabelTxt,
            {
                permanent: false,
                sticky: true,
                className: 'tooltip' + token + d
            }
        );
        // update main marker letter
        var letter = '';
        var mletter = $('#markerletter').is(':checked');
        if (mletter) {
            if (newalias !== '') {
                letter = newalias[0];
            }
            else {
                letter = devname[0];
            }
        }
        var radius = parseInt($('#pointradius').val());
        var shape = phonetrack.sessionShapes[token+d];
        var iconMarker = L.divIcon({
            iconAnchor: [radius, radius],
            className: shape + 'marker color' + token + d,
            html: '<b>' + letter + '</b>'
        });
        phonetrack.sessionMarkerLayers[token][d].setIcon(iconMarker);
    }

    function reaffectDeviceSession(token, deviceid, newSessionId) {
        var req = {
            token: token,
            deviceid: deviceid,
            newSessionId: newSessionId
        };
        var url = OC.generateUrl('/apps/phonetrack/reaffectDevice');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                reaffectDeviceSessionSuccess(token, deviceid, newSessionId);
            }
            else if (response.done === 3) {
                OC.Notification.showTemporary(t('phonetrack', 'Device already exists in target session'));
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'Impossible to move device to another session'));
            }
        }).always(function() {
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to move device'));
        });
    }

    function reaffectDeviceSessionSuccess(token, d, newSessionId) {
        removeDevice(token, d);
        refresh();
    }

    function getSessions() {
        var selected;
        var req = {
        };
        var url = OC.generateUrl('/apps/phonetrack/getSessions');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            var s;
            if (response.sessions.length > 0) {
                for (s in response.sessions) {
                    selected = false;
                    if (phonetrack.sessionsFromSavedOptions &&
                        phonetrack.sessionsFromSavedOptions.hasOwnProperty(response.sessions[s][1])
                    ) {
                        selected = true;
                    }
                    // session is shared by someone else
                    if (response.sessions[s].length < 5) {
                        addSession(
                            response.sessions[s][1],
                            response.sessions[s][0],
                            '',
                            0,
                            response.sessions[s][3],
                            [],
                            selected,
                            true,
                            response.sessions[s][2],
                            []
                        );
                    }
                    // session is mine !
                    else {
                        addSession(
                            response.sessions[s][1],
                            response.sessions[s][0],
                            response.sessions[s][2],
                            response.sessions[s][4],
                            response.sessions[s][3],
                            response.sessions[s][5],
                            selected,
                            false,
                            '',
                            response.sessions[s][6],
                            response.sessions[s][7],
                            response.sessions[s][8],
                            response.sessions[s][9]
                        );
                    }
                }
            }
            // in case some sessions are selected
            // refresh but don't loop
            refresh(false);
        }).always(function() {
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to get sessions'));
        });
    }

    function refresh(loop=true) {
        var url;
        var sessionsToWatch = [];
        // get new positions for all watched sessions
        $('.watchbutton i.fa-toggle-on').each(function() {
            var token = $(this).parent().parent().parent().attr('token');
            var lastTimes = phonetrack.lastTime[token];
            if (Object.keys(lastTimes).length === 0) {
                lastTimes = '';
            }
            var firstTimes = phonetrack.firstTime[token];
            if (Object.keys(firstTimes).length === 0) {
                firstTimes = '';
            }
            sessionsToWatch.push([token, lastTimes, firstTimes]);
        });

        if (phonetrack.currentRefreshAjax !== null) {
            phonetrack.currentRefreshAjax.abort();
        }

        if (sessionsToWatch.length > 0) {
            showLoadingAnimation();
            var req = {
                sessions: sessionsToWatch
            };
            if (pageIsPublicSessionWatch()) {
                url = OC.generateUrl('/apps/phonetrack/publicViewTrack');
            }
            else if (pageIsPublicWebLog()) {
                url = OC.generateUrl('/apps/phonetrack/publicWebLogTrack');
            }
            else {
                url = OC.generateUrl('/apps/phonetrack/track');
            }
            phonetrack.currentRefreshAjax = $.ajax({
                type: 'POST',
                url: url,
                data: req,
                async: true,
                xhr: function() {
                    var xhr = new window.XMLHttpRequest();
                    xhr.addEventListener('progress', function(evt) {
                        if (evt.lengthComputable) {
                            var percentComplete = evt.loaded / evt.total * 100;
                            $('#loadingpc').text(parseInt(percentComplete) + '%');
                        }
                    }, false);

                    return xhr;
                }
            }).done(function (response) {
                displayNewPoints(response.sessions, response.colors, response.names, response.geofences, response.aliases, response.proxims, response.shapes);
            }).always(function() {
                hideLoadingAnimation();
                phonetrack.currentRefreshAjax = null;
            }).fail(function() {
                // TODO check how to make it work when called from an ajax "done"
                //OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to refresh sessions'));
            });
        }
        // we always update the view
        showHideSelectedSessions();

        var uiVal = parseInt($('#updateinterval').val());
        if (uiVal === 0 || isNaN(uiVal)) {
            if (phonetrack.currentTimer !== null) {
                phonetrack.currentTimer.pause();
                phonetrack.currentTimer = null;
            }
            if ($('#countdown').hasClass('is-countdown')) {
                $('#countdown').countdown('destroy');
            }
        }
        if (loop && uiVal !== 0 && !isNaN(uiVal)) {
            // launch refresh again
            var updateinterval = 5000;
            if (uiVal !== '' && !isNaN(uiVal) && parseInt(uiVal) > 1) {
                updateinterval = parseInt(uiVal) * 1000;
            }
            // display countdown
            if ($('#countdown').hasClass('is-countdown')) {
                $('#countdown').countdown('destroy');
            }
            var t = new Date();
            t.setSeconds(t.getSeconds() + updateinterval/1000);
            $('#countdown').countdown({until: t, format: 'HMS', compact: true});
            // launch timer
            phonetrack.currentTimer = new Timer(function() {
                refresh();
            }, updateinterval);
        }
    }

    // transform a list of latlngs into multiple segments based on time/distance thresholds
    function segmentLines(ll, s, d) {
        var cuttime = parseInt($('#cuttime').val()) || null;
        var cutdistance = parseInt($('#cutdistance').val()) || null;
        if (ll.length === 0) {
            return [];
        }
        else if (ll.length === 1) {
            return [ll];
        }
        else if (cuttime === null && cutdistance === null) {
            return [ll];
        }
        else {
            var i = 1;
            var segments = [];
            var currentSegment = [ll[0]];
            var lastEntry    = phonetrack.sessionPointsEntriesById[s][d][ll[0][2]];
            var currentEntry = phonetrack.sessionPointsEntriesById[s][d][ll[1][2]];
            while (i < ll.length) {
                // fill current segment while possible
                while (i < ll.length &&
                       (cutdistance === null || phonetrack.map.distance(ll[i-1], ll[i]) < cutdistance) &&
                       (cuttime === null || ((currentEntry.timestamp - lastEntry.timestamp) < cuttime))
                ) {
                    currentSegment.push(ll[i]);
                    i++;
                    lastEntry = currentEntry;
                    if (i < ll.length) {
                        currentEntry = phonetrack.sessionPointsEntriesById[s][d][ll[i][2]];
                    }
                }
                // end of segment, add it to segment list
                segments.push(currentSegment);
                // and prepare next segment if there are more points
                if (i < ll.length) {
                    currentSegment = [ll[i]];
                    lastEntry = phonetrack.sessionPointsEntriesById[s][d][ll[i][2]];
                    i++;
                    // there are more points
                    if (i < ll.length) {
                        currentEntry = phonetrack.sessionPointsEntriesById[s][d][ll[i][2]];
                    }
                    // there is no more point after this one
                    else {
                        segments.push(currentSegment);
                    }
                }
            }
            var cl = 0;
            for (i=0; i<segments.length; i++) {
                cl = cl + segments[i].length;
            }
            console.assert(ll.length === cl, 'Warning : segmentation went wrong');
            return segments;
        }
    }

    function filterEntry(entry) {
        var filtersEnabled = phonetrack.filtersEnabled;

        var satellitesmin, satellitesmax, batterymin, batterymax,
            elevationmin, elevationmax, accuracymin, accuracymax,
            bearingmin, bearingmax, speedmin,
            speedmax, timestampMin, timestampMax;

        if (filtersEnabled) {
            satellitesmin = phonetrack.filterValues.satellitesmin;
            satellitesmax = phonetrack.filterValues.satellitesmax;
            batterymin    = phonetrack.filterValues.batterymin;
            batterymax    = phonetrack.filterValues.batterymax;
            elevationmin  = phonetrack.filterValues.elevationmin;
            elevationmax  = phonetrack.filterValues.elevationmax;
            accuracymin   = phonetrack.filterValues.accuracymin;
            accuracymax   = phonetrack.filterValues.accuracymax;
            bearingmin    = phonetrack.filterValues.bearingmin;
            bearingmax    = phonetrack.filterValues.bearingmax;
            speedmin      = phonetrack.filterValues.speedmin / 3.6;
            speedmax      = phonetrack.filterValues.speedmax / 3.6;

            timestampMin = phonetrack.filterValues.tsmin;
            timestampMax = phonetrack.filterValues.tsmax;
        }
        return (
            !filtersEnabled ||
            (
                 (!timestampMin || parseInt(entry.timestamp) >= timestampMin) &&
                 (!timestampMax || parseInt(entry.timestamp) <= timestampMax) &&
                 (!elevationmax || entry.altitude >= elevationmax) &&
                 (!elevationmin || entry.altitude <= elevationmin) &&
                 (!batterymin || entry.batterylevel >= batterymin) &&
                 (!batterymax || entry.batterylevel <= batterymax) &&
                 (!satellitesmin || entry.satellites >= satellitesmin) &&
                 (!satellitesmax || entry.satellites <= satellitesmax) &&
                 (!accuracymin || entry.accuracy >= accuracymin) &&
                 (!accuracymax || entry.accuracy <= accuracymax) &&
                 (!bearingmin || entry.bearing >= bearingmin) &&
                 (!bearingmax || entry.bearing <= bearingmax) &&
                 (!speedmin || entry.speed >= speedmin) &&
                 (!speedmax || entry.speed <= speedmax)
            )
        );
    }

    function filterList(list, token, deviceid) {
        var filtersEnabled = phonetrack.filtersEnabled;
        var resList, resDateList;

        if (filtersEnabled) {
            var satellitesmin = phonetrack.filterValues.satellitesmin;
            var satellitesmax = phonetrack.filterValues.satellitesmax;
            var batterymin    = phonetrack.filterValues.batterymin;
            var batterymax    = phonetrack.filterValues.batterymax;
            var elevationmin  = phonetrack.filterValues.elevationmin;
            var elevationmax  = phonetrack.filterValues.elevationmax;
            var accuracymin   = phonetrack.filterValues.accuracymin;
            var accuracymax   = phonetrack.filterValues.accuracymax;
            var bearingmin    = phonetrack.filterValues.bearingmin;
            var bearingmax    = phonetrack.filterValues.bearingmax;
            var speedmin      = phonetrack.filterValues.speedmin / 3.6;
            var speedmax      = phonetrack.filterValues.speedmax / 3.6;

            var timestampMin  = phonetrack.filterValues.tsmin;
            var timestampMax  = phonetrack.filterValues.tsmax;

            resDateList = [];
            resList = [];
            var i = 0;
            ////// DATES
            // we avoid everything under the min
            if (timestampMin) {
                while (i < list.length &&
                       (parseInt(phonetrack.sessionPointsEntriesById[token][deviceid][list[i][2]].timestamp) <= timestampMin)
                ) {
                    i++;
                }
            }
            // then we copy everything under the max
            if (timestampMax) {
                while (i < list.length &&
                       (parseInt(phonetrack.sessionPointsEntriesById[token][deviceid][list[i][2]].timestamp) <= timestampMax)
                ) {
                    resDateList.push(list[i]);
                    i++;
                }
            }
            else {
                while (i < list.length) {
                    resDateList.push(list[i]);
                    i++;
                }
            }
            // filter again with int values
            i = 0;
            var entry;
            while (i < resDateList.length) {
                entry = phonetrack.sessionPointsEntriesById[token][deviceid][resDateList[i][2]];
                if (
                    (!elevationmax || entry.altitude <= elevationmax) &&
                    (!elevationmin || entry.altitude >= elevationmin) &&
                    (!batterymin || entry.batterylevel >= batterymin) &&
                    (!batterymax || entry.batterylevel <= batterymax) &&
                    (!satellitesmin || entry.satellites >= satellitesmin) &&
                    (!satellitesmax || entry.satellites <= satellitesmax) &&
                    (!accuracymin || entry.accuracy >= accuracymin) &&
                    (!accuracymax || entry.accuracy <= accuracymax) &&
                    (!bearingmin || entry.bearing >= bearingmin) &&
                    (!bearingmax || entry.bearing <= bearingmax) &&
                    (!speedmin || entry.speed >= speedmin) &&
                    (!speedmax || entry.speed <= speedmax)
                ){
                    resList.push(resDateList[i]);
                }
                i++;
            }
        }
        else {
            resList = list;
        }
        return resList;
    }

    function storeFilters() {
        // simple fields
        $('#filterPointsTable input[type=number]').each(function() {
            phonetrack.filterValues[$(this).attr('id')] = parseInt($(this).val());
        });

        // date fields : we just want tsmin and tsmax
        var timestampMin = null;
        var timestampMax = null;
        var tab = $('#filterPointsTable');
        var dateminstr = tab.find('input#datemin').val();
        var hourminstr, minminstr, secminstr, momMin;
        var hourmaxstr, minmaxstr, secmaxstr, momMax;
        if (dateminstr) {
            hourminstr = parseInt(tab.find('input#hourmin').val()) || 0;
            minminstr = parseInt(tab.find('input#minutemin').val()) || 0;
            secminstr = parseInt(tab.find('input#secondmin').val()) || 0;
            var completeDateMinStr = dateminstr + ' ' + pad(hourminstr) + ':' + pad(minminstr) + ':' + pad(secminstr);
            momMin = moment(completeDateMinStr);
            timestampMin = momMin.unix();
        }
        // if no date is set but hour:min:sec is set, make it today
        else {
            hourminstr = parseInt(tab.find('input#hourmin').val());
            minminstr = parseInt(tab.find('input#minutemin').val());
            secminstr = parseInt(tab.find('input#secondmin').val());
            if (!isNaN(hourminstr) && !isNaN(minminstr) && !isNaN(secminstr)) {
                momMin = moment();
                momMin.hour(hourminstr);
                momMin.minute(minminstr);
                momMin.second(secminstr);
                timestampMin = momMin.unix();
            }
        }

        var datemaxstr = tab.find('input#datemax').val();
        if (datemaxstr) {
            hourmaxstr = parseInt(tab.find('input#hourmax').val()) || 23;
            minmaxstr = parseInt(tab.find('input#minutemax').val()) || 59;
            secmaxstr = parseInt(tab.find('input#secondmax').val()) || 59;
            var completeDateMaxStr = datemaxstr + ' ' + pad(hourmaxstr) + ':' + pad(minmaxstr) + ':' + pad(secmaxstr);
            momMax = moment(completeDateMaxStr);
            timestampMax = momMax.unix();
        }
        // if no date is set but hour:min:sec is set, make it today
        else {
            hourmaxstr = parseInt(tab.find('input#hourmax').val());
            minmaxstr = parseInt(tab.find('input#minutemax').val());
            secmaxstr = parseInt(tab.find('input#secondmax').val());
            if (!isNaN(hourmaxstr) && !isNaN(minmaxstr) && !isNaN(secmaxstr)) {
                momMax = moment();
                momMax.hour(hourmaxstr);
                momMax.minute(minmaxstr);
                momMax.second(secmaxstr);
                timestampMax = momMax.unix();
            }
        }

        var lastdays = parseInt(tab.find('input#lastdays').val());
        var lasthours = parseInt(tab.find('input#lasthours').val());
        var lastmins = parseInt(tab.find('input#lastmins').val());
        var momlast = moment();
        if (lastdays) {
            momlast.subtract(lastdays, 'days');
        }
        if (lasthours) {
            momlast.subtract(lasthours, 'hours');
        }
        if (lastmins) {
            momlast.subtract(lastmins, 'minutes');
        }
        if (lastdays || lasthours || lastmins) {
            var timestampLast = momlast.unix();
            // if there is no time min or if timelast is more recent than timemin
            if (!timestampMin || timestampLast > timestampMin) {
                timestampMin = timestampLast;
            }
        }
        phonetrack.filterValues.tsmin = timestampMin;
        phonetrack.filterValues.tsmax = timestampMax;
    }

    function changeApplyFilter() {
        var linewidth = parseInt($('#linewidth').val()) || 5;
        var linearrow = $('#linearrow').is(':checked');
        var linegradient = $('#linegradient').is(':checked');
        var filtersEnabled = $('#applyfilters').is(':checked');
        var coordsTmp, j;
        phonetrack.filtersEnabled = filtersEnabled;
        if (filtersEnabled) {
            storeFilters();
            $('#filterPointsTable').addClass('activatedFilters');
        }
        else {
            $('#filterPointsTable').removeClass('activatedFilters');
        }
        //$('#filterPointsTable input[type=number]').prop('disabled', filtersEnabled);
        //$('#filterPointsTable input[type=date]').prop('disabled', filtersEnabled);
        var s, d, id, i, displayedLatlngs, cutLines, line;
        var dragenabled = $('#dragcheck').is(':checked');

        if (filtersEnabled) {
            $('#sidebarFen').show();
            $('#sidebarFdis').hide();
        }
        else {
            $('#sidebarFen').hide();
            $('#sidebarFdis').show();
        }

        // simpler case : no filter
        if (!filtersEnabled) {
            for (s in phonetrack.sessionLineLayers) {
                for (d in phonetrack.sessionLineLayers[s]) {
                    // put all coordinates in lines
                    displayedLatlngs = phonetrack.sessionLatlngs[s][d];
                    cutLines = segmentLines(displayedLatlngs, s, d);
                    phonetrack.sessionLineLayers[s][d].clearLayers();
                    delete phonetrack.sessionDisplayedLatlngs[s][d];
                    phonetrack.sessionDisplayedLatlngs[s][d] = cutLines;
                    drawLine(s, d, cutLines, linegradient, linewidth, linearrow);

                    // add line points from sessionPointsLayersById in sessionPointsLayers
                    for (id in phonetrack.sessionPointsLayersById[s][d]) {
                        if (!phonetrack.sessionPointsLayers[s][d].hasLayer(phonetrack.sessionPointsLayersById[s][d][id])) {
                            phonetrack.sessionPointsLayers[s][d].addLayer(phonetrack.sessionPointsLayersById[s][d][id]);
                            if (!pageIsPublic() && !isSessionShared(s) && $('#dragcheck').is(':checked') &&
                                phonetrack.map.hasLayer(phonetrack.sessionPointsLayers[s][d])
                            ) {
                                phonetrack.sessionPointsLayersById[s][d][id].dragging.enable();
                            }
                        }
                    }
                }
            }
            $('#statlabel').text(t('phonetrack', 'Stats of all points'));
        }
        // there is at least a filter
        else {
            for (s in phonetrack.sessionLineLayers) {
                for (d in phonetrack.sessionLineLayers[s]) {
                    // put filtered coordinates in lines
                    displayedLatlngs = filterList(phonetrack.sessionLatlngs[s][d], s, d);
                    cutLines = segmentLines(displayedLatlngs, s, d);
                    phonetrack.sessionLineLayers[s][d].clearLayers();
                    delete phonetrack.sessionDisplayedLatlngs[s][d];
                    phonetrack.sessionDisplayedLatlngs[s][d] = cutLines;

                    drawLine(s, d, cutLines, linegradient, linewidth, linearrow);

                    // filter sessionPointsLayers
                    phonetrack.sessionPointsLayers[s][d].clearLayers();
                    for (i = 0; i < displayedLatlngs.length; i++) {
                        id = displayedLatlngs[i][2];
                        phonetrack.sessionPointsLayers[s][d].addLayer(phonetrack.sessionPointsLayersById[s][d][id]);
                    }
                    // if device is displayed and dragging is enabled : make it happen
                    if (dragenabled && $('.session[token='+s+'] .devicelist li[device="'+d+'"] .toggleDetail').hasClass('on')) {
                        for (i = 0; i < displayedLatlngs.length; i++) {
                            id = displayedLatlngs[i][2];
                            phonetrack.sessionPointsLayersById[s][d][id].dragging.enable();
                        }
                    }
                }
            }
            if (filtersEnabled) {
                $('#statlabel').text(t('phonetrack', 'Stats of filtered points'));
            }
            else {
                $('#statlabel').text(t('phonetrack', 'Stats of all points'));
            }
        }

        // anyway, filter or not, we adapt the markers
        for (s in phonetrack.sessionLineLayers) {
            var sessionname = getSessionName(s);
            for (d in phonetrack.sessionLineLayers[s]) {
                updateMarker(s, d, sessionname);
            }
        }
        if ($('#togglestats').is(':checked')) {
            updateStatTable();
        }
        changeTooltipStyle();
    }

    function updateMarker(s, d, sessionname) {
        var perm = $('#showtime').is(':checked');
        var mla, mln, mid, mentry, displayedLatlngs, oldlatlng;
        // TODO check if there is another way to get list of displayed latlngs
        var pointLayerList = phonetrack.sessionPointsLayers[s][d].getLayers();
        var lastll = null;
        var maxTime = -1;
        var ll;
        for (var i=0; i < pointLayerList.length; i++) {
            ll = pointLayerList[i].getLatLng();
            if (phonetrack.sessionPointsEntriesById[s][d][ll.alt].timestamp > maxTime) {
                maxTime = phonetrack.sessionPointsEntriesById[s][d][ll.alt].timestamp;
                lastll = ll;
            }
        }
        // if session is not watched or if there is no points to see
        if (!$('div.session[token='+s+'] .watchbutton i').hasClass('fa-toggle-on') || pointLayerList.length === 0) {
            if (phonetrack.map.hasLayer(phonetrack.sessionMarkerLayers[s][d])) {
                phonetrack.sessionMarkerLayers[s][d].remove();
            }
        }
        else {
            mla = lastll.lat;
            mln = lastll.lng;
            mid = lastll.alt;
            mentry = phonetrack.sessionPointsEntriesById[s][d][mid];
            oldlatlng = phonetrack.sessionMarkerLayers[s][d].getLatLng();
            // move and update tooltip/popup only if needed (marker has changed or coords are different)
            if (oldlatlng === null ||
                parseInt(oldlatlng.alt) !== parseInt(mid) ||
                mla !== oldlatlng.lat ||
                mln !== oldlatlng.lng
            ) {
                // move
                phonetrack.sessionMarkerLayers[s][d].setLatLng([mla, mln, mid]);
            }

            if (phonetrack.sessionMarkerLayers[s][d].pid === null ||
                parseInt(oldlatlng.alt) !== parseInt(mid)
            ) {
                phonetrack.sessionMarkerLayers[s][d].pid = mid;
            }

            // if marker was not already displayed
            if (!phonetrack.map.hasLayer(phonetrack.sessionMarkerLayers[s][d])) {
                phonetrack.map.addLayer(phonetrack.sessionMarkerLayers[s][d]);
                if (!pageIsPublic() &&
                    !isSessionShared(s) &&
                    $('.session[token='+s+'] .devicelist li[device="'+d+'"] .toggleDetail').hasClass('on')
                ) {
                    phonetrack.sessionMarkerLayers[s][d].dragging.enable();
                }
            }
        }
    }

    function displayNewPoints(sessions, colors, names, geofences={}, aliases={}, proxims={}, shapes={}) {
        var s, i, d, entry, device, timestamp, mom, icon,
            entryArray, dEntries, colorn, rgbc, devcol, devgeofences, devshape, devproxims,
            textcolor, sessionname;
        var perm = $('#showtime').is(':checked');
        for (s in sessions) {
            sessionname = getSessionName(s);
            // for all devices
            for (d in sessions[s]) {
                // add line and marker if necessary
                if (! phonetrack.sessionLineLayers[s].hasOwnProperty(d)) {
                    devcol = '';
                    devgeofences = [];
                    devproxims = [];
                    devshape = '';
                    if (colors.hasOwnProperty(s) && colors[s].hasOwnProperty(d)) {
                        devcol = colors[s][d];
                    }
                    if (proxims.hasOwnProperty(s) && proxims[s].hasOwnProperty(d)) {
                        devproxims = proxims[s][d];
                    }
                    if (shapes.hasOwnProperty(s) && shapes[s].hasOwnProperty(d)) {
                        devshape = shapes[s][d];
                    }
                    if (geofences.hasOwnProperty(s) && geofences[s].hasOwnProperty(d)) {
                        devgeofences = geofences[s][d];
                    }
                    if (phonetrack.sessionsFromSavedOptions &&
                        phonetrack.sessionsFromSavedOptions.hasOwnProperty(s) &&
                        phonetrack.sessionsFromSavedOptions[s].hasOwnProperty(d)) {
                        addDevice(
                            s, d, sessionname, devcol, names[s][d], devgeofences,
                            phonetrack.sessionsFromSavedOptions[s][d].zoom,
                            phonetrack.sessionsFromSavedOptions[s][d].line,
                            phonetrack.sessionsFromSavedOptions[s][d].point,
                            aliases[s][d],
                            devproxims,
                            devshape
                        );
                        // once restored, get rid of the data
                        delete phonetrack.sessionsFromSavedOptions[s][d];
                    }
                    else {
                        addDevice(s, d, sessionname, devcol, names[s][d], devgeofences, false, false, false, aliases[s][d], devproxims, devshape);
                    }
                }
                // for all new entries of this session
                dEntries = [];
                for (i in sessions[s][d]) {
                    entryArray = sessions[s][d][i];
                    entry = {
                        id: entryArray[0],
                        deviceid: d,
                        lat: entryArray[1],
                        lon: entryArray[2],
                        timestamp: entryArray[3],
                        accuracy: entryArray[4],
                        satellites: entryArray[5],
                        altitude: entryArray[6],
                        batterylevel: entryArray[7],
                        useragent: entryArray[8],
                        speed: entryArray[9],
                        bearing: entryArray[10]
                    };
                    dEntries.push(entry);
                }
                appendEntriesToDevice(s, d, dEntries, sessionname);
            }
        }
        if ($('#togglestats').is(':checked')) {
            updateStatTable();
        }
        // in case user click is between ajax request and response
        showHideSelectedSessions();

        if (phonetrack.sessionsFromSavedOptions) {
            zoomOnDisplayedMarkers();
            delete phonetrack.sessionsFromSavedOptions;
        }
    }

    function setDeviceCss(s, d, colorcode, opacity, shape) {
        var rgbc = hexToRgb(colorcode);
        var textcolor = 'black';
        if (rgbc.r + rgbc.g + rgbc.b < 3 * 80) {
            textcolor = 'white';
        }
        var background = 'background: rgba(' + rgbc.r + ', ' + rgbc.g + ', ' + rgbc.b + ', 0);';
        var border = 'border-color: rgba(' + rgbc.r + ', ' + rgbc.g + ', ' + rgbc.b + ', ' + opacity + ');';
        var devcolbackground = 'background: rgba(' + rgbc.r + ', ' + rgbc.g + ', ' + rgbc.b + ', 0);';
        var devcolborder = 'border-color: rgba(' + rgbc.r + ', ' + rgbc.g + ', ' + rgbc.b + ', 1);';
        if (shape !== 't') {
            background = 'background: rgba(' + rgbc.r + ', ' + rgbc.g + ', ' + rgbc.b + ', ' + opacity + ');';
            //border = 'border-color: rgba(' + rgbc.r + ', ' + rgbc.g + ', ' + rgbc.b + ', 0);';
            border = 'border: 1px solid grey;';
            devcolbackground = 'background: rgba(' + rgbc.r + ', ' + rgbc.g + ', ' + rgbc.b + ', 1);';
            //devcolborder = 'border-color: rgba(' + rgbc.r + ', ' + rgbc.g + ', ' + rgbc.b + ', 0);';
            devcolborder = 'border: 1px solid grey;';
        }
        $('style[tokendevice="' + s + d + '"]').remove();
        $('<style tokendevice="' + s + d + '">' +
            '.color' + s + d + ' { ' +
            background +
            border +
            'color: ' + textcolor + '; font-weight: bold;' +
            ' }' +
            '.devicecolor' + s + d + ' {' +
            devcolbackground +
            devcolborder +
            '}' +
            '.poly' + s + d + ' {' +
            'stroke: ' + colorcode + ';' +
            'opacity: ' + opacity + ';' +
            '}' +
            '.tooltip' + s + d + ' {' +
            'background: rgba(' + rgbc.r + ', ' + rgbc.g + ', ' + rgbc.b + ', 0.5);' +
            'color: ' + textcolor + '; font-weight: bold; }' +
            '.statcolor' + s + d + ' {' +
            'background: rgb(' + rgbc.r + ', ' + rgbc.g + ', ' + rgbc.b + ');' +
            'color: ' + textcolor + '; font-weight: bold;' +
            '}</style>').appendTo('body');
    }

    function changeDeviceStyle(s, d, colorcode) {
        var linegradient = $('#linegradient').is(':checked');
        if (linegradient) {
            phonetrack.sessionLineLayers[s][d].eachLayer(function (l) {
                l.options.outlineColor = colorcode;
                l.redraw();
            });
        }
        var shape = phonetrack.sessionShapes[s+d];
        var opacity = $('#pointlinealpha').val();
        setDeviceCss(s, d, colorcode, opacity, shape);
        // we apply change in DB
        if (!pageIsPublic()) {
            var req = {
                session: s,
                device: d,
                color: colorcode
            };
            var url = OC.generateUrl('/apps/phonetrack/setDeviceColor');
            $.ajax({
                type: 'POST',
                url: url,
                data: req,
                async: true
            }).done(function (response) {
                if (response.done === 1) {
                    OC.Notification.showTemporary(t('phonetrack', 'Device\'s color successfully changed'));
                }
                else {
                    OC.Notification.showTemporary(t('phonetrack', 'Failed to save device\'s color'));
                }
            }).always(function() {
            }).fail(function() {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to change device\'s color'));
            });
        }
    }

    function showColorPicker(s, d) {
        $('#tracknamecolor').attr('token', s);
        $('#tracknamecolor').attr('deviceid', d);
        var currentColor = phonetrack.sessionColors[s + d];
        $('#colorinput').val(currentColor);
        $('#colorinput').click();
    }

    function okColor() {
        var color = $('#colorinput').val();
        var s = $('#tracknamecolor').attr('token');
        var d = $('#tracknamecolor').attr('deviceid');
        phonetrack.sessionColors[s + d] = color;
        changeDeviceStyle(s, d, color);
    }

    function addDevice(s, d, sessionname, color='', name='', geofences=[], zoom=false, line=false, point=false, alias='', proxims=[], pshape='') {
        var colorn, textcolor, linetooltip, shape;
        if (pshape === '' || pshape === null) {
            shape = 'r';
        }
        else {
            shape = pshape;
        }
        phonetrack.sessionShapes[s + d] = shape;
        if (color === '' || color === null) {
            var theme = $('#colorthemeselect').val();
            var colorCodeArray;
            if (theme === 'dark') {
                colorCodeArray = colorCodeDark;
            }
            else if (theme === 'pastel') {
                colorCodeArray = colorCodePastel;
            }
            else {
                colorCodeArray = colorCodeBright;
            }
            colorn = ++lastColorUsed % colorCodeArray.length;
            phonetrack.sessionColors[s + d] = colorCodeArray[colorn];
        }
        else {
            phonetrack.sessionColors[s + d] = color;
        }
        var opacity = $('#pointlinealpha').val();
        setDeviceCss(s, d, phonetrack.sessionColors[s + d], opacity, shape);

        var shapeDiv = '';
        var deleteLink = '';
        var renameLink = '';
        var aliasLink = '';
        var geofencesLink = '';
        var geofencesDiv = '';
        var proximLink = '';
        var proximDiv = '';
        var renameInput = '';
        var aliasInput = '';
        var reaffectLink = '';
        var geoLink = '';
        var geoLinkQR = '';
        var routingGraphLink = '';
        var routingOsrmLink = '';
        var routingOrsLink = '';
        var reaffectSelect = '';
        var dropdowndevicebutton = '';
        var dropdowndevicecontent = '';
        geoLink = ' <button class="geoLinkDevice" token="' + s + '" device="' + d + '">' +
            '<i class="fa fa-map-marked-alt" aria-hidden="true"></i> ' + t('phonetrack', 'Geo link to open position in other app/software') + '</button>';
        geoLinkQR = ' <button class="geoLinkQRDevice" token="' + s + '" device="' + d + '">' +
            '<i class="fa fa-qrcode" aria-hidden="true"></i> <i class="fa fa-map-marked-alt" aria-hidden="true"></i> ' + t('phonetrack', 'Geo link QRcode to open position with a QRcode scanner') + '</button>';
        routingGraphLink = ' <button class="routingGraphDevice" token="' + s + '" device="' + d + '">' +
            '<i class="fa fa-route" aria-hidden="true"></i> ' + t('phonetrack', 'Get driving direction to this device with {s}', {'s': 'Graphhopper'}) + '</button>';
        routingOsrmLink = ' <button class="routingOsrmDevice" token="' + s + '" device="' + d + '">' +
            '<i class="fa fa-route" aria-hidden="true"></i> ' + t('phonetrack', 'Get driving direction to this device with {s}', {'s': 'Osrm'}) + '</button>';
        routingOrsLink = ' <button class="routingOrsDevice" token="' + s + '" device="' + d + '">' +
            '<i class="fa fa-route" aria-hidden="true"></i> ' + t('phonetrack', 'Get driving direction to this device with {s}', {'s': 'OpenRouteService'}) + '</button>';
        dropdowndevicebutton = '<button class="dropdowndevicebutton" title="'+t('phonetrack', 'More actions')+'">' +
            '<i class="fa fa-bars" aria-hidden="true"></i></button>';
        if (!pageIsPublic() && !isSessionShared(s)) {
            shapeDiv = '<div class="shapediv" title="">' +
                '<div><i class="fa fa-shapes" aria-hidden="true"></i> ' + t('phonetrack', 'Set device shape') + '</div>' +
            '<select role="shapeselect">' +
            '<option value="r">' + t('phonetrack', 'Round') + '</option>' +
            '<option value="s">' + t('phonetrack', 'Square') + '</option>' +
            '<option value="t">' + t('phonetrack', 'Triangle') + '</option>' +
            '</select>' +
            '</div>';
            deleteLink = ' <button class="deleteDevice" token="' + s + '" device="' + d + '">' +
                '<i class="fa fa-trash" aria-hidden="true"></i> ' + t('phonetrack', 'Delete this device') + '</button>';
            renameLink = ' <button class="renameDevice" token="' + s + '" device="' + d + '">' +
                '<i class="fa fa-pencil-alt" aria-hidden="true"></i> ' + t('phonetrack', 'Rename this device') + '</button>';
            renameInput = '<input type="text" class="renameDeviceInput" value="' + escapeHTML(name) + '"/> ';
            aliasLink = ' <button class="aliasDevice" token="' + s + '" device="' + d + '">' +
                '<i class="fa fa-pencil-alt" aria-hidden="true"></i> ' + t('phonetrack', 'Set device alias') + '</button>';
            aliasInput = '<input type="text" class="aliasDeviceInput" value="' + escapeHTML(alias || '') + '"/> ';
            reaffectLink = ' <button class="reaffectDevice" token="' + s + '" device="' + d + '">' +
                '<i class="fa fa-exchange-alt" aria-hidden="true"></i> ' + t('phonetrack', 'Move to another session') + '</button>';
            reaffectSelect = '<div class="reaffectDeviceDiv"><select class="reaffectDeviceSelect"></select>' +
                '<button class="reaffectDeviceOk"><i class="fa fa-check" aria-hidden="true"></i> ' +
                t('phonetrack', 'Ok') + '</button>' +
                '</div>';
        }
        dropdowndevicecontent = '<div class="dropdown-content">' +
            shapeDiv +
            deleteLink +
            renameLink +
            aliasLink +
            reaffectLink +
            geoLink +
            geoLinkQR +
            routingGraphLink +
            routingOsrmLink +
            routingOrsLink +
            '</div>';
        if (!pageIsPublic() && !isSessionShared(s)) {
            geofencesLink = ' <button class="toggleGeofences" ' +
                'title="' + t('phonetrack', 'Device geofencing zones') + '">' +
                '</button>';
            geofencesDiv = '<div class="geofencesDiv">' +
                '<div class="addgeofencediv">' +
                '<p>' + t('phonetrack', 'Zoom on geofencing area, then set values, then validate.') + '</p>' +
                '<label for="sendnotif'+s+d+'"> ' + t('phonetrack', 'Nextcloud notification') + '</label> ' +
                '<input type="checkbox" class="sendnotif" id="sendnotif'+s+d+'" checked/><br/>' +
                '<label for="sendemail'+s+d+'"> ' + t('phonetrack', 'Email notification') + '</label> ' +
                '<input type="checkbox" class="sendemail" id="sendemail'+s+d+'" checked/><br/>' +
                '<input type="text" id="geoemail'+s+d+'" class="geoemail" maxlength="500"' +
                'title="' + t('phonetrack', 'An empty value means the session owner\'s email address.') + "\n" +
                t('phonetrack', 'You can put multiple addresses separated by comas (,).') +'"/><br/>' +
                '<label for="urlenter'+s+d+'"><b>' + t('phonetrack', 'URL to request when entering') + '</b></label><br/>' +
                '<span>(<label for="urlenterpost'+s+d+'">' + t('phonetrack', 'Use POST method') +' </label>' +
                '<input type="checkbox" class="urlenterpost" id="urlenterpost'+s+d+'"/>)</span>' +
                '<input type="text" id="urlenter'+s+d+'" class="urlenter" maxlength="500" /><br/>' +
                '<label for="urlleave'+s+d+'"><b>' + t('phonetrack', 'URL to request when leaving') + '</b> </label><br/>' +
                '<span>(<label for="urlleavepost'+s+d+'">' + t('phonetrack', 'Use POST method') +' </label>' +
                '<input type="checkbox" class="urlleavepost" id="urlleavepost'+s+d+'"/>)</span>' +
                '<input type="text" id="urlleave'+s+d+'" class="urlleave" maxlength="500" />' +
                '<label><b>' + t('phonetrack', 'Geofencing zone coordinates') + '</b> ' + '(' + t('phonetrack', 'leave blank to use current map bounds') + ')' + '</label><br/>' +
                '<div class="addgeofenceleft">' +
                '<label for="north'+s+d+'"> ' + t('phonetrack', 'North') + ' </label>' +
                '<input id="north'+s+d+'" class="fencenorth" type="number" value="" min="-90" max="90" step="0.000001"/><br/>' +
                '<label for="south'+s+d+'"> ' + t('phonetrack', 'South') + ' </label>' +
                '<input id="south'+s+d+'" class="fencesouth" type="number" value="" min="-90" max="90" step="0.000001"/>' +
                '</div>' +
                '<div class="addgeofencecenter">' +
                '<button class="geonortheastbutton" title="' + t('phonetrack', 'Set North/East corner by clicking on the map') + '">' +
                '<i class="fa fa-crosshairs" aria-hidden="true"></i> ' + t('phonetrack', 'Set N/E') +
                '</button><br/>' +
                '<button class="geosouthwestbutton" title="' + t('phonetrack', 'Set South/West corner by clicking on the map') + '">' +
                '<i class="fa fa-crosshairs" aria-hidden="true"></i> ' + t('phonetrack', 'Set S/W') +
                '</button>' +
                '</div>' +
                '<div class="addgeofenceright">' +
                '<label for="east'+s+d+'"> ' + t('phonetrack', 'East') + ' </label> ' +
                '<input id="east'+s+d+'" class="fenceeast" type="number" value="" min="-180" max="180" step="0.000001"/><br/>' +
                '<label for="west'+s+d+'"> ' + t('phonetrack', 'West') + ' </label> ' +
                '<input id="west'+s+d+'" class="fencewest" type="number" value="" min="-180" max="180" step="0.000001"/>' +
                '</div>' +
                '<input type="text" class="geofencename" value="' + t('phonetrack', 'Fence name') + '"/>' +
                '<button class="addgeofencebutton" title="' + t('phonetrack', 'Use current map view as geofencing zone') + '">' +
                '<i class="fa fa-plus-circle" aria-hidden="true"></i> ' + t('phonetrack', 'Add zone') +
                '</button>' +
                '</div>' +
                '<ul class="geofencelist"></ul>' +
                '</div>';
            proximLink = ' <button class="toggleProxim" ' +
                'title="' + t('phonetrack', 'Device proximity notifications') + '">' +
                '<i class="fa fa-user-friends" aria-hidden="true"></i></button>';
            proximDiv = '<div class="proximDiv">' +
                '<div class="addproximdiv">' +
                '<p>' + t('phonetrack', 'Select a session, a device name and a distance, set the notification settings, then validate.') + ' ' +
                t('phonetrack', 'You will be notified when distance between devices gets bigger than high limit or smaller than low limit.') + '</p>' +
                '<label>' + t('phonetrack', 'Session') + ' </label> ' +
                '<select class="proximsession"></select>' +
                '<input type="text" class="devicename" value="' + t('phonetrack', 'Device name') + '"/>' +
                '<label for="lowlimit'+s+d+'"> ' + t('phonetrack', 'Low distance limit') + ' </label>' +
                '<input id="lowlimit'+s+d+'" class="lowlimit" type="number" value="500" min="1" max="20000000"/>' +
                t('phonetrack', 'meters') + '<br/>' +
                '<label for="highlimit'+s+d+'"> ' + t('phonetrack', 'High distance limit') + ' </label> ' +
                '<input id="highlimit'+s+d+'" class="highlimit" type="number" value="500" min="1" max="20000000"/>' +
                t('phonetrack', 'meters') + '<br/>' +
                '<label for="sendnotif'+s+d+'"> ' + t('phonetrack', 'Nextcloud notification') + ' </label>' +
                '<input type="checkbox" class="sendnotif" id="sendnotif'+s+d+'" checked/><br/>' +
                '<label for="sendemail'+s+d+'"> ' + t('phonetrack', 'Email notification') + ' </label>' +
                '<input type="checkbox" class="sendemail" id="sendemail'+s+d+'" checked/><br/>' +
                '<input type="text" id="proxemail'+s+d+'" class="proxemail" maxlength="500"' +
                'title="' + t('phonetrack', 'An empty value means the session owner\'s email address.') + "\n" +
                t('phonetrack', 'You can put multiple addresses separated by comas (,).') +'"/><br/>' +
                '<label for="urlclose'+s+d+'"><b>' + t('phonetrack', 'URL to request when devices get close') + '</b></label><br/>' +
                '<span>(<label for="urlclosepost'+s+d+'">' + t('phonetrack', 'Use POST method') +' </label>' +
                '<input type="checkbox" class="urlclosepost" id="urlclosepost'+s+d+'"/>)</span>' +
                '<input type="text" id="urlclose'+s+d+'" class="urlclose" maxlength="500" /><br/>' +
                '<label for="urlfar'+s+d+'"><b>' + t('phonetrack', 'URL to request when devices get far') + '</b> </label><br/>' +
                '<span>(<label for="urlfarpost'+s+d+'">' + t('phonetrack', 'Use POST method') +' </label>' +
                '<input type="checkbox" class="urlfarpost" id="urlfarpost'+s+d+'"/>)</span>' +
                '<input type="text" id="urlfar'+s+d+'" class="urlfar" maxlength="500" />' +
                '<button class="addproximbutton">' +
                '<i class="fa fa-plus-circle" aria-hidden="true"></i> ' + t('phonetrack', 'Add proximity notification') +
                '</button>' +
                '</div>' +
                '<ul class="proximlist"></ul>' +
                '</div>';
        }
        var urlPointToggle = getUrlParameter('pointToggle');
        var detailOnOff = 'off';
        if (point || (urlPointToggle && urlPointToggle !== '0')) {
            detailOnOff = 'on';
        }
        var detailLink = ' <button class="toggleDetail ' + detailOnOff + '" token="' + s + '" device="' + d + '" ' +
            'title="' + t('phonetrack', 'Toggle detail/edition points') + '">' +
            '<i class="fa fa-circle" aria-hidden="true"></i></button>';
        var urlLineToggle = getUrlParameter('lineToggle');
        var lineOnOff = 'off';
        if (line || (urlLineToggle && urlLineToggle !== '0')) {
            lineOnOff = 'on nc-theming-main-background';
        }
        var lineDeviceLink = ' <button class="toggleLineDevice ' + lineOnOff + '" ' +
            'token="' + s + '" device="' + d + '" ' +
            'title="' + t('phonetrack', 'Toggle lines') + '">' +
            '</button>';
        var zoomOnOff = 'off';
        if (zoom) {
            zoomOnOff = 'on nc-theming-main-background';
        }
        var autoZoomLink = ' <button class="toggleAutoZoomDevice ' + zoomOnOff + '" ' +
            'token="' + s + '" device="' + d + '" ' +
            'title="' + t('phonetrack', 'Follow this device (autozoom)') + '">' +
            '</button>';
        var nameLabelTxt;
        if (alias !== null && alias !== '') {
            nameLabelTxt = alias + ' (' + name + ')';
        }
        else {
            nameLabelTxt = name;
        }
        $('div.session[token="' + s + '"] ul.devicelist').append(
            '<li device="' + d + '" token="' + s + '">' +
                '<div>' +
                '<div class="devicecolor ' + shape + 'devicecolor devicecolor' + s + d + '"></div> ' +
                '<div class="deviceLabel" title="' +
                t('phonetrack', 'Center map on device') + '">' + escapeHTML(nameLabelTxt) + '</div> ' +
                renameInput +
                aliasInput +
                dropdowndevicebutton +
                dropdowndevicecontent +
                reaffectSelect +
                proximLink +
                geofencesLink +
                '<button class="zoomdevicebutton" title="' +
                t('phonetrack', 'Center map on device') + ' \'' + escapeHTML(name) + '\'">' +
                '<i class="fa fa-search" aria-hidden="true"></i></button>' +
                autoZoomLink +
                detailLink +
                lineDeviceLink +
                '</div><div style="clear: both;"></div>' +
                geofencesDiv +
                proximDiv +
                '</li>');

        // select shape
        if (shape !== '') {
            $('.session[token="' + s + '"] ul.devicelist li[device='+d+']').find('select[role=shapeselect]').val(shape);
        }

        // manage names/ids
        var intDid = parseInt(d);
        phonetrack.deviceNames[s][intDid] = escapeHTML(name);
        phonetrack.deviceAliases[s][intDid] = escapeHTML(alias || '');
        phonetrack.deviceIds[s][name] = intDid;

        phonetrack.sessionPointsLayers[s][d] = L.featureGroup();
        phonetrack.sessionPointsLayersById[s][d] = {};
        phonetrack.sessionPointsEntriesById[s][d] = {};
        phonetrack.sessionLatlngs[s][d] = [];
        phonetrack.sessionDisplayedLatlngs[s][d] = [];
        var linewidth = parseInt($('#linewidth').val()) || 5;
        phonetrack.sessionLineLayers[s][d] = L.featureGroup();
        var nameTxt;
        if (alias !== null && alias !== '') {
            nameTxt = alias + ' (' + name + ')';
        }
        else {
            nameTxt = name;
        }
        linetooltip = sessionname + ' | ' + nameTxt;
        phonetrack.sessionLineLayers[s][d].bindTooltip(
            linetooltip,
            {
                permanent: false,
                sticky: true,
                className: 'tooltip' + s + d
            }
        );
        var radius = parseInt($('#pointradius').val());
        var mletter = $('#markerletter').is(':checked');
        var letter = '';
        if (mletter) {
            if (alias !== null && alias !== '') {
                letter = alias[0];
            }
            else {
                letter = name[0];
            }
        }
        var markerIcon = L.divIcon({
            iconAnchor: [radius, radius],
            className: shape + 'marker color' + s + d,
            html: '<b>' + letter + '</b>'
        });
        var pointIcon = L.divIcon({
            iconAnchor: [radius, radius],
            className: shape + 'marker color' + s + d,
            html: ''
        });
        phonetrack.devicePointIcons[s][d] = pointIcon;

        phonetrack.sessionMarkerLayers[s][d] = L.marker([], {icon: markerIcon});
        phonetrack.sessionMarkerLayers[s][d].on('dragend', dragPointEnd);
        phonetrack.sessionMarkerLayers[s][d].session = s;
        phonetrack.sessionMarkerLayers[s][d].device = d;
        phonetrack.sessionMarkerLayers[s][d].pid = null;
        phonetrack.sessionMarkerLayers[s][d].setZIndexOffset(phonetrack.lastZindex++);
        if ($('#showtime').is(':checked')) {
            phonetrack.sessionMarkerLayers[s][d].on('mouseover', markerMouseover);
            phonetrack.sessionMarkerLayers[s][d].on('mouseout', markerMouseout);
        }
        phonetrack.sessionMarkerLayers[s][d].on('click', markerMouseClick);
        $('.session[token="' + s + '"] li[device='+d+']').find('.geofencesDiv').hide();
        $('.session[token="' + s + '"] li[device='+d+']').find('.proximDiv').hide();
        var llb, f, i, pr;
        for (i=0; i < geofences.length; i++) {
            f = geofences[i];
            llb = L.latLngBounds(L.latLng(f.latmin, f.lonmin), L.latLng(f.latmax, f.lonmax));
            addGeoFence(s, d, f.name, f.id, llb,
                        f.urlenter, f.urlleave,
                        f.urlenterpost, f.urlleavepost,
                        f.sendemail, f.emailaddr, f.sendnotif);
        }
        for (i=0; i < proxims.length; i++) {
            pr = proxims[i];
            addProxim(s, d, pr.id, pr.sname2, pr.deviceid2, pr.dname2,
                      pr.highlimit, pr.lowlimit, pr.urlclose, pr.urlfar,
                      pr.urlclosepost, pr.urlfarpost, pr.sendemail, pr.emailaddr, pr.sendnotif);
        }
    }

    // append entries ordered by timestamp
    function appendEntriesToDevice(s, d, entries, sessionname) {
        var lastEntryTimestamp, firstEntryTimestamp, device, i, e, entry, ts, m, j, coordsTmp, displayedLatlngs;
        var filter, radius, icon;
        var cutLines, line;
        var linewidth = parseInt($('#linewidth').val()) || 5;
        var linearrow = $('#linearrow').is(':checked');
        var linegradient = $('#linegradient').is(':checked');
        firstEntryTimestamp = parseInt(entries[0].timestamp);
        lastEntryTimestamp = parseInt(entries[entries.length-1].timestamp);
        device = d;
        if ((!phonetrack.lastTime[s].hasOwnProperty(device)) ||
            lastEntryTimestamp > phonetrack.lastTime[s][device])
        {
            phonetrack.lastTime[s][device] = lastEntryTimestamp;
        }
        if ((!phonetrack.firstTime[s].hasOwnProperty(device)) ||
            firstEntryTimestamp < phonetrack.firstTime[s][device])
        {
            phonetrack.firstTime[s][device] = firstEntryTimestamp;
        }

        /////////////////////////// LASTPOSONLY
        // we are in public page which should only display last point of each device
        if (pageIsPublic() && phonetrack.lastposonly === '1') {
            var lastEntryToAdd = entries[entries.length-1];
            var nbExistingEntries = phonetrack.sessionLatlngs[s][d].length;
            var lastExistingEntry = null;
            // we get the last existing entry only if there are entries
            if (nbExistingEntries > 0) {
                lastExistingEntry = phonetrack.sessionPointsEntriesById[s][d][phonetrack.sessionLatlngs[s][d][nbExistingEntries-1][2]];
            }
            // if there is nothing or new entry is more recent than last existing one :
            // only one pos : new entry
            if (nbExistingEntries === 0 || lastEntryToAdd.timestamp > lastExistingEntry.timestamp) {
                phonetrack.sessionPointsEntriesById[s][d][lastEntryToAdd.id] = lastEntryToAdd;
                phonetrack.sessionLatlngs[s][d] = [[lastEntryToAdd.lat, lastEntryToAdd.lon, lastEntryToAdd.id]];

                /////////// update FILTERED coordinates
                // increment lines, insert into displayed layer (sessionLineLayers)
                displayedLatlngs = filterList(phonetrack.sessionLatlngs[s][d], s, d);
                phonetrack.sessionLineLayers[s][d].clearLayers();
                delete phonetrack.sessionDisplayedLatlngs[s][d];
                phonetrack.sessionDisplayedLatlngs[s][d] = [displayedLatlngs];

                drawLine(s, d, [displayedLatlngs], linegradient, linewidth, linearrow);

                radius = parseInt($('#pointradius').val());
                icon = phonetrack.devicePointIcons[s][d];

                // reset point layers
                phonetrack.sessionPointsLayers[s][d].clearLayers();

                for (e = entries.length-1; e < entries.length; e++) {
                    entry = entries[e];
                    m = L.marker([entry.lat, entry.lon, entry.id],
                        {icon: icon}
                    );
                    m.session = s;
                    m.device = d;
                    m.pid = entry.id;
                    m.on('click', markerMouseClick);
                    m.on('mouseover', markerMouseover);
                    m.on('mouseout', markerMouseout);
                    m.on('dragend', dragPointEnd);
                    phonetrack.sessionPointsLayersById[s][d][entry.id] = m;
                    filter = filterEntry(entry);
                    if (filter) {
                        phonetrack.sessionPointsLayers[s][d].addLayer(m);
                        // no dragging
                    }
                }
            }
        }
        ///////////////////////////// NORMAL
        else {
            /////////// update global coordinates (not filtered)
            // we keep the same i because our points are already ordered
            i = 0;
            for (e = 0; e < entries.length; e++) {
                entry = entries[e];
                // add the entry to global dict
                phonetrack.sessionPointsEntriesById[s][d][entry.id] = entry;
                ts = entry.timestamp;
                while (i < phonetrack.sessionLatlngs[s][d].length &&
                    // ouch ;-)
                    ts > phonetrack.sessionPointsEntriesById[s][d][phonetrack.sessionLatlngs[s][d][i][2]].timestamp
                ) {
                    i++;
                }
                phonetrack.sessionLatlngs[s][d].splice(i, 0, [entry.lat, entry.lon, entry.id]);
                i++;
            }

            /////////// update FILTERED coordinates
            // increment lines, insert into displayed layer (sessionLineLayers)
            displayedLatlngs = filterList(phonetrack.sessionLatlngs[s][d], s, d);
            cutLines = segmentLines(displayedLatlngs, s, d);
            phonetrack.sessionLineLayers[s][d].clearLayers();
            delete phonetrack.sessionDisplayedLatlngs[s][d];
            phonetrack.sessionDisplayedLatlngs[s][d] = cutLines;

            drawLine(s, d, cutLines, linegradient, linewidth, linearrow);

            radius = parseInt($('#pointradius').val());
            icon = phonetrack.devicePointIcons[s][d];

            for (e = 0; e < entries.length; e++) {
                entry = entries[e];
                m = L.marker([entry.lat, entry.lon, entry.id],
                    {icon: icon}
                );
                m.session = s;
                m.device = d;
                m.pid = entry.id;
                m.on('click', markerMouseClick);
                m.on('mouseover', markerMouseover);
                m.on('mouseout', markerMouseout);
                m.on('dragend', dragPointEnd);
                phonetrack.sessionPointsLayersById[s][d][entry.id] = m;
                filter = filterEntry(entry);
                if (filter) {
                    phonetrack.sessionPointsLayers[s][d].addLayer(m);
                    // dragging
                    if (!pageIsPublic() && !isSessionShared(s) && $('#dragcheck').is(':checked')) {
                        if (phonetrack.map.hasLayer(phonetrack.sessionPointsLayers[s][d])) {
                            m.dragging.enable();
                        }
                    }
                }
            }
        }
    }

    // draw lines for a device, with arrows and gradient if needed
    function drawLine(s, d, linesCoords, linegradient, linewidth, linearrow) {
        var line, i, j;
        for (i = 0; i < linesCoords.length; i++) {
            if (linegradient) {
                var coordsTmp = [];
                for (j=0; j < linesCoords[i].length; j++) {
                    coordsTmp.push([linesCoords[i][j][0], linesCoords[i][j][1], j]);
                }
                line = L.hotline(coordsTmp, {
                    weight: linewidth,
                    outlineWidth: 2,
                    outlineColor: phonetrack.sessionColors[s + d],
                    palette: {0.0: 'white', 1.0: 'black'},
                    min: 0,
                    max: linesCoords[i].length-1
                });
            }
            else {
                line = L.polyline(linesCoords[i], {weight: linewidth, className: 'poly' + s + d});
            }
            phonetrack.sessionLineLayers[s][d].addLayer(line);

            if (linearrow && linesCoords[i].length > 1) {
                var arrows = L.polylineDecorator(line);
                arrows.setPatterns([{
                    offset: 30,
                    repeat: 100,
                    symbol: L.Symbol.arrowHead({
                        pixelSize: 15 + linewidth,
                        polygon: false,
                        pathOptions: {
                            stroke: true,
                            className: 'poly' + s + d,
                            opacity: 1,
                            weight: parseInt(linewidth * 0.6)
                        }
                    })
                }]);
                phonetrack.sessionLineLayers[s][d].addLayer(arrows);
            }
        }
    }

    function markerMouseClick(e) {
        var s = e.target.session;
        var d = e.target.device;
        if (!pageIsPublic() &&
            !isSessionShared(s) &&
            $('.session[token='+s+'] .devicelist li[device="'+d+'"] .toggleDetail').hasClass('on')
        ) {
            e.target.unbindPopup();
            var pid = e.target.pid;
            var entry = phonetrack.sessionPointsEntriesById[s][d][pid];
            var sessionname = getSessionName(s);
            e.target.bindPopup(getPointPopup(s, d, entry, sessionname), {closeOnClick: false});
            e.target.openPopup();
        }
    }

    function markerMouseover(e) {
        var d = e.target.device;
        var s = e.target.session;
        var pid = e.target.pid;
        var sessionname = getSessionName(s);
        var entry = phonetrack.sessionPointsEntriesById[s][d][pid];
        if ($('#acccirclecheck').is(':checked')) {
            var latlng = e.target.getLatLng();
            var acc = parseInt(phonetrack.sessionPointsEntriesById[s][d][pid].accuracy) || -1;
            if (acc !== -1) {
                phonetrack.currentPrecisionCircle = L.circle(latlng, {radius: acc});
                phonetrack.map.addLayer(phonetrack.currentPrecisionCircle);
            }
            else {
                phonetrack.currentPrecisionCircle = null;
            }
        }
        // tooltips
        var pointtooltip = getPointTooltipContent(entry, sessionname, s);
        e.target.bindTooltip(pointtooltip, {className: 'tooltip' + s + d});
        e.target.openTooltip();
    }

    function markerMouseout(e) {
        if (phonetrack.currentPrecisionCircle !== null &&
            phonetrack.map.hasLayer(phonetrack.currentPrecisionCircle)
        ) {
            phonetrack.map.removeLayer(phonetrack.currentPrecisionCircle);
            phonetrack.currentPrecisionCircle = null;
        }
        e.target.unbindTooltip();
        e.target.closeTooltip();
    }

    function isSessionActive(s) {
        return $('.session[token=' + s + '] .watchbutton i').hasClass('fa-toggle-on');
    }

    function isSessionShared(s) {
        return (phonetrack.isSessionShared[s]);
    }

    function editPointDB(token, deviceid, pointid, lat, lon, alt, acc, sat, bat, timestamp, useragent, speed, bearing) {
        var req = {
            token: token,
            deviceid: deviceid,
            pointid: pointid,
            timestamp: timestamp,
            lat: lat,
            lon: lon,
            alt: alt,
            acc: acc,
            bat: bat,
            sat: sat,
            useragent: useragent,
            speed: speed,
            bearing: bearing
        };
        var url = OC.generateUrl('/apps/phonetrack/updatePoint');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                updatePointMap(token, deviceid, pointid, lat, lon, alt, acc, sat, bat, timestamp, useragent, speed, bearing);
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'The point you want to edit does not exist or you\'re not allowed to edit it'));
            }
        }).always(function() {
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to edit point'));
        });
    }

    function updatePointMap(token, deviceid, pointid, lat, lon, alt, acc, sat, bat, timestamp, useragent, speed, bearing) {
        var perm = $('#showtime').is(':checked');
        var linearrow = $('#linearrow').is(':checked');
        var linegradient = $('#linegradient').is(':checked');
        var linewidth = parseInt($('#linewidth').val()) || 5;
        var i, j, coordsTmp;

        var sessionname = getSessionName(token);
        var entry = phonetrack.sessionPointsEntriesById[token][deviceid][pointid];
        // point needs to be moved ?
        var oldlat = parseFloat(entry.lat);
        var oldlon = parseFloat(entry.lon);
        var move = (oldlat !== lat || oldlon !== lon);
        var oldtimestamp = timestamp;
        var dateChanged = (oldtimestamp !== parseInt(entry.timestamp));
        var markerIsNotAnymore = false;
        entry.timestamp = timestamp;
        entry.lat = lat;
        entry.lon = lon;
        entry.altitude = alt;
        entry.batterylevel = bat;
        entry.satellites = sat;
        entry.accuracy = acc;
        entry.useragent = useragent;
        entry.speed = speed;
        entry.bearing = bearing;

        var filter = filterEntry(entry);
        var cutLines, line;

        // move line point
        if (move || dateChanged) {
            phonetrack.sessionPointsLayersById[token][deviceid][pointid].setLatLng([lat, lon, pointid]);
            if (!filter) {
                phonetrack.sessionPointsLayers[token][deviceid].removeLayer(
                    phonetrack.sessionPointsLayersById[token][deviceid][pointid]
                );
            }
        }
        // set new line latlngs if moved or date was modified
        if (move || dateChanged) {
            //var latlngs = phonetrack.sessionLineLayers[token][deviceid].getLatLngs();
            // we work on complete latlngs, not just the displayed one (that can be filtered)
            var latlngs = phonetrack.sessionLatlngs[token][deviceid];
            var newlatlngs = [];
            i = 0;
            // we copy until we get to the right place to insert moved point
            while (i < latlngs.length &&
                      ( (parseInt(pointid) === parseInt(latlngs[i][2])) ||
                         (timestamp > parseInt(phonetrack.sessionPointsEntriesById[token][deviceid][latlngs[i][2]].timestamp))
                      )
            ) {
                // we don't copy the edited point
                if (parseInt(pointid) !== parseInt(latlngs[i][2])) {
                    // copy
                    newlatlngs.push([latlngs[i][0], latlngs[i][1], latlngs[i][2]]);
                }
                i++;
            }
            // put the edited point
            newlatlngs.push([lat, lon, pointid]);
            // finish the copy
            while (i < latlngs.length) {
                if (parseInt(pointid) !== parseInt(latlngs[i][2])) {
                    // copy
                    newlatlngs.push([latlngs[i][0], latlngs[i][1], latlngs[i][2]]);
                }
                i++;
            }
            phonetrack.sessionLatlngs[token][deviceid] = newlatlngs;
            // modify line
            var filteredlatlngs = filterList(newlatlngs, token, deviceid);
            cutLines = segmentLines(filteredlatlngs, token, deviceid);
            phonetrack.sessionLineLayers[token][deviceid].clearLayers();
            delete phonetrack.sessionDisplayedLatlngs[token][deviceid];
            phonetrack.sessionDisplayedLatlngs[token][deviceid] = cutLines;

            drawLine(token, deviceid, cutLines, linegradient, linewidth, linearrow);

            // lastTime is independent from filters
            phonetrack.lastTime[token][deviceid] =
                phonetrack.sessionPointsEntriesById[token][deviceid][newlatlngs[newlatlngs.length - 1][2]].timestamp;
            phonetrack.firstTime[token][deviceid] =
                phonetrack.sessionPointsEntriesById[token][deviceid][newlatlngs[0][2]].timestamp;
        }

        updateMarker(token, deviceid, sessionname);
        if ($('#togglestats').is(':checked')) {
            updateStatTable();
        }
        changeTooltipStyle();

        phonetrack.map.closePopup();
    }

    function deletePointsDB(s, d, pidlist) {
        var token = s;
        var deviceid = d;
        var req = {
            token: token,
            deviceid: deviceid,
            pointids: pidlist
        };
        var url = OC.generateUrl('/apps/phonetrack/deletePoints');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                deletePointsMap(s, d, pidlist);
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'The point you want to delete does not exist or you\'re not allowed to delete it'));
            }
        }).always(function() {
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to delete point'));
        });
    }

    function deletePointsMap(s, d, pidlist) {
        var perm = $('#showtime').is(':checked');
        var linearrow = $('#linearrow').is(':checked');
        var linegradient = $('#linegradient').is(':checked');
        var linewidth = parseInt($('#linewidth').val()) || 5;
        var i, lat, lng, p, pid, m, j, coordsTmp;
        var cutLines, line;
        var sn = getSessionName(s);
        for (i = 0; i < pidlist.length; i++) {
            pid = pidlist[i];
            // remove associated point from sessionPointsLayers
            m = phonetrack.sessionPointsLayersById[s][d][pid];
            phonetrack.sessionPointsLayers[s][d].removeLayer(m);
            delete phonetrack.sessionPointsLayersById[s][d][pid];
            delete phonetrack.sessionPointsEntriesById[s][d][pid];
        }

        // remove point in the line
        var latlngs = phonetrack.sessionLatlngs[s][d];
        var newlatlngs = [];
        i = 0;
        for (i = 0; i < latlngs.length; i++) {
            if (pidlist.indexOf(latlngs[i][2]) === -1) {
                newlatlngs.push([latlngs[i][0], latlngs[i][1], latlngs[i][2]]);
            }
        }

        phonetrack.sessionLatlngs[s][d] = newlatlngs;
        var filteredlatlngs = filterList(newlatlngs, s, d);
        cutLines = segmentLines(filteredlatlngs, s, d);
        phonetrack.sessionLineLayers[s][d].clearLayers();
        delete phonetrack.sessionDisplayedLatlngs[s][d];
        phonetrack.sessionDisplayedLatlngs[s][d] = cutLines;

        drawLine(s, d, cutLines, linegradient, linewidth, linearrow);

        updateMarker(s, d, sn);

        // update lastTime : new last point time (independent from filter)
        if (newlatlngs.length > 0) {
            phonetrack.lastTime[s][d] =
                phonetrack.sessionPointsEntriesById[s][d][newlatlngs[newlatlngs.length - 1][2]].timestamp;
            phonetrack.firstTime[s][d] =
                phonetrack.sessionPointsEntriesById[s][d][newlatlngs[0][2]].timestamp;
        }
        else {
            // there is no point left for this device
            delete phonetrack.lastTime[s][d];
            delete phonetrack.firstTime[s][d];
        }
        if ($('#togglestats').is(':checked')) {
            updateStatTable();
        }

        phonetrack.map.closePopup();
    }

    function addPointDB(plat='', plon='', palt=null, pacc=null, psat=null, pbat=null, pmoment='', pspeed=null, pbearing=null) {
        var lat, lon, alt, acc, sat, bat, mom, speed, bearing;
        var tab = $('#addPointTable');
        var token = $('#addPointSession option:selected').attr('token');
        var devicename = $('#addPointDevice').val();
        lat = plat;
        lon = plon;
        alt = palt;
        acc = pacc;
        sat = psat;
        bat = pbat;
        mom = pmoment;
        speed = pspeed;
        bearing = pbearing;
        var timestamp = mom.unix();
        var req = {
            token: token,
            devicename: devicename,
            timestamp: timestamp,
            lat: lat,
            lon: lon,
            alt: alt,
            acc: acc,
            bat: bat,
            sat: sat,
            useragent: t('phonetrack', 'Manually added'),
            speed: speed,
            bearing: bearing
        };
        var url = OC.generateUrl('/apps/phonetrack/addPoint');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                // add the point on the map only if the session was displayed at least once
                if (phonetrack.sessionLineLayers.hasOwnProperty(token)) {
                    addPointMap(response.pointid, lat, lon, alt, acc, sat, bat, speed, bearing, timestamp, response.deviceid);
                }
            }
            else if (response.done === 2) {
                OC.Notification.showTemporary(t('phonetrack', 'Impossible to add this point'));
            }
            else if (response.done === 5) {
                OC.Notification.showTemporary(t('phonetrack', 'User quota was reached'));
            }
        }).always(function() {
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to add point'));
        });
    }

    function addPointMap(id, lat, lon, alt, acc, sat, bat, speed, bearing, timestamp, deviceid) {
        var perm = $('#showtime').is(':checked');
        var linearrow = $('#linearrow').is(':checked');
        var linegradient = $('#linegradient').is(':checked');
        var linewidth = parseInt($('#linewidth').val()) || 5;
        var tab = $('#addPointTable');
        var token = $('#addPointSession option:selected').attr('token');
        var devicename = $('#addPointDevice').val();
        var useragent = t('phonetrack', 'Manually added');
        var pid = parseInt(id);
        var cutLines, line;

        var entry = {id: pid};
        entry.deviceid = deviceid;
        entry.timestamp = timestamp;
        entry.lat = lat;
        entry.lon = lon;
        entry.altitude = alt;
        entry.batterylevel = bat;
        entry.satellites = sat;
        entry.accuracy = acc;
        entry.useragent = useragent;
        entry.speed = speed;
        entry.bearing = bearing;

        var filter = filterEntry(entry);

        var sessionname = getSessionName(token);

        // add device if it does not exist
        if (! phonetrack.sessionLineLayers[token].hasOwnProperty(deviceid)) {
            addDevice(token, deviceid, sessionname, '', devicename);
            appendEntriesToDevice(token, deviceid, [entry], sessionname);
        }
        // insert entry correctly ;)
        else {
            // add line point
            var icon = phonetrack.devicePointIcons[token][deviceid];
            var m = L.marker(
                [entry.lat, entry.lon, entry.id],
                {icon: icon}
            );
            m.session = token;
            m.device = deviceid;
            m.pid = entry.id;
            m.on('mouseover', markerMouseover);
            m.on('mouseout', markerMouseout);
            m.on('dragend', dragPointEnd);
            m.on('click', markerMouseClick);
            phonetrack.sessionPointsEntriesById[token][deviceid][entry.id] = entry;
            phonetrack.sessionPointsLayersById[token][deviceid][entry.id] = m;
            if (filter) {
                phonetrack.sessionPointsLayers[token][deviceid].addLayer(m);

                // manage draggable
                // if points are displayed
                if (phonetrack.map.hasLayer(phonetrack.sessionPointsLayers[token][deviceid])) {
                    // if dragging is allowed
                    if (!pageIsPublic() && !isSessionShared(token) && $('#dragcheck').is(':checked')) {
                        m.dragging.enable();
                    }
                }
            }

            // update line

            //var latlngs = phonetrack.sessionLineLayers[token][deviceid].getLatLngs();
            var latlngs = phonetrack.sessionLatlngs[token][deviceid];
            var newlatlngs = [];
            var i = 0;
            var j, coordsTmp;
            // we copy until we get to the right place to insert new point
            while (i < latlngs.length &&
                   timestamp > parseInt(phonetrack.sessionPointsEntriesById[token][deviceid][latlngs[i][2]].timestamp)
            ) {
                // copy
                newlatlngs.push([latlngs[i][0], latlngs[i][1], latlngs[i][2]]);
                i++;
            }
            // put the edited point
            newlatlngs.push([lat, lon, pid]);
            // finish the copy
            while (i < latlngs.length) {
                // copy
                newlatlngs.push([latlngs[i][0], latlngs[i][1], latlngs[i][2]]);
                i++;
            }
            // modify line
            phonetrack.sessionLatlngs[token][deviceid] = newlatlngs;
            var filteredlatlngs = filterList(newlatlngs, token, deviceid);
            cutLines = segmentLines(filteredlatlngs, token, deviceid);
            phonetrack.sessionLineLayers[token][deviceid].clearLayers();
            delete phonetrack.sessionDisplayedLatlngs[token][deviceid];
            phonetrack.sessionDisplayedLatlngs[token][deviceid] = cutLines;

            drawLine(token, deviceid, cutLines, linegradient, linewidth, linearrow);

            // update lastTime
            phonetrack.lastTime[token][deviceid] =
                phonetrack.sessionPointsEntriesById[token][deviceid][newlatlngs[newlatlngs.length - 1][2]].timestamp;
            phonetrack.firstTime[token][deviceid] =
                phonetrack.sessionPointsEntriesById[token][deviceid][newlatlngs[0][2]].timestamp;
        }
        updateMarker(token, deviceid, sessionname);
        if ($('#togglestats').is(':checked')) {
            updateStatTable();
        }
    }

    function getPointPopup(s, d, entry, sn) {
        var dateval = '';
        var hourval = '';
        var minval = '';
        var secval = '';
        if (entry.timestamp) {
            var mom = moment.unix(parseInt(entry.timestamp));
            dateval = mom.format('YYYY-MM-DD');
            hourval = mom.format('HH');
            minval = mom.format('mm');
            secval = mom.format('ss');
        }
        var res = '<table class="editPoint" pid="' + entry.id + '"' +
           ' token="' + s + '" deviceid="' + d + '" sessionname="' + sn + '">';
        res = res + '<tr title="' + t('phonetrack', 'Date') + '">';
        res = res + '<td><i class="fa fa-calendar-alt" style="font-size: 20px;"></i></td>';
        res = res + '<td><input role="date" type="date" value="' + dateval + '"/></td>';
        res = res + '</tr><tr title="' + t('phonetrack', 'Time') + '">';
        res = res + '<td><i class="far fa-clock" style="font-size: 20px;"></i></td>';
        res = res + '<td><input role="hour" type="number" value="' + hourval + '" min="0" max="23"/>h' +
            '<input role="minute" type="number" value="' + minval + '" min="0" max="59"/>' +
            'min<input role="second" type="number" value="' + secval + '" min="0" max="59"/>sec</td>';
        res = res + '</tr><tr title="' + t('phonetrack', 'Altitude') + '">';
        res = res + '<td><i class="fa fa-chart-area" style="font-size: 20px;"></td>';
        res = res + '<td><input role="altitude" type="number" value="' + entry.altitude + '" min="-1"/>m</td>';
        res = res + '</tr><tr title="' + t('phonetrack', 'Precision') + '">';
        res = res + '<td><i class="far fa-dot-circle" style="font-size: 20px;"></td>';
        res = res + '<td><input role="precision" type="number" value="' + entry.accuracy + '" min="-1"/>m</td>';
        res = res + '</tr><tr title="' + t('phonetrack', 'Speed') + '">';
        res = res + '<td><i class="fa fa-tachometer-alt" style="font-size: 20px;"></td>';
        var speed_kmph = entry.speed;
        if (entry.speed && parseInt(entry.speed) !== -1) {
            speed_kmph = parseFloat(entry.speed) * 3.6;
            speed_kmph = speed_kmph.toFixed(3);
        }
        res = res + '<td><input role="speed" type="number" value="' + speed_kmph + '" min="-1" step="0.01"/>km/h</td>';
        res = res + '</tr><tr title="' + t('phonetrack', 'Bearing') + '">';
        res = res + '<td><i class="fa fa-compass" style="font-size: 20px;"></td>';
        res = res + '<td><input role="bearing" type="number" value="' + entry.bearing + '" min="-1" max="360"/>°</td>';
        res = res + '</tr><tr title="' + t('phonetrack', 'Satellites') + '">';
        res = res + '<td><i class="fa fa-signal" style="font-size: 20px;"></td>';
        res = res + '<td><input role="satellites" type="number" value="' + entry.satellites + '" min="-1"/></td>';
        res = res + '</tr><tr title="' + t('phonetrack', 'Battery') + '">';
        res = res + '<td><i class="fa fa-battery-half" style="font-size: 20px;"></i></td>';
        res = res + '<td><input role="battery" type="number" value="' + entry.batterylevel + '" min="-1" max="100"/>%</td>';
        res = res + '</tr><tr title="' + t('phonetrack', 'User-agent') + '">';
        res = res + '<td><i class="fa fa-mobile-alt" style="font-size: 35px;"></i></td>';
        res = res + '<td><input role="useragent" type="text" value="' + entry.useragent + '"/></td>';
        res = res + '</tr><tr title="' + t('phonetrack', 'lat : lng') + '">';
        res = res + '<td><i class="fa fa-map-marker-alt" style="font-size: 20px;"></td>';
        res = res + '<td><input role="latlng" type="text" value="' +
            parseFloat(entry.lat).toFixed(5) + ' : ' + parseFloat(entry.lon).toFixed(5) + '" readonly/></td>';
        res = res + '</tr><tr title="' + t('phonetrack', 'DMS coords') + '">';
        res = res + '<td><i class="fa fa-globe" style="font-size: 20px;"></td>';
        res = res + '<td><input role="dms" type="text" value="' + convertDMS(entry.lat, entry.lon) + '" readonly/></td>';
        res = res + '</tr>';
        res = res + '</table>';
        res = res + '<button class="valideditpoint"><i class="fa fa-save" aria-hidden="true"></i> ' + t('phonetrack', 'Save') + '</button>';
        res = res + '<button class="deletepoint"><i class="fa fa-trash" aria-hidden="true" style="color:red;"></i> ' + t('phonetrack', 'Delete') + '</button>';
        res = res + '<br/><button class="movepoint"><i class="fa fa-arrows-alt" aria-hidden="true"></i> ' + t('phonetrack', 'Move') + '</button>';
        res = res + '<button class="canceleditpoint"><i class="fa fa-undo" aria-hidden="true" style="color:red;"></i> ' + t('phonetrack', 'Cancel') + '</button>';
        return res;
    }

    function getPointTooltipContent(entry, sn, s) {
        var mom;
        var name = getDeviceName(s, entry.deviceid);
        var alias = getDeviceAlias(s, entry.deviceid);
        var nameLabelTxt;
        if (alias !== null && alias !== '') {
            nameLabelTxt = alias + ' (' + name + ')';
        }
        else {
            nameLabelTxt = name;
        }
        var pointtooltip = sn + ' | ' + nameLabelTxt;
        if (entry.timestamp) {
            mom = moment.unix(parseInt(entry.timestamp));
            pointtooltip = pointtooltip + '<br/>' +
                mom.format('YYYY-MM-DD HH:mm:ss (Z)');
        }
        if ($('#tooltipshowelevation').is(':checked') && !isNaN(entry.altitude) && entry.altitude !== null) {
            pointtooltip = pointtooltip + '<br/>' +
                t('phonetrack', 'Altitude') + ' : ' + parseFloat(entry.altitude).toFixed(2) + 'm';
        }
        if ($('#tooltipshowaccuracy').is(':checked') && !isNaN(entry.accuracy) && entry.accuracy !== null &&
            parseFloat(entry.accuracy) >= 0) {
            pointtooltip = pointtooltip + '<br/>' +
                t('phonetrack', 'Precision') + ' : ' + entry.accuracy + 'm';
        }
        if ($('#tooltipshowspeed').is(':checked') && !isNaN(entry.speed) && entry.speed !== null &&
            parseFloat(entry.speed) >= 0) {
            var speed_kmph = parseFloat(entry.speed) * 3.6;
            speed_kmph = speed_kmph.toFixed(3);
            pointtooltip = pointtooltip + '<br/>' +
                t('phonetrack', 'Speed') + ' : ' + speed_kmph + 'km/h';
        }
        if ($('#tooltipshowbearing').is(':checked') && !isNaN(entry.bearing) && entry.bearing !== null &&
            parseFloat(entry.bearing) >= 0 && parseFloat(entry.bearing) <= 360) {
            pointtooltip = pointtooltip + '<br/>' +
                t('phonetrack', 'Bearing') + ' : ' + entry.bearing + '°';
        }
        if ($('#tooltipshowsatellites').is(':checked') && !isNaN(entry.satellites) && entry.satellites !== null &&
            parseInt(entry.satellites) >= 0) {
            pointtooltip = pointtooltip + '<br/>' +
                t('phonetrack', 'Satellites') + ' : ' + entry.satellites;
        }
        if ($('#tooltipshowbattery').is(':checked') && !isNaN(entry.batterylevel) && entry.batterylevel !== null &&
            parseFloat(entry.batterylevel) >= 0) {
            pointtooltip = pointtooltip + '<br/>' +
                t('phonetrack', 'Battery') + ' : ' + entry.batterylevel + '%';
        }
        if ($('#tooltipshowuseragent').is(':checked') && entry.useragent !== '' && entry.useragent !== null && entry.useragent !== 'nothing') {
            pointtooltip = pointtooltip + '<br/>' +
                t('phonetrack', 'User-agent') + ' : ' + escapeHTML(entry.useragent);
        }

        return pointtooltip;
    }

    function showHideSelectedSessions() {
        var token, d, displayedPointsLayers, sessionname;
        var displayedMarkers = [];
        var viewLines = $('#viewmove').is(':checked');
        $('.watchbutton i').each(function() {
            token = $(this).parent().parent().parent().attr('token');
            sessionname = getSessionName(token);
            if ($(this).hasClass('fa-toggle-on')) {
                for (d in phonetrack.sessionLineLayers[token]) {
                    if (viewLines) {
                        if (!phonetrack.map.hasLayer(phonetrack.sessionLineLayers[token][d])) {
                            // if linedevice activated
                            if ($('.session[token='+token+'] .devicelist li[device="'+d+'"] .toggleLineDevice').hasClass('on')) {
                                phonetrack.map.addLayer(phonetrack.sessionLineLayers[token][d]);
                            }
                        }
                    }
                    else {
                        if (phonetrack.map.hasLayer(phonetrack.sessionLineLayers[token][d])) {
                            phonetrack.map.removeLayer(phonetrack.sessionLineLayers[token][d]);
                        }
                    }
                }
                for (d in phonetrack.sessionPointsLayers[token]) {
                    if (!phonetrack.map.hasLayer(phonetrack.sessionPointsLayers[token][d])) {
                        if ($('.session[token='+token+'] .devicelist li[device="'+d+'"] .toggleDetail').hasClass('on')) {
                            phonetrack.map.addLayer(phonetrack.sessionPointsLayers[token][d]);
                            // manage draggable
                            if (!pageIsPublic() && !isSessionShared(token) && $('#dragcheck').is(':checked')) {
                                phonetrack.sessionPointsLayers[token][d].eachLayer(function(l) {
                                    l.dragging.enable();
                                });
                            }
                        }
                    }
                }
                for (d in phonetrack.sessionMarkerLayers[token]) {
                    updateMarker(token, d, sessionname);
                    displayedPointsLayers = phonetrack.sessionPointsLayers[token][d].getLayers();
                    if (displayedPointsLayers.length !== 0) {
                        displayedMarkers.push(phonetrack.sessionMarkerLayers[token][d].getLatLng());
                    }
                }
            }
            else {
                if (phonetrack.sessionLineLayers.hasOwnProperty(token)) {
                    for (d in phonetrack.sessionLineLayers[token]) {
                        if (phonetrack.map.hasLayer(phonetrack.sessionLineLayers[token][d])) {
                            phonetrack.map.removeLayer(phonetrack.sessionLineLayers[token][d]);
                        }
                    }
                }
                if (phonetrack.sessionPointsLayers.hasOwnProperty(token)) {
                    for (d in phonetrack.sessionPointsLayers[token]) {
                        if (phonetrack.map.hasLayer(phonetrack.sessionPointsLayers[token][d])) {
                            phonetrack.map.removeLayer(phonetrack.sessionPointsLayers[token][d]);
                        }
                    }
                }
                if (phonetrack.sessionMarkerLayers.hasOwnProperty(token)) {
                    for (d in phonetrack.sessionMarkerLayers[token]) {
                        if (phonetrack.map.hasLayer(phonetrack.sessionMarkerLayers[token][d])) {
                            phonetrack.map.removeLayer(phonetrack.sessionMarkerLayers[token][d]);
                        }
                    }
                }
            }

        });

        // ZOOM
        if ($('#autozoom').is(':checked') && displayedMarkers.length > 0) {
            zoomOnDisplayedMarkers();
        }
        // show/hide last marker tooltips
        changeTooltipStyle();
    }

    function zoomOnDisplayedMarkers(selectedSessionToken='') {
        var token, d, lls, i;
        var pointLatlngList = [];
        var layerList = [];
        var boundsToZoomOn;

        // first we check if there are devices selected for zoom
        var devicesToFollow = {};
        var nbDevicesToFollow = 0;
        $('.toggleAutoZoomDevice.on').each(function() {
            // we only take those for session which are watched
            var viewSessionCheck = $(this).parent().parent().parent().parent().find('.watchbutton i');
            var token = $(this).attr('token');
            if (viewSessionCheck.hasClass('fa-toggle-on') && (selectedSessionToken === '' || token === selectedSessionToken)) {
                var device = $(this).attr('device');
                if (!devicesToFollow.hasOwnProperty(token)) {
                    devicesToFollow[token] = [];
                }
                devicesToFollow[token].push(device);
                nbDevicesToFollow++;
            }
        });

        $('.watchbutton i').each(function() {
            token = $(this).parent().parent().parent().attr('token');
            if ($(this).hasClass('fa-toggle-on') && (selectedSessionToken === '' || token === selectedSessionToken)) {
                for (d in phonetrack.sessionMarkerLayers[token]) {
                    // if no device is followed => all devices are taken
                    // if some devices are followed, just take them
                    if (nbDevicesToFollow === 0 ||
                        (devicesToFollow.hasOwnProperty(token) && devicesToFollow[token].indexOf(d) !== -1)
                    ) {
                        if (phonetrack.map.hasLayer(phonetrack.sessionMarkerLayers[token][d])) {
                            pointLatlngList.push(phonetrack.sessionMarkerLayers[token][d].getLatLng());
                        }
                        if (phonetrack.map.hasLayer(phonetrack.sessionPointsLayers[token][d])) {
                            layerList.push(phonetrack.sessionPointsLayers[token][d]);
                        }
                        if (phonetrack.map.hasLayer(phonetrack.sessionLineLayers[token][d])) {
                            layerList.push(phonetrack.sessionLineLayers[token][d]);
                        }
                    }
                }
            }
        });

        if (pointLatlngList.length > 0) {
            boundsToZoomOn = L.latLngBounds(pointLatlngList);
            if (layerList.length > 0) {
                for (i=0; i < layerList.length; i++) {
                    boundsToZoomOn.extend(layerList[i].getBounds());
                }
            }

            // ZOOM
            phonetrack.map.fitBounds(boundsToZoomOn, {
                //animate: true,
                maxZoom: 15,
                paddingTopLeft: [parseInt($('#sidebar').css('width')), 50],
                paddingBottomRight: [50, 50]
            });
        }
        else {
            OC.Notification.showTemporary(t('phonetrack', 'Impossible to zoom, there is no point to zoom on for this session'));
        }
    }

    function changeTooltipStyle() {
        var perm = $('#showtime').is(':checked');
        var s, d, m, t, sessionname, entry, pointtooltip;
        for (s in phonetrack.sessionMarkerLayers) {
            for (d in phonetrack.sessionMarkerLayers[s]) {
                m = phonetrack.sessionMarkerLayers[s][d];
                // if there is a marker for this device
                if (m && m.pid) {
                    m.closeTooltip();
                    // if option is set, show permanent tooltip for last marker
                    if (perm) {
                        // is not affected by mouseover anymore
                        m.off('mouseover', markerMouseover);
                        m.off('mouseout', markerMouseout);
                        // bind permanent tooltip
                        entry = phonetrack.sessionPointsEntriesById[s][d][m.pid];
                        sessionname = getSessionName(s);
                        pointtooltip = getPointTooltipContent(entry, sessionname, s);
                        m.bindTooltip(pointtooltip, {permanent: perm, offset: offset, className: 'tooltip' + s + d});
                    }
                    else {
                        m.on('mouseover', markerMouseover);
                        m.on('mouseout', markerMouseout);
                    }
                }
            }
        }
    }

    function importSession(path) {
        if (!endsWith(path, '.gpx') && !endsWith(path, '.kml')) {
            OC.Notification.showTemporary(t('phonetrack', 'File extension must be \'.gpx\' or \'.kml\' to be imported'));
        }
        else {
            showLoadingAnimation();
            var req = {
                path: path
            };
            var url = OC.generateUrl('/apps/phonetrack/importSession');
            $.ajax({
                type: 'POST',
                url: url,
                data: req,
                async: true
            }).done(function (response) {
                if (response.done === 1) {
                    addSession(response.token, response.sessionName, response.publicviewtoken, [], response.devices, 1);
                }
                else if (response.done === 2) {
                    OC.Notification.showTemporary(t('phonetrack', 'Failed to create imported session'));
                }
                else if (response.done === 3) {
                    OC.Notification.showTemporary(
                        t('phonetrack', 'Failed to import session') + '. ' +
                        t('phonetrack', 'File is not readable')
                    );
                }
                else if (response.done === 4) {
                    OC.Notification.showTemporary(
                        t('phonetrack', 'Failed to import session') + '. ' +
                        t('phonetrack', 'File does not exist')
                    );
                }
                else if (response.done === 5) {
                    OC.Notification.showTemporary(
                        t('phonetrack', 'Failed to import session') + '. ' +
                        t('phonetrack', 'Malformed XML file')
                    );
                }
                else if (response.done === 6) {
                    OC.Notification.showTemporary(
                        t('phonetrack', 'Failed to import session') + '. ' +
                        t('phonetrack', 'There is no device to import in submitted file')
                    );
                }
            }).always(function() {
                hideLoadingAnimation();
            }).fail(function() {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to import session'));
            });
        }
    }

    function saveAction(name, token, targetPath, filename) {
        showLoadingAnimation();
        var req = {
            name: name,
            token: token,
            target: targetPath+'/'+filename
        };
        var url = OC.generateUrl('/apps/phonetrack/export');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done) {
                OC.Notification.showTemporary(t('phonetrack', 'Session successfully exported in') +
                    ' ' + targetPath + '/' + filename);
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to export session'));
            }
        }).always(function() {
            hideLoadingAnimation();
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to export session'));
        });
    }

    function locationFound(e) {
        if (pageIsPublicWebLog() && $('#logme').is(':checked')) {
            var deviceid = $('#logmedeviceinput').val();
            var lat, lon, alt, acc, timestamp;
            lat = e.latitude;
            lon = e.longitude;
            alt = e.altitude;
            acc = e.accuracy;
            timestamp = e.timestamp;
            var req = {
                lat: lat,
                lon: lon,
                alt: alt,
                acc: acc,
                timestamp: timestamp,
                useragent: 'browser'
            };
            var url = OC.generateUrl('/apps/phonetrack/logPost/' + phonetrack.token + '/' + deviceid);
            $.ajax({
                type: 'POST',
                url: url,
                data: req,
                async: true
            }).done(function (response) {
                //console.log(response);
            }).always(function() {
            }).fail(function() {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to log position'));
            });
        }
    }

    function toggleAutoZoomDevice(elem) {
        if (elem.hasClass('on')) {
            elem.addClass('off').removeClass('on nc-theming-main-background');
        }
        else {
            elem.addClass('on nc-theming-main-background').removeClass('off');
        }
    }

    function toggleLineDevice(elem) {
        var viewmove = $('#viewmove').is(':checked');
        var d = elem.parent().parent().attr('device');
        var s = elem.parent().parent().attr('token');
        var id;

        // line points
        if (viewmove) {
            if (phonetrack.map.hasLayer(phonetrack.sessionLineLayers[s][d])) {
                phonetrack.sessionLineLayers[s][d].remove();
                elem.addClass('off').removeClass('on nc-theming-main-background');
            }
            else{
                phonetrack.sessionLineLayers[s][d].addTo(phonetrack.map);
                elem.addClass('on nc-theming-main-background').removeClass('off');
            }
        }
        else {
            if (elem.hasClass('on')) {
                elem.addClass('off').removeClass('on nc-theming-main-background');
            }
            else {
                elem.addClass('on nc-theming-main-background').removeClass('off');
            }
        }
    }

    function toggleDetailDevice(elem) {
        var d = elem.parent().parent().attr('device');
        var s = elem.parent().parent().attr('token');
        var id;

        // line points
        if (phonetrack.map.hasLayer(phonetrack.sessionPointsLayers[s][d])) {
            phonetrack.sessionPointsLayers[s][d].eachLayer(function(l) {
                l.dragging.disable();
            });
            phonetrack.sessionPointsLayers[s][d].remove();
            elem.addClass('off').removeClass('on');
        }
        else{
            phonetrack.sessionPointsLayers[s][d].addTo(phonetrack.map);
            elem.addClass('on').removeClass('off');
            // manage draggable
            if (!pageIsPublic() && !isSessionShared(s) && $('#dragcheck').is(':checked')) {
                phonetrack.sessionPointsLayers[s][d].eachLayer(function(l) {
                    l.dragging.enable();
                });
            }
        }
        // marker
        if (!pageIsPublic() &&
            !isSessionShared(s) &&
            phonetrack.map.hasLayer(phonetrack.sessionMarkerLayers[s][d])
        ) {
            if (elem.hasClass('off')) {
                phonetrack.sessionMarkerLayers[s][d].dragging.disable();
            }
            else {
                if ($('#dragcheck').is(':checked')) {
                    // if marker is displayed (not filtered)
                    phonetrack.sessionMarkerLayers[s][d].dragging.enable();
                }
            }
        }
    }

    function zoomOnDevice(elem, t) {
        var id, dd, b, l;
        var perm = $('#showtime').is(':checked');
        var viewmove = $('#viewmove').is(':checked');
        var d = elem.parent().parent().attr('device');
        var s = elem.parent().parent().attr('token');
        var m = phonetrack.sessionMarkerLayers[s][d];

        if (phonetrack.sessionPointsLayers[s][d].getLayers().length > 0) {
            // if we show movement lines :
            // bring it to front, show/hide points
            // get correct zoom bounds
            if (phonetrack.map.hasLayer(phonetrack.sessionLineLayers[s][d])) {
                l = phonetrack.sessionLineLayers[s][d];
                // does not work with polylineDecorator
                //l.bringToFront();
                b = l.getBounds();
            }
            else if (phonetrack.map.hasLayer(phonetrack.sessionPointsLayers[s][d])) {
                l = phonetrack.sessionPointsLayers[s][d];
                l.bringToFront();
                b = l.getBounds();
            }
            else {
                b = L.latLngBounds(m.getLatLng(), m.getLatLng());
            }

            // covers all problematic cases
            if (b.getSouthWest().equals(b.getNorthWest())) {
                phonetrack.map.setView(m.getLatLng(), 15, {animate: true});
            }
            else {
                phonetrack.map.fitBounds(b, {
                    animate: true,
                    maxZoom: 15,
                    paddingTopLeft: [parseInt($('#sidebar').css('width')), 50],
                    paddingBottomRight: [50, 50]
                });
            }

            for (id in phonetrack.sessionPointsLayersById[s][d]) {
                phonetrack.sessionPointsLayersById[s][d][id].setZIndexOffset(phonetrack.lastZindex);
            }
            phonetrack.lastZindex += 10;

            m.setZIndexOffset(phonetrack.lastZindex++);
        }
        else {
            OC.Notification.showTemporary(t('phonetrack', 'Impossible to zoom, there is no point to zoom on for this device'));
        }
    }

    function hideAllDropDowns() {
        var dropdowns = document.getElementsByClassName('dropdown-content');
        var reafdropdowns = document.getElementsByClassName('reaffectDeviceDiv');
        var openDropdown;
        var i;
        for (i = 0; i < dropdowns.length; i++) {
            openDropdown = dropdowns[i];
            if (openDropdown.classList.contains('show')) {
                openDropdown.classList.remove('show');
            }
        }
        for (i = 0; i < reafdropdowns.length; i++) {
            openDropdown = reafdropdowns[i];
            if (openDropdown.classList.contains('show')) {
                openDropdown.classList.remove('show');
            }
        }
    }

    function addNameReservationDb(token, devicename) {
        var req = {
            token: token,
            devicename: devicename
        };
        var url = OC.generateUrl('/apps/phonetrack/addNameReservation');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                addNameReservation(token, devicename, response.nametoken);
            }
            else if (response.done === 2) {
                OC.Notification.showTemporary(t('phonetrack', '\'{n}\' is already reserved', {'n': devicename}));
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to reserve \'{n}\'', {'n': devicename}));
            }
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to reserve device name'));
        });
    }

    function addNameReservation(token, devicename, nametoken) {
        var li = '<li name="' + escapeHTML(devicename) + '"><label>' +
            escapeHTML(devicename) + ' : '+ escapeHTML(nametoken) + '</label>' +
            '<button class="deletereservedname"><i class="fa fa-trash"></i></li>';
        $('.session[token="' + token + '"]').find('.namereservlist').append(li);
        $('.session[token="' + token + '"]').find('.addnamereserv').val('');
    }

    function deleteNameReservationDb(token, devicename) {
        var req = {
            token: token,
            devicename: devicename
        };
        var url = OC.generateUrl('/apps/phonetrack/deleteNameReservation');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                var li = $('.session[token="' + token + '"]').find('.namereservlist li[name=' + devicename + ']');
                li.fadeOut('slow', function() {
                    li.remove();
                });
            }
            else if (response.done === 2) {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to delete reserved name') +
                '. ' + t('phonetrack', 'This device does not exist'));
            }
            else if (response.done === 3) {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to delete reserved name') +
                '. ' + t('phonetrack', 'This device name is not reserved, please reload this page'));
            }
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to delete reserved name'));
        });
    }

    function addUserShareDb(token, username) {
        var req = {
            token: token,
            username: username
        };
        var url = OC.generateUrl('/apps/phonetrack/addUserShare');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                addUserShare(token, username);
            }
            else if (response.done === 4) {
                OC.Notification.showTemporary(t('phonetrack', 'User does not exist'));
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to add user share'));
            }
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to add user share'));
        });
    }

    function addUserShare(token, username) {
        var li = '<li username="' + escapeHTML(username) + '"><label>' +
            t('phonetrack', 'Shared with {u}', {'u': username}) + '</label>' +
            '<button class="deleteusershare"><i class="fa fa-trash"></i></li>';
        $('.session[token="' + token + '"]').find('.usersharelist').append(li);
        $('.session[token="' + token + '"]').find('.addusershare').val('');
    }

    function deleteUserShareDb(token, username) {
        var req = {
            token: token,
            username: username
        };
        var url = OC.generateUrl('/apps/phonetrack/deleteUserShare');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                var li = $('.session[token="' + token + '"]').find('.usersharelist li[username=' + username + ']');
                li.fadeOut('slow', function() {
                    li.remove();
                });
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to delete user share'));
            }
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to delete user share'));
        });
    }

    function setPublicShareGeofencifyDb(token, sharetoken, geofencify) {
        var req = {
            token: token,
            sharetoken: sharetoken,
            geofencify: geofencify
        };
        var url = OC.generateUrl('/apps/phonetrack/setPublicShareGeofencify');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                OC.Notification.showTemporary(t('phonetrack', 'Public share has been successfully modified'));
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to modify public share'));
            }
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to modify public share'));
        });
    }

    function setPublicShareLastOnlyDb(token, sharetoken, lastposonly) {
        var req = {
            token: token,
            sharetoken: sharetoken,
            lastposonly: lastposonly
        };
        var url = OC.generateUrl('/apps/phonetrack/setPublicShareLastOnly');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                OC.Notification.showTemporary(t('phonetrack', 'Public share has been successfully modified'));
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to modify public share'));
            }
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to modify public share'));
        });
    }

    function setPublicShareDeviceDb(token, sharetoken, devicename) {
        var req = {
            token: token,
            sharetoken: sharetoken,
            devicename: devicename
        };
        var url = OC.generateUrl('/apps/phonetrack/setPublicShareDevice');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                OC.Notification.showTemporary(t('phonetrack', 'Device name restriction has been successfully set'));
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to set public share device name restriction'));
            }
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to set public share device name restriction'));
        });
    }

    function addGeoFenceDb(token, device, fencename, mapbounds, urlenter, urlleave, urlenterpost, urlleavepost, sendemail, emailaddr, sendnotif) {
        var latmin = mapbounds.getSouth();
        var latmax = mapbounds.getNorth();
        var lonmin = mapbounds.getWest();
        var lonmax = mapbounds.getEast();
        var req = {
            token: token,
            device: device,
            fencename: fencename,
            latmin: latmin,
            latmax: latmax,
            lonmin: lonmin,
            lonmax: lonmax,
            urlenter: urlenter,
            urlleave: urlleave,
            urlenterpost: urlenterpost,
            urlleavepost: urlleavepost,
            sendemail: sendemail,
            emailaddr: emailaddr,
            sendnotif: sendnotif
        };
        var url = OC.generateUrl('/apps/phonetrack/addGeofence');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1 || response.done === 4) {
                addGeoFence(token, device, fencename, response.fenceid, mapbounds, urlenter, urlleave, urlenterpost, urlleavepost, sendemail, emailaddr, sendnotif);
                if (response.done === 4) {
                    OC.Notification.showTemporary(t('phonetrack', 'Warning : User email and server admin email must be set to receive geofencing alerts.'));
                }
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to add geofencing zone'));
            }
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to add geofencing zone'));
        });
    }

    function addGeoFence(token, device, fencename, fenceid, llb, urlenter='', urlleave='', urlenterpost=0, urlleavepost=0, sendemail=1, emailaddr='', sendnotif=1) {
        var enterpostTxt = '';
        var leavepostTxt = '';
        if (parseInt(urlenterpost) !== 0) {
            enterpostTxt = '(POST)';
        }
        if (parseInt(urlleavepost) !== 0) {
            leavepostTxt = '(POST)';
        }
        var urlentertxt = '';
        if (urlenter && urlenter !== '') {
            urlentertxt = t('phonetrack', 'URL to request when entering') + ' ' + enterpostTxt + ' : ' + escapeHTML(urlenter) + '\n';
        }
        var urlleavetxt = '';
        if (urlleave && urlleave !== '') {
            urlleavetxt = t('phonetrack', 'URL to request when leaving') + ' ' + leavepostTxt + ' : ' + escapeHTML(urlleave) + '\n';
        }

        var sendemailTxt = t('phonetrack', 'no');
        if (parseInt(sendemail) !== 0) {
            sendemailTxt = t('phonetrack', 'yes');
        }
        var sendnotifTxt = t('phonetrack', 'no');
        if (parseInt(sendnotif) !== 0) {
            sendnotifTxt = t('phonetrack', 'yes');
        }
        var li = '<li fenceid="' + fenceid + '" latmin="' + llb.getSouth() + '" latmax="' + llb.getNorth() + '"' +
            'lonmin="' + llb.getWest() + '" lonmax="'+llb.getEast()+'" ' +
            'title="' +
            urlentertxt +
            urlleavetxt +
            t('phonetrack', 'Nextcloud notification') + ' : ' + sendnotifTxt + '\n' +
            t('phonetrack', 'Email notification') + ' : ' + sendemailTxt + '\n' +
            t('phonetrack', 'Email address(es)') + ' : ' + escapeHTML(emailaddr || '') +
            '">' +
            '<label class="geofencelabel">'+escapeHTML(fencename || '')+'</label>' +
            '<button class="deletegeofencebutton"><i class="fa fa-trash"></i></button>' +
            '<button class="zoomgeofencebutton"><i class="fa fa-search"></i></button>' +
            '</li>';
        $('.session[token="' + token + '"] .devicelist li[device='+device+'] .geofencesDiv .geofencelist').append(li);
    }

    function deleteGeoFenceDb(token, device, fenceid) {
        var req = {
            token: token,
            device: device,
            fenceid: fenceid
        };
        var url = OC.generateUrl('/apps/phonetrack/deleteGeofence');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                var li = $('.session[token="' + token + '"] .devicelist li[device=' + device + '] .geofencelist').find('li[fenceid=' + fenceid + ']');
                li.fadeOut('slow', function() {
                    li.remove();
                });
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to delete geofencing zone'));
            }
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to delete geofencing zone'));
        });
    }

    function addProximDb(token, device, sid, sname, dname, highlimit=500, lowlimit=500, urlclose='', urlfar='', urlclosepost=0, urlfarpost=0, sendemail=1, emailaddr='', sendnotif=1) {
        var req = {
            token: token,
            device: device,
            sid: sid,
            dname: dname,
            lowlimit: lowlimit,
            highlimit: highlimit,
            urlclose: urlclose,
            urlfar: urlfar,
            urlclosepost: urlclosepost,
            urlfarpost: urlfarpost,
            sendemail: sendemail,
            emailaddr: emailaddr,
            sendnotif: sendnotif
        };
        var url = OC.generateUrl('/apps/phonetrack/addProxim');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1 || response.done === 4) {
                addProxim(token, device, response.proximid, sname, response.targetdeviceid, dname, highlimit, lowlimit, urlclose, urlfar, urlclosepost, urlfarpost, sendemail, emailaddr, sendnotif);
                if (response.done === 4) {
                    OC.Notification.showTemporary(t('phonetrack', 'Warning : User email and server admin email must be set to receive proximity alerts.'));
                }
            }
            else if (response.done === 3 || response.done === 5) {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to add proximity alert'));
                OC.Notification.showTemporary(t('phonetrack', 'Device or session does not exist'));
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to add proximity alert'));
            }
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to add proximity alert'));
        });
    }

    function addProxim(token, device, proximid, sname, did, dname, highlimit=500, lowlimit=500, urlclose='', urlfar='', urlclosepost=0, urlfarpost=0, sendemail=1, emailaddr='', sendnotif=1) {
        var closepostTxt = '';
        var farpostTxt = '';
        if (parseInt(urlclosepost) !== 0) {
            closepostTxt = '(POST)';
        }
        if (parseInt(urlfarpost) !== 0) {
            farpostTxt = '(POST)';
        }
        var sendemailTxt = t('phonetrack', 'no');
        if (parseInt(sendemail) !== 0) {
            sendemailTxt = t('phonetrack', 'yes');
        }
        var sendnotifTxt = t('phonetrack', 'no');
        if (parseInt(sendnotif) !== 0) {
            sendnotifTxt = t('phonetrack', 'yes');
        }
        var li = '<li proximid="' + proximid + '"' +
            'title="' + t('phonetrack', 'URL to request when devices get close') + ' ' + closepostTxt + ' : ' + escapeHTML(urlclose || '') + '\n' +
            t('phonetrack', 'URL to request when devices get far') + ' ' + farpostTxt + ' : ' + escapeHTML(urlfar || '') + '\n' +
            t('phonetrack', 'Nextcloud notification') + ' : ' + sendnotifTxt + '\n' +
            t('phonetrack', 'Email notification') + ' : ' + sendemailTxt + '\n' +
            t('phonetrack', 'Email address(es)') + ' : ' + escapeHTML(emailaddr || '') + '\n' +
            t('phonetrack', 'Low distance limit : {nbmeters}m', {'nbmeters': lowlimit}) + '\n' +
            t('phonetrack', 'High distance limit : {nbmeters}m', {'nbmeters': highlimit}) +
            '">' +
            '<label class="proximlabel">'+escapeHTML(sname + ' -> ' + dname)+'</label>' +
            '<button class="deleteproximbutton"><i class="fa fa-trash"></i></button>' +
            '</li>';
        $('.session[token="' + token + '"] .devicelist li[device='+device+'] .proximDiv .proximlist').append(li);
    }

    function deleteProximDb(token, device, proximid) {
        var req = {
            token: token,
            device: device,
            proximid: proximid
        };
        var url = OC.generateUrl('/apps/phonetrack/deleteProxim');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                var li = $('.session[token="' + token + '"] .devicelist li[device=' + device + '] .proximlist').find('li[proximid=' + proximid + ']');
                li.fadeOut('slow', function() {
                    li.remove();
                });
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to delete proximity alert'));
            }
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to delete proximity alert'));
        });
    }

    function addPublicSessionShareDb(token) {
        var req = {
            token: token,
        };
        var url = OC.generateUrl('/apps/phonetrack/addPublicShare');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                addPublicSessionShare(token, response.sharetoken, response.filters);
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to add public share'));
            }
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to add public share'));
        });
    }

    function addPublicSessionShare(token, sharetoken, filters, name='', lastposonly=0, geofencify=0) {
        var geofencifyChecked = '';
        if (geofencify === '1') {
            geofencifyChecked = 'checked';
        }
        var lastposonlyChecked = '';
        if (lastposonly === '1') {
            lastposonlyChecked = 'checked';
        }
        var pl = $('#pubviewline').is(':checked') ? '1' : '0';
        var pp = $('#pubviewpoint').is(':checked') ? '1' : '0';
        var linePointParams = $.param({lineToggle: pl, pointToggle: pp});

        var publicurl = window.location.origin +
            OC.generateUrl('/apps/phonetrack/publicSessionWatch/' + sharetoken + '?') + linePointParams;
        var li = '<li class="filteredshare" filteredtoken="' + escapeHTML(sharetoken) + '" title="' +
            filtersToTxt(filters) + '">' +
            '<input type="text" class="publicFilteredShareUrl" value="' + publicurl + '"/>' +
            '<button class="deletePublicFilteredShare"><i class="fa fa-trash"></i></button><br/>' +
            '<label>' + t('phonetrack', 'Show this device only') + ' : </label>' +
            '<input type="text" role="device" value="' + escapeHTML(name || '') + '"/>' +
            '<br/><label for="fil'+sharetoken+'">' + t('phonetrack', 'Show last positions only') + ' : </label>' +
            '<input id="fil'+sharetoken+'" type="checkbox" role="lastposonly" ' + lastposonlyChecked + '/>' +
            '<br/><label for="geo'+sharetoken+'">' + t('phonetrack', 'Simplify positions to nearest geofencing zone center') + ' : </label>' +
            '<input id="geo'+sharetoken+'" type="checkbox" role="geofencify" ' + geofencifyChecked + '/>' +
            '</li>';
        $('.session[token="' + token + '"]').find('.publicfilteredsharelist').append(li);
    }

    function filtersToTxt(fstr) {
        var fjson = $.parseJSON(fstr);
        var res = '';
        var k;
        for (k in fjson) {
            if (k === 'tsmin' || k === 'tsmax') {
                res = res + k + ' : ' + moment.unix(fjson[k]).format('YYYY-MM-DD HH:mm:ss (Z)') + '\n';
            }
            else {
                res = res + k + ' : ' + fjson[k] + '\n';
            }
        }
        if (res === '') {
            res = t('phonetrack', 'No filters');
        }
        return res;
    }

    function deletePublicSessionShareDb(token, sharetoken) {
        var req = {
            token: token,
            sharetoken: sharetoken
        };
        var url = OC.generateUrl('/apps/phonetrack/deletePublicShare');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            if (response.done === 1) {
                var li = $('.session[token="' + token + '"]').find('.publicfilteredsharelist li[filteredtoken=' + sharetoken + ']');
                li.fadeOut('slow', function() {
                    li.remove();
                });
            }
            else {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to delete public share'));
            }
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to delete public share'));
        });
    }

    function addUserAutocompletion(input) {
        var req = {
        };
        var url = OC.generateUrl('/apps/phonetrack/getUserList');
        $.ajax({
            type: 'POST',
            url: url,
            data: req,
            async: true
        }).done(function (response) {
            input.autocomplete({
                source: response.users
            });
        }).fail(function() {
            OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to get user list'));
        });
    }

    function updateLinePointUrlParams() {
        var pl = $('#pubviewline').is(':checked') ? '1' : '0';
        var pp = $('#pubviewpoint').is(':checked') ? '1' : '0';
        var linePointParams = $.param({lineToggle: pl, pointToggle: pp});

        var sessionDiv, publicWebLogInput, publicWatchInput,
            jqInputs, inputList, value, i, j, elem, s;
        for (s in phonetrack.sessionLineLayers) {
            if (!isSessionShared(s)) {
                inputList = [];
                sessionDiv = $('div.session[token='+s+']');
                publicWebLogInput = sessionDiv.find('input[role=publicTrackurl]');
                publicWatchInput = sessionDiv.find('input[role=publicWatchUrl]');
                inputList.push(publicWebLogInput);
                inputList.push(publicWatchInput);

                jqInputs = $('div.session[token='+s+'] input.publicFilteredShareUrl');
                for (j = 0; j < jqInputs.length; j++) {
                    inputList.push($(jqInputs[j]));
                }

                for (i = 0; i < inputList.length; i++) {
                    elem = inputList[i];
                    value = elem.val().split('?')[0];
                    elem.val(value + '?' + linePointParams);
                }
            }
        }
    }

    function updateStatTable() {
        var s, d, id, dist, time, i, li, coordsList, ll, t1, t2;
        var nbsec, years, days, hours, minutes, seconds, nbspeeds, totspeed, entry, avgspeed, maxspeed;
        var table = '';
        for (s in phonetrack.sessionLineLayers) {
            // if session is watched
            if ($('div.session[token='+s+'] .watchbutton i').hasClass('fa-toggle-on')) {
                table = table + '<b>' + getSessionName(s) + ' :</b>';
                table = table + '<table class="stattable"><tr><th>' +
                    t('phonetrack', 'device name') + '</th><th>' +
                    t('phonetrack', 'distance (km)') + '</th><th>' +
                    t('phonetrack', 'duration') + '</th><th>' +
                    t('phonetrack', '#points') + '</th><th>' +
                    t('phonetrack', 'avg speed (km/h)') + '</th><th>' +
                    t('phonetrack', 'max speed (km/h)') + '</th>' +
                    '</tr>';
                for (d in phonetrack.sessionLineLayers[s]) {
                    nbspeeds = 0;
                    totspeed = 0;
                    avgspeed = '-';
                    maxspeed = 0;
                    dist = 0;
                    nbsec = 0;
                    coordsList = phonetrack.sessionDisplayedLatlngs[s][d];
                    for (li = 0; li < coordsList.length; li++) {
                        ll = coordsList[li];
                        // distance
                        for (i = 1; i < ll.length; i++) {
                            dist = dist + phonetrack.map.distance(ll[i-1], ll[i]);
                        }
                        // speed
                        for (i = 0; i < ll.length; i++) {
                            entry = phonetrack.sessionPointsEntriesById[s][d][ll[i][2]];
                            if (entry.speed !== null) {
                                totspeed = totspeed + entry.speed;
                                nbspeeds++;
                                if (entry.speed > maxspeed) {
                                    maxspeed = entry.speed;
                                }
                            }
                        }

                        // duration
                        if (ll.length > 1) {
                            t1 = moment.unix(phonetrack.sessionPointsEntriesById[s][d][ll[0][2]].timestamp);
                            t2 = moment.unix(phonetrack.sessionPointsEntriesById[s][d][ll[ll.length-1][2]].timestamp);
                            nbsec = nbsec + t2.diff(t1, 'seconds');
                        }
                    }

                    // process speed
                    if (nbspeeds > 0) {
                        avgspeed = totspeed / nbspeeds * 3.6;
                        avgspeed = avgspeed.toFixed(3);
                        maxspeed = maxspeed * 3.6;
                        maxspeed = maxspeed.toFixed(3);
                    }
                    else {
                        maxspeed = '-';
                    }

                    // process duration
                    if (nbsec > 0) {
                        years = 0;
                        days = 0;
                        // if more than one year
                        if (nbsec >= 31536000) {
                            years = Math.floor(nbsec / 31536000);
                        }
                        // if more than one day
                        if (nbsec >= 86400) {
                            days = Math.floor((nbsec % 31536000) / 86400);
                        }
                        hours = Math.floor((nbsec % 86400) / 3600);
                        minutes = Math.floor((nbsec % 3600) / 60);
                        seconds = Math.floor(nbsec % 60);
                    }
                    else {
                        years = days = hours = minutes = seconds = 0;
                    }

                    table = table + '<tr><td class="statcolor' + s +
                        d + '">' + getDeviceName(s, d) + '</td>';
                    table = table + '<td>'+formatDistance(dist)+'</td>';
                    table = table + '<td>';
                    if (years > 0) {
                        table = table + years + ' ' + t('phonetrack', 'years') + ' ';
                    }
                    if (days > 0) {
                        table = table + days + ' ' + t('phonetrack', 'days') + ' ';
                    }
                    table = table + pad(hours) + ':' + pad(minutes) + ':' + pad(seconds) + '</td>';
                    table = table + '<td>' + phonetrack.sessionPointsLayers[s][d].getLayers().length + '</td>';
                    table = table + '<td>'+avgspeed+'</td>';
                    table = table + '<td>'+maxspeed+'</td>';
                    table = table + '</tr>';
                }
                table = table + '</table>';
            }
        }
        $('#statdiv').html(table);
    }

    function formatDistance(d) {
        return (d / 1000).toFixed(2);
    }

    function clickUrlHelp(logger, url, sessionName) {
        var loggerName, content;
        content = '';
        if (logger === 'osmand') {
            loggerName = 'OsmAnd';
            content = t('phonetrack', 'In OsmAnd, go to \'Plugins\' in the main menu, then activate \'Trip recording\' plugin and go to its settings.') +
            ' ' + t('phonetrack', 'Copy the URL below into the \'Online tracking web address\' field.');
        }
        else if (logger === 'gpslogger') {
            loggerName = 'GpsLogger';
            content = t('phonetrack', 'In GpsLogger, go to \'Logging details\' in the sidebar menu, then activate \'Log to custom URL\'.') +
                ' ' + t('phonetrack', 'Copy the URL below into the \'URL\' field.');
        }
        else if (logger === 'owntracks') {
            loggerName = 'Owntracks';
            content = t('phonetrack', 'In the Owntracks preferences menu, go to \'Connections\'.') +
                ' ' + t('phonetrack', 'Change the connection Mode to \'Private HTTP\', Copy the URL below into the \'Host\' field.') +
                ' ' + t('phonetrack', 'Leave settings under \'Identification\' blank as they are not required.');
        }
        else if (logger === 'ulogger') {
            loggerName = 'Ulogger';
            content = t('phonetrack', 'In Ulogger, go to settings menu and copy the URL below into the \'Server URL\' field.') +
                ' ' + t('phonetrack', 'Set \'User name\' and \'Password\' mandatory fields to any value as they will be ignored by PhoneTrack.') +
                ' ' + t('phonetrack', 'Activate \'Live synchronization\'.');
        }
        else if (logger === 'traccar') {
            loggerName = 'Traccar';
            content = t('phonetrack', 'In Traccar client, copy the URL below into the \'server URL\' field.');
        }
        else if (logger === 'locusmap') {
            loggerName = 'LocusMap';
            content = t('phonetrack', 'In LocusMap, copy the URL below into the \'server URL\' field. It works with POST and GET methods.');
        }
        else if (logger === 'get') {
            loggerName = 'GET logger';
            content = t('phonetrack', 'You can log with any other client with a simple HTTP request.');
            content = content + ' ' + t('phonetrack', 'Make sure the logging system sets values for at least \'timestamp\', \'lat\' and \'lon\' GET parameters.');
        }
        else if (logger === 'opengts') {
            content = t('phonetrack', 'Use this URL as the server URL in your OpenGTS compatible logging app.');
            loggerName = t('phonetrack', 'OpenGTS compatible logger');
        }
        else if (logger === 'publicTrack') {
            loggerName = t('phonetrack', 'the browser');
            var logLabel = t('phonetrack', 'Log my position in this session');
            content = t('phonetrack', 'Visit this URL with a web browser and check "{loglabel}".', {loglabel: logLabel});
        }
        var title = t('phonetrack',
            'Configure {loggingApp} for logging to session \'{sessionName}\'',
            {sessionName: sessionName, loggingApp: loggerName}
        );

        $('#trackurlinput').show().val(url);
        $('#trackurlhint').show();
        $('#trackurlqrcode').html('');
        var img = new Image();
        // wait for the image to be loaded to generate the QRcode
        img.onload = function(){
            var qr = kjua({
                text: url,
                crisp: false,
                render: 'canvas',
                minVersion: 6,
                ecLevel: 'H',
                size: 210,
                back: "#ffffff",
                fill: phonetrack.themeColorDark,
                rounded: 100,
                quiet: 1,
                mode: 'image',
                mSize: 20,
                mPosX: 50,
                mPosY: 50,
                image: img,
                label: 'no label',
            });
            $('#trackurlqrcode').append(qr);
        };
        img.onerror = function() {
            var qr = kjua({
                text: url,
                crisp: false,
                render: 'canvas',
                minVersion: 6,
                ecLevel: 'H',
                size: 210,
                back: "#ffffff",
                fill: phonetrack.themeColorDark,
                rounded: 100,
                quiet: 1,
                mode: 'label',
                mSize: 10,
                mPosX: 50,
                mPosY: 50,
                image: img,
                label: logger,
                fontcolor: '#000000',
            });
            $('#trackurlqrcode').append(qr);
        };
        // dirty trick to get image URL from css url()... Anyone knows better ?
        var srcurl = $('#dummylogo').css('content').replace('url("', '').replace('")', '');
        if (logger !== 'opengts') {
            srcurl = srcurl.replace('phonetrack.png', 'ext_logos/'+logger+'.png');
        }
        img.src = srcurl;
        $('#trackurllabel').text(content);

        $('#trackurldialog').dialog({
            title: title,
            width: 500,
            height: 450,
            open: function(event, ui) {
                $('.ui-dialog-titlebar-close', ui.dialog | ui).html('<i class="far fa-times-circle"></i>');
            }
        });
        $('#trackurlinput').select();
    }

    function updateProximSessionsSelect(tog) {
        var prSel = tog.parent().parent().find('.proximDiv select.proximsession');
        prSel.html('');
        var s, sname;
        for (s in phonetrack.deviceNames) {
            sname = getSessionName(s);
            prSel.append('<option value="' + s + '" name="' + sname + '">' + sname + '</option>');
        }
    }

    function zoomongeofence(par) {
        var latmin = par.attr('latmin');
        var latmax = par.attr('latmax');
        var lonmin = par.attr('lonmin');
        var lonmax = par.attr('lonmax');
        var llb = L.latLngBounds(L.latLng(latmin, lonmin), L.latLng(latmax, lonmax));
        phonetrack.map.fitBounds(llb, {
            //padding: [10, 10],
            paddingTopLeft: [parseInt($('#sidebar').css('width')) + 30, 50],
            paddingBottomRight: [50, 50]
        });

        var bounds = [[latmin, lonmin], [latmax, lonmax]];
        var rec = L.rectangle(bounds, {color: "#ff7800", weight: 1}).addTo(phonetrack.map);

        setTimeout(function() {phonetrack.map.removeLayer(rec);}, 5000);
    }

    //////////////// MAIN /////////////////////

    $(document).ready(function() {
        phonetrack.pageIsPublicWebLog = (document.URL.indexOf('/publicWebLog') !== -1);
        phonetrack.pageIsPublicSessionWatch = (document.URL.indexOf('/publicSessionWatch') !== -1);
        if ( !pageIsPublic() ) {
            restoreOptions();
        }
        else {
            main();
        }
    });

    function main() {
        phonetrack.username = $('p#username').html();
        phonetrack.token = $('p#token').html();
        load_map();

        $('body').on('change', '#autozoomcheck', function() {
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }
        });
        $('body').on('change', '#arrowcheck', function() {
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }
        });

        // get key events
        document.onkeydown = checkKey;

        // Custom tile server management
        $('body').on('click', '#tileserverlist button', function(e) {
            deleteTileServer($(this).parent(), 'tile');
        });
        $('#addtileserver').click(function() {
            addTileServer('tile');
        });
        $('body').on('click', '#overlayserverlist button', function(e) {
            deleteTileServer($(this).parent(), 'overlay');
        });
        $('#addoverlayserver').click(function() {
            addTileServer('overlay');
        });

        $('body').on('click', '#tilewmsserverlist button', function(e) {
            deleteTileServer($(this).parent(), 'tilewms');
        });
        $('#addtileserverwms').click(function() {
            addTileServer('tilewms');
        });
        $('body').on('click', '#overlaywmsserverlist button', function(e) {
            deleteTileServer($(this).parent(), 'overlaywms');
        });
        $('#addoverlayserverwms').click(function() {
            addTileServer('overlaywms');
        });

        $('body').on('click','h3.customtiletitle', function(e) {
            var forAttr = $(this).attr('for');
            if ($('#'+forAttr).is(':visible')) {
                $('#'+forAttr).slideUp();
                $(this).find('i').removeClass('fa-angle-double-up').addClass('fa-angle-double-down');
            }
            else{
                $('#'+forAttr).slideDown();
                $(this).find('i').removeClass('fa-angle-double-down').addClass('fa-angle-double-up');
            }
        });

        // in public link and public folder link :
        // hide compare button and custom tiles server management
        if (pageIsPublic()) {
            $('div#tileserverlist').hide();
            $('div#tileserveradd').hide();
        }

        // show/hide options
        $('body').on('click','h3#optiontitle', function(e) {
            if ($('#optionscontent').is(':visible')) {
                $('#optionscontent').slideUp();
                $('#optiontoggle').html('<i class="fa fa-angle-double-down"></i>');
                $('#optiontoggle').animate({'left': 0}, 'slow');
            }
            else{
                $('#optionscontent').slideDown();
                $('#optiontoggle').html('<i class="fa fa-angle-double-up"></i>');
                var offset = parseInt($('#optiontitle').css('width')) -
                    parseInt($('#optiontoggle').css('width')) -
                    parseInt($('#optiontitletext').css('width')) - 5;
                $('#optiontoggle').animate({'left': offset}, 'slow');
            }
        });

        $('#showcreatesession').click(function() {
            var newsessiondiv = $('#newsessiondiv');
            if (newsessiondiv.is(':visible')) {
                newsessiondiv.slideUp('slow');
            }
            else {
                newsessiondiv.slideDown('slow');
            }
        });

        $('#sessionnameinput').on('keyup', function(e) {
            if (e.key === 'Enter') {
                createSession();
                $('#newsessiondiv').slideUp('slow');
            }
            else if (e.key === 'Escape') {
                $('#newsessiondiv').slideUp('slow');
            }
        });

        $('#newsession').click(function() {
            createSession();
            $('#newsessiondiv').slideUp('slow');
        });

        $('body').on('click','.removeSession', function(e) {
            var token = $(this).parent().parent().attr('token');
            var sessionname = getSessionName(token);
            OC.dialogs.confirm(
                t('phonetrack',
                    'Are you sure you want to delete the session {session} ?',
                    {session: sessionname}
                ),
                t('phonetrack','Confirm session deletion'),
                function (result) {
                    if (result) {
                        deleteSession(token);
                    }
                },
                true
            );
        });

        $('body').on('click','#refreshButton', function(e) {
            if (phonetrack.currentTimer !== null) {
                phonetrack.currentTimer.pause();
                phonetrack.currentTimer = null;
            }
            refresh();
        });

        $('body').on('click','.watchbutton', function(e) {
            if (!pageIsPublic()) {
                var icon = $(this).find('i');
                if (icon.hasClass('fa-toggle-on')) {
                    icon.addClass('fa-toggle-off').removeClass('fa-toggle-on');
                    $(this).parent().parent().find('.devicelist').slideUp('slow');
                    $(this).parent().parent().find('.sharediv').slideUp('slow');
                    $(this).parent().parent().find('.moreUrls').slideUp('slow');
                    //$(this).parent().parent().find('.toggleDetail').addClass('off').removeClass('on');
                    //$(this).parent().parent().find('.toggleLineDevice').addClass('on').removeClass('off');
                }
                else {
                    icon.addClass('fa-toggle-on').removeClass('fa-toggle-off');
                    $(this).parent().parent().find('.devicelist').slideDown('slow');
                }
                // we stop the refresh loop,
                // we save options and then we refresh
                if (phonetrack.currentTimer !== null) {
                    phonetrack.currentTimer.pause();
                    phonetrack.currentTimer = null;
                }
                refresh();
                saveOptions('activeSessions');
            }
        });

        $('#colorthemeselect').change(function() {
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }
        });

        $('#autoexportpath').change(function() {
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }
        });

        $('#autoexportpath').focus(function() {
            OC.dialogs.filepicker(
                t('phonetrack', 'Choose auto export target path'),
                function(targetPath) {
                    $('#autoexportpath').val(targetPath);
                    $('#autoexportpath').change();
                },
                false, "httpd/unix-directory", true
            );
        });

        $('#linewidth').change(function() {
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }
            var s, d, layers, l, i;
            var w = parseInt($(this).val());
            $('#linewidthlabel').text(w+'px');
            for (s in phonetrack.sessionLineLayers) {
                for (d in phonetrack.sessionLineLayers[s]) {
                    phonetrack.sessionLineLayers[s][d].setStyle({
                        weight: w
                    });
                    // permanent change of arrows
                    layers = phonetrack.sessionLineLayers[s][d].getLayers();
                    for (i = 0; i < layers.length; i++) {
                        l = layers[i];
                        if (typeof l.setPatterns === 'function') {
                            l.setPatterns([{
                                offset: 30,
                                repeat: 100,
                                symbol: L.Symbol.arrowHead({
                                    pixelSize: 15 + w,
                                    polygon: false,
                                    pathOptions: {
                                        stroke: true,
                                        className: 'poly' + s + d,
                                        opacity: 1,
                                        weight: parseInt(w * 0.6)
                                    }
                                })
                            }]);
                        }
                    }
                }
            }
        });

        $('#quotareached').click(function() {
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }
        });

        $('#autozoom').click(function() {
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }
            if ($(this).is(':checked')) {
                phonetrack.zoomButton.state('zoom');
            }
            else {
                phonetrack.zoomButton.state('nozoom');
            }
        });

        $('#showtime').click(function() {
            changeTooltipStyle();
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }
            if ($(this).is(':checked')) {
                phonetrack.timeButton.state('showtime');
            }
            else {
                phonetrack.timeButton.state('noshowtime');
            }
        });

        $('#pubviewline, #pubviewpoint').click(function() {
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
                updateLinePointUrlParams();
            }
        });
        $('#acccirclecheck').click(function() {
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }
        });

        $('#exportoneperdev').click(function() {
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }
        });

        $('#tooltipshowaccuracy, #tooltipshowsatellites, #tooltipshowbattery, #tooltipshowelevation, #tooltipshowuseragent, #tooltipshowspeed, #tooltipshowbearing').click(function() {
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }
        });

        $('#linearrow, #linegradient, #cutdistance, #cuttime').change(function() {
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }
            changeApplyFilter();
        });

        $('#nbpointsload').change(function() {
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }
        });

        $('#dragcheck').click(function() {
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }
            if (!pageIsPublic()) {
                var dragcheck = $(this).is(':checked');
                var id, s, d;
                $('.toggleDetail.on').each(function() {
                    if (!isSessionShared(s)) {
                        s = $(this).attr('token');
                        d = $(this).attr('device');
                        if (dragcheck) {
                            phonetrack.sessionPointsLayers[s][d].eachLayer(function(l) {
                                l.dragging.enable();
                            });
                            phonetrack.sessionMarkerLayers[s][d].dragging.enable();
                        }
                        else {
                            phonetrack.sessionPointsLayers[s][d].eachLayer(function(l) {
                                l.dragging.disable();
                            });
                            phonetrack.sessionMarkerLayers[s][d].dragging.disable();
                        }
                    }
                });
            }
        });

        $('#viewmove').click(function() {
            showHideSelectedSessions();
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }
            if ($(this).is(':checked')) {
                phonetrack.moveButton.state('move');
            }
            else {
                phonetrack.moveButton.state('nomove');
            }
        });

        $('body').on('change', '#updateinterval', function() {
            var val = parseInt($(this).val());
            if (val !== 0 && !isNaN(val) && phonetrack.currentTimer === null) {
                refresh();
            }
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }
        });

        $('body').on('change', '#filterPointsTable input[type=number], #filterPointsTable input[type=date]', function() {
            changeApplyFilter();
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'), $('#applyfilters').is(':checked'));
            }
        });

        $('body').on('click', '.export', function() {
            var name = $(this).parent().parent().parent().find('.sessionBar .sessionName').text();
            var token = $(this).parent().parent().parent().attr('token');
            var filename = $(this).parent().find('input[role=exportname]').val().replace('.gpx', '') + '.gpx';
            OC.dialogs.filepicker(
                t('phonetrack', 'Select storage location for \'{fname}\'', {fname: filename}),
                function(targetPath) {
                    saveAction(name, token, targetPath, filename);
                },
                false, 'httpd/unix-directory', true
            );
        });

        $('body').on('click', 'button.zoomsession', function(e) {
            var token = $(this).parent().parent().attr('token');
            zoomOnDisplayedMarkers(token);
        });

        $('#logme').click(function (e) {
            if ($('#logme').is(':checked')) {
                phonetrack.locateControl.start();
            }
            else {
                phonetrack.locateControl.stop();
            }
        });

        $('body').on('click', 'ul.devicelist li .zoomdevicebutton, ul.devicelist li .deviceLabel', function(e) {
            zoomOnDevice($(this), t);
        });

        $('body').on('click', 'ul.devicelist li .toggleDetail', function(e) {
            toggleDetailDevice($(this));
            if (!pageIsPublic()) {
                saveOptions('activeSessions', true);
            }
        });

        $('body').on('click', 'ul.devicelist li .toggleLineDevice', function(e) {
            toggleLineDevice($(this));
            if (!pageIsPublic()) {
                saveOptions('activeSessions', true);
            }
        });

        $('body').on('click', 'ul.devicelist li .toggleAutoZoomDevice', function(e) {
            toggleAutoZoomDevice($(this));
            if (!pageIsPublic()) {
                saveOptions('activeSessions');
            }
        });

        $('body').on('click','.reservNameButton', function(e) {
            var nameDiv = $(this).parent().parent().find('.namereservdiv');
            var urlDiv = $(this).parent().parent().find('.moreUrls');
            var sharediv = $(this).parent().parent().find('.sharediv');
            if (nameDiv.is(':visible')) {
                nameDiv.slideUp('slow');
            }
            else{
                nameDiv.slideDown('slow');
                urlDiv.slideUp('slow');
                sharediv.slideUp('slow');
            }
        });

        $('body').on('click','.moreUrlsButton', function(e) {
            var urlDiv = $(this).parent().parent().find('.moreUrls');
            var nameDiv = $(this).parent().parent().find('.namereservdiv');
            var sharediv = $(this).parent().parent().find('.sharediv');
            if (urlDiv.is(':visible')) {
                urlDiv.slideUp('slow');
            }
            else{
                urlDiv.slideDown('slow').css('display', 'grid');
                nameDiv.slideUp('slow');
                sharediv.slideUp('slow');
            }
        });

        $('body').on('click','.sharesession', function(e) {
            var sharediv = $(this).parent().parent().find('.sharediv');
            var nameDiv = $(this).parent().parent().find('.namereservdiv');
            var moreurldiv = $(this).parent().parent().find('.moreUrls');
            if (sharediv.is(':visible')) {
                sharediv.slideUp('slow');
            }
            else {
                sharediv.slideDown('slow');
                nameDiv.slideUp('slow');
                moreurldiv.slideUp('slow');
            }
        });

        $('body').on('click','.toggleGeofences', function(e) {
            var geoDiv = $(this).parent().parent().find('.geofencesDiv');
            if (geoDiv.is(':visible')) {
                geoDiv.slideUp('slow');
            }
            else{
                $('.geofencesDiv:visible, .proximDiv:visible').each(function() {
                    $(this).slideUp('slow');
                });
                geoDiv.slideDown('slow');
            }
        });

        $('body').on('click','.toggleProxim', function(e) {
            var prDiv = $(this).parent().parent().find('.proximDiv');
            if (prDiv.is(':visible')) {
                prDiv.slideUp('slow');
            }
            else{
                $('.geofencesDiv:visible, .proximDiv:visible').each(function() {
                    $(this).slideUp('slow');
                });
                prDiv.slideDown('slow');
                updateProximSessionsSelect($(this));
            }
        });
        $('body').on('click','.reaffectDevice', function(e) {
            var token = $(this).attr('token');
            var deviceid = $(this).attr('device');
            var reaffectSelect = '';
            $('.session').each(function() {
                if ($(this).attr('token') !== token &&
                    !isSessionShared($(this).attr('token'))
                ) {
                    reaffectSelect += '<option value="' + $(this).attr('token') + '">' + $(this).find('.sessionName').text() + '</option>';
                }
            });
            $(this).parent().parent().find('.reaffectDeviceSelect').html(reaffectSelect);

            var dcontent;
            dcontent = $(e.target).parent().parent().find('.reaffectDeviceDiv');
            hideAllDropDowns();
            var isVisible = dcontent.hasClass('show');
            if (!isVisible) {
                dcontent.toggleClass('show');
            }
            $(this).parent().parent().find('.reaffectDeviceSelect').select();
        });

        $('body').on('click','.reaffectDeviceOk', function(e) {
            var token = $(this).parent().parent().parent().attr('token');
            var deviceid = $(this).parent().parent().parent().attr('device');
            var newSessionId = $(this).parent().find('.reaffectDeviceSelect').val();

            $(this).parent().parent().find('.reaffectDeviceDiv').removeClass('show');
            reaffectDeviceSession(token, deviceid, newSessionId);
        });

        $('body').on('click','.geoLinkQRDevice', function(e) {
            var token = $(this).attr('token');
            var deviceid = $(this).attr('device');
            var ll = phonetrack.sessionLatlngs[token][deviceid];
            if (ll.length > 0) {
                var dname = getDeviceName(token, deviceid);
                var p = ll[ll.length-1];
                var lat = p[0];
                var lon = p[1];
                var geourl ='geo:' + lat + ',' + lon;
                $('#trackurlinput').hide();
                $('#trackurlhint').hide();
                $('#trackurlqrcode').html('');
                var img = new Image();
                // wait for the image to be loaded to generate the QRcode
                img.onload = function(){
                    var qr = kjua({
                        text: geourl,
                        crisp: false,
                        render: 'canvas',
                        minVersion: 6,
                        ecLevel: 'H',
                        size: 210,
                        back: "#ffffff",
                        fill: phonetrack.themeColorDark,
                        rounded: 100,
                        quiet: 1,
                        mode: 'image',
                        mSize: 20,
                        mPosX: 50,
                        mPosY: 50,
                        image: img,
                        label: 'no label',
                    });
                    $('#trackurlqrcode').append(qr);
                };
                img.onerror = function() {
                    var qr = kjua({
                        text: geourl,
                        crisp: false,
                        render: 'canvas',
                        minVersion: 6,
                        ecLevel: 'H',
                        size: 210,
                        back: "#ffffff",
                        fill: phonetrack.themeColorDark,
                        rounded: 100,
                        quiet: 1,
                        mode: 'label',
                        mSize: 10,
                        mPosX: 50,
                        mPosY: 50,
                        image: img,
                        label: '===>',
                        fontcolor: '#000000',
                    });
                    $('#trackurlqrcode').append(qr);
                };
                // dirty trick to get image URL from css url()... Anyone knows better ?
                img.src = $('#dummylogo').css('content').replace('url("', '').replace('")', '').replace('phonetrack.png', 'marker-icon.png');

                $('#trackurllabel').text(geourl);

                $('#trackurldialog').dialog({
                    title: t('phonetrack', 'Geo QRcode : last position of {dname}', {dname: dname}),
                    width: 250,
                    height: 300,
                    open: function(event, ui) {
                        $('.ui-dialog-titlebar-close', ui.dialog | ui).html('<i class="far fa-times-circle"></i>');
                    }
                });
            }
        });

        $('body').on('click','.geoLinkDevice', function(e) {
            var token = $(this).attr('token');
            var deviceid = $(this).attr('device');
            var ll = phonetrack.sessionLatlngs[token][deviceid];
            if (ll.length > 0) {
                var p = ll[ll.length-1];
                var lat = p[0];
                var lon = p[1];
                window.open(
                    'geo:' + lat + ',' + lon
                );
            }
        });

        $('body').on('click','.routingGraphDevice', function(e) {
            var token = $(this).attr('token');
            var deviceid = $(this).attr('device');
            var ll = phonetrack.sessionLatlngs[token][deviceid];
            var p = ll[ll.length-1];
            var lat = p[0];
            var lon = p[1];
            window.open(
                'https://graphhopper.com/maps/?point=::where_are_you::&' +
                'point='+lat+'%2C'+lon+'&locale=fr&vehicle=car&' +
                'weighting=fastest&elevation=true&use_miles=false&layer=Omniscale',
                '_blank'
            );
        });

        $('body').on('click','.routingOsrmDevice', function(e) {
            var token = $(this).attr('token');
            var deviceid = $(this).attr('device');
            var ll = phonetrack.sessionLatlngs[token][deviceid];
            var p = ll[ll.length-1];
            var lat = p[0];
            var lon = p[1];
            window.open(
                'https://map.project-osrm.org/?z=12&center='+lat+'%2C'+lon+'&loc=0.000000%2C0.000000&loc='+lat+'%2C'+lon+'&hl=en&alt=0',
                '_blank'
            );
        });

        $('body').on('click','.routingOrsDevice', function(e) {
            var token = $(this).attr('token');
            var deviceid = $(this).attr('device');
            var ll = phonetrack.sessionLatlngs[token][deviceid];
            var p = ll[ll.length-1];
            var lat = p[0];
            var lon = p[1];
            window.open(
                'https://maps.openrouteservice.org/directions?n1='+lat+'&n2='+lon+'&n3=12&a=null,null,'+lat+','+lon+'&b=0&c=0&k1=en-US&k2=km',
                '_blank'
            );
        });

        $('body').on('click','.renameDevice', function(e) {
            var token = $(this).attr('token');
            var deviceid = $(this).attr('device');
            var devicename = getDeviceName(token, deviceid);
            $(this).parent().parent().find('.deviceLabel').hide();
            $(this).parent().parent().find('.renameDeviceInput').show();
            $(this).parent().parent().find('.renameDeviceInput').val(devicename);
            $(this).parent().parent().find('.renameDeviceInput').select();
        });

        $('body').on('click','.aliasDevice', function(e) {
            var token = $(this).attr('token');
            var deviceid = $(this).attr('device');
            var devicealias = getDeviceAlias(token, deviceid);
            $(this).parent().parent().find('.deviceLabel').hide();
            $(this).parent().parent().find('.aliasDeviceInput').show();
            $(this).parent().parent().find('.aliasDeviceInput').val(devicealias);
            $(this).parent().parent().find('.aliasDeviceInput').select();
        });

        $('body').on('keyup','.renameDeviceInput', function(e) {
            if (e.key === 'Escape') {
                $(this).parent().parent().find('.deviceLabel').show();
                $(this).parent().parent().find('.renameDeviceInput').hide();
            }
            else if (e.key === 'Enter') {
                var token = $(this).parent().parent().attr('token');
                var deviceid = $(this).parent().parent().attr('device');
                var oldName = getDeviceName(token, deviceid);
                var newName = $(this).val();
                renameDevice(token, deviceid, oldName, newName);
                $(this).parent().parent().find('.deviceLabel').show();
                $(this).parent().parent().find('.renameDeviceInput').hide();
            }
        });

        $('body').on('keyup','.aliasDeviceInput', function(e) {
            if (e.key === 'Escape') {
                $(this).parent().parent().find('.deviceLabel').show();
                $(this).parent().parent().find('.aliasDeviceInput').hide();
            }
            else if (e.key === 'Enter') {
                var token = $(this).parent().parent().attr('token');
                var deviceid = $(this).parent().parent().attr('device');
                var newalias = $(this).val();
                setDeviceAlias(token, deviceid, newalias);
                $(this).parent().parent().find('.deviceLabel').show();
                $(this).parent().parent().find('.aliasDeviceInput').hide();
            }
        });

        $('body').on('click','.deleteDevice', function(e) {
            var token = $(this).attr('token');
            var deviceid = $(this).attr('device');
            var devicename = getDeviceName(token, deviceid);
            OC.dialogs.confirm(
                t('phonetrack',
                    'Are you sure you want to delete the device {device} ?',
                    {device: devicename}
                ),
                t('phonetrack','Confirm device deletion'),
                function (result) {
                    if (result) {
                        deleteDevice(token, deviceid);
                    }
                },
                true
            );
        });

        $('body').on('click','.editsessionbutton', function(e) {
            var token = $(this).attr('token');
            $(this).parent().parent().find('.sessionName').hide();
            $(this).parent().parent().find('.renameSessionInput').show();
            $(this).parent().parent().find('.renameSessionInput').val(
                $(this).parent().parent().find('.sessionName').text()
            );
            $(this).parent().parent().find('.renameSessionInput').select();
        });

        $('body').on('keyup','.renameSessionInput', function(e) {
            if (e.key === 'Escape') {
                $(this).parent().find('.sessionName').show();
                $(this).parent().find('.renameSessionInput').hide();
            }
            else if (e.key === 'Enter') {
                var token = $(this).parent().parent().attr('token');
                var oldname = $(this).parent().find('.sessionName').text();
                var newname = $(this).val();
                renameSession(token, oldname, newname);
                $(this).parent().find('.sessionName').show();
                $(this).parent().find('.renameSessionInput').hide();
            }
        });

        $('body').on('click','.publicsessionbutton', function(e) {
            var buttext = $(this).find('b');
            var icon = $(this).find('i');
            var pub = icon.hasClass('fa-toggle-off');
            var token = $(this).parent().parent().attr('token');
            var isPublic = 0;
            if (pub) {
                isPublic = 1;
            }
            var req = {
                token: token,
                public: isPublic
            };
            var url = OC.generateUrl('/apps/phonetrack/setSessionPublic');
            $.ajax({
                type: 'POST',
                url: url,
                data: req,
                async: true
            }).done(function (response) {
                if (response.done === 1) {
                }
                else if (response.done === 2) {
                    OC.Notification.showTemporary(t('phonetrack', 'Failed to toggle session public status, session does not exist'));
                }
            }).fail(function() {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to toggle session public status'));
                OC.Notification.showTemporary(t('phonetrack', 'Reload this page'));
            });
            if (pub) {
                icon.addClass('fa-toggle-on').removeClass('fa-toggle-off');
                buttext.text(t('phonetrack', 'Make session private'));
                $('.session[token="' + token + '"]').find('.publicWatchUrlDiv').slideDown();
            }
            else {
                icon.addClass('fa-toggle-off').removeClass('fa-toggle-on');
                buttext.text(t('phonetrack', 'Make session public'));
                $('.session[token="' + token + '"]').find('.publicWatchUrlDiv').slideUp();
            }
        });

        $('body').on('change','select[role=shapeselect]', function(e) {
            // to avoid clicking on another menu item
            hideAllDropDowns();
            var shape = $(this).val();
            var s = $(this).parent().parent().parent().parent().attr('token');
            var d = $(this).parent().parent().parent().parent().attr('device');
            var req = {
                session: s,
                device: d,
                shape: shape
            };
            var url = OC.generateUrl('/apps/phonetrack/setDeviceShape');
            $.ajax({
                type: 'POST',
                url: url,
                data: req,
                async: true
            }).done(function (response) {
                if (response.done === 1) {
                    phonetrack.sessionShapes[s+d] = shape;
                    var radius = $('#pointradius').val();
                    var opacity = $('#pointlinealpha').val();
                    var mletter = $('#markerletter').is(':checked');
                    var letter = '';
                    if (mletter) {
                        var dname = getDeviceName(s, d);
                        var dalias = getDeviceAlias(s, d);
                        if (dalias !== null && dalias !== '') {
                            letter = dalias[0];
                        }
                        else {
                            letter = dname[0];
                        }
                    }
                    var iconMarker = L.divIcon({
                        iconAnchor: [radius, radius],
                        className: shape + 'marker color' + s + d,
                        html: '<b>' + letter + '</b>'
                    });
                    phonetrack.sessionMarkerLayers[s][d].setIcon(iconMarker);

                    var icon = L.divIcon({
                        iconAnchor: [radius, radius],
                        className: shape + 'marker color' + s + d,
                        html: ''
                    });
                    phonetrack.devicePointIcons[s][d] = icon;
                    var pid;
                    for (pid in phonetrack.sessionPointsLayersById[s][d]) {
                        phonetrack.sessionPointsLayersById[s][d][pid].setIcon(icon);
                    }
                    // dev styles
                    setDeviceCss(s, d, phonetrack.sessionColors[s + d], opacity, shape);
                    $('.session[token='+s+'] ul.devicelist li[device='+d+'] .devicecolor').removeClass('rdevicecolor').removeClass('sdevicecolor').removeClass('tdevicecolor').addClass(shape+'devicecolor');
                }
                else if (response.done === 2) {
                    OC.Notification.showTemporary(t('phonetrack', 'Failed to set device shape'));
                }
            }).fail(function() {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to set device shape'));
            });
        });

        $('body').on('change','select[role=autoexport]', function(e) {
            var val = $(this).val();
            var token = $(this).parent().parent().parent().attr('token');
            var req = {
                token: token,
                value: val
            };
            var url = OC.generateUrl('/apps/phonetrack/setSessionAutoExport');
            $.ajax({
                type: 'POST',
                url: url,
                data: req,
                async: true
            }).done(function (response) {
                if (response.done === 1) {
                }
                else if (response.done === 2) {
                    OC.Notification.showTemporary(
                        t('phonetrack', 'Failed to set session auto export value') +
                        '. ' + t('phonetrack', 'session does not exist')
                    );
                }
            }).fail(function() {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to set session auto export value'));
            });
        });

        $('body').on('change','select[role=autopurge]', function(e) {
            var val = $(this).val();
            var token = $(this).parent().parent().parent().attr('token');
            var req = {
                token: token,
                value: val
            };
            var url = OC.generateUrl('/apps/phonetrack/setSessionAutoPurge');
            $.ajax({
                type: 'POST',
                url: url,
                data: req,
                async: true
            }).done(function (response) {
                if (response.done === 1) {
                }
                else if (response.done === 2) {
                    OC.Notification.showTemporary(
                        t('phonetrack', 'Failed to set session auto purge value') +
                        '. ' + t('phonetrack', 'session does not exist')
                    );
                }
            }).fail(function() {
                OC.Notification.showTemporary(t('phonetrack', 'Failed to contact server to set session auto purge value'));
            });
        });

        $('body').on('click','.canceleditpoint', function(e) {
            phonetrack.map.closePopup();
        });

        $('body').on('click','.movepoint', function(e) {
            var tab = $(this).parent().find('table');
            var token = tab.attr('token');
            var deviceid = tab.attr('deviceid');
            var pointid = tab.attr('pid');
            phonetrack.movepointSession = token;
            phonetrack.movepointDevice = deviceid;
            phonetrack.movepointId = pointid;
            enterMovePointMode();
            phonetrack.map.closePopup();
        });

        $('body').on('click','.valideditpoint', function(e) {
            var tab = $(this).parent().find('table');
            var token = tab.attr('token');
            var deviceid = parseInt(tab.attr('deviceid'));
            var pointid = parseInt(tab.attr('pid'));
            // unchanged latlng
            var lat = phonetrack.sessionPointsEntriesById[token][deviceid][pointid].lat;
            var lon = phonetrack.sessionPointsEntriesById[token][deviceid][pointid].lon;
            var alt = parseFloat(tab.find('input[role=altitude]').val());
            if (isNaN(alt)) { alt = null; }
            var acc = parseFloat(tab.find('input[role=precision]').val());
            if (isNaN(acc) || acc < 0) { acc = null; }
            var sat = parseInt(tab.find('input[role=satellites]').val());
            if (isNaN(sat) || sat < 0) { sat = null; }
            var speed = parseFloat(tab.find('input[role=speed]').val());
            if (!isNaN(speed)) {
                speed = speed / 3.6;
                if (speed < 0) {
                    speed = null;
                }
            }
            var bearing = parseFloat(tab.find('input[role=bearing]').val());
            if (isNaN(bearing) || bearing < 0 || bearing > 360) { bearing = null; }
            var bat = parseFloat(tab.find('input[role=battery]').val());
            if (isNaN(bat) || bat < 0 || bat > 100) { bat = null; }
            var useragent = tab.find('input[role=useragent]').val();
            var datestr = tab.find('input[role=date]').val();
            var hourstr = parseInt(tab.find('input[role=hour]').val());
            var minstr = parseInt(tab.find('input[role=minute]').val());
            var secstr = parseInt(tab.find('input[role=second]').val());
            var completeDateStr = datestr + ' ' + pad(hourstr) + ':' + pad(minstr) + ':' + pad(secstr);
            var mom = moment(completeDateStr);
            var timestamp = mom.unix();
            editPointDB(token, deviceid, pointid, lat, lon, alt, acc, sat, bat, timestamp, useragent, speed, bearing);
        });

        $('body').on('click', '.deletepoint', function(e) {
            var tab = $(this).parent().find('table');
            var s = tab.attr('token');
            var d = tab.attr('deviceid');
            var pid = parseInt(tab.attr('pid'));
            deletePointsDB(s, d, [pid]);
        });

        $('body').on('click', '.geonortheastbutton , .geosouthwestbutton', function(e) {
            enterNSEWMode($(this));
        });

        $('#validaddpoint').click(function(e) {
            enterAddPointMode();
        });

        $('#canceladdpoint').click(function(e) {
            leaveAddPointMode();
        });

        $('#validdeletepoint').click(function(e) {
            deleteMultiplePoints();
        });
        $('#validdeletevisiblepoint').click(function(e) {
            var mapbounds = phonetrack.map.getBounds();
            deleteMultiplePoints(mapbounds);
        });

        $('#importsession').click(function(e) {
            OC.dialogs.filepicker(
                t('phonetrack', 'Import gpx/kml session file'),
                function(targetPath) {
                    importSession(targetPath);
                },
                false,
                null,
                true
            );
        });

        $('#applyfilters').click(function(e) {
            changeApplyFilter();
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'), true);
            }
        });
        changeApplyFilter();

        window.onclick = function(event) {
            if (!event.target.matches('.dropdownbutton') && !event.target.matches('.dropdownbutton i') &&
                !event.target.matches('.reaffectDevice') && !event.target.matches('.reaffectDevice i') &&
                !event.target.matches('.reaffectDeviceDiv select') && !event.target.matches('.reaffectDeviceDiv') &&
                !event.target.matches('.reaffectDeviceDiv select *') &&
                !event.target.matches('input[role=exportname]') &&
                !event.target.matches('select[role=shapeselect]') &&
                !event.target.matches('select[role=shapeselect] option') &&
                !event.target.matches('select[role=autoexport]') &&
                !event.target.matches('select[role=autoexport] option') &&
                !event.target.matches('select[role=autopurge]') &&
                !event.target.matches('select[role=autopurge] option') &&
                !event.target.matches('.dropdowndevicebutton') &&
                !event.target.matches('.dropdowndevicebutton i')
            ) {
                hideAllDropDowns();
            }
        };

        $('body').on('click','.dropdownbutton', function(e) {
            var dcontent;
            if (e.target.nodeName === 'BUTTON') {
                dcontent = $(e.target).parent().parent().find('>.dropdown-content');
            }
            else {
                dcontent = $(e.target).parent().parent().parent().find('>.dropdown-content');
            }
            var isVisible = dcontent.hasClass('show');
            hideAllDropDowns();
            if (!isVisible) {
                dcontent.toggleClass('show');
            }
        });

        $('body').on('click','.dropdowndevicebutton', function(e) {
            var dcontent;
            if (e.target.nodeName === 'BUTTON') {
                dcontent = $(e.target).parent().find('.dropdown-content');
            }
            else {
                dcontent = $(e.target).parent().parent().find('.dropdown-content');
            }
            var isVisible = dcontent.hasClass('show');
            hideAllDropDowns();
            if (!isVisible) {
                dcontent.toggleClass('show');
            }
        });

        $('body').on('focus','.addusershare', function(e) {
            addUserAutocompletion($(this));
        });

        $('body').on('keyup','.addusershare', function(e) {
            if (e.key === 'Enter') {
                var token = $(this).parent().parent().parent().attr('token');
                var username = $(this).val();
                addUserShareDb(token, username);
            }
        });

        $('body').on('click','.deleteusershare', function(e) {
            var token = $(this).parent().parent().parent().parent().parent().attr('token');
            var username = $(this).parent().attr('username');
            deleteUserShareDb(token, username);
        });

        $('body').on('click','.addpublicfilteredshareButton', function(e) {
            var token = $(this).parent().parent().parent().attr('token');
            addPublicSessionShareDb(token);
        });

        $('body').on('click','.deletePublicFilteredShare', function(e) {
            var token = $(this).parent().parent().parent().parent().parent().attr('token');
            var sharetoken = $(this).parent().attr('filteredtoken');
            deletePublicSessionShareDb(token, sharetoken);
        });

        $('body').on('click','.addgeofencebutton', function(e) {
            var token = $(this).parent().parent().parent().attr('token');
            var device = $(this).parent().parent().parent().attr('device');
            var fencename = $(this).parent().find('.geofencename').val();
            var urlenter = $(this).parent().find('.urlenter').val();
            var urlleave = $(this).parent().find('.urlleave').val();
            var urlenterpost = $(this).parent().find('.urlenterpost').is(':checked') ? 1 : 0;
            var urlleavepost = $(this).parent().find('.urlleavepost').is(':checked') ? 1 : 0;
            var sendemail = $(this).parent().find('.sendemail').is(':checked') ? 1 : 0;
            var emailaddr = $(this).parent().find('.geoemail').val();
            var sendnotif = $(this).parent().find('.sendnotif').is(':checked') ? 1 : 0;
            var north = $(this).parent().find('.fencenorth').val();
            var south = $(this).parent().find('.fencesouth').val();
            var east = $(this).parent().find('.fenceeast').val();
            var west = $(this).parent().find('.fencewest').val();
            var zonebounds;
            if (north && west && east && south) {
                zonebounds = L.latLngBounds(L.latLng(north, west), L.latLng(south, east));
            }
            else {
                zonebounds = phonetrack.map.getBounds();
            }
            addGeoFenceDb(token, device, fencename, zonebounds, urlenter, urlleave, urlenterpost, urlleavepost, sendemail, emailaddr, sendnotif);
        });

        $('body').on('click','.deletegeofencebutton', function(e) {
            var token = $(this).parent().parent().parent().parent().attr('token');
            var device = $(this).parent().parent().parent().parent().attr('device');
            var fenceid = $(this).parent().attr('fenceid');
            deleteGeoFenceDb(token, device, fenceid);
        });

        $('body').on('click','.zoomgeofencebutton', function(e) {
            zoomongeofence($(this).parent());
        });

        $('body').on('click','.geofencelabel', function(e) {
            zoomongeofence($(this).parent());
        });

        $('body').on('click','.addproximbutton', function(e) {
            var s = $(this).parent().parent().parent().attr('token');
            var d = $(this).parent().parent().parent().attr('device');
            var sessiontoken = $(this).parent().find('.proximsession').val();
            var sessionname = $(this).parent().find('.proximsession option:selected').attr('name');
            var devicename = $(this).parent().find('.devicename').val();
            var highlimit = $(this).parent().find('.highlimit').val();
            var lowlimit = $(this).parent().find('.lowlimit').val();
            var urlclose = $(this).parent().find('.urlclose').val();
            var urlfar = $(this).parent().find('.urlfar').val();
            var urlclosepost = $(this).parent().find('.urlclosepost').is(':checked') ? 1 : 0;
            var urlfarpost = $(this).parent().find('.urlfarpost').is(':checked') ? 1 : 0;
            var sendnotif = $(this).parent().find('.sendnotif').is(':checked') ? 1 : 0;
            var sendemail = $(this).parent().find('.sendemail').is(':checked') ? 1 : 0;
            var emailaddr = $(this).parent().find('.proxemail').val();
            addProximDb(s, d, sessiontoken, sessionname, devicename, highlimit, lowlimit, urlclose, urlfar, urlclosepost, urlfarpost, sendemail, emailaddr, sendnotif);
        });

        $('body').on('click','.deleteproximbutton', function(e) {
            var token = $(this).parent().parent().parent().parent().attr('token');
            var device = $(this).parent().parent().parent().parent().attr('device');
            var proximid = $(this).parent().attr('proximid');
            deleteProximDb(token, device, proximid);
        });

        $('body').on('keyup','.addnamereserv', function(e) {
            if (e.key === 'Enter') {
                var token = $(this).parent().parent().attr('token');
                var devicename = $(this).val();
                addNameReservationDb(token, devicename);
            }
        });

        $('body').on('click','.deletereservedname', function(e) {
            var token = $(this).parent().parent().parent().parent().attr('token');
            var devicename = $(this).parent().attr('name');
            deleteNameReservationDb(token, devicename);
        });

        $('button#datemintoday').click(function() {
            var mom = moment();
            $('input#datemin').val(mom.format('YYYY-MM-DD'));
            changeApplyFilter();
            if (!pageIsPublic()) {
                saveOptions('datemin', $('#applyfilters').is(':checked'));
            }
        });

        $('button#datemaxtoday').click(function() {
            var mom = moment();
            $('input#datemax').val(mom.format('YYYY-MM-DD'));
            changeApplyFilter();
            if (!pageIsPublic()) {
                saveOptions('datemax', $('#applyfilters').is(':checked'));
            }
        });

        $('button#dateminplus').click(function() {
            if ($('input#datemin').val()) {
                var mom = moment($('input#datemin').val());
                mom.add(1, 'days');
                $('input#datemin').val(mom.format('YYYY-MM-DD'));
                changeApplyFilter();
            }
            if (!pageIsPublic()) {
                saveOptions('datemin', $('#applyfilters').is(':checked'));
            }
        });

        $('button#dateminminus').click(function() {
            if ($('input#datemin').val()) {
                var mom = moment($('input#datemin').val());
                mom.subtract(1, 'days');
                $('input#datemin').val(mom.format('YYYY-MM-DD'));
                changeApplyFilter();
            }
            if (!pageIsPublic()) {
                saveOptions('datemin', $('#applyfilters').is(':checked'));
            }
        });

        $('button#datemaxplus').click(function() {
            if ($('input#datemax').val()) {
                var mom = moment($('input#datemax').val());
                mom.add(1, 'days');
                $('input#datemax').val(mom.format('YYYY-MM-DD'));
                changeApplyFilter();
            }
            if (!pageIsPublic()) {
                saveOptions('datemax', $('#applyfilters').is(':checked'));
            }
        });

        $('button#datemaxminus').click(function() {
            if ($('input#datemax').val()) {
                var mom = moment($('input#datemax').val());
                mom.subtract(1, 'days');
                $('input#datemax').val(mom.format('YYYY-MM-DD'));
                changeApplyFilter();
            }
            if (!pageIsPublic()) {
                saveOptions('datemax', $('#applyfilters').is(':checked'));
            }
        });

        $('button#dateminmaxplus').click(function() {
            var mom;
            if ($('input#datemin').val()) {
                mom = moment($('input#datemin').val());
                mom.add(1, 'days');
                $('input#datemin').val(mom.format('YYYY-MM-DD'));
            }

            if ($('input#datemax').val()) {
                mom = moment($('input#datemax').val());
                mom.add(1, 'days');
                $('input#datemax').val(mom.format('YYYY-MM-DD'));
            }

            if ($('input#datemax').val() || $('input#datemin').val()) {
                changeApplyFilter();
            }
            if (!pageIsPublic()) {
                saveOptions(['datemax', 'datemin'], $('#applyfilters').is(':checked'));
            }
        });

        $('button#dateminmaxminus').click(function() {
            var mom;
            if ($('input#datemin').val()) {
                mom = moment($('input#datemin').val());
                mom.subtract(1, 'days');
                $('input#datemin').val(mom.format('YYYY-MM-DD'));
            }

            if ($('input#datemax').val()) {
                mom = moment($('input#datemax').val());
                mom.subtract(1, 'days');
                $('input#datemax').val(mom.format('YYYY-MM-DD'));
            }

            if ($('input#datemax').val() || $('input#datemin').val()) {
                changeApplyFilter();
            }
            if (!pageIsPublic()) {
                saveOptions(['datemax', 'datemin'], $('#applyfilters').is(':checked'));
            }
        });

        $('body').on('click','.resetFilterButton', function(e) {
            var tr = $(this).parent().parent();
            if (!pageIsPublic()) {
                var l = [];
                tr.find('input[type=date]').each(function () {
                    l.push($(this).attr('id'));
                    $(this).val('');
                });
                tr.find('input[type=number]').each(function () {
                    l.push($(this).attr('id'));
                    $(this).val('');
                });
                var i;
                if (l.length > 0) {
                    saveOptions(l, $('#applyfilters').is(':checked'));
                }
            }
            changeApplyFilter();
        });

        $('#togglestats').click(function() {
            if ($(this).is(':checked')) {
                $('#statdiv').show();
                $('#statlabel').show();
                updateStatTable();
            }
            else {
                $('#statdiv').hide();
                $('#statlabel').hide();
            }
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }
        });

        $('body').on('click', '.urlhelpbutton', function(e) {
            var logger = $(this).attr('logger');
            var sessionName = getSessionName($(this).parent().parent().parent().attr('token'));
            clickUrlHelp(logger, $(this).parent().parent().find('input[role='+logger+'url]').val(), sessionName);
        });

        $('body').on('change', '#colorinput', function(e) {
            okColor();
        });
        $('body').on('click', '.devicelist .devicecolor', function(e) {
            var s = $(this).parent().parent().attr('token');
            var d = $(this).parent().parent().attr('device');
            showColorPicker(s, d);
        });

        var radius = $('#pointradius').val();
        var diam = 2 * radius;
        $('<style role="divmarker">' +
            '.rmarker, .smarker { ' +
            'width: ' + diam + 'px !important;' +
            'height: ' + diam + 'px !important;' +
            'line-height: ' + (diam - 4) + 'px;' +
            '}' +
            '.tmarker { ' +
            'width: 0px !important;' +
            'height: 0px !important;' +
            'border-left: ' + radius + 'px solid transparent !important;' +
            'border-right: ' + radius + 'px solid transparent !important;' +
            'border-bottom-width: ' + diam + 'px;' +
            'border-bottom-style: solid;' +
            'line-height: ' + (diam) + 'px;' +
            '}' +
            '</style>').appendTo('body');

        $('#pointradius').change(function() {
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }
            var mletter = $('#markerletter').is(':checked');
            var radius = $(this).val();
            $('#pointradiuslabel').text(radius+'px');
            var diam = 2 * radius;
            $('style[role=divmarker]').html(
                '.rmarker, .smarker { ' +
                'width: ' + diam + 'px !important;' +
                'height: ' + diam + 'px !important;' +
                'line-height: ' + (diam - 4) + 'px;' +
                '}' +
                '.tmarker { ' +
                'width: 0px !important;' +
                'height: 0px !important;' +
                'border-left: ' + radius + 'px solid transparent !important;' +
                'border-right: ' + radius + 'px solid transparent !important;' +
                'border-bottom-width: ' + diam + 'px;' +
                'border-bottom-style: solid;' +
                'line-height: ' + (diam) + 'px;' +
                '}'
            );
            // change iconanchor
            var s, d, pid, icon, iconMarker, shape, dname, dalias, letter;
            for (s in phonetrack.sessionMarkerLayers) {
                for (d in phonetrack.sessionMarkerLayers[s]) {
                    letter = '';
                    if (mletter) {
                        dname = getDeviceName(s, d);
                        dalias = getDeviceAlias(s, d);
                        if (dalias !== null && dalias !== '') {
                            letter = dalias[0];
                        }
                        else {
                            letter = dname[0];
                        }
                    }
                    shape = phonetrack.sessionShapes[s+d];
                    iconMarker = L.divIcon({
                        iconAnchor: [radius, radius],
                        className: shape + 'marker color' + s + d,
                        html: '<b>' + letter + '</b>'
                    });
                    phonetrack.sessionMarkerLayers[s][d].setIcon(iconMarker);

                    icon = L.divIcon({
                        iconAnchor: [radius, radius],
                        className: shape + 'marker color' + s + d,
                        html: ''
                    });
                    phonetrack.devicePointIcons[s][d] = icon;
                    for (pid in phonetrack.sessionPointsLayersById[s][d]) {
                        phonetrack.sessionPointsLayersById[s][d][pid].setIcon(icon);
                    }
                }
            }
        });

        phonetrack.themeColor = '#0000FF';
        if (OCA.Theming) {
            phonetrack.themeColor = OCA.Theming.color;
        }
        phonetrack.themeColorDark = hexToDarkerHex(phonetrack.themeColor);

        $('<style role="buttons">.fa, .fab, .far, .fas { ' +
            'color: ' + phonetrack.themeColor + '; }' +
            '.dropdown-content button:hover i, ' +
            '.reaffectDeviceDiv button:hover i ' +
            '{ color: ' + phonetrack.themeColor + '; }' +
            '</style>').appendTo('body');

        var rgbTC = hexToRgb(phonetrack.themeColor);

        $('<style role="filtertable">.activatedFilters { ' +
            'background: rgba(' + rgbTC.r + ',' + rgbTC.g + ',' + rgbTC.b + ', 0.2); }' +
            '</style>').appendTo('body');

        $('#markerletter').change(function() {
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }

            var mletter = $(this).is(':checked');
            var radius = $('#pointradius').val();
            var s, d, shape, name, alias, letter, markerIcon;
            for (s in phonetrack.sessionMarkerLayers) {
                for (d in phonetrack.sessionMarkerLayers[s]) {
                    shape = phonetrack.sessionShapes[s+d];
                    letter = '';
                    if (mletter) {
                        name = getDeviceName(s, d);
                        alias = getDeviceAlias(s, d);
                        if (alias !== null && alias !== '') {
                            letter = alias[0];
                        }
                        else {
                            letter = name[0];
                        }
                    }
                    markerIcon = L.divIcon({
                        iconAnchor: [radius, radius],
                        className: shape + 'marker color' + s + d,
                        html: '<b>' + letter + '</b>'
                    });
                    phonetrack.sessionMarkerLayers[s][d].setIcon(markerIcon);
                }
            }
        });

        $('#pointlinealpha').change(function() {
            if (!pageIsPublic()) {
                saveOptions($(this).attr('id'));
            }
            var opacity = $(this).val();
            $('#pointlinealphalabel').text(opacity);
            var s, d, styletxt, shape, colorcode;
            for (s in phonetrack.sessionMarkerLayers) {
                for (d in phonetrack.sessionMarkerLayers[s]) {
                    shape = phonetrack.sessionShapes[s+d];
                    colorcode = phonetrack.sessionColors[s+d];
                    setDeviceCss(s, d, colorcode, opacity, shape);
                }
            }
        });

        $('.sidebar-tabs li').click(function() {
            if (!pageIsPublic()) {
                saveOptions('showsidebar');
            }
        });

        $('#savefilters').click(addFiltersBookmarkDb);

        $('body').on('click', '.deletebookbutton', function(e) {
            deleteFiltersBookmarkDb($(this));
        });

        $('body').on('click', '.applybookbutton, .booklabel', function(e) {
            applyFiltersBookmark($(this));
        });

        $('body').on('mouseenter', '.reservNameButton', function(e) {
            $(this).find('i').addClass('fa-female').removeClass('fa-male');
        });

        $('body').on('mouseleave', '.reservNameButton', function(e) {
            $(this).find('i').addClass('fa-male').removeClass('fa-female');
        });

        $('body').on('keyup','li.filteredshare input[role=device]', function(e) {
            if (e.key === 'Enter') {
                var filteredtoken = $(this).parent().attr('filteredtoken');
                var devicename = $(this).val();
                var token = $(this).parent().parent().parent().parent().parent().attr('token');
                setPublicShareDeviceDb(token, filteredtoken, devicename);
            }
        });

        $('body').on('click', 'input[role=lastposonly]', function(e) {
            var filteredtoken = $(this).parent().attr('filteredtoken');
            var checked = 0;
            if ($(this).is(':checked')) {
                checked = 1;
            }
            var token = $(this).parent().parent().parent().parent().parent().attr('token');
            setPublicShareLastOnlyDb(token, filteredtoken, checked);
        });

        $('body').on('click', 'input[role=geofencify]', function(e) {
            var filteredtoken = $(this).parent().attr('filteredtoken');
            var checked = 0;
            if ($(this).is(':checked')) {
                checked = 1;
            }
            var token = $(this).parent().parent().parent().parent().parent().attr('token');
            setPublicShareGeofencifyDb(token, filteredtoken, checked);
        });

        if (!pageIsPublic()) {
            getSessions();
        }
        // public page
        else {
            var params, token, deviceid, publicviewtoken;
            if (pageIsPublicWebLog()) {
                params = window.location.href.split('publicWebLog/')[1].split('?')[0].split('/');
                token = params[0];
                publicviewtoken = '';
                deviceid = params[1];
            }
            else {
                publicviewtoken = window.location.href.split('publicSessionWatch/')[1].split('?')[0];
                token = publicviewtoken;
            }
            phonetrack.token = token;
            phonetrack.lastposonly = $('#lastposonly').text();
            // apply filters
            phonetrack.sharefilters = $('#sharefilters').text();
            var filtDict = {};
            if (phonetrack.sharefilters !== '') {
                filtDict = $.parseJSON(phonetrack.sharefilters);
                if (filtDict === null || typeof filtDict === 'undefined') {
                    filtDict = {};
                }
            }
            if (filtDict.hasOwnProperty('lastdays')) {
                $('#filterPointsTable input#lastdays').val(filtDict.lastdays);
            }
            if (filtDict.hasOwnProperty('lasthours')) {
                $('#filterPointsTable input#lasthours').val(filtDict.lasthours);
            }
            if (filtDict.hasOwnProperty('lastmins')) {
                $('#filterPointsTable input#lastmins').val(filtDict.lastmins);
            }
            if (filtDict.hasOwnProperty('lastmins') ||
                filtDict.hasOwnProperty('lasthours') ||
                filtDict.hasOwnProperty('lastdays')
            ) {
                $('#applyfilters').prop('checked', true);
                changeApplyFilter();
            }

            var name = $('#publicsessionname').text();
            phonetrack.publicName = name;
            addSession(token, name, publicviewtoken, null, [], [], true);
            $('#addPointDiv').remove();
            $('#deletePointDiv').remove();
            $('.removeSession').remove();
            $('#customtilediv').remove();
            $('#newsessiondiv').remove();
            $('#createimportsessiondiv').remove();
            if (pageIsPublicWebLog()) {
                $('#logmediv').show();
                $('#logmedeviceinput').val(deviceid);
            }
            $('#autozoom').prop('checked', true);
            phonetrack.zoomButton.state('zoom');

            if (pageIsPublicSessionWatch()) {
                $('#sidebar').toggleClass('collapsed');
                $('#sidebar li.active').removeClass('active');
                $('#header').hide();
                $('div#content').css('padding-top', '0px');
            }
        }

        refresh();

    }

})(jQuery, OC);