climb tracker 🚵
  • Explorateur d’ascension
  • Explorateur de lieux
  • Espace des statistiques
  • A propos
raw = {
  // URLs of the GeoJSON files
  const geojsonUrl1 = "https://minio.lab.sspcloud.fr/lgaliana/cyclisme/data/geojson/alpes-nord-sommets.geojson";
  const geojsonUrl2 = "https://minio.lab.sspcloud.fr/lgaliana/cyclisme/data/geojson/alpes-sud-sommets.geojson";
  const geojsonUrl3 = "https://minio.lab.sspcloud.fr/lgaliana/cyclisme/data/geojson/pyrenees.geojson";
  const geojsonUrl4 = "https://minio.lab.sspcloud.fr/lgaliana/cyclisme/data/geojson/massif-central.geojson";
  const geojsonUrl5 = "https://minio.lab.sspcloud.fr/lgaliana/cyclisme/data/geojson/vosges.geojson";
  const geojsonUrl6 = "https://minio.lab.sspcloud.fr/lgaliana/cyclisme/data/geojson/autres-massifs.geojson";
  const geojsonUrl7 = "https://minio.lab.sspcloud.fr/lgaliana/cyclisme/data/geojson/autres.geojson";
  const geojsonUrl8 = "https://minio.lab.sspcloud.fr/lgaliana/cyclisme/data/geojson/missed.geojson";

  // Fetch
  const fetchGeoJSON1 = fetch(geojsonUrl1).then(response => response.json());
  const fetchGeoJSON2 = fetch(geojsonUrl2).then(response => response.json());
  const fetchGeoJSON3 = fetch(geojsonUrl3).then(response => response.json());
  const fetchGeoJSON4 = fetch(geojsonUrl4).then(response => response.json());
  const fetchGeoJSON5 = fetch(geojsonUrl5).then(response => response.json());
  const fetchGeoJSON6 = fetch(geojsonUrl6).then(response => response.json());
  const fetchGeoJSON7 = fetch(geojsonUrl7).then(response => response.json());
  const fetchGeoJSON8 = fetch(geojsonUrl8).then(response => response.json());

  // When both GeoJSONs are fetched, combine them
  let raw = Promise.all([
    fetchGeoJSON1, fetchGeoJSON2,
    fetchGeoJSON3, fetchGeoJSON4,
    fetchGeoJSON5, fetchGeoJSON6,
    fetchGeoJSON7, fetchGeoJSON8
    ]).then(values => {
    const combinedFeatures = [
      ...values[0].features,
      ...values[1].features,
      ...values[2].features,
      ...values[3].features,
      ...values[4].features,
      ...values[5].features,
      ...values[6].features,
      ...values[7].features
      ]; // Combine features from both GeoJSON files

    // Create a new GeoJSON object with combined features
    const combinedGeoJSON = {
      type: "FeatureCollection",
      features: combinedFeatures
    };

    return combinedGeoJSON

  }).catch(error => {
    console.error("Error fetching or combining GeoJSONs:", error);
  });

  return raw
}
sommets = geo.filter(raw, (d) => !d.vtt)
cols = sommets.features.map(d=>d.properties)
availableHeight = window.innerHeight
screenHeight = 0.9*(availableHeight - document.getElementById("quarto-header").offsetHeight)
availableWidth = window.innerWidth
mapWidth = (width > 400) ? 0.4*width : 0.95*width 
mapHeight = (width > 400) ? 0.85*screenHeight : 0.5*screenHeight
function Range(range, options = {}) {
  const [min, max] = range;
  const {
    className = "Range",
    vertical = false,
    label = null,
    format = (x) => +x,
    step = 1,
    value = (min + max) / 2,
    style = "",
    labelStyle = "",
    rangeStyle = "",
    valueStyle = "",
    unit = "" // New option for specifying a unit
  } = options;
  const rangeWrap = htl.html`<div class=${className} style="${style}"></div>`;
  Object.assign(rangeWrap.style, {
    display: "inline-flex",
    position: "relative",
    userSelect: "none",
    alignItems: "center",
    gap: "4px"
  });
  const valueDisplay = htl.html`<output style="${valueStyle}"></output>`;
  Object.assign(valueDisplay.style, {
    display: "inline-block"
  });
  const rangeInput = htl.html`<input type=range min=${min} max=${max} step=${step} value=${value} style=${rangeStyle}></input>`;
  Object.assign(rangeInput.style, {
    display: "inline-block"
  });
  if (vertical) {
    rangeInput.setAttribute("orient", "vertical");
    rangeInput.style.writingMode = "bt-lr";
    rangeInput.style["-webkit-appearance"] = "slider-vertical";
    rangeInput.style.width = "8px";
  }

  rangeWrap.append(rangeInput, valueDisplay);

  if (label) {
    const labelElement = htl.html`<label style=${labelStyle}>${label}</label>`;
    rangeWrap.prepend(labelElement);
  }

  rangeInput.oninput = () => {
    // Append the unit to the formatted number
    valueDisplay.textContent = `${format(rangeInput.valueAsNumber)}${unit}`;
    rangeWrap.value = rangeWrap.valueAsNumber = rangeInput.valueAsNumber;
    rangeWrap.dispatchEvent(new CustomEvent("input"));
  };

  rangeInput.oninput(); // Initialize the displayed value
  return rangeWrap;
}
startStop = () => {
const buttons = html`
<form class="form-language-switcher">
  <input value="🇫🇷" type="button" name="stop" id="button_french" class="button_lang current_lang">
  <input value="🇬🇧 🇺🇸" type="button" name="start" id="button_english" class="button_lang">
</form>
`
buttons.value = "fr"

// French button  
buttons.stop.onclick = event => {
  buttons.value = "fr";
  event.preventDefault(); // Don’t submit the form.
  buttons.dispatchEvent(new CustomEvent("input"));                      
  const french = document.getElementById("button_french") ;
  const english = document.getElementById("button_english") ;
  french.classList.add("current_lang")
  english.classList.remove("current_lang")
}
// English button  
buttons.start.onclick = event => {
  buttons.value = "en";
  event.preventDefault(); // Don’t submit the form.
  buttons.dispatchEvent(new CustomEvent("input")); 
  const french = document.getElementById("button_french") ;
  const english = document.getElementById("button_english") ;
  english.classList.add("current_lang")
  french.classList.remove("current_lang")
}

return buttons
}
category_label = html`<span class="info-container">
  <span style="color: orange" class="infopicto">${getIconSvg("info")}
    <span class="tooltip-content">
      ${lang == 'fr' ? 'Version simplifiée de la classification UCI <br>Dénivelé:' : 'Simplified UCI classification<br>Gain:'}
      ${classificationDetails}
    </span>
  </span>
</span>`
classificationDetails = `
<ul>
  <li>Cat 4: 80 - 159m</li>
  <li>Cat 3: 160 - 319m</li>
  <li>Cat 2: 320 - 639m</li>
  <li>Cat 1: 640 - 799m</li>
  <li>HC: 800m and more </li>
</ul>
`
function slice(...options) {
  return array => array.slice(...options);
}
function get_nearest_point(e, points){
  // 'e.latlng' gives you the LatLng object of where the mouse currently is
  const mouseLatLng = e.latlng;
  const targetPoint = turf.point([mouseLatLng.lng, mouseLatLng.lat]);
  var nearest = turf.nearestPoint(targetPoint, points);
  return nearest
}
// Function to convert array of coordinate arrays to a FeatureCollection
function toGeoJSONFeatureCollection(data) {
  const coordsArray = data.features.map(d => d.geometry.coordinates);
  // Map each coordinate array to a GeoJSON Point feature
  const features = coordsArray.flat().map(coords => ({
    "type": "Feature",
    "properties": {
      // If you have properties to include, add them here
      "altitude": coords[2]  // Example property
    },
    "geometry": {
      "type": "Point",
      "coordinates": [coords[0], coords[1]]  // Only longitude and latitude are used for Point
    }
  }));

  // Return as a FeatureCollection
  return {
    "type": "FeatureCollection",
    "features": features
  };
}
function normalize(gj) {
    if (!gj || !gj.type) return null;
    var types = {
        Point: 'geometry',
        MultiPoint: 'geometry',
        LineString: 'geometry',
        MultiLineString: 'geometry',
        Polygon: 'geometry',
        MultiPolygon: 'geometry',
        GeometryCollection: 'geometry',
        Feature: 'feature',
        FeatureCollection: 'featurecollection'
    };
    var type = types[gj.type];
    if (!type) return null;

    if (type === 'geometry') {
        return {
            type: 'FeatureCollection',
            features: [{
                type: 'Feature',
                properties: {},
                geometry: gj
            }]
        };
    } else if (type === 'feature') {
        return {
            type: 'FeatureCollection',
            features: [gj]
        };
    } else if (type === 'featurecollection') {
        return gj;
    }
}
function fetchAndNormalizeGeoJSON(id, baseUrl) {
    return fetch(`${baseUrl}/${id}.geojson`)
        .then(response => response.json())
        .then(normalize) // Assuming `normalize` function is available in the scope
        .catch(error => {
          console.error(`Failed to fetch or normalize GeoJSON for ID: ${id}`, error);
          return null;
        }) ;
}
function mergeGeoJSONs(ids, baseUrl) {
    const promises = ids.map(id => fetchAndNormalizeGeoJSON(id, baseUrl));

    return Promise.all(promises)
        .then(normalizedGeoJSONs => {
            return normalizedGeoJSONs.reduce((output, geojson) => {
                if (geojson && geojson.features) {
                    output.features.push(...geojson.features);
                }
                return output;
            }, {
                type: 'FeatureCollection',
                features: []
            });
        });
}
async function getCombinedGeoJSON(ids, baseUrl) {
  try {
    let combinedGeoJSON = await mergeGeoJSONs(ids, baseUrl);
    console.log('Combined GeoJSON:', combinedGeoJSON);
    // Handle the combined GeoJSON (e.g., display on map)
    return combinedGeoJSON; // This will be a GeoJSON object
  } catch (error) {
    console.error('An error occurred while merging GeoJSONs:', error);
    // Handle the error appropriately
  }
}
import {getIconSvg} from '@saneef/feather-icons'
turf = require("@turf/turf@6")
geo = require("geotoolbox@2")
Plot = require("https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6.13/dist/plot.umd.min.js")
import {multiAutoSelect} from "@linogaliana/multi-auto-select"
function rank_ascent(sommetsGeoJson, variableToPlot, selectedClimb, lang = "fr"){
 const namelabel = (lang == "fr") ? "Ascension" : "Ascent" 
 const gainlabel = (lang == "fr") ? "Dénivelé positif" : "Elevation gain" 
 const massiflabel = (lang == "fr") ? "Massif" : "Mountain side" 
 const percentlabel = (lang == "fr") ? "Pente moyenne" : "Average slope" 
 let p = Plot.plot({
  color: {
      type: "sequential",
      scheme: "Turbo"
    },
    marks: [
      Plot.ruleX(
        sommetsGeoJson.features.map(d=>d.properties),
        {
          x: variableToPlot, stroke: variableToPlot,
          channels: {
            nom: {label: namelabel, value: "nom"},
            deniv: {label: gainlabel, value: (d) => `${d.denivellation}m`},
            massif: {label: massiflabel, value: "massif"},
            percent: {value: (d) => `${d.percent_moyen}%`, label: percentlabel},
            },
          tip: {
            format: {
              stroke : false
            },
            anchor: "top"
          },
          strokeOpacity: 0.2}
      ),
      Plot.ruleX([selectedClimb], {x : variableToPlot, stroke: "red", strokeWidth: 2})
    ]
  })
  return p
}
viewof lang = startStop()
english = document.getElementById("button_english")
french = document.getElementById("button_french")

