Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/docs/build/
/docs/src/source/
.vscode/
.claude/
.DS_Store

benchmarks/Manifest.toml
23 changes: 23 additions & 0 deletions GeometryOpsTestHelpers/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name = "GeometryOpsTestHelpers"
uuid = "47ae2730-6264-4af1-919b-a18bc445f019"
authors = ["Anshul Singhvi <[email protected]>"]
version = "0.1.0"

[deps]
GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f"
GeometryOps = "3251bfac-6a57-4b6d-aa61-ac1fef2975ab"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[weakdeps]
ArchGDAL = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3"
GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326"
LibGEOS = "a90b1aa1-3769-5649-ba7e-abc5a9d163eb"

[extensions]
GeometryOpsTestHelpersArchGDALExt = "ArchGDAL"
GeometryOpsTestHelpersGeometryBasicsExt = "GeometryBasics"
GeometryOpsTestHelpersLibGEOSExt = "LibGEOS"

[compat]
julia = "1.9"

35 changes: 35 additions & 0 deletions GeometryOpsTestHelpers/ext/GeometryOpsTestHelpersArchGDALExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module GeometryOpsTestHelpersArchGDALExt

using GeometryOpsTestHelpers
using GeoInterface
using ArchGDAL

function __init__()
# Register ArchGDAL in the test modules list
push!(GeometryOpsTestHelpers.TEST_MODULES, ArchGDAL)
end

# Monkey-patch ArchGDAL to handle polygon conversion correctly
@eval ArchGDAL begin
function GeoInterface.convert(
::Type{T},
type::GeoInterface.PolygonTrait,
geom,
) where {T<:IGeometry}
f = get(lookup_method, typeof(type), nothing)
isnothing(f) && error(
"Cannot convert an object of $(typeof(geom)) with the $(typeof(type)) trait (yet). Please report an issue.",
)
poly = createpolygon()
foreach(GeoInterface.getring(geom)) do ring
xs = GeoInterface.x.(GeoInterface.getpoint(ring)) |> collect
ys = GeoInterface.y.(GeoInterface.getpoint(ring)) |> collect
subgeom = unsafe_createlinearring(xs, ys)
result = GDAL.ogr_g_addgeometrydirectly(poly, subgeom)
@ogrerr result "Failed to add linearring."
end
return poly
end
end

end # module GeometryOpsTestHelpersArchGDALExt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
module GeometryOpsTestHelpersGeometryBasicsExt

using GeometryOpsTestHelpers
using GeoInterface
using GeometryBasics
import GeometryOps as GO

function __init__()
# Register GeometryBasics in the test modules list
push!(GeometryOpsTestHelpers.TEST_MODULES, GeometryBasics)
end

# Monkey-patch GeometryBasics to have correct methods.
# TODO: push this up to GB!

# TODO: remove when GB GI pr lands
function GeoInterface.convert(
::Type{GeometryBasics.LineString},
::GeoInterface.LinearRingTrait,
geom
)
return GeoInterface.convert(GeometryBasics.LineString, GeoInterface.LineStringTrait(), geom)
end
GeometryBasics.geointerface_geomtype(::GeoInterface.LinearRingTrait) = GeometryBasics.LineString

function GeoInterface.convert(::Type{GeometryBasics.Line}, type::GeoInterface.LineTrait, geom)
g1, g2 = GeoInterface.getgeom(geom)
x, y = GeoInterface.x(g1), GeoInterface.y(g1)
if GeoInterface.is3d(geom)
z = GeoInterface.z(g1)
T = promote_type(typeof(x), typeof(y), typeof(z))
return GeometryBasics.Line{3,T}(GeometryBasics.Point{3,T}(x, y, z), GeometryBasics.Point{3,T}(GeoInterface.x(g2), GeoInterface.y(g2), GeoInterface.z(g2)))
else
T = promote_type(typeof(x), typeof(y))
return GeometryBasics.Line{2,T}(GeometryBasics.Point{2,T}(x, y), GeometryBasics.Point{2,T}(GeoInterface.x(g2), GeoInterface.y(g2)))
end
end

