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

add a function to calculate cell size #508

Merged
merged 4 commits into from
Sep 1, 2023
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
3 changes: 2 additions & 1 deletion ext/RastersArchGDALExt/RastersArchGDALExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ using Rasters: GDALsource, AbstractProjected, RasterStackOrArray, FileArray,
RES_KEYWORD, SIZE_KEYWORD, CRS_KEYWORD, FILENAME_KEYWORD, SUFFIX_KEYWORD, EXPERIMENTAL,
GDAL_EMPTY_TRANSFORM, GDAL_TOPLEFT_X, GDAL_WE_RES, GDAL_ROT1, GDAL_TOPLEFT_Y, GDAL_ROT2, GDAL_NS_RES

import Rasters: reproject, resample, warp
import Rasters: reproject, resample, warp, cellsize

const RA = Rasters
const DD = DimensionalData
const DA = DiskArrays
const GI = GeoInterface
const LA = LookupArrays

include("cellsize.jl")
include("gdal_source.jl")
include("reproject.jl")
include("resample.jl")
Expand Down
106 changes: 106 additions & 0 deletions ext/RastersArchGDALExt/cellsize.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""
cellsize(x)

Gives the approximate size of each cell in square km.
This function works for any projection, using an algorithm for polygons on a sphere. It approximates the true size to about 0.1%, depending on latitude.

# Arguments

- `x`: A `Raster` or a `Tuple` of `X` and `Y` dimensions.

## Example

