abonnement Unibet Coolblue Bitvavo
  zaterdag 18 juli 2026 @ 14:10:25 #126
311468 Van_Poppel
Voormalig kopman van Gertje
pi_221368637
Lidl mag weer aan de bak.
pi_221368641
quote:
1s.gif Op zaterdag 18 juli 2026 14:08 schreef Immerdebestebob het volgende:

[..]
Heb je een linkje? Kan die site nooit vinden.
https://racecenter.letour.fr/

Dit script draaien:
quote:
var default_settings = {
riders: [],
mycolor: '#ff4500',
min_gap: 4,
max_slow_speed: 5,
beep_for_all: false,
refresh_rate: 1
}

var show_bibs = false;
d = new Date();
var year = d.getUTCFullYear();
d = new Date(year,d.getUTCMonth(),d.getUTCDate(),d.getUTCHours()+2,0,0);
var today = d.toISOString().split('T')[0]; // yyyy-mm-dd
var xmlhttp = new XMLHttpRequest();
var peloton = [];
var teams = [];
var stages = [];
var segments = [];
var efforts = [];
var loadcount = 0;
var velofacts = new EventSource('/live-stream');
var paused = false;
var selected_team = '';
var race = location.hostname + '-' + year;
var currentStage;
var todaysSegments = [];
var settings;
var selectedRider;
var selectedSegment;
var stageDistance;
var segmentMapping = [];

if(!localStorage.getItem(race + '-settings')) {
settings = default_settings;
} else {
console.log('load settings');
settings = eval('(' + localStorage.getItem(race + '-settings') + ')');
}

str_pad_left = function(string, pad, length) {
return (new Array(length + 1).join(pad) + string).slice(-length);
};
pretyTime = function(s) {
var minutes = Math.floor(s / 60);
var seconds = s - minutes * 60;
return minutes + ':' + str_pad_left(seconds, '0', 2);
};

selectTeam = function() {
var jersey = document.getElementsByClassName("teamJersey");
[].forEach.call(jersey, function(el) {
el.classList.remove("selected");
});

//jersey.classList.remove("selected");
var this_id = this.getAttribute('data-id');
if (this_id == selected_team) {
// reset
selected_team = '';
this.classList.remove("selected");
} else {
selected_team = this_id;
this.classList.toggle("selected");
}
return false;
};

togglePause = function() {
paused = !paused;
document.getElementById('pause-icon').className = (paused) ? 'fas fa-play-circle' : 'fas fa-pause-circle';
// paused, onclick for every rider
if (paused) {
var riderlist = document.getElementsByClassName('rider');
for (var i=0; i < riderlist.length; i++) {
riderlist[i].onclick = function () {
var r = this.id.substring(1);
console.log('selected:' + r);
riderCard(r);
}
}
}

return false;
};

// no dark claas
document.body.className = document.body.className.replace("body--dark","");

var style = document.createElement('style');
style.innerHTML = '.group {border-bottom: 2px solid #ba4a19; margin-bottom: 3px; font-size: 11px; color: #333} .yellow div { background-color: #ff0; border: 1px solid #ffb700; } .green div { background-color: #45AE51; color: #fff} .orange div { background-color: '+ settings.mycolor +'; color: #fff} .slow span {color: #fff; background-color: #000} .team {color:#fff; background-color: #ba4a19; } #q-app {padding: 10px} #pause, #button-settings, #button-peloton, #button-segments {font-size: 40px; margin-left: 2px} .group div div { padding-left: 2px; padding-right: 2px;} #toolbar { display: flex; background: #fee5d9; padding: 10px; border-bottom: 2px solid #ba4a19; align-items: center } #toolbar > img { width: 72px; } #jerseyWrapper { display: flex;margin: auto; align-items: center; } .row { justify-content: center; } .selected { background: #ffa47b; } .fas { color: #ba4a19; } .row>.col-md-2 { padding: 2px 5px } .teamJersey:hover { background: #ffa47b; } #toolbar .distance { padding-left: 10px; font-size: 24px }';
style.innerHTML += ' #jerseyWrapper a {width: 2.5%} #jerseyWrapper img {max-width:100%}';
style.innerHTML += ' .darkblue div {background-color: #00008b; color: #fff}';
style.innerHTML += ' .babyblue div {background-color: #BFD7ED}';
style.innerHTML += ' .red div {background-color: #ff0000; color: #fff}';
style.innerHTML += ' .grey div {background-color: #aaa;}';
style.innerHTML += ' .pink div {background-color: #FFB6C1;}';
style.innerHTML += ' #settings { display: none} #settings input {width: 500px;}';
style.innerHTML += ' .bib {background-color: #fff; color: #000; font-size: 8px; border: 1px solid #000; padding-left: 1px; padding-right: 1px;}';
style.innerHTML += ' .color {border: 1px solid #000; padding: 2px; } .color div {height: 65px;} ';
style.innerHTML += ' h4 { background-color: #fee5d9; border-bottom: 1px solid #ba4a19 } ';
style.innerHTML += ' #efforts { padding: 8px} #efforts table {width: 100%} #efforts td { padding: 2px} ';
style.innerHTML += ' #efforts table td, #efforts table th { text-align: right} ';
style.innerHTML += ' #efforts table td.l { text-align: left} ';
style.innerHTML += ' #segments_form { padding-top: 5px; padding-bottom: 5px;} ';
style.innerHTML += ' #form_start, #form_end { width: 80px;} ';
style.innerHTML += ' #segment_list > div { border-bottom: 1px solid #000} ';
style.innerHTML += ' #efforts a{ color: #fc5200} ';

