Last active
May 27, 2022 00:04
-
-
Save mcdurdin/5639982 to your computer and use it in GitHub Desktop.
Strava Giro-style elevation graph bookmarklet
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var stravaOnSteroids = { | |
lengthMultiplierLarge : 0.001, | |
lengthMultiplierSmall : 1, | |
lengthUnitLarge:"km", | |
lengthUnitSmall:"m", | |
segName:'ride', | |
sections : [], | |
isRoute : false, | |
loading : false, | |
segDistance : 1, | |
segCat : '', | |
segGrade : 0, | |
segElevGain : 0, | |
create: function() { | |
var li = jQuery('#giro'); | |
if(li) { li.remove(); } | |
li = jQuery('.activity-charts .giro'); | |
if(li) li.remove(); | |
li = document.getElementById('giro-fonts'); | |
if(!li) { | |
li = document.createElement('link'); | |
li.id='giro-fonts'; | |
li.href='http://fonts.googleapis.com/css?family=Archivo+Narrow:400,700'; | |
li.rel='stylesheet'; | |
li.type='text/css'; | |
document.getElementsByTagName('head')[0].appendChild(li); | |
} | |
var ac = jQuery('.activity-charts'); | |
if(ac.length == 0) { | |
return stravaOnSteroids.createForRoute(); | |
} | |
var ul = jQuery('.activity-charts ul.horizontal-menu'); | |
var li = document.createElement('li'); | |
var a = document.createElement('a'); | |
var c = document.createElement('canvas'); | |
c.style.width='100%'; | |
c.id='stravaOnSteroids'; | |
var chart = document.createElement('div'); | |
jQuery(chart).addClass('chart').addClass('giro').addClass('background-off').css('display','none').append(c); | |
jQuery(ac).append(chart); | |
jQuery('#performance').after(li); | |
jQuery(li).append(a).attr('id', 'giro'); | |
jQuery(a).addClass('tab').text('Giro').bind('click', function(event) { | |
event.cancelBubble = true; | |
jQuery('> li', ul).removeClass('selected'); | |
jQuery(li).addClass('selected'); | |
jQuery('.elevation', ac).addClass('hidden').css('display','none'); | |
jQuery('.performance', ac).addClass('hidden').css('display','none'); | |
jQuery(chart).removeClass('hidden').css('display','block'); | |
stravaOnSteroids.redraw(); | |
return false; | |
}); | |
jQuery('> li > a', ul).not(a).bind('click', function(event) { | |
jQuery(li).removeClass('selected'); | |
jQuery(chart).addClass('hidden').css('display','none'); | |
}); | |
}, | |
createForRoute: function() { | |
stravaOnSteroids.isRoute = true; | |
var c = document.createElement('canvas'); | |
c.style.width='100%'; | |
c.id='stravaOnSteroids'; | |
jQuery('.giro').remove(); | |
var chart = document.createElement('div'); | |
jQuery(chart).addClass('giro').append(c); | |
jQuery('#chart-container').append(chart); | |
stravaOnSteroids.redraw(); | |
window.setTimeout(stravaOnSteroids.redrawDo, 5000); // give time for font to load... good enough | |
}, | |
redrawDo: function() { | |
stravaOnSteroids.redraw(); | |
}, | |
redraw: function() { | |
if (this.loading) return; | |
var canvas = document.getElementById('stravaOnSteroids'); | |
if (canvas.getContext) { | |
var ctx = canvas.getContext('2d'); | |
ctx.save(); | |
ctx.setTransform(1, 0, 0, 1, 0, 0); | |
/* Will always clear the right space */ | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
ctx.restore(); | |
} | |
if(stravaOnSteroids.isRoute) { | |
var data = {data: {altitude: [], distance: [], details: []}}; | |
var segs = pageView.pageContext().routeSegments(); | |
for(var i = 0; i < segs.length; i++) { | |
var seg = { | |
endX: segs[i].end_distance, | |
endY: null, | |
name: segs[i].name, | |
difficulty: parseFloat(segs[i].avg_grade)*parseFloat(segs[i].distance)*1000 | |
}; | |
seg.isClimb = seg.difficulty >= 8000; | |
data.data.details.push(seg); | |
} | |
var es = pageView.chartContext().dataContext().elevationStream(), startJ = 0; | |
for(var i = 0; i < es.length; i++) { | |
data.data.altitude.push(es[i].y); | |
data.data.distance.push(es[i].x); | |
for(var j = startJ; j < data.data.details.length; j++) { | |
if(data.data.details[j].endX <= es[i].x && data.data.details[j].endY === null) { | |
data.data.details[j].endY = es[i].y; | |
//startJ = j+1; | |
} //else { | |
//break; | |
//} | |
} | |
} | |
} else { | |
var data = {data: {altitude: gStreamData.altitude, distance: gStreamData.distance, details: gStreamData.activityData.effortData.details}}; | |
if (!data.data.altitude || !data.data.distance || !data.data.details) return; | |
} | |
data.data.minHeight = 10000; | |
data.data.maxHeight = -10000; | |
data.data.minHeightLocation = [0, 0]; | |
data.data.maxHeightLocation = [0, 0]; | |
stravaOnSteroids.data = data; | |
var i; | |
/* calculate max min altitude in metres */ | |
for (i = 0; i < data.data.altitude.length; i++) { | |
if (data.data.altitude[i] < data.data.minHeight) { | |
data.data.minHeight = data.data.altitude[i]; | |
} | |
if (data.data.altitude[i] > data.data.maxHeight) { | |
data.data.maxHeight = data.data.altitude[i]; | |
} | |
} | |
var desiredWidth = canvas.offsetWidth, desiredHeight = canvas.offsetWidth * 0.4; | |
var f = (desiredWidth) / (data.data.distance[data.data.distance.length-1]); | |
var xStep = 0.1; | |
var yStep = 50; | |
var overallGrad = (data.data.maxHeight - data.data.minHeight); /* / data.data.distance[data.data.distance.length-1]; */ | |
var vertMultiplier = Math.min(0.75, desiredHeight / overallGrad); /*Math.min(10, 10 * (1 - (overallGrad * 3)));*/ | |
/*alert(vertMultiplier);*/ | |
var FitGradient = false; | |
var angle = 8 * Math.PI / 180; | |
if (isNaN(angle) || isNaN(f) || isNaN(xStep) || isNaN(yStep)) return; | |
if (f > 0 && xStep > 0 && yStep > 0 && angle > 0) { | |
this.drawGiro(data.data, f, vertMultiplier, xStep, yStep, FitGradient, angle); | |
} | |
}, | |
matrix: [], | |
transform: function(x, y) { | |
return { x: x * matrix[0] + y * matrix[2] + 1 * matrix[4], y: x * matrix[1] + y * matrix[3] + 1 * matrix[5] }; | |
}, | |
inverse_transform: function(x, y, angle) { | |
var matrix = [1, Math.tan(angle), 0, 1, 0, 0]; /* Skew transform */ | |
return { x: x * matrix[0] + y * matrix[2] + 1 * matrix[4], y: x * matrix[1] + y * matrix[3] + 1 * matrix[5] }; | |
}, | |
drawGiro: function(data, xf, yf, xStep, yStep, FitGradient, angle) { | |
var c = document.getElementById('stravaOnSteroids'); | |
sections = []; | |
angle = 7.5 * Math.PI / 180; | |
var w = 22, ysubbase = (data.maxHeight - data.minHeight) * yf + 280, ybase = ysubbase - 40; | |
var dt = this.inverse_transform(((data.distance[data.distance.length - 1]) * xf + 48), ysubbase, angle); | |
c.width = dt.x; | |
c.height = dt.y + 50; | |
matrix = [1, Math.tan(-angle), 0, 1, 0, dt.x * Math.tan(angle)]; /* Skew transform */ | |
var dw = { x: -w * Math.cos(angle), y: -w * Math.sin(angle) }; /* Apply "3D" */ | |
dw = this.inverse_transform(dw.x, dw.y, angle); /* Remove skew */ | |
dw.y -= 8; | |
var x = -dw.x, y = ybase + dw.y - yf * (data.altitude[0] - data.minHeight); | |
if (c.getContext) { | |
var context = c.getContext('2d'); | |
context.fillStyle = 'rgb(255,255,255)'; | |
context.fillRect(0,0,c.width,c.height); | |
context.save(); | |
context.setTransform(matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]); /* "Isometric" */ | |
context.beginPath(); | |
context.moveTo(0, y); | |
context.strokeStyle = 'rgba(0,0,0,1)'; | |
context.lineTo(x, y); | |
} | |
var startSeg = 0, rise = 0, len = 0, gradient = 0, startX = x, startY = y; | |
for (var i = 1; i < data.altitude.length; i++) { | |
var seglen = (data.distance[i] - data.distance[i - 1]); | |
var segrise = (data.altitude[i] - data.altitude[i - 1]); | |
if (len > 0) { | |
var cut = !FitGradient ? | |
(Math.floor((data.distance[i]) / (xStep / this.lengthMultiplierLarge)) > Math.floor((data.distance[i - 1]) / (xStep / this.lengthMultiplierLarge))) : | |
Math.abs(1 - (rise / len) / (segrise / seglen)) > 0.5 && (seglen + len) * xf > 32; | |
if (cut) { | |
x += len * xf; y -= rise * yf; | |
sections.push({ startX: startX, startY: startY, endX: x, endY: y, gradient: ((startY - y) / yf) / ((x - startX) / xf) * 100, index: i }); | |
rise = 0; len = 0; | |
startX = x; startY = y; | |
} | |
} | |
rise += segrise; | |
len += seglen; | |
} | |
if (c.getContext) { | |
var lineargradient = context.createLinearGradient(0, 0, 0, ysubbase); | |
lineargradient.addColorStop(0, 'rgba(214, 195, 181, 1)'); | |
lineargradient.addColorStop(0.3, 'rgba(237, 205, 156, 1)'); | |
lineargradient.addColorStop(0.45, 'rgba(245, 237, 291, 1)'); | |
lineargradient.addColorStop(0.55, 'rgba(222, 233, 190, 1)'); | |
lineargradient.addColorStop(0.75, 'rgba(244, 244, 210, 1)'); | |
lineargradient.addColorStop(1, 'rgba(255, 255, 255, 1)'); | |
} | |
x += len * xf; y -= rise * yf; | |
sections.push({ startX: startX, startY: startY, endX: x, endY: y, gradient: ((startY - y) / yf) / ((x - startX) / xf) * 100, index: data.distance.length }); | |
if (c.getContext) { | |
/* Draw rear gradient line */ | |
context.lineCap = 'round'; | |
context.beginPath(); | |
var s = sections[sections.length - 1]; | |
context.moveTo(s.startX, s.startY); | |
context.strokeStyle='rgb(24,24,24)'; | |
context.lineWidth=6; | |
context.lineJoin='round'; | |
for (var i = sections.length - 1; i >= 0; i--) { | |
var s = sections[i]; | |
context.lineTo(s.startX + dw.x, s.startY + dw.y); | |
} | |
context.stroke(); | |
/* Draw the left edge of start of the graph */ | |
context.beginPath(); | |
context.moveTo(sections[0].startX, ysubbase); | |
context.lineTo(sections[0].startX + dw.x, ysubbase + dw.y); | |
context.lineTo(sections[0].startX + dw.x, sections[0].startY + dw.y); | |
context.lineTo(sections[0].startX, sections[0].startY); | |
context.fillStyle = 'rgb(153,152,158)'; | |
context.fill(); | |
context.lineWidth = 1; | |
/* Draw road surface - backwards so "earlier" sections clean up "later" ones */ | |
var prevGradient = -1; | |
for (var i = sections.length - 1; i >= 0; i--) { | |
var s = sections[i]; | |
context.beginPath(); | |
var c = ''; | |
if (s.gradient.toFixed(1) >= 4) c = 'rgb(160,161,163)'; | |
else c = c = 'rgb(164,165,169)'; | |
context.fillStyle = c; | |
context.strokeStyle = c; | |
context.moveTo(s.startX, s.startY); | |
context.lineTo(s.startX + dw.x, s.startY + dw.y); | |
context.lineTo(s.endX + dw.x, s.endY + dw.y); | |
context.lineTo(s.endX, s.endY); | |
context.lineTo(s.startX, s.startY); | |
context.fill(); | |
/* Fill seams | |
if the gradient is switching to positive, then draw a thick line across top? */ | |
var doDraw = | |
(s.endY <= s.startY) && (prevGradient < 0); | |
prevGradient = s.startY - s.endY; | |
if(doDraw) { | |
context.strokeStyle = 'rgb(24,24,24)'; | |
context.lineWidth = 2; | |
} else { | |
context.strokeStyle = c; | |
context.lineWidth = 1; | |
} | |
context.beginPath(); | |
context.moveTo(s.endX, s.endY); | |
context.lineTo(s.endX + dw.x, s.endY + dw.y); | |
context.stroke(); | |
} | |
for (var i = 0; i < sections.length; i++) { | |
var s = sections[i]; | |
context.beginPath(); | |
context.fillStyle = lineargradient; | |
context.moveTo(s.startX, s.startY+4); | |
context.lineTo(s.startX, ysubbase); | |
context.lineTo(s.endX+1, ysubbase); | |
context.lineTo(s.endX+1, s.endY+4); | |
context.fill(); | |
} | |
/* Draw road thick black line */ | |
context.beginPath(); | |
var s = sections[sections.length - 1]; | |
context.moveTo(s.startX, s.startY); | |
context.strokeStyle='rgb(24,24,24)'; | |
context.lineWidth=8; | |
for (var i = sections.length - 1; i >= 0; i--) { | |
var s = sections[i]; | |
context.lineTo(s.startX, s.startY); | |
} | |
context.stroke(); | |
/* Draw end cap lines */ | |
context.strokeStyle = 'rgb(24,24,24)'; | |
context.lineWidth = 6; | |
context.lineCap = 'butt'; | |
context.beginPath(); | |
context.moveTo(sections[0].startX + dw.x, ysubbase + dw.y); | |
context.lineTo(sections[0].startX + dw.x, sections[0].startY + dw.y); | |
context.stroke(); | |
context.strokeStyle = 'rgb(24,24,24)'; | |
context.lineWidth = 2; | |
context.beginPath(); | |
context.moveTo(sections[0].startX + dw.x, ysubbase - 1 + dw.y); | |
context.lineTo(sections[0].startX, ysubbase - 1); | |
context.lineTo(sections[0].startX, sections[0].startY); | |
context.lineTo(sections[0].startX + dw.x, sections[0].startY + dw.y); | |
context.stroke(); | |
context.beginPath(); | |
context.moveTo(sections[0].startX, ysubbase + 16); | |
context.lineTo(sections[0].startX, ysubbase); | |
context.stroke(); | |
context.beginPath(); | |
context.moveTo(sections[sections.length-1].endX, ysubbase + 16); | |
context.lineTo(sections[sections.length-1].endX, sections[sections.length-1].endY); | |
context.stroke(); | |
/* Draw segment lines */ | |
var segments = []; | |
for(var i in data.details) { | |
var det = data.details[i]; | |
if(!det.isClimb) continue; | |
if(stravaOnSteroids.isRoute) { | |
var xx = sections[0].startX + det.endX * xf; | |
var yy = det.endY; | |
} else { | |
var xx = sections[0].startX + data.distance[det.streamIndices[1]] * xf; | |
var yy = data.altitude[det.streamIndices[1]]; | |
} | |
for(var j = 0; j < sections.length; j++) { | |
if(sections[j].startX >= xx) { | |
xx = sections[j].startX; | |
yy = sections[j].startY; | |
break; | |
} | |
} | |
if(stravaOnSteroids.isRoute) { | |
segments.push({x:xx, y:yy, name: det.name, alt: det.endY, dst: det.endX}); | |
} else { | |
segments.push({x:xx, y:yy, name: det.name, alt: data.altitude[det.streamIndices[1]], dst: data.distance[det.streamIndices[1]]}); | |
} | |
} | |
segments.sort( function(a,b) {return a.x-b.x} ); | |
var lastXX = sections[sections.length-1].endX + 20; | |
for(var i = segments.length-1; i >= 0; i--) { | |
var det = segments[i]; | |
var xx = sections[0].startX + det.dst * xf; | |
if(lastXX-xx < 16) { det.skip = true; } else lastXX = xx; | |
} | |
context.lineWidth=1; | |
context.strokeStyle = 'rgb(24,24,24)'; | |
for(var i = 0; i < segments.length; i++) { | |
if(segments[i].skip) continue; | |
var det = segments[i]; | |
var xx = sections[0].startX + det.dst * xf; | |
var yy = det.alt; | |
for(var j = 0; j < sections.length; j++) { | |
if(sections[j].startX >= xx) { | |
xx = sections[j].startX; | |
yy = sections[j].startY; | |
break; | |
} | |
} | |
context.beginPath(); | |
context.moveTo(xx, ysubbase + 16); | |
context.lineTo(xx, yy); | |
context.stroke(); | |
context.beginPath(); | |
context.moveTo(xx + dw.x, yy + dw.y); | |
context.lineTo(xx + dw.x, yy + dw.y - 20); | |
context.stroke(); | |
} | |
context.lineCap = 'round'; | |
/* Fill road thick black line with thinner white line */ | |
context.beginPath(); | |
var s = sections[sections.length - 1]; | |
context.moveTo(s.startX, s.startY); | |
context.strokeStyle='rgb(255,255,255)'; | |
context.lineWidth=4; | |
for (var i = sections.length - 1; i >= 0; i--) { | |
var s = sections[i]; | |
context.lineTo(s.startX, s.startY); | |
} | |
context.stroke(); | |
/* Fill thick black + white line with pink road line */ | |
context.beginPath(); | |
var s = sections[sections.length - 1]; | |
context.moveTo(s.startX, s.startY); | |
context.strokeStyle='rgb(252,0,80)'; | |
context.lineWidth=1.75; | |
for (var i = sections.length - 1; i >= 0; i--) { | |
var s = sections[i]; | |
context.lineTo(s.startX, s.startY); | |
} | |
context.stroke(); | |
context.lineWidth = 1; | |
/* Draw distance markers */ | |
var distance_gap = 5000; | |
s = sections[sections.length-1]; | |
context.fillStyle = 'rgb(24,24,24)'; | |
context.strokeStyle = 'rgb(24,24,24)'; | |
var xx = 0; | |
for(var x = sections[0].startX, x0 = 0; x < s.endX; x0 += distance_gap, x += distance_gap * xf) { | |
var x1 = x; | |
var x2 = distance_gap * xf; | |
if(x + x2 > s.endX) x2 = s.endX - x1; | |
if(xx) { | |
context.strokeRect(x1, ysubbase - 8, x2, 8); | |
} else { | |
context.fillRect(x1, ysubbase - 8, x2, 8); | |
} | |
xx = !xx; | |
context.font = '8pt Archivo Narrow'; | |
var st = Math.round(x0/1000).toString(); | |
if(st > 0) context.fillText(st, x1 - context.measureText(st).width / 2, ysubbase + 8 + 3); | |
} | |
/* Rotate transform for text */ | |
context.rotate(-Math.PI/2); | |
context.fillStyle='rgb(24,24,24)'; | |
context.lineWidth=1; | |
context.strokeStyle = 'rgb(24,24,24)'; | |
for(var i = 0; i < segments.length; i++) { | |
if(segments[i].skip) continue; | |
var det = segments[i]; | |
var xx = sections[0].startX + det.dst * xf; | |
var yy = det.alt; | |
for(var j = 0; j < sections.length; j++) { | |
if(sections[j].startX >= xx) { | |
xx = sections[j].startX; | |
yy = sections[j].startY; | |
break; | |
} | |
} | |
context.font = "bold 14pt Archivo Narrow"; | |
var alt = Math.round(det.alt).toString(); | |
var dst = (Math.round(det.dst/100)/10).toString(); | |
var st = alt + ' - '+det.name.toUpperCase(), ste=''; | |
var nn = -yy-dw.y+22; | |
while(nn + context.measureText(st+ste).width > 0 && st != '') { st = st.substr(0,st.length-1); ste='...'; } | |
context.fillText(st+ste, -yy-dw.y+22, xx+dw.x+5); | |
context.fillText(dst, -ysubbase-20-context.measureText(dst).width, xx+5); | |
} | |
var dst = (Math.round(data.distance[data.distance.length-1]/100)/10).toString(); | |
context.fillText(dst, -ysubbase-20-context.measureText(dst).width, sections[sections.length-1].endX+5); | |
context.fillText('0.0', -ysubbase-20-context.measureText('0.0').width, sections[0].startX+5); | |
} | |
} | |
}; | |
stravaOnSteroids.create(); | |
var stravaOnSteroids_LeTour = { | |
lengthMultiplierLarge : 0.001, | |
lengthMultiplierSmall : 1, | |
lengthUnitLarge:"km", | |
lengthUnitSmall:"m", | |
segName:'ride', | |
loading : false, | |
segDistance : 1, | |
segCat : '', | |
segGrade : 0, | |
segElevGain : 0, | |
create: function() { | |
var li = jQuery('#letour'); | |
if(li) { li.remove(); } | |
li = jQuery('.effort-charts .letour'); | |
if(li) li.remove(); | |
var ac = jQuery('.effort-charts'); | |
var li = document.createElement('li'); | |
var a = document.createElement('a'); | |
var c = document.createElement('canvas'); | |
c.style.width='80%'; | |
c.style.marginLeft='10%'; | |
c.id='stravaOnSteroids_LeTour'; | |
var chart = document.createElement('div'); | |
jQuery(chart).addClass('chart').addClass('letour').addClass('background-off').css('display','none').append(c); | |
jQuery(ac).append(chart); | |
var ul = jQuery('.effort-charts > ul'); | |
jQuery('#rabbit').after(li); | |
jQuery(li).append(a).attr('id', 'letour'); | |
jQuery(li).bind('click', function() { | |
event.cancelBubble = true; | |
event.preventDefault(); | |
event.stopPropogation(); }); | |
jQuery(a).addClass('tab').text('Le Tour').bind('click', function(event) { | |
event.cancelBubble = true; | |
jQuery('> li', ul).removeClass('selected'); | |
jQuery(li).addClass('selected'); | |
jQuery('.rabbit', ac).css('display','none'); | |
jQuery('.elevation', ac).css('display','none'); | |
jQuery('.performance', ac).css('display','none'); | |
jQuery(chart).css('display','block'); | |
stravaOnSteroids_LeTour.redraw(); | |
//event.preventDefault(); | |
//event.stopPropogation(); | |
return false; | |
}); | |
jQuery('> li > a', ul).not(a).bind('click', function(event) { | |
jQuery(li).removeClass('selected'); | |
jQuery(chart).css('display','none'); | |
}); | |
}, | |
redraw: function() { | |
if (this.loading) return; | |
var canvas = document.getElementById('stravaOnSteroids_LeTour'); | |
canvas.height = canvas.width * 0.5; | |
// canvas.style.height = canvas.offsetWidth + 'px'; | |
if (canvas.getContext) { | |
var ctx = canvas.getContext('2d'); | |
ctx.save(); | |
ctx.setTransform(1, 0, 0, 1, 0, 0); | |
/* Will always clear the right space */ | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
ctx.restore(); | |
} | |
var effortId = location.hash.substring(1); | |
var data = {data: {altitude: gStreamData.altitude, distance: gStreamData.distance}}; | |
if (!data.data.altitude || !data.data.distance) return; | |
var effort = gStreamData.activityData.effortData.details[effortId]; | |
if(!effort) return; | |
data.data.altitude = data.data.altitude.slice(effort.streamIndices[0], effort.streamIndices[1]); | |
data.data.distance = data.data.distance.slice(effort.streamIndices[0], effort.streamIndices[1]); | |
data.data.minHeight = 10000; | |
data.data.maxHeight = -10000; | |
data.data.minHeightLocation = [0, 0]; | |
data.data.maxHeightLocation = [0, 0]; | |
var i, base=data.data.distance[0]; | |
/* calculate max min altitude in metres */ | |
for (i = 0; i < data.data.altitude.length; i++) { | |
if (data.data.altitude[i] < data.data.minHeight) { | |
data.data.minHeight = data.data.altitude[i]; | |
} | |
if (data.data.altitude[i] > data.data.maxHeight) { | |
data.data.maxHeight = data.data.altitude[i]; | |
} | |
data.data.distance[i]-=base; | |
} | |
var desiredWidth = canvas.offsetWidth, desiredHeight = canvas.offsetHeight; | |
var f = (desiredWidth) / (data.data.distance[data.data.distance.length-1]); | |
var xSteps = [10,25,50,100,250,500,1000,100000]; | |
var xStep = data.data.distance[data.data.distance.length-1] / 15; | |
if (xStep < 0.2) { xStep = 0.1; } | |
else if (xStep >= 0.2 && xStep < 0.4) { xStep = 0.25; } | |
else if (xStep >= 0.4 && xStep < 0.75) { xStep = 0.5; } | |
else if (xStep >= 0.75 && xStep < 2) { xStep = 1; } | |
else if (xStep >= 2 && xStep < 4) { xStep = 2.5; } | |
else if (xStep >= 4 && xStep < 7.5) { xStep = 5; } | |
else if (xStep >= 7.5 && xStep < 20) { xStep = 10; } | |
else if (xStep >= 20 && xStep < 40) { xStep = 25; } | |
else if (xStep >= 40 && xStep < 75) { xStep = 50; } | |
else if (xStep >= 75 && xStep < 200) { xStep = 100; } | |
else if (xStep >= 200 && xStep < 400) { xStep = 250; } | |
else if (xStep >= 400 && xStep < 750) { xStep = 500; } | |
else if (xStep >= 750 && xStep < 2000) { xStep = 1000; } | |
else if (xStep >= 2000 && xStep < 4000) { xStep = 2500; } | |
else if (xStep >= 4000 && xStep < 7500) { xStep = 5000; } | |
else { xStep = 10000; } | |
var yStep = (data.data.maxHeight-data.data.minHeight) / 10; | |
if (yStep < 2) { yStep = 1; } | |
else if (yStep >= 2 && yStep < 4) { yStep = 2.5; } | |
else if (yStep >= 4 && yStep < 7.5) { yStep = 5; } | |
else if (yStep >= 7.5 && yStep < 20) { yStep = 10; } | |
else if (yStep >= 20 && yStep < 40) { yStep = 25; } | |
else if (yStep >= 35 && yStep < 75) { yStep = 50; } | |
else if (yStep >= 75 && yStep < 200) { yStep = 100; } | |
else if (yStep >= 200 && yStep < 400) { yStep = 250; } | |
else if (yStep >= 400 && yStep < 750) { yStep = 500; } | |
else if (yStep >= 750 && yStep < 2000) { yStep = 1000; } | |
else if (yStep >= 2000 && yStep < 4000) { yStep = 2500; } | |
else if (yStep >= 4000 && yStep < 7500) { yStep = 5000; } | |
else { yStep = 10000; } | |
var overallGrad = (data.data.maxHeight - data.data.minHeight); /* / data.data.distance[data.data.distance.length-1]; */ | |
var vertMultiplier = Math.min(10, desiredHeight / overallGrad); /*Math.min(10, 10 * (1 - (overallGrad * 3)));*/ | |
var FitGradient = true; | |
var angle = 10 * Math.PI / 180; | |
if (isNaN(angle) || isNaN(f) || isNaN(xStep) || isNaN(yStep)) return; | |
if (f > 0 && xStep > 0 && yStep > 0 && angle > 0) { | |
this.drawLeTour(data.data, f, vertMultiplier, xStep, yStep, FitGradient, angle); | |
} | |
}, | |
matrix: [], | |
transform: function(x, y) { | |
return { x: x * matrix[0] + y * matrix[2] + 1 * matrix[4], y: x * matrix[1] + y * matrix[3] + 1 * matrix[5] }; | |
}, | |
inverse_transform: function(x, y, angle) { | |
var matrix = [1, Math.tan(angle), 0, 1, 0, 0]; /* Skew transform */ | |
return { x: x * matrix[0] + y * matrix[2] + 1 * matrix[4], y: x * matrix[1] + y * matrix[3] + 1 * matrix[5] }; | |
}, | |
findSectionIntersection: function(sections,y) { | |
for(var i = 0; i < sections.length; i++) { | |
var s = sections[i]; | |
if(s.startY >= y && s.endY < y) { | |
return s.startX + (s.endX - s.startX) * (s.startY-y) / (s.startY - s.endY); | |
} | |
} | |
return null; | |
}, | |
drawLeTour: function(data, xf, yf, xStep, yStep, FitGradient, angle) { | |
var c = document.getElementById('stravaOnSteroids_LeTour'); | |
var w = 16, ysubbase=(data.maxHeight-data.minHeight) * yf + 48, ybase=ysubbase-40; | |
var dt = this.inverse_transform((data.distance[data.distance.length-1] * xf + 48), ysubbase, angle); | |
c.width = dt.x; | |
c.height = dt.y; | |
//var angle = Math.PI/18; | |
matrix = [1,Math.tan(-angle),0,1,0,dt.x * Math.tan(angle)]; // Skew transform | |
var dw = {x:-w*Math.cos(angle), y:-w*Math.sin(angle)}; // Apply "3D" | |
dw = this.inverse_transform(dw.x,dw.y,angle); // Remove skew | |
var context = c.getContext('2d'); | |
context.fillStyle = 'rgb(255,255,255)'; | |
context.fillRect(0,0,c.width,c.height); | |
context.save(); | |
context.setTransform(matrix[0],matrix[1],matrix[2],matrix[3],matrix[4],matrix[5]); // "Isometric" | |
var x = -dw.x, y = ybase + dw.y; | |
context.beginPath(); | |
context.moveTo(0, y); | |
context.strokeStyle = 'rgba(0,0,0,1)'; | |
context.lineTo(x, y); | |
var startSeg = 0, rise = 0, len = 0, gradient = 0, sections = [], startX = x, startY = y; | |
for(var i = 1; i < data.altitude.length; i++) | |
{ | |
var seglen = data.distance[i] - data.distance[i-1]; | |
var segrise = data.altitude[i] - data.altitude[i-1]; | |
if(len > 0) | |
{ | |
var cut = !FitGradient ? | |
(Math.floor(data.distance[i]/xStep) > Math.floor(data.distance[i-1]/xStep)) : | |
Math.abs(1- (rise/len) / (segrise/seglen)) > 0.5 /*Math.abs(segrise/seglen - rise/len) > (rise/len)*/ && (seglen + len) * xf > 32; | |
if(cut) //Math.abs(1- (rise/len) / (segrise/seglen)) > 0.5 /*Math.abs(segrise/seglen - rise/len) > (rise/len)*/ && (seglen + len) * xf > 32) | |
{ | |
x += len * xf; y -= rise * yf; | |
sections.push({startX: startX, startY: startY, endX: x, endY: y, gradient: ((startY - y)/yf) / ((x - startX)/xf) * 100}); | |
rise=0; len=0; | |
startX = x; startY = y; | |
} | |
} | |
rise += segrise; | |
len += seglen; | |
} | |
var lineargradient = context.createLinearGradient(0,0,0,ysubbase); | |
lineargradient.addColorStop(0, 'rgba(200, 200, 200, 0.1)'); | |
lineargradient.addColorStop(1, 'rgba(200, 200, 200, 0.5)'); | |
x += len * xf; y -= rise * yf; | |
sections.push({startX: startX, startY: startY, endX: x, endY: y, gradient: (startY - y) / (x - startX) * 10}); | |
for (var i = sections.length - 1; i >= 0; i--) { | |
var s = sections[i]; | |
/* Draw slope of road */ | |
context.beginPath(); | |
if (s.gradient.toFixed(1) >= 15) context.fillStyle = 'rgb(0,0,0)'; | |
else if (s.gradient.toFixed(1) >= 10) context.fillStyle = 'rgb(255,16,16)'; | |
else if (s.gradient.toFixed(1) >= 5) context.fillStyle = 'rgb(32,32,200)'; | |
else if (s.gradient.toFixed(1) <= -15) context.fillStyle = 'rgb(0,0,0)'; | |
else if (s.gradient.toFixed(1) <= -10) context.fillStyle = 'rgb(168,11,11)'; | |
else if (s.gradient.toFixed(1) <= -5) context.fillStyle = 'rgb(19,19,99)'; | |
else if (s.gradient.toFixed(1) < 0) context.fillStyle = 'rgb(22,112,22)'; | |
else context.fillStyle = 'rgb(32,200,32)'; | |
context.moveTo(s.startX, s.startY); | |
context.lineTo(s.startX + dw.x, s.startY + dw.y); | |
context.lineTo(s.endX + dw.x, s.endY + dw.y); | |
context.lineTo(s.endX, s.endY); | |
context.lineTo(s.startX, s.startY); | |
context.fill(); | |
/* centre line on road */ | |
context.beginPath(); | |
context.strokeStyle = 'rgba(255,255,255,0.5)'; | |
context.dashedLineTo(s.startX + dw.x / 2, s.startY + dw.y / 2, s.endX + dw.x / 2, s.endY + dw.y / 2, [3, 2]); | |
context.stroke(); | |
} | |
for (var i = 0; i < sections.length; i++) { | |
var s = sections[i]; | |
/* Draw descenders */ | |
context.beginPath(); | |
context.fillStyle = lineargradient; | |
context.moveTo(s.startX, s.startY); | |
context.lineTo(s.startX, ysubbase); | |
context.lineTo(s.endX, ysubbase); | |
context.lineTo(s.endX, s.endY); | |
context.fill(); | |
context.beginPath(); | |
context.strokeStyle = '#8080e0'; | |
context.dashedLineTo(s.startX, s.startY, s.startX, ysubbase - 20, [3, 2]); | |
context.stroke(); | |
if (s.endX - s.startX >= 24) { | |
context.font = 'bold 9pt Calibri'; | |
context.textAlign = 'center'; | |
context.fillStyle = 'rgb(20,20,20)'; | |
context.fillText(s.gradient.toFixed(1), (s.startX + s.endX) / 2, ysubbase - 24); | |
} | |
} | |
/* Draw altitude ticks */ | |
//context.textAlign='left'; | |
//context.textBaseline='middle'; | |
s = sections[sections.length - 1]; | |
// height lines should not extend beyond track. | |
for (var y = yStep; y < data.maxHeight - data.minHeight; y += yStep) { | |
var xStart = 0; | |
var xEnd = -1; | |
var altY = ybase - y * yf; | |
for (var i = 0; i < sections.length; i++) { | |
var slocal = sections[i]; | |
if (slocal.startY >= altY && slocal.endY < altY) { | |
// if ascending then mark the startpoint for the line | |
xStart = slocal.startX + (slocal.endX - slocal.startX) * (slocal.startY - altY) / (slocal.startY - slocal.endY); | |
// draw a faint line between peaks | |
if (xEnd > -1) { | |
context.beginPath(); | |
context.strokeStyle = 'rgba(200,200,250,0.5)'; | |
context.dashedLineTo(xEnd, altY, xStart, altY, [3, 5]); | |
context.stroke(); | |
} | |
} | |
if (slocal.startY < altY && slocal.endY >= altY) { | |
// if descending then draw line to this point | |
xEnd = slocal.startX + (slocal.endX - slocal.startX) * (slocal.startY - altY) / (slocal.startY - slocal.endY); | |
context.beginPath(); | |
context.strokeStyle = '#c0c0e0'; | |
context.dashedLineTo(xStart, altY, xEnd, altY, [3, 2]); | |
context.stroke(); | |
xStart = -1; | |
} | |
} | |
// if segment finishes with a climb then draw lines to the right hand side | |
if (xStart > -1) { | |
context.beginPath(); | |
context.strokeStyle = '#c0c0e0'; | |
context.dashedLineTo(xStart, altY, s.endX, altY, [3, 2]); | |
context.stroke(); | |
} | |
else { | |
// if descending finish then draw much lighter line to the right hand side | |
context.beginPath(); | |
context.strokeStyle = 'rgba(200,200,250,0.5)'; | |
context.dashedLineTo(xEnd, altY, s.endX, altY, [3, 5]); | |
context.stroke(); | |
} | |
} | |
context.beginPath(); | |
context.strokeStyle = '#c0c0e0'; | |
//context.lineTo(0, ybase - 0 * yf, s.endX, ybase - 0 * yf); | |
context.moveTo(0, ybase - 0 * yf); | |
context.lineTo(s.endX, ybase - 0 * yf); | |
context.stroke(); | |
/* Draw distance markers */ | |
context.fillStyle = 'rgb(40,40,40)'; | |
context.fillRect(sections[0].startX, ysubbase - 20, s.endX - sections[0].startX, 20); | |
context.beginPath(); | |
context.moveTo(sections[0].startX, ysubbase - 20); | |
context.lineTo(sections[0].startX, ysubbase); | |
context.lineTo(sections[0].startX + dw.x, ysubbase + dw.y); | |
context.lineTo(sections[0].startX + dw.x, ysubbase - 20 + dw.y); | |
context.lineTo(sections[0].startX, ysubbase - 20); | |
context.fill(); | |
context.font = '9pt Calibri'; | |
context.textAlign = 'center'; | |
context.textBaseline = 'alphabetic'; | |
context.fillStyle = 'rgb(255,255,255)'; | |
for(var x = xStep; x * xf < s.endX; x += xStep) { | |
var xx = (data.distance[data.distance.length-1] >= 5000) ? x/1000 : x; | |
context.fillText(xx, x * xf + sections[0].startX, ysubbase - 4); | |
} | |
context.beginPath(); | |
context.moveTo(s.endX, s.endY); | |
context.lineTo(s.endX, ysubbase); | |
s = sections[0]; | |
context.lineTo(s.startX, ysubbase); | |
context.stroke(); | |
context.beginPath(); | |
context.fillStyle = '#c0c0e0'; | |
context.moveTo(s.startX, s.startY); | |
context.lineTo(s.startX + dw.x, s.startY + dw.y); | |
context.lineTo(s.startX + dw.x, ysubbase - 20 + dw.y); | |
context.lineTo(s.startX, ysubbase - 20); | |
context.lineTo(s.startX, s.startY); | |
context.fill(); | |
/* Switch out of transform */ | |
context.restore(); | |
/* Draw altitude text */ | |
s = sections[sections.length - 1]; | |
context.font = 'bold 9pt Calibri'; | |
context.textAlign = 'left'; | |
context.textBaseline = 'middle'; | |
context.fillStyle = '#000000'; | |
for(var y = -100; y < data.maxHeight; y += yStep) { | |
x = this.findSectionIntersection(sections, ybase - y * yf); | |
if(x) { | |
var dt = this.transform(s.endX + 4, ybase - y * yf); | |
context.fillText(y, dt.x, dt.y); | |
} | |
} | |
} | |
}; | |
/** | |
* dashedLineTo | |
**/ | |
CanvasRenderingContext2D.prototype.dashedLineTo = function (fromX, fromY, toX, toY, pattern) { | |
// Our growth rate for our line can be one of the following: | |
// (+,+), (+,-), (-,+), (-,-) | |
// Because of this, our algorithm needs to understand if the x-coord and | |
// y-coord should be getting smaller or larger and properly cap the values | |
// based on (x,y). | |
var lt = function (a, b) { return a <= b; }; | |
var gt = function (a, b) { return a >= b; }; | |
var capmin = function (a, b) { return Math.min(a, b); }; | |
var capmax = function (a, b) { return Math.max(a, b); }; | |
var checkX = { thereYet: gt, cap: capmin }; | |
var checkY = { thereYet: gt, cap: capmin }; | |
if (fromY - toY > 0) { | |
checkY.thereYet = lt; | |
checkY.cap = capmax; | |
} | |
if (fromX - toX > 0) { | |
checkX.thereYet = lt; | |
checkX.cap = capmax; | |
} | |
this.moveTo(fromX, fromY); | |
var offsetX = fromX; | |
var offsetY = fromY; | |
var idx = 0, dash = true; | |
while (!(checkX.thereYet(offsetX, toX) && checkY.thereYet(offsetY, toY))) { | |
var ang = Math.atan2(toY - fromY, toX - fromX); | |
var len = pattern[idx]; | |
offsetX = checkX.cap(toX, offsetX + (Math.cos(ang) * len)); | |
offsetY = checkY.cap(toY, offsetY + (Math.sin(ang) * len)); | |
if (dash) this.lineTo(offsetX, offsetY); | |
else this.moveTo(offsetX, offsetY); | |
idx = (idx + 1) % pattern.length; | |
dash = !dash; | |
} | |
}; | |
stravaOnSteroids_LeTour.create(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hey @mcdurdin - I have seen some route profile renderings made with your script. Would love to use it, but how? Can you pls provide some guidance? Thanks :)