abrensch / brouter

configurable OSM offline router with elevation awareness, Java + Android
MIT License
490 stars 118 forks source link

travel time calculation #6

Closed abrensch closed 5 years ago

abrensch commented 10 years ago

BRouter does not (yet) calculate travel times.

When used with OsmAnd, OsmAnds implicit "20km/h" for bikes is fine for me, but for car-routes, the implicit 60km/h is just a course guess and hust wrong for motorway trips.

Since BRouter's cost funtion is not based on speed, travel time calculations would require to create twins for the relevant terms in the routing profiles (costfactor, initialcost, turncost, ...) that deliver the estimated contribution to the travel time.

Simplest way to achieve that is to use separate files that are referenced from the profile-files that are processed by the same engine but deliver times in seconds instaed of costs and speeds instaed of costfactors.

abrensch commented 10 years ago

-> enhancement

cpesch commented 10 years ago

Hi Arndt, did you make any progress on this issue?

abrensch commented 10 years ago

No progress and not currently on my agenda.So looking for contributors...

poutnikl commented 9 years ago

Until anything better, Brouter could provide ( if interface allows it ) to Nav application the ETA value = Now + distance / ETA_flatspeed + Filtered_ascend / ETA_climbspeed. ( Like this online bike route planner does http://bikeroutetoaster.com/BRTWebUI/ )

ETA_flatspeed + ETA_climbspeed could be optional global profile parameters with implicit default values e.g. 20 km/h and 500 m / h.

Not perfect, but no ETA can be perfect.

tomdereub commented 6 years ago

I don't know if this subject is still topical, but I use this simple calculation to estimate my travel times, and when I'm alone with always the same bike it's surprisingly precise. I've just added "Plain_ascent/ETA_climbspeed", where Plain_ascent is positive or negative. To do things right, there should be 2 different values for ETA: City or road. For me, it's like 26 km/h on road, and 20 km/h in cities, including traffic, lights etc...

poutnikl commented 6 years ago

It is not easy for the BRouter to distinguish city or road scenarios. And it may be not wise to overcomplicate the rough estimation. The road/city scenario, physical health, the route length, the "hillyness" and mood, all affect the ETA.

I often use as 1-value-fits-all the value 15 km/h for medium-long bike travelling with baggage(*). It is statistically well justified by my own real cases, including short/medium breaks for rest, sightseeing, refreshment, meals or technical reasons.

The same hard value 15 km/h is used by some reasonable routers like was e.g. GPSmid(**). 20+ km/h is rather for short or sport cases with no breaks. ETA should be IMHO rather about estimation when you get there, and not when you get there by this current ( or short term average ) speed.

(*) medium-long distance 60-160 km routes with "hillyness" 600-1200m/100 km of filtered ascend. (**) a java based OSM navigation I was used to on my Nokia/Symbian.

tomdereub commented 6 years ago

Ok for the road/city scenario, that's right that it's too complicated. And if you give the possibility to change the ETA_flatspeed parameter, you can manually change it when you're in city or road. But on road for medium-long bike travelling, I find it really relevant to have this calculation with flatspeed and climbspeed. Of course physical health, mood, etc... will change the values, but it's up to the biker to change this values if necessary. And I don't think it will overcomplicate, if you have default values for people who doesn't really care for this estimation.

poutnikl commented 6 years ago

Sure, both ways, with flat + climb speed or without them, there are 2 of possible approaches. But that is all on @abrensch .

abrensch commented 6 years ago

1.4.9 now supports different "cost-models", with a new "kinematic-model" used for car routing.

This kinematic model does support travel time. However, that's not suited for bike routing. So volunteers welcome to proceed on that issue.

Phyks commented 6 years ago

I'm having a similar issue (and currently trying to look into it). Given a route computed by brouter, I'd like to compute an estimated travel time. For hiking, I know there are some functions which can be used to compute a good approximation given the elevations, such as https://en.wikipedia.org/wiki/Tobler%27s_hiking_function. Do you know anything similar which could be used for bikes?

tomdereub commented 6 years ago

I've recently found this that could interess you : http://www.velomath.fr/simulation.php Once you've found your parameters (Cx, f and power), what I did with a few tracks of different trips, its quite precise. You just export the gpx from brouter and import it on the site.

Phyks commented 6 years ago

@tomdereub oh, that's really nice! Actually, in the mean time I also came across https://linuxfr.org/users/jben/journaux/rv-un-moteur-de-recherche-d-itineraire-velo-en-utilisant-les-donnees-d-osm which seems to be using the same kind of model, except this one is doing the routing as well. Routing is not as good as what brouter offers and the software is mostly unmaintained but it might be worth extracting the time computation code to run it on brouter GPX output. Then, this would only need finding a rather generic set of coefficients (like one for "average", one for "sporty" and one for "slow" cyclists for instance, this could even be made part of brouter profile files).

tomdereub commented 6 years ago

Yes, I knew rv too. The difference I see is the speed limit in curve, that velomath is taking into account and not rv.

abrensch commented 6 years ago

So maybe the "kinematic model" I use for the car-eco profile could be ported to bikes? Because that does things like curve-speed-limit, but that's just some hard-coded heuristics adapted to cars. And I guess the model of velomath assumes constant power? Because for a "bike-eco" I would also assume low power for downhill sections, in order to get some sort of time/energy balance.

I recently made some changes to get the time estimates fom brouters kinematic model into Locus, however, there's not yet a release containing that patch.

poutnikl commented 6 years ago

Some analysis and notes from myself as 2 cents contribution:

The force needed by a bike:

F = F_roll + F_climb + F_drag

F_roll = m . g . C_roll (*), m is total bike+person mass g is gravitational acceleration constant cca 9.8 m/s/s

C_roll is typically 0.005 for trekking bike and good asphalt.

(*) More exactly, should be multiplied by cos(alfa), but for such cases where it matters,, the rolling term is negligible to the climbing term.

F_ climb = m . g . sin(Alfa), Alfa is a climbing angle. Obviously, for descents, the climbing term is negative and is subtracted from needed power.

F_drag = 0.5 . c_z . air density . crossection_area. v^2 = C_drag . v^2 c_z is aerodynamic drag coefficient, v is speed

So F = m . g . C_roll + m . g . sin(Alfa) + C_drag . v^2

As power P = F . v, P = [ m .g . ( C_roll + sin(Alfa))] . v + C_drag . v^3

C_drag = [ P - [ m .g . ( C_roll + sin(Alfa))] . v ] / v^3

Typical human long term sustained power output P_sust = 150W.

Let suppose this power is needed for the sustained flat speed v_flat 25 Km/h, i.e 6.94 m/s, for the consistence of units.

Let suppose 100kg total mass for a biker with a bike and luggage.

If we put the above values to the equation, we get

C_drag = [150 - 100 . 9.8 . 0.005 . 6.94] /6.94^3

C_drag = [150 - 4.9 . 6.94] /6.94^3 C_drag = 0.347

So we have all the below parameters set

P_sust = [ m .g . ( C_roll + sin(Alfa))] . v + C_drag . v^3

resp. simplified

P_sust = C1(Alfa) . v + C_drag . v^3

From this we can get simplified approximation v=f(alfa,power).

Any difference between needed force and provided force (P/v) Is source of acceleration/deceleration.

In my simulations in Excel file, I have realized the default downhill cutoff slope 1.5% needs zero power for high travel speed 17 km/h, 66W for the nominal 25 km/h, nominal power 150W for 30 km/h.

Spending biker energy on speed > 25 km/h becomes energy wasting for considered bike eco mode. 10% higher speed means 30% higher power spent on the air drag.

I have considered in my simulations for descents progressive ceasing of biker power from nominal sustained power 150W at nominal flat speed 25 km/h to 0W at 35 km/h.

The biker power is limited and trekking is done also on not paved surfaces. Therefore I consider as important for bike kinematic model to expose C_roll parameter in the way context of the profile to be computed there e.g from tags smoothness, surface, highway.

As it can be 0.005 for nominal fine asphalt, or (I guess) easily 0.1 for rough gravel tracks or rooty wood paths.

poutnikl commented 6 years ago

i think the bike curve speed limit is seldom an issue but at high speed, and has minimal impact on ETA.

OTOH subjective rationale speed limit plays a significant role for narrow tracks/paths a/o for difficult surfaces.

E.g. nobody will make me to ride 40+ km/h downhill on loose gravel track even without a curve, worse if turning.

P.S.: note that for very long distance trips, sustained power will be lower, 75-100W, so does sustained flat speed.

Phyks commented 5 years ago

I have some JS code which could be usable for brouter-web to compute ETA with bike profiles. Thanks @poutnikl for the detailed notes on your experimentations!

var route = {…}  // Response from BRouter-server API

const EARTH_RADIUS = 6378137; // in meters
// Gravitational constant, g
const GRAVITY = 9.81;  // in meters per second^(-2)
// Total mass of bike + biker + luggages, m
const MASS = 90;  // in kilograms
// Speed when we start braking and never go beyond
const MAX_SPEED = 45 * 3600 / 1000;  // in meters per second
// Equivalent surface for wind, S * C_x, F = -1/2 * S * C_x * v^2 = - S_C_x * v^2
const S_C_x = 0.5 * 0.45;  // in meters^2
// The resistance of the road, F = - m * g * C_r
const C_r = 0.01
// Biker power
const BIKER_POWER = 100;  // in W

function distance(latLng1, latLng2) {
    const lat1 = (latLng1[0] * Math.PI) / 180;
    const lng1 = (latLng1[1] * Math.PI) / 180;

    const lat2 = (latLng2[0] * Math.PI) / 180;
    const lng2 = (latLng2[1] * Math.PI) / 180;

    const a = (
        (Math.sin((lat2 - lat1) / 2.0) ** 2)
        + (Math.cos(lat1) * Math.cos(lat2) * (Math.sin((lng2 - lng1) / 2.0) ** 2))
    );
    const c = 2.0 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

    return EARTH_RADIUS * c;
}

var properties = route.features[0].properties;
var geometry = route.features[0].geometry;

segments = [];
let currentSegmentPropertiesIndex = 1;  // Skip the headers line
for (let i = 1; i < geometry.coordinates.length; i++) {
  let currentCoordinates = geometry.coordinates[i];

  let beginningElevation = geometry.coordinates[i - 1][2];
  let endElevation = currentCoordinates[2];

  let tags = {
    'way': properties.messages[currentSegmentPropertiesIndex][9],
    'node': properties.messages[currentSegmentPropertiesIndex][10]
  };
  let deltaElevation = endElevation - beginningElevation;
  let length = distance(
    [geometry.coordinates[i - 1][1], geometry.coordinates[i - 1][0]],
    [currentCoordinates[1], currentCoordinates[0]]
  );

  let alpha = Math.atan2(deltaElevation, length);
  let speed;
  if (tags['way'].search('bicycle=dismount') !== -1) {
    // Use Tobler's hiking function for walking sections
    speed = 6 * Math.exp(-3.5 * Math.abs(deltaElevation / length + 0.05)) * 1000 / 3600;
  } else {
    var cubic = new algebra.Equation(
      algebra.parse(`${BIKER_POWER} - (${MASS} * ${GRAVITY} * (${C_r} + (${Math.sin(alpha)}))) * v - ${S_C_x} * v^3`),
      0
    );
    speed = cubic.solveFor("v").filter((item) => item > 0)[0];
  }
  let duration = length / Math.min(speed, MAX_SPEED);

  segments.push({
    deltaElevation,
    duration,
    length,
    tags,
  });

  let currentSegmentLongitude = properties.messages[currentSegmentPropertiesIndex][0];
  let currentSegmentLatitude = properties.messages[currentSegmentPropertiesIndex][1];
  if (
    Math.abs(currentCoordinates[0] * 1000000 - Number.parseFloat(currentSegmentLongitude)) < Number.EPSILON
    && Math.abs(currentCoordinates[1] * 1000000 - Number.parseFloat(currentSegmentLatitude)) < Number.EPSILON
  ) {
    currentSegmentPropertiesIndex += 1;
  }
}

let totalDuration = 0;
segments.forEach((segment) => {
  totalDuration += segment.duration;
});

console.log(`${Math.round(totalDuration / 60, 2)} minutes`);

A demo CodePen is available at https://codepen.io/anon/pen/QJvjdN?editors=1010. cc @nrenner, not sure whether this might be worth pushing into brouter-web.

Note that I have an offset of about 150m on the total track length (on a total of almost 9000 meters) between my computed track length and the one sent by BRouter. I'm not sure where it comes from :/

Changing C_x based on surface type or putting extra penalty for traffic signals should be doable quite easily from this base code.

EDIT I've just came across https://github.com/abrensch/brouter/issues/3 and apparently the difference in total length is not really an issue, it is already a known issue in BRouter and is likely to come from here then.

nrenner commented 5 years ago

not sure whether this might be worth pushing into brouter-web

I guess it would make sense to also have this in the routing engine – someone to convert it to Java and integrate? But if that doesn't happen, I'm fine with a pull request until then.

Phyks commented 5 years ago

I gave a try at a first PR here. The code does not work at the moment, I'm not exactly sure why. I'll have a deeper look at it.

If anyone is willing to help on this, feel free to comment on the PR and branch off it :)

EDIT: It's working now :) (except the bike=dismount tag which I don't know how to handle in Java code… :/).