document.head.appendChild(style);

// html
document.getElementById("q-app").innerHTML = '<div id="toolbar"><a href="#" id="pause"><i id="pause-icon" class="fas fa-pause-circle"></i></a><a href="#" id="button-settings"><i id="settings-icon" class="fas fa-wrench"></i></a><a id="button-peloton"><i id="peloton-icon" class="fas fa-bicycle"></i></a><a id="button-segments"><i id="segments-icon" class="fas fa-clock"></i></a><span class="distance"><span id="distance">...</span> km</span><div id="jerseyWrapper"></div></div><div id="settings"></div><div id="ridercard" class="row"></div><div id="rows">  waiting for data...</div><div id="segments" class="row"><div id="segments_form"></div><div class="row"><div id="segment_list" class="col-3"></div><div id="efforts" class="col-9"></div><div id="effort_details"></div></div></div>';

// segments
document.getElementById('segments_form').innerHTML = '<p>Segment in stage <select id="form_stage"></select> starts at (km) <input id="form_start" type="number"> and ends at <input id="form_end" type="number">. Name: <input id="form_name" type="text"><button id="form_button">Add Segment</button>'

// settings
saveSettings = function () {
localStorage.setItem(race + '-settings',JSON.stringify(settings));
}

// min_gap
document.getElementById("settings").innerHTML += 'A new groups is formed when a gap is at least <select id="min_gap"><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option></select> seconds<br><br>';

// max_slow_speed
document.getElementById("settings").innerHTML += 'Mark rider when a rider\'s speed drops below <select id="max_slow_speed"><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option></select> km/h<br><br>';

// click events
// pause button
document.getElementById("pause").onclick = togglePause;
// settings button
document.getElementById("button-settings").onclick = function () {
var settings_layer = document.getElementById("settings");
if (settings_layer.style.display !== "block") {
settings_layer.style.display = "block";
show_bibs = true;
} else {
settings_layer.style.display = "none";
show_bibs = false;
}
return false;
};

showTab = function (id) {
document.getElementById("rows").style.display = (id == 'rows') ? 'block' : 'none';
document.getElementById("segments").style.display = (id == 'segments') ? 'block' : 'none';
}
showTab('rows');
document.getElementById("button-peloton").onclick = function () { showTab('rows')};
document.getElementById("button-segments").onclick = function () { showTab('segments')};

loadRiders = function (xhttp) {
// Proces the riders json
if (xhttp.readyState == 4 && xhttp.status == 200) {
var tmp = JSON.parse(xhttp.responseText);
var counter = 0;
for (var i = 0; i < tmp.length; i++) {
if (tmp[i].bib) {
peloton[tmp[i].bib] = tmp[i];
counter++;
}
}
loadcount++;
document.getElementById('rows').innerHTML += '<br>' + counter + ' riders loaded';
}
};

loadTeams = function (xhttp) {
if (xhttp.readyState == 4 && xhttp.status == 200) {
teams = JSON.parse(xhttp.responseText);
// team jerseys
for (var i=0; i<teams.length; i++) {
document.getElementById('jerseyWrapper').innerHTML += '<a href="#" class="teamJersey" id="team' + i + '" data-id="' + teams[i]._id + '" title="'+teams[i].name+'"><img src="' + teams[i].jersey_sm + '"></a>';
}
for (var i = 0; i < teams.length; i++) {
var thisButton = document.getElementById("team" + i);
thisButton.onclick = selectTeam;
}
loadcount++;
document.getElementById('rows').innerHTML += '<br>' + teams.length + ' teams loaded';
}
}

loadStages = function (xhttp) {
if (xhttp.readyState == 4 && xhttp.status == 200) {
var tmp = JSON.parse(xhttp.responseText);
tmp.sort(function (a,b) {if (a.date < b.date) {return -1} else {return 1}})
var first_date = '9999-99-99';
var last_date = '0000-00-00';
daySelect = document.getElementById('form_stage');
for (var i=0; i<tmp.length; i++) {
stage_date = tmp[i].date.substring(0,10);
if (stage_date > last_date) last_date = stage_date;
if (stage_date < first_date) first_date = stage_date;
// stage name
tmp[i].name = 'Stage ' + tmp[i].stage + ' - ' + tmp[i].arrivalCity.label;

stages[stage_date] = tmp[i];
// add options to form
myOption = document.createElement("option");
myOption.text = tmp[i].name;
myOption.value = stage_date;
daySelect.appendChild(myOption);
}
daySelect.onchange = function () {showSegments(this.value)}

// the official distance differs from the distance used in the gps files
stages['2026-07-04'].length = 19.53;
stages['2026-07-05'].length = 168.40;
stages['2026-07-06'].length = 195.87;
stages['2026-07-07'].length = 181.87;
stages['2026-07-08'].length = 158.22;
stages['2026-07-09'].length = 186.17;
stages['2026-07-10'].length = 175.06;
stages['2026-07-11'].length = 180.37;
stages['2026-07-12'].length = 185.48;

stages['2026-07-14'].length = 166.59;
stages['2026-07-15'].length = 161.29;
stages['2026-07-16'].length = 179.03;
stages['2026-07-17'].length = 205.78;
stages['2026-07-18'].length = 155.23;
stages['2026-07-19'].length = 183.82;

stages['2026-07-21'].length = 26.09;
stages['2026-07-22'].length = 174.61;
stages['2026-07-23'].length = 185.15;
stages['2026-07-24'].length = 127.87;
stages['2026-07-25'].length = 170.81;
stages['2026-07-26'].length = 132.94;

if (today < first_date) today = first_date;
if (today > last_date) today = last_date;
daySelect.value = today;

currentStage = stages[today];
stageDistance = currentStage.length;
loadcount++;
document.getElementById('rows').innerHTML += '<br>' + tmp.length + ' stages loaded';

readSegments();
showSegments(today);
}
}

addSegment = function (d,start,end,name,strava) {
start = parseFloat(start);
end = parseFloat(end);
if (name == '') {name = 'Segment ' + start + ' - ' + end}
if (end > start) {
var id = crypto.randomUUID();
segments.push(
{
"id" : id,
"date" : d,
"start" : start,
"end" : end,
"name" : name,
"strava" : strava,
"distance" : end - start
}
)
}
}

generateSegments = function () {
var version = 0;
if (localStorage.getItem(race + '-version')) {
version = parseInt(localStorage.getItem(race + '-version'));
}
if (version < 10) {
addSegment('2024-08-17',45.76,57.78,'Col de la Serra',36912429);
addSegment('2024-08-17',57.88,69.88,'Col de la Serra - descent',0);
addSegment('2024-08-17',152.54,156.56,'Les Villards -> St Jean',16411751);
addSegment('2024-08-17',159.4,166.24,'Montée du Chinaillon',36912462);

/*
addSegment('2024-06-29',39.94,44.54,'San Godenzo(fine paese)-->Cavallino(bivio 3 faggi)',3924978)
*/
}
if (version < 11) {
addSegment('2024-08-18',78.84,87.3,'First part Glandon',0);
addSegment('2024-08-18',89.38,98.02,'Part Col du Glandon',1872395);
addSegment('2024-08-18',98.52,120.16,'Col du Glandon long descent',0);
addSegment('2024-08-18',136.12,148.18,'Alpe d\'Huez',652851);
}
localStorage.setItem(race + '-version','11');
}

readSegments = function () {
var s = localStorage.getItem(race + '-segments');
if (s) {
s = eval(s);
segments = eval(s);
segmentMapping();
for (var i=0; i < segments.length; i++) {
readEfforts(segments[i].id);
}
}
generateSegments();
saveSegments();
}

segmentMapping = function () {
for (var i=0; i < segments.length; i++) {
segmentMapping[segments[i].id] = i;
}
}

saveSegments = function () {
localStorage.setItem(race + '-segments',JSON.stringify(segments));
segmentMapping();
}

deleteSegment = function(id) {
// remove efforts
efforts[id] = null;
saveEfforts(id);

// remove segment
var idx = false;
for (var i=0; i < segments.length; i++) {
if(segments[i].id == id) {
idx = i;
}
}
segments.splice(idx,1);
saveSegments();
if (id == selectedSegment) {
selectedSegment = '';
document.getElementById('efforts').innerHTML = '';
}

showSegments(today);
}

readEfforts = function (id) {
if (localStorage.getItem(id)) {
efforts[id] = eval(localStorage.getItem(id,JSON.stringify(efforts[id])));
} else {
efforts[id] = [];
}
}

saveEfforts = function (id) {
localStorage.setItem(id,JSON.stringify(efforts[id]));
}

compareEffort = function (a, b) {
if (a && b) {
var ca = (!a.duration) ? 10000000 + (!a.starttime ? 10000000 : a.starttime) : a.duration;
var cb = (!b.duration) ? 10000000 + (!b.starttime ? 10000000 : b.starttime) : b.duration;
if (ca < cb) {
return -1
} else if (ca > cb) {
return 1
}
return 0
} else return 2;
}

effortDetails = function(bib){
var effort = efforts[selectedSegment][bib];
var html = '<br><br><h4>Effort Details'+riders[bib].lastname + ' ' + riders[bib].lastname +'</h4><table>';
html += '<tr><td></td><td>Before start</td><td>After start</td><td>Before end</td><td>After end</td></tr>';
html += '<tr id="effort_time"><td>Time</td><td class="bs"></td><td class="as"></td><td class="be"></td><td class="ae"></td></tr>';
html += '<tr id="effort_distance"><td>Km to finish</td><td class="bs"></td><td class="as"></td><td class="be"></td><td class="ae"></td></tr>';
html += '<tr id="effort_longitude"><td>Longitude</td><td class="bs"></td><td class="as"></td><td class="be"></td><td class="ae"></td></tr>';
html += '<tr id="effort_latitude"><td>Latitude</td><td class="bs"></td><td class="as"></td><td class="be"></td><td class="ae"></td></tr>';
html += '</table>';
document.getElementById('')
if (effort.beforeStart) {

}
}

