/**
 * @file ouestmonbus.com main source file.
 * 
 * @copyright Benoît Meunier & Karin Meunier - 2015-2024
 * 
 * @license
 * ouestmonbus.com is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ouestmonbus.com is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with ouestmonbus.com.  If not, see <http://www.gnu.org/licenses/>.
 */

"use strict";

const STATION_CONTENT_REFRESH = 2 * 60 * 1000;
const STATION_LIST_REFRESH = 4 * 60 * 1000;

const version = '%VERSION%';
const date_version = '%DATEVERSION%';

const tz = 'Europe/Paris';
const apikey = "3324de385f6b7b5bdfa1727d3e1a9f4bed5dd1accb288db6df5e8487";

log.setLevel(1);

///////////////////////////////////////////////////////////////////////////////
///////////////////////// Classe OuestmonbusApp////////////////////////////////
///////////////////////////////////////////////////////////////////////////////

/**
 * Classe principale de l'application.
 * 
 * @constructor
 */
function OuestmonbusApp() {

    this.localstoragePermitted = false;
    try {
        localStorage.setItem("localStorage", 1);
        localStorage.removeItem("localStorage");
        this.localstoragePermitted = true;
    } catch (e) { }

    // Carte Leaflet
    this.map = null;
    // Classe de gestion de la localisation
    this.localisation = null;
    // Classe de gestion des info traffic (dans ce fichier)
    this.infostrafics = null;

    this.needHelp = this.getLocalStorage("alreadyDisplayHelp") === null;
    this.firstdisplayRetardHelp = this.getLocalStorage("alreadyDisplayRetardHelp") === null;
    this.firstdisplayNoBusHelp = this.getLocalStorage("alreadyDisplayNoBusHelp") === null;

    this.allStationsDataPath = this.getLocalStorage("allStationsDataPath");
    this.allStationsDataCache = JSON.parse(this.getLocalStorage("allStationsDataCache"));

    this.refreshAllStationsTimer = null;
    this.refreshStationTimer = null;

    this.firstStationDisplaySession = true;
    this.stationFoundInCenter = null;

    this.markerCurrentDisplay = null;
    this.dialogCurrentDisplay = null;

    this.lines_layers = [];

    this.bus_group_layer = L.layerGroup();
    this.stations_on_active_line_layer_group = L.layerGroup();

    this.bus_station_origin = null;

    this.last_lignes_selected = [];
    this.last_ligne_bus_fetch = null;

    this.waitSpinner = null;
    this.waitSpinnerDisplayed = false;

    this.progressbar = null;
    this.hash_change_from_move = false;

    this.md = new MobileDetect(window.navigator.userAgent);
}

OuestmonbusApp.prototype.getLocalStorage = function (key) {

    if (this.localstoragePermitted) {
        return localStorage.getItem(key);
    } else {
        return null;
    }
};

OuestmonbusApp.prototype.setLocalStorage = function (key, value) {

    if (this.localstoragePermitted) {
        try {
            localStorage.setItem(key, value);
        } catch (e) { }
    }
};

/**
 * Initialise l'application.
 */
OuestmonbusApp.prototype.init = function () {

    var hpg = 0.5;
    if (this.md.phone()) {
        hpg = 2;
    }
    this.progressbar = new ProgressBar.Line("#progressbar", {
        duration: STATION_CONTENT_REFRESH,
        strokeWidth: hpg
    });
    this.progressbar.set(0);

    $(window).resize(this.doOnResize.bind(this));

    this.initTexts();

    this.initMap();

    this.localisation = new Localization(this, this.map);

    this.infostrafics = new InfosTrafics(this);

    this.initMenuButtons();

    this.infostrafics.fetchLinesAndAlerts();

    this.initPDFLinks();

    window.onload = function () {
        this.start();
    }.bind(this);

    window.onhashchange = function () {
        this.onHashChange();
    }.bind(this);

    $(".app-container").show();
};


/**
 * Point d'entrée, lorsque la fenêtre est chargé on exécute le programme.
 */
OuestmonbusApp.prototype.start = function () {

    setTimeout(function () {
        this.showHelp();
    }.bind(this), 60000);

    var display = function () {
        this.getAllTodayStationsData(this.displayAllStations.bind(this));
    };

    var zoomLatLng = this.localisation.urlToZoomLatLng(window.location.hash);
    if (zoomLatLng) {
        this.moveMapTo(zoomLatLng[1], zoomLatLng[0], display.bind(this));
    } else {

        var last_zoom = this.getLocalStorage("last_zoom");
        var last_lat = this.getLocalStorage("last_lat");
        var last_lng = this.getLocalStorage("last_lng");

        if (last_zoom !== null && last_lat !== null && last_lng !== null) {
            this.moveMapTo(L.latLng(last_lat, last_lng), last_zoom, display.bind(this));
        } else {
            this.moveMapTo(this.map.DEFAULT_POSITION, this.map.DEFAULT_ZOOM, display.bind(this));
        }
    }
};

OuestmonbusApp.prototype.isMapHere = function (latlng, zoom) {

    var res = false;
    try {
        res = this.map.getCenter().distanceTo(latlng) < this.map.DEFAULT_DISTANCE_RESOLUTION && this.map.getZoom() === zoom;
    } catch (e) { }
    return res;
};

OuestmonbusApp.prototype.moveMapTo = function (latlng, zoom, onFinish) {

    if (typeof onFinish === "undefined") {
        onFinish = function () { };
    }
    if (!this.isMapHere(latlng, zoom)) {
        this.map.once("moveend", function () {
            onFinish();
        });
        this.map.setView(latlng, zoom);
    } else {
        onFinish();
    }
};

OuestmonbusApp.prototype.doOnResize = function () {

    this.placeLegal();
};

/**
 * Appelé quand l'URL change.
 */
OuestmonbusApp.prototype.onHashChange = function () {

    if (!this.hash_change_from_move) {
        var zoomLatLng = this.localisation.urlToZoomLatLng(window.location.hash);

        if (zoomLatLng) {
            this.moveMapTo(zoomLatLng[1], zoomLatLng[0]);
        }
    }
    this.hash_change_from_move = false;
};

/**
    Ajuste la position des éléments
    Les mentions légal dans div.leaflet-bottom ne sont pas positionné en bas
    sans un ajustement manuel.
 */
OuestmonbusApp.prototype.placeLegal = function () {

    // hauteur des mentions légal
    var legal_attribution_height = $("#legal_attribution").height();

    // taille du menu
    var appbar_height = $(".app-bar").height();

    // taille de la barre de progression
    var progress_height = $("#progressbar").height();

    // taille de la fenêtre du navigateur
    var window_height = $(window).height();

    // position des mentions légale
    var bottom_position = window_height - legal_attribution_height - progress_height;
    $("div.leaflet-bottom").css("top", `${bottom_position}px`);
    $("div.leaflet-top").css("top", `${appbar_height}px`);

    // taille de la carte
    var map_height = window_height - progress_height;
    $("#map").css("height", `${map_height}px`);
};

/**
    Ajuste la position des éléments
 */
OuestmonbusApp.prototype.addWaitCenterSpinner = function () {

    if (this.waitSpinner !== null && !this.waitSpinnerDisplayed) {
        this.waitSpinner.addTo(this.map);
        this.waitSpinnerDisplayed = true;
        this.waitSpinner.setPosition("topright");
        var spinner_size_px = 52;

        var padding_right = Math.round(Number($(window).width() / 2 - spinner_size_px / 2));
        var padding_top = Math.round(Number($(window).height() / 2 - spinner_size_px / 2 - $(".app-bar").height()));

        $("div.wait").css("padding-top", padding_top);
        $("div.wait").css("padding-right", padding_right);
    }
};

OuestmonbusApp.prototype.removeWaitCenterSpinner = function () {

    if (this.waitSpinner !== null && this.waitSpinnerDisplayed) {
        this.waitSpinner.remove(this.map);
        this.waitSpinnerDisplayed = false;
    }
};

/**
 * Ajuste la position des dialogues un peu plus en hauteur
 */
OuestmonbusApp.prototype.updateDialogPosition = function (element_id) {

    $(element_id).css("top", $(".app-bar").height() + Math.ceil(($(window).height() - $(".app-bar").height()) * 0.05));
};

/**
 * Initialise les textes
 */