function change(lang_on, lang_off) {
  if (!lang_on.classList.contains("current_lang")) {
    // if the span that the user clicks on does not have the "current_lang" class
    lang_on.classList.add("current_lang");
    // add the "current_lang" class to it
    lang_off.classList.remove("current_lang");
    // remove the "current_lang" class from the other span
  }
}

english.addEventListener("click", function() {
    change(english, french);
  }, false
);
french.addEventListener("click", function() {
    change(french, english);
  }, false
);
label_click_map = (lang == "fr") ? "Cliquer sur ce sommet pour en savoir plus" : "Click to learn more on that ascent"
no_ascent_label = (lang == "fr") ? "Pas d'ascension sélectionnée, cliquer sur la carte pour afficher le profil de l'ascension." : "No selected ascent, click on the map to see ascent details."
finding_ascent_label = (lang == "fr") ? "Ascensions correspondant à la recherche." : "Ascent corresponding to search criterium."
label_search_massif = (lang == "fr") ?
  "Choisir des massifs à afficher" : 
  "Choose climbing regions"
header_table_name = (lang == "fr") ? "Ascension" : "Ascent"
header_table_departure = (lang == "fr") ? "Départ" : "Start"
header_table_alt = (lang == "fr") ? "Altitude d'arrivée" : "Arrival height"
header_table_avgpercent = (lang == "fr") ? "Pente moyenne (%)" : "Average slope (%)"
header_table_maxpercent = (lang == "fr") ? "Pente maximale (%)" : "Maximum slope (%)"
search_label_choice = (lang == "fr") ? "Rechercher une ascension" : "Finding an ascent"
explorer_label_choice = (lang == "fr") ? "Laissez moi explorer !" : "Let me explore !"
switch_label =  (lang == "fr") ? "Switch to English version 🇬🇧 🇺🇸" : "Passer à la version française 🇫🇷"
message_category = lang == 'fr' ? `Version simplifiée de la classification UCI ${classificationDetails}` : `Simplified UCI classification ${classificationDetails}`
filter_checkbox_deniv = (lang == "fr") ? "Dénivelé" : "Height gain"
filter_checkbox_category = (lang == "fr") ? "Catégorie de l'ascension" : "Ascent category"
filter_checkbox_length = (lang == "fr") ? `Longueur de l'ascension` : "Ascension length"
filter_checkbox_slope = (lang == "fr") ? "Pente moyenne" : "Average slope"
filter_multiselect_group = (lang == "fr") ? "Massif de l'ascension" : "Mountain chain"
label_search_widget = (lang == "fr") ? "Rechercher un col" : "Finding an ascent"
label_find_adress = (lang == "fr") ?
  "1️⃣ Taper une adresse ou une localisation dans la barre de recherche 👇️" :
  "1️⃣ Search for a location below 👇️"