wkg = function(low, high, distance, duration, RiderWeight) {

var result = {};

// used constants and settings
var celsius = 20;
var ZeroKelvin = -273.15;
var BikeWeight = 8.2;
var P0 = 101325;
var Rs = 287.058;
var CwaHigh = 0.3500;
var CwaLow = 0.2625;
var FrictionCoefficient = 0.0050;
var g = 9.80665;

// Velocity
var Velocity = 1000 * distance / duration; // m/s
var VelocityKmh = distance / (duration / 3600); // km/h
result.velocity = Math.round(VelocityKmh*10)/10;

// ********* Air Density
// Convert celsius to Kelvin
var T = celsius - ZeroKelvin;
// Air Pressure
var p = P0 * (1-6.5*((high - low)/2)/288000) ** 5.256;
// Air Density
var AirDensity = p / (Rs * T);

// Slope
var Slope = (high - low) / (distance * 1000);

// Power Air
var PowerAirHigh = 0.5 * CwaHigh * AirDensity * Velocity ** 3;
var PowerAirLow = 0.5 * CwaLow * AirDensity * Velocity ** 3;
result.PowerAirHigh = PowerAirHigh;
result.PowerAirLow = PowerAirLow;

// Power Rolling Resistance
var PowerRolling = (RiderWeight + BikeWeight) * g * FrictionCoefficient * Velocity;

// Power Climb
var VelocityClimb = (Slope * Velocity) / Math.sqrt(1 + Slope * Slope);
var PowerClimb = (RiderWeight + BikeWeight) * g * VelocityClimb;

// Total Power
var PowerTotalHigh = PowerAirHigh + PowerRolling + PowerClimb;
result.powerTotalHigh = Math.round(PowerTotalHigh);
var PowerTotalLow = PowerAirLow + PowerRolling + PowerClimb;
result.powerTotalLow = Math.round(PowerTotalLow);

// W/kg
result.wkgHigh = Math.round(10 * PowerTotalHigh / RiderWeight)/10;
result.wkgLow = Math.round(10 * PowerTotalLow / RiderWeight)/10;

// Return the result of the function
return result;
}

calcTime = function (data, stageDistance) {
// calculate timeFromStart by using distance and average speed
var result = {};
var speed = data.kphAvg;
var t = data.timeStamp;
var distance = stageDistance - data.kmToFinish;
var seconds = (distance / speed) * 3600.0;
result.seconds = seconds;
result.timeFromStart = pretyTime(seconds);
var t = data.timeStamp - seconds;
t = new Date(t * 1000);
console.log(t);
result.start = t.toTimeString().substring(0,8);
result.kph = speed;
result.distance = distance;
return result;
}

showEfforts = function (idx) {
if (segments[idx]) {
var segment = segments[idx];
} else {
var segment = segments[segmentMapping[idx]];
}
var start_altitude = 0;
var start_altitude_count = 0;
var end_altitude = 0;
var end_altitude_count = 0;
// remember for auto represh
selectedSegment = segment.id;
var stage = stages[segment.date];
// titles
var html = '<h2><span id="stage_title">' + stages[segment.date].name + '</span></h2>';
html += '<p><span id="segment_title">' + segment.name + '</span>'
if (segment.strava) html += ' <a target="_blank" style="color: fc5200" href="https://www.strava.com/segments/' + segment.strava + '">View on strava</a>';
html += ' | from ' + Math.round(segment.start * 100) / 100 + 'km to ' + Math.round(segment.end * 100)/100 + 'km | distance ' + Math.round((segment.end - segment.start)*100)/100 +'km | starts at '+Math.round((stage.length - segment.start)*100)/100+'km to go</p>';
html += '<p>start altitude <span id="start_altitude">NA</span>, end altitude <span id="end_altitude">NA</span>, gradient <span id="gradient">NA</span> </p>';
html += '<p><button id="button_download">Download Results</button> <button id="button_segment_delete">Delete Segment</button></p>';
html += '<p></p>';
var selected_efforts = efforts[segment.id];
var show_efforts = [];
// collect efforts that have started
if (selected_efforts) {
for (var bib=0; bib < selected_efforts.length; bib++) {
var effort = selected_efforts[bib];
if (effort && effort.starttime) {
effort.bib = bib;
if (effort.beforeStart) {
start_altitude += effort.beforeStart.mAlt;
start_altitude_count++;
}
if (effort.afterStart) {
start_altitude += effort.afterStart.mAlt;
start_altitude_count++;
}
if (effort.beforeEnd) {
end_altitude += effort.beforeEnd.mAlt;
end_altitude_count++;
}
if (effort.afterEnd) {
end_altitude += effort.afterEnd.mAlt;
end_altitude_count++;
}
show_efforts.push(effort);
}
}
}
var gradient;
if (start_altitude_count > 0) {
start_altitude = parseInt(start_altitude / start_altitude_count);
}
if (end_altitude_count > 0) {
end_altitude = parseInt(end_altitude / end_altitude_count);
gradient = 100 * (end_altitude - start_altitude) / (1000 * (segment.end - segment.start));
}

// order by duration / starttime
show_efforts.sort(compareEffort);
// generate table with results
html += '<div id="efforts_download"><table id="efforts_table">';
html += '<thead><tr><th><br>bib</th><th></th><th><br>Started</th><th><br>Finished</th><th><br>Duration</th><th>W/kg<br>Full drafting</th><th>W/kg<br>No drafting</th><th><br>km/h</th></tr></thead>'
for(var i=0; i < show_efforts.length; i++) {
var e = show_efforts[i];
var t1 = new Date(e.starttime * 1000);
t1 = t1.toTimeString().substring(0,8);
var t2 = (e.endtime) ? new Date(e.endtime * 1000) : '';
if (t2 != '') t2 = t2.toTimeString().substring(0,8);

var details = wkg(start_altitude, end_altitude, segment.end - segment.start, e.duration, 65);
html += '<tr><td>' + e.bib + '</td><td class="l">' + peloton[e.bib].lastnameshort + ' ' + peloton[e.bib].firstname;
html += '</td><td>' + t1;
/*
if (t1) {
realtime = calcTime(effort.afterStart, stage.length);
html += '</td><td>' + realtime.start + '|' + realtime.seconds + '|' + realtime.timeFromStart + '|' + realtime.kph + '|' + realtime.distance;
} else {
html += '</td><td>';
}
*/
html += '</td><td>' + t2;
if (t1 && t2) {
html += '</td><td>' + pretyTime(e.duration);
if (gradient >= 3.0) {
html += '</td><td>' + details.wkgLow;
html += '</td><td>' + details.wkgHigh;
} else {
html += '</td><td></td><td>';
}
html += '</td><td>' + details.velocity;
} else {
html += '</td><td></td><td></td><td></td><td>';
}
html += '</td></tr>';
}

html += '</table></div>';
html += '<br><p>W/kg numbers are based on 65kg rider and 8.2kg Bike+gear</p>'

// show results
document.getElementById('efforts').innerHTML = html;

if (start_altitude_count > 0) {
document.getElementById('start_altitude').innerHTML = start_altitude + 'm';
}
if (end_altitude_count > 0) {
document.getElementById('end_altitude').innerHTML = end_altitude + 'm';
gradient = Math.round(gradient * 10) / 10;
document.getElementById('gradient').innerHTML = gradient + '%';
}
document.getElementById('button_download').onclick = downloadResults;
document.getElementById('button_segment_delete').onclick = function () {
if (document.getElementById('button_segment_delete').innerHTML == 'Delete Segment') {
document.getElementById('button_segment_delete').innerHTML = 'Click again to delete';
} else {
deleteSegment(selectedSegment);
}
};

}