# GeometryCollection interface - currently just a large Union
const _ALL_GB_GEOM_TYPES = Union{GeometryBasics.Point, GeometryBasics.LineString, GeometryBasics.Polygon, GeometryBasics.MultiPolygon, GeometryBasics.MultiLineString, GeometryBasics.MultiPoint}
GeometryBasics.geointerface_geomtype(::GeoInterface.GeometryCollectionTrait) = Vector{_ALL_GB_GEOM_TYPES}
function GeoInterface.convert(::Type{Vector{<: _ALL_GB_GEOM_TYPES}}, ::GeoInterface.GeometryCollectionTrait, geoms)
return _ALL_GB_GEOM_TYPES[GeoInterface.convert(GeometryBasics, g) for g in GeoInterface.getgeom(geoms)]
end

function GeoInterface.convert(
::Type{GeometryBasics.LineString},
type::GeoInterface.LineStringTrait,
geom::GeoInterface.Wrappers.LinearRing{false, false, GO.StaticArrays.SVector{N, Tuple{Float64, Float64}}, Nothing, Nothing} where N
)
return GeometryBasics.LineString(GeometryBasics.Point2{Float64}.(collect(geom.geom)))
end

end # module GeometryOpsTestHelpersGeometryBasicsExt
11 changes: 11 additions & 0 deletions GeometryOpsTestHelpers/ext/GeometryOpsTestHelpersLibGEOSExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module GeometryOpsTestHelpersLibGEOSExt

using GeometryOpsTestHelpers
using LibGEOS

function __init__()
# Register LibGEOS in the test modules list
push!(GeometryOpsTestHelpers.TEST_MODULES, LibGEOS)
end

end # module GeometryOpsTestHelpersLibGEOSExt
155 changes: 155 additions & 0 deletions GeometryOpsTestHelpers/src/GeometryOpsTestHelpers.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
module GeometryOpsTestHelpers

import GeometryOps as GO
using GeoInterface
using Test

export @test_implementations, @testset_implementations

# List of test modules - will be populated when extensions load
# GeoInterface is always available as it's a regular dependency
const TEST_MODULES = Module[GeoInterface]

"""
conversion_expr(mod, var, genkey)

Generate an expression to convert a geometry variable to a specific module's type.
Handles special cases for Extents and empty geometries.
"""
function conversion_expr(mod, var, genkey)
quote
$genkey = if $var isa $(GeoInterface.Extents.Extent)
# GeoInterface and LibGEOS support Extents directly
if string(nameof($mod)) in ("GeoInterface", "LibGEOS")
$var
else
$GeoInterface.convert($mod, $(GO.extent_to_polygon)($var))
end
# These modules do not support empty geometries.
# GDAL does but AG does not
elseif string(nameof($mod)) in ("GeoInterface", "ArchGDAL", "GeometryBasics") && $GeoInterface.isempty($var)
$var
else
$GeoInterface.convert($mod, $var)
end
end
end

"""
@test_implementations(code)
@test_implementations(modules, code)

Macro to run a block of `code` for multiple modules, using GeoInterface.convert
for each variable prefixed with `\$` in the code block.

# Examples
```julia
point = GI.Point(1.0, 2.0)
@test_implementations begin
\$point isa GeoInterface.AbstractGeometry
end
```
"""
macro test_implementations(code::Expr)
_test_implementations_inner(TEST_MODULES, code)
end
macro test_implementations(modules::Union{Expr,Vector}, code::Expr)
_test_implementations_inner(modules, code)
end

function _test_implementations_inner(modules::Union{Expr,Vector}, code::Expr)
vars = Dict{Symbol,Symbol}()
code1 = _quasiquote!(code, vars)
modules1 = modules isa Expr ? modules.args : modules
tests = Expr(:block)

for mod in modules1
expr = Expr(:block)
for (var, genkey) in pairs(vars)
push!(expr.args, conversion_expr(mod, var, genkey))
end
push!(expr.args, :(@test $code1))
push!(tests.args, expr)
end

