Computing attractiveness metrics on streets
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# # A GUI to display location "attractiveness" | |
# The following code creates a GUI which allows you to inspect | |
# a map of Toronto, and see the attractiveness of different locations | |
# based on different nearness measures. | |
# First, we load the necessary packages. | |
# We will build our GUI with the `Makie` toolchain, consisting of `Makie`, `[W]GLMakie`, and `Tyler` for map tile visualization. | |
# Data is from OpenStreetMap and is parsed using `OpenStreetMapX`. Attractiveness scores are obtained using `OSMToolset`. | |
# To install the packages we use here, | |
# ```julia | |
# using Pkg | |
# pkg"add Tyler#master TileProviders#master Makie GLMakie OSMToolset OpenStreetMapX DataFrames CSV Dagger#master TiledIteration TimerOutputs" | |
# ``` | |
using Makie, GLMakie | |
using Tyler # you must add Tyler#master TileProviders#master | |
using OSMToolset, OpenStreetMapX | |
using Makie.Colors, Makie.ColorSchemes | |
using DataFrames, CSV | |
using Statistics | |
using Dagger, TiledIteration # to compute and schedule. | |
## We use TimerOutputs to track performance. | |
using TimerOutputs | |
const to = TimerOutput() | |
# ## Loading the data | |
# We assume a downloaded file that you can load data from. | |
# This must be a `.osm` XML file, which is a section of the full OSM `planet` file. | |
# We pre-process the raw OSM data to extract and weight areas of interest. This is done using `OSMToolset`. | |
# Please replace the file paths with your own file names here. | |
osm_file = joinpath(dirname(@__DIR__), "data", "planet_-79.866,43.561_-79.065,43.959.osm") | |
attractiveness_file = joinpath(dirname(@__DIR__), "data", "toronto.attractiveness.osm") | |
if !isfile(attractiveness_file) | |
@time df = OSMToolset.find_poi(osm_file) | |
@time CSV.write(attractiveness_file, df) # Cache the data | |
end | |
# Now, we construct an R*-tree (a spatial index) with this data, to mimimize query time. | |
@time ix = AttractivenessSpatIndex(attractiveness_file) | |
# Since we're looking only at Toronto, we can switch CRS to a locally flat projection (ENU stands for easting, northing, up in meters). | |
lla_coords = LLA.(ix.df.lat, ix.df.lon) # convert lat/long to LLA objects from Geodesy.jl | |
lla_bbox = Makie.Rect2f(extrema(getproperty.(lla_coords, :lon)), extrema(getproperty.(lla_coords, :lat))) # get a bounding box | |
vals = ENU.(lla_coords, Ref(ix.refLLA)) | |
ix.df.x .= getfield.(vals, :east) | |
ix.df.y .= getfield.(vals, :north) | |
# This spatial index object is what we'll use to compute the attractiveness of a given point. | |
# It provides the following measures: | |
labels = String.(ix.measures) | |
# This is a utility function to convert from the Web mercator projection (which is what most map tiles are in) to lon/lat coordinates. | |
function from_web_mercator(wmx,wmy) | |
return Point2f(Tyler.MapTiles.project((wmx,wmy), Tyler.MapTiles.web_mercator, Tyler.MapTiles.wgs84)) | |
end | |
# ## Creating the GUI | |
# We create a GUI using `Tyler.Map`, which is fundamentally just a `Makie.Figure` with some tasks and buffers attached, | |
# to make it efficient. | |
tyler = Tyler.Map( | |
Makie.BBox(-79.866, -79.065, 43.561, 43.959); # Toronto bounding box | |
provider = Tyler.TileProviders.Provider("https://stamen-tiles.a.ssl.fastly.net/toner/{z}/{x}/{y}.png")# OpenStreetMap() | |
) | |
# We can extract the axis which Tyler plots into, since it's the only thing in the figure: | |
tyler_axis = tyler.figure.content[1] | |
display(tyler) # hide | |
# Now, we set up the rest of the UI - a menu to select the nearness measure, | |
# and a slider to control the contrast (using the astronomers' `zscale` algorithm.) | |
ui_layout = GridLayout(tyler.figure[2, 1]) | |
menu = Menu(ui_layout[1, 1], options = zip(labels, 1:length(labels)), fontsize = 25) | |
contrast_slider = with_theme(Theme(; fontsize = 20)) do; SliderGrid(ui_layout[2, 1], (; label = "Contrast", range = exp.(LinRange(log(1e-5), log(0.3), 75)), startvalue = 0.1); ); end | |
recompute_button = Button(ui_layout[1:2, 0], label = "Recompute", fontsize = 20) | |
recompute_button.height[] = Relative(1) # make the recompute button take up all available space | |
# ## Computing attractiveness | |
function compute_new_datacube(tyler_finallims, tyler_px_area) | |
global to | |
reset_timer!(to) | |
xs = LinRange{Float64}(left(tyler_finallims), right(tyler_finallims), widths(tyler_px_area)[1] ÷ 2) | |
ys = LinRange{Float64}(bottom(tyler_finallims), top(tyler_finallims), widths(tyler_px_area)[2] ÷ 2) | |
final_array = let | |
@timeit to "Dagger" begin | |
@timeit to "Scheduling" begin | |
tile_indices = TileIterator((Base.OneTo(length(xs)), Base.OneTo(length(ys))), (100, 100)) |> collect |> vec | |
tasks = [Dagger.@spawn assess_attractiveness(xs[xinds], ys[yinds], ix) for (xinds, yinds) in tile_indices] | |
end | |
@timeit to "Computing and fetching results" begin | |
final_values = fetch.(tasks) | |
end | |
@timeit to "Stitching results together (with Threads)" begin | |
final_array = Array{Float64, 3}(undef, length(xs), length(ys), length(ix.measures)) | |
Threads.@threads for (i, tileindex) in collect(enumerate(tile_indices)) | |
xinds, yinds = tileindex | |
final_array[xinds, yinds, :] = final_values[i] | |
end | |
end | |
final_array | |
end | |
end | |
display(to) | |
return (xs, ys, final_array) | |
end | |
# ## Plotting attractiveness | |
# We simply plot a heatmap, which we will then link to the UI elements. | |
# First, we get the data (function defined later using Dagger.jl for scheduling)L | |
xs, ys, final_array = compute_new_datacube(tyler_axis.finallimits[], tyler_axis.scene.px_area[]) | |
hm = heatmap!(tyler_axis, xs, ys, final_array[:,:,1]; xautolimits = false, yautolimits = false) | |
hm.colormap[] = cgrad(:viridis; alpha = 0.5) | |
colorbar = Colorbar(tyler.figure[1, 2], hm, label = "Attractiveness", labelpadding = 10, labelsize = 20, ticklabelsize = 15, ticklabelpad = 5) | |
tyler # hide | |
datacube = Ref(final_array) | |
## Set up the callbacks from UI elements to data! | |
on(menu.selection) do index | |
hm[3][] = view(datacube[], :, :, index) | |
end | |
lift(contrast_slider.sliders[1].value, hm[3]) do val, mat | |
hm.colorrange[] = Vec2f(Makie.PlotUtils.zscale(mat; contrast = val)) | |
end | |
on(recompute_button.clicks) do _ | |
xs, ys, zs = compute_new_datacube(tyler_axis.finallimits[], tyler_axis.scene.px_area[]) | |
hm.input_args[1].val = xs | |
hm.input_args[2].val = ys | |
hm.input_args[3][] = view(zs, :, :, menu.selection[]) | |
datacube[] = zs | |
end | |
hm.colormap[] = cgrad(:Reds, alpha = 0.7) | |
tyler # hide | |
## Now we can interact with it! |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment