From c0c85e2098c70fb2082ec2d671cd78316cd73a51 Mon Sep 17 00:00:00 2001 From: hellotechx <50263945+hellotechx@users.noreply.github.com> Date: Wed, 11 Mar 2020 20:15:40 +0800 Subject: [PATCH] add new car profile and libs for unidb (#214) * add new car profile and libs for unidb * Add an empty README file to log rules and changes * Remove README.md for this pr, will add it back in coming pr --- profiles/car-unidb.lua | 503 +++++++++++++++++ profiles/lib-unidb/access.lua | 15 + profiles/lib-unidb/destination.lua | 29 + profiles/lib-unidb/guidance.lua | 173 ++++++ profiles/lib-unidb/maxspeed.lua | 19 + profiles/lib-unidb/measure.lua | 111 ++++ profiles/lib-unidb/pprint.lua | 457 +++++++++++++++ profiles/lib-unidb/profile_debugger.lua | 139 +++++ profiles/lib-unidb/relations.lua | 262 +++++++++ profiles/lib-unidb/sequence.lua | 10 + profiles/lib-unidb/set.lua | 23 + profiles/lib-unidb/tags.lua | 143 +++++ profiles/lib-unidb/utils.lua | 43 ++ profiles/lib-unidb/way_handlers.lua | 722 ++++++++++++++++++++++++ 14 files changed, 2649 insertions(+) create mode 100644 profiles/car-unidb.lua create mode 100644 profiles/lib-unidb/access.lua create mode 100644 profiles/lib-unidb/destination.lua create mode 100644 profiles/lib-unidb/guidance.lua create mode 100644 profiles/lib-unidb/maxspeed.lua create mode 100644 profiles/lib-unidb/measure.lua create mode 100644 profiles/lib-unidb/pprint.lua create mode 100644 profiles/lib-unidb/profile_debugger.lua create mode 100644 profiles/lib-unidb/relations.lua create mode 100644 profiles/lib-unidb/sequence.lua create mode 100644 profiles/lib-unidb/set.lua create mode 100644 profiles/lib-unidb/tags.lua create mode 100644 profiles/lib-unidb/utils.lua create mode 100644 profiles/lib-unidb/way_handlers.lua diff --git a/profiles/car-unidb.lua b/profiles/car-unidb.lua new file mode 100644 index 00000000000..e3f35f83d64 --- /dev/null +++ b/profiles/car-unidb.lua @@ -0,0 +1,503 @@ +-- Car profile + +api_version = 4 + +Set = require('lib-unidb/set') +Sequence = require('lib-unidb/sequence') +Handlers = require("lib-unidb/way_handlers") +Relations = require("lib-unidb/relations") +find_access_tag = require("lib-unidb/access").find_access_tag +limit = require("lib-unidb/maxspeed").limit +Utils = require("lib-unidb/utils") +Measure = require("lib-unidb/measure") + +function setup() + return { + properties = { + max_speed_for_map_matching = 180/3.6, -- 180kmph -> m/s + -- For routing based on duration, but weighted for preferring certain roads + weight_name = 'routability', + -- For shortest duration without penalties for accessibility + -- weight_name = 'duration', + -- For shortest distance without penalties for accessibility + -- weight_name = 'distance', + process_call_tagless_node = false, + u_turn_penalty = 20, + continue_straight_at_waypoint = true, + use_turn_restrictions = true, + left_hand_driving = false, + traffic_light_penalty = 2, + }, + + default_mode = mode.driving, + default_speed = 10, + oneway_handling = true, + side_road_multiplier = 0.8, + turn_penalty = 7.5, + speed_reduction = 0.8, + turn_bias = 1.075, + cardinal_directions = false, + + -- Size of the vehicle, to be limited by physical restriction of the way + vehicle_height = 2.5, -- in meters, 2.5m is the height of van + vehicle_width = 1.9, -- in meters, ways with narrow tag are considered narrower than 2.2m + + -- Size of the vehicle, to be limited mostly by legal restriction of the way + vehicle_length = 4.8, -- in meters, 4.8m is the length of large or familly car + vehicle_weight = 3500, -- in kilograms + + -- a list of suffixes to suppress in name change instructions. The suffixes also include common substrings of each other + suffix_list = { + 'N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'North', 'South', 'West', 'East', 'Nor', 'Sou', 'We', 'Ea' + }, + + barrier_whitelist = Set { + 'cattle_grid', + 'border_control', + 'toll_booth', + 'sally_port', + 'gate', + 'lift_gate', + 'no', + 'entrance', + 'height_restrictor', + 'arch' + }, + + access_tag_whitelist = Set { + 'yes', + 'motorcar', + 'motor_vehicle', + 'vehicle', + 'permissive', + 'designated', + 'hov' + }, + + access_tag_blacklist = Set { + 'no', + 'agricultural', + 'forestry', + 'emergency', + 'psv', + 'customers', + 'private', + 'delivery', + 'destination' + }, + + -- tags disallow access to in combination with highway=service + service_access_tag_blacklist = Set { + 'private' + }, + + restricted_access_tag_list = Set { + 'private', + 'delivery', + 'destination', + 'customers', + }, + + access_tags_hierarchy = Sequence { + 'motorcar', + 'motor_vehicle', + 'vehicle', + 'access' + }, + + service_tag_forbidden = Set { + 'emergency_access' + }, + + restrictions = Sequence { + 'motorcar', + 'motor_vehicle', + 'vehicle' + }, + + classes = Sequence { + 'toll', 'motorway', 'ferry', 'restricted', 'tunnel' + }, + + -- classes to support for exclude flags + excludable = Sequence { + Set {'toll'}, + Set {'motorway'}, + Set {'ferry'} + }, + + avoid = Set { + 'area', + -- 'toll', -- uncomment this to avoid tolls + 'reversible', + 'impassable', + 'hov_lanes', + 'steps', + 'construction', + 'proposed' + }, + + speeds = Sequence { + highway = { + motorway = 90, + motorway_link = 45, + trunk = 85, + trunk_link = 40, + primary = 65, + primary_link = 30, + secondary = 55, + secondary_link = 25, + tertiary = 40, + tertiary_link = 20, + unclassified = 25, + residential = 25, + living_street = 10, + service = 15 + } + }, + + service_penalties = { + alley = 0.5, + parking = 0.5, + parking_aisle = 0.5, + driveway = 0.5, + ["drive-through"] = 0.5, + ["drive-thru"] = 0.5 + }, + + restricted_highway_whitelist = Set { + 'motorway', + 'motorway_link', + 'trunk', + 'trunk_link', + 'primary', + 'primary_link', + 'secondary', + 'secondary_link', + 'tertiary', + 'tertiary_link', + 'residential', + 'living_street', + 'unclassified', + 'service' + }, + + construction_whitelist = Set { + 'no', + 'widening', + 'minor', + }, + + route_speeds = { + ferry = 5, + shuttle_train = 10 + }, + + bridge_speeds = { + movable = 5 + }, + + -- surface/trackype/smoothness + -- values were estimated from looking at the photos at the relevant wiki pages + + -- max speed for surfaces + surface_speeds = { + asphalt = nil, -- nil mean no limit. removing the line has the same effect + concrete = nil, + ["concrete:plates"] = nil, + ["concrete:lanes"] = nil, + paved = nil, + + cement = 80, + compacted = 80, + fine_gravel = 80, + + paving_stones = 60, + metal = 60, + bricks = 60, + + grass = 40, + wood = 40, + sett = 40, + grass_paver = 40, + gravel = 40, + unpaved = 40, + ground = 40, + dirt = 40, + pebblestone = 40, + tartan = 40, + + cobblestone = 30, + clay = 30, + + earth = 20, + stone = 20, + rocky = 20, + sand = 20, + + mud = 10 + }, + + -- max speed for tracktypes + tracktype_speeds = { + grade1 = 60, + grade2 = 40, + grade3 = 30, + grade4 = 25, + grade5 = 20 + }, + + -- max speed for smoothnesses + smoothness_speeds = { + intermediate = 80, + bad = 40, + very_bad = 20, + horrible = 10, + very_horrible = 5, + impassable = 0 + }, + + -- http://wiki.openstreetmap.org/wiki/Speed_limits + maxspeed_table_default = { + urban = 50, + rural = 90, + trunk = 110, + motorway = 130 + }, + + -- List only exceptions + maxspeed_table = { + ["at:rural"] = 100, + ["at:trunk"] = 100, + ["be:motorway"] = 120, + ["be-vlg:rural"] = 70, + ["by:urban"] = 60, + ["by:motorway"] = 110, + ["ch:rural"] = 80, + ["ch:trunk"] = 100, + ["ch:motorway"] = 120, + ["cz:trunk"] = 0, + ["cz:motorway"] = 0, + ["de:living_street"] = 7, + ["de:rural"] = 100, + ["de:motorway"] = 0, + ["dk:rural"] = 80, + ["fr:rural"] = 80, + ["gb:nsl_single"] = (60*1609)/1000, + ["gb:nsl_dual"] = (70*1609)/1000, + ["gb:motorway"] = (70*1609)/1000, + ["nl:rural"] = 80, + ["nl:trunk"] = 100, + ['no:rural'] = 80, + ['no:motorway'] = 110, + ['pl:rural'] = 100, + ['pl:trunk'] = 120, + ['pl:motorway'] = 140, + ["ro:trunk"] = 100, + ["ru:living_street"] = 20, + ["ru:urban"] = 60, + ["ru:motorway"] = 110, + ["uk:nsl_single"] = (60*1609)/1000, + ["uk:nsl_dual"] = (70*1609)/1000, + ["uk:motorway"] = (70*1609)/1000, + ['za:urban'] = 60, + ['za:rural'] = 100, + ["none"] = 140 + }, + + relation_types = Sequence { + "route", + "traffic_sign" + }, + + -- classify highway tags when necessary for turn weights + highway_turn_classification = { + }, + + -- classify access tags when necessary for turn weights + access_turn_classification = { + } + } +end + +function process_node(profile, node, result, relations) + -- parse access and barrier tags + local access = find_access_tag(node, profile.access_tags_hierarchy) + if access then + if profile.access_tag_blacklist[access] and not profile.restricted_access_tag_list[access] then + result.barrier = true + end + else + local barrier = node:get_value_by_key("barrier") + if barrier then + -- check height restriction barriers + local restricted_by_height = false + if barrier == 'height_restrictor' then + local maxheight = Measure.get_max_height(node:get_value_by_key("maxheight"), node) + restricted_by_height = maxheight and maxheight < profile.vehicle_height + end + + -- make an exception for rising bollard barriers + local bollard = node:get_value_by_key("bollard") + local rising_bollard = bollard and "rising" == bollard + + if not profile.barrier_whitelist[barrier] and not rising_bollard or restricted_by_height then + result.barrier = true + end + end + end + + -- check if node is a traffic light + local tag = node:get_value_by_key("highway") + if "traffic_signals" == tag then + result.traffic_lights = true + end + -- local tag = relations:get_value_by_key("type") + -- if "traffic_signals" == tag then + -- result.traffic_lights = true + -- end +end + +function process_way(profile, way, result, relations) + -- the intial filtering of ways based on presence of tags + -- affects processing times significantly, because all ways + -- have to be checked. + -- to increase performance, prefetching and intial tag check + -- is done in 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 + local data = { + -- prefetch tags + highway = way:get_value_by_key('highway'), + bridge = way:get_value_by_key('bridge'), + route = way:get_value_by_key('route') + } + + -- perform an quick initial check and abort if the way is + -- obviously not routable. + -- highway or route tags must be in data table, bridge is optional + if (not data.highway or data.highway == '') and + (not data.route or data.route == '') + then + return + end + + 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, + WayHandlers.avoid_ways, + WayHandlers.handle_height, + WayHandlers.handle_width, + WayHandlers.handle_length, + WayHandlers.handle_weight, + + -- determine access status by checking our hierarchy of + -- access tags, e.g: motorcar, motor_vehicle, vehicle + WayHandlers.access, + + -- check whether forward/backward directions are routable + WayHandlers.oneway, + + -- check a road's destination + WayHandlers.destinations, + + -- check whether we're using a special transport mode + WayHandlers.ferries, + WayHandlers.movables, + + -- handle service road restrictions + WayHandlers.service, + + -- handle hov + WayHandlers.hov, + + -- compute speed taking into account way type, maxspeed tags, etc. + WayHandlers.speed, + WayHandlers.surface, + WayHandlers.maxspeed, + WayHandlers.penalties, + + -- compute class labels + WayHandlers.classes, + + -- handle turn lanes and road classification, used for guidance + WayHandlers.turn_lanes, + WayHandlers.classification, + + -- handle various other flags + WayHandlers.roundabouts, + WayHandlers.startpoint, + WayHandlers.driving_side, + + -- set name, ref and pronunciation + WayHandlers.names, + + -- set weight properties of the way + WayHandlers.weights, + + -- set classification of ways relevant for turns + WayHandlers.way_classification_for_turn + } + + WayHandlers.run(profile, way, result, data, handlers, relations) + + if profile.cardinal_directions then + Relations.process_way_refs(way, relations, result) + end +end + +function process_turn(profile, turn) + -- Use a sigmoid function to return a penalty that maxes out at turn_penalty + -- over the space of 0-180 degrees. Values here were chosen by fitting + -- the function to some turn penalty samples from real driving. + local turn_penalty = profile.turn_penalty + local turn_bias = turn.is_left_hand_driving and 1. / profile.turn_bias or profile.turn_bias + + if turn.has_traffic_light then + turn.duration = profile.properties.traffic_light_penalty + end + + if turn.number_of_roads > 2 or turn.source_mode ~= turn.target_mode or turn.is_u_turn then + if turn.angle >= 0 then + turn.duration = turn.duration + turn_penalty / (1 + math.exp( -((13 / turn_bias) * turn.angle/180 - 6.5*turn_bias))) + else + turn.duration = turn.duration + turn_penalty / (1 + math.exp( -((13 * turn_bias) * -turn.angle/180 - 6.5/turn_bias))) + end + + if turn.is_u_turn then + turn.duration = turn.duration + profile.properties.u_turn_penalty + end + end + + -- for distance based routing we don't want to have penalties based on turn angle + if profile.properties.weight_name == 'distance' then + turn.weight = 0 + else + turn.weight = turn.duration + end + + if profile.properties.weight_name == 'routability' then + -- penalize turns from non-local access only segments onto local access only tags + if not turn.source_restricted and turn.target_restricted then + turn.weight = constants.max_turn_weight + end + end +end + +return { + setup = setup, + process_way = process_way, + process_node = process_node, + process_turn = process_turn +} diff --git a/profiles/lib-unidb/access.lua b/profiles/lib-unidb/access.lua new file mode 100644 index 00000000000..678bd5c4b98 --- /dev/null +++ b/profiles/lib-unidb/access.lua @@ -0,0 +1,15 @@ +local ipairs = ipairs + +local Access = {} + +function Access.find_access_tag(source,access_tags_hierarchy) + for i,v in ipairs(access_tags_hierarchy) do + local tag = source:get_value_by_key(v) + if tag then + return tag + end + end + return nil +end + +return Access diff --git a/profiles/lib-unidb/destination.lua b/profiles/lib-unidb/destination.lua new file mode 100644 index 00000000000..a09330a701e --- /dev/null +++ b/profiles/lib-unidb/destination.lua @@ -0,0 +1,29 @@ +local Destination = {} + +function Destination.get_directional_tag(way, is_forward, tag) + local v + if is_forward then + v = way:get_value_by_key(tag .. ':forward') or way:get_value_by_key(tag) + else + v = way:get_value_by_key(tag .. ':backward') or way:get_value_by_key(tag) + end + if v then + return v.gsub(v, ';', ', ') + end +end + +-- Assemble destination as: "A59: Düsseldorf, Köln" +-- destination:ref ^ ^ destination + +function Destination.get_destination(way, is_forward) + ref = Destination.get_directional_tag(way, is_forward, 'destination:ref') + dest = Destination.get_directional_tag(way, is_forward, 'destination') + street = Destination.get_directional_tag(way, is_forward, 'destination:street') + if ref and dest then + return ref .. ': ' .. dest + else + return ref or dest or street or '' + end +end + +return Destination diff --git a/profiles/lib-unidb/guidance.lua b/profiles/lib-unidb/guidance.lua new file mode 100644 index 00000000000..bda38921f7d --- /dev/null +++ b/profiles/lib-unidb/guidance.lua @@ -0,0 +1,173 @@ +local Tags = require('lib-unidb/tags') +local Set = require('lib-unidb/set') + +local Guidance = {} + +-- Guidance: Default Mapping from roads to types/priorities +highway_classes = { + motorway = road_priority_class.motorway, + motorway_link = road_priority_class.motorway_link, + trunk = road_priority_class.trunk, + trunk_link = road_priority_class.trunk_link, + primary = road_priority_class.primary, + primary_link = road_priority_class.primary_link, + secondary = road_priority_class.secondary, + secondary_link = road_priority_class.secondary_link, + tertiary = road_priority_class.tertiary, + tertiary_link = road_priority_class.tertiary_link, + unclassified = road_priority_class.unclassified, + residential = road_priority_class.main_residential, + service = road_priority_class.alley, + living_street = road_priority_class.side_residential, + track = road_priority_class.bike_path, + path = road_priority_class.bike_path, + footway = road_priority_class.foot_path, + pedestrian = road_priority_class.foot_path, + steps = road_priority_class.foot_path +} + +default_highway_class = road_priority_class.connectivity; + +motorway_types = Set { + 'motorway', + 'motorway_link', + 'trunk', + 'trunk_link' +} + +-- these road types are set with a car in mind. For bicycle/walk we probably need different ones +road_types = Set { + 'motorway', + 'motorway_link', + 'trunk', + 'trunk_link', + 'primary', + 'primary_link', + 'secondary', + 'secondary_link', + 'tertiary', + 'tertiary_link', + 'unclassified', + 'residential', + 'living_street' +} + +link_types = Set { + 'motorway_link', + 'trunk_link', + 'primary_link', + 'secondary_link', + 'tertiary_link' +} + +-- roads like parking lots are very unimportant for normal driving +parking_class = Set{ + 'parking_aisle', + 'driveway', + 'drive-through', + 'emergency_access' +} + +function Guidance.set_classification (highway, result, input_way) + if motorway_types[highway] then + result.road_classification.motorway_class = true + end + if link_types[highway] then + result.road_classification.link_class = true + end + + -- All service roads are recognised as alley + if highway ~= nil and highway == 'service' then + local service_type = input_way:get_value_by_key('service'); + if service_type ~= nil and parking_class[service_type] then + result.road_classification.road_priority_class = road_priority_class.alley + else + if service_type ~= nil and service_type == 'alley' then + result.road_classification.road_priority_class = road_priority_class.alley + else + if serice_type == nil then + result.road_classification.road_priority_class = road_priority_class.alley + else + result.road_classification.road_priority_class = highway_classes[highway] + end + end + end + else + if highway_classes[highway] ~= nil then + result.road_classification.road_priority_class = highway_classes[highway] + else + result.road_classification.road_priority_class = default_highway_class + end + end + if road_types[highway] then + result.road_classification.may_be_ignored = false; + else + result.road_classification.may_be_ignored = true; + end + + local lane_count = input_way:get_value_by_key("lanes") + if lane_count then + local lc = tonumber(lane_count) + if lc ~= nil then + result.road_classification.num_lanes = lc + end + else + local total_count = 0 + local forward_count = input_way:get_value_by_key("lanes:forward") + if forward_count then + local fc = tonumber(forward_count) + if fc ~= nil then + total_count = fc + end + end + local backward_count = input_way:get_value_by_key("lanes:backward") + if backward_count then + local bc = tonumber(backward_count) + if bc ~= nil then + total_count = total_count + bc + end + end + if total_count ~= 0 then + result.road_classification.num_lanes = total_count + end + end +end + +-- returns forward,backward psv lane count +local function get_psv_counts(way,data) + local psv_forward, psv_backward = Tags.get_forward_backward_by_key(way,data,'lanes:psv') + if psv_forward then + psv_forward = tonumber(psv_forward) + end + if psv_backward then + psv_backward = tonumber(psv_backward) + end + return psv_forward or 0, + psv_backward or 0 +end + +-- trims lane string with regard to supported lanes +local function process_lanes(turn_lanes,vehicle_lanes,first_count,second_count) + if turn_lanes then + if vehicle_lanes then + return applyAccessTokens(turn_lanes,vehicle_lanes) + elseif first_count ~= 0 or second_count ~= 0 then + return trimLaneString(turn_lanes, first_count, second_count) + else + return turn_lanes + end + end +end + +-- this is broken for left-sided driving. It needs to switch left and right in case of left-sided driving +function Guidance.get_turn_lanes(way,data) + local psv_fw, psv_bw = get_psv_counts(way,data) + local turn_lanes_fw, turn_lanes_bw = Tags.get_forward_backward_by_key(way,data,'turn:lanes') + local vehicle_lanes_fw, vehicle_lanes_bw = Tags.get_forward_backward_by_key(way,data,'vehicle:lanes') + + --note: backward lanes swap psv_bw and psv_fw + return process_lanes(turn_lanes_fw,vehicle_lanes_fw,psv_bw,psv_fw) or turn_lanes, + process_lanes(turn_lanes_bw,vehicle_lanes_bw,psv_fw,psv_bw) or turn_lanes +end + +return Guidance diff --git a/profiles/lib-unidb/maxspeed.lua b/profiles/lib-unidb/maxspeed.lua new file mode 100644 index 00000000000..0dd9b822d0e --- /dev/null +++ b/profiles/lib-unidb/maxspeed.lua @@ -0,0 +1,19 @@ +local math = math + +local MaxSpeed = {} + +function MaxSpeed.limit(way,max,maxf,maxb) + if maxf and maxf>0 then + way.forward_speed = math.min(way.forward_speed, maxf) + elseif max and max>0 then + way.forward_speed = math.min(way.forward_speed, max) + end + + if maxb and maxb>0 then + way.backward_speed = math.min(way.backward_speed, maxb) + elseif max and max>0 then + way.backward_speed = math.min(way.backward_speed, max) + end +end + +return MaxSpeed diff --git a/profiles/lib-unidb/measure.lua b/profiles/lib-unidb/measure.lua new file mode 100644 index 00000000000..f6b301dba06 --- /dev/null +++ b/profiles/lib-unidb/measure.lua @@ -0,0 +1,111 @@ +local Sequence = require('lib-unidb/sequence') +local Sequence = require('lib-unidb/set') + +Measure = {} + +-- measurements conversion constants +local inch_to_meters = 0.0254 +local feet_to_inches = 12 +local pound_to_kilograms = 0.45359237 +local miles_to_kilometers = 1.609 + +-- Parse speed value as kilometers by hours. +function Measure.parse_value_speed(source, speed_unit) + local n = tonumber(source:match("%d*")) + if n then + if string.match(source, "mph") or string.match(source, "mp/h") then + n = n * miles_to_kilometers + elseif speed_unit and speed_unit == "M" then + n = n * miles_to_kilometers + end + + return n + end +end + +--- Parse string as a height in meters. +--- according to http://wiki.openstreetmap.org/wiki/Key:maxheight +function Measure.parse_value_meters(value) + local n = tonumber(value:gsub(",", "."):match("%d+%.?%d*")) + if n then + inches = value:match("'.*") + if inches then -- Imperial unit to metric + -- try to parse feets/inch + n = n * feet_to_inches + local m = tonumber(inches:match("%d+")) + if m then + n = n + m + end + n = n * inch_to_meters + end + return n + end +end + +--- Parse weight value in kilograms. +--- according to https://wiki.openstreetmap.org/wiki/Key:maxweight +function Measure.parse_value_kilograms(value) + local n = tonumber(value:gsub(",", "."):match("%d+%.?%d*")) + if n then + if string.match(value, "lbs") then + n = n * pound_to_kilograms + elseif string.match(value, "kg") then + -- n = n + else -- Default, metric tons + n = n * 1000 + end + return n + end +end + +--- Get maxspeed of specified way in kilometers by hours. +function Measure.get_max_speed(raw_value, speed_unit) + if raw_value then + return Measure.parse_value_speed(raw_value, speed_unit) + end +end + +-- default maxheight value defined in https://wiki.openstreetmap.org/wiki/Key:maxheight#Non-numerical_values +local default_maxheight = 4.5 +-- Available Non numerical values equal to 4.5; below_default and no_indications are not considered +local height_non_numerical_values = Set { "default", "none", "no-sign", "unsigned" } + +--- Get maxheight of specified way in meters. If there are no +--- max height, then return nil +function Measure.get_max_height(raw_value, element) + if raw_value then + if height_non_numerical_values[raw_value] then + if element then + return element:get_location_tag('maxheight') or default_maxheight + else + return default_maxheight + end + else + return Measure.parse_value_meters(raw_value) + end + end +end + +--- Get maxwidth of specified way in meters. +function Measure.get_max_width(raw_value) + if raw_value then + return Measure.parse_value_meters(raw_value) + end +end + +--- Get maxlength of specified way in meters. +function Measure.get_max_length(raw_value) + if raw_value then + return Measure.parse_value_meters(raw_value) + end +end + +--- Get maxweight of specified way in kilogramms. +function Measure.get_max_weight(raw_value) + if raw_value then + return Measure.parse_value_kilograms(raw_value) + end +end + + +return Measure; diff --git a/profiles/lib-unidb/pprint.lua b/profiles/lib-unidb/pprint.lua new file mode 100644 index 00000000000..38e9db14a6d --- /dev/null +++ b/profiles/lib-unidb/pprint.lua @@ -0,0 +1,457 @@ +-- Easy way to print data structes +-- From https://github.com/jagt/pprint.lua, file is license as pubic domain + +local pprint = { VERSION = '0.1' } + +pprint.defaults = { + -- type display trigger, hide not useful datatypes by default + -- custom types are treated as table + show_nil = true, + show_boolean = true, + show_number = true, + show_string = true, + show_table = true, + show_function = false, + show_thread = false, + show_userdata = false, + -- additional display trigger + show_metatable = false, -- show metatable + show_all = false, -- override other show settings and show everything + use_tostring = false, -- use __tostring to print table if available + filter_function = nil, -- called like callback(value[,key, parent]), return truty value to hide + object_cache = 'local', -- cache blob and table to give it a id, 'local' cache per print, 'global' cache + -- per process, falsy value to disable (might cause infinite loop) + -- format settings + indent_size = 2, -- indent for each nested table level + level_width = 80, -- max width per indent level + wrap_string = true, -- wrap string when it's longer than level_width + wrap_array = false, -- wrap every array elements + sort_keys = true, -- sort table keys +} + +local TYPES = { + ['nil'] = 1, ['boolean'] = 2, ['number'] = 3, ['string'] = 4, + ['table'] = 5, ['function'] = 6, ['thread'] = 7, ['userdata'] = 8 +} + +-- seems this is the only way to escape these, as lua don't know how to map char '\a' to 'a' +local ESCAPE_MAP = { + ['\a'] = '\\a', ['\b'] = '\\b', ['\f'] = '\\f', ['\n'] = '\\n', ['\r'] = '\\r', + ['\t'] = '\\t', ['\v'] = '\\v', ['\\'] = '\\\\', +} + +-- generic utilities +local function escape(s) + s = s:gsub('([%c\\])', ESCAPE_MAP) + local dq = s:find('"') + local sq = s:find("'") + if dq and sq then + return s:gsub('"', '\\"'), '"' + elseif sq then + return s, '"' + else + return s, "'" + end +end + +local function is_plain_key(key) + return type(key) == 'string' and key:match('^[%a_][%a%d_]*$') +end + +local CACHE_TYPES = { + ['table'] = true, ['function'] = true, ['thread'] = true, ['userdata'] = true +} + +-- cache would be populated to be like: +-- { +-- function = { `fun1` = 1, _cnt = 1 }, -- object id +-- table = { `table1` = 1, `table2` = 2, _cnt = 2 }, +-- visited_tables = { `table1` = 7, `table2` = 8 }, -- visit count +-- } +-- use weakrefs to avoid accidentall adding refcount +local function cache_apperance(obj, cache, option) + if not cache.visited_tables then + cache.visited_tables = setmetatable({}, {__mode = 'k'}) + end + local t = type(obj) + + -- TODO can't test filter_function here as we don't have the ix and key, + -- might cause different results? + -- respect show_xxx and filter_function to be consistent with print results + if (not TYPES[t] and not option.show_table) + or (TYPES[t] and not option['show_'..t]) then + return + end + + if CACHE_TYPES[t] or TYPES[t] == nil then + if not cache[t] then + cache[t] = setmetatable({}, {__mode = 'k'}) + cache[t]._cnt = 0 + end + if not cache[t][obj] then + cache[t]._cnt = cache[t]._cnt + 1 + cache[t][obj] = cache[t]._cnt + end + end + if t == 'table' or TYPES[t] == nil then + if cache.visited_tables[obj] == false then + -- already printed, no need to mark this and its children anymore + return + elseif cache.visited_tables[obj] == nil then + cache.visited_tables[obj] = 1 + else + -- visited already, increment and continue + cache.visited_tables[obj] = cache.visited_tables[obj] + 1 + return + end + for k, v in pairs(obj) do + cache_apperance(k, cache, option) + cache_apperance(v, cache, option) + end + local mt = getmetatable(obj) + if mt and option.show_metatable then + cache_apperance(mt, cache, option) + end + end +end + +-- makes 'foo2' < 'foo100000'. string.sub makes substring anyway, no need to use index based method +local function str_natural_cmp(lhs, rhs) + while #lhs > 0 and #rhs > 0 do + local lmid, lend = lhs:find('%d+') + local rmid, rend = rhs:find('%d+') + if not (lmid and rmid) then return lhs < rhs end + + local lsub = lhs:sub(1, lmid-1) + local rsub = rhs:sub(1, rmid-1) + if lsub ~= rsub then + return lsub < rsub + end + + local lnum = tonumber(lhs:sub(lmid, lend)) + local rnum = tonumber(rhs:sub(rmid, rend)) + if lnum ~= rnum then + return lnum < rnum + end + + lhs = lhs:sub(lend+1) + rhs = rhs:sub(rend+1) + end + return lhs < rhs +end + +local function cmp(lhs, rhs) + local tleft = type(lhs) + local tright = type(rhs) + if tleft == 'number' and tright == 'number' then return lhs < rhs end + if tleft == 'string' and tright == 'string' then return str_natural_cmp(lhs, rhs) end + if tleft == tright then return str_natural_cmp(tostring(lhs), tostring(rhs)) end + + -- allow custom types + local oleft = TYPES[tleft] or 9 + local oright = TYPES[tright] or 9 + return oleft < oright +end + +-- setup option with default +local function make_option(option) + if option == nil then + option = {} + end + for k, v in pairs(pprint.defaults) do + if option[k] == nil then + option[k] = v + end + if option.show_all then + for t, _ in pairs(TYPES) do + option['show_'..t] = true + end + option.show_metatable = true + end + end + return option +end + +-- override defaults and take effects for all following calls +function pprint.setup(option) + pprint.defaults = make_option(option) +end + +-- format lua object into a string +function pprint.pformat(obj, option, printer) + option = make_option(option) + local buf = {} + local function default_printer(s) + table.insert(buf, s) + end + printer = printer or default_printer + + local cache + if option.object_cache == 'global' then + -- steal the cache into a local var so it's not visible from _G or anywhere + -- still can't avoid user explicitly referentce pprint._cache but it shouldn't happen anyway + cache = pprint._cache or {} + pprint._cache = nil + elseif option.object_cache == 'local' then + cache = {} + end + + local last = '' -- used for look back and remove trailing comma + local status = { + indent = '', -- current indent + len = 0, -- current line length + } + + local wrapped_printer = function(s) + printer(last) + last = s + end + + local function _indent(d) + status.indent = string.rep(' ', d + #(status.indent)) + end + + local function _n(d) + wrapped_printer('\n') + wrapped_printer(status.indent) + if d then + _indent(d) + end + status.len = 0 + return true -- used to close bracket correctly + end + + local function _p(s, nowrap) + status.len = status.len + #s + if not nowrap and status.len > option.level_width then + _n() + wrapped_printer(s) + status.len = #s + else + wrapped_printer(s) + end + end + + local formatter = {} + local function format(v) + local f = formatter[type(v)] + f = f or formatter.table -- allow patched type() + if option.filter_function and option.filter_function(v, nil, nil) then + return '' + else + return f(v) + end + end + + local function tostring_formatter(v) + return tostring(v) + end + + local function number_formatter(n) + return n == math.huge and '[[math.huge]]' or tostring(n) + end + + local function nop_formatter(v) + return '' + end + + local function make_fixed_formatter(t, has_cache) + if has_cache then + return function (v) + return string.format('[[%s %d]]', t, cache[t][v]) + end + else + return function (v) + return '[['..t..']]' + end + end + end + + local function string_formatter(s, force_long_quote) + local s, quote = escape(s) + local quote_len = force_long_quote and 4 or 2 + if quote_len + #s + status.len > option.level_width then + _n() + -- only wrap string when is longer than level_width + if option.wrap_string and #s + quote_len > option.level_width then + -- keep the quotes together + _p('[[') + while #s + status.len >= option.level_width do + local seg = option.level_width - status.len + _p(string.sub(s, 1, seg), true) + _n() + s = string.sub(s, seg+1) + end + _p(s) -- print the remaining parts + return ']]' + end + end + + return force_long_quote and '[['..s..']]' or quote..s..quote + end + + local function table_formatter(t) + if option.use_tostring then + local mt = getmetatable(t) + if mt and mt.__tostring then + return string_formatter(tostring(t), true) + end + end + + local print_header_ix = nil + local ttype = type(t) + if option.object_cache then + local cache_state = cache.visited_tables[t] + local tix = cache[ttype][t] + -- FIXME should really handle `cache_state == nil` + -- as user might add things through filter_function + if cache_state == false then + -- already printed, just print the the number + return string_formatter(string.format('%s %d', ttype, tix), true) + elseif cache_state > 1 then + -- appeared more than once, print table header with number + print_header_ix = tix + cache.visited_tables[t] = false + else + -- appeared exactly once, print like a normal table + end + end + + local tlen = #t + local wrapped = false + _p('{') + _indent(option.indent_size) + _p(string.rep(' ', option.indent_size - 1)) + if print_header_ix then + _p(string.format('--[[%s %d]] ', ttype, print_header_ix)) + end + for ix = 1,tlen do + local v = t[ix] + if formatter[type(v)] == nop_formatter or + (option.filter_function and option.filter_function(v, ix, t)) then + -- pass + else + if option.wrap_array then + wrapped = _n() + end + _p(format(v)..', ') + end + end + + -- hashmap part of the table, in contrast to array part + local function is_hash_key(k) + local numkey = tonumber(k) + if numkey ~= k or numkey > tlen then + return true + end + end + + local function print_kv(k, v, t) + -- can't use option.show_x as obj may contain custom type + if formatter[type(v)] == nop_formatter or + formatter[type(k)] == nop_formatter or + (option.filter_function and option.filter_function(v, k, t)) then + return + end + wrapped = _n() + if is_plain_key(k) then + _p(k, true) + else + _p('[') + -- [[]] type string in key is illegal, needs to add spaces inbetween + local k = format(k) + if string.match(k, '%[%[') then + _p(' '..k..' ', true) + else + _p(k, true) + end + _p(']') + end + _p(' = ', true) + _p(format(v), true) + _p(',', true) + end + + if option.sort_keys then + local keys = {} + for k, _ in pairs(t) do + if is_hash_key(k) then + table.insert(keys, k) + end + end + table.sort(keys, cmp) + for _, k in ipairs(keys) do + print_kv(k, t[k], t) + end + else + for k, v in pairs(t) do + if is_hash_key(k) then + print_kv(k, v, t) + end + end + end + + if option.show_metatable then + local mt = getmetatable(t) + if mt then + print_kv('__metatable', mt, t) + end + end + + _indent(-option.indent_size) + -- make { } into {} + last = string.gsub(last, '^ +$', '') + -- peek last to remove trailing comma + last = string.gsub(last, ',%s*$', ' ') + if wrapped then + _n() + end + _p('}') + + return '' + end + + -- set formatters + formatter['nil'] = option.show_nil and tostring_formatter or nop_formatter + formatter['boolean'] = option.show_boolean and tostring_formatter or nop_formatter + formatter['number'] = option.show_number and number_formatter or nop_formatter -- need to handle math.huge + formatter['function'] = option.show_function and make_fixed_formatter('function', option.object_cache) or nop_formatter + formatter['thread'] = option.show_thread and make_fixed_formatter('thread', option.object_cache) or nop_formatter + formatter['userdata'] = option.show_userdata and make_fixed_formatter('userdata', option.object_cache) or nop_formatter + formatter['string'] = option.show_string and string_formatter or nop_formatter + formatter['table'] = option.show_table and table_formatter or nop_formatter + + if option.object_cache then + -- needs to visit the table before start printing + cache_apperance(obj, cache, option) + end + + _p(format(obj)) + printer(last) -- close the buffered one + + -- put cache back if global + if option.object_cache == 'global' then + pprint._cache = cache + end + + return table.concat(buf) +end + +-- pprint all the arguments +function pprint.pprint( ... ) + local args = {...} + -- select will get an accurate count of array len, counting trailing nils + local len = select('#', ...) + for ix = 1,len do + pprint.pformat(args[ix], nil, io.write) + io.write('\n') + end +end + +setmetatable(pprint, { + __call = function (_, ...) + pprint.pprint(...) + end +}) + +return pprint diff --git a/profiles/lib-unidb/profile_debugger.lua b/profiles/lib-unidb/profile_debugger.lua new file mode 100644 index 00000000000..3306d14ed2b --- /dev/null +++ b/profiles/lib-unidb/profile_debugger.lua @@ -0,0 +1,139 @@ +-- Enable calling our lua profile code directly from the lua command line, +-- which makes it easier to debug. +-- We simulate the normal C++ environment by defining the required globals and functions. + +-- See debug_example.lua for an example of how to require and use this file. + +-- for more convenient printing of tables +local pprint = require('lib-unidb/pprint') + + +-- globals that are normally set from C++ + +-- should match values defined in include/extractor/guidance/road_classification.hpp +road_priority_class = { + motorway = 0, + trunk = 2, + primary = 4, + secondary = 6, + tertiary = 8, + main_residential = 10, + side_residential = 11, + link_road = 14, + bike_path = 16, + foot_path = 18, + connectivity = 31, +} + +-- should match values defined in include/extractor/travel_mode.hpp +mode = { + inaccessible = 0, + driving = 1, + cycling = 2, + walking = 3, + ferry = 4, + train = 5, + pushing_bike = 6, +} + +-- Mock C++ helper functions which are called from LUA. +-- TODO +-- Debugging LUA code that uses these will not work correctly +-- unless we reimplement the methods in LUA. + +function durationIsValid(str) + return true +end + +function parseDuration(str) + return 1 +end + +function canonicalizeStringList(str) + return str +end + + + +-- debug helper +local Debug = {} + +-- helpers for sorting associative array +function Debug.get_keys_sorted_by_value(tbl, sortFunction) + local keys = {} + for key in pairs(tbl) do + table.insert(keys, key) + end + + table.sort(keys, function(a, b) + return sortFunction(tbl[a], tbl[b]) + end) + + return keys +end + +-- helper for printing sorted array +function Debug.print_sorted(sorted,associative) + for _, key in ipairs(sorted) do + print(associative[key], key) + end +end + +function Debug.report_tag_fetches() + print("Tag fetches:") + sorted_counts = Debug.get_keys_sorted_by_value(Debug.tags.counts, function(a, b) return a > b end) + Debug.print_sorted(sorted_counts, Debug.tags.counts) + print(Debug.tags.total, 'total') +end + +function Debug.load_profile(profile) + Debug.functions = require(profile) + Debug.profile = Debug.functions.setup() +end + +function Debug.reset_tag_fetch_counts() + Debug.tags = { + total = 0, + counts = {} + } +end + +function Debug.register_tag_fetch(k) + if Debug.tags.total then + Debug.tags.total = Debug.tags.total + 1 + else + Debug['tags']['total'] = 1 + end + + if Debug['tags']['counts'][k] then + Debug['tags']['counts'][k] = Debug['tags']['counts'][k] + 1 + else + Debug['tags']['counts'][k] = 1 + end + +end + +function Debug.process_way(way,result) + + -- setup result table + result.road_classification = {} + result.forward_speed = -1 + result.backward_speed = -1 + result.duration = 0 + result.forward_classes = {} + result.backward_classes = {} + + -- intercept tag function normally provided via C++ + function way:get_value_by_key(k) + Debug.register_tag_fetch(k) + return self[k] + end + + -- reset tag counts + Debug:reset_tag_fetch_counts() + + -- call the way processsing function + Debug.functions.process_way(Debug.profile,way,result) +end + +return Debug diff --git a/profiles/lib-unidb/relations.lua b/profiles/lib-unidb/relations.lua new file mode 100644 index 00000000000..f7351a7a6ad --- /dev/null +++ b/profiles/lib-unidb/relations.lua @@ -0,0 +1,262 @@ +-- Profile functions dealing with various aspects of relation parsing +-- +-- You can run a selection you find useful in your profile, +-- or do you own processing if/when required. + +Utils = require('lib-unidb/utils') + +Relations = {} + +function is_direction(role) + return (role == 'north' or role == 'south' or role == 'west' or role == 'east') +end + +-- match ref values to relations data +function Relations.match_to_ref(relations, ref) + + function calculate_scores(refs, tag_value) + local tag_tokens = Set(Utils.tokenize_common(tag_value)) + local result = {} + for i, r in ipairs(refs) do + local ref_tokens = Utils.tokenize_common(r) + local score = 0 + + for _, t in ipairs(ref_tokens) do + if tag_tokens[t] then + if Utils.is_number(t) then + score = score + 2 + else + score = score + 1 + end + end + end + + result[r] = score + end + + return result + end + + local references = Utils.string_list_tokens(ref) + local result_match = {} + local order = {} + for i, r in ipairs(references) do + result_match[r] = { forward = nil, backward = nil } + order[i] = r + end + + for i, rel in ipairs(relations) do + local name_scores = nil + local name_tokens = {} + local route_name = rel["route_name"] + if route_name then + name_scores = calculate_scores(references, route_name) + end + + local ref_scores = nil + local ref_tokens = {} + local route_ref = rel["route_ref"] + if route_ref then + ref_scores = calculate_scores(references, route_ref) + end + + -- merge scores + local direction = rel["route_direction"] + if direction then + local best_score = -1 + local best_ref = nil + + function find_best(scores) + if scores then + for k ,v in pairs(scores) do + if v > best_score then + best_ref = k + best_score = v + end + end + end + end + + find_best(name_scores) + find_best(ref_scores) + + if best_ref then + local result_direction = result_match[best_ref] + + local is_forward = rel["route_forward"] + if is_forward == nil then + result_direction.forward = direction + result_direction.backward = direction + elseif is_forward == true then + result_direction.forward = direction + else + result_direction.backward = direction + end + + result_match[best_ref] = result_direction + end + end + + end + + local result = {} + for i, r in ipairs(order) do + result[i] = { ref = r, dir = result_match[r] }; + end + + return result +end + +function get_direction_from_superrel(rel, relations) + local result = nil + local result_id = nil + local rel_id_list = relations:get_relations(rel) + + function set_result(direction, current_rel) + if (result ~= nil) and (direction ~= nil) then + print('WARNING: relation ' .. rel:id() .. ' is a part of more then one supperrelations ' .. result_id .. ' and ' .. current_rel:id()) + result = nil + else + result = direction + result_id = current_rel:id() + end + end + + for i, rel_id in ipairs(rel_id_list) do + local parent_rel = relations:relation(rel_id) + if parent_rel:get_value_by_key('type') == 'route' then + local role = parent_rel:get_role(rel) + + if is_direction(role) then + set_result(role, parent_rel) + else + local dir = parent_rel:get_value_by_key('direction') + if is_direction(dir) then + set_result(dir, parent_rel) + end + end + end + -- TODO: support forward/backward + end + + return result +end + +function Relations.parse_route_relation(rel, way, relations) + local t = rel:get_value_by_key("type") + local role = rel:get_role(way) + local result = {} + + function add_extra_data(m) + local name = rel:get_value_by_key("name") + if name then + result['route_name'] = name + end + + local ref = rel:get_value_by_key("ref") + if ref then + result['route_ref'] = ref + end + end + + if t == 'route' then + local role_direction = nil + local route = rel:get_value_by_key("route") + if route == 'road' then + -- process case, where directions set as role + if is_direction(role) then + role_direction = role + end + end + + local tag_direction = nil + local direction = rel:get_value_by_key('direction') + if direction then + direction = string.lower(direction) + if is_direction(direction) then + tag_direction = direction + end + end + + -- determine direction + local result_direction = role_direction + if result_direction == nil and tag_direction ~= '' then + result_direction = tag_direction + end + + if role_direction ~= nil and tag_direction ~= nil and role_direction ~= tag_direction then + result_direction = nil + print('WARNING: conflict direction in role of way ' .. way:id() .. ' and direction tag in relation ' .. rel:id()) + end + + + -- process superrelations + local super_dir = get_direction_from_superrel(rel, relations) + + -- check if there are data error + if (result_direction ~= nil) and (super_dir ~= nil) and (result_direction ~= super_dir) then + print('ERROR: conflicting relation directions found for way ' .. way:id() .. + ' relation direction is ' .. result_direction .. ' superrelation direction is ' .. super_dir) + result_direction = nil + elseif result_direction == nil then + result_direction = super_dir + end + + result['route_direction'] = result_direction + + if role == 'forward' then + result['route_forward'] = true + elseif role == 'backward' then + result['route_forward'] = false + else + result['route_forward'] = nil + end + + add_extra_data(m) + end + + return result +end + + +function Relations.process_way_refs(way, relations, result) + local parsed_rel_list = {} + local rel_id_list = relations:get_relations(way) + for i, rel_id in ipairs(rel_id_list) do + local rel = relations:relation(rel_id) + parsed_rel_list[i] = Relations.parse_route_relation(rel, way, relations) + end + + -- now process relations data + local matched_refs = nil; + if result.ref then + local match_res = Relations.match_to_ref(parsed_rel_list, result.ref) + + function gen_ref(is_forward) + local ref = '' + for _, m in pairs(match_res) do + if ref ~= '' then + ref = ref .. '; ' + end + + local dir = m.dir.forward + if is_forward == false then + dir = m.dir.backward + end + + if dir then + ref = ref .. m.ref .. ' $' .. dir + else + ref = ref .. m.ref + end + end + + return ref + end + + result.forward_ref = gen_ref(true) + result.backward_ref = gen_ref(false) + end +end + +return Relations diff --git a/profiles/lib-unidb/sequence.lua b/profiles/lib-unidb/sequence.lua new file mode 100644 index 00000000000..9cac7884a36 --- /dev/null +++ b/profiles/lib-unidb/sequence.lua @@ -0,0 +1,10 @@ +-- Sequence of items +-- Ordered, but have to loop through items to check for inclusion. +-- Currently the same as a table. +-- Adds the convenience function append() to append to the sequnce. + +function Sequence(source) + return source +end + +return Sequence \ No newline at end of file diff --git a/profiles/lib-unidb/set.lua b/profiles/lib-unidb/set.lua new file mode 100644 index 00000000000..bbd971911f7 --- /dev/null +++ b/profiles/lib-unidb/set.lua @@ -0,0 +1,23 @@ +-- Set of items +-- Fast check for inclusion, but unordered. +-- +-- Instead of having to do: +-- whitelist = { 'apple'=true, 'cherries'=true, 'melons'=true } +-- +-- you can do: +-- whitelist = Set { 'apple', 'cherries', 'melons' } +-- +-- and then use it as: +-- print( whitelist['cherries'] ) => true + +function Set(source) + set = {} + if source then + for i,v in ipairs(source) do + set[v] = true + end + end + return set +end + +return Set \ No newline at end of file diff --git a/profiles/lib-unidb/tags.lua b/profiles/lib-unidb/tags.lua new file mode 100644 index 00000000000..99b1b2245f7 --- /dev/null +++ b/profiles/lib-unidb/tags.lua @@ -0,0 +1,143 @@ +-- Helpers for searching and parsing tags + +local Tags = {} + +-- return speed unit values for a specific tag. + +function Tags.get_speed_unit_by_key(way, key) + local speed_unit = way:get_value_by_key(key) + + return speed_unit + +end + +-- return [forward,backward] values for a specific tag. +-- e.g. for maxspeed search forward: +-- maxspeed:forward +-- maxspeed +-- and backward: +-- maxspeed:backward +-- maxspeed + +function Tags.get_forward_backward_by_key(way,data,key) + local forward = way:get_value_by_key(key .. ':forward') + local backward = way:get_value_by_key(key .. ':backward') + + if not forward or not backward then + local common = way:get_value_by_key(key) + + if (data.oneway) then + if data.is_forward_oneway then + forward = forward or common + end + if data.is_reverse_oneway then + backward = backward or common + end + else + forward = forward or common + backward = backward or common + end + end + + return forward, backward +end + +-- return [forward,backward] values, searching a +-- prioritized sequence of tags +-- e.g. for the sequence [maxspeed,advisory] search forward: +-- maxspeed:forward +-- maxspeed +-- advisory:forward +-- advisory +-- and for backward: +-- maxspeed:backward +-- maxspeed +-- advisory:backward +-- advisory + +function Tags.get_forward_backward_by_set(way,data,keys) + local forward, backward + for i,key in ipairs(keys) do + if not forward then + forward = way:get_value_by_key(key .. ':forward') + end + if not backward then + backward = way:get_value_by_key(key .. ':backward') + end + if not forward or not backward then + local common = way:get_value_by_key(key) + forward = forward or common + backward = backward or common + end + if forward and backward then + break + end + end + + return forward, backward +end + +-- look through a sequence of keys combined with a prefix +-- e.g. for the sequence [motorcar,motor_vehicle,vehicle] and the prefix 'oneway' search for: +-- oneway:motorcar +-- oneway:motor_vehicle +-- oneway:vehicle + +function Tags.get_value_by_prefixed_sequence(way,seq,prefix) + local v + for i,key in ipairs(seq) do + v = way:get_value_by_key(prefix .. ':' .. key) + if v then + return v + end + end +end + +-- look through a sequence of keys combined with a postfix +-- e.g. for the sequence [motorcar,motor_vehicle,vehicle] and the postfix 'oneway' search for: +-- motorcar:oneway +-- motor_vehicle:oneway +-- vehicle:oneway + +function Tags.get_value_by_postfixed_sequence(way,seq,postfix) + local v + for i,key in ipairs(seq) do + v = way:get_value_by_key(key .. ':' .. postfix) + if v then + return v + end + end +end + +-- check if key-value pairs are set in a way and return a +-- corresponding constant if it is. e.g. for this input: +-- +-- local speeds = { +-- highway = { +-- residential = 20, +-- primary = 40 +-- }, +-- amenity = { +-- parking = 10 +-- } +-- } +-- +-- we would check whether the following key-value combinations +-- are set, and return the corresponding constant: +-- +-- highway = residential => 20 +-- highway = primary => 40 +-- amenity = parking => 10 + +function Tags.get_constant_by_key_value(way,lookup) + for key,set in pairs(lookup) do + local way_value = way:get_value_by_key(key) + for value,t in pairs(set) do + if way_value == value then + return key,value,t + end + end + end +end + +return Tags diff --git a/profiles/lib-unidb/utils.lua b/profiles/lib-unidb/utils.lua new file mode 100644 index 00000000000..b349f340e70 --- /dev/null +++ b/profiles/lib-unidb/utils.lua @@ -0,0 +1,43 @@ +-- Profile functions to implement common algorithms of data processing +-- +-- You can run a selection you find useful in your profile, +-- or do you own processing if/when required. + +Utils = {} + +-- split string 'a; b; c' to table with values ['a', 'b', 'c'] +-- so it use just one separator ';' +function Utils.string_list_tokens(str) + result = {} + local idx = 0 + for s in str.gmatch(str, "([^;]*)") do + if s ~= nil and s ~= '' then + idx = idx + 1 + result[idx] = s:gsub("^%s*(.-)%s*$", "%1") + end + end + + return result +end + +-- same as Utils.StringListTokens, but with many possible separators: +-- ',' | ';' | ' '| '(' | ')' +function Utils.tokenize_common(str) + result = {} + local idx = 0 + for s in str.gmatch(str, "%S+") do + if s ~= nil and s ~= '' then + idx = idx + 1 + result[idx] = s:gsub("^%s*(.-)%s*$", "%1") + end + end + + return result +end + +-- returns true, if string contains a number +function Utils.is_number(str) + return (tonumber(str) ~= nil) +end + +return Utils \ No newline at end of file diff --git a/profiles/lib-unidb/way_handlers.lua b/profiles/lib-unidb/way_handlers.lua new file mode 100644 index 00000000000..befd2d3a122 --- /dev/null +++ b/profiles/lib-unidb/way_handlers.lua @@ -0,0 +1,722 @@ +-- Profile handlers dealing with various aspects of tag parsing +-- +-- You can run a selection you find useful in your profile, +-- or do you own processing if/when required. + + +local get_turn_lanes = require("lib-unidb/guidance").get_turn_lanes +local set_classification = require("lib-unidb/guidance").set_classification +local get_destination = require("lib-unidb/destination").get_destination +local Tags = require('lib-unidb/tags') +local Measure = require("lib-unidb/measure") + +WayHandlers = {} + +-- check that way has at least one tag that could imply routability- +-- we store the checked tags in data, to avoid fetching again later +function WayHandlers.tag_prefetch(profile,way,result,data) + for key,v in pairs(profile.prefetch) do + data[key] = way:get_value_by_key( key ) + end + + return next(data) ~= nil +end + +-- set default mode +function WayHandlers.default_mode(profile,way,result,data) + result.forward_mode = profile.default_mode + result.backward_mode = profile.default_mode +end + +-- handles name, including ref and pronunciation +function WayHandlers.names(profile,way,result,data) + -- parse the remaining tags + local name = way:get_value_by_key("name") + local pronunciation = way:get_value_by_key("name:pronunciation") + local ref = way:get_value_by_key("ref") + local exits = way:get_value_by_key("junction:ref") + + -- Set the name that will be used for instructions + if name then + result.name = name + end + + if ref then + result.ref = canonicalizeStringList(ref, ";") + end + + if pronunciation then + result.pronunciation = pronunciation + end + + if exits then + result.exits = canonicalizeStringList(exits, ";") + end +end + +-- junctions +function WayHandlers.roundabouts(profile,way,result,data) + local junction = way:get_value_by_key("junction"); + + if junction == "roundabout" then + result.roundabout = true + end + + -- See Issue 3361: roundabout-shaped not following roundabout rules. + -- This will get us "At Strausberger Platz do Maneuver X" instead of multiple quick turns. + -- In a new API version we can think of having a separate type passing it through to the user. + if junction == "circular" then + result.circular = true + end +end + +-- determine if this way can be used as a start/end point for routing +function WayHandlers.startpoint(profile,way,result,data) + -- if profile specifies set of allowed start modes, then check for that + -- otherwise require default mode + if profile.allowed_start_modes then + result.is_startpoint = profile.allowed_start_modes[result.forward_mode] == true or + profile.allowed_start_modes[result.backward_mode] == true + else + result.is_startpoint = result.forward_mode == profile.default_mode or + result.backward_mode == profile.default_mode + end + -- highway=service and access tags check + local is_service = data.highway == "service" + if is_service then + if profile.service_access_tag_blacklist[data.forward_access] then + result.is_startpoint = false + end + end +end + +-- handle turn lanes +function WayHandlers.turn_lanes(profile,way,result,data) + local forward, backward = get_turn_lanes(way,data) + + if forward then + result.turn_lanes_forward = forward + end + + if backward then + result.turn_lanes_backward = backward + end +end + +-- set the road classification based on guidance globals configuration +function WayHandlers.classification(profile,way,result,data) + set_classification(data.highway,result,way) +end + +-- handle destination tags +function WayHandlers.destinations(profile,way,result,data) + if data.is_forward_oneway or data.is_reverse_oneway then + local destination = get_destination(way, data.is_forward_oneway) + result.destinations = canonicalizeStringList(destination, ",") + end +end + +-- handling ferries and piers +function WayHandlers.ferries(profile,way,result,data) + local route = data.route + if route then + local route_speed = profile.route_speeds[route] + if route_speed and route_speed > 0 then + local duration = way:get_value_by_key("duration") + if duration and durationIsValid(duration) then + result.duration = math.max( parseDuration(duration), 1 ) + end + result.forward_mode = mode.ferry + result.backward_mode = mode.ferry + result.forward_speed = route_speed + result.backward_speed = route_speed + end + end +end + +-- handling movable bridges +function WayHandlers.movables(profile,way,result,data) + local bridge = data.bridge + if bridge then + local bridge_speed = profile.bridge_speeds[bridge] + if bridge_speed and bridge_speed > 0 then + local capacity_car = way:get_value_by_key("capacity:car") + if capacity_car ~= 0 then + result.forward_mode = profile.default_mode + result.backward_mode = profile.default_mode + local duration = way:get_value_by_key("duration") + if duration and durationIsValid(duration) then + result.duration = math.max( parseDuration(duration), 1 ) + else + result.forward_speed = bridge_speed + result.backward_speed = bridge_speed + end + end + end + end +end + +-- service roads +function WayHandlers.service(profile,way,result,data) + local service = way:get_value_by_key("service") + if service then + -- Set don't allow access to certain service roads + if profile.service_tag_forbidden[service] then + result.forward_mode = mode.inaccessible + result.backward_mode = mode.inaccessible + return false + end + end +end + +-- all lanes restricted to hov vehicles? +function WayHandlers.has_all_designated_hov_lanes(lanes) + if not lanes then + return false + end + -- This gmatch call effectively splits the string on | chars. + -- we append an extra | to the end so that we can match the final part + for lane in (lanes .. '|'):gmatch("([^|]*)|") do + if lane and lane ~= "designated" then + return false + end + end + return true +end + +-- handle high occupancy vehicle tags +function WayHandlers.hov(profile,way,result,data) + -- respect user-preference for HOV + if not profile.avoid.hov_lanes then + return + end + + local hov = way:get_value_by_key("hov") + if "designated" == hov then + result.forward_restricted = true + result.backward_restricted = true + end + + data.hov_lanes_forward, data.hov_lanes_backward = Tags.get_forward_backward_by_key(way,data,'hov:lanes') + local all_hov_forward = WayHandlers.has_all_designated_hov_lanes(data.hov_lanes_forward) + local all_hov_backward = WayHandlers.has_all_designated_hov_lanes(data.hov_lanes_backward) + + -- in this case we will use turn penalties instead of filtering out + if profile.properties.weight_name == 'routability' then + if (all_hov_forward) then + result.forward_restricted = true + end + if (all_hov_backward) then + result.backward_restricted = true + end + return + end + + -- filter out ways where all lanes are hov only + if all_hov_forward then + result.forward_mode = mode.inaccessible + end + if all_hov_backward then + result.backward_mode = mode.inaccessible + end +end + + +-- set highway and access classification by user preference +function WayHandlers.way_classification_for_turn(profile,way,result,data) + local highway = way:get_value_by_key("highway") + local access = way:get_value_by_key("access") + + if highway and profile.highway_turn_classification[highway] then + assert(profile.highway_turn_classification[highway] < 16, "highway_turn_classification must be smaller than 16") + result.highway_turn_classification = profile.highway_turn_classification[highway] + end + if access and profile.access_turn_classification[access] then + assert(profile.access_turn_classification[access] < 16, "access_turn_classification must be smaller than 16") + result.access_turn_classification = profile.access_turn_classification[access] + end +end + + +-- check accessibility by traversing our access tag hierarchy +function WayHandlers.access(profile,way,result,data) + data.forward_access, data.backward_access = + Tags.get_forward_backward_by_set(way,data,profile.access_tags_hierarchy) + + -- only allow a subset of roads to be treated as restricted + if profile.restricted_highway_whitelist[data.highway] then + if profile.restricted_access_tag_list[data.forward_access] then + result.forward_restricted = true + end + + if profile.restricted_access_tag_list[data.backward_access] then + result.backward_restricted = true + end + end + + -- blacklist access tags that aren't marked as restricted + if profile.access_tag_blacklist[data.forward_access] and not result.forward_restricted then + result.forward_mode = mode.inaccessible + end + + if profile.access_tag_blacklist[data.backward_access] and not result.backward_restricted then + result.backward_mode = mode.inaccessible + end + + if result.forward_mode == mode.inaccessible and result.backward_mode == mode.inaccessible then + return false + end +end + +-- handle speed (excluding maxspeed) +function WayHandlers.speed(profile,way,result,data) + if result.forward_speed ~= -1 then + return -- abort if already set, eg. by a route + end + + local key,value,speed = Tags.get_constant_by_key_value(way,profile.speeds) + + if speed then + -- set speed by way type + result.forward_speed = speed + result.backward_speed = speed + else + -- Set the avg speed on ways that are marked accessible + if profile.access_tag_whitelist[data.forward_access] then + result.forward_speed = profile.default_speed + elseif data.forward_access and not profile.access_tag_blacklist[data.forward_access] then + result.forward_speed = profile.default_speed -- fallback to the avg speed if access tag is not blacklisted + elseif not data.forward_access and data.backward_access then + result.forward_mode = mode.inaccessible + end + + if profile.access_tag_whitelist[data.backward_access] then + result.backward_speed = profile.default_speed + elseif data.backward_access and not profile.access_tag_blacklist[data.backward_access] then + result.backward_speed = profile.default_speed -- fallback to the avg speed if access tag is not blacklisted + elseif not data.backward_access and data.forward_access then + result.backward_mode = mode.inaccessible + end + end + + if result.forward_speed == -1 and result.backward_speed == -1 and result.duration <= 0 then + return false + end +end + +-- add class information +function WayHandlers.classes(profile,way,result,data) + if not profile.classes then + return + end + + local allowed_classes = Set {} + for k, v in pairs(profile.classes) do + allowed_classes[v] = true + end + + local forward_toll, backward_toll = Tags.get_forward_backward_by_key(way, data, "toll") + local forward_route, backward_route = Tags.get_forward_backward_by_key(way, data, "route") + local tunnel = way:get_value_by_key("tunnel") + + if allowed_classes["tunnel"] and tunnel and tunnel ~= "no" then + result.forward_classes["tunnel"] = true + result.backward_classes["tunnel"] = true + end + + if allowed_classes["toll"] and forward_toll == "yes" then + result.forward_classes["toll"] = true + end + if allowed_classes["toll"] and backward_toll == "yes" then + result.backward_classes["toll"] = true + end + + if allowed_classes["ferry"] and forward_route == "ferry" then + result.forward_classes["ferry"] = true + end + if allowed_classes["ferry"] and backward_route == "ferry" then + result.backward_classes["ferry"] = true + end + + if allowed_classes["restricted"] and result.forward_restricted then + result.forward_classes["restricted"] = true + end + if allowed_classes["restricted"] and result.backward_restricted then + result.backward_classes["restricted"] = true + end + + if allowed_classes["motorway"] and (data.highway == "motorway" or data.highway == "motorway_link") then + result.forward_classes["motorway"] = true + result.backward_classes["motorway"] = true + end +end + +-- reduce speed on bad surfaces +function WayHandlers.surface(profile,way,result,data) + local surface = way:get_value_by_key("surface") + local tracktype = way:get_value_by_key("tracktype") + local smoothness = way:get_value_by_key("smoothness") + + if surface and profile.surface_speeds[surface] then + result.forward_speed = math.min(profile.surface_speeds[surface], result.forward_speed) + result.backward_speed = math.min(profile.surface_speeds[surface], result.backward_speed) + end + if tracktype and profile.tracktype_speeds[tracktype] then + result.forward_speed = math.min(profile.tracktype_speeds[tracktype], result.forward_speed) + result.backward_speed = math.min(profile.tracktype_speeds[tracktype], result.backward_speed) + end + if smoothness and profile.smoothness_speeds[smoothness] then + result.forward_speed = math.min(profile.smoothness_speeds[smoothness], result.forward_speed) + result.backward_speed = math.min(profile.smoothness_speeds[smoothness], result.backward_speed) + end +end + +-- scale speeds to get better average driving times +function WayHandlers.penalties(profile,way,result,data) + -- heavily penalize a way tagged with all HOV lanes + -- in order to only route over them if there is no other option + local service_penalty = 1.0 + local service = way:get_value_by_key("service") + if service and profile.service_penalties[service] then + service_penalty = profile.service_penalties[service] + end + + local width_penalty = 1.0 + local width = math.huge + local lanes = math.huge + local width_string = way:get_value_by_key("width") + if width_string and tonumber(width_string:match("%d*")) then + width = tonumber(width_string:match("%d*")) + end + + local lanes_string = way:get_value_by_key("lanes") + if lanes_string and tonumber(lanes_string:match("%d*")) then + lanes = tonumber(lanes_string:match("%d*")) + end + + local is_bidirectional = result.forward_mode ~= mode.inaccessible and + result.backward_mode ~= mode.inaccessible + + if width <= 3 or (lanes <= 1 and is_bidirectional) then + width_penalty = 0.5 + end + + -- Handle high frequency reversible oneways (think traffic signal controlled, changing direction every 15 minutes). + -- Scaling speed to take average waiting time into account plus some more for start / stop. + local alternating_penalty = 1.0 + if data.oneway == "alternating" then + alternating_penalty = 0.4 + end + + local sideroad_penalty = 1.0 + data.sideroad = way:get_value_by_key("side_road") + if "yes" == data.sideroad or "rotary" == data.sideroad then + sideroad_penalty = profile.side_road_multiplier + end + + local forward_penalty = math.min(service_penalty, width_penalty, alternating_penalty, sideroad_penalty) + local backward_penalty = math.min(service_penalty, width_penalty, alternating_penalty, sideroad_penalty) + + if profile.properties.weight_name == 'routability' then + if result.forward_speed > 0 then + result.forward_rate = (result.forward_speed * forward_penalty) / 3.6 + end + if result.backward_speed > 0 then + result.backward_rate = (result.backward_speed * backward_penalty) / 3.6 + end + if result.duration > 0 then + result.weight = result.duration / forward_penalty + end + end +end + +-- maxspeed and advisory maxspeed +function WayHandlers.maxspeed(profile,way,result,data) + local keys = Sequence { 'maxspeed:advisory', 'maxspeed', 'source:maxspeed', 'maxspeed:type' } + local forward, backward = Tags.get_forward_backward_by_set(way,data,keys) + + -- If speed_unit == 'M', then it's mile per hour + -- If speed_unit == 'K', then it's kilometer per hour + local speed_unit = Tags.get_speed_unit_by_key(way, 'speed_unit') + + forward = WayHandlers.parse_maxspeed(forward, profile, speed_unit) + backward = WayHandlers.parse_maxspeed(backward, profile, speed_unit) + + if forward and forward > 0 then + result.forward_speed = forward * profile.speed_reduction + end + + if backward and backward > 0 then + result.backward_speed = backward * profile.speed_reduction + end +end + +function WayHandlers.parse_maxspeed(source, profile, speed_unit) + if not source then + return 0 + end + + local n = Measure.get_max_speed(source, speed_unit) + if not n then + -- parse maxspeed like FR:urban + source = string.lower(source) + n = profile.maxspeed_table[source] + if not n then + local highway_type = string.match(source, "%a%a:(%a+)") + n = profile.maxspeed_table_default[highway_type] + if not n then + n = 0 + end + end + end + return n +end + +-- handle maxheight tags +function WayHandlers.handle_height(profile,way,result,data) + local keys = Sequence { 'maxheight:physical', 'maxheight' } + local forward, backward = Tags.get_forward_backward_by_set(way,data,keys) + forward = Measure.get_max_height(forward,way) + backward = Measure.get_max_height(backward,way) + + if forward and forward < profile.vehicle_height then + result.forward_mode = mode.inaccessible + end + + if backward and backward < profile.vehicle_height then + result.backward_mode = mode.inaccessible + end +end + +-- handle maxwidth tags +function WayHandlers.handle_width(profile,way,result,data) + local keys = Sequence { 'maxwidth:physical', 'maxwidth', 'width', 'est_width' } + local forward, backward = Tags.get_forward_backward_by_set(way,data,keys) + local narrow = way:get_value_by_key('narrow') + + if ((forward and forward == 'narrow') or (narrow and narrow == 'yes')) and profile.vehicle_width > 2.2 then + result.forward_mode = mode.inaccessible + elseif forward then + forward = Measure.get_max_width(forward) + if forward and forward <= profile.vehicle_width then + result.forward_mode = mode.inaccessible + end + end + + if ((backward and backward == 'narrow') or (narrow and narrow == 'yes')) and profile.vehicle_width > 2.2 then + result.backward_mode = mode.inaccessible + elseif backward then + backward = Measure.get_max_width(backward) + if backward and backward <= profile.vehicle_width then + result.backward_mode = mode.inaccessible + end + end +end + +-- handle maxweight tags +function WayHandlers.handle_weight(profile,way,result,data) + local keys = Sequence { 'maxweight' } + local forward, backward = Tags.get_forward_backward_by_set(way,data,keys) + forward = Measure.get_max_weight(forward) + backward = Measure.get_max_weight(backward) + + if forward and forward < profile.vehicle_weight then + result.forward_mode = mode.inaccessible + end + + if backward and backward < profile.vehicle_weight then + result.backward_mode = mode.inaccessible + end +end + +-- handle maxlength tags +function WayHandlers.handle_length(profile,way,result,data) + local keys = Sequence { 'maxlength' } + local forward, backward = Tags.get_forward_backward_by_set(way,data,keys) + forward = Measure.get_max_length(forward) + backward = Measure.get_max_length(backward) + + if forward and forward < profile.vehicle_length then + result.forward_mode = mode.inaccessible + end + + if backward and backward < profile.vehicle_length then + result.backward_mode = mode.inaccessible + end +end + +-- handle oneways tags +function WayHandlers.oneway(profile,way,result,data) + if not profile.oneway_handling then + return + end + + local oneway + if profile.oneway_handling == true then + oneway = Tags.get_value_by_prefixed_sequence(way,profile.restrictions,'oneway') or way:get_value_by_key("oneway") + elseif profile.oneway_handling == 'specific' then + oneway = Tags.get_value_by_prefixed_sequence(way,profile.restrictions,'oneway') + elseif profile.oneway_handling == 'conditional' then + -- Following code assumes that `oneway` and `oneway:conditional` tags have opposite values and takes weakest (always `no`). + -- So if we will have: + -- oneway=yes, oneway:conditional=no @ (condition1) + -- oneway=no, oneway:conditional=yes @ (condition2) + -- condition1 will be always true and condition2 will be always false. + if way:get_value_by_key("oneway:conditional") then + oneway = "no" + else + oneway = Tags.get_value_by_prefixed_sequence(way,profile.restrictions,'oneway') or way:get_value_by_key("oneway") + end + end + + data.oneway = oneway + + if oneway == "-1" then + data.is_reverse_oneway = true + result.forward_mode = mode.inaccessible + elseif oneway == "yes" or + oneway == "1" or + oneway == "true" then + data.is_forward_oneway = true + result.backward_mode = mode.inaccessible + elseif profile.oneway_handling == true then + local junction = way:get_value_by_key("junction") + if data.highway == "motorway" or + junction == "roundabout" or + junction == "circular" then + if oneway ~= "no" then + -- implied oneway + data.is_forward_oneway = true + result.backward_mode = mode.inaccessible + end + end + end +end + +function WayHandlers.weights(profile,way,result,data) + if profile.properties.weight_name == 'distance' then + result.weight = -1 + -- set weight rates to 1 for the distance weight, edge weights are distance / rate + if (result.forward_mode ~= mode.inaccessible and result.forward_speed > 0) then + result.forward_rate = 1 + end + if (result.backward_mode ~= mode.inaccessible and result.backward_speed > 0) then + result.backward_rate = 1 + end + end +end + + +-- handle general avoid rules + +function WayHandlers.avoid_ways(profile,way,result,data) + if profile.avoid[data.highway] then + return false + end +end + +-- handle various that can block access +function WayHandlers.blocked_ways(profile,way,result,data) + + -- areas + if profile.avoid.area and way:get_value_by_key("area") == "yes" then + return false + end + + -- toll roads + if profile.avoid.toll and way:get_value_by_key("toll") == "yes" then + return false + end + + -- don't route over steps + if profile.avoid.steps and data.highway == "steps" then + return false + end + + -- construction + -- TODO if highway is valid then we shouldn't check railway, and vica versa + if profile.avoid.construction and (data.highway == 'construction' or way:get_value_by_key('railway') == 'construction') then + return false + end + + -- In addition to the highway=construction tag above handle the construction=* tag + -- http://wiki.openstreetmap.org/wiki/Key:construction + -- https://taginfo.openstreetmap.org/keys/construction#values + if profile.avoid.construction then + local construction = way:get_value_by_key('construction') + + -- Of course there are negative tags to handle, too + if construction and not profile.construction_whitelist[construction] then + return false + end + end + + -- Not only are there multiple construction tags there is also a proposed=* tag. + -- http://wiki.openstreetmap.org/wiki/Key:proposed + -- https://taginfo.openstreetmap.org/keys/proposed#values + if profile.avoid.proposed and way:get_value_by_key('proposed') then + return false + end + + -- Reversible oneways change direction with low frequency (think twice a day): + -- do not route over these at all at the moment because of time dependence. + -- Note: alternating (high frequency) oneways are handled below with penalty. + if profile.avoid.reversible and way:get_value_by_key("oneway") == "reversible" then + return false + end + + -- impassables + if profile.avoid.impassable then + if way:get_value_by_key("impassable") == "yes" then + return false + end + + if way:get_value_by_key("status") == "impassable" then + return false + end + end +end + +function WayHandlers.driving_side(profile, way, result, data) + local driving_side = way:get_value_by_key('driving_side') + if driving_side == nil then + driving_side = way:get_location_tag('driving_side') + end + + if driving_side == 'left' then + result.is_left_hand_driving = true + elseif driving_side == 'right' then + result.is_left_hand_driving = false + else + result.is_left_hand_driving = profile.properties.left_hand_driving + end +end + + +-- Call a sequence of handlers, aborting in case a handler returns false. Example: +-- +-- handlers = Sequence { +-- WayHandlers.tag_prefetch, +-- WayHandlers.default_mode, +-- WayHandlers.blocked_ways, +-- WayHandlers.access, +-- WayHandlers.speed, +-- WayHandlers.names +-- } +-- +-- WayHandlers.run(handlers,way,result,data,profile) +-- +-- Each method in the list will be called on the WayHandlers object. +-- All handlers must accept the parameteres (profile, way, result, data, relations) and return false +-- if the handler chain should be aborted. +-- To ensure the correct order of method calls, use a Sequence of handler names. + +function WayHandlers.run(profile, way, result, data, handlers, relations) + for i,handler in ipairs(handlers) do + if handler(profile, way, result, data, relations) == false then + return false + end + end +end + +return WayHandlers