diff --git a/Project.toml b/Project.toml index 880b070..ac77b26 100644 --- a/Project.toml +++ b/Project.toml @@ -4,7 +4,10 @@ authors = ["Jack Chan "] version = "0.3.1" [deps] +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" +GeoInterfaceRecipes = "0329782f-3d07-4b52-b9f6-d3137cf03c7a" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" @@ -13,6 +16,7 @@ MetaGraphs = "626554b9-1ddb-594c-aa3c-2596fe9399a5" NearestNeighbors = "b8a86587-4115-5ab1-83bc-aa920d37bbce" Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a" QuickHeaps = "30b38841-0f52-47f8-a5f8-18d5d4064379" +RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" SimpleWeightedGraphs = "47aef6b3-ad0c-573a-a1e2-d07658019622" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" SpatialIndexing = "d4ead438-fe20-5cc5-a293-4fd39a41b74c" diff --git a/example.jl b/example.jl new file mode 100644 index 0000000..7b1b487 --- /dev/null +++ b/example.jl @@ -0,0 +1,38 @@ +using LightOSM, Plots, GeoInterfaceRecipes +using StatsBase: sample + +g = graph_from_download( + :place_name, + place_name="tiergarten, berlin germany", + network_type=:drive +) +sg = simplify_graph(g) + +# Set plot size +size = (1920, 1080) + +# Show original nodes +plot(g; size) +savefig("original_nodes") + +# Show relevant nodes +plot(sg; size) +savefig("relevant_nodes") + + + +osm_ids = sample(collect(values(sg.nodes)), 200) +for source in osm_ids + for target in osm_ids + path = shortest_path(g, source, target) + path_simplified = shortest_path(sg, source, target) + if isnothing(path) || isnothing(path_simplified) + continue + end + path_length = total_path_weight(g, path) + path_simplified_length = total_path_weight(sg, path_simplified) + if !isapprox(path_length, path_simplified_length) + error("Path from $source to $target is $path_length in the original graph and $path_simplified_length in the simplified graph") + end + end +end diff --git a/src/LightOSM.jl b/src/LightOSM.jl index 607f52f..1f57d53 100644 --- a/src/LightOSM.jl +++ b/src/LightOSM.jl @@ -5,7 +5,7 @@ using DataStructures: DefaultDict, OrderedDict, MutableLinkedList using QuickHeaps: BinaryHeap, FastMin using Statistics: mean using SparseArrays: SparseMatrixCSC, sparse, findnz -using Graphs: AbstractGraph, DiGraph, nv, outneighbors, weakly_connected_components, vertices +using Graphs: AbstractGraph, DiGraph, nv, outneighbors, weakly_connected_components, vertices, all_neighbors, indegree, outdegree, add_edge! using StaticGraphs: StaticDiGraph using SimpleWeightedGraphs: SimpleWeightedDiGraph using MetaGraphs: MetaDiGraph, set_prop! @@ -15,9 +15,14 @@ using JSON using LightXML using StaticArrays using SpatialIndexing +using DataFrames +using GeoInterface: MultiLineString, LineString, Point +using RecipesBase export GeoLocation, + AbstractOSMGraph, OSMGraph, + SimplifiedOSMGraph, Node, Way, EdgePoint, @@ -67,7 +72,11 @@ export index_to_node_id, set_dijkstra_state_with_index!, set_dijkstra_state_with_node_id!, maxspeed_from_index, - maxspeed_from_node_id + maxspeed_from_node_id, + simplify_graph, + node_gdf, + edge_gdf, + way_gdf include("types.jl") include("constants.jl") @@ -83,5 +92,8 @@ include("nearest_node.jl") include("nearest_way.jl") include("buildings.jl") include("subgraph.jl") +include("simplification.jl") +include("geodataframes.jl") +include("plotrecipes.jl") end # module diff --git a/src/geodataframes.jl b/src/geodataframes.jl new file mode 100644 index 0000000..2b1bdd0 --- /dev/null +++ b/src/geodataframes.jl @@ -0,0 +1,24 @@ +Point(node::Node) = Point(node.location.lon, node.location.lat) +LineString(g::AbstractOSMGraph, way::Way) = LineString(Point.(g.parent.nodes[i] for i in way.nodes)) + +function node_gdf(g::AbstractOSMGraph) + ids = collect(keys(g.nodes)) + nodes = (g.nodes[id] for id in ids) + return DataFrame(;id=ids, tags=getproperty.(nodes, :tags), geom=Point.(nodes)) +end + +function way_gdf(g::AbstractOSMGraph) + ids = collect(keys(g.ways)) + ways = (g.ways[id] for id in ids) + return DataFrame(;id=ids, tags=getproperty.(ways, :tags), geom=LineString.(Ref(g), ways)) +end + +function edge_gdf(g::SimplifiedOSMGraph) + edge_ids = collect(keys(g.edges)) + geom = map(edge_ids) do edge + path = g.edges[edge] + LineString(Point.(g.nodes[path])) + end + u, v, key = map(i -> getindex.(edge_ids, i), 1:3) + return DataFrame(;u, v, key, geom=geom) +end \ No newline at end of file diff --git a/src/graph_utilities.jl b/src/graph_utilities.jl index 4ea605a..dfd9be6 100644 --- a/src/graph_utilities.jl +++ b/src/graph_utilities.jl @@ -1,67 +1,67 @@ """ - index_to_node_id(g::OSMGraph, x::DEFAULT_OSM_INDEX_TYPE) - index_to_node_id(g::OSMGraph, x::Vector{<:DEFAULT_OSM_INDEX_TYPE}) + index_to_node_id(g::AbstractOSMGraph, x::DEFAULT_OSM_INDEX_TYPE) + index_to_node_id(g::AbstractOSMGraph, x::Vector{<:DEFAULT_OSM_INDEX_TYPE}) Maps node index to node id. """ -index_to_node_id(g::OSMGraph, x::DEFAULT_OSM_INDEX_TYPE) = g.index_to_node[x] -index_to_node_id(g::OSMGraph, x::Vector{<:DEFAULT_OSM_INDEX_TYPE}) = [index_to_node_id(g, i) for i in x] +index_to_node_id(g::AbstractOSMGraph, x::DEFAULT_OSM_INDEX_TYPE) = g.index_to_node[x] +index_to_node_id(g::AbstractOSMGraph, x::Vector{<:DEFAULT_OSM_INDEX_TYPE}) = [index_to_node_id(g, i) for i in x] """ - index_to_node(g::OSMGraph, x::DEFAULT_OSM_INDEX_TYPE) - index_to_node(g::OSMGraph, x::Vector{<:DEFAULT_OSM_INDEX_TYPE}) + index_to_node(g::AbstractOSMGraph, x::DEFAULT_OSM_INDEX_TYPE) + index_to_node(g::AbstractOSMGraph, x::Vector{<:DEFAULT_OSM_INDEX_TYPE}) Maps node index to node object. """ -index_to_node(g::OSMGraph, x::DEFAULT_OSM_INDEX_TYPE) = g.nodes[g.index_to_node[x]] -index_to_node(g::OSMGraph, x::Vector{<:DEFAULT_OSM_INDEX_TYPE}) = [index_to_node(g, i) for i in x] +index_to_node(g::AbstractOSMGraph, x::DEFAULT_OSM_INDEX_TYPE) = g.nodes[g.index_to_node[x]] +index_to_node(g::AbstractOSMGraph, x::Vector{<:DEFAULT_OSM_INDEX_TYPE}) = [index_to_node(g, i) for i in x] """ - node_id_to_index(g::OSMGraph, x::DEFAULT_OSM_ID_TYPE) - node_id_to_index(g::OSMGraph, x::Vector{<:DEFAULT_OSM_ID_TYPE}) + node_id_to_index(g::AbstractOSMGraph, x::DEFAULT_OSM_ID_TYPE) + node_id_to_index(g::AbstractOSMGraph, x::Vector{<:DEFAULT_OSM_ID_TYPE}) Maps node id to index. """ -node_id_to_index(g::OSMGraph, x::DEFAULT_OSM_ID_TYPE) = g.node_to_index[x] -node_id_to_index(g::OSMGraph, x::Vector{<:DEFAULT_OSM_ID_TYPE}) = [node_id_to_index(g, i) for i in x] +node_id_to_index(g::AbstractOSMGraph, x::DEFAULT_OSM_ID_TYPE) = g.node_to_index[x] +node_id_to_index(g::AbstractOSMGraph, x::Vector{<:DEFAULT_OSM_ID_TYPE}) = [node_id_to_index(g, i) for i in x] """ - node_to_index(g::OSMGraph, x::Node) - node_to_index(g::OSMGraph, x::Vector{Node}) + node_to_index(g::AbstractOSMGraph, x::Node) + node_to_index(g::AbstractOSMGraph, x::Vector{Node}) Maps node object to index. """ -node_to_index(g::OSMGraph, x::Node) = g.node_to_index[x.id] -node_to_index(g::OSMGraph, x::Vector{Node}) = [node_id_to_index(g, i.id) for i in x] +node_to_index(g::AbstractOSMGraph, x::Node) = g.node_to_index[x.id] +node_to_index(g::AbstractOSMGraph, x::Vector{Node}) = [node_id_to_index(g, i.id) for i in x] """ - index_to_dijkstra_state(g::OSMGraph, x::DEFAULT_OSM_INDEX_TYPE) + index_to_dijkstra_state(g::AbstractOSMGraph, x::DEFAULT_OSM_INDEX_TYPE) Maps node index to dijkstra state (parents). """ -index_to_dijkstra_state(g::OSMGraph, x::DEFAULT_OSM_INDEX_TYPE) = g.dijkstra_states[x] +index_to_dijkstra_state(g::AbstractOSMGraph, x::DEFAULT_OSM_INDEX_TYPE) = g.dijkstra_states[x] """ - node_id_to_dijkstra_state(g::OSMGraph, x::DEFAULT_OSM_ID_TYPE) + node_id_to_dijkstra_state(g::AbstractOSMGraph, x::DEFAULT_OSM_ID_TYPE) Maps node id to dijkstra state (parents). """ -node_id_to_dijkstra_state(g::OSMGraph, x::DEFAULT_OSM_ID_TYPE) = g.dijkstra_states[node_id_to_index(g, x)] +node_id_to_dijkstra_state(g::AbstractOSMGraph, x::DEFAULT_OSM_ID_TYPE) = g.dijkstra_states[node_id_to_index(g, x)] """ - set_dijkstra_state_with_index!(g::OSMGraph, index::DEFAULT_OSM_INDEX_TYPE, state) + set_dijkstra_state_with_index!(g::AbstractOSMGraph, index::DEFAULT_OSM_INDEX_TYPE, state) Set dijkstra state (parents) with node index. """ -set_dijkstra_state_with_index!(g::OSMGraph, index::DEFAULT_OSM_INDEX_TYPE, state) = push!(g.dijkstra_states, index, state) +set_dijkstra_state_with_index!(g::AbstractOSMGraph, index::DEFAULT_OSM_INDEX_TYPE, state) = push!(g.dijkstra_states, index, state) """ - set_dijkstra_state_with_node_id!(g::OSMGraph, index::DEFAULT_OSM_ID_TYPE, state) + set_dijkstra_state_with_node_id!(g::AbstractOSMGraph, index::DEFAULT_OSM_ID_TYPE, state) Set dijkstra state (parents) with node id. """ -set_dijkstra_state_with_node_id!(g::OSMGraph, node_id::DEFAULT_OSM_ID_TYPE, state) = push!(g.dijkstra_states, node_id_to_index(g, node_id), state) +set_dijkstra_state_with_node_id!(g::AbstractOSMGraph, node_id::DEFAULT_OSM_ID_TYPE, state) = push!(g.dijkstra_states, node_id_to_index(g, node_id), state) """ maxspeed_from_index(g, x::DEFAULT_OSM_INDEX_TYPE) maxspeed_from_node_id(g, x::DEFAULT_OSM_ID_TYPE) Get maxspeed from index id or node id. """ -maxspeed_from_index(g, x::DEFAULT_OSM_INDEX_TYPE) = index_to_node(g, x).tags["maxspeed"] -maxspeed_from_node_id(g, x::DEFAULT_OSM_ID_TYPE) = g.nodes[x].tags["maxspeed"] \ No newline at end of file +maxspeed_from_index(g::AbstractOSMGraph, x::DEFAULT_OSM_INDEX_TYPE) = index_to_node(g, x).tags["maxspeed"] +maxspeed_from_node_id(g::AbstractOSMGraph, x::DEFAULT_OSM_ID_TYPE) = g.nodes[x].tags["maxspeed"] \ No newline at end of file diff --git a/src/plotrecipes.jl b/src/plotrecipes.jl new file mode 100644 index 0000000..fa7d2e7 --- /dev/null +++ b/src/plotrecipes.jl @@ -0,0 +1,29 @@ +function aspect_ratio(g::AbstractOSMGraph) + max_y, min_y = extrema(first, g.node_coordinates) + mid_y = (max_y + min_y)/2 + return 1/cos(mid_y * pi/180) +end + +RecipesBase.@recipe function f(g::AbstractOSMGraph) + # set the aspect ratio + aspect_ratio --> aspect_ratio(g) + + # way color and thickness + color --> :black + linewdith --> 1.5 + # node color and size + markercolor --> :blue + markersize --> 2 + + # plot ways + @series begin + seriestype := :path + MultiLineString(way_gdf(g).geom) + end + + # plot nodes + @series begin + seriestype := :scatter + node_gdf(g).geom + end +end \ No newline at end of file diff --git a/src/shortest_path.jl b/src/shortest_path.jl index 1afee32..4149df9 100644 --- a/src/shortest_path.jl +++ b/src/shortest_path.jl @@ -1,6 +1,6 @@ """ shortest_path([PathAlgorithm,] - g::OSMGraph, + g::AbstractOSMGraph, origin::Union{Integer,Node}, destination::Union{Integer,Node}, [weights::AbstractMatrix=g.weights; @@ -21,7 +21,7 @@ Calculates the shortest path between two OpenStreetMap node ids. Faster for small graphs and/or long paths. - `AStarDict`: A* algorithm using the `Dict` implementation. Faster for large graphs and/or short paths. -- `g::OSMGraph{U,T,W}`: Graph container. +- `g::AbstractOSMGraph{U,T,W}`: Graph container. - `origin::Union{Integer,Node}`: Origin OpenStreetMap node or node id. - `destination::Union{Integer,Node},`: Destination OpenStreetMap node or node id. @@ -45,7 +45,7 @@ Calculates the shortest path between two OpenStreetMap node ids. the shortest path. """ function shortest_path(::Type{A}, - g::OSMGraph{U,T,W}, + g::AbstractOSMGraph{U,T,W}, origin::DEFAULT_OSM_ID_TYPE, destination::DEFAULT_OSM_ID_TYPE, weights::AbstractMatrix{W}; @@ -59,7 +59,7 @@ function shortest_path(::Type{A}, return index_to_node_id(g, path) end function shortest_path(::Type{A}, - g::OSMGraph{U,T,W}, + g::AbstractOSMGraph{U,T,W}, origin::DEFAULT_OSM_ID_TYPE, destination::DEFAULT_OSM_ID_TYPE, weights::AbstractMatrix{W}; @@ -73,35 +73,35 @@ function shortest_path(::Type{A}, isnothing(path) && return return index_to_node_id(g, path) end -function shortest_path(::Type{A}, g::OSMGraph{U,T,W}, origin::DEFAULT_OSM_ID_TYPE, destination::DEFAULT_OSM_ID_TYPE; kwargs...)::Union{Nothing,Vector{T}} where {A <: PathAlgorithm, U, T, W} +function shortest_path(::Type{A}, g::AbstractOSMGraph{U,T,W}, origin::DEFAULT_OSM_ID_TYPE, destination::DEFAULT_OSM_ID_TYPE; kwargs...)::Union{Nothing,Vector{T}} where {A <: PathAlgorithm, U, T, W} return shortest_path(A, g, origin, destination, g.weights; kwargs...) end -function shortest_path(::Type{A}, g::OSMGraph{U,T,W}, origin::Node{<:DEFAULT_OSM_ID_TYPE}, destination::Node{<:DEFAULT_OSM_ID_TYPE}, args...; kwargs...)::Union{Nothing,Vector{T}} where {A <: PathAlgorithm, U, T, W} +function shortest_path(::Type{A}, g::AbstractOSMGraph{U,T,W}, origin::Node{<:DEFAULT_OSM_ID_TYPE}, destination::Node{<:DEFAULT_OSM_ID_TYPE}, args...; kwargs...)::Union{Nothing,Vector{T}} where {A <: PathAlgorithm, U, T, W} return shortest_path(A, g, origin.id, destination.id, args...; kwargs...) end -function shortest_path(g::OSMGraph{U,T,W}, args...; kwargs...)::Union{Nothing,Vector{T}} where {U, T, W} +function shortest_path(g::AbstractOSMGraph{U,T,W}, args...; kwargs...)::Union{Nothing,Vector{T}} where {U, T, W} return shortest_path(Dijkstra, g, args...; kwargs...) end """ - set_dijkstra_state!(g::OSMGraph, src::Union{Integer,Vecotr{<:Integer}, weights::AbstractMatrix; cost_adjustment::Function=(u, v, parents) -> 0.0) + set_dijkstra_state!(g::AbstractOSMGraph, src::Union{Integer,Vecotr{<:Integer}, weights::AbstractMatrix; cost_adjustment::Function=(u, v, parents) -> 0.0) Compute and set the dijkstra parent states for one or multiple src vertices. Threads are used for multiple srcs. Note, computing dijkstra states for all vertices is a O(V² + ElogV) operation, use on large graphs with caution. """ -function set_dijkstra_state!(g::OSMGraph, src::Integer, weights::AbstractMatrix; cost_adjustment::Function=(u, v, parents) -> 0.0) +function set_dijkstra_state!(g::AbstractOSMGraph, src::Integer, weights::AbstractMatrix; cost_adjustment::Function=(u, v, parents) -> 0.0) g.dijkstra_states[src] = dijkstra(g.graph, weights, src; cost_adjustment=cost_adjustment) end -function set_dijkstra_state!(g::OSMGraph, srcs::Vector{<:Integer}, weights::AbstractMatrix; cost_adjustment::Function=(u, v, parents) -> 0.0) +function set_dijkstra_state!(g::AbstractOSMGraph, srcs::Vector{<:Integer}, weights::AbstractMatrix; cost_adjustment::Function=(u, v, parents) -> 0.0) Threads.@threads for src in srcs set_dijkstra_state!(g, src, weights; cost_adjustment=cost_adjustment) end return g end -set_dijkstra_state!(g::OSMGraph, src; kwargs...) = set_dijkstra_state!(g, src, g.weights; kwargs...) +set_dijkstra_state!(g::AbstractOSMGraph, src; kwargs...) = set_dijkstra_state!(g, src, g.weights; kwargs...) """ - shortest_path_from_dijkstra_state(g::OSMGraph, origin::Integer, destination::Integer) + shortest_path_from_dijkstra_state(g::AbstractOSMGraph, origin::Integer, destination::Integer) Extract shortest path from precomputed dijkstra state, from `origin` to `detination` node id. @@ -109,14 +109,14 @@ Note, function will raise `UndefRefError: access to undefined reference` if the origin node is not precomputed. # Arguments -- `g::OSMGraph`: Graph container. +- `g::AbstractOSMGraph`: Graph container. - `origin::Integer`: Origin OpenStreetMap node or node id. - `destination::Integer`: Destination OpenStreetMap node or node id. # Return - `Union{Nothing,Vector{T}}`: Array of OpenStreetMap node ids making up the shortest path. """ -function shortest_path_from_dijkstra_state(g::OSMGraph, origin::Integer, destination::Integer) +function shortest_path_from_dijkstra_state(g::AbstractOSMGraph, origin::Integer, destination::Integer) parents = node_id_to_dijkstra_state(g, origin) path = path_from_parents(parents, node_id_to_index(g, destination)) isnothing(path) && return @@ -186,7 +186,7 @@ function restriction_cost(restrictions::AbstractDict{V,Vector{MutableLinkedList{ end """ - restriction_cost_adjustment(g::OSMGraph) + restriction_cost_adjustment(g::AbstractOSMGraph) Returns the cost adjustment function (user in dijkstra and astar) for restrictions. The return function takes 3 arguments, `u` being the current node, `v` being the neighbour node, `parents` being the array @@ -195,6 +195,16 @@ of parent dijkstra states. By default `g.indexed_restrictions` is used to check """ restriction_cost_adjustment(g::OSMGraph) = (u, v, parents) -> restriction_cost(g.indexed_restrictions, u, v, parents) +""" + restriction_cost_adjustment(g::SinplifiedOSMGraph) + +Returns the cost adjustment function (user in dijkstra and astar) for restrictions. The return function +takes 3 arguments, `u` being the current node, `v` being the neighbour node, `parents` being the array +of parent dijkstra states. By default `g.indexed_restrictions` is used to check whether the path from +`u` to `v` is restricted given all previous nodes in `parents`. +""" +restriction_cost_adjustment(g::SimplifiedOSMGraph) = (u, v, parents) -> restriction_cost(g.parent.indexed_restrictions, u, v, parents) + """ distance_heuristic(g::OSMGraph) @@ -202,10 +212,10 @@ Returns the heuristic function used in astar shortest path calculation, should b `weight_type=:distance`. The heuristic function takes in 2 arguments, `u` being the current node and `v` being the neighbour node, and returns the haversine distance between them. """ -distance_heuristic(g::OSMGraph) = (u, v) -> haversine(g.node_coordinates[u], g.node_coordinates[v]) +distance_heuristic(g::AbstractOSMGraph) = (u, v) -> haversine(g.node_coordinates[u], g.node_coordinates[v]) """ - time_heuristic(g::OSMGraph) + time_heuristic(g::AbstractOSMGraph) Returns the heuristic function used in astar shortest path calculation, should be used with a graph with `weight_type=:time` or `weight_type=:lane_efficiency`. The heuristic function takes in 2 arguments, `u` @@ -214,40 +224,40 @@ Calculated by dividing the harversine distance by a fixed maxspeed of `100`. Rem path, it is important to pick an *underestimating* heuristic that best estimates the cost remaining to the `goal`, hence we pick the largest maxspeed across all ways. """ -time_heuristic(g::OSMGraph) = (u, v) -> haversine(g.node_coordinates[u], g.node_coordinates[v]) / 100.0 +time_heuristic(g::AbstractOSMGraph) = (u, v) -> haversine(g.node_coordinates[u], g.node_coordinates[v]) / 100.0 """ - weights_from_path(g::OSMGraph{U,T,W}, path::Vector{T}; weights=g.weights)::Vector{W} where {U <: DEFAULT_OSM_INDEX_TYPE,T <: DEFAULT_OSM_ID_TYPE,W <: Real} + weights_from_path(g::AbstractOSMGraph{U,T,W}, path::Vector{T}; weights=g.weights)::Vector{W} where {U <: DEFAULT_OSM_INDEX_TYPE,T <: DEFAULT_OSM_ID_TYPE,W <: Real} Extracts edge weights from a path using the weight matrix stored in `g.weights` unless a different matrix is passed to the `weights` kwarg. # Arguments -- `g::OSMGraph`: Graph container. +- `g::AbstractOSMGraph`: Graph container. - `path::Vector{T}`: Array of OpenStreetMap node ids. - `weights=g.weights`: the matrix that the edge weights are extracted from. Defaults to `g.weights`. # Return - `Vector{W}`: Array of edge weights, distances are in km, time is in hours. """ -function weights_from_path(g::OSMGraph{U,T,W}, path::Vector{T}; weights=g.weights)::Vector{W} where {U <: DEFAULT_OSM_INDEX_TYPE,T <: DEFAULT_OSM_ID_TYPE,W <: Real} +function weights_from_path(g::AbstractOSMGraph{U,T,W}, path::Vector{T}; weights=g.weights)::Vector{W} where {U <: DEFAULT_OSM_INDEX_TYPE,T <: DEFAULT_OSM_ID_TYPE,W <: Real} return [weights[g.node_to_index[path[i]], g.node_to_index[path[i + 1]]] for i in 1:length(path) - 1] end """ - total_path_weight(g::OSMGraph{U,T,W}, path::Vector{T}; weights=g.weights)::W where {U <: Integer,T <: DEFAULT_OSM_ID_TYPE,W <: Real} + total_path_weight(g::AbstractOSMGraph{U,T,W}, path::Vector{T}; weights=g.weights)::W where {U <: Integer,T <: DEFAULT_OSM_ID_TYPE,W <: Real} Extract total edge weight along a path. # Arguments -- `g::OSMGraph`: Graph container. +- `g::AbstractOSMGraph`: Graph container. - `path::Vector{T}`: Array of OpenStreetMap node ids. - `weights=g.weights`: the matrix that the edge weights are extracted from. Defaults to `g.weights`. # Return - `sum::W`: Total path edge weight, distances are in km, time is in hours. """ -function total_path_weight(g::OSMGraph{U,T,W}, path::Vector{T}; weights=g.weights)::W where {U <: Integer,T <: DEFAULT_OSM_ID_TYPE,W <: Real} +function total_path_weight(g::AbstractOSMGraph{U,T,W}, path::Vector{T}; weights=g.weights)::W where {U <: Integer,T <: DEFAULT_OSM_ID_TYPE,W <: Real} sum::W = zero(W) for i in 1:length(path) - 1 sum += weights[g.node_to_index[path[i]], g.node_to_index[path[i + 1]]] diff --git a/src/simplification.jl b/src/simplification.jl new file mode 100644 index 0000000..e9147d9 --- /dev/null +++ b/src/simplification.jl @@ -0,0 +1,131 @@ + +#adapted from osmnx: https://github.com/gboeing/osmnx/blob/main/osmnx/simplification.py +""" +Predicate wether v is source node in g +""" +is_source(g::AbstractGraph, v) = outdegree(g, v) == 0 + +""" +Predicate wether v is sink node in g +""" +is_sink(g::AbstractGraph, v) = indegree(g, v) == 0 + + +""" +Predicate wether v is an edge endpoint in the simplified version of g +""" +function is_endpoint(g::AbstractGraph, v) + neighbors = all_neighbors(g, v) + if is_source(g, v) || is_sink(g, v) + return true + elseif v in neighbors # has self loop + return true + elseif length(neighbors) != 2 || indegree(g, v) != outdegree(g, v) # change to/from one way + return true + end + return false +end + +""" +iterator over all endpoints in g +""" +endpoints(g::AbstractGraph) = (v for v in vertices(g) if is_endpoint(g, v)) + +""" +iterator over all paths in g which can be contracted +""" +function paths_to_reduce(g::AbstractGraph) + (path_to_endpoint(g, (u, v)) for u in endpoints(g) for v in outneighbors(g, u)) +end + +""" +path to the next endpoint starting in edge (ep, ep_succ) +""" +function path_to_endpoint(g::AbstractGraph, (ep, ep_succ)::Tuple{T,T}) where {T<:Integer} + path = [ep, ep_succ] + head = ep_succ + # ep_succ not in endpoints -> has 2 neighbors and degree 2 or 4 + while !is_endpoint(g, head) + neighbors = [n for n in outneighbors(g, head) if n != path[end-1]] + @assert length(neighbors) == 1 "found unmarked endpoint!" + head, = neighbors + push!(path, head) + (head == ep) && return path # self loop + end + return path +end + +""" +Return the total weight of a path given as a Vector of Ids. +""" +function total_weight(g::OSMGraph, path::Vector{<:Integer}) + sum((g.weights[path[[i, i+1]]...] for i in 1:length(path)-1)) +end + +function ways_in_path(g::OSMGraph, path::Vector{<:Integer}) + ways = Set{Int}() + for i in 1:(length(path)-1) + edge = [g.index_to_node[path[i]], g.index_to_node[path[i+1]]] + push!(ways, g.edge_to_way[edge]) + end + return collect(ways) +end + +""" +Build a new graph which simplifies the topology of osmg.graph. +The resulting graph only contains intersections and dead ends from the original graph. +The geometry of the contracted nodes is kept in the edge_gdf DataFrame +""" +function simplify_graph(g::OSMGraph{U, T, W}) where {U, T, W} + relevant_nodes = collect(endpoints(g.graph)) + n_relevant = length(relevant_nodes) + nodes = Dict{T,Node{T}}() + graph = DiGraph{U}(n_relevant) + weights = similar(g.weights, (n_relevant, n_relevant)) + node_coordinates = Vector{Vector{W}}(undef, n_relevant) + node_to_index = OrderedDict{T,U}() + index_to_node = OrderedDict{U,T}() + + index_mapping = Dict{U,U}() + for (new_i, old_i) in enumerate(relevant_nodes) + index_mapping[old_i] = new_i + node_coordinates[new_i] = g.node_coordinates[old_i] + osm_id = g.index_to_node[old_i] + nodes[osm_id] = g.nodes[osm_id] + index_to_node[new_i] = osm_id + node_to_index[osm_id] = new_i + end + + edges = Dict{NTuple{3,U}, Vector{U}}() + edge_to_way = Dict{NTuple{3,U}, Vector{T}}() + edge_count = Dict{Tuple{U,U}, Int}() + for path in paths_to_reduce(g.graph) + u = index_mapping[first(path)] + v = index_mapping[last(path)] + path_weight = total_weight(g, path) + if add_edge!(graph, (u, v)) + key = 0 + weights[u, v] = path_weight + edge_count[u,v] = 1 + else # parallel edge + key = edge_count[u,v] + edge_count[u,v] += 1 + weights[u, v] = min(path_weight, weights[u, v]) + end + edges[u,v,key] = path + edge_to_way[u,v,key] = ways_in_path(g, path) + end + + return SimplifiedOSMGraph( + g, + nodes, + node_coordinates, + node_to_index, + index_to_node, + edge_to_way, + graph, + edges, + weights, + nothing + ) +end diff --git a/src/types.jl b/src/types.jl index 79bc494..cc6f527 100644 --- a/src/types.jl +++ b/src/types.jl @@ -132,7 +132,10 @@ Container for storing OpenStreetMap node, way, relation and graph related obejct - `kdtree::Union{RTree,Nothing}`: R-tree used to calculate nearest nodes. - `weight_type::Union{Symbol,Nothing}`: Either `:distance`, `:time` or `:lane_efficiency`. """ -@with_kw mutable struct OSMGraph{U <: Integer,T <: Union{Integer, String},W <: Real} + + +abstract type AbstractOSMGraph{U <: Integer,T <: Union{Integer, String},W <: Real} end +@with_kw mutable struct OSMGraph{U,T,W} <: AbstractOSMGraph{U,T,W} nodes::Dict{T,Node{T}} = Dict{T,Node{T}}() node_coordinates::Vector{Vector{W}} = Vector{Vector{W}}() # needed for astar heuristic ways::Dict{T,Way{T}} = Dict{T,Way{T}}() @@ -158,6 +161,8 @@ function Base.getproperty(g::OSMGraph, field::Symbol) elseif field === :node_to_highway Base.depwarn("`node_to_highway` field is deprecated, use `node_to_way` field instead", :getproperty) return getfield(g, :node_to_way) + elseif field === :parent + return g elseif field === :edge_to_highway Base.depwarn("`edge_to_highway` field is deprecated, use `edge_to_way` field instead", :getproperty) return getfield(g, :edge_to_way) @@ -165,6 +170,31 @@ function Base.getproperty(g::OSMGraph, field::Symbol) return getfield(g, field) end end + +struct SimplifiedOSMGraph{U,T,W} <: AbstractOSMGraph{U,T,W} + parent::OSMGraph{U,T,W} + nodes::Dict{T,Node{T}} + node_coordinates::Vector{Vector{W}} # needed for astar heuristic + node_to_index::OrderedDict{T,U} + index_to_node::OrderedDict{U,T} + edge_to_way::Dict{NTuple{3, U},Vector{T}} + graph::Union{AbstractGraph,Nothing} + edges::Dict{NTuple{3, U}, Vector{U}} + weights::Union{SparseMatrixCSC{W,U},Nothing} + dijkstra_states::Union{Vector{Vector{U}},Nothing} +end + +function Base.getproperty(g::SimplifiedOSMGraph, field::Symbol) + # Ensure renaming of "highways" to "ways" is backwards compatible + if field in fieldnames(SimplifiedOSMGraph) + return getfield(g, field) + elseif field === :edge_to_highway + Base.depwarn("`edge_to_highway` field is deprecated, use `edge_to_way` field instead", :getproperty) + return getfield(g, :edge_to_way) + else + return getfield(g.parent, field) + end +end """ OpenStreetMap building polygon.