OuestmonbusApp.prototype.initTexts = function () {

    $("#version").html(`Version ${version} (${date_version})`);

    $("#app_news").html(marked.parse(`
**18/06/2024:**
- Correction de l'affichage de l'heure avec le mauvais fuseau horaire.
- Changement du fond de carte, utilisation du serveur openstreetmap.org au lieu de openstreetmap.fr.
    
**17/06/2024:**
- Correction suite au changement de format des données GTFS.

**03/05/2023:**
- Hébergement: Changement de serveur. Utilisation du serveur de tuile OpenStreetMap France.

**03/10/2022:**
- Correction de bug: les lignes du métro sont maintenant visibles.

**28/09/20022:**
- Harmonisation des couleurs et du design.
- Ajout des nouveautés.

**27/09/2022:**
- Correction du problème avec le fuseau horaire de l'API Star.
- Suppression des widgets Twitter (privacy et performances).

**26/09/2022:**
- Hébergement: suppression du CDN cloudflare (privacy).
- Mise à jours de la carte OpenStreetMap.

    `));

    $("#text_usage").html(marked.parse(`[ouestmonbus.com](https://ouestmonbus.com) est une carte interactive qui vous permet de consulter les prochains passages des bus de Rennes Métropole à leurs arrêts, de visualiser les lignes et d'observer la position des bus en temps réel.`));

    $("#text_who_develop").html(marked.parse(`
[ouestmonbus.com](https://ouestmonbus.com) est développé par [Benoît Meunier](mailto:contact@ouestmonbus.com) avec la participation de Karin Meunier (directrice graphisme et expérience utilisateur).

Ce service est fourni gratuitement sans garantie de fonctionnement.
Il est construit à l'aide des librairies [Leaflet](http://leafletjs.com/), [Metro UI CSS](https://metroui.org.ua/), [jQuery](https://jquery.com/), [mobile-detect.js](http://hgoebl.github.io/mobile-detect.js/), [underscore.js](https://underscorejs.org/) et [Moment.js](http://momentjs.com/).

L'application est basée sur des données ouvertes (Open Data). Elles proviennent des [contributeurs de OpenStreetMap](http://osm.org/copyright) et des informations du [service de Rennes Métropole et Keolis Rennes](https://data.explore.star.fr/).

Cette application web existe depuis 2015, c'est un logiciel libre sous [licence GPL v3](https://ouestmonbus.com/LICENSE), les sources sont [disponibles ici](https://ouestmonbus.com/ouestmonbus_src.zip). 
    `));
}

/**
 * Affichage de la carte.
 */
OuestmonbusApp.prototype.initMap = function () {

    // Création de la carte
    var southWest = L.latLng(47.9690, -2.0316);
    var northEast = L.latLng(48.3186, -1.3788);
    var maxBounds = L.latLngBounds(southWest, northEast);

    this.map = L.map("map", {
        attributionControl: false,
        maxBounds: maxBounds,
        minZoom: 12,
        zoomControl: true,
        photonControl: true,
        photonControlOptions: {
            placeholder: "Entrez un lieu...",
            position: "topleft",
            noResultLabel: "Pas de résultat",
            feedbackEmail: null,
            onSelected: function (feature) {
                this.map.setView([feature.geometry.coordinates[1], feature.geometry.coordinates[0]], 16);
                $(".photon-input").hide();
            }.bind(this)
        }
    });
    this.map.setView([48.1178, -1.67036], 16);
    this.map.zoomControl.setPosition('topright');

    //#map=16/48.1113/-1.67566
    this.map.DEFAULT_ZOOM = 17;
    this.map.DEFAULT_DISTANCE_RESOLUTION = 1;
    this.map.DEFAULT_POSITION = L.latLng(48.1178, -1.67036);
    this.map.MAXBOUNDS = maxBounds;

    // Ajout des tuiles
    var tiles_layer = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
        maxZoom: 18,
        bounds: maxBounds,
        opacity: 0.75
    });
    this.map.addLayer(tiles_layer);

    // Spinner d'attente de chargement des données
    this.waitSpinner = L.control();

    this.waitSpinner.onAdd = function () {
        this._div = L.DomUtil.create("div", "wait");
        this.update();
        return this._div;
    };
    this.waitSpinner.update = function () {
        this._div.innerHTML = "<div data-role='preloader' data-type='ring' data-style='dark'></div>";
    };

    // Ajout des clusters de marker
    this.map.cluster_markers = new L.markerClusterGroup({
        disableClusteringAtZoom: this.map.DEFAULT_ZOOM,
        spiderfyOnMaxZoom: false,
        maxClusterRadius: 200
    });

    // ajout du layer de bus
    this.bus_group_layer.addTo(this.map);

    // ajout du layer des stations du parcours
    this.stations_on_active_line_layer_group.addTo(this.map);

    // Ajout des mentions légals
    var legal = L.control.attribution({
        prefix: "<div id='legal_attribution'>&copy; 2015-2024 <a href='https://ouestmonbus.com'>ouestmonbus.com</a>"
    }).addAttribution("<a href='http://osm.org/copyright'>OpenStreetMap</a> | <a href='https://data.explore.star.fr/page/home/'>Data Keolis Rennes Métropole</a></div>");
    legal.setPosition("bottomright");
    legal.addTo(this.map);

    // Force un réalignement
    this.placeLegal();

    this.resetMapListener();
};

OuestmonbusApp.prototype.resetMapListener = function () {

    this.map.on("moveend", this.onMapMoveEnd.bind(this));
    this.map.on("zoomend", this.onMapZoomEnd.bind(this));
};

OuestmonbusApp.prototype.onMapZoomEnd = function () {

    if (this.stations_on_active_line_layer_group.getLayers().length > 0) {
        var currentZoom = this.map.getZoom();
        var icon_size = 22;
        var icon_anchor = 11;

        if (currentZoom < 16) {
            icon_size = currentZoom;
            icon_anchor = Math.floor(currentZoom / 2);
        }

        var iconSize = [icon_size, icon_size];
        var iconAnchor = [icon_anchor, icon_anchor];

        this.stations_on_active_line_layer_group.eachLayer(function (layers) {
            layers.bringToBack();
            layers.eachLayer(function (layer) {
                var icon_option = layer.getIcon();
                icon_option["options"]["iconSize"] = icon_size;
                icon_option["options"]["iconAnchor"] = icon_anchor;
                layer.setIcon(icon_option);
            });
        });
    }
};


OuestmonbusApp.prototype.onMapMoveEnd = function () {

    this.hideDlg(true);

    this.hash_change_from_move = true;

    var pos = this.map.getCenter();
    var lat = Number(pos.lat.toFixed(5));
    var lng = Number(pos.lng.toFixed(5));
    var z = this.map.getZoom();
    window.location.hash = `map=${z}/${lat}/${lng}`;
    this.setLocalStorage("last_zoom", this.map.getZoom());
    this.setLocalStorage("last_lat", lat);
    this.setLocalStorage("last_lng", lng);
};

OuestmonbusApp.prototype.imgPicto = function (ligne) {

    return `/images/picto_${ligne}.png`;
};

/**
 * Récupère les liens vers les fiches PDF des lignes
 */
OuestmonbusApp.prototype.initPDFLinks = function () {

    this.allpdflinks = {};

    var url_api = `https://data.explore.star.fr/api/records/1.0/search?apikey=${apikey}&dataset=mkt-information-documents-td&q=&rows=200&facet=nomtype&facet=datedebutvalidite&facet=datefinvalidite&facet=nomcourtligne&refine.nomtype=Fiche+horaire+de+ligne`

    log.debug("call API mkt-information-documents-td");
    $.getJSON(url_api,
        function (data) {
            var url_prefix = "https://data.explore.star.fr/explore/dataset/mkt-information-documents-td/files/"
            _.each(data.records, function (e, i) {
                var pdflink_id = e.fields.file.id;
                var ligne_id = e.fields.idligne;
                this.allpdflinks[ligne_id] = url_prefix + pdflink_id + '/download/';
            }.bind(this));
        }.bind(this)).fail(function () {
            this.app.displayAPIError("Impossible de récupérer les url des PDF");
        }.bind(this));
}

OuestmonbusApp.prototype.showOuestmonbusNewsDlg = function () {

    this.hideDlg(true);
    var dialog = $("#ouestmonbus_news_dlg").data("dialog");
    this.dialogCurrentDisplay = dialog;
    dialog.open();
    this.updateDialogPosition("#ouestmonbus_news_dlg");
}


OuestmonbusApp.prototype.hideDlg = function (dlg) {

    if (dlg) {
        if (this.dialogCurrentDisplay) {
            this.dialogCurrentDisplay.close();
            this.dialogCurrentDisplay = null;
        }
    }
};


/**
 * Initialise les boutons du menu.
 */
