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
}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>`// 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
}
}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
}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
);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 (%)"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"container_about = function(width){
if (width<600){
const small_container = html`
<div style="justify-content: center; align-items: center; width: 100%;">
<div id="profile-picture">
<img src="https://minio.lab.sspcloud.fr/lgaliana/generative-art/pythonds/catbike.png"
alt="Cat on a bike"
style="max-width: 100%; height: auto; border-radius: 50%;">
</div>
</div>
<br>
<div id="text-about" style="width: 100%;">${text_about}</div>
<br>
` ;
return small_container
}
if (width < 1000){
const middle_container = html`
<div class="main-container" style="display: flex; flex-direction: row; justify-content: space-between;">
<div id="text-about" style="width: 40%;">${text_about}</div>
<div class="column spacer" style="width: 10%;"></div> <!-- Spacer column -->
<div style="width: 50%;">
<div id="profile-picture">
<img src="https://minio.lab.sspcloud.fr/lgaliana/generative-art/pythonds/catbike.png"
alt="Cat on a bike"
style="max-width: 100%; height: auto; border-radius: 50%;">
</div>
</div>
</div>
` ;
return middle_container
}
const container_about = html`
<div class="main-container" style="display: flex; flex-direction: row; justify-content: space-between;">
<div class="column spacer" style="width: 20%;"></div> <!-- Spacer column -->
<div id="text-about" style="width: 40%;">${text_about}</div>
<div class="column spacer" style="width: 2%;"></div> <!-- Spacer column -->
<div style="width: 18%;">
<div id="profile-picture">
<img src="https://minio.lab.sspcloud.fr/lgaliana/generative-art/pythonds/catbike.png"
alt="Cat on a bike"
style="max-width: 100%; height: auto; border-radius: 50%;">
</div>
</div>
<div class="column spacer" style="width: 20%;"></div> <!-- Spacer column -->
</div>
` ;
return container_about
}text_french = md`
Voulant planifier le
point de chute idéal pour faire quelques
ascensions cet été et curieux de découvrir les
caractéristiques de nombreux
cols disponibles en France, j'ai commencé à construire
ce site à mes heures perdues.
Celui-ci simplifie
l'exploration des cols cyclistes disponibles en France
référencés sur [cols-cyclisme.com/](https://www.cols-cyclisme.com/).
Des statistiques sur près de 2500 ascensions sont répertoriées ici.
L'objectif du site est d'explorer librement les ascensions cyclistes
françaises. Celles-ci peuvent être choisies en fonction
de critères de difficultés (longueur, dénivelé, etc.) ou
bien peuvent être sélectionnées dans le rayon d'un
point de départ.
Le site s'enrichira prochainement de nouvelles pages, notamment
de nombreuses statistiques construites à partir de
données sur les ascensions cyclistes.
`text_english = md`
Wanting to plan the perfect spot for a few climbs this summer,
and curious to discover the characteristics of the many passes available in France,
I started building this site in my spare time.
It simplifies the exploration of cycling passes available in France
and referenced on [cols-cyclisme.com/](https://www.cols-cyclisme.com/).
Statistics on almost 2,500 climbs are listed here.
The aim of the site is to freely explore French cycling climbs.
These can be selected according to difficulty criteria (length, altitude difference, etc.)
or can be selected within the radius of a starting point.
The website will soon be enhanced with new pages, including
numerous statistics based on data
data on cycling climbs.
`