Skip to content

Commit bf1ad62

Browse files
authored
Implement an Applicator WithTrait (#305)
1 parent ff98edd commit bf1ad62

File tree

9 files changed

+257
-22
lines changed

9 files changed

+257
-22
lines changed

GeometryOpsCore/src/apply.jl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,12 +323,20 @@ end
323323
# So the `Target` is found. We apply `f` to geom and return it to previous
324324
# _apply calls to be wrapped with the outer geometries/feature/featurecollection/array.
325325
_apply(f::F, ::TraitTarget{Target}, ::Trait, geom; crs=GI.crs(geom), kw...) where {F,Target,Trait<:Target} = f(geom)
326+
function _apply(a::WithTrait{F}, ::TraitTarget{Target}, trait::Trait, geom; crs=GI.crs(geom), kw...) where {F,Target,Trait<:Target}
327+
a(trait, geom; Base.structdiff(values(kw), NamedTuple{(:threaded, :calc_extent)})...)
328+
end
326329
# Define some specific cases of this match to avoid method ambiguity
327330
for T in (
328331
GI.PointTrait, GI.LinearRing, GI.LineString,
329332
GI.MultiPoint, GI.FeatureTrait, GI.FeatureCollectionTrait
330333
)
331-
@eval _apply(f::F, target::TraitTarget{<:$T}, trait::$T, x; kw...) where F = f(x)
334+
@eval begin
335+
_apply(f::F, target::TraitTarget{<:$T}, trait::$T, x; kw...) where F = f(x)
336+
function _apply(a::WithTrait{F}, target::TraitTarget{<:$T}, trait::$T, x; kw...) where F
337+
a(trait, x; Base.structdiff(values(kw), NamedTuple{(:threaded, :calc_extent)})...)
338+
end
339+
end
332340
end
333341

334342

GeometryOpsCore/src/applyreduce.jl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ end
129129
@inline function _applyreduce(f::F, op::O, ::TraitTarget{Target}, ::Trait, x; kw...) where {F,O,Target,Trait<:Target}
130130
f(x)
131131
end
132+
@inline function _applyreduce(a::WithTrait{F}, op::O, ::TraitTarget{Target}, trait::Trait, x; kw...) where {F,O,Target,Trait<:Target}
133+
a(trait, x; Base.structdiff(values(kw), NamedTuple{(:threaded, :init)})...)
134+
end
132135
# Fail if we hit PointTrait
133136
# _applyreduce(f, op, target::TraitTarget{Target}, trait::PointTrait, geom; kw...) where Target =
134137
# throw(ArgumentError("target $target not found"))
@@ -137,7 +140,12 @@ for T in (
137140
GI.PointTrait, GI.LinearRing, GI.LineString,
138141
GI.MultiPoint, GI.FeatureTrait, GI.FeatureCollectionTrait
139142
)
140-
@eval _applyreduce(f::F, op::O, ::TraitTarget{<:$T}, trait::$T, x; kw...) where {F, O} = f(x)
143+
@eval begin
144+
_applyreduce(f::F, op::O, ::TraitTarget{<:$T}, trait::$T, x; kw...) where {F, O} = f(x)
145+
function _applyreduce(a::WithTrait{F}, op::O, ::TraitTarget{<:$T}, trait::$T, x; kw...) where {F, O}
146+
a(trait, x; Base.structdiff(values(kw), NamedTuple{(:threaded, :init)})...)
147+
end
148+
end
141149
end
142150

143151
### `_mapreducetasks` - flexible, threaded mapreduce

GeometryOpsCore/src/types/applicators.jl

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,26 @@ const WithXYZM = ApplyToPoint{true,true}
5454

5555
(t::WithXY)(p) = t.f(GI.x(p), GI.y(p))
5656
(t::WithXYZ)(p) = t.f(GI.x(p), GI.y(p), GI.z(p))
57-
(t::WithXYZM)(p) = t.f(GI.x(p), GI.y(p), GI.m(p))
58-
(t::WithXYM)(p) = t.f(GI.x(p), GI.y(p), GI.z(p), GI.m(p))
57+
(t::WithXYZM)(p) = t.f(GI.x(p), GI.y(p), GI.z(p), GI.m(p))
58+
(t::WithXYM)(p) = t.f(GI.x(p), GI.y(p), GI.m(p))
59+
60+
"""
61+
WithTrait(f)
62+
63+
WithTrait is a functor that applies a function to a trait and an object.
64+
65+
Specifically, the calling convention is for `f` is changed
66+
from `f(geom)` to `f(trait, geom; kw...)`.
67+
68+
This is useful to keep the trait materialized through the call stack,
69+
which can improve inferrability and performance.
70+
"""
71+
struct WithTrait{F} <: Applicator{F, Nothing}
72+
f::F
73+
end
74+
75+
(a::WithTrait)(trait::GI.AbstractTrait, obj; kw...) = a.f(trait, obj; kw...)
76+
rebuild(::WithTrait, f::F) where {F} = WithTrait{F}(f)
5977

6078
# ***
6179

src/GeometryOps.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import GeometryOpsCore:
1010
Algorithm, AutoAlgorithm, ManifoldIndependentAlgorithm, SingleManifoldAlgorithm, NoAlgorithm,
1111
BoolsAsTypes, True, False, booltype, istrue,
1212
TaskFunctors,
13+
WithTrait,
1314
WithXY, WithXYZ, WithXYM, WithXYZM,
1415
apply, applyreduce,
1516
flatten, reconstruct, rebuild, unwrap, _linearring,

src/methods/area.jl

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,7 @@ Result will be of type T, where T is an optional argument with a default value
6767
of Float64.
6868
"""
6969
function area(geom, ::Type{T} = Float64; threaded=false) where T <: AbstractFloat
70-
applyreduce(+, _AREA_TARGETS, geom; threaded, init=zero(T)) do g
71-
_area(T, GI.trait(g), g)
72-
end
70+
applyreduce(WithTrait((trait, g) -> _area(T, trait, g)), +, _AREA_TARGETS, geom; threaded, init=zero(T))
7371
end
7472

7573
"""

src/methods/centroid.jl

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ function centroid(
6262
centroid_and_length(trait, geom, T)[1]
6363
end
6464
centroid(trait, geom, ::Type{T}; threaded=false) where T =
65-
centroid_and_area(geom, T; threaded)[1]
65+
centroid_and_area(trait, geom, T; threaded)[1]
6666

6767
"""
6868
centroid_and_length(geom, [T=Float64])::(::Tuple{T, T}, ::Real)
@@ -76,9 +76,9 @@ function centroid_and_length(
7676
::Union{GI.LineStringTrait, GI.LinearRingTrait}, geom, ::Type{T},
7777
) where T
7878
# Initialize starting values
79-
xcentroid = T(0)
80-
ycentroid = T(0)
81-
length = T(0)
79+
xcentroid = zero(T)
80+
ycentroid = zero(T)
81+
length = zero(T)
8282
point₁ = GI.getpoint(geom, 1)
8383
# Loop over line segments of line string
8484
for point₂ in GI.getpoint(geom)
@@ -107,14 +107,17 @@ end
107107
Returns the centroid and area of a given geometry.
108108
"""
109109
function centroid_and_area(geom, ::Type{T}=Float64; threaded=false) where T
110+
trait = GI.trait(geom)
111+
centroid_and_area(trait, geom, T; threaded)
112+
end
113+
114+
function centroid_and_area(trait, geom, ::Type{T}; threaded=false) where T
110115
target = TraitTarget{Union{GI.PolygonTrait,GI.LineStringTrait,GI.LinearRingTrait}}()
111116
init = (zero(T), zero(T)), zero(T)
112-
applyreduce(_combine_centroid_and_area, target, geom; threaded, init) do g
113-
_centroid_and_area(GI.trait(g), g, T)
114-
end
117+
applyreduce(WithTrait((trait, g) -> centroid_and_area(trait, g, T)), _combine_centroid_and_area, target, geom; threaded, init)
115118
end
116119

117-
function _centroid_and_area(
120+
function centroid_and_area(
118121
::Union{GI.LineStringTrait, GI.LinearRingTrait}, geom, ::Type{T}
119122
) where T
120123
# Check that the geometry is closed
@@ -123,9 +126,9 @@ function _centroid_and_area(
123126
"centroid_and_area should only be used with closed geometries"
124127
)
125128
# Initialize starting values
126-
xcentroid = T(0)
127-
ycentroid = T(0)
128-
area = T(0)
129+
xcentroid = zero(T)
130+
ycentroid = zero(T)
131+
area = zero(T)
129132
point₁ = GI.getpoint(geom, 1)
130133
# Loop over line segments of linear ring
131134
for point₂ in GI.getpoint(geom)
@@ -144,16 +147,17 @@ function _centroid_and_area(
144147
ycentroid /= 6area
145148
return (xcentroid, ycentroid), abs(area)
146149
end
147-
function _centroid_and_area(::GI.PolygonTrait, geom, ::Type{T}) where T
150+
function centroid_and_area(::GI.PolygonTrait, geom, ::Type{T}) where T
148151
# Exterior ring's centroid and area
149-
(xcentroid, ycentroid), area = centroid_and_area(GI.getexterior(geom), T)
152+
exterior = GI.getexterior(geom)
153+
(xcentroid, ycentroid), area = centroid_and_area(GI.geomtrait(exterior), exterior, T)
150154
# Weight exterior centroid by area
151155
xcentroid *= area
152156
ycentroid *= area
153157
# Loop over any holes within the polygon
154158
for hole in GI.gethole(geom)
155159
# Hole polygon's centroid and area
156-
(xinterior, yinterior), interior_area = centroid_and_area(hole, T)
160+
(xinterior, yinterior), interior_area = centroid_and_area(GI.geomtrait(hole), hole, T)
157161
# Accumulate the area component into `area`
158162
area -= interior_area
159163
# Weighted average of centroid components

src/transformations/correction/geometry_correction.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ function fix(geometry; corrections = GeometryCorrection[ClosedRing(),], kwargs..
5252
isempty(available_corrections) && continue
5353
@debug "Correcting for $(Trait)"
5454
net_function = reduce(, corrections[available_corrections])
55-
final_geometry = apply(net_function, Trait, final_geometry; kwargs...)
55+
final_geometry = apply(WithTrait(net_function), TraitTarget(Trait), final_geometry; kwargs...)
5656
end
5757
return final_geometry
5858
end

test/core/applicators.jl

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
using Test
2+
import GeometryOps as GO, GeometryOpsCore as GOCore, GeoInterface as GI
3+
4+
@testset "WithTrait" begin
5+
# Basic functionality test
6+
@testset "Basic functionality" begin
7+
# Create a simple function that uses the trait
8+
f = (trait, obj) -> (trait, obj)
9+
awt = GOCore.WithTrait(f)
10+
11+
# Test with a point trait
12+
point = (1.0, 2.0)
13+
trait = GI.PointTrait()
14+
@test awt(trait, point) == (trait, point)
15+
16+
# Test with a polygon trait
17+
poly = GI.Polygon([GI.LinearRing([(1, 2), (3, 4), (5, 6), (1, 2)])])
18+
trait = GI.PolygonTrait()
19+
@test awt(trait, poly) == (trait, poly)
20+
end
21+
22+
# Test rebuild method
23+
@testset "Rebuild method" begin
24+
f1 = (trait, obj) -> (trait, obj)
25+
f2 = (trait, obj) -> (trait, obj, "extra")
26+
27+
awt = GOCore.WithTrait(f1)
28+
new_awt = GOCore.rebuild(awt, f2)
29+
30+
point = (1.0, 2.0)
31+
trait = GI.PointTrait()
32+
@test new_awt(trait, point) == (trait, point, "extra")
33+
end
34+
35+
# Test with apply
36+
@testset "Usage with apply" begin
37+
# Create a function that uses the trait to determine behavior
38+
f = (trait, obj) -> begin
39+
if trait isa GI.PointTrait
40+
(GI.x(obj) + 1, GI.y(obj) + 1)
41+
elseif trait isa GI.PolygonTrait
42+
GI.ngeom(obj)
43+
else
44+
error("Unexpected trait")
45+
end
46+
end
47+
48+
awt = GOCore.WithTrait(f)
49+
50+
# Test with a point
51+
point = (1.0, 2.0)
52+
result = GO.apply(awt, GI.PointTrait(), point)
53+
@test result == (2.0, 3.0)
54+
55+
# Test with a polygon
56+
poly = GI.Polygon([GI.LinearRing([(1, 2), (3, 4), (5, 6), (1, 2)])])
57+
result = GO.apply(awt, GI.PolygonTrait(), poly)
58+
@test result == 1 # One linear ring in the polygon
59+
end
60+
61+
# Test with applyreduce
62+
@testset "Usage with applyreduce" begin
63+
# Create a function that uses the trait to determine behavior
64+
f = (trait, obj) -> begin
65+
if trait isa GI.PointTrait
66+
GI.x(obj) + GI.y(obj)
67+
elseif trait isa GI.PolygonTrait
68+
GI.ngeom(obj)
69+
else
70+
error("Unexpected trait")
71+
end
72+
end
73+
74+
awt = GOCore.WithTrait(f)
75+
76+
# Test with a point
77+
point = (1.0, 2.0)
78+
result = GO.applyreduce(awt, +, GI.PointTrait(), point)
79+
@test result == 3.0
80+
81+
# Test with a polygon
82+
poly = GI.Polygon([GI.LinearRing([(1, 2), (3, 4), (5, 6), (1, 2)])])
83+
result = GO.applyreduce(awt, +, GI.PolygonTrait(), poly)
84+
@test result == 1 # One linear ring in the polygon
85+
end
86+
87+
# Test with keyword arguments
88+
@testset "Keyword arguments" begin
89+
f = (trait, obj; kw...) -> (trait, obj, kw)
90+
awt = GOCore.WithTrait(f)
91+
92+
point = (1.0, 2.0)
93+
trait = GI.PointTrait()
94+
result = awt(trait, point; extra=1, more="test")
95+
@test result == (trait, point, pairs((;extra=1, more="test")))
96+
end
97+
end
98+
99+
@testset "ApplyToPoint" begin
100+
@testset "Usage with apply" begin
101+
# Test WithXY with apply
102+
point = (1.0, 2.0)
103+
result = GO.apply(GOCore.WithXY((x, y) -> (x + 1, y + 1)), GI.PointTrait(), point)
104+
@test result == (2.0, 3.0)
105+
106+
# Test WithXYZ with apply
107+
point = (1.0, 2.0, 3.0)
108+
result = GO.apply(GOCore.WithXYZ((x, y, z) -> (x + 1, y + 1, z + 1)), GI.PointTrait(), point)
109+
@test result == (2.0, 3.0, 4.0)
110+
111+
# Test WithXYM with apply
112+
point = (1.0, 2.0, 3.0, 4.0) # m value
113+
result = GO.apply(GOCore.WithXYM((x, y, m) -> (x + 1, y + 1, m + 1)), GI.PointTrait(), point)
114+
@test result == (2.0, 3.0, 5.0)
115+
116+
# Test WithXYZM with apply
117+
point = (1.0, 2.0, 3.0, 4.0) # x, y, z, m
118+
result = GO.apply(GOCore.WithXYZM((x, y, z, m) -> (x + 1, y + 1, z + 1, m + 1)), GI.PointTrait(), point)
119+
@test result == (2.0, 3.0, 4.0, 5.0)
120+
end
121+
122+
@testset "Usage with applyreduce" begin
123+
# Test WithXY with applyreduce
124+
point = (1.0, 2.0)
125+
result = GO.applyreduce(GOCore.WithXY((x, y) -> x + y), +, GI.PointTrait(), point; init = 0.0)
126+
@test result == 3.0
127+
128+
# Test WithXYZ with applyreduce
129+
point = (1.0, 2.0, 3.0)
130+
result = GO.applyreduce(GOCore.WithXYZ((x, y, z) -> x + y + z), +, GI.PointTrait(), point; init = 0.0)
131+
@test result == 6.0
132+
end
133+
end
134+
135+
@testset "ApplyToArray" begin
136+
@testset "Usage with apply" begin
137+
# Test with array of geometries
138+
points = [(1.0, 2.0), (3.0, 4.0)]
139+
result = GO.apply(GOCore.WithXY((x, y) -> (x + 1, y + 1)), GI.PointTrait(), points)
140+
@test result == [(2.0, 3.0), (4.0, 5.0)]
141+
end
142+
143+
@testset "Usage with applyreduce" begin
144+
# Test with array of geometries
145+
points = [(1.0, 2.0), (3.0, 4.0)]
146+
result = GO.applyreduce(GOCore.WithXY((x, y) -> x + y), +, GI.PointTrait(), points; init = 0.0)
147+
@test result == 10.0
148+
end
149+
end
150+
151+
@testset "ApplyToFeatures" begin
152+
@testset "Usage with apply" begin
153+
# Test with feature collection
154+
features = [GI.Feature((1, 2)), GI.Feature((3, 4))]
155+
result = GO.apply(GOCore.WithXY((x, y) -> (x + 1, y + 1)), GI.PointTrait(), features)
156+
@test result == [GI.Feature((2, 3)), GI.Feature((4, 5))]
157+
end
158+
159+
@testset "Usage with applyreduce" begin
160+
# Test with feature collection
161+
features = [GI.Feature((1, 2)), GI.Feature((3, 4))]
162+
result = GO.applyreduce(GOCore.WithXY((x, y) -> x + y), +, GI.PointTrait(), features; init = 0.0)
163+
@test result == 10.0
164+
end
165+
end
166+
167+
@testset "ApplyToGeom" begin
168+
@testset "Usage with apply" begin
169+
# Test with polygon
170+
poly = GI.Polygon([GI.LinearRing([(1, 2), (3, 4), (5, 6), (1, 2)])])
171+
result = GO.apply(GOCore.WithXY((x, y) -> (x + 1, y + 1)), GI.PointTrait(), poly)
172+
@test result == GI.Polygon([GI.LinearRing([(2, 3), (4, 5), (6, 7), (2, 3)])])
173+
end
174+
175+
@testset "Usage with applyreduce" begin
176+
# Test with polygon
177+
poly = GI.Polygon([GI.LinearRing([(1, 2), (3, 4), (5, 6), (1, 2)])])
178+
result = GO.applyreduce(GOCore.WithXY((x, y) -> x + y), +, GI.PointTrait(), poly; init = 0.0)
179+
@test result == 24.0 # Sum of all x+y values
180+
end
181+
end
182+
183+
@testset "ApplyPointsToPolygon" begin
184+
@testset "Usage with apply" begin
185+
# Test with polygon
186+
poly = GI.Polygon([GI.LinearRing([(1, 2), (3, 4), (5, 6), (1, 2)])])
187+
result = GO.apply(GOCore.WithXY((x, y) -> (x + 1, y + 1)), GI.PointTrait(), poly)
188+
@test result == GI.Polygon([GI.LinearRing([(2, 3), (4, 5), (6, 7), (2, 3)])])
189+
end
190+
191+
@testset "Usage with applyreduce" begin
192+
# Test with polygon
193+
poly = GI.Polygon([GI.LinearRing([(1, 2), (3, 4), (5, 6), (1, 2)])])
194+
result = GO.applyreduce(GOCore.WithXY((x, y) -> x + y), +, GI.PointTrait(), poly; init = 0.0)
195+
@test result == 24.0 # Sum of all x+y values
196+
end
197+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ include("helpers.jl")
77
@testset "Core" begin
88
@safetestset "Algorithm" begin include("core/algorithm.jl") end
99
@safetestset "Manifold" begin include("core/manifold.jl") end
10+
@safetestset "Applicators" begin include("core/applicators.jl") end
1011
end
1112

1213
@safetestset "Types" begin include("types.jl") end

0 commit comments

Comments
 (0)