OuestmonbusApp.prototype.initMenuButtons = function () {

    // Bouton News ouestmonbus
    $("#ouestmonbus_btn").click(function () {
        this.showOuestmonbusNewsDlg();
    }.bind(this));
    if (this.md.mobile()) {
        $("#ouestmonbus_btn").unbind("mouseenter mouseleave");
    }

    // Bouton localisation
    $("#localise_btn").click(function () {
        this.hideDlg(true);
        this.addWaitCenterSpinner();
        this.localisation.autoLocate();
    }.bind(this));
    if (this.md.mobile()) {
        $("#localise_btn").unbind("mouseenter mouseleave");
    }

    // photon search
    $("#address_btn").click(function () {
        this.hideDlg(true);
        $(".photon-input").show();
        $(".photon-input").focus();
    }.bind(this));
    if (this.md.mobile()) {
        $("#address_btn").unbind("mouseenter mouseleave");
    }

    // Affichage des perturbations
    $("#perturbations_btn").click(function () {
        var h = Math.ceil(($(window).height() - $(".app-bar").height() - $("#progressbar").height() - $("#legal_attribution").height()) * 0.8);
        $("#trafic_dlg_content").height(h);
        this.hideDlg(true);
        var dialog = $("#trafic_dlg").data("dialog");
        this.dialogCurrentDisplay = dialog;
        dialog.open();
        this.updateDialogPosition("#trafic_dlg");
    }.bind(this));
    if (this.md.mobile()) {
        $("#perturbations_btn").unbind("mouseenter mouseleave");
    }

    // Bouton config
    $("#config_btn").click(function () {
        var val = this.getLocalStorage("all_stations_mode") === 'true';
        $("#all_stations_checkbox").prop('checked', val);
        this.hideDlg(true);
        var dialog = $("#config_dlg").data("dialog");
        this.dialogCurrentDisplay = dialog;
        dialog.open();
        this.updateDialogPosition("#config_dlg");
    }.bind(this));
    if (this.md.mobile()) {
        $("#config_btn").unbind("mouseenter mouseleave");
    }

    // Validation de la config
    $("#config_valid_btn").click(function () {
        var all_stations_mode = $("#all_stations_checkbox").is(":checked");
        this.setLocalStorage("all_stations_mode", all_stations_mode);
        location.reload();
    }.bind(this));
    if (this.md.mobile()) {
        $("#config_valid_btn").unbind("mouseenter mouseleave");
    }

    // Bouton aide
    $("#about_btn").click(function () {
        this.hideDlg(true);
        var dialog = $("#about_dlg").data("dialog");
        this.dialogCurrentDisplay = dialog;
        dialog.open();
        this.updateDialogPosition("#about_dlg");
    }.bind(this));
    if (this.md.mobile()) {
        $("#about_btn").unbind("mouseenter mouseleave");
    }
};

/**
 * Affichage de la notice d'aide.
 */
OuestmonbusApp.prototype.showHelp = function () {

    if (this.needHelp) {
        $.Notify({
            content: "Cliquez ou tapez sur les icônes " +
                "<img src='./images/station.png' height='22' width='22' alt='station'> " +
                "des arrêts de bus pour connaître les prochains passages en temps réel.",
            icon: "<span class='mif-question'></span>",
            type: "info",
            keepOpen: true
        });
    }
    this.setLocalStorage("alreadyDisplayHelp", true);
};

/**
 *	Affichage de la notification de légende retard.
 */
OuestmonbusApp.prototype.displayRetardHelp = function () {

    if (this.firstdisplayRetardHelp) {
        $.Notify({
            content: "Le symbole <span class='mif-sync-problem mif-lg'></span> " +
                "indique l'avance ou le retard par rapport à l'horaire initialement prévu",
            type: "info",
            keepOpen: true
        });
    }
    this.firstdisplayRetardHelp = false;
    this.setLocalStorage("alreadyDisplayRetardHelp", true);
};

/**
 *	Affichage de la notification de légende pas de bus.
 */
OuestmonbusApp.prototype.displayNoBusHelp = function () {

    if (this.firstdisplayNoBusHelp) {
        $.Notify({
            content: "Le symbole <span class='mif-not'></span> " +
                "indique qu'un bus est prévu avant la fin du service mais pas dans l'heure à venir",
            type: "warning",
            keepOpen: true
        });
    }
    this.firstdisplayNoBusHelp = false;
    this.setLocalStorage("alreadyDisplayNoBusHelp", true);
};

/**
 * Récupère le chemin des données des stations du jour.
 * @param  {moment} now - maintenant
 * @return {string} - chemin
 */
OuestmonbusApp.prototype.getDataStationPath = function (now) {

    var first = moment(now);
    first.hour(5);
    first.minute(0);
    first.second(0);
    first.millisecond(0);

    if (now.isBefore(first)) {
        now = now.subtract(1, "days");
    }

    var datestr = now.format("YYYY_MM_DD");
    return `./data/stations_${datestr}.geojson`;
};

/**
 * Récupère la liste des station.
 * 
 * @param  {function} onSucess - fonction à executer après la récupération
 */
OuestmonbusApp.prototype.getAllTodayStationsData = function (onSuccess) {

    var url_data = this.getDataStationPath(moment());

    if (this.allStationsDataPath !== url_data || this.allStationsDataCache === null) {

        this.addWaitCenterSpinner();
        $.getJSON(url_data,
            function (data) {
                this.allStationsDataCache = data;
                this.setLocalStorage("allStationsDataPath", url_data);
                this.setLocalStorage("allStationsDataCache", JSON.stringify(data));
                onSuccess(data);
            }.bind(this)).fail(function () {
                this.removeWaitCenterSpinner();
                this.displayAPIError("Connexion impossible aux données ouestmonbus");
            }.bind(this));
    } else {
        onSuccess(this.allStationsDataCache);
    }
};

/**
 * Filtre si il faut afficher la station ou non
 * @param  {moment} now
 * @param  {array} limits - [premier_passage,dernier_passage]
 * @return {boolean} - true si à afficher
 */
OuestmonbusApp.prototype.stationTimeFilter = function (now, limits) {

    // prevent error in GTFS data
    if (limits === null || limits[0] === null || limits[1] === null) {
        return false;
    }

    var now_ajusted = moment(now);

    var morning_start = moment(now).startOf("day");
    var morning_end = moment(morning_start).hours(5);

    var hour_end_limit = Number(limits[1].slice(0, 2));

    if (now.isBetween(morning_start, morning_end) && hour_end_limit >= 24) {
        now_ajusted.add(1, "days");
    }

    var start = moment(now);
    start.hours(Number(limits[0].slice(0, 2)) - 1);
    start.minute(Number(limits[0].slice(3, 5)));
    start.second(Number(limits[0].slice(6, 8)));

    var end = moment(now);
    end.hours(Number(limits[1].slice(0, 2)));
    end.minute(Number(limits[1].slice(3, 5)));
    end.second(Number(limits[1].slice(6, 8)));
    end.add(10, "minutes");

    if (now_ajusted.isBetween(start, end)) {
        return true;
    } else {
        return false;
    }
};

OuestmonbusApp.prototype.isMetroLine = function (lines) {

    var line_id = lines[0].split(",")[0];
    if (line_id.indexOf('-') > 0) {
        line_id = line_id.split('-')[1];
    }
    var res = _.size(lines) === 1 && (Number(line_id) === 1001 || Number(line_id) === 1002);
    return res;
};


/***
 *
 * Affiche les stations sur la ligne active
 *
 */
OuestmonbusApp.prototype.displayStationsOnActiveLine = function (line, sens) {

    this.deleteAllSelectedStationsOnMap();

    var line_sens = `${line},${sens}`;
    var iconpath = "/images/station_selected.png";
    if (this.isMetroLine([line_sens])) {
        iconpath = "/images/station-metro_selected.png";
    }

    // récupère les stations sur la ligne
    var stations = _.filter(this.allStationsDataCache, function (e) {
        return _.contains(e.properties.lines, line + ',' + sens);
    });

    // Génère un layer pour ces stations
    var geoJsonLayer = L.geoJson(stations, {
        pointToLayer: function (feature, latlng) {

            var stationIcon = L.icon({
                iconUrl: iconpath,
                shadowUrl: "",
                iconSize: [22, 22],
                shadowSize: [0, 0],
                iconAnchor: [11, 11],
                shadowAnchor: [0, 0],
                popupAnchor: [0, 0]
            });

            return L.marker(latlng, {
                icon: stationIcon
            });
        },
        onEachFeature: function (feature, marker) {
            marker.idarret = feature.properties.id;
            marker.nomarret = feature.properties.name;
            marker.lines = feature.properties.lines;
            marker.terminus = feature.properties.terminus;
            marker.parcours = feature.properties.parcours;
            marker.line_selected = true;

            this.onEachMakerAdd(marker);
        }.bind(this)
    });

    this.stations_on_active_line_layer_group.addLayer(geoJsonLayer);

    // ajust icon size
    this.onMapZoomEnd();
}

/**
      Callback lors d'un click sur une ligne
*/
OuestmonbusApp.prototype.displayLine = function (line, sens) {

    this.displayStationsOnActiveLine(line, sens);

    // met toutes les lignes en invisibles
    _.each(this.lines_layers, function (e) {
        var line_color = e.color;
        var invisible = {
            opacity: 0,
            color: line_color
        };
        e.setStyle(invisible);
    }.bind(this));

    // recupère la ligne selectionnée
    var line_layer = _.filter(this.lines_layers, function (e) {
        return (e.idligne === line && e.sens == sens);
    });

    var param = [`${line},${sens}`];
    this.displayLines(param, [], () => { });
};

/**
 * Affichage des lignes sur la carte
 * @param  {strings} lines "idligne,sens"
 * @param  {array[string]} extra_lines "ajoute une ligne qui n'est pas dans les parcours de la station (bus exceptionnel stade,..)"
 */
