GPS / GPX Visualization
Visualizing GPS track data in GPX format.
GPS tracks can be recorded by smartphone apps like Strava, as well as other devices such as GoPro and Insta360 cameras, and Garmin and Apple watches.
Click the map to copy the current lat/long coordinates.
Source Code for this Sketch
const border = 50
const playX = 750
const rewindX = 700
let track
let frame = 0
let speed = 0
let heightRatio = 1
let trail = []
let trailLength = 300
let current
let playing = true
let lastLat = 0
let lastLong = 0
let lastSpeed = 0
let lastAvgSpeed = 0
let lastElevation = 0
let lastAvgElevation = 0
// speedGraph draws speeds at position y, height of scale, and time offset
function speedGraph(y, scale, offset) {
strokeWeight(2)
let speedNow = track.normalizeSpeed(track.points[offset].speed)*scale
let speedPrev = 0
let speedPrev2 = 0
let speedNext = 0
for (let i = offset; i < track.points.length-1 && i-offset < width+1; i++) {
// Lookahead for end of catmull rom
if (track.points[i+1].speed) {
speedNext = track.normalizeSpeed(track.points[i+1].speed)*scale
} else {
// If there is no forward data, reused the last good data
speedNext = speedNow
}
stroke(speedNow/scale*140+220, 100, 50)
// Use a curve if the speedNow gap is > 4 pixels, but not at the beginning
// or end of the scale (to avoid looking outside the array bounds)
if (i > 2) {
// console.log("curving", i-2, speedPrev2, i-1, speedPrev, i, speedNow, i+1, speedNext)
curve(i-offset-2, y+scale-speedPrev2,
i-offset-1, y+scale-speedPrev,
i-offset, y+scale-speedNow,
i-offset+1, y+scale-speedNext)
}
// rolling window of speeds
speedPrev2 = speedPrev
speedPrev = speedNow
speedNow = speedNext
}
}
function drawTrail(newest) {
strokeWeight(2)
stroke(0)
if(trail.length < trailLength){
trail.push(createVector(newest.x, newest.y))
for(let i = 0; i < trail.length; i++){
point(trail[i].x, trail[i].y)
}
} else {
for(let i = 0; i < trail.length-1; i++){
trail[i] = trail[i+1]
point(trail[i].x, trail[i].y)
}
trail[trail.length-1] = createVector(newest.x, newest.y)
point(newest.x, newest.y)
}
}
let speedometer = new RollingAverage(30)
let altimeter = new RollingAverage(30)
function drawCoords() {
// Calculate coordinates using some brain-agonizing lat/long/x/y conversion
// at x,y = 0,0 we should have minLong and maxLat
// and by x,y = width-100,height-100 we should have maxLong and minLat
// or something like that.
let long = current.longitude
let lat = current.latitude
let speed = current.speed
let elevation = current.elevation
if (long && lat && speed && elevation) {
lastLong = long
lastLat = lat
lastSpeed = speed
lastElevation = elevation
}
if(frame % 10 === 0) {
lastAvgSpeed = speedometer.average(lastSpeed)
lastAvgElevation = altimeter.average(lastElevation)
} else {
speedometer.average(lastSpeed)
altimeter.average(lastElevation)
}
let speedWS = 5-lastAvgSpeed.toFixed(1).length
let elevWS = 5-lastAvgElevation.toFixed(1).length
let x = track.normalizeLong(current.longitude)*(800-border*2)+border
let y = height-(track.normalizeLat(current.latitude)*(800-border*2)*heightRatio)-border
let leftOffset = x-185/2
let bottomOffset = height-y-30
// keep on screen
if(leftOffset < 20) {
leftOffset = 20
}
if(leftOffset > width-20-185) {
leftOffset = width-20-185
}
if (bottomOffset < 60) {
bottomOffset = 60
}
// avoid the current position when the current position is within 80 units
// of the bottom
if(height-y < 100) {
if (x < width/2) {
leftOffset += 100
} else {
leftOffset -= 100
}
}
// Tooltip window around the coordinates area
stroke(100)
fill(0)
rect(leftOffset-5, height-bottomOffset-15, 185, 50)
// Draw coordinates
noStroke()
textSize(12)
textFont("monospace")
fill(100)
text("Speed "+" ".repeat(speedWS)+lastAvgSpeed.toFixed(1)+"km/h", leftOffset, height-bottomOffset)
text("Elevation "+" ".repeat(elevWS)+lastAvgElevation.toFixed(0)+"m", leftOffset, height-bottomOffset+15)
text(lastLat.toFixed(8)+", "+lastLong.toFixed(8), leftOffset, height-bottomOffset+30)
strokeWeight(2)
stroke(100)
point(leftOffset+100, height-bottomOffset+4)
point(leftOffset+100, height-bottomOffset+19)
stroke(track.normalizeSpeed(lastSpeed)*240, 100, 50)
line(leftOffset, height-bottomOffset+4, leftOffset+track.normalizeSpeed(lastSpeed)*100, height-bottomOffset+4)
stroke(220-track.normalizeElevation(lastElevation)*220, 100, 50)
line(leftOffset, height-bottomOffset+19, leftOffset+track.normalizeElevation(lastElevation)*100, height-bottomOffset+19)
}
function inRect(x, y, w, h) {
return (mouseX >= x && mouseX < x+w && mouseY >= y && mouseY < y+h)
}
function rewind() {
frame = 0
background(0)
playing = false
trail = []
}
function mouseClicked() {
// play button
if (inRect(playX, height-40, 20, 20)) {
playing = !playing
} else if (inRect(rewindX, height-40, 20, 20)) {
rewind()
} else if (inRect(0, 0, width, height)) {
console.log(lastLat+", "+lastLong)
window.navigator.clipboard.writeText(lastLat+", "+lastLong)
}
}
function playButton() {
noStroke()
if(playing) {
if(inRect(playX, height-40, 20, 20)) {
fill(0, 100, 70)
} else {
fill(0, 100, 50)
}
rect(playX, height-40, 20, 20)
} else {
fill(0, 0, 0)
rect(playX, height-40, 20, 20)
if(inRect(playX, height-40, 20, 20)) {
fill(150, 100, 70)
} else {
fill(150, 100, 40)
}
triangle(playX, height-40, playX+20, height-30, playX, height-20)
}
}
function rewindButton() {
noStroke()
if (frame === 0) {
fill(200, 0, 50)
} else {
if (inRect(rewindX, height-40, 20, 20)) {
fill(220, 100, 70)
} else {
fill(220, 100, 50)
}
}
triangle(rewindX+20, height-40, rewindX+10, height-30, rewindX+20, height-20)
triangle(rewindX+10, height-40, rewindX, height-30, rewindX+10, height-20)
}
function preload() {
track = loadGPX("/assets/malibu-track.gpx")
}
function setup() {
heightRatio = track.latRange / track.longRange
createCanvas(800, 700*heightRatio+border*2, P2D, canvas);
colorMode(HSL)
background(0)
current = createVector(0, 0)
}
function draw() {
background(0)
strokeWeight(1)
// Draw the route in reverse so we can color it in a different color as we
// make progress
stroke(100)
let pp
for (let i = track.points.length-1; i > 0; i--) {
pp = track.points[i]
point(track.normalizeLong(pp.longitude)*(800-border*2)+border,
height-(track.normalizeLat(pp.latitude)*(800-border*2)*heightRatio)-border)
}
current = track.points[frame]
stroke(100)
strokeWeight(1)
fill(0, 100, 50)
circle(track.normalizeLong(current.longitude)*(800-border*2)+border,
height-(track.normalizeLat(current.latitude)*(800-border*2)*heightRatio)-border, 10)
playButton()
rewindButton()
drawCoords()
if (!playing) {
return
}
// Stop drawing when we reach the end
if(frame >= track.points.length-1) {
rewind()
}
frame++
}
You may use all or part of the p5 code seen on this page for your own creations, without restriction. Attribution is appreciated but not required.
This sketch also includes the following sources: p5common.js gpx.js . Sources are available under their respective licenses.