Skip to content

Commit 5600628

Browse files
authored
Merge branch 'main' into as/trees
2 parents 8663700 + 5a4351c commit 5600628

39 files changed

+982
-423
lines changed

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.6"
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

Project.toml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "GeometryOps"
22
uuid = "3251bfac-6a57-4b6d-aa61-ac1fef2975ab"
33
authors = ["Anshul Singhvi <[email protected]>", "Rafael Schouten <[email protected]>", "Skylar Gering <[email protected]>", "and contributors"]
4-
version = "0.1.23"
4+
version = "0.1.24"
55

66
[deps]
77
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
@@ -13,20 +13,22 @@ ExactPredicates = "429591f6-91af-11e9-00e2-59fbe8cec110"
1313
Extents = "411431e0-e8b7-467b-b5e0-f676ba4f2910"
1414
GeoFormatTypes = "68eda718-8dee-11e9-39e7-89f7f65f511f"
1515
GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f"
16-
GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326"
1716
GeometryOpsCore = "05efe853-fabf-41c8-927e-7063c8b9f013"
1817
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1918
SortTileRecursiveTree = "746ee33f-1797-42c2-866d-db2fce69d14d"
19+
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
2020
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
2121
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
2222

2323
[weakdeps]
24+
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
2425
FlexiJoins = "e37f2e79-19fa-4eb7-8510-b63b51fe0a37"
2526
LibGEOS = "a90b1aa1-3769-5649-ba7e-abc5a9d163eb"
2627
Proj = "c94c279d-25a6-4763-9509-64d165bea63e"
2728
TGGeometry = "d7e755d2-3c95-4bcf-9b3c-79ab1a78647b"
2829

2930
[extensions]
31+
GeometryOpsDataFramesExt = "DataFrames"
3032
GeometryOpsFlexiJoinsExt = "FlexiJoins"
3133
GeometryOpsLibGEOSExt = "LibGEOS"
3234
GeometryOpsProjExt = "Proj"
@@ -37,14 +39,15 @@ AbstractTrees = "0.4"
3739
AdaptivePredicates = "1.2"
3840
CoordinateTransformations = "0.5, 0.6"
3941
DataAPI = "1"
42+
DataFrames = "1"
4043
DelaunayTriangulation = "1.0.4"
4144
ExactPredicates = "2.2.8"
4245
Extents = "0.1.5"
4346
FlexiJoins = "0.1.30"
4447
GeoFormatTypes = "0.4"
4548
GeoInterface = "1.2"
4649
GeometryBasics = "0.4.7, 0.5"
47-
GeometryOpsCore = "=0.1.5"
50+
GeometryOpsCore = "=0.1.6"
4851
LibGEOS = "0.9.2"
4952
LinearAlgebra = "1"
5053
Proj = "1"
@@ -63,6 +66,7 @@ Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
6366
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
6467
FlexiJoins = "e37f2e79-19fa-4eb7-8510-b63b51fe0a37"
6568
GeoJSON = "61d90e0f-e114-555e-ac52-39dfb47a3ef9"
69+
GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326"
6670
JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819"
6771
LibGEOS = "a90b1aa1-3769-5649-ba7e-abc5a9d163eb"
6872
NaturalEarth = "436b0209-26ab-4e65-94a9-6526d86fea76"
@@ -77,4 +81,4 @@ TGGeometry = "d7e755d2-3c95-4bcf-9b3c-79ab1a78647b"
7781
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
7882

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

benchmarks/benchmarks.jl

Lines changed: 3 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -62,87 +62,6 @@ end
6262
plot_trials(circle_area_suite)
6363

6464