OuestmonbusApp.prototype.displayLines = function (lines, extra_lines, fdone) {

    log.debug(`displayLines: ${lines} | ${extra_lines}`);

    // Efface les précédentes lignes en cache
    this.deleteAllSelectedStationsOnMap();
    _.each(this.lines_layers, function (layer) {
        this.map.removeLayer(layer);
    }.bind(this));
    this.lines_layers = [];

    var parcours_array = _.map(this.markerCurrentDisplay.parcours, function (item) {
        return "id=" + item;
    });
    var q = parcours_array.join(" OR ");

    if (extra_lines.length > 0) {
        var extra_lines_array = _.map(extra_lines, function (e) {
            var line_id = e.split(",")[0];
            if (line_id.indexOf('-') > 0) {
                line_id = line_id.split('-')[1];
            }
            return "idligne=" + line_id;
        });
        q += " OR " + extra_lines_array.join(" OR ");
    }

    if (lines.length === 1) {
        var line = lines[0].split(",")[0];
        if (line.indexOf('-') > 0) {
            line = line.split('-')[1];
        }
        q += " OR idligne=" + line;
    }

    // Ajoute les ligne dans l'ordre pour que la première ligne soit en premier plan.
    var first_line = lines[0].split(",")[0];
    if (first_line.indexOf('-') > 0) {
        first_line = first_line.split('-')[1];
    }
    var lines_array = _.union(lines, extra_lines);
    var args = [first_line, lines_array, fdone];

    var q = encodeURIComponent(q);
    if (this.isMetroLine(lines)) {
        var url_api = `https://data.explore.star.fr/api/records/1.0/search?apikey=${apikey}&dataset=tco-metro-topologie-parcours-td&sort=nomcourtligne&facet=nomcourtligne&facet=senscommercial&facet=type&facet=nomarretdepart&facet=nomarretarrivee&facet=estaccessiblepmr&q=${q}&timezone=${tz}`;
        log.debug("call API tco-metro-topologie-parcours-td");
    } else {
        var url_api = `https://data.explore.star.fr/api/records/1.0/search?apikey=${apikey}&dataset=tco-bus-topologie-parcours-td&facet=idligne&facet=nomcourtligne&facet=senscommercial&facet=type&facet=nomarretdepart&facet=nomarretarrivee&facet=estaccessiblepmr&q=${q}&timezone=${tz}`;
        log.debug("call API tco-bus-topologie-parcours-td");
    }
    $.getJSON(url_api,
        args,
        function (data) {
            var first_line = args[0];
            var lines_array = args[1];
            var fdone = args[2];

            var data_sorted = _.sortBy(data.records, function (i) {
                return i.fields.idligne === first_line;
            });


            _.each(data_sorted, function (trace, index, list) {

                // Si première ligne on selectionne
                if (index == (list.length - 1)) {
                    log.debug(`First line is ${trace.fields.idligne}`);
                    this.displayStationsOnActiveLine(trace.fields.idligne, trace.fields.sens);
                }

                // Bug de l'API: trace.fields.couleurtracetrace au lieu de trace.fields.couleurtrace quand métro.
                var line_color = trace.fields.couleurtrace !== undefined ? trace.fields.couleurtrace : trace.fields.couleurtracetrace;

                // filtre avec les parcours et montre la ligne uniquement si sur un des parcours de la station active
                // (garde les données pour la position des bus mais n'affiche pas sinon)
                var line_style_visible = {
                    opacity: 1,
                    color: line_color
                };
                var line_sens = `${trace.fields.idligne},${trace.fields.sens}`;
                if (lines_array.includes(line_sens)) {
                    log.debug(`Add ${line_sens} to map`);
                    var layer = L.geoJson(trace.fields.parcours, {
                        style: line_style_visible
                    });
                    var sensstr = "";
                    if (Number(trace.fields.sens) === 0) {
                        sensstr = "aller";
                    } else {
                        sensstr = "retour";
                    }
                    layer.bindPopup(this.generatePopupLineContent(trace.fields.idligne, sensstr, trace.fields.couleurtrace, trace.fields.libellelong, this.allpdflinks[trace.fields.idligne]));
                    layer.on("click", function (e) {
                        $(".line_info").parent().css("margin", 0);
                        $(".leaflet-popup-tip-container").remove();
                        e.layer.bringToFront();
                    }.bind(this));

                    layer.idligne = trace.fields.idligne;
                    layer.sens = trace.fields.sens;
                    layer.dest = trace.fields.nomarretarrivee;
                    layer.color = line_color;
                    this.lines_layers.push(layer);
                    layer.addTo(this.map);
                }
            }.bind(this));
            fdone();
        }.bind(this));
};

/**
 * Génération du contenu de la popup des lignes
 */
OuestmonbusApp.prototype.generatePopupLineContent = function (idligne, sens, couleur, nom, pdfurl) {

    var img_picto = this.imgPicto(idligne);
    var pdf_part = pdfurl ? `<br><a target='_blank' href='${pdfurl}'>Téléchargement de la fiche horaire</a>` : ``;
    return `<div class='line_info' style='border:0.3rem solid #${couleur};'><img src='${img_picto}' height='22' width='22' alt='${idligne}'> ${nom} (sens ${sens})<br>${pdf_part}</div>`
};

/**
 * Affiche les stations.
 * Cette fonction devrait régulièrement être appelé pour mettre a jour
 * la présence ou non des arrêts
 * 
 * @param  {geoJson} geoJsonData - stations du jour
 */
OuestmonbusApp.prototype.displayAllStations = function (geoJsonData) {

    this.removeWaitCenterSpinner();

    var geoJsonLayer = L.geoJson(geoJsonData, {
        pointToLayer: function (feature, latlng) {

            var busIcon = L.icon({
                iconUrl: "./images/station.png",
                shadowUrl: "",
                iconSize: [22, 22],
                shadowSize: [0, 0],
                iconAnchor: [11, 11],
                shadowAnchor: [0, 0],
                popupAnchor: [0, 0]
            });

            var metroIcon = L.icon({
                iconUrl: "./images/station-metro.png",
                shadowUrl: "",
                iconSize: [22, 22],
                shadowSize: [0, 0],
                iconAnchor: [11, 11],
                shadowAnchor: [0, 0],
                popupAnchor: [0, 0]
            });

            var icon = busIcon;

            var line_id = feature.properties.lines[0].split(",")[0];
            if (line_id.indexOf('-') > 0) {
                line_id = line_id.split('-')[1];
            }

            if (Number(line_id) === 1001 || Number(line_id) === 1002) {
                icon = metroIcon;
            }

            return L.marker(latlng, {
                icon: icon
            });
        }.bind(this),
        filter: function (feature) {
            var all_stations_mode = this.getLocalStorage("all_stations_mode") === 'true';
            var time_ok = this.stationTimeFilter(moment(), feature.properties.limits);
            return all_stations_mode || time_ok;
        }.bind(this),
        onEachFeature: function (feature, marker) {
            marker.idarret = feature.properties.id;
            marker.nomarret = feature.properties.name;
            marker.lines = feature.properties.lines;
            marker.terminus = feature.properties.terminus;
            marker.parcours = feature.properties.parcours;
            marker.line_selected = false;

            this.onEachMakerAdd(marker);
            if (this.firstStationDisplaySession) {
                if (this.stationFoundInCenter === null) {
                    var lat = Number(marker.getLatLng().lat).toFixed(5);
                    var lng = Number(marker.getLatLng().lng).toFixed(5);
                    if (this.map.getCenter().distanceTo(L.latLng(lat, lng)) < this.map.DEFAULT_DISTANCE_RESOLUTION * 2) {
                        this.stationFoundInCenter = marker;
                    }
                }
            }
        }.bind(this)
    });
    this.map.cluster_markers.clearLayers();
    this.map.cluster_markers.addLayer(geoJsonLayer);

    if (!this.map.hasLayer(this.map.cluster_markers)) {
        this.map.cluster_markers.addTo(this.map);
    }

    if (this.stationFoundInCenter !== null) {
        this.openStation(this.stationFoundInCenter);
    } else {
        if (this.firstStationDisplaySession && this.map.getZoom() >= this.map.DEFAULT_ZOOM) {
            this.localisation.showLocation(this.map.getCenter(), false, false, 0);
        }
    }

    this.firstStationDisplaySession = false;
    this.delayRefreshAllStations();
};


/**
 * Appelé à chaque ajout de marker
 * 
 * @param  {marker}
 */
OuestmonbusApp.prototype.onEachMakerAdd = function (marker) {

    var popup = L.popup({
        autoPan: true,
        autoPanPaddingTopLeft: L.point(Math.ceil($(window).width() * 0.05), $(".app-bar").height() + Math.ceil(($(window).height() - $(".app-bar").height()) * 0.05)),
        maxWidth: Math.ceil($(window).width() * 0.8),
        maxHeight: Math.ceil(($(window).height() - $(".app-bar").height() - $("#progressbar").height() - $("#legal_attribution").height()) * 0.8)
    });
    popup.setContent("<div data-role='preloader' data-type='ring' data-style='dark'></div>");
    marker.bindPopup(popup);
    marker.on("click", function () {
        this.openStation(marker);
    }.bind(this));
};

