Skip to content

Commit 881b221

Browse files
authored
Merge branch 'main' into as/clipping_crs
2 parents 5390016 + 248e985 commit 881b221

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1120
-411
lines changed

.github/workflows/CI.yml

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,11 @@ jobs:
3434
arch: arm64
3535
steps:
3636
- uses: actions/checkout@v2
37-
- uses: julia-actions/cache@v1
38-
- uses: julia-actions/setup-julia@v1
37+
- uses: julia-actions/cache@v2
38+
- uses: julia-actions/setup-julia@v2
3939
with:
4040
version: ${{ matrix.version }}
4141
arch: ${{ matrix.arch }}
42-
- uses: julia-actions/cache@v1
4342
- name: Dev GeometryOpsCore and add other packages
4443
run: julia --project=. -e 'using Pkg; Pkg.develop(; path = joinpath(".", "GeometryOpsCore"));'
4544
- uses: julia-actions/julia-buildpkg@v1
@@ -62,8 +61,8 @@ jobs:
6261
actions: write
6362
steps:
6463
- uses: actions/checkout@v2
65-
- uses: julia-actions/cache@v1
66-
- uses: julia-actions/setup-julia@v1
64+
- uses: julia-actions/cache@v2
65+
- uses: julia-actions/setup-julia@v2
6766
with:
6867
version: '1'
6968
- name: Build and add versions
@@ -82,9 +81,9 @@ jobs:
8281
statuses: write
8382
actions: write
8483
steps:
85-
- uses: actions/checkout@v2
86-
- uses: julia-actions/cache@v1
87-
- uses: julia-actions/setup-julia@v1
84+
- uses: actions/checkout@v4
85+
- uses: julia-actions/cache@v2
86+
- uses: julia-actions/setup-julia@v2
8887
with:
8988
version: '1'
9089
- name: Build and add versions

GeometryOpsCore/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "GeometryOpsCore"
22
uuid = "05efe853-fabf-41c8-927e-7063c8b9f013"
33
authors = ["Anshul Singhvi <[email protected]>", "Rafael Schouten <[email protected]>", "Skylar Gering <[email protected]>", "and contributors"]
4-
version = "0.1.5"
4+
version = "0.1.7"
55

66
[deps]
77
DataAPI = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a"