65-
# ## Vancouver watershed benchmarks
66-
#=
67-
68-
Vancouver Island has ~1,300 watersheds. LibGEOS uses this exact data
69-
in their tests, so we'll use it in ours as well!
70-
71-
We'll start by loading the data, and then we'll use it to benchmark various operations.
72-
73-
=#
74-
75-
# The CRS for this file is EPSG:3005, or as a PROJ string,
76-
# `"+proj=aea +lat_1=50 +lat_2=58.5 +lat_0=45 +lon_0=-126 +x_0=1000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs"`
77-
# TODO: this doesn't work with WellKnownGeometry. Why?
78-
wkt_geoms = LG.readgeom.(eachline("/Users/anshul/Downloads/watersheds.wkt"), (LG.WKTReader(LG.get_global_context()),))
79-
vancouver_polygons = GI.getgeom.(wkt_geoms, 1); #.|> GO.tuples;
80-
81-
import SortTileRecursiveTree as STR
82-
tree = STR.STRtree(vancouver_polygons)
83-
query_result = STR.query(tree, GI.extent(vancouver_polygons[1]))
84-
85-
GO.intersects.((vancouver_polygons[1],), vancouver_polygons[query_result])
86-
87-
go_vp = GO.tuples.(vancouver_polygons[1:2])
88-
@be GO.union($(go_vp[1]), $(go_vp[2]); target = $GI.PolygonTrait())
89-
@be LG.union($(vancouver_polygons[1]), $(vancouver_polygons[2]))
90-
91-
all_intersected = falses(length(vancouver_polygons))
92-
accumulator = deepcopy(vancouver_polygons[1])
93-
all_intersected[1] = true
94-
i = 1
95-
# query_result = STR.query(tree, GI.extent(accumulator))
96-
# for idx in query_result
97-
# println("Assessing $idx")
98-
# if !all_intersected[idx] && LG.intersects(vancouver_polygons[idx], accumulator)
99-
# println("Assimilating $idx")
100-
# result = LG.union(vancouver_polygons[idx], accumulator#=; target = GI.PolygonTrait()=#)
101-
# # @show length(result)
102-
# accumulator = result#[1]
103-
# all_intersected[idx] = true
104-
# end
105-
# end
106-
display(poly(vancouver_polygons[all_intersected]; color = rand(RGBf, sum(all_intersected))))
107-
display(poly(accumulator))
108-
@time while !(all(all_intersected)) && i < length(vancouver_polygons)
109-
println("Began iteration $i")
110-
query_result = STR.query(tree, GI.extent(accumulator))
111-
for idx in query_result
112-
if !(all_intersected[idx] || !(LG.intersects(vancouver_polygons[idx], accumulator)))
113-
println("Assimilating $idx")
114-
result = LG.union(vancouver_polygons[idx], accumulator#=; target = GI.PolygonTrait()=#)
115-
# @show length(result)
116-
accumulator = result#[1]
117-
all_intersected[idx] = true
118-
end
119-
end
120-
display(poly(vancouver_polygons[all_intersected]; color = rand(RGBf, sum(all_intersected))))
121-
println("Finished iteration $i")
122-
# wait_for_key("Press any key to continue to the next iteration.")
123-
i += 1
124-
end
125-
126-
fig = Figure()
127-
ax = Axis(fig[1, 1]; title = "STRTree query for polygons", aspect = DataAspect())
128-
for (i, result_index) in enumerate(query_result)
129-
poly!(ax, vancouver_polygons[result_index]; color = Makie.wong_colors()[i], label = "$result_index")
130-
end
131-
Legend(fig[1, 2], ax)
132-
fig
133-
134-
135-
136-
# TODO:
137-
# - Vancouver watersheds:
138-
# - Intersection on pre-buffered geometry
139-
# - Polygon union by reduction (perhaps pre-sort by border order, so we don't end up with useless polygons)
140-
# - Queries using STRTree.jl
141-
# - Potentially using a prepared geometry based approach to multithreaded reductive joining
142-
# - Implement multipolygon joining. How? Query intersection or touching for each individual geometry,
143-
# and implement a
144-
145-
14665
## Segmentization
14766

14867

@@ -180,9 +99,9 @@ n_points_values = round.(Int, exp10.(LinRange(0.7, 6, 15)))
18099
circle_union_suite["LibGEOS"][n_points] = @be LG.union($lg_circle_left, $lg_circle_right)
181100
end
182101

183-
plot_trials(circle_difference_suite)
184-
plot_trials(circle_intersection_suite)
185-
plot_trials(circle_union_suite)
102+
plot_trials(circle_difference_suite; legend_position = (2, 1), legend_kws = (; orientation = :horizontal, nbanks = 2))
103+
plot_trials(circle_intersection_suite; legend_position = (2, 1), legend_kws = (; orientation = :horizontal, nbanks = 2))
104+
plot_trials(circle_union_suite; legend_position = (2, 1), legend_kws = (; orientation = :horizontal, nbanks = 2))
186105

187106
usa_poly_suite = BenchmarkGroup()
188107
usa_difference_suite = usa_poly_suite["difference"] = BenchmarkGroup(["title:USA difference", "subtitle:Tested on CONUS"])

docs/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
3636
MakieThemes = "e296ed71-da82-5faf-88ab-0034a9761098"
3737
Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7"
3838
MonteCarloMeasurements = "0987c9cc-fe09-11e8-30f0-b96dd679fdca"
39+
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
3940
MultiFloats = "bdf0d083-296b-4888-a5b6-7498122e68a5"
4041
NaturalEarth = "436b0209-26ab-4e65-94a9-6526d86fea76"
4142
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"

0 commit comments

Comments
 (0)