return esc(tests)
end

"""
@testset_implementations(code)
@testset_implementations(title, code)
@testset_implementations(modules, code)
@testset_implementations(title, modules, code)

Macro to run a block of `code` for multiple modules within separate testsets,
using GeoInterface.convert for each variable prefixed with `\$` in the code block.

# Examples
```julia
point = GI.Point(1.0, 2.0)
@testset_implementations "Point tests" begin
@test GeoInterface.x(\$point) == 1.0
end
```
"""
macro testset_implementations(code::Expr)
_testset_implementations_inner("", TEST_MODULES, code)
end
macro testset_implementations(arg, code::Expr)
if arg isa String || arg isa Expr && arg.head == :string
_testset_implementations_inner(arg, TEST_MODULES, code)
else
_testset_implementations_inner("", arg, code)
end
end
macro testset_implementations(title, modules::Union{Expr,Vector}, code::Expr)
_testset_implementations_inner(title, modules, code)
end

function _testset_implementations_inner(title, modules::Union{Expr,Vector}, code::Expr)
vars = Dict{Symbol,Symbol}()
code1 = _quasiquote!(code, vars)
modules1 = modules isa Expr ? modules.args : modules
testsets = Expr(:block)

for mod in modules1
expr = Expr(:block)
for (var, genkey) in pairs(vars)
push!(expr.args, conversion_expr(mod, var, genkey))
end
# Manually define the testset macrocall and all string interpolation
testset = Expr(
:macrocall,
Symbol("@testset"),
LineNumberNode(@__LINE__, @__FILE__),
Expr(:string, mod, " ", title),
code1
)
push!(expr.args, testset)
push!(testsets.args, expr)
end

return esc(testsets)
end

# Taken from BenchmarkTools.jl
_quasiquote!(ex, vars) = ex
function _quasiquote!(ex::Expr, vars::Dict)
if ex.head === :($)
v = ex.args[1]
gen = if v isa Symbol
haskey(vars, v) ? vars[v] : gensym(v)
else
gensym()
end
vars[v] = gen
return v
elseif ex.head !== :quote
for i in 1:length(ex.args)
ex.args[i] = _quasiquote!(ex.args[i], vars)
end
end
return ex
end

end # module GeometryOpsTestHelpers
7 changes: 6 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
FlexiJoins = "e37f2e79-19fa-4eb7-8510-b63b51fe0a37"
GeoJSON = "61d90e0f-e114-555e-ac52-39dfb47a3ef9"
GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326"
GeometryOpsTestHelpers = "47ae2730-6264-4af1-919b-a18bc445f019"
JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819"
LibGEOS = "a90b1aa1-3769-5649-ba7e-abc5a9d163eb"
NaturalEarth = "436b0209-26ab-4e65-94a9-6526d86fea76"
Expand All @@ -84,4 +85,8 @@ TGGeometry = "d7e755d2-3c95-4bcf-9b3c-79ab1a78647b"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["ArchGDAL", "CoordinateTransformations", "DataFrames", "Distributions", "DimensionalData", "Downloads", "FlexiJoins", "GeoJSON", "GeometryBasics", "Proj", "JLD2", "LibGEOS", "Random", "Rasters", "NaturalEarth", "OffsetArrays", "Polylabel", "SafeTestsets", "Shapefile", "TGGeometry", "Test"]
test = ["ArchGDAL", "CoordinateTransformations", "DataFrames", "Distributions", "DimensionalData", "Downloads", "FlexiJoins", "GeoJSON", "GeometryBasics", "GeometryOpsTestHelpers", "Proj", "JLD2", "LibGEOS", "Random", "Rasters", "NaturalEarth", "OffsetArrays", "Polylabel", "SafeTestsets", "Shapefile", "TGGeometry", "Test"]

[sources]
GeometryOpsTestHelpers = {path = "GeometryOpsTestHelpers"}
GeometryOpsCore = {path = "GeometryOpsCore"}
Loading
Loading