From c3bbee7724b94ffe9cd6849f05bdf4bfb5a8a442 Mon Sep 17 00:00:00 2001 From: Felix Boewing Date: Wed, 21 Oct 2020 19:19:18 +0200 Subject: [PATCH] add elevation aware profile --- docs/profiles.md | 14 +- .../elevation_aware_bicycle.lua | 750 ++++++++++++++++++ .../generate_rastersource.py | 9 + .../bike_formula.py | 46 ++ .../bike_slope.png | Bin 0 -> 32197 bytes 5 files changed, 818 insertions(+), 1 deletion(-) create mode 100644 profiles/examples/elevation_aware_bicycle/elevation_aware_bicycle.lua create mode 100644 profiles/examples/elevation_aware_bicycle/generate_rastersource.py create mode 100644 profiles/examples/elevation_aware_bicycle/speed_by_elevation_of_bicycle/bike_formula.py create mode 100644 profiles/examples/elevation_aware_bicycle/speed_by_elevation_of_bicycle/bike_slope.png diff --git a/docs/profiles.md b/docs/profiles.md index fa74b8780..7d548877b 100644 --- a/docs/profiles.md +++ b/docs/profiles.md @@ -337,7 +337,7 @@ function setup() end ``` -The input data must an ASCII file with rows of integers. e.g.: +The input data must be an ASCII file with rows of integers. e.g.: ``` 0 0 0 0 @@ -367,6 +367,18 @@ end See [rasterbot.lua](../profiles/rasterbot.lua) and [rasterbotinterp.lua](../profiles/rasterbotinterp.lua) for examples. +#### Using elevation data +If you want to make your routing-machine elevation aware, you can use the profile +[elevation_aware_bicycle](../profiles/examples/elevation_aware_bicycle). +The following preprocessing steps are necessary to use this profile: + +1. Download the elevation data as ASCII tiles e.g. from [SRTM](http://srtm.csi.cgiar.org/srtmdata/) +1. Adapt and run [generate_rastersource.py](../profiles/examples/elevation_aware_bicycle) to fuse the raw tiles and obtain `rastersource.asc`. +1. Set the boundaries of your rastersource in `raster:load()` in [elevation_aware_bicycle.lua](../profiles/examples/elevation_aware_bicycle/elevation_aware_bicycle.lua) +1. [Quick start](https://github.com/Project-OSRM/osrm-backend/wiki/Running-OSRM#quickstart) OSRM, +but use [elevation_aware_bicycle.lua](../profiles/examples/elevation_aware_bicycle/elevation_aware_bicycle.lua) +and `rastersource.asc` instead of `car.lua`. + ### Helper functions There are a few helper functions defined in the global scope that profiles can use: diff --git a/profiles/examples/elevation_aware_bicycle/elevation_aware_bicycle.lua b/profiles/examples/elevation_aware_bicycle/elevation_aware_bicycle.lua new file mode 100644 index 000000000..10730e2ed --- /dev/null +++ b/profiles/examples/elevation_aware_bicycle/elevation_aware_bicycle.lua @@ -0,0 +1,750 @@ +-- Bicycle profile +-- +-- Takes elevation into account. +-- + +api_version = 4 + +Set = require('lib/set') +Sequence = require('lib/sequence') +Handlers = require("lib/way_handlers") +find_access_tag = require("lib/access").find_access_tag +limit = require("lib/maxspeed").limit + +list = {0.4040111813705065,0.4077509707194649,0.4115930094776843,0.41554190707638683,0.419602561205936,0.42378018077868934,0.4280803111034602,0.43250886151983375,0.43707213577212073,0.4417768654386842,0.44663024677340407,0.4516399813629164,0.45681432105685077,0.46216211768961296,0.46769287818247157,0.47341682569514565,0.4793449675882814,0.4854891710638476,0.4918622474715554,0.49847804640803445,0.5053515608941388,0.5124990450969183,0.5199381462693039,0.5276880528151419,0.5357696606525774,0.5442057603471956,0.5530212478192406,0.5622433617966259,0.5719019515848806,0.5820297791502957,0.5926628599510663,0.6038408473818764,0.6156074660856684,0.6280109996784702,0.6411048385477748,0.6549480932017125,0.6696062779906035,0.6851520686459992,0.7016661346382446,0.7192380433680241,0.7379672270496413,0.7579639940024726,0.7793505529460762,0.8022620006557876,0.826847198831215,0.8532694343893874,0.8817067185549876,0.9123515356952666,0.9454098073807813,0.9810987996660977,1.0196436869904062,1.0612725155267435,1.1062094072354913,1.154666035145556,1.206831686614459,1.262862588811012,1.3228715297368607,1.386919058946579,1.4550075750313445,1.5270793258312663,1.603018782583213,1.6826591333307126,1.7657919757398155,1.8521788610886971,1.9415632418225015,2.0336815766458516,2.1282727347390007,2.2250852757628166,2.3238825566437655,2.4244458751913984,2.526575998468329,2.6300934640510762,2.734838018511459,2.8406675004812123,2.9474564080893972,3.05509432606533,3.1634843330276547,3.2725414664448174,3.382191290874559,3.492368592633609,3.6030162089714017,3.7140839902231964,3.825527887713802,3.937309157144547,4.049393665912615,4.1617512926354046,4.274355407635642,4.387182423985552,4.50021140971591,4.613423752846485,4.726802871914744,4.840333965631132,4.954003796150395,5.0678005012152,5.181713431100397,5.295733006870892,5.409850596970382,5.524058409591401,5.638349398647695,5.7527171814864015} +N = 100 +min_slope = -0.25 +max_slope = 0.25 + +function setup() + + local default_speed = 15 + local walking_speed = 4 + + local raster_path = os.getenv('OSRM_RASTER_SOURCE') or "rastersource.asc" + + return { + properties = { + force_split_edges = true, + u_turn_penalty = 20, + traffic_light_penalty = 2, + --weight_name = 'cyclability', + weight_name = 'duration', + process_call_tagless_node = false, + max_speed_for_map_matching = 110/3.6, -- kmph -> m/s + use_turn_restrictions = false, + continue_straight_at_waypoint = false, + mode_change_penalty = 30, + }, + + raster_source = raster:load( + raster_path, + 5, -- lon_min + 15, -- lon_max + 45, -- lat_min + 50, -- lat_max + 6000, -- nrows + 12000 -- ncols + ), + + default_mode = mode.cycling, + default_speed = default_speed, + walking_speed = walking_speed, + oneway_handling = true, + turn_penalty = 6, + turn_bias = 1.4, + use_public_transport = true, + + allowed_start_modes = Set { + mode.cycling, + mode.pushing_bike + }, + + barrier_blacklist = Set { + 'yes', + 'wall', + 'fence' + }, + + access_tag_whitelist = Set { + 'yes', + 'permissive', + 'designated' + }, + + access_tag_blacklist = Set { + 'no', + 'private', + 'agricultural', + 'forestry', + 'delivery' + }, + + restricted_access_tag_list = Set { }, + + restricted_highway_whitelist = Set { }, + + -- tags disallow access to in combination with highway=service + service_access_tag_blacklist = Set { }, + + construction_whitelist = Set { + 'no', + 'widening', + 'minor', + }, + + access_tags_hierarchy = Sequence { + 'bicycle', + 'vehicle', + 'access' + }, + + restrictions = Set { + 'bicycle' + }, + + cycleway_tags = Set { + 'track', + 'lane', + 'share_busway', + 'sharrow', + 'shared', + 'shared_lane' + }, + + opposite_cycleway_tags = Set { + 'opposite', + 'opposite_lane', + 'opposite_track', + }, + + -- reduce the driving speed by 30% for unsafe roads + -- only used for cyclability metric + unsafe_highway_list = { + primary = 0.5, + secondary = 0.65, + tertiary = 0.8, + primary_link = 0.5, + secondary_link = 0.65, + tertiary_link = 0.8, + }, + + service_penalties = { + alley = 0.5, + }, + + bicycle_speeds = { + cycleway = default_speed, + primary = default_speed, + primary_link = default_speed, + secondary = default_speed, + secondary_link = default_speed, + tertiary = default_speed, + tertiary_link = default_speed, + residential = default_speed, + unclassified = default_speed, + living_street = default_speed, + road = default_speed, + service = default_speed, + track = 12, + path = 12 + }, + + pedestrian_speeds = { + footway = walking_speed, + pedestrian = walking_speed, + steps = 2 + }, + + railway_speeds = { + train = 10, + railway = 10, + subway = 10, + light_rail = 10, + monorail = 10, + tram = 10 + }, + + platform_speeds = { + platform = walking_speed + }, + + amenity_speeds = { + parking = 10, + parking_entrance = 10 + }, + + man_made_speeds = { + pier = walking_speed + }, + + route_speeds = { + ferry = 5 + }, + + bridge_speeds = { + movable = 5 + }, + + surface_speeds = { + asphalt = default_speed, + ["cobblestone:flattened"] = 10, + paving_stones = 10, + compacted = 10, + cobblestone = 6, + unpaved = 6, + fine_gravel = 6, + gravel = 6, + pebblestone = 6, + ground = 6, + dirt = 6, + earth = 6, + grass = 6, + mud = 3, + sand = 3, + sett = 10 + }, + + classes = Sequence { + 'ferry', 'tunnel' + }, + + -- Which classes should be excludable + -- This increases memory usage so its disabled by default. + excludable = Sequence { +-- Set {'ferry'} + }, + + tracktype_speeds = { + }, + + smoothness_speeds = { + }, + + avoid = Set { + 'impassable', + 'construction' + } + } +end + +function process_segment (profile, segment) + local sourceData = raster:interpolate(profile.raster_source, segment.source.lon, segment.source.lat) + local targetData = raster:interpolate(profile.raster_source, segment.target.lon, segment.target.lat) + + local invalid = sourceData.invalid_data() + local scaled_weight = segment.weight + local scaled_duration = segment.duration + --io.write("evaluating segment: " .. sourceData.datum .. " m to " .. targetData.datum .. " m with distance " .. segment.distance .. "\n") + + if sourceData.datum ~= invalid and targetData.datum ~= invalid and segment.distance > 0 then + local slope = (targetData.datum - sourceData.datum) / segment.distance + local slope_idx = math.floor((slope - min_slope) / (max_slope - min_slope) * N) + if slope_idx < 1 then + slope_idx = 1 + end + if slope_idx > N then + slope_idx = N + end + + scaled_weight = list[slope_idx] * scaled_weight + scaled_duration = list[slope_idx] * scaled_duration + + --original calculation: is undefined for slope = 0.2 + --scaled_weight = scaled_weight / (1.0 - (slope * 5.0)) + --scaled_duration = scaled_duration / (1.0 - (slope * 5.0)) + --io.write(" slope: " .. slope .. "\n") + --io.write(" was weight: " .. segment.weight .. "\n") + --io.write(" new weight: " .. scaled_weight .. "\n") + --io.write(" was duration: " .. segment.duration .. "\n") + --io.write(" new duration: " .. scaled_duration .. "\n") + end + + segment.weight = scaled_weight + segment.duration = scaled_duration +end + +local function parse_maxspeed(source) + if not source then + return 0 + end + local n = tonumber(source:match("%d*")) + if not n then + n = 0 + end + if string.match(source, "mph") or string.match(source, "mp/h") then + n = (n*1609)/1000 + end + return n +end + +function process_node(profile, node, result) + -- io.write("process_node\n") + -- parse access and barrier tags + local highway = node:get_value_by_key("highway") + local is_crossing = highway and highway == "crossing" + + local access = find_access_tag(node, profile.access_tags_hierarchy) + if access and access ~= "" then + -- access restrictions on crossing nodes are not relevant for + -- the traffic on the road + if profile.access_tag_blacklist[access] and not is_crossing then + result.barrier = true + end + else + local barrier = node:get_value_by_key("barrier") + if barrier and "" ~= barrier then + if profile.barrier_blacklist[barrier] then + result.barrier = true + end + end + end + + -- check if node is a traffic light + local tag = node:get_value_by_key("highway") + if tag and "traffic_signals" == tag then + result.traffic_lights = true + end +end + +function handle_bicycle_tags(profile,way,result,data) + -- initial routability check, filters out buildings, boundaries, etc + data.route = way:get_value_by_key("route") + data.man_made = way:get_value_by_key("man_made") + data.railway = way:get_value_by_key("railway") + data.amenity = way:get_value_by_key("amenity") + data.public_transport = way:get_value_by_key("public_transport") + data.bridge = way:get_value_by_key("bridge") + + if (not data.highway or data.highway == '') and + (not data.route or data.route == '') and + (not profile.use_public_transport or not data.railway or data.railway=='') and + (not data.amenity or data.amenity=='') and + (not data.man_made or data.man_made=='') and + (not data.public_transport or data.public_transport=='') and + (not data.bridge or data.bridge=='') + then + return false + end + + -- access + data.access = find_access_tag(way, profile.access_tags_hierarchy) + if data.access and profile.access_tag_blacklist[data.access] then + return false + end + + -- other tags + data.junction = way:get_value_by_key("junction") + data.maxspeed = parse_maxspeed(way:get_value_by_key ( "maxspeed") ) + data.maxspeed_forward = parse_maxspeed(way:get_value_by_key( "maxspeed:forward")) + data.maxspeed_backward = parse_maxspeed(way:get_value_by_key( "maxspeed:backward")) + data.barrier = way:get_value_by_key("barrier") + data.oneway = way:get_value_by_key("oneway") + data.oneway_bicycle = way:get_value_by_key("oneway:bicycle") + data.cycleway = way:get_value_by_key("cycleway") + data.cycleway_left = way:get_value_by_key("cycleway:left") + data.cycleway_right = way:get_value_by_key("cycleway:right") + data.duration = way:get_value_by_key("duration") + data.service = way:get_value_by_key("service") + data.foot = way:get_value_by_key("foot") + data.foot_forward = way:get_value_by_key("foot:forward") + data.foot_backward = way:get_value_by_key("foot:backward") + data.bicycle = way:get_value_by_key("bicycle") + + speed_handler(profile,way,result,data) + + oneway_handler(profile,way,result,data) + + cycleway_handler(profile,way,result,data) + + bike_push_handler(profile,way,result,data) + + + -- maxspeed + limit( result, data.maxspeed, data.maxspeed_forward, data.maxspeed_backward ) + + -- not routable if no speed assigned + -- this avoid assertions in debug builds + if result.forward_speed <= 0 and result.duration <= 0 then + result.forward_mode = mode.inaccessible + end + if result.backward_speed <= 0 and result.duration <= 0 then + result.backward_mode = mode.inaccessible + end + + safety_handler(profile,way,result,data) +end + +function speed_handler(profile,way,result,data) + + data.way_type_allows_pushing = false + + -- speed + local bridge_speed = profile.bridge_speeds[data.bridge] + if (bridge_speed and bridge_speed > 0) then + data.highway = data.bridge + if data.duration and durationIsValid(data.duration) then + result.duration = math.max( parseDuration(data.duration), 1 ) + end + result.forward_speed = bridge_speed + result.backward_speed = bridge_speed + data.way_type_allows_pushing = true + elseif profile.route_speeds[data.route] then + -- ferries (doesn't cover routes tagged using relations) + result.forward_mode = mode.ferry + result.backward_mode = mode.ferry + if data.duration and durationIsValid(data.duration) then + result.duration = math.max( 1, parseDuration(data.duration) ) + else + result.forward_speed = profile.route_speeds[data.route] + result.backward_speed = profile.route_speeds[data.route] + end + -- railway platforms (old tagging scheme) + elseif data.railway and profile.platform_speeds[data.railway] then + result.forward_speed = profile.platform_speeds[data.railway] + result.backward_speed = profile.platform_speeds[data.railway] + data.way_type_allows_pushing = true + -- public_transport platforms (new tagging platform) + elseif data.public_transport and profile.platform_speeds[data.public_transport] then + result.forward_speed = profile.platform_speeds[data.public_transport] + result.backward_speed = profile.platform_speeds[data.public_transport] + data.way_type_allows_pushing = true + -- railways + elseif profile.use_public_transport and data.railway and profile.railway_speeds[data.railway] and profile.access_tag_whitelist[data.access] then + result.forward_mode = mode.train + result.backward_mode = mode.train + result.forward_speed = profile.railway_speeds[data.railway] + result.backward_speed = profile.railway_speeds[data.railway] + elseif data.amenity and profile.amenity_speeds[data.amenity] then + -- parking areas + result.forward_speed = profile.amenity_speeds[data.amenity] + result.backward_speed = profile.amenity_speeds[data.amenity] + data.way_type_allows_pushing = true + elseif profile.bicycle_speeds[data.highway] then + -- regular ways + result.forward_speed = profile.bicycle_speeds[data.highway] + result.backward_speed = profile.bicycle_speeds[data.highway] + data.way_type_allows_pushing = true + elseif data.access and profile.access_tag_whitelist[data.access] then + -- unknown way, but valid access tag + result.forward_speed = profile.default_speed + result.backward_speed = profile.default_speed + data.way_type_allows_pushing = true + end +end + +function oneway_handler(profile,way,result,data) + -- oneway + data.implied_oneway = data.junction == "roundabout" or data.junction == "circular" or data.highway == "motorway" + data.reverse = false + + if data.oneway_bicycle == "yes" or data.oneway_bicycle == "1" or data.oneway_bicycle == "true" then + result.backward_mode = mode.inaccessible + elseif data.oneway_bicycle == "no" or data.oneway_bicycle == "0" or data.oneway_bicycle == "false" then + -- prevent other cases + elseif data.oneway_bicycle == "-1" then + result.forward_mode = mode.inaccessible + data.reverse = true + elseif data.oneway == "yes" or data.oneway == "1" or data.oneway == "true" then + result.backward_mode = mode.inaccessible + elseif data.oneway == "no" or data.oneway == "0" or data.oneway == "false" then + -- prevent other cases + elseif data.oneway == "-1" then + result.forward_mode = mode.inaccessible + data.reverse = true + elseif data.implied_oneway then + result.backward_mode = mode.inaccessible + end +end + +function cycleway_handler(profile,way,result,data) + -- cycleway + data.has_cycleway_forward = false + data.has_cycleway_backward = false + data.is_twoway = result.forward_mode ~= mode.inaccessible and result.backward_mode ~= mode.inaccessible and not data.implied_oneway + + -- cycleways on normal roads + if data.is_twoway then + if data.cycleway and profile.cycleway_tags[data.cycleway] then + data.has_cycleway_backward = true + data.has_cycleway_forward = true + end + if (data.cycleway_right and profile.cycleway_tags[data.cycleway_right]) or (data.cycleway_left and profile.opposite_cycleway_tags[data.cycleway_left]) then + data.has_cycleway_forward = true + end + if (data.cycleway_left and profile.cycleway_tags[data.cycleway_left]) or (data.cycleway_right and profile.opposite_cycleway_tags[data.cycleway_right]) then + data.has_cycleway_backward = true + end + else + local has_twoway_cycleway = (data.cycleway and profile.opposite_cycleway_tags[data.cycleway]) or (data.cycleway_right and profile.opposite_cycleway_tags[data.cycleway_right]) or (data.cycleway_left and profile.opposite_cycleway_tags[data.cycleway_left]) + local has_opposite_cycleway = (data.cycleway_left and profile.opposite_cycleway_tags[data.cycleway_left]) or (data.cycleway_right and profile.opposite_cycleway_tags[data.cycleway_right]) + local has_oneway_cycleway = (data.cycleway and profile.cycleway_tags[data.cycleway]) or (data.cycleway_right and profile.cycleway_tags[data.cycleway_right]) or (data.cycleway_left and profile.cycleway_tags[data.cycleway_left]) + + -- set cycleway even though it is an one-way if opposite is tagged + if has_twoway_cycleway then + data.has_cycleway_backward = true + data.has_cycleway_forward = true + elseif has_opposite_cycleway then + if not data.reverse then + data.has_cycleway_backward = true + else + data.has_cycleway_forward = true + end + elseif has_oneway_cycleway then + if not data.reverse then + data.has_cycleway_forward = true + else + data.has_cycleway_backward = true + end + + end + end + + if data.has_cycleway_backward then + result.backward_mode = mode.cycling + result.backward_speed = profile.bicycle_speeds["cycleway"] + end + + if data.has_cycleway_forward then + result.forward_mode = mode.cycling + result.forward_speed = profile.bicycle_speeds["cycleway"] + end +end + +function bike_push_handler(profile,way,result,data) + -- pushing bikes - if no other mode found + if result.forward_mode == mode.inaccessible or result.backward_mode == mode.inaccessible or + result.forward_speed == -1 or result.backward_speed == -1 then + if data.foot ~= 'no' then + local push_forward_speed = nil + local push_backward_speed = nil + + if profile.pedestrian_speeds[data.highway] then + push_forward_speed = profile.pedestrian_speeds[data.highway] + push_backward_speed = profile.pedestrian_speeds[data.highway] + elseif data.man_made and profile.man_made_speeds[data.man_made] then + push_forward_speed = profile.man_made_speeds[data.man_made] + push_backward_speed = profile.man_made_speeds[data.man_made] + else + if data.foot == 'yes' then + push_forward_speed = profile.walking_speed + if not data.implied_oneway then + push_backward_speed = profile.walking_speed + end + elseif data.foot_forward == 'yes' then + push_forward_speed = profile.walking_speed + elseif data.foot_backward == 'yes' then + push_backward_speed = profile.walking_speed + elseif data.way_type_allows_pushing then + push_forward_speed = profile.walking_speed + if not data.implied_oneway then + push_backward_speed = profile.walking_speed + end + end + end + + if push_forward_speed and (result.forward_mode == mode.inaccessible or result.forward_speed == -1) then + result.forward_mode = mode.pushing_bike + result.forward_speed = push_forward_speed + end + if push_backward_speed and (result.backward_mode == mode.inaccessible or result.backward_speed == -1)then + result.backward_mode = mode.pushing_bike + result.backward_speed = push_backward_speed + end + + end + + end + + -- dismount + if data.bicycle == "dismount" then + result.forward_mode = mode.pushing_bike + result.backward_mode = mode.pushing_bike + result.forward_speed = profile.walking_speed + result.backward_speed = profile.walking_speed + end +end + +function safety_handler(profile,way,result,data) + -- convert duration into cyclability + if profile.properties.weight_name == 'cyclability' then + local safety_penalty = profile.unsafe_highway_list[data.highway] or 1. + local is_unsafe = safety_penalty < 1 + + -- primaries that are one ways are probably huge primaries where the lanes need to be separated + if is_unsafe and data.highway == 'primary' and not data.is_twoway then + safety_penalty = safety_penalty * 0.5 + end + if is_unsafe and data.highway == 'secondary' and not data.is_twoway then + safety_penalty = safety_penalty * 0.6 + end + + local forward_is_unsafe = is_unsafe and not data.has_cycleway_forward + local backward_is_unsafe = is_unsafe and not data.has_cycleway_backward + local is_undesireable = data.highway == "service" and profile.service_penalties[data.service] + local forward_penalty = 1. + local backward_penalty = 1. + if forward_is_unsafe then + forward_penalty = math.min(forward_penalty, safety_penalty) + end + if backward_is_unsafe then + backward_penalty = math.min(backward_penalty, safety_penalty) + end + + if is_undesireable then + forward_penalty = math.min(forward_penalty, profile.service_penalties[data.service]) + backward_penalty = math.min(backward_penalty, profile.service_penalties[data.service]) + end + + if result.forward_speed > 0 then + -- convert from km/h to m/s + result.forward_rate = result.forward_speed / 3.6 * forward_penalty + end + if result.backward_speed > 0 then + -- convert from km/h to m/s + result.backward_rate = result.backward_speed / 3.6 * backward_penalty + end + if result.duration > 0 then + result.weight = result.duration / forward_penalty + end + + if data.highway == "bicycle" then + safety_bonus = safety_bonus + 0.2 + if result.forward_speed > 0 then + -- convert from km/h to m/s + result.forward_rate = result.forward_speed / 3.6 * safety_bonus + end + if result.backward_speed > 0 then + -- convert from km/h to m/s + result.backward_rate = result.backward_speed / 3.6 * safety_bonus + end + if result.duration > 0 then + result.weight = result.duration / safety_bonus + end + end + end +end + +function process_way(profile, way, result) + -- the initial filtering of ways based on presence of tags + -- affects processing times significantly, because all ways + -- have to be checked. + -- to increase performance, prefetching and initial tag check + -- is done directly instead of via a handler. + + -- in general we should try to abort as soon as + -- possible if the way is not routable, to avoid doing + -- unnecessary work. this implies we should check things that + -- commonly forbids access early, and handle edge cases later. + + -- data table for storing intermediate values during processing + + --io.write("process_way\n") + + local data = { + -- prefetch tags + highway = way:get_value_by_key('highway'), + + route = nil, + man_made = nil, + railway = nil, + amenity = nil, + public_transport = nil, + bridge = nil, + + access = nil, + + junction = nil, + maxspeed = nil, + maxspeed_forward = nil, + maxspeed_backward = nil, + barrier = nil, + oneway = nil, + oneway_bicycle = nil, + cycleway = nil, + cycleway_left = nil, + cycleway_right = nil, + duration = nil, + service = nil, + foot = nil, + foot_forward = nil, + foot_backward = nil, + bicycle = nil, + + way_type_allows_pushing = false, + has_cycleway_forward = false, + has_cycleway_backward = false, + is_twoway = true, + reverse = false, + implied_oneway = false + } + + local handlers = Sequence { + -- set the default mode for this profile. if can be changed later + -- in case it turns we're e.g. on a ferry + WayHandlers.default_mode, + + -- check various tags that could indicate that the way is not + -- routable. this includes things like status=impassable, + -- toll=yes and oneway=reversible + WayHandlers.blocked_ways, + + -- our main handler + handle_bicycle_tags, + + -- compute speed taking into account way type, maxspeed tags, etc. + WayHandlers.surface, + + -- handle turn lanes and road classification, used for guidance + WayHandlers.classification, + + -- handle allowed start/end modes + WayHandlers.startpoint, + + -- handle roundabouts + WayHandlers.roundabouts, + + -- set name, ref and pronunciation + WayHandlers.names, + + -- set classes + WayHandlers.classes, + + -- set weight properties of the way + WayHandlers.weights + } + + WayHandlers.run(profile, way, result, data, handlers) +end + +function process_turn(profile, turn) + -- compute turn penalty as angle^2, with a left/right bias + local normalized_angle = turn.angle / 90.0 + if normalized_angle >= 0.0 then + turn.duration = normalized_angle * normalized_angle * profile.turn_penalty / profile.turn_bias + else + turn.duration = normalized_angle * normalized_angle * profile.turn_penalty * profile.turn_bias + end + + if turn.is_u_turn then + turn.duration = turn.duration + profile.properties.u_turn_penalty + end + + if turn.has_traffic_light then + turn.duration = turn.duration + profile.properties.traffic_light_penalty + end + if profile.properties.weight_name == 'cyclability' then + turn.weight = turn.duration + end + if turn.source_mode == mode.cycling and turn.target_mode ~= mode.cycling then + turn.weight = turn.weight + profile.properties.mode_change_penalty + end +end + +return { + setup = setup, + process_way = process_way, + process_segment = process_segment, + process_node = process_node, + process_turn = process_turn +} diff --git a/profiles/examples/elevation_aware_bicycle/generate_rastersource.py b/profiles/examples/elevation_aware_bicycle/generate_rastersource.py new file mode 100644 index 000000000..b4b8d99e4 --- /dev/null +++ b/profiles/examples/elevation_aware_bicycle/generate_rastersource.py @@ -0,0 +1,9 @@ +import numpy as np + +# SRTM Elevation Raster Tiles: http://srtm.csi.cgiar.org/srtmdata/ +# This example fuses the two srtm tiles covering Switzerland. +# Change this python script as needed by arranging any number of tiles with `np.hstack` and `np.vstack`. +srtm_39_03 = np.loadtxt("srtm_39_03.asc", skiprows=6, dtype=np.int) +srtm_38_03 = np.loadtxt("srtm_38_03.asc", skiprows=6, dtype=np.int) +rastersource = np.hstack((srtm_38_03, srtm_39_03)) +np.savetxt("rastersource.asc", rastersource, fmt="%d") \ No newline at end of file diff --git a/profiles/examples/elevation_aware_bicycle/speed_by_elevation_of_bicycle/bike_formula.py b/profiles/examples/elevation_aware_bicycle/speed_by_elevation_of_bicycle/bike_formula.py new file mode 100644 index 000000000..c9781b6ae --- /dev/null +++ b/profiles/examples/elevation_aware_bicycle/speed_by_elevation_of_bicycle/bike_formula.py @@ -0,0 +1,46 @@ +import numpy as np +import matplotlib.pyplot as plt + +## bike modelling + +P = 150 # W +l = 1000 # m +t = 1000/5 # s +m = 70 #kg +g = 9.81 #m / s ** 2 +N = 100 +min_slope = -0.25 +max_slope = 0.25 + +c2 = m*g +s_bar = np.linspace(min_slope, max_slope, N) +t_scaled = np.empty(N) +for i in range(N): + for root in np.roots([P*(t**3)/l, -s_bar[i]*c2*(t**2), 0, -P * t ** 3 / l]): + if np.isreal(root): + t_scaled[i] = root + + +# export the function of scaled time to the lua file +print("list = {", end='') +for t_elem in t_scaled: + print(t_elem, end='') + if t_elem != t_scaled[-1]: + print(",", end='') +print("}") +print("N =", N) +print("min_slope =", min_slope) +print("max_slope =", max_slope) + +# see a plot with the generated function of scaled time. + +t_simple = 1 / (1 - s_bar * 5.0) + +plt.plot(s_bar, t_scaled, s_bar, t_simple) +plt.ylim([-1, 6]) +plt.title("How much longer a bicycle takes when going uphill") +plt.xlabel('$slope [d_{vertical}/d_{horizontal}]$') +plt.ylabel('$time [t_{withelevation} / t_{flat}]$') +plt.legend(['$t_{proper}$', '$t_{simple}$']) +plt.savefig('bike_slope.png') +plt.show() \ No newline at end of file diff --git a/profiles/examples/elevation_aware_bicycle/speed_by_elevation_of_bicycle/bike_slope.png b/profiles/examples/elevation_aware_bicycle/speed_by_elevation_of_bicycle/bike_slope.png new file mode 100644 index 0000000000000000000000000000000000000000..445f8a0a3190aecd8be9c9feee571dd3b1542acc GIT binary patch literal 32197 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 MN-IcZOX&OjFDl;wZ2$lO literal 0 HcmV?d00001