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
}
max_length = d3.max(
  sommets.features.map(d=>d.properties.longueur)
)

max_denivelation = d3.max(
  sommets.features.map(d=>d.properties.denivellation)
)

max_slope = d3.max(
  sommets.features.map(d=>d.properties.percent_moyen)
)
categories = unique(sommets.features.map(d => d.properties.category))
massifs = unique(sommets.features.map(d => d.properties.massif))
selectedRoute = d3.json(`https://minio.lab.sspcloud.fr/lgaliana/cyclisme/data/geojson/split/${selectedClimb.id}.geojson`)
details_altitude = selectedRoute.features[0].geometry.coordinates.map((coord, index) => {
  return { index: index, altitude: coord[2]}
});
printed_points = {
  let printed_points;
  if (mode === "explorer") {
      printed_points = geo.filter(sommets, (d) => filter_active_width(d, filter_choice));
  } else {
      const searchIds = search.map(s => s.id);
      printed_points = geo.filter(sommets, (d) => searchIds.includes(d.id));
      if (width < 400) {
        console.log(printed_points)
        printed_points = geo.properties.head({
          x: printed_points, // a geojson
          field: "nom", // a colname (string)
          nb: 100 // default:10. Number of features to get. 
        })
      }

  }
  return printed_points
}
mutable selectedClimb = null
unique = (arr) => [...new Set(arr)]
function sparkbar(max) {
  return x => htl.html`<div class="sparkbar" style="width: ${100 * x / max}%;">${x.toLocaleString("en")}</div>`
}
widget_to_print = {
  switch (filter_choice) {
    case filter_checkbox_length:
      console.log("Choosing filter by length")
      return Inputs.bind(
          Range([1, max_length], {unit: "km"}),
          viewof climb_length
      )
    case filter_checkbox_deniv:
      console.log("Choosing filter by height gain")
      return Inputs.bind(
        Range([1, max_denivelation], {unit: "m", value: 1200}),
        viewof climb_height
      )
    case filter_checkbox_category:
      console.log("Choosing filter by category")
      return Inputs.bind(
        Inputs.checkbox(
          categories,
          {
            sort: "descending",
            value: ["HC", "Cat 1"],
            //label: category_label
          }
        ),
        viewof category
      )
    case filter_checkbox_slope:
      console.log("Choosing filter by average slope")
      return Inputs.bind(
        Range([0, max_slope], {unit: "%", step: 0.2}),
        viewof climb_slope
      )
    case filter_multiselect_group:
      console.log("Choosing filter by climbing group")
      return Inputs.bind(
         multiAutoSelect({
          options: massifs.sort(),
          placeholder: label_search_massif,
          value: "Arves et Grandes Rousses, France"
        }),
         viewof selected_massif
      )
    default:
      return false
  }
}

function filter_active_width(d, widget) {
  switch (widget) {
    case filter_checkbox_deniv:
      return d.denivellation > climb_height;
    case filter_checkbox_length:
      return d.longueur > climb_length;
    case filter_checkbox_category:
      return category.includes(d.category);
    case filter_checkbox_slope:
      return d.percent_moyen > climb_slope;
    case filter_multiselect_group :
      return selected_massif.includes(d.massif);
    default:
      return false; // or any default case handling you prefer
  }
}
viewof mode = Inputs.radio(new Map([[search_label_choice, "search"], [explorer_label_choice, "explorer"]]), {value: "explorer"})
viewof search = Inputs.search(
  cols, {placeholder: label_search_widget, datalist: ["nom", "départ", "massif"]}
)
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
})
viewof filter_choice = Inputs.select(
  [filter_checkbox_length, filter_checkbox_deniv, filter_checkbox_category, filter_checkbox_slope, filter_multiselect_group],
  {value: filter_checkbox_deniv}
  )
viewof selected_massif = multiAutoSelect({
  options: massifs.sort(),
  placeholder: label_search_massif,
  value: "Arves et Grandes Rousses, France"
})
viewof climb_slope = Inputs.range([0, max_slope], {step: 0.2})
viewof climb_length = Inputs.range([0, max_length], {step: 1})
viewof climb_height = Inputs.range([0, max_denivelation], {step: 1})
viewof category = Inputs.checkbox(
  categories,
  {sort: "descending", value: ["HC", "Cat 1"]}
)
html
`
<div class="lang-selector">${viewof lang}</div>
<div class="container">
  <div class="mode-selector">${viewof mode}</div>
<div/>
`
selectors = {
  let selectors ;

  if (mode == "explorer"){
    selectors = html
    `
    <div class="container">
      <div class="select-container">
        <div>${viewof filter_choice}</div>
        <div class="visible-widget">${widget_to_print}</div>
      </div>
    </div>
    `
    return selectors
  }

  selectors = html`<div class="search-widget">${viewof search}</div>`
  return selectors
}
html`${selectors}`
(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>
  `
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([0, 0], 5);

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


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

  }
  
    var geojsonLayer = L.geoJson(printed_points, {
      onEachFeature: popEventSummit,
      pointToLayer: function(feature, coordinates) {
        return createIcon(feature, coordinates)        
      }
    }).addTo(map);
  
    geojsonLayer.setStyle({
      color: 'red',
      weight: 5
  });

  
    map.fitBounds(geojsonLayer.getBounds())
}
{
  document.querySelector('#stats-denivellation').appendChild(
    rank_ascent(sommets, "denivellation", selectedClimb, lang)
  )
  document.querySelector('#stats-denivellation').appendChild(
    rank_ascent(sommets, "longueur", selectedClimb, lang)
  )

}