Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
SortTileRecursiveTree = "746ee33f-1797-42c2-866d-db2fce69d14d"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"

[weakdeps]
Expand All @@ -35,6 +36,7 @@ LinearAlgebra = "1"
Proj = "1"
SortTileRecursiveTree = "0.1"
Statistics = "1"
StatsBase = "0.34"
Tables = "1"
julia = "1.9"

Expand Down
2 changes: 2 additions & 0 deletions src/GeometryOps.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ using GeoInterface
using GeometryBasics
import Tables
using LinearAlgebra, Statistics
import StatsBase
import GeometryBasics.StaticArrays
import ExactPredicates
import Base.@kwdef
Expand Down Expand Up @@ -47,6 +48,7 @@ include("methods/geom_relations/intersects.jl")
include("methods/geom_relations/overlaps.jl")
include("methods/geom_relations/touches.jl")
include("methods/geom_relations/within.jl")
include("methods/sample.jl")
include("methods/orientation.jl")
include("methods/polygonize.jl")

Expand Down
61 changes: 61 additions & 0 deletions src/methods/sample.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#=
# Sample

```@example sample
import GeoInterface as GI, GeometryOps as GO
p1 = GI.Polygon([[[-55965.680060140774, -31588.16072168928], [-55956.50771556479, -31478.09258677756], [-31577.548550575284, -6897.015828572996], [-15286.184961223798, -15386.952072224134], [-9074.387601621409, -27468.20712382156], [-8183.4538916097845, -31040.003969070774], [-27011.85123029944, -38229.02388009402], [-54954.72822634951, -32258.9734800704], [-55965.680060140774, -31588.16072168928]]])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use tuple points in examples? (To teach good habits)

points = GO.sample(p1, 100)
using CairoMakie
f, a, p = poly(p1)
scatter!(a, points)
f
```

=#

export sample, UniformSampling

struct UniformSampling
end

application_level(::UniformSampling) = TraitTarget(GI.MultiPolygonTrait(), GI.MultiLineStringTrait(), GI.MultiPointTrait(), GI.PolygonTrait(), GI.LineStringTrait())

function sample(geom, n::Int)
return sample(UniformSampling(), geom, n)
end

function sample(alg, geom, n)
return apply(x -> _sample(alg, GI.trait(x), x, n), application_level(alg), geom)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we even use apply for this? What happens to a feature collection or DataFrame?

My intuitive understanding would be to sample points from all geometries of all of the features weighted by area.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point each feature will be sampled independently, so it's a dataframe to dataframe transfer. Since everything else also works the same way (centroid etc) I think this is at least consistent?

Am open to discussion though, not sure how people generally use this stuff.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GeoPandas samples per geometry, and returns a multipoint:
https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoSeries.sample_points.html

GO.centroid does return a single point even if you pass in a feature collection or table. (There's an issue with the Natural Earth north Korea geometry as well :D)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aah, yeah apply won't work here since it will try to reconstruct.

I guess this is a good time to figure out the generic apply iterator thing?

end

function _sample(alg::UniformSampling, ::Union{GI.PolygonTrait, GI.MultiPolygonTrait}, geom, n)
(; X, Y) = GI.extent(geom)
points = fill((0.0, 0.0), n)
i = 1
while i <= n
x = rand() * (X[2] - X[1]) + X[1]
y = rand() * (Y[2] - Y[1]) + Y[1]
if contains(geom, (x, y))
points[i] = (x, y)
i += 1
end
end
return points
end

function _sample(alg::UniformSampling, ::GI.LineStringTrait, geom, n)
edges = to_edges(geom)
edge_lengths = map(splat(distance), edges)
# normalize the vector
edge_lengths .= edge_lengths
edge_probabilities = edge_lengths ./ sum(edge_lengths)
edge_idxs = 1:length(edges)
return map(1:n) do _
edge_idx = sample(edge_idxs, edge_probabilities)
x1, y1 = edges[edge_idx][1]
x2, y2 = edges[edge_idx][2]
distance = edge_lengths[edge_idx]
t = rand() * distance
(x1 + t * (x2 - x1), y1 + t * (y2 - y1))
end
Comment on lines +52 to +59
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The better way to do this is probably to define a vector of points, and then set indices in that vector. Could also open the way for multithreading in the future.

end