function availableLayers(L, map){
  
  // Define tile layers
  var defaultLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
    attribution: '© OpenStreetMap contributors'
  }).addTo(map); // Set as the default layer
  
  var topoLayer = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
    maxZoom: 17,
    attribution: '© OpenStreetMap contributors, © OpenTopoMap (CC-BY-SA)'
  });
  
  var cycleLayer = L.tileLayer('https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', {
    maxZoom: 20,
    attribution: '© OpenStreetMap contributors, © CyclOSM'
  });
  
  // Add the base layers to a layer control
  var baseLayers = {
    "Default": defaultLayer,
    "Topographic": topoLayer,
    "Cycling": cycleLayer
  };

  return baseLayers  
}
whenClicked =  (event) => {
  mutable selectedClimb = event.target.feature.properties
  }
function featureEvents(feature, layer) {
   layer.on({
     click: whenClicked, //callback functions
   });
}
function fetchAndDisplayAscentRoute(map, feature, color = 'green') {
  const id = feature.properties.id;
  const polylineUrl = `https://minio.lab.sspcloud.fr/lgaliana/cyclisme/data/geojson/split/${id}.geojson`;

  fetch(polylineUrl)
    .then(response => response.json())
    .then(data => {
      // Modify the ascentTrackPolyline function or create a similar one to accept a color parameter
      ascentTrackPolyline(map, data, feature, color);
    })
    .catch(error => console.error('Error loading the polyline GeoJSON:', error));
}
function createIcon(feature, coordinates, color = null){
        const color_value = (color == null) ? getColorForCategory(feature.properties.category) : color ;
        const iconUrl = `https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-${color_value}.png`;
                
        var icon_color = new L.Icon({
          iconUrl: iconUrl,
          shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
          iconSize: [25, 41],
          iconAnchor: [12, 41],
          popupAnchor: [1, -34],
          shadowSize: [41, 41]
        });
        
        return L.marker(coordinates, {icon: icon_color})

}
function getColorForCategory(category) {
  switch (category) {
    case 'Cat 1':
      return 'blue';
    case 'Cat 2':
      return 'green';
    case 'Cat 3':
      return 'yellow';
    case 'Cat 4':
      return 'orange';
    case 'HC':
      return 'red';
    default:
      return 'gray';
  }
}
function ascentTrackPolyline(map, data, feature, color){
            // Create a polyline from the GeoJSON and add it to the map
            const polyline = L.geoJson(data, {
              color: color, // You can customize the polyline's style here
              weight: 3
            }).addTo(map);

            // Mouseover for ascent characteristics 
            polyline.on('mouseover', function() {
              this.bindTooltip(
                 tooltipRoute(feature),
                {permanent: false, direction: "top"}).openTooltip();
              
            });

            // Get mouse location on the map to interactively update a figure
            const points = toGeoJSONFeatureCollection(data);
            
            // polyline.on('mousemove', function(e) {
              // var nearest = get_nearest_point(e, points)
              // Update the mutable hover_alt to the altitude
              // mutable hover_alt = nearest;
            // });

  return polyline
}
function popUpSummit(feature, lang){
  const AscentName = (lang == "fr") ? "Ascension" : "Ascent" ;
  const departure = (lang == "fr") ? "Départ" : "Start" ;
  const maxAlt = (lang == "fr") ? "Sommet" : "Summit" ;
  const climbLength = (lang == "fr") ? "Longueur" : "Length" ;
  const gain = (lang == "fr") ? "Dénivelé" : "Gain" ;
  const averageSlope = (lang == "fr") ? "Pente moyenne" : "Average slope" ; 
  const maxSlope = (lang == "fr") ? "Pente maximale" : "Maximum slope" ; 
  const hrefText =  (lang == "fr") ? "Voir sur" : "See on"
  const popup = `
          <b>${AscentName}</b>: ${feature.properties.nom}<br>
          <b>${departure}</b>: ${feature.properties.depart} (${feature.properties.massif})<br> 
          <b>${maxAlt}</b>: ${feature.properties.alt}m<br>
          <b>${climbLength}</b>: ${feature.properties.longueur}km<br>
          <b>${gain}</b>: ${feature.properties.denivellation}m (${feature.properties.category})<br>
          <b>${averageSlope}</b>: ${feature.properties.percent_moyen}%<br>
          <b>${maxSlope}</b>: ${feature.properties.percent_maximal}%<br>
          <a href="${feature.properties.href}" target="_blank">${hrefText} <code>https://cols-cyclisme.com/</code></a>
` 
  return popup

}
function tooltipSummit(feature){
  const tooltipMessage = `<b>${feature.properties.nom}</b><br><i>${label_click_map}</i>`
  return tooltipMessage
}
function tooltipRoute(feature){
  const departure = (lang == "fr") ? "Départ de" : "Start from" ;
  const maxAlt = (lang == "fr") ? "altitude d'arrivée:" : "summit at" ;
  const climbLength = (lang == "fr") ? "Ascension de" : "Climbing length is" ;
  const averageSlope = (lang == "fr") ? "de moyenne" : "on average" ; 
  const denivelle = (lang == "fr") ? "D+" : "gain" ; 
  const maxSlope = (lang == "fr") ? "Pente maximale" : "Maximum slope" ; 

  const popup = `
                  <b>${feature.properties.nom}</b><br>
                  ${departure} ${feature.properties.depart} (${maxAlt} ${feature.properties.altitude}m)<br>
                  ${climbLength} ${feature.properties.longueur}km, ${feature.properties.percent_moyen}% ${averageSlope} (${feature.properties.denivellation}m ${denivelle})<br>
                  <i>${maxSlope}:  ${feature.properties.percent_maximal}%</i>
  `
  return popup
}
function container_ascent(feature, lang, width){

  const imageInitial = feature.properties.profil_image_url ;

  const imageURL = imageInitial.replace(
          'profils.cols-cyclisme.com',
          'minio.lab.sspcloud.fr/lgaliana/cyclisme/data/images'
        ) ;

  let container ;

  if (width > 400){
    container = `
      <div id="container-statistics" style="display: flex; flex-direction: column;">
        <div style="display: flex; justify-content: space-between; align-items: flex-start; width: 100%;">
          <div style="width: 20%;">
            ${popUpSummit(feature, lang, width > 400)}
          </div>
          <div id="ascent-image" style="width: 80%;">
            <img src="${imageURL}" alt="" style="max-width: 100%; height: auto; max-height:${screenHeight*0.5};">
          </div>
        </div>
        <div id="stats-denivellation" style="width: 100%;">
          <!-- Content for stats-denivellation goes here -->
        </div>
      </div>
          ` ;
  } else{
    container = `
      <div id="container-statistics" style="display: flex; flex-direction: column;">
          <div style="width: 100%;">
            ${popUpSummit(feature, lang, width > 400)}
          </div>
        <div id="stats-denivellation" style="width: 100%;">
          <!-- Content for stats-denivellation goes here -->
        </div>
          <div id="ascent-image" style="width: 80%;">
            <img src="${imageURL}" alt="" style="max-width: 100%; height: auto; max-height:${screenHeight*0.5};">
          </div>
      </div>
    `
  }

  return container
}
L.GeocoderBAN = L.Control.extend({
        options: {
            position: 'topleft',
            style: 'control',
            placeholder: 'adresse',
            resultsNumber: 7,
            collapsed: true,
            serviceUrl: 'https://api-adresse.data.gouv.fr/search/',
            minIntervalBetweenRequests: 250,
            defaultMarkgeocode: true,
            autofocus: true
        },
        includes: L.Evented.prototype || L.Mixin.Events,
        initialize: function (options) {
            L.Util.setOptions(this, options);
        },
        onRemove: function (map) {
            map.off('click', this.collapseHack, this);
        },
        onAdd: function (map) {
            var className = 'leaflet-control-geocoder-ban';
            var container = this.container = L.DomUtil.create('div', className + ' leaflet-bar');
            var icon = this.icon = L.DomUtil.create('button', className + '-icon', container);
            var form = this.form = L.DomUtil.create('div', className + '-form', container);
            var input;
            
            map.on('click', this.collapseHack, this);
            
            icon.innerHTML = '&nbsp;';
            icon.type = 'button';
            
            input = this.input = L.DomUtil.create('input', '', form);
            input.type = 'text';
            input.placeholder = this.options.placeholder;
            
            this.alts = L.DomUtil.create('ul',
            className + '-alternatives ' + className + '-alternatives-minimized',
            container);
            
            L.DomEvent.on(icon, 'click', function (e) {
                this.toggle();
                L.DomEvent.preventDefault(e);
            }, this);
            L.DomEvent.addListener(input, 'keyup', this.keyup, this);
            
            L.DomEvent.disableScrollPropagation(container);
            L.DomEvent.disableClickPropagation(container);
            
            if (!this.options.collapsed) {
                this.expand();
                if (this.options.autofocus) {
                    setTimeout(function () { input.focus(); }, 250);
                }
            }
            if (this.options.style === 'searchBar') {
                L.DomUtil.addClass(container, 'searchBar');
                var rootEl = document.getElementsByClassName('leaflet-control-container')[0];
                rootEl.appendChild(container);
                return L.DomUtil.create('div', 'hidden');
            } else {
                return container;
            }
        },
        minimizeControl: function () {
            if (this.options.style === 'control') {
                this.collapse();
            } else {
                // for the searchBar: only hide results, not the bar
                L.DomUtil.addClass(this.alts, 'leaflet-control-geocoder-ban-alternatives-minimized');
            }
        },
        toggle: function () {
            if (this.style != 'searchBar') {
                if (L.DomUtil.hasClass(this.container, 'leaflet-control-geocoder-ban-expanded')) {
                    this.collapse();
                } else {
                    this.expand();
                }
            }
        },
        expand: function () {
            L.DomUtil.addClass(this.container, 'leaflet-control-geocoder-ban-expanded');
            if (this.geocodeMarker) {
                this._map.removeLayer(this.geocodeMarker);
            }
            this.input.select();
        },
        collapse: function () {
            L.DomUtil.removeClass(this.container, 'leaflet-control-geocoder-ban-expanded');
            L.DomUtil.addClass(this.alts, 'leaflet-control-geocoder-ban-alternatives-minimized');
            this.input.blur();
        },
        collapseHack: function (e) {
            // leaflet bug (see #5507) before v1.1.0 that converted enter keypress to click.
            if (e.originalEvent instanceof MouseEvent) {
                this.minimizeControl();
            }
        },
        moveSelection: function (direction) {
            var s = document.getElementsByClassName('leaflet-control-geocoder-ban-selected');
            var el;
            if (!s.length) {
                el = this.alts[direction < 0 ? 'firstChild' : 'lastChild'];
                L.DomUtil.addClass(el, 'leaflet-control-geocoder-ban-selected');
            } else {
                var currentSelection = s[0];
                L.DomUtil.removeClass(currentSelection, 'leaflet-control-geocoder-ban-selected');
                if (direction > 0) {
                    el = currentSelection.previousElementSibling ? currentSelection.previousElementSibling : this.alts['lastChild'];
                } else {
                    el = currentSelection.nextElementSibling ? currentSelection.nextElementSibling : this.alts['firstChild'];
                }
            }
            if (el) {
                L.DomUtil.addClass(el, 'leaflet-control-geocoder-ban-selected');
            }
        },
        keyup: function (e) {
            switch (e.keyCode) {
                case 27:
                    // escape
                    this.minimizeControl();
                    L.DomEvent.preventDefault(e);
                    break;
                case 38:
                    // down
                    this.moveSelection(1);
                    L.DomEvent.preventDefault(e);
                    break;
                case 40:
                    // up
                    this.moveSelection(-1);
                    L.DomEvent.preventDefault(e);
                    break;
                case 13:
                    // enter
                    var s = document.getElementsByClassName('leaflet-control-geocoder-ban-selected');
                    if (s.length) {
                        this.geocodeResult(s[0].geocodedFeatures);
                    }
                    L.DomEvent.preventDefault(e);
                    break;
                default:
                    if (this.input.value) {
                        var params = {q: this.input.value, limit: this.options.resultsNumber};
                        var t = this;
                        if (this.setTimeout) {
                            clearTimeout(this.setTimeout);
                        }
                        // avoid responses collision if typing quickly
                        this.setTimeout = setTimeout(function () {
                            getJSON(t.options.serviceUrl, params, t.displayResults(t));
                        }, this.options.minIntervalBetweenRequests);
                    } else {
                        this.clearResults();
                    }
                    L.DomEvent.preventDefault(e);
            }
        },
        clearResults: function () {
            while (this.alts.firstChild) {
                this.alts.removeChild(this.alts.firstChild);
            }
        },
        displayResults: function (t) {
            t.clearResults();
            return function (res) {
                if (res && res.features) {
                    var features = res.features;
                    L.DomUtil.removeClass(t.alts, 'leaflet-control-geocoder-ban-alternatives-minimized');
                    for (var i = 0; i < Math.min(features.length, t.options.resultsNumber); i++) {
                        t.alts.appendChild(t.createAlt(features[i], i));
                    }
                }
            };
        },
        createAlt: function (feature, index) {
            var li = L.DomUtil.create('li', '');
            var a = L.DomUtil.create('a', '', li);
            li.setAttribute('data-result-index', index);
            a.innerHTML = '<strong>' + feature.properties.label + '</strong>, ' + feature.properties.context;
            li.geocodedFeatures = feature;
            var clickHandler = function (e) {
                this.minimizeControl();
                this.geocodeResult(feature);
            };
            var mouseOverHandler = function (e) {
                var s = document.getElementsByClassName('leaflet-control-geocoder-ban-selected');
                if (s.length) {
                    L.DomUtil.removeClass(s[0], 'leaflet-control-geocoder-ban-selected');
                }
                L.DomUtil.addClass(li, 'leaflet-control-geocoder-ban-selected');
            };
            var mouseOutHandler = function (e) {
                L.DomUtil.removeClass(li, 'leaflet-control-geocoder-ban-selected');
            };
            L.DomEvent.on(li, 'click', clickHandler, this);
            L.DomEvent.on(li, 'mouseover', mouseOverHandler, this);
            L.DomEvent.on(li, 'mouseout', mouseOutHandler, this);
            return li;
        },
        geocodeResult: function (feature) {
            this.minimizeControl();
            this.markGeocode(feature);
        },
        markGeocode: function (feature) {
            var latlng = [feature.geometry.coordinates[1], feature.geometry.coordinates[0]];
            this._map.setView(latlng, 14);
            this.geocodeMarker = new L.Marker(latlng)
                .bindPopup(feature.properties.label)
                .addTo(this._map)
                .openPopup();
        }
    });
