Closed juliusfriedman closed 4 years ago
I took another shot at my train example this time attempting to filter our unusable track
-- train globals
name = "train"
vehicle_types = { "vehicle", "motor_vehicle", "motorcar", "train" }
constraints = { "maxweight", "maxwidth" }
minspeed = 30
maxspeed = 200
-- default speed profiles
speed_profile = {
["rail"] = 120,
["light_rail"] = 120,
["railway"] = 120,
["railway_link"] = 120,
["motorway"] = 120,
["motorway_link"] = 120,
["trunk"] = 90,
["trunk_link"] = 90,
["primary"] = 90,
["primary_link"] = 90,
["secondary"] = 70,
["secondary_link"] = 70,
["tertiary"] = 70,
["tertiary_link"] = 70,
["unclassified"] = 50,
["residential"] = 50,
["service"] = 30,
["services"] = 30,
["road"] = 30,
["track"] = 30,
["living_street"] = 5,
["ferry"] = 5,
["movable"] = 5,
["shuttle_train"] = 10,
["default"] = 10
}
-- default access values
access_values = {
["private"] = true,
["yes"] = true,
["no"] = true,
["permissive"] = true,
["destination"] = true,
["customers"] = false,
["designated"] = true,
["public"] = true,
["delivery"] = true,
["use_sidepath"] = false
}
-- whitelists for profile and meta
profile_whitelist = {
"highway",
"rail",
"railway",
"ligh_rail",
"oneway",
"motorcar",
"motor_vehicle",
"vehicle",
"access",
"maxspeed",
"junction",
"route",
"barrier"
}
meta_whitelist = {
"name",
"bridge",
"tunnel"
}
-- profile definitions linking a function to a profile
profiles = {
{
name = "",
function_name = "factor_and_speed",
metric = "time"
},
{
name = "shortest",
function_name = "factor_and_speed",
metric = "distance",
},
{
name = "classifications",
function_name = "factor_and_speed_classifications",
metric = "custom"
}
}
barriers = {
["gate"] = true,
["bollard"] = true,
["fence"] = true
}
--tags of rail which are not able to be traversed
unusable = {
["disused"] = true,
["razed"] = true,
["dismantled"] = true,
["demolished"] = true,
["abandoned"] = true,
["tram"] = true,
["proposed"] = true,
}
-- interprets node restrictions
function node_restriction (attributes, results)
results.attributes_to_keep = {}
local barrier = attributes.barrier
if barrier == nil then
return
end
local barrier_type = barriers[barrier]
if barrier_type == nil then
return
end
results.attributes_to_keep.barrier = barrier
results.vehicle = "train"
end
-- interprets access tags
function can_access (attributes, result)
local last_access = nil
local access = access_values[attributes.access]
if access != nil then
result.attributes_to_keep.access = true
last_access = access
end
for i=0, 10 do
local access_key_key = vehicle_types[i]
local access_key = attributes[access_key_key]
if access_key then
access = access_values[access_key]
if access != nil then
result.attributes_to_keep[access_key_key] = true
last_access = access
end
end
end
return last_access
end
-- turns a oneway tag value into a direction
function is_oneway (attributes, name)
local oneway = attributes[name]
if oneway != nil then
if oneway == "yes" or
oneway == "true" or
oneway == "1" then
return 1
end
if oneway == "-1" then
return 2
end
end
return nil
end
-- the main function turning attributes into a factor_and_speed and a tag whitelist
function factor_and_speed (attributes, result)
local highway = attributes.highway
result.speed = 0
result.direction = 0
result.canstop = true
result.attributes_to_keep = {}
-- set to rail when route is train or rail
local route = attributes.route;
if route == "train" or
route == "rail" then
highway = "rail"
result.attributes_to_keep.route = highway
end
-- do not consider unusable track
if unusable[attributes.railway] then
return
end
-- get default speed profiles
local highway_speed = speed_profile[highway]
if highway_speed then
result.speed = highway_speed * 0.75
result.direction = 0
result.canstop = true
result.attributes_to_keep.highway = highway
if highway == "railway" or
highway == "railway_link" or
highway == "rail" then
result.canstop = false
end
else
return
end
-- interpret access tags
if can_access (attributes, result) == false then
result.speed = 0
result.direction = 0
result.canstop = true
return
end
-- get maxspeed if any.
if attributes.maxspeed then
local speed = itinero.parsespeed (attributes.maxspeed)
if speed then
result.speed = speed * 0.75
result.attributes_to_keep.maxspeed = true
end
end
-- get directional information
local junction = attributes.junction
if junction == "roundabout" then
result.direction = 1
result.attributes_to_keep.junction = true
end
local direction = is_oneway (attributes, "oneway")
if direction != nil then
result.direction = direction
result.attributes_to_keep.oneway = true
end
end
-- multiplication factors per classification
classifications_factors = {
["rail"] = 10,
["railway"] = 10,
["light_rail"] = 10,
["motorway"] = 10,
["motorway_link"] = 10,
["trunk"] = 9,
["trunk_link"] = 9,
["primary"] = 8,
["primary_link"] = 8,
["secondary"] = 7,
["secondary_link"] = 7,
["tertiary"] = 6,
["tertiary_link"] = 6,
["unclassified"] = 5,
["residential"] = 5
}
-- the classifications function for the classifications profile
function factor_and_speed_classifications (attributes, result)
factor_and_speed (attributes, result)
if result.speed == 0 then
return
end
result.factor = 1.0 / (result.speed / 3.6)
local classification_factor = classifications_factors[attributes.highway]
if classification_factor != nil then
result.factor = result.factor / classification_factor
else
result.factor = result.factor / 4
end
end
-- instruction generators
instruction_generators = {
{
applies_to = "", -- applies to all profiles when empty
generators = {
{
name = "start",
function_name = "get_start"
},
{
name = "stop",
function_name = "get_stop"
},
{
name = "roundabout",
function_name = "get_roundabout"
},
{
name = "turn",
function_name = "get_turn"
}
}
}
}
-- gets the first instruction
function get_start (route_position, language_reference, instruction)
if route_position.is_first() then
local direction = route_position.direction()
instruction.text = itinero.format(language_reference.get("Start {0}."), language_reference.get(direction));
instruction.shape = route_position.shape
return 1
end
return 0
end
-- gets the last instruction
function get_stop (route_position, language_reference, instruction)
if route_position.is_last() then
instruction.text = language_reference.get("Arrived at destination.");
instruction.shape = route_position.shape
return 1
end
return 0
end
function contains (attributes, key, value)
if attributes then
return localvalue == attributes[key];
end
end
-- gets a roundabout instruction
function get_roundabout (route_position, language_reference, instruction)
if route_position.attributes.junction == "roundabout" and
(not route_position.is_last()) then
local attributes = route_position.next().attributes
if attributes.junction then
else
local exit = 1
local count = 1
local previous = route_position.previous()
while previous and previous.attributes.junction == "roundabout" do
local branches = previous.branches
if branches then
branches = branches.get_traversable()
if branches.count > 0 then
exit = exit + 1
end
end
count = count + 1
previous = previous.previous()
end
instruction.text = itinero.format(language_reference.get("Take the {0}th exit at the next roundabout."), "" .. exit)
if exit == 1 then
instruction.text = itinero.format(language_reference.get("Take the first exit at the next roundabout."))
elseif exit == 2 then
instruction.text = itinero.format(language_reference.get("Take the second exit at the next roundabout."))
elseif exit == 3 then
instruction.text = itinero.format(language_reference.get("Take the third exit at the next roundabout."))
end
instruction.type = "roundabout"
instruction.shape = route_position.shape
return count
end
end
return 0
end
-- gets a turn
function get_turn (route_position, language_reference, instruction)
local relative_direction = route_position.relative_direction().direction
local turn_relevant = false
local branches = route_position.branches
if branches then
branches = branches.get_traversable()
if relative_direction == "straighton" and
branches.count >= 2 then
turn_relevant = true -- straight on at cross road
end
if relative_direction != "straighton" and
branches.count > 0 then
turn_relevant = true -- an actual normal turn
end
end
if relative_direction == "unknown" then
turn_relevant = false -- turn could not be calculated.
end
if turn_relevant then
local next = route_position.next()
local name = nil
if next then
name = next.attributes.name
end
if name then
instruction.text = itinero.format(language_reference.get("Go {0} on {1}."),
language_reference.get(relative_direction), name)
instruction.shape = route_position.shape
else
instruction.text = itinero.format(language_reference.get("Go {0}."),
language_reference.get(relative_direction))
instruction.shape = route_position.shape
end
return 1
end
return 0
end
I am going by what I find at https://wiki.openstreetmap.org/wiki/Railways https://wiki.openstreetmap.org/wiki/Tag:route%3Dtrain#Network_by_countries
Overall it seems that there needs to be 3 or 4 different profiles
1 for Train 1 for Subway 1 for Tram 1 for Monorail (unless covered by tram)
Train is most important for me and my current project but I imagine the others would be useful as well and if you can help me with Train I can try to help with the rest.
that osm.pbf file was obtained from http://download.geofabrik.de/north-america.html
I filter the osm.pbf file by using the following command:
osmium tags-filter -o na-rail-latest.osm.pbf north-america-latest.osm.pbf nw/railway --progress
This leaves me only nodes and ways with railway so I should be able to create a LUA profile which can be routed on using a custom profile, I just need the profile help to get it to work as I have already tested by re-writing from railway to highway and that works fine.
Any assistance you can provide would be super helpful!
Thanks in advance!
Furthermore I think C# profiles are broken or simply poorly documented
Consider this
public class Train : Vehicle
{
static FactorAndSpeed DefaultFactorAndSpeed = new FactorAndSpeed()
{
Constraints = new[] { 1.0f },
SpeedFactor = 1,
Direction = 1,
Value = 1
};
public override string Name => nameof(Train);
public override FactorAndSpeed FactorAndSpeed(IAttributeCollection attributes, Whitelist whitelist)
{
return DefaultFactorAndSpeed;
}
}
When attempting to use this implementation I get:
System.IndexOutOfRangeException: 'Index was outside the bounds of the array.'
" at Itinero.IO.Osm.Normalizer.AttributeCollectionExtensions.NormalizeAccess(IAttributeCollection tags, IAttributeCollection profileTags, Boolean defaultAccess, String[] accessTags) in ...`
This is because the line:
profileTags.AddOrReplace(accessTags[accessTags.Length - 1], "no");
Has 0 values and I am not sure how to populate accessTags in the attribute collection.
Even something like this doesn't work:
public class Train : Vehicle
{
public Train()
{
MetaWhiteList.Add("railway");
MetaWhiteList.Add("light_rail");
ProfileWhiteList.Add("");
ProfileWhiteList.Add("rail");
}
static FactorAndSpeed DefaultFactorAndSpeed = new FactorAndSpeed()
{
Constraints = new[] { 1.0f },
SpeedFactor = 1,
Direction = 1,
Value = 1
};
static string[] VehicleTypesArray = new[] { "train" };
public override string Name => nameof(Train);
public override string[] VehicleTypes => VehicleTypesArray;
public override FactorAndSpeed FactorAndSpeed(IAttributeCollection attributes, Whitelist whitelist)
{
return DefaultFactorAndSpeed;
}
}
Doesn't work because the Fastest
method passes string,empty as the value which I have no idea how to populate from the C# class. This results in KeyNotFoundException because _profiles
of the Vehicle class Train
has 0 length and I have no idea how to populate that from the given examples.
I get around this by registering profiles in the constructor of Train
like so:
Register(new Profile("shortest", ProfileMetric.DistanceInMeters, VehicleTypes, null, this));
Register(new Profile(string.Empty, ProfileMetric.TimeInSeconds, VehicleTypes, null, this));
But surprisingly enough I can't seem to find any RouterPoints or ways so I am cornfused.
Any help you would be able to provide would be great!
Thank you!
Looking at the linked issue their LUA seems like it works to route on trains, I modified it slightly to not consider rails which are demolished or otherwise.
This seems to work but I am wondering if I need to also handle spur, siding etc or if this will suffice.
If this is good enough than why was this removed from Itinero?
-- train globals
name = "train"
vehicle_types = { "vehicle", "motor_vehicle", "motorcar" }
constraints = { "maxweight", "maxwidth" }
minspeed = 30
maxspeed = 200
-- default speed profiles
speed_profile = {
["rail"] = 60,
["default"] = 60
}
-- default access values
access_values = {
["private"] = false,
["yes"] = true,
["no"] = false,
["permissive"] = true,
["destination"] = true,
["customers"] = false,
["designated"] = true,
["public"] = true,
["delivery"] = true,
["use_sidepath"] = false
}
--tags of rail which are not able to be traversed
unusable = {
["disused"] = true,
["razed"] = true,
["dismantled"] = true,
["demolished"] = true,
["abandoned"] = true,
["tram"] = true,
["proposed"] = true,
}
-- whitelists for profile and meta
profile_whitelist = {
"railway"
}
meta_whitelist = {
"name"
}
-- profile definitions linking a function to a profile
profiles = {
{
name = "",
function_name = "factor_and_speed",
metric = "time"
},
{
name = "shortest",
function_name = "factor_and_speed",
metric = "distance",
},
{
name = "classifications",
function_name = "factor_and_speed_classifications",
metric = "custom"
}
}
-- interprets access tags
function can_access (attributes, result)
local last_access = nil
local access = access_values[attributes.access]
if access != nil then
result.attributes_to_keep.access = true
last_access = access
end
for i=0, 10 do
local access_key_key = vehicle_types[i]
local access_key = attributes[access_key_key]
if access_key then
access = access_values[access_key]
if access != nil then
result.attributes_to_keep[access_key_key] = true
last_access = access
end
end
end
return last_access
end
-- turns a oneway tag value into a direction
function is_oneway (attributes, name)
local oneway = attributes[name]
if oneway != nil then
if oneway == "yes" or
oneway == "true" or
oneway == "1" then
return 1
end
if oneway == "-1" then
return 2
end
end
return nil
end
-- the main function turning attributes into a factor_and_speed and a tag whitelist
function factor_and_speed (attributes, result)
local railway = attributes.railway
result.speed = 0
result.direction = 0
result.canstop = true
result.attributes_to_keep = {}
-- do not consider unusable track
if unusable[attributes.railway] then
return
end
-- get default speed profiles
local railway_speed = speed_profile[railway]
if railway_speed then
result.speed = railway_speed
result.direction = 0
result.canstop = true
result.attributes_to_keep.railway = railway
if railway == "motorway" or
railway == "motorway_link" then
result.canstop = false
end
else
return
end
-- interpret access tags
if can_access (attributes, result) == false then
result.speed = 0
result.direction = 0
result.canstop = true
return
end
-- get maxspeed if any.
if attributes.maxspeed then
local speed = itinero.parsespeed (attributes.maxspeed)
if speed then
result.speed = speed * 0.75
result.attributes_to_keep.maxspeed = true
end
end
-- get maxweight and maxwidth constraints if any
local maxweight = 0
local maxwidth = 0
if attributes.maxweight then
maxweight = itinero.parseweight (attributes.maxweight)
end
if attributes.maxwidth then
maxwidth = itinero.parseweight (attributes.maxwidth)
end
if maxwidth != 0 or maxweight != 0 then
result.constraints = { maxweight, maxwidth }
result.attributes_to_keep.maxweight = true
result.attributes_to_keep.maxwidth = true
end
-- get directional information
local junction = attributes.junction
if junction == "roundabout" then
result.direction = 1
end
local direction = is_oneway (attributes, "oneway")
if direction != nil then
result.direction = direction
end
end
-- multiplication factors per classification
classifications_factors = {
["rail"] = 5
}
-- the classifications function for the classifications profile
function factor_and_speed_classifications (attributes, result)
factor_and_speed (attributes, result)
if result.speed == 0 then
return
end
result.factor = 1.0 / (result.speed / 3.6)
local classification_factor = classifications_factors[attributes.railway]
if classification_factor != nil then
result.factor = result.factor / classification_factor
else
result.factor = result.factor / 4
end
end
-- instruction generators
instruction_generators = {
{
applies_to = "", -- applies to all profiles when empty
generators = {
{
name = "start",
function_name = "get_start"
},
{
name = "stop",
function_name = "get_stop"
},
{
name = "roundabout",
function_name = "get_roundabout"
},
{
name = "turn",
function_name = "get_turn"
}
}
}
}
-- gets the first instruction
function get_start (route_position, language_reference, instruction)
if route_position.is_first() then
local direction = route_position.direction()
instruction.text = itinero.format(language_reference.get("Start {0}."), language_reference.get(direction));
instruction.shape = route_position.shape
return 1
end
return 0
end
-- gets the last instruction
function get_stop (route_position, language_reference, instruction)
if route_position.is_last() then
instruction.text = language_reference.get("Arrived at destination.");
instruction.shape = route_position.shape
return 1
end
return 0
end
function contains (attributes, key, value)
if attributes then
return localvalue == attributes[key];
end
end
-- gets a roundabout instruction
function get_roundabout (route_position, language_reference, instruction)
if route_position.attributes.junction == "roundabout" and
(not route_position.is_last()) then
local attributes = route_position.next().attributes
if attributes.junction then
else
local exit = 1
local count = 1
local previous = route_position.previous()
while previous and previous.attributes.junction == "roundabout" do
local branches = previous.branches
if branches then
branches = branches.get_traversable()
if branches.count > 0 then
exit = exit + 1
end
end
count = count + 1
previous = previous.previous()
end
instruction.text = itinero.format(language_reference.get("Take the {0}th exit at the next roundabout."), "" .. exit)
if exit == 1 then
instruction.text = itinero.format(language_reference.get("Take the first exit at the next roundabout."))
elseif exit == 2 then
instruction.text = itinero.format(language_reference.get("Take the second exit at the next roundabout."))
elseif exit == 3 then
instruction.text = itinero.format(language_reference.get("Take the third exit at the next roundabout."))
end
instruction.type = "roundabout"
instruction.shape = route_position.shape
return count
end
end
return 0
end
-- gets a turn
function get_turn (route_position, language_reference, instruction)
local relative_direction = route_position.relative_direction().direction
local turn_relevant = false
local branches = route_position.branches
if branches then
branches = branches.get_traversable()
if relative_direction == "straighton" and
branches.count >= 2 then
turn_relevant = true -- straight on at cross road
end
if relative_direction != "straighton" and
branches.count > 0 then
turn_relevant = true -- an actual normal turn
end
end
if turn_relevant then
local next = route_position.next()
local name = nil
if next then
name = next.attributes.name
end
if name then
instruction.text = itinero.format(language_reference.get("Go {0} on {1}."),
language_reference.get(relative_direction), name)
instruction.shape = route_position.shape
else
instruction.text = itinero.format(language_reference.get("Go {0}."),
language_reference.get(relative_direction))
instruction.shape = route_position.shape
end
return 1
end
return 0
end
Here is a slightly updated version which also allows crossovers and spurs and sidings to be stopped on I think train.lua.txt
And here is the C# code complete which should be able to at least get a position
public class Train : Profiles.Vehicle
{
public Train()
{
Register(new Profile("shortest", ProfileMetric.DistanceInMeters, VehicleTypes, null, this));
Register(new Profile(string.Empty, ProfileMetric.TimeInSeconds, VehicleTypes, null, this));
MetaWhiteList.Add("name");
ProfileWhiteList.Add("railway");
}
static FactorAndSpeed DefaultFactorAndSpeed = new FactorAndSpeed()
{
Constraints = new[] { 1.0f },
SpeedFactor = 1,
Direction = 1,
Value = 1
};
static string[] VehicleTypesArray = new[] { "vehicle", "motor_vehicle", "motorcar" };
public override string Name => nameof(Train);
public override string[] VehicleTypes => VehicleTypesArray;
public override FactorAndSpeed FactorAndSpeed(IAttributeCollection attributes, Whitelist whitelist)
{
return DefaultFactorAndSpeed;
}
}
I am still not sure why the same c# code can't get a position but it seems the train.lua file can.
Any help you would be able to provide to improve this will help and I will see about trams monorail as separate profiles if you wish.
I can also see about enhancing to make sure track types are the same e.g. electrified vs non etc but again some help would be needed to get me going.
Once Train / Rail is solidified I can work further to have profiles for Tram, Subway, ElectricTrain etc
This is very similar to what https://github.com/geofabrik/OpenRailRouting can do in Java and is likely the only C# alternative which can work that I have found.
I have done this type of routing before in this repository to extra shapes for GTFS feeds from OSM:
https://github.com/itinero/GTFS/tree/features/shapes/src/GTFS.Shapes
There are some example profiles that may help:
https://github.com/itinero/GTFS/tree/features/shapes/src/GTFS.Shapes/Osm/profiles
I will be getting back into this soon as we are again working with GTFS data and again the shapes are missing. What is your goal with this project?
Apologies for not being more responsive, trying to keep up with other work has been tough.
You can also calculate routing islands, to prevent a snapping to a part of the network that is not connect to the rest:
// add island data.
routerDb.AddIslandData(profile);
Once you've done that Itinero should know these part of the network are islands and it should detect what parts of the network are connected.
Using the profile in the repo I sent I manage to calculate this route:
http://geojson.io/#id=gist:xivk/ca34ab29d3f09a33a346c1ed301e6e90&map=12/51.1169/4.4588
Thank you for getting back to me!
My goals are using this project to provide length and transit estimates to enhance the data provided by my contractors sensors.
E.g. they have sensors on a train and they need to know given various points what the likely route was and then along with that information we know the actual speed and heading so we can augment that to provide ETA and various other services.
Thank you for your help.
I will let you know more what I find through the weekend and definitely next week.
Your profile seems to work the same way as mine did only it seems it doesn't check speeds so I am going to use that as the simple profile by default and use mine for other things... I am going to close this issue. Thank you for your help!
Looking further into the source code it seems that it should be possible to route on rail or subway simply making different profiles which describe the links and paths which can be traversed on that profile. e.g. how bikes can ride on track yet cars cannot.
I think the only thing which need to be mainly changed is the wording of the exceptions to use the nomenclature from the profile rather than simply saying road in the exception e.g.
To
Could not resolve point at [52.74539, -1.13073]. Probably too far from closest track or outside of the loaded network
Where the "road" or "track" would come from the lua profile e.g. @ ResolveAlgoithmn.cs @ 113
if (edgeIds[0] == Constants.NO_EDGE) { // oeps, no edge was found, too far from road network. this.ErrorMessage = string.Format("Could not resolve point at [{0}, {1}]. Probably too far from closest road or outside of the loaded network.", _latitude.ToInvariantString(), _longitude.ToInvariantString()); return; }
But would need the profile names left as a formattable string so that when the algorithmn returns the profile could format the
resolver.ErrorMessage
appropriately. One could also use less verbose terminology such as edge and it would also suit multiple profiles.Please see my attempt at an additional profile for train train.lua.txt
I have taken an attempt to using the following LUA to create a profile which can be used with out having to transform highway to rail.
Looking at the source code for Itinero it seems it should be possible as the nodes and ways are built using the information in the LUA profiles (OR the C# profiles)
Is there anyway you would be able to help me do this so that we don't first have to pre-process osm.pbf files to drop all highway and then convert rail to highway?
If you can show me how to do the Train I should be able to do the subway and contribute it back although I am sure you would be able to also provide a subway lua :)
thank you