NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Runkeeper Fastest 5k // @author JRI // @oujs:author JRI // @namespace inge.org.uk/userscripts // @description Shows the fastest time you ran 5k (or other set distances) within a longer Runkeeper activity. // @version 0.0.2 // @license MIT; http://www.opensource.org/licenses/mit-license.php // @copyright 2021, James Inge (http://geo.inge.org.uk/) // @include https://runkeeper.com/user/* // @run-at document-idle // @grant unsafeWindow // @icon https://geo.inge.org.uk/userscripts/fastest5k48.png // @icon64 https://geo.inge.org.uk/userscripts/fastest5k64.png // @updateURL https://geo.inge.org.uk/userscripts/Runkeeper_Fastest_5k.meta.js // @downloadURL https://openuserjs.org/install/JRI/Runkeeper_Fastest_5k.user.js // ==/UserScript== /* jshint esversion: 8 */ /* globals unsafeWindow, mapController */ async function main() { const distances = [ {title: "1km", dist: 1000}, {title: "5km", dist: 5000}, {title: "10km", dist: 10000}, {title: "10mi", dist: 16093.4}, {title: "20km", dist: 20000}, {title: "½ mar", dist: 21097.5}, {title: "mar", dist: 42195} ]; function processData() { const msToMins = (ms) => ( (ms >= 3600000) ? Math.floor(ms/3600000) + ":" + String(Math.floor(ms % 3600000 / 60000)).padStart(2,"0") + ":" + String(Math.floor((ms % 60000)/1000)).padStart(2,"0") : Math.floor(ms / 60000) + ":" + String(Math.floor((ms % 60000)/1000)).padStart(2,"0") ); function present(title, times, even) { const miles = (mapController.model.distanceUnits === "mi"); // Default to km. const units = (miles ? "mi" : "km"); const metresPerUnit = (miles ? 1609.34 : 1000); const pace = msToMins(times.pace * metresPerUnit); const startPoint = Number(times.start_dist / metresPerUnit).toFixed(1); return `<div class="row-fluid ${even?"even":"odd"} distanceSplit"> <div class="span4 number micro-text">${title}</div> <div class="span4 pace micro-text" title="Pace: ${pace}/${units}">${msToMins(times.fastest)}</div> <div class="span4 climb micro-text" title="${startPoint}${units}">${msToMins(times.start)}</div> </div>`; } function fastest(distance, points) { // Calculate fastest time to cover distance within the given points. // Times in ms, distances in m. function checkTime(pt, i, pts) { const d = pt.dist - pts[0].dist; if (d > distance) { const t = pt.time - pts[0].time; if (t < min_time) { min_time = t; start_time = pts[0].time; start_dist = pts[0].dist; } return true; } else { return false; } } const max_dist = points[points.length - 1].dist; let min_time = points[points.length - 1].time; let start_time = 0; let start_dist = 0; points.every(function(pt, i, pts) { if (max_dist - pt.dist < distance) { return false; } pts.slice(i).some(checkTime); return true; }); return { fastest: min_time, start: start_time, pace: min_time / distance, "start_dist": start_dist }; } const points = mapController.model.initialPoints; const points_cum = points.reduce((acc, val, i) => ((i === 0) ? [{dist: 0, time: 0}] : [...acc, { dist: val.deltaDistance + acc[i - 1].dist, time: val.deltaTime + acc[i -1].time }]), []); const total = points_cum[points_cum.length - 1].dist; const html = distances.filter((x) => x.dist <= total) .map((x, i) => present(x.title, fastest(x.dist, points_cum), (i % 2 === 0))) .join(""); const divider = document.createElement("div"); divider.className = "colDivider"; target.after(divider); const fastestBox = document.createElement("div"); fastestBox.innerHTML = `<div class="mainColumnPadding clearfix"><h4>Fastest distances</h4></div> <div id="fastestDistances"> <div class="row-fluid header"> <div class="span4 labelHeader">distance</div> <div class="span4 labelHeader">time</div> <div class="span4 labelHeader">start</div> </div> ${html} </div>`; divider.after(fastestBox); } const target = document.getElementById("splitsBox"); if (target === null) { console.warn("Couldn't add FastestTimes block"); return; } const waitPoints = (resolve) => setTimeout(resolve, 500); while(!((mapController.model.initialPoints !== null) && (mapController.model.initialPoints.length > 0))) { await new Promise(waitPoints); } processData(); } async function load(delayedFn, requiredVars = [], params = []) { // Wait for content variables to exist before running delayedFn with given params function varExists(v = "", root = unsafeWindow) { function check(vars, newroot) { if (vars.length === 0) { return true; } if (newroot.hasOwnProperty(vars[0])) { return check(vars.slice(1), newroot[vars[0]]); } else { return false; } } if (root === undefined) { return false; } else { return check(v.split("."), root); } } const waitPromise = (resolve) => setTimeout(resolve, 500); const varExistsTrue = (x) => varExists(x); while(!requiredVars.every(varExistsTrue)) { await new Promise(waitPromise); } delayedFn(params); } const css = `.activity #fastestDistances .row-fluid.even { background-color: #e9e9e9; } .activity #fastestDistances .row-fluid.header { border-bottom:1px solid #e9e9e9; } .activity #fastestDistances .row-fluid .span4 { color:#444; height:30px; line-height:30px; padding-left:25px; } .activity #fastestDistances .row-fluid.header .span4 { color:#888; text-transform: uppercase; height:auto; font-size:11px; }`; function insertCSS(css) { if (typeof css !== "string") { console.warn("insertCSS not called with string: " + typeof css); return; } const styleLink = document.createElement("link"); styleLink.setAttribute("rel", "stylesheet"); styleLink.setAttribute("href", "data:text/css;charset=UTF-8," + encodeURIComponent(css)); document.head.appendChild(styleLink); } function inject(fn) { const script = document.createElement("script"); script.text = "(" + fn.toString() + ")();"; document.body.appendChild(script); } if (document.location.pathname.includes("/activity/")) { load(inject, ["mapController.model.initialPoints"], [main]); insertCSS(css); }