getJSON = function (url, params, callback) {
  var xmlHttp = new XMLHttpRequest()
  xmlHttp.onreadystatechange = function () {
    if (xmlHttp.readyState !== 4) {
      return
    }
    if (xmlHttp.status !== 200 && xmlHttp.status !== 304) {
      return
    }
    callback(JSON.parse(xmlHttp.response))
  }
  xmlHttp.open('GET', url + L.Util.getParamString(params), true)
  xmlHttp.setRequestHeader('Accept', 'application/json')
  xmlHttp.send(null)
}
L.geocoderBAN = function (options) {
    return new L.GeocoderBAN(options)
}
viewof radiusInner = Inputs.range([0, 100], {value: 20})
radius = Inputs.bind(
          Range([1, 100], {unit: "km", value: 20, step: 1}),
          viewof radiusInner
)
table_selected = Inputs.table(
  search,
  {
    columns: [
      "nom",
      "depart",
      "longueur",
      "denivellation",
      "altitude",
      "percent_moyen",
      "percent_maximal"
    ],
    header: {
        nom: header_table_name,
        depart: header_table_departure,
        longueur: filter_checkbox_length,
        denivellation: filter_checkbox_deniv,
        altitude: header_table_alt,
        percent_moyen: header_table_avgpercent,
        percent_maximal: header_table_maxpercent
      },
    sort: "denivellation", reverse: true
})
help_page2 = {
  if (selectlatlng == null){
    return label_find_adress
  }
  return html`<div>${label_choose_radius}</div><div class="radius-selector">${radius}</div>`
}
function messageRadius(foundfeatures){
  if (foundfeatures == null){
    return null
  }
  const message = (lang == "fr") ?
  `2️⃣ Choisissez un rayon autour de <span class="foundplace">${foundfeatures.properties.label}</span> puis explorez la carte 👇️` :
  `2️⃣ Choose a radius size around <span class="foundplace">${foundfeatures.properties.label}</span> and explore the map 👇️`
  return message
}
label_choose_radius = messageRadius(foundfeatures)
html
`
<div class="lang-selector">${viewof lang}</div>
<div class="container">
  <div class="message-input-select">${help_page2}</div>
<div/>
`
search = (aroundClimb == null) ? cols : aroundClimb.features.map(d => d.properties)
message_climb_radius = function(aroundClimb){

  if (aroundClimb == null){
    return ``
  }

  const message = (lang == "fr") ?
    html`${search.features.length} ascensions dans un rayon de ${radius} km` :
    html`${search.features.length} climb in a ${radius} km radius` ;

  return message 

}
(width > 400) ?
  html`
  <div class="main-container">
    <div id = "leafletmap" style="width: 40%;">${leafletmap}</div>
    <div class="column spacer" style="width: 2%;">
        <!-- Spacer column -->
    </div>
    <div style="width: 58%;">
      <div id="available-ascent">
      <div><i>${finding_ascent_label}</i></div>
      <br>
      ${table_selected}
      <br>
      </div>
    <div id="statsElement"><i>${no_ascent_label}</i></div>
    </div>
  </div>
  ` :
  html`
    <div id = "leafletmap" style="width: 100%;">${leafletmap}</div>
    <div style="width: 100%;">
      <div id="available-ascent">
      <div><i>${finding_ascent_label}</i></div>
      <br>
      ${table_selected}
      <br>
      </div>
    <div id="statsElement"><i>${no_ascent_label}</i></div>
    </div>
  `