showSegments = function (day) {
// today's segments for adding efforts
todaysSegments = [];
for (var i = 0; i < segments.length; i++) {
if (segments[i].date == today) {
todaysSegments.push(segments[i]);
}
}
// segments for selection of efforts
var html = '';
for (let s in stages) {
if (s >= day) {
html += '<h4>' + stages[s].name + '</h4>';
for (var i = 0; i < segments.length; i++) {
if (segments[i].date == s) {
html += '<div><a class="cursor-pointer" idx="'+i+'" id="' + segments[i].id + '">'+segments[i].name+'</a></div>';
}
}
}
}
document.getElementById('segment_list').innerHTML = html;
for (var i = 0; i < segments.length; i++) {
if (document.getElementById(segments[i].id)) {
document.getElementById(segments[i].id).onclick=function () {
showEfforts(this.getAttribute('idx'));
}
}
}
}

mySegment = function() {
addSegment(
document.getElementById('form_stage').value,
document.getElementById('form_start').value,
document.getElementById('form_end').value,
document.getElementById('form_name').value
);
saveSegments();
showSegments(today);
document.getElementById('form_start').value = '';
document.getElementById('form_end').value = '';
document.getElementById('form_name').value = '';
}
document.getElementById('form_button').onclick = mySegment;

download = function (text, filename){
var blob = new Blob([text], {type: "text/html"});
var url = window.URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
}

downloadResults = function () {
download(document.getElementById('efforts_download').innerHTML,(document.getElementById('stage_title').innerHTML + '-' + document.getElementById('segment_title').innerHTML).replace(/[^a-zA-Z0-9]/g, '-'));
}

// settings form
document.getElementById('min_gap').value = settings.min_gap;
document.getElementById('min_gap').addEventListener("change", function() {
settings.min_gap = document.getElementById('min_gap').value;
saveSettings();
});
document.getElementById('max_slow_speed').value = settings.max_slow_speed;
document.getElementById('max_slow_speed').addEventListener("change", function() {
settings.min_gap = document.getElementById('max_slow_speed').value;
saveSettings();
console.log('max_slow_speed updated');
});

loadData = function (url, cFunction) {
const xhttp = new XMLHttpRequest();
xhttp.onload = function() {cFunction(this);}
xhttp.open("GET", url);
xhttp.send();
}


// load data jsons
loadData("/api/allCompetitors-" + year, loadRiders);
loadData("/api/stage-" + year, loadStages);
loadData("/api/team-" + year, loadTeams);
startListening();

