Newer
Older
gpx_web_viewer / js / gpx.js
/* gpx.js - display GPX tracks on a leaflet maps
 * see leaflet-gpx https://github.com/mpetazzoni/leaflet-gpx for the underlying leaflet plugin
 */

// keep track of how many tracks we've added
var trackCount = 0
var trackArray = []

var map = L.map('map')

L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    minZoom: 6, // avoid zooming outside track range, roughly, to prevent extra tile requests
    maxZoom: 15,
    attribution: 'Map data &copy <a href="http://www.osm.org">OpenStreetMap</a>'
}).addTo(map)

function download(url) {
    const a = document.createElement('a')
    a.href = url
    a.download = url.split('/').pop()
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
}

function sortedAppend(t) {
    // splice might fail "not a function" otherwise
    if(trackArray.length == 0) {
        trackArray.push(t)
        return
    }
    // find idx to splice
    var idx = 0
    for(const track of trackArray) {
        if(t.date > track.date) {
            break
        }
        idx++
    }
    trackArray.splice(idx, 0, t)
    trackCount++
}

function createTrackList(arr) {
    var tracklist = document.getElementById('tracklist')
    arr.forEach((track, idx, array) => {
        const date = track.date
        const text = track.name
        const bounds = track.bounds
        var t = document.createElement('section')
        t.className = 'track'
        t.innerHTML = `<a><date>${date.toISOString().split('T')[0]}</date><span>${text}</span></a>`

        /* create side information */
        t.onclick = function() {
            map.fitBounds(bounds)

            // clear previous descriptions
            var oldDesc = document.getElementById("bottomtext")
            if(oldDesc) {
                document.body.removeChild(oldDesc)
            }
            
            var description = document.createElement('div')
            description.className = "bottomtext"
            description.id = "bottomtext"

            /* download button */
            const button = document.createElement('button')
            button.onclick = function() { download(track.filename) }
            button.className = "dbutton"
            button.innerHTML = `Download GPX`
            button.role = "button"

            /* close button */
            const cb = document.createElement('button')
            cb.onclick = function() { this.parentNode.parentNode.removeChild(this.parentNode) } // TODO find a way to open this again
            cb.className = "dbutton cbutton"
            cb.innerHTML = `Close`
            cb.role = "button"

            const txt = document.createElement('span')
            txt.innerHTML = `
<h3>${track.name}</h3>
<b>Distance: ${track.kms.toFixed(2)} km</b><br>
<b>Elevation gain: ${track.elevation.toFixed()} m d+</b><br><br>
Total time: ${track.totalAvgTime} h<br>
Moving time: ${track.movingTime} h<br>
Avg speed: ${track.totalAvgSpeed.toFixed(2)} km/h<br>
Avg moving speed: ${track.movingSpeed.toFixed(2)} km/h <br><br>`

            description.appendChild(txt)
            description.appendChild(button)
            description.appendChild(cb)

            document.body.appendChild(description)
        }
        tracklist.appendChild(t)
    })
}

function addGPX(map, gpx, lineColor) {
    new L.GPX(gpx, { // TODO handle errors when loading tracks
        async: true,
        marker_options: {
            startIconUrl: 'images/pin-icon-start.png',
            endIconUrl: 'images/pin-icon-end.png',
            shadowUrl: 'images/pin-shadow.png'
        },
        polyline_options: {
            smoothFactor: 3,
            color: lineColor,
            opacity: 1,
            weight: 3,
            lineCap: 'round'
        }
    }).on('loaded', function(e) {
        // add track to list
        const track = {
            date: e.target.get_start_time(),
            name: e.target.get_name(),
            bounds: e.target.getBounds(),
            filename: gpx,
            kms: e.target.get_distance() / 1000,
            elevation: e.target.get_elevation_gain(),
            movingTime: e.target.get_duration_string_iso(e.target.get_moving_time(), true),
            movingSpeed: e.target.get_moving_speed(),
            totalAvgTime: e.target.get_duration_string_iso(e.target.get_total_time(), true),
            totalAvgSpeed: e.target.get_total_speed()
        }
        sortedAppend(track)
        // fit map to track bounds on loading
        map.fitBounds(e.target.getBounds())
    }).on('mouseover', function(e) {
        var paths = gpx.split("/")
        var filename = paths[paths.length - 1]
        var kms = e.target.get_distance() / 1000
        var movingTime = e.target.get_duration_string_iso(e.target.get_moving_time(), true)
        var movingSpeed = e.target.get_moving_speed()
        var totalAvgTime = e.target.get_duration_string_iso(e.target.get_total_time(), true)
        var totalAvgSpeed = e.target.get_total_speed()
        var tooltip = `
<div class="maptooltip">
    <h3>${e.target.get_name()}</h3>
    <div class="maptooltip-left">
        <b>Distance: ${kms.toFixed(2)} km</b><br>
        Total time: ${totalAvgTime} h<br>
        Moving time: ${movingTime} h<br>
    </div>
    <div class="maptooltip-right">
        <b>Elevation gain: ${e.target.get_elevation_gain().toFixed()} m d+</b><br>
        Avg speed: ${totalAvgSpeed.toFixed(2)} km/h<br>
        Avg moving speed: ${movingSpeed.toFixed(2)} km/h <br>
    </div>
    <div class="clear"></div>
    <h4>Click to download: ${filename}</h4>
</div>`
        this.bindTooltip(tooltip, {sticky: true})
        this.openPopup(e.latlng)
    }).on('click', function(e) {
        download(gpx)
    }).addTo(map)
}

// load tracks from json file 
async function getTracks() {
    const url = "/tracklist.json"
    try {
        const response = await fetch(url)
        if (!response.ok) {
            throw new Error(`Response status: ${response.status}`)
        }

        // parse response as json
        const result = await response.json()

        // add tracks to the leaflet map
        result.tracklist.forEach((gpx) => addGPX(map, "/" + gpx, "purple"))

        // return the result length to display the list afterwards
        return result.tracklist.length
    } catch (error) {
        console.error(error.message)
        return 0
    }
}

// wait for tracks to be loaded and list them
function sleep(ms) {
    return new Promise(resolve => { setTimeout(resolve, ms) })
}
async function listTracksAfterLoading(tl) {
    while (trackArray.length < tl) {
        await(sleep(100))
    }
    this.createTrackList(trackArray)
}

// resolve promises
getTracks()
    .then(tl => listTracksAfterLoading(tl))