function addGeoJSONToMap(geoJSON, map, pointsWithin) {
    L.geoJSON(geoJSON, {
        style: function(feature) {
            // Define the style of the polyline here if needed
            return { color: feature.properties.stroke || 'blue', weight: 4 };
        },
        onEachFeature: function(feature, layer) {
            const id = feature.properties.url.replace(".gpx", '') ;
            const summit_info = geo.filter(pointsWithin, (d) => d.id == id)
            let selectedLayer = null;

            layer.bindTooltip(
              tooltipSummit(
                summit_info.features[0]
              ),
                {permanent: false, direction: 'auto'}
            );

            layer.bindPopup(
              popUpSummit(summit_info.features[0], lang)
            );

            layer.on({
              click: (event) => {
                if (selectedLayer && selectedLayer != layer) {
                      selectedLayer.setStyle({
                        color: selectedLayer.feature.properties.stroke || 'blue', // Reset to original color
                        weight: 4 // Reset to original weight
                      });
                    }
                mutable selectedClimb = summit_info.features[0] ;
                // Set the clicked layer as the new selected layer
                selectedLayer = layer; 
                layer.setStyle({
                        color: 'red', // Set color to red for the selected polyline
                        weight: 4
                    });               
              },
            });

            // Existing event listener for popup opens
            layer.on('popupopen', function() {
              const statsElement = document.getElementById('statsElement');
              const availableAscents = document.getElementById('available-ascent');        
              
              // Update the content of 'statsElement' with the desired statistics from 'feature.properties'
              statsElement.innerHTML = container_ascent(summit_info.features[0], lang, width) ;
              availableAscents.innerHTML = "" ;

              // Fetch route for selected ascent
              fetchAndDisplayAscentRoute(map, summit_info.features[0], 'red');
            });


            layer.on('click', function() {
                this.setStyle({
                    color: 'red', // Change polyline color on mouseover
                    weight: 4 // Increase polyline weight on mouseover
                });
            });
            layer.on('mouseover', function() {
              if (layer !== selectedLayer) {
                this.setStyle({
                    color: 'yellow', // Change polyline color on mouseover
                    weight: 6 // Increase polyline weight on mouseover
                });
              }
            });
            layer.on('mouseout', function() {
              if (layer !== selectedLayer) {
                this.setStyle({
                    color: feature.properties.stroke || 'blue', // Reset color
                    weight: 2 // Reset weight
                });
              }
            });
        }
    }).addTo(map);
}
mutable foundfeatures = null ;
function getPointsWithinCircle(centerCircle, sommets){

    var options = {steps: 64, units: 'kilometers'};
    var circle = turf.circle(
      centerCircle , radiusInner, options
    );
    // Perform spatial query with Turf.js using the drawn circle
    var pointsWithin = turf.pointsWithinPolygon(sommets, circle);

    return pointsWithin    
  }