GeometryOpsCore/src/apply.jl

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,11 @@ with the same schema, but with the new geometry column.
175175
This new table may be of the same type as the old one iff `Tables.materializer` is defined for
176176
that table. If not, then a `NamedTuple` is returned.
177177
=#
178-
function _apply_table(f::F, target, iterable::IterableType; geometrycolumn = nothing, preserve_default_metadata = false, threaded, kw...) where {F, IterableType}
179-
_get_col_pair(colname) = colname => Tables.getcolumn(iterable, colname)
180-
# We extract the geometry column and run `apply` on it.
178+
function _apply_table(f::F, target, iterable::IterableType; geometrycolumn = nothing, preserve_default_metadata = false, threaded, kw...) where {F, IterableType} # We extract the geometry column and run `apply` on it.
179+
# First, we need the table schema:
180+
input_schema = Tables.schema(iterable)
181+
input_colnames = input_schema.names
182+
# then, we find the geometry column(s)
181183
geometry_columns = if isnothing(geometrycolumn)
182184
GI.geometrycolumns(iterable)
183185
elseif geometrycolumn isa NTuple{N, <: Symbol} where N
@@ -187,31 +189,31 @@ function _apply_table(f::F, target, iterable::IterableType; geometrycolumn = not
187189
else
188190
throw(ArgumentError("geometrycolumn must be a Symbol or a tuple of Symbols, got a $(typeof(geometrycolumn))"))
189191
end
190-
if !all(Base.Fix2(in, Tables.columnnames(iterable)), geometry_columns)
192+
if !Base.issubset(geometry_columns, input_colnames)
191193
throw(ArgumentError(
192194
"""
193195
`apply`: the `geometrycolumn` kwarg must be a subset of the column names of the table,
194196
got $(geometry_columns)
195197
but the table has columns
196-
$(Tables.columnnames(iterable))
198+
$(input_colnames)
197199
"""
198200
))
199201
end
202+
# here we apply the function to the geometry column(s).
203+
apply_kw = if isempty(used_reconstruct_table_kwargs(iterable))
204+
kw
205+
else
206+
Base.structdiff(values(kw), NamedTuple{used_reconstruct_table_kwargs(iterable)})
207+
end
200208
new_geometry_vecs = map(geometry_columns) do colname
201-
_apply(f, target, Tables.getcolumn(iterable, colname); threaded, kw...)
209+
_apply(f, target, Tables.getcolumn(iterable, colname); threaded, apply_kw...)
202210
end
203-
# Then, we obtain the schema of the table,
204-
old_schema = Tables.schema(iterable)
205-
# filter the geometry column out,
206-
new_names = filter(x -> !(x in geometry_columns), old_schema.names)
211+
# Then, we filter the geometry column(s) out,
212+
new_names = filter(x -> !(x in geometry_columns), input_colnames)
207213
# and try to rebuild the same table as the best type - either the original type of `iterable`,
208214
# or a named tuple which is the default fallback.
209-
result = Tables.materializer(iterable)(
210-
merge(
211-
NamedTuple{geometry_columns, Base.Tuple{typeof.(new_geometry_vecs)...}}(new_geometry_vecs),
212-
NamedTuple(Iterators.map(_get_col_pair, new_names))
213-
)
214-
)
215+
# See the function directly below this one for the actual fallback implementation.
216+
result = reconstruct_table(iterable, geometry_columns, new_geometry_vecs, new_names; kw...)
215217
# Finally, we ensure that metadata is propagated correctly.
216218
# This can only happen if the original table supports metadata reads,
217219
# and the result supports metadata writes.
@@ -246,6 +248,51 @@ function _apply_table(f::F, target, iterable::IterableType; geometrycolumn = not
246248
return result
247249
end
248250

251+
252+
"""
253+
used_reconstruct_table_kwargs(input)
254+
255+
Return a tuple of the kwargs that should be passed to `reconstruct_table` for the given input.
256+
257+
This is "semi-public" API, and required for any input type that defines `reconstruct_table`.
258+
"""
259+
function used_reconstruct_table_kwargs(input)
260+
()
261+
end
262+
263+
"""
264+
reconstruct_table(input, geometry_column_names, geometry_columns, other_column_names, args...; kwargs...)
265+
266+
Reconstruct a table from the given input, geometry column names,
267+
geometry columns, and other column names.
268+
269+
Any function that defines `reconstruct_table` must also define `used_reconstruct_table_kwargs`.
270+
271+
The input must be a table.
272+
273+
The function should return a best-effort attempt at a table of the same type as the input,
274+
with the new geometry column(s) and other columns.
275+
276+
The fallback implementation invokes `Tables.materializer`. But if you want to be efficient
277+
and pass e.g. arbitrary kwargs to the materializer, or materialize in a different way, you
278+
can do so by overloading this function for your desired input type.
279+
280+
This is "semi-public" API and while it may add optional arguments, it will not add new required
281+
positional arguments. All implementations must allow arbitrary kwargs to pass through and harvest
282+
what they need.
283+
"""
284+
function reconstruct_table(input, geometry_column_names, geometry_columns, other_column_names, args...; kwargs...)
285+
@assert Tables.istable(input)
286+
_get_col_pair(colname) = colname => Tables.getcolumn(input, colname)
287+
288+
return Tables.materializer(input)(
289+
merge(
290+
NamedTuple{geometry_column_names, Base.Tuple{typeof.(geometry_columns)...}}(geometry_columns),
291+
NamedTuple(Iterators.map(_get_col_pair, other_column_names))
292+
)
293+
)
294+
end
295+
249296
# Rewrap all FeatureCollectionTrait feature collections as GI.FeatureCollection
250297
# Maybe use threads to call _apply on component features
251298
@inline function _apply(f::F, target, ::GI.FeatureCollectionTrait, fc;
@@ -323,12 +370,20 @@ end
323370
# So the `Target` is found. We apply `f` to geom and return it to previous
324371
# _apply calls to be wrapped with the outer geometries/feature/featurecollection/array.
325372
_apply(f::F, ::TraitTarget{Target}, ::Trait, geom; crs=GI.crs(geom), kw...) where {F,Target,Trait<:Target} = f(geom)
373+
function _apply(a::WithTrait{F}, ::TraitTarget{Target}, trait::Trait, geom; crs=GI.crs(geom), kw...) where {F,Target,Trait<:Target}
374+
a(trait, geom; Base.structdiff(values(kw), NamedTuple{(:threaded, :calc_extent)})...)
375+
end
326376
# Define some specific cases of this match to avoid method ambiguity
327377
for T in (
328378
GI.PointTrait, GI.LinearRing, GI.LineString,
329379
GI.MultiPoint, GI.FeatureTrait, GI.FeatureCollectionTrait
330380
)
331-
@eval _apply(f::F, target::TraitTarget{<:$T}, trait::$T, x; kw...) where F = f(x)
381+
@eval begin
382+
_apply(f::F, target::TraitTarget{<:$T}, trait::$T, x; kw...) where F = f(x)
383+
function _apply(a::WithTrait{F}, target::TraitTarget{<:$T}, trait::$T, x; kw...) where F
384+
a(trait, x; Base.structdiff(values(kw), NamedTuple{(:threaded, :calc_extent)})...)
385+
end
386+
end
332387
end
333388

334389

GeometryOpsCore/src/applyreduce.jl

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Literate.jl source code is below.
5151
=#
5252

5353
"""
54-
applyreduce(f, op, target::Union{TraitTarget, GI.AbstractTrait}, obj; threaded)
54+
applyreduce(f, op, target::Union{TraitTarget, GI.AbstractTrait}, obj; threaded, init, kw...)
5555
5656
Apply function `f` to all objects with the `target` trait,
5757
and reduce the result with an `op` like `+`.
@@ -60,6 +60,8 @@ The order and grouping of application of `op` is not guaranteed.
6060
6161
If `threaded==true` threads will be used over arrays and iterables,
6262
feature collections and nested geometries.
63+
64+
`init` functions the same way as it does in base Julia functions like `reduce`.
6365
"""
6466
@inline function applyreduce(
6567
f::F, op::O, target, geom; threaded=false, init=nothing
@@ -129,6 +131,9 @@ end
129131
@inline function _applyreduce(f::F, op::O, ::TraitTarget{Target}, ::Trait, x; kw...) where {F,O,Target,Trait<:Target}
130132
f(x)
131133
end
134+
@inline function _applyreduce(a::WithTrait{F}, op::O, ::TraitTarget{Target}, trait::Trait, x; kw...) where {F,O,Target,Trait<:Target}
135+
a(trait, x; Base.structdiff(values(kw), NamedTuple{(:threaded, :init)})...)
136+
end
132137
# Fail if we hit PointTrait
133138
# _applyreduce(f, op, target::TraitTarget{Target}, trait::PointTrait, geom; kw...) where Target =
134139
# throw(ArgumentError("target $target not found"))
@@ -137,7 +142,12 @@ for T in (
137142
GI.PointTrait, GI.LinearRing, GI.LineString,
138143
GI.MultiPoint, GI.FeatureTrait, GI.FeatureCollectionTrait
139144
)
140-
@eval _applyreduce(f::F, op::O, ::TraitTarget{<:$T}, trait::$T, x; kw...) where {F, O} = f(x)
145+
@eval begin
146+
_applyreduce(f::F, op::O, ::TraitTarget{<:$T}, trait::$T, x; kw...) where {F, O} = f(x)
147+
function _applyreduce(a::WithTrait{F}, op::O, ::TraitTarget{<:$T}, trait::$T, x; kw...) where {F, O}
148+
a(trait, x; Base.structdiff(values(kw), NamedTuple{(:threaded, :init)})...)
149+
end
150+
end
141151
end
142152

143153
### `_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

GeometryOpsCore/src/types/exceptions.jl

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,16 @@ This makes it substantially easier to catch specific kinds of errors and show th
88
For example, we can catch `WrongManifoldException` and show a nice error message,
99
and error hinters can be specialized to that as well.
1010
11+
We also have a custom error type for missing keywords in an algorithm,
12+
which could eventually be extended to have typo detection, et cetera.
13+
1114
```@docs; canonical=false
12-
GeometryOpsCore.WrongManifoldException
15+
WrongManifoldException
16+
MissingKeywordInAlgorithmException
17+
```
18+
19+
```@meta
20+
CollapsedDocStrings = true
1321
```
1422
=#
1523

@@ -46,4 +54,60 @@ function Base.showerror(io::IO, e::WrongManifoldException{I,D,A}) where {I,D,A}
4654
print(io, "\n\n")
4755
print(io, e.description)
4856
end
57+
end
58+
59+
60+
"""
61+
MissingKeywordInAlgorithmException{Alg, F} <: Exception
62+
63+
An error type which is thrown when a keyword argument is missing from an algorithm.
64+
65+
The `alg` argument is the algorithm struct, and the `keyword` argument is the keyword
66+
that was missing.
67+
68+
This error message is used in the [`enforce`](@ref GeometryOps.enforce) method.
69+
70+
## Usage
71+
72+
This is of course not how you would actually use this error type, but it is
73+
how you construct and throw it.
74+
75+
```julia
76+
throw(MissingKeywordInAlgorithmException(GEOS(; tokl = 1.0), my_function, :tol))
77+
```
78+
79+
Real world usage will often look like this:
80+
81+
```julia
82+
function my_function(alg::CLibraryPlanarAlgorithm, args...)
83+
mykwarg = enforce(alg, :mykwarg, my_function) # this will throw an error if :mykwarg is not present in alg
84+
end
85+
```
86+
"""
87+
struct MissingKeywordInAlgorithmException{Alg, F} <: Base.Exception
88+
alg::Alg
89+
f::F
90+
keyword::Symbol
91+
end
92+
93+
_name_of(x::Any) = nameof(typeof(x))
94+
_name_of(x::Function) = nameof(x)
95+
_name_of(x::Type) = nameof(x)
96+
_name_of(s::String) = s
97+
98+
# This is just the generic dispatch, different algorithms can choose to dispatch
99+
# on their own types to provide more specific or interesting error messages.
100+
function Base.showerror(io::IO, e::MissingKeywordInAlgorithmException)
101+
algorithm_name = _name_of(typeof(e.alg))
102+
function_name = _name_of(e.f)
103+
print(io, "The ")
104+
printstyled(io, e.keyword; color = :red)
105+
print(io, " parameter is required for the ")
106+
printstyled(io, algorithm_name; bold = true)
107+
println(io, " algorithm in `$(function_name)`,")
108+
println(io, "but it was not provided.")
109+
println(io)
110+
println(io, "Provide it to the algorithm at construction time, like so:")
111+
println(io, "`$(algorithm_name)(; $(e.keyword) = ...)`")
112+
println(io, "and pass that as the algorithm to `$(function_name)`, usually the first argument.")
49113
end

0 commit comments

Comments
 (0)