/**
 * Récupère les infos du prochain dépare à une station.
 * 
 * @param  {string} idarret
 * @param  {function} onSuccess - callback
 */
OuestmonbusApp.prototype.getNextDepartures = function (lines, idarret, onSuccess, onFailed) {

    log.debug(`getNextDepartures: ${lines} ${idarret}`);

    if (lines.indexOf('-') > 0) {
        lines = lines.split('-')[1];
    }
    if (idarret.indexOf('-') > 0) {
        idarret = idarret.split('-')[1];
    }

    if (this.isMetroLine(lines)) {
        var url = `https://data.explore.star.fr/api/records/1.0/search?apikey=${apikey}&dataset=tco-metro-circulation-passages-tr&sort=-depart&refine.idarret=${idarret}&timezone=${tz}`;
        log.debug("call API tco-metro-circulation-passages-tr");
    } else {
        var url = `https://data.explore.star.fr/api/records/1.0/search?apikey=${apikey}&dataset=tco-bus-circulation-passages-tr&sort=-depart&refine.idarret=${idarret}&timezone=${tz}`;
        log.debug("call API tco-bus-circulation-passages-tr");
    }

    $.getJSON(url,
        function (data) {
            if (data) {

                // check server time
                if (data.records.length > 0) {
                    var record_timestamp = moment(data.records[0].record_timestamp);
                    var diff_ms = moment().diff(record_timestamp);
                    var d = moment.duration(diff_ms);
                    // Feature ou bug de l'API, le champs .record_timestamp est à UTC ...
                    var utc_offset = moment().utcOffset();

                    if (d.asMinutes() > (30 + utc_offset)) {
                        var dialog = $("#api_time_failure_dlg").data("dialog");
                        this.dialogCurrentDisplay = dialog;
                        dialog.open();
                    }
                    onSuccess(data);

                } else {
                    onSuccess(data);
                }
            } else {
                this.displayAPIError("données invalides du service opendata STAR");
                onFailed();

            }
        }.bind(this)).fail(function () {
            this.displayAPIError("échec de connexion au service opendata STAR");
            onFailed();
        }.bind(this));
};

OuestmonbusApp.prototype.deleteAllBusOnMap = function () {
    this.bus_group_layer.clearLayers();
};

OuestmonbusApp.prototype.deleteAllSelectedStationsOnMap = function () {
    this.stations_on_active_line_layer_group.clearLayers();
};

OuestmonbusApp.prototype.generatePopupBusContent = function (idligne, destination) {
    var img_picto = this.imgPicto(idligne);
    return `<div class='bus_info'><img src='${img_picto}' height='22' width='22' alt='${idligne}'><br><span>Direction ${destination}</span></div>`;
};

OuestmonbusApp.prototype.getBusPositions = function (data, latlng) {

    // prepare data
    var all_records = _.reduce(data.records, function (seed, item) {
        seed.push(item.fields);
        return seed;
    }, []);

    var lignes_selected = _.groupBy(all_records, "idligne");
    lignes_selected = _.map(lignes_selected, function (item) {
        var first = _.first(item);
        return [`${first.idligne},${first.sens}`];
    });
    lignes_selected = _.flatten(lignes_selected);

    var request = _.map(lignes_selected, function (e) {
        var idligne = e.split(",")[0];
        if (idligne.indexOf('-') > 0) {
            idligne = idligne.split('-')[1];
        }
        var sens = e.split(",")[1];
        return `idligne=${idligne} AND sens=${sens}`;
    });

    var q = encodeURIComponent(request.join(" OR "));

    var url_api = `https://data.explore.star.fr/api/records/1.0/search?apikey=${apikey}&dataset=tco-bus-vehicules-position-tr&q=${q}&timezone=${tz}&refine.etat=En+ligne`;


    log.debug("call API tco-bus-vehicules-position-tr");
    $.getJSON(url_api,
        function (data) {
            if (data) {
                // Ajoute la ligne comme clé de dict
                var records = _.groupBy(data.records, function (e) {
                    return `${e.fields.idligne},${e.fields.sens}`;
                });

                // Position de la station
                var station_l_latlng = this.normalizeLatLng(latlng);
                // Résultat des positions classé par distance de la station et uniquement les ** 4 premiers bus **
                var records_distance_filtred = _.mapObject(records, function (r) {
                    return _.first(_.sortBy(r, function (e) {
                        return this.normalizeLatLng(e.geometry.coordinates).distanceTo(station_l_latlng);
                    }.bind(this)), 4);
                }.bind(this));

                // Ligne avec idlgine comme clé
                var lines_grouped = _.groupBy(this.lines_layers, function (e) {
                    return `${e.idligne},${e.sens}`;
                });
                var lines = _.mapObject(lines_grouped, function (e) {
                    return _.map(e, function (x) {
                        return x.getLayers()[0]._latlngs;
                    });
                });

                _.each(records_distance_filtred, function (value, key) {
                    this.genSegmentsTrajet(key, value, lines[key]);
                }.bind(this));
            }
        }.bind(this)
    ).fail(function () {
        // TODO add error message
    }.bind(this));
    return;
}

/*
Normalise les coordonnées. L'api peut répondre un array dans les deux sens.
*/
OuestmonbusApp.prototype.normalizeLatLng = function (a) {

    if (a[0] > 47 && a[1] < 0) {
        return L.latLng(a[0], a[1]);
    }
    if (a[1] > 47 && a[0] < 0) {
        return L.latLng(a[1], a[0]);
    }
    if (a.lat > 47 && a.lng < 0) {
        return L.latLng(a.lat, a.lng);
    }
    if (a.lat < 0 && a.lng > 47) {
        return L.latLng(a.lng, a.lat);
    }
    // Ne devrait jamais se produire
    log.debug(`Error: bad conversion: ${a}`);
}

OuestmonbusApp.prototype.genSegmentsTrajet = function (ligne_sens, bus_positions, lines) {

    _.each(bus_positions, function (bus_position) {
        var idbus = bus_position.fields.idbus;

        var url_api = `https://data.explore.star.fr/api/records/1.0/search?apikey=${apikey}&sort=-depart&dataset=tco-bus-circulation-passages-tr&refine.idbus=${idbus}&timezone=${tz}`;

        var idligne = ligne_sens.split(',')[0];
        var sens = ligne_sens.split(',')[1];

        log.debug("call API tco-bus-circulation-passages-tr");
        $.getJSON(url_api,
            function (data) {
                if (data) {
                    // On ne tient pas compte des stations à moins de 10 secondes
                    var records_10s_filtred = _.filter(data.records, function (e) {
                        var now = moment();
                        var station_depart = moment(e.fields.depart);
                        var diff_second = station_depart.diff(now, 'seconds');
                        return diff_second >= 10;
                    });
                    // On utilise les 3 prochaines stations pour calculer le déplacement
                    var first_next_stations = _.first(records_10s_filtred, 3);
                    if (first_next_stations.length == 0) {
                        log.debug(`Pas de stations trouvé pour le bus ${idbus} - ${ligne_sens}`);
                    } else {
                        // Selection la ligne pour la trajectoire
                        var selected_line = this.getLignesForNextStation(first_next_stations, lines);
                        if (selected_line) {
                            var segments_time = this.getParcoursSegmentsTimes(bus_position, first_next_stations, selected_line);
                            if (!_.isUndefined(segments_time) && segments_time[0].length > 0) {
                                this.addBusIconOnMap(idbus, segments_time[0], segments_time[1], idligne, sens, bus_position.fields.destination);
                                return;
                            } else {
                                log.debug(`Impossible de calculer la trajectoire pour le bus ${idbus} - ${ligne_sens}`);
                            }
                        } else {
                            log.debug(`Pas de ligne trouvé pour la trajectoire du bus ${idbus} - ${ligne_sens}`);
                        }

                    }

                    log.debug("Ajout d'un bus sans animation")
                    var uniq_position = [bus_position.fields.coordonnees, bus_position.fields.coordonnees];
                    this.addBusIconOnMap(idbus, uniq_position, 0, idligne, sens, bus_position.fields.destination);
                }
            }.bind(this));
    }.bind(this));
};

/*
Cette fonction prend en paramètre, la position du bus, les 3 prochaine stations et la ligne sur lequel le bus circule.
Elle retourne un tableau [0]: un array de L.latLng du déplacement du bus à venir, [1]: le temps estimé que va prendre le bus pour parcourir le trajet.
Le trajet est calculé en utilisant les prochaines station jusqu'à 2 minutes de trajet soit atteint.
*/
OuestmonbusApp.prototype.getParcoursSegmentsTimes = function (bus_position, first_next_stations, line) {

    var parcour_points = [];
    var parcour_duration = 0;

    // calcul position du point de départ
    var first_point = L.GeometryUtil.closest(this.map, line, this.normalizeLatLng(bus_position.fields.coordonnees), true);
    log.debug(`Distance depuis le point de départ: ${first_point.distance}`);
    if (first_point.distance > 250) {
        log.debug("Le point de départ est hors parcours");
        return;
    }
    // index dans la ligne du point de départ du bus
    var first_index = _.findIndex(line, function (e) {
        return e.lat == first_point.lat && e.lon == first_point.lon;
    });

    var begin_index = first_index;
    _.each(first_next_stations, function (e) {
        if (parcour_duration < STATION_CONTENT_REFRESH) {
            var next_station_point = L.GeometryUtil.closest(this.map, line, this.normalizeLatLng(e.fields.coordonnees), true);
            if (next_station_point.distance > 250) {
                log.debug("Bus hors parcours");
                return;
            }
            var next_station_index = _.findIndex(line, function (x) {
                return x.lat == next_station_point.lat && x.lon == next_station_point.lon;
            });
            var part_line = line.slice(begin_index, next_station_index);
            begin_index = next_station_index;
            var part_duration = moment(e.fields.depart).diff(moment());

            parcour_points = _.union(parcour_points, part_line);
            parcour_duration += part_duration;
        }
    }.bind(this));

    return [parcour_points, parcour_duration];
};


