Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tweak tile load order #119

Merged
merged 9 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ext/TylerGeoMakieExt/TylerGeoMakieExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Tyler: tile_reloader, create_tileplot!, update_tile_plot!,
Map, AbstractMap, ImageData, PlotConfig, DebugPlotConfig,
SimpleTiling, Halo2DTiling

using Tyler: to_rect
using Tyler: to_rect, grow_extent, OrderedSet, optimal_zoom, min_zoom, max_zoom


using Makie, Makie.GeometryBasics
Expand Down
67 changes: 38 additions & 29 deletions ext/TylerGeoMakieExt/tile-fetching.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,55 +12,64 @@ function Tyler.tile_reloader(m::Map{GeoAxis})
end
end


# Here, the `area` has already been transformed to the tile CRS
function Tyler.get_tiles_for_area(m::Map{GeoAxis}, scheme::Halo2DTiling, area::Union{Rect,Extent})
function Tyler.get_tiles_for_area(m::Map{<: GeoAxis}, scheme::Halo2DTiling, area::Union{Rect,Extent})
area = typeof(area) <: Rect ? Extents.extent(area) : area
# `depth` determines the number of layers below the current
# layer to load. Tiles are downloaded in order from lowest to highest zoom.
depth = scheme.depth

# Calculate the zoom level
# TODO, also early return if too many tiles to plot?
ideal_zoom, zoom, approx_ntiles = Tyler.optimal_zoom(m, norm(widths(to_rect(area))))
ideal_zoom, zoom, approx_ntiles = optimal_zoom(m, norm(widths(to_rect(area))))
m.zoom[] = zoom

# And the z layers we will plot
layer_range = max(Tyler.min_zoom(m), zoom - depth):zoom
layer_range = max(min_zoom(m), zoom - depth):zoom
# Get the tiles around the mouse first
xpos, ypos = Makie.mouseposition(m.axis.scene)
# transform the mouse position to tile CRS
# TODO: we should instead transform areas after they are calculated in the axis's CRS
xpos, ypos = Makie.apply_transform(GeoMakie.create_transform(m.crs, m.axis.dest[]), Point2f(xpos, ypos))
# Use the closest in-bounds point
xpos = max(min(xpos, area.X[2]), area.X[1])
ypos = max(min(ypos, area.Y[2]), area.Y[1])
# Define a 1% resolution extent around the mouse
xspan = (area.X[2] - area.X[1]) * 0.01
yspan = (area.Y[2] - area.Y[1]) * 0.01
mouse_area = Extents.Extent(; X=(xpos - xspan, xpos + xspan), Y=(ypos - yspan, ypos + yspan))
# Make a halo around the mouse tile to load next, intersecting area so we don't download outside the plot
mouse_halo_area = Tyler.grow_extent(mouse_area, 10)
# Define a halo around the area to download last, so pan/zoom are filled already
halo_area = Tyler.grow_extent(area, scheme.halo) # We don't mind that the middle tiles are the same, the OrderedSet will remove them

# transform the areas to tile crs

# Define all the tiles in the order they will load in
background_areas = if Extents.intersects(mouse_halo_area, area)
mha = Extents.intersection(mouse_halo_area, area)
if Extents.intersects(mouse_area, area)
[Extents.intersection(mouse_area, area), mha, halo_area]
else
[mha, halo_area]
end
else
[halo_area]
end
halo_area = grow_extent(area, scheme.halo) # We don't mind that the middle tiles are the same, the OrderedSet will remove them

