Skip to content
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
Expand Down
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"

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

using GeometryOpsTestHelpers
using GeoInterface
import ArchGDAL as AG
import ArchGDAL

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

# Monkey-patch ArchGDAL to handle polygon conversion correctly
function GeoInterface.convert(
::Type{T},
type::GeoInterface.PolygonTrait,
geom,
) where {T<:AG.IGeometry}
f = get(AG.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 = AG.createpolygon()
foreach(GeoInterface.getring(geom)) do ring
xs = GeoInterface.x.(GeoInterface.getpoint(ring)) |> collect
ys = GeoInterface.y.(GeoInterface.getpoint(ring)) |> collect
subgeom = AG.unsafe_createlinearring(xs, ys)
result = AG.GDAL.ogr_g_addgeometrydirectly(poly, subgeom)
AG.@ogrerr result "Failed to add linearring."
end
return poly
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
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,4 @@ Tables = "1"
julia = "1.10"

[workspace]
projects = ["GeometryOpsCore", "test", "docs"]
projects = ["GeometryOpsCore", "GeometryOpsTestHelpers", "test", "docs"]
6 changes: 6 additions & 0 deletions test/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ GeoJSON = "61d90e0f-e114-555e-ac52-39dfb47a3ef9"
GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326"
GeometryOps = "3251bfac-6a57-4b6d-aa61-ac1fef2975ab"
GeometryOpsCore = "05efe853-fabf-41c8-927e-7063c8b9f013"
GeometryOpsTestHelpers = "47ae2730-6264-4af1-919b-a18bc445f019"
JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819"
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
LibGEOS = "a90b1aa1-3769-5649-ba7e-abc5a9d163eb"
Expand Down Expand Up @@ -43,3 +44,8 @@ GeometryOps = {path = ".."}
ArchGDAL = "0.10.10"
GeometryBasics = "0.4.7, 0.5"
Test = "1"

[sources]
GeometryOps = {path = ".."}
GeometryOpsCore = {path = "../GeometryOpsCore"}
GeometryOpsTestHelpers = {path = "../GeometryOpsTestHelpers"}
Loading
Loading