/*
Prend en paramètre les 3 prochaines stations d'un bus et les parcours connus du bus.
Retourne le parcours le plus proche des 3 prochaines stations.
Si > 2 km on retourn null, on considère que le parcours est inconnu.
*/
OuestmonbusApp.prototype.getLignesForNextStation = function (first_next_stations, lines) {

    // Genere un array des positions des prochaines stations
    var first_next_stations_array = _.map(first_next_stations, function (e) {
        return e.geometry.coordinates;
    });

    // pour chaque ligne
    var distances = _.map(lines, function (line_pos) {
        var distance = 0;
        // Pour chaque station
        _.each(first_next_stations_array, function (station_pos) {
            // On inverse lat et lon car API Star donne dans le sens inverse Leaflet
            var pos_station = this.normalizeLatLng(station_pos);
            // Le point le plus proche de la station sur tous les points de la ligne
            var c = L.GeometryUtil.closest(this.map, line_pos, pos_station);
            // On fait la somme des distances
            distance += c.distance;
        }.bind(this));
        return [line_pos, distance];
    }.bind(this));

    var res = _.first(_.sortBy(distances, function (e) {
        return e[1]
    }));
    if (_.isUndefined(res)) {
        log.debug("Pas de ligne trouvé");
        return;
    }
    if (res[1] > 2000) {
        log.debug("Le bus est situé à plus de 2 km de son parcours");
        return;
    }
    return res[0];
}


OuestmonbusApp.prototype.addBusIconOnMap = function (idbus, parcours, durations, idligne, sens, destination) {

    log.debug(`Ajout de bus sur la carte idligne: ${idligne}`);

    var busIcon = L.icon({
        iconUrl: this.imgPicto(idligne),
        shadowUrl: "./images/bus_shadow.png",
        iconSize: [20, 20],
        shadowSize: [32, 32],
        iconAnchor: [10, 10],
        shadowAnchor: [16, 16],
        popupAnchor: [0, 0]
    });

    var marker = L.Marker.movingMarker(parcours, durations, {
        autostart: true,
        icon: busIcon,
        zIndexOffset: -2
    });

    marker.idligne = idligne;
    marker.sens = sens;
    marker.idbus = idbus;
    marker.last_refresh = moment();

    marker.bindPopup(this.generatePopupBusContent(idligne, destination), {
        closeButton: false,
        className: "bus_popup"
    });
    marker.on("click", function () {

        $(".bus_info").parent().css("margin", 0);
        $(".leaflet-popup-tip-container").remove();
    }.bind(this));

    this.updateBusMarker(marker);
    this.refreshBusCountHtml();
};

/*
Enleve l'ancien layer du bus et ajoute le nouveau.
*/
OuestmonbusApp.prototype.updateBusMarker = function (busMarker) {

    var bus_already_on_map = false;
    this.bus_group_layer.eachLayer(function (layer) {
        if (layer.idbus === busMarker.idbus) {
            this.bus_group_layer.removeLayer(layer);
            this.bus_group_layer.addLayer(busMarker);
            bus_already_on_map = true;
        }
    }.bind(this));

    if (!bus_already_on_map) {
        this.bus_group_layer.addLayer(busMarker);
    }
};


OuestmonbusApp.prototype.refreshBusCountHtml = function () {

    var bus_count_on_map = 0;
    this.bus_group_layer.eachLayer(function (layer) {
        if (!_.isUndefined(layer.idbus)) {
            bus_count_on_map++;
        }
    });

    var content = "";
    if (bus_count_on_map === 0) {
        content = "Pas de <img src='./images/bus_shadow.png' height='22' width='22' alt='bus'> sur la carte pour cet arrêt";
    }
    if (bus_count_on_map > 1) {
        content = `Voir les <b>${bus_count_on_map}</b> <img src='./images/bus_shadow.png' height='22' width='22' alt='bus'> sur la carte`;
    }
    if (bus_count_on_map === 1) {
        content = "Voir le <img src='./images/bus_shadow.png' height='22' width='22' alt='bus'> sur la carte";
    }
    $("#bus_count").html(content);
    $("#bus_count").click(this.onBusCountClick.bind(this));
};

/*
Callback lors du click sur le bouton ce recentrage de la carte sur les bus.
*/
OuestmonbusApp.prototype.onBusCountClick = function () {

    var bus_map_bounds = [this.markerCurrentDisplay.getLatLng()];

    this.bus_group_layer.eachLayer(function (layer) {
        if (!_.isUndefined(layer.idbus)) {
            bus_map_bounds.push(layer.getLatLng());
        }
    }.bind(this));

    this.map.fitBounds(bus_map_bounds, {
        padding: L.point(100, 100)
    });
};

/**
 * Ouvre la popup d'une station
 * 
 * @param  {L.marker} marker -
 */
OuestmonbusApp.prototype.openStation = function (marker) {

    this.needHelp = false;
    this.delayRefreshAllStations();
    this.markerCurrentDisplay = marker;

    this.getNextDepartures(marker.lines, marker.idarret, function (data) {
        this.setPopupContent(marker, data);
        marker.openPopup();
        if (Number(data.nhits) > 0) {
            this.refreshLinesBus(marker, data);
        }
    }.bind(this), function () {
        this.setPopupContent(marker, null);
        marker.openPopup();
    }.bind(this));

    this.refreshStation();

};

OuestmonbusApp.prototype.setOrigin = function (marker) {

    this.localisation.deleteMarkerMe.call(this.localisation);

    if (this.bus_station_origin !== null && this.map.hasLayer(this.bus_station_origin)) {
        this.map.removeLayer(this.bus_station_origin);
    }
    this.bus_station_origin = L.marker(marker.getLatLng(), {
        zIndexOffset: -1
    });

    this.bus_station_origin.on("click", function (e) {
        this.moveMapTo(e.latlng, this.map.DEFAULT_ZOOM);
    }.bind(this));

    this.bus_station_origin.addTo(this.map);
};

/**
 * Fonction apppellé lorsque l'utilisateur clique sur la station la plus
 * proche après une localisation.
 */
OuestmonbusApp.prototype.followNearestStation = function () {

    this.moveMapTo(this.localisation.nearestStation.marker.getLatLng(),
        this.map.DEFAULT_ZOOM,
        this.openStation.bind(this, this.localisation.nearestStation.marker)
    );
};

/**
 * Mets à jour le contenue d'une popup de station
 */
OuestmonbusApp.prototype.setPopupContent = function (marker, data) {

    var content = this.generatePopupContent(marker, data);
    var popup = marker.getPopup();
    popup.setContent(content);
    //popup.update();
};

OuestmonbusApp.prototype.getStationUrl = function (marker) {

    var z = this.map.DEFAULT_ZOOM;
    var lat = Number(marker.getLatLng().lat).toFixed(5);
    var lng = Number(marker.getLatLng().lng).toFixed(5);

    var station_url = `#map=${z}/${lat}/${lng}`;
    return station_url;
};

OuestmonbusApp.prototype.generateImgHtml = function (idligne, sens) {
    var img_picto = this.imgPicto(idligne);
    return `<a href='#' alt='afficher ligne' onclick='app.displayLine("${idligne}","${sens}");return false;'><img src='${img_picto}' height='22' width='22' alt='${idligne}'></a>`;
};

/**
 * Généré le contenu d'une popup de station.
 * 
 * @param  {L.marker} marker - marker de la station
 * @param  {object} data - donnée reçu de l'API explore
 * 
 * @return {string} - code html
 */
