Hiking path time

Hello,
I’m using the online service to plan a hiking trip. The duration estimation doesn’t take into account the altitude, making the service much less useful in a mountain environment.
I’ve programmed a simple python script to calculate a better timing estimation from the ORS JSON output, based on the equation proposed by “Suisse Rando Calcul du temps de marche, version 2020.1”, and gives much better results. It would be really good to integrate something similar in the time estimation of the web service, making it a wonderful mountain hiking tool!

Hello,

thank you for this suggestion! We have created a new issue about this on github: Provide more realistic hiking times in mountain areas · Issue #1121 · GIScience/openrouteservice · GitHub.

Would you also be willing to provide your python script?

Best regards,

I am not familiar with “Suisse Rando Calcul du temps de marche, version 2020.1”, but there is also Naismith’s rule for determining hiking times in mountainous terrain. I use that one for my own hiking most of the time…

I would be glad to share the file. What is the best way to attach it?

Hi @Peeno,
you can either copy the contents here (for a code block use ``` at start and end).
Or send it directly to Sascha.Fendrich[at]heigit.org.

Best regards

Here it is. It loads a kml file containing a track exported from openrouteservice and extracts path length, altitude gain/loss, and estimated time.
The name of the kml file is fixed in the code (Lerinon.kml), so you need to change it to whatever is the filename you wish to analyze.

import xml.etree.ElementTree as ET
import math


# Coefficients for the hiking speed calculation
coefs = [14.271,0.36992,0.025922,-0.0014384,0.000032105,
         8.1542E-06,-9.0261E-08,-2.0757E-08,1.0192E-10,
         2.8588E-11,-5.7466E-14,-2.1842E-14,1.5176E-17,
         8.6894E-18,-1.3584E-21,-1.4026E-21]


# Function to read the kml (which is an xml) and extract the namespace. Found on the web
def parse_and_get_ns(file):
    events = "start", "start-ns"
    root = None
    ns = {}
    for event, elem in ET.iterparse(file, events):
        if event == "start-ns":
            if elem[0] in ns and ns[elem[0]] != elem[1]:
                # NOTE: It is perfectly valid to have the same prefix refer
                #     to different URI namespaces in different parts of the
                #     document. This exception serves as a reminder that this
                #     solution is not robust.    Use at your own peril.
                raise KeyError("Duplicate prefix with different URI found.")
            ns[elem[0]] = "%s" % elem[1]
        elif event == "start":
            if root is None:
                root = elem
    return ET.ElementTree(root), ns

    
#Calculate the time required for a constant slope trait
def calcTimeOneStep(dist,slope):
    # The equation makes no sense for steepness above 40%
    if slope > 40:
        localSlope = 40
    elif slope < -40:
        localSlope = -40
    else:
        localSlope = slope

    minPerKm = coefs[0]
    for exp,coef in enumerate(coefs[1::]):
        minPerKm = minPerKm + coef * localSlope**(exp+1)

    # For steepness above 40%, keep the same vertical speed as for 40% slopes
    if slope > 40:
        return dist * minPerKm / 1000 * slope/40
    elif slope < -40:
        return dist * minPerKm / 1000 * slope/-40        
    else:
        return dist * minPerKm / 1000
    

# Read the track from the XML, extracting the coordinates of the points    
def getCoordinates(a):
    i=a.find(".//LineString/coordinates",ns)
    coordsTxt=i.text.split()
    coords = []
    for j in coordsTxt:        
        coords.append([float(item) for item in j.split(',')])
    return coords


# Calculate the hiking time. The input is an array of coordinates and altitudes
def calcTime(a):

    distances = []

    totLen = 0
    flatLen = 0
    dPlus = 0
    dMin = 0
    totTime = 0
    for index,elem in enumerate(a[0:-1]):
        #Calculate longitude and latitude difference for each step
        dlon = elem[0]-a[index+1][0]
        dlat = elem[1]-a[index+1][1]        
        dh = -elem[2]+a[index+1][2]

        #Calculate the length of the step in m
        m_per_deg_lat = 111132.954 - 559.822 * math.cos( math.radians(2 * elem[1]) ) + 1.175 * math.cos( math.radians(4 * elem[1]));
        m_per_deg_lon = 111132.954 * math.cos ( math.radians(elem[1]) );
        dx = dlat * m_per_deg_lat
        dy = dlon * m_per_deg_lon
        dl = math.sqrt(dx*dx+dy*dy)

        #Calculate the slope
        slope = (dh/dl)*100

        #Accumulate the total length
        totLen = totLen + dl

        #Accumulate the length of almost flat tracks. Just for information.
        if slope < 5 and slope > -5:
            flatLen = flatLen + dl
        
        #Calculate the time required for the step
        time = calcTimeOneStep(dl,slope)

        #Accumulate the total time and the elevation gain/loss
        totTime = totTime + time
        if(dh > 0):
            dPlus = dPlus + dh
        else:
            dMin = dMin - dh
            
        #Append the information for the step. Just for debug
        distances.append([dl,dh,slope,time])

    #print(distances)

    print("Path length: {0:.2f}km\nLength of almost flat tracks:{4:.2f}km\nTotal ascent: {1:.0f}m\nTotal descent: {2:.0f}m\nEstimated hiking time:{3:.1f}h".format(totLen/1000,dPlus,dMin,totTime/60,flatLen/1000))
    return totTime





#Replace the name of the kml file with the one containing the path to be analyzed
tree,ns = parse_and_get_ns('Lerinon.kml')

ET.register_namespace('',ns[''])
ns = {'':"http://www.opengis.net/kml/2.2"}
root = tree.getroot()

coords = getCoordinates(root)

totTime = calcTime(coords)

Sightly improved using just a fifth order polynomial, gives the same results with less than 2% difference:

import xml.etree.ElementTree as ET
import math


# Coefficients for the hiking speed calculation
#coefs = [14.271,0.36992,0.025922,-0.0014384,0.000032105,
#         8.1542E-06,-9.0261E-08,-2.0757E-08,1.0192E-10,
#         2.8588E-11,-5.7466E-14,-2.1842E-14,1.5176E-17,
#         8.6894E-18,-1.3584E-21,-1.4026E-21]

maxSlope = 40
minSlope = -30

coefs = [14.14221824,0.293799084,0.031619121,3.88741E-05,-5.70469E-06,3.37396E-08]

# Function to read the kml (which is an xml) and extract the namespace. Found on the web
def parse_and_get_ns(file):
    events = "start", "start-ns"
    root = None
    ns = {}
    for event, elem in ET.iterparse(file, events):
        if event == "start-ns":
            if elem[0] in ns and ns[elem[0]] != elem[1]:
                # NOTE: It is perfectly valid to have the same prefix refer
                #     to different URI namespaces in different parts of the
                #     document. This exception serves as a reminder that this
                #     solution is not robust.    Use at your own peril.
                raise KeyError("Duplicate prefix with different URI found.")
            ns[elem[0]] = "%s" % elem[1]
        elif event == "start":
            if root is None:
                root = elem
    return ET.ElementTree(root), ns

    
#Calculate the time required for a constant slope trait
def calcTimeOneStep(dist,slope):
    # The equation makes no sense for steepness above 40%
    if slope > maxSlope:
        localSlope = maxSlope
    elif slope < minSlope:
        localSlope = minSlope
    else:
        localSlope = slope

    minPerKm = coefs[0]
    for exp,coef in enumerate(coefs[1::]):
        minPerKm = minPerKm + coef * localSlope**(exp+1)

    # For steepness above 40%, keep the same vertical speed as for 40% slopes
    if slope > maxSlope:
        return dist * minPerKm / 1000 * slope/maxSlope
    elif slope < minSlope:
        return dist * minPerKm / 1000 * slope/minSlope   
    else:
        return dist * minPerKm / 1000
    

# Read the track from the XML, extracting the coordinates of the points    
def getCoordinates(a):
    i=a.find(".//LineString/coordinates",ns)
    coordsTxt=i.text.split()
    coords = []
    for j in coordsTxt:        
        coords.append([float(item) for item in j.split(',')])
    return coords


# Calculate the hiking time. The input is an array of coordinates and altitudes
def calcTime(a):

    distances = []

    totLen = 0
    flatLen = 0
    dPlus = 0
    dMin = 0
    totTime = 0
    for index,elem in enumerate(a[0:-1]):
        #Calculate longitude and latitude difference for each step
        dlon = elem[0]-a[index+1][0]
        dlat = elem[1]-a[index+1][1]        
        dh = -elem[2]+a[index+1][2]

        #Calculate the length of the step in m
        m_per_deg_lat = 111132.954 - 559.822 * math.cos( math.radians(2 * elem[1]) ) + 1.175 * math.cos( math.radians(4 * elem[1]));
        m_per_deg_lon = 111132.954 * math.cos ( math.radians(elem[1]) );
        dx = dlat * m_per_deg_lat
        dy = dlon * m_per_deg_lon
        dl = math.sqrt(dx*dx+dy*dy)

        #Calculate the slope
        slope = (dh/dl)*100

        #Accumulate the total length
        totLen = totLen + dl

        #Accumulate the length of almost flat tracks. Just for information.
        if slope < 5 and slope > -5:
            flatLen = flatLen + dl
        
        #Calculate the time required for the step
        time = calcTimeOneStep(dl,slope)

        #Accumulate the total time and the elevation gain/loss
        totTime = totTime + time
        if(dh > 0):
            dPlus = dPlus + dh
        else:
            dMin = dMin - dh
            
        #Append the information for the step. Just for debug
        distances.append([dl,dh,slope,time])

    #print(distances)

    print("Path length: {0:.2f}km\nLength of almost flat tracks:{4:.2f}km\nTotal ascent: {1:.0f}m\nTotal descent: {2:.0f}m\nEstimated hiking time:{3:.3f}h".format(totLen/1000,dPlus,dMin,totTime/60,flatLen/1000))
    return totTime





#Replace the name of the kml file with the one containing the path to be analyzed
tree,ns = parse_and_get_ns('SecondoGiorno.kml')

ET.register_namespace('',ns[''])
ns = {'':"http://www.opengis.net/kml/2.2"}
root = tree.getroot()

coords = getCoordinates(root)

totTime = calcTime(coords)