```jldoctest
using Rasters, Rasters.LookupArrays
dimz = X(Projected(90.0:0.1:120; sampling=Intervals(Start()), order = ForwardOrdered(), span = Regular(0.1), crs=EPSG(4326))),
Y(Projected(0.0:0.1:50; sampling=Intervals(Start()), order = ForwardOrdered(), span = Regular(0.1), crs=EPSG(4326)))

cs = cellsize(dimz)

```
$EXPERIMENTAL
"""

function _great_circle_bearing(lon1::AbstractFloat, lat1::AbstractFloat, lon2::AbstractFloat, lat2::AbstractFloat)
dLong = lon1 - lon2

s = cos(lat2)*sin(dLong)
c = cos(lat1)*sin(lat2) - sin(lat1)*cos(lat2)*cos(dLong)

return atan(s, c)
end

## Get the area of a LinearRing with coordinates in radians
# Using Gidard's theorem
function _area_from_rads(ring; R = 6371.0088)
n = GI.npoint(ring)
area = -(n-3)*pi

prevpoint = GI.getpoint(ring, n-1)
point = GI.getpoint(ring, 1)

for i in 2:n
nextpoint = GI.getpoint(ring, i)

beta1 = _great_circle_bearing(GI.x(point), GI.y(point), GI.x(prevpoint), GI.y(prevpoint))
beta2 = _great_circle_bearing(GI.x(point), GI.y(point), GI.x(nextpoint), GI.y(nextpoint))
angle = acos(cos(-beta1)*cos(-beta2) + sin(-beta1)*sin(-beta2))
area += angle

prevpoint = point
point = nextpoint
end

return area*R^2
end

_area_from_coords(transform, geom) = _area_from_coords(transform, GI.trait(geom), geom)
_area_from_coords(geom) = _area_from_coords(GI.trait(geom), geom)
function _area_from_coords(::GI.LinearRingTrait, ring) # no ArchGDAL, assumes degrees
points = map(GI.getpoint(ring)) do p
(deg2rad(GI.x(p)), deg2rad(GI.y(p)))
end

return _area_from_rads(GI.LinearRing(points))
end
function _area_from_coords(transform::ArchGDAL.CoordTransform, ::GI.LinearRingTrait, ring)
points = map(GI.getpoint(ring)) do p
t = ArchGDAL.transform!(ArchGDAL.createpoint(p...), transform)
(deg2rad(GI.x(t)), deg2rad(GI.y(t)))
end
return _area_from_rads(GI.LinearRing(points))
end

function cellsize(dims::Tuple{X, Y})
xbnds, ybnds = DimensionalData.intervalbounds(dims)
if convert(CoordSys, crs(dims)) == CoordSys("Earth Projection 1, 104") # check if need to reproject
areas = [_area_from_coords(
GI.LinearRing([
(xb[1], yb[1]),
(xb[2], yb[1]),
(xb[2], yb[2]),
(xb[1], yb[2]),
(xb[1], yb[1])
]))
for xb in xbnds, yb in ybnds]
else
areas = ArchGDAL.crs2transform(crs(dims), EPSG(4326)) do transform
[_area_from_coords(
transform,
GI.LinearRing([
(xb[1], yb[1]),
(xb[2], yb[1]),
(xb[2], yb[2]),
(xb[1], yb[2]),
(xb[1], yb[1])
]))
for xb in xbnds, yb in ybnds]
end
end

return Raster(areas, dims)
end

function cellsize(x::Raster)
cellsize(dims(x, (X, Y)))
end
2 changes: 1 addition & 1 deletion src/Rasters.jl
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export missingval, boolmask, missingmask, replace_missing, replace_missing!,
aggregate, aggregate!, disaggregate, disaggregate!, mask, mask!,
resample, warp, zonal, crop, extend, trim, slice, combine, points,
classify, classify!, mosaic, mosaic!, extract, rasterize, rasterize!,
coverage, coverage!, setcrs, setmappedcrs, smapseries
coverage, coverage!, setcrs, setmappedcrs, smapseries, cellsize
export crs, mappedcrs, mappedindex, mappedbounds, projectedindex, projectedbounds
export reproject, convertlookup

Expand Down
29 changes: 11 additions & 18 deletions src/extensions.jl
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
# extensions

# stubs that need ArchGDAL
function resample(args...; kw...)
function throw_extension_error(f::Function, package::String, extension::Symbol, args)
@static if isdefined(Base, :get_extension) # julia > 1.9
if isnothing(Base.get_extension(Rasters, :RastersArchGDALExt))
throw(BackendException("ArchGDAL"))
else
throw(MethodError(resample, args))
end
if isnothing(Base.get_extension(Rasters, extension))
throw(BackendException(package))
else
throw(BackendException("ArchGDAL"))
throw(MethodError(f, args))
end
end
function warp(args...; kw...)
@static if isdefined(Base, :get_extension) # julia > 1.9
if isnothing(Base.get_extension(Rasters, :RastersArchGDALExt))
throw(BackendException("ArchGDAL"))
else
throw(MethodError(warp, args))
end
else
throw(BackendException("ArchGDAL"))
throw(BackendException(package))
end
end


# stubs that need ArchGDAL
resample(args...; kw...) = throw_extension_error(resample, "ArchGDAL", :RastersArchGDALExt, args)
warp(args...; kw...) = throw_extension_error(warp, "ArchGDAL", :RastersArchGDALExt, args)
cellsize(args...; kw...) = throw_extension_error(cellsize, "ArchGDAL", :RastersArchGDALExt, args)

# Other shared stubs
function layerkeys end
function smapseries end
Expand Down
25 changes: 25 additions & 0 deletions test/cellsize.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Rasters, Rasters.LookupArrays, ArchGDAL
using Test

include(joinpath(dirname(pathof(Rasters)), "../test/test_utils.jl"))

@testset "cellsize" begin
dimz = X(Projected(90.0:0.1:99.9; sampling=Intervals(Start()), order = ForwardOrdered(), span = Regular(0.1), crs=EPSG(4326))),
Y(Projected(0.0:0.1:89.9; sampling=Intervals(Start()), order = ForwardOrdered(), span = Regular(0.1), crs=EPSG(4326)))

dimz_25832 = X(Projected(0.0:100:10000.0; sampling=Intervals(Start()), order = ForwardOrdered(), span = Regular(100), crs=EPSG(25832))),
Y(Projected(0.0:100:10000.0; sampling=Intervals(Start()), order = ForwardOrdered(), span = Regular(100), crs=EPSG(25832)))

cs = cellsize(dimz)
cs2 = cellsize(dimz_25832)

# Check the output is a raster
@test cs isa Raster
@test cs2 isa Raster
# Test that the total area matches the expected area (1/72th of the Earth surface)
@test sum(cs) ≈ 510.1e6/72 rtol = 0.01
# Test all areas are about 0.01 km2
@test maximum(cs2) ≈ 0.01 rtol = 0.01
@test minimum(cs2) ≈ 0.01 rtol = 0.01

end