OuestmonbusApp.prototype.generatePopupContent = function (marker, data) {

    var station_url = this.getStationUrl(marker);

    var lines_header = [];
    var lines_img = "";

    var lines_array = marker.lines;

    _.each(lines_array, function (item) {
        var a = item.split(",");
        var line_id = a[0];
        if (line_id.indexOf('-') > 0) {
            line_id = line_id.split('-')[1];
        }
        var sens = a[1];
        if (_.indexOf(lines_header, line_id) === -1) {
            lines_img += this.generateImgHtml(line_id, sens);
            lines_img += ' ';
            lines_header.push(line_id);
        }
    }.bind(this));

    var full_content = "";
    // si il y a moins de 4 icones on les mets à coté du nom de la station
    if (_.size(lines_header) < 4) {
        full_content = `<div class='popup_title_inline'><div class='list_lines_inline'>${lines_img}</div><div class='station_name'><a class='fg-white' href='${station_url}'>${marker.nomarret}</a></div></div><!-- idarret=${marker.idarret} --> <hr>`;
    } else { // sinon les icones sont dessous
        full_content = `<div class='popup_title'><div class='station_name'><a class='fg-white' href='${station_url}'>${marker.nomarret}</a></div><div class='list_lines'>${lines_img}</div></div><!-- idarret=${marker.idarret} --> <hr>`;
    }

    if (data !== null) {

        var metrobus = this.isMetroLine(marker.lines) ? "métro" : "bus"

        if (Number(data.nhits) > 0) {

            // prepare data
            var all_records = _.reduce(data.records, function (seed, item) {
                seed.push(item.fields);
                return seed;
            }, []);

            this.refreshBusCountHtml();
            var records = _.groupBy(all_records, "idligne");
            var pluriel = all_records.length > 1 ? "s" : "";
            full_content += `<div class='header_station'><b>prochain${pluriel} passage${pluriel} de ${metrobus}:</b></div><div class='lines_content'`;

            _.each(records, function (record_line) {

                _.each(_.groupBy(record_line, "destination"), function (item) {

                    var lines_sens = _.first(item).idligne + "," + _.first(item).sens;
                    var is_terminus = (_.indexOf(marker.terminus, lines_sens) !== -1);

                    var content = this.generateLineContent(this.isMetroLine(marker.lines), is_terminus, item);
                    full_content += content;
                }.bind(this));

            }.bind(this));

        } else {
            this.displayNoBusHelp();
            this.stopprogressBar();

            full_content += "<div class='line_content'><span class='mif-not'></span> pas de passage dans l'heure</div>";
        }
        full_content += "</div><br>";
        if (!this.isMetroLine(marker.lines)) {
            full_content += "<button style='width:100%; text-align:center;' id='bus_count'>Pas de <img src='./images/bus_shadow.png' height='22' width='22' alt='bus'> sur la carte pour cet arrêt</button>";
        }
    } else {
        full_content += "<div class='header_station' style='height: 1rem; width:9rem; display: inline-block;'></div>";
    }
    full_content += "</div>";
    return full_content;
};

/**
 * Génère le contenu de la ligne dans la popup de station.
 * 
 * @param  {object} records - champs de records des données de l'API explore
 * @param  {string} idligne - numéro de ligne
 * 
 * @return {string} code html
 */
OuestmonbusApp.prototype.generateLineContent = function (isMetro, is_terminus, record) {

    var content = "";

    if (isMetro && is_terminus) {
        return "";
    }

    _.each(_.first(record, 2), function (element, index) {
        if (index === 0) {
            var destination = element.destination;

            if (_.isUndefined(destination)) {
                destination = "Ligne " + element.idligne;
            } else {
                destination = destination.replace("- | ", "-");
                destination = destination.replace(" | ", "&nbsp;");
                destination = `<b>&#9658;&nbsp;Direction&nbsp;${destination}</b>`;
            }

            if (is_terminus) {
                destination = "<b><font color='#787878 '>&nbsp; Terminus à cet arrêt</font></b>";
            }
            content += this.generateImgHtml(element.idligne, element.sens);
            content += `<span>${destination}</span><br>`;
        }
        var accurate = (element.precision === "Temps réel") && (index === 0);
        content += this.generateLineDeparture(
            new Date(),
            element.depart,
            accurate,
            element.departtheorique,
            is_terminus,
            isMetro);
    }.bind(this));

    return `<div class='line_content'>${content}</div>`;
};


/**
 * Génère le code html de la ligne d'horaire (ex "12:56:00 dans 45min ! retard 5min")
 * 
 * @param  {string} server_localtime - heure courante du serveur d'API
 * @param  {string} time - heure de passage
 * @param  {Boolean} accurate - temps réel
 * @param  {string} expected - heure théorique
 * 
 * @return {string} code html
 */
OuestmonbusApp.prototype.generateLineDeparture = function (localtime, time, accurate, expected, is_terminus, isMetro) {

    var now = moment();

    var departure = moment(time);
    var wait_min = departure.diff(now, "minutes");
    expected = moment(expected);
    var retard_min = departure.diff(expected, "minutes");

    var retard_min_prefix = "";
    if (retard_min > 0) {
        retard_min_prefix = "en retard de";
    }
    if (retard_min < 0) {
        retard_min_prefix = "en avance de";
    }
    var retard_str = "";

    var t = Math.abs(retard_min);
    if (accurate && (retard_min < -1)) {
        this.displayRetardHelp();
        retard_str = ` <span class='mif-sync-problem mif-lg fg-red'></span> <b>${retard_min_prefix} ${t} min</b>`;
    } else
        if (accurate && (retard_min !== 0)) {
            this.displayRetardHelp();
            retard_str = ` <span class='mif-sync-problem mif-lg'></span> <b>${retard_min_prefix} ${t} min</b>`;
        }

    var line_h = "";
    var wait_str = "";
    if (wait_min < 1) {
        if (is_terminus) {
            wait_str = "<span><b> dans < 1 min</b></span>";
        } else {
            wait_str = "<span class= 'fg-red'><b> dans < 1 min</b></span>";
        }
    } else {
        if (wait_min < 3 && !is_terminus && !isMetro) {
            wait_str = `<span class= 'fg-red'>dans <b>${wait_min} min</b></span>`;
        } else {
            if (wait_min > 60) {
                var hours = departure.diff(now, "hours");
                var minutes = wait_min - (hours * 60);
                wait_str = `<span>dans <b>${hours}h ${minutes} min</b></span>`;
            } else {
                wait_str = `<span>dans <b>${wait_min} min</b></span>`;
            }
        }
    }
    departure.tz('Europe/Paris');
    var t = departure.format("HH:mm:ss");
    line_h = `<u>${t}</u> ${wait_str} ${retard_str}`;

    return `<div class='popup_line'>${line_h}</div>`;
};

/**
 * Informe l'utilisateur que l'API STAR est en rade..
 */
OuestmonbusApp.prototype.displayAPIError = function (err) {

    if (this.markerCurrentDisplay) {
        this.markerCurrentDisplay.getPopup().setContent(`<b>Erreur:</b> ${err}`);
    }

    $.Notify({
        caption: "Erreur",
        content: `Impossible d'interroger les données en temps réel (${err})`,
        type: "error"
    });
};

/**
 * Annule et relance un timer pour le rafraîchissement des stations.
 */
OuestmonbusApp.prototype.delayRefreshAllStations = function () {

    if (this.refreshAllStationsTimer) {
        clearTimeout(this.refreshAllStationsTimer);
    }
    this.refreshAllStationsTimer = setTimeout(function () {
        this.getAllTodayStationsData(this.displayAllStations.bind(this));
    }.bind(this), STATION_LIST_REFRESH);
};


OuestmonbusApp.prototype.refreshLinesBus = function (markerCurrentDisplay, data) {

    // prepare data
    var all_records = _.reduce(data.records, function (seed, item) {
        seed.push(item.fields);
        return seed;
    }, []);

    // Affichage des lignes
    var lines_sens = _.map(all_records, function (item) {
        return item.idligne + "," + item.sens;
    });
    lines_sens = _.uniq(lines_sens);

    // extra lines : lignes annoncées au passage mais qui n'étaient pas prévu par les données GTFS
    // cas observé une fois, à vérifier si pertinent de garder le code.
    var extra_lines = _.reject(lines_sens, function (e) {
        return _.contains(this, e);
    }, this.markerCurrentDisplay.lines);

    var lignes_selected = _.groupBy(all_records, "idligne");
    lignes_selected = _.map(lignes_selected, function (item) {
        var first = _.first(item);
        return ["" + first.idligne + "," + first.sens];
    });
    lignes_selected = _.flatten(lignes_selected);

    if ((lignes_selected.join() !== this.last_lignes_selected.join()) && markerCurrentDisplay.line_selected == false) {
        this.displayLines(lines_sens, extra_lines, function () {
            this.deleteAllBusOnMap();
            this.getBusPositions(data, this.markerCurrentDisplay._latlng);
            this.last_lignes_selected = lignes_selected;
            this.last_ligne_bus_fetch = moment();
        }.bind(this));
    } else {
        var next_fresh_duration = moment().diff(this.last_ligne_bus_fetch);
        if (next_fresh_duration > STATION_CONTENT_REFRESH) {
            this.getBusPositions(data, this.markerCurrentDisplay._latlng);
            this.last_ligne_bus_fetch = moment();
        } else {
            var t = STATION_CONTENT_REFRESH - next_fresh_duration;
            log.debug(`Prochain rafraichissement de la position des bus dans ${t} ms`);
            this.refreshBusCountHtml();
        }
    }
};

/**
 * Rafraichie le contenue de la station actuellement ouverte
 */
