From 27dc936dc9e45cfafd12ac678d03c71de64bc52c Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Mon, 23 Dec 2024 18:43:56 +0530 Subject: [PATCH] Activate GeoAxis Maps using a GeoMakie extension (#114) * add an extension to GeoMakie so that Tyler works * Make this an actual extension * Fix Map docstring to render correctly * Remove unused import from Makie for compat with v0.22 * Set up axis with correct limits --- Project.toml | 7 +++ ext/TylerGeoMakieExt/TylerGeoMakieExt.jl | 50 ++++++++++++++++++ ext/TylerGeoMakieExt/tile-fetching.jl | 66 ++++++++++++++++++++++++ ext/TylerGeoMakieExt/tile-plotting.jl | 53 +++++++++++++++++++ src/Tyler.jl | 2 +- src/map.jl | 26 +++++----- 6 files changed, 190 insertions(+), 14 deletions(-) create mode 100644 ext/TylerGeoMakieExt/TylerGeoMakieExt.jl create mode 100644 ext/TylerGeoMakieExt/tile-fetching.jl create mode 100644 ext/TylerGeoMakieExt/tile-plotting.jl diff --git a/Project.toml b/Project.toml index dfb5f3f9..5fe7a624 100644 --- a/Project.toml +++ b/Project.toml @@ -27,6 +27,12 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" ThreadSafeDicts = "4239201d-c60e-5e0a-9702-85d713665ba7" TileProviders = "263fe934-28e1-4ae9-998a-c2629c5fede6" +[weakdeps] +GeoMakie = "db073c08-6b98-4ee5-b6a4-5efafb3259c6" + +[extensions] +TylerGeoMakieExt = ["GeoMakie"] + [compat] ArchGDAL = "0.10" Colors = "0.12, 0.13" @@ -35,6 +41,7 @@ Extents = "0.1.2" FileIO = "1" GeoFormatTypes = "0.4" GeoInterface = "1" +GeoMakie = "0.7.9" GeometryBasics = "0.4, 0.5" GeometryOps = "0.1" HTTP = "1" diff --git a/ext/TylerGeoMakieExt/TylerGeoMakieExt.jl b/ext/TylerGeoMakieExt/TylerGeoMakieExt.jl new file mode 100644 index 00000000..eff41108 --- /dev/null +++ b/ext/TylerGeoMakieExt/TylerGeoMakieExt.jl @@ -0,0 +1,50 @@ +module TylerGeoMakieExt + +using Tyler, GeoMakie + +using LinearAlgebra, OrderedCollections + +import Tyler: tile_reloader, create_tileplot!, update_tile_plot!, + Map, AbstractMap, ImageData, PlotConfig, DebugPlotConfig, + SimpleTiling, Halo2DTiling + +using Tyler: to_rect + + +using Makie, Makie.GeometryBasics + +using Makie: AbstractAxis, Mat + +using Extents +using TileProviders, MapTiles + +function Tyler.setup_axis!(axis::GeoAxis, ext_target, crs) + X = ext_target.X + Y = ext_target.Y + + # Set the axis's limits + rect = Rect2f((X[1], Y[1]), (X[2] - X[1], Y[2] - Y[1])) + transf = GeoMakie.create_transform(axis.source[], crs) + transformed_limits = Makie.apply_transform(transf, rect) + + tXmin, tYmin = transformed_limits.origin + tXmax, tYmax = transformed_limits.origin .+ transformed_limits.widths + + axis.limits[] = (tXmin, tXmax, tYmin, tYmax) + Makie.reset_limits!(axis) + + # axis.elements[:background].depth_shift[] = 0.1f0 + # translate!(axis.elements[:background], 0, 0, -1000) + # axis.elements[:background].color = :transparent + # axis.xgridvisible = false + # axis.ygridvisible = false + return +end + +# functions directly related to plotting +include("tile-plotting.jl") + +# functions related to fetching tiles +include("tile-fetching.jl") + +end \ No newline at end of file diff --git a/ext/TylerGeoMakieExt/tile-fetching.jl b/ext/TylerGeoMakieExt/tile-fetching.jl new file mode 100644 index 00000000..f51a2814 --- /dev/null +++ b/ext/TylerGeoMakieExt/tile-fetching.jl @@ -0,0 +1,66 @@ +function Tyler.tile_reloader(m::Map{GeoAxis}) + axis = m.axis + throttled = Makie.Observables.throttle(0.2, axis.finallimits) + map_inverse_transform = lift(axis.scene, axis.dest) do dest + GeoMakie.create_transform(#= destination =# m.crs, #= source =# dest) + end + + onany(axis.scene, map_inverse_transform, throttled; update=true) do ax2map, axis_finallimits + new_extent = Makie.apply_transform(ax2map, axis_finallimits) + Tyler.update_tiles!(m, new_extent) + return + 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}) + 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)))) + m.zoom[] = zoom + + # And the z layers we will plot + layer_range = max(Tyler.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)) + 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 + + foreground = OrderedSet{Tile}(MapTiles.TileGrid(area, zoom, m.crs)) + background = OrderedSet{Tile}() + for z in layer_range + z == zoom && continue + for ext in background_areas + union!(background, MapTiles.TileGrid(ext, z, m.crs)) + end + end + return foreground, background +end diff --git a/ext/TylerGeoMakieExt/tile-plotting.jl b/ext/TylerGeoMakieExt/tile-plotting.jl new file mode 100644 index 00000000..257e49b9 --- /dev/null +++ b/ext/TylerGeoMakieExt/tile-plotting.jl @@ -0,0 +1,53 @@ +######################################## +# PlotConfig # +######################################## + + +function Tyler.create_tileplot!(config::PlotConfig, axis::GeoAxis, data::ImageData, bounds::Rect, tile_crs) + mini, maxi = extrema(bounds) + plot = GeoMakie.meshimage!( + axis, + (mini[1], maxi[1]), (mini[2], maxi[2]), data; + uv_transform=:rotl90, + inspectable=false, + reset_limits=false, # substitute for plotting directly to scene, since GeoMakie handles the CRS stuff + source = tile_crs[2], # tile_crs is a tuple of (tile, crs), we can pass the CRS directly to GeoMakie though + config.attributes... + ) + return plot +end + +function Tyler.update_tile_plot!(plot::GeoMakie.MeshImage, ::PlotConfig, axis::AbstractAxis, data::ImageData, bounds::Rect, tile_crs) + mini, maxi = extrema(bounds) + plot[1] = (mini[1], maxi[1]) + plot[2] = (mini[2], maxi[2]) + plot[3] = data + return +end + + +######################################## +# DebugPlotConfig # +######################################## + +function create_tileplot!(config::DebugPlotConfig, axis::GeoAxis, data::ImageData, bounds::Rect, tile_crs) + plot = Makie.poly!( + axis, + bounds; + color=reverse(data; dims=1), + strokewidth=2, + strokecolor=:black, + inspectable=false, + stroke_depth_shift=-0.01f0, + reset_limits=false, # substitute for plotting directly to scene, since GeoMakie handles the CRS stuff + source = tile_crs[2], # tile_crs is a tuple of (tile, crs), we can pass the CRS directly to GeoMakie though + config.attributes... + ) + return plot +end + +function update_tile_plot!(plot::Makie.Poly, ::DebugPlotConfig, axis::GeoAxis, data::ImageData, bounds::Rect, tile_crs) + plot[1] = bounds + plot.color = reverse(data; dims=1) + return +end \ No newline at end of file diff --git a/src/Tyler.jl b/src/Tyler.jl index 3b3770e0..f905ab67 100644 --- a/src/Tyler.jl +++ b/src/Tyler.jl @@ -7,7 +7,7 @@ using GeometryBasics: GeometryBasics, GLTriangleFace, Point2f, Vec2f, Rect2f, Re using HTTP: HTTP using LRUCache: LRUCache, LRU using MapTiles: MapTiles, Tile, TileGrid, web_mercator, wgs84, CoordinateReferenceSystemFormat -using Makie: Makie, Observable, Figure, Axis, LScene, RGBAf, on, isopen, meta, mesh!, translate!, scale!, Plot +using Makie: Makie, Observable, Figure, Axis, LScene, RGBAf, on, isopen, mesh!, translate!, scale!, Plot using OrderedCollections: OrderedCollections, OrderedSet using ThreadSafeDicts: ThreadSafeDicts, ThreadSafeDict using TileProviders: TileProviders, AbstractProvider, geturl, min_zoom, max_zoom diff --git a/src/map.jl b/src/map.jl index 0eb2b342..d3b141c4 100644 --- a/src/map.jl +++ b/src/map.jl @@ -14,24 +14,24 @@ When layering providers over each other with `Map(map::Map; ...)`, you can use ` # Arguments --`extent`: the initial extent of the map, as a `GeometryBasics.Rect` +- `extent`: the initial extent of the map, as a `GeometryBasics.Rect` or an `Extents.Extent` in the projection of `extent_crs`. --`extent_crs`: Any `GeoFormatTypes` compatible crs, the default is wsg84. +- `extent_crs`: Any `GeoFormatTypes` compatible crs, the default is wsg84. # Keywords --`size`: The figure size. --`figure`: an existing `Makie.Figure` object. --`crs`: The providers coordinate reference system. --`provider`: a TileProviders.jl `Provider`. --`max_parallel_downloads`: limits the attempted simultaneous downloads, with a default of `16`. --`cache_size_gb`: limits the cache for storing tiles, with a default of `5`. --`fetching_scheme=Halo2DTiling()`: The tile fetching scheme. Can be SimpleTiling(), Halo2DTiling(), or Tiling3D(). --`scale`: a tile scaling factor. Low number decrease the downloads but reduce the resolution. +- `size`: The figure size. +- `figure`: an existing `Makie.Figure` object. +- `crs`: The providers coordinate reference system. +- `provider`: a TileProviders.jl `Provider`. +- `max_parallel_downloads`: limits the attempted simultaneous downloads, with a default of `16`. +- `cache_size_gb`: limits the cache for storing tiles, with a default of `5`. +- `fetching_scheme=Halo2DTiling()`: The tile fetching scheme. Can be SimpleTiling(), Halo2DTiling(), or Tiling3D(). +- `scale`: a tile scaling factor. Low number decrease the downloads but reduce the resolution. The default is `0.5`. --`plot_config`: A `PlotConfig` object to change the way tiles are plotted. --`max_zoom`: The maximum zoom level to display, with a default of `TileProviders.max_zoom(provider)`. --`max_plots=400:` The maximum number of plots to keep displayed at the same time. +- `plot_config`: A `PlotConfig` object to change the way tiles are plotted. +- `max_zoom`: The maximum zoom level to display, with a default of `TileProviders.max_zoom(provider)`. +- `max_plots=400:` The maximum number of plots to keep displayed at the same time. """ struct Map{Ax<:Makie.AbstractAxis} <: AbstractMap provider::AbstractProvider