riderCard = function (bib) {
var rider = peloton[bib];
selectedRider = bib;
var html = '';
html += '<div class="col-3">';
html += ' <img class="rankingTables__row__profile--picture" src="'+rider.profile_sm+'"> ';
html += ' <span><span data-bib="#123" class="flag flag--with-bib js-display-lazy flag--'+rider.nationality+'"></span></span> ' + rider.lastname + ' ' + rider.firstname;
html += '</div>';
html += '<div class="select_color col-3 row">';
html += ' <div id="white" class="color white col-1 cursor-pointer"><div></div></div>';
html += ' <div id="yellow" class="color yellow col-1 cursor-pointer"><div></div></div>';
html += ' <div id="green" class="color green col-1 cursor-pointer"><div></div></div>';
html += ' <div id="orange" class="color orange col-1 cursor-pointer"><div></div></div>';
html += ' <div id="darkblue" class="color darkblue col-1 cursor-pointer"><div></div></div>';
html += ' <div id="babyblue" class="color babyblue col-1 cursor-pointer"><div></div></div>';
html += ' <div id="red" class="color red col-1 cursor-pointer"><div></div></div>';
html += ' <div id="grey" class="color grey col-1 cursor-pointer"><div></div></div>';
html += ' <div id="pink" class="color pink col-1 cursor-pointer"><div></div></div>';
html += '</div>';
html += '<div class="col-3">';
html += ' <a id="close-rider-card">close</a>';
html += '</div>';
document.getElementById('ridercard').innerHTML = html;

// click events
var colorButtons = document.getElementsByClassName('color');
for (var i=0; i < colorButtons.length; i++) {
colorButtons[i].onclick = function () {
console.log(this.id);
settings.riders[selectedRider] = { color: this.id };
var elm = document.getElementById('r' + selectedRider);
elm.className = elm.className.replace("yellow","");
elm.className = elm.className.replace("green","");
elm.className = elm.className.replace("orange","");
elm.className = elm.className.replace("white","");
elm.className = elm.className.replace("darkblue","");
elm.className = elm.className.replace("babyblue","");
elm.className = elm.className.replace("pink","");
elm.className = elm.className.replace("grey","");
elm.className = elm.className.replace("red","");
elm.className += ' ' + this.id
saveSettings();
}
}
document.getElementById('close-rider-card').onclick = function () {
document.getElementById('ridercard').innerHTML = '';
}
}

getTimeStamp = function (distanceBefore, distanceAfter, timeStampBefore, timeStampAfter) {
var distance = distanceBefore + distanceAfter;
if (distance == 0) {
return timeStampBefore;
} else {
var deltaTime = timeStampAfter - timeStampBefore;
return timeStampBefore + (distanceBefore / distance) * deltaTime;
}
}

