diff --git a/Project.toml b/Project.toml index 7f3807f..7aa8d2c 100644 --- a/Project.toml +++ b/Project.toml @@ -32,7 +32,7 @@ PyramidSchemeMakieExt = "Makie" [compat] Aqua = "0.8" ArchGDAL = "0.10.4" -CairoMakie = "0.12" +CairoMakie = "0.12,0.13,0.14,0.15" Colors = "0.12" DimensionalData = "0.28, 0.29" DiskArrayEngine = "0.1.2, 0.2" @@ -42,13 +42,13 @@ Extents = "0.1.3" FillArrays = "1.11" Flatten = "0.4.3" GeoInterface = "1.3.4" -Makie = "0.21.2, 0.22, 0.23, 0.24" +Makie = "0.24" OffsetArrays = "1.14" Proj = "1.7.1" -Rasters = "0.11.8, 0.12, 0.13" +Rasters = "0.11.8, 0.12, 0.13, 0.14" Statistics = "1.10" Test = "1.10" -TestItemRunner = "0.2.3" +TestItemRunner = "1" YAXArrayBase = "0.6.1, 0.7" YAXArrays = "0.5.6, 0.6" Zarr = "0.9.3" diff --git a/ext/PyramidSchemeMakieExt.jl b/ext/PyramidSchemeMakieExt.jl index be3c34f..05d1b8e 100644 --- a/ext/PyramidSchemeMakieExt.jl +++ b/ext/PyramidSchemeMakieExt.jl @@ -1,62 +1,150 @@ module PyramidSchemeMakieExt using Makie: Axis, Colorbar, DataAspect, Figure, FigureAxisPlot, Observable, Relative -using Makie: on, heatmap!, image! -import Makie: plot, plot! +using Makie: on, heatmap!, map! +using Makie.ComputePipeline: add_input! +using Makie + using PyramidScheme: Pyramid, switchkeys, levels, selectlevel, xkey, ykey using DimensionalData: DimensionalData as DD using DimensionalData.Dimensions: XDim, YDim using Extents: Extent, extent, intersects miss2nan(x) = ismissing(x) ? NaN : x -""" - plot(pyramids) -Plot a Pyramid. -This will plot the coarsest resolution level at the beginning and will plot higher resolutions after zooming into the plot. -This is expected to be used with interactive Makie backends. -""" -function plot(pyramid::Pyramid;colorbar=true, size=(1155, 1043), kwargs...) - #This should be converted into a proper recipe for Makie but this would depend on a pyramid type. - fig = Figure(;size) - lon, lat = DD.dims(DD.parent(pyramid)) - ax = Axis(fig[1,1], limits=(extrema(lon), extrema(lat)), aspect=DataAspect()) - hmap = plot!(ax, pyramid;kwargs...) - if colorbar - Colorbar(fig[1,2], hmap, height = Relative(3.5 / 4)) + +# hacks to get around DD hacks that get around Makie issues +for p in (Heatmap, Image, Contour, Contourf, Contour3d, Spy, Surface) + f = Makie.plotkey(p) + @eval begin + function Makie.$f(A::Pyramid; kwargs...) + invoke(Makie.$f, Tuple{AbstractMatrix{<: Any}}, A; kwargs...) + end + function Makie.$f(A::Observable{<: Pyramid}; kwargs...) + invoke(Makie.$f, Tuple{<:Union{<:AbstractMatrix{<: Any}, <:Observable{<: AbstractMatrix{<:Any}}}}, A; kwargs...) + end + Makie.expand_dimensions(::Type{$p}, p::Pyramid) = (p,) + Makie.convert_arguments(::Type{$p}, p::Pyramid) = (p,) end - ax.autolimitaspect = 1 - FigureAxisPlot(fig, ax, hmap) end -function plot!(ax, pyramid::Pyramid;interp=false, kwargs...)#; rastercrs=crs(parent(pyramid)),plotcrs=EPSG(3857), kwargs...) - tip = levels(pyramid)[end-2][:,:] - #@show typeof(tip) - subtypes = Union{typeof.(pyramid.levels)..., typeof(parent(pyramid))} - data = Observable{DD.AbstractDimMatrix}(tip) - xval = only(values(extent(pyramid, XDim))) - yval = only(values(extent(pyramid, YDim))) - rasdataext = Extent(X=xval, Y=yval) - rasext = extent(pyramid) - on(ax.scene.viewport) do viewport - limext = extent(ax.finallimits[]) - - datalimit = switchkeys(limext, rasext) - data.val = miss2nan.(selectlevel(pyramid, datalimit, target_imsize=viewport.widths)) - notify(data) - end - on(ax.finallimits) do limits - limext = extent(limits) - # Compute limit in raster projection - #trans = Proj.Transformation(plotcrs, rastercrs, always_xy=true) - #datalimit = trans_bounds(trans, limext) - datalimit = switchkeys(limext, rasext) - if intersects(rasdataext, limext) - rasdata = selectlevel(pyramid, datalimit, target_imsize=ax.scene.viewport[].widths) - # Project selected data to plotcrs - #data.val = Rasters.resample(rasdata, crs=plotcrs, method=:bilinear ) - data.val = miss2nan.(rasdata) +struct PyramidConversion <: Makie.ConversionTrait end + +function Makie.conversion_trait(::Type{<:Heatmap}, ::Pyramid) + return PyramidConversion() +end + +function Makie.types_for_plot_arguments(::Type{<:Heatmap}, ::PyramidConversion) + return Tuple{<:Pyramid} +end + +Makie.expand_dimensions(::PyramidConversion, p::Pyramid) = (p,) +Makie.convert_arguments(::PyramidConversion, p::Pyramid) = (p,) + + + +Makie.plottype(::Pyramid) = Makie.Heatmap + +function Makie.plot!(plot::Heatmap{<: Tuple{<: Pyramid}}) + #= + Go from a relative space rectangle + to the rectangle in data space and pixel space, + thus getting `ax.finallimits` and `ax.viewport` + respectively. + + This is a bit arcane but probably the simplest thing in the long run. + =# + inputpositions = [Point2f(0, 0), Point2f(1, 1)] + add_input!(plot.attributes, :__pyramid_input_positions, inputpositions) + Makie.register_positions_projected!( + plot; input_space = :relative, output_space = :space, + input_name = :__pyramid_input_positions, + output_name = :__pyramid_dataspace_positions, + ) + Makie.register_positions_projected!( + plot; input_space = :relative, output_space = :pixel, + input_name = :__pyramid_input_positions, + output_name = :__pyramid_pixelspace_positions, + ) + + data = Observable{DD.AbstractDimMatrix}(levels(plot.arg1[])[end-2][:, :]) + onany(plot, plot.arg1, plot.__pyramid_dataspace_positions, plot.__pyramid_pixelspace_positions) do pyramid, datapos, pixelpos + xyext = values.(extent(pyramid, (XDim, YDim))) + xval, yval = first(xyext), last(xyext) + pyramid_data_ext = Extent(X=xval, Y=yval) + pyramid_ext = extent(pyramid) + + data_limits_ext = Extent(X = extrema(first, datapos), Y = extrema(x -> x[2], datapos)) + pixel_widths = Point2f(abs.(pixelpos[2] .- pixelpos[1])) + + datalimit = switchkeys(data_limits_ext, pyramid_ext) + + if intersects(pyramid_data_ext, data_limits_ext) + data[] = miss2nan.( + selectlevel(pyramid, datalimit, target_imsize = pixel_widths) + ) end - notify(data) + nothing end - #@show typeof(data) - hmap = heatmap!(ax, data; interpolate=interp, kwargs...) + + heatmap!(plot, plot.attributes, data) end + +function Makie.data_limits(p::Heatmap{<: Tuple{<: Pyramid}}) + extX, extY = extent(p.arg1[], (XDim, YDim)) + rect = Rect3f((extX[1], extY[1],0), (extX[2] - extX[1], extY[2] - extY[1], 0)) + return rect +end +Makie.boundingbox(p::Heatmap{<: Tuple{<: Pyramid}}, space::Symbol = :data) = Makie.apply_transform_and_model(p, Makie.data_limits(p)) + + +# """ +# plot(pyramids) +# Plot a Pyramid. +# This will plot the coarsest resolution level at the beginning and will plot higher resolutions after zooming into the plot. +# This is expected to be used with interactive Makie backends. +# """ +# function plot(pyramid::Pyramid;colorbar=true, size=(1155, 1043), kwargs...) +# #This should be converted into a proper recipe for Makie but this would depend on a pyramid type. +# fig = Figure(;size) +# lon, lat = DD.dims(DD.parent(pyramid)) +# ax = Axis(fig[1,1], limits=(extrema(lon), extrema(lat)), aspect=DataAspect()) +# hmap = plot!(ax, pyramid;kwargs...) +# if colorbar +# Colorbar(fig[1,2], hmap, height = Relative(3.5 / 4)) +# end +# ax.autolimitaspect = 1 +# FigureAxisPlot(fig, ax, hmap) +# end + +# function plot!(ax, pyramid::Pyramid;interp=false, kwargs...)#; rastercrs=crs(parent(pyramid)),plotcrs=EPSG(3857), kwargs...) +# tip = levels(pyramid)[end-2][:,:] +# #@show typeof(tip) +# subtypes = Union{typeof.(pyramid.levels)..., typeof(parent(pyramid))} +# data = Observable{DD.AbstractDimMatrix}(tip) +# xyext = values.(extent(pyramid, (XDim, YDim))) +# xval = first(xyext) +# yval = last(xyext) +# rasdataext = Extent(X=xval, Y=yval) +# rasext = extent(pyramid) +# on(ax.scene.viewport) do viewport +# limext = extent(ax.finallimits[]) + +# datalimit = switchkeys(limext, rasext) +# data[] = miss2nan.(selectlevel(pyramid, datalimit, target_imsize=viewport.widths)) +# end +# on(ax.finallimits) do limits +# limext = extent(limits) +# # Compute limit in raster projection +# #trans = Proj.Transformation(plotcrs, rastercrs, always_xy=true) +# #datalimit = trans_bounds(trans, limext) +# datalimit = switchkeys(limext, rasext) +# if intersects(rasdataext, limext) +# rasdata = selectlevel(pyramid, datalimit, target_imsize=ax.scene.viewport[].widths) +# # Project selected data to plotcrs +# #data.val = Rasters.resample(rasdata, crs=plotcrs, method=:bilinear ) +# data.val = miss2nan.(rasdata) +# end +# notify(data) +# end +# #@show typeof(data) +# hmap = heatmap!(ax, data; interpolate=interp, kwargs...) +# end end \ No newline at end of file diff --git a/src/PyramidScheme.jl b/src/PyramidScheme.jl index 41e1312..6372346 100644 --- a/src/PyramidScheme.jl +++ b/src/PyramidScheme.jl @@ -275,7 +275,6 @@ function agg_axis(d,n) # TODO this is only correct for points not intervals npoints = cld(length(d), n) half_stepsize = step(d) * (n-1) / 2 - @show half_stepsize sgn = DD.isreverse(d) ? -1 : 1 DD.set(d, LinRange(first(d) + sgn * half_stepsize, last(d) - sgn * half_stepsize, npoints)) end diff --git a/test/runtests.jl b/test/runtests.jl index c2eb717..4adad5b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -42,6 +42,9 @@ end @test size(subpyramid) == (10,10) @test parent(subpyramid) == data[1:10,1:10] fig, axis, h = plot(pyramid) + @test length(axis.scene.plots) == 1 + plot!(axis, pyramid) + @test length(axis.scene.plots) == 2 end @testitem "Pyramid building RGB eltype" begin @@ -137,6 +140,8 @@ end end end + + #= @testitem "Comparing zarr pyramid with tif pyramid" begin using PyramidScheme: PyramidScheme as PS