LightOSM.jl Tutorial

This is a comprehensive tutorial on the usage of LightOSM.jl, see the documentation for further details on the interface and methods.

Setup and Prerequisites

First configure the logger:

In [1]:
using Logging
logger = SimpleLogger(stdout)
global_logger(logger);

Install the Julia dependencies:

In [2]:
using Pkg
Pkg.add("LightOSM")
Pkg.add("PyCall");

Set a seed so this tutorial is reproducible:

In [3]:
using Random
Random.seed!(1234);

For visualisation, this tutorial uses the Python package pydeck (a Python binding for Uber's graphics library deck.gl), and is called from Julia using PyCall.jl.

The official release of pydeck does not work with PyCall, you must install this forked version to the same virtual environment that PyCall is built with (might take a couple of minutes):

git clone --single-branch --branch pydeck/julia-pycall-binding git@github.com:captchanjack/deck.gl.git cd deck.gl/bindings/pydeck yarn bootstrap pip install .

Download OpenStreetMap network data

Download :drive network data as an object by searching a :place_name, notice there is an option to save that object to disk, the default data format is :osm (this is basically an .xml file but can be opened and visualized with JOSM, note JOSM can only open the file if keyword argument metadata=true when downloading):

In [4]:
using LightOSM
data = download_osm_network(:place_name,
                            place_name="melbourne, australia",
                            network_type=:drive,
                            save_to_file_location="melbourne_drive_network.osm");
┌ Info: Using Polygon for City of Melbourne, Victoria, Australia
â”” @ LightOSM /LightOSM.jl/src/download.jl:147
┌ Info: Overpass server is available for download
â”” @ LightOSM /LightOSM.jl/src/download.jl:7
┌ Info: Downloaded osm network data from ["place_name: melbourne, australia"] in osm format
â”” @ LightOSM /LightOSM.jl/src/download.jl:350
┌ Info: Saved osm network data to disk: melbourne_drive_network.osm
â”” @ LightOSM /LightOSM.jl/src/download.jl:363

Understanding the OSMGraph object

With the downloaded data we can instantiate an OSMGraph object, which is a container for all the parsed data, DiGraph object, and KDTree object needed for shortest path and nearest node calculations. By default the graph object is a StaticDiGraph from StaticGraphs.jl as it is more memory-efficient:

In [5]:
g = graph_from_object(data, weight_type=:distance); # default weight_type is travel :time
# or g = graph_from_file("melbourne_drive_network.osm")
# or g = graph_from_download(:place_name, place_name="melbourne, australia")
┌ Info: Created OSMGraph object with kwargs: network_type=drive, weight_type=distance, graph_type=static, precompute_dijkstra_states=false, largest_connected_component=true
â”” @ LightOSM /LightOSM.jl/src/graph.jl:40

Mapping of all Node objects:

In [6]:
g.nodes # node id => Node
Out[6]:
Dict{Int64,Node{Int64}} with 19330 entries:
  443298633  => Node{Int64}(443298633, GeoLocation(-37.8155, 144.988, 0.0), Dic…
  1287222136 => Node{Int64}(1287222136, GeoLocation(-37.7968, 144.968, 0.0), Di…
  529205673  => Node{Int64}(529205673, GeoLocation(-37.8242, 144.937, 0.0), Dic…
  529158996  => Node{Int64}(529158996, GeoLocation(-37.8236, 144.941, 0.0), Dic…
  7368224775 => Node{Int64}(7368224775, GeoLocation(-37.8407, 144.912, 0.0), Di…
  6408311394 => Node{Int64}(6408311394, GeoLocation(-37.8331, 144.911, 0.0), Di…
  7798178225 => Node{Int64}(7798178225, GeoLocation(-37.7927, 144.942, 0.0), Di…
  1525908983 => Node{Int64}(1525908983, GeoLocation(-37.8231, 144.978, 0.0), Di…
  777806543  => Node{Int64}(777806543, GeoLocation(-37.8032, 144.956, 0.0), Dic…
  587669129  => Node{Int64}(587669129, GeoLocation(-37.7966, 144.974, 0.0), Dic…
  2279007260 => Node{Int64}(2279007260, GeoLocation(-37.815, 144.96, 0.0), Dict…
  36824733   => Node{Int64}(36824733, GeoLocation(-37.8253, 144.947, 0.0), Dict…
  6167441070 => Node{Int64}(6167441070, GeoLocation(-37.8161, 144.964, 0.0), Di…
  54392200   => Node{Int64}(54392200, GeoLocation(-37.7995, 144.949, 0.0), Dict…
  1927197433 => Node{Int64}(1927197433, GeoLocation(-37.8033, 144.951, 0.0), Di…
  3215192029 => Node{Int64}(3215192029, GeoLocation(-37.8087, 144.946, 0.0), Di…
  6731479806 => Node{Int64}(6731479806, GeoLocation(-37.7871, 144.941, 0.0), Di…
  643991276  => Node{Int64}(643991276, GeoLocation(-37.8245, 144.989, 0.0), Dic…
  3855677559 => Node{Int64}(3855677559, GeoLocation(-37.7877, 144.925, 0.0), Di…
  7715031562 => Node{Int64}(7715031562, GeoLocation(-37.8211, 144.947, 0.0), Di…
  7334876835 => Node{Int64}(7334876835, GeoLocation(-37.7936, 144.945, 0.0), Di…
  130042874  => Node{Int64}(130042874, GeoLocation(-37.8034, 144.931, 0.0), Dic…
  6696357515 => Node{Int64}(6696357515, GeoLocation(-37.8175, 144.959, 0.0), Di…
  683096494  => Node{Int64}(683096494, GeoLocation(-37.8257, 144.931, 0.0), Dic…
  7620884608 => Node{Int64}(7620884608, GeoLocation(-37.7991, 144.97, 0.0), Dic…
  â‹®          => â‹®

Let's pick two Nodes for use throughout this tutorial:

In [7]:
n1 = g.nodes[443298633]
n2 = g.nodes[7620884608];

Node location:

In [8]:
n1.location
Out[8]:
GeoLocation
  lat: Float64 -37.8154728
  lon: Float64 144.9879921
  alt: Float64 0.0

Haversine distance between n1 and n2:

In [9]:
distance(n1, n2) # km
Out[9]:
2.395554533089452

Bearing from n1 to n2:

In [10]:
heading(n1, n2) # degrees from North
Out[10]:
-40.36552337808793

Node tags:

In [11]:
n1.tags
Out[11]:
Dict{String,Any} with 2 entries:
  "lanes"    => 1
  "maxspeed" => 50

Mapping of all Way objects:

In [12]:
g.highways # way id => Highway
Out[12]:
Dict{Int64,Way{Int64}} with 4400 entries:
  787583810 => Way{Int64}(787583810, [36821291, 3293340285], Dict{String,Any}("…
  209747362 => Way{Int64}(209747362, [233274943, 1367584675, 30287457, 59579069…
  529826996 => Way{Int64}(529826996, [5146355194, 5146355195], Dict{String,Any}…
  13752475  => Way{Int64}(13752475, [127621530, 569932903, 6670658533, 31378997…
  189294754 => Way{Int64}(189294754, [1999366331, 7237853204, 1999366335, 19993…
  23704198  => Way{Int64}(23704198, [256671028, 531294448, 6207076770, 24717517…
  330450401 => Way{Int64}(330450401, [3374134100, 3374134109, 3374134104, 33741…
  581959661 => Way{Int64}(581959661, [2189145387, 6167231029, 6722694954, 26034…
  313510008 => Way{Int64}(313510008, [2209383462, 4326397440, 4326397441, 22093…
  716908489 => Way{Int64}(716908489, [30467776, 2209383606], Dict{String,Any}("…
  663076235 => Way{Int64}(663076235, [6206995797, 331143092], Dict{String,Any}(…
  658620796 => Way{Int64}(658620796, [6167279406, 2181268846, 2841675049], Dict…
  113549279 => Way{Int64}(113549279, [1287339283, 1287463903, 1287463753, 12874…
  831017475 => Way{Int64}(831017475, [7759495227, 7759495228], Dict{String,Any}…
  26619050  => Way{Int64}(26619050, [60317254, 7253929659, 36819069], Dict{Stri…
  13399279  => Way{Int64}(13399279, [123345146, 7508953581, 123346803, 75089547…
  23430975  => Way{Int64}(23430975, [7323283149, 3373060053, 3373060019], Dict{…
  448301309 => Way{Int64}(448301309, [758618459, 560987716, 758618464, 67587950…
  208677471 => Way{Int64}(208677471, [767587066, 2384091389, 767592114, 7675921…
  24430463  => Way{Int64}(24430463, [243096971, 846565381, 3961293500, 66274100…
  814432214 => Way{Int64}(814432214, [3589883222, 7607507086], Dict{String,Any}…
  233932672 => Way{Int64}(233932672, [474497654, 259621412], Dict{String,Any}("…
  5865175   => Way{Int64}(5865175, [47334639, 47334799], Dict{String,Any}("lit"…
  37523958  => Way{Int64}(37523958, [1954066453, 439233998, 439233996, 43923398…
  266143949 => Way{Int64}(266143949, [2717237172, 2717237175], Dict{String,Any}…
  â‹®         => â‹®

Highway nodes:

In [13]:
g.highways[13752475].nodes
Out[13]:
4-element Array{Int64,1}:
  127621530
  569932903
 6670658533
 3137899744

Highway tags:

In [14]:
g.highways[13752475].tags
Out[14]:
Dict{String,Any} with 9 entries:
  "cycleway:left"  => "shared_lane"
  "cycleway:right" => "lane"
  "name"           => "Canning Street"
  "reverseway"     => false
  "oneway"         => false
  "highway"        => "residential"
  "lanes"          => 1
  "maxspeed"       => 50
  "surface"        => "asphalt"

Mapping of all restriction objects:

In [15]:
g.restrictions # relation id => Restriction
Out[15]:
Dict{Int64,Restriction{Int64}} with 447 entries:
  8227939  => Restriction{Int64}(8227939, "via_node", Dict{String,Any}("restric…
  9898340  => Restriction{Int64}(9898340, "via_way", Dict{String,Any}("restrict…
  8227932  => Restriction{Int64}(8227932, "via_way", Dict{String,Any}("restrict…
  8112493  => Restriction{Int64}(8112493, "via_way", Dict{String,Any}("restrict…
  8228035  => Restriction{Int64}(8228035, "via_node", Dict{String,Any}("restric…
  8223894  => Restriction{Int64}(8223894, "via_way", Dict{String,Any}("restrict…
  8152424  => Restriction{Int64}(8152424, "via_node", Dict{String,Any}("restric…
  4076490  => Restriction{Int64}(4076490, "via_node", Dict{String,Any}("restric…
  3908891  => Restriction{Int64}(3908891, "via_node", Dict{String,Any}("restric…
  1348017  => Restriction{Int64}(1348017, "via_node", Dict{String,Any}("hour_on…
  7939315  => Restriction{Int64}(7939315, "via_node", Dict{String,Any}("restric…
  8198043  => Restriction{Int64}(8198043, "via_way", Dict{String,Any}("restrict…
  8184622  => Restriction{Int64}(8184622, "via_way", Dict{String,Any}("restrict…
  11506590 => Restriction{Int64}(11506590, "via_node", Dict{String,Any}("restri…
  9406142  => Restriction{Int64}(9406142, "via_node", Dict{String,Any}("restric…
  5944613  => Restriction{Int64}(5944613, "via_node", Dict{String,Any}("restric…
  8108891  => Restriction{Int64}(8108891, "via_node", Dict{String,Any}("restric…
  8109817  => Restriction{Int64}(8109817, "via_way", Dict{String,Any}("restrict…
  8109265  => Restriction{Int64}(8109265, "via_node", Dict{String,Any}("restric…
  8110072  => Restriction{Int64}(8110072, "via_way", Dict{String,Any}("restrict…
  8215522  => Restriction{Int64}(8215522, "via_node", Dict{String,Any}("restric…
  11188427 => Restriction{Int64}(11188427, "via_node", Dict{String,Any}("restri…
  7737787  => Restriction{Int64}(7737787, "via_node", Dict{String,Any}("restric…
  8150421  => Restriction{Int64}(8150421, "via_node", Dict{String,Any}("restric…
  6616864  => Restriction{Int64}(6616864, "via_node", Dict{String,Any}("restric…
  â‹®        => â‹®

Restriction tags:

In [16]:
g.restrictions[8221605].tags
Out[16]:
Dict{String,Any} with 2 entries:
  "restriction" => "no_u_turn"
  "type"        => "restriction"

Visualise network data with pydeck and PyCall

First initialise the pydeck object, view state and Mapbox token, if you don't have one you can create a free account:

In [17]:
using PyCall
pydeck = pyimport("pydeck")

MAPBOX_TOKEN = "pk.eyJ1IjoiY2FwdGNoYW5qYWNrIiwiYSI6ImNrMzJ1enJoZjBueWwzY245ZDV0YjJ3Z3YifQ.VAWolOVu6eDYSnj3SC4NeQ"
MAXPBOX_STYLE = "mapbox://styles/captchanjack/ckepp735v2m2719lirl762qbi"
VIEWPORT_LOCATION = GeoLocation(-37.8142176, 144.9631608) # Melbourne (lat, lon)

view_state = pydeck.ViewState(longitude=VIEWPORT_LOCATION.lon,
                              latitude=VIEWPORT_LOCATION.lat,
                              zoom=13,
                              min_zoom=1,
                              max_zoom=25,
                              pitch=50,
                              bearing=-45);

Visualise Node objects using the ScatterplotLayer:

In [18]:
# Transform data
nodes_pydeck_data = [
    Dict(
        "ID" => id,
        "Type" => "Node",
        "Longitude" => node.location.lon,
        "Latitude" => node.location.lat,
        "Coordinates" => string([node.location.lon, node.location.lat]),
        "Name" => "",
        "Maxspeed" => "",
        "Lanes" => "",
        "Oneway" => "",
    ) for (id, node) in g.nodes
]

# Build the layer
nodes_layer = pydeck.Layer("ScatterplotLayer",
                           nodes_pydeck_data,
                           pickable=true,
                           opacity=0.8,
                           stroked=true,
                           filled=true,
                           line_width_min_pixels=5,
                           line_width_max_pixels=5,
                           line_width_scale=1,
                           auto_highlight=true,
                           get_position=["Longitude", "Latitude"],
                           get_radius=1,
                           radius_scale=1,
                           get_line_width=1,
                           get_line_color=[255, 98, 0, 255],
                           get_fill_color=[255,156,93, 255])

# Define tooltip
tooltip_style = Dict(
    "color" => "white",
    "border-radius" => "10px",
    "border-color" => "dark grey",
    "background-color" => "CadetBlue",
    "font-family" => "Trebuchet MS",
    "z-index" => 3,
    "position" => "absolute"
)

nodes_tooltip = Dict(
    "html" => "<b>ID:</b> {ID}<br><b>Coordinates:</b> {Coordinates}",
    "style" => tooltip_style
)

# Build the deck
r = pydeck.Deck(layers=[nodes_layer],
                initial_view_state=view_state,
                mapbox_key=MAPBOX_TOKEN,
                map_style=MAXPBOX_STYLE,
                tooltip=nodes_tooltip)

# Save to .html file and display
r.to_html("nodes.html", notebook_display=true)
Out[18]:

Add Way objects to the same deck using the PathLayer:

In [19]:
# Create a mapping of number of lanes to colours (monochromatic green)
COLOUR_MAPPING = Dict(
    1 => [18, 231, 114],
    2 => [53, 181, 53],
    3 => [0, 107, 60],
    4 => [0, 86, 63],
    5 => [1, 50, 32]
)

DEFAULT_GREEN = [5, 102, 68] # more than 5 lanes

delete_quotes(str) = replace(str, r"'|\"" => " ") # deals with names with quotes (single or double apostrophe)

# Transform data
ways_pydeck_data = [
    Dict(
        "ID" => id,
        "Longitude" => "",
        "Latitude" => "",
        "Coordinates" => "",
        "Type" => "Highway - $(way.tags["highway"])",
        "color" => get(COLOUR_MAPPING, way.tags["lanes"], DEFAULT_GREEN),
        "Path" => [[g.nodes[n_id].location.lon, g.nodes[n_id].location.lat] for n_id in way.nodes],
        "Name" => delete_quotes(get(way.tags, "name", "")),
        "Maxspeed" => way.tags["maxspeed"],
        "Lanes" => way.tags["lanes"],
        "Oneway" => way.tags["oneway"]
    ) for (id, way) in g.highways
]

# Build the layer
ways_layer = pydeck.Layer("PathLayer",
                          ways_pydeck_data,
                          pickable=true,
                          get_color="color",
                          width_scale=1,
                          width_min_pixels=2,
                          get_path="Path",
                          get_width=2,
                          auto_highlight=true,
                          rounded=true)

# Define tooltip
ways_tooltip = Dict(
    "html" => "
    <b>ID:</b> {ID}<br>
    <b>Type:</b> {Type}<br>
    <b>Name:</b> {Name}<br>
    <b>Coordinates:</b> {Coordinates}<br>
    <b>Maxspeed:</b> {Maxspeed}<br>
    <b>Lanes:</b> {Lanes}<br>
    <b>Oneway:</b> {Oneway}<br>
    ",
    "style" => tooltip_style
)

# Build the deck
r = pydeck.Deck(layers=[ways_layer, nodes_layer],
                initial_view_state=view_state,
                mapbox_key=MAPBOX_TOKEN,
                map_style=MAXPBOX_STYLE,
                tooltip=ways_tooltip)

# Save to .html file and display
r.to_html("nodes_and_ways.html", notebook_display=true)
Out[19]:

Nearest Node

Nearest Node calculations in this package is implemented with a K-D Tree data structure for fast querying. We can pick any point on the map (either a Node or a GeoLocation) to query N nearest Nodes and the straight line distances (Euclidean) to each of these neighbours.

If we use a Node's GeoLocation, the closest node will technically be itself:

In [20]:
nearest_node(g, n1.location) # returns a Tuple ([[nearest nodes]], [[distances in km]])
Out[20]:
(Array{Integer,1}[[443298633]], Array{AbstractFloat,1}[[0.0]])

If a Node or Node.id is given as input, itself is not considered the closest node:

In [21]:
nearest_node(g, n1)
Out[21]:
(Array{Integer,1}[[443298637]], Array{AbstractFloat,1}[[0.004462946816484592]])

We can use any [latitude, longitude] pair to query:

In [22]:
nearest_node(g, [-37.8142176, 144.9631608])
Out[22]:
(Array{Integer,1}[[4326586205]], Array{AbstractFloat,1}[[0.005904078708424897]])

We can query from multiple points:

In [23]:
nearest_node(g, [n1, n2]) # returns ([[n1_nbrs...], [n2_nbrs...]], [[n1_nbr_dists...], [n2_nbr_dists...]])
Out[23]:
(Array{Integer,1}[[443298637], [140980425]], Array{AbstractFloat,1}[[0.004462946816484592], [0.0031555483505411827]])

And search for multiple neighbours:

In [24]:
neighbours, distances = nearest_node(g, [n1, n2], 100)
Out[24]:
(Array{Integer,1}[[443298637, 443298632, 7874103844, 549652802, 7874103843, 443298630, 6721996787, 370751788, 370751789, 443298627  …  7874091888, 1449266795, 2810279738, 1449266729, 1449266819, 2358053629, 1449266810, 1449266816, 2810279737, 547568845], [140980425, 3189814179, 7620884607, 368095253, 368380516, 7620884605, 368380515, 7620884606, 370241633, 2717237177  …  244222120, 140980441, 244222126, 6381233871, 242532860, 476239626, 33318861, 242532866, 34160626, 6381233868]], Array{AbstractFloat,1}[[0.004462946816484592, 0.0071321935783766706, 0.00795875495346229, 0.008519153503832555, 0.009077641044340702, 0.010272253622579261, 0.01123825199042817, 0.013535511730369461, 0.013726717625786741, 0.01520324212837471  …  0.14514663733401525, 0.14576408434894478, 0.14590922988410407, 0.14685317976593867, 0.1469257246287555, 0.14780251272288453, 0.14835830829858915, 0.14884898011842507, 0.1497535934172486, 0.15313706444179412], [0.0031555483505411827, 0.012706691886680638, 0.01423621652497138, 0.01461912199804779, 0.016498375391242378, 0.019081182231291665, 0.020740529082450054, 0.022948659020046787, 0.030035044002628266, 0.030419252881018603  …  0.17749986700943501, 0.1787046811917185, 0.1787735830270336, 0.17893391043474097, 0.18009248729119662, 0.18116439171364287, 0.18235388920092924, 0.18309202172286101, 0.18310335347114173, 0.18362751886623974]])

Visualise neighbours and distances in an IconLayer:

In [25]:
# Create a new view state
VIEWPORT_LOCATION_NBR = GeoLocation(-37.8073,144.9771)
view_state_nbr = pydeck.ViewState(longitude=VIEWPORT_LOCATION_NBR.lon,
                                  latitude=VIEWPORT_LOCATION_NBR.lat,
                                  zoom=14,
                                  min_zoom=1,
                                  max_zoom=25,
                                  pitch=50,
                                  bearing=225);

# Define icon data
neighbour_icon_data = Dict(
    # Icon taken from https://thenounproject.com/
    "url" => "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMUExQTFBIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTAwIDEwMDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGQ9Ik04NS45LDM4LjVDODUuOSwxOC43LDY5LjgsMi42LDUwLDIuNlMxNC4xLDE4LjcsMTQuMSwzOC41YzAsMTYuMywxMC44LDMwLDI1LjYsMzQuNGw3LjUsMTggIGMxLjIsMi4yLDQuMywyLjIsNS41LDBsNy41LTE4Qzc1LjEsNjguNSw4NS45LDU0LjgsODUuOSwzOC41eiI+PC9wYXRoPjwvc3ZnPg==",
    "width" => 242,
    "height" => 242,
    "anchorY" => 242,
    "mask" => true # allows color to be altered with get_color kwarg
)

centroid_icon_data = Dict(
    "url" => "https://cdn.iconscout.com/icon/premium/png-512-thumb/destination-flag-8-902948.png",
    "width" => 242,
    "height" => 242,
    "anchorY" => 242,
)

# Define red-green colour scale, where 0 <= n <= 100
red(n) = Int(round(255 * n / 100))
green(n) = Int(round(255 * (100 - n) / 100)) 
blue(n) = 0

# Transform data
nbrs = [(neighbours...)...] # flatten
dists = [(distances...)...] # flatten
max_dist = max(dists...)
scaled_dist(d) = (d / max_dist) * 100

neighbours_pydeck_data = [
    Dict(
        "ID" => n,
        "Type" => "Neighbour",
        "Longitude" => g.nodes[n].location.lon,
        "Latitude" => g.nodes[n].location.lat,
        "Coordinates" => string([g.nodes[n].location.lon, g.nodes[n].location.lat]),
        "Distance" => string(round(dists[i]*1000, digits=2))*"m",
        "Icon" => neighbour_icon_data,
        "Colour" => [red(scaled_dist(dists[i])), green(scaled_dist(dists[i])), blue(scaled_dist(dists[i]))]
    ) for (i, n) in enumerate(nbrs)
]

centroid_pydeck_data = [
    Dict(
        "ID" => n.id,
        "Type" => "Centroid",
        "Longitude" => n.location.lon,
        "Latitude" => n.location.lat,
        "Coordinates" => string([n.location.lon, n.location.lat]),
        "Icon" => centroid_icon_data,
        "Distance" => "0m"
    ) for n in [n1, n2]
]

# Build the layer
neighbours_layer = pydeck.Layer("IconLayer",
                                data=neighbours_pydeck_data,
                                get_icon="Icon",
                                get_size=5,
                                size_min_pixels=50,
                                size_max_pixels=30,
                                filled=true,
                                get_color="Colour",
                                opacity=0.5,
                                get_position=["Longitude", "Latitude"],
                                pickable=true,
                                auto_highlight=true)

centroid_layer = pydeck.Layer("IconLayer",
                              data=centroid_pydeck_data,
                              get_icon="Icon",
                              get_size=5,
                              size_min_pixels=60,
                              size_max_pixels=60,
                              filled=true,
                              get_color=[72, 209, 204],
                              get_position=["Longitude", "Latitude"],
                              pickable=true,
                              auto_highlight=true)

# Define tooltip
neighbours_tooltip = Dict(
    "html" => "
    <b>ID:</b> {ID}<br>
    <b>Type:</b> {Type}<br>
    <b>Coordinates:</b> {Coordinates}<br>
    <b>Distance From Centroid:</b> {Distance}<br>
    ",
    "style" => tooltip_style
)

# Build the deck
r = pydeck.Deck(layers=[neighbours_layer, centroid_layer],
                initial_view_state=view_state_nbr,
                mapbox_key=MAPBOX_TOKEN,
                map_style=MAXPBOX_STYLE,
                tooltip=neighbours_tooltip)


# Save to .html file and display
r.to_html("neighbours.html", notebook_display=true)
Out[25]:

Shortest Path

To calculate the shortest path between two Nodes, we can use the Dijkstra or A* algorithm. These differ to those implemented in LightGraphs and OpenStreetMapX.jl as LightOSM.jl takes into account turn restrictions.

Let's start by calculating the shortest path between n1 and n2:

In [26]:
path = shortest_path(g, n1, n2, algorithm=:dijkstra)
Out[26]:
148-element Array{Int64,1}:
  443298633
  443298637
  549652802
 6721996787
  370751788
  443298645
  370751779
 2778777179
  443298619
 6721996788
  443298624
 2778777180
  443298627
          â‹®
  140980553
  242532866
  242532860
  244222126
  140980441
 2717237189
  368380426
 2717237187
  370241971
 2717237182
 1367608940
 7620884608

Retrieve the Edge weights for the path (:distance was selected as the :weight_type earlier):

In [27]:
weights = weights_from_path(g, path) # km
Out[27]:
147-element Array{Float64,1}:
 0.004462946816413719
 0.004290747057591968
 0.003115201997094804
 0.003538136602076443
 0.003982706591644379
 0.0026814180280363455
 0.002907393014295063
 0.003964188285755361
 0.0035257366875182483
 0.003718849769607274
 0.0032235566229289825
 0.0028742391174888687
 0.0031418792114619033
 â‹®
 0.004414202602339186
 0.00455288720003064
 0.006391852077814258
 0.0033208392646459015
 0.005158198012944676
 0.022580351877951065
 0.020347548494178653
 0.03875753870428935
 0.012695945329880286
 0.04328134657450959
 0.00961970470379917
 0.031428835412139644

Cumulative edge distance:

In [28]:
cum_weights = cumsum(weights)
Out[28]:
147-element Array{Float64,1}:
 0.004462946816413719
 0.008753693874005687
 0.011868895871100491
 0.015407032473176933
 0.019389739064821315
 0.02207115709285766
 0.024978550107152723
 0.028942738392908084
 0.03246847508042633
 0.036187324850033606
 0.03941088147296259
 0.042285120590451454
 0.04542699980191336
 â‹®
 3.2077815457893064
 3.212334432989337
 3.2187262850671514
 3.222047124331797
 3.227205322344742
 3.249785674222693
 3.270133222716872
 3.308890761421161
 3.3215867067510416
 3.364868053325551
 3.37448775802935
 3.40591659344149

Total path distance in km is therefore the last index:

In [29]:
total_distance = cum_weights[end]
Out[29]:
3.40591659344149

Edges are segments of a Way and are defined as adjacent origin-destination Node pairs:

In [30]:
edges = [[path[i], path[i + 1]] for i in 1:length(path) - 1]
Out[30]:
147-element Array{Array{Int64,1},1}:
 [443298633, 443298637]
 [443298637, 549652802]
 [549652802, 6721996787]
 [6721996787, 370751788]
 [370751788, 443298645]
 [443298645, 370751779]
 [370751779, 2778777179]
 [2778777179, 443298619]
 [443298619, 6721996788]
 [6721996788, 443298624]
 [443298624, 2778777180]
 [2778777180, 443298627]
 [443298627, 370751789]
 â‹®
 [244222125, 140980553]
 [140980553, 242532866]
 [242532866, 242532860]
 [242532860, 244222126]
 [244222126, 140980441]
 [140980441, 2717237189]
 [2717237189, 368380426]
 [368380426, 2717237187]
 [2717237187, 370241971]
 [370241971, 2717237182]
 [2717237182, 1367608940]
 [1367608940, 7620884608]

Use Edges to find the corresponding Ways:

In [31]:
way_ids = [g.edge_to_highway[e] for e in edges]
Out[31]:
147-element Array{Int64,1}:
  32921470
  32921470
  32921470
  32921470
  32921470
  32921470
  32921470
  32921470
  32921470
  32921470
  32921470
  32921470
  32921470
         â‹®
 744184364
 744184364
 744184364
 744184364
 744184364
  22819455
  22819455
  22819455
  22819455
  22819455
  22819455
  22819455

Now with our Nodes, Edges and Ways we can plot the shortest path using a LineLayer:

In [32]:
# Define icon data
start_finish_icons = [
    Dict(
        "url" => "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMUExQTFBIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGRhdGEtbmFtZT0iTGF5ZXIgMSIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHg9IjBweCIgeT0iMHB4Ij48dGl0bGU+VmVoaWNsZXM8L3RpdGxlPjxwYXRoIGQ9Ik0xMDAsNTcuNTZ2LS4wOGE0Ljc1LDQuNzUsMCwwLDAtLjE5LTFDOTguNTEsNTAuNDgsOTQsNDUuMDYsODgsNDMuNTJBNjQuMzksNjQuMzksMCwwLDAsNzksNDJhNy4yOSw3LjI5LDAsMCwxLTUuNzEtMy4yNmMtNS4zMy03LjYyLTEyLjc3LTEyLTIxLjg5LTEzLjY1YTc5LjYxLDc5LjYxLDAsMCwwLTI1LjgyLjA2LDE2LjU3LDE2LjU3LDAsMCwwLTExLjkzLDcuNzdjLTEuMDksMS42OS0xLjk0LDMuNTYtMy4xMiw1LjE4YTE5LjY2LDE5LjY2LDAsMCwxLTQuMSw0LjY4QTEyLjYyLDEyLjYyLDAsMCwwLC45MSw1MC44OEMuNSw1My4wOS4yMyw1NS4zMiwwLDU3LjU2SC4wNWE1LjE5LDUuMTksMCwwLDAsMCwuNjh2NWEyLDIsMCwwLDAsMiwxLjk1aDcuNWExMi4wOSwxMi4wOSwwLDAsMSwyNC4wNSwwSDY5LjM3YzAtLjE1LDAtLjMsMC0uNDVhMTIuMSwxMi4xLDAsMCwxLDI0LjE5LDBjMCwuMTUsMCwuMywwLC40NWg0LjUzYTIsMiwwLDAsMCwyLTEuOTVWNTcuODJhMi4zNSwyLjM1LDAsMCwwLDAtLjI2Wk0zOS40LDQ0LjExYy02LDAtMTEuODQuMTUtMTcuNjctLjA3LTIuODctLjEtMy41NC0xLjYyLTIuNDgtNC4zNCwzLjUyLTksOS4yLTExLjk0LDIwLjE1LTEwLjI2Wm0yNS44MSwwYy02LjUxLjEtMTMsMC0xOS44OSwwVjI5LjU5YzcuNTEtMS4zOSwxOC43OCw0LjQsMjIsMTEuMjZDNjguMyw0Myw2Ny43Myw0NC4xMiw2NS4yMSw0NC4xNloiPjwvcGF0aD48cGF0aCBkPSJNMjEuNDcsNTdBOS41MSw5LjUxLDAsMSwwLDMxLDY2LjQ3LDkuNSw5LjUsMCwwLDAsMjEuNDcsNTdabTAsMTMuNzNhNC4yMyw0LjIzLDAsMSwxLDQuMjMtNC4yMkE0LjIyLDQuMjIsMCwwLDEsMjEuNDcsNzAuNjlaIj48L3BhdGg+PHBhdGggZD0iTTkxLDY0Ljc2YTkuNTEsOS41MSwwLDEsMC05LjUsOS41MUE5LjUsOS41LDAsMCwwLDkxLDY0Ljc2Wm0tMTMuNzMsMEE0LjIzLDQuMjMsMCwxLDEsODEuNDUsNjksNC4yNCw0LjI0LDAsMCwxLDc3LjIyLDY0Ljc2WiI+PC9wYXRoPjwvc3ZnPg==",
        "width" => 242,
        "height" => 242,
        "anchorY" => 242,
        "mask" => true
    ),
    Dict(
        "url" => "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMUExQTFBIiB4bWxuczp4PSJodHRwOi8vbnMuYWRvYmUuY29tL0V4dGVuc2liaWxpdHkvMS4wLyIgeG1sbnM6aT0iaHR0cDovL25zLmFkb2JlLmNvbS9BZG9iZUlsbHVzdHJhdG9yLzEwLjAvIiB4bWxuczpncmFwaD0iaHR0cDovL25zLmFkb2JlLmNvbS9HcmFwaHMvMS4wLyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDY0IDY0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA2NCA2NCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PG1ldGFkYXRhPjxzZncgeG1sbnM9Imh0dHA6Ly9ucy5hZG9iZS5jb20vU2F2ZUZvcldlYi8xLjAvIj48c2xpY2VzPjwvc2xpY2VzPjxzbGljZVNvdXJjZUJvdW5kcyB4PSItNzg2NSIgeT0iLTIzMCIgd2lkdGg9IjE2MzgzIiBoZWlnaHQ9IjIyOCIgYm90dG9tTGVmdE9yaWdpbj0idHJ1ZSI+PC9zbGljZVNvdXJjZUJvdW5kcz48L3Nmdz48L21ldGFkYXRhPjxwYXRoIGQ9Ik01Myw1SDM5SDI1Yy0wLjU1MiwwLTEsMC40NDgtMSwxdjEwdjEwYzAsMC41NTIsMC40NDgsMSwxLDFoMTRoMTRjMC41NTIsMCwxLTAuNDQ4LDEtMVYxNlY2QzU0LDUuNDQ4LDUzLjU1Miw1LDUzLDV6ICAgTTUyLDE1SDQwVjdoMTJWMTV6IE0yNiwxN2gxMnY4SDI2VjE3eiI+PC9wYXRoPjxwYXRoIGQ9Ik0yOC45OTcsNjAuNzg3Yy0wLjEwNC0zLjk3Ni0zLjEyNi03LjIzMS02Ljk5Ny03LjcxN1YzYzAtMC41NTItMC40NDgtMS0xLTFzLTEsMC40NDgtMSwxdjUwLjA2OSAgYy0zLjk0LDAuNDk1LTcsMy44NTktNyw3LjkzMWMwLDAuNTUyLDAuNDQ4LDEsMSwxaDE0YzAuMDA3LDAsMC4wMTMtMC4wMDEsMC4wMiwwYzAuNTUyLDAsMS0wLjQ0OCwxLTEgIEMyOS4wMiw2MC45MjcsMjkuMDEyLDYwLjg1NSwyOC45OTcsNjAuNzg3eiI+PC9wYXRoPjwvc3ZnPg==",
        "width" => 242,
        "height" => 242,
        "anchorY" => 242,
        "mask" => true,
    )
]

# Define red-blue colour scale, where 0 <= n <= 100
red2(n) = Int(round(255 * n / 100))
green2(n) = 0
blue2(n) = Int(round(255 * (100 - n) / 100)) 
scaled_w(d) = (d / total_distance) * 100

# Transform data
labels = ["Origin Node", "Destination Node"]
start_finish_data = [
    Dict(
        "ID" => n.id,
        "Type" => labels[i],
        "Longitude" => n.location.lon,
        "Latitude" => n.location.lat,
        "Coordinates" => string([n.location.lon, n.location.lat]),
        "Icon" => start_finish_icons[i],
        "Distance" => "",
        "Name" => "",
        "Maxspeed" => "",
        "Lanes" => "",
        "Oneway" => "",
        "Edge" => ""
    ) for (i, n) in enumerate([n1, n2])
]

shortest_path_pydeck_data = [
    Dict(
        "ID" => way_ids[i],
        "Edge" => string([origin, destination]),
        "Type" => "Highway - $(g.highways[way_ids[i]].tags["highway"])",
        "start" => [g.nodes[origin].location.lon, g.nodes[origin].location.lat],
        "end" => [g.nodes[destination].location.lon, g.nodes[destination].location.lat],
        "Colour" => [red2(scaled_w(cum_weights[i])), green2(scaled_w(cum_weights[i])), blue2(scaled_w(cum_weights[i]))],
        "Name" => delete_quotes(get(g.highways[way_ids[i]].tags, "name", "")),
        "Maxspeed" => g.highways[way_ids[i]].tags["maxspeed"],
        "Lanes" => g.highways[way_ids[i]].tags["lanes"],
        "Oneway" => g.highways[way_ids[i]].tags["oneway"],
        "Coordinates" => "",
        "Distance" => string(round(cum_weights[i] * 1000, digits=2)) * "m"
    ) for (i, (origin, destination)) in enumerate(edges)
]


# Build the layer
start_finish_layer = pydeck.Layer("IconLayer",
                                  data=start_finish_data,
                                  get_icon="Icon",
                                  get_size=5,
                                  size_min_pixels=60,
                                  size_max_pixels=60,
                                  filled=true,
                                  get_position=["Longitude", "Latitude"],
                                  pickable=true,
                                  highlight_color=[106, 110, 117],
                                  auto_highlight=true)

shortest_path_layer = pydeck.Layer("LineLayer",
                                   shortest_path_pydeck_data,
                                   get_source_position="start",
                                   get_target_position="end",
                                   get_color="Colour",
                                   get_width=7,
                                   picking_radius=10,
                                   auto_highlight=true,
                                   highlight_color=[106, 110, 117],
                                   rounded=true,
                                   pickable=true)

# Define tooltip
shortest_path_tooltip = Dict(
    "html" => "
    <b>ID:</b> {ID}<br>
    <b>Edge:</b> {Edge}<br>
    <b>Type:</b> {Type}<br>
    <b>Coordinates:</b> {Coordinates}<br>
    <b>Name:</b> {Name}<br>
    <b>Maxspeed:</b> {Maxspeed}<br>
    <b>Lanes:</b> {Lanes}<br>
    <b>Oneway:</b> {Oneway}<br>
    <b>Distance From Origin:</b> {Distance}<br>
    ",
    "style" => tooltip_style
)

# Build the deck
r = pydeck.Deck(layers=[start_finish_layer, shortest_path_layer],
                initial_view_state=view_state_nbr,
                mapbox_key=MAPBOX_TOKEN,
                map_style=MAXPBOX_STYLE,
                tooltip=shortest_path_tooltip)


# Save to .html file and display
r.to_html("shortest_path.html", notebook_display=true)
Out[32]:

Now let's generate some random origin-destination Node pairs:

In [33]:
n_paths = 1000
rand_o_d_indices = rand(1:length(g.nodes), n_paths, 2)
rand_o_d_nodes = [[g.index_to_node[o], g.index_to_node[d]] for (o, d) in eachrow(rand_o_d_indices) if o != d]
Out[33]:
1000-element Array{Array{Int64,1},1}:
 [67666075, 249909873]
 [7660353918, 7637480397]
 [7637182097, 4728842524]
 [3144992687, 2266563638]
 [1525908943, 6197570974]
 [2080791971, 6380996551]
 [304156656, 5069152060]
 [7379382822, 794180870]
 [683096374, 3285622992]
 [5487860803, 67677930]
 [7637181792, 6167191177]
 [7377423782, 242400916]
 [1492145777, 824391676]
 â‹®
 [5065350172, 635530384]
 [247417094, 6731875618]
 [29897216, 2324385467]
 [6731366474, 5880339366]
 [3394726643, 638044478]
 [332544614, 5785884764]
 [1296963647, 5884356131]
 [310728324, 767585517]
 [711763700, 7663155631]
 [6806839682, 1283661451]
 [444352340, 4162999399]
 [1525908967, 844482565]

Calculate shortest path between each origin-destination Node pair:

In [34]:
paths = []

@time for (o, d) in rand_o_d_nodes
    try
        p = shortest_path(g, o, d, algorithm=:dijkstra)
        push!(paths, p)
    catch
        # Error exception will be thrown if path does not exist from origin to destination node
    end
end
  3.366023 seconds (2.07 M allocations: 392.882 MiB, 2.07% gc time)

Retrieve total distance in km for each path:

In [35]:
total_distances = round.(sum.([weights_from_path(g, p) for p in paths]), digits=2)
Out[35]:
871-element Array{Float64,1}:
 1.53
 1.5
 1.3
 3.51
 8.47
 7.82
 7.79
 2.46
 9.01
 5.74
 2.03
 4.63
 0.97
 â‹®
 8.81
 4.05
 2.29
 4.71
 5.45
 4.63
 1.42
 1.93
 2.21
 4.52
 2.44
 4.32

We can plot these shortest paths using the TripsLayer, unfortunately unlike its parent library deck.gl, pydeck currently does not support dynamic animation of the TripsLayer against current_time (this requires some JavaScript methods such as requestAnimationFrame):

In [36]:
# Transform data
trips_pydeck_data = [
    Dict(
        "Coordinates" => [[g.nodes[node_id].location.lon, g.nodes[node_id].location.lat] for node_id in path],
        "Timestamps" => collect(0:length(path)-1),
        "Colour" => [rand(1:255), rand(1:255), rand(1:255)],
        "Distance" => total_distances[i],
        "Origin" => path[1],
        "Destination" => path[end]
    ) for (i, path) in enumerate(paths)
]

# Build the layer
trips_layer = pydeck.Layer("TripsLayer",
                           trips_pydeck_data,
                           get_path="Coordinates",
                           get_timestamps="Timestamps",
                           get_color="Colour",
                           opacity=0.8,
                           width_min_pixels=5,
                           rounded=true,
                           trail_length=50,
                           current_time=100,
                           pickable=true,
                           auto_highlight=true)

# Define tooltip
trips_tooltip=Dict(
    "html" => "
    <b>Path Distance:</b> {Distance}km<br>
    <b>Origin:</b> {Origin}<br>
    <b>Destination:</b> {Destination}<br>
    ",
    "style" => tooltip_style
)

# Build the deck
r = pydeck.Deck(layers=[trips_layer],
                initial_view_state=view_state,
                mapbox_key=MAPBOX_TOKEN,
                map_style=MAXPBOX_STYLE,
                tooltip=trips_tooltip)

# Save to .html file and display
r.to_html("trips.html", notebook_display=true)
Out[36]:

Download and visualise buildings

Similar to downloading an OpenStreetMap network, we can also download Building polygons by searching with a :place_name, centroid :point or :bbox:

In [37]:
buildings_data = download_osm_buildings(:place_name,
                                        place_name="melbourne, australia",
                                        save_to_file_location="melbourne_buildings.osm");
┌ Info: Using Polygon for City of Melbourne, Victoria, Australia
â”” @ LightOSM /LightOSM.jl/src/download.jl:147
┌ Info: Overpass server is available for download
â”” @ LightOSM /LightOSM.jl/src/download.jl:7
┌ Info: Downloaded osm buildings data from ["place_name: melbourne, australia"] in osm format
â”” @ LightOSM /LightOSM.jl/src/buildings.jl:65
┌ Info: Saved osm buildings data to disk: melbourne_buildings.osm
â”” @ LightOSM /LightOSM.jl/src/buildings.jl:78

We can then parse and instantiate Building objects from the data downloaded (either using the in-memory data object, a saved file or a direct download method):

In [38]:
buildings = buildings_from_object(buildings_data)
# or buildings = buildings_from_file("melbourne_buildings.osm")
# or buildings = buildings_from_download(:place_name, place_name="melbourne, australia")
Out[38]:
Dict{Integer,Building} with 6724 entries:
  471348214 => Building{Int64}(471348214, false, LightOSM.Polygon{Int64}[Polygo…
  380918527 => Building{Int64}(380918527, false, LightOSM.Polygon{Int64}[Polygo…
  50339003  => Building{Int64}(50339003, false, LightOSM.Polygon{Int64}[Polygon…
  707141690 => Building{Int64}(707141690, false, LightOSM.Polygon{Int64}[Polygo…
  789199797 => Building{Int64}(789199797, false, LightOSM.Polygon{Int64}[Polygo…
  710677664 => Building{Int64}(710677664, false, LightOSM.Polygon{Int64}[Polygo…
  93507507  => Building{Int64}(93507507, false, LightOSM.Polygon{Int64}[Polygon…
  307236348 => Building{Int64}(307236348, false, LightOSM.Polygon{Int64}[Polygo…
  67287537  => Building{Int64}(67287537, false, LightOSM.Polygon{Int64}[Polygon…
  468911048 => Building{Int64}(468911048, false, LightOSM.Polygon{Int64}[Polygo…
  743140149 => Building{Int64}(743140149, false, LightOSM.Polygon{Int64}[Polygo…
  93321822  => Building{Int64}(93321822, false, LightOSM.Polygon{Int64}[Polygon…
  321369245 => Building{Int64}(321369245, false, LightOSM.Polygon{Int64}[Polygo…
  830995671 => Building{Int64}(830995671, false, LightOSM.Polygon{Int64}[Polygo…
  224318304 => Building{Int64}(224318304, false, LightOSM.Polygon{Int64}[Polygo…
  835651998 => Building{Int64}(835651998, false, LightOSM.Polygon{Int64}[Polygo…
  689573109 => Building{Int64}(689573109, false, LightOSM.Polygon{Int64}[Polygo…
  835646831 => Building{Int64}(835646831, false, LightOSM.Polygon{Int64}[Polygo…
  643752378 => Building{Int64}(643752378, false, LightOSM.Polygon{Int64}[Polygo…
  479840888 => Building{Int64}(479840888, false, LightOSM.Polygon{Int64}[Polygo…
  507121465 => Building{Int64}(507121465, false, LightOSM.Polygon{Int64}[Polygo…
  435142088 => Building{Int64}(435142088, false, LightOSM.Polygon{Int64}[Polygo…
  835646785 => Building{Int64}(835646785, false, LightOSM.Polygon{Int64}[Polygo…
  67287560  => Building{Int64}(67287560, false, LightOSM.Polygon{Int64}[Polygon…
  13307317  => Building{Int64}(13307317, false, LightOSM.Polygon{Int64}[Polygon…
  â‹®         => â‹®

Buildings consist of an array of Polygons, the first element is always the outer ring, followed by any optional inner rings (inner rings are holes in the outer ring), each with their own set of Nodes:

In [39]:
buildings[127595640].polygons[1].nodes
buildings[127595640].polygons[1].is_outer
Out[39]:
true

Building metadata tags:

In [40]:
buildings[127595640].tags # height is in metres
Out[40]:
Dict{String,Any} with 7 entries:
  "name"            => "Peter Doherty Institute"
  "height"          => 44
  "start_date"      => 2014
  "operator"        => "University of Melbourne"
  "building:levels" => 11
  "building"        => "yes"
  "ref:unimelb"     => 248

Visualise Buildings with a PolygonLayer:

In [41]:
# Transform data
max_height = max([b.tags["height"] for (id, b) in buildings]...)
rbg(d) = 255 - Int(round((d / max_height * 255)))

buildings_pydeck_data = [
    Dict(
        "Polygons" => [[[node.location.lon, node.location.lat] for node in poly.nodes] for poly in b.polygons], # array of polygons (i.e. array of array of coordinates)
        "Height" => b.tags["height"],
        "Colour" => [rand(1:255), rand(1:255), rand(1:255)],
        "Name" => delete_quotes(string(get(b.tags, "name", ""))),
        "Description" => delete_quotes(string(get(b.tags, "description", ""))),
        "Website" => delete_quotes(string(get(b.tags, "website", ""))),
        "Colour" => [rbg(b.tags["height"]), rbg(b.tags["height"]), rbg(b.tags["height"])], # grey scale
        "ID" => id
    ) for (id, b) in buildings
]

# Build the layer
buildings_layer = pydeck.Layer("PolygonLayer",
                               buildings_pydeck_data,
                               id="geojson",
                               opacity=0.2,
                               stroked=false,
                               get_polygon="Polygons",
                               filled=true,
                               extruded=true,
                               wireframe=true,
                               get_elevation="Height",
                               get_fill_color="Colour",
                               get_line_color=[255, 255, 255],
                               auto_highlight=true,
                               pickable=true,
)

# Define tooltip
buildings_tooltip = Dict(
    "html" => "
    <b>ID:</b> {ID}<br>
    <b>Name:</b> {Name}<br>
    <b>Description:</b> {Description}<br>
    <b>Height:</b> {Height}m<br>
    <b>Website:</b> {Website}<br>
    ",
    "style" => tooltip_style
)

# Build the deck
r = pydeck.Deck(layers=[buildings_layer],
                initial_view_state=view_state,
                mapbox_key=MAPBOX_TOKEN,
                map_style=MAXPBOX_STYLE,
                tooltip=buildings_tooltip)

# Save to .html file and display
r.to_html("buildings.html", notebook_display=true)
Out[41]: