├── docs ├── src │ ├── interface.md │ ├── defaults.md │ ├── nearest_node.md │ ├── nearest_way.md │ ├── types.md │ ├── create_buildings.md │ ├── create_graph.md │ ├── geolocation.md │ ├── download_buildings.md │ ├── download_network.md │ ├── graph_utilities.md │ ├── shortest_path.md │ ├── index.md │ └── testing_use.md ├── Project.toml └── make.jl ├── .github └── workflows │ ├── TagBot.yml │ ├── docs.yml │ └── ci.yml ├── benchmark ├── Project.toml ├── benchmarks_plot.jl └── benchmarks.jl ├── test ├── Project.toml ├── graph.jl ├── types.jl ├── runtests.jl ├── subgraph.jl ├── nearest_way.jl ├── constants.jl ├── nearest_node.jl ├── shortest_path_int.jl ├── geometry.jl ├── download.jl ├── shortest_path_str.jl ├── utilities.jl ├── traversal.jl └── stub.jl ├── travis.yml ├── .gitignore ├── Project.toml ├── LICENSE ├── src ├── LightOSM.jl ├── subgraph.jl ├── graph_utilities.jl ├── nearest_way.jl ├── nearest_node.jl ├── types.jl ├── utilities.jl ├── constants.jl ├── geometry.jl ├── shortest_path.jl ├── traversal.jl ├── parse.jl ├── buildings.jl └── download.jl └── README.md /docs/src/interface.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/src/defaults.md: -------------------------------------------------------------------------------- 1 | # Default Values 2 | 3 | ```@docs 4 | LightOSM.set_defaults 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/src/nearest_node.md: -------------------------------------------------------------------------------- 1 | # Nearest Node 2 | 3 | ```@docs 4 | nearest_node 5 | nearest_nodes 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/Project.toml: -------------------------------------------------------------------------------- 1 | [deps] 2 | Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" 3 | 4 | [compat] 5 | Documenter = "0.25" -------------------------------------------------------------------------------- /docs/src/nearest_way.md: -------------------------------------------------------------------------------- 1 | # Nearest Way 2 | 3 | ```@docs 4 | nearest_way 5 | nearest_ways 6 | nearest_point_on_way 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/src/types.md: -------------------------------------------------------------------------------- 1 | # Types 2 | 3 | ```@docs 4 | OSMGraph 5 | GeoLocation 6 | Node 7 | Way 8 | Restriction 9 | Building 10 | PathAlgorithm 11 | ``` -------------------------------------------------------------------------------- /docs/src/create_buildings.md: -------------------------------------------------------------------------------- 1 | # Create [`Building`](@ref) Objects 2 | 3 | ```@docs 4 | buildings_from_object 5 | buildings_from_download 6 | buildings_from_file 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/src/create_graph.md: -------------------------------------------------------------------------------- 1 | # Create [`OSMGraph`](@ref) Object 2 | 3 | ```@docs 4 | graph_from_object 5 | graph_from_download 6 | graph_from_file 7 | osm_subgraph 8 | ``` 9 | -------------------------------------------------------------------------------- /docs/src/geolocation.md: -------------------------------------------------------------------------------- 1 | # [`GeoLocation`](@ref) Methods 2 | 3 | ```@docs 4 | distance 5 | heading 6 | calculate_location 7 | LightOSM.to_cartesian 8 | LightOSM.bounding_box_from_point 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/src/download_buildings.md: -------------------------------------------------------------------------------- 1 | # Download OpenStreetMap Buildings 2 | 3 | ```@docs 4 | download_osm_buildings 5 | LightOSM.osm_buildings_from_place_name 6 | LightOSM.osm_buildings_from_point 7 | LightOSM.osm_buildings_from_bbox 8 | ``` -------------------------------------------------------------------------------- /docs/src/download_network.md: -------------------------------------------------------------------------------- 1 | # Download OpenStreetMap Network 2 | 3 | ```@docs 4 | download_osm_network 5 | LightOSM.osm_network_from_place_name 6 | LightOSM.osm_network_from_point 7 | LightOSM.osm_network_from_bbox 8 | LightOSM.osm_network_from_polygon 9 | ``` -------------------------------------------------------------------------------- /docs/src/graph_utilities.md: -------------------------------------------------------------------------------- 1 | # Graph Utilities 2 | 3 | ```@docs 4 | index_to_node_id 5 | index_to_node 6 | node_id_to_index 7 | node_to_index 8 | index_to_dijkstra_state 9 | node_id_to_dijkstra_state 10 | set_dijkstra_state_with_index! 11 | set_dijkstra_state_with_node_id! 12 | maxspeed_from_index 13 | ``` 14 | -------------------------------------------------------------------------------- /.github/workflows/TagBot.yml: -------------------------------------------------------------------------------- 1 | name: TagBot 2 | on: 3 | schedule: 4 | - cron: 0 * * * * 5 | workflow_dispatch: 6 | jobs: 7 | TagBot: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: JuliaRegistries/TagBot@v1 11 | with: 12 | token: ${{ secrets.GITHUB_TOKEN }} 13 | ssh: ${{ secrets.DOCUMENTER_KEY }} 14 | -------------------------------------------------------------------------------- /benchmark/Project.toml: -------------------------------------------------------------------------------- 1 | [deps] 2 | BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" 3 | OpenStreetMapX = "86cd37e6-c0ff-550b-95fe-21d72c8d4fc9" 4 | LightGraphs = "093fc24a-ae57-5d10-9952-331d41423f4d" 5 | JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" 6 | DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" 7 | DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" -------------------------------------------------------------------------------- /docs/src/shortest_path.md: -------------------------------------------------------------------------------- 1 | # Shortest Path 2 | 3 | ```@docs 4 | shortest_path 5 | LightOSM.astar 6 | LightOSM.dijkstra 7 | shortest_path_from_dijkstra_state 8 | set_dijkstra_state! 9 | LightOSM.restriction_cost_adjustment 10 | LightOSM.distance_heuristic 11 | LightOSM.time_heuristic 12 | weights_from_path 13 | total_path_weight 14 | path_from_parents 15 | ``` 16 | -------------------------------------------------------------------------------- /test/Project.toml: -------------------------------------------------------------------------------- 1 | [deps] 2 | Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" 3 | HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" 4 | JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" 5 | LightXML = "9c8b4983-aa76-5018-a973-4c85ecc9e179" 6 | Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" 7 | SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" 8 | Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" 9 | -------------------------------------------------------------------------------- /travis.yml: -------------------------------------------------------------------------------- 1 | ## Documentation: http://docs.travis-ci.com/user/languages/julia/ 2 | language: julia 3 | 4 | codecov: true 5 | 6 | os: 7 | - linux 8 | - windows 9 | 10 | julia: 11 | - 1.0 12 | - 1 # latest release 13 | - nightly 14 | 15 | notifications: 16 | email: false 17 | 18 | jobs: 19 | allow_failures: 20 | - julia: nightly 21 | # include: 22 | # - stage: "Documentation" 23 | # julia: 1 24 | # os: linux 25 | # script: 26 | # - julia --project=docs/ -e 'using Pkg; Pkg.instantiate()' 27 | # - julia --project=docs/ docs/make.jl 28 | # after_success: skip -------------------------------------------------------------------------------- /test/graph.jl: -------------------------------------------------------------------------------- 1 | graphs = [ 2 | basic_osm_graph_stub_string(), 3 | basic_osm_graph_stub() 4 | ] 5 | @testset "Backwards compatibility" begin 6 | for g in graphs 7 | @test g.ways === g.ways 8 | @test g.node_to_way === g.node_to_way 9 | @test g.edge_to_way === g.edge_to_way 10 | end 11 | end 12 | @testset "Regression test for railway lane parsing" begin 13 | g = graph_from_download( 14 | :place_name, 15 | place_name = "bern, switzerland", 16 | network_type = :rail, 17 | weight_type = :distance 18 | ) 19 | _, way = rand(g.ways) 20 | @test way.tags["lanes"] isa Integer 21 | end -------------------------------------------------------------------------------- /test/types.jl: -------------------------------------------------------------------------------- 1 | g_int = basic_osm_graph_stub() 2 | g_str = basic_osm_graph_stub_string() 3 | @testset "GeoLocation tests integer" begin 4 | # Testing for int graph 5 | ep = LightOSM.EdgePoint(1003, 1004, 0.4) 6 | expected_response = GeoLocation(lon=145.3326838, lat=-38.0754037) 7 | actual_response = GeoLocation(g_int, ep) 8 | @test expected_response ≈ actual_response 9 | 10 | #Testing for str graph 11 | ep = LightOSM.EdgePoint("1003", "1004", 0.4) 12 | expected_response = GeoLocation(lon=145.3326838, lat=-38.0754037) 13 | actual_response = GeoLocation(g_str, ep) 14 | @test expected_response ≈ actual_response 15 | end -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: '*' 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: julia-actions/setup-julia@latest 16 | with: 17 | version: '1' 18 | - name: Install dependencies 19 | run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' 20 | - name: Build and deploy 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For authentication with GitHub Actions token 23 | DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # For authentication with SSH deploy key 24 | run: julia --project=docs/ docs/make.jl -------------------------------------------------------------------------------- /docs/make.jl: -------------------------------------------------------------------------------- 1 | using Documenter 2 | using LightOSM 3 | 4 | makedocs( 5 | sitename="LightOSM.jl Documentation", 6 | # format=Documenter.HTML(prettyurls=false), 7 | pages=[ 8 | "Home" => "index.md", 9 | "Interface" => [ 10 | "types.md", 11 | "download_network.md", 12 | "create_graph.md", 13 | "shortest_path.md", 14 | "nearest_node.md", 15 | "nearest_way.md", 16 | "download_buildings.md", 17 | "create_buildings.md", 18 | "geolocation.md", 19 | "defaults.md" 20 | ], 21 | "Unit Test Use" => "testing_use.md", 22 | ] 23 | ) 24 | 25 | deploydocs( 26 | repo="github.com/DeloitteOptimalReality/LightOSM.jl.git", 27 | devurl="docs", 28 | push_preview=true, 29 | ) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files generated by invoking Julia with --code-coverage 2 | *.jl.cov 3 | *.jl.*.cov 4 | 5 | # Files generated by invoking Julia with --track-allocation 6 | *.jl.mem 7 | 8 | # System-specific files and directories generated by the BinaryProvider and BinDeps packages 9 | # They contain absolute paths specific to the host computer, and so should not be committed 10 | deps/deps.jl 11 | deps/build.log 12 | deps/downloads/ 13 | deps/usr/ 14 | deps/src/ 15 | 16 | # Build artifacts for creating documentation generated by the Documenter package 17 | docs/build/ 18 | docs/site/ 19 | 20 | # File generated by Pkg, the package manager, based on a corresponding Project.toml 21 | # It records a fixed state of all packages used by the project. As such, it should not be 22 | # committed for packages, but should be committed for applications that require a static 23 | # environment. 24 | Manifest.toml 25 | 26 | .vscode/ 27 | .ipynb_checkpoints/ -------------------------------------------------------------------------------- /test/runtests.jl: -------------------------------------------------------------------------------- 1 | using Graphs 2 | using HTTP 3 | using JSON 4 | using LightOSM 5 | using LightXML 6 | using SparseArrays 7 | using Random 8 | using Test 9 | 10 | include("stub.jl") 11 | 12 | const TEST_OSM_URL = "https://raw.githubusercontent.com/DeloitteOptimalReality/LightOSMFiles.jl/main/maps/south-yarra.json" 13 | 14 | @testset "LightOSM Tests" begin 15 | @testset "Constants" begin include("constants.jl") end 16 | @testset "Utilities" begin include("utilities.jl") end 17 | @testset "Geometry" begin include("geometry.jl") end 18 | @testset "Download" begin include("download.jl") end 19 | @testset "Nearest Node" begin include("nearest_node.jl") end 20 | @testset "Nearest Way" begin include("nearest_way.jl") end 21 | @testset "Shortest Path Integer" begin include("shortest_path_int.jl") end 22 | @testset "Shortest Path String" begin include("shortest_path_str.jl") end 23 | @testset "Graph" begin include("graph.jl") end 24 | @testset "Traversal" begin include("traversal.jl") end 25 | @testset "Subgraph" begin include("subgraph.jl") end 26 | @testset "Types" begin include("types.jl") end 27 | end 28 | -------------------------------------------------------------------------------- /Project.toml: -------------------------------------------------------------------------------- 1 | name = "LightOSM" 2 | uuid = "d1922b25-af4e-4ba3-84af-fe9bea896051" 3 | authors = ["Jack Chan "] 4 | version = "0.3.1" 5 | 6 | [deps] 7 | DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" 8 | Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" 9 | HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" 10 | JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" 11 | LightXML = "9c8b4983-aa76-5018-a973-4c85ecc9e179" 12 | MetaGraphs = "626554b9-1ddb-594c-aa3c-2596fe9399a5" 13 | NearestNeighbors = "b8a86587-4115-5ab1-83bc-aa920d37bbce" 14 | Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a" 15 | QuickHeaps = "30b38841-0f52-47f8-a5f8-18d5d4064379" 16 | SimpleWeightedGraphs = "47aef6b3-ad0c-573a-a1e2-d07658019622" 17 | SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" 18 | SpatialIndexing = "d4ead438-fe20-5cc5-a293-4fd39a41b74c" 19 | StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" 20 | StaticGraphs = "4c8beaf5-199b-59a0-a7f2-21d17de635b6" 21 | Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" 22 | 23 | [compat] 24 | DataStructures = "0.17.20, 0.18" 25 | Graphs = "1.4.0" 26 | HTTP = "0.8.17, 0.9, 1" 27 | JSON = "0.21.0" 28 | LightXML = "0.9.0" 29 | MetaGraphs = "0.7.0" 30 | NearestNeighbors = "0.4.6" 31 | Parameters = "0.12.1" 32 | QuickHeaps = "0.1.1" 33 | SimpleWeightedGraphs = "1.2.0" 34 | SpatialIndexing = "0.1.3" 35 | StaticArrays = "1.4.6" 36 | StaticGraphs = "0.3.0" 37 | julia = "1" 38 | -------------------------------------------------------------------------------- /test/subgraph.jl: -------------------------------------------------------------------------------- 1 | graphs = [ 2 | basic_osm_graph_stub_string(), 3 | basic_osm_graph_stub() 4 | ] 5 | @testset "Subgraph" begin 6 | for g in graphs 7 | # Create a subgraph using all nodes/vertices from g for testing 8 | nlist = [n.id for n in values(g.nodes)] 9 | sg = osm_subgraph(g, nlist) 10 | @test get_graph_type(g) === :static 11 | @test typeof(sg.graph) === typeof(g.graph) 12 | @test sg.nodes == g.nodes 13 | @test sg.ways == g.ways 14 | @test sg.restrictions == g.restrictions 15 | @test sg.weight_type == g.weight_type 16 | @test isdefined(sg.dijkstra_states, 1) == isdefined(g.dijkstra_states, 1) 17 | if isdefined(g.dijkstra_states, 1) 18 | @test sg.dijkstra_states == g.dijkstra_states 19 | end 20 | @test isdefined(sg.kdtree, 1) == isdefined(g.kdtree, 1) 21 | if isdefined(g.kdtree, 1) 22 | @test typeof(sg.kdtree) == typeof(g.kdtree) 23 | end 24 | 25 | g.graph = nothing 26 | LightOSM.add_graph!(g, :light) 27 | sg = osm_subgraph(g, nlist) 28 | @test typeof(sg.graph) === typeof(g.graph) 29 | 30 | g.graph = nothing 31 | LightOSM.add_graph!(g, :simple_weighted) 32 | sg = osm_subgraph(g, nlist) 33 | @test typeof(sg.graph) === typeof(g.graph) 34 | 35 | g.graph = nothing 36 | LightOSM.add_graph!(g, :meta) 37 | sg = osm_subgraph(g, nlist) 38 | @test typeof(sg.graph) === typeof(g.graph) 39 | end 40 | end 41 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2020, Deloitte Digital. All rights reserved. 2 | 3 | LightOSM.jl can be downloaded from: https://github.com/DeloitteOptimalReality/LightOSM.jl 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | Redistributions of source code must retain the above copyright notice, this list 9 | of conditions and the following disclaimer. 10 | 11 | Redistributions in binary form must reproduce the above copyright notice, this list 12 | of conditions and the following disclaimer in the documentation and/or other 13 | materials provided with the distribution. 14 | 15 | Neither the name of the copyright holder nor the names of its contributors may be 16 | used to endorse or promote products derived from this software without specific 17 | prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 22 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 23 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 24 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 25 | OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 26 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | push: 7 | branches: 8 | - master 9 | tags: '*' 10 | jobs: 11 | test: 12 | name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | version: 18 | - '1' 19 | - 'nightly' 20 | os: 21 | - ubuntu-latest 22 | - windows-latest 23 | arch: 24 | - x64 25 | steps: 26 | - uses: actions/checkout@v2 27 | - uses: julia-actions/setup-julia@v1 28 | with: 29 | version: ${{ matrix.version }} 30 | arch: ${{ matrix.arch }} 31 | - uses: actions/cache@v1 32 | env: 33 | cache-name: cache-artifacts 34 | with: 35 | path: ~/.julia/artifacts 36 | key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} 37 | restore-keys: | 38 | ${{ runner.os }}-test-${{ env.cache-name }}- 39 | ${{ runner.os }}-test- 40 | ${{ runner.os }}- 41 | - uses: julia-actions/julia-buildpkg@v1 42 | continue-on-error: ${{ matrix.version == 'nightly' }} 43 | - uses: julia-actions/julia-runtest@v1 44 | continue-on-error: ${{ matrix.version == 'nightly' }} 45 | - uses: julia-actions/julia-processcoverage@v1 46 | continue-on-error: ${{ matrix.version == 'nightly' }} 47 | - uses: codecov/codecov-action@v1 48 | continue-on-error: ${{ matrix.version == 'nightly' }} 49 | with: 50 | file: lcov.info -------------------------------------------------------------------------------- /test/nearest_way.jl: -------------------------------------------------------------------------------- 1 | Random.seed!(1234) 2 | rand_offset(d=0.000001) = rand() * d * 2 - d 3 | 4 | g_int = basic_osm_graph_stub() 5 | g_str = basic_osm_graph_stub_string() 6 | graphs = [g_int, g_str] 7 | 8 | for g in graphs 9 | node_ids = keys(g.nodes) 10 | # Select a node in the middle of a way 11 | test_node = rand(node_ids) 12 | if length(g.node_to_way[test_node]) > 1 13 | test_node = rand(node_ids) 14 | end 15 | test_node_loc = g.nodes[test_node].location 16 | test_way = g.node_to_way[test_node][1] 17 | 18 | # Point with a tiny offset 19 | test_point1 = GeoLocation(test_node_loc.lat + rand_offset(), test_node_loc.lon + rand_offset()) 20 | test_dist1 = distance(test_node_loc, test_point1) 21 | 22 | # Point with a massive offset, shouldn't have a nearest way 23 | test_point2 = GeoLocation(test_node_loc.lat + 1.0, test_node_loc.lon + 1.0) 24 | 25 | # nearest_way 26 | way_id, dist, ep = nearest_way(g, test_point1) 27 | @test way_id == test_way 28 | @test dist <= test_dist1 29 | @test ep.n1 == test_node || ep.n2 == test_node 30 | 31 | # nearest_way with search_radius 32 | way_id, dist, ep = nearest_way(g, test_point1, test_dist1) 33 | @test way_id == test_way 34 | @test dist <= test_dist1 35 | @test ep.n1 == test_node || ep.n2 == test_node 36 | 37 | # nearest_ways 38 | way_ids, dists, eps = nearest_ways(g, test_point1, test_dist1) 39 | @test test_way in way_ids 40 | 41 | # nearest_way with far away point, automatically choosing search radius 42 | way_id, dist, ep = nearest_way(g, test_point2) 43 | @test !isnothing(way_id) 44 | 45 | # nearest_way with far away point, search radius is too small 46 | way_id, dist, ep = nearest_way(g, test_point2, 0.1) 47 | @test isnothing(way_id) 48 | 49 | # nearest_ways with far away point, search radius is too small 50 | way_ids, dists, eps = nearest_ways(g, test_point2, 0.1) 51 | @test isempty(way_ids) 52 | end 53 | -------------------------------------------------------------------------------- /src/LightOSM.jl: -------------------------------------------------------------------------------- 1 | module LightOSM 2 | 3 | using Parameters 4 | using DataStructures: DefaultDict, OrderedDict, MutableLinkedList 5 | using QuickHeaps: BinaryHeap, FastMin 6 | using Statistics: mean 7 | using SparseArrays: SparseMatrixCSC, sparse, findnz 8 | using Graphs: AbstractGraph, DiGraph, nv, outneighbors, weakly_connected_components, vertices 9 | using StaticGraphs: StaticDiGraph 10 | using SimpleWeightedGraphs: SimpleWeightedDiGraph 11 | using MetaGraphs: MetaDiGraph, set_prop! 12 | using NearestNeighbors: Euclidean, KDTree, knn, nn 13 | using HTTP 14 | using JSON 15 | using LightXML 16 | using StaticArrays 17 | using SpatialIndexing 18 | 19 | export GeoLocation, 20 | OSMGraph, 21 | Node, 22 | Way, 23 | EdgePoint, 24 | Restriction, 25 | Building, 26 | PathAlgorithm, 27 | Dijkstra, 28 | DijkstraVector, 29 | DijkstraDict, 30 | AStar, 31 | AStarVector, 32 | AStarDict, 33 | distance, 34 | heading, 35 | calculate_location, 36 | download_osm_network, 37 | graph_from_object, 38 | graph_from_download, 39 | graph_from_file, 40 | shortest_path, 41 | shortest_path_from_dijkstra_state, 42 | set_dijkstra_state!, 43 | restriction_cost_adjustment, 44 | distance_heuristic, 45 | time_heuristic, 46 | weights_from_path, 47 | total_path_weight, 48 | path_from_parents, 49 | nearest_node, 50 | nearest_nodes, 51 | nearest_way, 52 | nearest_ways, 53 | nearest_point_on_way, 54 | download_osm_buildings, 55 | buildings_from_object, 56 | buildings_from_download, 57 | buildings_from_file, 58 | osm_subgraph, 59 | get_graph_type 60 | 61 | export index_to_node_id, 62 | index_to_node, 63 | node_id_to_index, 64 | node_to_index, 65 | index_to_dijkstra_state, 66 | node_id_to_dijkstra_state, 67 | set_dijkstra_state_with_index!, 68 | set_dijkstra_state_with_node_id!, 69 | maxspeed_from_index, 70 | maxspeed_from_node_id 71 | 72 | include("types.jl") 73 | include("constants.jl") 74 | include("utilities.jl") 75 | include("geometry.jl") 76 | include("download.jl") 77 | include("parse.jl") 78 | include("graph.jl") 79 | include("graph_utilities.jl") 80 | include("traversal.jl") 81 | include("shortest_path.jl") 82 | include("nearest_node.jl") 83 | include("nearest_way.jl") 84 | include("buildings.jl") 85 | include("subgraph.jl") 86 | 87 | end # module 88 | -------------------------------------------------------------------------------- /src/subgraph.jl: -------------------------------------------------------------------------------- 1 | """ 2 | osm_subgraph(g::OSMGraph{U, T, W}, 3 | vertex_list::Vector{U} 4 | )::OSMGraph where {U <: DEFAULT_OSM_INDEX_TYPE, T <: DEFAULT_OSM_ID_TYPE, W <: Real} 5 | 6 | Create an OSMGraph representing a subgraph of another OSMGraph containing 7 | specified vertices. 8 | The resulting OSMGraph object will also contain vertices from all ways that the 9 | specified vertices (nodes) are members of. 10 | Vertex numbers within the original graph object are not mapped to the subgraph. 11 | """ 12 | function osm_subgraph(g::OSMGraph{U, T, W}, 13 | vertex_list::Vector{U} 14 | ) where {U <: DEFAULT_OSM_INDEX_TYPE, T <: DEFAULT_OSM_ID_TYPE, W <: Real} 15 | 16 | # Get all nodes and ways for the subgraph 17 | nodelist = [g.nodes[g.index_to_node[v]] for v in vertex_list] 18 | waylist = [g.ways[w] for w in collect(Iterators.flatten([g.node_to_way[n.id] for n in nodelist]))] 19 | way_ids = [w.id for w in waylist] 20 | ways = Dict{T, Way{T}}(way_ids .=> waylist) 21 | 22 | # Make sure number of nodes matches number of nodes from ways (adds some nodes to graph) 23 | for way in values(ways) 24 | append!(nodelist, g.nodes[n_id] for n_id in g.ways[way.id].nodes) 25 | end 26 | unique!(nodelist) 27 | nodes = Dict{T, Node{T}}([n.id for n in nodelist] .=> nodelist) 28 | 29 | # Get restrictions that involve selected ways 30 | restrictions = Dict{T, Restriction{T}}() 31 | for res in values(g.restrictions) 32 | if in(res.from_way, way_ids) || in(res.to_way, way_ids) 33 | restrictions[res.id] = res 34 | end 35 | end 36 | 37 | # Construct the OSMGraph 38 | osg = OSMGraph{U, T, W}(nodes=nodes, ways=ways, restrictions=restrictions) 39 | add_node_and_edge_mappings!(osg) 40 | !isnothing(g.weight_type) && add_weights!(osg, g.weight_type) 41 | add_graph!(osg, get_graph_type(g)) 42 | add_node_tags!(osg) 43 | 44 | if isdefined(g.dijkstra_states, 1) 45 | add_dijkstra_states!(osg) 46 | else 47 | osg.dijkstra_states = Vector{Vector{U}}(undef, length(osg.nodes)) 48 | end 49 | 50 | if !isnothing(g.kdtree) || !isnothing(g.rtree) 51 | cartesian_locations = get_cartesian_locations(g) 52 | !isnothing(g.kdtree) && add_kdtree!(osg, cartesian_locations) 53 | !isnothing(g.rtree) && add_rtree!(osg, cartesian_locations) 54 | end 55 | 56 | return osg 57 | end 58 | 59 | function osm_subgraph(g::OSMGraph{U, T, W}, node_list::Vector{T}) where {U <: DEFAULT_OSM_INDEX_TYPE, T <: DEFAULT_OSM_ID_TYPE, W <: Real} 60 | return osm_subgraph(g, [g.node_to_index[n] for n in node_list]) 61 | end -------------------------------------------------------------------------------- /docs/src/index.md: -------------------------------------------------------------------------------- 1 | # LightOSM.jl 2 | 3 | **[`LightOSM.jl`](https://github.com/DeloitteOptimalReality/LightOSM.jl)** is **[Julia](https://julialang.org/)** package for downloading and analysing geospatial data from **[OpenStreetMap](https://wiki.openstreetmap.org/wiki/Main_Page)** APIs (**[Nominatim](https://nominatim.openstreetmap.org/ui/search.html)** and **[Overpass](https://overpass-api.de)**), such as nodes, ways, relations and building polygons. 4 | 5 | ## Interface 6 | 7 | ```@contents 8 | Pages = [ 9 | "types.md", 10 | "download_network.md", 11 | "create_graph.md", 12 | "shortest_path.md", 13 | "nearest_node.md", 14 | "nearest_way.md", 15 | "download_buildings.md", 16 | "create_buildings.md", 17 | "geolocation.md", 18 | "graph_utilities.md", 19 | "defaults.md" 20 | ] 21 | ``` 22 | 23 | ## Acknowledgements 24 | 25 | **[`LightOSM.jl`](https://github.com/DeloitteOptimalReality/LightOSM.jl)** is inspired by the Python package **[OSMnx](https://github.com/gboeing/osmnx)** for its interface and Overpass query logic. Graph analysis algorithms (connected components and shortest path) are based on **[LightGraphs.jl](https://github.com/JuliaGraphs/LightGraphs.jl)** implementation, but adapted to account for turn restrictions and improve runtime performance. 26 | 27 | Another honourable mention goes to an existing Julia package **[OpenStreetMapX.jl](https://github.com/pszufe/OpenStreetMapX.jl)** as many learnings were taken to improve parsing of raw OpenStreetMap data. 28 | 29 | ## Key Features 30 | 31 | - `Search`, `download` and `save` OpenSteetMap data in .osm, .xml or .json, using a place name, centroid point or bounding box 32 | - Parse OpenStreetMap `transport network` data such as motorway, cycleway or walkway 33 | - Parse OpenStreetMap `buildings` data into a format consistent with the **[GeoJSON](https://tools.ietf.org/html/rfc7946)** standard, allowing for visualisation with libraries such as **[deck.gl](https://github.com/visgl/deck.gl)** 34 | - Calculate `shortest path` between two nodes using the Dijkstra or A\* algorithm (based on LightGraphs.jl, but adapted for better performance and use cases such as `turn resrictions`) 35 | - Find `nearest nodes` from a query point using a K-D Tree data structure (implemented using **[NearestNeighbors.jl](https://github.com/KristofferC/NearestNeighbors.jl)**) 36 | 37 | ## Usage 38 | 39 | A comprehensive tutorial can be found found **[here](https://deloitteoptimalreality.github.io/LightOSM.jl/notebooks/tutorial)**. 40 | 41 | ## Using `OSMGraph`s in Unit Tests 42 | 43 | ```@contents 44 | Pages = ["testing_use.md"] 45 | ``` 46 | 47 | ## Benchmarks 48 | 49 | Benchmark comparison for shortest path algorithms can be found **[here](https://deloitteoptimalreality.github.io/LightOSM.jl/notebooks/benchmarks)**. 50 | -------------------------------------------------------------------------------- /src/graph_utilities.jl: -------------------------------------------------------------------------------- 1 | """ 2 | index_to_node_id(g::OSMGraph, x::DEFAULT_OSM_INDEX_TYPE) 3 | index_to_node_id(g::OSMGraph, x::Vector{<:DEFAULT_OSM_INDEX_TYPE}) 4 | 5 | Maps node index to node id. 6 | """ 7 | index_to_node_id(g::OSMGraph, x::DEFAULT_OSM_INDEX_TYPE) = g.index_to_node[x] 8 | index_to_node_id(g::OSMGraph, x::Vector{<:DEFAULT_OSM_INDEX_TYPE}) = [index_to_node_id(g, i) for i in x] 9 | 10 | """ 11 | index_to_node(g::OSMGraph, x::DEFAULT_OSM_INDEX_TYPE) 12 | index_to_node(g::OSMGraph, x::Vector{<:DEFAULT_OSM_INDEX_TYPE}) 13 | 14 | Maps node index to node object. 15 | """ 16 | index_to_node(g::OSMGraph, x::DEFAULT_OSM_INDEX_TYPE) = g.nodes[g.index_to_node[x]] 17 | index_to_node(g::OSMGraph, x::Vector{<:DEFAULT_OSM_INDEX_TYPE}) = [index_to_node(g, i) for i in x] 18 | 19 | """ 20 | node_id_to_index(g::OSMGraph, x::DEFAULT_OSM_ID_TYPE) 21 | node_id_to_index(g::OSMGraph, x::Vector{<:DEFAULT_OSM_ID_TYPE}) 22 | 23 | Maps node id to index. 24 | """ 25 | node_id_to_index(g::OSMGraph, x::DEFAULT_OSM_ID_TYPE) = g.node_to_index[x] 26 | node_id_to_index(g::OSMGraph, x::Vector{<:DEFAULT_OSM_ID_TYPE}) = [node_id_to_index(g, i) for i in x] 27 | """ 28 | node_to_index(g::OSMGraph, x::Node) 29 | node_to_index(g::OSMGraph, x::Vector{Node}) 30 | 31 | Maps node object to index. 32 | """ 33 | node_to_index(g::OSMGraph, x::Node) = g.node_to_index[x.id] 34 | node_to_index(g::OSMGraph, x::Vector{Node}) = [node_id_to_index(g, i.id) for i in x] 35 | 36 | """ 37 | index_to_dijkstra_state(g::OSMGraph, x::DEFAULT_OSM_INDEX_TYPE) 38 | 39 | Maps node index to dijkstra state (parents). 40 | """ 41 | index_to_dijkstra_state(g::OSMGraph, x::DEFAULT_OSM_INDEX_TYPE) = g.dijkstra_states[x] 42 | """ 43 | node_id_to_dijkstra_state(g::OSMGraph, x::DEFAULT_OSM_ID_TYPE) 44 | 45 | Maps node id to dijkstra state (parents). 46 | """ 47 | node_id_to_dijkstra_state(g::OSMGraph, x::DEFAULT_OSM_ID_TYPE) = g.dijkstra_states[node_id_to_index(g, x)] 48 | """ 49 | set_dijkstra_state_with_index!(g::OSMGraph, index::DEFAULT_OSM_INDEX_TYPE, state) 50 | 51 | Set dijkstra state (parents) with node index. 52 | """ 53 | set_dijkstra_state_with_index!(g::OSMGraph, index::DEFAULT_OSM_INDEX_TYPE, state) = push!(g.dijkstra_states, index, state) 54 | """ 55 | set_dijkstra_state_with_node_id!(g::OSMGraph, index::DEFAULT_OSM_ID_TYPE, state) 56 | 57 | Set dijkstra state (parents) with node id. 58 | """ 59 | 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) 60 | """ 61 | maxspeed_from_index(g, x::DEFAULT_OSM_INDEX_TYPE) 62 | maxspeed_from_node_id(g, x::DEFAULT_OSM_ID_TYPE) 63 | 64 | Get maxspeed from index id or node id. 65 | """ 66 | maxspeed_from_index(g, x::DEFAULT_OSM_INDEX_TYPE) = index_to_node(g, x).tags["maxspeed"] 67 | maxspeed_from_node_id(g, x::DEFAULT_OSM_ID_TYPE) = g.nodes[x].tags["maxspeed"] -------------------------------------------------------------------------------- /test/constants.jl: -------------------------------------------------------------------------------- 1 | @testset "concatenate_exclusions tests" begin 2 | dict = Dict( 3 | "first" => ["single"], 4 | "second" => ["dual1", "dual2"] 5 | ) 6 | 7 | single_dict = Dict("first" => ["single"]) 8 | single_str = LightOSM.concatenate_exclusions(single_dict) 9 | @test startswith(single_str, '[') 10 | @test endswith(single_str, ']') 11 | args_only = chop(single_str, head=1, tail=1) 12 | args = split(args_only, "!~") 13 | @test args[1] == "\"first\"" 14 | @test args[2] == "\"single\"" 15 | 16 | dual_dict = Dict("second" => ["dual1", "dual2"]) 17 | dual_str = LightOSM.concatenate_exclusions(dual_dict) 18 | @test startswith(dual_str, '[') 19 | @test endswith(dual_str, ']') 20 | args_only = chop(dual_str, head=1, tail=1) 21 | args = split(args_only, "!~") 22 | @test args[1] == "\"second\"" 23 | @test args[2] == "\"dual1|dual2\"" 24 | 25 | combined_dict = dict = Dict( 26 | "first" => ["single"], 27 | "second" => ["dual1", "dual2"] 28 | ) 29 | str = LightOSM.concatenate_exclusions(combined_dict) 30 | @test str == single_str * dual_str || str == dual_str * single_str # either order 31 | end 32 | 33 | @testset "set_defaults tests" begin 34 | resp = HTTP.get(TEST_OSM_URL) 35 | data = JSON.parse(String(resp.body)) 36 | 37 | # Get original defaults 38 | original_maxspeeds = deepcopy(LightOSM.DEFAULT_MAXSPEEDS[]) 39 | original_lanes = deepcopy(LightOSM.DEFAULT_LANES[]) 40 | 41 | # Create graph using originals 42 | original_g = LightOSM.graph_from_object(deepcopy(data); graph_type=:static, weight_type=:lane_efficiency) 43 | 44 | # New defaults 45 | new_maxspeeds = Dict( 46 | "motorway" => 100, 47 | "trunk" => 100, 48 | "primary" => 60, 49 | "secondary" => 60, 50 | "tertiary" => 50, 51 | "unclassified" => 50, 52 | "residential" => 40, 53 | "other" => 50 54 | ) 55 | new_lanes = Dict( 56 | "motorway" => 5, 57 | "trunk" => 4, 58 | "primary" => 3, 59 | "secondary" => 1, 60 | "tertiary" => 1, 61 | "unclassified" => 1, 62 | "residential" => 1, 63 | "other" => 1 64 | ) 65 | LightOSM.set_defaults( 66 | maxspeeds=new_maxspeeds, 67 | lanes=new_lanes 68 | ) 69 | 70 | # Create graph using new values 71 | new_g = LightOSM.graph_from_object(deepcopy(data); graph_type=:static, weight_type=:lane_efficiency) 72 | 73 | # Test way 217499573, Chapel St with tags: 74 | # "highway": "secondary" 75 | # "name": "Chapel Street" 76 | # "surface": "asphalt" 77 | @test original_g.ways[217499573].tags["maxspeed"] == original_maxspeeds["secondary"] 78 | @test new_g.ways[217499573].tags["maxspeed"] == new_maxspeeds["secondary"] 79 | @test original_g.ways[217499573].tags["lanes"] == original_lanes["secondary"] 80 | @test new_g.ways[217499573].tags["lanes"] == new_lanes["secondary"] 81 | end 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LightOSM.jl 2 | 3 | [![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://deloitteoptimalreality.github.io/LightOSM.jl/docs/) 4 | [![Tutorial](https://img.shields.io/badge/docs-tutorial-informational.svg)](https://deloitteoptimalreality.github.io/LightOSM.jl/notebooks/tutorial) 5 | [![Build Status](https://github.com/DeloitteOptimalReality/LightOSM.jl/workflows/CI/badge.svg?branch=master)](https://github.com/DeloitteOptimalReality/LightOSM.jl/actions?query=workflow%3ACI+branch%3Amaster) 6 | [![Codecov](https://codecov.io/gh/DeloitteOptimalReality/LightOSM.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/DeloitteOptimalReality/LightOSM.jl) 7 | 8 | 9 | **[`LightOSM.jl`](https://github.com/DeloitteOptimalReality/LightOSM.jl)** is **[Julia](https://julialang.org/)** package for downloading and analysing geospatial data from **[OpenStreetMap](https://wiki.openstreetmap.org/wiki/Main_Page)** APIs (**[Nominatim](https://nominatim.openstreetmap.org/ui/search.html)** and **[Overpass](https://overpass-api.de)**), such as nodes, ways, relations and building polygons. 10 | 11 | ## Acknowledgements 12 | 13 | **[`LightOSM.jl`](https://github.com/DeloitteOptimalReality/LightOSM.jl)** is inspired by the Python package **[OSMnx](https://github.com/gboeing/osmnx)** for its interface and Overpass query logic. Graph analysis algorithms (connected components and shortest path) are based on **[LightGraphs.jl](https://github.com/JuliaGraphs/LightGraphs.jl)** implementation, but adapted to account for turn restrictions and improve runtime performance. 14 | 15 | Another honourable mention goes to an existing Julia package **[OpenStreetMapX.jl](https://github.com/pszufe/OpenStreetMapX.jl)** as many learnings were taken to improve parsing of raw OpenStreetMap data. 16 | 17 | ## Key Features 18 | 19 | - `Search`, `download` and `save` OpenSteetMap data in .osm, .xml or .json, using a place name, centroid point or bounding box 20 | - Parse OpenStreetMap `transport network` data such as motorway, cycleway or walkway 21 | - Parse OpenStreetMap `buildings` data into a format consistent with the **[GeoJSON](https://tools.ietf.org/html/rfc7946)** standard, allowing for visualisation with libraries such as **[deck.gl](https://github.com/visgl/deck.gl)** 22 | - Calculate `shortest path` between two nodes using the Dijkstra or A\* algorithm (based on LightGraphs.jl, but adapted for better performance and use cases such as `turn resrictions`) 23 | - Find `nearest nodes` from a query point using a K-D Tree data structure (implemented using **[NearestNeighbors.jl](https://github.com/KristofferC/NearestNeighbors.jl)**) 24 | 25 | ## Documentation 26 | 27 | Documentation for the API can be found **[here](https://deloitteoptimalreality.github.io/LightOSM.jl/docs)**. 28 | 29 | ## Usage 30 | 31 | A comprehensive tutorial can be found found **[here](https://deloitteoptimalreality.github.io/LightOSM.jl/notebooks/tutorial)**. 32 | 33 | ## Benchmarks 34 | 35 | Benchmark comparison for shortest path algorithms can be found **[here](https://deloitteoptimalreality.github.io/LightOSM.jl/notebooks/benchmarks)**. 36 | -------------------------------------------------------------------------------- /test/nearest_node.jl: -------------------------------------------------------------------------------- 1 | g_int = basic_osm_graph_stub(); 2 | g_str = basic_osm_graph_stub_string(); 3 | graphs = [g_int, g_str]; 4 | 5 | for g in graphs 6 | node_ids = keys(g.nodes) 7 | n1_id, state = iterate(node_ids) # doesn't matter what one it is 8 | n2_id, _ = iterate(node_ids, state) # doesn't matter what one it is 9 | n1 = g.nodes[n1_id] 10 | n2 = g.nodes[n2_id] 11 | 12 | # Test GeoLocation methods, expect same node to be returned 13 | idxs, dists = nearest_node(g, n1.location) 14 | @test idxs == n1.id 15 | @test dists == 0.0 16 | idxs, dists = nearest_node(g, [n1.location]) 17 | @test idxs[1] == n1.id 18 | @test dists[1] == 0.0 19 | idxs, dists = nearest_nodes(g, n1.location, 1) 20 | @test idxs[1] == n1.id 21 | @test dists[1] == 0.0 22 | idxs, dists = nearest_nodes(g, [n1.location], 1) 23 | @test idxs[1][1] == n1.id 24 | @test dists[1][1] == 0.0 25 | 26 | # Test vector methods, expect same node to be returned 27 | point1 = [n1.location.lat, n1.location.lon, n1.location.alt] 28 | point2 = [n2.location.lat, n2.location.lon, n2.location.alt] 29 | idxs, dists = nearest_node(g, point1) 30 | @test idxs == n1.id 31 | @test dists == 0.0 32 | idxs, dists = nearest_node(g, [point1]) 33 | @test idxs[1] == n1.id 34 | @test dists[1] == 0.0 35 | idxs, dists = nearest_nodes(g, point1, 1) 36 | @test idxs[1] == n1.id 37 | @test dists[1] == 0.0 38 | idxs, dists = nearest_nodes(g, [point1], 1) 39 | @test idxs[1][1] == n1.id 40 | @test dists[1][1] == 0.0 41 | idxs, dists = nearest_node(g, [point1, point2]) 42 | @test idxs == [n1.id, n2.id] 43 | @test all(x -> x == 0.0, dists) 44 | 45 | # Test equality between vector of coords and location 46 | @test nearest_node(g, n1.location) == nearest_node(g, [n1.location.lat, n1.location.lon, n1.location.alt]) 47 | @test nearest_node(g, [n1.location]) == nearest_node(g, [[n1.location.lat, n1.location.lon, n1.location.alt]]) 48 | @test nearest_nodes(g, n1.location, 2) == nearest_nodes(g, [n1.location.lat, n1.location.lon, n1.location.alt], 2) 49 | @test nearest_nodes(g, [n1.location], 2) == nearest_nodes(g, [[n1.location.lat, n1.location.lon, n1.location.alt]], 2) 50 | 51 | # Test Node methods, expect different node to be returned 52 | idxs, dists = nearest_node(g, n1) 53 | @test idxs !== n1.id 54 | @test dists !== 0.0 55 | idxs, dists = nearest_node(g, [n1]) 56 | @test idxs[1] !== n1.id 57 | @test dists[1] !== 0.0 58 | 59 | idxs, dists = nearest_nodes(g, [n1, n2], 2) 60 | @test all(x -> x !== n1.id, idxs[1]) 61 | @test all(x -> x !== n2.id, idxs[2]) 62 | @test all(x -> x !== 0.0, dists[1]) 63 | @test all(x -> x !== 0.0, dists[2]) 64 | @test all(length.(idxs) .== 2) # Two points returned 65 | @test all(x -> x[2] > x[1], dists) # 2nd point further away 66 | idxs, dists = nearest_nodes(g, n1, 2) 67 | @test all(x -> x !== n1.id, idxs) 68 | 69 | # Test equality between Node and node ID methods 70 | @test nearest_node(g, n1) == nearest_node(g, n1.id) 71 | @test nearest_node(g, [n1]) == nearest_node(g, [n1.id]) 72 | @test nearest_nodes(g, n1, 2) == nearest_nodes(g, n1.id, 2) 73 | @test nearest_nodes(g, [n1], 2) == nearest_nodes(g, [n1.id], 2) 74 | end 75 | 76 | # Test two nodes we know are closest in the stub graph 77 | n1_id = 1005 78 | idxs, dists = nearest_node(g_int, n1_id) 79 | @test idxs == 1004 80 | 81 | n1_id = "1005" 82 | idxs, dists = nearest_node(g_str, n1_id) 83 | @test idxs == "1004" 84 | -------------------------------------------------------------------------------- /docs/src/testing_use.md: -------------------------------------------------------------------------------- 1 | # Using LightOSM in Unit Tests 2 | 3 | To avoid having to download graphs within unit tests, it is suggested that something 4 | similar to the `OSMGraph` stub used in LightOSM's own tests (see [`test/stub.jl`](https://github.com/DeloitteOptimalReality/LightOSM.jl/blob/master/test/stub.jl)) is 5 | used by your package. This allows you to have explicit control over the structure of 6 | the graph and therefore to have explicit tests. 7 | 8 | ### Manual Stub Creation 9 | 10 | To create your own graph stub, the nodes and ways must be manually created and inputted. 11 | Restrictions can also be optionally added. 12 | 13 | #### Nodes 14 | 15 | Nodes must be have an ID and GeoLocation 16 | 17 | ```julia 18 | lats = [-38.0751637, -38.0752637, -38.0753637, -38.0754637] 19 | lons = [145.3326838, 145.3326838, 145.3326838, 145.3326833] 20 | node_ids = [1001, 1002, 1003, 1004] 21 | nodes = Dict( 22 | id => Node( 23 | id, 24 | GeoLocation(lat, lon), 25 | Dict{String, Any}() # Don't need tags 26 | ) for (lat, lon, id) in zip(lats, lons, node_ids) 27 | ) 28 | ``` 29 | 30 | #### Ways 31 | 32 | Ways must have an ID, a node list that only includes nodes you have defined and 33 | must include the tags in the example shown below. 34 | 35 | ```julia 36 | way_ids = [2001, 2002] 37 | way_nodes = [ 38 | [1001, 1002, 1003], 39 | [1003, 1004], 40 | ] 41 | tag_dicts = [ 42 | Dict{String, Any}( 43 | "oneway" => false, 44 | "reverseway" => false, 45 | "maxspeed" => Int16(50), 46 | "lanes" => Int8(2) 47 | ), 48 | Dict{String, Any}( 49 | "oneway" => false, 50 | "reverseway" => false, 51 | "maxspeed" => Int16(50), 52 | "lanes" => Int8(2) 53 | ), 54 | ] 55 | ways = Dict(way_id => Way(way_id, nodes, tag_dict) for (way_id, nodes, tag_dict) in zip(way_ids, way_nodes, tag_dicts)) 56 | ``` 57 | 58 | #### Graph creation 59 | 60 | !!! warning 61 | The functions here are not part of the stable API and will be replaced by a `generate_graph` function or similar 62 | 63 | Creating the graph relies on some LightOSM internals to populate all other fields of the `OSMGraph` object 64 | 65 | ```julia 66 | U = LightOSM.DEFAULT_OSM_INDEX_TYPE 67 | T = LightOSM.DEFAULT_OSM_ID_TYPE 68 | W = LightOSM.DEFAULT_OSM_EDGE_WEIGHT_TYPE 69 | g = OSMGraph{U,T,W}(nodes=nodes, ways=ways) 70 | LightOSM.add_node_and_edge_mappings!(g) 71 | LightOSM.add_weights!(g, :distance) # or :time 72 | LightOSM.add_graph!(g, :static) # or any desired graph type 73 | LightOSM.add_node_tags!(g) 74 | g.dijkstra_states = Vector{Vector{U}}(undef, length(g.nodes)) 75 | LightOSM.add_kdtree!(g) 76 | ``` 77 | 78 | #### Restrictions 79 | 80 | Optionally, restrictions can be added. 81 | 82 | ```julia 83 | restriction1 = Restriction( 84 | 3001, 85 | "via_node", 86 | Dict{String, Any}("restriction"=>"only_straight_on","type"=>"restriction"), 87 | 2001, 88 | 2002, 89 | 1003, # must be set if restriction is via_node 90 | nothing, # must be set if restriction is via_way 91 | false, # true for no_left_turn, no_right_turn, no_u_turn, no_straight_on 92 | true # true for only_right_turn, only_left_turn, only_straight_on 93 | ) 94 | restrictions = Dict(restriction1.id => restriction1) 95 | ``` 96 | 97 | See [`Restriction`](@ref) for more information on `Restriction` objects. 98 | 99 | And then when instantiating the `OSMGraph` 100 | 101 | ```julia 102 | g = OSMGraph{U,T,W}(nodes=nodes, ways=ways, restrictions=restrictions) 103 | LightOSM.add_indexed_restrictions!(g) 104 | ``` 105 | 106 | ### Using the LightOSM stub 107 | 108 | !!! danger "This is not part of the API" 109 | Using this function is not part of the packages API, and thereore may change without respecting Semantic Versioning. It is suggested to create your own stub for your own package, rather than using this. 110 | 111 | To use the stub provided in LightOSM, run the following in your tests 112 | 113 | ```julia 114 | using LightOSM 115 | include(joinpath(pathof(LightOSM), "test", "stub.jl")) 116 | g = basic_osm_graph_stub() 117 | ``` -------------------------------------------------------------------------------- /test/shortest_path_int.jl: -------------------------------------------------------------------------------- 1 | g_distance = basic_osm_graph_stub() 2 | g_time = basic_osm_graph_stub(:time) 3 | 4 | # Basic use 5 | edge = iterate(keys(g_distance.edge_to_way))[1] 6 | node1_id = edge[1] 7 | node2_id = edge[2] 8 | path = shortest_path(g_distance, node1_id, node2_id) 9 | @test path[1] == node1_id 10 | @test path[2] == node2_id 11 | 12 | # Test using nodes rather than node IDs get same results 13 | path_from_nodes = shortest_path(g_distance, g_distance.nodes[node1_id], g_distance.nodes[node2_id]) 14 | @test path_from_nodes == path 15 | 16 | # Pass in weights directly 17 | path_with_weights = shortest_path(g_distance, g_distance.nodes[node1_id], g_distance.nodes[node2_id], g_distance.weights) 18 | @test path_with_weights == path 19 | 20 | # Also test other algorithms 21 | for T in (AStar, AStarVector, AStarDict, Dijkstra, DijkstraVector, DijkstraDict) 22 | path_T = shortest_path(T, g_distance, node1_id, node2_id) 23 | @test path_T==path 24 | end 25 | 26 | # Test edge weight sum equals the weight in g_distance.weights 27 | @test total_path_weight(g_distance, path) == g_distance.weights[g_distance.node_to_index[node1_id], g_distance.node_to_index[node2_id]] 28 | @test total_path_weight(g_distance, path) == sum(weights_from_path(g_distance, path)) 29 | n_nodes = length(g_distance.nodes) 30 | ones_weights = ones(n_nodes, n_nodes) 31 | @test total_path_weight(g_distance, path, weights=ones_weights) == 1 * (length(path) - 1) 32 | @test all(weights_from_path(g_distance, path, weights=ones_weights) .== 1) 33 | 34 | # Test time weights 35 | path_time_weights = shortest_path(g_time, node1_id, node2_id) 36 | @test path_time_weights[1] == node1_id 37 | @test path_time_weights[2] == node2_id 38 | for T in (AStar, AStarVector, AStarDict, Dijkstra, DijkstraVector, DijkstraDict) 39 | path_time_weights_T = shortest_path(T, g_time, node1_id, node2_id) 40 | @test path_time_weights_T == path_time_weights 41 | end 42 | 43 | edge_speed = g_distance.ways[g_distance.edge_to_way[edge]].tags["maxspeed"] 44 | @test isapprox(total_path_weight(g_distance, path) / total_path_weight(g_time, path), edge_speed) 45 | 46 | # Test paths we know the result of from the stub graph 47 | path = shortest_path(g_time, 1001, 1004) 48 | @test path == [1001, 1006, 1007, 1004] # this highway is twice the speed so should be quicker 49 | path = shortest_path(g_distance, 1001, 1004) 50 | @test path == [1001, 1002, 1003, 1004] 51 | 52 | # Test restriction (and bug fixed in PR #42). Restriction in stub stops 1007 -> 1004 -> 1003 right turn 53 | path = shortest_path(g_distance, 1007, 1003; cost_adjustment=(u, v, parents) -> 0.0) 54 | @test path == [1007, 1004, 1003] 55 | path = shortest_path(g_distance, 1007, 1003; cost_adjustment=restriction_cost_adjustment(g_distance)) # using g.indexed_restrictions in cost_adjustment 56 | @test path == [1007, 1006, 1001, 1002, 1003] 57 | 58 | # Test bug fixed in PR #42 59 | g_temp = deepcopy(g_distance) 60 | g_temp.weights[g_temp.node_to_index[1004], g_temp.node_to_index[1003]] = 100 61 | path = shortest_path(g_temp, 1007, 1003; cost_adjustment=(u, v, parents) -> 0.0) 62 | @test path == [1007, 1006, 1001, 1002, 1003] 63 | 64 | # Test no path returns nothing 65 | @test isnothing(shortest_path(basic_osm_graph_stub(), 1007, 1008)) 66 | 67 | # Test above with all algorithms 68 | for T in (AStar, AStarVector, AStarDict, Dijkstra, DijkstraVector, DijkstraDict) 69 | local path_T 70 | if T <: AStar 71 | path_T = shortest_path(T, g_time, 1001, 1004, heuristic=time_heuristic(g_time)) 72 | else 73 | path_T = shortest_path(T, g_time, 1001, 1004) 74 | end 75 | @test path_T == [1001, 1006, 1007, 1004] 76 | path_T = shortest_path(T, g_distance, 1001, 1004) 77 | @test path_T == [1001, 1002, 1003, 1004] 78 | path_T = shortest_path(T, g_distance, 1007, 1003; cost_adjustment=(u, v, parents) -> 0.0) 79 | @test path_T == [1007, 1004, 1003] 80 | path_T = shortest_path(T, g_distance, 1007, 1003; cost_adjustment=restriction_cost_adjustment(g_distance)) 81 | @test path_T == [1007, 1006, 1001, 1002, 1003] 82 | g_temp_T = deepcopy(g_distance) 83 | g_temp_T.weights[g_temp.node_to_index[1004], g_temp.node_to_index[1003]] = 100 84 | path_T = shortest_path(T, g_temp, 1007, 1003; cost_adjustment=(u, v, parents) -> 0.0) 85 | @test path_T == [1007, 1006, 1001, 1002, 1003] 86 | @test isnothing(shortest_path(T, basic_osm_graph_stub(), 1007, 1008)) 87 | end 88 | -------------------------------------------------------------------------------- /test/geometry.jl: -------------------------------------------------------------------------------- 1 | a1 = GeoLocation(-33.8308, 151.223, 0.0) 2 | a2 = GeoLocation(-33.8293, 151.221, 0.0) 3 | A = [a1, a2] 4 | b1 = GeoLocation(-33.8294, 150.22, 0.0) 5 | b2 = GeoLocation(-33.8301, 151.22, 0.0) 6 | B = [b1, b2] 7 | node_a1 = Node(1,a1,nothing) 8 | node_a2 = Node(1,a2,nothing) 9 | node_b1 = Node(1,b1,nothing) 10 | node_b2 = Node(1,b2,nothing) 11 | 12 | @testset "Distance tests" begin 13 | @test isapprox(LightOSM.euclidean(a1, b1), 92.6448020780204) 14 | @test all(isapprox.(LightOSM.euclidean(A, B), [92.6448020780204, 0.1282389369277561])) 15 | @test isapprox(LightOSM.haversine(a1, b1), 92.64561837286445) 16 | @test all(isapprox.(LightOSM.haversine(A, B), [92.64561837286445, 0.1282389369295829])) 17 | @test LightOSM.euclidean(a1, b1) == LightOSM.euclidean(node_a1, node_b1) 18 | @test LightOSM.euclidean(A, B) == LightOSM.euclidean([node_a1, node_a2], [node_b1, node_b2]) 19 | @test LightOSM.haversine(a1, b1) == LightOSM.haversine(node_a1, node_b1) 20 | @test LightOSM.haversine(A, B) == LightOSM.haversine([node_a1, node_a2], [node_b1, node_b2]) 21 | @test LightOSM.euclidean([a1.lat, a1.lon], [b1.lat, b1.lon]) == LightOSM.euclidean(node_a1, node_b1) 22 | @test LightOSM.haversine([a1.lat, a1.lon], [b1.lat, b1.lon]) == LightOSM.haversine(node_a1, node_b1) 23 | 24 | @test isapprox(distance(a1, b1, :euclidean), 92.6448020780204) 25 | @test isapprox(distance(a1, b1, :haversine), 92.64561837286445) 26 | @test all(isapprox.(distance(A, B, :euclidean), [92.6448020780204, 0.1282389369277561])) 27 | @test all(isapprox.(distance(A, B, :haversine), [92.64561837286445, 0.1282389369295829])) 28 | @test_throws ArgumentError distance(a1, b1, :unknown_method) 29 | end 30 | 31 | @testset "Heading tests" begin 32 | a = GeoLocation(-33.8308, 151.223, 0.0) 33 | b = GeoLocation(-33.8294, 151.22, 0.0) 34 | # Used http://instantglobe.com/CRANES/GeoCoordTool.html to manually calculate headings and distance 35 | # Note: heading function returns bearings in range of [-180, 180], to convert to [0, 360] scale we need to adjust by (θ + 360) % 360 36 | @test abs((heading(a, b, :degrees) + 360) % 360 - 299.32559505087795) < 0.01 37 | @test sum((abs.(heading(A, B, :degrees) .+ 360) .% 360 - [299.32559505087795, 226.07812206538435])) < 0.01 38 | 39 | # Node methods 40 | @test heading(A, B, :degrees) == heading([node_a1, node_a2], [node_b1, node_b2], :degrees) 41 | 42 | # Radians 43 | @test isapprox(heading(a, b, :radians), deg2rad(heading(a, b, :degrees))) 44 | 45 | # Error handling 46 | @test_throws ArgumentError heading(a, b, :unknown_units) 47 | end 48 | 49 | @testset "calculate_location tests" begin 50 | a = GeoLocation(-33.8308, 151.223, 0.0) 51 | b = GeoLocation(-33.8294, 151.22, 0.0) 52 | dist_a = 100.0 53 | dist_b = 100.0 54 | heading_a = 10.0 55 | heading_b = 10.0 56 | end_a = calculate_location(a, heading_a, dist_a) 57 | @test isapprox(end_a.lat, -32.945001009035984) 58 | @test isapprox(end_a.lon, 151.40908284685628) 59 | @test isapprox(end_a.alt, 0.0) 60 | ends = calculate_location([a, b], [heading_a, heading_b], [dist_a, dist_b]) 61 | @test ends[1] == end_a 62 | 63 | @test calculate_location(a, heading_a, 0.0) == a 64 | 65 | # Node methods 66 | node_a = Node(1, a, Dict{String, Any}()) 67 | node_b = Node(1, b, Dict{String, Any}()) 68 | @test calculate_location([a, b], [heading_a, heading_b], [dist_a, dist_b]) == calculate_location([node_a, node_b], [heading_a, heading_b], [dist_a, dist_b]) 69 | end 70 | 71 | @testset "nearest_point_on_line tests" begin 72 | # Matching middle of line 73 | x, y, pos = LightOSM.nearest_point_on_line( 74 | 1.0, 1.0, 75 | 2.0, 2.0, 76 | 2.0, 1.0 77 | ) 78 | @test x ≈ 1.5 79 | @test y ≈ 1.5 80 | @test pos ≈ 0.5 81 | # Matching start of line 82 | x, y, pos = LightOSM.nearest_point_on_line( 83 | 1.0, 1.0, 84 | 2.0, 2.0, 85 | 0.0, 1.0 86 | ) 87 | @test x ≈ 1.0 88 | @test y ≈ 1.0 89 | @test pos ≈ 0.0 90 | # Matching end of line 91 | x, y, pos = LightOSM.nearest_point_on_line( 92 | 1.0, 1.0, 93 | 2.0, 2.0, 94 | 3.0, 4.0 95 | ) 96 | @test x ≈ 2.0 97 | @test y ≈ 2.0 98 | @test pos ≈ 1.0 99 | end 100 | -------------------------------------------------------------------------------- /test/download.jl: -------------------------------------------------------------------------------- 1 | @testset "Downloader selection" begin 2 | @test LightOSM.osm_network_downloader(:place_name) == LightOSM.osm_network_from_place_name 3 | @test LightOSM.osm_network_downloader(:bbox) == LightOSM.osm_network_from_bbox 4 | @test LightOSM.osm_network_downloader(:point) == LightOSM.osm_network_from_point 5 | @test LightOSM.osm_network_downloader(:polygon) == LightOSM.osm_network_from_polygon 6 | @test LightOSM.osm_network_downloader(:custom_filters) == LightOSM.osm_network_from_custom_filters 7 | @test_throws ArgumentError LightOSM.osm_network_downloader(:doesnt_exist) 8 | end 9 | 10 | function wait_for_overpass() 11 | count = 0 12 | while !LightOSM.is_overpass_server_availabile() 13 | count == 7 && break # Can't wait indefinitely, and the failure will be caught in the tests 14 | count += 1 15 | @info "Waiting for overpass server..." 16 | sleep(5 * count) 17 | end 18 | end 19 | 20 | @testset "Downloads" begin 21 | filenames = ["map.osm", "map.json"] 22 | formats = [:osm, :json] 23 | for (filename, format) in zip(filenames, formats) 24 | try 25 | wait_for_overpass() 26 | data = download_osm_network(:point, 27 | radius=0.5, 28 | point=GeoLocation(-37.8136, 144.9631), 29 | network_type=:drive, 30 | download_format=format, 31 | save_to_file_location=filename) 32 | @test isfile(filename) 33 | g = graph_from_file(filename) # Check it doesn't error 34 | catch err 35 | # Sometimes gets HTTP.ExceptionRequest.StatusError in tests due to connection to overpass 36 | !isa(err, HTTP.ExceptionRequest.StatusError) && rethrow() 37 | @error "Test failed due to connection issue" exception = (err, catch_backtrace()) 38 | end 39 | 40 | try 41 | rm(filename) 42 | catch 43 | end 44 | end 45 | end 46 | 47 | @testset "Download with custom filters" begin 48 | filename = normpath("map.json") # for compatability with Windows 49 | format = :json 50 | #= 51 | Compared to the defauilt network_type=:drive, this filter: 52 | - Excludes all ways with highway=tertiary, secondary, primary, living_street 53 | - Includes all ways with highway=service 54 | =# 55 | custom_filters = """ 56 | way 57 | ["highway"] 58 | ["motorcar"!~"no"] 59 | ["area"!~"yes"] 60 | ["highway"!~"elevator|steps|tertiary|construction|bridleway|proposed|track|pedestrian|secondary|path|living_street|cycleway|primary|footway|platform|abandoned|escalator|corridor|raceway"] 61 | ["motor_vehicle"!~"no"]["access"!~"private"] 62 | ["service"!~"parking|parking_aisle|driveway|private|emergency_access"] 63 | ; 64 | > 65 | ; 66 | """ 67 | bbox = [-37.816779513558274, 144.9590750877158, -37.81042034950731, 144.967124565619] 68 | 69 | try 70 | wait_for_overpass() 71 | test_custom_query = download_osm_network( 72 | :custom_filters, 73 | download_format=format, 74 | save_to_file_location=filename, 75 | custom_filters=custom_filters, 76 | bbox = bbox 77 | ) 78 | 79 | @test isfile(filename) 80 | g = graph_from_file(filename, filter_network_type=false) 81 | 82 | # Make sure Overpass included/excluded these tags according to the custom filter 83 | excluded_tags = ["primary", "secondary", "tertiary", "living_street"] 84 | included_tags = ["service"] 85 | included_tags_count = 0 86 | for (_, way) in g.ways 87 | if haskey(way.tags, "highway") 88 | @test way.tags["highway"] ∉ excluded_tags 89 | (way.tags["highway"] ∈ included_tags) && (included_tags_count += 1) 90 | end 91 | end 92 | @test included_tags_count > 0 93 | catch err 94 | # Sometimes gets HTTP.ExceptionRequest.StatusError in tests due to connection to overpass 95 | !isa(err, HTTP.ExceptionRequest.StatusError) && rethrow() 96 | @error "Test failed due to connection issue" exception = (err, catch_backtrace()) 97 | end 98 | 99 | # Remove file after test 100 | try 101 | rm(filename) 102 | catch 103 | end 104 | end -------------------------------------------------------------------------------- /test/shortest_path_str.jl: -------------------------------------------------------------------------------- 1 | g_distance = basic_osm_graph_stub_string() 2 | g_time = basic_osm_graph_stub_string(:time) 3 | 4 | # Basic use 5 | edge = iterate(keys(g_distance.edge_to_way))[1] 6 | node1_id = edge[1] 7 | node2_id = edge[2] 8 | path = shortest_path(g_distance, node1_id, node2_id) 9 | @test path[1] == node1_id 10 | @test path[2] == node2_id 11 | 12 | # Test using nodes rather than node IDs get same results 13 | path_from_nodes = shortest_path(g_distance, g_distance.nodes[node1_id], g_distance.nodes[node2_id]) 14 | @test path_from_nodes == path 15 | 16 | # Pass in weights directly 17 | path_with_weights = shortest_path(g_distance, g_distance.nodes[node1_id], g_distance.nodes[node2_id], g_distance.weights) 18 | @test path_with_weights == path 19 | 20 | # Also test other algorithms 21 | for T in (AStar, AStarVector, AStarDict, Dijkstra, DijkstraVector, DijkstraDict) 22 | path_T = shortest_path(T, g_distance, node1_id, node2_id) 23 | @test path_T==path 24 | end 25 | 26 | # Test edge weight sum equals the weight in g_distance.weights 27 | @test total_path_weight(g_distance, path) == g_distance.weights[g_distance.node_to_index[node1_id], g_distance.node_to_index[node2_id]] 28 | @test total_path_weight(g_distance, path) == sum(weights_from_path(g_distance, path)) 29 | n_nodes = length(g_distance.nodes) 30 | ones_weights = ones(n_nodes, n_nodes) 31 | @test total_path_weight(g_distance, path, weights=ones_weights) == 1 * (length(path) - 1) 32 | @test all(weights_from_path(g_distance, path, weights=ones_weights) .== 1) 33 | 34 | # Test time weights 35 | path_time_weights = shortest_path(g_time, node1_id, node2_id) 36 | @test path_time_weights[1] == node1_id 37 | @test path_time_weights[2] == node2_id 38 | for T in (AStar, AStarVector, AStarDict, Dijkstra, DijkstraVector, DijkstraDict) 39 | path_time_weights_T = shortest_path(T, g_time, node1_id, node2_id) 40 | @test path_time_weights_T == path_time_weights 41 | end 42 | 43 | edge_speed = g_distance.ways[g_distance.edge_to_way[edge]].tags["maxspeed"] 44 | @test isapprox(total_path_weight(g_distance, path) / total_path_weight(g_time, path), edge_speed) 45 | 46 | # Test paths we know the result of from the stub graph 47 | path = shortest_path(g_time, "1001", "1004") 48 | @test path == ["1001", "1006", "1007", "1004"] # this highway is twice the speed so should be quicker 49 | path = shortest_path(g_distance, "1001", "1004") 50 | @test path == ["1001", "1002", "1003", "1004"] 51 | 52 | # Test restriction (and bug fixed in PR #42). Restriction in stub stops 1007 -> 1004 -> 1003 right turn 53 | path = shortest_path(g_distance, "1007", "1003"; cost_adjustment=(u, v, parents) -> 0.0) 54 | @test path == ["1007", "1004", "1003"] 55 | path = shortest_path(g_distance, "1007", "1003"; cost_adjustment=restriction_cost_adjustment(g_distance)) # using g.indexed_restrictions in cost_adjustment 56 | @test path == ["1007", "1006", "1001", "1002", "1003"] 57 | 58 | # Test bug fixed in PR #42 59 | g_temp = deepcopy(g_distance) 60 | g_temp.weights[g_temp.node_to_index["1004"], g_temp.node_to_index["1003"]] = 100 61 | path = shortest_path(g_temp, "1007", "1003"; cost_adjustment=(u, v, parents) -> 0.0) 62 | @test path == ["1007", "1006", "1001", "1002", "1003"] 63 | 64 | # Test no path returns nothing 65 | @test isnothing(shortest_path(basic_osm_graph_stub_string(), "1007", "1008")) 66 | 67 | # Test above with all algorithms 68 | for T in (AStar, AStarVector, AStarDict, Dijkstra, DijkstraVector, DijkstraDict) 69 | local path_T 70 | if T <: AStar 71 | path_T = shortest_path(T, g_time, "1001", "1004", heuristic=time_heuristic(g_time)) 72 | else 73 | path_T = shortest_path(T, g_time, "1001", "1004") 74 | end 75 | @test path_T == ["1001", "1006", "1007", "1004"] 76 | path_T = shortest_path(T, g_distance, "1001", "1004") 77 | @test path_T == ["1001", "1002", "1003", "1004"] 78 | path_T = shortest_path(T, g_distance, "1007", "1003"; cost_adjustment=(u, v, parents) -> 0.0) 79 | @test path_T == ["1007", "1004", "1003"] 80 | path_T = shortest_path(T, g_distance, "1007", "1003"; cost_adjustment=restriction_cost_adjustment(g_distance)) 81 | @test path_T == ["1007", "1006", "1001", "1002", "1003"] 82 | g_temp_T = deepcopy(g_distance) 83 | g_temp_T.weights[g_temp.node_to_index["1004"], g_temp.node_to_index["1003"]] = 100 84 | path_T = shortest_path(T, g_temp, "1007", "1003"; cost_adjustment=(u, v, parents) -> 0.0) 85 | @test path_T == ["1007", "1006", "1001", "1002", "1003"] 86 | @test isnothing(shortest_path(T, basic_osm_graph_stub_string(), "1007", "1008")) 87 | end 88 | -------------------------------------------------------------------------------- /benchmark/benchmarks_plot.jl: -------------------------------------------------------------------------------- 1 | using Plots 2 | using DataFrames 3 | using JSON 4 | using LightOSM 5 | using DataStructures 6 | 7 | results = JSON.parsefile(joinpath(@__DIR__, "benchmark_results.json")) 8 | 9 | time_units_mapping = Dict( 10 | "s" => 1, 11 | "ms" => 1e-3, 12 | "μs" => 1e-9 13 | ) 14 | 15 | memory_units_mapping = Dict( 16 | "MiB" => 1, 17 | "KiB" => 1e-3, 18 | "GiB" => 1e+3, 19 | ) 20 | 21 | df_dict_display = DefaultDict(Dict) 22 | df_dict_time = DefaultDict(Dict) 23 | df_dict_memory = DefaultDict(Dict) 24 | 25 | for (algorithm, r) in results 26 | n_paths = collect(keys(r)) 27 | n_paths_numeric = parse.(Float64, n_paths) 28 | 29 | df_dict_display[algorithm] = DefaultDict(Any[], "No. Paths" => n_paths) 30 | df_dict_time[algorithm] = DefaultDict(Number[], "No. Paths" => n_paths_numeric) 31 | df_dict_memory[algorithm] = DefaultDict(Number[], "No. Paths" => n_paths_numeric) 32 | 33 | for (n, packages) in r 34 | for (p, items) in packages 35 | mean_time = items["mean time"] 36 | gc_match = match(r"\((.*?)\)", mean_time) 37 | gc = LightOSM.remove_non_numeric(gc_match[1]) # % 38 | mean_time = replace(mean_time, gc_match.match => "") 39 | mean_time_units = strip(LightOSM.remove_numeric(mean_time)) 40 | mean_time_units_factor = time_units_mapping[mean_time_units] 41 | mean_time = LightOSM.remove_non_numeric(mean_time) * mean_time_units_factor # s 42 | 43 | allocs = items["allocs estimate"] 44 | allocs = LightOSM.remove_non_numeric(allocs) 45 | 46 | memory = items["memory estimate"] 47 | memory_units = strip(LightOSM.remove_numeric(memory)) 48 | memory_units_factor = memory_units_mapping[memory_units] 49 | memory = LightOSM.remove_non_numeric(memory) * memory_units_factor # MiB 50 | 51 | metrics = "$(round(mean_time, digits=4))s ($(round(memory, digits=2)) MiB)" 52 | push!(df_dict_display[algorithm][p], metrics) 53 | push!(df_dict_time[algorithm][p], mean_time) 54 | push!(df_dict_memory[algorithm][p], memory) 55 | 56 | end 57 | end 58 | end 59 | 60 | # Summary Tables 61 | dijkstra_results = DataFrame(;[Symbol(k) => v for (k, v) in df_dict_display["dijkstra"]]...) 62 | sort!(dijkstra_results, "No. Paths") 63 | dijkstra_results = dijkstra_results[:, [Symbol("No. Paths"), :losm, :losm_precompute, :osmx, :lg]] 64 | astar_results = DataFrame(;[Symbol(k) => v for (k, v) in df_dict_display["astar"]]...) 65 | sort!(astar_results, "No. Paths") 66 | astar_results = astar_results[:, [Symbol("No. Paths"), :losm, :losm_precompute, :osmx, :lg]] 67 | 68 | # Dijsktra Time Comparison 69 | dijkstra_df_time = DataFrame(;[Symbol(k) => v for (k, v) in df_dict_time["dijkstra"]]...) 70 | sort!(dijkstra_df_time, "No. Paths") 71 | 72 | dijkstra_time_x = dijkstra_df_time["No. Paths"] 73 | dijkstra_time_y = Matrix(select!(dijkstra_df_time, Not(Symbol("No. Paths")))) 74 | dijkstra_time_labels = permutedims(names(dijkstra_df_time)) 75 | plot_dijkstra_time() = plot(dijkstra_time_x, dijkstra_time_y, title="Dijkstra Shortest Path - Runtime (s) vs. No. Paths", label=dijkstra_time_labels, ylabel="Runtime (s)", xlabel="No. Paths", lw=3, fmt=:png) 76 | 77 | # Dijsktra Memory Comparison 78 | dijkstra_df_memory = DataFrame(;[Symbol(k) => v for (k, v) in df_dict_memory["dijkstra"]]...) 79 | sort!(dijkstra_df_memory, "No. Paths") 80 | 81 | dijkstra_memory_x = dijkstra_df_memory["No. Paths"] 82 | dijkstra_memory_y = Matrix(select!(dijkstra_df_memory, Not(Symbol("No. Paths")))) 83 | dijkstra_memory_labels = permutedims(names(dijkstra_df_memory)) 84 | plot_dijkstra_memory() = plot(dijkstra_memory_x, dijkstra_memory_y, title="Dijkstra Shortest Path - Memory (MiB) vs. No. Paths", label=dijkstra_memory_labels, ylabel="Memory (MiB)", xlabel="No. Paths", lw=3, fmt=:png) 85 | 86 | # A* Time Comparison 87 | astar_df_time = DataFrame(;[Symbol(k) => v for (k, v) in df_dict_time["astar"]]...) 88 | sort!(astar_df_time, "No. Paths") 89 | 90 | astar_time_x = astar_df_time["No. Paths"] 91 | astar_time_y = Matrix(select!(astar_df_time, Not(Symbol("No. Paths")))) 92 | astar_time_labels = permutedims(names(astar_df_time)) 93 | plot_astar_time() = plot(astar_time_x, astar_time_y, title="A* Shortest Path - Runtime (s) vs. No. Paths", label=astar_time_labels, ylabel="Runtime (s)", xlabel="No. Paths", lw=3, fmt=:png) 94 | 95 | # A* Memory Comparison 96 | astar_df_memory = DataFrame(;[Symbol(k) => v for (k, v) in df_dict_memory["astar"]]...) 97 | sort!(astar_df_memory, "No. Paths") 98 | 99 | astar_memory_x = astar_df_memory["No. Paths"] 100 | astar_memory_y = Matrix(select!(astar_df_memory, Not(Symbol("No. Paths")))) 101 | astar_memory_labels = permutedims(names(astar_df_memory)) 102 | plot_astar_memory() = plot(astar_memory_x, astar_memory_y, title="A* Shortest Path - Memory (MiB) vs. No. Paths", label=astar_memory_labels, ylabel="Memory (MiB)", xlabel="No. Paths", lw=3, fmt=:png) -------------------------------------------------------------------------------- /src/nearest_way.jl: -------------------------------------------------------------------------------- 1 | """ 2 | nearest_way(g, point, [search_radius]) 3 | 4 | Finds the nearest way from a point using a `SpatialIndexing.jl` R-tree. 5 | 6 | The search area is a cube centred on `point` with an edge length of `2 * search_radius`. If 7 | `search_radius` is not specified, it is automatically determined from the distance to the 8 | nearest node. 9 | 10 | A larger `search_radius` will incur a performance penalty as more ways must be checked for 11 | their closest points. Choose this value carefully according to the desired use case. 12 | 13 | # Arguments 14 | - `g::OSMGraph`: Graph container. 15 | - `point`: Single point as a `GeoLocation` or `[lat, lon, alt]`. 16 | - `search_radius::AbstractFloat`: Size of cube to search around `point`. 17 | 18 | # Return 19 | - `::Tuple{<:Integer,Float64,EdgePoint}`: If nearest way was found. 20 | - `::Integer`: Nearest way ID. 21 | - `::Float64`: Distance to nearest way. 22 | - `::EdgePoint`: Closest point on the nearest way. 23 | - `::Tuple{Nothing,Nothing,Nothing}`: If no ways were found within `search_radius`. 24 | """ 25 | nearest_way(g::OSMGraph, point::AbstractVector{<:AbstractFloat}, search_radius::Union{Nothing,AbstractFloat}=nothing) = nearest_way(g, GeoLocation(point), search_radius) 26 | function nearest_way(g::OSMGraph{U,T,W}, 27 | point::GeoLocation, 28 | search_radius::Union{Nothing,AbstractFloat}=nothing 29 | )::Union{Tuple{T,Float64,EdgePoint},Tuple{Nothing,Nothing,Nothing}} where {U,T,W} 30 | # Automatically determine search radius from nearest node 31 | # WARNING: this won't work if there are nodes not attached to a way. This is not 32 | # currently possible in LightOSM but would need to be changed if this ever happens. 33 | if isnothing(search_radius) 34 | _, search_radius = nearest_node(g, point) 35 | end 36 | 37 | # Get nearest ways 38 | x = nearest_ways(g, point, search_radius) 39 | 40 | # Returning a tuple here to keep it compatible with this syntax: 41 | # way_ids, dists, ep = nearest_way(g, p, ...) 42 | isempty(x[1]) && return nothing, nothing, nothing 43 | 44 | # Return the closest way 45 | _, nearest_idx = findmin(x[2]) 46 | return x[1][nearest_idx], x[2][nearest_idx], x[3][nearest_idx] 47 | end 48 | 49 | """ 50 | nearest_ways(g, point, [search_radius]) 51 | 52 | Finds nearby ways from a point using a `SpatialIndexing.jl` R-tree. 53 | 54 | The search area is a cube centred on `point` with an edge length of `2 * search_radius`. 55 | 56 | A larger `search_radius` will incur a performance penalty as more ways must be checked for 57 | their closest points. Choose this value carefully according to the desired use case. 58 | 59 | # Arguments 60 | - `g::OSMGraph`: Graph container. 61 | - `point`/`points`: Single point as a `GeoLocation` or `[lat, lon, alt]`. 62 | - `search_radius::AbstractFloat=0.1`: Size of cube to search around `point`. 63 | 64 | # Return 65 | - `::Tuple{Vector{<:Integer},Vector{Float64},Vector{EdgePoint}}`: 66 | - `::Vector{<:Integer}`: Nearest way IDs. 67 | - `::Vector{Float64}`: Distance to each corresponding nearby way. 68 | - `::Vector{EdgePoint}`: Closest point on each corresponding nearby way. 69 | """ 70 | nearest_ways(g::OSMGraph, point::Vector{<:AbstractFloat}, search_radius::AbstractFloat=0.1) = nearest_ways(g, GeoLocation(point), search_radius) 71 | function nearest_ways(g::OSMGraph{U,T,W}, 72 | point::GeoLocation, 73 | search_radius::AbstractFloat=0.1 74 | )::Tuple{Vector{T},Vector{Float64},Vector{EdgePoint}} where {U,T,W} 75 | # Construct a cube around the point in Cartesian space 76 | p = to_cartesian(point) 77 | bbox = SpatialIndexing.Rect( 78 | (p[1] - search_radius, p[2] - search_radius, p[3] - search_radius), 79 | (p[1] + search_radius, p[2] + search_radius, p[3] + search_radius) 80 | ) 81 | 82 | # Find all nearby way IDs that intersect with this point 83 | way_ids = T[T(x.id) for x in intersects_with(g.rtree, bbox)] 84 | 85 | # Calculate distances to each way ID (nearest_points is a vector of tuples) 86 | nearest_points = nearest_point_on_way.(Ref(g), Ref(point), way_ids) 87 | 88 | return way_ids, [x[2] for x in nearest_points], [x[1] for x in nearest_points] 89 | end 90 | 91 | """ 92 | nearest_point_on_way(g::OSMGraph, point::GeoLocation, way_id::DEFAULT_OSM_ID_TYPE) 93 | 94 | Finds the nearest position on a way to a given point. Matches to an `EdgePoint`. 95 | 96 | # Arguments 97 | - `g::OSMGraph`: LightOSM graph. 98 | - `point::GeoLocation`: Point to find nearest position to. 99 | - `wid::Integer`: Way ID to search. 100 | 101 | # Returns 102 | - `::Tuple`: 103 | - `::EdgePoint`: Nearest position along the way between two nodes. 104 | - `::Float64`: Distance from `point` to the nearest position on the way. 105 | """ 106 | function nearest_point_on_way(g::OSMGraph, point::GeoLocation, way_id::DEFAULT_OSM_ID_TYPE) 107 | nodes = g.ways[way_id].nodes 108 | min_edge = nothing 109 | min_dist = floatmax() 110 | min_pos = 0.0 111 | for edge in zip(nodes[1:end-1], nodes[2:end]) 112 | x1 = g.nodes[edge[1]].location.lon 113 | y1 = g.nodes[edge[1]].location.lat 114 | x2 = g.nodes[edge[2]].location.lon 115 | y2 = g.nodes[edge[2]].location.lat 116 | x, y, pos = nearest_point_on_line(x1, y1, x2, y2, point.lon, point.lat) 117 | d = distance(GeoLocation(y, x), point) 118 | if d < min_dist 119 | min_edge = edge 120 | min_dist = d 121 | min_pos = pos 122 | end 123 | end 124 | return EdgePoint(min_edge[1], min_edge[2], min_pos), min_dist 125 | end -------------------------------------------------------------------------------- /test/utilities.jl: -------------------------------------------------------------------------------- 1 | @testset "deserializers" begin 2 | @test LightOSM.string_deserializer(:xml) == LightXML.parse_string 3 | @test LightOSM.string_deserializer(:osm) == LightXML.parse_string 4 | @test LightOSM.string_deserializer(:json) == JSON.parse 5 | @test_throws ArgumentError LightOSM.string_deserializer(:other) 6 | end 7 | 8 | @testset "string utils" begin 9 | #tryparse_string_to_number 10 | @test LightOSM.tryparse_string_to_number("1") === 1 11 | @test LightOSM.tryparse_string_to_number("1.0") === 1.0 12 | @test LightOSM.tryparse_string_to_number("a") === "a" 13 | 14 | # remove_non_numeric 15 | @test LightOSM.remove_non_numeric("1.0") === 1.0 16 | @test LightOSM.remove_non_numeric("1") === 1 17 | @test LightOSM.remove_non_numeric("a") === 0 18 | 19 | # remove_sub_string_after 20 | @test LightOSM.remove_sub_string_after("after_this_remove", "after_this") === "" 21 | @test LightOSM.remove_sub_string_after("keep_after_this_remove", "after_this") === "keep_" 22 | @test LightOSM.remove_sub_string_after("keep_after_this_remove_after_this", "after_this") === "keep_" 23 | 24 | # remove_numeric 25 | @test LightOSM.remove_numeric("a1a") === "aa" 26 | @test LightOSM.remove_numeric("a1.0a") === "aa" 27 | @test LightOSM.remove_numeric("aa1bb2") === "aabb" 28 | end 29 | 30 | @testset "array tools" begin 31 | # trailing_elements 32 | @test LightOSM.trailing_elements([1]) == [1,1] 33 | @test LightOSM.trailing_elements([1,3]) == [1,3] 34 | @test LightOSM.trailing_elements([1,2,3]) == [1,3] 35 | 36 | # first_common_trailing_element 37 | @test LightOSM.first_common_trailing_element([1,2],[2,1]) == 1 38 | @test LightOSM.first_common_trailing_element([1,2],[2,2]) == 2 39 | @test LightOSM.first_common_trailing_element([1,3, 2],[2,3, 2]) == 2 40 | @test_throws ArgumentError LightOSM.first_common_trailing_element([1,2],[3,4]) == 3 41 | @test_throws ArgumentError LightOSM.first_common_trailing_element([1,5, 2],[3,5, 4]) == 3 42 | 43 | # join_two_arrays_on_common_trailing_elements 44 | @test LightOSM.join_two_arrays_on_common_trailing_elements([1,2,3],[3,4,5]) == [1,2,3,4,5] 45 | @test LightOSM.join_two_arrays_on_common_trailing_elements([1,2,3],[5,4,3]) == [1,2,3,4,5] 46 | @test LightOSM.join_two_arrays_on_common_trailing_elements([3,2,1],[5,4,3]) == [1,2,3,4,5] 47 | @test LightOSM.join_two_arrays_on_common_trailing_elements([3,2,1],[3,4,5]) == [1,2,3,4,5] 48 | @test LightOSM.join_two_arrays_on_common_trailing_elements([3,2,1],[1,4,3]) == [1,2,3,4,1] 49 | 50 | # join_arrays_on_common_trailing_elements 51 | @test LightOSM.join_arrays_on_common_trailing_elements([1,2],[2,3],[3,4]) == [1,2,3,4] 52 | @test LightOSM.join_arrays_on_common_trailing_elements([1,2],[4,3],[3,2]) == [1,2,3,4] 53 | @test LightOSM.join_arrays_on_common_trailing_elements([1,2]) == [1,2] 54 | @test_throws ErrorException LightOSM.join_arrays_on_common_trailing_elements([1,2],[4,3],[5,6]) == [1,2,3,4] 55 | 56 | # flatten 57 | @test LightOSM.flatten([1]) == [1] 58 | @test LightOSM.flatten([[1,2],[[3], [4]],1,2]) == [1,2,3,4,1,2] 59 | end 60 | 61 | @testset "file_deserializer" begin 62 | # check_valid_filename 63 | @test LightOSM.check_valid_filename("map.osm") 64 | @test LightOSM.check_valid_filename("map.json") 65 | @test LightOSM.check_valid_filename("map.xml") 66 | @test_throws ArgumentError LightOSM.check_valid_filename("map.osm.doc") 67 | @test_throws ArgumentError LightOSM.check_valid_filename("map.json.doc") 68 | @test_throws ArgumentError LightOSM.check_valid_filename("map.xml.doc") 69 | @test_throws ArgumentError LightOSM.check_valid_filename("map") 70 | 71 | # file_deserializer 72 | files = ["data.osm", "data.xml", "data.json", "data.doc"] 73 | foreach(touch, files) 74 | 75 | @test LightOSM.file_deserializer("data.osm") == LightXML.parse_file 76 | @test LightOSM.file_deserializer("data.xml") == LightXML.parse_file 77 | @test LightOSM.file_deserializer("data.json") == JSON.parsefile 78 | @test_throws ArgumentError LightOSM.file_deserializer("data.doc") 79 | 80 | foreach(rm, files) # tidy up 81 | end 82 | 83 | @testset "validate_save_location" begin 84 | osmfilename = "folder/filename.osm" 85 | @test LightOSM.validate_save_location(osmfilename, :osm) == osmfilename 86 | @test_throws ArgumentError LightOSM.validate_save_location(osmfilename, :xml) 87 | @test_throws ArgumentError LightOSM.validate_save_location(osmfilename, :json) 88 | 89 | jsonfilename = "folder/filename.json" 90 | @test LightOSM.validate_save_location(jsonfilename, :json) == jsonfilename 91 | @test_throws ArgumentError LightOSM.validate_save_location(jsonfilename, :osm) 92 | @test_throws ArgumentError LightOSM.validate_save_location(jsonfilename, :xml) 93 | 94 | xmlfilename = "folder/filename.xml" 95 | @test LightOSM.validate_save_location(xmlfilename, :xml) == xmlfilename 96 | @test_throws ArgumentError LightOSM.validate_save_location(xmlfilename, :osm) 97 | @test_throws ArgumentError LightOSM.validate_save_location(xmlfilename, :json) 98 | 99 | noextension = "filename" 100 | @test LightOSM.validate_save_location(noextension, :xml) == noextension * ".xml" 101 | @test LightOSM.validate_save_location(noextension, :json) == noextension * ".json" 102 | @test LightOSM.validate_save_location(noextension, :osm) == noextension * ".osm" 103 | end 104 | 105 | @testset "type consistency" begin 106 | floats = Vector{Vector{Float64}}([ 107 | Vector{Float64}([23.0, 123.0]), 108 | Vector{Float64}([-23.0, 122.0]), 109 | Vector{Float64}([-30.0, 121.0]) 110 | ]) 111 | ints = Vector{Vector{Int64}}([ 112 | Vector{Int64}([23, 123]), 113 | Vector{Int64}([-23, 122]), 114 | Vector{Int64}([-30, 121]) 115 | ]) 116 | @test GeoLocation(floats[1][1], floats[1][2]) === GeoLocation(ints[1][1], ints[1][2]) 117 | @test GeoLocation(floats[1]) === GeoLocation(ints[1]) 118 | @test GeoLocation(floats) == GeoLocation(ints) 119 | @test typeof(GeoLocation(floats)) === typeof(GeoLocation(ints)) 120 | end -------------------------------------------------------------------------------- /test/traversal.jl: -------------------------------------------------------------------------------- 1 | g1, w1 = stub_graph1() 2 | g2, w2 = stub_graph2() 3 | g3, w3 = stub_graph3() 4 | 5 | # No goal, returns parents 6 | @test LightOSM.dijkstra(g1, w1, 1) == [0, 1, 1, 1, 4] 7 | @test LightOSM.dijkstra(g2, w2, 1) == [0, 1, 4, 1, 4, 7, 4] 8 | @test LightOSM.dijkstra(g3, w3, 1) == [0, 4, 2, 1, 4] 9 | 10 | # With goal, returns shortest path 11 | @test LightOSM.astar(g1, w1, 1, 5) == LightOSM.dijkstra(g1, w1, 1, 5) == [1, 4, 5] 12 | @test LightOSM.astar(g2, w2, 1, 6) == LightOSM.dijkstra(g2, w2, 1, 6) == [1, 4, 7, 6] 13 | @test LightOSM.astar(g3, w3, 1, 3) == LightOSM.dijkstra(g3, w3, 1, 3) == [1, 4, 2, 3] 14 | for (T1, T2) in Base.product((AStar, AStarVector, AStarDict), (Dijkstra, DijkstraVector, DijkstraVector)) 15 | @test LightOSM.astar(T1, g1, w1, 1, 5) == LightOSM.dijkstra(T2, g1, w1, 1, 5) == [1, 4, 5] 16 | @test LightOSM.astar(T1, g2, w2, 1, 6) == LightOSM.dijkstra(T2, g2, w2, 1, 6) == [1, 4, 7, 6] 17 | @test LightOSM.astar(T1, g3, w3, 1, 3) == LightOSM.dijkstra(T2, g3, w3, 1, 3) == [1, 4, 2, 3] 18 | end 19 | 20 | # Construct shortest path from parents 21 | @test LightOSM.path_from_parents(LightOSM.dijkstra(g1, w1, 1), 5) == [1, 4, 5] 22 | @test LightOSM.path_from_parents(LightOSM.dijkstra(g2, w2, 1), 6) == [1, 4, 7, 6] 23 | @test LightOSM.path_from_parents(LightOSM.dijkstra(g3, w3, 1), 3) == [1, 4, 2, 3] 24 | 25 | # astar with heuristic 26 | g = basic_osm_graph_stub() 27 | 28 | for T in (AStar, AStarVector, AStarDict) 29 | LightOSM.astar( 30 | T, 31 | g.graph, 32 | g.weights, 33 | g.node_to_index[1008], 34 | g.node_to_index[1003]; 35 | heuristic=LightOSM.distance_heuristic(g) 36 | ) == [ 37 | g.node_to_index[1008], 38 | g.node_to_index[1007], 39 | g.node_to_index[1004], 40 | g.node_to_index[1003] 41 | ] 42 | 43 | @test LightOSM.astar( 44 | T, 45 | g.graph, 46 | g.weights, 47 | g.node_to_index[1008], 48 | g.node_to_index[1002]; 49 | heuristic=LightOSM.distance_heuristic(g) 50 | ) == [ 51 | g.node_to_index[1008], 52 | g.node_to_index[1007], 53 | g.node_to_index[1004], 54 | g.node_to_index[1003], 55 | g.node_to_index[1002] 56 | ] 57 | 58 | @test LightOSM.astar( 59 | T, 60 | g.graph, 61 | g.weights, 62 | g.node_to_index[1003], 63 | g.node_to_index[1008]; 64 | heuristic=LightOSM.distance_heuristic(g) 65 | ) === nothing 66 | end 67 | 68 | # download graph, pick random nodes and test dijkstra and astar equality 69 | data = HTTP.get(TEST_OSM_URL) 70 | data = JSON.parse(String(data.body)) 71 | 72 | # distance weights 73 | distance_g = LightOSM.graph_from_object(data; graph_type=:static, weight_type=:distance) 74 | origin = rand(1:length(distance_g.nodes)) 75 | destination = rand(1:length(distance_g.nodes)) 76 | 77 | astar_path = LightOSM.astar(distance_g.graph, distance_g.weights, origin, destination; heuristic=LightOSM.distance_heuristic(distance_g)) 78 | dijkstra_path = LightOSM.dijkstra(distance_g.graph, distance_g.weights, origin, destination) 79 | @test astar_path == dijkstra_path 80 | 81 | if !isnothing(astar_path) && !isnothing(dijkstra_path) 82 | @test total_path_weight(distance_g, index_to_node_id(distance_g, astar_path)) == total_path_weight(distance_g, index_to_node_id(distance_g, dijkstra_path)) 83 | @test astar_path[1] == origin 84 | @test astar_path[end] == destination 85 | end 86 | 87 | result = Ref{Bool}(true) 88 | for destination in 1:length(distance_g.nodes) 89 | # shortest path from vertex 4 to all others 90 | # vertex 4 is sensitive to heuristic choice (i.e. yields non-optiomal solution if a poor heuristic is chosen) 91 | local origin = 4 92 | local astar_path = LightOSM.astar(distance_g.graph, distance_g.weights, origin, destination; heuristic=LightOSM.distance_heuristic(distance_g)) 93 | local dijkstra_path = LightOSM.dijkstra(distance_g.graph, distance_g.weights, origin, destination) 94 | (isnothing(astar_path) || isnothing(dijkstra_path)) && continue 95 | result[] = result[] && astar_path == dijkstra_path 96 | result[] = result[] && total_path_weight(distance_g, index_to_node_id(distance_g, astar_path)) == total_path_weight(distance_g, index_to_node_id(distance_g, dijkstra_path)) 97 | result[] = result[] && astar_path[1] == origin 98 | result[] = result[] && astar_path[end] == destination 99 | !result[] && break 100 | end 101 | @test result[] 102 | 103 | # time weights 104 | time_g = LightOSM.graph_from_object(data; graph_type=:static, weight_type=:time) 105 | origin = rand(1:length(time_g.nodes)) 106 | destination = rand(1:length(time_g.nodes)) 107 | 108 | astar_path = LightOSM.astar(time_g.graph, time_g.weights, origin, destination; heuristic=LightOSM.time_heuristic(time_g)) 109 | dijkstra_path = LightOSM.dijkstra(time_g.graph, time_g.weights, origin, destination) 110 | @test astar_path == dijkstra_path 111 | 112 | if !isnothing(astar_path) && !isnothing(dijkstra_path) 113 | @test total_path_weight(time_g, index_to_node_id(time_g, astar_path)) == total_path_weight(time_g, index_to_node_id(time_g, dijkstra_path)) 114 | @test astar_path[1] == origin 115 | @test astar_path[end] == destination 116 | end 117 | 118 | result[] = true 119 | for destination in 1:length(time_g.nodes) 120 | # shortest path from vertex 4 to all others 121 | # vertex 4 is sensitive to heuristic choice (i.e. yields non-optiomal solution if a poor heuristic is chosen) 122 | local origin = 4 123 | local astar_path = LightOSM.astar(time_g.graph, time_g.weights, origin, destination; heuristic=LightOSM.time_heuristic(time_g)) 124 | local dijkstra_path = LightOSM.dijkstra(time_g.graph, time_g.weights, origin, destination) 125 | (isnothing(astar_path) || isnothing(dijkstra_path)) && continue 126 | result[] = result[] && astar_path == dijkstra_path 127 | result[] = result[] && total_path_weight(time_g, index_to_node_id(time_g, astar_path)) == total_path_weight(time_g, index_to_node_id(time_g, dijkstra_path)) 128 | result[] = result[] && astar_path[1] == origin 129 | result[] = result[] && astar_path[end] == destination 130 | !result[] && break 131 | end 132 | @test result[] -------------------------------------------------------------------------------- /src/nearest_node.jl: -------------------------------------------------------------------------------- 1 | """ 2 | nearest_node(g::OSMGraph, point::GeoLocation) 3 | nearest_node(g::OSMGraph, points::Vector{GeoLocation}) 4 | nearest_node(g::OSMGraph, point::AbstractVector{<:AbstractFloat}) 5 | nearest_node(g::OSMGraph, points::AbstractVector{<:AbstractVector{<:AbstractFloat}}) 6 | 7 | Finds the nearest node from a point (specified by a `GeoLocation` or set of Latitude 8 | Longitude coordinates) or `Vector` of points using a `NearestNeighbors.jl` KDTree. 9 | 10 | # Arguments 11 | - `g::OSMGraph`: Graph container. 12 | - `point`/`points`: Single point as a `GeoLocation` or `[lat, lon, alt]`, or a `Vector` of such points 13 | 14 | # Return 15 | - Tuple of neighbours and straight line euclidean distances from each point `([neighbours...], [dists...])`. 16 | Tuple elements are `Vector`s if a `Vector` of points is inputted, and numbers if a single point is inputted. 17 | """ 18 | nearest_node(g::OSMGraph, point::AbstractVector{<:AbstractFloat}) = nearest_node(g, GeoLocation(point)) 19 | nearest_node(g::OSMGraph, points::AbstractVector{<:AbstractVector{<:AbstractFloat}}) = nearest_node(g, GeoLocation(points)) 20 | function nearest_node(g::OSMGraph, point::GeoLocation, skip=(index)->false) 21 | cartesian_location = reshape([to_cartesian(point)...], (3,1)) 22 | idxs, dists = nn(g.kdtree, cartesian_location, skip) 23 | return g.index_to_node[idxs[1]], dists[1] 24 | end 25 | function nearest_node(g::OSMGraph, points::AbstractVector{GeoLocation}) 26 | cartesian_locations = to_cartesian(points) 27 | idxs, dists = nn(g.kdtree, cartesian_locations) 28 | return [g.index_to_node[index] for index in idxs], dists 29 | end 30 | 31 | """ 32 | nearest_node(g::OSMGraph, node::Node) 33 | nearest_node(g::OSMGraph, nodes::Vector{<:Node}) 34 | nearest_node(g::OSMGraph, node_ids::AbstractVector{<:Integer}) 35 | nearest_node(g::OSMGraph, node_id::Integer) 36 | 37 | Finds the nearest node from a node (specified by the `Node` object or node id) or 38 | `Vector` of nodes using a `NearestNeighbors.jl` KDTree. The origin node itself is 39 | not included in the results. 40 | 41 | # Arguments 42 | - `g::OSMGraph`: Graph container. 43 | - `node`/`nodes`/`node_id`/`node_ids`: Single node or `Vector` of nodes specified by `Node` objects or id. 44 | 45 | # Return 46 | - Tuple of neighbours and straight line euclidean distances from each node `([neighbours...], [dists...])`. 47 | Tuple elements are `Vector`sif a `Vector` of nodes is inputted, and numbers if a single point is inputted. 48 | """ 49 | nearest_node(g::OSMGraph, node::Node) = nearest_node(g, node.location, (index)->index==g.node_to_index[node.id]) 50 | nearest_node(g::OSMGraph, node_id::DEFAULT_OSM_ID_TYPE) = nearest_node(g, g.nodes[node_id]) 51 | nearest_node(g::OSMGraph, nodes::Vector{<:Node}) = nearest_node(g, [n.id for n in nodes]) 52 | function nearest_node(g::OSMGraph, node_ids::AbstractVector{<:DEFAULT_OSM_ID_TYPE}) 53 | locations = [g.nodes[n].location for n in node_ids] 54 | cartesian_locations = to_cartesian(locations) 55 | idxs, dists = knn(g.kdtree, cartesian_locations, 2, true) 56 | return [g.index_to_node[index[2]] for index in idxs], [d[2] for d in dists] 57 | end 58 | 59 | 60 | """ 61 | nearest_nodes(g::OSMGraph, point::GeoLocation, n_neighbours::Integer=1) 62 | nearest_nodes(g::OSMGraph, points::AbstractVector{GeoLocation}, n_neighbours::Integer=1) 63 | nearest_nodes(g::OSMGraph, point::AbstractVector{<:AbstractFloat}, n_neighbours::Integer=1) 64 | nearest_nodes(g::OSMGraph, points::AbstractVector{<:AbstractVector{<:AbstractFloat}}, n_neighbours::Integer=1) 65 | 66 | Finds nearest nodes from a point or `Vector` of points using a `NearestNeighbors.jl` KDTree. 67 | 68 | # Arguments 69 | - `g::OSMGraph`: Graph container. 70 | - `point`/`points`: Single point as a `GeoLocation` or `[lat, lon, alt]`, or a `Vector` of such points 71 | - `n_neighbours::Integer`: Number of neighbours to query for each point. 72 | 73 | # Return 74 | - Tuple of neighbours and straight line euclidean distances from each point `([[neighbours]...], [[dists]...])`. 75 | Tuple elements are `Vector{Vector}` if a `Vector` of points is inputted, and `Vector` if a single point is inputted. 76 | """ 77 | nearest_nodes(g::OSMGraph, point::Vector{<:AbstractFloat}, n_neighbours::Integer=1) = nearest_nodes(g, GeoLocation(point), n_neighbours) 78 | nearest_nodes(g::OSMGraph, points::Vector{<:Vector{<:AbstractFloat}}, n_neighbours::Integer=1) = nearest_nodes(g, GeoLocation(points), n_neighbours) 79 | function nearest_nodes(g::OSMGraph, point::GeoLocation, n_neighbours::Integer=1, skip=(index)->false) 80 | cartesian_location = reshape([to_cartesian(point)...], (3,1)) 81 | idxs, dists = knn(g.kdtree, cartesian_location, n_neighbours, true, skip) 82 | return [g.index_to_node[index] for index in idxs[1]], dists[1] 83 | end 84 | function nearest_nodes(g::OSMGraph, points::AbstractVector{GeoLocation}, n_neighbours::Integer=1) 85 | cartesian_locations = to_cartesian(points) 86 | idxs, dists = knn(g.kdtree, cartesian_locations, n_neighbours, true) 87 | neighbours = [[g.index_to_node[index] for index in _idxs] for _idxs in idxs] 88 | return neighbours, dists 89 | end 90 | 91 | """ 92 | nearest_nodes(g::OSMGraph, node_id::DEFAULT_OSM_ID_TYPE, n_neighbours::Integer=1) 93 | nearest_nodes(g::OSMGraph, node_ids::Vector{<:DEFAULT_OSM_ID_TYPE}, n_neighbours::Integer=1) 94 | nearest_nodes(g::OSMGraph, node::Node, n_neighbours::Integer=1) 95 | nearest_nodes(g::OSMGraph, nodes::AbstractVector{<:Node}, n_neighbours::Integer=1) 96 | 97 | Finds nearest nodes from a point or `Vector` of points using a `NearestNeighbors.jl` KDTree. 98 | 99 | # Arguments 100 | - `g::OSMGraph`: Graph container. 101 | - `node`/`nodes`/`node_id`/`node_ids`: Single node or `Vector` of nodes specified by `Node` objects or id. 102 | - `n_neighbours::Integer`: Number of neighbours to query for each point. 103 | 104 | # Return 105 | - Tuple of neighbours and straight line euclidean distances from each point `([[neighbours]...], [[dists]...])`. 106 | Tuple elements are `Vector{Vector}` if a `Vector` of points is inputted, and `Vector` if a single point is inputted. 107 | """ 108 | nearest_nodes(g::OSMGraph, node::Node, n_neighbours::Integer=1) = nearest_nodes(g, node.location, n_neighbours, (index)->index==g.node_to_index[node.id]) 109 | nearest_nodes(g::OSMGraph, node_id::DEFAULT_OSM_ID_TYPE, n_neighbours::Integer=1) = nearest_nodes(g, g.nodes[node_id], n_neighbours) 110 | nearest_nodes(g::OSMGraph, nodes::Vector{<:Node}, n_neighbours::Integer=1) = nearest_nodes(g, [n.id for n in nodes], n_neighbours) 111 | function nearest_nodes(g::OSMGraph, node_ids::Vector{<:DEFAULT_OSM_ID_TYPE}, n_neighbours::Integer=1) 112 | locations = [g.nodes[n].location for n in node_ids] 113 | n_neighbours += 1 # Closest node is always the input node itself, exclude self from result 114 | cartesian_locations = to_cartesian(locations) 115 | idxs, dists = knn(g.kdtree, cartesian_locations, n_neighbours, true) 116 | return [[g.index_to_node[index] for index in @view(_idxs[2:end])] for _idxs in idxs], [@view(d[2:end]) for d in dists] 117 | end -------------------------------------------------------------------------------- /test/stub.jl: -------------------------------------------------------------------------------- 1 | """ 2 | basic_osm_graph_stub(weight_type=:distance, graph_type=:static) 3 | 4 | The graph for the network below is returned, with all ways being two way except for 2004. 5 | The diagram is approximately geospatially correct. 6 | 7 | ```ascii 8 | 1001─┐ 9 | │ └─┐100km/h, way=2002, dual carriageway 10 | 50km/h, way=2001 │ └─┐ 11 | │ │ 12 | 1002 1006 13 | │ │ 14 | 50km/h, way=2001 │ │ 100km/h, way=2002, dual carriageway 15 | │ │ 16 | 1003 1007─────────1008 (1008 to 1007 is way 2004, 50km/h and one way) 17 | │ │ 18 | 50km/h, way=2001 │ ┌─┘ 19 | │ ┌─┘100km/h, way=2002, dual carriageway 20 | 1004─┘ 21 | │ 22 | 50km/h, way=2003 │ 23 | │ 24 | 1005 25 | ``` 26 | """ 27 | function basic_osm_graph_stub(weight_type=:distance, graph_type=:static) 28 | # Nodes 29 | lats = [-38.0751637, -38.0752637, -38.0753637, -38.0754637, -38.0755637, -38.0752637, -38.0753637, -38.0753637] 30 | lons = [145.3326838, 145.3326838, 145.3326838, 145.3326838, 145.3326838, 145.3327838, 145.3327838, 145.3328838] 31 | node_ids = [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008] 32 | nodes = Dict(id => Node(id, GeoLocation(lat, lon), Dict{String, Any}()) for (lat, lon, id) in zip(lats, lons, node_ids)) 33 | 34 | # Ways 35 | way_ids = [2001, 2002, 2003, 2004] 36 | way_nodes = [ 37 | [1001, 1002, 1003, 1004], 38 | [1001, 1006, 1007, 1004], 39 | [1004, 1005], 40 | [1008, 1007], 41 | ] 42 | tag_dicts = [ 43 | Dict{String, Any}("oneway" => false, "reverseway" => false, "maxspeed" => Int16(50), "lanes" => Int8(2)), 44 | Dict{String, Any}("oneway" => false, "reverseway" => false, "maxspeed" => Int16(100), "lanes" => Int8(4)), 45 | Dict{String, Any}("oneway" => false, "reverseway" => false, "maxspeed" => Int16(50), "lanes" => Int8(2)), 46 | Dict{String, Any}("oneway" => true, "reverseway" => false, "maxspeed" => Int16(50), "lanes" => Int8(1)), 47 | ] 48 | ways = Dict(way_id => Way(way_id, nodes, tag_dict) for (way_id, nodes, tag_dict) in zip(way_ids, way_nodes, tag_dicts)) 49 | 50 | restriction1 = Restriction( 51 | 3001, 52 | "via_node", 53 | Dict{String, Any}("restriction"=>"no_right_turn","type"=>"restriction"), 54 | 2002, 55 | 2001, 56 | 1004, 57 | nothing, 58 | true, 59 | false 60 | ) 61 | restrictions = Dict(restriction1.id => restriction1) 62 | U = LightOSM.DEFAULT_OSM_INDEX_TYPE 63 | T = Int 64 | W = LightOSM.DEFAULT_OSM_EDGE_WEIGHT_TYPE 65 | g = OSMGraph{U,T,W}(nodes=nodes, ways=ways, restrictions=restrictions) 66 | LightOSM.add_node_and_edge_mappings!(g) 67 | LightOSM.add_weights!(g, weight_type) 68 | LightOSM.add_graph!(g, graph_type) 69 | LightOSM.add_node_tags!(g) 70 | LightOSM.add_indexed_restrictions!(g) 71 | g.dijkstra_states = Vector{Vector{U}}(undef, length(g.nodes)) 72 | LightOSM.add_kdtree_and_rtree!(g) 73 | return g 74 | end 75 | 76 | function basic_osm_graph_stub_string(weight_type=:distance, graph_type=:static) 77 | # Nodes 78 | lats = [-38.0751637, -38.0752637, -38.0753637, -38.0754637, -38.0755637, -38.0752637, -38.0753637, -38.0753637] 79 | lons = [145.3326838, 145.3326838, 145.3326838, 145.3326838, 145.3326838, 145.3327838, 145.3327838, 145.3328838] 80 | node_ids = ["1001", "1002", "1003", "1004", "1005", "1006", "1007", "1008"] 81 | nodes = Dict(id => Node(id, GeoLocation(lat, lon), Dict{String, Any}()) for (lat, lon, id) in zip(lats, lons, node_ids)) 82 | 83 | # Ways 84 | way_ids = ["2001", "2002", "2003", "2004"] 85 | way_nodes = [ 86 | ["1001", "1002", "1003", "1004"], 87 | ["1001", "1006", "1007", "1004"], 88 | ["1004", "1005"], 89 | ["1008", "1007"], 90 | ] 91 | tag_dicts = [ 92 | Dict{String, Any}("oneway" => false, "reverseway" => false, "maxspeed" => Int16(50), "lanes" => Int8(2)), 93 | Dict{String, Any}("oneway" => false, "reverseway" => false, "maxspeed" => Int16(100), "lanes" => Int8(4)), 94 | Dict{String, Any}("oneway" => false, "reverseway" => false, "maxspeed" => Int16(50), "lanes" => Int8(2)), 95 | Dict{String, Any}("oneway" => true, "reverseway" => false, "maxspeed" => Int16(50), "lanes" => Int8(1)), 96 | ] 97 | ways = Dict(way_id => Way(way_id, nodes, tag_dict) for (way_id, nodes, tag_dict) in zip(way_ids, way_nodes, tag_dicts)) 98 | 99 | restriction1 = Restriction( 100 | "3001", 101 | "via_node", 102 | Dict{String, Any}("restriction"=>"no_right_turn","type"=>"restriction"), 103 | "2002", 104 | "2001", 105 | "1004", 106 | nothing, 107 | true, 108 | false 109 | ) 110 | restrictions = Dict(restriction1.id => restriction1) 111 | U = LightOSM.DEFAULT_OSM_INDEX_TYPE 112 | T = String 113 | W = LightOSM.DEFAULT_OSM_EDGE_WEIGHT_TYPE 114 | g = OSMGraph{U,T,W}(nodes=nodes, ways=ways, restrictions=restrictions) 115 | LightOSM.add_node_and_edge_mappings!(g) 116 | LightOSM.add_weights!(g, weight_type) 117 | LightOSM.add_graph!(g, graph_type) 118 | LightOSM.add_node_tags!(g) 119 | LightOSM.add_indexed_restrictions!(g) 120 | g.dijkstra_states = Vector{Vector{U}}(undef, length(g.nodes)) 121 | LightOSM.add_kdtree_and_rtree!(g) 122 | return g 123 | end 124 | 125 | """ 126 | stub_graph1() 127 | 128 | Returns a directed graph object (DiGraph). Nodes represented by circles, edge distances (weights) represented by numbers along the lines. 129 | 130 | Shortest path from ① to ⑤ = ① → ④ → ⑤ 131 | Shotest distance from ① to ⑤ = 3 132 | 133 | ```ascii 134 | 135 | ①─┐2─→②──4─┐ 136 | │ └┐ ↑ ↓ 137 | 1 1─┐1 ⑤ 138 | ↓ ↓ ↑ 139 | ③──2─→④──2─┘ 140 | 141 | ⋅ 2 1 1 ⋅ 142 | ⋅ ⋅ ⋅ 1 4 143 | ⋅ ⋅ ⋅ 2 ⋅ 144 | ⋅ 1 ⋅ ⋅ 2 145 | ⋅ ⋅ ⋅ ⋅ ⋅ 146 | ``` 147 | """ 148 | function stub_graph1() 149 | weights = sparse( 150 | [1, 1, 1, 2, 4, 3, 2, 4], # origin 151 | [2, 3, 4, 4, 2, 4, 5, 5], # destination 152 | [2, 1, 1, 1, 1, 2, 4, 2], # weights 153 | 5, # n nodes 154 | 5 # n nodes 155 | ) 156 | return DiGraph(weights), weights 157 | end 158 | 159 | """ 160 | stub_graph2() 161 | 162 | Returns a directed graph object (DiGraph). 163 | 164 | Shortest path from ① to ⑥ = ① → ④ -> ⑦ -> ⑥ 165 | Shotest distance from ① to ⑥ = 6 166 | 167 | ```ascii 168 | 169 | ⋅ 2 ⋅ 1 ⋅ ⋅ ⋅ 170 | ⋅ ⋅ ⋅ 3 10 ⋅ ⋅ 171 | 4 ⋅ ⋅ ⋅ ⋅ 5 ⋅ 172 | ⋅ ⋅ 2 ⋅ 2 8 4 173 | ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ 6 174 | ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ 175 | ⋅ ⋅ ⋅ ⋅ ⋅ 1 ⋅ 176 | ``` 177 | """ 178 | function stub_graph2() 179 | weights = sparse( 180 | [1, 1, 2, 2, 3, 3, 4, 4, 4, 4, 5, 7], # origin 181 | [2, 4, 4, 5, 1, 6, 3, 5, 6, 7, 7, 6], # destination 182 | [2, 1, 3, 10, 4, 5, 2, 2, 8, 4, 6, 1], # weights 183 | 7, # n nodes 184 | 7 # n nodes 185 | ) 186 | return DiGraph(weights), weights 187 | end 188 | 189 | """ 190 | stub_graph3() 191 | 192 | Returns a directed graph object (DiGraph). 193 | 194 | Shortest path from ① to ③ = ① → ④ -> ② -> ③ 195 | Shotest distance from ① to ③ = 9 196 | 197 | ```ascii 198 | 199 | ⋅ 10 ⋅ 5 ⋅ 200 | ⋅ ⋅ 1 2 ⋅ 201 | ⋅ ⋅ ⋅ ⋅ 4 202 | ⋅ 3 9 ⋅ 2 203 | 7 ⋅ 6 ⋅ ⋅ 204 | ``` 205 | """ 206 | function stub_graph3() 207 | weights = sparse( 208 | [1, 1, 2, 2, 3, 4, 4, 4, 5, 5], # origin 209 | [2, 4, 3, 4, 5, 2, 3, 5, 1, 3], # destination 210 | [10, 5, 1, 2, 4, 3, 9, 2, 7, 6], # weights 211 | 5, # n nodes 212 | 5 # n nodes 213 | ) 214 | return DiGraph(weights), weights 215 | end -------------------------------------------------------------------------------- /benchmark/benchmarks.jl: -------------------------------------------------------------------------------- 1 | using Random 2 | using BenchmarkTools 3 | using LightOSM 4 | using OpenStreetMapX 5 | using Graphs 6 | using DataStructures 7 | using JSON 8 | 9 | """ 10 | Setup 11 | """ 12 | 13 | Random.seed!(1234) # Set seed so experiment is reproducible 14 | 15 | point = GeoLocation(-37.8142176, 144.9631608) # Melbourne, Australia 16 | radius = 5 # km 17 | data_file = joinpath(@__DIR__, "benchmark_map.osm") 18 | 19 | g_losm = LightOSM.graph_from_download(:point, 20 | point=point, 21 | radius=radius, 22 | weight_type=:distance, 23 | save_to_file_location=data_file) 24 | 25 | # g_losm = LightOSM.graph_from_file(data_file, weight_type=:distance) 26 | @time g_losm_precompute = LightOSM.graph_from_file(data_file, weight_type=:distance, precompute_dijkstra_states=true) 27 | 28 | g_osmx = OpenStreetMapX.get_map_data(data_file, 29 | use_cache=false, 30 | only_intersections=false, 31 | trim_to_connected_graph=true) 32 | 33 | 34 | """ 35 | Define benchmark functions 36 | """ 37 | 38 | function losm_shortest_path(g::LightOSM.OSMGraph, o_d_nodes, algorithm) 39 | for (o, d) in o_d_nodes 40 | try 41 | LightOSM.shortest_path(g, o, d, algorithm=algorithm) 42 | catch 43 | # Error exception will be thrown if path does not exist from origin to destination node 44 | end 45 | end 46 | end 47 | 48 | function osmx_shortest_path(g::OpenStreetMapX.MapData, o_d_nodes, algorithm) 49 | for (o, d) in o_d_nodes 50 | try 51 | OpenStreetMapX.find_route(g, o, d, g.w, routing=algorithm, heuristic=(u, v) -> OpenStreetMapX.get_distance(u, v, g_osmx.nodes, g_osmx.n), get_distance=false, get_time=false) 52 | catch 53 | # Error exception will be thrown if path does not exist from origin to destination node 54 | end 55 | end 56 | end 57 | 58 | function lg_shortest_path(g::LightOSM.OSMGraph, o_d_indices, algorithm) 59 | if algorithm == :astar 60 | for (o, d) in o_d_indices 61 | try 62 | Graphs.a_star(g.graph, o, d, g.weights) 63 | catch 64 | # Error exception will be thrown if path does not exist from origin to destination node 65 | end 66 | end 67 | elseif algorithm == :dijkstra 68 | for (o, d) in o_d_indices 69 | try 70 | state = Graphs.dijkstra_shortest_paths(g.graph, o, g.weights) 71 | Graphs.enumerate_paths(state, d) 72 | catch 73 | # Error exception will be thrown if path does not exist from origin to destination node 74 | end 75 | end 76 | end 77 | end 78 | 79 | function extract(bmark_trial) 80 | io = IOBuffer() 81 | show(io, "text/plain", bmark_trial) 82 | 83 | s = String(take!(io)) 84 | s = split.((split(s, "\n")), ":") 85 | 86 | result = Dict{String,String}() 87 | 88 | for item in s 89 | if length(item) >= 2 90 | result[strip(item[1])] = strip(item[2]) 91 | end 92 | end 93 | @info "Extracted benchmark result: $result" 94 | return result 95 | end 96 | 97 | """ 98 | Define experiment 99 | """ 100 | 101 | n_paths = [1, 10, 100, 1000, 10000] 102 | o_d_indices = OrderedDict() 103 | o_d_nodes = OrderedDict() 104 | 105 | losm_nodes = collect(keys(g_losm.nodes)) 106 | osmx_nodes = collect(keys(g_osmx.nodes)) 107 | common_nodes = intersect(losm_nodes, osmx_nodes) 108 | 109 | for n in n_paths 110 | rand_o_d_indices = rand(1:length(common_nodes), n, 2) 111 | o_d_indices[n] = [[o, d] for (o, d) in eachrow(rand_o_d_indices) if o != d] 112 | o_d_nodes[n] = [[common_nodes[o], common_nodes[d]] for (o, d) in eachrow(rand_o_d_indices) if o != d] 113 | end 114 | 115 | """ 116 | Run experiment 117 | """ 118 | results = Dict(:dijkstra => DefaultDict(Dict), :astar => DefaultDict(Dict)) 119 | 120 | results[:dijkstra][1][:losm] = extract(@benchmark losm_shortest_path(g_losm, o_d_nodes[1], :dijkstra)) 121 | results[:dijkstra][1][:losm_precompute] = extract(@benchmark losm_shortest_path(g_losm_precompute, o_d_nodes[1], :dijkstra)) 122 | results[:dijkstra][1][:osmx] = extract(@benchmark osmx_shortest_path(g_osmx, o_d_nodes[1], :dijkstra)) 123 | results[:dijkstra][1][:lg] = extract(@benchmark lg_shortest_path(g_losm, o_d_indices[1], :dijkstra)) 124 | 125 | results[:dijkstra][10][:losm] = extract(@benchmark losm_shortest_path(g_losm, o_d_nodes[10], :dijkstra)) 126 | results[:dijkstra][10][:losm_precompute] = extract(@benchmark losm_shortest_path(g_losm_precompute, o_d_nodes[10], :dijkstra)) 127 | results[:dijkstra][10][:osmx] = extract(@benchmark osmx_shortest_path(g_osmx, o_d_nodes[10], :dijkstra)) 128 | results[:dijkstra][10][:lg] = extract(@benchmark lg_shortest_path(g_losm, o_d_indices[10], :dijkstra)) 129 | 130 | results[:dijkstra][100][:losm] = extract(@benchmark losm_shortest_path(g_losm, o_d_nodes[100], :dijkstra)) 131 | results[:dijkstra][100][:losm_precompute] = extract(@benchmark losm_shortest_path(g_losm_precompute, o_d_nodes[100], :dijkstra)) 132 | results[:dijkstra][100][:osmx] = extract(@benchmark osmx_shortest_path(g_osmx, o_d_nodes[100], :dijkstra)) 133 | results[:dijkstra][100][:lg] = extract(@benchmark lg_shortest_path(g_losm, o_d_indices[100], :dijkstra)) 134 | 135 | results[:dijkstra][1000][:losm] = extract(@benchmark losm_shortest_path(g_losm, o_d_nodes[1000], :dijkstra)) 136 | results[:dijkstra][1000][:losm_precompute] = extract(@benchmark losm_shortest_path(g_losm_precompute, o_d_nodes[1000], :dijkstra)) 137 | results[:dijkstra][1000][:osmx] = extract(@benchmark osmx_shortest_path(g_osmx, o_d_nodes[1000], :dijkstra)) 138 | results[:dijkstra][1000][:lg] = extract(@benchmark lg_shortest_path(g_losm, o_d_indices[1000], :dijkstra)) 139 | 140 | results[:dijkstra][10000][:losm] = extract(@benchmark losm_shortest_path(g_losm, o_d_nodes[10000], :dijkstra)) 141 | results[:dijkstra][10000][:losm_precompute] = extract(@benchmark losm_shortest_path(g_losm_precompute, o_d_nodes[10000], :dijkstra)) 142 | results[:dijkstra][10000][:osmx] = extract(@benchmark osmx_shortest_path(g_osmx, o_d_nodes[10000], :dijkstra)) 143 | results[:dijkstra][10000][:lg] = extract(@benchmark lg_shortest_path(g_losm, o_d_indices[10000], :dijkstra)) 144 | 145 | results[:astar][1][:losm] = extract(@benchmark losm_shortest_path(g_losm, o_d_nodes[1], :astar)) 146 | results[:astar][1][:losm_precompute] = extract(@benchmark losm_shortest_path(g_losm_precompute, o_d_nodes[1], :astar)) 147 | results[:astar][1][:osmx] = extract(@benchmark osmx_shortest_path(g_osmx, o_d_nodes[1], :astar)) 148 | results[:astar][1][:lg] = extract(@benchmark lg_shortest_path(g_losm, o_d_indices[1], :astar)) 149 | 150 | results[:astar][10][:losm] = extract(@benchmark losm_shortest_path(g_losm, o_d_nodes[10], :astar)) 151 | results[:astar][10][:losm_precompute] = extract(@benchmark losm_shortest_path(g_losm_precompute, o_d_nodes[10], :astar)) 152 | results[:astar][10][:osmx] = extract(@benchmark osmx_shortest_path(g_osmx, o_d_nodes[10], :astar)) 153 | results[:astar][10][:lg] = extract(@benchmark lg_shortest_path(g_losm, o_d_indices[10], :astar)) 154 | 155 | results[:astar][100][:losm] = extract(@benchmark losm_shortest_path(g_losm, o_d_nodes[100], :astar)) 156 | results[:astar][100][:losm_precompute] = extract(@benchmark losm_shortest_path(g_losm_precompute, o_d_nodes[100], :astar)) 157 | results[:astar][100][:osmx] = extract(@benchmark osmx_shortest_path(g_osmx, o_d_nodes[100], :astar)) 158 | results[:astar][100][:lg] = extract(@benchmark lg_shortest_path(g_losm, o_d_indices[100], :astar)) 159 | 160 | results[:astar][1000][:losm] = extract(@benchmark losm_shortest_path(g_losm, o_d_nodes[1000], :astar)) 161 | results[:astar][1000][:losm_precompute] = extract(@benchmark losm_shortest_path(g_losm_precompute, o_d_nodes[1000], :astar)) 162 | results[:astar][1000][:osmx] = extract(@benchmark osmx_shortest_path(g_osmx, o_d_nodes[1000], :astar)) 163 | results[:astar][1000][:lg] = extract(@benchmark lg_shortest_path(g_losm, o_d_indices[1000], :astar)) 164 | 165 | results[:astar][10000][:losm] = extract(@benchmark losm_shortest_path(g_losm, o_d_nodes[10000], :astar)) 166 | results[:astar][10000][:losm_precompute] = extract(@benchmark losm_shortest_path(g_losm_precompute, o_d_nodes[10000], :astar)) 167 | results[:astar][10000][:osmx] = extract(@benchmark osmx_shortest_path(g_osmx, o_d_nodes[10000], :astar)) 168 | results[:astar][10000][:lg] = extract(@benchmark lg_shortest_path(g_losm, o_d_indices[10000], :astar)) 169 | 170 | """ 171 | Export Results 172 | """ 173 | 174 | open(joinpath(@__DIR__, "benchmark_results.json"), "w") do io 175 | write(io, json(results)) 176 | end -------------------------------------------------------------------------------- /src/types.jl: -------------------------------------------------------------------------------- 1 | """ 2 | Representation of a geospatial coordinates. 3 | 4 | # Fields 5 | - `lat::Float64`: Latitude. 6 | - `lon::Float64`: Longitude. 7 | - `alt::Float64`: Altitude. 8 | 9 | # Constructors 10 | GeoLocation(lat, lon) 11 | GeoLocation(point::Vector{<:Real}) 12 | GeoLocation(point_vector::Vector{<:Vector{<:Real}}) 13 | GeoLocation(g::OSMGraph, ep::EdgePoint) 14 | """ 15 | @with_kw_noshow struct GeoLocation 16 | lat::Float64 17 | lon::Float64 18 | alt::Float64 = 0.0 19 | end 20 | GeoLocation(lat::AbstractFloat, lon::AbstractFloat)::GeoLocation = GeoLocation(lat=lat, lon=lon) 21 | GeoLocation(lat::Real, lon::Real)::GeoLocation = GeoLocation(lat=float(lat), lon=float(lon)) 22 | GeoLocation(point::Vector{<:AbstractFloat})::GeoLocation = GeoLocation(point...) 23 | GeoLocation(point::Vector{<:Real})::GeoLocation = GeoLocation([float(coord) for coord in point]...) 24 | GeoLocation(point_vector::Vector{<:Vector{<:Real}})::Vector{GeoLocation} = [GeoLocation(p...) for p in point_vector] 25 | 26 | function Base.:(==)(loc1::GeoLocation, loc2::GeoLocation) 27 | return loc1.lat == loc2.lat && loc1.lon == loc2.lon && loc1.alt == loc2.alt 28 | end 29 | function Base.isapprox(loc1::GeoLocation, loc2::GeoLocation) 30 | return loc1.lat ≈ loc2.lat && loc1.lon ≈ loc2.lon && loc1.alt ≈ loc2.alt 31 | end 32 | function Base.hash(loc::GeoLocation, h::UInt) 33 | for field in fieldnames(GeoLocation) 34 | h = hash(getproperty(loc, field), h) 35 | end 36 | return h 37 | end 38 | 39 | """ 40 | OpenStreetMap node. 41 | 42 | # Fields 43 | `T<:String` 44 | - `id::T`: OpenStreetMap node id. 45 | - `nodes::Vector{T}`: Node's GeoLocation. 46 | - `tags::AbstractDict{String,Any}`: Metadata tags. 47 | """ 48 | struct Node{T <: Union{Integer, String}} 49 | id::T 50 | location::GeoLocation 51 | tags::Union{Dict{String,Any},Nothing} 52 | end 53 | 54 | """ 55 | OpenStreetMap way. 56 | 57 | # Fields 58 | `T<:Integer` 59 | - `id::T`: OpenStreetMap way id. 60 | - `nodes::Vector{T}`: Ordered list of node ids making up the way. 61 | - `tags::AbstractDict{String,Any}`: Metadata tags. 62 | """ 63 | struct Way{T <: Union{Integer, String}} 64 | id::T 65 | nodes::Vector{T} 66 | tags::Dict{String,Any} 67 | end 68 | Way(id::T, nodes, tags::Dict{String, Any}) where T <: Union{Integer, String} = Way(id, convert(Vector{T}, nodes), tags) 69 | 70 | 71 | """ 72 | EdgePoint{T<:Integer} 73 | 74 | A point along the edge between two OSM nodes. 75 | 76 | # Fields 77 | - `n1::T`: First node of edge. 78 | - `n2::T`: Second node of edge. 79 | - `pos::Float64`: Position from `n1` to `n2`, from 0 to 1. 80 | """ 81 | struct EdgePoint{T<:Union{Integer, String}} 82 | n1::T 83 | n2::T 84 | pos::Float64 85 | end 86 | 87 | """ 88 | OpenStreetMap turn restriction (relation). 89 | 90 | # Fields 91 | `T<:String` 92 | - `id::T`: OpenStreetMap relation id. 93 | - `type::String`: Either a `via_way` or `via_node` turn restriction. 94 | - `tags::AbstractDict{String,Any}`: Metadata tags. 95 | - `from_way::T`: Incoming way id to the turn restriction. 96 | - `to_way::T`: Outgoing way id to the turn restriction. 97 | - `via_node::Union{T,Nothing}`: Node id at the centre of the turn restriction. 98 | - `via_way::Union{Vector{T},Nothing}`: Way id at the centre of the turn restriction. 99 | - `is_exclusion::Bool`: Turn restrictions such as `no_left_turn`, `no_right_turn` or `no_u_turn`. 100 | - `is_exclusive::Bool`: Turn restrictions such as `striaght_on_only`, `left_turn_only`, `right_turn_only`. 101 | """ 102 | @with_kw struct Restriction{T <: Union{Integer, String}} 103 | id::T 104 | type::String 105 | tags::Dict{String,Any} 106 | from_way::T 107 | to_way::T 108 | via_node::Union{T,Nothing} = nothing 109 | via_way::Union{Vector{T},Nothing} = nothing 110 | is_exclusion::Bool = false 111 | is_exclusive::Bool = false 112 | end 113 | 114 | """ 115 | Container for storing OpenStreetMap node, way, relation and graph related obejcts. 116 | 117 | # Fields 118 | `U <: Integer,T <: Union{String, Int},W <: Real` 119 | - `nodes::Dict{T,Node{T}}`: Mapping of node ids to node objects. 120 | - `node_coordinates::Vector{Vector{W}}`: Vector of node coordinates [[lat, lon]...], indexed by graph vertices. 121 | - `ways::Dict{T,Way{T}}`: Mapping of way ids to way objects. Previously called `highways`. 122 | - `node_to_index::OrderedDict{T,U}`: Mapping of node ids to graph vertices. 123 | - `index_to_node::OrderedDict{U,T}`: Mapping of graph vertices to node ids. 124 | - `node_to_way::Dict{T,Vector{T}}`: Mapping of node ids to vector of way ids. Previously called `node_to_highway`. 125 | - `edge_to_way::Dict{Vector{T},T}`: Mapping of edges (adjacent node pairs) to way ids. Previously called `edge_to_highway`. 126 | - `restrictions::Dict{T,Restriction{T}}`: Mapping of relation ids to restriction objects. 127 | - `indexed_restrictions::Union{DefaultDict{U,Vector{MutableLinkedList{U}}},Nothing}`: Mapping of via node ids to ordered sequences of restricted node ids. 128 | - `graph::Union{AbstractGraph,Nothing}`: Either DiGraph, StaticDiGraph, SimpleWeightedDiGraph or MetaDiGraph. 129 | - `weights::Union{SparseMatrixCSC{W,U},Nothing}`: Sparse adjacency matrix (weights between graph vertices), either `:distance` (km), `:time` (hours) or `:lane_efficiency` (time scaled by number of lanes). 130 | - `dijkstra_states::Vector{Vector{U}}`: Vector of dijkstra parent states indexed by source vertices, used to retrieve shortest path from source vertex to any other vertex. 131 | - `kdtree::Union{KDTree,Nothing}`: KDTree used to calculate nearest nodes. 132 | - `kdtree::Union{RTree,Nothing}`: R-tree used to calculate nearest nodes. 133 | - `weight_type::Union{Symbol,Nothing}`: Either `:distance`, `:time` or `:lane_efficiency`. 134 | """ 135 | @with_kw mutable struct OSMGraph{U <: Integer,T <: Union{Integer, String},W <: Real} 136 | nodes::Dict{T,Node{T}} = Dict{T,Node{T}}() 137 | node_coordinates::Vector{Vector{W}} = Vector{Vector{W}}() # needed for astar heuristic 138 | ways::Dict{T,Way{T}} = Dict{T,Way{T}}() 139 | node_to_index::OrderedDict{T,U} = OrderedDict{T,U}() 140 | index_to_node::OrderedDict{U,T} = OrderedDict{U,T}() 141 | node_to_way::Dict{T,Vector{T}} = Dict{T,Vector{T}}() 142 | edge_to_way::Dict{Vector{T},T} = Dict{Vector{T},T}() 143 | restrictions::Dict{T,Restriction{T}} = Dict{T,Restriction{T}}() 144 | indexed_restrictions::Union{DefaultDict{U,Vector{MutableLinkedList{U}}},Nothing} = nothing 145 | graph::Union{AbstractGraph,Nothing} = nothing 146 | weights::Union{SparseMatrixCSC{W,U},Nothing} = nothing #errors 147 | dijkstra_states::Union{Vector{Vector{U}},Nothing} = nothing 148 | kdtree::Union{KDTree{StaticArrays.SVector{3, W},Euclidean,W},Nothing} = nothing 149 | rtree::Union{RTree{Float64,3,SpatialElem{Float64,3,T,Nothing}},Nothing} = nothing 150 | weight_type::Union{Symbol,Nothing} = nothing 151 | end 152 | 153 | function Base.getproperty(g::OSMGraph, field::Symbol) 154 | # Ensure renaming of "highways" to "ways" is backwards compatible 155 | if field === :highways 156 | Base.depwarn("`highways` field is deprecated, use `ways` field instead", :getproperty) 157 | return getfield(g, :ways) 158 | elseif field === :node_to_highway 159 | Base.depwarn("`node_to_highway` field is deprecated, use `node_to_way` field instead", :getproperty) 160 | return getfield(g, :node_to_way) 161 | elseif field === :edge_to_highway 162 | Base.depwarn("`edge_to_highway` field is deprecated, use `edge_to_way` field instead", :getproperty) 163 | return getfield(g, :edge_to_way) 164 | else 165 | return getfield(g, field) 166 | end 167 | end 168 | 169 | """ 170 | OpenStreetMap building polygon. 171 | 172 | # Fields 173 | `T<:Integer` 174 | - `id::T`: OpenStreetMap building way id. 175 | - `nodes::Vector{T}`: Ordered list of node ids making up the building polyogn. 176 | - `is_outer::Bool`: True if polygon is the outer ring of a multi-polygon. 177 | """ 178 | struct Polygon{T <: Union{Integer, String}} 179 | id::T 180 | nodes::Vector{Node{T}} 181 | is_outer::Bool # or inner 182 | end 183 | 184 | """ 185 | OpenStreetMap building. 186 | 187 | # Fields 188 | `T<:String` 189 | - `id::T`: OpenStreetMap building way id a simple polygon, relation id if a multi-polygon 190 | - `is_relation::Bool`: True if building is a a multi-polygon / relation. 191 | - `polygons::Vector{Polygon{T}}`: List of building polygons, first is always the outer ring. 192 | - `tags::AbstractDict{String,Any}`: Metadata tags. 193 | """ 194 | struct Building{T <: Union{Integer, String}} 195 | id::T 196 | is_relation::Bool # or way 197 | polygons::Vector{Polygon{T}} 198 | tags::AbstractDict{String,Any} 199 | end 200 | 201 | """ 202 | PathAlgorithm. 203 | 204 | Abstract type for path finding algorithms: 205 | - `Dijkstra` 206 | - `AStar` 207 | """ 208 | abstract type PathAlgorithm end 209 | abstract type Dijkstra <: PathAlgorithm end 210 | abstract type DijkstraVector <: Dijkstra end 211 | abstract type DijkstraDict <: Dijkstra end 212 | abstract type AStar <: PathAlgorithm end 213 | abstract type AStarVector <: AStar end 214 | abstract type AStarDict <: AStar end 215 | 216 | """ 217 | Additional GeoLocation methods that require types defined above. 218 | """ 219 | GeoLocation(g::OSMGraph, ep::EdgePoint)::GeoLocation = GeoLocation( 220 | lon = g.nodes[ep.n1].location.lon + (g.nodes[ep.n2].location.lon - g.nodes[ep.n1].location.lon) * ep.pos, 221 | lat = g.nodes[ep.n1].location.lat + (g.nodes[ep.n2].location.lat - g.nodes[ep.n1].location.lat) * ep.pos 222 | ) 223 | -------------------------------------------------------------------------------- /src/utilities.jl: -------------------------------------------------------------------------------- 1 | """ 2 | string_deserializer(format::Symbol)::Function 3 | 4 | Retrieves string deserializer for downloaded OpenStreetMap data. 5 | 6 | # Arguments 7 | - `format::Symbol`: Format of OpenStreetMap darta `:xml`, `:osm` or `:json`. 8 | 9 | # Return 10 | - `Function`: Either LightXML or JSON parser. 11 | """ 12 | function string_deserializer(format::Symbol)::Function 13 | if format == :xml || format == :osm 14 | return LightXML.parse_string 15 | elseif format == :json 16 | return JSON.parse 17 | else 18 | throw(ArgumentError("String deserializer for $format format does not exist")) 19 | end 20 | end 21 | 22 | """ 23 | has_extension(filename) 24 | 25 | Returns `true` if `filename` has an extension. 26 | """ 27 | has_extension(filename) = length(split(filename, '.')) > 1 28 | 29 | """ 30 | check_valid_filename(filename) 31 | 32 | Check that `filename` ends in ".json", ".osm" or ".xml", 33 | """ 34 | function check_valid_filename(filename) 35 | split_name = split(filename, '.') 36 | if !has_extension(filename) || !in(split_name[end], ("json", "osm", "xml")) 37 | err_msg = "File $filename does not have an extension of .json, .osm or .xml" 38 | throw(ArgumentError(err_msg)) 39 | end 40 | return true 41 | end 42 | 43 | """ 44 | get_extension(filename) 45 | 46 | Return extension of `filename`. 47 | """ 48 | get_extension(filename) = split(filename, '.')[end] 49 | 50 | """ 51 | file_deserializer(file_path)::Function 52 | 53 | Retrieves file deserializer for downloaded OpenStreetMap data. 54 | 55 | # Arguments 56 | - `file_path`: File path of OSM data. 57 | 58 | # Return 59 | - `Function`: Either LightXML or JSON parser. 60 | """ 61 | function file_deserializer(file_path)::Function 62 | check_valid_filename(file_path) 63 | format = Symbol(get_extension(file_path)) 64 | if format == :xml || format == :osm 65 | return LightXML.parse_file 66 | elseif format == :json 67 | return JSON.parsefile 68 | else 69 | throw(ArgumentError("File deserializer for $format format does not exist")) 70 | end 71 | end 72 | 73 | """ 74 | validate_save_location(save_to_file_location, download_format) 75 | 76 | Check the extension of `save_to_file_location` and do the following: 77 | 78 | - If it is a valid download format but not the download format used, 79 | then error. 80 | - If the extension matches `download_format`, return `save_to_file_location` 81 | - If there is a different extension, append correct extension and return. This 82 | allows users to have periods in their file names if they wanted to 83 | - If no extension, add the correct extension and return 84 | """ 85 | function validate_save_location(save_to_file_location, download_format) 86 | valid_formats = ("osm", "xml", "json") 87 | if has_extension(save_to_file_location) 88 | extension = get_extension(save_to_file_location) 89 | if extension == string(download_format) 90 | # File extension matches download format, all good 91 | return save_to_file_location 92 | elseif extension in valid_formats 93 | # File extension is a different download format, error 94 | throw(ArgumentError("Extension of save location $save_to_file_location does not match download format $download_format")) 95 | end 96 | end 97 | return save_to_file_location *= "." * string(download_format) 98 | end 99 | 100 | """ 101 | tryparse_string_to_number(str::AbstractString)::Union{Number,AbstractString} 102 | 103 | Attempts to parse a stringified number as an Integer or Float. 104 | """ 105 | function tryparse_string_to_number(str::AbstractString)::Union{Number,AbstractString} 106 | result = tryparse(Int, str) 107 | if !(result isa Nothing) 108 | return result 109 | end 110 | 111 | result = tryparse(Float64, str) 112 | if !(result isa Nothing) 113 | return result 114 | end 115 | 116 | return str 117 | end 118 | 119 | """ 120 | remove_non_numeric(str::AbstractString)::Number 121 | 122 | Removes any non numeric characters from a string, then converts it to a number. 123 | """ 124 | function remove_non_numeric(str::AbstractString)::Number 125 | numeric_only = replace(str, r"[^\d\.]" => "") 126 | return isempty(numeric_only) ? 0 : tryparse_string_to_number(numeric_only) 127 | end 128 | 129 | """ 130 | remove_sub_string_after(str::AbstractString, after::AbstractString)::AbstractString 131 | 132 | Removes all characters in a string that occurs `after` some input match pattern. 133 | """ 134 | function remove_sub_string_after(str::AbstractString, after::AbstractString)::AbstractString 135 | regex = Regex("$after.*\$") 136 | return replace(str, regex => "") 137 | end 138 | 139 | """ 140 | remove_numeric(str::AbstractString)::AbstractString 141 | 142 | Removes numeric characters from a string. 143 | """ 144 | function remove_numeric(str::AbstractString)::AbstractString 145 | return replace(str, r"[\d\.]" => "") 146 | end 147 | 148 | """ 149 | xml_to_dict(root_node::Union{XMLNode,XMLElement}, attributes_to_exclude::Set=Set())::AbstractDict 150 | 151 | Parses a LightXML object to a dictionary. 152 | 153 | # Arguments 154 | - `root_node::Union{XMLNode,XMLElement}`: LightXML object to parse. 155 | - `attributes_to_exclude::Set=Set()`: Set of tags to ignore when parsing. 156 | 157 | # Return 158 | - `AbstractDict`: XML parsed as a dictionary. 159 | """ 160 | function xml_to_dict(root_node::Union{XMLNode,XMLElement}, attributes_to_exclude::Set=Set())::AbstractDict 161 | result = Dict{String, Any}() 162 | 163 | for a in attributes(root_node) 164 | if !(name(a) in attributes_to_exclude) 165 | result[name(a)] = tryparse_string_to_number(value(a)) 166 | end 167 | end 168 | 169 | if has_children(root_node) 170 | for c in child_elements(root_node) 171 | key = name(c) 172 | 173 | if key in attributes_to_exclude 174 | continue 175 | end 176 | 177 | if haskey(result, name(c)) 178 | push!(result[key]::Vector{Dict{String, Any}}, xml_to_dict(c, attributes_to_exclude)) 179 | else 180 | result[key] = [xml_to_dict(c, attributes_to_exclude)] 181 | end 182 | end 183 | end 184 | 185 | return result 186 | end 187 | 188 | """ 189 | Returns the first and last element of an array. 190 | """ 191 | trailing_elements(array::AbstractArray)::AbstractArray = [array[1], array[end]] 192 | 193 | """ 194 | Returns the common trailing element (first or last element) of two arrays if it exists. 195 | """ 196 | function first_common_trailing_element(a1::AbstractArray{T}, a2::AbstractArray{T})::T where T <: Any 197 | intersection = intersect(trailing_elements(a1), trailing_elements(a2)) 198 | return length(intersection) >= 1 ? intersection[1] : throw(ArgumentError("No common trailinging elements between $a1 and $a2")) 199 | end 200 | 201 | """ 202 | Joins two arrays into a single array on a common trailing element. 203 | """ 204 | function join_two_arrays_on_common_trailing_elements(a1::AbstractArray{T}, a2::AbstractArray{T})::AbstractArray{T} where T <: Any 205 | el = first_common_trailing_element(a1, a2) 206 | 207 | if el == a1[1] == a2[1] 208 | return [reverse(a1)..., a2[2:end]...] 209 | elseif el == a1[1] == a2[end] 210 | return [reverse(a1)..., reverse(a2)[2:end]...] 211 | elseif el == a1[end] == a2[1] 212 | return [a1..., a2[2:end]...] 213 | elseif el == a1[end] == a2[end] 214 | return [a1..., reverse(a2)[2:end]...] 215 | end 216 | end 217 | 218 | """ 219 | Joins an array of arrays into a single array, on common trailing elements. 220 | """ 221 | function join_arrays_on_common_trailing_elements(arrays::AbstractVector{T}...)::AbstractVector{T} where T <: Any 222 | # Can we make this type stable? IE for input of Vector{Vector{Int}} a return of Vector{Int} can be detected by @code_warntype 223 | current = arrays[1] 224 | others = setdiff(arrays, [current]) 225 | 226 | if !isempty(others) 227 | for (i, other) in enumerate(others) 228 | try 229 | current = join_two_arrays_on_common_trailing_elements(current, other) 230 | deleteat!(others, i) 231 | return join_arrays_on_common_trailing_elements(current, others...) 232 | catch 233 | continue 234 | end 235 | end 236 | throw(ErrorException("Could not join $current on $others")) 237 | else 238 | return current 239 | end 240 | end 241 | 242 | """ 243 | delete_from_dict!(dict::AbstractDict, items_to_delete::Union{AbstractArray,AbstractSet}, how::Symbol=:on_key) 244 | 245 | Deletes key-value pairs from a dictionary. 246 | 247 | # Arguments 248 | - `dict::AbstractDict`: Any dictionary to delete from. 249 | - `items_to_delete::Union{AbstractArray,AbstractSet}`: List of items to delete, either keys or values. 250 | - `how::Symbol=:on_key`: To delete `:on_key` or `:on_value`. 251 | 252 | # Return 253 | - `AbstractDict`: Filtered dictionary. 254 | """ 255 | function delete_from_dict!(dict::AbstractDict, items_to_delete::Union{AbstractArray,AbstractSet}, how::Symbol=:on_key) 256 | if how == :on_key 257 | for k in items_to_delete 258 | delete!(dict, k) 259 | end 260 | elseif how == :on_value 261 | for (k, v) in dict 262 | if v in items_to_delete 263 | delete!(dict, k) 264 | end 265 | end 266 | else 267 | throw(ErrorException("Choose `how` as either `:on_key` or `on_value`")) 268 | end 269 | end 270 | 271 | """ 272 | flatten(array::AbstractArray)::AbstractArray 273 | 274 | Flattens an array of arrays. 275 | """ 276 | function flatten(array::AbstractArray)::AbstractArray 277 | flattened = collect(Iterators.flatten(array)) 278 | if any(x -> typeof(x) <: AbstractArray, flattened) 279 | return flatten(flattened) 280 | end 281 | return flattened 282 | end 283 | -------------------------------------------------------------------------------- /src/constants.jl: -------------------------------------------------------------------------------- 1 | """ 2 | Default data types used to construct OSMGraph object. 3 | """ 4 | const DEFAULT_OSM_INDEX_TYPE = Int32 5 | const DEFAULT_OSM_ID_TYPE = Union{Integer, String} 6 | const DEFAULT_OSM_EDGE_WEIGHT_TYPE = Float64 7 | const DEFAULT_OSM_MAXSPEED_TYPE = Int16 8 | const DEFAULT_OSM_LANES_TYPE = Int8 9 | 10 | """ 11 | Approximate radius of the Earth (km) used in geoemetry functions. 12 | """ 13 | const RADIUS_OF_EARTH_KM = 6371.0 14 | 15 | """ 16 | Factor used to convert speed from mph to kph units. 17 | """ 18 | const KMH_PER_MPH = 1.60934 19 | 20 | """ 21 | URLs to OpenStreetMap APIs. 22 | """ 23 | const OSM_URLS = Dict( 24 | :nominatim_search => "https://nominatim.openstreetmap.org/search", 25 | :overpass_map => "http://overpass-api.de/api/interpreter", 26 | :overpass_status => "https://overpass-api.de/api/status" 27 | ) 28 | 29 | """ 30 | Exclusion filters used when querying different OpenStreetMap networks. 31 | """ 32 | const WAY_EXCLUSION_FILTERS = Dict( 33 | :drive_mainroads => Dict( 34 | "area" => ["yes"], 35 | "highway" => ["cycleway", "footway", "path", "pedestrian", "steps", "track", "corridor", "elevator", "escalator", "proposed", "construction", "bridleway", "abandoned", "platform", "raceway", "service", "residential"], 36 | "motor_vehicle" => ["no"], 37 | "motorcar" => ["no"], 38 | "access" => ["private"], 39 | "service" => ["parking", "parking_aisle", "driveway", "private", "emergency_access"] 40 | ), 41 | :drive => Dict( 42 | "area" => ["yes"], 43 | "highway" => ["cycleway", "footway", "path", "pedestrian", "steps", "track", "corridor", "elevator", "escalator", "proposed", "construction", "bridleway", "abandoned", "platform", "raceway", "service"], 44 | "motor_vehicle" => ["no"], 45 | "motorcar" => ["no"], 46 | "access" => ["private"], 47 | "service" => ["parking", "parking_aisle", "driveway", "private", "emergency_access"] 48 | ), 49 | :drive_service => Dict( 50 | "area" => ["yes"], 51 | "highway" => ["cycleway", "footway", "path", "pedestrian", "steps", "track", "corridor", "elevator", "escalator", "proposed", "construction", "bridleway", "abandoned", "platform", "raceway"], 52 | "motor_vehicle" => ["no"], 53 | "motorcar" => ["no"], 54 | "access" => ["private"], 55 | "service" => ["parking", "parking_aisle", "private", "emergency_access"] 56 | ), 57 | :walk => Dict( 58 | "area" => ["yes"], 59 | "highway" => ["cycleway", "motor", "proposed", "construction", "abandoned", "platform", "raceway"], 60 | "foot" => ["no"], 61 | "access" => ["private"], 62 | "service" => ["private"] 63 | ), 64 | :bike => Dict( 65 | "area" => ["yes"], 66 | "highway" => ["footway", "steps", "corridor", "elevator", "escalator", "motor", "proposed", "construction", "abandoned", "platform", "raceway"], 67 | "bicycle" => ["no"], 68 | "access" => ["private"], 69 | "service" => ["private"] 70 | ), 71 | :all => Dict( 72 | "area" => ["yes"], 73 | "highway" => ["proposed", "construction", "abandoned", "platform", "raceway"], 74 | "access" => ["private"], 75 | "service" => ["private"] 76 | ), 77 | :all_private => Dict( 78 | "area" => ["yes"], 79 | "highway" => ["proposed", "construction", "abandoned", "platform", "raceway"] 80 | ), 81 | :none => Dict{AbstractString,Vector{AbstractString}}(), 82 | :rail => Dict("highway" => ["proposed", "platform"]) 83 | ) 84 | 85 | """ 86 | Concantenates OpenStreetMap exclusion filters into a query string. 87 | """ 88 | function concatenate_exclusions(exclusions::AbstractDict{S,Vector{S}})::S where {S <: AbstractString} 89 | filters = "" 90 | 91 | for (k, v) in exclusions 92 | filters *= """["$k"!~"$(join(v, '|'))"]""" 93 | end 94 | 95 | return filters 96 | end 97 | 98 | """ 99 | OpenStreetMap query strings used for different transport networks, to test queries see `https://overpass-api.de/query_form.html`. 100 | """ 101 | const WAY_FILTERS_QUERY = Dict( 102 | :drive_mainroads => """["highway"]$(concatenate_exclusions(WAY_EXCLUSION_FILTERS[:drive_mainroads]))""", 103 | :drive => """["highway"]$(concatenate_exclusions(WAY_EXCLUSION_FILTERS[:drive]))""", 104 | :drive_service => """["highway"]$(concatenate_exclusions(WAY_EXCLUSION_FILTERS[:drive_service]))""", 105 | :walk => """["highway"]$(concatenate_exclusions(WAY_EXCLUSION_FILTERS[:walk]))""", 106 | :bike => """["highway"]$(concatenate_exclusions(WAY_EXCLUSION_FILTERS[:bike]))""", 107 | :all => """["highway"]$(concatenate_exclusions(WAY_EXCLUSION_FILTERS[:all]))""", 108 | :all_private => """["highway"]$(concatenate_exclusions(WAY_EXCLUSION_FILTERS[:all_private]))""", 109 | :none => """["highway"]$(concatenate_exclusions(WAY_EXCLUSION_FILTERS[:none]))""", 110 | :rail => """["railway"]$(concatenate_exclusions(WAY_EXCLUSION_FILTERS[:rail]))""" 111 | ) 112 | 113 | """ 114 | OpenStreetMap query strings used for getting relation data in addition to nodes and ways that are contained within. 115 | """ 116 | const RELATION_FILTERS_QUERY = Dict( 117 | :drive_mainroads => """["type"="restriction"]["restriction"][!"conditional"][!"hgv"]""", 118 | :drive => """["type"="restriction"]["restriction"][!"conditional"][!"hgv"]""", 119 | :drive_service => """["type"="restriction"]["restriction"][!"conditional"][!"hgv"]""", 120 | :walk => nothing, 121 | :bike => nothing, 122 | :all => """["type"="restriction"]["restriction"][!"conditional"][!"hgv"]""", 123 | :all_private => """["type"="restriction"]["restriction"][!"conditional"][!"hgv"]""", 124 | :none => nothing, 125 | :rail => nothing 126 | ) 127 | 128 | """ 129 | OpenStreetMap metadata tags. 130 | """ 131 | const OSM_METADATA = Set(["version", "timestamp", "changeset", "uid", "user", "generator", "note", "meta", "count"]) 132 | 133 | """ 134 | OpenStreetMap download formats. 135 | """ 136 | const OSM_DOWNLOAD_FORMAT = Dict( 137 | :osm => "xml", 138 | :xml => "xml", 139 | :json => "json" 140 | ) 141 | 142 | """ 143 | Default maxspeed based on highway type. 144 | """ 145 | const DEFAULT_MAXSPEEDS = Ref(Dict{String,DEFAULT_OSM_MAXSPEED_TYPE}( 146 | "motorway" => 100, 147 | "trunk" => 100, 148 | "primary" => 100, 149 | "secondary" => 100, 150 | "tertiary" => 50, 151 | "unclassified" => 50, 152 | "residential" => 50, 153 | "other" => 50 154 | )) 155 | 156 | """ 157 | Default number of lanes based on highway type. 158 | """ 159 | const DEFAULT_LANES = Ref(Dict{String,DEFAULT_OSM_LANES_TYPE}( 160 | "motorway" => 3, 161 | "trunk" => 3, 162 | "primary" => 2, 163 | "secondary" => 2, 164 | "tertiary" => 1, 165 | "unclassified" => 1, 166 | "residential" => 1, 167 | "other" => 1 168 | )) 169 | 170 | """ 171 | Default oneway attribute based on highway type. 172 | """ 173 | const DEFAULT_ONEWAY = Dict( 174 | "motorway" => false, 175 | "trunk" => false, 176 | "primary" => false, 177 | "secondary" => false, 178 | "tertiary" => false, 179 | "unclassified" => false, 180 | "residential" => false, 181 | "roundabout" => true, 182 | "other" => false 183 | ) 184 | 185 | """ 186 | Values that a determine a road is oneway. 187 | """ 188 | const ONEWAY_TRUE = Set(["true", "yes", "1", "-1", 1, -1]) 189 | 190 | """ 191 | Values that a determine a road is not oneway. 192 | """ 193 | const ONEWAY_FALSE = Set(["false", "no", "0", 0]) 194 | 195 | """ 196 | Default factor applied to maxspeed when the `lane_efficiency` weight is used to contruct OSMGraph object. 197 | """ 198 | const LANE_EFFICIENCY = Ref(Dict{DEFAULT_OSM_LANES_TYPE,DEFAULT_OSM_EDGE_WEIGHT_TYPE}( 199 | 1 => 0.7, 200 | 2 => 0.8, 201 | 3 => 0.9, 202 | 4 => 1.0 203 | )) 204 | 205 | """ 206 | Default height of buildings in metres. 207 | """ 208 | const DEFAULT_BUILDING_HEIGHT_PER_LEVEL = Ref{Float64}(4) 209 | 210 | """ 211 | Default maximum levels of buildings. 212 | """ 213 | const DEFAULT_MAX_BUILDING_LEVELS = Ref{Int}(3) 214 | 215 | """ 216 | Delimiters used to clean maxspeed and lanes data. 217 | """ 218 | const COMMON_OSM_STRING_DELIMITERS = r"[+^:;,|-]" 219 | 220 | """ 221 | LightOSM.set_defaults(kwargs...) 222 | 223 | Sets default values that LightOSM uses when generating the graph. All arguments are 224 | optional. 225 | 226 | # Keyword Arguments 227 | - `maxspeeds::AbstractDict{String,<:Real}`: If no `maxspeed` way tag is available, these 228 | values are used instead based on the value of the `highway` way tag. If no 229 | `highway` way tag is available, the value for `"other"` is used. Unit is km/h. 230 | Default value: 231 | ``` 232 | Dict( 233 | "motorway" => 100, 234 | "trunk" => 100, 235 | "primary" => 100, 236 | "secondary" => 100, 237 | "tertiary" => 50, 238 | "unclassified" => 50, 239 | "residential" => 50, 240 | "other" => 50 241 | ) 242 | ``` 243 | - `lanes::AbstractDict{String,<:Integer}`: If no `lanes` way tag is available, these 244 | values are used instead based on the value of the `highway` way tag. If no 245 | `highway` way tag is available, the value for `"other"` is used. 246 | Default value: 247 | ``` 248 | Dict( 249 | "motorway" => 3, 250 | "trunk" => 3, 251 | "primary" => 2, 252 | "secondary" => 2, 253 | "tertiary" => 1, 254 | "unclassified" => 1, 255 | "residential" => 1, 256 | "other" => 1 257 | ) 258 | ``` 259 | - `lane_efficiency::AbstractDict{<:Integer,<:Real}`: Gives the lane efficiency based on 260 | number of lanes. `1.0` is used for any number of lanes not specified here. 261 | Default value: 262 | ``` 263 | LANE_EFFICIENCY = Dict( 264 | 1 => 0.7, 265 | 2 => 0.8, 266 | 3 => 0.9, 267 | 4 => 1.0 268 | ) 269 | ``` 270 | - `building_height_per_level::Integer`: If the `height` building tag is not available, 271 | it is calculated by multiplying this value by the number of levels from the 272 | `building:levels` tag. Unit is metres. Default value: 273 | ``` 274 | 4 275 | ``` 276 | - `max_building_levels::Integer`: If the `building:levels` tag is not available, a number 277 | is randomly chosen between 1 and this value. Default value: 278 | ``` 279 | 3 280 | ``` 281 | """ 282 | function set_defaults(; 283 | maxspeeds::AbstractDict{String,<:Real}=DEFAULT_MAXSPEEDS[], 284 | lanes::AbstractDict{String,<:Integer}=DEFAULT_LANES[], 285 | lane_efficiency::AbstractDict{<:Integer,<:Real}=LANE_EFFICIENCY[], 286 | building_height_per_level::Real=DEFAULT_BUILDING_HEIGHT_PER_LEVEL[], 287 | max_building_levels::Integer=DEFAULT_MAX_BUILDING_LEVELS[] 288 | ) 289 | DEFAULT_MAXSPEEDS[] = maxspeeds 290 | DEFAULT_LANES[] = lanes 291 | LANE_EFFICIENCY[] = lane_efficiency 292 | DEFAULT_BUILDING_HEIGHT_PER_LEVEL[] = building_height_per_level 293 | DEFAULT_MAX_BUILDING_LEVELS[] = max_building_levels 294 | return 295 | end 296 | -------------------------------------------------------------------------------- /src/geometry.jl: -------------------------------------------------------------------------------- 1 | """ 2 | to_cartesian(lat::T, lon::T, r::T) where {T} 3 | to_cartesian(location::GeoLocation) 4 | to_cartesian(locations::Vector{GeoLocation}) 5 | 6 | Converts a vector of GeoLocations to (x, y, z) cartesian coordinates (based on radius of the Earth). 7 | """ 8 | function to_cartesian(lat::T, lon::T, r::T) where {T} 9 | x = r * cos(lat) * cos(lon) 10 | y = r * cos(lat) * sin(lon) 11 | z = r * sin(lat) 12 | return x, y, z 13 | end 14 | function to_cartesian(loc::GeoLocation) 15 | lat = deg2rad(loc.lat) 16 | lon = deg2rad(loc.lon) 17 | r = loc.alt + RADIUS_OF_EARTH_KM # approximate radius of earth + alt in km 18 | x, y, z = to_cartesian(lat, lon, r) 19 | return x, y, z 20 | end 21 | function to_cartesian(locs::Vector{GeoLocation}) 22 | n_points = length(locs) 23 | results = Matrix{Float64}(undef, 3, n_points) 24 | @inbounds for i in 1:n_points 25 | x, y, z = to_cartesian(locs[i]) 26 | results[1, i] = x 27 | results[2, i] = y 28 | results[3, i] = z 29 | end 30 | return results 31 | end 32 | 33 | """ 34 | haversine(a_lat::T, a_lon::T, b_lat::T, b_lon::T) where {T} 35 | haversine(GeoLocation, B::GeoLocation) 36 | haversine(Node, B::Node) 37 | haversine(A::Vector{GeoLocation}, B::Vector{GeoLocation}) 38 | haversine(A::Vector{Node}, B::Vector{Node}) 39 | haversine(a::Vector{U}, b::Vector{U})::U where {U <: AbstractFloat} 40 | 41 | Calculates the haversine distance (km) between two points. 42 | """ 43 | 44 | function haversine(a_lat::T, a_lon::T, b_lat::T, b_lon::T) where {T} 45 | d = sin((a_lat - b_lat) / 2)^2 + cos(b_lat) * cos(a_lat) * sin((a_lon - b_lon) / 2)^2 46 | return 2 * RADIUS_OF_EARTH_KM * asin(sqrt(d)) 47 | end 48 | function haversine(A::GeoLocation, B::GeoLocation) 49 | a_lat = deg2rad(A.lat) 50 | a_lon = deg2rad(A.lon) 51 | b_lat = deg2rad(B.lat) 52 | b_lon = deg2rad(B.lon) 53 | return haversine(a_lat, a_lon, b_lat, b_lon) 54 | end 55 | haversine(a::Node, b::Node) = haversine(a.location, b.location) 56 | haversine(A::Vector{<:GeoLocation}, B::Vector{<:GeoLocation}) = haversine.(A, B) 57 | haversine(A::Vector{<:Node}, B::Vector{<:Node}) = haversine.(A, B) 58 | function haversine(a::Vector{U}, b::Vector{U})::U where {U <: AbstractFloat} 59 | a_lat = deg2rad(a[1]) 60 | a_lon = deg2rad(a[2]) 61 | b_lat = deg2rad(b[1]) 62 | b_lon = deg2rad(b[2]) 63 | return haversine(a_lat, a_lon, b_lat, b_lon) 64 | end 65 | 66 | """ 67 | euclidean(a_x::T, a_y::T, a_z::T, b_x::T, b_y::T, b_z::T) where {T} 68 | euclidean(A::GeoLocation, B::GeoLocation) 69 | euclidean(A::Node, B::Node) 70 | euclidean(A::Vector{GeoLocation}, B::Vector{GeoLocation}) 71 | euclidean(A::Vector{<:Node}, B::Vector{<:Node}) 72 | euclidean(a::Vector{U}, b::Vector{U})::U where {U <: AbstractFloat} 73 | 74 | Calculates the euclidean distance (km) between two points. 75 | """ 76 | euclidean(a_x::T, a_y::T, a_z::T, b_x::T, b_y::T, b_z::T) where {T} = hypot(a_x-b_x, a_y-b_y, a_z-b_z) 77 | euclidean(A::GeoLocation, B::GeoLocation) = euclidean(to_cartesian(A)...,to_cartesian(B)...) 78 | euclidean(a::Node, b::Node) = euclidean(a.location, b.location) 79 | euclidean(A::Vector{GeoLocation}, B::Vector{GeoLocation}) = euclidean.(A, B) 80 | euclidean(A::Vector{<:Node}, B::Vector{<:Node}) = euclidean.(A, B) 81 | function euclidean(a::Vector{U}, b::Vector{U})::U where {U <: AbstractFloat} 82 | a_lat = deg2rad(a[1]) 83 | a_lon = deg2rad(a[2]) 84 | b_lat = deg2rad(b[1]) 85 | b_lon = deg2rad(b[2]) 86 | return euclidean( 87 | to_cartesian(a_lat, a_lon, RADIUS_OF_EARTH_KM)..., 88 | to_cartesian(b_lat, b_lon, RADIUS_OF_EARTH_KM)... 89 | ) 90 | end 91 | 92 | """ 93 | distance(A::Union{Vector{GeoLocation}, GeoLocation, Vector{<:Node}, Node, Vector{<:AbstractFloat}}, 94 | B::Union{Vector{GeoLocation}, GeoLocation, Vector{<:Node}, Node, Vector{<:AbstractFloat}}, 95 | type::Symbol=:haversine 96 | ) 97 | 98 | Calculates the distance (km) between two points or two vectors of points. 99 | 100 | # Arguments 101 | - `A::Union{Vector{GeoLocation}, GeoLocation, Vector{<:Node}, Node, Vector{<:AbstractFloat}}`: Vector of origin points. 102 | - `B::Union{Vector{GeoLocation}, GeoLocation, Vector{<:Node}, Node, Vector{<:AbstractFloat}}`: Vector of destination points. 103 | - `method::Symbol=:haversine`: Either `:haversine` or `:euclidean`. 104 | 105 | # Return 106 | - Distance between origin and destination points in km. 107 | """ 108 | function distance(A::Union{Vector{GeoLocation},GeoLocation,Vector{<:Node},Node,Vector{<:AbstractFloat}}, 109 | B::Union{Vector{GeoLocation},GeoLocation,Vector{<:Node},Node,Vector{<:AbstractFloat}}, 110 | method::Symbol=:haversine 111 | ) 112 | if method == :haversine 113 | return haversine(A, B) 114 | elseif method == :euclidean 115 | return euclidean(A, B) 116 | else 117 | throw(ArgumentError("Distance method $method not implemented")) 118 | end 119 | end 120 | 121 | """ 122 | heading(a::GeoLocation, b::GeoLocation, return_units::Symbol=:degrees) 123 | heading(a::Node, b::Node, return_units::Symbol=:degrees) 124 | heading(A::Vector{GeoLocation}, B::Vector{GeoLocation}, return_units::Symbol=:degrees) 125 | heading(A::Vector{Node}, B::Vector{Node}, return_units::Symbol=:degrees) 126 | 127 | Calculates heading(s) / bearing(s) between two points (`a` is origin, `b` is destination) 128 | or two vectors of points (`A` is vector of origins, `B` is vector of destinations). Points 129 | can be either `GeoLocation`s or `Node`s. 130 | 131 | Depending on the `return_units` chosen, the return angle is in range of [-π, π] if `:radians` 132 | or [-180, 180] if `:degrees`. Additionally, adjusts destination longitude in case the straight 133 | line path between a and b crosses the International Date Line. 134 | """ 135 | function heading(a::GeoLocation, b::GeoLocation, return_units::Symbol=:degrees) 136 | a_lat = a.lat 137 | a_lon = a.lon 138 | b_lat = b.lat 139 | b_lon = b.lon 140 | 141 | # Adjust destination longitude in case straight line path between A and B crosses the International Date Line 142 | a_lon_left_idx = (b_lon <= a_lon) * ((b_lon + 180) + (180 - a_lon) > (a_lon - b_lon)) 143 | a_lon_right_idx = (b_lon <= a_lon) * ((b_lon + 180) + (180 - a_lon) <= (a_lon - b_lon)) 144 | 145 | b_lon_left_idx = (b_lon > a_lon) * ((a_lon + 180) + (180 - b_lon) .< (b_lon - a_lon)) 146 | b_lon_right_idx = (b_lon > a_lon) * ((a_lon + 180) + (180 - b_lon) >= (b_lon - a_lon)) 147 | 148 | b_lon_fixed = b_lon_left_idx * (-180 - (180 - b_lon)) + 149 | b_lon_right_idx * b_lon + 150 | a_lon_left_idx * b_lon + 151 | a_lon_right_idx * (180 - abs(-180 - b_lon)) 152 | 153 | a_lat = deg2rad(a_lat) 154 | a_lon = deg2rad(a_lon) 155 | b_lat = deg2rad(b_lat) 156 | b_lon_fixed = deg2rad(b_lon_fixed) 157 | 158 | y = sin(b_lon_fixed - a_lon) * cos(b_lat) 159 | x = cos(a_lat) * sin(b_lat) - sin(a_lat) * cos(b_lat) * cos(b_lon_fixed - a_lon) 160 | 161 | heading = atan.(y, x) 162 | 163 | if return_units == :radians 164 | return heading 165 | elseif return_units == :degrees 166 | return rad2deg(heading) 167 | else 168 | throw(ArgumentError("Incorrect input for argument `return_units`, choose either `:degrees` or `:radians`")) 169 | end 170 | end 171 | heading(A::Vector{GeoLocation}, B::Vector{GeoLocation}, return_units::Symbol=:degrees) = heading.(A, B, return_units) 172 | heading(a::Node, b::Node, return_units::Symbol=:degrees)::AbstractFloat = heading(a.location, b.location, return_units) 173 | heading(A::Vector{<:Node}, B::Vector{<:Node}, return_units::Symbol=:degrees) = heading.(A, B, return_units) 174 | 175 | """ 176 | calculate_location(origin::GeoLocation, heading::Number, distance::Number) 177 | calculate_location(origin::Node, heading::Number, distance::Number) 178 | calculate_location(origin::Vector{GeoLocation}, heading::Vector{<:Number}, distance::Vector{<:Number}) 179 | calculate_location(origin::Vector{Node}, heading::Vector{<:Number}, distance::Vector{<:Number}) 180 | 181 | Calculates next location(s) given origin `GeoLocation`(s) or `Node`(s), heading(s) (degrees) 182 | and distance(s) (km). 183 | 184 | Locations are returned as `GeoLocation`s. 185 | """ 186 | function calculate_location(origin::GeoLocation, heading::Number, distance::Number) 187 | lat = deg2rad(origin.lat) 188 | lon = deg2rad(origin.lon) 189 | heading = deg2rad(heading) 190 | 191 | lat_final = asin(sin(lat) * cos(distance / RADIUS_OF_EARTH_KM) + cos(lat) * sin(distance / RADIUS_OF_EARTH_KM) * cos(heading)) 192 | lon_final = lon + atan(sin(heading) * sin(distance / RADIUS_OF_EARTH_KM) * cos(lat), cos(distance / RADIUS_OF_EARTH_KM) - sin(lat) * sin(lat_final)) 193 | 194 | return GeoLocation(rad2deg(lat_final), rad2deg(lon_final)) 195 | end 196 | calculate_location(origins::Vector{GeoLocation}, headings::Vector{<:Number}, distances::Vector{<:Number}) = calculate_location.(origins, headings, distances) 197 | calculate_location(origin::Node, heading::Number, distance::Number)::GeoLocation = calculate_location(origin.location, heading, distance) 198 | calculate_location(origins::Vector{<:Node}, headings::Vector{<:Number}, distances::Vector{<:Number}) = calculate_location.(origins, headings, distances) 199 | 200 | """ 201 | bounding_box_from_point(point::GeoLocation, radius::Number)::NamedTuple 202 | 203 | Calculates the coordinates of the bounding box given a centroid point and radius (km). 204 | 205 | # Arguments 206 | - `point::GeoLocation`: Centroid of the bounding box as an GeoLocation. 207 | - `radius::Number`: Radius in km of the bounding box (to each corner). 208 | 209 | # Return 210 | - `NamedTuple`: Named tuple with attributes minlat, minlon, maxlat, right_lon. 211 | """ 212 | function bounding_box_from_point(point::GeoLocation, radius::Number)::NamedTuple 213 | bottom_left, top_right = calculate_location([point, point], [225, 45], [radius, radius]) 214 | return (minlat = bottom_left.lat, minlon = bottom_left.lon, maxlat = top_right.lat, maxlon = top_right.lon) 215 | end 216 | 217 | """ 218 | nearest_point_on_line(x1::T, 219 | y1::T, 220 | x2::T, 221 | y2::T, 222 | x::T, 223 | y::T 224 | )::Tuple{T,T,T} where {T <: AbstractFloat} 225 | 226 | Finds the nearest position along a straight line to a given point. 227 | 228 | # Arguments 229 | - `x1::T`, `y1::T`: Starting point of the line. 230 | - `x2::T`, `y2::T`: Ending point of the line. 231 | - `x::T`, `y::T`: Point to nearest position to. 232 | 233 | # Returns 234 | - `::Tuple`: 235 | - `::T`: x-coordinate of nearest position. 236 | - `::T`: y-coordinate of nearest position. 237 | - `::T`: Position along the line, from 0 to 1. 238 | """ 239 | function nearest_point_on_line(x1::T, 240 | y1::T, 241 | x2::T, 242 | y2::T, 243 | x::T, 244 | y::T 245 | )::Tuple{T,T,T} where {T <: AbstractFloat} 246 | A = x - x1 247 | B = y - y1 248 | C = x2 - x1 249 | D = y2 - y1 250 | dot = A * C + B * D 251 | len_sq = C * C + D * D 252 | param = -one(T) 253 | if len_sq != 0 # in case of 0 length line 254 | param = dot / len_sq 255 | end 256 | if param < 0.0 257 | return (x1, y1, zero(T)) 258 | elseif param > 1.0 259 | return (x2, y2, one(T)) 260 | else 261 | return (x1 + param * C, y1 + param * D, param) 262 | end 263 | end -------------------------------------------------------------------------------- /src/shortest_path.jl: -------------------------------------------------------------------------------- 1 | """ 2 | shortest_path([PathAlgorithm,] 3 | g::OSMGraph, 4 | origin::Union{Integer,Node}, 5 | destination::Union{Integer,Node}, 6 | [weights::AbstractMatrix=g.weights; 7 | cost_adjustment::Function=(u, v, parents) -> 0.0), 8 | max_distance::W=typemax(W)] 9 | 10 | Calculates the shortest path between two OpenStreetMap node ids. 11 | 12 | # Arguments 13 | - `PathAlgorithm` (optional): Path finding algorithm, possible values are: 14 | - `Dijkstra`: Same as `DijkstraVector`. This is the default algorithm. 15 | - `DijkstraVector`: Dijkstra's algorithm using the `Vector` implementation. 16 | Faster for small graphs and/or long paths. 17 | - `DijkstraDict`: Dijkstra's algorithm using the `Dict` implementation. 18 | Faster for large graphs and/or short paths. 19 | - `AStar`: Same as `AStarVector`. 20 | - `AStarVector`: A* algorithm using the `Vector` implementation. 21 | Faster for small graphs and/or long paths. 22 | - `AStarDict`: A* algorithm using the `Dict` implementation. 23 | Faster for large graphs and/or short paths. 24 | - `g::OSMGraph{U,T,W}`: Graph container. 25 | - `origin::Union{Integer,Node}`: Origin OpenStreetMap node or node id. 26 | - `destination::Union{Integer,Node},`: Destination OpenStreetMap node or node 27 | id. 28 | - `weights`: Optional matrix of node to node edge weights, defaults to 29 | `g.weights`. If a custom weights matrix is being used with algorithm set to 30 | `AStar`, make sure that a correct heuristic is being used. 31 | - `cost_adjustment::Function=(u,v,parents) -> 0.0`: Option to pass in a function 32 | to adjust the cost between each pair of vetices `u` and `v`, normally the 33 | cost is just the weight between `u` and `v`, `cost_adjustment` takes in 3 34 | arguments; `u`, `v` and `parents`; to apply an additive cost to the default 35 | weight. Defaults no adjustment. Use `restriction_cost_adjustment` to 36 | consider turn restrictions. 37 | - `heuristic::Function=distance_heuristic(g)`: Use custom heuristic with the 38 | `AStar` algorithms only. Defaults to a function 39 | `h(u, v) -> haversine(u, v)`, i.e. returns the haversine distances between 40 | `u`, the current node, and `v`, the neighbouring node. If `g.weight_type` 41 | is `:time` or `:lane_efficiency`, use `time_heuristic(g)` instead. 42 | 43 | # Return 44 | - `Union{Nothing,Vector{T}}`: Array of OpenStreetMap node ids making up 45 | the shortest path. 46 | """ 47 | function shortest_path(::Type{A}, 48 | g::OSMGraph{U,T,W}, 49 | origin::DEFAULT_OSM_ID_TYPE, 50 | destination::DEFAULT_OSM_ID_TYPE, 51 | weights::AbstractMatrix{W}; 52 | cost_adjustment::Function=(u, v, parents) -> 0.0, 53 | max_distance::W=typemax(W) 54 | )::Union{Nothing,Vector{T}} where {A <: Dijkstra, U, T, W} 55 | o_index = node_id_to_index(g, origin) 56 | d_index = node_id_to_index(g, destination) 57 | path = dijkstra(A, g.graph, weights, o_index, d_index; cost_adjustment=cost_adjustment, max_distance=max_distance) 58 | isnothing(path) && return 59 | return index_to_node_id(g, path) 60 | end 61 | function shortest_path(::Type{A}, 62 | g::OSMGraph{U,T,W}, 63 | origin::DEFAULT_OSM_ID_TYPE, 64 | destination::DEFAULT_OSM_ID_TYPE, 65 | weights::AbstractMatrix{W}; 66 | cost_adjustment::Function=(u, v, parents) -> 0.0, 67 | heuristic::Function=distance_heuristic(g), 68 | max_distance::W=typemax(W) 69 | )::Union{Nothing,Vector{T}} where {A <: AStar, U, T, W} 70 | o_index = node_id_to_index(g, origin) 71 | d_index = node_id_to_index(g, destination) 72 | path = astar(A, g.graph, weights, o_index, d_index; cost_adjustment=cost_adjustment, heuristic=heuristic, max_distance=max_distance) 73 | isnothing(path) && return 74 | return index_to_node_id(g, path) 75 | end 76 | 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} 77 | return shortest_path(A, g, origin, destination, g.weights; kwargs...) 78 | end 79 | 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} 80 | return shortest_path(A, g, origin.id, destination.id, args...; kwargs...) 81 | end 82 | function shortest_path(g::OSMGraph{U,T,W}, args...; kwargs...)::Union{Nothing,Vector{T}} where {U, T, W} 83 | return shortest_path(Dijkstra, g, args...; kwargs...) 84 | end 85 | 86 | """ 87 | set_dijkstra_state!(g::OSMGraph, src::Union{Integer,Vecotr{<:Integer}, weights::AbstractMatrix; cost_adjustment::Function=(u, v, parents) -> 0.0) 88 | 89 | Compute and set the dijkstra parent states for one or multiple src vertices. Threads are used for multiple srcs. 90 | Note, computing dijkstra states for all vertices is a O(V² + ElogV) operation, use on large graphs with caution. 91 | """ 92 | function set_dijkstra_state!(g::OSMGraph, src::Integer, weights::AbstractMatrix; cost_adjustment::Function=(u, v, parents) -> 0.0) 93 | g.dijkstra_states[src] = dijkstra(g.graph, weights, src; cost_adjustment=cost_adjustment) 94 | end 95 | function set_dijkstra_state!(g::OSMGraph, srcs::Vector{<:Integer}, weights::AbstractMatrix; cost_adjustment::Function=(u, v, parents) -> 0.0) 96 | Threads.@threads for src in srcs 97 | set_dijkstra_state!(g, src, weights; cost_adjustment=cost_adjustment) 98 | end 99 | return g 100 | end 101 | set_dijkstra_state!(g::OSMGraph, src; kwargs...) = set_dijkstra_state!(g, src, g.weights; kwargs...) 102 | 103 | """ 104 | shortest_path_from_dijkstra_state(g::OSMGraph, origin::Integer, destination::Integer) 105 | 106 | Extract shortest path from precomputed dijkstra state, from `origin` to `detination` node id. 107 | 108 | Note, function will raise `UndefRefError: access to undefined reference` if the dijkstra state of the 109 | origin node is not precomputed. 110 | 111 | # Arguments 112 | - `g::OSMGraph`: Graph container. 113 | - `origin::Integer`: Origin OpenStreetMap node or node id. 114 | - `destination::Integer`: Destination OpenStreetMap node or node id. 115 | 116 | # Return 117 | - `Union{Nothing,Vector{T}}`: Array of OpenStreetMap node ids making up the shortest path. 118 | """ 119 | function shortest_path_from_dijkstra_state(g::OSMGraph, origin::Integer, destination::Integer) 120 | parents = node_id_to_dijkstra_state(g, origin) 121 | path = path_from_parents(parents, node_id_to_index(g, destination)) 122 | isnothing(path) && return 123 | return index_to_node_id(g, path) 124 | end 125 | 126 | """ 127 | is_restricted(restriction_ll::MutableLinkedList{V}, u::U, v::U, parents::P)::Bool where {P <: Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}} where {U <: Integer,V <: Integer} 128 | 129 | Given parents, returns `true` if path between `u` and `v` is restricted by the restriction linked list, `false` otherwise. 130 | 131 | # Arguments 132 | - `restriction_ll::MutableLinkedList{V}`: Linked list holding vertices in order of v -> parents. 133 | - `u::U`: Current vertex visiting. 134 | - `v::U`: Current neighbour vertex. 135 | - `parents::Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}`: Mapping of shortest path children to parents. 136 | 137 | # Return 138 | - `Bool`: Returns true if path between `u` and `v` is restricted. 139 | """ 140 | function is_restricted(restriction_ll::MutableLinkedList{V}, u::U, v::U, parents::P)::Bool where {P <: Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}} where {U <: Integer,V <: Integer} 141 | current = restriction_ll.node.next 142 | 143 | if v != current.data 144 | return false 145 | end 146 | 147 | checked = 1 # already checked v 148 | 149 | while checked < restriction_ll.len 150 | current = current.next 151 | 152 | if u == current.data 153 | u = get(parents, u, zero(U)) 154 | else 155 | return false 156 | end 157 | 158 | checked += 1 159 | end 160 | 161 | return true 162 | end 163 | 164 | """ 165 | restriction_cost(restrictions::AbstractDict{V,Vector{MutableLinkedList{V}}}, u::U, v::U, parents::Vector{U})::Float64 where {U <: DEFAULT_OSM_INDEX_TYPE,V <: Integer} 166 | 167 | Given parents, returns `Inf64` if path between `u` and `v` is restricted by the set of restriction linked lists, `0.0` otherwise. 168 | 169 | # Arguments 170 | - `restrictions::AbstractDict{V,Vector{MutableLinkedList{V}}}`: Set of linked lists holding vertices in order of v -> parents. 171 | - `u::U`: Current vertex visiting. 172 | - `v::U`: Current neighbour vertex. 173 | - `parents::Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}`: Mapping of shortest path children to parents. 174 | 175 | # Return 176 | - `Float64`: Returns `Inf64` if path between u and v is restricted, `0.0` otherwise. 177 | """ 178 | function restriction_cost(restrictions::AbstractDict{V,Vector{MutableLinkedList{V}}}, u::U, v::U, parents::P)::Float64 where {P <: Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}} where {U <: DEFAULT_OSM_INDEX_TYPE,V <: Integer} 179 | !haskey(restrictions, u) && return 0.0 180 | 181 | for ll in restrictions[u] 182 | is_restricted(ll, u, v, parents) && return typemax(Float64) 183 | end 184 | 185 | return 0.0 186 | end 187 | 188 | """ 189 | restriction_cost_adjustment(g::OSMGraph) 190 | 191 | Returns the cost adjustment function (user in dijkstra and astar) for restrictions. The return function 192 | takes 3 arguments, `u` being the current node, `v` being the neighbour node, `parents` being the array 193 | of parent dijkstra states. By default `g.indexed_restrictions` is used to check whether the path from 194 | `u` to `v` is restricted given all previous nodes in `parents`. 195 | """ 196 | restriction_cost_adjustment(g::OSMGraph) = (u, v, parents) -> restriction_cost(g.indexed_restrictions, u, v, parents) 197 | 198 | """ 199 | distance_heuristic(g::OSMGraph) 200 | 201 | Returns the heuristic function used in astar shortest path calculation, should be used with a graph with 202 | `weight_type=:distance`. The heuristic function takes in 2 arguments, `u` being the current node and `v` 203 | being the neighbour node, and returns the haversine distance between them. 204 | """ 205 | distance_heuristic(g::OSMGraph) = (u, v) -> haversine(g.node_coordinates[u], g.node_coordinates[v]) 206 | 207 | """ 208 | time_heuristic(g::OSMGraph) 209 | 210 | Returns the heuristic function used in astar shortest path calculation, should be used with a graph with 211 | `weight_type=:time` or `weight_type=:lane_efficiency`. The heuristic function takes in 2 arguments, `u` 212 | being the current node and `v` being the neighbour node, and returns the estimated travel time between them. 213 | Calculated by dividing the harversine distance by a fixed maxspeed of `100`. Remember to achieve an optimal 214 | path, it is important to pick an *underestimating* heuristic that best estimates the cost remaining to the `goal`, 215 | hence we pick the largest maxspeed across all ways. 216 | """ 217 | time_heuristic(g::OSMGraph) = (u, v) -> haversine(g.node_coordinates[u], g.node_coordinates[v]) / 100.0 218 | 219 | """ 220 | 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} 221 | 222 | Extracts edge weights from a path using the weight matrix stored in `g.weights` unless 223 | a different matrix is passed to the `weights` kwarg. 224 | 225 | # Arguments 226 | - `g::OSMGraph`: Graph container. 227 | - `path::Vector{T}`: Array of OpenStreetMap node ids. 228 | - `weights=g.weights`: the matrix that the edge weights are extracted from. Defaults to `g.weights`. 229 | 230 | # Return 231 | - `Vector{W}`: Array of edge weights, distances are in km, time is in hours. 232 | """ 233 | 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} 234 | return [weights[g.node_to_index[path[i]], g.node_to_index[path[i + 1]]] for i in 1:length(path) - 1] 235 | end 236 | 237 | """ 238 | 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} 239 | 240 | Extract total edge weight along a path. 241 | 242 | # Arguments 243 | - `g::OSMGraph`: Graph container. 244 | - `path::Vector{T}`: Array of OpenStreetMap node ids. 245 | - `weights=g.weights`: the matrix that the edge weights are extracted from. Defaults to `g.weights`. 246 | 247 | # Return 248 | - `sum::W`: Total path edge weight, distances are in km, time is in hours. 249 | """ 250 | 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} 251 | sum::W = zero(W) 252 | for i in 1:length(path) - 1 253 | sum += weights[g.node_to_index[path[i]], g.node_to_index[path[i + 1]]] 254 | end 255 | return sum 256 | end 257 | -------------------------------------------------------------------------------- /src/traversal.jl: -------------------------------------------------------------------------------- 1 | """ 2 | astar([::Type{<:AStar},] 3 | g::AbstractGraph{U}, 4 | weights::AbstractMatrix{T}, 5 | src::W, 6 | goal::W; 7 | heuristic::Function=(u, v) -> 0.0, 8 | cost_adjustment::Function=(u, v, parents) -> 0.0, 9 | max_distance::T=typemax(T) 10 | ) where {T <: Real, U <: Integer, W <: Integer} 11 | 12 | A* shortest path algorithm. Implemented with a min heap. Using a min heap is 13 | faster than using a priority queue given the sparse nature of OpenStreetMap 14 | data, i.e. vertices far outnumber edges. 15 | 16 | There are two implementations: 17 | - `AStarVector` is faster for small graphs and/or long paths. This is default. 18 | It pre-allocates vectors at the start of the algorithm to store 19 | distances, parents and visited nodes. This speeds up graph traversal at the 20 | cost of large memory usage. 21 | - `AStarDict` is faster for large graphs and/or short paths. 22 | It dynamically allocates memory during traversal to store distances, 23 | parents and visited nodes. This is faster compared to `AStarVector` when 24 | the graph contains a large number of nodes and/or not much traversal is 25 | required. 26 | 27 | Compared to `jl`, this version improves runtime, memory usage, has a flexible 28 | heuristic function, and accounts for OpenStreetMap turn restrictions through 29 | the `cost_adjustment` function. 30 | 31 | **Note**: A heuristic that does not accurately estimate the remaining cost to 32 | `goal` (i.e. overestimating heuristic) will result in a non-optimal path 33 | (i.e. not the shortest), dijkstra on the other hand guarantees the optimal path 34 | as the heuristic cost is zero. 35 | 36 | # Arguments 37 | - `::Type{<:AStar}`: Implementation to use, either `AStarVector` (default) or 38 | `AStarDict`. 39 | - `g::AbstractGraph{U}`: Graphs abstract graph object. 40 | - `weights::AbstractMatrix{T}`: Edge weights matrix. 41 | - `src::W`: Source vertex. 42 | - `goal::W`: Goal vertex. 43 | - `heuristic::Function=h(u, v) = 0.0`: Heuristic cost function, takes a source 44 | and target vertex, default is 0. 45 | - `cost_adjustment:::Function=r(u, v, parents) = 0.0`: Optional cost adjustment 46 | function for use cases such as turn restrictions, takes a source and target 47 | vertex, defaults to 0. 48 | - `max_distance::T=typemax(T)`: Maximum weight to traverse the graph, returns 49 | `nothing` if this is reached. 50 | 51 | # Return 52 | - `Union{Nothing,Vector{U}}`: Array veritces represeting shortest path from 53 | `src` to `goal`. 54 | """ 55 | function astar(::Type{A}, 56 | g::AbstractGraph{U}, 57 | weights::AbstractMatrix{T}, 58 | src::W, 59 | goal::W; 60 | heuristic::Function=(u, v) -> 0.0, 61 | cost_adjustment::Function=(u, v, parents) -> 0.0, 62 | max_distance::T=typemax(T) 63 | ) where {A <: AStar, T <: Real, U <: Integer, W <: Integer} 64 | # Preallocate 65 | heap = BinaryHeap{Tuple{T, U, U}}(FastMin) # (f = g + h, current, path length) 66 | dists = fill(typemax(T), nv(g)) 67 | parents = zeros(U, nv(g)) 68 | visited = zeros(Bool, nv(g)) 69 | len = zero(U) 70 | 71 | # Initialize src 72 | dists[src] = zero(T) 73 | push!(heap, (zero(T), src, len)) 74 | 75 | while !isempty(heap) 76 | _, u, len = pop!(heap) # (f = g + h, current, path length) 77 | visited[u] && continue 78 | visited[u] = true 79 | len += one(U) 80 | u == goal && break # optimal path to goal found 81 | d = dists[u] 82 | d > max_distance && return # reached max distance 83 | 84 | for v in outneighbors(g, u) 85 | visited[v] && continue 86 | alt = d + weights[u, v] + cost_adjustment(u, v, parents) # turn restriction would imply `Inf` cost adjustment 87 | 88 | if alt < dists[v] 89 | dists[v] = alt 90 | parents[v] = u 91 | push!(heap, (alt + heuristic(v, goal), v, len)) 92 | end 93 | end 94 | end 95 | 96 | return path_from_parents(parents, goal, len) 97 | end 98 | function astar(::Type{AStarDict}, 99 | g::AbstractGraph{U}, 100 | weights::AbstractMatrix{T}, 101 | src::W, 102 | goal::W; 103 | heuristic::Function=(u, v) -> 0.0, 104 | cost_adjustment::Function=(u, v, parents) -> 0.0, 105 | max_distance::T=typemax(T) 106 | ) where {T <: Real, U <: Integer, W <: Integer} 107 | # Preallocate 108 | heap = BinaryHeap{Tuple{T, U, U}}(FastMin) # (f = g + h, current, path length) 109 | dists = Dict{U, T}() 110 | parents = Dict{U, U}() 111 | visited = Set{U}() 112 | len = zero(U) 113 | 114 | # Initialize src 115 | dists[src] = zero(T) 116 | push!(heap, (zero(T), src, len)) 117 | 118 | while !isempty(heap) 119 | _, u, len = pop!(heap) # (f = g + h, current, path length) 120 | u in visited && continue 121 | push!(visited, u) 122 | len += one(U) 123 | u == goal && break # optimal path to goal found 124 | d = get(dists, u, typemax(T)) 125 | d > max_distance && return # reached max distance 126 | 127 | for v in outneighbors(g, u) 128 | v in visited && continue 129 | alt = d + weights[u, v] + cost_adjustment(u, v, parents) # turn restriction would imply `Inf` cost adjustment 130 | 131 | if alt < get(dists, v, typemax(T)) 132 | dists[v] = alt 133 | parents[v] = u 134 | push!(heap, (alt + heuristic(v, goal), v, len)) 135 | end 136 | end 137 | end 138 | 139 | return path_from_parents(parents, goal, len) 140 | end 141 | function astar(g::AbstractGraph{U}, 142 | weights::AbstractMatrix{T}, 143 | src::W, 144 | goal::W; 145 | kwargs... 146 | ) where {T <: Real, U <: Integer, W <: Integer} 147 | return astar(AStarVector, g, weights, src, goal; kwargs...) 148 | end 149 | 150 | """ 151 | dijkstra([::Type{<:Dijkstra},] 152 | g::AbstractGraph{U}, 153 | weights::AbstractMatrix{T}, 154 | src::W, 155 | goal::W; 156 | cost_adjustment::Function=(u, v, parents) -> 0.0, 157 | max_distance::T=typemax(T) 158 | ) where {T <: Real, U <: Integer, W <: Integer} 159 | 160 | Dijkstra's shortest path algorithm with an early exit condition, is the same as 161 | astar with heuristic cost as 0. 162 | 163 | There are two implementations: 164 | - `DijkstraVector` is faster for small graphs and/or long paths. This is default. 165 | It pre-allocates vectors at the start of the algorithm to store 166 | distances, parents and visited nodes. This speeds up graph traversal at the 167 | cost of large memory usage. 168 | - `DijkstraDict` is faster for large graphs and/or short paths. 169 | It dynamically allocates memory during traversal to store distances, 170 | parents and visited nodes. This is faster compared to `AStarVector` when 171 | the graph contains a large number of nodes and/or not much traversal is 172 | required. 173 | 174 | # Arguments 175 | - `::Type{<:Dijkstra}`: Implementation to use, either `DijkstraVector` 176 | (default) or `DijkstraDict`. 177 | - `g::AbstractGraph{U}`: Graphs abstract graph object. 178 | - `weights::AbstractMatrix{T}`: Edge weights matrix. 179 | - `src::W`: Source vertex. 180 | - `goal::W`: Goal vertex. 181 | - `cost_adjustment:::Function=r(u, v, parents) = 0.0`: Optional cost adjustment 182 | function for use cases such as turn restrictions, takes a source and target 183 | vertex, defaults to 0. 184 | - `max_distance::T=typemax(T)`: Maximum weight to traverse the graph, returns 185 | `nothing` if this is reached. 186 | 187 | # Return 188 | - `Union{Nothing,Vector{U}}`: Array veritces represeting shortest path between `src` to `goal`. 189 | """ 190 | function dijkstra(::Type{A}, 191 | g::AbstractGraph{U}, 192 | weights::AbstractMatrix{T}, 193 | src::W, 194 | goal::W; 195 | kwargs... 196 | ) where {A <: Dijkstra, T <: Real, U <: Integer, W <: Integer} 197 | return astar(AStarVector, g, weights, src, goal; kwargs...) 198 | end 199 | function dijkstra(::Type{DijkstraDict}, 200 | g::AbstractGraph{U}, 201 | weights::AbstractMatrix{T}, 202 | src::W, 203 | goal::W; 204 | kwargs... 205 | ) where {T <: Real, U <: Integer, W <: Integer} 206 | return astar(AStarDict, g, weights, src, goal; kwargs...) 207 | end 208 | function dijkstra(g::AbstractGraph{U}, 209 | weights::AbstractMatrix{T}, 210 | src::W, 211 | goal::W; 212 | kwargs... 213 | ) where {T <: Real, U <: Integer, W <: Integer} 214 | return dijkstra(DijkstraVector, g, weights, src, goal; kwargs...) 215 | end 216 | 217 | """ 218 | dijkstra(g::AbstractGraph{U}, 219 | weights::AbstractMatrix{T}, 220 | src::W; 221 | cost_adjustment::Function=(u, v, parents) -> 0.0 222 | ) where {T <: Real, U <: Integer, W <: Integer} 223 | 224 | Dijkstra's shortest path algorithm, implemented with a min heap. Using a min heap is faster than using 225 | a priority queue given the sparse nature of OpenStreetMap data, i.e. vertices far outnumber edges. 226 | 227 | This dispatch returns full set of `parents` or the `dijkstra state` given a source vertex, i.e. without 228 | and early exit condition of `goal`. 229 | 230 | # Arguments 231 | - `g::AbstractGraph{U}`: Graphs abstract graph object. 232 | - `weights::AbstractMatrix{T}`: Edge weights matrix. 233 | - `src::W`: Source vertex. 234 | - `cost_adjustment:::Function=r(u, v, parents) = 0.0`: Optional cost adjustment function for use cases such as turn restrictions, takes a source and target vertex, defaults to 0. 235 | 236 | # Return 237 | - `Vector{U}`: Array parent veritces from which the shortest path can be extracted. 238 | """ 239 | function dijkstra(g::AbstractGraph{U}, 240 | weights::AbstractMatrix{T}, 241 | src::W; 242 | cost_adjustment::Function=(u, v, parents) -> 0.0 243 | ) where {T <: Real, U <: Integer, W <: Integer} 244 | # Preallocate 245 | heap = BinaryHeap{Tuple{T, U}}(FastMin) # (weight, current) 246 | dists = fill(typemax(T), nv(g)) 247 | parents = zeros(U, nv(g)) 248 | visited = zeros(Bool, nv(g)) 249 | 250 | # Initialize src 251 | push!(heap, (zero(T), src)) 252 | dists[src] = zero(T) 253 | 254 | while !isempty(heap) 255 | _, u = pop!(heap) # (weight, current) 256 | visited[u] && continue 257 | visited[u] = true 258 | d = dists[u] 259 | 260 | for v in outneighbors(g, u) 261 | visited[v] && continue 262 | alt = d + weights[u, v] + cost_adjustment(u, v, parents) # turn restriction would imply `Inf` cost adjustment 263 | 264 | if alt < dists[v] 265 | dists[v] = alt 266 | parents[v] = u 267 | push!(heap, (alt, v)) 268 | end 269 | end 270 | end 271 | 272 | return parents 273 | end 274 | 275 | """ 276 | path_from_parents(parents::P, goal::V) where {P <: Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}} where {U <: Integer, V <: Integer} 277 | 278 | Extracts shortest path given dijkstra parents of a given source. 279 | 280 | # Arguments 281 | - `parents::Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}`: Mapping of 282 | dijkstra parent states. 283 | - `goal::V`: Goal vertex. 284 | 285 | # Return 286 | - `Union{Nothing,Vector{U}}`: Array veritces represeting shortest path to `goal`. 287 | """ 288 | function path_from_parents(parents::P, goal::V) where {P <: Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}} where {U <: Integer, V <: Integer} 289 | parents[goal] == 0 && return 290 | 291 | pointer = goal 292 | path = U[] 293 | 294 | while pointer != 0 # parent of origin is always 0 295 | push!(path, pointer) 296 | pointer = parents[pointer] 297 | end 298 | 299 | return reverse(path) 300 | end 301 | 302 | """ 303 | path_from_parents(parents::P, goal::V, path_length::N) where {P <: Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}} where {U <: Integer, V <: Integer, N <: Integer} 304 | 305 | Extracts shortest path given dijkstra parents of a given source, providing `path_length` allows 306 | preallocation of the array and avoids the need to reverse the path. 307 | 308 | # Arguments 309 | - `parents::Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}`: Mapping of dijkstra parent states. 310 | - `goal::V`: Goal vertex. 311 | - `path_kength::N`: Known length of the return path, allows preallocation of final path array. 312 | 313 | # Return 314 | - `Union{Nothing,Vector{U}}`: Array veritces represeting shortest path to `goal`. 315 | """ 316 | function path_from_parents(parents::P, goal::V, path_length::N) where {P <: Union{<:AbstractVector{<:U}, <:AbstractDict{<:U, <:U}}} where {U <: Integer, V <: Integer, N <: Integer} 317 | get(parents, goal, zero(U)) == 0 && return 318 | 319 | pointer = goal 320 | path = Vector{U}(undef, path_length) 321 | 322 | for i in one(U):(path_length - 1) 323 | path[path_length - i + one(U)] = pointer 324 | pointer = parents[pointer] 325 | end 326 | path[1] = pointer 327 | 328 | return path 329 | end 330 | -------------------------------------------------------------------------------- /src/parse.jl: -------------------------------------------------------------------------------- 1 | """ 2 | Determine road maxspeeds given osm way tags dictionary. 3 | """ 4 | function maxspeed(tags::AbstractDict)::DEFAULT_OSM_MAXSPEED_TYPE 5 | maxspeed = get(tags, "maxspeed", "default") 6 | U = DEFAULT_OSM_MAXSPEED_TYPE 7 | 8 | if maxspeed != "default" 9 | if maxspeed isa Integer 10 | return maxspeed 11 | elseif maxspeed isa AbstractFloat 12 | return U(round(maxspeed)) 13 | elseif maxspeed isa String 14 | if occursin("conditional", maxspeed) 15 | maxspeed = remove_sub_string_after(maxspeed, "conditional") 16 | end 17 | 18 | maxspeed = split(maxspeed, COMMON_OSM_STRING_DELIMITERS) 19 | 20 | cleaned_maxspeeds = [] 21 | for speed in maxspeed 22 | speed = occursin("mph", speed) ? remove_non_numeric(speed) * KMH_PER_MPH : remove_non_numeric(speed) 23 | push!(cleaned_maxspeeds, speed) 24 | end 25 | 26 | return U(round(mean(cleaned_maxspeeds))) 27 | else 28 | throw(ErrorException("Maxspeed is neither a string nor number, check data quality: $maxspeed")) 29 | end 30 | else 31 | highway_type = get(tags, "highway", "other") 32 | key = getkey(DEFAULT_MAXSPEEDS[], highway_type, "other") 33 | return U(DEFAULT_MAXSPEEDS[][key]) 34 | end 35 | end 36 | 37 | """ 38 | Determine number of lanes given osm way tags dictionary. 39 | """ 40 | function lanes(tags::AbstractDict)::DEFAULT_OSM_LANES_TYPE 41 | lanes = get(tags, "lanes", "default") 42 | U = DEFAULT_OSM_LANES_TYPE 43 | 44 | if lanes != "default" 45 | if lanes isa Integer 46 | return lanes 47 | elseif lanes isa AbstractFloat 48 | return U(round(lanes)) 49 | elseif lanes isa String 50 | lanes = split(lanes, COMMON_OSM_STRING_DELIMITERS) 51 | lanes = [remove_non_numeric(l) for l in lanes] 52 | return U(round(mean(lanes))) 53 | else 54 | throw(ErrorException("Lanes is neither a string nor number, check data quality: $lanes")) 55 | end 56 | else 57 | highway_type = get(tags, "highway", "other") 58 | key = getkey(DEFAULT_LANES[], highway_type, "other") 59 | return U(DEFAULT_LANES[][key]) 60 | end 61 | end 62 | 63 | 64 | """ 65 | Determind if way is a roundabout given osm way tags dictionary. 66 | """ 67 | is_roundabout(tags::AbstractDict)::Bool = get(tags, "junction", "") == "roundabout" ? true : false 68 | 69 | """ 70 | Determine oneway road attribute given osm way tags dictionary. 71 | """ 72 | function is_oneway(tags::AbstractDict)::Bool 73 | oneway = get(tags, "oneway", "") 74 | 75 | if oneway in ONEWAY_FALSE 76 | return false 77 | elseif oneway in ONEWAY_TRUE 78 | return true 79 | elseif is_roundabout(tags) 80 | return true 81 | else 82 | highway_type = get(tags, "highway", "other") 83 | key = getkey(DEFAULT_ONEWAY, highway_type, "other") 84 | return DEFAULT_ONEWAY[key] 85 | end 86 | end 87 | 88 | """ 89 | Determine reverseway road attribute given osm way tags dictionary. 90 | """ 91 | is_reverseway(tags::AbstractDict)::Bool = get(tags, "oneway", "") in Set(["-1", -1]) ? true : false 92 | 93 | """ 94 | Determine if way is of highway type given osm way tags dictionary. 95 | """ 96 | is_highway(tags::AbstractDict)::Bool = haskey(tags, "highway") ? true : false 97 | 98 | """ 99 | Determine if way is of railway type given osm way tags dictionary. 100 | """ 101 | is_railway(tags::AbstractDict)::Bool = haskey(tags, "railway") ? true : false 102 | 103 | """ 104 | Determine if way matches the specified network type. 105 | """ 106 | function matches_network_type(tags::AbstractDict, network_type::Symbol)::Bool 107 | for (k, v) in WAY_EXCLUSION_FILTERS[network_type] 108 | if haskey(tags, k) 109 | if tags[k] in Set(v) 110 | return false 111 | end 112 | end 113 | end 114 | return true 115 | end 116 | 117 | """ 118 | Determine if relation is a restriction. 119 | """ 120 | is_restriction(tags::AbstractDict)::Bool = get(tags, "type", "") == "restriction" && haskey(tags, "restriction") ? true : false 121 | 122 | """ 123 | Determine if a restriction is valid and has usable data. 124 | """ 125 | function is_valid_restriction(members::AbstractArray, ways::AbstractDict{T,Way{T}})::Bool where T <: DEFAULT_OSM_ID_TYPE 126 | role_counts = DefaultDict(0) 127 | role_type_counts = DefaultDict(0) 128 | ways_set = Set{Int}() 129 | ways_mapping = DefaultDict(Vector) 130 | via_node = nothing 131 | 132 | for member in members 133 | id = member["ref"] 134 | type = member["type"] 135 | role = member["role"] 136 | 137 | if type == "way" 138 | if !haskey(ways, id) || id in ways_set 139 | # Cannot process missing and duplicate from/via/to ways 140 | return false 141 | else 142 | push!(ways_set, id) 143 | push!(ways_mapping[role], id) 144 | end 145 | 146 | elseif type == "node" 147 | via_node = id 148 | end 149 | 150 | role_counts[role] += 1 151 | role_type_counts["$(role)_$(type)"] += 1 152 | end 153 | 154 | if !(role_counts["from"] == 1) || 155 | !(role_counts["to"] == 1) || 156 | !( 157 | (role_type_counts["via_node"] == 1 && role_type_counts["via_way"] < 1) || 158 | (role_type_counts["via_node"] < 1 && role_type_counts["via_way"] >= 1) 159 | ) 160 | # Restrictions with multiple "from" and "to" members cannot be processed 161 | # Restrictions with multiple "via" "node" members cannot be processed 162 | # Restrictions with combination of "via" "node" and "via" "way" members cannot be processed 163 | return false 164 | end 165 | 166 | to_way = ways_mapping["to"][1] 167 | trailing_to_way_nodes = trailing_elements(ways[to_way].nodes) 168 | from_way = ways_mapping["from"][1] 169 | trailing_from_way_nodes = trailing_elements(ways[from_way].nodes) 170 | 171 | if via_node isa Integer && (!(via_node in trailing_to_way_nodes) || !(via_node in trailing_from_way_nodes)) 172 | # Via node must be a trailing node on to_way and from_way 173 | return false 174 | end 175 | 176 | if haskey(ways_mapping, "via") 177 | try 178 | # Via way trailing nodes must intersect with all to_ways and from_ways 179 | via_ways = ways_mapping["via"] 180 | via_way_nodes_list = [ways[w].nodes for w in via_ways] 181 | via_way_nodes = join_arrays_on_common_trailing_elements(via_way_nodes_list...) 182 | trailing_via_way_nodes = trailing_elements(via_way_nodes) 183 | 184 | if isempty(intersect(trailing_via_way_nodes, trailing_to_way_nodes)) || 185 | isempty(intersect(trailing_via_way_nodes, trailing_from_way_nodes)) 186 | return false 187 | end 188 | catch 189 | # Restriction cannot be process - via ways cannot be joined on common trailing nodes 190 | return false 191 | end 192 | end 193 | 194 | return true 195 | end 196 | 197 | """ 198 | Parse OpenStreetMap data into `Node`, `Way` and `Restriction` objects. 199 | """ 200 | function parse_osm_network_dict(osm_network_dict::AbstractDict, 201 | network_type::Symbol=:drive; 202 | filter_network_type::Bool=true 203 | )::OSMGraph 204 | 205 | U = DEFAULT_OSM_INDEX_TYPE 206 | T = get_id_type(osm_network_dict) 207 | W = DEFAULT_OSM_EDGE_WEIGHT_TYPE 208 | L = DEFAULT_OSM_LANES_TYPE 209 | 210 | ways = Dict{T,Way{T}}() 211 | highway_nodes = Set{T}([]) 212 | for way in osm_network_dict["way"] 213 | if haskey(way, "tags") && haskey(way, "nodes") 214 | tags = way["tags"] 215 | if is_highway(tags) && (!filter_network_type || matches_network_type(tags, network_type)) 216 | tags["maxspeed"] = maxspeed(tags) 217 | tags["lanes"] = lanes(tags) 218 | tags["oneway"] = is_oneway(tags) 219 | tags["reverseway"] = is_reverseway(tags) 220 | nds = way["nodes"] 221 | union!(highway_nodes, nds) 222 | id = way["id"] 223 | ways[id] = Way(id, nds, tags) 224 | elseif is_railway(tags) && (!filter_network_type || matches_network_type(tags, network_type)) 225 | tags["rail_type"] = get(tags, "railway", "unknown") 226 | tags["electrified"] = get(tags, "electrified", "unknown") 227 | tags["gauge"] = get(tags, "gauge", nothing) 228 | tags["usage"] = get(tags, "usage", "unknown") 229 | tags["name"] = get(tags, "name", "unknown") 230 | tags["lanes"] = lanes(tags) 231 | tags["maxspeed"] = maxspeed(tags) 232 | tags["oneway"] = is_oneway(tags) 233 | tags["reverseway"] = is_reverseway(tags) 234 | nds = way["nodes"] 235 | union!(highway_nodes, nds) 236 | id = way["id"] 237 | ways[id] = Way(id, nds, tags) 238 | end 239 | end 240 | end 241 | 242 | nodes = Dict{T,Node{T}}() 243 | for node in osm_network_dict["node"] 244 | id = node["id"] 245 | if id in highway_nodes 246 | nodes[id] = Node{T}( 247 | id, 248 | GeoLocation(node["lat"], node["lon"]), 249 | haskey(node, "tags") ? node["tags"] : Dict{String,Any}() 250 | ) 251 | end 252 | end 253 | 254 | restrictions = Dict{T,Restriction{T}}() 255 | if haskey(osm_network_dict, "relation") 256 | for relation in osm_network_dict["relation"] 257 | if haskey(relation, "tags") && haskey(relation, "members") 258 | tags = relation["tags"] 259 | members = relation["members"] 260 | 261 | if is_restriction(tags) && is_valid_restriction(members, ways) 262 | restriction_kwargs = DefaultDict(Vector) 263 | for member in members 264 | key = "$(member["role"])_$(member["type"])" 265 | if key == "via_way" 266 | push!(restriction_kwargs[Symbol(key)], member["ref"]) 267 | else 268 | restriction_kwargs[Symbol(key)] = member["ref"] 269 | end 270 | end 271 | 272 | id = relation["id"] 273 | restrictions[id] = Restriction{T}( 274 | id=id, 275 | tags=tags, 276 | type=haskey(restriction_kwargs, :via_way) ? "via_way" : "via_node", 277 | is_exclusion=occursin("no", tags["restriction"]) ? true : false, 278 | is_exclusive=occursin("only", tags["restriction"]) ? true : false, 279 | ;restriction_kwargs... 280 | ) 281 | end 282 | end 283 | end 284 | end 285 | return OSMGraph{U,T,W}(nodes=nodes, ways=ways, restrictions=restrictions) 286 | end 287 | 288 | """ 289 | Parse OpenStreetMap data downloaded in `:xml` or `:osm` format into a dictionary consistent with data downloaded in `:json` format. 290 | """ 291 | function parse_xml_dict_to_json_dict(dict::AbstractDict)::AbstractDict 292 | # This function is needed so dict parsed from xml is consistent with dict parsed from json 293 | for (type, elements) in dict 294 | for (i, el) in enumerate(elements) 295 | !(el isa AbstractDict) && continue 296 | sub_dict::Dict{String, Any} = el 297 | if haskey(sub_dict, "tag") 298 | dict[type][i]["tags"] = Dict{String,Any}(tag["k"] => tag["v"] for tag in sub_dict["tag"] if tag["k"] isa String) 299 | delete!(dict[type][i], "tag") 300 | end 301 | 302 | if haskey(sub_dict, "member") 303 | dict[type][i]["members"] = pop!(dict[type][i], "member") # rename 304 | end 305 | 306 | if haskey(sub_dict, "nd") 307 | dict[type][i]["nodes"] = [nd["ref"] for nd in sub_dict["nd"]] 308 | delete!(dict[type][i], "nd") 309 | end 310 | end 311 | end 312 | 313 | return dict 314 | end 315 | 316 | """ 317 | Parse OpenStreetMap data downloaded in `:xml` or `:osm` format into a dictionary. 318 | """ 319 | function osm_dict_from_xml(osm_xml_object::XMLDocument)::AbstractDict 320 | root_node = root(osm_xml_object) 321 | dict = xml_to_dict(root_node, OSM_METADATA) 322 | return parse_xml_dict_to_json_dict(dict) 323 | end 324 | 325 | """ 326 | Reorder OpenStreetMap data downloaded in `:json` format so items are groups by type. 327 | """ 328 | function osm_dict_from_json(osm_json_object::AbstractDict)::AbstractDict 329 | dict = DefaultDict(Vector) 330 | 331 | for el in osm_json_object["elements"] 332 | push!(dict[el["type"]], el) 333 | end 334 | 335 | return dict 336 | end 337 | 338 | """ 339 | Initialises the OSMGraph object from OpenStreetMap data downloaded in `:xml` or `:osm` format. 340 | """ 341 | function init_graph_from_object(osm_xml_object::XMLDocument, 342 | network_type::Symbol=:drive; 343 | filter_network_type::Bool=true 344 | )::OSMGraph 345 | dict_to_parse = osm_dict_from_xml(osm_xml_object) 346 | return parse_osm_network_dict( 347 | dict_to_parse, 348 | network_type; 349 | filter_network_type=filter_network_type 350 | ) 351 | end 352 | 353 | """ 354 | Initialises the OSMGraph object from OpenStreetMap data downloaded in `:json` format. 355 | """ 356 | function init_graph_from_object(osm_json_object::AbstractDict, 357 | network_type::Symbol=:drive; 358 | filter_network_type::Bool=true 359 | )::OSMGraph 360 | dict_to_parse = osm_dict_from_json(osm_json_object) 361 | return parse_osm_network_dict( 362 | dict_to_parse, 363 | network_type; 364 | filter_network_type=filter_network_type 365 | ) 366 | end 367 | 368 | 369 | """ 370 | get_id_type(osm_network_dict::AbstractDict)::Type 371 | 372 | Finds the node id type of an osm dict. 373 | """ 374 | function get_id_type(osm_network_dict::AbstractDict)::Type 375 | if isempty(osm_network_dict["node"]) 376 | return Int64 377 | end 378 | 379 | first_id = osm_network_dict["node"][1]["id"] 380 | 381 | if first_id isa Integer 382 | return Int64 383 | elseif first_id isa String 384 | return String 385 | else 386 | throw(ErrorException("OSM ID type not supported: $(typeof(first_id))")) 387 | end 388 | end -------------------------------------------------------------------------------- /src/buildings.jl: -------------------------------------------------------------------------------- 1 | function overpass_polygon_buildings_query(geojson_polygons::Vector{Vector{Any}}, 2 | metadata::Bool=false, 3 | download_format::Symbol=:osm 4 | )::String 5 | filters = "" 6 | for polygon in geojson_polygons 7 | polygon = map(x -> [x[2], x[1]], polygon) # switch lon-lat to lat-lon 8 | polygon_str = replace("$polygon", r"[\[,\]]" => "") 9 | filters *= """node["building"](poly:"$polygon_str");<;way["building"](poly:"$polygon_str");>;rel["building"](poly:"$polygon_str");>;""" 10 | end 11 | 12 | return overpass_query(filters, metadata, download_format) 13 | end 14 | 15 | """ 16 | osm_buildings_from_place_name(;place_name::String, 17 | metadata::Bool=false, 18 | download_format::Symbol=:osm 19 | )::String 20 | 21 | Downloads OpenStreetMap buildings using any place name string. 22 | 23 | # Arguments 24 | - `place_name::String`: Any place name string used as a search argument to the Nominatim API. 25 | - `metadata::Bool=false`: Set true to return metadata. 26 | - `download_format::Symbol=:osm`: Download format, either `:osm`, `:xml` or `json`. 27 | 28 | # Return 29 | - `String`: OpenStreetMap buildings data response string. 30 | """ 31 | function osm_buildings_from_place_name(;place_name::String, 32 | metadata::Bool=false, 33 | download_format::Symbol=:osm 34 | )::String 35 | geojson_polygons = polygon_from_place_name(place_name) 36 | query = overpass_polygon_buildings_query(geojson_polygons, metadata, download_format) 37 | return overpass_request(query) 38 | end 39 | 40 | """ 41 | osm_buildings_from_bbox(;minlat::AbstractFloat, 42 | minlon::AbstractFloat, 43 | maxlat::AbstractFloat, 44 | maxlon::AbstractFloat, 45 | metadata::Bool=false, 46 | download_format::Symbol=:osm 47 | )::String 48 | 49 | Downloads OpenStreetMap buildings using bounding box coordinates. 50 | 51 | # Arguments 52 | - `minlat::AbstractFloat`: Bottom left bounding box latitude coordinate. 53 | - `minlon::AbstractFloat`: Bottom left bounding box longitude coordinate. 54 | - `maxlat::AbstractFloat`: Top right bounding box latitude coordinate. 55 | - `maxlon::AbstractFloat`: Top right bounding box longitude coordinate. 56 | - `metadata::Bool=false`: Set true to return metadata. 57 | - `download_format::Symbol=:osm`: Download format, either `:osm`, `:xml` or `json`. 58 | 59 | # Return 60 | - `String`: OpenStreetMap buildings data response string. 61 | """ 62 | function osm_buildings_from_bbox(;minlat::Float64, 63 | minlon::Float64, 64 | maxlat::Float64, 65 | maxlon::Float64, 66 | metadata::Bool=false, 67 | download_format::Symbol=:osm 68 | )::String 69 | filters = """node["building"];<;way["building"];>;rel["building"];>;""" 70 | bbox = [minlat, minlon, maxlat, maxlon] 71 | query = overpass_query(filters, metadata, download_format, bbox) 72 | return overpass_request(query) 73 | end 74 | 75 | """ 76 | osm_buildings_from_point(;point::GeoLocation, 77 | radius::Number, 78 | metadata::Bool=false, 79 | download_format::Symbol=:osm 80 | )::String 81 | 82 | Downloads OpenStreetMap buildings using bounding box coordinates calculated from a centroid point and radius (km). 83 | 84 | # Arguments 85 | - `point::GeoLocation`: Centroid point to draw the bounding box around. 86 | - `radius::Number`: Distance (km) from centroid point to each bounding box corner. 87 | - `metadata::Bool=false`: Set true to return metadata. 88 | - `download_format::Symbol=:osm`: Download format, either `:osm`, `:xml` or `json`. 89 | 90 | # Return 91 | - `String`: OpenStreetMap buildings data response string. 92 | """ 93 | function osm_buildings_from_point(;point::GeoLocation, 94 | radius::Number, 95 | metadata::Bool=false, 96 | download_format::Symbol=:osm 97 | )::String 98 | bbox = bounding_box_from_point(point, radius) 99 | return osm_buildings_from_bbox(;bbox..., metadata=metadata, download_format=download_format) 100 | end 101 | 102 | function osm_buildings_downloader(download_method::Symbol)::Function 103 | if download_method == :place_name 104 | return osm_buildings_from_place_name 105 | elseif download_method == :bbox 106 | return osm_buildings_from_bbox 107 | elseif download_method == :point 108 | return osm_buildings_from_point 109 | else 110 | throw(ErrorException("OSM buildings downloader $download_method does not exist")) 111 | end 112 | end 113 | 114 | """ 115 | download_osm_buildings(download_method::Symbol; 116 | metadata::Bool=false, 117 | download_format::Symbol=:osm, 118 | save_to_file_location::Union{String,Nothing}=nothing, 119 | download_kwargs... 120 | )::Union{XMLDocument,Dict{String,Any}} 121 | 122 | Downloads OpenStreetMap buildings data by querying with a place name, bounding box, or centroid point. 123 | 124 | # Arguments 125 | - `download_method::Symbol`: Download method, choose from `:place_name`, `:bbox` or `:point`. 126 | - `metadata::Bool=false`: Set true to return metadata. 127 | - `download_format::Symbol=:osm`: Download format, either `:osm`, `:xml` or `json`. 128 | - `save_to_file_location::Union{String,Nothing}=nothing`: Specify a file location to save downloaded data to disk. 129 | 130 | # Required Download Kwargs 131 | 132 | *`download_method=:place_name`* 133 | - `place_name::String`: Any place name string used as a search argument to the Nominatim API. 134 | 135 | *`download_method=:bbox`* 136 | - `minlat::AbstractFloat`: Bottom left bounding box latitude coordinate. 137 | - `minlon::AbstractFloat`: Bottom left bounding box longitude coordinate. 138 | - `maxlat::AbstractFloat`: Top right bounding box latitude coordinate. 139 | - `maxlon::AbstractFloat`: Top right bounding box longitude coordinate. 140 | 141 | *`download_method=:point`* 142 | - `point::GeoLocation`: Centroid point to draw the bounding box around. 143 | - `radius::Number`: Distance (km) from centroid point to each bounding box corner. 144 | 145 | # Return 146 | - `Union{XMLDocument,Dict{String,Any}}`: OpenStreetMap buildings data parsed as either XML or Dictionary object depending on the download method. 147 | """ 148 | function download_osm_buildings(download_method::Symbol; 149 | metadata::Bool=false, 150 | download_format::Symbol=:osm, 151 | save_to_file_location::Union{String,Nothing}=nothing, 152 | download_kwargs... 153 | )::Union{XMLDocument,Dict{String,Any}} 154 | downloader = osm_buildings_downloader(download_method) 155 | data = downloader(metadata=metadata, download_format=download_format; download_kwargs...) 156 | @info "Downloaded osm buildings data from $(["$k: $v" for (k, v) in download_kwargs]) in $download_format format" 157 | 158 | if !(save_to_file_location isa Nothing) 159 | save_to_file_location = validate_save_location(save_to_file_location, download_format) 160 | write(save_to_file_location, data) 161 | @info "Saved osm buildings data to disk: $save_to_file_location" 162 | end 163 | 164 | deserializer = string_deserializer(download_format) 165 | 166 | return deserializer(data) 167 | end 168 | 169 | is_building(tags::Dict)::Bool = haskey(tags, "building") ? true : false 170 | 171 | function height(tags::Dict)::Number 172 | height = get(tags, "height", nothing) 173 | levels = get(tags, "building:levels", nothing) !== nothing ? tags["building:levels"] : get(tags, "level", nothing) 174 | 175 | if height !== nothing 176 | return height isa String ? max([remove_non_numeric(h) for h in split(height, r"[+^;,-]")]...) : height 177 | elseif levels !== nothing 178 | levels = levels isa String ? round(max([remove_non_numeric(l) for l in split(levels, r"[+^;,-]")]...)) : levels 179 | levels = levels == 0 ? rand(1:DEFAULT_MAX_BUILDING_LEVELS[]) : levels 180 | else 181 | levels = rand(1:DEFAULT_MAX_BUILDING_LEVELS[]) 182 | end 183 | 184 | return levels * DEFAULT_BUILDING_HEIGHT_PER_LEVEL[] 185 | end 186 | 187 | function parse_osm_buildings_dict(osm_buildings_dict::AbstractDict)::Dict{Integer,Building} 188 | T = DEFAULT_OSM_ID_TYPE 189 | 190 | nodes = Dict{T,Node{T}}() 191 | for node in osm_buildings_dict["node"] 192 | id = node["id"] 193 | nodes[id] = Node{T}( 194 | id, 195 | GeoLocation(node["lat"], node["lon"]), 196 | haskey(node, "tags") ? node["tags"] : nothing 197 | ) 198 | end 199 | 200 | ways = Dict(way["id"] => way for way in osm_buildings_dict["way"]) # for lookup 201 | 202 | added_ways = Set{T}() 203 | buildings = Dict{T,Building{T}}() 204 | for relation in osm_buildings_dict["relation"] 205 | is_relation = true 206 | 207 | if haskey(relation, "tags") && is_building(relation["tags"]) 208 | tags = relation["tags"] 209 | rel_id = relation["id"] 210 | members = relation["members"] 211 | 212 | polygons = Vector{Polygon{T}}() 213 | for member in members 214 | member["type"] != "way" && continue 215 | way_id = member["ref"] 216 | way = ways[way_id] 217 | 218 | haskey(way, "tags") && merge!(tags, way["tags"]) # could potentially overwrite some data 219 | push!(added_ways, way_id) 220 | 221 | is_outer = member["role"] == "outer" ? true : false 222 | nds = [nodes[n] for n in way["nodes"]] 223 | push!(polygons, Polygon(way_id, nds, is_outer)) 224 | end 225 | 226 | tags["height"] = height(tags) 227 | sort!(polygons, by = x -> x.is_outer, rev=true) # sorting so outer polygon is always first 228 | buildings[rel_id] = Building{T}(rel_id, is_relation, polygons, tags) 229 | end 230 | end 231 | 232 | for (way_id, way) in ways 233 | is_relation = false 234 | is_outer = true 235 | 236 | if haskey(way, "tags") && is_building(way["tags"]) && !(way_id in added_ways) 237 | tags = way["tags"] 238 | tags["height"] = height(tags) 239 | nds = [nodes[n] for n in way["nodes"]] 240 | polygons = [Polygon(way_id, nds, is_outer)] 241 | buildings[way_id] = Building{T}(way_id, is_relation, polygons, tags) 242 | end 243 | end 244 | 245 | return buildings 246 | end 247 | 248 | """ 249 | buildings_from_object(buildings_xml_object::XMLDocument)::Dict{Integer,Building} 250 | 251 | Creates `Building` objects from data downloaded with `download_osm_buildings`. 252 | 253 | # Arguments 254 | - `buildings_xml_object::XMLDocument`: Buildings data downloaded in `:xml` or `:osm` format. 255 | 256 | # Return 257 | - `Dict{Integer,Building}`: Mapping from building relation/way ids to `Building` objects. 258 | """ 259 | function buildings_from_object(buildings_xml_object::XMLDocument)::Dict{Integer,Building} 260 | dict_to_parse = osm_dict_from_xml(buildings_xml_object) 261 | return parse_osm_buildings_dict(dict_to_parse) 262 | end 263 | 264 | """ 265 | buildings_from_object(buildings_json_object::AbstractDict)::Dict{Integer,Building} 266 | 267 | Creates `Building` objects from data downloaded with `download_osm_buildings`. 268 | 269 | # Arguments 270 | - `buildings_json_object::AbstractDict`: Buildings data downloaded in `:json` format. 271 | 272 | # Return 273 | - `Dict{Integer,Building}`: Mapping from building relation/way ids to `Building` objects. 274 | """ 275 | function buildings_from_object(buildings_json_object::AbstractDict)::Dict{Integer,Building} 276 | dict_to_parse = osm_dict_from_json(buildings_json_object) 277 | return parse_osm_buildings_dict(dict_to_parse) 278 | end 279 | 280 | """ 281 | buildings_from_file(file_path::String)::Dict{Integer,Building} 282 | 283 | Creates `Building` objects from OpenStreetMap data file (either `:osm`, `:xml` or `:json` format). 284 | 285 | # Arguments 286 | - `file_path::String`: Path to OpenStreetMap data file. 287 | 288 | # Return 289 | - `Dict{Integer,Building}`: Mapping from building relation/way ids to `Building` objects. 290 | """ 291 | function buildings_from_file(file_path::String)::Dict{Integer,Building} 292 | !isfile(file_path) && throw(ArgumentError("File $file_path does not exist")) 293 | deserializer = file_deserializer(file_path) 294 | obj = deserializer(file_path) 295 | return buildings_from_object(obj) 296 | end 297 | 298 | """ 299 | buildings_from_download(download_method::Symbol; 300 | metadata::Bool=false, 301 | download_format::Symbol=:osm, 302 | save_to_file_location::Union{String,Nothing}=nothing, 303 | download_kwargs... 304 | )::Dict{Integer,Building} 305 | 306 | Downloads and Creates `Building` objects from OpenStreetMap APIs. 307 | 308 | # Arguments 309 | - `download_method::Symbol`: Download method, choose from `:place_name`, `:bbox` or `:point`. 310 | - `metadata::Bool=false`: Set true to return metadata. 311 | - `download_format::Symbol=:osm`: Download format, either `:osm`, `:xml` or `json`. 312 | - `save_to_file_location::Union{String,Nothing}=nothing`: Specify a file location to save downloaded data to disk. 313 | 314 | # Required Download Kwargs 315 | 316 | *`download_method=:place_name`* 317 | - `place_name::String`: Any place name string used as a search argument to the Nominatim API. 318 | 319 | *`download_method=:bbox`* 320 | - `minlat::AbstractFloat`: Bottom left bounding box latitude coordinate. 321 | - `minlon::AbstractFloat`: Bottom left bounding box longitude coordinate. 322 | - `maxlat::AbstractFloat`: Top right bounding box latitude coordinate. 323 | - `maxlon::AbstractFloat`: Top right bounding box longitude coordinate. 324 | 325 | *`download_method=:point`* 326 | - `point::GeoLocation`: Centroid point to draw the bounding box around. 327 | - `radius::Number`: Distance (km) from centroid point to each bounding box corner. 328 | 329 | # Return 330 | - `Dict{Integer,Building}`: Mapping from building relation/way ids to `Building` objects. 331 | """ 332 | function buildings_from_download(download_method::Symbol; 333 | metadata::Bool=false, 334 | download_format::Symbol=:osm, 335 | save_to_file_location::Union{String,Nothing}=nothing, 336 | download_kwargs... 337 | )::Dict{Integer,Building} 338 | obj = download_osm_buildings(download_method, 339 | metadata=metadata, 340 | download_format=download_format, 341 | save_to_file_location=save_to_file_location; 342 | download_kwargs...) 343 | return buildings_from_object(obj) 344 | end 345 | -------------------------------------------------------------------------------- /src/download.jl: -------------------------------------------------------------------------------- 1 | """ 2 | is_overpass_server_availabile() 3 | 4 | Returns `true` if server is available, `false` if not. 5 | """ 6 | function is_overpass_server_availabile() 7 | response = String(HTTP.get(OSM_URLS[:overpass_status]).body) 8 | return occursin("slots available now", response) 9 | end 10 | 11 | """ 12 | Checks the availability of Overpass servers by sending a request to https://overpass-api.de/api/status. 13 | """ 14 | function check_overpass_server_availability() 15 | if is_overpass_server_availabile() 16 | @info "Overpass server is available for download" 17 | else 18 | throw(ErrorException("Overpass server is NOT available for download, please wait and try again")) 19 | end 20 | end 21 | 22 | """ 23 | nominatim_request(query::Dict)::String 24 | 25 | Sends a GET request to the Nomatim API: https://nominatim.openstreetmap.org/search. 26 | 27 | # Arguments 28 | - `query::Dict`: HTTP GET request query aguments. 29 | 30 | # Return 31 | - `String`: Response body as string. 32 | """ 33 | function nominatim_request(query::Dict)::String 34 | return String(HTTP.get(OSM_URLS[:nominatim_search], query=query).body) 35 | end 36 | 37 | """ 38 | nominatim_request(data::Dict)::String 39 | 40 | Sends a POST request to the Overpass API: http://overpass-api.de/api/interpreter. 41 | 42 | # Arguments 43 | - `data::String`: HTTP POST data. 44 | 45 | # Return 46 | - `String`: Response body as string. 47 | """ 48 | function overpass_request(data::String)::String 49 | check_overpass_server_availability() 50 | return String(HTTP.post(OSM_URLS[:overpass_map], body=data).body) 51 | end 52 | 53 | """ 54 | nominatim_polygon_query(place_name::String)::Dict{String,Any} 55 | 56 | Forms query arguments required for a Nomatim search at https://nominatim.openstreetmap.org/search. 57 | 58 | # Arguments 59 | - `place_name::String`: Place name string used in request to the nominatim api. 60 | 61 | # Return 62 | - `Dict{String,Any}`: GET request query arguments. 63 | """ 64 | function nominatim_polygon_query(place_name::String)::Dict{String,Any} 65 | return Dict( 66 | "format" => "json", 67 | "limit" => 5, 68 | "dedupe" => 0, 69 | "polygon_geojson" => 1, 70 | "q" => place_name 71 | ) 72 | end 73 | 74 | """ 75 | overpass_query(filters::String, 76 | metadata::Bool=false, 77 | download_format::Symbol=:json, 78 | bbox::Union{Vector{AbstractFloat},Nothing}=nothing 79 | )::String 80 | 81 | Forms an Overpass query string. For a guide on the OSM query language, see https://wiki.openstreetmap.org/wiki/Overpass_API/Language_Guide. 82 | 83 | # Arguments 84 | - `filters::String`: Filters for the query, e.g. polygon filter, highways only, traffic lights only, etc. 85 | - `metadata::Bool=false`: Set true to return metadata. 86 | - `download_format::Symbol=:json`: Download format, either `:osm`, `:xml` or `json`. 87 | - `bbox::Union{Vector{AbstractFloat},Nothing}=nothing`: Optional bounding box filter. 88 | 89 | # Return 90 | - `String`: Overpass query string. 91 | """ 92 | function overpass_query(filters::String, 93 | metadata::Bool=false, 94 | download_format::Symbol=:json, 95 | bbox::Union{Vector{<:AbstractFloat},Nothing}=nothing 96 | )::String 97 | download_format_str = """[out:$(OSM_DOWNLOAD_FORMAT[download_format])]""" 98 | bbox_str = bbox === nothing ? "" : """[bbox:$(replace("$bbox", r"[\[ \]]" => ""))]""" 99 | metadata_str = metadata ? "meta" : "" 100 | query = """$download_format_str[timeout:180]$bbox_str;($filters);out count;out $metadata_str;""" 101 | @debug """Making overpass query: $query""" 102 | return query 103 | end 104 | 105 | """ 106 | osm_network_from_custom_filters(custom_filters::String, 107 | metadata::Bool=false, 108 | download_format::Symbol=:json, 109 | bbox::Union{Vector{AbstractFloat},Nothing}=nothing 110 | )::String 111 | 112 | To pass in a custom query string, filters and bbox 113 | 114 | # Arguments 115 | - `custom_filters::String`: Custom filters for the query, e.g. polygon filter, highways only, traffic lights only, etc. 116 | - `metadata::Bool=false`: Set true to return metadata. 117 | - `download_format::Symbol=:json`: Download format, either `:osm`, `:xml` or `json`. 118 | - `bbox::Union{Vector{AbstractFloat},Nothing}=nothing`: Optional bounding box filter. 119 | - `network_type::Symbol=:drive`: 120 | 121 | # Return 122 | - `String`: Overpass query string. 123 | """ 124 | function osm_network_from_custom_filters(;custom_filters::String, 125 | network_type::Symbol=:drive, 126 | metadata::Bool=false, 127 | download_format::Symbol=:json, 128 | bbox::Union{Vector{<:AbstractFloat},Nothing}=nothing)::String 129 | # return overpass_request(query) 130 | query = overpass_query(custom_filters, metadata, download_format, bbox) 131 | response = overpass_request(query) 132 | return response 133 | end 134 | 135 | """ 136 | overpass_polygon_network_query(geojson_polygons::Vector{Vector{Any}}, 137 | network_type::Symbol=:drive, 138 | metadata::Bool=false, 139 | download_format::Symbol=:json 140 | )::String 141 | 142 | Forms an Overpass query string using geojosn polygon coordinates as a filter. 143 | 144 | # Arguments 145 | - `geojson_polygons::Vector{Vector{Any}}`: Vector of `[lat, lon, ...]` polygon coordinates. 146 | - `network_type::Symbol=:drive`: Network type filter, pick from `:drive`, `:drive_service`, `:walk`, `:bike`, `:all`, `:all_private`, `:none`, `:rail`. 147 | - `metadata::Bool=false`: Set true to return metadata. 148 | - `download_format::Symbol=:json`: Download format, either `:osm`, `:xml` or `json`. 149 | 150 | # Return 151 | - `String`: Overpass query string. 152 | """ 153 | function overpass_polygon_network_query(geojson_polygons::AbstractVector{<:AbstractVector}, 154 | network_type::Symbol=:drive, 155 | metadata::Bool=false, 156 | download_format::Symbol=:json 157 | )::String 158 | way_filter = WAY_FILTERS_QUERY[network_type] 159 | relation_filter = RELATION_FILTERS_QUERY[network_type] 160 | 161 | filters = "" 162 | for polygon in geojson_polygons 163 | polygon = map(x -> [x[2], x[1]], polygon) # switch lon-lat to lat-lon 164 | polygon_str = replace("$polygon", r"[\[,\]]" => "") 165 | filters *= """way$way_filter(poly:"$polygon_str");>;""" 166 | if !isnothing(relation_filter) 167 | filters *= """rel$relation_filter(poly:"$polygon_str");>;""" 168 | end 169 | end 170 | 171 | return overpass_query(filters, metadata, download_format) 172 | end 173 | 174 | """ 175 | polygon_from_place_name(place_name::String)::Vector{Vector{Any}} 176 | 177 | Retrieves polygon coordinates using any place name string. 178 | 179 | # Arguments 180 | - `place_name::String`: Any place name string used as a search argument to the Nominatim API. 181 | 182 | # Return 183 | - `Vector{Vector{Any}}`: GeoJSON polygon coordiantes. 184 | """ 185 | function polygon_from_place_name(place_name::String)::Vector{Vector{Any}} 186 | query = nominatim_polygon_query(place_name) 187 | response = JSON.parse(nominatim_request(query)) 188 | 189 | for item in response 190 | if item["geojson"]["type"] == "Polygon" 191 | @info "Using Polygon for $(item["display_name"])" 192 | return item["geojson"]["coordinates"] 193 | elseif item["geojson"]["type"] == "MultiPolygon" 194 | @info "Using MultiPolygon for $(item["display_name"])" 195 | return collect(Iterators.flatten(item["geojson"]["coordinates"])) 196 | end 197 | end 198 | 199 | throw(ErrorException("Could not find valid polygon for $place_name")) 200 | end 201 | 202 | """ 203 | osm_network_from_place_name(;place_name::String, 204 | network_type::Symbol=:drive, 205 | metadata::Bool=false, 206 | download_format::Symbol=:json 207 | )::String 208 | 209 | Downloads an OpenStreetMap network using any place name string. 210 | 211 | # Arguments 212 | - `place_name::String`: Any place name string used as a search argument to the Nominatim API. 213 | - `network_type::Symbol=:drive`: Network type filter, pick from `:drive`, `:drive_service`, `:walk`, `:bike`, `:all`, `:all_private`, `:none`, `:rail`. 214 | - `metadata::Bool=false`: Set true to return metadata. 215 | - `download_format::Symbol=:json`: Download format, either `:osm`, `:xml` or `json`. 216 | 217 | # Return 218 | - `String`: OpenStreetMap network data response string. 219 | """ 220 | function osm_network_from_place_name(;place_name::String, 221 | network_type::Symbol=:drive, 222 | metadata::Bool=false, 223 | download_format::Symbol=:json 224 | )::String 225 | geojson_polygons = polygon_from_place_name(place_name) 226 | query = overpass_polygon_network_query(geojson_polygons, network_type, metadata, download_format) 227 | return overpass_request(query) 228 | end 229 | 230 | """ 231 | osm_network_from_polygon(;polygon::AbstractVector, 232 | network_type::Symbol=:drive, 233 | metadata::Bool=false, 234 | download_format::Symbol=:json 235 | )::String 236 | 237 | Downloads an OpenStreetMap network using a polygon. 238 | 239 | # Arguments 240 | - `polygon::AbstractVector`: Vector of longitude-latitude pairs. 241 | - `network_type::Symbol=:drive`: Network type filter, pick from `:drive`, `:drive_service`, `:walk`, `:bike`, `:all`, `:all_private`, `:none`, `:rail`. 242 | - `metadata::Bool=false`: Set true to return metadata. 243 | - `download_format::Symbol=:json`: Download format, either `:osm`, `:xml` or `json`. 244 | 245 | # Return 246 | - `String`: OpenStreetMap network data response string. 247 | """ 248 | function osm_network_from_polygon(;polygon::AbstractVector{<:AbstractVector{<:Real}}, 249 | network_type::Symbol=:drive, 250 | metadata::Bool=false, 251 | download_format::Symbol=:json 252 | )::String 253 | query = overpass_polygon_network_query([polygon], network_type, metadata, download_format) 254 | return overpass_request(query) 255 | end 256 | 257 | """ 258 | overpass_bbox_network_query(bbox::Vector{AbstractFloat}, 259 | network_type::Symbol=:drive, 260 | metadata::Bool=false, 261 | download_format::Symbol=:json 262 | )::String 263 | 264 | Forms an Overpass query string using a bounding box as a filter. 265 | 266 | # Arguments 267 | - `bbox::Vector{AbstractFloat}`: Vector of bounding box coordinates `[minlat, minlon, maxlat, maxlon]`. 268 | - `network_type::Symbol=:drive`: Network type filter, pick from `:drive`, `:drive_service`, `:walk`, `:bike`, `:all`, `:all_private`, `:none`, `:rail`. 269 | - `metadata::Bool=false`: Set true to return metadata. 270 | - `download_format::Symbol=:json`: Download format, either `:osm`, `:xml` or `json`. 271 | 272 | # Return 273 | - `String`: Overpass query string. 274 | """ 275 | function overpass_bbox_network_query(bbox::Vector{<:AbstractFloat}, 276 | network_type::Symbol=:drive, 277 | metadata::Bool=false, 278 | download_format::Symbol=:json 279 | )::String 280 | way_filter = WAY_FILTERS_QUERY[network_type] 281 | relation_filter = RELATION_FILTERS_QUERY[network_type] 282 | filters = "way$way_filter;>;" 283 | if !isnothing(relation_filter) 284 | filters *= """rel$relation_filter;>;""" 285 | end 286 | return overpass_query(filters, metadata, download_format, bbox) 287 | end 288 | 289 | """ 290 | osm_network_from_bbox(;minlat::AbstractFloat, 291 | minlon::AbstractFloat, 292 | maxlat::AbstractFloat, 293 | maxlon::AbstractFloat, 294 | network_type::Symbol=:drive, 295 | metadata::Bool=false, 296 | download_format::Symbol=:json 297 | )::String 298 | 299 | Downloads an OpenStreetMap network using bounding box coordinates. 300 | 301 | # Arguments 302 | - `minlat::AbstractFloat`: Bottom left bounding box latitude coordinate. 303 | - `minlon::AbstractFloat`: Bottom left bounding box longitude coordinate. 304 | - `maxlat::AbstractFloat`: Top right bounding box latitude coordinate. 305 | - `maxlon::AbstractFloat`: Top right bounding box longitude coordinate. 306 | - `network_type::Symbol=:drive`: Network type filter, pick from `:drive`, `:drive_service`, `:walk`, `:bike`, `:all`, `:all_private`, `:none`, `:rail`. 307 | - `metadata::Bool=false`: Set true to return metadata. 308 | - `download_format::Symbol=:json`: Download format, either `:osm`, `:xml` or `json`. 309 | 310 | # Return 311 | - `String`: OpenStreetMap network data response string. 312 | """ 313 | function osm_network_from_bbox(;minlat::AbstractFloat, 314 | minlon::AbstractFloat, 315 | maxlat::AbstractFloat, 316 | maxlon::AbstractFloat, 317 | network_type::Symbol=:drive, 318 | metadata::Bool=false, 319 | download_format::Symbol=:json 320 | )::String 321 | query = overpass_bbox_network_query([minlat, minlon, maxlat, maxlon], network_type, metadata, download_format) 322 | return overpass_request(query) 323 | end 324 | 325 | """ 326 | osm_network_from_point(;point::GeoLocation, 327 | radius::Number, 328 | network_type::Symbol=:drive, 329 | metadata::Bool=false, 330 | download_format::Symbol=:json 331 | )::String 332 | 333 | Downloads an OpenStreetMap network using bounding box coordinates calculated from a centroid point and radius (km). 334 | 335 | # Arguments 336 | - `point::GeoLocation`: Centroid point to draw the bounding box around. 337 | - `radius::Number`: Distance (km) from centroid point to each bounding box corner. 338 | - `network_type::Symbol=:drive`: Network type filter, pick from `:drive`, `:drive_service`, `:walk`, `:bike`, `:all`, `:all_private`, `:none`, `:rail`. 339 | - `metadata::Bool=false`: Set true to return metadata. 340 | - `download_format::Symbol=:json`: Download format, either `:osm`, `:xml` or `json`. 341 | 342 | # Return 343 | - `String`: OpenStreetMap network data response string. 344 | """ 345 | function osm_network_from_point(;point::GeoLocation, 346 | radius::Number, 347 | network_type::Symbol=:drive, 348 | metadata::Bool=false, 349 | download_format::Symbol=:json 350 | )::String 351 | bbox = bounding_box_from_point(point, radius) 352 | return osm_network_from_bbox(;bbox..., network_type=network_type, metadata=metadata, download_format=download_format) 353 | end 354 | 355 | """ 356 | Factory method for retrieving download functions. 357 | """ 358 | function osm_network_downloader(download_method::Symbol)::Function 359 | if download_method == :place_name 360 | return osm_network_from_place_name 361 | elseif download_method == :bbox 362 | return osm_network_from_bbox 363 | elseif download_method == :point 364 | return osm_network_from_point 365 | elseif download_method == :polygon 366 | return osm_network_from_polygon 367 | elseif download_method == :custom_filters 368 | return osm_network_from_custom_filters 369 | else 370 | throw(ArgumentError("OSM network downloader $download_method does not exist")) 371 | end 372 | end 373 | 374 | """ 375 | download_osm_network(download_method::Symbol; 376 | network_type::Symbol=:drive, 377 | metadata::Bool=false, 378 | download_format::Symbol=:json, 379 | save_to_file_location::Union{String,Nothing}=nothing, 380 | download_kwargs... 381 | )::Union{XMLDocument,Dict{String,Any}} 382 | 383 | Downloads an OpenStreetMap network by querying with a place name, bounding box, or centroid point. 384 | 385 | # Arguments 386 | - `download_method::Symbol`: Download method, choose from `:place_name`, `:bbox` or `:point`. 387 | - `network_type::Symbol=:drive`: Network type filter, pick from `:drive`, `:drive_service`, `:walk`, `:bike`, `:all`, `:all_private`, `:none`, `:rail` 388 | - `metadata::Bool=false`: Set true to return metadata. 389 | - `download_format::Symbol=:json`: Download format, either `:osm`, `:xml` or `json`. 390 | - `save_to_file_location::Union{String,Nothing}=nothing`: Specify a file location to save downloaded data to disk. 391 | 392 | # Required Kwargs for each Download Method 393 | 394 | *`download_method=:place_name`* 395 | - `place_name::String`: Any place name string used as a search argument to the Nominatim API. 396 | 397 | *`download_method=:bbox`* 398 | - `minlat::AbstractFloat`: Bottom left bounding box latitude coordinate. 399 | - `minlon::AbstractFloat`: Bottom left bounding box longitude coordinate. 400 | - `maxlat::AbstractFloat`: Top right bounding box latitude coordinate. 401 | - `maxlon::AbstractFloat`: Top right bounding box longitude coordinate. 402 | 403 | *`download_method=:point`* 404 | - `point::GeoLocation`: Centroid point to draw the bounding box around. 405 | - `radius::Number`: Distance (km) from centroid point to each bounding box corner. 406 | 407 | *`download_method=:polygon`* 408 | - `polygon::AbstractVector`: Vector of longitude-latitude pairs. 409 | 410 | *`download_method=:custom_filters`* 411 | - `custom_filters::String`: Filters for the query, e.g. polygon filter, highways only, traffic lights only, etc. 412 | - `metadata::Bool=false`: Set true to return metadata. 413 | - `download_format::Symbol=:json`: Download format, either `:osm`, `:xml` or `json`. 414 | - `bbox::Union{Vector{AbstractFloat},Nothing}=nothing`: Optional bounding box filter. 415 | 416 | # Network Types 417 | - `:drive`: Motorways excluding private and service ways. 418 | - `:drive_service`: Motorways including private and service ways. 419 | - `:walk`: Walkways only. 420 | - `:bike`: Cycleways only. 421 | - `:all`: All motorways, walkways and cycleways excluding private ways. 422 | - `:all_private`: All motorways, walkways and cycleways including private ways. 423 | - `:none`: No network filters. 424 | - `:rail`: Railways excluding proposed and platform. 425 | 426 | # Return 427 | - `Union{XMLDocument,Dict{String,Any}}`: OpenStreetMap network data parsed as either XML or Dictionary object depending on the download method. 428 | """ 429 | function download_osm_network(download_method::Symbol; 430 | network_type::Symbol=:drive, 431 | metadata::Bool=false, 432 | download_format::Symbol=:json, 433 | save_to_file_location::Union{String,Nothing}=nothing, 434 | download_kwargs... 435 | )::Union{XMLDocument,Dict{String,Any}} 436 | downloader = osm_network_downloader(download_method) 437 | data = downloader(network_type=network_type, metadata=metadata, download_format=download_format; download_kwargs...) 438 | @info "Downloaded osm network data from $(["$k: $v" for (k, v) in download_kwargs]) in $download_format format" 439 | 440 | if !(save_to_file_location isa Nothing) 441 | save_to_file_location = validate_save_location(save_to_file_location, download_format) 442 | Base.write(save_to_file_location, data) 443 | @info "Saved osm network data to disk: $save_to_file_location" 444 | end 445 | 446 | deserializer = string_deserializer(download_format) 447 | 448 | return deserializer(data) 449 | end 450 | --------------------------------------------------------------------------------