OuestmonbusApp.prototype.refreshStation = function () {

    this.startprogressBar();
    if (this.refreshStationTimer) {
        clearTimeout(this.refreshStationTimer);
    }
    this.refreshStationTimer = setTimeout(function () {
        if (this.markerCurrentDisplay) {
            this.getNextDepartures(this.markerCurrentDisplay.lines, this.markerCurrentDisplay.idarret, function (data) {
                // Generation du contenue de la popup
                this.setPopupContent(this.markerCurrentDisplay, data);
                if (Number(data.nhits) > 0) {
                    // Update lines and bus
                    this.refreshLinesBus(this.markerCurrentDisplay, data);
                }
            }.bind(this), function () {
                this.setPopupContent(this.markerCurrentDisplay, null);
            }.bind(this));
            this.refreshStation();
        }
    }.bind(this), STATION_CONTENT_REFRESH);
};

OuestmonbusApp.prototype.startprogressBar = function () {

    this.stopprogressBar();
    this.progressbar.animate(1);
};

OuestmonbusApp.prototype.stopprogressBar = function () {

    this.progressbar.stop();
    this.progressbar.set(0);
};


///////////////////////////////////////////////////////////////////////////////
///////////////////////// Classe InfosTrafics /////////////////////////////////
///////////////////////////////////////////////////////////////////////////////

function InfosTrafics(app) {

    this.app = app;
    this.html = "";
    this.count = 0;

}

InfosTrafics.prototype.fetchLinesAndAlerts = function () {

    var isoNow = moment().toISOString(false);
    var q = encodeURIComponent(`debutvalidite < ${isoNow} AND finvalidite > ${isoNow}`);

    var url_api = `https://data.explore.star.fr/api/records/1.0/search?apikey=${apikey}&dataset=tco-busmetro-trafic-alertes-tr&sort=idligne&rows=100&facet=idligne&q=${q}&timezone=${tz}`;

    this.html += "<div class='accordion' data-role='accordion'>";

    log.debug("call API tco-busmetro-trafic-alertes-tr");
    $.getJSON(url_api,
        function (data) {
            _.each(data.records, function (info) {
                var picto_html = this.getPictoHtml(info.fields.idligne);
                var c = this.formatDetail(info.fields.description);
                this.html += `<div class='frame'><div class='heading'>${info.fields.titre}<div class='place-right'>${picto_html}</div></div><div class='content'>${c}</div></div>`;
                this.count += 1;
            }.bind(this));
        }.bind(this)).fail(function () {
            this.app.displayAPIError("Impossible de récupérer les infos trafics (connect)");
        }.bind(this)).done(function () {
            this.html += "</div>";
            $("#trafic_dlg_content").html(this.html);
            $("#perturbations_btn").attr("data-hint", `Infos trafic| ${this.count} pertubations`);
        }.bind(this));
};

InfosTrafics.prototype.getPictoHtml = function (ligne) {

    var img_picto = this.app.imgPicto(ligne);
    var html = `<img src='${img_picto}' height='22' width='22' alt='${ligne}' style='margin-right:2px;'>`;
    return html;
};

InfosTrafics.prototype.formatDetail = function (input) {

    return input.replace(/(?:\r\n|\r|\n)/g, "<br>");
};


///////////////////////////////////////////////////////////////////////////////
///////////////////////// Classe Localization /////////////////////////////////
///////////////////////////////////////////////////////////////////////////////

/**
 * Localisation de l'utilisateur.
 * @constructor
 */
function Localization(app, map) {

    this.app = app;
    this.map = map;

    this.marker_me = null; // position "vous êtes ici"
    this.circle_me = null; // rayon ||

    this.nearestStation = null;

    this.last_lat = null;
    this.last_lng = null;

    this.last_localisation = null;
    this.current_loc = null;
}

/**
 * Localise l'utilisateur avec le navigateur.
 */
Localization.prototype.autoLocate = function () {

    this.map.locate({
        setView: true,
        maxZoom: this.map.DEFAULT_ZOOM
    });
    this.map.once("locationfound", this.onLocationFound.bind(this));
    this.map.once("locationerror", this.onLocationError.bind(this));
};

/**
 * Appelé quand la localisation est trouvée.
 */
Localization.prototype.onLocationFound = function (e) {

    this.app.removeWaitCenterSpinner();
    this.showLocation(e.latlng, true, true, Math.round(e.accuracy / 2));
};

Localization.prototype.deleteMarkerMe = function () {

    if (this.marker_me && this.map.hasLayer(this.marker_me)) {
        this.map.removeLayer(this.marker_me);
    }
    if (this.circle_me && this.map.hasLayer(this.circle_me)) {
        this.map.removeLayer(this.circle_me);
    }
};

Localization.prototype.showLocation = function (latlng, youarehere, show_circle, radius) {

    if (this.map.getBounds().contains(latlng)) {

        if (radius > 1000) {
            this.onLocationError();
        } else {

            this.deleteMarkerMe();

            this.nearestStation = this.findNearestStation(latlng);

            if (this.nearestStation.marker) {
                if (this.nearestStation.distance < 50) {
                    this.app.moveMapTo(this.nearestStation.marker.getLatLng(), this.map.DEFAULT_ZOOM, this.app.openStation.bind(app, this.nearestStation.marker));

                } else {
                    this.marker_me = L.marker(latlng);

                    var station_url = this.app.getStationUrl(this.nearestStation.marker);
                    var station_html = `<a class='fg-white' id='nearestStationLink' href='${station_url}' onclick='app.followNearestStation();return false;'><div class='station_name'>${this.nearestStation.marker.nomarret}</div></a>`;

                    var marker_str = "";
                    if (youarehere) {
                        marker_str = "<b>Vous êtes ici !</b><br>";
                        if (this.nearestStation.distance < 1000) {
                            var d = Math.round(this.nearestStation.distance);
                            marker_str += `L'arrêt de bus le plus proche est: <br>${station_html} à ${d} m.`;
                        }

                        this.marker_me.bindPopup(marker_str);
                        this.marker_me.addTo(this.map);
                        this.marker_me.openPopup();
                    } else {
                        if (this.nearestStation.distance < 1000) {
                            var d = Math.round(this.nearestStation.distance);
                            marker_str += `L'arrêt de bus le plus proche est: <br>${station_html} à ${d} m.`;
                            this.marker_me.bindPopup(marker_str);
                            this.marker_me.addTo(this.map);
                            this.marker_me.openPopup();
                        } else {
                            this.marker_me.addTo(this.map);
                        }
                    }
                }
            }
            if (show_circle) {
                this.circle_me = L.circle(latlng, radius).addTo(this.map);
            }
        }
    } else {

        var dialog = $("#localisation_nothere_dlg").data("dialog");
        this.app.dialogCurrentDisplay = dialog;

        $("#localisation_nothere_dlg_close").click({
            dlg: dialog
        }, function (e) {
            e.data.dlg.close();
            $(".photon-input").show();
            $(".photon-input").focus();
        });

        dialog.open();
        this.app.updateDialogPosition("#localisation_nothere_dlg");

        this.app.moveMapTo(this.map.DEFAULT_POSITION, this.map.DEFAULT_ZOOM);
    }
};


Localization.prototype.findNearestStation = function (latlng) {

    var nearestStation = {};
    var res = L.GeometryUtil.closestLayer(this.map, this.map.cluster_markers.getLayers(), latlng);

    if (res) {
        nearestStation.marker = res.layer;
        nearestStation.distance = res.distance;
    }

    return nearestStation;
};


/**
 * Appelé quand la localisation n'est trouvée.
 */
Localization.prototype.onLocationError = function () {

    this.app.removeWaitCenterSpinner();
    var dialog = $("#localisation_failed_dlg").data("dialog");
    this.app.dialogCurrentDisplay = dialog;

    $("#localisation_failed_dlg_close").click({
        dlg: dialog
    }, function (e) {
        e.data.dlg.close();
        $(".photon-input").show();
        $(".photon-input").focus();
    });

    dialog.open();
    this.app.updateDialogPosition("#localisation_failed_dlg_close");
};

/**
 * Converti le hash URL en LatLng.
 * 
 * @param {string} url
 */
Localization.prototype.urlToZoomLatLng = function (url) {

    var res = null;
    try {
        var map_pos = url.substring(url.indexOf("map"));

        var zoom_str_x = map_pos.indexOf("=") + 1;
        var lat_str_x = map_pos.indexOf("/") + 1;
        var lng_str_x = map_pos.indexOf("/", lat_str_x);

        var zoom = Number(map_pos.substring(zoom_str_x, lat_str_x - 1));

        var lat = Number(map_pos.substring(lat_str_x, lng_str_x)).toFixed(5);
        var lng = Number(map_pos.substring(lng_str_x + 1)).toFixed(5);

        if (isNaN(lat) || isNaN(lng)) {
            throw "Nan";
        }

        var latlng = L.latLng(lat, lng);

        if (!this.map.MAXBOUNDS.contains(latlng)) {
            throw "not in Map";
        }

        res = [zoom, latlng];
    } catch (err) {
        //log.debug("Err:"+err);
        res = null;
    }
    return res;
};