foreground = OrderedSet{Tile}(MapTiles.TileGrid(area, zoom, m.crs))
# Set up empty tile lists
foreground = OrderedSet{Tile}()
background = OrderedSet{Tile}()
offscreen = OrderedSet{Tile}()
# Fill tiles for each z layer
for z in layer_range
z == zoom && continue
for ext in background_areas
union!(background, MapTiles.TileGrid(ext, z, m.crs))
# Get rings of tiles around the mouse, intersecting
# area so we don't get tiles outside the plot
for ext_scale in 1:4:100
# Get an extent
mouse_halo_area = grow_extent(mouse_area, ext_scale)
# Check if it intersects the plot area
ext = Extents.intersection(mouse_halo_area, area)
# No intersection so continue
isnothing(ext) && continue
tilegrid = MapTiles.TileGrid(ext, z, m.crs)
if z == zoom
union!(foreground, tilegrid)
else
union!(background, tilegrid)
end
end
# Get the halo ring tiles to load offscreen
area_grid = MapTiles.TileGrid(area, z, m.crs)
halo_grid = MapTiles.TileGrid(halo_area, z, m.crs)
# Remove tiles inside the area grid
halo_tiles = setdiff(halo_grid, area_grid)
# Update the offscreen tiles set
union!(offscreen, halo_tiles)
end
return foreground, background
tiles = (; foreground, background, offscreen)
# Reverse the order of the groups. Reversing the ranges
# above doesn't have the same effect due to then unions
return map(OrderedSet ∘ reverse ∘ collect, tiles)
end
10 changes: 5 additions & 5 deletions src/map.jl
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ struct Map{Ax<:Makie.AbstractAxis} <: AbstractMap
# The tile downloader + cacher
tiles::TileCache
# The tiles for the current zoom level - we may plot many more than this
current_tiles::ThreadSafeDict{Tile,Bool}
foreground_tiles::ThreadSafeDict{Tile,Bool}
# The plots we have created but are not currently visible and can be reused
unused_plots::Vector{Makie.Plot}
# All tile plots we're currently plotting
Expand Down Expand Up @@ -113,7 +113,7 @@ toggle_visibility!(m::Map) = m.axis.scene.visible[] = !m.axis.scene.visible[]