console.log(foundfeatures)
leafletmap = {

  // Create a container element for leaflet map
  let parent = DOM.element('div', { style: `width:${mapWidth}px;height:${mapHeight}px` });
  yield parent;
  var map = L.map(parent).setView([45.853459, 2.349312], 5);

  const baseUrl = 'https://minio.lab.sspcloud.fr/lgaliana/cyclisme/data/geojson/split';
  var radiustemp = radius ;

  // Add the layer control to the map
  let baseLayers = availableLayers(L, map) ;
  L.control.layers(baseLayers).addTo(map);


  var geocoder = L.geocoderBAN({
    collapsed: false, style: 'searchBar'
    }).addTo(map)

  geocoder.markGeocode = function(feature) {
    var latlng = [feature.geometry.coordinates[1], feature.geometry.coordinates[0]]
    map.setView(latlng, 8)
    mutable selectlatlng = latlng;
    mutable foundfeatures = feature ;
  }

  if (selectlatlng != null){

    console.log(selectlatlng)

    // Center of the circle
    var foundpoint = createIcon(
      null, selectlatlng, "black"
    ).addTo(map);
    foundpoint.bindPopup(foundfeatures.properties.label).openPopup() ;
    // Circle
    var circle = L.circle(selectlatlng, { radius: radiusInner*1000 }).addTo(map);
    map.fitBounds(circle.getBounds())

    // Detect points within circle
    const pointsWithin = getPointsWithinCircle(selectlatlng.reverse(), sommets) ;
    mutable aroundClimb = pointsWithin

    // Add rot

    // Add summit markers
    var geojsonLayer = L.geoJson(
        pointsWithin, {
          onEachFeature: popEventSummit,
          pointToLayer: function(feature, coordinates) {
            return createIcon(feature, coordinates)        
        }
    }).addTo(map);


    // Add ascent routes tracks
    const ids = pointsWithin.features.map(d=> d.properties.id) ;
    getCombinedGeoJSON(ids, baseUrl)
    .then(combinedGeoJSON => {
        // Add each feature from the combined GeoJSON as a separate polyline
        combinedGeoJSON.features.forEach((feature) => {
            addGeoJSONToMap(feature, map, pointsWithin);
        });
    })
    .catch(error => {
        console.error('An error occurred:', error);
    });

    }

  // Define a function that will be called for each feature in your GeoJSON layer
  function popEventSummit(feature, layer) {

      // Add mouseover event listener
      layer.on('mouseover', function() {
        const tooltipMessage = tooltipSummit(feature) ;
        layer.bindTooltip(
          tooltipMessage, {permanent: false, direction: "auto"}
          ).openTooltip();
      });

      // Create popup for ascent summit
      layer.bindPopup(
        popUpSummit(feature, lang)
      );

     layer.on({
       click: whenClicked, //callback functions
     });
      
      // Existing event listener for popup opens
      layer.on('popupopen', function() {
        const statsElement = document.getElementById('statsElement');
        const availableAscents = document.getElementById('available-ascent');        
        
        // Update the content of 'statsElement' with the desired statistics from 'feature.properties'
        statsElement.innerHTML = container_ascent(feature, lang, width) ;
        availableAscents.innerHTML = "" ;

        // Fetch route for selected ascent
        fetchAndDisplayAscentRoute(map, feature, 'red');
      });

  }

}
mutable selectlatlng = null
mutable selectedClimb = null ;
mutable aroundClimb = null
{
  document.querySelector('#stats-denivellation').appendChild(
    rank_ascent(sommets, "denivellation", selectedClimb, lang)
  )
  document.querySelector('#stats-denivellation').appendChild(
    rank_ascent(sommets, "longueur", selectedClimb, lang)
  )

}