function startListening() {
velofacts.addEventListener("update", function(e) {
if (loadcount == 3) {
var d = JSON.parse(e.data);
var html = '<div class="row group">';
var previous_gap = 0;
if (d.bind == 'telemetryCompetitor-' + year) {
// all data has been loaded, process snapshot
var sound = "";
var riders = d.data.Riders;
var timeStamp = d.data.TimeStamp;
// in 2026 riders aren't always ordered by gap
riders.sort(function (a,b) {if (a.secToFirstRider < b.secToFirstRider) {return -1} else {return 1}});

for (var i = 0; i < riders.length; i++) {
var rider = riders[i];
var bib = rider.Bib;
if (i == 0) { document.getElementById("distance").innerHTML = rider.kmToFinish}
var gap = rider.secToFirstRider;
var speed = rider.kph;
var speedAvg = rider.kphAvg;
var extra_class = '';
if ((gap - previous_gap) > settings.min_gap) {
html += '</div><div class="row group">';
}
if (settings.beep_for_all && speed < settings.max_slow_speed) {
sound = 'sound1';
}
if (selected_team == '') {
if (settings.riders[bib]) {
extra_class += ' ' + settings.riders[bib].color;
}
} else {
if (peloton[rider.Bib].$team.split(':')[1] == selected_team) extra_class += ' team';
}
if (speed < settings.max_slow_speed) { extra_class += ' slow'; };
bib_html = '';
if (show_bibs) {
bib_html = '<span class="bib">'+rider.Bib+'</span> ';
}
html += '<div id="r'+rider.Bib+'" title="Speed: ' + speed + 'km/h | Average Speed: ' + speedAvg + 'km/h | ' + rider.kmToFinish + 'km to go, bib:'+rider.Bib+'" class="rider col-md-2 ' + extra_class + '"><div><span>' + bib_html + peloton[rider.Bib].lastnameshort + ' ' + peloton[rider.Bib].firstname + ' ' + pretyTime(gap) + '</span></div></div>';
if (gap > 0) previous_gap = gap;

//
// start efforts
//

// loop through today's segments
for (var s = 0; s < todaysSegments.length; s++) {
var updateStorage = false;

var segment = todaysSegments[s];
var kmFromStart = stageDistance - rider.kmToFinish;
var distanceFromSegmentStart = segment.start - kmFromStart;
var distanceFromSegmentEnd = segment.end - kmFromStart;

// init effort
if (!efforts[segment.id]) {
efforts[segment.id] = [];
}
if (!efforts[segment.id][bib]) {
efforts[segment.id][bib] = {
beforeStart : null,
afterStart: null,
beforeEnd: null,
afterEnd: null,
starttime: null,
endtime: null,
duration: null
}
}

// if position within 200 meters before segment start
if (distanceFromSegmentStart >= 0 && distanceFromSegmentStart <= 0.2) {
// remember data
var update = true;
if (efforts[segment.id][bib].beforeStart && efforts[segment.id][bib].beforeStart.distance == distanceFromSegmentStart) {
// rider hasn't moved, no update
update = false;
}
if (update) {
efforts[segment.id][bib].beforeStart = rider;
efforts[segment.id][bib].beforeStart.timeStamp = timeStamp;
efforts[segment.id][bib].beforeStart.distance = distanceFromSegmentStart;
}
}

// if position within 200 meters after start and !start
if (distanceFromSegmentStart <= 0 && distanceFromSegmentStart >= -0.2 && !efforts[segment.id][bib].afterStart) {
// remember position: segment.effort[bib].beforeEnd = rider
efforts[segment.id][bib].afterStart = rider;
efforts[segment.id][bib].afterStart.timeStamp = timeStamp;
efforts[segment.id][bib].afterStart.distance = -1 * distanceFromSegmentStart;
// calculate start time: segment.effort[bib].start = time
if (efforts[segment.id][bib].beforeStart) {
// estimate time at the start
updateStorage = true;
efforts[segment.id][bib].starttime = getTimeStamp(
efforts[segment.id][bib].beforeStart.distance,
efforts[segment.id][bib].afterStart.distance,
efforts[segment.id][bib].beforeStart.timeStamp,
efforts[segment.id][bib].afterStart.timeStamp
)
}
}
// if position within 200 meters before finish
if (distanceFromSegmentEnd >= 0 && distanceFromSegmentEnd <= 0.2) {
// remember data
update = true;
if (efforts[segment.id][bib].beforeEnd && efforts[segment.id][bib].beforeEnd.distance == distanceFromSegmentEnd) {
// rider hasn't moved, no update
update = false;
}
if (update) {
efforts[segment.id][bib].beforeEnd = rider;
efforts[segment.id][bib].beforeEnd.timeStamp = timeStamp;
efforts[segment.id][bib].beforeEnd.distance = distanceFromSegmentEnd;
}
}

// if position within 200 meters after finish and !finish
if (distanceFromSegmentEnd <= 0 && distanceFromSegmentEnd >= -0.2 && !efforts[segment.id][bib].afterEnd) {
// remember data
efforts[segment.id][bib].afterEnd = rider;
efforts[segment.id][bib].afterEnd.timeStamp = timeStamp;
efforts[segment.id][bib].afterEnd.distance = -1 * distanceFromSegmentEnd;
if (efforts[segment.id][bib].beforeEnd) {
// estimate time at the end
efforts[segment.id][bib].endtime = getTimeStamp(
efforts[segment.id][bib].beforeEnd.distance,
efforts[segment.id][bib].afterEnd.distance,
efforts[segment.id][bib].beforeEnd.timeStamp,
efforts[segment.id][bib].afterEnd.timeStamp
)
// calculate duration if you also recorded the start
if (efforts[segment.id][bib].starttime) {
efforts[segment.id][bib].duration = parseInt(efforts[segment.id][bib].endtime - efforts[segment.id][bib].starttime);
updateStorage = true;
}
}
}

if (updateStorage) {
saveEfforts(segment.id);
// refresh efforts
if (segment.id == selectedSegment) showEfforts(selectedSegment);
}

}
// end todaysSegments loop

//
// end efforts
//

}
if (!paused) {
if (sound != "") {
// annoying beep
playBeep(240,50);
};
document.getElementById("rows").innerHTML = html + '</div>';
}
}
}
});
}

playBeep = function(frequency = 440, duration = 500) {
// Create an audio context
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();

// Create an oscillator node (for generating sound)
const oscillator = audioCtx.createOscillator();

// Set the frequency of the beep (440Hz is a common pitch, like an A note)
oscillator.frequency.setValueAtTime(frequency, audioCtx.currentTime);

// Connect the oscillator to the audio context's destination (speakers)
oscillator.connect(audioCtx.destination);

// Start the sound
oscillator.start();

// Stop the sound after the duration specified in milliseconds
setTimeout(() => {
oscillator.stop();
audioCtx.close(); // Close the audio context when done to save resources
}, duration);
}

// for iOS shortcut, unremark
// var result=[]
// completion(result)//
pi_221368642
Pidcock rijdt een Hennie Kuiper-Tour.
Ik doe het ff rustig aan.
pi_221368645
quote:
0s.gif Op zaterdag 18 juli 2026 14:08 schreef Rellende_Rotscholier het volgende:
Campenaerts kan er niets meer van.
Die moet zich drie maanden maniakaal ergens op voorbereiden, anders is het middelmaat.
pi_221368647
quote:
0s.gif Op zaterdag 18 juli 2026 14:11 schreef wimderon het volgende:

[..]
Die moet zich drie maanden maniakaal ergens op voorbereiden, anders is het middelmaat.
Hij heeft vandaag in elk geval de sokken er niet in.
Ik doe het ff rustig aan.
  zaterdag 18 juli 2026 @ 14:14:46 #131
328924 Frozen-assassin
STAY STRONG APPIE
pi_221368657
Wat als UAE er niet was. Dan had je een mooie koers met RedBull/Visma/Lidl-tactics. Nu word je gewurgd tot aan Haag
pi_221368658
Pidcock ook gewoon weer mee na gisteren, daar zal de rest niet heel blij mee zijn
pi_221368681
quote:
0s.gif Op zaterdag 18 juli 2026 14:14 schreef Frozen-assassin het volgende:
Wat als UAE er niet was. Dan had je een mooie koers met RedBull/Visma/Lidl-tactics. Nu word je gewurgd tot aan Haag
Wat als wielrennen er niet was.

Dan hadden we vandaag wel wat beters te doen.
Ik doe het ff rustig aan.
  zaterdag 18 juli 2026 @ 14:17:20 #134
187810 Szura
Kijk eens aan!
pi_221368683
Neusspray Lenny draait vierkant
Lekker zuipen, lekker dansen en daarna lekker neuken.
pi_221368684
Ongelooflijk dat ze Pidcock weer mee laten gaan.
The problem with socialism is that you eventually run out of other people's money
pi_221368690
quote:
0s.gif Op zaterdag 18 juli 2026 14:16 schreef Toby56 het volgende:

[..]
Wat als wielrennen er niet was.

Dan hadden we vandaag wel wat beters te doen.
Dat trek ik in twijfel.
  zaterdag 18 juli 2026 @ 14:19:51 #137
328924 Frozen-assassin
STAY STRONG APPIE
pi_221368691
quote:
0s.gif Op zaterdag 18 juli 2026 14:17 schreef Szura het volgende:
Neusspray Lenny draait vierkant
Dat is nou zo'n renner die je na een week naar huis kan sturen (hallo Picnic)
pi_221368694
quote:
0s.gif Op zaterdag 18 juli 2026 14:19 schreef Frozen-assassin het volgende:

[..]
Dat is nou zo'n renner die je na een week naar huis kan sturen (hallo Picnic)
Dat kun je überhaupt wel overwegen met een gebroken rib.
pi_221368701
Jeroen en Karsten komen er nu achter dat Pidcock erbij zit _O-
The problem with socialism is that you eventually run out of other people's money
  zaterdag 18 juli 2026 @ 14:23:45 #140
414990 Immerdebestebob
Frikandellenfetisjist
pi_221368709
Wel bizar hoe passief Decathlon is deze hele Tour.
pi_221368715
quote:
0s.gif Op zaterdag 18 juli 2026 14:21 schreef TAmaru het volgende:
Jeroen en Karsten komen er nu achter dat Pidcock erbij zit _O-
Jakkes.

Zou Evenepoel het dan al weten?
Ik doe het ff rustig aan.
pi_221368716
quote:
1s.gif Op zaterdag 18 juli 2026 14:23 schreef Immerdebestebob het volgende:
Wel bizar hoe passief Decathlon is deze hele Tour.
Etappe is binnen en Seixas kan in relatieve rust een klassement rijden in zijn eerste grote ronde? Wat had je ze dan willen zien doen.
pi_221368718
quote:
0s.gif Op zaterdag 18 juli 2026 14:17 schreef TAmaru het volgende:
Ongelooflijk dat ze Pidcock weer mee laten gaan.
Uae zal het voorlopig geen zorgen baren maar ze zullen geen kwartier krijgen vandaag
  Redactie Sport zaterdag 18 juli 2026 @ 14:25:42 #144
451829 crew  H.Vviv
pi_221368720
quote:
1s.gif Op zaterdag 18 juli 2026 14:23 schreef Immerdebestebob het volgende:
Wel bizar hoe passief Decathlon is deze hele Tour.
Niet heel gek hoe de ploeg is samengesteld.
pi_221368721
quote:
1s.gif Op zaterdag 18 juli 2026 14:23 schreef Immerdebestebob het volgende:
Wel bizar hoe passief Decathlon is deze hele Tour.
APP is mee!
pi_221368724
quote:
0s.gif Op zaterdag 18 juli 2026 14:21 schreef TAmaru het volgende:
Jeroen en Karsten komen er nu achter dat Pidcock erbij zit _O-
Ach, Renaat is nu een totaal foute lijst van PCS aan het voorlezen.
  zaterdag 18 juli 2026 @ 14:27:32 #147
414990 Immerdebestebob
Frikandellenfetisjist
pi_221368730
quote:
1s.gif Op zaterdag 18 juli 2026 14:24 schreef Makamba_ het volgende:

[..]
Etappe is binnen en Seixas kan in relatieve rust een klassement rijden in zijn eerste grote ronde? Wat had je ze dan willen zien doen.
Dat ze vandaag en gisteren gewoon meespringen met APP, Benoot en Prodhomme.
  zaterdag 18 juli 2026 @ 14:29:07 #148
414990 Immerdebestebob
Frikandellenfetisjist
pi_221368734
Guillaume solliciteert naar een plek bij Tudor.
  Redactie Sport zaterdag 18 juli 2026 @ 14:29:23 #149
451829 crew  H.Vviv
pi_221368736
Politt is maar tegen Campenaerts aan het rijden.
  zaterdag 18 juli 2026 @ 14:30:32 #150
414990 Immerdebestebob
Frikandellenfetisjist
pi_221368740
quote:
0s.gif Op zaterdag 18 juli 2026 14:26 schreef wimderon het volgende:

[..]
Ach, Renaat is nu een totaal foute lijst van PCS aan het voorlezen.
Berwick niet mee?
abonnement Unibet Coolblue Bitvavo
Forum Opties
Forumhop:
Hop naar:
(afkorting, bv 'KLB')