function Base.close(m::Map)
cleanup_queue!(m, OrderedSet{Tile}())
empty!(m.current_tiles)
empty!(m.foreground_tiles)
empty!(m.unused_plots)
empty!(m.plots)
empty!(m.should_get_plotted)
Expand Down Expand Up @@ -148,7 +148,7 @@ function Map(extent, extent_crs=wgs84;

plots = ThreadSafeDict{String,Tuple{Makie.Plot,Tile,Rect}}()
should_get_plotted = ThreadSafeDict{String,Tile}()
current_tiles = ThreadSafeDict{Tile,Bool}()
foreground_tiles = ThreadSafeDict{Tile,Bool}()
unused_plots = Makie.Plot[]
display_task = Base.RefValue{Task}()

Expand All @@ -157,7 +157,7 @@ function Map(extent, extent_crs=wgs84;
axis,
plot_config,
tiles,
current_tiles,
foreground_tiles,
unused_plots,
plots,
should_get_plotted,
Expand Down Expand Up @@ -207,7 +207,7 @@ function Base.wait(m::AbstractMap; timeout=50)
wait(m.tiles; timeout=timeout)
start = time()
while true
tile_keys = Set(tile_key.((m.provider,), keys(m.current_tiles)))
tile_keys = Set(tile_key.((m.provider,), keys(m.foreground_tiles)))
if all(k -> haskey(m.plots, k), tile_keys)
break
end
Expand Down
112 changes: 68 additions & 44 deletions src/tile-fetching.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,54 +30,57 @@ end

function update_tiles!(m::Map, arealike)
# Get the tiles to be plotted from the fetching scheme and arealike
new_tiles_set, background_tiles = get_tiles_for_area(m, m.fetching_scheme, arealike)
if length(new_tiles_set) > m.max_plots
tiles = get_tiles_for_area(m, m.fetching_scheme, arealike)
if length(tiles.foreground) > m.max_plots
@warn "Too many tiles to plot, which means zoom level is not supported. Plotting no tiles for this zoomlevel." maxlog = 1
new_tiles_set = OrderedSet{Tile}()
background_tiles = OrderedSet{Tile}()
empty!(tiles.foreground)
empty!(tiles.background)
empty!(tiles.offscreen)
end
queued_or_plotted = values(m.should_get_plotted)
to_add = setdiff(new_tiles_set, queued_or_plotted)
# Queue tiles to be downloaded & displayed
to_add = map(t -> setdiff(t, queued_or_plotted), tiles)

# We don't add any background tile to the current_tiles, so they stay shifted to the back
# They get added async, so at this point `to_add` won't be in currently_plotted yet
will_be_plotted = union(new_tiles_set, queued_or_plotted)
# replace
empty!(m.current_tiles)
for tile in new_tiles_set
m.current_tiles[tile] = true
empty!(m.foreground_tiles)
for tile in tiles.foreground
m.foreground_tiles[tile] = true
end

# Move all plots to the back, that aren't in the newest tileset anymore
for (key, (plot, tile, bounds)) in m.plots
dist = abs(m.zoom[] - tile.z)
if haskey(m.current_tiles, tile)
if haskey(m.foreground_tiles, tile)
move_in_front!(plot, dist, bounds)
else
move_to_back!(plot, dist, bounds)
end
end

# Queue tiles to be downloaded & displayed
to_add_background = setdiff(background_tiles, will_be_plotted)
# Remove any item from queue, that isn't in the new set
to_keep = union(background_tiles, will_be_plotted)
to_keep_queued = union(tiles...)
# Remove all tiles that are not in the new set from the queue
cleanup_queue!(m, to_keep)
cleanup_queue!(m, to_keep_queued)

# The unique is needed to avoid tiles referencing the same tile
# TODO, we should really consider to disallow this for tile providers,
# This is currently only allowed because of the PointCloudProvider
background = unique(t -> tile_key(m.provider, t), to_add_background)
foreground = unique(t -> tile_key(m.provider, t), to_add)
to_add_keys = map(to_add) do ta
unique(t -> tile_key(m.provider, t), ta)
end

# We lock the queue, to put all tiles in one go into the tile queue
# Since download workers take the last tiles first, foreground tiles go last
# Without the lock, a few (n_download_threads) background tiles would be downloaded first,
# since they will be the last in the queue until we add the foreground tiles
# Without the lock, a few (n_download_threads) old tiles will be downloaded first
# since they will be the last in the queue until we add the new tiles
lock(m.tiles.tile_queue) do
foreach(tile -> queue_plot!(m, tile), background)
foreach(tile -> queue_plot!(m, tile), foreground)
# Offscreen tiles show last, so scroll and zoom don't show
# empty white areas or low resolution tiles
foreach(tile -> queue_plot!(m, tile), to_add_keys.offscreen)
# Foreground tiles show in the middle, filling out details
foreach(tile -> queue_plot!(m, tile), to_add_keys.foreground)
# Lower-resolution background tiles show first
# Its quick to get them and they immediately fill the plot
foreach(tile -> queue_plot!(m, tile), to_add_keys.background)
end
end

Expand Down Expand Up @@ -107,34 +110,50 @@ function get_tiles_for_area(m::Map{Axis}, scheme::Halo2DTiling, area::Union{Rect
layer_range = max(min_zoom(m), zoom - depth):zoom
# Get the tiles around the mouse first
xpos, ypos = Makie.mouseposition(m.axis.scene)
# Use the closest in-bounds point
xpos = max(min(xpos, area.X[2]), area.X[1])
ypos = max(min(ypos, area.Y[2]), area.Y[1])
# Define a 1% resolution extent around the mouse
xspan = (area.X[2] - area.X[1]) * 0.01
yspan = (area.Y[2] - area.Y[1]) * 0.01
mouse_area = Extents.Extent(; X=(xpos - xspan, xpos + xspan), Y=(ypos - yspan, ypos + yspan))
# Make a halo around the mouse tile to load next, intersecting area so we don't download outside the plot
mouse_halo_area = grow_extent(mouse_area, 10)
# Define a halo around the area to download last, so pan/zoom are filled already
halo_area = grow_extent(area, scheme.halo) # We don't mind that the middle tiles are the same, the OrderedSet will remove them
# Define all the tiles in the order they will load in
background_areas = if Extents.intersects(mouse_halo_area, area)
mha = Extents.intersection(mouse_halo_area, area)
if Extents.intersects(mouse_area, area)
[Extents.intersection(mouse_area, area), mha, halo_area]
else
[mha, halo_area]
end
else
[halo_area]
end

foreground = OrderedSet{Tile}(MapTiles.TileGrid(area, zoom, m.crs))
# Set up empty tile lists
foreground = OrderedSet{Tile}()
background = OrderedSet{Tile}()
offscreen = OrderedSet{Tile}()
# Fill tiles for each z layer
for z in layer_range
z == zoom && continue
for ext in background_areas
union!(background, MapTiles.TileGrid(ext, z, m.crs))
# Get rings of tiles around the mouse, intersecting
# area so we don't get tiles outside the plot
for ext_scale in 1:4:100
# Get an extent
mouse_halo_area = grow_extent(mouse_area, ext_scale)
# Check if it intersects the plot area
ext = Extents.intersection(mouse_halo_area, area)
# No intersection so continue
isnothing(ext) && continue
tilegrid = MapTiles.TileGrid(ext, z, m.crs)
if z == zoom
union!(foreground, tilegrid)
else
union!(background, tilegrid)
end
end
# Get the halo ring tiles to load offscreen
area_grid = MapTiles.TileGrid(area, z, m.crs)
halo_grid = MapTiles.TileGrid(halo_area, z, m.crs)
# Remove tiles inside the area grid
halo_tiles = setdiff(halo_grid, area_grid)
# Update the offscreen tiles set
union!(offscreen, halo_tiles)
end
return foreground, background
tiles = (; foreground, background, offscreen)
# Reverse the order of the groups. Reversing the ranges
# above doesn't have the same effect due to then unions
return map(OrderedSet ∘ reverse ∘ collect, tiles)
end

#########################################################################################
Expand All @@ -149,7 +168,10 @@ function get_tiles_for_area(m::Map, ::SimpleTiling, area::Union{Rect,Extent})
# Calculate the zoom level
ideal_zoom, zoom, approx_ntiles = optimal_zoom(m, diag)
m.zoom[] = zoom
return OrderedSet{Tile}(MapTiles.TileGrid(area, zoom, m.crs)), OrderedSet{Tile}()
foreground = OrderedSet{Tile}(MapTiles.TileGrid(area, zoom, m.crs))
background = OrderedSet{Tile}()
offscreen = OrderedSet{Tile}()
return (; foreground, background, offscreen)
end

#########################################################################################
Expand All @@ -165,9 +187,11 @@ function get_tiles_for_area(m::Map{LScene}, ::Tiling3D, (cam, camc)::Tuple{Camer
camc.far[] = maxdist
camc.near[] = eyepos[3] * 0.01
update_cam!(m.axis.scene)
return tiles_from_poly(m, points), OrderedSet{Tile}()
foreground = tiles_from_poly(m, points)
background = OrderedSet{Tile}()
offscreen = OrderedSet{Tile}()
return (; foreground, background, offscreen)
end

function get_tiles_for_area(m::Map{LScene}, s::SimpleTiling, (cam, camc)::Tuple{Camera,Camera3D})
area = area_around_lookat(camc)
return get_tiles_for_area(m, s, area)
Expand Down
10 changes: 5 additions & 5 deletions src/tile-plotting.jl
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,15 @@ function filter_overlapping!(m::Map, bounds::Rect3, tile, key)
other_bounds2d = Rect2d(other_bounds)
# If overlap
if bounds2d in other_bounds2d || other_bounds2d in bounds2d
if haskey(m.current_tiles, tile)
if haskey(m.foreground_tiles, tile)
# the new plot has priority since it's in the newest current tile set
remove_plot!(m, other_key)
elseif haskey(m.current_tiles, other_tile)
elseif haskey(m.foreground_tiles, other_tile)
delete!(m.should_get_plotted, key)
# the existing plot has priority so we skip the new plot
return true
else
# If both are not in current_tiles, we remove the plot farthest away from the current zoom level
# If both are not in foreground_tiles, we remove the plot farthest away from the current zoom level
if abs(tile.z - m.zoom[]) <= abs(other_tile.z - m.zoom[])
remove_plot!(m, other_key)
else
Expand All @@ -111,7 +111,7 @@ function cull_plots!(m::Map)
if length(m.plots) >= (m.max_plots - 1)
# remove the oldest plot
p_tiles = plotted_tiles(m)
available_to_remove = setdiff(p_tiles, keys(m.current_tiles))
available_to_remove = setdiff(p_tiles, keys(m.foreground_tiles))
sort!(available_to_remove, by=tile -> abs(tile.z - m.zoom[]))
n_avail = length(available_to_remove)
need_to_remove = min(n_avail, length(m.plots) - m.max_plots)
Expand Down Expand Up @@ -154,7 +154,7 @@ function create_tile_plot!(m::AbstractMap, tile::Tile, data)
update_tile_plot!(mplot, cfg, m.axis, data_processed, bounds, (tile, m.crs))
end

if haskey(m.current_tiles, tile)
if haskey(m.foreground_tiles, tile)
move_in_front!(mplot, abs(m.zoom[] - tile.z), bounds)
else
move_to_back!(mplot, abs(m.zoom[] - tile.z), bounds)
Expand Down
1 change: 1 addition & 0 deletions src/tiles.jl
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ function TileCache(provider; cache_size_gb=5, max_parallel_downloads=1)
fetched_tiles = LRU{String,Union{Nothing, TileFormat}}(; maxsize=cache_size_gb * 10^9, by=Base.summarysize)
downloaded_tiles = Channel{Tuple{Tile,Union{Nothing, TileFormat}}}(Inf)
tile_queue = Channel{Tile}(Inf)

async = Threads.nthreads(:default) <= 1
if async && max_parallel_downloads > 1
@warn "Multiple download threads are not supported with Threads.nthreads()==1, falling back to async. Start Julia with more threads for parallel downloads."
Expand Down
10 changes: 5 additions & 5 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,22 @@ london = Rect2f(-0.0921, 51.5, 0.04, 0.025)
m = Tyler.Map(london); m.figure.scene
s = display(m) # waits until all tiles are displayed
@test isempty(m.tiles.tile_queue)
@test length(m.current_tiles) == 25
@test length(m.foreground_tiles) == 25
@test length(m.tiles.fetched_tiles) == 48

london = Rect2f(-0.0921, 51.5, 0.04, 0.025)
# TODO: Google in WGS84 doesn't really make sense
m = Tyler.Map(london; scale=1, provider=Tyler.TileProviders.Google(), crs=Tyler.MapTiles.WGS84()) # waits until all tiles are displayed
s = display(m) # waits until all tiles are displayed
@test isempty(m.tiles.tile_queue)
@test length(m.current_tiles) == 35
@test length(m.tiles.fetched_tiles) == 66
@test length(m.foreground_tiles) == 35
@test length(m.tiles.fetched_tiles) == 71

# test Extent input
london = Extents.Extent(X=(-0.0921, -0.0521), Y=(51.5, 51.525))
m = Tyler.Map(london; scale=1) # waits until all tiles are displayed
display(m)
@test isempty(m.tiles.tile_queue)
@test length(m.current_tiles) == 25
@test length(m.foreground_tiles) == 25
@test length(m.tiles.fetched_tiles) == 48

@testset "Interfaces" begin
Expand Down
Loading
Loading