Pure Julia code
Fast, understandable, extensible functions
diff --git a/previews/PR149/404.html b/previews/PR149/404.html new file mode 100644 index 000000000..4240732fb --- /dev/null +++ b/previews/PR149/404.html @@ -0,0 +1,22 @@ + + +
+ + +Warning
This page is still very much WIP!
Documentation for GeometryOps's full API (only for reference!).
GeometryOps.AbstractBarycentricCoordinateMethod
GeometryOps.ClosedRing
GeometryOps.DiffIntersectingPolygons
GeometryOps.DouglasPeucker
GeometryOps.GeodesicSegments
GeometryOps.GeometryCorrection
GeometryOps.LineOrientation
GeometryOps.LinearSegments
GeometryOps.MeanValue
GeometryOps.PointOrientation
GeometryOps.RadialDistance
GeometryOps.SimplifyAlg
GeometryOps.TraitTarget
GeometryOps.UnionIntersectingPolygons
GeometryOps.VisvalingamWhyatt
GeometryOps._det
GeometryOps._equals_curves
GeometryOps.angles
GeometryOps.angles
GeometryOps.apply
GeometryOps.apply
GeometryOps.applyreduce
GeometryOps.applyreduce
GeometryOps.area
GeometryOps.area
GeometryOps.barycentric_coordinates
GeometryOps.barycentric_coordinates
GeometryOps.barycentric_coordinates!
GeometryOps.barycentric_coordinates!
GeometryOps.barycentric_interpolate
GeometryOps.barycentric_interpolate
GeometryOps.centroid
GeometryOps.centroid
GeometryOps.centroid_and_area
GeometryOps.centroid_and_length
GeometryOps.contains
GeometryOps.contains
GeometryOps.coverage
GeometryOps.coveredby
GeometryOps.coveredby
GeometryOps.covers
GeometryOps.covers
GeometryOps.crosses
GeometryOps.crosses
GeometryOps.cut
GeometryOps.difference
GeometryOps.disjoint
GeometryOps.disjoint
GeometryOps.distance
GeometryOps.distance
GeometryOps.embed_extent
GeometryOps.embed_extent
GeometryOps.equals
GeometryOps.equals
GeometryOps.equals
GeometryOps.equals
GeometryOps.equals
GeometryOps.equals
GeometryOps.equals
GeometryOps.equals
GeometryOps.equals
GeometryOps.equals
GeometryOps.equals
GeometryOps.equals
GeometryOps.equals
GeometryOps.equals
GeometryOps.equals
GeometryOps.equals
GeometryOps.flatten
GeometryOps.flatten
GeometryOps.flip
GeometryOps.intersection
GeometryOps.intersection_points
GeometryOps.intersects
GeometryOps.intersects
GeometryOps.isclockwise
GeometryOps.isconcave
GeometryOps.overlaps
GeometryOps.overlaps
GeometryOps.overlaps
GeometryOps.overlaps
GeometryOps.overlaps
GeometryOps.overlaps
GeometryOps.overlaps
GeometryOps.overlaps
GeometryOps.overlaps
GeometryOps.overlaps
GeometryOps.polygon_to_line
GeometryOps.polygonize
GeometryOps.rebuild
GeometryOps.rebuild
GeometryOps.reconstruct
GeometryOps.reconstruct
GeometryOps.reproject
GeometryOps.segmentize
GeometryOps.signed_area
GeometryOps.signed_area
GeometryOps.signed_distance
GeometryOps.signed_distance
GeometryOps.simplify
GeometryOps.t_value
GeometryOps.to_edges
GeometryOps.touches
GeometryOps.touches
GeometryOps.transform
GeometryOps.transform
GeometryOps.tuples
GeometryOps.union
GeometryOps.unwrap
GeometryOps.weighted_mean
GeometryOps.within
GeometryOps.within
apply
and associated functions apply(f, target::Union{TraitTarget, GI.AbstractTrait}, obj; kw...)
Reconstruct a geometry, feature, feature collection, or nested vectors of either using the function f
on the target
trait.
f(target_geom) => x
where x
also has the target
trait, or a trait that can be substituted. For example, swapping PolgonTrait
to MultiPointTrait
will fail if the outer object has MultiPolygonTrait
, but should work if it has FeatureTrait
.
Objects "shallower" than the target trait are always completely rebuilt, like a Vector
of FeatureCollectionTrait
of FeatureTrait
when the target has PolygonTrait
and is held in the features. These will always be GeoInterface geometries/feature/feature collections. But "deeper" objects may remain unchanged or be whatever GeoInterface compatible objects f
returns.
The result is a functionally similar geometry with values depending on f
.
threaded
: true
or false
. Whether to use multithreading. Defaults to false
.
crs
: The CRS to attach to geometries. Defaults to nothing
.
calc_extent
: true
or false
. Whether to calculate the extent. Defaults to false
.
Example
Flipped point the order in any feature or geometry, or iterables of either:
import GeoInterface as GI
+import GeometryOps as GO
+geom = GI.Polygon([GI.LinearRing([(1, 2), (3, 4), (5, 6), (1, 2)]),
+ GI.LinearRing([(3, 4), (5, 6), (6, 7), (3, 4)])])
+
+flipped_geom = GO.apply(GI.PointTrait, geom) do p
+ (GI.y(p), GI.x(p))
+end
applyreduce(f, op, target::Union{TraitTarget, GI.AbstractTrait}, obj; threaded)
Apply function f
to all objects with the target
trait, and reduce the result with an op
like +
.
The order and grouping of application of op
is not guaranteed.
If threaded==true
threads will be used over arrays and iterables, feature collections and nested geometries.
reproject(geometry; source_crs, target_crs, transform, always_xy, time)
+reproject(geometry, source_crs, target_crs; always_xy, time)
+reproject(geometry, transform; always_xy, time)
Reproject any GeoInterface.jl compatible geometry
from source_crs
to target_crs
.
The returned object will be constructed from GeoInterface.WrapperGeometry
geometries, wrapping views of a Vector{Proj.Point{D}}
, where D
is the dimension.
Tip
The Proj.jl
package must be loaded for this method to work, since it is implemented in a package extension.
Arguments
geometry
: Any GeoInterface.jl compatible geometries.
source_crs
: the source coordinate referece system, as a GeoFormatTypes.jl object or a string.
target_crs
: the target coordinate referece system, as a GeoFormatTypes.jl object or a string.
If these a passed as keywords, transform
will take priority. Without it target_crs
is always needed, and source_crs
is needed if it is not retreivable from the geometry with GeoInterface.crs(geometry)
.
Keywords
always_xy
: force x, y coordinate order, true
by default. false
will expect and return points in the crs coordinate order.
time
: the time for the coordinates. Inf
by default.
threaded
: true
or false
. Whether to use multithreading. Defaults to false
.
crs
: The CRS to attach to geometries. Defaults to nothing
.
calc_extent
: true
or false
. Whether to calculate the extent. Defaults to false
.
transform(f, obj)
Apply a function f
to all the points in obj
.
Points will be passed to f
as an SVector
to allow using CoordinateTransformations.jl and Rotations.jl without hassle.
SVector
is also a valid GeoInterface.jl point, so will work in all GeoInterface.jl methods.
Example
julia> import GeoInterface as GI
+
+julia> import GeometryOps as GO
+
+julia> geom = GI.Polygon([GI.LinearRing([(1, 2), (3, 4), (5, 6), (1, 2)]), GI.LinearRing([(3, 4), (5, 6), (6, 7), (3, 4)])]);
+
+julia> f = CoordinateTransformations.Translation(3.5, 1.5)
+Translation(3.5, 1.5)
+
+julia> GO.transform(f, geom)
+GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.Linea
+rRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}(StaticArraysCo
+re.SVector{2, Float64}[[4.5, 3.5], [6.5, 5.5], [8.5, 7.5], [4.5, 3.5]], nothing, nothing), GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}(StaticA
+rraysCore.SVector{2, Float64}[[6.5, 5.5], [8.5, 7.5], [9.5, 8.5], [6.5, 5.5]], nothing, nothing)], nothing, nothing)
With Rotations.jl you need to actuall multiply the Rotation by the SVector
point, which is easy using an anonymous function.
julia> using Rotations
+
+julia> GO.transform(p -> one(RotMatrix{2}) * p, geom)
+GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Int64}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.LinearR
+ing{false, false, Vector{StaticArraysCore.SVector{2, Int64}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Int64}}, Nothing, Nothing}(StaticArraysCore.SVe
+ctor{2, Int64}[[2, 1], [4, 3], [6, 5], [2, 1]], nothing, nothing), GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Int64}}, Nothing, Nothing}(StaticArraysCore.SVector{2, Int64
+}[[4, 3], [6, 5], [7, 6], [4, 3]], nothing, nothing)], nothing, nothing)
contains(g1::AbstractGeometry, g2::AbstractGeometry)::Bool
Return true if the second geometry is completely contained by the first geometry. The interiors of both geometries must intersect and the interior and boundary of the secondary (g2) must not intersect the exterior of the first (g1).
contains
returns the exact opposite result of within
.
Examples
import GeometryOps as GO, GeoInterface as GI
+line = GI.LineString([(1, 1), (1, 2), (1, 3), (1, 4)])
+point = GI.Point((1, 2))
+
+GO.contains(line, point)
+# output
+true
coveredby(g1, g2)::Bool
Return true
if the first geometry is completely covered by the second geometry. The interior and boundary of the primary geometry (g1) must not intersect the exterior of the secondary geometry (g2).
Furthermore, coveredby
returns the exact opposite result of covers
. They are equivalent with the order of the arguments swapped.
Examples
import GeometryOps as GO, GeoInterface as GI
+p1 = GI.Point(0.0, 0.0)
+p2 = GI.Point(1.0, 1.0)
+l1 = GI.Line([p1, p2])
+
+GO.coveredby(p1, l1)
+# output
+true
covers(g1::AbstractGeometry, g2::AbstractGeometry)::Bool
Return true if the first geometry is completely covers the second geometry, The exterior and boundary of the second geometry must not be outside of the interior and boundary of the first geometry. However, the interiors need not intersect.
covers
returns the exact opposite result of coveredby
.
Examples
import GeometryOps as GO, GeoInterface as GI
+l1 = GI.LineString([(1.0, 1.0), (1.0, 2.0), (1.0, 3.0), (1.0, 4.0)])
+l2 = GI.LineString([(1.0, 1.0), (1.0, 2.0)])
+
+GO.covers(l1, l2)
+# output
+true
crosses(geom1, geom2)::Bool
Return true
if the intersection results in a geometry whose dimension is one less than the maximum dimension of the two source geometries and the intersection set is interior to both source geometries.
TODO: broken
Examples
import GeoInterface as GI, GeometryOps as GO
+# TODO: Add working example
disjoint(geom1, geom2)::Bool
Return true
if the first geometry is disjoint from the second geometry.
Return true
if the first geometry is disjoint from the second geometry. The interiors and boundaries of both geometries must not intersect.
Examples
import GeometryOps as GO, GeoInterface as GI
+
+line = GI.LineString([(1, 1), (1, 2), (1, 3), (1, 4)])
+point = (2, 2)
+GO.disjoint(point, line)
+
+# output
+true
intersects(geom1, geom2)::Bool
Return true if the interiors or boundaries of the two geometries interact.
intersects
returns the exact opposite result of disjoint
.
Example
import GeoInterface as GI, GeometryOps as GO
+
+line1 = GI.Line([(124.584961,-12.768946), (126.738281,-17.224758)])
+line2 = GI.Line([(123.354492,-15.961329), (127.22168,-14.008696)])
+GO.intersects(line1, line2)
+
+# output
+true
overlaps(geom1, geom2)::Bool
Compare two Geometries of the same dimension and return true if their intersection set results in a geometry different from both but of the same dimension. This means one geometry cannot be within or contain the other and they cannot be equal
Examples
import GeometryOps as GO, GeoInterface as GI
+poly1 = GI.Polygon([[(0,0), (0,5), (5,5), (5,0), (0,0)]])
+poly2 = GI.Polygon([[(1,1), (1,6), (6,6), (6,1), (1,1)]])
+
+GO.overlaps(poly1, poly2)
+# output
+true
overlaps(::GI.AbstractTrait, geom1, ::GI.AbstractTrait, geom2)::Bool
For any non-specified pair, all have non-matching dimensions, return false.
overlaps(
+ ::GI.MultiPointTrait, points1,
+ ::GI.MultiPointTrait, points2,
+)::Bool
If the multipoints overlap, meaning some, but not all, of the points within the multipoints are shared, return true.
overlaps(::GI.LineTrait, line1, ::GI.LineTrait, line)::Bool
If the lines overlap, meaning that they are colinear but each have one endpoint outside of the other line, return true. Else false.
overlaps(
+ ::Union{GI.LineStringTrait, GI.LinearRing}, line1,
+ ::Union{GI.LineStringTrait, GI.LinearRing}, line2,
+)::Bool
If the curves overlap, meaning that at least one edge of each curve overlaps, return true. Else false.
overlaps(
+ trait_a::GI.PolygonTrait, poly_a,
+ trait_b::GI.PolygonTrait, poly_b,
+)::Bool
If the two polygons intersect with one another, but are not equal, return true. Else false.
overlaps(
+ ::GI.PolygonTrait, poly1,
+ ::GI.MultiPolygonTrait, polys2,
+)::Bool
Return true if polygon overlaps with at least one of the polygons within the multipolygon. Else false.
overlaps(
+ ::GI.MultiPolygonTrait, polys1,
+ ::GI.PolygonTrait, poly2,
+)::Bool
Return true if polygon overlaps with at least one of the polygons within the multipolygon. Else false.
overlaps(
+ ::GI.MultiPolygonTrait, polys1,
+ ::GI.MultiPolygonTrait, polys2,
+)::Bool
Return true if at least one pair of polygons from multipolygons overlap. Else false.
touches(geom1, geom2)::Bool
Return true
if the first geometry touches the second geometry. In other words, the two interiors cannot interact, but one of the geometries must have a boundary point that interacts with either the other geometies interior or boundary.
Examples
import GeometryOps as GO, GeoInterface as GI
+
+l1 = GI.Line([(0.0, 0.0), (1.0, 0.0)])
+l2 = GI.Line([(1.0, 1.0), (1.0, -1.0)])
+
+GO.touches(l1, l2)
+# output
+true
within(geom1, geom2)::Bool
Return true
if the first geometry is completely within the second geometry. The interiors of both geometries must intersect and the interior and boundary of the primary geometry (geom1) must not intersect the exterior of the secondary geometry (geom2).
Furthermore, within
returns the exact opposite result of contains
.
Examples
import GeometryOps as GO, GeoInterface as GI
+
+line = GI.LineString([(1, 1), (1, 2), (1, 3), (1, 4)])
+point = (1, 2)
+GO.within(point, line)
+
+# output
+true
equals(geom1, geom2)::Bool
Compare two Geometries return true if they are the same geometry.
Examples
import GeometryOps as GO, GeoInterface as GI
+poly1 = GI.Polygon([[(0,0), (0,5), (5,5), (5,0), (0,0)]])
+poly2 = GI.Polygon([[(0,0), (0,5), (5,5), (5,0), (0,0)]])
+
+GO.equals(poly1, poly2)
+# output
+true
equals(::T, geom_a, ::T, geom_b)::Bool
Two geometries of the same type, which don't have a equals function to dispatch off of should throw an error.
equals(trait_a, geom_a, trait_b, geom_b)
Two geometries which are not of the same type cannot be equal so they always return false.
equals(::GI.PointTrait, p1, ::GI.PointTrait, p2)::Bool
Two points are the same if they have the same x and y (and z if 3D) coordinates.
equals(::GI.PointTrait, p1, ::GI.MultiPointTrait, mp2)::Bool
A point and a multipoint are equal if the multipoint is composed of a single point that is equivalent to the given point.
equals(::GI.MultiPointTrait, mp1, ::GI.PointTrait, p2)::Bool
A point and a multipoint are equal if the multipoint is composed of a single point that is equivalent to the given point.
equals(::GI.MultiPointTrait, mp1, ::GI.MultiPointTrait, mp2)::Bool
Two multipoints are equal if they share the same set of points.
equals(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, l1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, l2,
+)::Bool
Two lines/linestrings are equal if they share the same set of points going along the curve. Note that lines/linestrings aren't closed by defintion.
equals(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, l1,
+ ::GI.LinearRingTrait, l2,
+)::Bool
A line/linestring and a linear ring are equal if they share the same set of points going along the curve. Note that lines aren't closed by defintion, but rings are, so the line must have a repeated last point to be equal
equals(
+ ::GI.LinearRingTrait, l1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, l2,
+)::Bool
A linear ring and a line/linestring are equal if they share the same set of points going along the curve. Note that lines aren't closed by defintion, but rings are, so the line must have a repeated last point to be equal
equals(
+ ::GI.LinearRingTrait, l1,
+ ::GI.LinearRingTrait, l2,
+)::Bool
Two linear rings are equal if they share the same set of points going along the curve. Note that rings are closed by definition, so they can have, but don't need, a repeated last point to be equal.
equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool
Two polygons are equal if they share the same exterior edge and holes.
equals(::GI.PolygonTrait, geom_a, ::GI.MultiPolygonTrait, geom_b)::Bool
A polygon and a multipolygon are equal if the multipolygon is composed of a single polygon that is equivalent to the given polygon.
equals(::GI.MultiPolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool
A polygon and a multipolygon are equal if the multipolygon is composed of a single polygon that is equivalent to the given polygon.
equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool
Two multipolygons are equal if they share the same set of polygons.
centroid(geom, [T=Float64])::Tuple{T, T}
Returns the centroid of a given line segment, linear ring, polygon, or mutlipolygon.
distance(point, geom, ::Type{T} = Float64)::T
Calculates the ditance from the geometry g1
to the point
. The distance will always be positive or zero.
The method will differ based on the type of the geometry provided: - The distance from a point to a point is just the Euclidean distance between the points. - The distance from a point to a line is the minimum distance from the point to the closest point on the given line. - The distance from a point to a linestring is the minimum distance from the point to the closest segment of the linestring. - The distance from a point to a linear ring is the minimum distance from the point to the closest segment of the linear ring. - The distance from a point to a polygon is zero if the point is within the polygon and otherwise is the minimum distance from the point to an edge of the polygon. This includes edges created by holes. - The distance from a point to a multigeometry or a geometry collection is the minimum distance between the point and any of the sub-geometries.
Result will be of type T, where T is an optional argument with a default value of Float64.
signed_distance(point, geom, ::Type{T} = Float64)::T
Calculates the signed distance from the geometry geom
to the given point. Points within geom
have a negative signed distance, and points outside of geom
have a positive signed distance. - The signed distance from a point to a point, line, linestring, or linear ring is equal to the distance between the two. - The signed distance from a point to a polygon is negative if the point is within the polygon and is positive otherwise. The value of the distance is the minimum distance from the point to an edge of the polygon. This includes edges created by holes. - The signed distance from a point to a multigeometry or a geometry collection is the minimum signed distance between the point and any of the sub-geometries.
Result will be of type T, where T is an optional argument with a default value of Float64.
area(geom, [T = Float64])::T
Returns the area of a geometry or collection of geometries. This is computed slightly differently for different geometries:
- The area of a point/multipoint is always zero.
+- The area of a curve/multicurve is always zero.
+- The area of a polygon is the absolute value of the signed area.
+- The area multi-polygon is the sum of the areas of all of the sub-polygons.
+- The area of a geometry collection, feature collection of array/iterable
+ is the sum of the areas of all of the sub-geometries.
Result will be of type T, where T is an optional argument with a default value of Float64.
signed_area(geom, [T = Float64])::T
Returns the signed area of a single geometry, based on winding order. This is computed slighly differently for different geometries:
- The signed area of a point is always zero.
+- The signed area of a curve is always zero.
+- The signed area of a polygon is computed with the shoelace formula and is
+positive if the polygon coordinates wind clockwise and negative if
+counterclockwise.
+- You cannot compute the signed area of a multipolygon as it doesn't have a
+meaning as each sub-polygon could have a different winding order.
Result will be of type T, where T is an optional argument with a default value of Float64.
angles(geom, ::Type{T} = Float64)
Returns the angles of a geometry or collection of geometries. This is computed differently for different geometries:
- The angles of a point is an empty vector.
+- The angles of a single line segment is an empty vector.
+- The angles of a linestring or linearring is a vector of angles formed by the curve.
+- The angles of a polygin is a vector of vectors of angles formed by each ring.
+- The angles of a multi-geometry collection is a vector of the angles of each of the
+ sub-geometries as defined above.
Result will be a Vector, or nested set of vectors, of type T where an optional argument with a default value of Float64.
embed_extent(obj)
Recursively wrap the object with a GeoInterface.jl geometry, calculating and adding an Extents.Extent
to all objects.
This can improve performance when extents need to be checked multiple times, such when needing to check if many points are in geometries, and using their extents as a quick filter for obviously exterior points.
Keywords
threaded
: true
or false
. Whether to use multithreading. Defaults to false
.
crs
: The CRS to attach to geometries. Defaults to nothing
.
barycentric_coordinates(method = MeanValue(), polygon, point)
Returns the barycentric coordinates of point
in polygon
using the barycentric coordinate method method
.
barycentric_coordinates!(λs::Vector{<: Real}, method::AbstractBarycentricCoordinateMethod, polygon, point)
Loads the barycentric coordinates of point
in polygon
into λs
using the barycentric coordinate method method
.
λs
must be of the length of the polygon plus its holes.
Tip
Use this method to avoid excess allocations when you need to calculate barycentric coordinates for many points.
barycentric_interpolate(method = MeanValue(), polygon, values::AbstractVector{V}, point)
Returns the interpolated value at point
within polygon
using the barycentric coordinate method method
. values
are the per-point values for the polygon which are to be interpolated.
Returns an object of type V
.
Warning
Barycentric interpolation is currently defined only for 2-dimensional polygons. If you pass a 3-D polygon in, the Z coordinate will be used as per-vertex value to be interpolated (the M coordinate in GIS parlance).
abstract type AbstractBarycentricCoordinateMethod
Abstract supertype for barycentric coordinate methods. The subtypes may serve as dispatch types, or may cache some information about the target polygon.
API
The following methods must be implemented for all subtypes:
barycentric_coordinates!(λs::Vector{<: Real}, method::AbstractBarycentricCoordinateMethod, exterior::Vector{<: Point{2, T1}}, point::Point{2, T2})
barycentric_interpolate(method::AbstractBarycentricCoordinateMethod, exterior::Vector{<: Point{2, T1}}, values::Vector{V}, point::Point{2, T2})::V
barycentric_interpolate(method::AbstractBarycentricCoordinateMethod, exterior::Vector{<: Point{2, T1}}, interiors::Vector{<: Vector{<: Point{2, T1}}} values::Vector{V}, point::Point{2, T2})::V
The rest of the methods will be implemented in terms of these, and have efficient dispatches for broadcasting.
ClosedRing() <: GeometryCorrection
This correction ensures that a polygon's exterior and interior rings are closed.
It can be called on any geometry correction as usual.
See also GeometryCorrection
.
DiffIntersectingPolygons() <: GeometryCorrection
This correction ensures that the polygons included in a multipolygon aren't intersecting. If any polygon's are intersecting, they will be made nonintersecting through the difference
operation to create a unique set of disjoint (other than potentially connections by a single point) polygons covering the same area. See also GeometryCorrection
, UnionIntersectingPolygons
.
DouglasPeucker <: SimplifyAlg
+
+DouglasPeucker(; number, ratio, tol)
Simplifies geometries by removing points below tol
distance from the line between its neighboring points.
Keywords
ratio
: the fraction of points that should remain after simplify
. Useful as it will generalise for large collections of objects.
number
: the number of points that should remain after simplify
. Less useful for large collections of mixed size objects.
tol
: the minimum distance a point will be from the line joining its neighboring points.
Note: user input tol
is squared to avoid uneccesary computation in algorithm.
GeodesicSegments(; max_distance::Real, equatorial_radius::Real=6378137, flattening::Real=1/298.257223563)
A method for segmentizing geometries by adding extra vertices to the geometry so that no segment is longer than a given distance. This method calculates the distance between points on the geodesic, and assumes input in lat/long coordinates.
Warning
Any input geometries must be in lon/lat coordinates! If not, the method may fail or error.
Arguments
max_distance::Real
: The maximum distance, in meters, between vertices in the geometry.
equatorial_radius::Real=6378137
: The equatorial radius of the Earth, in meters. Passed to Proj.geod_geodesic
.
flattening::Real=1/298.257223563
: The flattening of the Earth, which is the ratio of the difference between the equatorial and polar radii to the equatorial radius. Passed to Proj.geod_geodesic
.
One can also omit the equatorial_radius
and flattening
keyword arguments, and pass a geodesic
object directly to the eponymous keyword.
This method uses the Proj/GeographicLib API for geodesic calculations.
abstract type GeometryCorrection
This abstract type represents a geometry correction.
Interface
Any GeometryCorrection
must implement two functions: * application_level(::GeometryCorrection)::AbstractGeometryTrait
: This function should return the GeoInterface
trait that the correction is intended to be applied to, like PointTrait
or LineStringTrait
or PolygonTrait
. * (::GeometryCorrection)(::AbstractGeometryTrait, geometry)::(some_geometry)
: This function should apply the correction to the given geometry, and return a new geometry.
Enum LineOrientation
Enum for the orientation of a line with respect to a curve. A line can be line_cross
(crossing over the curve), line_hinge
(crossing the endpoint of the curve), line_over
(colinear with the curve), or line_out
(not interacting with the curve).
LinearSegments(; max_distance::Real)
A method for segmentizing geometries by adding extra vertices to the geometry so that no segment is longer than a given distance.
Here, max_distance
is a purely nondimensional quantity and will apply in the input space. This is to say, that if the polygon is provided in lat/lon coordinates then the max_distance
will be in degrees of arc. If the polygon is provided in meters, then the max_distance
will be in meters.
MeanValue() <: AbstractBarycentricCoordinateMethod
This method calculates barycentric coordinates using the mean value method.
References
Enum PointOrientation
Enum for the orientation of a point with respect to a curve. A point can be point_in
the curve, point_on
the curve, or point_out
of the curve.
RadialDistance <: SimplifyAlg
Simplifies geometries by removing points less than tol
distance from the line between its neighboring points.
Keywords
ratio
: the fraction of points that should remain after simplify
. Useful as it will generalise for large collections of objects.
number
: the number of points that should remain after simplify
. Less useful for large collections of mixed size objects.
tol
: the minimum distance between points.
Note: user input tol
is squared to avoid uneccesary computation in algorithm.
abstract type SimplifyAlg
Abstract type for simplification algorithms.
API
For now, the algorithm must hold the number
, ratio
and tol
properties.
Simplification algorithm types can hook into the interface by implementing the _simplify(trait, alg, geom)
methods for whichever traits are necessary.
TraitTarget{T}
This struct holds a trait parameter or a union of trait parameters.
It is primarily used for dispatch into methods which select trait levels, like apply
, or as a parameter to target
.
Constructors
TraitTarget(GI.PointTrait())
+TraitTarget(GI.LineStringTrait(), GI.LinearRingTrait()) # and other traits as you may like
+TraitTarget(TraitTarget(...))
+# There are also type based constructors available, but that's not advised.
+TraitTarget(GI.PointTrait)
+TraitTarget(Union{GI.LineStringTrait, GI.LinearRingTrait})
+# etc.
UnionIntersectingPolygons() <: GeometryCorrection
This correction ensures that the polygon's included in a multipolygon aren't intersecting. If any polygon's are intersecting, they will be combined through the union operation to create a unique set of disjoint (other than potentially connections by a single point) polygons covering the same area.
See also GeometryCorrection
.
VisvalingamWhyatt <: SimplifyAlg
+
+VisvalingamWhyatt(; kw...)
Simplifies geometries by removing points below tol
distance from the line between its neighboring points.
Keywords
ratio
: the fraction of points that should remain after simplify
. Useful as it will generalise for large collections of objects.
number
: the number of points that should remain after simplify
. Less useful for large collections of mixed size objects.
tol
: the minimum area of a triangle made with a point and its neighboring points.
Note: user input tol
is doubled to avoid uneccesary computation in algorithm.
_det(s1::Point2{T1}, s2::Point2{T2}) where {T1 <: Real, T2 <: Real}
Returns the determinant of the matrix formed by hcat
'ing two points s1
and s2
.
Specifically, this is:
s1[1] * s2[2] - s1[2] * s2[1]
_equals_curves(c1, c2, closed_type1, closed_type2)::Bool
Two curves are equal if they share the same set of point, representing the same geometry. Both curves must must be composed of the same set of points, however, they do not have to wind in the same direction, or start on the same point to be equivalent. Inputs: c1 first geometry c2 second geometry closed_type1::Bool true if c1 is closed by definition (polygon, linear ring) closed_type2::Bool true if c2 is closed by definition (polygon, linear ring)
angles(geom, ::Type{T} = Float64)
Returns the angles of a geometry or collection of geometries. This is computed differently for different geometries:
- The angles of a point is an empty vector.
+- The angles of a single line segment is an empty vector.
+- The angles of a linestring or linearring is a vector of angles formed by the curve.
+- The angles of a polygin is a vector of vectors of angles formed by each ring.
+- The angles of a multi-geometry collection is a vector of the angles of each of the
+ sub-geometries as defined above.
Result will be a Vector, or nested set of vectors, of type T where an optional argument with a default value of Float64.
apply(f, target::Union{TraitTarget, GI.AbstractTrait}, obj; kw...)
Reconstruct a geometry, feature, feature collection, or nested vectors of either using the function f
on the target
trait.
f(target_geom) => x
where x
also has the target
trait, or a trait that can be substituted. For example, swapping PolgonTrait
to MultiPointTrait
will fail if the outer object has MultiPolygonTrait
, but should work if it has FeatureTrait
.
Objects "shallower" than the target trait are always completely rebuilt, like a Vector
of FeatureCollectionTrait
of FeatureTrait
when the target has PolygonTrait
and is held in the features. These will always be GeoInterface geometries/feature/feature collections. But "deeper" objects may remain unchanged or be whatever GeoInterface compatible objects f
returns.
The result is a functionally similar geometry with values depending on f
.
threaded
: true
or false
. Whether to use multithreading. Defaults to false
.
crs
: The CRS to attach to geometries. Defaults to nothing
.
calc_extent
: true
or false
. Whether to calculate the extent. Defaults to false
.
Example
Flipped point the order in any feature or geometry, or iterables of either:
import GeoInterface as GI
+import GeometryOps as GO
+geom = GI.Polygon([GI.LinearRing([(1, 2), (3, 4), (5, 6), (1, 2)]),
+ GI.LinearRing([(3, 4), (5, 6), (6, 7), (3, 4)])])
+
+flipped_geom = GO.apply(GI.PointTrait, geom) do p
+ (GI.y(p), GI.x(p))
+end
applyreduce(f, op, target::Union{TraitTarget, GI.AbstractTrait}, obj; threaded)
Apply function f
to all objects with the target
trait, and reduce the result with an op
like +
.
The order and grouping of application of op
is not guaranteed.
If threaded==true
threads will be used over arrays and iterables, feature collections and nested geometries.
area(geom, [T = Float64])::T
Returns the area of a geometry or collection of geometries. This is computed slightly differently for different geometries:
- The area of a point/multipoint is always zero.
+- The area of a curve/multicurve is always zero.
+- The area of a polygon is the absolute value of the signed area.
+- The area multi-polygon is the sum of the areas of all of the sub-polygons.
+- The area of a geometry collection, feature collection of array/iterable
+ is the sum of the areas of all of the sub-geometries.
Result will be of type T, where T is an optional argument with a default value of Float64.
barycentric_coordinates!(λs::Vector{<: Real}, method::AbstractBarycentricCoordinateMethod, polygon, point)
Loads the barycentric coordinates of point
in polygon
into λs
using the barycentric coordinate method method
.
λs
must be of the length of the polygon plus its holes.
Tip
Use this method to avoid excess allocations when you need to calculate barycentric coordinates for many points.
barycentric_coordinates(method = MeanValue(), polygon, point)
Returns the barycentric coordinates of point
in polygon
using the barycentric coordinate method method
.
barycentric_interpolate(method = MeanValue(), polygon, values::AbstractVector{V}, point)
Returns the interpolated value at point
within polygon
using the barycentric coordinate method method
. values
are the per-point values for the polygon which are to be interpolated.
Returns an object of type V
.
Warning
Barycentric interpolation is currently defined only for 2-dimensional polygons. If you pass a 3-D polygon in, the Z coordinate will be used as per-vertex value to be interpolated (the M coordinate in GIS parlance).
centroid(geom, [T=Float64])::Tuple{T, T}
Returns the centroid of a given line segment, linear ring, polygon, or mutlipolygon.
centroid_and_area(geom, [T=Float64])::(::Tuple{T, T}, ::Real)
Returns the centroid and area of a given geometry.
centroid_and_length(geom, [T=Float64])::(::Tuple{T, T}, ::Real)
Returns the centroid and length of a given line/ring. Note this is only valid for line strings and linear rings.
contains(g1::AbstractGeometry, g2::AbstractGeometry)::Bool
Return true if the second geometry is completely contained by the first geometry. The interiors of both geometries must intersect and the interior and boundary of the secondary (g2) must not intersect the exterior of the first (g1).
contains
returns the exact opposite result of within
.
Examples
import GeometryOps as GO, GeoInterface as GI
+line = GI.LineString([(1, 1), (1, 2), (1, 3), (1, 4)])
+point = GI.Point((1, 2))
+
+GO.contains(line, point)
+# output
+true
coverage(geom, xmin, xmax, ymin, ymax, [T = Float64])::T
Returns the area of intersection between given geometry and grid cell defined by its minimum and maximum x and y-values. This is computed differently for different geometries:
The signed area of a point is always zero.
The signed area of a curve is always zero.
The signed area of a polygon is calculated by tracing along its edges and switching to the cell edges if needed.
The coverage of a geometry collection, multi-geometry, feature collection of array/iterable is the sum of the coverages of all of the sub-geometries.
Result will be of type T, where T is an optional argument with a default value of Float64.
coveredby(g1, g2)::Bool
Return true
if the first geometry is completely covered by the second geometry. The interior and boundary of the primary geometry (g1) must not intersect the exterior of the secondary geometry (g2).
Furthermore, coveredby
returns the exact opposite result of covers
. They are equivalent with the order of the arguments swapped.
Examples
import GeometryOps as GO, GeoInterface as GI
+p1 = GI.Point(0.0, 0.0)
+p2 = GI.Point(1.0, 1.0)
+l1 = GI.Line([p1, p2])
+
+GO.coveredby(p1, l1)
+# output
+true
covers(g1::AbstractGeometry, g2::AbstractGeometry)::Bool
Return true if the first geometry is completely covers the second geometry, The exterior and boundary of the second geometry must not be outside of the interior and boundary of the first geometry. However, the interiors need not intersect.
covers
returns the exact opposite result of coveredby
.
Examples
import GeometryOps as GO, GeoInterface as GI
+l1 = GI.LineString([(1.0, 1.0), (1.0, 2.0), (1.0, 3.0), (1.0, 4.0)])
+l2 = GI.LineString([(1.0, 1.0), (1.0, 2.0)])
+
+GO.covers(l1, l2)
+# output
+true
crosses(geom1, geom2)::Bool
Return true
if the intersection results in a geometry whose dimension is one less than the maximum dimension of the two source geometries and the intersection set is interior to both source geometries.
TODO: broken
Examples
import GeoInterface as GI, GeometryOps as GO
+# TODO: Add working example
cut(geom, line, [T::Type])
Return given geom cut by given line as a list of geometries of the same type as the input geom. Return the original geometry as only list element if none are found. Line must cut fully through given geometry or the original geometry will be returned.
Note: This currently doesn't work for degenerate cases there line crosses through vertices.
Example
import GeoInterface as GI, GeometryOps as GO
+
+poly = GI.Polygon([[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0), (0.0, 0.0)]])
+line = GI.Line([(5.0, -5.0), (5.0, 15.0)])
+cut_polys = GO.cut(poly, line)
+GI.coordinates.(cut_polys)
+
+# output
+2-element Vector{Vector{Vector{Vector{Float64}}}}:
+ [[[0.0, 0.0], [5.0, 0.0], [5.0, 10.0], [0.0, 10.0], [0.0, 0.0]]]
+ [[[5.0, 0.0], [10.0, 0.0], [10.0, 10.0], [5.0, 10.0], [5.0, 0.0]]]
difference(geom_a, geom_b, [T::Type]; target::Type, fix_multipoly = UnionIntersectingPolygons())
Return the difference between two geometries as a list of geometries. Return an empty list if none are found. The type of the list will be constrained as much as possible given the input geometries. Furthermore, the user can provide a taget
type as a keyword argument and a list of target geometries found in the difference will be returned. The user can also provide a float type that they would like the points of returned geometries to be. If the user is taking a intersection involving one or more multipolygons, and the multipolygon might be comprised of polygons that intersect, if fix_multipoly
is set to an IntersectingPolygons
correction (the default is UnionIntersectingPolygons()
), then the needed multipolygons will be fixed to be valid before performing the intersection to ensure a correct answer. Only set fix_multipoly
to false if you know that the multipolygons are valid, as it will avoid unneeded computation.
Example
import GeoInterface as GI, GeometryOps as GO
+
+poly1 = GI.Polygon([[[0.0, 0.0], [5.0, 5.0], [10.0, 0.0], [5.0, -5.0], [0.0, 0.0]]])
+poly2 = GI.Polygon([[[3.0, 0.0], [8.0, 5.0], [13.0, 0.0], [8.0, -5.0], [3.0, 0.0]]])
+diff_poly = GO.difference(poly1, poly2; target = GI.PolygonTrait())
+GI.coordinates.(diff_poly)
+
+# output
+1-element Vector{Vector{Vector{Vector{Float64}}}}:
+ [[[6.5, 3.5], [5.0, 5.0], [0.0, 0.0], [5.0, -5.0], [6.5, -3.5], [3.0, 0.0], [6.5, 3.5]]]
disjoint(geom1, geom2)::Bool
Return true
if the first geometry is disjoint from the second geometry.
Return true
if the first geometry is disjoint from the second geometry. The interiors and boundaries of both geometries must not intersect.
Examples
import GeometryOps as GO, GeoInterface as GI
+
+line = GI.LineString([(1, 1), (1, 2), (1, 3), (1, 4)])
+point = (2, 2)
+GO.disjoint(point, line)
+
+# output
+true
distance(point, geom, ::Type{T} = Float64)::T
Calculates the ditance from the geometry g1
to the point
. The distance will always be positive or zero.
The method will differ based on the type of the geometry provided: - The distance from a point to a point is just the Euclidean distance between the points. - The distance from a point to a line is the minimum distance from the point to the closest point on the given line. - The distance from a point to a linestring is the minimum distance from the point to the closest segment of the linestring. - The distance from a point to a linear ring is the minimum distance from the point to the closest segment of the linear ring. - The distance from a point to a polygon is zero if the point is within the polygon and otherwise is the minimum distance from the point to an edge of the polygon. This includes edges created by holes. - The distance from a point to a multigeometry or a geometry collection is the minimum distance between the point and any of the sub-geometries.
Result will be of type T, where T is an optional argument with a default value of Float64.
embed_extent(obj)
Recursively wrap the object with a GeoInterface.jl geometry, calculating and adding an Extents.Extent
to all objects.
This can improve performance when extents need to be checked multiple times, such when needing to check if many points are in geometries, and using their extents as a quick filter for obviously exterior points.
Keywords
threaded
: true
or false
. Whether to use multithreading. Defaults to false
.
crs
: The CRS to attach to geometries. Defaults to nothing
.
equals(trait_a, geom_a, trait_b, geom_b)
Two geometries which are not of the same type cannot be equal so they always return false.
equals(geom1, geom2)::Bool
Compare two Geometries return true if they are the same geometry.
Examples
import GeometryOps as GO, GeoInterface as GI
+poly1 = GI.Polygon([[(0,0), (0,5), (5,5), (5,0), (0,0)]])
+poly2 = GI.Polygon([[(0,0), (0,5), (5,5), (5,0), (0,0)]])
+
+GO.equals(poly1, poly2)
+# output
+true
equals(
+ ::GI.LinearRingTrait, l1,
+ ::GI.LinearRingTrait, l2,
+)::Bool
Two linear rings are equal if they share the same set of points going along the curve. Note that rings are closed by definition, so they can have, but don't need, a repeated last point to be equal.
equals(
+ ::GI.LinearRingTrait, l1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, l2,
+)::Bool
A linear ring and a line/linestring are equal if they share the same set of points going along the curve. Note that lines aren't closed by defintion, but rings are, so the line must have a repeated last point to be equal
equals(::GI.MultiPointTrait, mp1, ::GI.MultiPointTrait, mp2)::Bool
Two multipoints are equal if they share the same set of points.
equals(::GI.MultiPointTrait, mp1, ::GI.PointTrait, p2)::Bool
A point and a multipoint are equal if the multipoint is composed of a single point that is equivalent to the given point.
equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool
Two multipolygons are equal if they share the same set of polygons.
equals(::GI.MultiPolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool
A polygon and a multipolygon are equal if the multipolygon is composed of a single polygon that is equivalent to the given polygon.
equals(::GI.PointTrait, p1, ::GI.MultiPointTrait, mp2)::Bool
A point and a multipoint are equal if the multipoint is composed of a single point that is equivalent to the given point.
equals(::GI.PointTrait, p1, ::GI.PointTrait, p2)::Bool
Two points are the same if they have the same x and y (and z if 3D) coordinates.
equals(::GI.PolygonTrait, geom_a, ::GI.MultiPolygonTrait, geom_b)::Bool
A polygon and a multipolygon are equal if the multipolygon is composed of a single polygon that is equivalent to the given polygon.
equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool
Two polygons are equal if they share the same exterior edge and holes.
equals(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, l1,
+ ::GI.LinearRingTrait, l2,
+)::Bool
A line/linestring and a linear ring are equal if they share the same set of points going along the curve. Note that lines aren't closed by defintion, but rings are, so the line must have a repeated last point to be equal
equals(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, l1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, l2,
+)::Bool
Two lines/linestrings are equal if they share the same set of points going along the curve. Note that lines/linestrings aren't closed by defintion.
equals(::T, geom_a, ::T, geom_b)::Bool
Two geometries of the same type, which don't have a equals function to dispatch off of should throw an error.
flatten(target::Type{<:GI.AbstractTrait}, obj)
+flatten(f, target::Type{<:GI.AbstractTrait}, obj)
Lazily flatten any AbstractArray
, iterator, FeatureCollectionTrait
, FeatureTrait
or AbstractGeometryTrait
object obj
, so that objects with the target
trait are returned by the iterator.
If f
is passed in it will be applied to the target geometries.
flip(obj)
Swap all of the x and y coordinates in obj, otherwise keeping the original structure (but not necessarily the original type).
Keywords
threaded
: true
or false
. Whether to use multithreading. Defaults to false
.
crs
: The CRS to attach to geometries. Defaults to nothing
.
calc_extent
: true
or false
. Whether to calculate the extent. Defaults to false
.
intersection(geom_a, geom_b, [T::Type]; target::Type, fix_multipoly = UnionIntersectingPolygons())
Return the intersection between two geometries as a list of geometries. Return an empty list if none are found. The type of the list will be constrained as much as possible given the input geometries. Furthermore, the user can provide a target
type as a keyword argument and a list of target geometries found in the intersection will be returned. The user can also provide a float type that they would like the points of returned geometries to be. If the user is taking a intersection involving one or more multipolygons, and the multipolygon might be comprised of polygons that intersect, if fix_multipoly
is set to an IntersectingPolygons
correction (the default is UnionIntersectingPolygons()
), then the needed multipolygons will be fixed to be valid before performing the intersection to ensure a correct answer. Only set fix_multipoly
to nothing if you know that the multipolygons are valid, as it will avoid unneeded computation.
Example
import GeoInterface as GI, GeometryOps as GO
+
+line1 = GI.Line([(124.584961,-12.768946), (126.738281,-17.224758)])
+line2 = GI.Line([(123.354492,-15.961329), (127.22168,-14.008696)])
+inter_points = GO.intersection(line1, line2; target = GI.PointTrait())
+GI.coordinates.(inter_points)
+
+# output
+1-element Vector{Vector{Float64}}:
+ [125.58375366067548, -14.83572303404496]
intersection_points(
+ geom_a,
+ geom_b,
+)::Union{
+ ::Vector{::Tuple{::Real, ::Real}},
+ ::Nothing,
+}
Return a list of intersection points between two geometries of type GI.Point. If no intersection point was possible given geometry extents, returns an empty list.
intersects(geom1, geom2)::Bool
Return true if the interiors or boundaries of the two geometries interact.
intersects
returns the exact opposite result of disjoint
.
Example
import GeoInterface as GI, GeometryOps as GO
+
+line1 = GI.Line([(124.584961,-12.768946), (126.738281,-17.224758)])
+line2 = GI.Line([(123.354492,-15.961329), (127.22168,-14.008696)])
+GO.intersects(line1, line2)
+
+# output
+true
isclockwise(line::Union{LineString, Vector{Position}})::Bool
Take a ring and return true or false whether or not the ring is clockwise or counter-clockwise.
Example
import GeoInterface as GI, GeometryOps as GO
+
+ring = GI.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)])
+GO.isclockwise(ring)
+
+# output
+true
isconcave(poly::Polygon)::Bool
Take a polygon and return true or false as to whether it is concave or not.
Examples
import GeoInterface as GI, GeometryOps as GO
+
+poly = GI.Polygon([[(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]])
+GO.isconcave(poly)
+
+# output
+false
overlaps(geom1, geom2)::Bool
Compare two Geometries of the same dimension and return true if their intersection set results in a geometry different from both but of the same dimension. This means one geometry cannot be within or contain the other and they cannot be equal
Examples
import GeometryOps as GO, GeoInterface as GI
+poly1 = GI.Polygon([[(0,0), (0,5), (5,5), (5,0), (0,0)]])
+poly2 = GI.Polygon([[(1,1), (1,6), (6,6), (6,1), (1,1)]])
+
+GO.overlaps(poly1, poly2)
+# output
+true
overlaps(::GI.AbstractTrait, geom1, ::GI.AbstractTrait, geom2)::Bool
For any non-specified pair, all have non-matching dimensions, return false.
overlaps(::GI.LineTrait, line1, ::GI.LineTrait, line)::Bool
If the lines overlap, meaning that they are colinear but each have one endpoint outside of the other line, return true. Else false.
overlaps(
+ ::GI.MultiPointTrait, points1,
+ ::GI.MultiPointTrait, points2,
+)::Bool
If the multipoints overlap, meaning some, but not all, of the points within the multipoints are shared, return true.
overlaps(
+ ::GI.MultiPolygonTrait, polys1,
+ ::GI.MultiPolygonTrait, polys2,
+)::Bool
Return true if at least one pair of polygons from multipolygons overlap. Else false.
overlaps(
+ ::GI.MultiPolygonTrait, polys1,
+ ::GI.PolygonTrait, poly2,
+)::Bool
Return true if polygon overlaps with at least one of the polygons within the multipolygon. Else false.
overlaps(
+ ::GI.PolygonTrait, poly1,
+ ::GI.MultiPolygonTrait, polys2,
+)::Bool
Return true if polygon overlaps with at least one of the polygons within the multipolygon. Else false.
overlaps(
+ trait_a::GI.PolygonTrait, poly_a,
+ trait_b::GI.PolygonTrait, poly_b,
+)::Bool
If the two polygons intersect with one another, but are not equal, return true. Else false.
overlaps(
+ ::Union{GI.LineStringTrait, GI.LinearRing}, line1,
+ ::Union{GI.LineStringTrait, GI.LinearRing}, line2,
+)::Bool
If the curves overlap, meaning that at least one edge of each curve overlaps, return true. Else false.
polygon_to_line(poly::Polygon)
Converts a Polygon to LineString or MultiLineString
Examples
import GeometryOps as GO, GeoInterface as GI
+
+poly = GI.Polygon([[(-2.275543, 53.464547), (-2.275543, 53.489271), (-2.215118, 53.489271), (-2.215118, 53.464547), (-2.275543, 53.464547)]])
+GO.polygon_to_line(poly)
+# output
+GeoInterface.Wrappers.LineString{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}([(-2.275543, 53.464547), (-2.275543, 53.489271), (-2.215118, 53.489271), (-2.215118, 53.464547), (-2.275543, 53.464547)], nothing, nothing)
polygonize(A::AbstractMatrix{Bool}; kw...)
+polygonize(f, A::AbstractMatrix; kw...)
+polygonize(xs, ys, A::AbstractMatrix{Bool}; kw...)
+polygonize(f, xs, ys, A::AbstractMatrix; kw...)
Polygonize an AbstractMatrix
of values, currently to a single class of polygons.
Returns a MultiPolygon
for Bool
values and f
return values, and a FeatureCollection
of Feature
s holding MultiPolygon
for all other values.
Function f
should return either true
or false
or a transformation of values into simpler groups, especially useful for floating point arrays.
If xs
and ys
are ranges, they are used as the pixel/cell center points. If they are Vector
of Tuple
they are used as the lower and upper bounds of each pixel/cell.
Keywords
minpoints
: ignore polygons with less than minpoints
points.
values
: the values to turn into polygons. By default these are union(A)
, If function f
is passed these refer to the return values of f
, by default union(map(f, A)
. If values Bool
, false is ignored and a single MultiPolygon
is returned rather than a FeatureCollection
.
Example
using GeometryOps
+A = rand(100, 100)
+multipolygon = polygonize(>(0.5), A);
rebuild(geom, child_geoms)
Rebuild a geometry from child geometries.
By default geometries will be rebuilt as a GeoInterface.Wrappers
geometry, but rebuild
can have methods added to it to dispatch on geometries from other packages and specify how to rebuild them.
(Maybe it should go into GeoInterface.jl)
reconstruct(geom, components)
Reconstruct geom
from an iterable of component objects that match its structure.
All objects in components
must have the same GeoInterface.trait
.
Ususally used in combination with flatten
.
segmentize([method = LinearSegments()], geom; max_distance::Real, threaded)
Segmentize a geometry by adding extra vertices to the geometry so that no segment is longer than a given distance. This is useful for plotting geometries with a limited number of vertices, or for ensuring that a geometry is not too "coarse" for a given application.
Arguments
method::SegmentizeMethod = LinearSegments()
: The method to use for segmentizing the geometry. At the moment, only LinearSegments
and GeodesicSegments
are available.
geom
: The geometry to segmentize. Must be a LineString
, LinearRing
, or greater in complexity.
max_distance::Real
: The maximum distance, in the input space, between vertices in the geometry. Only used if you don't explicitly pass a method
.
Returns a geometry of similar type to the input geometry, but resampled.
signed_area(geom, [T = Float64])::T
Returns the signed area of a single geometry, based on winding order. This is computed slighly differently for different geometries:
- The signed area of a point is always zero.
+- The signed area of a curve is always zero.
+- The signed area of a polygon is computed with the shoelace formula and is
+positive if the polygon coordinates wind clockwise and negative if
+counterclockwise.
+- You cannot compute the signed area of a multipolygon as it doesn't have a
+meaning as each sub-polygon could have a different winding order.
Result will be of type T, where T is an optional argument with a default value of Float64.
signed_distance(point, geom, ::Type{T} = Float64)::T
Calculates the signed distance from the geometry geom
to the given point. Points within geom
have a negative signed distance, and points outside of geom
have a positive signed distance. - The signed distance from a point to a point, line, linestring, or linear ring is equal to the distance between the two. - The signed distance from a point to a polygon is negative if the point is within the polygon and is positive otherwise. The value of the distance is the minimum distance from the point to an edge of the polygon. This includes edges created by holes. - The signed distance from a point to a multigeometry or a geometry collection is the minimum signed distance between the point and any of the sub-geometries.
Result will be of type T, where T is an optional argument with a default value of Float64.
simplify(obj; kw...)
+simplify(::SimplifyAlg, obj; kw...)
Simplify a geometry, feature, feature collection, or nested vectors or a table of these.
RadialDistance
, DouglasPeucker
, or VisvalingamWhyatt
algorithms are available, listed in order of increasing quality but decreaseing performance.
PoinTrait
and MultiPointTrait
are returned unchanged.
The default behaviour is simplify(DouglasPeucker(; kw...), obj)
. Pass in other SimplifyAlg
to use other algorithms.
Keywords
prefilter_alg
: SimplifyAlg
algorithm used to pre-filter object before using primary filtering algorithm.
threaded
: true
or false
. Whether to use multithreading. Defaults to false
.
crs
: The CRS to attach to geometries. Defaults to nothing
.
calc_extent
: true
or false
. Whether to calculate the extent. Defaults to false
.
Keywords for DouglasPeucker are allowed when no algorithm is specified:
Keywords
ratio
: the fraction of points that should remain after simplify
. Useful as it will generalise for large collections of objects.
number
: the number of points that should remain after simplify
. Less useful for large collections of mixed size objects.
tol
: the minimum distance a point will be from the line joining its neighboring points.
Example
Simplify a polygon to have six points:
import GeoInterface as GI
+import GeometryOps as GO
+
+poly = GI.Polygon([[
+ [-70.603637, -33.399918],
+ [-70.614624, -33.395332],
+ [-70.639343, -33.392466],
+ [-70.659942, -33.394759],
+ [-70.683975, -33.404504],
+ [-70.697021, -33.419406],
+ [-70.701141, -33.434306],
+ [-70.700454, -33.446339],
+ [-70.694274, -33.458369],
+ [-70.682601, -33.465816],
+ [-70.668869, -33.472117],
+ [-70.646209, -33.473835],
+ [-70.624923, -33.472117],
+ [-70.609817, -33.468107],
+ [-70.595397, -33.458369],
+ [-70.587158, -33.442901],
+ [-70.587158, -33.426283],
+ [-70.590591, -33.414248],
+ [-70.594711, -33.406224],
+ [-70.603637, -33.399918]]])
+
+simple = GO.simplify(poly; number=6)
+GI.npoint(simple)
+
+# output
+6
t_value(sᵢ, sᵢ₊₁, rᵢ, rᵢ₊₁)
Returns the "T-value" as described in Hormann's presentation [1] on how to calculate the mean-value coordinate.
Here, sᵢ
is the vector from vertex vᵢ
to the point, and rᵢ
is the norm (length) of sᵢ
. s
must be Point
and r
must be real numbers.
+
+[source](https://github.com/JuliaGeo/GeometryOps.jl/blob/c83415a097210b2572fe758d480f894307407bca/src/methods/barycentric.jl#L289-L305)
+
+</div>
+<br>
+<div style='border-width:1px; border-style:solid; border-color:black; padding: 1em; border-radius: 25px;'>
+<a id='GeometryOps.to_edges-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T' href='#GeometryOps.to_edges-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T'>#</a> <b><u>GeometryOps.to_edges</u></b> — <i>Method</i>.
+
+
+
+
+\`\`\`julia
+to_edges()
Convert any geometry or collection of geometries into a flat vector of Tuple{Tuple{Float64,Float64},Tuple{Float64,Float64}}
edges.
touches(geom1, geom2)::Bool
Return true
if the first geometry touches the second geometry. In other words, the two interiors cannot interact, but one of the geometries must have a boundary point that interacts with either the other geometies interior or boundary.
Examples
import GeometryOps as GO, GeoInterface as GI
+
+l1 = GI.Line([(0.0, 0.0), (1.0, 0.0)])
+l2 = GI.Line([(1.0, 1.0), (1.0, -1.0)])
+
+GO.touches(l1, l2)
+# output
+true
transform(f, obj)
Apply a function f
to all the points in obj
.
Points will be passed to f
as an SVector
to allow using CoordinateTransformations.jl and Rotations.jl without hassle.
SVector
is also a valid GeoInterface.jl point, so will work in all GeoInterface.jl methods.
Example
julia> import GeoInterface as GI
+
+julia> import GeometryOps as GO
+
+julia> geom = GI.Polygon([GI.LinearRing([(1, 2), (3, 4), (5, 6), (1, 2)]), GI.LinearRing([(3, 4), (5, 6), (6, 7), (3, 4)])]);
+
+julia> f = CoordinateTransformations.Translation(3.5, 1.5)
+Translation(3.5, 1.5)
+
+julia> GO.transform(f, geom)
+GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.Linea
+rRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}(StaticArraysCo
+re.SVector{2, Float64}[[4.5, 3.5], [6.5, 5.5], [8.5, 7.5], [4.5, 3.5]], nothing, nothing), GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}(StaticA
+rraysCore.SVector{2, Float64}[[6.5, 5.5], [8.5, 7.5], [9.5, 8.5], [6.5, 5.5]], nothing, nothing)], nothing, nothing)
With Rotations.jl you need to actuall multiply the Rotation by the SVector
point, which is easy using an anonymous function.
julia> using Rotations
+
+julia> GO.transform(p -> one(RotMatrix{2}) * p, geom)
+GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Int64}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.LinearR
+ing{false, false, Vector{StaticArraysCore.SVector{2, Int64}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Int64}}, Nothing, Nothing}(StaticArraysCore.SVe
+ctor{2, Int64}[[2, 1], [4, 3], [6, 5], [2, 1]], nothing, nothing), GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Int64}}, Nothing, Nothing}(StaticArraysCore.SVector{2, Int64
+}[[4, 3], [6, 5], [7, 6], [4, 3]], nothing, nothing)], nothing, nothing)
tuples(obj)
Convert all points in obj
to Tuple
s, wherever the are nested.
Returns a similar object or collection of objects using GeoInterface.jl geometries wrapping Tuple
points.
Keywords
threaded
: true
or false
. Whether to use multithreading. Defaults to false
.
crs
: The CRS to attach to geometries. Defaults to nothing
.
calc_extent
: true
or false
. Whether to calculate the extent. Defaults to false
.
union(geom_a, geom_b, [::Type{T}]; target::Type, fix_multipoly = UnionIntersectingPolygons())
Return the union between two geometries as a list of geometries. Return an empty list if none are found. The type of the list will be constrained as much as possible given the input geometries. Furthermore, the user can provide a taget
type as a keyword argument and a list of target geometries found in the difference will be returned. The user can also provide a float type 'T' that they would like the points of returned geometries to be. If the user is taking a intersection involving one or more multipolygons, and the multipolygon might be comprised of polygons that intersect, if fix_multipoly
is set to an IntersectingPolygons
correction (the default is UnionIntersectingPolygons()
), then the needed multipolygons will be fixed to be valid before performing the intersection to ensure a correct answer. Only set fix_multipoly
to false if you know that the multipolygons are valid, as it will avoid unneeded computation.
Calculates the union between two polygons.
Example
import GeoInterface as GI, GeometryOps as GO
+
+p1 = GI.Polygon([[(0.0, 0.0), (5.0, 5.0), (10.0, 0.0), (5.0, -5.0), (0.0, 0.0)]])
+p2 = GI.Polygon([[(3.0, 0.0), (8.0, 5.0), (13.0, 0.0), (8.0, -5.0), (3.0, 0.0)]])
+union_poly = GO.union(p1, p2; target = GI.PolygonTrait())
+GI.coordinates.(union_poly)
+
+# output
+1-element Vector{Vector{Vector{Vector{Float64}}}}:
+ [[[6.5, 3.5], [5.0, 5.0], [0.0, 0.0], [5.0, -5.0], [6.5, -3.5], [8.0, -5.0], [13.0, 0.0], [8.0, 5.0], [6.5, 3.5]]]
unwrap(target::Type{<:AbstractTrait}, obj)
+unwrap(f, target::Type{<:AbstractTrait}, obj)
Unwrap the object to vectors, down to the target trait.
If f
is passed in it will be applied to the target geometries as they are found.
weighted_mean(weight::Real, x1, x2)
Returns the weighted mean of x1
and x2
, where weight
is the weight of x1
.
Specifically, calculates x1 * weight + x2 * (1 - weight)
.
Note
The idea for this method is that you can override this for custom types, like Color types, in extension modules.
within(geom1, geom2)::Bool
Return true
if the first geometry is completely within the second geometry. The interiors of both geometries must intersect and the interior and boundary of the primary geometry (geom1) must not intersect the exterior of the secondary geometry (geom2).
Furthermore, within
returns the exact opposite result of contains
.
Examples
import GeometryOps as GO, GeoInterface as GI
+
+line = GI.LineString([(1, 1), (1, 2), (1, 3), (1, 4)])
+point = (1, 2)
+GO.within(point, line)
+
+# output
+true
K. Hormann and N. Sukumar. Generalized Barycentric Coordinates in Computer Graphics and Computational Mechanics. Taylor & Fancis, CRC Press, 2017. ↩︎
See GeometryOps#114.
[ ] Exact predicates can be defined for lower-level, more atomic predicates within GeometryOps.
[ ] Add Shewchuck's adaptive math as a stage for exact predicates.
[x] @skygering to write docstrings for the predicates
[ ] Finish clipping degeneracies
[ ] Fix cross & overlap functions
[x] Benchmarks to show why things you couldn't concieve of in R are doable in Julia
[x] profile functions for exponential improvements
[ ] A list of projects people can work on...the beauty here is that each function is kind of self-contained so it's an undergrad level project
[ ] Doc improvements
more
benchmarks page
Methods to validate and fix geometry
[ ] Polygons and LinearRings:
[ ] self-intersection
[ ] holes are actually within the polygon
[ ] Polygon exteriors must be counterclockwise, holes clockwise.
[ ] length of all rings > 4
[ ] repeated last point
[ ] LineStrings: NaN/Inf points
[x] Fix linear rings at some point to make sure the ring is closed, i.e., points[end] == points[begin]
Tests
[x] Simplify functions
[x] Polygonize
Barycentric tests for n_vertices > 4
Rename bools.jl
to something more relevant to the actual code -> orientation.jl
Doc improvements:
=0)c=r.activeElement;else{var f=i.tabbableGroups[0],p=f&&f.firstTabbableNode;c=p||h("fallbackFocus")}if(!c)throw new Error("Your focus-trap needs to have at least one focusable element");return c},v=function(){if(i.containerGroups=i.containers.map(function(c){var f=br(c,a.tabbableOptions),p=wr(c,a.tabbableOptions),C=f.length>0?f[0]:void 0,I=f.length>0?f[f.length-1]:void 0,M=p.find(function(m){return le(m)}),z=p.slice().reverse().find(function(m){return le(m)}),P=!!f.find(function(m){return se(m)>0});return{container:c,tabbableNodes:f,focusableNodes:p,posTabIndexesFound:P,firstTabbableNode:C,lastTabbableNode:I,firstDomTabbableNode:M,lastDomTabbableNode:z,nextTabbableNode:function(x){var $=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0,K=f.indexOf(x);return K<0?$?p.slice(p.indexOf(x)+1).find(function(Q){return le(Q)}):p.slice(0,p.indexOf(x)).reverse().find(function(Q){return le(Q)}):f[K+($?1:-1)]}}}),i.tabbableGroups=i.containerGroups.filter(function(c){return c.tabbableNodes.length>0}),i.tabbableGroups.length<=0&&!h("fallbackFocus"))throw new Error("Your focus-trap must have at least one container with at least one tabbable node in it at all times");if(i.containerGroups.find(function(c){return c.posTabIndexesFound})&&i.containerGroups.length>1)throw new Error("At least one node with a positive tabindex was found in one of your focus-trap's multiple containers. Positive tabindexes are only supported in single-container focus-traps.")},y=function w(c){var f=c.activeElement;if(f)return f.shadowRoot&&f.shadowRoot.activeElement!==null?w(f.shadowRoot):f},b=function w(c){if(c!==!1&&c!==y(document)){if(!c||!c.focus){w(d());return}c.focus({preventScroll:!!a.preventScroll}),i.mostRecentlyFocusedNode=c,Ar(c)&&c.select()}},E=function(c){var f=h("setReturnFocus",c);return f||(f===!1?!1:c)},g=function(c){var f=c.target,p=c.event,C=c.isBackward,I=C===void 0?!1:C;f=f||Ae(p),v();var M=null;if(i.tabbableGroups.length>0){var z=l(f,p),P=z>=0?i.containerGroups[z]:void 0;if(z<0)I?M=i.tabbableGroups[i.tabbableGroups.length-1].lastTabbableNode:M=i.tabbableGroups[0].firstTabbableNode;else if(I){var m=ft(i.tabbableGroups,function(B){var U=B.firstTabbableNode;return f===U});if(m<0&&(P.container===f||_e(f,a.tabbableOptions)&&!le(f,a.tabbableOptions)&&!P.nextTabbableNode(f,!1))&&(m=z),m>=0){var x=m===0?i.tabbableGroups.length-1:m-1,$=i.tabbableGroups[x];M=se(f)>=0?$.lastTabbableNode:$.lastDomTabbableNode}else ge(p)||(M=P.nextTabbableNode(f,!1))}else{var K=ft(i.tabbableGroups,function(B){var U=B.lastTabbableNode;return f===U});if(K<0&&(P.container===f||_e(f,a.tabbableOptions)&&!le(f,a.tabbableOptions)&&!P.nextTabbableNode(f))&&(K=z),K>=0){var Q=K===i.tabbableGroups.length-1?0:K+1,q=i.tabbableGroups[Q];M=se(f)>=0?q.firstTabbableNode:q.firstDomTabbableNode}else ge(p)||(M=P.nextTabbableNode(f))}}else M=h("fallbackFocus");return M},S=function(c){var f=Ae(c);if(!(l(f,c)>=0)){if(ye(a.clickOutsideDeactivates,c)){s.deactivate({returnFocus:a.returnFocusOnDeactivate});return}ye(a.allowOutsideClick,c)||c.preventDefault()}},T=function(c){var f=Ae(c),p=l(f,c)>=0;if(p||f instanceof Document)p&&(i.mostRecentlyFocusedNode=f);else{c.stopImmediatePropagation();var C,I=!0;if(i.mostRecentlyFocusedNode)if(se(i.mostRecentlyFocusedNode)>0){var M=l(i.mostRecentlyFocusedNode),z=i.containerGroups[M].tabbableNodes;if(z.length>0){var P=z.findIndex(function(m){return m===i.mostRecentlyFocusedNode});P>=0&&(a.isKeyForward(i.recentNavEvent)?P+1 Accurate arithmetic is a technique which allows you to calculate using more precision than the provided numeric type. We will use the accurate sum routines from AccurateArithmetic.jl to show the difference! @example accurate GI.Polygon.(GO.flatten(Union{GI.LineStringTrait, GI.LinearRingTrait}, all_adm0) |> collect .|> x -> [x]) .|> GO.signed_area |> sum_kbn \`\`\` Exact vs fast predicates GeometryOps.jl is a package for geometric calculations on (primarily 2D) geometries. The driving idea behind this package is to unify all the disparate packages for geometric calculations in Julia, and make them GeoInterface.jl-compatible. We seem to be focusing primarily on 2/2.5D geometries for now. Most of the usecases are driven by GIS and similar Earth data workflows, so this might be a bit specialized towards that, but methods should always be general to any coordinate space. We welcome contributions, either as pull requests or discussion on issues! Note See the Primitive Functions page for more information on this. The Functionally, it's similar to Write a comment about GeoInterface.Wrapper and why it helps in type stability to guarantee a particular return type. GeometryOps exposes functions like Below, we'll describe some of the foundational paradigms of GeometryOps, and why you should care! The Functionally, it's similar to The In general, the idea here is to be able to invoke several actions efficiently and simultaneously, for example when correcting invalid geometries, or instantiating a It is recommended for consistency that In polygon set operations like We use the This also allows for a lot more type stability - when you ask for polygons, we won't return a geometrycollection with line segments. Especially in simulation workflows, this is excellent for simplified data processing. Warning These are internals and explicitly not public API, meaning they may change at any time! When dispatch can be controlled by the value of a boolean variable, this introduces type instability. Instead of introducing type instability, we chose to encode our boolean decision variables, like Import all names from GeoInterface and Extents, so users can do Handle all available errors! This page was generated using Literate.jl. Angles are the angles formed by a given geometries line segments, if it has line segments. To provide an example, consider this rectangle: This is clearly a rectangle, with angles of 90 degrees. This is the GeoInterface-compatible implementation. First, we implement a wrapper method that dispatches to the correct implementation based on the geometry trait. This is also used in the implementation, since it's a lot less work! Points and single line segments have no angles Find angles of a curve and insert the values into the angle_list. If offset is true, then save space for the angle at the first vertex, as the curve is closed, at the front of angle_list. If close_geom is true, then despite the first and last point not being explicitly repeated, the curve is closed and the angle of the last point should be added to angle_list. If interior is true, then all angles will be on the same side of the line Loop through the curve and find each of the angels If the last point of geometry should be the same as the first, calculate closing angle If needed, calculate first angle corresponding to the first point Calculate the angle between two vectors defined by the previous and current Δx and Δys. Angle will have a sign corresponding to the sign of the cross product between the two vectors. All angles of one sign in a given geometry are convex, while those of the other sign are concave. However, the sign corresponding to each of these can vary based on geometry and thus you must compare to an angle that is know to be convex or concave. This page was generated using Literate.jl. Area is the amount of space occupied by a two-dimensional figure. It is always a positive value. Signed area is simply the integral over the exterior path of a polygon, minus the sum of integrals over its interior holes. It is signed such that a clockwise path has a positive area, and a counterclockwise path has a negative area. The area is the absolute value of the signed area. To provide an example, consider this rectangle: This is clearly a rectangle, etc. But now let's look at how the points look: The points are ordered in a counterclockwise fashion, which means that the signed area is negative. If we reverse the order of the points, we get a postive area. This is the GeoInterface-compatible implementation. First, we implement a wrapper method that dispatches to the correct implementation based on the geometry trait. This is also used in the implementation, since it's a lot less work! Note that area and signed area are zero for all points and curves, even if the curves are closed like with a linear ring. Also note that signed area really only makes sense for polygons, given with a multipolygon can have several polygons each with a different orientation and thus the absolute value of the signed area might not be the area. This is why signed area is only implemented for polygons. Targets for applys functions Points, MultiPoints, Curves, MultiCurves LibGEOS treats linear rings as zero area. I disagree with that but we should probably maintain compatibility... Polygons Remove hole areas from total Winding of exterior ring determines sign One term of the shoelace area formula Integrate the area under the curve Skip the first and do it later This lets us work within one iteration over geom, which means on C call when using points from external libraries. Accumulate the area into Complete the last edge. If the first and last where the same this will be zero This page was generated using Literate.jl. Generalized barycentric coordinates are a generalization of barycentric coordinates, which are typically used in triangles, to arbitrary polygons. They provide a way to express a point within a polygon as a weighted average of the polygon's vertices. As with the triangle case, the weights sum to 1, and each is non-negative. This example was taken from this page of CGAL's documentation. In some cases, we actually want barycentric interpolation, and have no interest in the coordinates themselves. However, the coordinates can be useful for debugging, and when performing 3D rendering, multiple barycentric values (depth, uv) are needed for depth buffering. This is the GeoInterface-compatible method. This is the GeoInterface-compatible method. 3D polygons are considered to have their vertices in the XY plane, and the Z coordinate must represent some value. This is to say that the Z coordinate is interpreted as an M coordinate. This method is the one which supports GeoInterface. Before we go to the actual implementation, there are some quick and simple utility functions that we need to implement. These are mainly for convenience and code brevity. This performs an inplace accumulation, using less memory and is faster. That's particularly good if you are using a polygon with a large number of points... When you have holes, then you have to be careful about the order you iterate around points. Specifically, you have to iterate around each linear ring separately and ensure there are no degenerate/repeated points at the start and end! Now, we set the interpolated value to the first point's value, multiplied by the weight computed relative to the first point in the polygon. Increment counters + set variables Updates - first the interpolated value, then the accumulators for total weight and current index. This page was generated using Literate.jl. The centroid is the geometric center of a line string or area(s). Note that the centroid does not need to be inside of a concave area. Further note that by convention a line, or linear ring, is calculated by weighting the line segments by their length, while polygons and multipolygon centroids are calculated by weighting edge's by their 'area components'. To provide an example, consider this concave polygon in the shape of a 'C': Let's see what the centroid looks like (plotted in red): This is the GeoInterface-compatible implementation. First, we implement a wrapper method that dispatches to the correct implementation based on the geometry trait. This is also used in the implementation, since it's a lot less work! Note that if you call centroid on a LineString or LinearRing, the centroid_and_length function will be called due to the weighting scheme described above, while centroid_and_area is called for polygons and multipolygons. However, centroid_and_area can still be called on a LineString or LinearRing when they are closed, for example as the interior hole of a polygon. The helper functions centroid_and_length and centroid_and_area are made availible just in case the user also needs the area or length to decrease repeat computation. Initialize starting values Loop over line segments of line string Calculate length of line segment Accumulate the line segment length into Weighted average of line segment centroids Advance the point buffer by 1 point to move to next line segment Check that the geometry is closed Initialize starting values Loop over line segments of linear ring Accumulate the area component into Weighted average of centroid components Advance the point buffer by 1 point Exterior ring's centroid and area Weight exterior centroid by area Loop over any holes within the polygon Hole polygon's centroid and area Accumulate the area component into Weighted average of centroid components The This page was generated using Literate.jl. This file contains the shared helper functions for the polygon clipping functionalities. This enum defines which side of an edge a point is on Constants assigned for readability Checks equality of two PolyNodes by backing point value, fractional value, and intersection status This function takes in two polygon rings and calls '_build_a_list', '_build_b_list', and '_flag_ent_exit' in order to fully form a_list and b_list. The 'a_list' and 'b_list' that it returns are the fully updated vectors of PolyNodes that represent the rings 'poly_a' and 'poly_b', respectively. This function also returns 'a_idx_list', which at its "ith" index stores the index in 'a_list' at which the "ith" intersection point lies. Make a list for nodes of each polygon Flag crossings Flag the entry and exits Set node indices and filter a_idx_list to just crossing points This function take in two polygon rings and creates a vector of PolyNodes to represent poly_a, including its intersection points with poly_b. The information stored in each PolyNode is needed for clipping using the Greiner-Hormann clipping algorithm. Note: After calling this function, a_list is not fully formed because the neighboring indicies of the intersection points in b_list still need to be updated. Also we still have not update the entry and exit flags for a_list. The a_idx_list is a list of the indicies of intersection points in a_list. The value at index i of a_idx_list is the location in a_list where the ith intersection point lies. Loop through points of poly_a Add the first point of the edge to the list of points in a_list Find intersections with edges of poly_b Determine if edges intersect and how they intersect Determine if a1 or b1 should be added to a_list If lines are collinear and overlapping, a second intersection exists Add intersection points determined above Order intersection points by placement along edge using fracs value This function takes in the a_list and a_idx_list build in _build_a_list and poly_b and creates a vector of PolyNodes to represent poly_b. The information stored in each PolyNode is needed for clipping using the Greiner-Hormann clipping algorithm. Note: after calling this function, b_list is not fully updated. The entry/exit flags still need to be updated. However, the neightbor value in a_list is now updated. Sort intersection points by insertion order in b_list Initialize needed values and lists Loop over points in poly_b and add each point and intersection point intersection point is vertex of b This function marks all intersection points as either bouncing or crossing points. "Delayed" crossing or bouncing intersections (a chain of edges where the central edges overlap and thus only the first and last edge of the chain determine if the chain is bounding or crossing) are marked as follows: the first and the last points are marked as crossing if the chain is crossing and delayed otherwise and all middle points are marked as bouncing. Additionally, the start and end points of the chain are marked as endpoints using the endpoints field. start centered on last point keep track of unmatched bouncing chains loop over list points determine if any segments are on top of one another determine which side of a segments the p points are on no sides overlap end of overlapping chain update end of chain with endpoint and crossing / bouncing tags update start of chain with endpoint and crossing / bouncing tags start of overlapping chain if we started in the middle of overlapping chain, close chain update end of chain with endpoint and crossing / bouncing tags update start of chain with endpoint and crossing / bouncing tags Check if PolyNode is a vertex of original polygon Determine side orientation of b_prev and b_next Determines if Q lies to the left or right of the line formed by P1-P2-P3 Check if a PolyNode is an intersection point This function flags all the intersection points as either an 'entry' or 'exit' point in relation to the given polygon. For non-delayed crossings we simply alternate the enter/exit status. This also holds true for the first and last points of a delayed bouncing, where they both have an opposite entry/exit flag. Conversely, the first and last point of a delayed crossing have the same entry/exit status. Furthermore, the crossing/bouncing flag of delayed crossings and bouncings may be updated. This depends on function specific rules that determine which of the start or end points (if any) should be marked as crossing for used during polygon tracing. A consistent rule is that the start and end points of a delayed crossing will have different crossing/bouncing flags, while a the endpoints of a delayed bounce will be the same. Used for clipping polygons by other polygons. Find starting index if there is one Loop over points and mark entry and exit status update start of chain point This function flags all the intersection points as either an 'entry' or 'exit' point in relation to the given line. Returns true if there are crossing points to classify, else returns false. Used for cutting polygons by lines. Assumes that the first point is outside of the polygon and not on an edge. Loop over points and mark entry and exit status This function takes the outputs of _build_ab_list and traces the lists to determine which polygons are formed as described in Greiner and Hormann. The function f_step determines in which direction the lists are traced. This function is different for intersection, difference, and union. f_step must take in two arguments: the most recent intersection node's entry/exit status and a boolean that is true if we are currently tracing a_list and false if we are tracing b_list. The functions used for each clipping operation are follows: - Intersection: (x, y) -> x ? 1 : (-1) - Difference: (x, y) -> (x ⊻ y) ? 1 : (-1) - Union: (x, y) -> x ? (-1) : 1 A list of GeoInterface polygons is returned from this function. Note: Keep track of number of processed intersection points Find first unprocessed intersecting point in subject polygon Set first point in polygon changed curr_not_intr to curr_not_same_ent_flag Traverse polygon either forwards or backwards Get current node and add to pt_list Keep track of processed intersection points Switch to next list and next point Get type of polygons that will be made TODO: Increase type options For polygns with no crossing intersection points, either one polygon is inside of another, or they are seperate polygons with no intersection (other than an edge or point). Return two booleans that represent if a is inside b (potentially with shared edges / points) and visa versa if b is inside of a. The holes specified by the hole iterator are added to the polygons in the return_polys list. If this creates more polygons, they are added to the end of the list. If this removes polygons, they are removed from the list Remove set of holes from all polygons loop through all pieces of original polygon (new pieces added to end of list) replace original polygon is completly within hole Remove all polygon that were marked for removal The new hole is combined with any existing holes in curr_poly. The holes can be combined into a larger hole if they are intersecting. If this happens, then the new, combined hole is returned with the orignal holes making up the new hole removed from curr_poly. Additionally, if the combined holes form a ring, the interior is added to the return_polys as a new polygon piece. Additionally, holes leftover after combination will be checked for it they are in the "main" polygon or in one of these new pieces and moved accordingly. If the holes don't touch or curr_poly has no holes, then new_hole is returned without any changes. Combine any existing holes in curr_poly with new hole If the holes intersect, combine them into a bigger hole Remove redundant holes If new polygon pieces created, make sure remaining holes are in the correct piece resize and reset removing index buffer check if p2 is approximatly on the edge formed by p1 and p3 - remove if so Check if the first point (which is repeated as the last point) is needed Remove unneeded collinear points Check if enough points are left to form a polygon This page was generated using Literate.jl. Coverage is the amount of geometry area within a bounding box defined by the minimum and maximum x and y-coordiantes of that bounding box, or an Extent containing that information. To provide an example, consider this rectangle: It is clear that half of the polygon is within the cell, so the coverage should be 1.0, half of the area of the rectangle. This is the GeoInterface-compatible implementation. First, we implement a wrapper method that dispatches to the correct implementation based on the geometry trait. This is also used in the implementation, since it's a lot less work! Note that the coverage is zero for all points and curves, even if the curves are closed like with a linear ring. Targets for applys functions Wall types for coverage Points, MultiPoints, Curves, MultiCurves Polygons Remove hole coverage from total Loop over edges of polygon Must rotate clockwise for the algorithm to work Determine if edge points are within the cell If entire line segment is inside cell If edge passes outside of rectangle, determine which edge segments are added Endpoints of segment within the cell and wall they are on if known Add edge component if unmatched in-point at beginning, close polygon with last out point if grid cell is within polygon then the area is grid cell area Returns true of the given point is within the bounding box determined by x and y values Returns true if b is between a and c, exclusive of the maximum value, else false. Calculate and check potential intersections Finds point of cell edge between p1 and p2 given which walls they are on From the point to the corner of wall 1 Any intermediate walls (full length) From the corner of wall 2 to the point True if (x1, y1) is clockwise from (x2, y2) on the same wall This page was generated using Literate.jl. The cut function cuts a polygon through a line segment. This is inspired by functions such as Matlab's To provide an example, consider the following polygon and line: This function depends on polygon clipping helper function and is inspired by the Greiner-Hormann clipping algorithm used elsewhere in this library. The inspiration came from this Stack Overflow discussion. output If an impossible number of intersection points, return original polygon Cut polygon by line Close coords and create polygons Add original polygon holes back in Many types aren't implemented Sort and catagorize the intersection points Add first point to output list Walk around original polygon to find split polygons Find cross back point for current polygon Check if current point is a cross back point This page was generated using Literate.jl. output Get the exterior of the polygons Find the difference of the exterior of the polygons if no crossing points, determine if either poly is inside of the other add case for if they polygons are the same (all intersection points!) add a find_first check to find first non-inter poly! If the original polygons had holes, take that into account. Remove uneeded collinear points on same edge Many type and target combos aren't implemented This page was generated using Literate.jl. output Curve-Curve Intersections with target Point First we get the exteriors of 'poly_a' and 'poly_b' Then we find the intersection of the exteriors If the original polygons had holes, take that into account. Remove uneeded collinear points on same edge Many type and target combos aren't implemented Initialize an empty list of points Check if the geometries extents even overlap Create a list of edges from the two input geometries Loop over pairs of edges and add any intersection points to results TODO: Add in degenerate intersection points when line_over Default answer for no intersection Seperate out line segment points Check if envelopes of lines intersect Check orientation of two line segments with respect to one another Determine intersection type and intersection point(s) Intersection is collinear if all endpoints lie on the same line Intersection is a hinge if the intersection point is an endpoint Intersection is a cross if there is only one non-endpoint intersection point Define default return for no intersection points Determine collinear line overlaps Determine line distances Set collinear intersection points if they exist First line runs from a to a + Δa Second line runs from b to b + Δb Differences between starting points Determine α value where 0 < α < 1 and β value where 0 < β < 1 Check if point is within segment envelopes and adjust to endpoint if not Find endpoint of either segment that is closest to the opposite segment Create lines from segments and calculate segment length Determine distance from a1 to segment b Determine distance from a2 to segment b Determine distance from b1 to segment a Determine distance from b2 to segment a Return point with smallest distance Return value of x/y clamped between ϵ and 1 - ϵ This page was generated using Literate.jl. If If Exact cross product calculation using If function cross(a, b, c) # try Predicates._cross_naive(a, b, c) # check the error bound there # then try Predicates._cross_adaptive(a, b, c) # then try Predicates._cross_exact end This page was generated using Literate.jl. output First, I get the exteriors of the two polygons Then, I get the union of the exteriors Check if one polygon totally within other and if so, return the larger polygon the first element is the exterior, the rest are holes Add in holes Remove uneeded collinear points on same edge Loop over all holes in both original polygons If polygons intersect and form a new polygon, swap out polygon If they don't intersect, poly_b is now a part of the union as its own polygon Many type and target combos aren't implemented This page was generated using Literate.jl. Distance is the distance of a point to another geometry. This is always a positive number. If a point is inside of geometry, so on a curve or inside of a polygon, the distance will be zero. Signed distance is mainly used for polygons and multipolygons. If a point is outside of a geometry, signed distance has the same value as distance. However, points within the geometry have a negative distance representing the distance of a point to the closest boundary. Therefore, for all "non-filled" geometries, like curves, the distance will either be postitive or 0. To provide an example, consider this rectangle: This is clearly a rectangle with one point inside and one point outside. The points are both an equal distance to the polygon. The distance to Consider also a heatmap of signed distances around this object: This is the GeoInterface-compatible implementation. First, we implement a wrapper method that dispatches to the correct implementation based on the geometry trait. This is also used in the implementation, since it's a lot less work! Distance and signed distance are only implemented for points to other geometries right now. This could be extended to include distance from other geometries in the future. The distance calculated is the Euclidean distance using the Pythagorean theorem. Also note that singed_distance only makes sense for "filled-in" shapes, like polygons, so it isn't implemented for curves. Needed for method ambiguity Point-Point, Point-Line, Point-LineString, Point-LinearRing Point-Polygon Needed for method ambiguity Point-Geom (just calls _distance) Point-Polygon negative if point is inside polygon Returns the Euclidean distance between two points. Returns the square of the euclidean distance between two points Returns the Euclidean distance between two points given their x and y values. Returns the squared Euclidean distance between two points given their x and y values. Returns the minimum distance from point p0 to the line defined by endpoints p1 and p2. Returns the squared minimum distance from point p0 to the line defined by endpoints p1 and p2. Returns the minimum distance from the given point to the given curve. If close_curve is true, make sure to include the edge from the first to last point of the curve, even if it isn't explicitly repeated. see if linear ring has explicitly repeated last point in coordinates find minimum distance Returns the minimum distance from the given point to an edge of the given polygon, including from edges created by holes. Assumes polygon isn't filled and treats the exterior and each hole as a linear ring. This page was generated using Literate.jl. The equals function checks if two geometries are equal. They are equal if they share the same set of points and edges to define the same shape. To provide an example, consider these two lines: We can see that the two lines do not share a commen set of points and edges in the plot, so they are not equal: This is the GeoInterface-compatible implementation. First, we implement a wrapper method that dispatches to the correct implementation based on the geometry trait. This is also used in the implementation, since it's a lot less work! Note that while we need the same set of points and edges, they don't need to be provided in the same order for polygons. For for example, we need the same set points for two multipoints to be equal, but they don't have to be saved in the same order. The winding order also doesn't have to be the same to represent the same geometry. This requires checking every point against every other point in the two geometries we are comparing. Also, some geometries must be "closed" like polygons and linear rings. These will be assumed to be closed, even if they don't have a repeated last point explicity written in the coordinates. Additionally, geometries and multi-geometries can be equal if the multi-geometry only includes that single geometry. output Check if both curves are closed or not How many points in each curve Find offset between curves no point matches the first point found match for only point if isn't closed and first or last point don't match, not same curve Check if curves are going in same direction if only 2 points, we have already compared both Check all remaining points are the same wrapping around line Check if exterior is equal Check if number of holes are equal Check if holes are equal Check if same number of polygons Check if each polygon has a matching polygon This page was generated using Literate.jl. The contains function checks if a given geometry completly contains another geometry, or in other words, that the second geometry is completly within the first. This requires that the two interiors intersect and that the interior and boundary of the second geometry is not in the exterior of the first geometry. To provide an example, consider these two lines: We can see that all of the points and edges of l2 are within l1, so l1 contains l2. However, l2 does not contain l1. This is the GeoInterface-compatible implementation. Given that contains is the exact opposite of within, we simply pass the two inputs variables, swapped in order, to within. output This page was generated using Literate.jl. The coveredby function checks if one geometry is covered by another geometry. This is an extension of within that does not require the interiors of the two geometries to intersect, but still does require that the interior and boundary of the first geometry isn't outside of the second geometry. To provide an example, consider this point and line: As we can see, This is the GeoInterface-compatible implementation. First, we implement a wrapper method that dispatches to the correct implementation based on the geometry trait. Each of these calls a method in the geom_geom_processors file. The methods in this file determine if the given geometries meet a set of criteria. For the The code for the specific implementations is in the geom_geom_processors file. output Point is coveredby another point if those points are equal Point is coveredby a line/linestring if it is on a line vertex or an edge Point is coveredby a linearring if it is on a vertex or an edge of ring Point is coveredby a polygon if it is inside polygon, including edges/vertices Points cannot cover any geometry other than points Polygons cannot covered by any curves This page was generated using Literate.jl. The covers function checks if a given geometry completly covers another geometry. For this to be true, the "contained" geometry's interior and boundaries must be covered by the "covering" geometry's interior and boundaries. The interiors do not need to overlap. To provide an example, consider these two lines: This is the GeoInterface-compatible implementation. Given that covers is the exact opposite of coveredby, we simply pass the two inputs variables, swapped in order, to coveredby. output This page was generated using Literate.jl. TODO: Add working example TODO use better predicate for crossing here Will constprop optimise these away? This page was generated using Literate.jl. The disjoint function checks if one geometry is outside of another geometry, without sharing any boundaries or interiors. To provide an example, consider these two lines: We can see that none of the edges or vertices of l1 interact with l2 so they are disjoint. This is the GeoInterface-compatible implementation. First, we implement a wrapper method that dispatches to the correct implementation based on the geometry trait. Each of these calls a method in the geom_geom_processors file. The methods in this file determine if the given geometries meet a set of criteria. For the The code for the specific implementations is in the geom_geom_processors file. output Point is disjoint from another point if the points are not equal. Point is disjoint from a linestring if it is not on the line's edges/vertices. Point is disjoint from a linearring if it is not on the ring's edges/vertices. This page was generated using Literate.jl. Determines if a point meets the given checks with respect to a curve. If in_allow is true, the point can be on the curve interior. If on_allow is true, the point can be on the curve boundary. If out_allow is true, the point can be disjoint from the curve. If the point is in an "allowed" location, return true. Else, return false. If closed_curve is true, curve is treated as a closed curve where the first and last point are connected by a segment. Determine if curve is closed Loop through all curve segments Determines if a point meets the given checks with respect to a polygon. If in_allow is true, the point can be within the polygon interior If on_allow is true, the point can be on the polygon boundary. If out_allow is true, the point can be disjoint from the polygon. If the point is in an "allowed" location, return true. Else, return false. Check interaction of geom with polygon's exterior boundary If a point is outside, it isn't interacting with any holes if a point is on an external boundary, it isn't interacting with any holes If geom is within the polygon, need to check interactions with holes If a point in in a hole, it is outside of the polygon If a point in on a hole edge, it is on the edge of the polygon Point is within external boundary and on in/on any holes Determines if a line meets the given checks with respect to a curve. If over_allow is true, segments of the line and curve can be co-linear. If cross_allow is true, segments of the line and curve can cross. If on_allow is true, endpoints of either the line or curve can intersect a segment of the other geometry. If cross_allow is true, segments of the line and curve can be disjoint. If in_require is true, the interiors of the line and curve must meet in at least one point. If on_require is true, the bounday of one of the two geometries can meet the interior or boundary of the other geometry in at least one point. If out_require is true, there must be at least one point of the given line that is exterior of the curve. If the point is in an "allowed" location and meets all requirments, return true. Else, return false. If closed_line is true, line is treated as a closed line where the first and last point are connected by a segment. Same with closed_curve. Set up requirments Determine curve endpoints Loop over each line segment Loop over each curve segment Check if line and curve segments meet If segments are co-linear at least one point in, meets requirments If entire segment isn't covered, consider remaining section Determine location of intersection point on each segment If needed, determine if hinge actually crosses Find next pieces of hinge to see if line and curve cross no overlap for a give segment, some of segment must be out of curve Determines if a line meets the given checks with respect to a polygon. If in_allow is true, segments of the line can be in the polygon interior. If on_allow is true, segments of the line can be on the polygon's boundary. If out_allow is true, segments of the line can be outside of the polygon. If in_require is true, the interiors of the line and polygon must meet in at least one point. If on_require is true, the line must have at least one point on the polygon'same boundary. If out_require is true, the line must have at least one point outside of the polygon. If the point is in an "allowed" location and meets all requirments, return true. Else, return false. If closed_line is true, line is treated as a closed line where the first and last point are connected by a segment. Check interaction of line with polygon's exterior boundary If no points within the polygon, the line is disjoint and we are done Loop over polygon holes Determines if a polygon meets the given checks with respect to a polygon. If in_allow is true, the polygon's interiors must intersect. If on_allow is true, the one of the polygon's boundaries must either interact with the other polygon's boundary or interior. If out_allow is true, the first polygon must have interior regions outside of the second polygon. If in_require is true, the polygon interiors must meet in at least one point. If on_require is true, one of the polygon's must have at least one boundary point in or on the other polygon. If out_require is true, the first polygon must have at least one interior point outside of the second polygon. If the point is in an "allowed" location and meets all requirments, return true. Else, return false. Check if exterior of poly1 is within poly2 Check if exterior of poly1 is in polygon 2 if exterior ring isn't in poly2, check if it surrounds poly2 If interiors interact, check if poly2 interacts with any of poly1's holes If hole isn't in poly2, see if poly2 is in hole hole encompasses all of poly2 If any of poly2 holes are within poly1, part of poly1 is exterior to poly2 Determines if a point is in, on, or out of a segment. If the point is Point should be an object of point trait and curve should be an object with a linestring or linearring trait. Can provide values of in, on, and out keywords, which determines return values for each scenario. Parse out points If point is equal to the segment start or end points Determine if point is in, on, or out of a closed curve, which includes the space enclosed by the closed curve. Point should be an object of point trait and curve should be an object with a linestring or linearring trait, that is assumed to be closed, regardless of repeated last point. Can provide values of in, on, and out keywords, which determines return values for each scenario. Note that this uses the Algorithm by Hao and Sun (2018): https://doi.org/10.3390/sym10100477 Paper seperates orientation of point and edge into 26 cases. For each case, it is either a case where the point is on the edge (returns on), where a ray from the point (x, y) to infinity along the line y = y cut through the edge (k += 1), or the ray does not pass through the edge (do nothing and continue). If the ray passes through an odd number of edges, it is within the curve, else outside of of the curve if it didn't return 'on'. See paper for more information on cases denoted in comments. Determines the types of interactions of a line with a filled-in curve. By filled-in curve, I am referring to the exterior ring of a poylgon, for example. Returns a tuple of booleans: (in_curve, on_curve, out_curve). If in_curve is true, some of the lines interior points interact with the curve's interior points. If on_curve is true, endpoints of either the line intersect with the curve or the line interacts with the polygon boundary. If out_curve is true, at least one segments of the line is outside the curve. If closed_line is true, line is treated as a closed line where the first and last point are connected by a segment. Determine number of points in curve and line See if first point is in an acceptable orientation Check for any intersections between line and curve If already interacted with all regions of curve, can stop Check next segment of line against curve Check if two line and curve segments meet If line and curve meet, then at least one point is on boundary When crossing boundary, line is both in and out of curve already checked segment against whole filled curve Determines the types of interactions of a line with a polygon. Returns a tuple of booleans: (in_poly, on_poly, out_poly). If in_poly is true, some of the lines interior points interact with the polygon interior points. If in_poly is true, endpoints of either the line intersect with the polygon or the line interacts with the polygon boundary, including hole bounaries. If out_curve is true, at least one segments of the line is outside the polygon, including inside of holes. If closed_line is true, line is treated as a closed line where the first and last point are connected by a segment. Loop over polygon holes Disjoint extent optimisation: skip work based on geom extent intersection returns Tuple{Bool, Bool} for (skip, returnval) can't tell anything about this case points not allowed in exterior, but geoms are disjoint This page was generated using Literate.jl. The intersects function checks if a given geometry intersects with another geometry, or in other words, the either the interiors or boundaries of the two geometries intersect. To provide an example, consider these two lines: We can see that they intersect, so we expect intersects to return true, and we can visualize the intersection point in red. This is the GeoInterface-compatible implementation. Given that intersects is the exact opposite of disjoint, we simply pass the two inputs variables, swapped in order, to disjoint. output This page was generated using Literate.jl. The overlaps function checks if two geometries overlap. Two geometries can only overlap if they have the same dimension, and if they overlap, but one is not contained, within, or equal to the other. Note that this means it is impossible for a single point to overlap with a single point and a line only overlaps with another line if only a section of each line is colinear. To provide an example, consider these two lines: We can see that the two lines overlap in the plot: This is the GeoInterface-compatible implementation. First, we implement a wrapper method that dispatches to the correct implementation based on the geometry trait. This is also used in the implementation, since it's a lot less work! Note that that since only elements of the same dimension can overlap, any two geometries with traits that are of different dimensions autmoatically can return false. For geometries with the same trait dimension, we must make sure that they share a point, an edge, or area for points, lines, and polygons/multipolygons respectivly, without being contained. output meets in more than one point one end point is outside of other segment Checks if point is on a segment Parse out points Determine if point is on segment is line between endpoints Extents.intersects(to_extent(edges_a), to_extent(edges_b)) || return false Returns true if there is at least one intersection between two edges. This page was generated using Literate.jl. The touches function checks if one geometry touches another geometry. In other words, the interiors of the two geometries don't interact, but one of the geometries must have a boundary point that interacts with either the other geometies interior or boundary. To provide an example, consider these two lines: We can see that these two lines touch only at their endpoints. This is the GeoInterface-compatible implementation. First, we implement a wrapper method that dispatches to the correct implementation based on the geometry trait. Each of these calls a method in the geom_geom_processors file. The methods in this file determine if the given geometries meet a set of criteria. For the The code for the specific implementations is in the geom_geom_processors file. output Point cannot touch another point as if they are equal, interiors interact Point touches a linestring if it equal to the first of last point of the line Point cannot 'touch' a linearring given that the ring has no boundary points Point touches a polygon if it is on the boundary of that polygon This page was generated using Literate.jl. The within function checks if one geometry is inside another geometry. This requires that the two interiors intersect and that the interior and boundary of the first geometry is not in the exterior of the second geometry. To provide an example, consider these two lines: We can see that all of the points and edges of l2 are within l1, so l2 is within l1, but l1 is not within l2 This is the GeoInterface-compatible implementation. First, we implement a wrapper method that dispatches to the correct implementation based on the geometry trait. Each of these calls a method in the geom_geom_processors file. The methods in this file determine if the given geometries meet a set of criteria. For the The code for the specific implementations is in the geom_geom_processors file. output Point is within another point if those points are equal. Point is within a linearring if it is on a vertex or an edge of that ring. No geometries other than points can be within points Polygons cannot be within any curves This page was generated using Literate.jl. The orientation of a geometry is whether it runs clockwise or counter-clockwise. This is defined for linestrings, linear rings, or vectors of points. A polygon is concave if it has at least one interior angle greater than 180 degrees, meaning that the interior of the polygon is not a convex set. These are all adapted from Turf.jl. The may not necessarily be what want in the end but work for now! output sum will be zero for the first point as x is subtracted from itself output FIXME handle not closed polygons This is commented out. julia import GeoInterface as GI, GeometryOps as GO julia> line1 = GI.LineString([(9.170356, 45.477985), (9.164434, 45.482551), (9.166644, 45.484003)]) GeoInterface.Wrappers.LineString{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}([(9.170356, 45.477985), (9.164434, 45.482551), (9.166644, 45.484003)], nothing, nothing) julia> line2 = GI.LineString([(9.169356, 45.477985), (9.163434, 45.482551), (9.165644, 45.484003)]) GeoInterface.Wrappers.LineString{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}([(9.169356, 45.477985), (9.163434, 45.482551), (9.165644, 45.484003)], nothing, nothing) julia> GO.isparallel(line1, line2) true This is actual code: This page was generated using Literate.jl. Keywords Example Make vectors of pixel bounds Make bounds ranges first to avoid floating point error making gaps or overlaps Extract the CRS of the array (if it is some kind of geo array / raster) Define buffers for edges and rings Get edges from the array A Keep dict keys separately in a vector for performance We don't delete keys we just reduce length with nkeys Now create rings from the edges, looping until there are no edge keys left Loop until we find a key that hasn't been removed, decrementing nkeys as we go. Take the first node from the array If we found nothing this time, we are done Check if there are one or two lines going through this node and take one of them, then update the status Start a new ring Loop until we close a the ring and break Find a node that matches the next node When there are two possible node, choose the node that is the furthest to the left We also need to check if we are on a straight line to avoid adding unnecessary points. Update edges Here we simply choose the first (and only valid) node Replace the edge nodes with empty nodes, they will be skipped later Check if we are on a straight line Update the current and next nodes with the next and selected nodes Update the current node or add a new node to the ring replace the last node we don't need it add a new node, we have turned a corner If the ring is closed, break the loop and start a new one Define wrapped LinearRings, with embedded extents so we only calculate them once Separate exteriors from holes by winding direction Then we add the holes to the polygons they are inside of Hole is in the exterior, so add it to the polygon TODO: this really should return an emtpty MultiPolygon but GeoInterface wrappers cant do that yet, which is not ideal... Otherwise return a wrapped MultiPolygon Create one feature per value Get union of f return values with resolved eltype We ignore pure Bool Get or write in one go, to skip a hash lookup If we actually fetched an existing node, update it First we collect all the edges around target pixels xs and ys hold pixel bounds We check the Von Neumann neighborhood to decide what edges are needed, if any. This page was generated using Literate.jl. This file mainly defines the In general, the idea behind the This allows for a simple and consistent framework within which users can define their own operations trivially easily, and removes a lot of the complexity involved with handling complex geometry structures. For example, a simple way to flip the x and y coordinates of a geometry is: As simple as that. There's no need to implement your own decomposition because it's done for you. Functions like Missing docstring. Missing docstring for Missing docstring. Missing docstring for Missing docstring. Missing docstring for Lazily flatten any If Reconstruct All objects in Ususally used in combination with Rebuild a geometry from child geometries. By default geometries will be rebuilt as a (Maybe it should go into GeoInterface.jl) Missing docstring. Missing docstring for We pass This is to help compilation - with a type to hold on to, it's easier for the compiler to separate threaded and non-threaded code paths. Note that if we didn't include the parent abstract type, this would have been really type unstable, since the compiler couldn't tell what would be returned! We had to add the type annotation on the There are also type based constructors available, but that's not advised. etc. The outer recursive functions then progressively rebuild the object using GeoInterface objects matching the original traits. If To handle this possibility it may be necessary to make Be careful making a union across "levels" of nesting, e.g. Threading is used at the outermost level possible - over an array, feature collection, or e.g. a MultiPolygonTrait where each Currently, threading defaults to Call _apply again with the trait of There is no trait and this is an AbstractArray - so just iterate over it calling _apply on the contents For an Array there is nothing else to do but map There is no trait and this is not an AbstractArray. Try to call _apply over it. We can't use threading as we don't know if we can can index into it. So just Try the Tables.jl interface first We extract the geometry column and run Then, we obtain the schema of the table, filter the geometry column out, and try to rebuild the same table as the best type - either the original type of Rewrap all FeatureCollectionTrait feature collections as GI.FeatureCollection Maybe use threads to call _apply on component features Run _apply on all Calculate the extent of the features Return a FeatureCollection with features, crs and caculated extent Return a FeatureCollection with features and crs Rewrap all FeatureTrait features as GI.Feature, keeping the properties Run _apply on the contained geometry Get the feature properties Calculate the extent of the geometry Return a new Feature with the new geometry and calculated extent, but the oroginal properties and crs Return a new Feature with the new geometry, but the oroginal properties and crs Reconstruct nested geometries, maybe using threads to call _apply on component geoms Map Calculate the extent of the sub geometries Return a new geometry of the same trait as Return a new geometryof the same trait as Fail loudly if we hit PointTrait without running Define some specific cases of this match to avoid method ambiguity Maybe use threads recucing over arrays Try to applyreduce over iterables Try to In this case, we don't reconstruct the table, but only operate on the geometry column. We extract the geometry column and run If We extract the geometry column and run Maybe use threads reducing over features of feature collections Features just applyreduce to their geometry Maybe use threads over components of nested geometries Don't thread over points it won't pay off Apply f to the target Fail if we hit PointTrait _applyreduce(f, op, target::TraitTarget{Target}, trait::PointTrait, geom; kw...) where Target = throw(ArgumentError("target target not found")) Specific cases to avoid method ambiguity Add dispatch argument for trait Try to unwrap over iterables Rewrap feature collections Apply f to the target geometry Fail if we hit PointTrait Specific cases to avoid method ambiguity Try to flatten over iterables Flatten feature collections Apply f to the target geometry Fail if we hit PointTrait without running Specific cases to avoid method ambiguity Try to reconstruct over iterables iter is updated by _reconstruct here Reconstruct feature collections iter is updated by _reconstruct here iter is updated by _reconstruct here Apply f to the target geometry Specific cases to avoid method ambiguity Fail if we hit PointTrait without running The Boolean type parameters here indicate "3d-ness" and "measure" coordinate, respectively. Threading utility, modified Mason Protters threading PSA run Customize this as needed. More tasks have more overhead, but better load balancing partition the range into chunks Map over the chunks Spawn a task to process this chunk Where we map Finally we join the results into a new vector Here we use the compiler directive Threading utility, modified Mason Protters threading PSA run WARNING: this will not work for mean/median - only ops where grouping is possible Customize this as needed. More tasks have more overhead, but better load balancing partition the range into chunks Map over the chunks Spawn a task to process this chunk Where we map Finally we join the results into a new vector This page was generated using Literate.jl. A closed ring is a ring that has the same start and end point. This is a requirement for a valid polygon (technically, for a valid LinearRing). This correction is used to ensure that the polygon is valid. The reason this operates on the polygon level is that several packages are loose about whether they return LinearRings (which is correct) or LineStrings (which is incorrect) for the contents of a polygon. Therefore, we decompose manually to ensure correctness. Many polygon providers do not close their polygons, which makes them invalid according to the specification. Quite a few geometry algorithms assume that polygons are closed, and leaving them open can lead to incorrect results! For example, the following polygon is not valid: even though it will look correct when visualized, and indeed appears correct. the ring is closed, all hail the ring Assemble the ring as a vector Close the ring Return an actual ring This page was generated using Literate.jl. This file simply defines the A geometry correction is a transformation that is applied to a geometry to correct it in some way. For example, a All See below for the full interface specification. This abstract type represents a geometry correction. Interface Any Any geometry correction must implement the interface as given above. This correction ensures that a polygon's exterior and interior rings are closed. It can be called on any geometry correction as usual. See also This correction ensures that the polygons included in a multipolygon aren't intersecting. If any polygon's are intersecting, they will be made nonintersecting through the This abstract type represents a geometry correction. Interface Any This correction ensures that the polygon's included in a multipolygon aren't intersecting. If any polygon's are intersecting, they will be combined through the union operation to create a unique set of disjoint (other than potentially connections by a single point) polygons covering the same area. See also This page was generated using Literate.jl. If the sub-polygons of a multipolygon are intersecting, this makes them invalid according to specification. Each sub-polygon of a multipolygon being disjoint (other than by a single point) is a requirment for a valid multipolygon. However, different libraries may achieve this in different ways. For example, taking the union of all sub-polygons of a multipolygon will create a new multipolygon where each sub-polygon is disjoint. This can be done with the The reason this operates on a multipolygon level is that it is easy for users to mistakenly create multipolygon's that overlap, which can then be detrimental to polygon clipping performance and even create wrong answers. Multipolygon providers may not check that the polygons making up their multipolygons do not intersect, which makes them invalid according to the specification. For example, the following multipolygon is not valid: given that the two sub-polygons are the exact same shape. You can see that the the multipolygon now only contains one sub-polygon, rather than the two identical ones provided. Combine any sub-polygons that intersect Break apart any sub-polygons that intersect This page was generated using Literate.jl. Keywords This page was generated using Literate.jl. This is a simple example of how to use the This page was generated using Literate.jl. This file is pretty simple - it simply reprojects a geometry pointwise from one CRS to another. It uses the Note that the actual implementation is in the This works using the We also inject a method error handler, which prints a suggestion if the Proj extension is not loaded. This page was generated using Literate.jl. This function "segmentizes" or "densifies" a geometry by adding extra vertices to the geometry so that no segment is longer than a given distance. This is useful for plotting geometries with a limited number of vertices, or for ensuring that a geometry is not too "coarse" for a given application. Info We plan to add interpolated segmentization from DataInterpolations.jl in the future, which will be available to any vector of point-like objects. For now, this function only works on 2D geometries. We will also support 3D geometries, as well as measure interpolation, in the future. You can see that this geometry was segmentized correctly, and now has 8 vertices where it previously had only 4. Now, we'll also segmentize this using the geodesic method, which is more accurate for lat/lon coordinates. This has a lot of points! It's important to keep in mind that the Now, let's see what they look like! To make this fair, we'll use approximately the same number of points for both. There are two methods available for segmentizing geometries at the moment: Missing docstring. Missing docstring for Missing docstring. Missing docstring for We benchmark our method against LibGEOS's Add an error hint for GeodesicSegments if Proj is not loaded! End the line with the original coordinate, to avoid any multiplication errors. Note The This page was generated using Literate.jl. This file holds implementations for the RadialDistance, Douglas-Peucker, and Visvalingam-Whyatt algorithms for simplifying geometries (specifically for polygons and lines). A quick and dirty example is: We benchmark these methods against LibGEOS's This is the complex polygon we'll be benchmarking. Keywords Example output Default algorithm is DouglasPeucker square tolerance for reduced computation square tolerance for reduced computation Determine stopping critetia Set up queue Set up results vector Loop through points until stopping criteria are fulfilled Add next point to results Determine which point to add next by checking left and right of point Add and remove values from queue Value in queue is next value to add to results Add left and/or right values to queue or delete used queue value Determine new maximum queue value Check start/endpoint distance to other points to see if it meets criteria Remove start point and replace with second point Remove start point and add point with maximum distance still remaining double tolerance for reduced computation Calculates double the area of a triangle given its vertices Check SimplifyAlgs inputs to make sure they are valid for below algorithms This page was generated using Literate.jl. This page was generated using Literate.jl. Keywords This page was generated using Literate.jl. Examples output This page was generated using Literate.jl. Spatial joins can be done between any geometry types (from geometrycollections to points), just as geometrical predicates can be evaluated on any geometries. In this tutorial, we will show how to perform a spatial join on first a toy dataset and then two Natural Earth datasets, to show how this can be used in the real world. In order to perform the spatial join, we use FlexiJoins.jl to perform the join, specifically using its We have enabled the use of all of GeometryOps' boolean comparisons here. These are: Tip Always place the dataframe with more complex geometries second, as that is the one which will be sorted into a tree. This example demonstrates how to perform a spatial join between two datasets: a set of polygons and a set of randomly generated points. The polygons are represented as a DataFrame with geometries and colors, while the points are stored in a separate DataFrame. The spatial join is performed using the First, we generate our data. We create two triangle polygons which, together, span the rectangle (0, 0, 1, 1), and a set of points which are randomly distributed within this rectangle. Here, the upper polygon is blue, and the lower polygon is red. Keep this in mind! Now, we generate the points. You can see that they are evenly distributed around the box. But how do we know which points are in which polygons? We have to join the two dataframes based on which polygon (if any) each point lies within. Now, we can perform the "spatial join" using FlexiJoins. We are performing an outer join here Here, you can see that the colors were assigned appropriately to the scattered points! Suppose I have a list of polygons representing administrative regions (or mining sites, or what have you), and I have a list of polygons for each country. I want to find the country each region is in. Warning This is how you would do this, but it doesn't work yet, since the GeometryOps predicates are quite slow on large polygons. If you try this, the code will continue to run for a very, very long time (it took 12 hours on my laptop, but with minimal CPU usage). In case you want to use a custom predicate, you only need to define a method to tell FlexiJoins how to use it. For example, let's suppose you wanted to perform a spatial join on geometries which are some distance away from each other: You would need to define This will enable FlexiJoins to support your custom function, when it's passed to GeometryOps.jl is a package for geometric calculations on (primarily 2D) geometries. The driving idea behind this package is to unify all the disparate packages for geometric calculations in Julia, and make them GeoInterface.jl-compatible. We seem to be focusing primarily on 2/2.5D geometries for now. Most of the usecases are driven by GIS and similar Earth data workflows, so this might be a bit specialized towards that, but methods should always be general to any coordinate space. We welcome contributions, either as pull requests or discussion on issues!Accurate accumulation
import GeometryOps as GO, GeoInterface as GI
+using GeoJSON
+using AccurateArithmetic
+using NaturalEarth
+
+all_adm0 = naturalearth("admin_0_countries", 10)
FeatureCollection with 258 Features
GO.area(all_adm0)
21427.909318372607
AccurateArithmetic.sum_oro(GO.area.(all_adm0.geometry))
21427.909318372607
AccurateArithmetic.sum_kbn(GO.area.(all_adm0.geometry))
21427.909318372607
GI.Polygon.(GO.flatten(Union{GI.LineStringTrait, GI.LinearRingTrait}, all_adm0) |> collect .|> x -> [x]) .|> GO.signed_area |> sum
-21427.90063612163
GI.Polygon.(GO.flatten(Union{GI.LineStringTrait, GI.LinearRingTrait}, all_adm0) |> collect .|> x -> [x]) .|> GO.signed_area |> sum_oro
-21427.90063612163
Predicates
Orient
using CairoMakie
+import GeometryOps as GO, GeoInterface as GI, LibGEOS as LG
+import ExactPredicates
+using MultiFloats
+using Chairmarks: @be
+using BenchmarkTools: prettytime
+using Statistics
+
+function orient_f64(p, q, r)
+ return sign((GI.x(p) - GI.x(r))*(GI.y(q) - GI.y(r)) - (GI.y(p) - GI.y(r))*(GI.x(q) - GI.x(r)))
+end
+
+function orient_adaptive(p, q, r)
+ px, py = Float64x2(GI.x(p)), Float64x2(GI.y(p))
+ qx, qy = Float64x2(GI.x(q)), Float64x2(GI.y(q))
+ rx, ry = Float64x2(GI.x(r)), Float64x2(GI.y(r))
+ return sign((px - rx)*(qy - ry) - (py - ry)*(qx - rx))
+end
+# Create an interactive Makie dashboard which can show what is done here
+labels = ["Float64", "Adaptive", "Exact"]
+funcs = [orient_f64, orient_adaptive, ExactPredicates.orient]
+fig = Figure()
+axs = [Axis(fig[1, i]; aspect = DataAspect(), xticklabelrotation = pi/4, title) for (i, title) in enumerate(labels)]
+w, r, q, p = 42.0, 0.95, 18.0, 16.8
+function generate_heatmap_args(func, w, r, q, p, heatmap_size = 1000)
+ w_range = LinRange(0, 0+2.0^(-w), heatmap_size)
+ orient_field = [func((p, p), (q, q), (r+x, r+y)) for x in w_range, y in w_range]
+ return (w_range, w_range, orient_field)
+end
+for (i, (ax, func)) in enumerate(zip(axs, funcs))
+ heatmap!(ax, generate_heatmap_args(func, w, r, q, p)...)
+ # now get timing
+ w_range = LinRange(0, 0+2.0^(-w), 5) # for timing - we want to sample stable + unstable points
+ @time timings = [@be $(func)($((p, p)), $((q, q)), $((r+x, r+y))) for x in w_range, y in w_range]
+ median_timings = map.(x -> getproperty(x, :time), getproperty.(timings, :samples)) |> Iterators.flatten |> collect
+ ax.subtitle = prettytime(Statistics.median(median_timings)*10^9)
+ # create time histogram plot
+ # hist(fig[2, i], median_timings; axis = (; xticklabelrotation = pi/4))
+ display(fig)
+end
+resize!(fig, 1000, 450)
+fig
Dashboard
using WGLMakie
+import GeometryOps as GO, GeoInterface as GI, LibGEOS as LG
+import ExactPredicates
+using MultiFloats
+
+function orient_f64(p, q, r)
+ return sign((GI.x(p) - GI.x(r))*(GI.y(q) - GI.y(r)) - (GI.y(p) - GI.y(r))*(GI.x(q) - GI.x(r)))
+end
+
+function orient_adaptive(p, q, r)
+ px, py = Float64x2(GI.x(p)), Float64x2(GI.y(p))
+ qx, qy = Float64x2(GI.x(q)), Float64x2(GI.y(q))
+ rx, ry = Float64x2(GI.x(r)), Float64x2(GI.y(r))
+ return sign((px - rx)*(qy - ry) - (py - ry)*(qx - rx))
+end
+# Create an interactive Makie dashboard which can show what is done here
+fig = Figure()
+ax = Axis(fig[1, 1]; aspect = DataAspect())
+sliders = SliderGrid(fig[2, 1],
+ (label = L"w = 2^{-v} (zoom)", range = LinRange(40, 44, 100), startvalue = 42),
+ (label = L"r = (x, y),~ x, y ∈ v + [0..w)", range = 0:0.01:3, startvalue = 0.95),
+ (label = L"q = (k, k),~ k = v", range = LinRange(0, 30, 100), startvalue = 18),
+ (label = L"p = (k, k),~ k = v", range = LinRange(0, 30, 100), startvalue = 16.8),
+)
+orient_funcs = [orient_f64, orient_adaptive, ExactPredicates.orient]
+menu = Menu(fig[3, 1], options = zip(string.(orient_funcs), orient_funcs))
+w_obs, r_obs, q_obs, p_obs = getproperty.(sliders.sliders, :value)
+orient_obs = menu.selection
+
+heatmap_size = @lift maximum(widths($(ax.scene.viewport)))*4
+
+matrix_observable = lift(orient_obs, w_obs, r_obs, q_obs, p_obs, heatmap_size) do orient, w, r, q, p, heatmap_size
+ return [orient((p, p), (q, q), (r+x, r+y)) for x in LinRange(0, 0+2.0^(-w), heatmap_size), y in LinRange(0, 0+2.0^(-w), heatmap_size)]
+end
+heatmap!(ax, matrix_observable; colormap = [:red, :green, :blue])
+resize!(fig, 500, 700)
+fig
Testing robust vs regular predicates
+import GeoInterface as GI, GeometryOps as GO, LibGEOS as LG
+using MultiFloats
+c1 = [[-28083.868447876892, -58059.13401805979], [-9833.052704767595, -48001.726711609794], [-16111.439295815226, -2.856614689791036e-11], [-76085.95770326033, -2.856614689791036e-11], [-28083.868447876892, -58059.13401805979]]
+c2 = [[-53333.333333333336, 0.0], [0.0, 0.0], [0.0, -80000.0], [-60000.0, -80000.0], [-53333.333333333336, 0.0]]
+
+p1 = GI.Polygon([c1])
+p2 = GI.Polygon([c2])
+GO.intersection(p1, p2; target = GI.PolygonTrait(), fix_multipoly = nothing)
+
+p1_m, p2_m = GO.transform(x -> (Float64x2.(x)), [p1, p2])
+GO.intersection(p1_m, p2_m; target = GI.PolygonTrait(), fix_multipoly = nothing)
+
+p1 = GI.Polygon([[[-57725.80869813739, -52709.704377648755], [-53333.333333333336, 0.0], [-41878.01362848005, 0.0], [-36022.23699059147, -43787.61366192682], [-48268.44121252392, -52521.18593721105], [-57725.80869813739, -52709.704377648755]]])
+p2 = GI.Polygon([[[-60000.0, 80000.0], [0.0, 80000.0], [0.0, 0.0], [-53333.33333333333, 0.0], [-50000.0, 40000.0], [-60000.0, 80000.0]]])
+p1_m, p2_m = GO.transform(x -> (Float64x2.(x)), [p1, p2])
+f, a, p__1 = poly(p1; label = "p1")
+p__2 = poly!(a, p2; label = "p2")
+
+GO.intersection(p1_m, p2_m; target = GI.PolygonTrait(), fix_multipoly = nothing)
+LG.intersection(p1_m, p2_m)
Incircle
`,10),l=[p];function t(E,e,r,d,g,y){return a(),i("div",null,l)}const D=s(n,[["render",t]]);export{C as __pageData,D as default};
diff --git a/previews/PR149/assets/experiments_predicates.md.Mdm6NeU-.lean.js b/previews/PR149/assets/experiments_predicates.md.Mdm6NeU-.lean.js
new file mode 100644
index 000000000..1f362a696
--- /dev/null
+++ b/previews/PR149/assets/experiments_predicates.md.Mdm6NeU-.lean.js
@@ -0,0 +1 @@
+import{_ as s,c as i,o as a,a6 as h}from"./chunks/framework.CDOaXZPW.js";const k="/GeometryOps.jl/previews/PR149/assets/lpjuviw.BVgx7YYB.png",C=JSON.parse('{"title":"Predicates","description":"","frontmatter":{},"headers":[],"relativePath":"experiments/predicates.md","filePath":"experiments/predicates.md","lastUpdated":null}'),n={name:"experiments/predicates.md"},p=h("",10),l=[p];function t(E,e,r,d,g,y){return a(),i("div",null,l)}const D=s(n,[["render",t]]);export{C as __pageData,D as default};
diff --git a/previews/PR149/assets/fhcfnwd.BD0hVfse.png b/previews/PR149/assets/fhcfnwd.BD0hVfse.png
new file mode 100644
index 000000000..212d1e6a0
Binary files /dev/null and b/previews/PR149/assets/fhcfnwd.BD0hVfse.png differ
diff --git a/previews/PR149/assets/gjrlysw.BOOG5oTW.png b/previews/PR149/assets/gjrlysw.BOOG5oTW.png
new file mode 100644
index 000000000..fd0dcd164
Binary files /dev/null and b/previews/PR149/assets/gjrlysw.BOOG5oTW.png differ
diff --git a/previews/PR149/assets/hxpbxsu.BEFUMtlf.png b/previews/PR149/assets/hxpbxsu.BEFUMtlf.png
new file mode 100644
index 000000000..b400f44a5
Binary files /dev/null and b/previews/PR149/assets/hxpbxsu.BEFUMtlf.png differ
diff --git a/previews/PR149/assets/index.md.CYCEqvPe.js b/previews/PR149/assets/index.md.CYCEqvPe.js
new file mode 100644
index 000000000..a96067cbc
--- /dev/null
+++ b/previews/PR149/assets/index.md.CYCEqvPe.js
@@ -0,0 +1 @@
+import{_ as i,c as a,o,j as e,a as t}from"./chunks/framework.CDOaXZPW.js";const b=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"GeometryOps.jl","text":"","tagline":"Blazing fast geometry operations in pure Julia","image":{"src":"/logo.png","alt":"GeometryOps"},"actions":[{"theme":"brand","text":"Introduction","link":"/introduction"},{"theme":"alt","text":"View on Github","link":"https://github.com/JuliaGeo/GeometryOps.jl"},{"theme":"alt","text":"API Reference","link":"/api"}]},"features":[{"icon":"","title":"Pure Julia code","details":"Fast, understandable, extensible functions","link":"/introduction"},{"icon":"","title":"Literate programming","details":"Documented source code with examples!","link":"/source/methods/clipping/cut"},{"icon":"","title":"Full integration with GeoInterface","details":"Use any GeoInterface.jl-compatible geometry","link":"https://juliageo.org/GeoInterface.jl/stable"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":null}'),s={name:"index.md"},n=e("p",{style:{"margin-bottom":"2cm"}},null,-1),l=e("div",{class:"vp-doc",style:{width:"80%",margin:"auto"}},[e("h1",null," What is GeometryOps.jl? "),e("p",null,"GeometryOps.jl is a package for geometric calculations on (primarily 2D) geometries."),e("p",null,[t("The driving idea behind this package is to unify all the disparate packages for geometric calculations in Julia, and make them "),e("a",{href:"https://github.com/JuliaGeo/GeoInterface.jl",target:"_blank",rel:"noreferrer"},"GeoInterface.jl"),t("-compatible. We seem to be focusing primarily on 2/2.5D geometries for now.")]),e("p",null,"Most of the usecases are driven by GIS and similar Earth data workflows, so this might be a bit specialized towards that, but methods should always be general to any coordinate space."),e("p",null,"We welcome contributions, either as pull requests or discussion on issues!")],-1),r=[n,l];function c(d,u,h,m,g,p){return o(),a("div",null,r)}const _=i(s,[["render",c]]);export{b as __pageData,_ as default};
diff --git a/previews/PR149/assets/index.md.CYCEqvPe.lean.js b/previews/PR149/assets/index.md.CYCEqvPe.lean.js
new file mode 100644
index 000000000..a96067cbc
--- /dev/null
+++ b/previews/PR149/assets/index.md.CYCEqvPe.lean.js
@@ -0,0 +1 @@
+import{_ as i,c as a,o,j as e,a as t}from"./chunks/framework.CDOaXZPW.js";const b=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"GeometryOps.jl","text":"","tagline":"Blazing fast geometry operations in pure Julia","image":{"src":"/logo.png","alt":"GeometryOps"},"actions":[{"theme":"brand","text":"Introduction","link":"/introduction"},{"theme":"alt","text":"View on Github","link":"https://github.com/JuliaGeo/GeometryOps.jl"},{"theme":"alt","text":"API Reference","link":"/api"}]},"features":[{"icon":"","title":"Pure Julia code","details":"Fast, understandable, extensible functions","link":"/introduction"},{"icon":"","title":"Literate programming","details":"Documented source code with examples!","link":"/source/methods/clipping/cut"},{"icon":"","title":"Full integration with GeoInterface","details":"Use any GeoInterface.jl-compatible geometry","link":"https://juliageo.org/GeoInterface.jl/stable"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":null}'),s={name:"index.md"},n=e("p",{style:{"margin-bottom":"2cm"}},null,-1),l=e("div",{class:"vp-doc",style:{width:"80%",margin:"auto"}},[e("h1",null," What is GeometryOps.jl? "),e("p",null,"GeometryOps.jl is a package for geometric calculations on (primarily 2D) geometries."),e("p",null,[t("The driving idea behind this package is to unify all the disparate packages for geometric calculations in Julia, and make them "),e("a",{href:"https://github.com/JuliaGeo/GeoInterface.jl",target:"_blank",rel:"noreferrer"},"GeoInterface.jl"),t("-compatible. We seem to be focusing primarily on 2/2.5D geometries for now.")]),e("p",null,"Most of the usecases are driven by GIS and similar Earth data workflows, so this might be a bit specialized towards that, but methods should always be general to any coordinate space."),e("p",null,"We welcome contributions, either as pull requests or discussion on issues!")],-1),r=[n,l];function c(d,u,h,m,g,p){return o(),a("div",null,r)}const _=i(s,[["render",c]]);export{b as __pageData,_ as default};
diff --git a/previews/PR149/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 b/previews/PR149/assets/inter-italic-cyrillic-ext.r48I6akx.woff2
new file mode 100644
index 000000000..b6b603d59
Binary files /dev/null and b/previews/PR149/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 differ
diff --git a/previews/PR149/assets/inter-italic-cyrillic.By2_1cv3.woff2 b/previews/PR149/assets/inter-italic-cyrillic.By2_1cv3.woff2
new file mode 100644
index 000000000..def40a4f6
Binary files /dev/null and b/previews/PR149/assets/inter-italic-cyrillic.By2_1cv3.woff2 differ
diff --git a/previews/PR149/assets/inter-italic-greek-ext.1u6EdAuj.woff2 b/previews/PR149/assets/inter-italic-greek-ext.1u6EdAuj.woff2
new file mode 100644
index 000000000..e070c3d30
Binary files /dev/null and b/previews/PR149/assets/inter-italic-greek-ext.1u6EdAuj.woff2 differ
diff --git a/previews/PR149/assets/inter-italic-greek.DJ8dCoTZ.woff2 b/previews/PR149/assets/inter-italic-greek.DJ8dCoTZ.woff2
new file mode 100644
index 000000000..a3c16ca40
Binary files /dev/null and b/previews/PR149/assets/inter-italic-greek.DJ8dCoTZ.woff2 differ
diff --git a/previews/PR149/assets/inter-italic-latin-ext.CN1xVJS-.woff2 b/previews/PR149/assets/inter-italic-latin-ext.CN1xVJS-.woff2
new file mode 100644
index 000000000..2210a899e
Binary files /dev/null and b/previews/PR149/assets/inter-italic-latin-ext.CN1xVJS-.woff2 differ
diff --git a/previews/PR149/assets/inter-italic-latin.C2AdPX0b.woff2 b/previews/PR149/assets/inter-italic-latin.C2AdPX0b.woff2
new file mode 100644
index 000000000..790d62dc7
Binary files /dev/null and b/previews/PR149/assets/inter-italic-latin.C2AdPX0b.woff2 differ
diff --git a/previews/PR149/assets/inter-italic-vietnamese.BSbpV94h.woff2 b/previews/PR149/assets/inter-italic-vietnamese.BSbpV94h.woff2
new file mode 100644
index 000000000..1eec0775a
Binary files /dev/null and b/previews/PR149/assets/inter-italic-vietnamese.BSbpV94h.woff2 differ
diff --git a/previews/PR149/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 b/previews/PR149/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2
new file mode 100644
index 000000000..2cfe61536
Binary files /dev/null and b/previews/PR149/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 differ
diff --git a/previews/PR149/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 b/previews/PR149/assets/inter-roman-cyrillic.C5lxZ8CY.woff2
new file mode 100644
index 000000000..e3886dd14
Binary files /dev/null and b/previews/PR149/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 differ
diff --git a/previews/PR149/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 b/previews/PR149/assets/inter-roman-greek-ext.CqjqNYQ-.woff2
new file mode 100644
index 000000000..36d67487d
Binary files /dev/null and b/previews/PR149/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 differ
diff --git a/previews/PR149/assets/inter-roman-greek.BBVDIX6e.woff2 b/previews/PR149/assets/inter-roman-greek.BBVDIX6e.woff2
new file mode 100644
index 000000000..2bed1e85e
Binary files /dev/null and b/previews/PR149/assets/inter-roman-greek.BBVDIX6e.woff2 differ
diff --git a/previews/PR149/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 b/previews/PR149/assets/inter-roman-latin-ext.4ZJIpNVo.woff2
new file mode 100644
index 000000000..9a8d1e2b5
Binary files /dev/null and b/previews/PR149/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 differ
diff --git a/previews/PR149/assets/inter-roman-latin.Di8DUHzh.woff2 b/previews/PR149/assets/inter-roman-latin.Di8DUHzh.woff2
new file mode 100644
index 000000000..07d3c53ae
Binary files /dev/null and b/previews/PR149/assets/inter-roman-latin.Di8DUHzh.woff2 differ
diff --git a/previews/PR149/assets/inter-roman-vietnamese.BjW4sHH5.woff2 b/previews/PR149/assets/inter-roman-vietnamese.BjW4sHH5.woff2
new file mode 100644
index 000000000..57bdc22ae
Binary files /dev/null and b/previews/PR149/assets/inter-roman-vietnamese.BjW4sHH5.woff2 differ
diff --git a/previews/PR149/assets/introduction.md.BLWLlAIo.js b/previews/PR149/assets/introduction.md.BLWLlAIo.js
new file mode 100644
index 000000000..a14b872dc
--- /dev/null
+++ b/previews/PR149/assets/introduction.md.BLWLlAIo.js
@@ -0,0 +1 @@
+import{_ as e,c as a,o as t,a6 as o}from"./chunks/framework.CDOaXZPW.js";const f=JSON.parse('{"title":"Introduction","description":"","frontmatter":{},"headers":[],"relativePath":"introduction.md","filePath":"introduction.md","lastUpdated":null}'),i={name:"introduction.md"},r=o('Introduction
Main concepts
The
apply
paradigm apply
function allows you to decompose a given collection of geometries down to a certain level, and then operate on it.map
in the way you apply it to geometries.apply
and applyreduce
take any geometry, vector of geometries, collection of geometries, or table (like Shapefile.Table
, DataFrame
, or GeoTable
)!What's this
GeoInterface.Wrapper
thing? Paradigms
apply
and applyreduce
, as well as the fix
and prepare
APIs, that represent paradigms of programming, by which we mean the ability to program in a certain way, and in so doing, fit neatly into the tools we've built without needing to re-implement the wheel.apply
apply
function allows you to decompose a given collection of geometries down to a certain level, operate on it, and reconstruct it back to the same nested form as the original. In general, its invocation is:apply(f, trait::Trait, geom)
map
in the way you apply it to geometries - except that you tell it at which level it should stop, by passing a trait
to it.apply
will start by decomposing the geometry, feature, featurecollection, iterable, or table that you pass to it, and stop when it encounters a geometry for which GI.trait(geom) isa Trait
. This encompasses unions of traits especially, but beware that any geometry which is not explicitly handled, and hits GI.PointTrait
, will cause an error.apply
is unlike map
in that it returns reconstructed geometries, instead of the raw output of the function. If you want a purely map-like behaviour, like calculating the length of each linestring in your feature collection, then call GO.flatten(f, trait, geom)
, which will decompose each geometry to the given trait
and apply f
to it, returning the decomposition as a flattened vector.applyreduce
applyreduce
is like the previous map
-based approach that we mentioned, except that it reduce
s the result of f
by op
. Note that applyreduce
does not guarantee associativity, so it's best to have typeof(init) == returntype(op)
.fix
and prepare
fix
and prepare
paradigms are different from apply
, though they are built on top of it. They involve the use of structs as "actions", where a constructed object indicates an action that should be taken. A trait like interface prescribes the level (polygon, linestring, point, etc) at which each action should be applied.Prepared
geometry with several preparations (sorted edge lists, rtrees, monotone chains, etc.)Peculiarities
What does
apply
return and why? apply
returns the target geometries returned by f
, whatever type/package they are from, but geometries, features or feature collections that wrapped the target are replaced with GeoInterace.jl wrappers with matching GeoInterface.trait
to the originals. All non-geointerface iterables become Array
s. Tables.jl compatible tables are converted either back to the original type if a Tables.materializer
is defined, and if not then returned as generic NamedTuple
column tables (i.e., a NamedTuple of vectors).f
returns GeoInterface geometries unless there is a performance/conversion overhead to doing that.Why do you want me to provide a
target
in set operations? intersection
, difference
, and union
, many different geometry types may be obtained - depending on the relationship between the polygons. For example, when performing an union on two nonintersecting polygons, one would technically have two disjoint polygons as an output.target
keyword to allow the user to control which kinds of geometry they want back. For example, setting target
to PolygonTrait
will cause a vector of polygons to be returned (this is the only currently supported behaviour). In future, we may implement MultiPolygonTrait
or GeometryCollectionTrait
targets which will return a single geometry, as LibGEOS and ArchGDAL do._True
and _False
(or BoolsAsTypes
) threaded
and calc_extent
in apply
, as types. This allows the compiler to reason about what will happen, and call the correct compiled method, in a stable way without worrying aboutGeometryOps.jl
module GeometryOps
+
+using GeoInterface
+using GeometryBasics
+import Tables
+using LinearAlgebra, Statistics
+import GeometryBasics.StaticArrays
+import ExactPredicates
+import Base.@kwdef
+
+using GeoInterface.Extents: Extents
+
+const GI = GeoInterface
+const GB = GeometryBasics
+
+const TuplePoint{T} = Tuple{T, T} where T <: AbstractFloat
+const Edge{T} = Tuple{TuplePoint{T},TuplePoint{T}} where T
+
+include("primitives.jl")
+include("utils.jl")
+
+include("methods/angles.jl")
+include("methods/area.jl")
+include("methods/barycentric.jl")
+include("methods/centroid.jl")
+include("methods/distance.jl")
+include("methods/equals.jl")
+include("methods/clipping/predicates.jl")
+include("methods/clipping/clipping_processor.jl")
+include("methods/clipping/coverage.jl")
+include("methods/clipping/cut.jl")
+include("methods/clipping/intersection.jl")
+include("methods/clipping/difference.jl")
+include("methods/clipping/union.jl")
+include("methods/geom_relations/contains.jl")
+include("methods/geom_relations/coveredby.jl")
+include("methods/geom_relations/covers.jl")
+include("methods/geom_relations/crosses.jl")
+include("methods/geom_relations/disjoint.jl")
+include("methods/geom_relations/geom_geom_processors.jl")
+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/orientation.jl")
+include("methods/polygonize.jl")
+
+include("transformations/extent.jl")
+include("transformations/flip.jl")
+include("transformations/reproject.jl")
+include("transformations/segmentize.jl")
+include("transformations/simplify.jl")
+include("transformations/tuples.jl")
+include("transformations/transform.jl")
+include("transformations/correction/geometry_correction.jl")
+include("transformations/correction/closed_ring.jl")
+include("transformations/correction/intersecting_polygons.jl")
GO.extent
or GO.trait
.for name in names(GeoInterface)
+ @eval using GeoInterface: $name
+end
+for name in names(Extents)
+ @eval using GeoInterface.Extents: $name
+end
+
+function __init__()
Base.Experimental.register_error_hint(_reproject_error_hinter, MethodError)
+ Base.Experimental.register_error_hint(_geodesic_segments_error_hinter, MethodError)
+end
+
+end
Angles
export angles
What is angles?
import GeometryOps as GO
+import GeoInterface as GI
+using Makie, CairoMakie
+
+rect = GI.Polygon([[(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)]])
+f, a, p = poly(collect(GI.getpoint(rect)); axis = (; aspect = DataAspect()))
GO.angles(rect) # [90, 90, 90, 90]
4-element Vector{Float64}:
+ 90.0
+ 90.0
+ 90.0
+ 90.0
Implementation
const _ANGLE_TARGETS = TraitTarget{Union{GI.PolygonTrait,GI.AbstractCurveTrait,GI.MultiPointTrait,GI.PointTrait}}()
+
+"""
+ angles(geom, ::Type{T} = Float64)
+
+Returns the angles of a geometry or collection of geometries.
+This is computed differently for different geometries:
+
+ - The angles of a point is an empty vector.
+ - The angles of a single line segment is an empty vector.
+ - The angles of a linestring or linearring is a vector of angles formed by the curve.
+ - The angles of a polygin is a vector of vectors of angles formed by each ring.
+ - The angles of a multi-geometry collection is a vector of the angles of each of the
+ sub-geometries as defined above.
+
+Result will be a Vector, or nested set of vectors, of type T where an optional argument with
+a default value of Float64.
+"""
+function angles(geom, ::Type{T} = Float64; threaded =false) where T <: AbstractFloat
+ applyreduce(vcat, _ANGLE_TARGETS, geom; threaded, init = Vector{T}()) do g
+ _angles(T, GI.trait(g), g)
+ end
+end
_angles(::Type{T}, ::Union{GI.PointTrait, GI.MultiPointTrait, GI.LineTrait}, geom) where T = T[]
+
+#= The angles of a linestring are the angles formed by the line. If the first and last point
+are not explicitly repeated, the geom is not considered closed. The angles should all be on
+one side of the line, but a particular side is not guaranteed by this function. =#
+function _angles(::Type{T}, ::GI.LineStringTrait, geom) where T
+ npoints = GI.npoint(geom)
+ first_last_equal = equals(GI.getpoint(geom, 1), GI.getpoint(geom, npoints))
+ angle_list = Vector{T}(undef, npoints - (first_last_equal ? 1 : 2))
+ _find_angles!(
+ T, angle_list, geom;
+ offset = first_last_equal, close_geom = false,
+ )
+ return angle_list
+end
+
+#= The angles of a linearring are the angles within the closed line and include the angles
+formed by connecting the first and last points of the curve. =#
+function _angles(::Type{T}, ::GI.LinearRingTrait, geom; interior = true) where T
+ npoints = GI.npoint(geom)
+ first_last_equal = equals(GI.getpoint(geom, 1), GI.getpoint(geom, npoints))
+ angle_list = Vector{T}(undef, npoints - (first_last_equal ? 1 : 0))
+ _find_angles!(
+ T, angle_list, geom;
+ offset = true, close_geom = !first_last_equal, interior = interior,
+ )
+ return angle_list
+end
+
+#= The angles of a polygon is a vector of polygon angles. Note that if there are holes
+within the polyogn, the angles will be listed after the exterior ring angles in order of the
+holes. All angles, including the hole angles, are interior angles of the polygon.=#
+function _angles(::Type{T}, ::GI.PolygonTrait, geom) where T
+ angles = _angles(T, GI.LinearRingTrait(), GI.getexterior(geom); interior = true)
+ for h in GI.gethole(geom)
+ append!(angles, _angles(T, GI.LinearRingTrait(), h; interior = false))
+ end
+ return angles
+end
function _find_angles!(
+ ::Type{T}, angle_list, geom;
+ offset, close_geom, interior = true,
+) where T
+ local p1, prev_p1_diff, p2_p1_diff
+ local start_point, start_diff
+ local extreem_idx, extreem_x, extreem_y
+ i_offset = offset ? 1 : 0
for (i, p2) in enumerate(GI.getpoint(geom))
+ xp2, yp2 = GI.x(p2), GI.y(p2)
+ #= Find point with smallest x values (and smallest y in case of a tie) as this point
+ is know to be convex. =#
+ if i == 1 || (xp2 < extreem_x || (xp2 == extreem_x && yp2 < extreem_y))
+ extreem_idx = i
+ extreem_x, extreem_y = xp2, yp2
+ end
+ if i > 1
+ p2_p1_diff = (xp2 - GI.x(p1), yp2 - GI.y(p1))
+ if i == 2
+ start_point = p1
+ start_diff = p2_p1_diff
+ else
+ angle_list[i - 2 + i_offset] = _diffs_calc_angle(T, prev_p1_diff, p2_p1_diff)
+ end
+ prev_p1_diff = -1 .* p2_p1_diff
+ end
+ p1 = p2
+ end
if close_geom
+ p2_p1_diff = (GI.x(start_point) - GI.x(p1), GI.y(start_point) - GI.y(p1))
+ angle_list[end] = _diffs_calc_angle(T, prev_p1_diff, p2_p1_diff)
+ prev_p1_diff = -1 .* p2_p1_diff
+ end
if offset
+ angle_list[1] = _diffs_calc_angle(T, prev_p1_diff, start_diff)
+ end
+ #= Make sure that all of the angles are on the same side of the line and inside of the
+ closed ring if the input geometry is closed. =#
+ inside_sgn = sign(angle_list[extreem_idx]) * (interior ? 1 : -1)
+ for i in eachindex(angle_list)
+ idx_sgn = sign(angle_list[i])
+ if idx_sgn == -1
+ angle_list[i] = abs(angle_list[i])
+ end
+ if idx_sgn != inside_sgn
+ angle_list[i] = 360 - angle_list[i]
+ end
+ end
+ return
+end
function _diffs_calc_angle(::Type{T}, (Δx_prev, Δy_prev), (Δx_curr, Δy_curr)) where T
+ cross_prod = Δx_prev * Δy_curr - Δy_prev * Δx_curr
+ dot_prod = Δx_prev * Δx_curr + Δy_prev * Δy_curr
+ prev_mag = max(sqrt(Δx_prev^2 + Δy_prev^2), eps(T))
+ curr_mag = max(sqrt(Δx_curr^2 + Δy_curr^2), eps(T))
+ val = clamp(dot_prod / (prev_mag * curr_mag), -one(T), one(T))
+ angle = real(acos(val) * 180 / π)
+ return angle * (cross_prod < 0 ? -1 : 1)
+end
Area and signed area
export area, signed_area
What is area? What is signed area?
import GeometryOps as GO
+import GeoInterface as GI
+using Makie
+using CairoMakie
+
+rect = GI.Polygon([[(0,0), (0,1), (1,1), (1,0), (0, 0)]])
+f, a, p = poly(collect(GI.getpoint(rect)); axis = (; aspect = DataAspect()))
lines!(
+ collect(GI.getpoint(rect));
+ color = 1:GI.npoint(rect), linewidth = 10.0)
+f
GO.signed_area(rect) # -1.0
-1.0
Implementation
const _AREA_TARGETS = TraitTarget{Union{GI.PolygonTrait,GI.AbstractCurveTrait,GI.MultiPointTrait,GI.PointTrait}}()
+
+"""
+ area(geom, [T = Float64])::T
+
+Returns the area of a geometry or collection of geometries.
+This is computed slightly differently for different geometries:
+
+ - The area of a point/multipoint is always zero.
+ - The area of a curve/multicurve is always zero.
+ - The area of a polygon is the absolute value of the signed area.
+ - The area multi-polygon is the sum of the areas of all of the sub-polygons.
+ - The area of a geometry collection, feature collection of array/iterable
+ is the sum of the areas of all of the sub-geometries.
+
+Result will be of type T, where T is an optional argument with a default value
+of Float64.
+"""
+function area(geom, ::Type{T} = Float64; threaded=false) where T <: AbstractFloat
+ applyreduce(+, _AREA_TARGETS, geom; threaded, init=zero(T)) do g
+ _area(T, GI.trait(g), g)
+ end
+end
+
+"""
+ signed_area(geom, [T = Float64])::T
+
+Returns the signed area of a single geometry, based on winding order.
+This is computed slighly differently for different geometries:
+
+ - The signed area of a point is always zero.
+ - The signed area of a curve is always zero.
+ - The signed area of a polygon is computed with the shoelace formula and is
+ positive if the polygon coordinates wind clockwise and negative if
+ counterclockwise.
+ - You cannot compute the signed area of a multipolygon as it doesn't have a
+ meaning as each sub-polygon could have a different winding order.
+
+Result will be of type T, where T is an optional argument with a default value
+of Float64.
+"""
+signed_area(geom, ::Type{T} = Float64) where T <: AbstractFloat =
+ _signed_area(T, GI.trait(geom), geom)
_area(::Type{T}, ::GI.AbstractGeometryTrait, geom) where T = zero(T)
+
+_signed_area(::Type{T}, ::GI.AbstractGeometryTrait, geom) where T = zero(T)
_area(::Type{T}, tr::GI.LinearRingTrait, geom) where T = 0 # could be abs(_signed_area(T, tr, geom))
+
+_signed_area(::Type{T}, ::GI.LinearRingTrait, geom) where T = 0 # could be _signed_area(T, tr, geom)
_area(::Type{T}, trait::GI.PolygonTrait, poly) where T =
+ abs(_signed_area(T, trait, poly))
+
+function _signed_area(::Type{T}, ::GI.PolygonTrait, poly) where T
+ GI.isempty(poly) && return zero(T)
+ s_area = _signed_area(T, GI.getexterior(poly))
+ area = abs(s_area)
+ area == 0 && return area
for hole in GI.gethole(poly)
+ area -= abs(_signed_area(T, hole))
+ end
return area * sign(s_area)
+end
_area_component(p1, p2) = GI.x(p1) * GI.y(p2) - GI.y(p1) * GI.x(p2)
+
+#= Calculates the signed area of a given curve. This is equivalent to integrating
+to find the area under the curve. Even if curve isn't explicitly closed by
+repeating the first point at the end of the coordinates, curve is still assumed
+to be closed. =#
+function _signed_area(::Type{T}, geom) where T
+ area = zero(T)
+ np = GI.npoint(geom)
+ np == 0 && return area
+
+ first = true
+ local pfirst, p1
for p2 in GI.getpoint(geom)
if first
+ p1 = pfirst = p2
+ first = false
+ continue
+ end
area
area += _area_component(p1, p2)
+ p1 = p2
+ end
p2 = pfirst
+ area += _area_component(p1, p2)
+ return T(area / 2)
+end
Barycentric coordinates
export barycentric_coordinates, barycentric_coordinates!, barycentric_interpolate
+export MeanValue
Example
using GeometryOps
+using GeometryOps.GeometryBasics
+using Makie
+using CairoMakie
+# Define a polygon
+polygon_points = Point3f[
+(0.03, 0.05, 0.00), (0.07, 0.04, 0.02), (0.10, 0.04, 0.04),
+(0.14, 0.04, 0.06), (0.17, 0.07, 0.08), (0.20, 0.09, 0.10),
+(0.22, 0.11, 0.12), (0.25, 0.11, 0.14), (0.27, 0.10, 0.16),
+(0.30, 0.07, 0.18), (0.31, 0.04, 0.20), (0.34, 0.03, 0.22),
+(0.37, 0.02, 0.24), (0.40, 0.03, 0.26), (0.42, 0.04, 0.28),
+(0.44, 0.07, 0.30), (0.45, 0.10, 0.32), (0.46, 0.13, 0.34),
+(0.46, 0.19, 0.36), (0.47, 0.26, 0.38), (0.47, 0.31, 0.40),
+(0.47, 0.35, 0.42), (0.45, 0.37, 0.44), (0.41, 0.38, 0.46),
+(0.38, 0.37, 0.48), (0.35, 0.36, 0.50), (0.32, 0.35, 0.52),
+(0.30, 0.37, 0.54), (0.28, 0.39, 0.56), (0.25, 0.40, 0.58),
+(0.23, 0.39, 0.60), (0.21, 0.37, 0.62), (0.21, 0.34, 0.64),
+(0.23, 0.32, 0.66), (0.24, 0.29, 0.68), (0.27, 0.24, 0.70),
+(0.29, 0.21, 0.72), (0.29, 0.18, 0.74), (0.26, 0.16, 0.76),
+(0.24, 0.17, 0.78), (0.23, 0.19, 0.80), (0.24, 0.22, 0.82),
+(0.24, 0.25, 0.84), (0.21, 0.26, 0.86), (0.17, 0.26, 0.88),
+(0.12, 0.24, 0.90), (0.07, 0.20, 0.92), (0.03, 0.15, 0.94),
+(0.01, 0.10, 0.97), (0.02, 0.07, 1.00)]
+# Plot it!
+# First, we'll plot the polygon using Makie's rendering:
+f, a1, p1 = poly(
+ polygon_points;
+ color = last.(polygon_points),
+ colormap = cgrad(:jet, 18; categorical = true),
+ axis = (;
+ type = Axis, aspect = DataAspect(), title = "Makie mesh based polygon rendering", subtitle = "CairoMakie"
+ ),
+ figure = (; size = (800, 400),)
+)
+hidedecorations!(a1)
+
+ext = GeometryOps.GI.Extent(X = (0, 0.5), Y = (0, 0.42))
+
+a2 = Axis(
+ f[1, 2],
+ aspect = DataAspect(),
+ title = "Barycentric coordinate based polygon rendering", subtitle = "GeometryOps",
+ limits = (ext.X, ext.Y)
+ )
+hidedecorations!(a2)
+
+p2box = poly!( # Now, we plot a cropping rectangle around the axis so we only show the polygon
+ a2,
+ GeometryOps.GeometryBasics.Polygon( # This is a rectangle with an internal hole shaped like the polygon.
+ Point2f[(ext.X[1], ext.Y[1]), (ext.X[2], ext.Y[1]), (ext.X[2], ext.Y[2]), (ext.X[1], ext.Y[2]), (ext.X[1], ext.Y[1])], # exterior
+ [reverse(Point2f.(polygon_points))] # hole
+ ); color = :white, xautolimits = false, yautolimits = false
+)
+cb = Colorbar(f[2, :], p1.plots[1]; vertical = false, flipaxis = true)
+# Finally, we perform barycentric interpolation on a grid,
+xrange = LinRange(ext.X..., 400)
+yrange = LinRange(ext.Y..., 400)
+@time mean_values = barycentric_interpolate.(
+ (MeanValue(),), # The barycentric coordinate algorithm (MeanValue is the only one for now)
+ (Point2f.(polygon_points),), # The polygon points as \`Point2f\`
+ (last.(polygon_points,),), # The values per polygon point - can be anything which supports addition and division
+ Point2f.(xrange, yrange') # The points at which to interpolate
+)
+# and render!
+hm = heatmap!(a2, xrange, yrange, mean_values; colormap = p1.colormap, colorrange = p1.plots[1].colorrange[], xautolimits = false, yautolimits = false)
+translate!(hm, 0, 0, -1) # translate the heatmap behind the cropping polygon!
+f # finally, display the figure
Barycentric-coordinate API
const _VecTypes = Union{Tuple{Vararg{T, N}}, GeometryBasics.StaticArraysCore.StaticArray{Tuple{N}, T, 1}} where {N, T}
+
+"""
+ abstract type AbstractBarycentricCoordinateMethod
+
+Abstract supertype for barycentric coordinate methods.
+The subtypes may serve as dispatch types, or may cache
+some information about the target polygon.
+
+# API
+The following methods must be implemented for all subtypes:
+- \`barycentric_coordinates!(λs::Vector{<: Real}, method::AbstractBarycentricCoordinateMethod, exterior::Vector{<: Point{2, T1}}, point::Point{2, T2})\`
+- \`barycentric_interpolate(method::AbstractBarycentricCoordinateMethod, exterior::Vector{<: Point{2, T1}}, values::Vector{V}, point::Point{2, T2})::V\`
+- \`barycentric_interpolate(method::AbstractBarycentricCoordinateMethod, exterior::Vector{<: Point{2, T1}}, interiors::Vector{<: Vector{<: Point{2, T1}}} values::Vector{V}, point::Point{2, T2})::V\`
+The rest of the methods will be implemented in terms of these, and have efficient dispatches for broadcasting.
+"""
+abstract type AbstractBarycentricCoordinateMethod end
+
+Base.@propagate_inbounds function barycentric_coordinates!(λs::Vector{<: Real}, method::AbstractBarycentricCoordinateMethod, polypoints::AbstractVector{<: Point{N1, T1}}, point::Point{N2, T2}) where {N1, N2, T1 <: Real, T2 <: Real}
+ @boundscheck @assert length(λs) == length(polypoints)
+ @boundscheck @assert length(polypoints) >= 3
+
+ @error("Not implemented yet for method $(method).")
+end
+Base.@propagate_inbounds barycentric_coordinates!(λs::Vector{<: Real}, polypoints::AbstractVector{<: Point{N1, T1}}, point::Point{N2, T2}) where {N1, N2, T1 <: Real, T2 <: Real} = barycentric_coordinates!(λs, MeanValue(), polypoints, point)
"""
+ barycentric_coordinates!(λs::Vector{<: Real}, method::AbstractBarycentricCoordinateMethod, polygon, point)
+
+Loads the barycentric coordinates of \`point\` in \`polygon\` into \`λs\` using the barycentric coordinate method \`method\`.
+
+\`λs\` must be of the length of the polygon plus its holes.
+
+!!! tip
+ Use this method to avoid excess allocations when you need to calculate barycentric coordinates for many points.
+"""
+Base.@propagate_inbounds function barycentric_coordinates!(λs::Vector{<: Real}, method::AbstractBarycentricCoordinateMethod, polygon, point)
+ @assert GeoInterface.trait(polygon) isa GeoInterface.PolygonTrait
+ @assert GeoInterface.trait(point) isa GeoInterface.PointTrait
+ passable_polygon = GeoInterface.convert(GeometryBasics, polygon)
+ @assert passable_polygon isa GeometryBasics.Polygon "The polygon was converted to a $(typeof(passable_polygon)), which is not a \`GeometryBasics.Polygon\`."
+ passable_point = GeoInterface.convert(GeometryBasics, point)
+ return barycentric_coordinates!(λs, method, passable_polygon, Point2(passable_point))
+end
+
+Base.@propagate_inbounds function barycentric_coordinates(method::AbstractBarycentricCoordinateMethod, polypoints::AbstractVector{<: Point{N1, T1}}, point::Point{N2, T2}) where {N1, N2, T1 <: Real, T2 <: Real}
+ λs = zeros(promote_type(T1, T2), length(polypoints))
+ barycentric_coordinates!(λs, method, polypoints, point)
+ return λs
+end
+Base.@propagate_inbounds barycentric_coordinates(polypoints::AbstractVector{<: Point{N1, T1}}, point::Point{N2, T2}) where {N1, N2, T1 <: Real, T2 <: Real} = barycentric_coordinates(MeanValue(), polypoints, point)
"""
+ barycentric_coordinates(method = MeanValue(), polygon, point)
+
+Returns the barycentric coordinates of \`point\` in \`polygon\` using the barycentric coordinate method \`method\`.
+"""
+Base.@propagate_inbounds function barycentric_coordinates(method::AbstractBarycentricCoordinateMethod, polygon, point)
+ @assert GeoInterface.trait(polygon) isa GeoInterface.PolygonTrait
+ @assert GeoInterface.trait(point) isa GeoInterface.PointTrait
+ passable_polygon = GeoInterface.convert(GeometryBasics, polygon)
+ @assert passable_polygon isa GeometryBasics.Polygon "The polygon was converted to a $(typeof(passable_polygon)), which is not a \`GeometryBasics.Polygon\`."
+ passable_point = GeoInterface.convert(GeometryBasics, point)
+ return barycentric_coordinates(method, passable_polygon, Point2(passable_point))
+end
+
+Base.@propagate_inbounds function barycentric_interpolate(method::AbstractBarycentricCoordinateMethod, polypoints::AbstractVector{<: Point{N, T1}}, values::AbstractVector{V}, point::Point{N, T2}) where {N, T1 <: Real, T2 <: Real, V}
+ @boundscheck @assert length(values) == length(polypoints)
+ @boundscheck @assert length(polypoints) >= 3
+ λs = barycentric_coordinates(method, polypoints, point)
+ return sum(λs .* values)
+end
+Base.@propagate_inbounds barycentric_interpolate(polypoints::AbstractVector{<: Point{N, T1}}, values::AbstractVector{V}, point::Point{N, T2}) where {N, T1 <: Real, T2 <: Real, V} = barycentric_interpolate(MeanValue(), polypoints, values, point)
+
+Base.@propagate_inbounds function barycentric_interpolate(method::AbstractBarycentricCoordinateMethod, exterior::AbstractVector{<: Point{N, T1}}, interiors::AbstractVector{<: Point{N, T1}}, values::AbstractVector{V}, point::Point{N, T2}) where {N, T1 <: Real, T2 <: Real, V}
+ @boundscheck @assert length(values) == length(exterior) + isempty(interiors) ? 0 : sum(length.(interiors))
+ @boundscheck @assert length(exterior) >= 3
+ λs = barycentric_coordinates(method, exterior, interiors, point)
+ return sum(λs .* values)
+end
+Base.@propagate_inbounds barycentric_interpolate(exterior::AbstractVector{<: Point{N, T1}}, interiors::AbstractVector{<: Point{N, T1}}, values::AbstractVector{V}, point::Point{N, T2}) where {N, T1 <: Real, T2 <: Real, V} = barycentric_interpolate(MeanValue(), exterior, interiors, values, point)
+
+Base.@propagate_inbounds function barycentric_interpolate(method::AbstractBarycentricCoordinateMethod, polygon::Polygon{2, T1}, values::AbstractVector{V}, point::Point{2, T2}) where {T1 <: Real, T2 <: Real, V}
+ exterior = decompose(Point{2, promote_type(T1, T2)}, polygon.exterior)
+ if isempty(polygon.interiors)
+ @boundscheck @assert length(values) == length(exterior)
+ return barycentric_interpolate(method, exterior, values, point)
+ else # the poly has interiors
+ interiors = reverse.(decompose.((Point{2, promote_type(T1, T2)},), polygon.interiors))
+ @boundscheck @assert length(values) == length(exterior) + sum(length.(interiors))
+ return barycentric_interpolate(method, exterior, interiors, values, point)
+ end
+end
+Base.@propagate_inbounds barycentric_interpolate(polygon::Polygon{2, T1}, values::AbstractVector{V}, point::Point{2, T2}) where {T1 <: Real, T2 <: Real, V} = barycentric_interpolate(MeanValue(), polygon, values, point)
Base.@propagate_inbounds function barycentric_interpolate(method::AbstractBarycentricCoordinateMethod, polygon::Polygon{3, T1}, point::Point{2, T2}) where {T1 <: Real, T2 <: Real}
+ exterior_point3s = decompose(Point{3, promote_type(T1, T2)}, polygon.exterior)
+ exterior_values = getindex.(exterior_point3s, 3)
+ exterior_points = Point2f.(exterior_point3s)
+ if isempty(polygon.interiors)
+ return barycentric_interpolate(method, exterior_points, exterior_values, point)
+ else # the poly has interiors
+ interior_point3s = decompose.((Point{3, promote_type(T1, T2)},), polygon.interiors)
+ interior_values = collect(Iterators.flatten((getindex.(point3s, 3) for point3s in interior_point3s)))
+ interior_points = map(point3s -> Point2f.(point3s), interior_point3s)
+ return barycentric_interpolate(method, exterior_points, interior_points, vcat(exterior_values, interior_values), point)
+ end
+end
+Base.@propagate_inbounds barycentric_interpolate(polygon::Polygon{3, T1}, point::Point{2, T2}) where {T1 <: Real, T2 <: Real} = barycentric_interpolate(MeanValue(), polygon, point)
"""
+ barycentric_interpolate(method = MeanValue(), polygon, values::AbstractVector{V}, point)
+
+Returns the interpolated value at \`point\` within \`polygon\` using the barycentric coordinate method \`method\`.
+\`values\` are the per-point values for the polygon which are to be interpolated.
+
+Returns an object of type \`V\`.
+
+!!! warning
+ Barycentric interpolation is currently defined only for 2-dimensional polygons.
+ If you pass a 3-D polygon in, the Z coordinate will be used as per-vertex value to be interpolated
+ (the M coordinate in GIS parlance).
+"""
+Base.@propagate_inbounds function barycentric_interpolate(method::AbstractBarycentricCoordinateMethod, polygon, values::AbstractVector{V}, point) where V
+ @assert GeoInterface.trait(polygon) isa GeoInterface.PolygonTrait
+ @assert GeoInterface.trait(point) isa GeoInterface.PointTrait
+ passable_polygon = GeoInterface.convert(GeometryBasics, polygon)
+ @assert passable_polygon isa GeometryBasics.Polygon "The polygon was converted to a $(typeof(passable_polygon)), which is not a \`GeometryBasics.Polygon\`."
+ # first_poly_point = GeoInterface.getpoint(GeoInterface.getexterior(polygon))
+ passable_point = GeoInterface.convert(GeometryBasics, point)
+ return barycentric_interpolate(method, passable_polygon, Point2(passable_point))
+end
+Base.@propagate_inbounds barycentric_interpolate(polygon, values::AbstractVector{V}, point) where V = barycentric_interpolate(MeanValue(), polygon, values, point)
+
+"""
+ weighted_mean(weight::Real, x1, x2)
+
+Returns the weighted mean of \`x1\` and \`x2\`, where \`weight\` is the weight of \`x1\`.
+
+Specifically, calculates \`x1 * weight + x2 * (1 - weight)\`.
+
+!!! note
+ The idea for this method is that you can override this for custom types, like Color types, in extension modules.
+"""
+function weighted_mean(weight::WT, x1, x2) where {WT <: Real}
+ return muladd(x1, weight, x2 * (oneunit(WT) - weight))
+end
+
+
+"""
+ MeanValue() <: AbstractBarycentricCoordinateMethod
+
+This method calculates barycentric coordinates using the mean value method.
+
+# References
+
+"""
+struct MeanValue <: AbstractBarycentricCoordinateMethod
+end
"""
+ _det(s1::Point2{T1}, s2::Point2{T2}) where {T1 <: Real, T2 <: Real}
+
+Returns the determinant of the matrix formed by \`hcat\`'ing two points \`s1\` and \`s2\`.
+
+Specifically, this is:
+\`\`\`julia
+s1[1] * s2[2] - s1[2] * s2[1]
+\`\`\`
+"""
+function _det(s1::_VecTypes{2, T1}, s2::_VecTypes{2, T2}) where {T1 <: Real, T2 <: Real}
+ return s1[1] * s2[2] - s1[2] * s2[1]
+end
+
+"""
+ t_value(sᵢ, sᵢ₊₁, rᵢ, rᵢ₊₁)
+
+Returns the "T-value" as described in Hormann's presentation [^HormannPresentation] on how to calculate
+the mean-value coordinate.
+
+Here, \`sᵢ\` is the vector from vertex \`vᵢ\` to the point, and \`rᵢ\` is the norm (length) of \`sᵢ\`.
+\`s\` must be \`Point\` and \`r\` must be real numbers.
+
+\`\`\`math
+tᵢ = \\\\frac{\\\\mathrm{det}\\\\left(sᵢ, sᵢ₊₁\\\\right)}{rᵢ * rᵢ₊₁ + sᵢ ⋅ sᵢ₊₁}
+\`\`\`
+
+[^HormannPresentation]: K. Hormann and N. Sukumar. Generalized Barycentric Coordinates in Computer Graphics and Computational Mechanics. Taylor & Fancis, CRC Press, 2017.
+\`\`\`
+
+"""
+function t_value(sᵢ::_VecTypes{N, T1}, sᵢ₊₁::_VecTypes{N, T1}, rᵢ::T2, rᵢ₊₁::T2) where {N, T1 <: Real, T2 <: Real}
+ return _det(sᵢ, sᵢ₊₁) / muladd(rᵢ, rᵢ₊₁, dot(sᵢ, sᵢ₊₁))
+end
+
+
+function barycentric_coordinates!(λs::Vector{<: Real}, ::MeanValue, polypoints::AbstractVector{<: Point{2, T1}}, point::Point{2, T2}) where {T1 <: Real, T2 <: Real}
+ @boundscheck @assert length(λs) == length(polypoints)
+ @boundscheck @assert length(polypoints) >= 3
+ n_points = length(polypoints)
+ # Initialize counters and register variables
+ # Points - these are actually vectors from point to vertices
+ # polypoints[i-1], polypoints[i], polypoints[i+1]
+ sᵢ₋₁ = polypoints[end] - point
+ sᵢ = polypoints[begin] - point
+ sᵢ₊₁ = polypoints[begin+1] - point
+ # radius / Euclidean distance between points.
+ rᵢ₋₁ = norm(sᵢ₋₁)
+ rᵢ = norm(sᵢ )
+ rᵢ₊₁ = norm(sᵢ₊₁)
+ # Perform the first computation explicitly, so we can cut down on
+ # a mod in the loop.
+ λs[1] = (t_value(sᵢ₋₁, sᵢ, rᵢ₋₁, rᵢ) + t_value(sᵢ, sᵢ₊₁, rᵢ, rᵢ₊₁)) / rᵢ
+ # Loop through the rest of the vertices, compute, store in λs
+ for i in 2:n_points
+ # Increment counters + set variables
+ sᵢ₋₁ = sᵢ
+ sᵢ = sᵢ₊₁
+ sᵢ₊₁ = polypoints[mod1(i+1, n_points)] - point
+ rᵢ₋₁ = rᵢ
+ rᵢ = rᵢ₊₁
+ rᵢ₊₁ = norm(sᵢ₊₁) # radius / Euclidean distance between points.
+ λs[i] = (t_value(sᵢ₋₁, sᵢ, rᵢ₋₁, rᵢ) + t_value(sᵢ, sᵢ₊₁, rᵢ, rᵢ₊₁)) / rᵢ
+ end
+ # Normalize λs to the 1-norm (sum=1)
+ λs ./= sum(λs)
+ return λs
+end
function barycentric_coordinates(::MeanValue, polypoints::NTuple{N, Point{2, T2}}, point::Point{2, T1},) where {N, T1, T2}
+ ## Initialize counters and register variables
+ ## Points - these are actually vectors from point to vertices
+ ## polypoints[i-1], polypoints[i], polypoints[i+1]
+ sᵢ₋₁ = polypoints[end] - point
+ sᵢ = polypoints[begin] - point
+ sᵢ₊₁ = polypoints[begin+1] - point
+ ## radius / Euclidean distance between points.
+ rᵢ₋₁ = norm(sᵢ₋₁)
+ rᵢ = norm(sᵢ )
+ rᵢ₊₁ = norm(sᵢ₊₁)
+ λ₁ = (t_value(sᵢ₋₁, sᵢ, rᵢ₋₁, rᵢ) + t_value(sᵢ, sᵢ₊₁, rᵢ, rᵢ₊₁)) / rᵢ
+ λs = ntuple(N) do i
+ if i == 1
+ return λ₁
+ end
+ ## Increment counters + set variables
+ sᵢ₋₁ = sᵢ
+ sᵢ = sᵢ₊₁
+ sᵢ₊₁ = polypoints[mod1(i+1, N)] - point
+ rᵢ₋₁ = rᵢ
+ rᵢ = rᵢ₊₁
+ rᵢ₊₁ = norm(sᵢ₊₁) # radius / Euclidean distance between points.
+ return (t_value(sᵢ₋₁, sᵢ, rᵢ₋₁, rᵢ) + t_value(sᵢ, sᵢ₊₁, rᵢ, rᵢ₊₁)) / rᵢ
+ end
+
+ ∑λ = sum(λs)
+
+ return ntuple(N) do i
+ λs[i] / ∑λ
+ end
+end
function barycentric_interpolate(::MeanValue, polypoints::AbstractVector{<: Point{2, T1}}, values::AbstractVector{V}, point::Point{2, T2}) where {T1 <: Real, T2 <: Real, V}
+ @boundscheck @assert length(values) == length(polypoints)
+ @boundscheck @assert length(polypoints) >= 3
+
+ n_points = length(polypoints)
+ # Initialize counters and register variables
+ # Points - these are actually vectors from point to vertices
+ # polypoints[i-1], polypoints[i], polypoints[i+1]
+ sᵢ₋₁ = polypoints[end] - point
+ sᵢ = polypoints[begin] - point
+ sᵢ₊₁ = polypoints[begin+1] - point
+ # radius / Euclidean distance between points.
+ rᵢ₋₁ = norm(sᵢ₋₁)
+ rᵢ = norm(sᵢ )
+ rᵢ₊₁ = norm(sᵢ₊₁)
+ # Now, we set the interpolated value to the first point's value, multiplied
+ # by the weight computed relative to the first point in the polygon.
+ wᵢ = (t_value(sᵢ₋₁, sᵢ, rᵢ₋₁, rᵢ) + t_value(sᵢ, sᵢ₊₁, rᵢ, rᵢ₊₁)) / rᵢ
+ wₜₒₜ = wᵢ
+ interpolated_value = values[begin] * wᵢ
+ for i in 2:n_points
+ # Increment counters + set variables
+ sᵢ₋₁ = sᵢ
+ sᵢ = sᵢ₊₁
+ sᵢ₊₁ = polypoints[mod1(i+1, n_points)] - point
+ rᵢ₋₁ = rᵢ
+ rᵢ = rᵢ₊₁
+ rᵢ₊₁ = norm(sᵢ₊₁)
+ # Now, we calculate the weight:
+ wᵢ = (t_value(sᵢ₋₁, sᵢ, rᵢ₋₁, rᵢ) + t_value(sᵢ, sᵢ₊₁, rᵢ, rᵢ₊₁)) / rᵢ
+ # perform a weighted sum with the interpolated value:
+ interpolated_value += values[i] * wᵢ
+ # and add the weight to the total weight accumulator.
+ wₜₒₜ += wᵢ
+ end
+ # Return the normalized interpolated value.
+ return interpolated_value / wₜₒₜ
+end
function barycentric_interpolate(::MeanValue, exterior::AbstractVector{<: Point{N, T1}}, interiors::AbstractVector{<: AbstractVector{<: Point{N, T1}}}, values::AbstractVector{V}, point::Point{N, T2}) where {N, T1 <: Real, T2 <: Real, V}
+ # @boundscheck @assert length(values) == (length(exterior) + isempty(interiors) ? 0 : sum(length.(interiors)))
+ # @boundscheck @assert length(exterior) >= 3
+
+ current_index = 1
+ l_exterior = length(exterior)
+
+ sᵢ₋₁ = exterior[end] - point
+ sᵢ = exterior[begin] - point
+ sᵢ₊₁ = exterior[begin+1] - point
+ rᵢ₋₁ = norm(sᵢ₋₁) # radius / Euclidean distance between points.
+ rᵢ = norm(sᵢ ) # radius / Euclidean distance between points.
+ rᵢ₊₁ = norm(sᵢ₊₁) # radius / Euclidean distance between points.
wᵢ = (t_value(sᵢ₋₁, sᵢ, rᵢ₋₁, rᵢ) + t_value(sᵢ, sᵢ₊₁, rᵢ, rᵢ₊₁)) / rᵢ
+ wₜₒₜ = wᵢ
+ interpolated_value = values[begin] * wᵢ
+
+ for i in 2:l_exterior
sᵢ₋₁ = sᵢ
+ sᵢ = sᵢ₊₁
+ sᵢ₊₁ = exterior[mod1(i+1, l_exterior)] - point
+ rᵢ₋₁ = rᵢ
+ rᵢ = rᵢ₊₁
+ rᵢ₊₁ = norm(sᵢ₊₁) # radius / Euclidean distance between points.
+ wᵢ = (t_value(sᵢ₋₁, sᵢ, rᵢ₋₁, rᵢ) + t_value(sᵢ, sᵢ₊₁, rᵢ, rᵢ₊₁)) / rᵢ
interpolated_value += values[current_index] * wᵢ
wₜₒₜ += wᵢ
+ current_index += 1
+
+ end
+ for hole in interiors
+ l_hole = length(hole)
+ sᵢ₋₁ = hole[end] - point
+ sᵢ = hole[begin] - point
+ sᵢ₊₁ = hole[begin+1] - point
+ rᵢ₋₁ = norm(sᵢ₋₁) # radius / Euclidean distance between points.
+ rᵢ = norm(sᵢ ) # radius / Euclidean distance between points.
+ rᵢ₊₁ = norm(sᵢ₊₁) # radius / Euclidean distance between points.
+ # Now, we set the interpolated value to the first point's value, multiplied
+ # by the weight computed relative to the first point in the polygon.
+ wᵢ = (t_value(sᵢ₋₁, sᵢ, rᵢ₋₁, rᵢ) + t_value(sᵢ, sᵢ₊₁, rᵢ, rᵢ₊₁)) / rᵢ
+
+ interpolated_value += values[current_index] * wᵢ
+
+ wₜₒₜ += wᵢ
+ current_index += 1
+
+ for i in 2:l_hole
+ # Increment counters + set variables
+ sᵢ₋₁ = sᵢ
+ sᵢ = sᵢ₊₁
+ sᵢ₊₁ = hole[mod1(i+1, l_hole)] - point
+ rᵢ₋₁ = rᵢ
+ rᵢ = rᵢ₊₁
+ rᵢ₊₁ = norm(sᵢ₊₁) ## radius / Euclidean distance between points.
+ wᵢ = (t_value(sᵢ₋₁, sᵢ, rᵢ₋₁, rᵢ) + t_value(sᵢ, sᵢ₊₁, rᵢ, rᵢ₊₁)) / rᵢ
+ interpolated_value += values[current_index] * wᵢ
+ wₜₒₜ += wᵢ
+ current_index += 1
+ end
+ end
+ return interpolated_value / wₜₒₜ
+
+end
+
+struct Wachspress <: AbstractBarycentricCoordinateMethod
+end
Centroid
export centroid, centroid_and_length, centroid_and_area
What is the centroid?
import GeometryOps as GO
+import GeoInterface as GI
+using Makie
+using CairoMakie
+
+cshape = GI.Polygon([[(0,0), (0,3), (3,3), (3,2), (1,2), (1,1), (3,1), (3,0), (0,0)]])
+f, a, p = poly(collect(GI.getpoint(cshape)); axis = (; aspect = DataAspect()))
cent = GO.centroid(cshape)
+scatter!(GI.x(cent), GI.y(cent), color = :red)
+f
Implementation
"""
+ centroid(geom, [T=Float64])::Tuple{T, T}
+
+Returns the centroid of a given line segment, linear ring, polygon, or
+mutlipolygon.
+"""
+centroid(geom, ::Type{T} = Float64; threaded=false) where T =
+ centroid(GI.trait(geom), geom, T; threaded)
+function centroid(
+ trait::Union{GI.LineStringTrait, GI.LinearRingTrait}, geom, ::Type{T}=Float64; threaded=false
+) where T
+ centroid_and_length(trait, geom, T)[1]
+end
+centroid(trait, geom, ::Type{T}; threaded=false) where T =
+ centroid_and_area(geom, T; threaded)[1]
+
+"""
+ centroid_and_length(geom, [T=Float64])::(::Tuple{T, T}, ::Real)
+
+Returns the centroid and length of a given line/ring. Note this is only valid
+for line strings and linear rings.
+"""
+centroid_and_length(geom, ::Type{T}=Float64) where T =
+ centroid_and_length(GI.trait(geom), geom, T)
+function centroid_and_length(
+ ::Union{GI.LineStringTrait, GI.LinearRingTrait}, geom, ::Type{T},
+) where T
xcentroid = T(0)
+ ycentroid = T(0)
+ length = T(0)
+ point₁ = GI.getpoint(geom, 1)
for point₂ in GI.getpoint(geom)
length_component = sqrt(
+ (GI.x(point₂) - GI.x(point₁))^2 +
+ (GI.y(point₂) - GI.y(point₁))^2
+ )
length
length += length_component
xcentroid += (GI.x(point₁) + GI.x(point₂)) * (length_component / 2)
+ ycentroid += (GI.y(point₁) + GI.y(point₂)) * (length_component / 2)
+ #centroid = centroid .+ ((point₁ .+ point₂) .* (length_component / 2))
point₁ = point₂
+ end
+ xcentroid /= length
+ ycentroid /= length
+ return (xcentroid, ycentroid), length
+end
+
+"""
+ centroid_and_area(geom, [T=Float64])::(::Tuple{T, T}, ::Real)
+
+Returns the centroid and area of a given geometry.
+"""
+function centroid_and_area(geom, ::Type{T}=Float64; threaded=false) where T
+ target = TraitTarget{Union{GI.PolygonTrait,GI.LineStringTrait,GI.LinearRingTrait}}()
+ init = (zero(T), zero(T)), zero(T)
+ applyreduce(_combine_centroid_and_area, target, geom; threaded, init) do g
+ _centroid_and_area(GI.trait(g), g, T)
+ end
+end
+
+function _centroid_and_area(
+ ::Union{GI.LineStringTrait, GI.LinearRingTrait}, geom, ::Type{T}
+) where T
@assert(
+ GI.getpoint(geom, 1) == GI.getpoint(geom, GI.ngeom(geom)),
+ "centroid_and_area should only be used with closed geometries"
+ )
xcentroid = T(0)
+ ycentroid = T(0)
+ area = T(0)
+ point₁ = GI.getpoint(geom, 1)
for point₂ in GI.getpoint(geom)
+ area_component = GI.x(point₁) * GI.y(point₂) -
+ GI.x(point₂) * GI.y(point₁)
area
area += area_component
xcentroid += (GI.x(point₁) + GI.x(point₂)) * area_component
+ ycentroid += (GI.y(point₁) + GI.y(point₂)) * area_component
point₁ = point₂
+ end
+ area /= 2
+ xcentroid /= 6area
+ ycentroid /= 6area
+ return (xcentroid, ycentroid), abs(area)
+end
+function _centroid_and_area(::GI.PolygonTrait, geom, ::Type{T}) where T
(xcentroid, ycentroid), area = centroid_and_area(GI.getexterior(geom), T)
xcentroid *= area
+ ycentroid *= area
for hole in GI.gethole(geom)
(xinterior, yinterior), interior_area = centroid_and_area(hole, T)
area
area -= interior_area
xcentroid -= xinterior * interior_area
+ ycentroid -= yinterior * interior_area
+ end
+ xcentroid /= area
+ ycentroid /= area
+ return (xcentroid, ycentroid), area
+end
op
argument for _applyreduce and point / area It combines two (point, area) tuples into one, taking the average of the centroid points weighted by the area of the geom they are from.function _combine_centroid_and_area(((x1, y1), area1), ((x2, y2), area2))
+ area = area1 + area2
+ x = (x1 * area1 + x2 * area2) / area
+ y = (y1 * area1 + y2 * area2) / area
+ return (x, y), area
+end
Polygon clipping helpers
@enum PointEdgeSide left=1 right=2 unknown=3
const enter, exit = true, false
+const crossing, bouncing = true, false
+
+#= A point can either be the start or end of an overlapping chain of points between two
+polygons, or not an endpoint of a chain. =#
+@enum EndPointType start_chain=1 end_chain=2 not_endpoint=3
+
+#= This is the struct that makes up a_list and b_list. Many values are only used if point is
+an intersection point (ipt). =#
+@kwdef struct PolyNode{T <: AbstractFloat}
+ point::Tuple{T,T} # (x, y) values of given point
+ inter::Bool = false # If ipt, true, else 0
+ neighbor::Int = 0 # If ipt, index of equivalent point in a_list or b_list, else 0
+ idx::Int = 0 # If crossing point, index within sorted a_idx_list
+ ent_exit::Bool = false # If ipt, true if enter and false if exit, else false
+ crossing::Bool = false # If ipt, true if intersection crosses from out/in polygon, else false
+ endpoint::EndPointType = not_endpoint # If ipt, denotes if point is the start or end of an overlapping chain
+ fracs::Tuple{T,T} = (0., 0.) # If ipt, fractions along edges to ipt (a_frac, b_frac), else (0, 0)
+end
+
+#= Create a new node with all of the same field values as the given PolyNode unless
+alternative values are provided, in which case those should be used. =#
+PolyNode(node::PolyNode{T};
+ point = node.point, inter = node.inter, neighbor = node.neighbor, idx = node.idx,
+ ent_exit = node.ent_exit, crossing = node.crossing, endpoint = node.endpoint,
+ fracs = node.fracs,
+) where T = PolyNode{T}(;
+ point = point, inter = inter, neighbor = neighbor, idx = idx, ent_exit = ent_exit,
+ crossing = crossing, endpoint = endpoint, fracs = fracs)
equals(pn1::PolyNode, pn2::PolyNode) = pn1.point == pn2.point && pn1.inter == pn2.inter && pn1.fracs == pn2.fracs
_build_ab_list(::Type{T}, poly_a, poly_b, delay_cross_f, delay_bounce_f; exact) ->
+ (a_list, b_list, a_idx_list)
function _build_ab_list(::Type{T}, poly_a, poly_b, delay_cross_f::F1, delay_bounce_f::F2; exact) where {T, F1, F2}
a_list, a_idx_list, n_b_intrs = _build_a_list(T, poly_a, poly_b; exact)
+ b_list = _build_b_list(T, a_idx_list, a_list, n_b_intrs, poly_b)
_classify_crossing!(T, a_list, b_list; exact)
_flag_ent_exit!(T, GI.LinearRingTrait(), poly_b, a_list, delay_cross_f, Base.Fix2(delay_bounce_f, true); exact)
+ _flag_ent_exit!(T, GI.LinearRingTrait(), poly_a, b_list, delay_cross_f, Base.Fix2(delay_bounce_f, false); exact)
_index_crossing_intrs!(a_list, b_list, a_idx_list)
+
+ return a_list, b_list, a_idx_list
+end
_build_a_list(::Type{T}, poly_a, poly_b) -> (a_list, a_idx_list)
function _build_a_list(::Type{T}, poly_a, poly_b; exact) where T
+ n_a_edges = _nedge(poly_a)
+ a_list = PolyNode{T}[] # list of points in poly_a
+ sizehint!(a_list, n_a_edges)
+ a_idx_list = Vector{Int}() # finds indices of intersection points in a_list
+ a_count = 0 # number of points added to a_list
+ n_b_intrs = 0
local a_pt1
+ for (i, a_p2) in enumerate(GI.getpoint(poly_a))
+ a_pt2 = (T(GI.x(a_p2)), T(GI.y(a_p2)))
+ if i <= 1 || (a_pt1 == a_pt2) # don't repeat points
+ a_pt1 = a_pt2
+ continue
+ end
new_point = PolyNode{T}(;point = a_pt1)
+ a_count += 1
+ push!(a_list, new_point)
local b_pt1
+ prev_counter = a_count
+ for (j, b_p2) in enumerate(GI.getpoint(poly_b))
+ b_pt2 = _tuple_point(b_p2, T)
+ if j <= 1 || (b_pt1 == b_pt2) # don't repeat points
+ b_pt1 = b_pt2
+ continue
+ end
line_orient, intr1, intr2 = _intersection_point(T, (a_pt1, a_pt2), (b_pt1, b_pt2); exact)
+ if line_orient != line_out # edges intersect
+ if line_orient == line_cross # Intersection point that isn't a vertex
+ int_pt, fracs = intr1
+ new_intr = PolyNode{T}(;
+ point = int_pt, inter = true, neighbor = j - 1,
+ crossing = true, fracs = fracs,
+ )
+ a_count += 1
+ n_b_intrs += 1
+ push!(a_list, new_intr)
+ push!(a_idx_list, a_count)
+ else
+ (_, (α1, β1)) = intr1
add_a1 = α1 == 0 && 0 ≤ β1 < 1
+ a1_β = add_a1 ? β1 : zero(T)
+ add_b1 = β1 == 0 && 0 < α1 < 1
+ b1_α = add_b1 ? α1 : zero(T)
if line_orient == line_over
+ (_, (α2, β2)) = intr2
+ if α2 == 0 && 0 ≤ β2 < 1
+ add_a1, a1_β = true, β2
+ end
+ if β2 == 0 && 0 < α2 < 1
+ add_b1, b1_α = true, α2
+ end
+ end
if add_a1
+ n_b_intrs += a1_β == 0 ? 0 : 1
+ a_list[prev_counter] = PolyNode{T}(;
+ point = a_pt1, inter = true, neighbor = j - 1,
+ fracs = (zero(T), a1_β),
+ )
+ push!(a_idx_list, prev_counter)
+ end
+ if add_b1
+ new_intr = PolyNode{T}(;
+ point = b_pt1, inter = true, neighbor = j - 1,
+ fracs = (b1_α, zero(T)),
+ )
+ a_count += 1
+ push!(a_list, new_intr)
+ push!(a_idx_list, a_count)
+ end
+ end
+ end
+ b_pt1 = b_pt2
+ end
if prev_counter < a_count
+ Δintrs = a_count - prev_counter
+ inter_points = @view a_list[(a_count - Δintrs + 1):a_count]
+ sort!(inter_points, by = x -> x.fracs[1])
+ end
+ a_pt1 = a_pt2
+ end
+ return a_list, a_idx_list, n_b_intrs
+end
_build_b_list(::Type{T}, a_idx_list, a_list, poly_b) -> b_list
function _build_b_list(::Type{T}, a_idx_list, a_list, n_b_intrs, poly_b) where T
sort!(a_idx_list, by = x-> a_list[x].neighbor + a_list[x].fracs[2])
n_b_edges = _nedge(poly_b)
+ n_intr_pts = length(a_idx_list)
+ b_list = PolyNode{T}[]
+ sizehint!(b_list, n_b_edges + n_b_intrs)
+ intr_curr = 1
+ b_count = 0
local b_pt1
+ for (i, b_p2) in enumerate(GI.getpoint(poly_b))
+ b_pt2 = _tuple_point(b_p2, T)
+ if i ≤ 1 || (b_pt1 == b_pt2) # don't repeat points
+ b_pt1 = b_pt2
+ continue
+ end
+ b_count += 1
+ push!(b_list, PolyNode{T}(; point = b_pt1))
+ if intr_curr ≤ n_intr_pts
+ curr_idx = a_idx_list[intr_curr]
+ curr_node = a_list[curr_idx]
+ prev_counter = b_count
+ while curr_node.neighbor == i - 1 # Add all intersection points on current edge
+ b_idx = 0
+ new_intr = PolyNode(curr_node; neighbor = curr_idx)
+ if curr_node.fracs[2] == 0 # if curr_node is segment start point
b_idx = prev_counter
+ b_list[b_idx] = new_intr
+ else
+ b_count += 1
+ b_idx = b_count
+ push!(b_list, new_intr)
+ end
+ a_list[curr_idx] = PolyNode(curr_node; neighbor = b_idx)
+ intr_curr += 1
+ intr_curr > n_intr_pts && break
+ curr_idx = a_idx_list[intr_curr]
+ curr_node = a_list[curr_idx]
+ end
+ end
+ b_pt1 = b_pt2
+ end
+ sort!(a_idx_list) # return a_idx_list to order of points in a_list
+ return b_list
+end
_classify_crossing!(T, poly_b, a_list; exact)
function _classify_crossing!(::Type{T}, a_list, b_list; exact) where T
+ napts = length(a_list)
+ nbpts = length(b_list)
a_prev = a_list[end - 1]
+ curr_pt = a_list[end]
+ i = napts
start_chain_edge, start_chain_idx = unknown, 0
+ unmatched_end_chain_edge, unmatched_end_chain_idx = unknown, 0
+ same_winding = true
for next_idx in 1:napts
+ a_next = a_list[next_idx]
+ if curr_pt.inter && !curr_pt.crossing
+ j = curr_pt.neighbor
+ b_prev = j == 1 ? b_list[end] : b_list[j-1]
+ b_next = j == nbpts ? b_list[1] : b_list[j+1]
a_prev_is_b_prev = a_prev.inter && equals(a_prev, b_prev)
+ a_prev_is_b_next = a_prev.inter && equals(a_prev, b_next)
+ a_next_is_b_prev = a_next.inter && equals(a_next, b_prev)
+ a_next_is_b_next = a_next.inter && equals(a_next, b_next)
b_prev_side, b_next_side = _get_sides(b_prev, b_next, a_prev, curr_pt, a_next,
+ i, j, a_list, b_list; exact)
if !a_prev_is_b_prev && !a_prev_is_b_next && !a_next_is_b_prev && !a_next_is_b_next
+ if b_prev_side != b_next_side # lines cross
+ a_list[i] = PolyNode(curr_pt; crossing = true)
+ b_list[j] = PolyNode(b_list[j]; crossing = true)
+ end
elseif !a_next_is_b_prev && !a_next_is_b_next
+ b_side = a_prev_is_b_prev ? b_next_side : b_prev_side
+ if start_chain_edge == unknown # start loop on overlapping chain
+ unmatched_end_chain_edge = b_side
+ unmatched_end_chain_idx = i
+ same_winding = a_prev_is_b_prev
+ else # close overlapping chain
crossing = b_side != start_chain_edge
+ a_list[i] = PolyNode(curr_pt;
+ crossing = crossing,
+ endpoint = end_chain,
+ )
+ b_list[j] = PolyNode(b_list[j];
+ crossing = crossing,
+ endpoint = same_winding ? end_chain : start_chain,
+ )
start_pt = a_list[start_chain_idx]
+ a_list[start_chain_idx] = PolyNode(start_pt;
+ crossing = crossing,
+ endpoint = start_chain,
+ )
+ b_list[start_pt.neighbor] = PolyNode(b_list[start_pt.neighbor];
+ crossing = crossing,
+ endpoint = same_winding ? start_chain : end_chain,
+ )
+ end
elseif !a_prev_is_b_prev && !a_prev_is_b_next
+ b_side = a_next_is_b_prev ? b_next_side : b_prev_side
+ start_chain_edge = b_side
+ start_chain_idx = i
+ same_winding = a_next_is_b_next
+ end
+ end
+ a_prev = curr_pt
+ curr_pt = a_next
+ i = next_idx
+ end
if unmatched_end_chain_edge != unknown
+ crossing = unmatched_end_chain_edge != start_chain_edge
end_chain_pt = a_list[unmatched_end_chain_idx]
+ a_list[unmatched_end_chain_idx] = PolyNode(end_chain_pt;
+ crossing = crossing,
+ endpoint = end_chain,
+ )
+ b_list[end_chain_pt.neighbor] = PolyNode(b_list[end_chain_pt.neighbor];
+ crossing = crossing,
+ endpoint = same_winding ? end_chain : start_chain,
+ )
start_pt = a_list[start_chain_idx]
+ a_list[start_chain_idx] = PolyNode(start_pt;
+ crossing = crossing,
+ endpoint = start_chain,
+ )
+ b_list[start_pt.neighbor] = PolyNode(b_list[start_pt.neighbor];
+ crossing = crossing,
+ endpoint = same_winding ? start_chain : end_chain,
+ )
+ end
+end
_is_vertex(pt) = !pt.inter || pt.fracs[1] == 0 || pt.fracs[1] == 1 || pt.fracs[2] == 0 || pt.fracs[2] == 1
+
+#= Determines which side (right or left) of the segment a_prev-curr_pt-a_next the points
+b_prev and b_next are on. Given this is only called when curr_pt is an intersection point
+that wasn't initially classified as crossing, we know that curr_pt is either from a hinge or
+overlapping intersection and thus is an original vertex of either poly_a or poly_b. Due to
+floating point error when calculating new intersection points, we only want to use original
+vertices to determine orientation. Thus, for other points, find nearest point that is a
+vertex. Given other intersection points will be collinear along existing segments, this
+won't change the orientation. =#
+function _get_sides(b_prev, b_next, a_prev, curr_pt, a_next, i, j, a_list, b_list; exact)
+ b_prev_pt = if _is_vertex(b_prev)
+ b_prev.point
+ else # Find original start point of segment formed by b_prev and curr_pt
+ prev_idx = findprev(_is_vertex, b_list, j - 1)
+ prev_idx = isnothing(prev_idx) ? findlast(_is_vertex, b_list) : prev_idx
+ b_list[prev_idx].point
+ end
+ b_next_pt = if _is_vertex(b_next)
+ b_next.point
+ else # Find original end point of segment formed by curr_pt and b_next
+ next_idx = findnext(_is_vertex, b_list, j + 1)
+ next_idx = isnothing(next_idx) ? findfirst(_is_vertex, b_list) : next_idx
+ b_list[next_idx].point
+ end
+ a_prev_pt = if _is_vertex(a_prev)
+ a_prev.point
+ else # Find original start point of segment formed by a_prev and curr_pt
+ prev_idx = findprev(_is_vertex, a_list, i - 1)
+ prev_idx = isnothing(prev_idx) ? findlast(_is_vertex, a_list) : prev_idx
+ a_list[prev_idx].point
+ end
+ a_next_pt = if _is_vertex(a_next)
+ a_next.point
+ else # Find original end point of segment formed by curr_pt and a_next
+ next_idx = findnext(_is_vertex, a_list, i + 1)
+ next_idx = isnothing(next_idx) ? findfirst(_is_vertex, a_list) : next_idx
+ a_list[next_idx].point
+ end
b_prev_side = _get_side(b_prev_pt, a_prev_pt, curr_pt.point, a_next_pt; exact)
+ b_next_side = _get_side(b_next_pt, a_prev_pt, curr_pt.point, a_next_pt; exact)
+ return b_prev_side, b_next_side
+end
function _get_side(Q, P1, P2, P3; exact)
+ s1 = Predicates.orient(Q, P1, P2; exact)
+ s2 = Predicates.orient(Q, P2, P3; exact)
+ s3 = Predicates.orient(P1, P2, P3; exact)
+
+ side = if s3 ≥ 0
+ (s1 < 0) || (s2 < 0) ? right : left
+ else # s3 < 0
+ (s1 > 0) || (s2 > 0) ? left : right
+ end
+ return side
+end
+
+#= Given a list of PolyNodes, find the first element that isn't an intersection point. Then,
+test if this element is in or out of the given polygon. Return the next index, as well as
+the enter/exit status of the next intersection point (the opposite of the in/out check). If
+all points are intersection points, find the first element that either is the end of a chain
+or a crossing point that isn't in a chain. Then take the midpoint of this point and the next
+point in the list and perform the in/out check. If none of these points exist, return
+a \`next_idx\` of \`nothing\`. =#
+function _pt_off_edge_status(::Type{T}, pt_list, poly, npts; exact) where T
+ start_idx, is_non_intr_pt = findfirst(_is_not_intr, pt_list), true
+ if isnothing(start_idx)
+ start_idx, is_non_intr_pt = findfirst(_next_edge_off, pt_list), false
+ isnothing(start_idx) && return (start_idx, false)
+ end
+ next_idx = start_idx < npts ? (start_idx + 1) : 1
+ start_pt = if is_non_intr_pt
+ pt_list[start_idx].point
+ else
+ (pt_list[start_idx].point .+ pt_list[next_idx].point) ./ 2
+ end
+ start_status = !_point_filled_curve_orientation(start_pt, poly; in = true, on = false, out = false, exact)
+ return next_idx, start_status
+end
_is_not_intr(pt) = !pt.inter
+#= Check if a PolyNode is the last point of a chain or a non-overlapping crossing point.
+The next midpoint of one of these points and the next point within a polygon must not be on
+the polygon edge. =#
+_next_edge_off(pt) = (pt.endpoint == end_chain) || (pt.crossing && pt.endpoint == not_endpoint)
_flag_ent_exit!(::Type{T}, ::GI.LinearRingTrait, poly, pt_list, delay_cross_f, delay_bounce_f; exact)
function _flag_ent_exit!(::Type{T}, ::GI.LinearRingTrait, poly, pt_list, delay_cross_f, delay_bounce_f; exact) where T
+ npts = length(pt_list)
next_idx, status = _pt_off_edge_status(T, pt_list, poly, npts; exact)
+ isnothing(next_idx) && return
+ start_idx = next_idx - 1
start_chain_idx = 0
+ for ii in Iterators.flatten((next_idx:npts, 1:start_idx))
+ curr_pt = pt_list[ii]
+ if curr_pt.endpoint == start_chain
+ start_chain_idx = ii
+ elseif curr_pt.crossing || curr_pt.endpoint == end_chain
+ start_crossing, end_crossing = curr_pt.crossing, curr_pt.crossing
+ if curr_pt.endpoint == end_chain # ending overlapping chain
+ start_pt = pt_list[start_chain_idx]
+ if curr_pt.crossing # delayed crossing
+ #= start and end crossing status are different and depend on current
+ entry/exit status =#
+ start_crossing, end_crossing = delay_cross_f(status)
+ else # delayed bouncing
+ next_idx = ii < npts ? (ii + 1) : 1
+ next_val = (curr_pt.point .+ pt_list[next_idx].point) ./ 2
+ pt_in_poly = _point_filled_curve_orientation(next_val, poly; in = true, on = false, out = false, exact)
+ #= start and end crossing status are the same and depend on if adjacent
+ edges of pt_list are within poly =#
+ start_crossing = delay_bounce_f(pt_in_poly)
+ end_crossing = start_crossing
+ end
pt_list[start_chain_idx] = PolyNode(start_pt; ent_exit = status, crossing = start_crossing)
+ if !curr_pt.crossing
+ status = !status
+ end
+ end
+ pt_list[ii] = PolyNode(curr_pt; ent_exit = status, crossing = end_crossing)
+ status = !status
+ end
+ end
+ return
+end
_flag_ent_exit!(::GI.LineTrait, line, pt_list; exact)
function _flag_ent_exit!(::GI.LineTrait, poly, pt_list; exact)
+ status = !_point_filled_curve_orientation(pt_list[1].point, poly; in = true, on = false, out = false, exact)
for (ii, curr_pt) in enumerate(pt_list)
+ if curr_pt.crossing
+ pt_list[ii] = PolyNode(curr_pt; ent_exit = status)
+ status = !status
+ end
+ end
+ return
+end
+
+#= Filters a_idx_list to just include crossing points and sets the index of all crossing
+points (which element they correspond to within a_idx_list). =#
+function _index_crossing_intrs!(a_list, b_list, a_idx_list)
+ filter!(x -> a_list[x].crossing, a_idx_list)
+ for (i, a_idx) in enumerate(a_idx_list)
+ curr_node = a_list[a_idx]
+ neighbor_node = b_list[curr_node.neighbor]
+ a_list[a_idx] = PolyNode(curr_node; idx = i)
+ b_list[curr_node.neighbor] = PolyNode(neighbor_node; idx = i)
+ end
+ return
+end
_trace_polynodes(::Type{T}, a_list, b_list, a_idx_list, f_step)::Vector{GI.Polygon}
poly_a
and poly_b
are temporary inputs used for debugging and can be removed eventually.function _trace_polynodes(::Type{T}, a_list, b_list, a_idx_list, f_step, poly_a, poly_b) where T
+ n_a_pts, n_b_pts = length(a_list), length(b_list)
+ total_pts = n_a_pts + n_b_pts
+ n_cross_pts = length(a_idx_list)
+ return_polys = Vector{_get_poly_type(T)}(undef, 0)
visited_pts = 0
+ processed_pts = 0
+ first_idx = 1
+ while processed_pts < n_cross_pts
+ curr_list, curr_npoints = a_list, n_a_pts
+ on_a_list = true
visited_pts += 1
+ processed_pts += 1
+ first_idx = findnext(x -> x != 0, a_idx_list, first_idx)
+ idx = a_idx_list[first_idx]
+ a_idx_list[first_idx] = 0
+ start_pt = a_list[idx]
curr = curr_list[idx]
+ pt_list = [curr.point]
+
+ curr_not_start = true
+ while curr_not_start
+ step = f_step(curr.ent_exit, on_a_list)
same_status, prev_status = true, curr.ent_exit
+ while same_status
+ @assert visited_pts < total_pts "Clipping tracing hit every point - clipping error. Please open an issue with polygons: $(GI.coordinates(poly_a)) and $(GI.coordinates(poly_b))."
idx += step
+ idx = (idx > curr_npoints) ? mod(idx, curr_npoints) : idx
+ idx = (idx == 0) ? curr_npoints : idx
curr = curr_list[idx]
+ push!(pt_list, curr.point)
+ if (curr.crossing || curr.endpoint != not_endpoint)
same_status = curr.ent_exit == prev_status
+ curr_not_start = curr != start_pt && curr != b_list[start_pt.neighbor]
+ !curr_not_start && break
+ if (on_a_list && curr.crossing) || (!on_a_list && a_list[curr.neighbor].crossing)
+ processed_pts += 1
+ a_idx_list[curr.idx] = 0
+ end
+ end
+ visited_pts += 1
+ end
curr_list, curr_npoints = on_a_list ? (b_list, n_b_pts) : (a_list, n_a_pts)
+ on_a_list = !on_a_list
+ idx = curr.neighbor
+ curr = curr_list[idx]
+ end
+ push!(return_polys, GI.Polygon([pt_list]))
+ end
+ return return_polys
+end
_get_poly_type(::Type{T}) where T =
+ GI.Polygon{false, false, Vector{GI.LinearRing{false, false, Vector{Tuple{T, T}}, Nothing, Nothing}}, Nothing, Nothing}
_find_non_cross_orientation(a_list, b_list, a_poly, b_poly; exact)
function _find_non_cross_orientation(a_list, b_list, a_poly, b_poly; exact)
+ non_intr_a_idx = findfirst(x -> !x.inter, a_list)
+ non_intr_b_idx = findfirst(x -> !x.inter, b_list)
+ #= Determine if non-intersection point is in or outside of polygon - if there isn't A
+ non-intersection point, then all points are on the polygon edge =#
+ a_pt_orient = isnothing(non_intr_a_idx) ? point_on :
+ _point_filled_curve_orientation(a_list[non_intr_a_idx].point, b_poly; exact)
+ b_pt_orient = isnothing(non_intr_b_idx) ? point_on :
+ _point_filled_curve_orientation(b_list[non_intr_b_idx].point, a_poly; exact)
+ a_in_b = a_pt_orient != point_out && b_pt_orient != point_in
+ b_in_a = b_pt_orient != point_out && a_pt_orient != point_in
+ return a_in_b, b_in_a
+end
_add_holes_to_polys!(::Type{T}, return_polys, hole_iterator, remove_poly_idx; exact)
function _add_holes_to_polys!(::Type{T}, return_polys, hole_iterator, remove_poly_idx; exact) where T
+ n_polys = length(return_polys)
+ remove_hole_idx = Int[]
for i in 1:n_polys
+ n_new_per_poly = 0
+ for curr_hole in Iterators.map(tuples, hole_iterator) # loop through all holes
for j in Iterators.flatten((i:i, (n_polys + 1):(n_polys + n_new_per_poly)))
+ curr_poly = return_polys[j]
+ remove_poly_idx[j] && continue
+ curr_poly_ext = GI.nhole(curr_poly) > 0 ? GI.Polygon(StaticArrays.SVector(GI.getexterior(curr_poly))) : curr_poly
+ in_ext, on_ext, out_ext = _line_polygon_interactions(curr_hole, curr_poly_ext; exact, closed_line = true)
+ if in_ext # hole is at least partially within the polygon's exterior
+ new_hole, new_hole_poly, n_new_pieces = _combine_holes!(T, curr_hole, curr_poly, return_polys, remove_hole_idx)
+ if n_new_pieces > 0
+ append!(remove_poly_idx, falses(n_new_pieces))
+ n_new_per_poly += n_new_pieces
+ end
+ if !on_ext && !out_ext # hole is completly within exterior
+ push!(curr_poly.geom, new_hole)
+ else # hole is partially within and outside of polygon's exterior
+ new_polys = difference(curr_poly_ext, new_hole_poly, T; target=GI.PolygonTrait())
+ n_new_polys = length(new_polys) - 1
curr_poly.geom[1] = GI.getexterior(new_polys[1])
+ append!(curr_poly.geom, GI.gethole(new_polys[1]))
+ if n_new_polys > 0 # add any extra pieces
+ append!(return_polys, @view new_polys[2:end])
+ append!(remove_poly_idx, falses(n_new_polys))
+ n_new_per_poly += n_new_polys
+ end
+ end
elseif coveredby(curr_poly_ext, GI.Polygon(StaticArrays.SVector(curr_hole)))
+ remove_poly_idx[j] = true
+ end
+ end
+ end
+ n_polys += n_new_per_poly
+ end
deleteat!(return_polys, remove_poly_idx)
+ return
+end
_combine_holes!(::Type{T}, new_hole, curr_poly, return_polys)
function _combine_holes!(::Type{T}, new_hole, curr_poly, return_polys, remove_hole_idx) where T
+ n_new_polys = 0
+ empty!(remove_hole_idx)
+ new_hole_poly = GI.Polygon(StaticArrays.SVector(new_hole))
for (k, old_hole) in enumerate(GI.gethole(curr_poly))
+ old_hole_poly = GI.Polygon(StaticArrays.SVector(old_hole))
+ if intersects(new_hole_poly, old_hole_poly)
hole_union = union(new_hole_poly, old_hole_poly, T; target = GI.PolygonTrait())[1]
+ push!(remove_hole_idx, k + 1)
+ new_hole = GI.getexterior(hole_union)
+ new_hole_poly = GI.Polygon(StaticArrays.SVector(new_hole))
+ n_pieces = GI.nhole(hole_union)
+ if n_pieces > 0 # if the hole has a hole, then this is a new polygon piece!
+ append!(return_polys, [GI.Polygon([h]) for h in GI.gethole(hole_union)])
+ n_new_polys += n_pieces
+ end
+ end
+ end
deleteat!(curr_poly.geom, remove_hole_idx)
+ empty!(remove_hole_idx)
@views for piece in return_polys[end - n_new_polys + 1:end]
+ for (k, old_hole) in enumerate(GI.gethole(curr_poly))
+ if !(k in remove_hole_idx) && within(old_hole, piece)
+ push!(remove_hole_idx, k + 1)
+ push!(piece.geom, old_hole)
+ end
+ end
+ end
+ deleteat!(curr_poly.geom, remove_hole_idx)
+ return new_hole, new_hole_poly, n_new_polys
+end
+
+#= Remove collinear edge points, other than the first and last edge vertex, to simplify
+polygon - including both the exterior ring and any holes=#
+function _remove_collinear_points!(polys, remove_idx, poly_a, poly_b)
+ for (i, poly) in Iterators.reverse(enumerate(polys))
+ for (j, ring) in Iterators.reverse(enumerate(GI.getring(poly)))
+ n = length(ring.geom)
resize!(remove_idx, n)
+ fill!(remove_idx, false)
+ local p1, p2
+ for (i, p) in enumerate(ring.geom)
+ if i == 1
+ p1 = p
+ continue
+ elseif i == 2
+ p2 = p
+ continue
+ else
+ p3 = p
if Predicates.orient(p1, p2, p3; exact = _False()) == 0
+ remove_idx[i - 1] = true
+ end
+ end
+ p1, p2 = p2, p3
+ end
if Predicates.orient(ring.geom[end - 1], ring.geom[1], ring.geom[2]; exact = _False()) == 0
+ remove_idx[1], remove_idx[end] = true, true
+ end
deleteat!(ring.geom, remove_idx)
if length(ring.geom) ≤ (remove_idx[1] ? 2 : 3)
+ if j == 1
+ deleteat!(polys, i)
+ break
+ else
+ deleteat!(poly.geom, j)
+ continue
+ end
+ end
+ if remove_idx[1] # make sure the last point is repeated
+ push!(ring.geom, ring.geom[1])
+ end
+ end
+ end
+ return
+end
export coverage
What is coverage?
import GeometryOps as GO
+import GeoInterface as GI
+using Makie
+using CairoMakie
+
+rect = GI.Polygon([[(-1,0), (-1,1), (1,1), (1,0), (-1,0)]])
+cell = GI.Polygon([[(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)]])
+xmin, xmax, ymin, ymax = 0, 2, 0, 2
+f, a, p = poly(collect(GI.getpoint(cell)); axis = (; aspect = DataAspect()))
+poly!(collect(GI.getpoint(rect)))
+f
GO.coverage(rect, xmin, xmax, ymin, ymax)
1.0
Implementation
const _COVERAGE_TARGETS = TraitTarget{Union{GI.PolygonTrait,GI.AbstractCurveTrait,GI.MultiPointTrait,GI.PointTrait}}()
const UNKNOWN, NORTH, EAST, SOUTH, WEST = 0:4
+
+"""
+ coverage(geom, xmin, xmax, ymin, ymax, [T = Float64])::T
+
+Returns the area of intersection between given geometry and grid cell defined by its minimum
+and maximum x and y-values. This is computed differently for different geometries:
+
+- The signed area of a point is always zero.
+- The signed area of a curve is always zero.
+- The signed area of a polygon is calculated by tracing along its edges and switching to the
+ cell edges if needed.
+- The coverage of a geometry collection, multi-geometry, feature collection of
+ array/iterable is the sum of the coverages of all of the sub-geometries.
+
+Result will be of type T, where T is an optional argument with a default value
+of Float64.
+"""
+function coverage(geom, xmin, xmax, ymin, ymax,::Type{T} = Float64; threaded=false) where T <: AbstractFloat
+ applyreduce(+, _COVERAGE_TARGETS, geom; threaded, init=zero(T)) do g
+ _coverage(T, GI.trait(g), g, T(xmin), T(xmax), T(ymin), T(ymax))
+ end
+end
+
+function coverage(geom, cell_ext::Extents.Extent, ::Type{T} = Float64; threaded=false) where T <: AbstractFloat
+ (xmin, xmax), (ymin, ymax) = values(cell_ext)
+ return coverage(geom, xmin, xmax, ymin, ymax, T; threaded = threaded)
+end
_coverage(::Type{T}, ::GI.AbstractGeometryTrait, geom, xmin, xmax, ymin, ymax; kwargs...) where T = zero(T)
function _coverage(::Type{T}, ::GI.PolygonTrait, poly, xmin, xmax, ymin, ymax; exact = _False()) where T
+ GI.isempty(poly) && return zero(T)
+ cov_area = _coverage(T, GI.getexterior(poly), xmin, xmax, ymin, ymax; exact)
+ cov_area == 0 && return cov_area
for hole in GI.gethole(poly)
+ cov_area -= _coverage(T, hole, xmin, xmax, ymin, ymax; exact)
+ end
+ return cov_area
+end
+
+#= Calculates the area of the filled ring within the cell defined by corners with (xmin, ymin),
+(xmin, ymax), (xmax, ymax), and (xmax, ymin). =#
+function _coverage(::Type{T}, ring, xmin, xmax, ymin, ymax; exact) where T
+ cov_area = zero(T)
+ unmatched_out_wall, unmatched_out_point = UNKNOWN, (zero(T), zero(T))
+ unmatched_in_wall, unmatched_in_point = unmatched_out_wall, unmatched_out_point
start_idx = 1
+ for (i, p) in enumerate(GI.getpoint(ring))
+ if !_point_in_cell(p, xmin, xmax, ymin, ymax)
+ start_idx = i
+ break
+ end
+ end
+ ring_cw = isclockwise(ring)
+ p1 = _tuple_point(GI.getpoint(ring, start_idx), T)
point_idx = ring_cw ? Iterators.flatten((start_idx + 1:GI.npoint(ring), 1:start_idx)) :
+ Iterators.flatten((start_idx - 1:-1:1, GI.npoint(ring):-1:start_idx))
+ for i in point_idx
+ p2 = _tuple_point(GI.getpoint(ring, i), T)
p1_in_cell = _point_in_cell(p1, xmin, xmax, ymin, ymax)
+ p2_in_cell = _point_in_cell(p2, xmin, xmax, ymin, ymax)
if p1_in_cell && p2_in_cell
+ cov_area += _area_component(p1, p2)
+ p1 = p2
+ continue
+ end
inter1, inter2 = _line_intersect_cell(T, p1, p2, xmin, xmax, ymin, ymax)
(start_wall, start_point), (end_wall, end_point) =
+ if p1_in_cell
+ ((UNKNOWN, p1), inter1)
+ elseif p2_in_cell
+ (inter1, (UNKNOWN, p2))
+ else
+ i1_to_p1 = _squared_euclid_distance(T, inter1[2], p1)
+ i2_to_p1 = _squared_euclid_distance(T, inter2[2], p1)
+ i1_to_p1 < i2_to_p1 ? (inter1, inter2) : (inter2, inter1)
+ end
cov_area += _area_component(start_point, end_point)
+
+ if start_wall != UNKNOWN # p1 out of cell
+ if unmatched_out_wall == UNKNOWN
+ unmatched_in_point = start_point
+ unmatched_in_wall = start_wall
+ else
+ check_point = find_point_on_cell(unmatched_out_point, start_point,
+ unmatched_out_wall, start_wall,xmin, xmax, ymin, ymax)
+ if _point_filled_curve_orientation(check_point, ring; in = true, on = false, out = false, exact)
+ cov_area += connect_edges(T, unmatched_out_point, start_point,
+ unmatched_out_wall, start_wall,xmin, xmax, ymin, ymax)
+ else
+ cov_area += connect_edges(T, unmatched_out_point, unmatched_in_point,
+ unmatched_out_wall, unmatched_in_wall,xmin, xmax, ymin, ymax)
+ unmatched_out_wall == UNKNOWN
+ end
+ end
+ end
+ if end_wall != UNKNOWN # p2 out of cell
+ unmatched_out_wall, unmatched_out_point = end_wall, end_point
+ end
+ p1 = p2
+ end
if unmatched_in_wall != UNKNOWN
+ cov_area += connect_edges(T, unmatched_out_point, unmatched_in_point,
+ unmatched_out_wall, unmatched_in_wall,xmin, xmax, ymin, ymax)
+ end
+ cov_area = abs(cov_area) / 2
if cov_area == 0
+ if _point_filled_curve_orientation((xmin, ymin), ring; in = true, on = true, out = false, exact)
+ cov_area = abs((xmax - xmin) * (ymax - ymin))
+ end
+ end
+ return cov_area
+end
_point_in_cell(p, xmin, xmax, ymin, ymax) = xmin <= GI.x(p) <= xmax && ymin <= GI.y(p) <= ymax
_between(b, a, c) = a ≤ b < c || c ≤ b < a
+
+#= Determine intersections of the line from (x1, y1) to (x2, y2) with the bounding box
+defined by the minimum and maximum x/y values. Since we are dealing with a single line
+segment, we know that there is at maximum two intersection points.
+
+For each intersection point that we find, return the wall that it passes through, as well as
+the intersection point itself as a a tuple. If an intersection point isn't found, return the
+wall as UNKNOWN and the point as a pair of zeros. =#
+function _line_intersect_cell(::Type{T}, (x1, y1), (x2, y2), xmin, xmax, ymin, ymax) where T
+ Δx, Δy = x2 - x1, y2 - y1
+ inter1 = (UNKNOWN, (zero(T), zero(T)))
+ inter2 = inter1
+ if Δx == 0 # If line is vertical, only consider north and south
+ if xmin ≤ x1 ≤ xmax
+ inter1 = _between(ymax, y1, y2) ? (NORTH, (x1, ymax)) : inter1
+ inter2 = _between(ymin, y1, y2) ? (SOUTH, (x1, ymin)) : inter2
+ end
+ elseif Δy == 0 # If line is horizontal, only consider east and west
+ if ymin ≤ y1 ≤ ymax
+ inter1 = _between(xmax, x1, x2) ? (EAST, (xmax, y1)) : inter1
+ inter2 = _between(xmin, x1, x2) ? (WEST, (xmin, y1)) : inter2
+ end
+ else # Line is tilted, must consider all edges, but only two can intersect
+ m = Δy / Δx
+ b = y1 - m * x1
xn = (ymax - b) / m
+ if xmin ≤ xn ≤ xmax && _between(xn, x1, x2) && _between(ymax, y1, y2)
+ inter1 = (NORTH, (xn, ymax))
+ end
+ xs = (ymin - b) / m
+ if xmin ≤ xs ≤ xmax && _between(xs, x1, x2) && _between(ymin, y1, y2)
+ new_intr = (SOUTH, (xs, ymin))
+ (inter1[1] == UNKNOWN) ? (inter1 = new_intr) : (inter2 = new_intr)
+ end
+ ye = m * xmax + b
+ if ymin ≤ ye ≤ ymax && _between(ye, y1, y2) && _between(xmax, x1, x2)
+ new_intr = (EAST, (xmax, ye))
+ (inter1[1] == UNKNOWN) ? (inter1 = new_intr) : (inter2 = new_intr)
+ end
+ yw = m * xmin + b
+ if ymin ≤ yw ≤ ymax && _between(yw, y1, y2) && _between(xmin, x1, x2)
+ new_intr = (WEST, (xmin, yw))
+ (inter1[1] == UNKNOWN) ? (inter1 = new_intr) : (inter2 = new_intr)
+ end
+ end
+ if inter1[1] == UNKNOWN # first intersection must be known, if one exists
+ inter1, inter2 = inter2, inter1
+ end
+ return inter1, inter2
+end
function find_point_on_cell(p1, p2, wall1, wall2, xmin, xmax, ymin, ymax)
+ x1, y1 = p1
+ x2, y2 = p2
+ mid_point = if wall1 == wall2 && _is_clockwise_from(p1, p2, wall1)
+ (x1 + x2) / 2, (y1 + y2) / 2
+ elseif wall1 == NORTH
+ (xmax, ymax)
+ elseif wall1 == EAST
+ (xmax, ymin)
+ elseif wall1 == SOUTH
+ (xmin, ymin)
+ else
+ (xmin, ymax)
+ end
+ return mid_point
+end
+
+#= Area component of shoelace formula coming from the distance between point 1 and point 2
+along grid cell walls in between the two points. =#
+function connect_edges(::Type{T}, p1, p2, wall1, wall2, xmin, xmax, ymin, ymax) where {T}
+ connect_area = zero(T)
+ if wall1 == wall2 && _is_clockwise_from(p1, p2, wall1)
+ connect_area += _area_component(p1, p2)
+ else
connect_area += _partial_edge_out_area(p1, xmin, xmax, ymin, ymax, wall1)
next_wall, last_wall = wall1 + 1, wall2 - 1
+ if wall2 > wall1
+ for wall in next_wall:last_wall
+ connect_area += _full_edge_area(xmin, xmax, ymin, ymax, wall)
+ end
+ else
+ for wall in Iterators.flatten((next_wall:WEST, NORTH:last_wall))
+ connect_area += _full_edge_area(xmin, xmax, ymin, ymax, wall)
+ end
+ end
connect_area += _partial_edge_in_area(p2, xmin, xmax, ymin, ymax, wall2)
+ end
+ return connect_area
+end
_is_clockwise_from((x1, y1), (x2, y2), wall) = (wall == NORTH && x2 > x1) ||
+ (wall == EAST && y2 < y1) || (wall == SOUTH && x2 < x1) || (wall == WEST && y2 > y1)
+
+#= Returns the area component of a full edge of the bounding box defined by the min and max
+values and the wall. =#
+_full_edge_area(xmin, xmax, ymin, ymax, wall) = if wall == NORTH
+ ymax * (xmin - xmax)
+ elseif wall == EAST
+ xmax * (ymin - ymax)
+ elseif wall == SOUTH
+ ymin * (xmax - xmin)
+ else
+ xmin * (ymax - ymin)
+ end
+
+#= Returns the area component of part of one wall, from its "starting corner" (going
+clockwise) to the point (x2, y2). =#
+function _partial_edge_in_area((x2, y2), xmin, xmax, ymin, ymax, wall)
+ x_wall = (wall == NORTH || wall == WEST) ? xmin : xmax
+ y_wall = (wall == NORTH || wall == EAST) ? ymax : ymin
+ return x_wall * y2 - x2 * y_wall
+end
+
+#= Returns the area component of part of one wall, from the point (x1, y1) to its
+"ending corner" (going clockwise). =#
+function _partial_edge_out_area((x1, y1), xmin, xmax, ymin, ymax, wall)
+ x_wall = (wall == NORTH || wall == EAST) ? xmax : xmin
+ y_wall = (wall == NORTH || wall == WEST) ? ymax : ymin
+ return x1 * y_wall - x_wall * y1
+end
Polygon cutting
export cut
What is cut?
cutpolygon
function.import GeoInterface as GI, GeometryOps as GO
+using CairoMakie
+using Makie
+
+poly = GI.Polygon([[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0), (0.0, 0.0)]])
+line = GI.Line([(5.0, -5.0), (5.0, 15.0)])
+cut_polys = GO.cut(poly, line)
+
+f, a, p1 = Makie.poly(collect(GI.getpoint(cut_polys[1])); color = (:blue, 0.5))
+Makie.poly!(collect(GI.getpoint(cut_polys[2])); color = (:orange, 0.5))
+Makie.lines!(GI.getpoint(line); color = :black)
+f
Implementation
"""
+ cut(geom, line, [T::Type])
+
+Return given geom cut by given line as a list of geometries of the same type as the input
+geom. Return the original geometry as only list element if none are found. Line must cut
+fully through given geometry or the original geometry will be returned.
+
+Note: This currently doesn't work for degenerate cases there line crosses through vertices.
+
+# Example
+
+\`\`\`jldoctest
+import GeoInterface as GI, GeometryOps as GO
+
+poly = GI.Polygon([[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0), (0.0, 0.0)]])
+line = GI.Line([(5.0, -5.0), (5.0, 15.0)])
+cut_polys = GO.cut(poly, line)
+GI.coordinates.(cut_polys)
2-element Vector{Vector{Vector{Vector{Float64}}}}:
+ [[[0.0, 0.0], [5.0, 0.0], [5.0, 10.0], [0.0, 10.0], [0.0, 0.0]]]
+ [[[5.0, 0.0], [10.0, 0.0], [10.0, 10.0], [5.0, 10.0], [5.0, 0.0]]]
+\`\`\`
+"""
+cut(geom, line, ::Type{T} = Float64) where {T <: AbstractFloat} =
+ _cut(T, GI.trait(geom), geom, GI.trait(line), line; exact = _True())
+
+#= Cut a given polygon by given line. Add polygon holes back into resulting pieces if there
+are any holes. =#
+function _cut(::Type{T}, ::GI.PolygonTrait, poly, ::GI.LineTrait, line; exact) where T
+ ext_poly = GI.getexterior(poly)
+ poly_list, intr_list = _build_a_list(T, ext_poly, line; exact)
+ n_intr_pts = length(intr_list)
if n_intr_pts < 2 || isodd(n_intr_pts)
+ return [tuples(poly)]
+ end
cut_coords = _cut(T, ext_poly, line, poly_list, intr_list, n_intr_pts; exact)
for c in cut_coords
+ push!(c, c[1])
+ end
+ cut_polys = [GI.Polygon([c]) for c in cut_coords]
remove_idx = falses(length(cut_polys))
+ _add_holes_to_polys!(T, cut_polys, GI.gethole(poly), remove_idx; exact)
+ return cut_polys
+end
function _cut(::Type{T}, trait::GI.AbstractTrait, geom, line; kwargs...) where T
+ @assert(
+ false,
+ "Cutting of $trait isn't implemented yet.",
+ )
+ return nothing
+end
+
+#= Cutting algorithm inspired by Greiner and Hormann clipping algorithm. Returns coordinates
+of cut geometry in Vector{Vector{Tuple}} format.
+
+Note: degenerate cases where intersection points are vertices do not work right now. =#
+function _cut(::Type{T}, geom, line, geom_list, intr_list, n_intr_pts; exact) where T
sort!(intr_list, by = x -> geom_list[x].fracs[2])
+ _flag_ent_exit!(GI.LineTrait(), line, geom_list; exact)
return_coords = [[geom_list[1].point]]
+ cross_backs = [(T(Inf),T(Inf))]
+ poly_idx = 1
+ n_polys = 1
for (pt_idx, curr) in enumerate(geom_list)
+ if pt_idx > 1
+ push!(return_coords[poly_idx], curr.point)
+ end
+ if curr.inter
intr_idx = findfirst(x -> equals(curr.point, geom_list[x].point), intr_list)
+ cross_idx = intr_idx + (curr.ent_exit ? 1 : -1)
+ cross_idx = cross_idx < 1 ? n_intr_pts : cross_idx
+ cross_idx = cross_idx > n_intr_pts ? 1 : cross_idx
+ cross_backs[poly_idx] = geom_list[intr_list[cross_idx]].point
next_poly_idx = findfirst(x -> equals(x, curr.point), cross_backs)
+ if isnothing(next_poly_idx)
+ push!(return_coords, [curr.point])
+ push!(cross_backs, curr.point)
+ n_polys += 1
+ poly_idx = n_polys
+ else
+ push!(return_coords[next_poly_idx], curr.point)
+ poly_idx = next_poly_idx
+ end
+ end
+ end
+ return return_coords
+end
Difference Polygon Clipping
export difference
+
+
+"""
+ difference(geom_a, geom_b, [T::Type]; target::Type, fix_multipoly = UnionIntersectingPolygons())
+
+Return the difference between two geometries as a list of geometries. Return an empty list
+if none are found. The type of the list will be constrained as much as possible given the
+input geometries. Furthermore, the user can provide a \`taget\` type as a keyword argument and
+a list of target geometries found in the difference will be returned. The user can also
+provide a float type that they would like the points of returned geometries to be. If the
+user is taking a intersection involving one or more multipolygons, and the multipolygon
+might be comprised of polygons that intersect, if \`fix_multipoly\` is set to an
+\`IntersectingPolygons\` correction (the default is \`UnionIntersectingPolygons()\`), then the
+needed multipolygons will be fixed to be valid before performing the intersection to ensure
+a correct answer. Only set \`fix_multipoly\` to false if you know that the multipolygons are
+valid, as it will avoid unneeded computation.
+
+# Example
+
+\`\`\`jldoctest
+import GeoInterface as GI, GeometryOps as GO
+
+poly1 = GI.Polygon([[[0.0, 0.0], [5.0, 5.0], [10.0, 0.0], [5.0, -5.0], [0.0, 0.0]]])
+poly2 = GI.Polygon([[[3.0, 0.0], [8.0, 5.0], [13.0, 0.0], [8.0, -5.0], [3.0, 0.0]]])
+diff_poly = GO.difference(poly1, poly2; target = GI.PolygonTrait())
+GI.coordinates.(diff_poly)
1-element Vector{Vector{Vector{Vector{Float64}}}}:
+ [[[6.5, 3.5], [5.0, 5.0], [0.0, 0.0], [5.0, -5.0], [6.5, -3.5], [3.0, 0.0], [6.5, 3.5]]]
+\`\`\`
+"""
+function difference(
+ geom_a, geom_b, ::Type{T} = Float64; target=nothing, kwargs...,
+) where {T<:AbstractFloat}
+ return _difference(
+ TraitTarget(target), T, GI.trait(geom_a), geom_a, GI.trait(geom_b), geom_b;
+ exact = _True(), kwargs...,
+ )
+end
+
+#= The 'difference' function returns the difference of two polygons as a list of polygons.
+The algorithm to determine the difference was adapted from "Efficient clipping of efficient
+polygons," by Greiner and Hormann (1998). DOI: https://doi.org/10.1145/274363.274364 =#
+function _difference(
+ ::TraitTarget{GI.PolygonTrait}, ::Type{T},
+ ::GI.PolygonTrait, poly_a,
+ ::GI.PolygonTrait, poly_b;
+ exact, kwargs...
+) where T
ext_a = GI.getexterior(poly_a)
+ ext_b = GI.getexterior(poly_b)
a_list, b_list, a_idx_list = _build_ab_list(T, ext_a, ext_b, _diff_delay_cross_f, _diff_delay_bounce_f; exact)
+ polys = _trace_polynodes(T, a_list, b_list, a_idx_list, _diff_step, poly_a, poly_b)
if isempty(polys)
+ a_in_b, b_in_a = _find_non_cross_orientation(a_list, b_list, ext_a, ext_b; exact)
if b_in_a && !a_in_b # b in a and can't be the same polygon
+ poly_a_b_hole = GI.Polygon([tuples(ext_a), tuples(ext_b)])
+ push!(polys, poly_a_b_hole)
+ elseif !b_in_a && !a_in_b # polygons don't intersect
+ push!(polys, tuples(poly_a))
+ return polys
+ end
+ end
+ remove_idx = falses(length(polys))
if GI.nhole(poly_a) != 0
+ _add_holes_to_polys!(T, polys, GI.gethole(poly_a), remove_idx; exact)
+ end
+ if GI.nhole(poly_b) != 0
+ for hole in GI.gethole(poly_b)
+ hole_poly = GI.Polygon(StaticArrays.SVector(hole))
+ new_polys = intersection(hole_poly, poly_a, T; target = GI.PolygonTrait)
+ if length(new_polys) > 0
+ append!(polys, new_polys)
+ end
+ end
+ end
_remove_collinear_points!(polys, remove_idx, poly_a, poly_b)
+ return polys
+end
Helper functions for Differences with Greiner and Hormann Polygon Clipping
#= When marking the crossing status of a delayed crossing, the chain start point is crossing
+when the start point is a entry point and is a bouncing point when the start point is an
+exit point. The end of the chain has the opposite crossing / bouncing status. =#
+_diff_delay_cross_f(x) = (x, !x)
+#= When marking the crossing status of a delayed bouncing, the chain start and end points
+are crossing if the current polygon's adjacent edges are within the non-tracing polygon and
+we are tracing b_list or if the edges are outside and we are on a_list. Otherwise the
+endpoints are marked as crossing. x is a boolean representing if the edges are inside or
+outside of the polygon and y is a variable that is true if we are on a_list and false if we
+are on b_list. =#
+_diff_delay_bounce_f(x, y) = x ⊻ y
+#= When tracing polygons, step forwards if the most recent intersection point was an entry
+point and we are currently tracing b_list or if it was an exit point and we are currently
+tracing a_list, else step backwards, where x is the entry/exit status and y is a variable
+that is true if we are on a_list and false if we are on b_list. =#
+_diff_step(x, y) = (x ⊻ y) ? 1 : (-1)
+
+#= Polygon with multipolygon difference - note that all intersection regions between
+\`poly_a\` and any of the sub-polygons of \`multipoly_b\` are removed from \`poly_a\`. =#
+function _difference(
+ target::TraitTarget{GI.PolygonTrait}, ::Type{T},
+ ::GI.PolygonTrait, poly_a,
+ ::GI.MultiPolygonTrait, multipoly_b;
+ kwargs...,
+) where T
+ polys = [tuples(poly_a, T)]
+ for poly_b in GI.getpolygon(multipoly_b)
+ isempty(polys) && break
+ polys = mapreduce(p -> difference(p, poly_b; target), append!, polys)
+ end
+ return polys
+end
+
+#= Multipolygon with polygon difference - note that all intersection regions between
+sub-polygons of \`multipoly_a\` and \`poly_b\` will be removed from the corresponding
+sub-polygon. Unless specified with \`fix_multipoly = nothing\`, \`multipolygon_a\` will be
+validated using the given (default is \`UnionIntersectingPolygons()\`) correction. =#
+function _difference(
+ target::TraitTarget{GI.PolygonTrait}, ::Type{T},
+ ::GI.MultiPolygonTrait, multipoly_a,
+ ::GI.PolygonTrait, poly_b;
+ fix_multipoly = UnionIntersectingPolygons(), kwargs...,
+) where T
+ if !isnothing(fix_multipoly) # Fix multipoly_a to prevent returning an invalid multipolygon
+ multipoly_a = fix_multipoly(multipoly_a)
+ end
+ polys = Vector{_get_poly_type(T)}()
+ sizehint!(polys, GI.npolygon(multipoly_a))
+ for poly_a in GI.getpolygon(multipoly_a)
+ append!(polys, difference(poly_a, poly_b; target))
+ end
+ return polys
+end
+
+#= Multipolygon with multipolygon difference - note that all intersection regions between
+sub-polygons of \`multipoly_a\` and sub-polygons of \`multipoly_b\` will be removed from the
+corresponding sub-polygon of \`multipoly_a\`. Unless specified with \`fix_multipoly = nothing\`,
+\`multipolygon_a\` will be validated using the given (defauly is \`UnionIntersectingPolygons()\`)
+correction. =#
+function _difference(
+ target::TraitTarget{GI.PolygonTrait}, ::Type{T},
+ ::GI.MultiPolygonTrait, multipoly_a,
+ ::GI.MultiPolygonTrait, multipoly_b;
+ fix_multipoly = UnionIntersectingPolygons(), kwargs...,
+) where T
+ if !isnothing(fix_multipoly) # Fix multipoly_a to prevent returning an invalid multipolygon
+ multipoly_a = fix_multipoly(multipoly_a)
+ fix_multipoly = nothing
+ end
+ local polys
+ for (i, poly_b) in enumerate(GI.getpolygon(multipoly_b))
+ polys = difference(i == 1 ? multipoly_a : GI.MultiPolygon(polys), poly_b; target, fix_multipoly)
+ end
+ return polys
+end
function _difference(
+ ::TraitTarget{Target}, ::Type{T},
+ trait_a::GI.AbstractTrait, geom_a,
+ trait_b::GI.AbstractTrait, geom_b,
+) where {Target, T}
+ @assert(
+ false,
+ "Difference between $trait_a and $trait_b with target $Target isn't implemented yet.",
+ )
+ return nothing
+end
Geometry Intersection
export intersection, intersection_points
+
+"""
+ Enum LineOrientation
+Enum for the orientation of a line with respect to a curve. A line can be
+\`line_cross\` (crossing over the curve), \`line_hinge\` (crossing the endpoint of the curve),
+\`line_over\` (colinear with the curve), or \`line_out\` (not interacting with the curve).
+"""
+@enum LineOrientation line_cross=1 line_hinge=2 line_over=3 line_out=4
+
+"""
+ intersection(geom_a, geom_b, [T::Type]; target::Type, fix_multipoly = UnionIntersectingPolygons())
+
+Return the intersection between two geometries as a list of geometries. Return an empty list
+if none are found. The type of the list will be constrained as much as possible given the
+input geometries. Furthermore, the user can provide a \`target\` type as a keyword argument and
+a list of target geometries found in the intersection will be returned. The user can also
+provide a float type that they would like the points of returned geometries to be. If the
+user is taking a intersection involving one or more multipolygons, and the multipolygon
+might be comprised of polygons that intersect, if \`fix_multipoly\` is set to an
+\`IntersectingPolygons\` correction (the default is \`UnionIntersectingPolygons()\`), then the
+needed multipolygons will be fixed to be valid before performing the intersection to ensure
+a correct answer. Only set \`fix_multipoly\` to nothing if you know that the multipolygons are
+valid, as it will avoid unneeded computation.
+
+# Example
+
+\`\`\`jldoctest
+import GeoInterface as GI, GeometryOps as GO
+
+line1 = GI.Line([(124.584961,-12.768946), (126.738281,-17.224758)])
+line2 = GI.Line([(123.354492,-15.961329), (127.22168,-14.008696)])
+inter_points = GO.intersection(line1, line2; target = GI.PointTrait())
+GI.coordinates.(inter_points)
1-element Vector{Vector{Float64}}:
+ [125.58375366067548, -14.83572303404496]
+\`\`\`
+"""
+function intersection(
+ geom_a, geom_b, ::Type{T}=Float64; target=nothing, kwargs...,
+) where {T<:AbstractFloat}
+ return _intersection(
+ TraitTarget(target), T, GI.trait(geom_a), geom_a, GI.trait(geom_b), geom_b;
+ exact = _True(), kwargs...,
+ )
+end
_intersection(
+ ::TraitTarget{GI.PointTrait}, ::Type{T},
+ trait_a::Union{GI.LineTrait, GI.LineStringTrait, GI.LinearRingTrait}, geom_a,
+ trait_b::Union{GI.LineTrait, GI.LineStringTrait, GI.LinearRingTrait}, geom_b;
+ kwargs...,
+) where T = _intersection_points(T, trait_a, geom_a, trait_b, geom_b)
+
+#= Polygon-Polygon Intersections with target Polygon
+The algorithm to determine the intersection was adapted from "Efficient clipping
+of efficient polygons," by Greiner and Hormann (1998).
+DOI: https://doi.org/10.1145/274363.274364 =#
+function _intersection(
+ ::TraitTarget{GI.PolygonTrait}, ::Type{T},
+ ::GI.PolygonTrait, poly_a,
+ ::GI.PolygonTrait, poly_b;
+ exact, kwargs...,
+) where {T}
ext_a = GI.getexterior(poly_a)
+ ext_b = GI.getexterior(poly_b)
a_list, b_list, a_idx_list = _build_ab_list(T, ext_a, ext_b, _inter_delay_cross_f, _inter_delay_bounce_f; exact)
+ polys = _trace_polynodes(T, a_list, b_list, a_idx_list, _inter_step, poly_a, poly_b)
+ if isempty(polys) # no crossing points, determine if either poly is inside the other
+ a_in_b, b_in_a = _find_non_cross_orientation(a_list, b_list, ext_a, ext_b; exact)
+ if a_in_b
+ push!(polys, GI.Polygon([tuples(ext_a)]))
+ elseif b_in_a
+ push!(polys, GI.Polygon([tuples(ext_b)]))
+ end
+ end
+ remove_idx = falses(length(polys))
if GI.nhole(poly_a) != 0 || GI.nhole(poly_b) != 0
+ hole_iterator = Iterators.flatten((GI.gethole(poly_a), GI.gethole(poly_b)))
+ _add_holes_to_polys!(T, polys, hole_iterator, remove_idx; exact)
+ end
_remove_collinear_points!(polys, remove_idx, poly_a, poly_b)
+ return polys
+end
Helper functions for Intersections with Greiner and Hormann Polygon Clipping
#= When marking the crossing status of a delayed crossing, the chain start point is bouncing
+when the start point is a entry point and is a crossing point when the start point is an
+exit point. The end of the chain has the opposite crossing / bouncing status. x is the
+entry/exit status. =#
+_inter_delay_cross_f(x) = (!x, x)
+#= When marking the crossing status of a delayed bouncing, the chain start and end points
+are crossing if the current polygon's adjacent edges are within the non-tracing polygon. If
+the edges are outside then the chain endpoints are marked as bouncing. x is a boolean
+representing if the edges are inside or outside of the polygon. =#
+_inter_delay_bounce_f(x, _) = x
+#= When tracing polygons, step forward if the most recent intersection point was an entry
+point, else step backwards where x is the entry/exit status. =#
+_inter_step(x, _) = x ? 1 : (-1)
+
+#= Polygon with multipolygon intersection - note that all intersection regions between
+\`poly_a\` and any of the sub-polygons of \`multipoly_b\` are counted as intersection polygons.
+Unless specified with \`fix_multipoly = nothing\`, \`multipolygon_b\` will be validated using
+the given (default is \`UnionIntersectingPolygons()\`) correction. =#
+function _intersection(
+ target::TraitTarget{GI.PolygonTrait}, ::Type{T},
+ ::GI.PolygonTrait, poly_a,
+ ::GI.MultiPolygonTrait, multipoly_b;
+ fix_multipoly = UnionIntersectingPolygons(), kwargs...,
+) where T
+ if !isnothing(fix_multipoly) # Fix multipoly_b to prevent duplicated intersection regions
+ multipoly_b = fix_multipoly(multipoly_b)
+ end
+ polys = Vector{_get_poly_type(T)}()
+ for poly_b in GI.getpolygon(multipoly_b)
+ append!(polys, intersection(poly_a, poly_b; target))
+ end
+ return polys
+end
+
+#= Multipolygon with polygon intersection is equivalent to taking the intersection of the
+poylgon with the multipolygon and thus simply switches the order of operations and calls the
+above method. =#
+_intersection(
+ target::TraitTarget{GI.PolygonTrait}, ::Type{T},
+ ::GI.MultiPolygonTrait, multipoly_a,
+ ::GI.PolygonTrait, poly_b;
+ kwargs...,
+) where T = intersection(poly_b, multipoly_a; target , kwargs...)
+
+#= Multipolygon with multipolygon intersection - note that all intersection regions between
+any sub-polygons of \`multipoly_a\` and any of the sub-polygons of \`multipoly_b\` are counted
+as intersection polygons. Unless specified with \`fix_multipoly = nothing\`, both
+\`multipolygon_a\` and \`multipolygon_b\` will be validated using the given (default is
+\`UnionIntersectingPolygons()\`) correction. =#
+function _intersection(
+ target::TraitTarget{GI.PolygonTrait}, ::Type{T},
+ ::GI.MultiPolygonTrait, multipoly_a,
+ ::GI.MultiPolygonTrait, multipoly_b;
+ fix_multipoly = UnionIntersectingPolygons(), kwargs...,
+) where T
+ if !isnothing(fix_multipoly) # Fix both multipolygons to prevent duplicated regions
+ multipoly_a = fix_multipoly(multipoly_a)
+ multipoly_b = fix_multipoly(multipoly_b)
+ fix_multipoly = nothing
+ end
+ polys = Vector{_get_poly_type(T)}()
+ for poly_a in GI.getpolygon(multipoly_a)
+ append!(polys, intersection(poly_a, multipoly_b; target, fix_multipoly))
+ end
+ return polys
+end
function _intersection(
+ ::TraitTarget{Target}, ::Type{T},
+ trait_a::GI.AbstractTrait, geom_a,
+ trait_b::GI.AbstractTrait, geom_b;
+ kwargs...,
+) where {Target, T}
+ @assert(
+ false,
+ "Intersection between $trait_a and $trait_b with target $Target isn't implemented yet.",
+ )
+ return nothing
+end
+
+"""
+ intersection_points(
+ geom_a,
+ geom_b,
+ )::Union{
+ ::Vector{::Tuple{::Real, ::Real}},
+ ::Nothing,
+ }
+
+Return a list of intersection points between two geometries of type GI.Point.
+If no intersection point was possible given geometry extents, returns an empty
+list.
+"""
+intersection_points(geom_a, geom_b, ::Type{T} = Float64) where T <: AbstractFloat =
+ _intersection_points(T, GI.trait(geom_a), geom_a, GI.trait(geom_b), geom_b)
+
+
+#= Calculates the list of intersection points between two geometries, inlcuding line
+segments, line strings, linear rings, polygons, and multipolygons. If no intersection points
+were possible given geometry extents or if none are found, return an empty list of
+GI.Points. =#
+function _intersection_points(::Type{T}, ::GI.AbstractTrait, a, ::GI.AbstractTrait, b; exact = _False()) where T
result = GI.Point[]
Extents.intersects(GI.extent(a), GI.extent(b)) || return result
edges_a, edges_b = map(sort! ∘ to_edges, (a, b))
+ npoints_a, npoints_b = length(edges_a), length(edges_b)
+ a_closed = npoints_a > 1 && edges_a[1][1] == edges_a[end][1]
+ b_closed = npoints_b > 1 && edges_b[1][1] == edges_b[end][1]
+ if npoints_a > 0 && npoints_b > 0
for i in eachindex(edges_a), j in eachindex(edges_b)
+ line_orient, intr1, _ = _intersection_point(T, edges_a[i], edges_b[j]; exact)
if line_orient == line_cross || line_orient == line_hinge
+ #=
+ Determine if point is on edge (all edge endpoints excluded
+ except for the last edge for an open geometry)
+ =#
+ point, (α, β) = intr1
+ on_a_edge = (!a_closed && i == npoints_a && 0 <= α <= 1) ||
+ (0 <= α < 1)
+ on_b_edge = (!b_closed && j == npoints_b && 0 <= β <= 1) ||
+ (0 <= β < 1)
+ if on_a_edge && on_b_edge
+ push!(result, GI.Point(point))
+ end
+ end
+ end
+ end
+ return result
+end
+
+#= Calculates the intersection points between two lines if they exists and the fractional
+component of each line from the initial end point to the intersection point where α is the
+fraction along (a1, a2) and β is the fraction along (b1, b2).
+
+Note that the first return is the type of intersection (line_cross, line_hinge, line_over,
+or line_out). The type of intersection determines how many intersection points there are.
+If the intersection is line_out, then there are no intersection points and the two
+intersections aren't valid and shouldn't be used. If the intersection is line_cross or
+line_hinge then the lines meet at one point and the first intersection is valid, while the
+second isn't. Finally, if the intersection is line_over, then both points are valid and they
+are the two points that define the endpoints of the overlapping region between the two
+lines.
+
+Also note again that each intersection is a tuple of two tuples. The first is the
+intersection point (x,y) while the second is the ratio along the initial lines (α, β) for
+that point.
+
+Calculation derivation can be found here: https://stackoverflow.com/questions/563198/ =#
+function _intersection_point(::Type{T}, (a1, a2)::Edge, (b1, b2)::Edge; exact) where T
line_orient = line_out
+ intr1 = ((zero(T), zero(T)), (zero(T), zero(T)))
+ intr2 = intr1
+ no_intr_result = (line_orient, intr1, intr2)
(a1x, a1y), (a2x, a2y) = _tuple_point(a1, T), _tuple_point(a2, T)
+ (b1x, b1y), (b2x, b2y) = _tuple_point(b1, T), _tuple_point(b2, T)
a_ext = Extent(X = minmax(a1x, a2x), Y = minmax(a1y, a2y))
+ b_ext = Extent(X = minmax(b1x, b2x), Y = minmax(b1y, b2y))
+ !Extents.intersects(a_ext, b_ext) && return no_intr_result
a1_orient = Predicates.orient(b1, b2, a1; exact)
+ a2_orient = Predicates.orient(b1, b2, a2; exact)
+ a1_orient != 0 && a1_orient == a2_orient && return no_intr_result # α < 0 or α > 1
+ b1_orient = Predicates.orient(a1, a2, b1; exact)
+ b2_orient = Predicates.orient(a1, a2, b2; exact)
+ b1_orient != 0 && b1_orient == b2_orient && return no_intr_result # β < 0 or β > 1
if a1_orient == a2_orient == b1_orient == b2_orient == 0
line_orient, intr1, intr2 = _find_collinear_intersection(T, a1, a2, b1, b2, a_ext, b_ext, no_intr_result)
+ elseif a1_orient == 0 || a2_orient == 0 || b1_orient == 0 || b2_orient == 0
line_orient = line_hinge
+ intr1 = _find_hinge_intersection(T, a1, a2, b1, b2, a1_orient, a2_orient, b1_orient)
+ else
line_orient = line_cross
+ intr1 = _find_cross_intersection(T, a1, a2, b1, b2, a_ext, b_ext)
+ end
+ return line_orient, intr1, intr2
+end
+
+#= If lines defined by (a1, a2) and (b1, b2) are collinear, find endpoints of overlapping
+region if they exist. This could result in three possibilities. First, there could be no
+overlapping region, in which case, the default 'no_intr_result' intersection information is
+returned. Second, the two regions could just meet at one shared endpoint, in which case it
+is a hinge intersection with one intersection point. Otherwise, it is a overlapping
+intersection defined by two of the endpoints of the line segments. =#
+function _find_collinear_intersection(::Type{T}, a1, a2, b1, b2, a_ext, b_ext, no_intr_result) where T
line_orient, intr1, intr2 = no_intr_result
a1_in_b = _point_in_extent(a1, b_ext)
+ a2_in_b = _point_in_extent(a2, b_ext)
+ b1_in_a = _point_in_extent(b1, a_ext)
+ b2_in_a = _point_in_extent(b2, a_ext)
a_dist, b_dist = distance(a1, a2, T), distance(b1, b2, T)
if a1_in_b && a2_in_b # 1st vertex of a and 2nd vertex of a form overlap
+ line_orient = line_over
+ β1 = _clamped_frac(distance(a1, b1, T), b_dist)
+ β2 = _clamped_frac(distance(a2, b1, T), b_dist)
+ intr1 = (_tuple_point(a1, T), (zero(T), β1))
+ intr2 = (_tuple_point(a2, T), (one(T), β2))
+ elseif b1_in_a && b2_in_a # 1st vertex of b and 2nd vertex of b form overlap
+ line_orient = line_over
+ α1 = _clamped_frac(distance(b1, a1, T), a_dist)
+ α2 = _clamped_frac(distance(b2, a1, T), a_dist)
+ intr1 = (_tuple_point(b1, T), (α1, zero(T)))
+ intr2 = (_tuple_point(b2, T), (α2, one(T)))
+ elseif a1_in_b && b1_in_a # 1st vertex of a and 1st vertex of b form overlap
+ if equals(a1, b1)
+ line_orient = line_hinge
+ intr1 = (_tuple_point(a1, T), (zero(T), zero(T)))
+ else
+ line_orient = line_over
+ intr1, intr2 = _set_ab_collinear_intrs(T, a1, b1, zero(T), zero(T), a1, b1, a_dist, b_dist)
+ end
+ elseif a1_in_b && b2_in_a # 1st vertex of a and 2nd vertex of b form overlap
+ if equals(a1, b2)
+ line_orient = line_hinge
+ intr1 = (_tuple_point(a1, T), (zero(T), one(T)))
+ else
+ line_orient = line_over
+ intr1, intr2 = _set_ab_collinear_intrs(T, a1, b2, zero(T), one(T), a1, b1, a_dist, b_dist)
+ end
+ elseif a2_in_b && b1_in_a # 2nd vertex of a and 1st vertex of b form overlap
+ if equals(a2, b1)
+ line_orient = line_hinge
+ intr1 = (_tuple_point(a2, T), (one(T), zero(T)))
+ else
+ line_orient = line_over
+ intr1, intr2 = _set_ab_collinear_intrs(T, a2, b1, one(T), zero(T), a1, b1, a_dist, b_dist)
+ end
+ elseif a2_in_b && b2_in_a # 2nd vertex of a and 2nd vertex of b form overlap
+ if equals(a2, b2)
+ line_orient = line_hinge
+ intr1 = (_tuple_point(a2, T), (one(T), one(T)))
+ else
+ line_orient = line_over
+ intr1, intr2 = _set_ab_collinear_intrs(T, a2, b2, one(T), one(T), a1, b1, a_dist, b_dist)
+ end
+ end
+ return line_orient, intr1, intr2
+end
+
+#= Determine intersection points and segment fractions when overlap is made up one one
+endpoint of segment (a1, a2) and one endpoint of segment (b1, b2). =#
+_set_ab_collinear_intrs(::Type{T}, a_pt, b_pt, a_pt_α, b_pt_β, a1, b1, a_dist, b_dist) where T =
+ (
+ (_tuple_point(a_pt, T), (a_pt_α, _clamped_frac(distance(a_pt, b1, T), b_dist))),
+ (_tuple_point(b_pt, T), (_clamped_frac(distance(b_pt, a1, T), a_dist), b_pt_β))
+ )
+
+#= If lines defined by (a1, a2) and (b1, b2) are just touching at one of those endpoints and
+are not collinear, then they form a hinge, with just that one shared intersection point.
+Point equality is checked before segment orientation to have maximal accurary on fractions
+to avoid floating point errors. If the points are not equal, we know that the hinge does not
+take place at an endpoint and the fractions must be between 0 or 1 (exclusive). =#
+function _find_hinge_intersection(::Type{T}, a1, a2, b1, b2, a1_orient, a2_orient, b1_orient) where T
+ pt, α, β = if equals(a1, b1)
+ _tuple_point(a1, T), zero(T), zero(T)
+ elseif equals(a1, b2)
+ _tuple_point(a1, T), zero(T), one(T)
+ elseif equals(a2, b1)
+ _tuple_point(a2, T), one(T), zero(T)
+ elseif equals(a2, b2)
+ _tuple_point(a2, T), one(T), one(T)
+ elseif a1_orient == 0
+ β_val = _clamped_frac(distance(b1, a1, T), distance(b1, b2, T), eps(T))
+ _tuple_point(a1, T), zero(T), β_val
+ elseif a2_orient == 0
+ β_val = _clamped_frac(distance(b1, a2, T), distance(b1, b2, T), eps(T))
+ _tuple_point(a2, T), one(T), β_val
+ elseif b1_orient == 0
+ α_val = _clamped_frac(distance(a1, b1, T), distance(a1, a2, T), eps(T))
+ _tuple_point(b1, T), α_val, zero(T)
+ else # b2_orient == 0
+ α_val = _clamped_frac(distance(a1, b2, T), distance(a1, a2, T), eps(T))
+ _tuple_point(b2, T), α_val, one(T)
+ end
+ return pt, (α, β)
+end
+
+#= If lines defined by (a1, a2) and (b1, b2) meet at one point that is not an endpoint of
+either segment, they form a crossing intersection with a singular intersection point. That
+point is caculated by finding the fractional distance along each segment the point occurs
+at (α, β). If the point is too close to an endpoint to be distinct, the point shares a value
+with the endpoint, but with a non-zero and non-one fractional value. If the intersection
+point calculated is outside of the envelope of the two segments due to floating point error,
+it is set to the endpoint of the two segments that is closest to the other segment.
+Regardless of point value, we know that it does not actually occur at an endpoint so the
+fractions must be between 0 or 1 (exclusive). =#
+function _find_cross_intersection(::Type{T}, a1, a2, b1, b2, a_ext, b_ext) where T
(a1x, a1y), (a2x, a2y) = _tuple_point(a1, T), _tuple_point(a2, T)
+ Δax, Δay = a2x - a1x, a2y - a1y
(b1x, b1y), (b2x, b2y) = _tuple_point(b1, T), _tuple_point(b2, T)
+ Δbx, Δby = b2x - b1x, b2y - b1y
Δbax = b1x - a1x
+ Δbay = b1y - a1y
+ a_cross_b = Δax * Δby - Δay * Δbx
α = _clamped_frac(Δbax * Δby - Δbay * Δbx, a_cross_b, eps(T))
+ β = _clamped_frac(Δbax * Δay - Δbay * Δax, a_cross_b, eps(T))
+
+ #= Intersection will be where a1 + α * Δa = b1 + β * Δb. However, due to floating point
+ innacurracies, α and β calculations may yeild different intersection points. Average
+ both points together to minimize difference from real value, as long as segment isn't
+ vertical or horizontal as this will almost certianly lead to the point being outside the
+ envelope due to floating point error. Also note that floating point limitations could
+ make intersection be endpoint if α≈0 or α≈1.=#
+ x = if Δax == 0
+ a1x
+ elseif Δbx == 0
+ b1x
+ else
+ (a1x + α * Δax + b1x + β * Δbx) / 2
+ end
+ y = if Δay == 0
+ a1y
+ elseif Δby == 0
+ b1y
+ else
+ (a1y + α * Δay + b1y + β * Δby) / 2
+ end
+ pt = (x, y)
if !_point_in_extent(pt, a_ext) || !_point_in_extent(pt, b_ext)
+ pt, α, β = _nearest_endpoint(T, a1, a2, b1, b2)
+ end
+ return (pt, (α, β))
+end
function _nearest_endpoint(::Type{T}, a1, a2, b1, b2) where T
a_line, a_dist = GI.Line(StaticArrays.SVector(a1, a2)), distance(a1, a2, T)
+ b_line, b_dist = GI.Line(StaticArrays.SVector(b1, b2)), distance(b1, b2, T)
min_pt, min_dist = a1, distance(a1, b_line, T)
+ α, β = eps(T), _clamped_frac(distance(min_pt, b1, T), b_dist, eps(T))
dist = distance(a2, b_line, T)
+ if dist < min_dist
+ min_pt, min_dist = a2, dist
+ α, β = one(T) - eps(T), _clamped_frac(distance(min_pt, b1, T), b_dist, eps(T))
+ end
dist = distance(b1, a_line, T)
+ if dist < min_dist
+ min_pt, min_dist = b1, dist
+ α, β = _clamped_frac(distance(min_pt, a1, T), a_dist, eps(T)), eps(T)
+ end
dist = distance(b2, a_line, T)
+ if dist < min_dist
+ min_pt, min_dist = b2, dist
+ α, β = _clamped_frac(distance(min_pt, a2, T), a_dist, eps(T)), one(T) - eps(T)
+ end
return _tuple_point(min_pt, T), α, β
+end
_clamped_frac(x::T, y::T, ϵ = zero(T)) where T = clamp(x / y, ϵ, one(T) - ϵ)
module Predicates
+ using ExactPredicates, ExactPredicates.Codegen
+ import ExactPredicates: ext
+ import ExactPredicates.Codegen: group!, @genpredicate
+ import GeometryOps: _False, _True, _booltype, _tuple_point
+ import GeoInterface as GI
+
+ #= Determine the orientation of c with regards to the oriented segment (a, b).
+ Return 1 if c is to the left of (a, b).
+ Return -1 if c is to the right of (a, b).
+ Return 0 if c is on (a, b) or if a == b. =#
+ orient(a, b, c; exact) = _orient(_booltype(exact), a, b, c)
exact
is true
, use ExactPredicates
to calculate the orientation. _orient(::_True, a, b, c) = ExactPredicates.orient(_tuple_point(a, Float64), _tuple_point(b, Float64), _tuple_point(c, Float64))
exact
is false
, calculate the orientation without using ExactPredicates
. function _orient(exact::_False, a, b, c)
+ a = a .- c
+ b = b .- c
+ return _cross(exact, a, b)
+ end
+
+ #= Determine the sign of the cross product of a and b.
+ Return 1 if the cross product is positive.
+ Return -1 if the cross product is negative.
+ Return 0 if the cross product is 0. =#
+ cross(a, b; exact) = _cross(_booltype(exact), a, b)
+
+ #= If \`exact\` is \`true\`, use exact cross product calculation created using
+ \`ExactPredicates\`generated predicate. Note that as of now \`ExactPredicates\` requires
+ Float64 so we must convert points a and b. =#
+ _cross(::_True, a, b) = _cross_exact(_tuple_point(a, Float64), _tuple_point(b, Float64))
ExactPredicates
. @genpredicate function _cross_exact(a :: 2, b :: 2)
+ group!(a...)
+ group!(b...)
+ ext(a, b)
+ end
exact
is false
, calculate the cross product without using ExactPredicates
. function _cross(::_False, a, b)
+ c_t1 = GI.x(a) * GI.y(b)
+ c_t2 = GI.y(a) * GI.x(b)
+ c_val = if isapprox(c_t1, c_t2)
+ 0
+ else
+ sign(c_t1 - c_t2)
+ end
+ return c_val
+ end
+
+end
+
+import .Predicates
If we want to inject adaptivity, we would do something like:
Union Polygon Clipping
export union
+
+"""
+ union(geom_a, geom_b, [::Type{T}]; target::Type, fix_multipoly = UnionIntersectingPolygons())
+
+Return the union between two geometries as a list of geometries. Return an empty list if
+none are found. The type of the list will be constrained as much as possible given the input
+geometries. Furthermore, the user can provide a \`taget\` type as a keyword argument and a
+list of target geometries found in the difference will be returned. The user can also
+provide a float type 'T' that they would like the points of returned geometries to be. If
+the user is taking a intersection involving one or more multipolygons, and the multipolygon
+might be comprised of polygons that intersect, if \`fix_multipoly\` is set to an
+\`IntersectingPolygons\` correction (the default is \`UnionIntersectingPolygons()\`), then the
+needed multipolygons will be fixed to be valid before performing the intersection to ensure
+a correct answer. Only set \`fix_multipoly\` to false if you know that the multipolygons are
+valid, as it will avoid unneeded computation.
+
+Calculates the union between two polygons.
+# Example
+
+\`\`\`jldoctest
+import GeoInterface as GI, GeometryOps as GO
+
+p1 = GI.Polygon([[(0.0, 0.0), (5.0, 5.0), (10.0, 0.0), (5.0, -5.0), (0.0, 0.0)]])
+p2 = GI.Polygon([[(3.0, 0.0), (8.0, 5.0), (13.0, 0.0), (8.0, -5.0), (3.0, 0.0)]])
+union_poly = GO.union(p1, p2; target = GI.PolygonTrait())
+GI.coordinates.(union_poly)
1-element Vector{Vector{Vector{Vector{Float64}}}}:
+ [[[6.5, 3.5], [5.0, 5.0], [0.0, 0.0], [5.0, -5.0], [6.5, -3.5], [8.0, -5.0], [13.0, 0.0], [8.0, 5.0], [6.5, 3.5]]]
+\`\`\`
+"""
+function union(
+ geom_a, geom_b, ::Type{T}=Float64; target=nothing, kwargs...
+) where {T<:AbstractFloat}
+ return _union(
+ TraitTarget(target), T, GI.trait(geom_a), geom_a, GI.trait(geom_b), geom_b;
+ exact = _True(), kwargs...,
+ )
+end
+
+#= This 'union' implementation returns the union of two polygons. The algorithm to determine
+the union was adapted from "Efficient clipping of efficient polygons," by Greiner and
+Hormann (1998). DOI: https://doi.org/10.1145/274363.274364 =#
+function _union(
+ ::TraitTarget{GI.PolygonTrait}, ::Type{T},
+ ::GI.PolygonTrait, poly_a,
+ ::GI.PolygonTrait, poly_b;
+ exact, kwargs...,
+) where T
ext_a = GI.getexterior(poly_a)
+ ext_b = GI.getexterior(poly_b)
a_list, b_list, a_idx_list = _build_ab_list(T, ext_a, ext_b, _union_delay_cross_f, _union_delay_bounce_f; exact)
+ polys = _trace_polynodes(T, a_list, b_list, a_idx_list, _union_step, poly_a, poly_b)
+ n_pieces = length(polys)
a_in_b, b_in_a = false, false
+ if n_pieces == 0 # no crossing points, determine if either poly is inside the other
+ a_in_b, b_in_a = _find_non_cross_orientation(a_list, b_list, ext_a, ext_b; exact)
+ if a_in_b
+ push!(polys, GI.Polygon([tuples(ext_b)]))
+ elseif b_in_a
+ push!(polys, GI.Polygon([tuples(ext_a)]))
+ else
+ push!(polys, tuples(poly_a))
+ push!(polys, tuples(poly_b))
+ return polys
+ end
+ elseif n_pieces > 1
+ #= extra polygons are holes (n_pieces == 1 is the desired state) and since
+ holes are formed by regions exterior to both poly_a and poly_b, they can't interact
+ with pre-existing holes =#
+ sort!(polys, by = area, rev = true) # sort by area so first element is the exterior
@views append!(polys[1].geom, (GI.getexterior(p) for p in polys[2:end]))
+ keepat!(polys, 1)
+ end
if GI.nhole(poly_a) != 0 || GI.nhole(poly_b) != 0
+ _add_union_holes!(polys, a_in_b, b_in_a, poly_a, poly_b; exact)
+ end
_remove_collinear_points!(polys, [false], poly_a, poly_b)
+ return polys
+end
Helper functions for Unions with Greiner and Hormann Polygon Clipping
#= When marking the crossing status of a delayed crossing, the chain start point is crossing
+when the start point is a entry point and is a bouncing point when the start point is an
+exit point. The end of the chain has the opposite crossing / bouncing status. =#
+_union_delay_cross_f(x) = (x, !x)
+
+#= When marking the crossing status of a delayed bouncing, the chain start and end points
+are bouncing if the current polygon's adjacent edges are within the non-tracing polygon. If
+the edges are outside then the chain endpoints are marked as crossing. x is a boolean
+representing if the edges are inside or outside of the polygon. =#
+_union_delay_bounce_f(x, _) = !x
+
+#= When tracing polygons, step backwards if the most recent intersection point was an entry
+point, else step forwards where x is the entry/exit status. =#
+_union_step(x, _) = x ? (-1) : 1
+
+#= Add holes from two polygons to the exterior polygon formed by their union. If adding the
+the holes reveals that the polygons aren't actually intersecting, return the original
+polygons. =#
+function _add_union_holes!(polys, a_in_b, b_in_a, poly_a, poly_b; exact)
+ if a_in_b
+ _add_union_holes_contained_polys!(polys, poly_a, poly_b; exact)
+ elseif b_in_a
+ _add_union_holes_contained_polys!(polys, poly_b, poly_a; exact)
+ else # Polygons intersect, but neither is contained in the other
+ n_a_holes = GI.nhole(poly_a)
+ ext_poly_a = GI.Polygon(StaticArrays.SVector(GI.getexterior(poly_a)))
+ ext_poly_b = GI.Polygon(StaticArrays.SVector(GI.getexterior(poly_b)))
+ #= Start with poly_b when comparing with holes from poly_a and then switch to poly_a
+ to compare with holes from poly_b. For current_poly, use ext_poly_b to avoid
+ repeating overlapping holes in poly_a and poly_b =#
+ curr_exterior_poly = n_a_holes > 0 ? ext_poly_b : ext_poly_a
+ current_poly = n_a_holes > 0 ? ext_poly_b : poly_a
for (i, ih) in enumerate(Iterators.flatten((GI.gethole(poly_a), GI.gethole(poly_b))))
+ in_ext, _, _ = _line_polygon_interactions(ih, curr_exterior_poly; exact, closed_line = true)
+ if !in_ext
+ #= if the hole isn't in the overlapping region between the two polygons, add
+ the hole to the resulting polygon as we know it can't interact with any
+ other holes =#
+ push!(polys[1].geom, ih)
+ else
+ #= if the hole is at least partially in the overlapping region, take the
+ difference of the hole from the polygon it didn't originate from - note that
+ when current_poly is poly_a this includes poly_a holes so overlapping holes
+ between poly_a and poly_b within the overlap are added, in addition to all
+ holes in non-overlapping regions =#
+ h_poly = GI.Polygon(StaticArrays.SVector(ih))
+ new_holes = difference(h_poly, current_poly; target = GI.PolygonTrait())
+ append!(polys[1].geom, (GI.getexterior(new_h) for new_h in new_holes))
+ end
+ if i == n_a_holes
+ curr_exterior_poly = ext_poly_a
+ current_poly = poly_a
+ end
+ end
+ end
+ return
+end
+
+#= Add holes holes to the union of two polygons where one of the original polygons was
+inside of the other. If adding the the holes reveal that the polygons aren't actually
+intersecting, return the original polygons.=#
+function _add_union_holes_contained_polys!(polys, interior_poly, exterior_poly; exact)
+ union_poly = polys[1]
+ ext_int_ring = GI.getexterior(interior_poly)
+ for (i, ih) in enumerate(GI.gethole(exterior_poly))
+ poly_ih = GI.Polygon(StaticArrays.SVector(ih))
+ in_ih, on_ih, out_ih = _line_polygon_interactions(ext_int_ring, poly_ih; exact, closed_line = true)
+ if in_ih # at least part of interior polygon exterior is within the ith hole
+ if !on_ih && !out_ih
+ #= interior polygon is completly within the ith hole - polygons aren't
+ touching and do not actually form a union =#
+ polys[1] = tuples(interior_poly)
+ push!(polys, tuples(exterior_poly))
+ return polys
+ else
+ #= interior polygon is partially within the ith hole - area of interior
+ polygon reduces the size of the hole =#
+ new_holes = difference(poly_ih, interior_poly; target = GI.PolygonTrait())
+ append!(union_poly.geom, (GI.getexterior(new_h) for new_h in new_holes))
+ end
+ else # none of interior polygon exterior is within the ith hole
+ if !out_ih
+ #= interior polygon's exterior is the same as the ith hole - polygons do
+ form a union, but do not overlap so all holes stay in final polygon =#
+ append!(union_poly.geom, Iterators.drop(GI.gethole(exterior_poly), i))
+ append!(union_poly.geom, GI.gethole(interior_poly))
+ return polys
+ else
+ #= interior polygon's exterior is outside of the ith hole - the interior
+ polygon could either be disjoint from the hole, or contain the hole =#
+ ext_int_poly = GI.Polygon(StaticArrays.SVector(ext_int_ring))
+ in_int, _, _ = _line_polygon_interactions(ih, ext_int_poly; exact, closed_line = true)
+ if in_int
+ #= interior polygon contains the hole - overlapping holes between the
+ interior and exterior polygons will be added =#
+ for jh in GI.gethole(interior_poly)
+ poly_jh = GI.Polygon(StaticArrays.SVector(jh))
+ if intersects(poly_ih, poly_jh)
+ new_holes = intersection(poly_ih, poly_jh; target = GI.PolygonTrait())
+ append!(union_poly.geom, (GI.getexterior(new_h) for new_h in new_holes))
+ end
+ end
+ else
+ #= interior polygon and the exterior polygon are disjoint - add the ith
+ hole as it is not covered by the interior polygon =#
+ push!(union_poly.geom, ih)
+ end
+ end
+ end
+ end
+ return
+end
+
+#= Polygon with multipolygon union - note that all sub-polygons of \`multipoly_b\` will be
+included, unioning these sub-polygons with \`poly_a\` where they intersect. Unless specified
+with \`fix_multipoly = nothing\`, \`multipolygon_b\` will be validated using the given (default
+is \`UnionIntersectingPolygons()\`) correction. =#
+function _union(
+ target::TraitTarget{GI.PolygonTrait}, ::Type{T},
+ ::GI.PolygonTrait, poly_a,
+ ::GI.MultiPolygonTrait, multipoly_b;
+ fix_multipoly = UnionIntersectingPolygons(), kwargs...,
+) where T
+ if !isnothing(fix_multipoly) # Fix multipoly_b to prevent repeated regions in the output
+ multipoly_b = fix_multipoly(multipoly_b)
+ end
+ polys = [tuples(poly_a, T)]
+ for poly_b in GI.getpolygon(multipoly_b)
+ if intersects(polys[1], poly_b)
new_polys = union(polys[1], poly_b; target)
+ if length(new_polys) > 1 # case where they intersect by just one point
+ push!(polys, tuples(poly_b, T)) # add poly_b to list
+ else
+ polys[1] = new_polys[1]
+ end
+ else
push!(polys, tuples(poly_b, T))
+ end
+ end
+ return polys
+end
+
+#= Multipolygon with polygon union is equivalent to taking the union of the poylgon with the
+multipolygon and thus simply switches the order of operations and calls the above method. =#
+_union(
+ target::TraitTarget{GI.PolygonTrait}, ::Type{T},
+ ::GI.MultiPolygonTrait, multipoly_a,
+ ::GI.PolygonTrait, poly_b;
+ kwargs...,
+) where T = union(poly_b, multipoly_a; target, kwargs...)
+
+#= Multipolygon with multipolygon union - note that all of the sub-polygons of \`multipoly_a\`
+and the sub-polygons of \`multipoly_b\` are included and combined together where there are
+intersections. Unless specified with \`fix_multipoly = nothing\`, \`multipolygon_b\` will be
+validated using the given (default is \`UnionIntersectingPolygons()\`) correction. =#
+function _union(
+ target::TraitTarget{GI.PolygonTrait}, ::Type{T},
+ ::GI.MultiPolygonTrait, multipoly_a,
+ ::GI.MultiPolygonTrait, multipoly_b;
+ fix_multipoly = UnionIntersectingPolygons(), kwargs...,
+) where T
+ if !isnothing(fix_multipoly) # Fix multipoly_b to prevent repeated regions in the output
+ multipoly_b = fix_multipoly(multipoly_b)
+ fix_multipoly = nothing
+ end
+ multipolys = multipoly_b
+ local polys
+ for poly_a in GI.getpolygon(multipoly_a)
+ polys = union(poly_a, multipolys; target, fix_multipoly)
+ multipolys = GI.MultiPolygon(polys)
+ end
+ return polys
+end
function _union(
+ ::TraitTarget{Target}, ::Type{T},
+ trait_a::GI.AbstractTrait, geom_a,
+ trait_b::GI.AbstractTrait, geom_b;
+ kwargs...
+) where {Target,T}
+ throw(ArgumentError("Union between $trait_a and $trait_b with target $Target isn't implemented yet."))
+ return nothing
+end
Distance and signed distance
export distance, signed_distance
What is distance? What is signed distance?
import GeometryOps as GO
+import GeoInterface as GI
+using Makie
+using CairoMakie
+
+rect = GI.Polygon([[(0,0), (0,1), (1,1), (1,0), (0, 0)]])
+point_in = (0.5, 0.5)
+point_out = (0.5, 1.5)
+f, a, p = poly(collect(GI.getpoint(rect)); axis = (; aspect = DataAspect()))
+scatter!(GI.x(point_in), GI.y(point_in); color = :red)
+scatter!(GI.x(point_out), GI.y(point_out); color = :orange)
+f
point_in
is negative while the distance to point_out
is positive.(
+GO.distance(point_in, rect), # == 0
+GO.signed_distance(point_in, rect), # < 0
+GO.signed_distance(point_out, rect) # > 0
+)
(0.0, -0.5, 0.5)
xrange = yrange = LinRange(-0.5, 1.5, 300)
+f, a, p = heatmap(xrange, yrange, GO.signed_distance.(Point2f.(xrange, yrange'), Ref(rect)); colormap = :RdBu, colorrange = (-0.75, 0.75))
+a.aspect = DataAspect(); Colorbar(f[1, 2], p, label = "Signed distance"); lines!(a, GI.convert(GO.GeometryBasics, rect)); f
Implementation
const _DISTANCE_TARGETS = TraitTarget{Union{GI.AbstractPolygonTrait,GI.LineStringTrait,GI.LinearRingTrait,GI.LineTrait,GI.PointTrait}}()
+
+"""
+ distance(point, geom, ::Type{T} = Float64)::T
+
+Calculates the ditance from the geometry \`g1\` to the \`point\`. The distance
+will always be positive or zero.
+
+The method will differ based on the type of the geometry provided:
+ - The distance from a point to a point is just the Euclidean distance
+ between the points.
+ - The distance from a point to a line is the minimum distance from the point
+ to the closest point on the given line.
+ - The distance from a point to a linestring is the minimum distance from the
+ point to the closest segment of the linestring.
+ - The distance from a point to a linear ring is the minimum distance from
+ the point to the closest segment of the linear ring.
+ - The distance from a point to a polygon is zero if the point is within the
+ polygon and otherwise is the minimum distance from the point to an edge of
+ the polygon. This includes edges created by holes.
+ - The distance from a point to a multigeometry or a geometry collection is
+ the minimum distance between the point and any of the sub-geometries.
+
+Result will be of type T, where T is an optional argument with a default value
+of Float64.
+"""
+function distance(
+ geom1, geom2, ::Type{T} = Float64; threaded=false
+) where T<:AbstractFloat
+ distance(GI.trait(geom1), geom1, GI.trait(geom2), geom2, T; threaded)
+end
+function distance(
+ trait1, geom, trait2::GI.PointTrait, point, ::Type{T} = Float64;
+ threaded=false
+) where T<:AbstractFloat
+ distance(trait2, point, trait1, geom, T) # Swap order
+end
+function distance(
+ trait1::GI.PointTrait, point, trait2, geom, ::Type{T} = Float64;
+ threaded=false
+) where T<:AbstractFloat
+ applyreduce(min, _DISTANCE_TARGETS, geom; threaded, init=typemax(T)) do g
+ _distance(T, trait1, point, GI.trait(g), g)
+ end
+end
function distance(
+ trait1::GI.PointTrait, point1, trait2::GI.PointTrait, point2, ::Type{T} = Float64;
+ threaded=false
+) where T<:AbstractFloat
+ _distance(T, trait1, point1, trait2, point2)
+end
_distance(::Type{T}, ::GI.PointTrait, point, ::GI.PointTrait, geom) where T =
+ _euclid_distance(T, point, geom)
+_distance(::Type{T}, ::GI.PointTrait, point, ::GI.LineTrait, geom) where T =
+ _distance_line(T, point, GI.getpoint(geom, 1), GI.getpoint(geom, 2))
+_distance(::Type{T}, ::GI.PointTrait, point, ::GI.LineStringTrait, geom) where T =
+ _distance_curve(T, point, geom; close_curve = false)
+_distance(::Type{T}, ::GI.PointTrait, point, ::GI.LinearRingTrait, geom) where T =
+ _distance_curve(T, point, geom; close_curve = true)
function _distance(::Type{T}, ::GI.PointTrait, point, ::GI.PolygonTrait, geom) where T
+ within(point, geom) && return zero(T)
+ return _distance_polygon(T, point, geom)
+end
+
+"""
+ signed_distance(point, geom, ::Type{T} = Float64)::T
+
+Calculates the signed distance from the geometry \`geom\` to the given point.
+Points within \`geom\` have a negative signed distance, and points outside of
+\`geom\` have a positive signed distance.
+ - The signed distance from a point to a point, line, linestring, or linear
+ ring is equal to the distance between the two.
+ - The signed distance from a point to a polygon is negative if the point is
+ within the polygon and is positive otherwise. The value of the distance is
+ the minimum distance from the point to an edge of the polygon. This includes
+ edges created by holes.
+ - The signed distance from a point to a multigeometry or a geometry
+ collection is the minimum signed distance between the point and any of the
+ sub-geometries.
+
+Result will be of type T, where T is an optional argument with a default value
+of Float64.
+"""
+function signed_distance(
+ geom1, geom2, ::Type{T} = Float64; threaded=false
+) where T<:AbstractFloat
+ signed_distance(GI.trait(geom1), geom1, GI.trait(geom2), geom2, T; threaded)
+end
+function signed_distance(
+ trait1, geom, trait2::GI.PointTrait, point, ::Type{T} = Float64;
+ threaded=false
+) where T<:AbstractFloat
+ signed_distance(trait2, point, trait1, geom, T; threaded) # Swap order
+end
+function signed_distance(
+ trait1::GI.PointTrait, point, trait2, geom, ::Type{T} = Float64;
+ threaded=false
+) where T<:AbstractFloat
+ applyreduce(min, _DISTANCE_TARGETS, geom; threaded, init=typemax(T)) do g
+ _signed_distance(T, trait1, point, GI.trait(g), g)
+ end
+end
function signed_distance(
+ trait1::GI.PointTrait, point1, trait2::GI.PointTrait, point2, ::Type{T} = Float64;
+ threaded=false
+) where T<:AbstractFloat
+ _signed_distance(T, trait1, point1, trait2, point2)
+end
function _signed_distance(
+ ::Type{T}, ptrait::GI.PointTrait, point, gtrait::GI.AbstractGeometryTrait, geom
+) where T
+ _distance(T, ptrait, point, gtrait, geom)
+end
function _signed_distance(::Type{T}, ::GI.PointTrait, point, ::GI.PolygonTrait, geom) where T
+ min_dist = _distance_polygon(T, point, geom)
+ return within(point, geom) ? -min_dist : min_dist
end
Base.@propagate_inbounds _euclid_distance(::Type{T}, p1, p2) where T =
+ sqrt(_squared_euclid_distance(T, p1, p2))
Base.@propagate_inbounds _squared_euclid_distance(::Type{T}, p1, p2) where T =
+ _squared_euclid_distance(
+ T,
+ GeoInterface.x(p1), GeoInterface.y(p1),
+ GeoInterface.x(p2), GeoInterface.y(p2),
+ )
Base.@propagate_inbounds _euclid_distance(::Type{T}, x1, y1, x2, y2) where T =
+ sqrt(_squared_euclid_distance(T, x1, y1, x2, y2))
Base.@propagate_inbounds _squared_euclid_distance(::Type{T}, x1, y1, x2, y2) where T =
+ T((x2 - x1)^2 + (y2 - y1)^2)
_distance_line(::Type{T}, p0, p1, p2) where T =
+ sqrt(_squared_distance_line(T, p0, p1, p2))
function _squared_distance_line(::Type{T}, p0, p1, p2) where T
+ x0, y0 = GeoInterface.x(p0), GeoInterface.y(p0)
+ x1, y1 = GeoInterface.x(p1), GeoInterface.y(p1)
+ x2, y2 = GeoInterface.x(p2), GeoInterface.y(p2)
+
+ xfirst, yfirst, xlast, ylast = x1 < x2 ? (x1, y1, x2, y2) : (x2, y2, x1, y1)
+
+ #=
+ Vectors from first point to last point (v) and from first point to point of
+ interest (w) to find the projection of w onto v to find closest point
+ =#
+ v = (xlast - xfirst, ylast - yfirst)
+ w = (x0 - xfirst, y0 - yfirst)
+
+ c1 = sum(w .* v)
+ if c1 <= 0 # p0 is closest to first endpoint
+ return _squared_euclid_distance(T, x0, y0, xfirst, yfirst)
+ end
+
+ c2 = sum(v .* v)
+ if c2 <= c1 # p0 is closest to last endpoint
+ return _squared_euclid_distance(T, x0, y0, xlast, ylast)
+ end
+
+ b2 = c1 / c2 # projection fraction
+ return _squared_euclid_distance(T, x0, y0, xfirst + (b2 * v[1]), yfirst + (b2 * v[2]))
+end
function _distance_curve(::Type{T}, point, curve; close_curve = false) where T
np = GI.npoint(curve)
+ first_last_equal = equals(GI.getpoint(curve, 1), GI.getpoint(curve, np))
+ close_curve &= first_last_equal
+ np -= first_last_equal ? 1 : 0
min_dist = typemax(T)
+ p1 = GI.getpoint(curve, close_curve ? np : 1)
+ for i in (close_curve ? 1 : 2):np
+ p2 = GI.getpoint(curve, i)
+ dist = _distance_line(T, point, p1, p2)
+ min_dist = dist < min_dist ? dist : min_dist
+ p1 = p2
+ end
+ return min_dist
+end
function _distance_polygon(::Type{T}, point, poly) where T
+ min_dist = _distance_curve(T, point, GI.getexterior(poly); close_curve = true)
+ @inbounds for hole in GI.gethole(poly)
+ dist = _distance_curve(T, point, hole; close_curve = true)
+ min_dist = dist < min_dist ? dist : min_dist
+ end
+ return min_dist
+end
Equals
export equals
What is equals?
import GeometryOps as GO
+import GeoInterface as GI
+using Makie
+using CairoMakie
+
+l1 = GI.LineString([(0.0, 0.0), (0.0, 10.0)])
+l2 = GI.LineString([(0.0, -10.0), (0.0, 3.0)])
+f, a, p = lines(GI.getpoint(l1), color = :blue)
+scatter!(GI.getpoint(l1), color = :blue)
+lines!(GI.getpoint(l2), color = :orange)
+scatter!(GI.getpoint(l2), color = :orange)
+f
GO.equals(l1, l2) # returns false
false
Implementation
"""
+ equals(geom1, geom2)::Bool
+
+Compare two Geometries return true if they are the same geometry.
+
+# Examples
+\`\`\`jldoctest
+import GeometryOps as GO, GeoInterface as GI
+poly1 = GI.Polygon([[(0,0), (0,5), (5,5), (5,0), (0,0)]])
+poly2 = GI.Polygon([[(0,0), (0,5), (5,5), (5,0), (0,0)]])
+
+GO.equals(poly1, poly2)
true
+\`\`\`
+"""
+equals(geom_a, geom_b) = equals(
+ GI.trait(geom_a), geom_a,
+ GI.trait(geom_b), geom_b,
+)
+
+"""
+ equals(::T, geom_a, ::T, geom_b)::Bool
+
+Two geometries of the same type, which don't have a equals function to dispatch
+off of should throw an error.
+"""
+equals(::T, geom_a, ::T, geom_b) where T = error("Cant compare $T yet")
+
+"""
+ equals(trait_a, geom_a, trait_b, geom_b)
+
+Two geometries which are not of the same type cannot be equal so they always
+return false.
+"""
+equals(trait_a, geom_a, trait_b, geom_b) = false
+
+"""
+ equals(::GI.PointTrait, p1, ::GI.PointTrait, p2)::Bool
+
+Two points are the same if they have the same x and y (and z if 3D) coordinates.
+"""
+function equals(::GI.PointTrait, p1, ::GI.PointTrait, p2)
+ GI.ncoord(p1) == GI.ncoord(p2) || return false
+ GI.x(p1) == GI.x(p2) || return false
+ GI.y(p1) == GI.y(p2) || return false
+ if GI.is3d(p1)
+ GI.z(p1) == GI.z(p2) || return false
+ end
+ return true
+end
+
+"""
+ equals(::GI.PointTrait, p1, ::GI.MultiPointTrait, mp2)::Bool
+
+A point and a multipoint are equal if the multipoint is composed of a single
+point that is equivalent to the given point.
+"""
+function equals(::GI.PointTrait, p1, ::GI.MultiPointTrait, mp2)
+ GI.npoint(mp2) == 1 || return false
+ return equals(p1, GI.getpoint(mp2, 1))
+end
+
+"""
+ equals(::GI.MultiPointTrait, mp1, ::GI.PointTrait, p2)::Bool
+
+A point and a multipoint are equal if the multipoint is composed of a single
+point that is equivalent to the given point.
+"""
+equals(trait1::GI.MultiPointTrait, mp1, trait2::GI.PointTrait, p2) =
+ equals(trait2, p2, trait1, mp1)
+
+"""
+ equals(::GI.MultiPointTrait, mp1, ::GI.MultiPointTrait, mp2)::Bool
+
+Two multipoints are equal if they share the same set of points.
+"""
+function equals(::GI.MultiPointTrait, mp1, ::GI.MultiPointTrait, mp2)
+ GI.npoint(mp1) == GI.npoint(mp2) || return false
+ for p1 in GI.getpoint(mp1)
+ has_match = false # if point has a matching point in other multipoint
+ for p2 in GI.getpoint(mp2)
+ if equals(p1, p2)
+ has_match = true
+ break
+ end
+ end
+ has_match || return false # if no matching point, can't be equal
+ end
+ return true # all points had a match
+end
+
+"""
+ _equals_curves(c1, c2, closed_type1, closed_type2)::Bool
+
+Two curves are equal if they share the same set of point, representing the same
+geometry. Both curves must must be composed of the same set of points, however,
+they do not have to wind in the same direction, or start on the same point to be
+equivalent.
+Inputs:
+ c1 first geometry
+ c2 second geometry
+ closed_type1::Bool true if c1 is closed by definition (polygon, linear ring)
+ closed_type2::Bool true if c2 is closed by definition (polygon, linear ring)
+"""
+function _equals_curves(c1, c2, closed_type1, closed_type2)
n1 = GI.npoint(c1)
+ n2 = GI.npoint(c2)
+ c1_repeat_point = GI.getpoint(c1, 1) == GI.getpoint(c1, n1)
+ n2 = GI.npoint(c2)
+ c2_repeat_point = GI.getpoint(c2, 1) == GI.getpoint(c2, n2)
+ closed1 = closed_type1 || c1_repeat_point
+ closed2 = closed_type2 || c2_repeat_point
+ closed1 == closed2 || return false
n1 -= c1_repeat_point ? 1 : 0
+ n2 -= c2_repeat_point ? 1 : 0
+ n1 == n2 || return false
+ n1 == 0 && return true
jstart = nothing
+ p1 = GI.getpoint(c1, 1)
+ for i in 1:n2
+ if equals(p1, GI.getpoint(c2, i))
+ jstart = i
+ break
+ end
+ end
isnothing(jstart) && return false
n1 == 1 && return true
!closed_type1 && (jstart != 1 && jstart != n1) && return false
i = 2
+ j = jstart + 1
+ j -= j > n2 ? n2 : 0
+ same_direction = equals(GI.getpoint(c1, i), GI.getpoint(c2, j))
n1 == 2 && return same_direction
jstep = same_direction ? 1 : -1
+ for i in 2:n1
+ ip = GI.getpoint(c1, i)
+ j = jstart + (i - 1) * jstep
+ j += (0 < j <= n2) ? 0 : (n2 * -jstep)
+ jp = GI.getpoint(c2, j)
+ equals(ip, jp) || return false
+ end
+ return true
+end
+
+"""
+ equals(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, l1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, l2,
+ )::Bool
+
+Two lines/linestrings are equal if they share the same set of points going
+along the curve. Note that lines/linestrings aren't closed by defintion.
+"""
+equals(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, l1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, l2,
+) = _equals_curves(l1, l2, false, false)
+
+"""
+ equals(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, l1,
+ ::GI.LinearRingTrait, l2,
+ )::Bool
+
+A line/linestring and a linear ring are equal if they share the same set of
+points going along the curve. Note that lines aren't closed by defintion, but
+rings are, so the line must have a repeated last point to be equal
+"""
+equals(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, l1,
+ ::GI.LinearRingTrait, l2,
+) = _equals_curves(l1, l2, false, true)
+
+"""
+ equals(
+ ::GI.LinearRingTrait, l1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, l2,
+ )::Bool
+
+A linear ring and a line/linestring are equal if they share the same set of
+points going along the curve. Note that lines aren't closed by defintion, but
+rings are, so the line must have a repeated last point to be equal
+"""
+equals(
+ ::GI.LinearRingTrait, l1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, l2,
+) = _equals_curves(l1, l2, true, false)
+
+"""
+ equals(
+ ::GI.LinearRingTrait, l1,
+ ::GI.LinearRingTrait, l2,
+ )::Bool
+
+Two linear rings are equal if they share the same set of points going along the
+curve. Note that rings are closed by definition, so they can have, but don't
+need, a repeated last point to be equal.
+"""
+equals(
+ ::GI.LinearRingTrait, l1,
+ ::GI.LinearRingTrait, l2,
+) = _equals_curves(l1, l2, true, true)
+
+"""
+ equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool
+
+Two polygons are equal if they share the same exterior edge and holes.
+"""
+function equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)
_equals_curves(
+ GI.getexterior(geom_a), GI.getexterior(geom_b),
+ true, true, # linear rings are closed by definition
+ ) || return false
GI.nhole(geom_a) == GI.nhole(geom_b) || return false
for ihole in GI.gethole(geom_a)
+ has_match = false
+ for jhole in GI.gethole(geom_b)
+ if _equals_curves(
+ ihole, jhole,
+ true, true, # linear rings are closed by definition
+ )
+ has_match = true
+ break
+ end
+ end
+ has_match || return false
+ end
+ return true
+end
+
+"""
+ equals(::GI.PolygonTrait, geom_a, ::GI.MultiPolygonTrait, geom_b)::Bool
+
+A polygon and a multipolygon are equal if the multipolygon is composed of a
+single polygon that is equivalent to the given polygon.
+"""
+function equals(::GI.PolygonTrait, geom_a, ::MultiPolygonTrait, geom_b)
+ GI.npolygon(geom_b) == 1 || return false
+ return equals(geom_a, GI.getpolygon(geom_b, 1))
+end
+
+"""
+ equals(::GI.MultiPolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool
+
+A polygon and a multipolygon are equal if the multipolygon is composed of a
+single polygon that is equivalent to the given polygon.
+"""
+equals(trait_a::GI.MultiPolygonTrait, geom_a, trait_b::PolygonTrait, geom_b) =
+ equals(trait_b, geom_b, trait_a, geom_a)
+
+"""
+ equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool
+
+Two multipolygons are equal if they share the same set of polygons.
+"""
+function equals(::GI.MultiPolygonTrait, geom_a, ::GI.MultiPolygonTrait, geom_b)
GI.npolygon(geom_a) == GI.npolygon(geom_b) || return false
for poly_a in GI.getpolygon(geom_a)
+ has_match = false
+ for poly_b in GI.getpolygon(geom_b)
+ if equals(poly_a, poly_b)
+ has_match = true
+ break
+ end
+ end
+ has_match || return false
+ end
+ return true
+end
Contains
export contains
What is contains?
import GeometryOps as GO
+import GeoInterface as GI
+using Makie
+using CairoMakie
+
+l1 = GI.LineString([(0.0, 0.0), (1.0, 0.0), (0.0, 0.1)])
+l2 = GI.LineString([(0.25, 0.0), (0.75, 0.0)])
+f, a, p = lines(GI.getpoint(l1), color = :blue)
+scatter!(GI.getpoint(l1), color = :blue)
+lines!(GI.getpoint(l2), color = :orange)
+scatter!(GI.getpoint(l2), color = :orange)
+f
GO.contains(l1, l2) # returns true
+GO.contains(l2, l1) # returns false
false
Implementation
"""
+ contains(g1::AbstractGeometry, g2::AbstractGeometry)::Bool
+
+Return true if the second geometry is completely contained by the first
+geometry. The interiors of both geometries must intersect and the interior and
+boundary of the secondary (g2) must not intersect the exterior of the first
+(g1).
+
+\`contains\` returns the exact opposite result of \`within\`.
+
+# Examples
+
+\`\`\`jldoctest
+import GeometryOps as GO, GeoInterface as GI
+line = GI.LineString([(1, 1), (1, 2), (1, 3), (1, 4)])
+point = GI.Point((1, 2))
+
+GO.contains(line, point)
true
+\`\`\`
+"""
+contains(g1, g2) = GeometryOps.within(g2, g1)
CoveredBy
export coveredby
What is coveredby?
import GeometryOps as GO
+import GeoInterface as GI
+using Makie
+using CairoMakie
+
+p1 = (0.0, 0.0)
+l1 = GI.Line([p1, (1.0, 1.0)])
+f, a, p = lines(GI.getpoint(l1))
+scatter!(p1, color = :red)
+f
p1
is on the endpoint of l1. This means it is not within
, but it does meet the definition of coveredby
.GO.coveredby(p1, l1) # true
true
Implementation
coveredby
function and arguments g1 and g2, this criteria is as follows: - points of g1 are allowed to be in the interior of g2 (either through overlap or crossing for lines) - points of g1 are allowed to be on the boundary of g2 - points of g1 are not allowed to be in the exterior of g2 - no points of g1 are required to be in the interior of g2 - no points of g1 are required to be on the boundary of g2 - no points of g1 are required to be in the exterior of g2const COVEREDBY_ALLOWS = (in_allow = true, on_allow = true, out_allow = false)
+const COVEREDBY_CURVE_ALLOWS = (over_allow = true, cross_allow = true, on_allow = true, out_allow = false)
+const COVEREDBY_CURVE_REQUIRES = (in_require = false, on_require = false, out_require = false)
+const COVEREDBY_POLYGON_REQUIRES = (in_require = true, on_require = false, out_require = false,)
+const COVEREDBY_EXACT = (exact = _False(),)
+
+"""
+ coveredby(g1, g2)::Bool
+
+Return \`true\` if the first geometry is completely covered by the second
+geometry. The interior and boundary of the primary geometry (g1) must not
+intersect the exterior of the secondary geometry (g2).
+
+Furthermore, \`coveredby\` returns the exact opposite result of \`covers\`. They are
+equivalent with the order of the arguments swapped.
+
+# Examples
+\`\`\`jldoctest setup=:(using GeometryOps, GeometryBasics)
+import GeometryOps as GO, GeoInterface as GI
+p1 = GI.Point(0.0, 0.0)
+p2 = GI.Point(1.0, 1.0)
+l1 = GI.Line([p1, p2])
+
+GO.coveredby(p1, l1)
true
+\`\`\`
+"""
+coveredby(g1, g2) = _coveredby(trait(g1), g1, trait(g2), g2)
Convert features to geometries
_coveredby(::GI.FeatureTrait, g1, ::Any, g2) = coveredby(GI.geometry(g1), g2)
+_coveredby(::Any, g1, t2::GI.FeatureTrait, g2) = coveredby(g1, GI.geometry(g2))
+_coveredby(::FeatureTrait, g1, ::FeatureTrait, g2) = coveredby(GI.geometry(g1), GI.geometry(g2))
Points coveredby geometries
_coveredby(
+ ::GI.PointTrait, g1,
+ ::GI.PointTrait, g2,
+) = equals(g1, g2)
_coveredby(
+ ::GI.PointTrait, g1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _point_curve_process(
+ g1, g2;
+ COVEREDBY_ALLOWS...,
+ closed_curve = false,
+)
_coveredby(
+ ::GI.PointTrait, g1,
+ ::GI.LinearRingTrait, g2,
+) = _point_curve_process(
+ g1, g2;
+ COVEREDBY_ALLOWS...,
+ closed_curve = true,
+)
_coveredby(
+ ::GI.PointTrait, g1,
+ ::GI.PolygonTrait, g2,
+) = _point_polygon_process(
+ g1, g2;
+ COVEREDBY_ALLOWS...,
+ COVEREDBY_EXACT...,
+)
_coveredby(
+ ::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+ ::GI.PointTrait, g2,
+) = false
Lines coveredby geometries
#= Linestring is coveredby a line if all interior and boundary points of the
+first line are on the interior/boundary points of the second line. =#
+_coveredby(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _line_curve_process(
+ g1, g2;
+ COVEREDBY_CURVE_ALLOWS...,
+ COVEREDBY_CURVE_REQUIRES...,
+ COVEREDBY_EXACT...,
+ closed_line = false,
+ closed_curve = false,
+)
+
+#= Linestring is coveredby a ring if all interior and boundary points of the
+line are on the edges of the ring. =#
+_coveredby(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+ ::GI.LinearRingTrait, g2,
+) = _line_curve_process(
+ g1, g2;
+ COVEREDBY_CURVE_ALLOWS...,
+ COVEREDBY_CURVE_REQUIRES...,
+ COVEREDBY_EXACT...,
+ closed_line = false,
+ closed_curve = true,
+)
+
+#= Linestring is coveredby a polygon if all interior and boundary points of the
+line are in the polygon interior or on its edges, inlcuding hole edges. =#
+_coveredby(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+ ::GI.PolygonTrait, g2,
+) = _line_polygon_process(
+ g1, g2;
+ COVEREDBY_ALLOWS...,
+ COVEREDBY_CURVE_REQUIRES...,
+ COVEREDBY_EXACT...,
+ closed_line = false,
+)
Rings covered by geometries
#= Linearring is covered by a line if all vertices and edges of the ring are on
+the edges and vertices of the line. =#
+_coveredby(
+ ::GI.LinearRingTrait, g1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _line_curve_process(
+ g1, g2;
+ COVEREDBY_CURVE_ALLOWS...,
+ COVEREDBY_CURVE_REQUIRES...,
+ COVEREDBY_EXACT...,
+ closed_line = true,
+ closed_curve = false,
+)
+
+#= Linearring is covered by another linear ring if all vertices and edges of the
+first ring are on the edges/vertices of the second ring. =#
+_coveredby(
+ ::GI.LinearRingTrait, g1,
+ ::GI.LinearRingTrait, g2,
+) = _line_curve_process(
+ g1, g2;
+ COVEREDBY_CURVE_ALLOWS...,
+ COVEREDBY_CURVE_REQUIRES...,
+ COVEREDBY_EXACT...,
+ closed_line = true,
+ closed_curve = true,
+)
+
+#= Linearring is coveredby a polygon if all vertices and edges of the ring are
+in the polygon interior or on the polygon edges, inlcuding hole edges. =#
+_coveredby(
+ ::GI.LinearRingTrait, g1,
+ ::GI.PolygonTrait, g2,
+) = _line_polygon_process(
+ g1, g2;
+ COVEREDBY_ALLOWS...,
+ COVEREDBY_CURVE_REQUIRES...,
+ COVEREDBY_EXACT...,
+ closed_line = true,
+)
Polygons covered by geometries
#= Polygon is covered by another polygon if if the interior and edges of the
+first polygon are in the second polygon interior or on polygon edges, including
+hole edges.=#
+_coveredby(
+ ::GI.PolygonTrait, g1,
+ ::GI.PolygonTrait, g2,
+) = _polygon_polygon_process(
+ g1, g2;
+ COVEREDBY_ALLOWS...,
+ COVEREDBY_POLYGON_REQUIRES...,
+ COVEREDBY_EXACT...,
+)
_coveredby(
+ ::GI.PolygonTrait, g1,
+ ::GI.AbstractCurveTrait, g2,
+) = false
Geometries coveredby multi-geometry/geometry collections
#= Geometry is covered by a multi-geometry or a collection if one of the elements
+of the collection cover the geometry. =#
+function _coveredby(
+ ::Union{GI.PointTrait, GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+ ::Union{
+ GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+ GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+ }, g2,
+)
+ for sub_g2 in GI.getgeom(g2)
+ coveredby(g1, sub_g2) && return true
+ end
+ return false
+end
Multi-geometry/geometry collections coveredby geometries
#= Multi-geometry or a geometry collection is covered by a geometry if all
+elements of the collection are covered by the geometry. =#
+function _coveredby(
+ ::Union{
+ GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+ GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+ }, g1,
+ ::GI.AbstractGeometryTrait, g2,
+)
+ for sub_g1 in GI.getgeom(g1)
+ !coveredby(sub_g1, g2) && return false
+ end
+ return true
+end
Covers
export covers
What is covers?
import GeometryOps as GO
+import GeoInterface as GI
+using Makie
+using CairoMakie
+
+p1 = (0.0, 0.0)
+p2 = (1.0, 1.0)
+l1 = GI.Line([p1, p2])
+
+f, a, p = lines(GI.getpoint(l1))
+scatter!(p1, color = :red)
+f
GO.covers(l1, p1) # returns true
+GO.covers(p1, l1) # returns false
false
Implementation
"""
+ covers(g1::AbstractGeometry, g2::AbstractGeometry)::Bool
+
+Return true if the first geometry is completely covers the second geometry,
+The exterior and boundary of the second geometry must not be outside of the
+interior and boundary of the first geometry. However, the interiors need not
+intersect.
+
+\`covers\` returns the exact opposite result of \`coveredby\`.
+
+# Examples
+
+\`\`\`jldoctest
+import GeometryOps as GO, GeoInterface as GI
+l1 = GI.LineString([(1.0, 1.0), (1.0, 2.0), (1.0, 3.0), (1.0, 4.0)])
+l2 = GI.LineString([(1.0, 1.0), (1.0, 2.0)])
+
+GO.covers(l1, l2)
true
+\`\`\`
+"""
+covers(g1, g2)::Bool = GeometryOps.coveredby(g2, g1)
Crossing checks
"""
+ crosses(geom1, geom2)::Bool
+
+Return \`true\` if the intersection results in a geometry whose dimension is one less than
+the maximum dimension of the two source geometries and the intersection set is interior to
+both source geometries.
+
+TODO: broken
+
+# Examples
+\`\`\`julia
+import GeoInterface as GI, GeometryOps as GO
\`\`\`
+"""
+crosses(g1, g2)::Bool = crosses(trait(g1), g1, trait(g2), g2)::Bool
+crosses(t1::FeatureTrait, g1, t2, g2)::Bool = crosses(GI.geometry(g1), g2)
+crosses(t1, g1, t2::FeatureTrait, g2)::Bool = crosses(g1, geometry(g2))
+crosses(::MultiPointTrait, g1, ::LineStringTrait, g2)::Bool = multipoint_crosses_line(g1, g2)
+crosses(::MultiPointTrait, g1, ::PolygonTrait, g2)::Bool = multipoint_crosses_poly(g1, g2)
+crosses(::LineStringTrait, g1, ::MultiPointTrait, g2)::Bool = multipoint_crosses_lines(g2, g1)
+crosses(::LineStringTrait, g1, ::PolygonTrait, g2)::Bool = line_crosses_poly(g1, g2)
+crosses(::LineStringTrait, g1, ::LineStringTrait, g2)::Bool = line_crosses_line(g1, g2)
+crosses(::PolygonTrait, g1, ::MultiPointTrait, g2)::Bool = multipoint_crosses_poly(g2, g1)
+crosses(::PolygonTrait, g1, ::LineStringTrait, g2)::Bool = line_crosses_poly(g2, g1)
+
+function multipoint_crosses_line(geom1, geom2)
+ int_point = false
+ ext_point = false
+ i = 1
+ np2 = GI.npoint(geom2)
+
+ while i < GI.npoint(geom1) && !int_point && !ext_point
+ for j in 1:GI.npoint(geom2) - 1
+ exclude_boundary = (j === 1 || j === np2 - 2) ? :none : :both
+ if _point_on_segment(GI.getpoint(geom1, i), (GI.getpoint(geom2, j), GI.getpoint(geom2, j + 1)); exclude_boundary)
+ int_point = true
+ else
+ ext_point = true
+ end
+ end
+ i += 1
+ end
+ return int_point && ext_point
+end
+
+function line_crosses_line(line1, line2)
+ np2 = GI.npoint(line2)
+ if GeometryOps.intersects(line1, line2)
+ for i in 1:GI.npoint(line1) - 1
+ for j in 1:GI.npoint(line2) - 1
+ exclude_boundary = (j === 1 || j === np2 - 2) ? :none : :both
+ pa = GI.getpoint(line1, i)
+ pb = GI.getpoint(line1, i + 1)
+ p = GI.getpoint(line2, j)
+ _point_on_segment(p, (pa, pb); exclude_boundary) && return true
+ end
+ end
+ end
+ return false
+end
+
+function line_crosses_poly(line, poly)
+ for l in flatten(AbstractCurveTrait, poly)
+ intersects(line, l) && return true
+ end
+ return false
+end
+
+function multipoint_crosses_poly(mp, poly)
+ int_point = false
+ ext_point = false
+
+ for p in GI.getpoint(mp)
+ if _point_polygon_process(
+ p, poly;
+ in_allow = true, on_allow = true, out_allow = false, exact = _False()
+ )
+ int_point = true
+ else
+ ext_point = true
+ end
+ int_point && ext_point && return true
+ end
+ return false
+end
+
+#= TODO: Once crosses is swapped over to use the geom relations workflow, can
+delete these helpers. =#
+
+function _point_on_segment(point, (start, stop); exclude_boundary::Symbol=:none)::Bool
+ x, y = GI.x(point), GI.y(point)
+ x1, y1 = GI.x(start), GI.y(start)
+ x2, y2 = GI.x(stop), GI.y(stop)
+
+ dxc = x - x1
+ dyc = y - y1
+ dx1 = x2 - x1
+ dy1 = y2 - y1
cross = dxc * dy1 - dyc * dx1
+ cross != 0 && return false
if exclude_boundary === :none
+ if abs(dx1) >= abs(dy1)
+ return dx1 > 0 ? x1 <= x && x <= x2 : x2 <= x && x <= x1
+ end
+ return dy1 > 0 ? y1 <= y && y <= y2 : y2 <= y && y <= y1
+ elseif exclude_boundary === :start
+ if abs(dx1) >= abs(dy1)
+ return dx1 > 0 ? x1 < x && x <= x2 : x2 <= x && x < x1
+ end
+ return dy1 > 0 ? y1 < y && y <= y2 : y2 <= y && y < y1
+ elseif exclude_boundary === :end
+ if abs(dx1) >= abs(dy1)
+ return dx1 > 0 ? x1 <= x && x < x2 : x2 < x && x <= x1
+ end
+ return dy1 > 0 ? y1 <= y && y < y2 : y2 < y && y <= y1
+ elseif exclude_boundary === :both
+ if abs(dx1) >= abs(dy1)
+ return dx1 > 0 ? x1 < x && x < x2 : x2 < x && x < x1
+ end
+ return dy1 > 0 ? y1 < y && y < y2 : y2 < y && y < y1
+ end
+ return false
+end
Disjoint
export disjoint
What is disjoint?
import GeometryOps as GO
+import GeoInterface as GI
+using Makie
+using CairoMakie
+
+l1 = GI.LineString([(0.0, 0.0), (1.0, 0.0), (0.0, 0.1)])
+l2 = GI.LineString([(2.0, 0.0), (2.75, 0.0)])
+f, a, p = lines(GI.getpoint(l1), color = :blue)
+scatter!(GI.getpoint(l1), color = :blue)
+lines!(GI.getpoint(l2), color = :orange)
+scatter!(GI.getpoint(l2), color = :orange)
+f
GO.disjoint(l1, l2) # returns true
true
Implementation
disjoint
function and arguments g1 and g2, this criteria is as follows: - points of g1 are not allowed to be in the interior of g2 - points of g1 are not allowed to be on the boundary of g2 - points of g1 are allowed to be in the exterior of g2 - no points required to be in the interior of g2 - no points of g1 are required to be on the boundary of g2 - no points of g1 are required to be in the exterior of g2const DISJOINT_ALLOWS = (in_allow = false, on_allow = false, out_allow = true)
+const DISJOINT_CURVE_ALLOWS = (over_allow = false, cross_allow = false, on_allow = false, out_allow = true)
+const DISJOINT_REQUIRES = (in_require = false, on_require = false, out_require = false)
+const DISJOINT_EXACT = (exact = _False(),)
+
+"""
+ disjoint(geom1, geom2)::Bool
+
+Return \`true\` if the first geometry is disjoint from the second geometry.
+
+Return \`true\` if the first geometry is disjoint from the second geometry. The
+interiors and boundaries of both geometries must not intersect.
+
+# Examples
+\`\`\`jldoctest setup=:(using GeometryOps, GeoInterface)
+import GeometryOps as GO, GeoInterface as GI
+
+line = GI.LineString([(1, 1), (1, 2), (1, 3), (1, 4)])
+point = (2, 2)
+GO.disjoint(point, line)
true
+\`\`\`
+"""
+disjoint(g1, g2) = _disjoint(trait(g1), g1, trait(g2), g2)
Convert features to geometries
_disjoint(::FeatureTrait, g1, ::Any, g2) = disjoint(GI.geometry(g1), g2)
+_disjoint(::Any, g1, ::FeatureTrait, g2) = disjoint(g1, geometry(g2))
+_disjoint(::FeatureTrait, g1, ::FeatureTrait, g2) = disjoint(GI.geometry(g1), GI.geometry(g2))
Point disjoint geometries
_disjoint(
+ ::GI.PointTrait, g1,
+ ::GI.PointTrait, g2,
+) = !equals(g1, g2)
_disjoint(
+ ::GI.PointTrait, g1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _point_curve_process(
+ g1, g2;
+ DISJOINT_ALLOWS...,
+ closed_curve = false,
+)
_disjoint(
+ ::GI.PointTrait, g1,
+ ::GI.LinearRingTrait, g2,
+) = _point_curve_process(
+ g1, g2;
+ DISJOINT_ALLOWS...,
+ closed_curve = true,
+)
+
+#= Point is disjoint from a polygon if it is not on any edges, vertices, or
+within the polygon's interior. =#
+_disjoint(
+ ::GI.PointTrait, g1,
+ ::GI.PolygonTrait, g2,
+) = _point_polygon_process(
+ g1, g2;
+ DISJOINT_ALLOWS...,
+ DISJOINT_EXACT...,
+)
+
+#= Geometry is disjoint from a point if the point is not in the interior or on
+the boundary of the geometry. =#
+_disjoint(
+ trait1::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+ trait2::GI.PointTrait, g2,
+) = _disjoint(trait2, g2, trait1, g1)
Lines disjoint geometries
#= Linestring is disjoint from another line if they do not share any interior
+edge/vertex points or boundary points. =#
+_disjoint(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _line_curve_process(
+ g1, g2;
+ DISJOINT_CURVE_ALLOWS...,
+ DISJOINT_REQUIRES...,
+ DISJOINT_EXACT...,
+ closed_line = false,
+ closed_curve = false,
+)
+
+#= Linestring is disjoint from a linearring if they do not share any interior
+edge/vertex points or boundary points. =#
+_disjoint(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+ ::GI.LinearRingTrait, g2,
+) = _line_curve_process(
+ g1, g2;
+ DISJOINT_CURVE_ALLOWS...,
+ DISJOINT_REQUIRES...,
+ DISJOINT_EXACT...,
+ closed_line = false,
+ closed_curve = true,
+)
+
+#= Linestring is disjoint from a polygon if the interior and boundary points of
+the line are not in the polygon's interior or on the polygon's boundary. =#
+_disjoint(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+ ::GI.PolygonTrait, g2,
+) = _line_polygon_process(
+ g1, g2;
+ DISJOINT_ALLOWS...,
+ DISJOINT_REQUIRES...,
+ DISJOINT_EXACT...,
+ closed_line = false,
+)
+
+#= Geometry is disjoint from a linestring if the line's interior and boundary
+points don't intersect with the geometrie's interior and boundary points. =#
+_disjoint(
+ trait1::Union{GI.LinearRingTrait, GI.PolygonTrait}, g1,
+ trait2::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _disjoint(trait2, g2, trait1, g1)
Rings disjoint geometries
#= Linearrings is disjoint from another linearring if they do not share any
+interior edge/vertex points or boundary points.=#
+_disjoint(
+ ::GI.LinearRingTrait, g1,
+ ::GI.LinearRingTrait, g2,
+) = _line_curve_process(
+ g1, g2;
+ DISJOINT_CURVE_ALLOWS...,
+ DISJOINT_REQUIRES...,
+ DISJOINT_EXACT...,
+ closed_line = true,
+ closed_curve = true,
+)
+
+#= Linearring is disjoint from a polygon if the interior and boundary points of
+the ring are not in the polygon's interior or on the polygon's boundary. =#
+_disjoint(
+ ::GI.LinearRingTrait, g1,
+ ::GI.PolygonTrait, g2,
+) = _line_polygon_process(
+ g1, g2;
+ DISJOINT_ALLOWS...,
+ DISJOINT_REQUIRES...,
+ DISJOINT_EXACT...,
+ closed_line = true,
+)
Polygon disjoint geometries
#= Polygon is disjoint from another polygon if they do not share any edges or
+vertices and if their interiors do not intersect, excluding any holes. =#
+_disjoint(
+ ::GI.PolygonTrait, g1,
+ ::GI.PolygonTrait, g2,
+) = _polygon_polygon_process(
+ g1, g2;
+ DISJOINT_ALLOWS...,
+ DISJOINT_REQUIRES...,
+ DISJOINT_EXACT...,
+)
Geometries disjoint multi-geometry/geometry collections
#= Geometry is disjoint from a multi-geometry or a collection if all of the
+elements of the collection are disjoint from the geometry. =#
+function _disjoint(
+ ::Union{GI.PointTrait, GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+ ::Union{
+ GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+ GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+ }, g2,
+)
+ for sub_g2 in GI.getgeom(g2)
+ !disjoint(g1, sub_g2) && return false
+ end
+ return true
+end
Multi-geometry/geometry collections coveredby geometries
#= Multi-geometry or a geometry collection is covered by a geometry if all
+elements of the collection are covered by the geometry. =#
+function _disjoint(
+ ::Union{
+ GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+ GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+ }, g1,
+ ::GI.AbstractGeometryTrait, g2,
+)
+ for sub_g1 in GI.getgeom(g1)
+ !disjoint(sub_g1, g2) && return false
+ end
+ return true
+end
Line-curve interaction
#= Code is based off of DE-9IM Standards (https://en.wikipedia.org/wiki/DE-9IM)
+and attempts a standardized solution for most of the functions.
+=#
+
+"""
+ Enum PointOrientation
+
+Enum for the orientation of a point with respect to a curve. A point can be
+\`point_in\` the curve, \`point_on\` the curve, or \`point_out\` of the curve.
+"""
+@enum PointOrientation point_in=1 point_on=2 point_out=3
function _point_curve_process(
+ point, curve;
+ in_allow, on_allow, out_allow,
+ closed_curve = false,
+)
n = GI.npoint(curve)
+ first_last_equal = equals(GI.getpoint(curve, 1), GI.getpoint(curve, n))
+ closed_curve |= first_last_equal
+ n -= first_last_equal ? 1 : 0
p_start = GI.getpoint(curve, closed_curve ? n : 1)
+ @inbounds for i in (closed_curve ? 1 : 2):n
+ p_end = GI.getpoint(curve, i)
+ seg_val = _point_segment_orientation(point, p_start, p_end)
+ seg_val == point_in && return in_allow
+ if seg_val == point_on
+ if !closed_curve # if point is on curve endpoints, it is "on"
+ i == 2 && equals(point, p_start) && return on_allow
+ i == n && equals(point, p_end) && return on_allow
+ end
+ return in_allow
+ end
+ p_start = p_end
+ end
+ return out_allow
+end
function _point_polygon_process(
+ point, polygon;
+ in_allow, on_allow, out_allow, exact,
+)
ext_val = _point_filled_curve_orientation(point, GI.getexterior(polygon); exact)
ext_val == point_out && return out_allow
ext_val == point_on && return on_allow
for hole in GI.gethole(polygon)
+ hole_val = _point_filled_curve_orientation(point, hole; exact)
hole_val == point_in && return out_allow
hole_val == point_on && return on_allow
+ end
return in_allow
+end
@inline function _line_curve_process(line, curve;
+ over_allow, cross_allow, kw...
+)
+ skip, returnval = _maybe_skip_disjoint_extents(line, curve;
+ in_allow=(over_allow | cross_allow), kw...
+ )
+ skip && return returnval
+
+ return _inner_line_curve_process(line, curve; over_allow, cross_allow, kw...)
+end
+
+function _inner_line_curve_process(
+ line, curve;
+ over_allow, cross_allow, on_allow, out_allow,
+ in_require, on_require, out_require,
+ closed_line = false, closed_curve = false,
+ exact,
+)
in_req_met = !in_require
+ on_req_met = !on_require
+ out_req_met = !out_require
nl = GI.npoint(line)
+ nc = GI.npoint(curve)
+ first_last_equal_line = equals(GI.getpoint(line, 1), GI.getpoint(line, nl))
+ first_last_equal_curve = equals(GI.getpoint(curve, 1), GI.getpoint(curve, nc))
+ nl -= first_last_equal_line ? 1 : 0
+ nc -= first_last_equal_curve ? 1 : 0
+ closed_line |= first_last_equal_line
+ closed_curve |= first_last_equal_curve
l_start = _tuple_point(GI.getpoint(line, closed_line ? nl : 1))
+ i = closed_line ? 1 : 2
+ while i ≤ nl
+ l_end = _tuple_point(GI.getpoint(line, i))
+ c_start = _tuple_point(GI.getpoint(curve, closed_curve ? nc : 1))
for j in (closed_curve ? 1 : 2):nc
+ c_end = _tuple_point(GI.getpoint(curve, j))
seg_val, intr1, _ = _intersection_point(Float64, (l_start, l_end), (c_start, c_end); exact)
if seg_val == line_over
+ !over_allow && return false
in_req_met = true
+ point_val = _point_segment_orientation(l_start, c_start, c_end)
if point_val != point_out
+ i, l_start, break_off = _find_new_seg(i, l_start, l_end, c_start, c_end)
+ break_off && break
+ end
+ else
+ if seg_val == line_cross
+ !cross_allow && return false
+ in_req_met = true
+ elseif seg_val == line_hinge # could cross or overlap
(_, (α, β)) = intr1
+ if ( # Don't consider edges of curves as they can't cross
+ (!closed_line && ((α == 0 && i == 2) || (α == 1 && i == nl))) ||
+ (!closed_curve && ((β == 0 && j == 2) || (β == 1 && j == nc)))
+ )
+ !on_allow && return false
+ on_req_met = true
+ else
+ in_req_met = true
if (!cross_allow || !over_allow) && α != 0 && β != 0
l, c = _find_hinge_next_segments(
+ α, β, l_start, l_end, c_start, c_end,
+ i, line, j, curve,
+ )
+ next_val, _, _ = _intersection_point(Float64, l, c; exact)
+ if next_val == line_hinge
+ !cross_allow && return false
+ else
+ !over_allow && return false
+ end
+ end
+ end
+ end
if j == nc
+ !out_allow && return false
+ out_req_met = true
+ end
+ end
+ c_start = c_end # consider next segment of curve
+ if j == nc # move on to next line segment
+ i += 1
+ l_start = l_end
+ end
+ end
+ end
+ return in_req_met && on_req_met && out_req_met
+end
+
+#= If entire segment (le to ls) isn't covered by segment (cs to ce), find remaining section
+part of section outside of cs to ce. If completly covered, increase segment index i. =#
+function _find_new_seg(i, ls, le, cs, ce)
+ break_off = true
+ if _point_segment_orientation(le, cs, ce) != point_out
+ ls = le
+ i += 1
+ elseif !equals(ls, cs) && _point_segment_orientation(cs, ls, le) != point_out
+ ls = cs
+ elseif !equals(ls, ce) && _point_segment_orientation(ce, ls, le) != point_out
+ ls = ce
+ else
+ break_off = false
+ end
+ return i, ls, break_off
+end
+
+#= Find next set of segments needed to determine if given hinge segments cross or not.=#
+function _find_hinge_next_segments(α, β, ls, le, cs, ce, i, line, j, curve)
+ next_seg = if β == 1
+ if α == 1 # hinge at endpoints, so next segment of both is needed
+ ((le, _tuple_point(GI.getpoint(line, i + 1))), (ce, _tuple_point(GI.getpoint(curve, j + 1))))
+ else # hinge at curve endpoint and line interior point, curve next segment needed
+ ((ls, le), (ce, _tuple_point(GI.getpoint(curve, j + 1))))
+ end
+ else # hinge at curve interior point and line endpoint, line next segment needed
+ ((le, _tuple_point(GI.getpoint(line, i + 1))), (cs, ce))
+ end
+ return next_seg
+end
@inline function _line_polygon_process(line, polygon; kw...)
+ skip, returnval = _maybe_skip_disjoint_extents(line, polygon; kw...)
+ skip && return returnval
+ return _inner_line_polygon_process(line, polygon; kw...)
+end
+
+function _inner_line_polygon_process(
+ line, polygon;
+ in_allow, on_allow, out_allow,
+ in_require, on_require, out_require,
+ exact, closed_line = false,
+)
+ in_req_met = !in_require
+ on_req_met = !on_require
+ out_req_met = !out_require
in_curve, on_curve, out_curve = _line_filled_curve_interactions(
+ line, GI.getexterior(polygon);
+ exact, closed_line = closed_line,
+ )
+ if on_curve
+ !on_allow && return false
+ on_req_met = true
+ end
+ if out_curve
+ !out_allow && return false
+ out_req_met = true
+ end
!in_curve && return in_req_met && on_req_met && out_req_met
for hole in GI.gethole(polygon)
+ in_hole, on_hole, out_hole =_line_filled_curve_interactions(
+ line, hole;
+ exact, closed_line = closed_line,
+ )
+ if in_hole # line in hole is equivalent to being out of polygon
+ !out_allow && return false
+ out_req_met = true
+ end
+ if on_hole # hole bounday is polygon boundary
+ !on_allow && return false
+ on_req_met = true
+ end
+ if !out_hole # entire line is in/on hole, can't be in/on other holes
+ in_curve = false
+ break
+ end
+ end
+ if in_curve # entirely of curve isn't within a hole
+ !in_allow && return false
+ in_req_met = true
+ end
+ return in_req_met && on_req_met && out_req_met
+end
@inline function _polygon_polygon_process(poly1, poly2; kw...)
+ skip, returnval = _maybe_skip_disjoint_extents(poly1, poly2; kw...)
+ skip && return returnval
+ return _inner_polygon_polygon_process(poly1, poly2; kw...)
+end
+
+function _inner_polygon_polygon_process(
+ poly1, poly2;
+ in_allow, on_allow, out_allow,
+ in_require, on_require, out_require,
+ exact,
+)
+ in_req_met = !in_require
+ on_req_met = !on_require
+ out_req_met = !out_require
ext1 = GI.getexterior(poly1)
+ ext2 = GI.getexterior(poly2)
e1_in_p2, e1_on_p2, e1_out_p2 = _line_polygon_interactions(
+ ext1, poly2;
+ exact, closed_line = true,
+ )
+ if e1_on_p2
+ !on_allow && return false
+ on_req_met = true
+ end
+ if e1_out_p2
+ !out_allow && return false
+ out_req_met = true
+ end
+
+ if !e1_in_p2
_, _, e2_out_e1 = _line_filled_curve_interactions(
+ ext2, ext1;
+ exact, closed_line = true,
+ ) # if they really are disjoint, we are done
+ e2_out_e1 && return in_req_met && on_req_met && out_req_met
+ end
for h1 in GI.gethole(poly1)
+ h1_in_p2, h1_on_p2, h1_out_p2 = _line_polygon_interactions(
+ h1, poly2;
+ exact, closed_line = true,
+ )
+ if h1_on_p2
+ !on_allow && return false
+ on_req_met = true
+ end
+ if h1_out_p2
+ !out_allow && return false
+ out_req_met = true
+ end
+ if !h1_in_p2
_, _, e2_out_h1 = _line_filled_curve_interactions(
+ ext2, h1;
+ exact, closed_line = true,
+ )
!e2_out_h1 && return in_req_met && on_req_met && out_req_met
+ break
+ end
+ end
+ #=
+ poly2 isn't outside of poly1 and isn't in a hole, poly1 interior must
+ interact with poly2 interior
+ =#
+ !in_allow && return false
+ in_req_met = true
for h2 in GI.gethole(poly2)
+ h2_in_p1, h2_on_p1, _ = _line_polygon_interactions(
+ h2, poly1;
+ exact, closed_line = true,
+ )
+ if h2_on_p1
+ !on_allow && return false
+ on_req_met = true
+ end
+ if h2_in_p1
+ !out_allow && return false
+ out_req_met = true
+ end
+ end
+ return in_req_met && on_req_met && out_req_met
+end
on
the segment it is on one of the segments endpoints. If it is in
, it is on any other point of the segment. If the point is not on any part of the segment, it is out
of the segment.function _point_segment_orientation(
+ point, start, stop;
+ in::T = point_in, on::T = point_on, out::T = point_out,
+) where {T}
x, y = GI.x(point), GI.y(point)
+ x1, y1 = GI.x(start), GI.y(start)
+ x2, y2 = GI.x(stop), GI.y(stop)
+ Δx_seg = x2 - x1
+ Δy_seg = y2 - y1
+ Δx_pt = x - x1
+ Δy_pt = y - y1
+ if (Δx_pt == 0 && Δy_pt == 0) || (Δx_pt == Δx_seg && Δy_pt == Δy_seg)
return on
+ else
+ #=
+ Determine if the point is on the segment -> see if vector from segment
+ start to point is parallel to segment and if point is between the
+ segment endpoints
+ =#
+ on_line = _isparallel(Δx_seg, Δy_seg, Δx_pt, Δy_pt)
+ !on_line && return out
+ between_endpoints =
+ (x2 > x1 ? x1 <= x <= x2 : x2 <= x <= x1) &&
+ (y2 > y1 ? y1 <= y <= y2 : y2 <= y <= y1)
+ !between_endpoints && return out
+ end
+ return in
+end
In
means the point is within the closed curve (excluding edges and vertices). On
means the point is on an edge or a vertex of the closed curve. Out
means the point is outside of the closed curve.function _point_filled_curve_orientation(
+ point, curve;
+ in::T = point_in, on::T = point_on, out::T = point_out, exact,
+) where {T}
+ x, y = GI.x(point), GI.y(point)
+ n = GI.npoint(curve)
+ n -= equals(GI.getpoint(curve, 1), GI.getpoint(curve, n)) ? 1 : 0
+ k = 0 # counter for ray crossings
+ p_start = GI.getpoint(curve, n)
+ for (i, p_end) in enumerate(GI.getpoint(curve))
+ i > n && break
+ v1 = GI.y(p_start) - y
+ v2 = GI.y(p_end) - y
+ if !((v1 < 0 && v2 < 0) || (v1 > 0 && v2 > 0)) # if not cases 11 or 26
+ u1, u2 = GI.x(p_start) - x, GI.x(p_end) - x
+ f = Predicates.cross((u1, u2), (v1, v2); exact)
+ if v2 > 0 && v1 ≤ 0 # Case 3, 9, 16, 21, 13, or 24
+ f == 0 && return on # Case 16 or 21
+ f > 0 && (k += 1) # Case 3 or 9
+ elseif v1 > 0 && v2 ≤ 0 # Case 4, 10, 19, 20, 12, or 25
+ f == 0 && return on # Case 19 or 20
+ f < 0 && (k += 1) # Case 4 or 10
+ elseif v2 == 0 && v1 < 0 # Case 7, 14, or 17
+ f == 0 && return on # Case 17
+ elseif v1 == 0 && v2 < 0 # Case 8, 15, or 18
+ f == 0 && return on # Case 18
+ elseif v1 == 0 && v2 == 0 # Case 1, 2, 5, 6, 22, or 23
+ u2 ≤ 0 && u1 ≥ 0 && return on # Case 1
+ u1 ≤ 0 && u2 ≥ 0 && return on # Case 2
+ end
+ end
+ p_start = p_end
+ end
+ return iseven(k) ? out : in
+end
function _line_filled_curve_interactions(
+ line, curve;
+ exact, closed_line = false,
+)
+ in_curve = false
+ on_curve = false
+ out_curve = false
nl = GI.npoint(line)
+ nc = GI.npoint(curve)
+ first_last_equal_line = equals(GI.getpoint(line, 1), GI.getpoint(line, nl))
+ first_last_equal_curve = equals(GI.getpoint(curve, 1), GI.getpoint(curve, nc))
+ nl -= first_last_equal_line ? 1 : 0
+ nc -= first_last_equal_curve ? 1 : 0
+ closed_line |= first_last_equal_line
l_start = _tuple_point(GI.getpoint(line, closed_line ? nl : 1))
+ point_val = _point_filled_curve_orientation(l_start, curve; exact)
+ if point_val == point_in
+ in_curve = true
+ elseif point_val == point_on
+ on_curve = true
+ else # point_val == point_out
+ out_curve = true
+ end
for i in (closed_line ? 1 : 2):nl
+ l_end = _tuple_point(GI.getpoint(line, i))
+ c_start = _tuple_point(GI.getpoint(curve, nc))
in_curve && on_curve && out_curve && break
for j in 1:nc
+ c_end = _tuple_point(GI.getpoint(curve, j))
seg_val, _, _ = _intersection_point(Float64, (l_start, l_end), (c_start, c_end); exact)
+ if seg_val != line_out
on_curve = true
+ if seg_val == line_cross
in_curve = true
+ out_curve = true
+ else
+ if seg_val == line_over
+ sp = _point_segment_orientation(l_start, c_start, c_end)
+ lp = _point_segment_orientation(l_end, c_start, c_end)
+ if sp != point_in || lp != point_in
+ #=
+ Line crosses over segment endpoint, creating a hinge
+ with another segment.
+ =#
+ seg_val = line_hinge
+ end
+ end
+ if seg_val == line_hinge
+ #=
+ Can't determine all types of interactions (in, out) with
+ hinge as it could pass through multiple other segments
+ so calculate if segment endpoints and intersections are
+ in/out of filled curve
+ =#
+ ipoints = intersection_points(GI.Line(StaticArrays.SVector(l_start, l_end)), curve)
+ npoints = length(ipoints) # since hinge, at least one
+ dist_from_lstart = let l_start = l_start
+ x -> _euclid_distance(Float64, x, l_start)
+ end
+ sort!(ipoints, by = dist_from_lstart)
+ p_start = _tuple_point(l_start)
+ for i in 1:(npoints + 1)
+ p_end = i ≤ npoints ? _tuple_point(ipoints[i]) : l_end
+ mid_val = _point_filled_curve_orientation((p_start .+ p_end) ./ 2, curve; exact)
+ if mid_val == point_in
+ in_curve = true
+ elseif mid_val == point_out
+ out_curve = true
+ end
+ end
l_start = l_end
+ break
+ end
+ end
+ end
+ c_start = c_end
+ end
+ l_start = l_end
+ end
+ return in_curve, on_curve, out_curve
+end
function _line_polygon_interactions(
+ line, polygon;
+ exact, closed_line = false,
+)
+
+ in_poly, on_poly, out_poly = _line_filled_curve_interactions(
+ line, GI.getexterior(polygon);
+ exact, closed_line = closed_line,
+ )
+ !in_poly && return (in_poly, on_poly, out_poly)
for hole in GI.gethole(polygon)
+ in_hole, on_hole, out_hole =_line_filled_curve_interactions(
+ line, hole;
+ exact, closed_line = closed_line,
+ )
+ if in_hole
+ out_poly = true
+ end
+ if on_hole
+ on_poly = true
+ end
+ if !out_hole # entire line is in/on hole, can't be in/on other holes
+ in_poly = false
+ return (in_poly, on_poly, out_poly)
+ end
+ end
+ return in_poly, on_poly, out_poly
+end
@inline function _maybe_skip_disjoint_extents(a, b;
+ in_allow, on_allow, out_allow,
+ in_require, on_require, out_require,
+ kw...
+)
+ ext_disjoint = Extents.disjoint(GI.extent(a), GI.extent(b))
+ skip, returnval = if !ext_disjoint
false, false
+ elseif out_allow # && ext_disjoint
+ if in_require || on_require
+ true, false
+ else
+ true, true
+ end
+ else # !out_allow && ext_disjoint
true, false
+ end
+ return skip, returnval
+end
Intersection checks
export intersects
What is
intersects
? import GeometryOps as GO
+import GeoInterface as GI
+using Makie
+using CairoMakie
+
+line1 = GI.Line([(124.584961,-12.768946), (126.738281,-17.224758)])
+line2 = GI.Line([(123.354492,-15.961329), (127.22168,-14.008696)])
+f, a, p = lines(GI.getpoint(line1))
+lines!(GI.getpoint(line2))
+f
GO.intersects(line1, line2) # true
true
Implementation
"""
+ intersects(geom1, geom2)::Bool
+
+Return true if the interiors or boundaries of the two geometries interact.
+
+\`intersects\` returns the exact opposite result of \`disjoint\`.
+
+# Example
+
+\`\`\`jldoctest
+import GeoInterface as GI, GeometryOps as GO
+
+line1 = GI.Line([(124.584961,-12.768946), (126.738281,-17.224758)])
+line2 = GI.Line([(123.354492,-15.961329), (127.22168,-14.008696)])
+GO.intersects(line1, line2)
true
+\`\`\`
+"""
+intersects(geom1, geom2) = !disjoint(geom1, geom2)
Overlaps
export overlaps
What is overlaps?
import GeometryOps as GO
+import GeoInterface as GI
+using Makie
+using CairoMakie
+
+l1 = GI.LineString([(0.0, 0.0), (0.0, 10.0)])
+l2 = GI.LineString([(0.0, -10.0), (0.0, 3.0)])
+f, a, p = lines(GI.getpoint(l1), color = :blue)
+scatter!(GI.getpoint(l1), color = :blue)
+lines!(GI.getpoint(l2), color = :orange)
+scatter!(GI.getpoint(l2), color = :orange)
+f
GO.overlaps(l1, l2) # true
true
Implementation
"""
+ overlaps(geom1, geom2)::Bool
+
+Compare two Geometries of the same dimension and return true if their
+intersection set results in a geometry different from both but of the same
+dimension. This means one geometry cannot be within or contain the other and
+they cannot be equal
+
+# Examples
+\`\`\`jldoctest
+import GeometryOps as GO, GeoInterface as GI
+poly1 = GI.Polygon([[(0,0), (0,5), (5,5), (5,0), (0,0)]])
+poly2 = GI.Polygon([[(1,1), (1,6), (6,6), (6,1), (1,1)]])
+
+GO.overlaps(poly1, poly2)
true
+\`\`\`
+"""
+overlaps(geom1, geom2)::Bool = overlaps(
+ GI.trait(geom1),
+ geom1,
+ GI.trait(geom2),
+ geom2,
+)
+
+"""
+ overlaps(::GI.AbstractTrait, geom1, ::GI.AbstractTrait, geom2)::Bool
+
+For any non-specified pair, all have non-matching dimensions, return false.
+"""
+overlaps(::GI.AbstractTrait, geom1, ::GI.AbstractTrait, geom2) = false
+
+"""
+ overlaps(
+ ::GI.MultiPointTrait, points1,
+ ::GI.MultiPointTrait, points2,
+ )::Bool
+
+If the multipoints overlap, meaning some, but not all, of the points within the
+multipoints are shared, return true.
+"""
+function overlaps(
+ ::GI.MultiPointTrait, points1,
+ ::GI.MultiPointTrait, points2,
+)
+ one_diff = false # assume that all the points are the same
+ one_same = false # assume that all points are different
+ for p1 in GI.getpoint(points1)
+ match_point = false
+ for p2 in GI.getpoint(points2)
+ if equals(p1, p2) # Point is shared
+ one_same = true
+ match_point = true
+ break
+ end
+ end
+ one_diff |= !match_point # Point isn't shared
+ one_same && one_diff && return true
+ end
+ return false
+end
+
+"""
+ overlaps(::GI.LineTrait, line1, ::GI.LineTrait, line)::Bool
+
+If the lines overlap, meaning that they are colinear but each have one endpoint
+outside of the other line, return true. Else false.
+"""
+overlaps(::GI.LineTrait, line1, ::GI.LineTrait, line) =
+ _overlaps((a1, a2), (b1, b2))
+
+"""
+ overlaps(
+ ::Union{GI.LineStringTrait, GI.LinearRing}, line1,
+ ::Union{GI.LineStringTrait, GI.LinearRing}, line2,
+ )::Bool
+
+If the curves overlap, meaning that at least one edge of each curve overlaps,
+return true. Else false.
+"""
+function overlaps(
+ ::Union{GI.LineStringTrait, GI.LinearRing}, line1,
+ ::Union{GI.LineStringTrait, GI.LinearRing}, line2,
+)
+ edges_a, edges_b = map(sort! ∘ to_edges, (line1, line2))
+ for edge_a in edges_a
+ for edge_b in edges_b
+ _overlaps(edge_a, edge_b) && return true
+ end
+ end
+ return false
+end
+
+"""
+ overlaps(
+ trait_a::GI.PolygonTrait, poly_a,
+ trait_b::GI.PolygonTrait, poly_b,
+ )::Bool
+
+If the two polygons intersect with one another, but are not equal, return true.
+Else false.
+"""
+function overlaps(
+ trait_a::GI.PolygonTrait, poly_a,
+ trait_b::GI.PolygonTrait, poly_b,
+)
+ edges_a, edges_b = map(sort! ∘ to_edges, (poly_a, poly_b))
+ return _line_intersects(edges_a, edges_b) &&
+ !equals(trait_a, poly_a, trait_b, poly_b)
+end
+
+"""
+ overlaps(
+ ::GI.PolygonTrait, poly1,
+ ::GI.MultiPolygonTrait, polys2,
+ )::Bool
+
+Return true if polygon overlaps with at least one of the polygons within the
+multipolygon. Else false.
+"""
+function overlaps(
+ ::GI.PolygonTrait, poly1,
+ ::GI.MultiPolygonTrait, polys2,
+)
+ for poly2 in GI.getgeom(polys2)
+ overlaps(poly1, poly2) && return true
+ end
+ return false
+end
+
+"""
+ overlaps(
+ ::GI.MultiPolygonTrait, polys1,
+ ::GI.PolygonTrait, poly2,
+ )::Bool
+
+Return true if polygon overlaps with at least one of the polygons within the
+multipolygon. Else false.
+"""
+overlaps(trait1::GI.MultiPolygonTrait, polys1, trait2::GI.PolygonTrait, poly2) =
+ overlaps(trait2, poly2, trait1, polys1)
+
+"""
+ overlaps(
+ ::GI.MultiPolygonTrait, polys1,
+ ::GI.MultiPolygonTrait, polys2,
+ )::Bool
+
+Return true if at least one pair of polygons from multipolygons overlap. Else
+false.
+"""
+function overlaps(
+ ::GI.MultiPolygonTrait, polys1,
+ ::GI.MultiPolygonTrait, polys2,
+)
+ for poly1 in GI.getgeom(polys1)
+ overlaps(poly1, polys2) && return true
+ end
+ return false
+end
+
+#= If the edges overlap, meaning that they are colinear but each have one endpoint
+outside of the other edge, return true. Else false. =#
+function _overlaps(
+ (a1, a2)::Edge,
+ (b1, b2)::Edge,
+ exact = _False(),
+)
seg_val, _, _ = _intersection_point(Float64, (a1, a2), (b1, b2); exact)
a_fully_within = _point_on_seg(a1, b1, b2) && _point_on_seg(a2, b1, b2)
+ b_fully_within = _point_on_seg(b1, a1, a2) && _point_on_seg(b2, a1, a2)
+ return seg_val == line_over && (!a_fully_within && !b_fully_within)
+end
+
+#= TODO: Once overlaps is swapped over to use the geom relations workflow, can
+delete these helpers. =#
function _point_on_seg(point, start, stop)
x, y = GI.x(point), GI.y(point)
+ x1, y1 = GI.x(start), GI.y(start)
+ x2, y2 = GI.x(stop), GI.y(stop)
+ Δxl = x2 - x1
+ Δyl = y2 - y1
cross = (x - x1) * Δyl - (y - y1) * Δxl
+ if cross == 0 # point is on line extending to infinity
if abs(Δxl) >= abs(Δyl) # is line between endpoints
+ return Δxl > 0 ? x1 <= x <= x2 : x2 <= x <= x1
+ else
+ return Δyl > 0 ? y1 <= y <= y2 : y2 <= y <= y1
+ end
+ end
+ return false
+end
+
+#= Returns true if there is at least one intersection between edges within the
+two lists of edges. =#
+function _line_intersects(
+ edges_a::Vector{<:Edge},
+ edges_b::Vector{<:Edge};
+)
for edge_a in edges_a
+ for edge_b in edges_b
+ _line_intersects(edge_a, edge_b) && return true
+ end
+ end
+ return false
+end
function _line_intersects(edge_a::Edge, edge_b::Edge)
+ seg_val, _, _ = _intersection_point(Float64, edge_a, edge_b; exact = _False())
+ return seg_val != line_out
+end
Touches
export touches
What is touches?
import GeometryOps as GO
+import GeoInterface as GI
+using Makie
+using CairoMakie
+
+l1 = GI.Line([(0.0, 0.0), (1.0, 0.0)])
+l2 = GI.Line([(1.0, 0.0), (1.0, -1.0)])
+
+f, a, p = lines(GI.getpoint(l1))
+lines!(GI.getpoint(l2))
+f
GO.touches(l1, l2) # true
true
Implementation
touches
function and arguments g1 and g2, this criteria is as follows: - points of g1 are not allowed to be in the interior of g2 - points of g1 are allowed to be on the boundary of g2 - points of g1 are allowed to be in the exterior of g2 - no points of g1 are required to be in the interior of g2 - at least one point of g1 is required to be on the boundary of g2 - no points of g1 are required to be in the exterior of g2const TOUCHES_POINT_ALLOWED = (in_allow = false, on_allow = true, out_allow = false)
+const TOUCHES_CURVE_ALLOWED = (over_allow = false, cross_allow = false, on_allow = true, out_allow = true)
+const TOUCHES_POLYGON_ALLOWS = (in_allow = false, on_allow = true, out_allow = true)
+const TOUCHES_REQUIRES = (in_require = false, on_require = true, out_require = false)
+const TOUCHES_EXACT = (exact = _False(),)
+
+"""
+ touches(geom1, geom2)::Bool
+
+Return \`true\` if the first geometry touches the second geometry. In other words,
+the two interiors cannot interact, but one of the geometries must have a
+boundary point that interacts with either the other geometies interior or
+boundary.
+
+# Examples
+\`\`\`jldoctest setup=:(using GeometryOps, GeometryBasics)
+import GeometryOps as GO, GeoInterface as GI
+
+l1 = GI.Line([(0.0, 0.0), (1.0, 0.0)])
+l2 = GI.Line([(1.0, 1.0), (1.0, -1.0)])
+
+GO.touches(l1, l2)
true
+\`\`\`
+"""
+touches(g1, g2)::Bool = _touches(trait(g1), g1, trait(g2), g2)
Convert features to geometries
_touches(::GI.FeatureTrait, g1, ::Any, g2) = touches(GI.geometry(g1), g2)
+_touches(::Any, g1, t2::GI.FeatureTrait, g2) = touches(g1, GI.geometry(g2))
+_touches(::FeatureTrait, g1, ::FeatureTrait, g2) = touches(GI.geometry(g1), GI.geometry(g2))
Point touches geometries
_touches(
+ ::GI.PointTrait, g1,
+ ::GI.PointTrait, g2,
+) = false
function _touches(
+ ::GI.PointTrait, g1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+)
+ n = GI.npoint(g2)
+ p1 = GI.getpoint(g2, 1)
+ pn = GI.getpoint(g2, n)
+ equals(p1, pn) && return false
+ return equals(g1, p1) || equals(g1, pn)
+end
_touches(
+ ::GI.PointTrait, g1,
+ ::GI.LinearRingTrait, g2,
+) = false
_touches(
+ ::GI.PointTrait, g1,
+ ::GI.PolygonTrait, g2,
+) = _point_polygon_process(
+ g1, g2;
+ TOUCHES_POINT_ALLOWED...,
+ TOUCHES_EXACT...,
+)
+
+#= Geometry touches a point if the point is on the geometry boundary. =#
+_touches(
+ trait1::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+ trait2::GI.PointTrait, g2,
+) = _touches(trait2, g2, trait1, g1)
Lines touching geometries
#= Linestring touches another line if at least one bounday point interacts with
+the bounday of interior of the other line, but the interiors don't interact. =#
+_touches(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _line_curve_process(
+ g1, g2;
+ TOUCHES_CURVE_ALLOWED...,
+ TOUCHES_REQUIRES...,
+ TOUCHES_EXACT...,
+ closed_line = false,
+ closed_curve = false,
+)
+
+
+#= Linestring touches a linearring if at least one of the boundary points of the
+line interacts with the linear ring, but their interiors can't interact. =#
+_touches(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+ ::GI.LinearRingTrait, g2,
+) = _line_curve_process(
+ g1, g2;
+ TOUCHES_CURVE_ALLOWED...,
+ TOUCHES_REQUIRES...,
+ TOUCHES_EXACT...,
+ closed_line = false,
+ closed_curve = true,
+)
+
+#= Linestring touches a polygon if at least one of the boundary points of the
+line interacts with the boundary of the polygon. =#
+_touches(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+ ::GI.PolygonTrait, g2,
+) = _line_polygon_process(
+ g1, g2;
+ TOUCHES_POLYGON_ALLOWS...,
+ TOUCHES_REQUIRES...,
+ TOUCHES_EXACT...,
+ closed_line = false,
+)
Rings touch geometries
#= Linearring touches a linestring if at least one of the boundary points of the
+line interacts with the linear ring, but their interiors can't interact. =#
+_touches(
+ trait1::GI.LinearRingTrait, g1,
+ trait2::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _touches(trait2, g2, trait1, g1)
+
+#= Linearring cannot touch another linear ring since they are both exclusively
+made up of interior points and no bounday points =#
+_touches(
+ ::GI.LinearRingTrait, g1,
+ ::GI.LinearRingTrait, g2,
+) = false
+
+#= Linearring touches a polygon if at least one of the points of the ring
+interact with the polygon bounday and non are in the polygon interior. =#
+_touches(
+ ::GI.LinearRingTrait, g1,
+ ::GI.PolygonTrait, g2,
+) = _line_polygon_process(
+ g1, g2;
+ TOUCHES_POLYGON_ALLOWS...,
+ TOUCHES_REQUIRES...,
+ TOUCHES_EXACT...,
+ closed_line = true,
+)
Polygons touch geometries
#= Polygon touches a curve if at least one of the curve bounday points interacts
+with the polygon's bounday and no curve points interact with the interior.=#
+_touches(
+ trait1::GI.PolygonTrait, g1,
+ trait2::GI.AbstractCurveTrait, g2
+) = _touches(trait2, g2, trait1, g1)
+
+
+#= Polygon touches another polygon if they share at least one boundary point and
+no interior points. =#
+_touches(
+ ::GI.PolygonTrait, g1,
+ ::GI.PolygonTrait, g2,
+) = _polygon_polygon_process(
+ g1, g2;
+ TOUCHES_POLYGON_ALLOWS...,
+ TOUCHES_REQUIRES...,
+ TOUCHES_EXACT...,
+)
Geometries touch multi-geometry/geometry collections
#= Geometry touch a multi-geometry or a collection if the geometry touches at
+least one of the elements of the collection. =#
+function _touches(
+ ::Union{GI.PointTrait, GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+ ::Union{
+ GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+ GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+ }, g2,
+)
+ for sub_g2 in GI.getgeom(g2)
+ !touches(g1, sub_g2) && return false
+ end
+ return true
+end
Multi-geometry/geometry collections cross geometries
#= Multi-geometry or a geometry collection touches a geometry if at least one
+elements of the collection touches the geometry. =#
+function _touches(
+ ::Union{
+ GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+ GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+ }, g1,
+ ::GI.AbstractGeometryTrait, g2,
+)
+ for sub_g1 in GI.getgeom(g1)
+ !touches(sub_g1, g2) && return false
+ end
+ return true
+end
Within
export within
What is within?
import GeometryOps as GO
+import GeoInterface as GI
+using Makie
+using CairoMakie
+
+l1 = GI.LineString([(0.0, 0.0), (1.0, 0.0), (0.0, 0.1)])
+l2 = GI.LineString([(0.25, 0.0), (0.75, 0.0)])
+f, a, p = lines(GI.getpoint(l1), color = :blue)
+scatter!(GI.getpoint(l1), color = :blue)
+lines!(GI.getpoint(l2), color = :orange)
+scatter!(GI.getpoint(l2), color = :orange)
+f
GO.within(l1, l2) # false
+GO.within(l2, l1) # true
true
Implementation
within
function and arguments g1 and g2, this criteria is as follows: - points of g1 are allowed to be in the interior of g2 (either through overlap or crossing for lines) - points of g1 are allowed to be on the boundary of g2 - points of g1 are not allowed to be in the exterior of g2 - at least one point of g1 is required to be in the interior of g2 - no points of g1 are required to be on the boundary of g2 - no points of g1 are required to be in the exterior of g2const WITHIN_POINT_ALLOWS = (in_allow = true, on_allow = false, out_allow = false)
+const WITHIN_CURVE_ALLOWS = (over_allow = true, cross_allow = true, on_allow = true, out_allow = false)
+const WITHIN_POLYGON_ALLOWS = (in_allow = true, on_allow = true, out_allow = false)
+const WITHIN_REQUIRES = (in_require = true, on_require = false, out_require = false)
+const WITHIN_EXACT = (exact = _False(),)
+
+"""
+ within(geom1, geom2)::Bool
+
+Return \`true\` if the first geometry is completely within the second geometry.
+The interiors of both geometries must intersect and the interior and boundary of
+the primary geometry (geom1) must not intersect the exterior of the secondary
+geometry (geom2).
+
+Furthermore, \`within\` returns the exact opposite result of \`contains\`.
+
+# Examples
+\`\`\`jldoctest setup=:(using GeometryOps, GeometryBasics)
+import GeometryOps as GO, GeoInterface as GI
+
+line = GI.LineString([(1, 1), (1, 2), (1, 3), (1, 4)])
+point = (1, 2)
+GO.within(point, line)
true
+\`\`\`
+"""
+within(g1, g2) = _within(trait(g1), g1, trait(g2), g2)
Convert features to geometries
_within(::GI.FeatureTrait, g1, ::Any, g2) = within(GI.geometry(g1), g2)
+_within(::Any, g1, t2::GI.FeatureTrait, g2) = within(g1, GI.geometry(g2))
+_within(::FeatureTrait, g1, ::FeatureTrait, g2) = within(GI.geometry(g1), GI.geometry(g2))
Points within geometries
_within(
+ ::GI.PointTrait, g1,
+ ::GI.PointTrait, g2,
+) = equals(g1, g2)
+
+#= Point is within a linestring if it is on a vertex or an edge of that line,
+excluding the start and end vertex if the line is not closed. =#
+_within(
+ ::GI.PointTrait, g1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _point_curve_process(
+ g1, g2;
+ WITHIN_POINT_ALLOWS...,
+ closed_curve = false,
+)
_within(
+ ::GI.PointTrait, g1,
+ ::GI.LinearRingTrait, g2,
+) = _point_curve_process(
+ g1, g2;
+ WITHIN_POINT_ALLOWS...,
+ closed_curve = true,
+)
+
+#= Point is within a polygon if it is inside of that polygon, excluding edges,
+vertices, and holes. =#
+_within(
+ ::GI.PointTrait, g1,
+ ::GI.PolygonTrait, g2,
+) = _point_polygon_process(
+ g1, g2;
+ WITHIN_POINT_ALLOWS...,
+ WITHIN_EXACT...,
+)
_within(
+ ::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+ ::GI.PointTrait, g2,
+) = false
Lines within geometries
#= Linestring is within another linestring if their interiors intersect and no
+points of the first line are in the exterior of the second line. =#
+_within(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _line_curve_process(
+ g1, g2;
+ WITHIN_CURVE_ALLOWS...,
+ WITHIN_REQUIRES...,
+ WITHIN_EXACT...,
+ closed_line = false,
+ closed_curve = false,
+)
+
+#= Linestring is within a linear ring if their interiors intersect and no points
+of the line are in the exterior of the ring. =#
+_within(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+ ::GI.LinearRingTrait, g2,
+) = _line_curve_process(
+ g1, g2;
+ WITHIN_CURVE_ALLOWS...,
+ WITHIN_REQUIRES...,
+ WITHIN_EXACT...,
+ closed_line = false,
+ closed_curve = true,
+)
+
+#= Linestring is within a polygon if their interiors intersect and no points of
+the line are in the exterior of the polygon, although they can be on an edge. =#
+_within(
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+ ::GI.PolygonTrait, g2,
+) = _line_polygon_process(
+ g1, g2;
+ WITHIN_POLYGON_ALLOWS...,
+ WITHIN_REQUIRES...,
+ WITHIN_EXACT...,
+ closed_line = false,
+)
Rings covered by geometries
#= Linearring is within a linestring if their interiors intersect and no points
+of the ring are in the exterior of the line. =#
+_within(
+ ::GI.LinearRingTrait, g1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _line_curve_process(
+ g1, g2;
+ WITHIN_CURVE_ALLOWS...,
+ WITHIN_REQUIRES...,
+ WITHIN_EXACT...,
+ closed_line = true,
+ closed_curve = false,
+)
+
+#= Linearring is within another linearring if their interiors intersect and no
+points of the first ring are in the exterior of the second ring. =#
+_within(
+ ::GI.LinearRingTrait, g1,
+ ::GI.LinearRingTrait, g2,
+) = _line_curve_process(
+ g1, g2;
+ WITHIN_CURVE_ALLOWS...,
+ WITHIN_REQUIRES...,
+ WITHIN_EXACT...,
+ closed_line = true,
+ closed_curve = true,
+)
+
+#= Linearring is within a polygon if their interiors intersect and no points of
+the ring are in the exterior of the polygon, although they can be on an edge. =#
+_within(
+ ::GI.LinearRingTrait, g1,
+ ::GI.PolygonTrait, g2,
+) = _line_polygon_process(
+ g1, g2;
+ WITHIN_POLYGON_ALLOWS...,
+ WITHIN_REQUIRES...,
+ WITHIN_EXACT...,
+ closed_line = true,
+)
Polygons within geometries
#= Polygon is within another polygon if the interior of the first polygon
+intersects with the interior of the second and no points of the first polygon
+are outside of the second polygon. =#
+_within(
+ ::GI.PolygonTrait, g1,
+ ::GI.PolygonTrait, g2,
+) = _polygon_polygon_process(
+ g1, g2;
+ WITHIN_POLYGON_ALLOWS...,
+ WITHIN_REQUIRES...,
+ WITHIN_EXACT...,
+)
_within(
+ ::GI.PolygonTrait, g1,
+ ::GI.AbstractCurveTrait, g2,
+) = false
Geometries within multi-geometry/geometry collections
#= Geometry is within a multi-geometry or a collection if the geometry is within
+at least one of the collection elements. =#
+function _within(
+ ::Union{GI.PointTrait, GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+ ::Union{
+ GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+ GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+ }, g2,
+)
+ for sub_g2 in GI.getgeom(g2)
+ within(g1, sub_g2) && return true
+ end
+ return false
+end
Multi-geometry/geometry collections within geometries
#= Multi-geometry or a geometry collection is within a geometry if all
+elements of the collection are within the geometry. =#
+function _within(
+ ::Union{
+ GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+ GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+ }, g1,
+ ::GI.AbstractGeometryTrait, g2,
+)
+ for sub_g1 in GI.getgeom(g1)
+ !within(sub_g1, g2) && return false
+ end
+ return true
+end
Orientation
export isclockwise, isconcave
isclockwise
isconcave
"""
+ isclockwise(line::Union{LineString, Vector{Position}})::Bool
+
+Take a ring and return true or false whether or not the ring is clockwise or
+counter-clockwise.
+
+# Example
+
+\`\`\`jldoctest
+import GeoInterface as GI, GeometryOps as GO
+
+ring = GI.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)])
+GO.isclockwise(ring)
true
+\`\`\`
+"""
+isclockwise(geom)::Bool = isclockwise(GI.trait(geom), geom)
+
+function isclockwise(::AbstractCurveTrait, line)::Bool
+ sum = 0.0
+ prev = GI.getpoint(line, 1)
+ for p in GI.getpoint(line)
sum += (GI.x(p) - GI.x(prev)) * (GI.y(p) + GI.y(prev))
+ prev = p
+ end
+
+ return sum > 0.0
+end
+
+"""
+ isconcave(poly::Polygon)::Bool
+
+Take a polygon and return true or false as to whether it is concave or not.
+
+# Examples
+\`\`\`jldoctest
+import GeoInterface as GI, GeometryOps as GO
+
+poly = GI.Polygon([[(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]])
+GO.isconcave(poly)
false
+\`\`\`
+"""
+function isconcave(poly)::Bool
+ sign = false
+
+ exterior = GI.getexterior(poly)
GI.npoint(exterior) <= 4 && return false
+ n = GI.npoint(exterior) - 1
+
+ for i in 1:n
+ j = ((i + 1) % n) === 0 ? 1 : (i + 1) % n
+ m = ((i + 2) % n) === 0 ? 1 : (i + 2) % n
+
+ pti = GI.getpoint(exterior, i)
+ ptj = GI.getpoint(exterior, j)
+ ptm = GI.getpoint(exterior, m)
+
+ dx1 = GI.x(ptm) - GI.x(ptj)
+ dy1 = GI.y(ptm) - GI.y(ptj)
+ dx2 = GI.x(pti) - GI.x(ptj)
+ dy2 = GI.y(pti) - GI.y(ptj)
+
+ cross = (dx1 * dy2) - (dy1 * dx2)
+
+ if i === 0
+ sign = cross > 0
+ elseif sign !== (cross > 0)
+ return true
+ end
+ end
+
+ return false
+end
"""
+ isparallel(line1::LineString, line2::LineString)::Bool
+
+Return \`true\` if each segment of \`line1\` is parallel to the correspondent segment of \`line2\`
+
+## Examples
"""
+function isparallel(line1, line2)::Bool
+ seg1 = linesegment(line1)
+ seg2 = linesegment(line2)
+
+ for i in eachindex(seg1)
+ coors2 = nothing
+ coors1 = seg1[i]
+ coors2 = seg2[i]
+ _isparallel(coors1, coors2) == false && return false
+ end
+ return true
+end
+
+@inline function _isparallel(p1, p2)
+ slope1 = bearing_to_azimuth(rhumb_bearing(GI.x(p1), GI.x(p2)))
+ slope2 = bearing_to_azimuth(rhumb_bearing(GI.y(p1), GI.y(p2)))
+
+ return slope1 === slope2
+end
_isparallel(((ax, ay), (bx, by)), ((cx, cy), (dx, dy))) =
+ _isparallel(bx - ax, by - ay, dx - cx, dy - cy)
+
+_isparallel(Δx1, Δy1, Δx2, Δy2) = (Δx1 * Δy2 == Δy1 * Δx2)
Polygonizing raster data
export polygonize
+
+#=
+The methods in this file convert a raster image into a set of polygons,
+by contour detection using a clockwise Moore neighborhood method.
+
+The resulting polygons are snapped to the boundaries of the cells of the input raster,
+so they will look different from traditional contours from a plotting package.
+
+The main entry point is the \`polygonize\` function.
+
+\`\`\`@docs
+polygonize
+\`\`\`
+
+# Example
+
+Here's a basic example, using the \`Makie.peaks()\` function. First, let's investigate the nature of the function:
+\`\`\`@example polygonize
+using Makie, GeometryOps
+n = 49
+xs, ys = LinRange(-3, 3, n), LinRange(-3, 3, n)
+zs = Makie.peaks(n)
+z_max_value = maximum(abs.(extrema(zs)))
+f, a, p = heatmap(
+ xs, ys, zs;
+ axis = (; aspect = DataAspect(), title = "Exact function")
+)
+cb = Colorbar(f[1, 2], p; label = "Z-value")
+f
+\`\`\`
+
+Now, we can use the \`polygonize\` function to convert the raster data into polygons.
+
+For this particular example, we chose a range of z-values between 0.8 and 3.2,
+which would provide two distinct polyogns with holes.
+
+\`\`\`@example polygonize
+polygons = polygonize(xs, ys, 0.8 .< zs .< 3.2)
+\`\`\`
+This returns a \`GI.MultiPolygon\`, which is directly plottable. Let's see how these look:
+
+\`\`\`@example polygonize
+f, a, p = poly(polygons; label = "Polygonized polygons", axis = (; aspect = DataAspect()))
+\`\`\`
+
+Finally, let's plot the Makie contour lines on top, to see how the polygonization compares:
+\`\`\`@example polygonize
+contour!(a, xs, ys, zs; labels = true, levels = [0.8, 3.2], label = "Contour lines")
+f
+\`\`\`
+
+# Implementation
+
+The implementation follows:
+=#
+
+"""
+ polygonize(A::AbstractMatrix{Bool}; kw...)
+ polygonize(f, A::AbstractMatrix; kw...)
+ polygonize(xs, ys, A::AbstractMatrix{Bool}; kw...)
+ polygonize(f, xs, ys, A::AbstractMatrix; kw...)
+
+Polygonize an \`AbstractMatrix\` of values, currently to a single class of polygons.
+
+Returns a \`MultiPolygon\` for \`Bool\` values and \`f\` return values, and
+a \`FeatureCollection\` of \`Feature\`s holding \`MultiPolygon\` for all other values.
+
+
+Function \`f\` should return either \`true\` or \`false\` or a transformation
+of values into simpler groups, especially useful for floating point arrays.
+
+If \`xs\` and \`ys\` are ranges, they are used as the pixel/cell center points.
+If they are \`Vector\` of \`Tuple\` they are used as the lower and upper bounds of each pixel/cell.
- \`minpoints\`: ignore polygons with less than \`minpoints\` points.
+- \`values\`: the values to turn into polygons. By default these are \`union(A)\`,
+ If function \`f\` is passed these refer to the return values of \`f\`, by
+ default \`union(map(f, A)\`. If values \`Bool\`, false is ignored and a single
+ \`MultiPolygon\` is returned rather than a \`FeatureCollection\`.
\`\`\`julia
+using GeometryOps
+A = rand(100, 100)
+multipolygon = polygonize(>(0.5), A);
+\`\`\`
+"""
+polygonize(A::AbstractMatrix{Bool}; kw...) = polygonize(identity, A; kw...)
+polygonize(f::Base.Callable, A::AbstractMatrix; kw...) = polygonize(f, axes(A)..., A; kw...)
+polygonize(A::AbstractMatrix; kw...) = polygonize(axes(A)..., A; kw...)
+polygonize(xs::AbstractVector, ys::AbstractVector, A::AbstractMatrix{Bool}; kw...) =
+ _polygonize(identity, xs, ys, A)
+function polygonize(xs::AbstractVector, ys::AbstractVector, A::AbstractMatrix;
+ values=sort!(Base.union(A)), kw...
+)
+ _polygonize_featurecollection(identity, xs, ys, A; values, kw...)
+end
+function polygonize(f::Base.Callable, xs::AbstractRange, ys::AbstractRange, A::AbstractMatrix;
+ values=_default_values(f, A), kw...
+)
+ if isnothing(values)
+ _polygonize(f, xs, ys, A; kw...)
+ else
+ _polygonize_featurecollection(f, xs, ys, A; kw...)
+ end
+end
+function _polygonize(f::Base.Callable, xs::AbstractRange, ys::AbstractRange, A::AbstractMatrix;
+ kw...
+)
xhalf = step(xs) / 2
+ yhalf = step(ys) / 2
xbounds = first(xs) - xhalf : step(xs) : last(xs) + xhalf
+ ybounds = first(ys) - yhalf : step(ys) : last(ys) + yhalf
+ Tx = eltype(xbounds)
+ Ty = eltype(ybounds)
+ xvec = similar(Vector{Tuple{Tx,Tx}}, xs)
+ yvec = similar(Vector{Tuple{Ty,Ty}}, ys)
+ for (xind, i) in enumerate(eachindex(xvec))
+ xvec[i] = xbounds[xind], xbounds[xind+1]
+ end
+ for (yind, i) in enumerate(eachindex(yvec))
+ yvec[i] = ybounds[yind], ybounds[yind+1]
+ end
+ return _polygonize(f, xvec, yvec, A; kw...)
+end
+function _polygonize(f, xs::AbstractVector{T}, ys::AbstractVector{T}, A::AbstractMatrix;
+ minpoints=0,
+) where T<:Tuple
+ (length(xs), length(ys)) == size(A) || throw(ArgumentError("length of xs and ys must match the array size"))
crs = GI.crs(A)
rings = Vector{T}[]
+
+ strait = true
+ turning = false
edges = _pixel_edges(f, xs, ys, A)
edgekeys = collect(keys(edges))
nkeys = length(edgekeys)
while nkeys > 0
+ found = false
+ local firstnode, nextnodes, nodestatus
while nkeys > 0
firstnode::T = edgekeys[nkeys]
+ nextnodes = edges[firstnode]
+ nodestatus = map(!=(typemax(first(firstnode))) ∘ first, nextnodes)
+ if any(nodestatus)
+ found = true
+ break
+ else
+ nkeys -= 1
+ end
+ end
found == false && break
if nodestatus[2]
+ nextnode = nextnodes[2]
+ edges[firstnode] = (nextnodes[1], map(typemax, nextnode))
+ else
+ nkeys -= 1
+ nextnode = nextnodes[1]
+ edges[firstnode] = (map(typemax, nextnode), map(typemax, nextnode))
+ end
currentnode = firstnode
+ ring = [currentnode, nextnode]
+ push!(rings, ring)
while true
(c1, c2) = possiblenodes = edges[nextnode]
+ nodestatus = map(!=(typemax(first(firstnode))) ∘ first, possiblenodes)
+ if nodestatus[2]
selectednode, remainingnode, straightline = if currentnode[1] == nextnode[1] # vertical
+ wasincreasing = nextnode[2] > currentnode[2]
+ firstisstraight = nextnode[1] == c1[1]
+ firstisleft = nextnode[1] > c1[1]
+ secondisstraight = nextnode[1] == c2[1]
+ secondisleft = nextnode[1] > c2[1]
+ if firstisstraight
+ if secondisleft
+ if wasincreasing
+ (c2, c1, turning)
+ else
+ (c1, c2, straight)
+ end
+ else
+ if wasincreasing
+ (c1, c2, straight)
+ else
+ (c2, c1, secondisstraight)
+ end
+ end
+ elseif firstisleft
+ if wasincreasing
+ (c1, c2, turning)
+ else
+ (c2, c1, secondisstraight)
+ end
+ else # firstisright
+ if wasincreasing
+ (c2, c1, secondisstraight)
+ else
+ (c1, c2, turning)
+ end
+ end
+ else # horizontal
+ wasincreasing = nextnode[1] > currentnode[1]
+ firstisstraight = nextnode[2] == c1[2]
+ firstisleft = nextnode[2] > c1[2]
+ secondisleft = nextnode[2] > c2[2]
+ secondisstraight = nextnode[2] == c2[2]
+ if firstisstraight
+ if secondisleft
+ if wasincreasing
+ (c1, c2, straight)
+ else
+ (c2, c1, turning)
+ end
+ else
+ if wasincreasing
+ (c2, c1, turning)
+ else
+ (c1, c2, straight)
+ end
+ end
+ elseif firstisleft
+ if wasincreasing
+ (c2, c1, secondisstraight)
+ else
+ (c1, c2, turning)
+ end
+ else # firstisright
+ if wasincreasing
+ (c1, c2, turning)
+ else
+ (c2, c1, secondisstraight)
+ end
+ end
+ end
edges[nextnode] = (remainingnode, map(typemax, remainingnode))
+ else
selectednode = c1
edges[nextnode] = (map(typemax, c1), map(typemax, c1))
straightline = currentnode[1] == nextnode[1] == c1[1] ||
+ currentnode[2] == nextnode[2] == c1[2]
+ end
currentnode, nextnode = nextnode, selectednode
if straightline
ring[end] = nextnode
+ else
push!(ring, nextnode)
+ end
nextnode == firstnode && break
+ end
+ end
linearrings = map(rings) do ring
+ extent = GI.extent(GI.LinearRing(ring))
+ GI.LinearRing(ring; extent, crs)
+ end
direction = (last(last(xs)) - first(first(xs))) * (last(last(ys)) - first(first(ys)))
+ exterior_inds = if direction > 0
+ .!isclockwise.(linearrings)
+ else
+ isclockwise.(linearrings)
+ end
+ holes = linearrings[.!exterior_inds]
+ polygons = map(view(linearrings, exterior_inds)) do lr
+ GI.Polygon([lr]; extent=GI.extent(lr), crs)
+ end
assigned = fill(false, length(holes))
+ for i in eachindex(holes)
+ hole = holes[i]
+ prepared_hole = GI.LinearRing(holes[i]; extent=GI.extent(holes[i]))
+ for poly in polygons
+ exterior = GI.Polygon(StaticArrays.SVector(GI.getexterior(poly)); extent=GI.extent(poly))
+ if covers(exterior, prepared_hole)
push!(poly.geom, hole)
+ assigned[i] = true
+ break
+ end
+ end
+ end
+
+ assigned_holes = count(assigned)
+ assigned_holes == length(holes) || @warn "Not all holes were assigned to polygons, $(length(holes) - assigned_holes) where missed from $(length(holes)) holes and $(length(polygons)) polygons"
+
+ if isempty(polygons)
@warn "No polgons found, check your data or try another function for \`f\`"
+ return nothing
+ else
return GI.MultiPolygon(polygons; crs, extent = mapreduce(GI.extent, Extents.union, polygons))
+ end
+end
+
+function _polygonize_featurecollection(f::Base.Callable, xs::AbstractRange, ys::AbstractRange, A::AbstractMatrix;
+ values=_default_values(f, A), kw...
+)
+ crs = GI.crs(A)
features = map(values) do value
+ multipolygon = _polygonize(x -> isequal(f(x), value), xs, ys, A; kw...)
+ GI.Feature(multipolygon; properties=(; value), extent = GI.extent(multipolygon), crs)
+ end
+
+ return GI.FeatureCollection(features; extent = mapreduce(GI.extent, Extents.union, features), crs)
+end
+
+function _default_values(f, A)
values = map(identity, sort!(Base.union(Iterators.map(f, A))))
return eltype(values) == Bool ? nothing : collect(skipmissing(values))
+end
+
+function update_edge!(dict, key, node)
+ newnodes = (node, map(typemax, node))
existingnodes = get!(() -> newnodes, dict, key)
if existingnodes[1] != node
+ dict[key] = (existingnodes[1], node)
+ end
+end
+
+function _pixel_edges(f, xs::AbstractVector{T}, ys::AbstractVector{T}, A) where T<:Tuple
+ edges = Dict{T,Tuple{T,T}}()
fi, fj = map(first, axes(A))
+ li, lj = map(last, axes(A))
+ for j in axes(A, 2)
+ y1, y2 = ys[j]
+ for i in axes(A, 1)
+ if f(A[i, j]) # This is a pixel inside a polygon
x1, x2 = xs[i]
(j == fi || !f(A[i, j-1])) && update_edge!(edges, (x1, y1), (x2, y1)) # S
+ (i == fj || !f(A[i-1, j])) && update_edge!(edges, (x1, y2), (x1, y1)) # W
+ (j == lj || !f(A[i, j+1])) && update_edge!(edges, (x2, y2), (x1, y2)) # N
+ (i == li || !f(A[i+1, j])) && update_edge!(edges, (x2, y1), (x2, y2)) # E
+ end
+ end
+ end
+ return edges
+end
Primitive functions
export apply, applyreduce, TraitTarget
apply
and applyreduce
functions, and some related functionality.apply
framework is to take as input any geometry, vector of geometries, or feature collection, deconstruct it to the given trait target (any arbitrary GI.AbstractTrait or TraitTarget
union thereof, like PointTrait
or PolygonTrait
) and perform some operation on it.flipped_geom = GO.apply(GI.PointTrait(), geom) do p
+ (GI.y(p), GI.x(p))
+end
flip
, reproject
, transform
, even segmentize
and simplify
have been implemented using the apply
framework. Similarly, centroid
, area
and distance
have been implemented using the applyreduce
framework.Docstrings
Functions
apply
. Check Documenter's build log for details.applyreduce
. Check Documenter's build log for details.GeometryOps.unwrap
. Check Documenter's build log for details.flatten(target::Type{<:GI.AbstractTrait}, obj)
+flatten(f, target::Type{<:GI.AbstractTrait}, obj)
AbstractArray
, iterator, FeatureCollectionTrait
, FeatureTrait
or AbstractGeometryTrait
object obj
, so that objects with the target
trait are returned by the iterator.f
is passed in it will be applied to the target geometries.reconstruct(geom, components)
geom
from an iterable of component objects that match its structure.components
must have the same GeoInterface.trait
.flatten
.rebuild(geom, child_geoms)
GeoInterface.Wrappers
geometry, but rebuild
can have methods added to it to dispatch on geometries from other packages and specify how to rebuild them.Types
TraitTarget
. Check Documenter's build log for details.Implementation
threading
and calc_extent
as types, not simple boolean values._booltype(::Bool)
method for this reason as well.abstract type BoolsAsTypes end
+struct _True <: BoolsAsTypes end
+struct _False <: BoolsAsTypes end
+
+@inline _booltype(x::Bool)::BoolsAsTypes = x ? _True() : _False()
+@inline _booltype(x::BoolsAsTypes)::BoolsAsTypes = x
+
+"""
+ TraitTarget{T}
+
+This struct holds a trait parameter or a union of trait parameters.
+
+It is primarily used for dispatch into methods which select trait levels,
+like \`apply\`, or as a parameter to \`target\`.
+
+# Constructors
+\`\`\`julia
+TraitTarget(GI.PointTrait())
+TraitTarget(GI.LineStringTrait(), GI.LinearRingTrait()) # and other traits as you may like
+TraitTarget(TraitTarget(...))
TraitTarget(GI.PointTrait)
+TraitTarget(Union{GI.LineStringTrait, GI.LinearRingTrait})
\`\`\`
+
+"""
+struct TraitTarget{T} end
+TraitTarget(::Type{T}) where T = TraitTarget{T}()
+TraitTarget(::T) where T<:GI.AbstractTrait = TraitTarget{T}()
+TraitTarget(::TraitTarget{T}) where T = TraitTarget{T}()
+TraitTarget(::Type{<:TraitTarget{T}}) where T = TraitTarget{T}()
+TraitTarget(traits::GI.AbstractTrait...) = TraitTarget{Union{map(typeof, traits)...}}()
+
+const THREADED_KEYWORD = "- \`threaded\`: \`true\` or \`false\`. Whether to use multithreading. Defaults to \`false\`."
+const CRS_KEYWORD = "- \`crs\`: The CRS to attach to geometries. Defaults to \`nothing\`."
+const CALC_EXTENT_KEYWORD = "- \`calc_extent\`: \`true\` or \`false\`. Whether to calculate the extent. Defaults to \`false\`."
+
+const APPLY_KEYWORDS = """
+$THREADED_KEYWORD
+$CRS_KEYWORD
+$CALC_EXTENT_KEYWORD
+"""
What is
apply
? apply
applies some function to every geometry matching the Target
GeoInterface trait, in some arbitrarily nested object made up of:AbstractArray
s (we also try to iterate other non-GeoInteface compatible object)FeatureCollectionTrait
objectsFeatureTrait
objectsAbstractGeometryTrait
objectsapply
recursively calls itself through these nested layers until it reaches objects with the Target
GeoInterface trait. When found apply
applies the function f
, and stops.PointTrait
is found but it is not the Target
, an error is thrown. This likely means the object contains a different geometry trait to the target, such as MultiPointTrait
when LineStringTrait
was specified.Target
a Union
of traits found at the same level of nesting, and define methods of f
to handle all cases.Union{FeatureTrait,PolygonTrait}
, as _apply
will just never reach PolygonTrait
when all the polygons are wrapped in a FeatureTrait
object.Embedding:
extent
and crs
can be embedded in all geometries, features, and feature collections as part of apply
. Geometries deeper than Target
will of course not have new extent
or crs
embedded.calc_extent
signals to recalculate an Extent
and embed it.crs
will be embedded as-isThreading
PolygonTrait
sub-geometry may be calculated on a different thread.false
for all objects, but can be turned on by passing the keyword argument threaded=true
to apply
."""
+ apply(f, target::Union{TraitTarget, GI.AbstractTrait}, obj; kw...)
+
+Reconstruct a geometry, feature, feature collection, or nested vectors of
+either using the function \`f\` on the \`target\` trait.
+
+\`f(target_geom) => x\` where \`x\` also has the \`target\` trait, or a trait that can
+be substituted. For example, swapping \`PolgonTrait\` to \`MultiPointTrait\` will fail
+if the outer object has \`MultiPolygonTrait\`, but should work if it has \`FeatureTrait\`.
+
+Objects "shallower" than the target trait are always completely rebuilt, like
+a \`Vector\` of \`FeatureCollectionTrait\` of \`FeatureTrait\` when the target
+has \`PolygonTrait\` and is held in the features. These will always be GeoInterface
+geometries/feature/feature collections. But "deeper" objects may remain
+unchanged or be whatever GeoInterface compatible objects \`f\` returns.
+
+The result is a functionally similar geometry with values depending on \`f\`.
+
+$APPLY_KEYWORDS
+
+# Example
+
+Flipped point the order in any feature or geometry, or iterables of either:
+
+\`\`\`julia
+import GeoInterface as GI
+import GeometryOps as GO
+geom = GI.Polygon([GI.LinearRing([(1, 2), (3, 4), (5, 6), (1, 2)]),
+ GI.LinearRing([(3, 4), (5, 6), (6, 7), (3, 4)])])
+
+flipped_geom = GO.apply(GI.PointTrait, geom) do p
+ (GI.y(p), GI.x(p))
+end
+\`\`\`
+"""
+@inline function apply(
+ f::F, target, geom; calc_extent=false, threaded=false, kw...
+) where F
+ threaded = _booltype(threaded)
+ calc_extent = _booltype(calc_extent)
+ _apply(f, TraitTarget(target), geom; threaded, calc_extent, kw...)
+end
geom
@inline _apply(f::F, target, geom; kw...) where F =
+ _apply(f, target, GI.trait(geom), geom; kw...)
@inline function _apply(f::F, target, ::Nothing, A::AbstractArray; threaded, kw...) where F
_apply
over all values _maptasks may run this level threaded if threaded==true
, but deeper _apply
called in the closure will not be threaded apply_to_array(i) = _apply(f, target, A[i]; threaded=_False(), kw...)
+ _maptasks(apply_to_array, eachindex(A), threaded)
+end
map
.@inline function _apply(f::F, target, ::Nothing, iterable::IterableType; threaded, kw...) where {F, IterableType}
if Tables.istable(iterable)
+ _apply_table(f, target, iterable; threaded, kw...)
+ else # this is probably some form of iterable...
+ if threaded isa _True
collect
first so we can use threads _apply(f, target, collect(iterable); threaded, kw...)
+ else
+ apply_to_iterable(x) = _apply(f, target, x; kw...)
+ map(apply_to_iterable, iterable)
+ end
+ end
+end
+#=
+Doing this inline in \`_apply\` is _heavily_ type unstable, so it's best to separate this
+by a function barrier.
+
+This function operates \`apply\` on the \`geometry\` column of the table, and returns a new table
+with the same schema, but with the new geometry column.
+
+This new table may be of the same type as the old one iff \`Tables.materializer\` is defined for
+that table. If not, then a \`NamedTuple\` is returned.
+=#
+function _apply_table(f::F, target, iterable::IterableType; threaded, kw...) where {F, IterableType}
+ _get_col_pair(colname) = colname => Tables.getcolumn(iterable, colname)
apply
on it. geometry_column = first(GI.geometrycolumns(iterable))
+ new_geometry = _apply(f, target, Tables.getcolumn(iterable, geometry_column); threaded, kw...)
old_schema = Tables.schema(iterable)
new_names = filter(Base.Fix1(!==, geometry_column), old_schema.names)
iterable
, or a named tuple which is the default fallback. return Tables.materializer(iterable)(
+ merge(
+ NamedTuple{(geometry_column,), Base.Tuple{typeof(new_geometry)}}((new_geometry,)),
+ NamedTuple(Iterators.map(_get_col_pair, new_names))
+ )
+ )
+end
@inline function _apply(f::F, target, ::GI.FeatureCollectionTrait, fc;
+ crs=GI.crs(fc), calc_extent=_False(), threaded
+) where F
features
in the feature collection, possibly threaded apply_to_feature(i) =
+ _apply(f, target, GI.getfeature(fc, i); crs, calc_extent, threaded=_False())::GI.Feature
+ features = _maptasks(apply_to_feature, 1:GI.nfeature(fc), threaded)
+ if calc_extent isa _True
extent = mapreduce(GI.extent, Extents.union, features)
return GI.FeatureCollection(features; crs, extent)
+ else
return GI.FeatureCollection(features; crs)
+ end
+end
@inline function _apply(f::F, target, ::GI.FeatureTrait, feature;
+ crs=GI.crs(feature), calc_extent=_False(), threaded
+) where F
geometry = _apply(f, target, GI.geometry(feature); crs, calc_extent, threaded)
properties = GI.properties(feature)
+ if calc_extent isa _True
extent = GI.extent(geometry)
return GI.Feature(geometry; properties, crs, extent)
+ else
return GI.Feature(geometry; properties, crs)
+ end
+end
@inline function _apply(f::F, target, trait, geom;
+ crs=GI.crs(geom), calc_extent=_False(), threaded
+)::(GI.geointerface_geomtype(trait)) where F
_apply
over all sub geometries of geom
to create a new vector of geometries TODO handle zero length apply_to_geom(i) = _apply(f, target, GI.getgeom(geom, i); crs, calc_extent, threaded=_False())
+ geoms = _maptasks(apply_to_geom, 1:GI.ngeom(geom), threaded)
+ return _apply_inner(geom, geoms, crs, calc_extent)
+end
+function _apply_inner(geom, geoms, crs, calc_extent::_True)
extent = mapreduce(GI.extent, Extents.union, geoms)
geom
, holding tnew geoms
with crs
and calcualted extent return rebuild(geom, geoms; crs, extent)
+end
+function _apply_inner(geom, geoms, crs, calc_extent::_False)
geom
, holding the new geoms
with crs
return rebuild(geom, geoms; crs)
+end
f
(after PointTrait there is no further to dig with _apply
) @inline _apply(f, ::TraitTarget{Target}, trait::GI.PointTrait, geom; crs=nothing, kw...) where Target = throw(ArgumentError("target Target not found, but reached a PointTrait
leaf")) Finally, these short methods are the main purpose of apply
. The Trait
is a subtype of the Target
(or identical to it) So the Target
is found. We apply f
to geom and return it to previous _apply calls to be wrapped with the outer geometries/feature/featurecollection/array._apply(f::F, ::TraitTarget{Target}, ::Trait, geom; crs=GI.crs(geom), kw...) where {F,Target,Trait<:Target} = f(geom)
for T in (
+ GI.PointTrait, GI.LinearRing, GI.LineString,
+ GI.MultiPoint, GI.FeatureTrait, GI.FeatureCollectionTrait
+)
+ @eval _apply(f::F, target::TraitTarget{<:$T}, trait::$T, x; kw...) where F = f(x)
+end
+
+"""
+ applyreduce(f, op, target::Union{TraitTarget, GI.AbstractTrait}, obj; threaded)
+
+Apply function \`f\` to all objects with the \`target\` trait,
+and reduce the result with an \`op\` like \`+\`.
+
+The order and grouping of application of \`op\` is not guaranteed.
+
+If \`threaded==true\` threads will be used over arrays and iterables,
+feature collections and nested geometries.
+"""
+@inline function applyreduce(
+ f::F, op::O, target, geom; threaded=false, init=nothing
+) where {F, O}
+ threaded = _booltype(threaded)
+ _applyreduce(f, op, TraitTarget(target), geom; threaded, init)
+end
+
+@inline _applyreduce(f::F, op::O, target, geom; threaded, init) where {F, O} =
+ _applyreduce(f, op, target, GI.trait(geom), geom; threaded, init)
@inline function _applyreduce(f::F, op::O, target, ::Nothing, A::AbstractArray; threaded, init) where {F, O}
+ applyreduce_array(i) = _applyreduce(f, op, target, A[i]; threaded=_False(), init)
+ _mapreducetasks(applyreduce_array, op, eachindex(A), threaded; init)
+end
@inline function _applyreduce(f::F, op::O, target, ::Nothing, iterable::IterableType; threaded, init) where {F, O, IterableType}
+ if Tables.istable(iterable)
+ _applyreduce_table(f, op, target, iterable; threaded, init)
+ else
+ applyreduce_iterable(i) = _applyreduce(f, op, target, i; threaded=_False(), init)
+ if threaded isa _True # Try to \`collect\` and reduce over the vector with threads
+ _applyreduce(f, op, target, collect(iterable); threaded, init)
+ else
mapreduce
the iterable as-is mapreduce(applyreduce_iterable, op, iterable; init)
+ end
+ end
+end
function _applyreduce_table(f::F, op::O, target, iterable::IterableType; threaded, init) where {F, O, IterableType}
applyreduce
on it. geometry_column = first(GI.geometrycolumns(iterable))
+ return _applyreduce(f, op, target, Tables.getcolumn(iterable, geometry_column); threaded, init)
+end
applyreduce
wants features, then applyreduce over the rows as GI.Feature
s.function _applyreduce_table(f::F, op::O, target::GI.FeatureTrait, iterable::IterableType; threaded, init) where {F, O, IterableType}
apply
on it. geometry_column = first(GI.geometrycolumns(iterable))
+ property_names = Iterators.filter(!=(geometry_column), Tables.schema(iterable).names)
+ features = map(Tables.rows(iterable)) do row
+ GI.Feature(Tables.getcolumn(row, geometry_column), properties=NamedTuple(Iterators.map(Base.Fix1(_get_col_pair, row), property_names)))
+ end
+ return _applyreduce(f, op, target, features; threaded, init)
+end
@inline function _applyreduce(f::F, op::O, target, ::GI.FeatureCollectionTrait, fc; threaded, init) where {F, O}
+ applyreduce_fc(i) = _applyreduce(f, op, target, GI.getfeature(fc, i); threaded=_False(), init)
+ _mapreducetasks(applyreduce_fc, op, 1:GI.nfeature(fc), threaded; init)
+end
@inline _applyreduce(f::F, op::O, target, ::GI.FeatureTrait, feature; threaded, init) where {F, O} =
+ _applyreduce(f, op, target, GI.geometry(feature); threaded, init)
@inline function _applyreduce(f::F, op::O, target, trait, geom; threaded, init) where {F, O}
+ applyreduce_geom(i) = _applyreduce(f, op, target, GI.getgeom(geom, i); threaded=_False(), init)
+ _mapreducetasks(applyreduce_geom, op, 1:GI.ngeom(geom), threaded; init)
+end
@inline function _applyreduce(
+ f::F, op::O, target, trait::Union{GI.LinearRing,GI.LineString,GI.MultiPoint}, geom;
+ threaded, init
+) where {F, O}
+ _applyreduce(f, op, target, GI.getgeom(geom); threaded=_False(), init)
+end
@inline function _applyreduce(f::F, op::O, ::TraitTarget{Target}, ::Trait, x; kw...) where {F,O,Target,Trait<:Target}
+ f(x)
+end
for T in (
+ GI.PointTrait, GI.LinearRing, GI.LineString,
+ GI.MultiPoint, GI.FeatureTrait, GI.FeatureCollectionTrait
+)
+ @eval _applyreduce(f::F, op::O, ::TraitTarget{<:$T}, trait::$T, x; kw...) where {F, O} = f(x)
+end
+
+"""
+ unwrap(target::Type{<:AbstractTrait}, obj)
+ unwrap(f, target::Type{<:AbstractTrait}, obj)
+
+Unwrap the object to vectors, down to the target trait.
+
+If \`f\` is passed in it will be applied to the target geometries
+as they are found.
+"""
+function unwrap end
+unwrap(target::Type, geom) = unwrap(identity, target, geom)
unwrap(f, target::Type, geom) = unwrap(f, target, GI.trait(geom), geom)
unwrap(f, target::Type, ::Nothing, iterable) =
+ map(x -> unwrap(f, target, x), iterable)
unwrap(f, target::Type, ::GI.FeatureCollectionTrait, fc) =
+ map(x -> unwrap(f, target, x), GI.getfeature(fc))
+unwrap(f, target::Type, ::GI.FeatureTrait, feature) =
+ unwrap(f, target, GI.geometry(feature))
+unwrap(f, target::Type, trait, geom) = map(g -> unwrap(f, target, g), GI.getgeom(geom))
unwrap(f, ::Type{Target}, ::Trait, geom) where {Target,Trait<:Target} = f(geom)
unwrap(f, target::Type, trait::GI.PointTrait, geom) =
+ throw(ArgumentError("target $target not found, but reached a \`PointTrait\` leaf"))
unwrap(f, target::Type{GI.PointTrait}, trait::GI.PointTrait, geom) = f(geom)
+unwrap(f, target::Type{GI.FeatureTrait}, ::GI.FeatureTrait, feature) = f(feature)
+unwrap(f, target::Type{GI.FeatureCollectionTrait}, ::GI.FeatureCollectionTrait, fc) = f(fc)
+
+"""
+ flatten(target::Type{<:GI.AbstractTrait}, obj)
+ flatten(f, target::Type{<:GI.AbstractTrait}, obj)
+
+Lazily flatten any \`AbstractArray\`, iterator, \`FeatureCollectionTrait\`,
+\`FeatureTrait\` or \`AbstractGeometryTrait\` object \`obj\`, so that objects
+with the \`target\` trait are returned by the iterator.
+
+If \`f\` is passed in it will be applied to the target geometries.
+"""
+flatten(::Type{Target}, geom) where {Target<:GI.AbstractTrait} = flatten(identity, Target, geom)
+flatten(f, ::Type{Target}, geom) where {Target<:GI.AbstractTrait} = _flatten(f, Target, geom)
+
+_flatten(f, ::Type{Target}, geom) where Target = _flatten(f, Target, GI.trait(geom), geom)
_flatten(f, ::Type{Target}, ::Nothing, iterable) where Target =
+ Iterators.flatten(Iterators.map(x -> _flatten(f, Target, x), iterable))
function _flatten(f, ::Type{Target}, ::GI.FeatureCollectionTrait, fc) where Target
+ Iterators.map(GI.getfeature(fc)) do feature
+ _flatten(f, Target, feature)
+ end |> Iterators.flatten
+end
+_flatten(f, ::Type{Target}, ::GI.FeatureTrait, feature) where Target =
+ _flatten(f, Target, GI.geometry(feature))
_flatten(f, ::Type{Target}, ::Trait, geom) where {Target,Trait<:Target} = (f(geom),)
+_flatten(f, ::Type{Target}, trait, geom) where Target =
+ Iterators.flatten(Iterators.map(g -> _flatten(f, Target, g), GI.getgeom(geom)))
f
_flatten(f, ::Type{Target}, trait::GI.PointTrait, geom) where Target =
+ throw(ArgumentError("target $Target not found, but reached a \`PointTrait\` leaf"))
_flatten(f, ::Type{<:GI.PointTrait}, ::GI.PointTrait, geom) = (f(geom),)
+_flatten(f, ::Type{<:GI.FeatureTrait}, ::GI.FeatureTrait, feature) = (f(feature),)
+_flatten(f, ::Type{<:GI.FeatureCollectionTrait}, ::GI.FeatureCollectionTrait, fc) = (f(fc),)
+
+
+"""
+ reconstruct(geom, components)
+
+Reconstruct \`geom\` from an iterable of component objects that match its structure.
+
+All objects in \`components\` must have the same \`GeoInterface.trait\`.
+
+Ususally used in combination with \`flatten\`.
+"""
+function reconstruct(geom, components)
+ obj, iter = _reconstruct(geom, components)
+ return obj
+end
+
+_reconstruct(geom, components) =
+ _reconstruct(typeof(GI.trait(first(components))), geom, components, 1)
+_reconstruct(::Type{Target}, geom, components, iter) where Target =
+ _reconstruct(Target, GI.trait(geom), geom, components, iter)
function _reconstruct(::Type{Target}, ::Nothing, iterable, components, iter) where Target
+ vect = map(iterable) do x
obj, iter = _reconstruct(Target, x, components, iter)
+ obj
+ end
+ return vect, iter
+end
function _reconstruct(::Type{Target}, ::GI.FeatureCollectionTrait, fc, components, iter) where Target
+ features = map(GI.getfeature(fc)) do feature
newfeature, iter = _reconstruct(Target, feature, components, iter)
+ newfeature
+ end
+ return GI.FeatureCollection(features; crs=GI.crs(fc)), iter
+end
+function _reconstruct(::Type{Target}, ::GI.FeatureTrait, feature, components, iter) where Target
+ geom, iter = _reconstruct(Target, GI.geometry(feature), components, iter)
+ return GI.Feature(geom; properties=GI.properties(feature), crs=GI.crs(feature)), iter
+end
+function _reconstruct(::Type{Target}, trait, geom, components, iter) where Target
+ geoms = map(GI.getgeom(geom)) do subgeom
subgeom1, iter = _reconstruct(Target, GI.trait(subgeom), subgeom, components, iter)
+ subgeom1
+ end
+ return rebuild(geom, geoms), iter
+end
_reconstruct(::Type{Target}, ::Trait, geom, components, iter) where {Target,Trait<:Target} =
+ iterate(components, iter)
_reconstruct(::Type{<:GI.PointTrait}, ::GI.PointTrait, geom, components, iter) = iterate(components, iter)
+_reconstruct(::Type{<:GI.FeatureTrait}, ::GI.FeatureTrait, feature, components, iter) = iterate(feature, iter)
+_reconstruct(::Type{<:GI.FeatureCollectionTrait}, ::GI.FeatureCollectionTrait, fc, components, iter) = iterate(fc, iter)
f
_reconstruct(::Type{Target}, trait::GI.PointTrait, geom, components, iter) where Target =
+ throw(ArgumentError("target $Target not found, but reached a \`PointTrait\` leaf"))
+
+
+const BasicsGeoms = Union{GB.AbstractGeometry,GB.AbstractFace,GB.AbstractPoint,GB.AbstractMesh,
+ GB.AbstractPolygon,GB.LineString,GB.MultiPoint,GB.MultiLineString,GB.MultiPolygon,GB.Mesh}
+
+"""
+ rebuild(geom, child_geoms)
+
+Rebuild a geometry from child geometries.
+
+By default geometries will be rebuilt as a \`GeoInterface.Wrappers\`
+geometry, but \`rebuild\` can have methods added to it to dispatch
+on geometries from other packages and specify how to rebuild them.
+
+(Maybe it should go into GeoInterface.jl)
+"""
+rebuild(geom, child_geoms; kw...) = rebuild(GI.trait(geom), geom, child_geoms; kw...)
+function rebuild(trait::GI.AbstractTrait, geom, child_geoms; crs=GI.crs(geom), extent=nothing)
+ T = GI.geointerface_geomtype(trait)
+ if GI.is3d(geom)
return T{true,false}(child_geoms; crs, extent)
+ else
+ return T{false,false}(child_geoms; crs, extent)
+ end
+end
+
+using Base.Threads: nthreads, @threads, @spawn
f
over ntasks, where f recieves an AbstractArray/range of linear indices@inline function _maptasks(f::F, taskrange, threaded::_True)::Vector where F
+ ntasks = length(taskrange)
tasks_per_thread = 2
+ chunk_size = max(1, ntasks ÷ (tasks_per_thread * nthreads()))
task_chunks = Iterators.partition(taskrange, chunk_size)
tasks = map(task_chunks) do chunk
@spawn begin
f
over the chunk indices map(f, chunk)
+ end
+ end
return mapreduce(fetch, vcat, tasks)
+end
@assume_effects :foldable
to force the compiler to lookup through the closure. This alone makes e.g. flip
2.5x faster!Base.@assume_effects :foldable @inline function _maptasks(f::F, taskrange, threaded::_False)::Vector where F
+ map(f, taskrange)
+end
f
over ntasks, where f recieves an AbstractArray/range of linear indices@inline function _mapreducetasks(f::F, op, taskrange, threaded::_True; init) where F
+ ntasks = length(taskrange)
tasks_per_thread = 2
+ chunk_size = max(1, ntasks ÷ (tasks_per_thread * nthreads()))
task_chunks = Iterators.partition(taskrange, chunk_size)
tasks = map(task_chunks) do chunk
@spawn begin
f
over the chunk indices mapreduce(f, op, chunk; init)
+ end
+ end
return mapreduce(fetch, op, tasks; init)
+end
+Base.@assume_effects :foldable function _mapreducetasks(f::F, op, taskrange, threaded::_False; init) where F
+ mapreduce(f, op, taskrange; init)
+end
Closed Rings
export ClosedRing
Example
import GeoInterface as GI
+polygon = GI.Polygon([[(0, 0), (1, 0), (1, 1), (0, 1)]])
GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Int64, Int64}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Int64, Int64}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Int64, Int64}}, Nothing, Nothing}([(0, 0), (1, 0), (1, 1), (0, 1)], nothing, nothing)], nothing, nothing)
import GeometryOps as GO
+GO.fix(polygon, corrections = [GO.ClosedRing()])
GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)], nothing, nothing)], nothing, nothing)
Implementation
"""
+ ClosedRing() <: GeometryCorrection
+
+This correction ensures that a polygon's exterior and interior rings are closed.
+
+It can be called on any geometry correction as usual.
+
+See also \`GeometryCorrection\`.
+"""
+struct ClosedRing <: GeometryCorrection end
+
+application_level(::ClosedRing) = GI.PolygonTrait
+
+function (::ClosedRing)(::GI.PolygonTrait, polygon)
+ exterior = _close_linear_ring(GI.getexterior(polygon))
+
+ holes = map(GI.gethole(polygon)) do hole
+ _close_linear_ring(hole) # TODO: make this more efficient, or use tuples!
+ end
+
+ return GI.Wrappers.Polygon([exterior, holes...])
+end
+
+function _close_linear_ring(ring)
+ if GI.getpoint(ring, 1) == GI.getpoint(ring, GI.npoint(ring))
return ring
+ else
tups = tuples.(GI.getpoint(ring))
push!(tups, tups[1])
return GI.LinearRing(tups)
+ end
+end
Geometry Corrections
export fix
GeometryCorrection
abstract type, and the interface that any GeometryCorrection
must implement.ClosedRing
correction might be applied to a Polygon
to ensure that its exterior ring is closed.Interface
GeometryCorrection
s are callable structs which, when called, apply the correction to the given geometry, and return either a copy or the original geometry (if nothing needed to be corrected).abstract type GeometryCorrection
GeometryCorrection
must implement two functions: * application_level(::GeometryCorrection)::AbstractGeometryTrait
: This function should return the GeoInterface
trait that the correction is intended to be applied to, like PointTrait
or LineStringTrait
or PolygonTrait
. * (::GeometryCorrection)(::AbstractGeometryTrait, geometry)::(some_geometry)
: This function should apply the correction to the given geometry, and return a new geometry."""
+ abstract type GeometryCorrection
+
+This abstract type represents a geometry correction.
+
+# Interface
+
+Any \`GeometryCorrection\` must implement two functions:
+ * \`application_level(::GeometryCorrection)::AbstractGeometryTrait\`: This function should return the \`GeoInterface\` trait that the correction is intended to be applied to, like \`PointTrait\` or \`LineStringTrait\` or \`PolygonTrait\`.
+ * \`(::GeometryCorrection)(::AbstractGeometryTrait, geometry)::(some_geometry)\`: This function should apply the correction to the given geometry, and return a new geometry.
+"""
+abstract type GeometryCorrection end
+
+application_level(gc::GeometryCorrection) = error("Not implemented yet for $(gc)")
+
+(gc::GeometryCorrection)(geometry) = gc(GI.trait(geometry), geometry)
+
+(gc::GeometryCorrection)(trait::GI.AbstractGeometryTrait, geometry) = error("Not implemented yet for $(gc) and $(trait).")
+
+function fix(geometry; corrections = GeometryCorrection[ClosedRing(),], kwargs...)
+ traits = application_level.(corrections)
+ final_geometry = geometry
+ for Trait in (GI.PointTrait, GI.MultiPointTrait, GI.LineStringTrait, GI.LinearRingTrait, GI.MultiLineStringTrait, GI.PolygonTrait, GI.MultiPolygonTrait)
+ available_corrections = findall(x -> x == Trait, traits)
+ isempty(available_corrections) && continue
+ @debug "Correcting for $(Trait)"
+ net_function = reduce(∘, corrections[available_corrections])
+ final_geometry = apply(net_function, Trait, final_geometry; kwargs...)
+ end
+ return final_geometry
+end
Available corrections
ClosedRing() <: GeometryCorrection
GeometryCorrection
.DiffIntersectingPolygons() <: GeometryCorrection
difference
operation to create a unique set of disjoint (other than potentially connections by a single point) polygons covering the same area. See also GeometryCorrection
, UnionIntersectingPolygons
.abstract type GeometryCorrection
GeometryCorrection
must implement two functions: * application_level(::GeometryCorrection)::AbstractGeometryTrait
: This function should return the GeoInterface
trait that the correction is intended to be applied to, like PointTrait
or LineStringTrait
or PolygonTrait
. * (::GeometryCorrection)(::AbstractGeometryTrait, geometry)::(some_geometry)
: This function should apply the correction to the given geometry, and return a new geometry.UnionIntersectingPolygons() <: GeometryCorrection
GeometryCorrection
.Intersecting Polygons
export UnionIntersectingPolygons
UnionIntersectingPolygons
correction.Example
import GeoInterface as GI
+polygon = GI.Polygon([[(0.0, 0.0), (3.0, 0.0), (3.0, 3.0), (0.0, 3.0), (0.0, 0.0)]])
+multipolygon = GI.MultiPolygon([polygon, polygon])
GeoInterface.Wrappers.MultiPolygon{false, false, Vector{GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}}, Nothing, Nothing}[GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}([(0.0, 0.0), (3.0, 0.0), (3.0, 3.0), (0.0, 3.0), (0.0, 0.0)], nothing, nothing)], nothing, nothing), GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}([(0.0, 0.0), (3.0, 0.0), (3.0, 3.0), (0.0, 3.0), (0.0, 0.0)], nothing, nothing)], nothing, nothing)], nothing, nothing)
import GeometryOps as GO
+GO.fix(multipolygon, corrections = [GO.UnionIntersectingPolygons()])
GeoInterface.Wrappers.MultiPolygon{false, false, Vector{GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}}, Nothing, Nothing}[GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}([(0.0, 0.0), (3.0, 0.0), (3.0, 3.0), (0.0, 3.0), (0.0, 0.0)], nothing, nothing)], nothing, nothing)], nothing, nothing)
Implementation
"""
+ UnionIntersectingPolygons() <: GeometryCorrection
+
+This correction ensures that the polygon's included in a multipolygon aren't intersecting.
+If any polygon's are intersecting, they will be combined through the union operation to
+create a unique set of disjoint (other than potentially connections by a single point)
+polygons covering the same area.
+
+See also \`GeometryCorrection\`.
+"""
+struct UnionIntersectingPolygons <: GeometryCorrection end
+
+application_level(::UnionIntersectingPolygons) = GI.MultiPolygonTrait
+
+function (::UnionIntersectingPolygons)(::GI.MultiPolygonTrait, multipoly)
+ union_multipoly = tuples(multipoly)
+ n_polys = GI.npolygon(multipoly)
+ if n_polys > 1
+ keep_idx = trues(n_polys) # keep track of sub-polygons to remove
for (curr_idx, _) in Iterators.filter(last, Iterators.enumerate(keep_idx))
+ curr_poly = union_multipoly.geom[curr_idx]
+ poly_disjoint = false
+ while !poly_disjoint
+ poly_disjoint = true # assume current polygon is disjoint from others
+ for (next_idx, _) in Iterators.filter(last, Iterators.drop(Iterators.enumerate(keep_idx), curr_idx))
+ next_poly = union_multipoly.geom[next_idx]
+ if intersects(curr_poly, next_poly) # if two polygons intersect
+ new_polys = union(curr_poly, next_poly; target = GI.PolygonTrait())
+ n_new_polys = length(new_polys)
+ if n_new_polys == 1 # if polygons combined
+ poly_disjoint = false
+ union_multipoly.geom[curr_idx] = new_polys[1]
+ curr_poly = union_multipoly.geom[curr_idx]
+ keep_idx[next_idx] = false
+ end
+ end
+ end
+ end
+ end
+ keepat!(union_multipoly.geom, keep_idx)
+ end
+ return union_multipoly
+end
+
+"""
+ DiffIntersectingPolygons() <: GeometryCorrection
+This correction ensures that the polygons included in a multipolygon aren't intersecting.
+If any polygon's are intersecting, they will be made nonintersecting through the \`difference\`
+operation to create a unique set of disjoint (other than potentially connections by a single point)
+polygons covering the same area.
+See also \`GeometryCorrection\`, \`UnionIntersectingPolygons\`.
+"""
+struct DiffIntersectingPolygons <: GeometryCorrection end
+
+application_level(::DiffIntersectingPolygons) = GI.MultiPolygonTrait
+
+function (::DiffIntersectingPolygons)(::GI.MultiPolygonTrait, multipoly)
+ diff_multipoly = tuples(multipoly)
+ n_starting_polys = GI.npolygon(multipoly)
+ n_polys = n_starting_polys
+ if n_polys > 1
+ keep_idx = trues(n_polys) # keep track of sub-polygons to remove
for curr_idx in 1:n_starting_polys
+ !keep_idx[curr_idx] && continue
+ for next_idx in (curr_idx + 1):n_starting_polys
+ !keep_idx[next_idx] && continue
+ next_poly = diff_multipoly.geom[next_idx]
+ n_new_polys = 0
+ curr_pieces_added = (n_polys + 1):(n_polys + n_new_polys)
+ for curr_piece_idx in Iterators.flatten((curr_idx:curr_idx, curr_pieces_added))
+ !keep_idx[curr_piece_idx] && continue
+ curr_poly = diff_multipoly.geom[curr_piece_idx]
+ if intersects(curr_poly, next_poly) # if two polygons intersect
+ new_polys = difference(curr_poly, next_poly; target = GI.PolygonTrait())
+ n_new_pieces = length(new_polys) - 1
+ if n_new_pieces < 0 # current polygon is covered by next_polygon
+ keep_idx[curr_piece_idx] = false
+ break
+ elseif n_new_pieces ≥ 0
+ diff_multipoly.geom[curr_piece_idx] = new_polys[1]
+ curr_poly = diff_multipoly.geom[curr_piece_idx]
+ if n_new_pieces > 0 # current polygon breaks into several pieces
+ append!(diff_multipoly.geom, @view new_polys[2:end])
+ append!(keep_idx, trues(n_new_pieces))
+ n_new_polys += n_new_pieces
+ end
+ end
+ end
+ end
+ n_polys += n_new_polys
+ end
+ end
+ keepat!(diff_multipoly.geom, keep_idx)
+ end
+ return diff_multipoly
+end
Extent embedding
"""
+ embed_extent(obj)
+
+Recursively wrap the object with a GeoInterface.jl geometry,
+calculating and adding an \`Extents.Extent\` to all objects.
+
+This can improve performance when extents need to be checked multiple times,
+such when needing to check if many points are in geometries, and using their extents
+as a quick filter for obviously exterior points.
$THREADED_KEYWORD
+$CRS_KEYWORD
+"""
+embed_extent(x; threaded=false, crs=nothing) =
+ apply(identity, GI.PointTrait(), x; calc_extent=true, threaded, crs)
Coordinate flipping
apply
functionality in a function, by flipping the x and y coordinates of a geometry."""
+ flip(obj)
+
+Swap all of the x and y coordinates in obj, otherwise
+keeping the original structure (but not necessarily the
+original type).
+
+# Keywords
+
+$APPLY_KEYWORDS
+"""
+function flip(geom; kw...)
+ if _is3d(geom)
+ return apply(PointTrait(), geom; kw...) do p
+ (GI.y(p), GI.x(p), GI.z(p))
+ end
+ else
+ return apply(PointTrait(), geom; kw...) do p
+ (GI.y(p), GI.x(p))
+ end
+ end
+end
Geometry reprojection
export reproject
Proj
package for the transformation, but this could be moved to an extension if needed.GeometryOpsProjExt
extension module.apply
functionality."""\n reproject(geometry; source_crs, target_crs, transform, always_xy, time)\n reproject(geometry, source_crs, target_crs; always_xy, time)\n reproject(geometry, transform; always_xy, time)\n\nReproject any GeoInterface.jl compatible `geometry` from `source_crs` to `target_crs`.\n\nThe returned object will be constructed from `GeoInterface.WrapperGeometry`\ngeometries, wrapping views of a `Vector{Proj.Point{D}}`, where `D` is the dimension.\n\n!!! tip\n The `Proj.jl` package must be loaded for this method to work,\n since it is implemented in a package extension.\n\n# Arguments\n\n- `geometry`: Any GeoInterface.jl compatible geometries.\n- `source_crs`: the source coordinate referece system, as a GeoFormatTypes.jl object or a string.\n- `target_crs`: the target coordinate referece system, as a GeoFormatTypes.jl object or a string.\n\nIf these a passed as keywords, `transform` will take priority.\nWithout it `target_crs` is always needed, and `source_crs` is\nneeded if it is not retreivable from the geometry with `GeoInterface.crs(geometry)`.\n\n# Keywords\n\n- `always_xy`: force x, y coordinate order, `true` by default.\n `false` will expect and return points in the crs coordinate order.\n- `time`: the time for the coordinates. `Inf` by default.\n$APPLY_KEYWORDS\n"""\nfunction reproject end
Method error handling
function _reproject_error_hinter(io, exc, argtypes, kwargs)\n if isnothing(Base.get_extension(GeometryOps, :GeometryOpsProjExt)) && exc.f == reproject\n print(io, "\\n\\nThe `reproject` method requires the Proj.jl package to be explicitly loaded.\\n")\n print(io, "You can do this by simply typing ")\n printstyled(io, "using Proj"; color = :cyan, bold = true)\n println(io, " in your REPL, \\nor otherwise loading Proj.jl via using or import.")\n else # this is a more general error\n nothing\n end\nend
Segmentize
export segmentize
+export LinearSegments, GeodesicSegments
Examples
import GeometryOps as GO, GeoInterface as GI
+rectangle = GI.Wrappers.Polygon([[(0.0, 50.0), (7.071, 57.07), (0, 64.14), (-7.07, 57.07), (0.0, 50.0)]])
+linear = GO.segmentize(rectangle; max_distance = 5)
+collect(GI.getpoint(linear))
9-element Vector{Tuple{Float64, Float64}}:
+ (0.0, 50.0)
+ (3.5355, 53.535)
+ (7.071, 57.07)
+ (3.5355, 60.605000000000004)
+ (0.0, 64.14)
+ (-3.535, 60.605000000000004)
+ (-7.07, 57.07)
+ (-3.535, 53.535)
+ (0.0, 50.0)
using Proj # required to activate the \`GeodesicSegments\` method!
+geodesic = GO.segmentize(GO.GeodesicSegments(max_distance = 1000), rectangle)
+length(GI.getpoint(geodesic) |> collect)
3585
max_distance
is in meters, so this is a very fine-grained segmentation.using CairoMakie
+linear = GO.segmentize(rectangle; max_distance = 0.01)
+geodesic = GO.segmentize(GO.GeodesicSegments(; max_distance = 1000), rectangle)
+f, a, p = poly(collect(GI.getpoint(linear)); label = "Linear", axis = (; aspect = DataAspect()))
+p2 = poly!(collect(GI.getpoint(geodesic)); label = "Geodesic")
+axislegend(a; position = :lt)
+f
LinearSegments
. Check Documenter's build log for details.GeodesicSegments
. Check Documenter's build log for details.Benchmark
GEOSDensify
method, which is a similar method for densifying geometries.using BenchmarkTools: BenchmarkGroup
+using Chairmarks: @be
+using Main: plot_trials
+using CairoMakie
+
+import GeometryOps as GO, GeoInterface as GI, LibGEOS as LG
+
+segmentize_suite = BenchmarkGroup(["title:Segmentize", "subtitle:Segmentize a rectangle"])
+
+rectangle = GI.Wrappers.Polygon([[(0.0, 50.0), (7.071, 57.07), (0.0, 64.14), (-7.07, 57.07), (0.0, 50.0)]])
+lg_rectangle = GI.convert(LG, rectangle)
POLYGON ((0 50, 7.071 57.07, 0 64.14, -7.07 57.07, 0 50))
# These are initial distances, which yield similar numbers of points
+# in the final geometry.
+init_lin = 0.01
+init_geo = 900
+
+# LibGEOS.jl doesn't offer this function, so we just wrap it ourselves!
+function densify(obj::LG.Geometry, tol::Real, context::LG.GEOSContext = LG.get_context(obj))
+ result = LG.GEOSDensify_r(context, obj, tol)
+ if result == C_NULL
+ error("LibGEOS: Error in GEOSDensify")
+ end
+ LG.geomFromGEOS(result, context)
+end
+# now, we get to the actual benchmarking:
+for scalefactor in exp10.(LinRange(log10(0.1), log10(10), 5))
+ lin_dist = init_lin * scalefactor
+ geo_dist = init_geo * scalefactor
+
+ npoints_linear = GI.npoint(GO.segmentize(rectangle; max_distance = lin_dist))
+ npoints_geodesic = GO.segmentize(GO.GeodesicSegments(; max_distance = geo_dist), rectangle) |> GI.npoint
+ npoints_libgeos = GI.npoint(densify(lg_rectangle, lin_dist))
+
+ segmentize_suite["Linear"][npoints_linear] = @be GO.segmentize(GO.LinearSegments(; max_distance = $lin_dist), $rectangle) seconds=1
+ segmentize_suite["Geodesic"][npoints_geodesic] = @be GO.segmentize(GO.GeodesicSegments(; max_distance = $geo_dist), $rectangle) seconds=1
+ segmentize_suite["LibGEOS"][npoints_libgeos] = @be densify($lg_rectangle, $lin_dist) seconds=1
+
+end
+
+plot_trials(segmentize_suite)
abstract type SegmentizeMethod end
+"""
+ LinearSegments(; max_distance::Real)
+
+A method for segmentizing geometries by adding extra vertices to the geometry so that no segment is longer than a given distance.
+
+Here, \`max_distance\` is a purely nondimensional quantity and will apply in the input space. This is to say, that if the polygon is
+provided in lat/lon coordinates then the \`max_distance\` will be in degrees of arc. If the polygon is provided in meters, then the
+\`max_distance\` will be in meters.
+"""
+Base.@kwdef struct LinearSegments <: SegmentizeMethod
+ max_distance::Float64
+end
+
+"""
+ GeodesicSegments(; max_distance::Real, equatorial_radius::Real=6378137, flattening::Real=1/298.257223563)
+
+A method for segmentizing geometries by adding extra vertices to the geometry so that no segment is longer than a given distance.
+This method calculates the distance between points on the geodesic, and assumes input in lat/long coordinates.
+
+!!! warning
+ Any input geometries must be in lon/lat coordinates! If not, the method may fail or error.
+
+# Arguments
+- \`max_distance::Real\`: The maximum distance, **in meters**, between vertices in the geometry.
+- \`equatorial_radius::Real=6378137\`: The equatorial radius of the Earth, in meters. Passed to \`Proj.geod_geodesic\`.
+- \`flattening::Real=1/298.257223563\`: The flattening of the Earth, which is the ratio of the difference between the equatorial and polar radii to the equatorial radius. Passed to \`Proj.geod_geodesic\`.
+
+One can also omit the \`equatorial_radius\` and \`flattening\` keyword arguments, and pass a \`geodesic\` object directly to the eponymous keyword.
+
+This method uses the Proj/GeographicLib API for geodesic calculations.
+"""
+struct GeodesicSegments{T} <: SegmentizeMethod
+ geodesic::T# ::Proj.geod_geodesic
+ max_distance::Float64
+end
function _geodesic_segments_error_hinter(io, exc, argtypes, kwargs)
+ if isnothing(Base.get_extension(GeometryOps, :GeometryOpsProjExt)) && exc.f == GeodesicSegments
+ print(io, "\\n\\nThe \`GeodesicSegments\` method requires the Proj.jl package to be explicitly loaded.\\n")
+ print(io, "You can do this by simply typing ")
+ printstyled(io, "using Proj"; color = :cyan, bold = true)
+ println(io, " in your REPL, \\nor otherwise loading Proj.jl via using or import.")
+ end
+end
Implementation
"""
+ segmentize([method = LinearSegments()], geom; max_distance::Real, threaded)
+
+Segmentize a geometry by adding extra vertices to the geometry so that no segment is longer than a given distance.
+This is useful for plotting geometries with a limited number of vertices, or for ensuring that a geometry is not too "coarse" for a given application.
+
+# Arguments
+- \`method::SegmentizeMethod = LinearSegments()\`: The method to use for segmentizing the geometry. At the moment, only \`LinearSegments\` and \`GeodesicSegments\` are available.
+- \`geom\`: The geometry to segmentize. Must be a \`LineString\`, \`LinearRing\`, or greater in complexity.
+- \`max_distance::Real\`: The maximum distance, **in the input space**, between vertices in the geometry. Only used if you don't explicitly pass a \`method\`.
+
+Returns a geometry of similar type to the input geometry, but resampled.
+"""
+function segmentize(geom; max_distance, threaded::Union{Bool, BoolsAsTypes} = _False())
+ return segmentize(LinearSegments(; max_distance), geom; threaded = _booltype(threaded))
+end
+function segmentize(method::SegmentizeMethod, geom; threaded::Union{Bool, BoolsAsTypes} = _False())
+ @assert method.max_distance > 0 "\`max_distance\` should be positive and nonzero! Found $(method.max_distance)."
+ segmentize_function = Base.Fix1(_segmentize, method)
+ return apply(segmentize_function, Union{GI.LinearRingTrait, GI.LineStringTrait}, geom; threaded)
+end
+
+_segmentize(method, geom) = _segmentize(method, geom, GI.trait(geom))
+#=
+This is a method which performs the common functionality for both linear and geodesic algorithms,
+and calls out to the "kernel" function which we've defined per linesegment.
+=#
+function _segmentize(method::Union{LinearSegments, GeodesicSegments}, geom, T::Union{GI.LineStringTrait, GI.LinearRingTrait})
+ first_coord = GI.getpoint(geom, 1)
+ x1, y1 = GI.x(first_coord), GI.y(first_coord)
+ new_coords = NTuple{2, Float64}[]
+ sizehint!(new_coords, GI.npoint(geom))
+ push!(new_coords, (x1, y1))
+ for coord in Iterators.drop(GI.getpoint(geom), 1)
+ x2, y2 = GI.x(coord), GI.y(coord)
+ _fill_linear_kernel!(method, new_coords, x1, y1, x2, y2)
+ x1, y1 = x2, y2
+ end
+ return rebuild(geom, new_coords)
+end
+
+function _fill_linear_kernel!(method::LinearSegments, new_coords::Vector, x1, y1, x2, y2)
+ dx, dy = x2 - x1, y2 - y1
+ distance = hypot(dx, dy) # this is a more stable way to compute the Euclidean distance
+ if distance > method.max_distance
+ n_segments = ceil(Int, distance / method.max_distance)
+ for i in 1:(n_segments - 1)
+ t = i / n_segments
+ push!(new_coords, (x1 + t * dx, y1 + t * dy))
+ end
+ end
push!(new_coords, (x2, y2))
+ return nothing
+end
_fill_linear_kernel
definition for GeodesicSegments
is in the GeometryOpsProjExt
extension module, in the segmentize.jl
file.Geometry simplification
Examples
using Makie, GeoInterfaceMakie
+import GeoInterface as GI
+import GeometryOps as GO
+
+original = GI.Polygon([[[-70.603637, -33.399918], [-70.614624, -33.395332], [-70.639343, -33.392466], [-70.659942, -33.394759], [-70.683975, -33.404504], [-70.697021, -33.419406], [-70.701141, -33.434306], [-70.700454, -33.446339], [-70.694274, -33.458369], [-70.682601, -33.465816], [-70.668869, -33.472117], [-70.646209, -33.473835], [-70.624923, -33.472117], [-70.609817, -33.468107], [-70.595397, -33.458369], [-70.587158, -33.442901], [-70.587158, -33.426283], [-70.590591, -33.414248], [-70.594711, -33.406224], [-70.603637, -33.399918]]])
+
+simple = GO.simplify(original; number=6)
+
+f, a, p = poly(original; label = "Original")
+poly!(simple; label = "Simplified")
+axislegend(a)
+f
Benchmark
simplify
implementation, which uses the Douglas-Peucker algorithm.using BenchmarkTools, Chairmarks, GeoJSON, CairoMakie
+import GeometryOps as GO, LibGEOS as LG, GeoInterface as GI
+using CoordinateTransformations
+lg_and_go(geometry) = (GI.convert(LG, geometry), GO.tuples(geometry))
+# Load in the Natural Earth admin GeoJSON, then extract the USA's geometry
+fc = GeoJSON.read(read(download("https://rawcdn.githack.com/nvkelso/natural-earth-vector/ca96624a56bd078437bca8184e78163e5039ad19/geojson/ne_10m_admin_0_countries.geojson")))
+usa_multipoly = fc.geometry[findfirst(==("United States of America"), fc.NAME)] |> x -> GI.convert(LG, x) |> LG.makeValid |> GO.tuples
+include(joinpath(dirname(dirname(pathof(GO))), "test", "data", "polygon_generation.jl"))
+
+usa_poly = GI.getgeom(usa_multipoly, findmax(GO.area.(GI.getgeom(usa_multipoly)))[2]) # isolate the poly with the most area
+usa_centroid = GO.centroid(usa_poly)
+usa_reflected = GO.transform(Translation(usa_centroid...) ∘ LinearMap(Makie.rotmatrix2d(π)) ∘ Translation((-).(usa_centroid)...), usa_poly)
+f, a, p = plot(usa_poly; label = "Original", axis = (; aspect = DataAspect()))#; plot!(usa_reflected; label = "Reflected")
simplify_suite = BenchmarkGroup(["Simplify"])
+singlepoly_suite = BenchmarkGroup(["Polygon", "title:Polygon simplify", "subtitle:Random blob"])
+
+include(joinpath(dirname(dirname(pathof(GO))), "test", "data", "polygon_generation.jl"))
+
+for n_verts in round.(Int, exp10.(LinRange(log10(10), log10(10_000), 10)))
+ geom = GI.Wrappers.Polygon(generate_random_poly(0, 0, n_verts, 2, 0.2, 0.3))
+ geom_lg, geom_go = lg_and_go(LG.makeValid(GI.convert(LG, geom)))
+ singlepoly_suite["GO-DP"][GI.npoint(geom)] = @be GO.simplify($geom_go; tol = 0.1) seconds=1
+ singlepoly_suite["GO-VW"][GI.npoint(geom)] = @be GO.simplify($(GO.VisvalingamWhyatt(; tol = 0.1)), $geom_go) seconds=1
+ singlepoly_suite["GO-RD"][GI.npoint(geom)] = @be GO.simplify($(GO.RadialDistance(; tol = 0.1)), $geom_go) seconds=1
+ singlepoly_suite["LibGEOS"][GI.npoint(geom)] = @be LG.simplify($geom_lg, 0.1) seconds=1
+end
+
+plot_trials(singlepoly_suite; legend_position=(1, 1, TopRight()), legend_valign = -2, legend_halign = 1.2, legend_orientation = :horizontal)
multipoly_suite = BenchmarkGroup(["MultiPolygon", "title:Multipolygon simplify", "subtitle:USA multipolygon"])
+
+for frac in exp10.(LinRange(log10(0.3), log10(1), 6)) # TODO: this example isn't the best. How can we get this better?
+ geom = GO.simplify(usa_multipoly; ratio = frac)
+ geom_lg, geom_go = lg_and_go(geom)
+ _tol = 0.001
+ multipoly_suite["GO-DP"][GI.npoint(geom)] = @be GO.simplify($geom_go; tol = $_tol) seconds=1
+ # multipoly_suite["GO-VW"][GI.npoint(geom)] = @be GO.simplify($(GO.VisvalingamWhyatt(; tol = $_tol)), $geom_go) seconds=1
+ multipoly_suite["GO-RD"][GI.npoint(geom)] = @be GO.simplify($(GO.RadialDistance(; tol = _tol)), $geom_go) seconds=1
+ multipoly_suite["LibGEOS"][GI.npoint(geom)] = @be LG.simplify($geom_lg, $_tol) seconds=1
+ println("""
+ For $(GI.npoint(geom)) points, the algorithms generated polygons with the following number of vertices:
+ GO-DP : $(GI.npoint( GO.simplify(geom_go; tol = _tol)))
+ GO-RD : $(GI.npoint( GO.simplify((GO.RadialDistance(; tol = _tol)), geom_go)))
+ LGeos : $(GI.npoint( LG.simplify(geom_lg, _tol)))
+ """)
+ # GO-VW : $(GI.npoint( GO.simplify((GO.VisvalingamWhyatt(; tol = _tol)), geom_go)))
+ println()
+end
+plot_trials(multipoly_suite)
export simplify, VisvalingamWhyatt, DouglasPeucker, RadialDistance
+
+const _SIMPLIFY_TARGET = TraitTarget{Union{GI.PolygonTrait, GI.AbstractCurveTrait, GI.MultiPointTrait, GI.PointTrait}}()
+const MIN_POINTS = 3
+const SIMPLIFY_ALG_KEYWORDS = """
+# Keywords
+
+- \`ratio\`: the fraction of points that should remain after \`simplify\`.
+ Useful as it will generalise for large collections of objects.
+- \`number\`: the number of points that should remain after \`simplify\`.
+ Less useful for large collections of mixed size objects.
+"""
+const DOUGLAS_PEUCKER_KEYWORDS = """
+$SIMPLIFY_ALG_KEYWORDS
+- \`tol\`: the minimum distance a point will be from the line
+ joining its neighboring points.
+"""
+
+"""
+ abstract type SimplifyAlg
+
+Abstract type for simplification algorithms.
+
+# API
+
+For now, the algorithm must hold the \`number\`, \`ratio\` and \`tol\` properties.
+
+Simplification algorithm types can hook into the interface by implementing
+the \`_simplify(trait, alg, geom)\` methods for whichever traits are necessary.
+"""
+abstract type SimplifyAlg end
+
+"""
+ simplify(obj; kw...)
+ simplify(::SimplifyAlg, obj; kw...)
+
+Simplify a geometry, feature, feature collection,
+or nested vectors or a table of these.
+
+\`RadialDistance\`, \`DouglasPeucker\`, or
+\`VisvalingamWhyatt\` algorithms are available,
+listed in order of increasing quality but decreaseing performance.
+
+\`PoinTrait\` and \`MultiPointTrait\` are returned unchanged.
+
+The default behaviour is \`simplify(DouglasPeucker(; kw...), obj)\`.
+Pass in other \`SimplifyAlg\` to use other algorithms.
- \`prefilter_alg\`: \`SimplifyAlg\` algorithm used to pre-filter object before
+ using primary filtering algorithm.
+$APPLY_KEYWORDS
+
+
+Keywords for DouglasPeucker are allowed when no algorithm is specified:
+
+$DOUGLAS_PEUCKER_KEYWORDS
Simplify a polygon to have six points:
+
+\`\`\`jldoctest
+import GeoInterface as GI
+import GeometryOps as GO
+
+poly = GI.Polygon([[
+ [-70.603637, -33.399918],
+ [-70.614624, -33.395332],
+ [-70.639343, -33.392466],
+ [-70.659942, -33.394759],
+ [-70.683975, -33.404504],
+ [-70.697021, -33.419406],
+ [-70.701141, -33.434306],
+ [-70.700454, -33.446339],
+ [-70.694274, -33.458369],
+ [-70.682601, -33.465816],
+ [-70.668869, -33.472117],
+ [-70.646209, -33.473835],
+ [-70.624923, -33.472117],
+ [-70.609817, -33.468107],
+ [-70.595397, -33.458369],
+ [-70.587158, -33.442901],
+ [-70.587158, -33.426283],
+ [-70.590591, -33.414248],
+ [-70.594711, -33.406224],
+ [-70.603637, -33.399918]]])
+
+simple = GO.simplify(poly; number=6)
+GI.npoint(simple)
6
+\`\`\`
+"""
+simplify(alg::SimplifyAlg, data; kw...) = _simplify(alg, data; kw...)
simplify(
+ data; prefilter_alg = nothing,
+ calc_extent=false, threaded=false, crs=nothing, kw...,
+ ) = _simplify(DouglasPeucker(; kw...), data; prefilter_alg, calc_extent, threaded, crs)
+
+
+#= For each algorithm, apply simplication to all curves, multipoints, and
+points, reconstructing everything else around them. =#
+function _simplify(alg::SimplifyAlg, data; prefilter_alg=nothing, kw...)
+ simplifier(geom) = _simplify(GI.trait(geom), alg, geom; prefilter_alg)
+ return apply(simplifier, _SIMPLIFY_TARGET, data; kw...)
+end
+
+
+# For Point and MultiPoint traits we do nothing
+_simplify(::GI.PointTrait, alg, geom; kw...) = geom
+_simplify(::GI.MultiPointTrait, alg, geom; kw...) = geom
+
+# For curves, rings, and polygon we simplify
+function _simplify(
+ ::GI.AbstractCurveTrait, alg, geom;
+ prefilter_alg, preserve_endpoint = true,
+)
+ points = if isnothing(prefilter_alg)
+ tuple_points(geom)
+ else
+ _simplify(prefilter_alg, tuple_points(geom), preserve_endpoint)
+ end
+ return rebuild(geom, _simplify(alg, points, preserve_endpoint))
+end
+
+function _simplify(::GI.PolygonTrait, alg, geom; kw...)
+ # Force treating children as LinearRing
+ simplifier(g) = _simplify(
+ GI.LinearRingTrait(), alg, g;
+ kw..., preserve_endpoint = false,
+ )
+ lrs = map(simplifier, GI.getgeom(geom))
+ return rebuild(geom, lrs)
+end
Simplify with RadialDistance Algorithm
"""
+ RadialDistance <: SimplifyAlg
+
+Simplifies geometries by removing points less than
+\`tol\` distance from the line between its neighboring points.
+
+$SIMPLIFY_ALG_KEYWORDS
+- \`tol\`: the minimum distance between points.
+
+Note: user input \`tol\` is squared to avoid uneccesary computation in algorithm.
+"""
+@kwdef struct RadialDistance <: SimplifyAlg
+ number::Union{Int64,Nothing} = nothing
+ ratio::Union{Float64,Nothing} = nothing
+ tol::Union{Float64,Nothing} = nothing
+
+ function RadialDistance(number, ratio, tol)
+ _checkargs(number, ratio, tol)
tol = isnothing(tol) ? tol : tol^2
+ new(number, ratio, tol)
+ end
+end
+
+function _simplify(alg::RadialDistance, points::Vector, _)
+ previous = first(points)
+ distances = Array{Float64}(undef, length(points))
+ for i in eachindex(points)
+ point = points[i]
+ distances[i] = _squared_euclid_distance(Float64, point, previous)
+ previous = point
+ end
+ # Never remove the end points
+ distances[begin] = distances[end] = Inf
+ return _get_points(alg, points, distances)
+end
Simplify with DouglasPeucker Algorithm
"""
+ DouglasPeucker <: SimplifyAlg
+
+ DouglasPeucker(; number, ratio, tol)
+
+Simplifies geometries by removing points below \`tol\`
+distance from the line between its neighboring points.
+
+$DOUGLAS_PEUCKER_KEYWORDS
+Note: user input \`tol\` is squared to avoid uneccesary computation in algorithm.
+"""
+@kwdef struct DouglasPeucker <: SimplifyAlg
+ number::Union{Int64,Nothing} = nothing
+ ratio::Union{Float64,Nothing} = nothing
+ tol::Union{Float64,Nothing} = nothing
+
+ function DouglasPeucker(number, ratio, tol)
+ _checkargs(number, ratio, tol)
tol = isnothing(tol) ? tol : tol^2
+ return new(number, ratio, tol)
+ end
+end
+
+#= Simplify using the DouglasPeucker algorithm - nice gif of process on wikipedia:
+(https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm). =#
+function _simplify(alg::DouglasPeucker, points::Vector, preserve_endpoint)
+ npoints = length(points)
+ npoints <= MIN_POINTS && return points
max_points = if !isnothing(alg.tol)
+ npoints
+ else
+ npts = !isnothing(alg.number) ? alg.number : max(3, round(Int, alg.ratio * npoints))
+ npts ≥ npoints && return points
+ npts
+ end
+ max_tol = !isnothing(alg.tol) ? alg.tol : zero(Float64)
queue = Vector{Tuple{Int, Int, Int, Float64}}()
+ queue_idx, queue_dist = 0, zero(Float64)
+ len_queue = 0
results = Vector{Int}(undef, max_points + (preserve_endpoint ? 0 : 1))
+ results[1], results[2] = 1, npoints
i = 2 # already have first and last point added
+ start_idx, end_idx = 1, npoints
+ max_idx, max_dist = _find_max_squared_dist(points, start_idx, end_idx)
+ while i ≤ min(MIN_POINTS + 1, max_points) || (i < max_points && max_dist > max_tol)
i += 1
+ results[i] = max_idx
left_idx, left_dist = _find_max_squared_dist(points, start_idx, max_idx)
+ right_idx, right_dist = _find_max_squared_dist(points, max_idx, end_idx)
+ left_vals = (start_idx, left_idx, max_idx, left_dist)
+ right_vals = (max_idx, right_idx, end_idx, right_dist)
if queue_dist > left_dist && queue_dist > right_dist
start_idx, max_idx, end_idx, max_dist = queue[queue_idx]
if left_dist > 0
+ queue[queue_idx] = left_vals
+ if right_dist > 0
+ push!(queue, right_vals)
+ len_queue += 1
+ end
+ elseif right_dist > 0
+ queue[queue_idx] = right_vals
+ else
+ deleteat!(queue, queue_idx)
+ len_queue -= 1
+ end
queue_dist, queue_idx = !isempty(queue) ?
+ findmax(x -> x[4], queue) : (zero(Float64), 0)
+ elseif left_dist > right_dist # use left value as next value to add to results
+ push!(queue, right_vals) # add right value to queue
+ len_queue += 1
+ if right_dist > queue_dist
+ queue_dist = right_dist
+ queue_idx = len_queue
+ end
+ start_idx, max_idx, end_idx, max_dist = left_vals
+ else # use right value as next value to add to results
+ push!(queue, left_vals) # add left value to queue
+ len_queue += 1
+ if left_dist > queue_dist
+ queue_dist = left_dist
+ queue_idx = len_queue
+ end
+ start_idx, max_idx, end_idx, max_dist = right_vals
+ end
+ end
+ sorted_results = sort!(@view results[1:i])
+ if !preserve_endpoint && i > 3
pre_pt, post_pt = points[sorted_results[end - 1]], points[sorted_results[2]]
+ endpt_dist = _squared_distance_line(Float64, points[1], pre_pt, post_pt)
+ if !isnothing(alg.tol)
if endpt_dist < max_tol
+ results[i] = results[2]
+ sorted_results = @view results[2:i]
+ end
+ else
if endpt_dist < max_dist
+ insert!(results, searchsortedfirst(sorted_results, max_idx), max_idx)
+ results[i+1] = results[2]
+ sorted_results = @view results[2:i+1]
+ end
+ end
+ end
+ return points[sorted_results]
+end
+
+#= find maximum distance of any point between the start_idx and end_idx to the line formed
+by conencting the points at start_idx and end_idx. Note that the first index of maximum
+value will be used, which might cause differences in results from other algorithms.=#
+function _find_max_squared_dist(points, start_idx, end_idx)
+ max_idx = start_idx
+ max_dist = zero(Float64)
+ for i in (start_idx + 1):(end_idx - 1)
+ d = _squared_distance_line(Float64, points[i], points[start_idx], points[end_idx])
+ if d > max_dist
+ max_dist = d
+ max_idx = i
+ end
+ end
+ return max_idx, max_dist
+end
Simplify with VisvalingamWhyatt Algorithm
"""
+ VisvalingamWhyatt <: SimplifyAlg
+
+ VisvalingamWhyatt(; kw...)
+
+Simplifies geometries by removing points below \`tol\`
+distance from the line between its neighboring points.
+
+$SIMPLIFY_ALG_KEYWORDS
+- \`tol\`: the minimum area of a triangle made with a point and
+ its neighboring points.
+Note: user input \`tol\` is doubled to avoid uneccesary computation in algorithm.
+"""
+@kwdef struct VisvalingamWhyatt <: SimplifyAlg
+ number::Union{Int,Nothing} = nothing
+ ratio::Union{Float64,Nothing} = nothing
+ tol::Union{Float64,Nothing} = nothing
+
+ function VisvalingamWhyatt(number, ratio, tol)
+ _checkargs(number, ratio, tol)
tol = isnothing(tol) ? tol : tol*2
+ return new(number, ratio, tol)
+ end
+end
+
+function _simplify(alg::VisvalingamWhyatt, points::Vector, _)
+ length(points) <= MIN_POINTS && return points
+ areas = _build_tolerances(_triangle_double_area, points)
+ return _get_points(alg, points, areas)
+end
_triangle_double_area(p1, p2, p3) =
+ abs(p1[1] * (p2[2] - p3[2]) + p2[1] * (p3[2] - p1[2]) + p3[1] * (p1[2] - p2[2]))
Shared utils
function _build_tolerances(f, points)
+ nmax = length(points)
+ real_tolerances = _flat_tolerances(f, points)
+
+ tolerances = copy(real_tolerances)
+ i = [n for n in 1:nmax]
+
+ this_tolerance, min_vert = findmin(tolerances)
+ _remove!(tolerances, min_vert)
+ deleteat!(i, min_vert)
+
+ while this_tolerance < Inf
+ skip = false
+
+ if min_vert < length(i)
+ right_tolerance = f(
+ points[i[min_vert - 1]],
+ points[i[min_vert]],
+ points[i[min_vert + 1]],
+ )
+ if right_tolerance <= this_tolerance
+ right_tolerance = this_tolerance
+ skip = min_vert == 1
+ end
+
+ real_tolerances[i[min_vert]] = right_tolerance
+ tolerances[min_vert] = right_tolerance
+ end
+
+ if min_vert > 2
+ left_tolerance = f(
+ points[i[min_vert - 2]],
+ points[i[min_vert - 1]],
+ points[i[min_vert]],
+ )
+ if left_tolerance <= this_tolerance
+ left_tolerance = this_tolerance
+ skip = min_vert == 2
+ end
+ real_tolerances[i[min_vert - 1]] = left_tolerance
+ tolerances[min_vert - 1] = left_tolerance
+ end
+
+ if !skip
+ min_vert = argmin(tolerances)
+ end
+ deleteat!(i, min_vert)
+ this_tolerance = tolerances[min_vert]
+ _remove!(tolerances, min_vert)
+ end
+
+ return real_tolerances
+end
+
+function tuple_points(geom)
+ points = Array{Tuple{Float64,Float64}}(undef, GI.npoint(geom))
+ for (i, p) in enumerate(GI.getpoint(geom))
+ points[i] = (GI.x(p), GI.y(p))
+ end
+ return points
+end
+
+function _get_points(alg, points, tolerances)
+ # This assumes that \`alg\` has the properties
+ # \`tol\`, \`number\`, and \`ratio\` available...
+ tol = alg.tol
+ number = alg.number
+ ratio = alg.ratio
+ bit_indices = if !isnothing(tol)
+ _tol_indices(alg.tol::Float64, points, tolerances)
+ elseif !isnothing(number)
+ _number_indices(alg.number::Int64, points, tolerances)
+ else
+ _ratio_indices(alg.ratio::Float64, points, tolerances)
+ end
+ return points[bit_indices]
+end
+
+function _tol_indices(tol, points, tolerances)
+ tolerances .>= tol
+end
+
+function _number_indices(n, points, tolerances)
+ tol = partialsort(tolerances, length(points) - n + 1)
+ bit_indices = _tol_indices(tol, points, tolerances)
+ nselected = sum(bit_indices)
+ # If there are multiple values exactly at \`tol\` we will get
+ # the wrong output length. So we need to remove some.
+ while nselected > n
+ min_tol = Inf
+ min_i = 0
+ for i in eachindex(bit_indices)
+ bit_indices[i] || continue
+ if tolerances[i] < min_tol
+ min_tol = tolerances[i]
+ min_i = i
+ end
+ end
+ nselected -= 1
+ bit_indices[min_i] = false
+ end
+ return bit_indices
+end
+
+function _ratio_indices(r, points, tolerances)
+ n = max(3, round(Int, r * length(points)))
+ return _number_indices(n, points, tolerances)
+end
+
+function _flat_tolerances(f, points)::Vector{Float64}
+ result = Vector{Float64}(undef, length(points))
+ result[1] = result[end] = Inf
+
+ for i in 2:length(result) - 1
+ result[i] = f(points[i-1], points[i], points[i+1])
+ end
+ return result
+end
+
+function _remove!(s, i)
+ for j in i:lastindex(s)-1
+ s[j] = s[j+1]
+ end
+end
function _checkargs(number, ratio, tol)
+ count(isnothing, (number, ratio, tol)) == 2 ||
+ error("Must provide one of \`number\`, \`ratio\` or \`tol\` keywords")
+ if !isnothing(number)
+ if number < MIN_POINTS
+ error("\`number\` must be $MIN_POINTS or larger. Got $number")
+ end
+ elseif !isnothing(ratio)
+ if ratio <= 0 || ratio > 1
+ error("\`ratio\` must be 0 < ratio <= 1. Got $ratio")
+ end
+ else # !isnothing(tol)
+ if tol ≤ 0
+ error("\`tol\` must be a positive number. Got $tol")
+ end
+ end
+ return nothing
+end
Pointwise transformation
"""
+ transform(f, obj)
+
+Apply a function \`f\` to all the points in \`obj\`.
+
+Points will be passed to \`f\` as an \`SVector\` to allow
+using CoordinateTransformations.jl and Rotations.jl
+without hassle.
+
+\`SVector\` is also a valid GeoInterface.jl point, so will
+work in all GeoInterface.jl methods.
+
+# Example
+
+\`\`\`julia
+julia> import GeoInterface as GI
+
+julia> import GeometryOps as GO
+
+julia> geom = GI.Polygon([GI.LinearRing([(1, 2), (3, 4), (5, 6), (1, 2)]), GI.LinearRing([(3, 4), (5, 6), (6, 7), (3, 4)])]);
+
+julia> f = CoordinateTransformations.Translation(3.5, 1.5)
+Translation(3.5, 1.5)
+
+julia> GO.transform(f, geom)
+GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.Linea
+rRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}(StaticArraysCo
+re.SVector{2, Float64}[[4.5, 3.5], [6.5, 5.5], [8.5, 7.5], [4.5, 3.5]], nothing, nothing), GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}(StaticA
+rraysCore.SVector{2, Float64}[[6.5, 5.5], [8.5, 7.5], [9.5, 8.5], [6.5, 5.5]], nothing, nothing)], nothing, nothing)
+\`\`\`
+
+With Rotations.jl you need to actuall multiply the Rotation
+by the \`SVector\` point, which is easy using an anonymous function.
+
+\`\`\`julia
+julia> using Rotations
+
+julia> GO.transform(p -> one(RotMatrix{2}) * p, geom)
+GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Int64}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.LinearR
+ing{false, false, Vector{StaticArraysCore.SVector{2, Int64}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Int64}}, Nothing, Nothing}(StaticArraysCore.SVe
+ctor{2, Int64}[[2, 1], [4, 3], [6, 5], [2, 1]], nothing, nothing), GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Int64}}, Nothing, Nothing}(StaticArraysCore.SVector{2, Int64
+}[[4, 3], [6, 5], [7, 6], [4, 3]], nothing, nothing)], nothing, nothing)
+\`\`\`
+"""
+function transform(f, geom; kw...)
+ if _is3d(geom)
+ return apply(PointTrait(), geom; kw...) do p
+ f(StaticArrays.SVector{3}((GI.x(p), GI.y(p), GI.z(p))))
+ end
+ else
+ return apply(PointTrait(), geom; kw...) do p
+ f(StaticArrays.SVector{2}((GI.x(p), GI.y(p))))
+ end
+ end
+end
Tuple conversion
"""
+ tuples(obj)
+
+Convert all points in \`obj\` to \`Tuple\`s, wherever the are nested.
+
+Returns a similar object or collection of objects using GeoInterface.jl
+geometries wrapping \`Tuple\` points.
$APPLY_KEYWORDS
+"""
+function tuples(geom, ::Type{T} = Float64; kw...) where T
+ if _is3d(geom)
+ return apply(PointTrait(), geom; kw...) do p
+ (T(GI.x(p)), T(GI.y(p)), T(GI.z(p)))
+ end
+ else
+ return apply(PointTrait(), geom; kw...) do p
+ (T(GI.x(p)), T(GI.y(p)))
+ end
+ end
+end
Utility functions
_is3d(geom)::Bool = _is3d(GI.trait(geom), geom)
+_is3d(::GI.AbstractGeometryTrait, geom)::Bool = GI.is3d(geom)
+_is3d(::GI.FeatureTrait, feature)::Bool = _is3d(GI.geometry(feature))
+_is3d(::GI.FeatureCollectionTrait, fc)::Bool = _is3d(GI.getfeature(fc, 1))
+_is3d(::Nothing, geom)::Bool = _is3d(first(geom)) # Otherwise step into an itererable
+
+_npoint(x) = _npoint(trait(x), x)
+_npoint(::Nothing, xs::AbstractArray) = sum(_npoint, xs)
+_npoint(::GI.FeatureCollectionTrait, fc) = sum(_npoint, GI.getfeature(fc))
+_npoint(::GI.FeatureTrait, f) = _npoint(GI.geometry(f))
+_npoint(::GI.AbstractGeometryTrait, x) = GI.npoint(trait(x), x)
+
+_nedge(x) = _nedge(trait(x), x)
+_nedge(::Nothing, xs::AbstractArray) = sum(_nedge, xs)
+_nedge(::GI.FeatureCollectionTrait, fc) = sum(_nedge, GI.getfeature(fc))
+_nedge(::GI.FeatureTrait, f) = _nedge(GI.geometry(f))
+function _nedge(::GI.AbstractGeometryTrait, x)
+ n = 0
+ for g in GI.getgeom(x)
+ n += _nedge(g)
+ end
+ return n
+end
+_nedge(::GI.AbstractCurveTrait, x) = GI.npoint(x) - 1
+_nedge(::GI.PointTrait, x) = error("Cant get edges from points")
+
+
+"""
+ polygon_to_line(poly::Polygon)
+
+Converts a Polygon to LineString or MultiLineString
\`\`\`jldoctest
+import GeometryOps as GO, GeoInterface as GI
+
+poly = GI.Polygon([[(-2.275543, 53.464547), (-2.275543, 53.489271), (-2.215118, 53.489271), (-2.215118, 53.464547), (-2.275543, 53.464547)]])
+GO.polygon_to_line(poly)
GeoInterface.Wrappers.LineString{false, false, Vector{Tuple{Float64, Float64}}, Nothing, Nothing}([(-2.275543, 53.464547), (-2.275543, 53.489271), (-2.215118, 53.489271), (-2.215118, 53.464547), (-2.275543, 53.464547)], nothing, nothing)
+\`\`\`
+"""
+function polygon_to_line(poly)
+ @assert GI.trait(poly) isa PolygonTrait
+ GI.ngeom(poly) > 1 && return GI.MultiLineString(collect(GI.getgeom(poly)))
+ return GI.LineString(collect(GI.getgeom(GI.getgeom(poly, 1))))
+end
+
+
+"""
+ to_edges()
+
+Convert any geometry or collection of geometries into a flat
+vector of \`Tuple{Tuple{Float64,Float64},Tuple{Float64,Float64}}\` edges.
+"""
+function to_edges(x, ::Type{T} = Float64) where T
+ edges = Vector{Edge{T}}(undef, _nedge(x))
+ _to_edges!(edges, x, 1)
+ return edges
+end
+
+_to_edges!(edges::Vector, x, n) = _to_edges!(edges, trait(x), x, n)
+function _to_edges!(edges::Vector, ::GI.FeatureCollectionTrait, fc, n)
+ for f in GI.getfeature(fc)
+ n = _to_edges!(edges, f, n)
+ end
+end
+_to_edges!(edges::Vector, ::GI.FeatureTrait, f, n) = _to_edges!(edges, GI.geometry(f), n)
+function _to_edges!(edges::Vector, ::GI.AbstractGeometryTrait, fc, n)
+ for f in GI.getgeom(fc)
+ n = _to_edges!(edges, f, n)
+ end
+end
+function _to_edges!(edges::Vector, ::GI.AbstractCurveTrait, geom, n)
+ p1 = GI.getpoint(geom, 1)
+ p1x, p1y = GI.x(p1), GI.y(p1)
+ for i in 2:GI.npoint(geom)
+ p2 = GI.getpoint(geom, i)
+ p2x, p2y = GI.x(p2), GI.y(p2)
+ edges[n] = (p1x, p1y), (p2x, p2y)
+ p1x, p1y = p2x, p2y
+ n += 1
+ end
+ return n
+end
+
+_tuple_point(p) = GI.x(p), GI.y(p)
+_tuple_point(p, ::Type{T}) where T = T(GI.x(p)), T(GI.y(p))
+
+function to_extent(edges::Vector{Edge})
+ x, y = extrema(first, edges)
+ Extents.Extent(X=x, Y=y)
+end
+
+function to_points(x, ::Type{T} = Float64) where T
+ points = Vector{TuplePoint{T}}(undef, _npoint(x))
+ _to_points!(points, x, 1)
+ return points
+end
+
+_to_points!(points::Vector, x, n) = _to_points!(points, trait(x), x, n)
+function _to_points!(points::Vector, ::FeatureCollectionTrait, fc, n)
+ for f in GI.getfeature(fc)
+ n = _to_points!(points, f, n)
+ end
+end
+_to_points!(points::Vector, ::FeatureTrait, f, n) = _to_points!(points, GI.geometry(f), n)
+function _to_points!(points::Vector, ::AbstractGeometryTrait, fc, n)
+ for f in GI.getgeom(fc)
+ n = _to_points!(points, f, n)
+ end
+end
+function _to_points!(points::Vector, ::Union{AbstractCurveTrait,MultiPointTrait}, geom, n)
+ n = 0
+ for p in GI.getpoint(geom)
+ n += 1
+ points[n] = _tuple_point(p)
+ end
+ return n
+end
+
+function _point_in_extent(p, extent::Extents.Extent)
+ (x1, x2), (y1, y2) = extent.X, extent.Y
+ return x1 ≤ GI.x(p) ≤ x2 && y1 ≤ GI.y(p) ≤ y2
+end
by_pred
joining method. This allows the user to specify a predicate in the following manner:[inner/left/right/outer/...]join((table1, table1),
+ by_pred(:table1_column, predicate_function, :table2_column) # & add other conditions here
+)
GO.contains, GO.within, GO.intersects, GO.touches, GO.crosses, GO.disjoint, GO.overlaps, GO.covers, GO.coveredby, GO.equals
Simple example
contains
predicate from GeometryOps, which checks if each point is contained within any of the polygons. The resulting joined DataFrame is then used to plot the points, colored according to the containing polygon.import GeoInterface as GI, GeometryOps as GO
+using FlexiJoins, DataFrames
+
+using CairoMakie, GeoInterfaceMakie
+
+pl = GI.Polygon([GI.LinearRing([(0, 0), (1, 0), (1, 1), (0, 0)])])
+pu = GI.Polygon([GI.LinearRing([(0, 0), (0, 1), (1, 1), (0, 0)])])
+poly_df = DataFrame(geometry = [pl, pu], color = [:red, :blue])
+f, a, p = poly(poly_df.geometry; color = tuple.(poly_df.color, 0.3))
points = tuple.(rand(1000), rand(1000))
+points_df = DataFrame(geometry = points)
+scatter!(points_df.geometry)
+f
@time joined_df = FlexiJoins.innerjoin(
+ (points_df, poly_df),
+ by_pred(:geometry, GO.within, :geometry)
+)
scatter!(a, joined_df.geometry; color = joined_df.color)
+f
Real-world example
import GeoInterface as GI, GeometryOps as GO
+using FlexiJoins, DataFrames, GADM # GADM gives us country and sublevel geometry
+
+using CairoMakie, GeoInterfaceMakie
+
+country_df = GADM.get.(["JPN", "USA", "IND", "DEU", "FRA"]) |> DataFrame
+country_df.geometry = GI.GeometryCollection.(GO.tuples.(country_df.geom))
+
+state_doublets = [
+ ("USA", "New York"),
+ ("USA", "California"),
+ ("IND", "Karnataka"),
+ ("DEU", "Berlin"),
+ ("FRA", "Grand Est"),
+ ("JPN", "Tokyo"),
+]
+
+state_full_df = (x -> GADM.get(x...)).(state_doublets) |> DataFrame
+state_full_df.geom = GO.tuples.(only.(state_full_df.geom))
+state_compact_df = state_full_df[:, [:geom, :NAME_1]]
innerjoin((state_compact_df, country_df), by_pred(:geom, GO.within, :geometry))
+innerjoin((state_compact_df, view(country_df, 1:1, :)), by_pred(:geom, GO.within, :geometry))
Enabling custom predicates
my_predicate_function = <(5) ∘ abs ∘ GO.distance
FlexiJoins.supports_mode
on your predicate:FlexiJoins.supports_mode(
+ ::FlexiJoins.Mode.NestedLoopFast,
+ ::FlexiJoins.ByPred{typeof(my_predicate_function)},
+ datas
+) = true
by_pred(:geometry, my_predicate_function, :geometry)
. What is GeometryOps.jl?