Pure Julia code
Fast, understandable, extensible functions
diff --git a/previews/PR239/404.html b/previews/PR239/404.html new file mode 100644 index 000000000..61cab2d93 --- /dev/null +++ b/previews/PR239/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.GEOS
GeometryOps.GeodesicSegments
GeometryOps.GeometryCorrection
GeometryOps.LineOrientation
GeometryOps.LinearSegments
GeometryOps.MeanValue
GeometryOps.MonotoneChainMethod
GeometryOps.PointOrientation
GeometryOps.RadialDistance
GeometryOps.SimplifyAlg
GeometryOps.UnionIntersectingPolygons
GeometryOps.VisvalingamWhyatt
GeometryOps._det
GeometryOps._equals_curves
GeometryOps.angles
GeometryOps.angles
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.convex_hull
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.enforce
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.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.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.weighted_mean
GeometryOps.within
GeometryOps.within
GeometryOpsCore.apply
GeometryOpsCore.applyreduce
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 reference system, as a GeoFormatTypes.jl object or a string.
target_crs
: the target coordinate reference 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 retrievable 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 actually 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 collinear 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.
`,29))]),i("details",f,[i("summary",null,[s[33]||(s[33]=i("a",{id:"GeometryOps.touches",href:"#GeometryOps.touches"},[i("span",{class:"jlbinding"},"GeometryOps.touches")],-1)),s[34]||(s[34]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[35]||(s[35]=a(`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 geometry's 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 definition.
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 definition, 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 definition, 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.
`,47))]),i("details",v,[i("summary",null,[s[42]||(s[42]=i("a",{id:"GeometryOps.centroid",href:"#GeometryOps.centroid"},[i("span",{class:"jlbinding"},"GeometryOps.centroid")],-1)),s[43]||(s[43]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[44]||(s[44]=a('centroid(geom, [T=Float64])::Tuple{T, T}
Returns the centroid of a given line segment, linear ring, polygon, or mutlipolygon.
',3))]),i("details",A,[i("summary",null,[s[45]||(s[45]=i("a",{id:"GeometryOps.distance",href:"#GeometryOps.distance"},[i("span",{class:"jlbinding"},"GeometryOps.distance")],-1)),s[46]||(s[46]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[47]||(s[47]=a('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.
',5))]),i("details",j,[i("summary",null,[s[48]||(s[48]=i("a",{id:"GeometryOps.signed_distance",href:"#GeometryOps.signed_distance"},[i("span",{class:"jlbinding"},"GeometryOps.signed_distance")],-1)),s[49]||(s[49]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[50]||(s[50]=a('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.
',4))]),i("details",B,[i("summary",null,[s[51]||(s[51]=i("a",{id:"GeometryOps.area",href:"#GeometryOps.area"},[i("span",{class:"jlbinding"},"GeometryOps.area")],-1)),s[52]||(s[52]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[53]||(s[53]=a(`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.
`,5))]),i("details",O,[i("summary",null,[s[54]||(s[54]=i("a",{id:"GeometryOps.signed_area",href:"#GeometryOps.signed_area"},[i("span",{class:"jlbinding"},"GeometryOps.signed_area")],-1)),s[55]||(s[55]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[56]||(s[56]=a(`signed_area(geom, [T = Float64])::T
Returns the signed area of a single geometry, based on winding order. This is computed slightly 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.
`,5))]),i("details",D,[i("summary",null,[s[57]||(s[57]=i("a",{id:"GeometryOps.angles",href:"#GeometryOps.angles"},[i("span",{class:"jlbinding"},"GeometryOps.angles")],-1)),s[58]||(s[58]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[59]||(s[59]=a(`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 polygon 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.
`,5))]),i("details",Q,[i("summary",null,[s[60]||(s[60]=i("a",{id:"GeometryOps.embed_extent",href:"#GeometryOps.embed_extent"},[i("span",{class:"jlbinding"},"GeometryOps.embed_extent")],-1)),s[61]||(s[61]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[62]||(s[62]=a('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.
',7))]),i("details",M,[i("summary",null,[s[75]||(s[75]=i("a",{id:"GeometryOps.ClosedRing",href:"#GeometryOps.ClosedRing"},[i("span",{class:"jlbinding"},"GeometryOps.ClosedRing")],-1)),s[76]||(s[76]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[77]||(s[77]=a('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 unnecessary computation in algorithm.
GEOS(; params...)
A struct which instructs the method it's passed to as an algorithm to use the appropriate GEOS function via LibGEOS.jl
for the operation.
Dispatch is generally carried out using the names of the keyword arguments. For example, segmentize
will only accept a GEOS
struct with only a max_distance
keyword, and no other.
It's generally a lot slower than the native Julia implementations, since it must convert to the LibGEOS implementation and back - so be warned!
',5))]),i("details",S,[i("summary",null,[s[87]||(s[87]=i("a",{id:"GeometryOps.GeodesicSegments",href:"#GeometryOps.GeodesicSegments"},[i("span",{class:"jlbinding"},"GeometryOps.GeodesicSegments")],-1)),s[88]||(s[88]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[89]||(s[89]=a('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.
',8))]),i("details",V,[i("summary",null,[s[90]||(s[90]=i("a",{id:"GeometryOps.GeometryCorrection",href:"#GeometryOps.GeometryCorrection"},[i("span",{class:"jlbinding"},"GeometryOps.GeometryCorrection")],-1)),s[91]||(s[91]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[92]||(s[92]=a('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
(collinear 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
',4))]),i("details",N,[i("summary",null,[s[102]||(s[102]=i("a",{id:"GeometryOps.MonotoneChainMethod",href:"#GeometryOps.MonotoneChainMethod"},[i("span",{class:"jlbinding"},"GeometryOps.MonotoneChainMethod")],-1)),s[103]||(s[103]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[104]||(s[104]=a('MonotoneChainMethod()
This is an algorithm for the convex_hull
function.
Uses DelaunayTriangulation.jl
to compute the convex hull. This is a pure Julia algorithm which provides an optimal Delaunay triangulation.
See also convex_hull
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 unnecessary 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.
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 unnecessary 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)
',3))]),i("details",Y,[i("summary",null,[s[126]||(s[126]=i("a",{id:"GeometryOps.angles-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.angles-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.angles")],-1)),s[127]||(s[127]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[128]||(s[128]=a(`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 polygon 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.
`,5))]),i("details",ss,[i("summary",null,[s[129]||(s[129]=i("a",{id:"GeometryOps.area-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.area-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.area")],-1)),s[130]||(s[130]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[131]||(s[131]=a(`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.
`,5))]),i("details",is,[i("summary",null,[s[132]||(s[132]=i("a",{id:"GeometryOps.barycentric_coordinates!-Tuple{Vector{<:Real}, GeometryOps.AbstractBarycentricCoordinateMethod, Any, Any}",href:"#GeometryOps.barycentric_coordinates!-Tuple{Vector{<:Real}, GeometryOps.AbstractBarycentricCoordinateMethod, Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.barycentric_coordinates!")],-1)),s[133]||(s[133]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[134]||(s[134]=a('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.
',3))]),i("details",ns,[i("summary",null,[s[144]||(s[144]=i("a",{id:"GeometryOps.centroid_and_area-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T",href:"#GeometryOps.centroid_and_area-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T"},[i("span",{class:"jlbinding"},"GeometryOps.centroid_and_area")],-1)),s[145]||(s[145]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[146]||(s[146]=a('centroid_and_area(geom, [T=Float64])::(::Tuple{T, T}, ::Real)
Returns the centroid and area of a given geometry.
',3))]),i("details",ls,[i("summary",null,[s[147]||(s[147]=i("a",{id:"GeometryOps.centroid_and_length-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T",href:"#GeometryOps.centroid_and_length-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T"},[i("span",{class:"jlbinding"},"GeometryOps.centroid_and_length")],-1)),s[148]||(s[148]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[149]||(s[149]=a('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.
',3))]),i("details",ps,[i("summary",null,[s[150]||(s[150]=i("a",{id:"GeometryOps.contains-Tuple{Any, Any}",href:"#GeometryOps.contains-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.contains")],-1)),s[151]||(s[151]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[152]||(s[152]=a(`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
convex_hull([method], geometries)
Compute the convex hull of the points in geometries
. Returns a GI.Polygon
representing the convex hull.
Note that the polygon returned is wound counterclockwise as in the Simple Features standard by default. If you choose GEOS, the winding order will be inverted.
Warning
This interface only computes the 2-dimensional convex hull!
For higher dimensional hulls, use the relevant package (Qhull.jl, Quickhull.jl, or similar).
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.
',5))]),i("details",rs,[i("summary",null,[s[159]||(s[159]=i("a",{id:"GeometryOps.coveredby-Tuple{Any, Any}",href:"#GeometryOps.coveredby-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.coveredby")],-1)),s[160]||(s[160]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[161]||(s[161]=a(`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.
',5))]),i("details",us,[i("summary",null,[s[180]||(s[180]=i("a",{id:"GeometryOps.embed_extent-Tuple{Any}",href:"#GeometryOps.embed_extent-Tuple{Any}"},[i("span",{class:"jlbinding"},"GeometryOps.embed_extent")],-1)),s[181]||(s[181]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[182]||(s[182]=a('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
.
enforce(alg::GO.GEOS, kw::Symbol, f)
Enforce the presence of a keyword argument in a GEOS
algorithm, and return alg.params[kw]
.
Throws an error if the key is not present, and mentions f
in the error message (since there isn't a good way to get the name of the function that called this method).
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.
',3))]),i("details",Cs,[i("summary",null,[s[189]||(s[189]=i("a",{id:"GeometryOps.equals-Tuple{Any, Any}",href:"#GeometryOps.equals-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[190]||(s[190]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[191]||(s[191]=a(`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.
`,3))]),i("details",fs,[i("summary",null,[s[195]||(s[195]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.LinearRingTrait, Any, Union{GeoInterface.LineStringTrait, GeoInterface.LineTrait}, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.LinearRingTrait, Any, Union{GeoInterface.LineStringTrait, GeoInterface.LineTrait}, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[196]||(s[196]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[197]||(s[197]=a(`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 definition, but rings are, so the line must have a repeated last point to be equal
`,3))]),i("details",Ts,[i("summary",null,[s[198]||(s[198]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.MultiPointTrait, Any, GeoInterface.MultiPointTrait, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.MultiPointTrait, Any, GeoInterface.MultiPointTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[199]||(s[199]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[200]||(s[200]=a('equals(::GI.MultiPointTrait, mp1, ::GI.MultiPointTrait, mp2)::Bool
Two multipoints are equal if they share the same set of points.
',3))]),i("details",Gs,[i("summary",null,[s[201]||(s[201]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.MultiPointTrait, Any, GeoInterface.PointTrait, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.MultiPointTrait, Any, GeoInterface.PointTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[202]||(s[202]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[203]||(s[203]=a('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.
',3))]),i("details",vs,[i("summary",null,[s[204]||(s[204]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.MultiPolygonTrait, Any, GeoInterface.MultiPolygonTrait, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.MultiPolygonTrait, Any, GeoInterface.MultiPolygonTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[205]||(s[205]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[206]||(s[206]=a('equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool
Two multipolygons are equal if they share the same set of polygons.
',3))]),i("details",As,[i("summary",null,[s[207]||(s[207]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.MultiPolygonTrait, Any, GeoInterface.PolygonTrait, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.MultiPolygonTrait, Any, GeoInterface.PolygonTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[208]||(s[208]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[209]||(s[209]=a('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.
',3))]),i("details",js,[i("summary",null,[s[210]||(s[210]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.PointTrait, Any, GeoInterface.MultiPointTrait, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.PointTrait, Any, GeoInterface.MultiPointTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[211]||(s[211]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[212]||(s[212]=a('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.
',3))]),i("details",Bs,[i("summary",null,[s[213]||(s[213]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.PointTrait, Any, GeoInterface.PointTrait, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.PointTrait, Any, GeoInterface.PointTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[214]||(s[214]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[215]||(s[215]=a('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.
',3))]),i("details",Os,[i("summary",null,[s[216]||(s[216]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.PolygonTrait, Any, GeoInterface.MultiPolygonTrait, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.PolygonTrait, Any, GeoInterface.MultiPolygonTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[217]||(s[217]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[218]||(s[218]=a('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.
',3))]),i("details",Ds,[i("summary",null,[s[219]||(s[219]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.PolygonTrait, Any, GeoInterface.PolygonTrait, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.PolygonTrait, Any, GeoInterface.PolygonTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[220]||(s[220]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[221]||(s[221]=a('equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool
Two polygons are equal if they share the same exterior edge and holes.
',3))]),i("details",Qs,[i("summary",null,[s[222]||(s[222]=i("a",{id:"GeometryOps.equals-Tuple{Union{GeoInterface.LineStringTrait, GeoInterface.LineTrait}, Any, GeoInterface.LinearRingTrait, Any}",href:"#GeometryOps.equals-Tuple{Union{GeoInterface.LineStringTrait, GeoInterface.LineTrait}, Any, GeoInterface.LinearRingTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[223]||(s[223]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[224]||(s[224]=a(`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 definition, but rings are, so the line must have a repeated last point to be equal
`,3))]),i("details",xs,[i("summary",null,[s[225]||(s[225]=i("a",{id:"GeometryOps.equals-Tuple{Union{GeoInterface.LineStringTrait, GeoInterface.LineTrait}, Any, Union{GeoInterface.LineStringTrait, GeoInterface.LineTrait}, Any}",href:"#GeometryOps.equals-Tuple{Union{GeoInterface.LineStringTrait, GeoInterface.LineTrait}, Any, Union{GeoInterface.LineStringTrait, GeoInterface.LineTrait}, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[226]||(s[226]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[227]||(s[227]=a(`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 definition.
`,3))]),i("details",ws,[i("summary",null,[s[228]||(s[228]=i("a",{id:"GeometryOps.equals-Union{Tuple{T}, Tuple{T, Any, T, Any}} where T",href:"#GeometryOps.equals-Union{Tuple{T}, Tuple{T, Any, T, Any}} where T"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[229]||(s[229]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[230]||(s[230]=a('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.
',3))]),i("details",Ls,[i("summary",null,[s[231]||(s[231]=i("a",{id:"GeometryOps.flip-Tuple{Any}",href:"#GeometryOps.flip-Tuple{Any}"},[i("span",{class:"jlbinding"},"GeometryOps.flip")],-1)),s[232]||(s[232]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[233]||(s[233]=a('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, [T::Type])
Return a list of intersection tuple points between two geometries. If no intersection points exist, returns an empty list.
Example
+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_points(line1, line2)
+
+**output**
+
+1-element Vector{Tuple{Float64, Float64}}: (125.58375366067548, -14.83572303404496)
+
+
+[source](https://github.com/JuliaGeo/GeometryOps.jl/blob/9f0e22db49f7b49d5352750e9f69705f13c06d64/src/methods/clipping/intersection.jl#L177-L195)
+
+</details>
+
+<details class='jldocstring custom-block' open>
+<summary><a id='GeometryOps.intersects-Tuple{Any, Any}' href='#GeometryOps.intersects-Tuple{Any, Any}'><span class="jlbinding">GeometryOps.intersects</span></a> <Badge type="info" class="jlObjectType jlMethod" text="Method" /></summary>
+
+
+
+\`\`\`julia
+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
if the line goes clockwise, or false
if the line goes counter-clockwise. "Going clockwise" means, mathematically,
Example
julia> import GeoInterface as GI, GeometryOps as GO
+julia> ring = GI.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]);
+julia> 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.
',3))]),i("details",Us,[i("summary",null,[s[255]||(s[255]=i("a",{id:"GeometryOps.overlaps-Tuple{GeoInterface.LineTrait, Any, GeoInterface.LineTrait, Any}",href:"#GeometryOps.overlaps-Tuple{GeoInterface.LineTrait, Any, GeoInterface.LineTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[256]||(s[256]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[257]||(s[257]=a('overlaps(::GI.LineTrait, line1, ::GI.LineTrait, line)::Bool
If the lines overlap, meaning that they are collinear but each have one endpoint outside of the other line, return true. Else false.
',3))]),i("details",Hs,[i("summary",null,[s[258]||(s[258]=i("a",{id:"GeometryOps.overlaps-Tuple{GeoInterface.MultiPointTrait, Any, GeoInterface.MultiPointTrait, Any}",href:"#GeometryOps.overlaps-Tuple{GeoInterface.MultiPointTrait, Any, GeoInterface.MultiPointTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[259]||(s[259]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[260]||(s[260]=a(`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.
`,3))]),i("details",Ns,[i("summary",null,[s[261]||(s[261]=i("a",{id:"GeometryOps.overlaps-Tuple{GeoInterface.MultiPolygonTrait, Any, GeoInterface.MultiPolygonTrait, Any}",href:"#GeometryOps.overlaps-Tuple{GeoInterface.MultiPolygonTrait, Any, GeoInterface.MultiPolygonTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[262]||(s[262]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[263]||(s[263]=a(`overlaps(
+ ::GI.MultiPolygonTrait, polys1,
+ ::GI.MultiPolygonTrait, polys2,
+)::Bool
Return true if at least one pair of polygons from multipolygons overlap. Else false.
`,3))]),i("details",Ws,[i("summary",null,[s[264]||(s[264]=i("a",{id:"GeometryOps.overlaps-Tuple{GeoInterface.MultiPolygonTrait, Any, GeoInterface.PolygonTrait, Any}",href:"#GeometryOps.overlaps-Tuple{GeoInterface.MultiPolygonTrait, Any, GeoInterface.PolygonTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[265]||(s[265]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[266]||(s[266]=a(`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.
`,3))]),i("details",zs,[i("summary",null,[s[267]||(s[267]=i("a",{id:"GeometryOps.overlaps-Tuple{GeoInterface.PolygonTrait, Any, GeoInterface.MultiPolygonTrait, Any}",href:"#GeometryOps.overlaps-Tuple{GeoInterface.PolygonTrait, Any, GeoInterface.MultiPolygonTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[268]||(s[268]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[269]||(s[269]=a(`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.
`,3))]),i("details",Zs,[i("summary",null,[s[270]||(s[270]=i("a",{id:"GeometryOps.overlaps-Tuple{GeoInterface.PolygonTrait, Any, GeoInterface.PolygonTrait, Any}",href:"#GeometryOps.overlaps-Tuple{GeoInterface.PolygonTrait, Any, GeoInterface.PolygonTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[271]||(s[271]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[272]||(s[272]=a(`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.
`,3))]),i("details",_s,[i("summary",null,[s[273]||(s[273]=i("a",{id:"GeometryOps.overlaps-Tuple{Union{GeoInterface.LineStringTrait, GeoInterface.Wrappers.LinearRing}, Any, Union{GeoInterface.LineStringTrait, GeoInterface.Wrappers.LinearRing}, Any}",href:"#GeometryOps.overlaps-Tuple{Union{GeoInterface.LineStringTrait, GeoInterface.Wrappers.LinearRing}, Any, Union{GeoInterface.LineStringTrait, GeoInterface.Wrappers.LinearRing}, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[274]||(s[274]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[275]||(s[275]=a(`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.
`,3))]),i("details",Ks,[i("summary",null,[s[276]||(s[276]=i("a",{id:"GeometryOps.polygon_to_line-Tuple{Any}",href:"#GeometryOps.polygon_to_line-Tuple{Any}"},[i("span",{class:"jlbinding"},"GeometryOps.polygon_to_line")],-1)),s[277]||(s[277]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[278]||(s[278]=a(`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);
segmentize([method = Planar()], 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::Manifold = Planar()
: The method to use for segmentizing the geometry. At the moment, only Planar
(assumes a flat plane) and Geodesic
(assumes geometry on the ellipsoidal Earth and uses Vincenty's formulae) are available.
geom
: The geometry to segmentize. Must be a LineString
, LinearRing
, Polygon
, MultiPolygon
, or GeometryCollection
, or some vector or table of those.
max_distance::Real
: The maximum distance between vertices in the geometry. Beware: for Planar
, this is in the units of the geometry, but for Geodesic
and Spherical
it's in units of the radius of the sphere.
Returns a geometry of similar type to the input geometry, but resampled.
',6))]),i("details",Ys,[i("summary",null,[s[285]||(s[285]=i("a",{id:"GeometryOps.signed_area-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.signed_area-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.signed_area")],-1)),s[286]||(s[286]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[287]||(s[287]=a(`signed_area(geom, [T = Float64])::T
Returns the signed area of a single geometry, based on winding order. This is computed slightly 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.
`,5))]),i("details",si,[i("summary",null,[s[288]||(s[288]=i("a",{id:"GeometryOps.signed_distance-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.signed_distance-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.signed_distance")],-1)),s[289]||(s[289]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[290]||(s[290]=a('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.
',4))]),i("details",ii,[i("summary",null,[s[291]||(s[291]=i("a",{id:"GeometryOps.simplify-Tuple{GeometryOps.SimplifyAlg, Any}",href:"#GeometryOps.simplify-Tuple{GeometryOps.SimplifyAlg, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.simplify")],-1)),s[292]||(s[292]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[293]||(s[293]=a(`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 decreasing 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/9f0e22db49f7b49d5352750e9f69705f13c06d64/src/methods/barycentric.jl#L289-L305)
+
+</details>
+
+<details class='jldocstring custom-block' open>
+<summary><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'><span class="jlbinding">GeometryOps.to_edges</span></a> <Badge type="info" class="jlObjectType jlMethod" text="Method" /></summary>
+
+
+
+\`\`\`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 geometry's 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 actually 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]]]
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. ↩︎
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.GEOS
GeometryOps.GeodesicSegments
GeometryOps.GeometryCorrection
GeometryOps.LineOrientation
GeometryOps.LinearSegments
GeometryOps.MeanValue
GeometryOps.MonotoneChainMethod
GeometryOps.PointOrientation
GeometryOps.RadialDistance
GeometryOps.SimplifyAlg
GeometryOps.UnionIntersectingPolygons
GeometryOps.VisvalingamWhyatt
GeometryOps._det
GeometryOps._equals_curves
GeometryOps.angles
GeometryOps.angles
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.convex_hull
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.enforce
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.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.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.weighted_mean
GeometryOps.within
GeometryOps.within
GeometryOpsCore.apply
GeometryOpsCore.applyreduce
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 reference system, as a GeoFormatTypes.jl object or a string.
target_crs
: the target coordinate reference 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 retrievable 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 actually 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 collinear 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.
`,29))]),i("details",f,[i("summary",null,[s[33]||(s[33]=i("a",{id:"GeometryOps.touches",href:"#GeometryOps.touches"},[i("span",{class:"jlbinding"},"GeometryOps.touches")],-1)),s[34]||(s[34]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[35]||(s[35]=a(`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 geometry's 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 definition.
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 definition, 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 definition, 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.
`,47))]),i("details",v,[i("summary",null,[s[42]||(s[42]=i("a",{id:"GeometryOps.centroid",href:"#GeometryOps.centroid"},[i("span",{class:"jlbinding"},"GeometryOps.centroid")],-1)),s[43]||(s[43]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[44]||(s[44]=a('centroid(geom, [T=Float64])::Tuple{T, T}
Returns the centroid of a given line segment, linear ring, polygon, or mutlipolygon.
',3))]),i("details",A,[i("summary",null,[s[45]||(s[45]=i("a",{id:"GeometryOps.distance",href:"#GeometryOps.distance"},[i("span",{class:"jlbinding"},"GeometryOps.distance")],-1)),s[46]||(s[46]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[47]||(s[47]=a('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.
',5))]),i("details",j,[i("summary",null,[s[48]||(s[48]=i("a",{id:"GeometryOps.signed_distance",href:"#GeometryOps.signed_distance"},[i("span",{class:"jlbinding"},"GeometryOps.signed_distance")],-1)),s[49]||(s[49]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[50]||(s[50]=a('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.
',4))]),i("details",B,[i("summary",null,[s[51]||(s[51]=i("a",{id:"GeometryOps.area",href:"#GeometryOps.area"},[i("span",{class:"jlbinding"},"GeometryOps.area")],-1)),s[52]||(s[52]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[53]||(s[53]=a(`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.
`,5))]),i("details",O,[i("summary",null,[s[54]||(s[54]=i("a",{id:"GeometryOps.signed_area",href:"#GeometryOps.signed_area"},[i("span",{class:"jlbinding"},"GeometryOps.signed_area")],-1)),s[55]||(s[55]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[56]||(s[56]=a(`signed_area(geom, [T = Float64])::T
Returns the signed area of a single geometry, based on winding order. This is computed slightly 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.
`,5))]),i("details",D,[i("summary",null,[s[57]||(s[57]=i("a",{id:"GeometryOps.angles",href:"#GeometryOps.angles"},[i("span",{class:"jlbinding"},"GeometryOps.angles")],-1)),s[58]||(s[58]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[59]||(s[59]=a(`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 polygon 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.
`,5))]),i("details",Q,[i("summary",null,[s[60]||(s[60]=i("a",{id:"GeometryOps.embed_extent",href:"#GeometryOps.embed_extent"},[i("span",{class:"jlbinding"},"GeometryOps.embed_extent")],-1)),s[61]||(s[61]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[62]||(s[62]=a('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.
',7))]),i("details",M,[i("summary",null,[s[75]||(s[75]=i("a",{id:"GeometryOps.ClosedRing",href:"#GeometryOps.ClosedRing"},[i("span",{class:"jlbinding"},"GeometryOps.ClosedRing")],-1)),s[76]||(s[76]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[77]||(s[77]=a('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 unnecessary computation in algorithm.
GEOS(; params...)
A struct which instructs the method it's passed to as an algorithm to use the appropriate GEOS function via LibGEOS.jl
for the operation.
Dispatch is generally carried out using the names of the keyword arguments. For example, segmentize
will only accept a GEOS
struct with only a max_distance
keyword, and no other.
It's generally a lot slower than the native Julia implementations, since it must convert to the LibGEOS implementation and back - so be warned!
',5))]),i("details",S,[i("summary",null,[s[87]||(s[87]=i("a",{id:"GeometryOps.GeodesicSegments",href:"#GeometryOps.GeodesicSegments"},[i("span",{class:"jlbinding"},"GeometryOps.GeodesicSegments")],-1)),s[88]||(s[88]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[89]||(s[89]=a('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.
',8))]),i("details",V,[i("summary",null,[s[90]||(s[90]=i("a",{id:"GeometryOps.GeometryCorrection",href:"#GeometryOps.GeometryCorrection"},[i("span",{class:"jlbinding"},"GeometryOps.GeometryCorrection")],-1)),s[91]||(s[91]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[92]||(s[92]=a('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
(collinear 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
',4))]),i("details",N,[i("summary",null,[s[102]||(s[102]=i("a",{id:"GeometryOps.MonotoneChainMethod",href:"#GeometryOps.MonotoneChainMethod"},[i("span",{class:"jlbinding"},"GeometryOps.MonotoneChainMethod")],-1)),s[103]||(s[103]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[104]||(s[104]=a('MonotoneChainMethod()
This is an algorithm for the convex_hull
function.
Uses DelaunayTriangulation.jl
to compute the convex hull. This is a pure Julia algorithm which provides an optimal Delaunay triangulation.
See also convex_hull
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 unnecessary 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.
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 unnecessary 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)
',3))]),i("details",Y,[i("summary",null,[s[126]||(s[126]=i("a",{id:"GeometryOps.angles-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.angles-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.angles")],-1)),s[127]||(s[127]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[128]||(s[128]=a(`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 polygon 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.
`,5))]),i("details",ss,[i("summary",null,[s[129]||(s[129]=i("a",{id:"GeometryOps.area-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.area-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.area")],-1)),s[130]||(s[130]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[131]||(s[131]=a(`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.
`,5))]),i("details",is,[i("summary",null,[s[132]||(s[132]=i("a",{id:"GeometryOps.barycentric_coordinates!-Tuple{Vector{<:Real}, GeometryOps.AbstractBarycentricCoordinateMethod, Any, Any}",href:"#GeometryOps.barycentric_coordinates!-Tuple{Vector{<:Real}, GeometryOps.AbstractBarycentricCoordinateMethod, Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.barycentric_coordinates!")],-1)),s[133]||(s[133]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[134]||(s[134]=a('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.
',3))]),i("details",ns,[i("summary",null,[s[144]||(s[144]=i("a",{id:"GeometryOps.centroid_and_area-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T",href:"#GeometryOps.centroid_and_area-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T"},[i("span",{class:"jlbinding"},"GeometryOps.centroid_and_area")],-1)),s[145]||(s[145]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[146]||(s[146]=a('centroid_and_area(geom, [T=Float64])::(::Tuple{T, T}, ::Real)
Returns the centroid and area of a given geometry.
',3))]),i("details",ls,[i("summary",null,[s[147]||(s[147]=i("a",{id:"GeometryOps.centroid_and_length-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T",href:"#GeometryOps.centroid_and_length-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T"},[i("span",{class:"jlbinding"},"GeometryOps.centroid_and_length")],-1)),s[148]||(s[148]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[149]||(s[149]=a('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.
',3))]),i("details",ps,[i("summary",null,[s[150]||(s[150]=i("a",{id:"GeometryOps.contains-Tuple{Any, Any}",href:"#GeometryOps.contains-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.contains")],-1)),s[151]||(s[151]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[152]||(s[152]=a(`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
convex_hull([method], geometries)
Compute the convex hull of the points in geometries
. Returns a GI.Polygon
representing the convex hull.
Note that the polygon returned is wound counterclockwise as in the Simple Features standard by default. If you choose GEOS, the winding order will be inverted.
Warning
This interface only computes the 2-dimensional convex hull!
For higher dimensional hulls, use the relevant package (Qhull.jl, Quickhull.jl, or similar).
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.
',5))]),i("details",rs,[i("summary",null,[s[159]||(s[159]=i("a",{id:"GeometryOps.coveredby-Tuple{Any, Any}",href:"#GeometryOps.coveredby-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.coveredby")],-1)),s[160]||(s[160]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[161]||(s[161]=a(`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.
',5))]),i("details",us,[i("summary",null,[s[180]||(s[180]=i("a",{id:"GeometryOps.embed_extent-Tuple{Any}",href:"#GeometryOps.embed_extent-Tuple{Any}"},[i("span",{class:"jlbinding"},"GeometryOps.embed_extent")],-1)),s[181]||(s[181]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[182]||(s[182]=a('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
.
enforce(alg::GO.GEOS, kw::Symbol, f)
Enforce the presence of a keyword argument in a GEOS
algorithm, and return alg.params[kw]
.
Throws an error if the key is not present, and mentions f
in the error message (since there isn't a good way to get the name of the function that called this method).
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.
',3))]),i("details",Cs,[i("summary",null,[s[189]||(s[189]=i("a",{id:"GeometryOps.equals-Tuple{Any, Any}",href:"#GeometryOps.equals-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[190]||(s[190]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[191]||(s[191]=a(`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.
`,3))]),i("details",fs,[i("summary",null,[s[195]||(s[195]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.LinearRingTrait, Any, Union{GeoInterface.LineStringTrait, GeoInterface.LineTrait}, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.LinearRingTrait, Any, Union{GeoInterface.LineStringTrait, GeoInterface.LineTrait}, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[196]||(s[196]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[197]||(s[197]=a(`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 definition, but rings are, so the line must have a repeated last point to be equal
`,3))]),i("details",Ts,[i("summary",null,[s[198]||(s[198]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.MultiPointTrait, Any, GeoInterface.MultiPointTrait, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.MultiPointTrait, Any, GeoInterface.MultiPointTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[199]||(s[199]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[200]||(s[200]=a('equals(::GI.MultiPointTrait, mp1, ::GI.MultiPointTrait, mp2)::Bool
Two multipoints are equal if they share the same set of points.
',3))]),i("details",Gs,[i("summary",null,[s[201]||(s[201]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.MultiPointTrait, Any, GeoInterface.PointTrait, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.MultiPointTrait, Any, GeoInterface.PointTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[202]||(s[202]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[203]||(s[203]=a('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.
',3))]),i("details",vs,[i("summary",null,[s[204]||(s[204]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.MultiPolygonTrait, Any, GeoInterface.MultiPolygonTrait, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.MultiPolygonTrait, Any, GeoInterface.MultiPolygonTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[205]||(s[205]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[206]||(s[206]=a('equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool
Two multipolygons are equal if they share the same set of polygons.
',3))]),i("details",As,[i("summary",null,[s[207]||(s[207]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.MultiPolygonTrait, Any, GeoInterface.PolygonTrait, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.MultiPolygonTrait, Any, GeoInterface.PolygonTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[208]||(s[208]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[209]||(s[209]=a('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.
',3))]),i("details",js,[i("summary",null,[s[210]||(s[210]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.PointTrait, Any, GeoInterface.MultiPointTrait, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.PointTrait, Any, GeoInterface.MultiPointTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[211]||(s[211]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[212]||(s[212]=a('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.
',3))]),i("details",Bs,[i("summary",null,[s[213]||(s[213]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.PointTrait, Any, GeoInterface.PointTrait, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.PointTrait, Any, GeoInterface.PointTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[214]||(s[214]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[215]||(s[215]=a('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.
',3))]),i("details",Os,[i("summary",null,[s[216]||(s[216]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.PolygonTrait, Any, GeoInterface.MultiPolygonTrait, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.PolygonTrait, Any, GeoInterface.MultiPolygonTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[217]||(s[217]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[218]||(s[218]=a('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.
',3))]),i("details",Ds,[i("summary",null,[s[219]||(s[219]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.PolygonTrait, Any, GeoInterface.PolygonTrait, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.PolygonTrait, Any, GeoInterface.PolygonTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[220]||(s[220]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[221]||(s[221]=a('equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool
Two polygons are equal if they share the same exterior edge and holes.
',3))]),i("details",Qs,[i("summary",null,[s[222]||(s[222]=i("a",{id:"GeometryOps.equals-Tuple{Union{GeoInterface.LineStringTrait, GeoInterface.LineTrait}, Any, GeoInterface.LinearRingTrait, Any}",href:"#GeometryOps.equals-Tuple{Union{GeoInterface.LineStringTrait, GeoInterface.LineTrait}, Any, GeoInterface.LinearRingTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[223]||(s[223]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[224]||(s[224]=a(`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 definition, but rings are, so the line must have a repeated last point to be equal
`,3))]),i("details",xs,[i("summary",null,[s[225]||(s[225]=i("a",{id:"GeometryOps.equals-Tuple{Union{GeoInterface.LineStringTrait, GeoInterface.LineTrait}, Any, Union{GeoInterface.LineStringTrait, GeoInterface.LineTrait}, Any}",href:"#GeometryOps.equals-Tuple{Union{GeoInterface.LineStringTrait, GeoInterface.LineTrait}, Any, Union{GeoInterface.LineStringTrait, GeoInterface.LineTrait}, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[226]||(s[226]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[227]||(s[227]=a(`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 definition.
`,3))]),i("details",ws,[i("summary",null,[s[228]||(s[228]=i("a",{id:"GeometryOps.equals-Union{Tuple{T}, Tuple{T, Any, T, Any}} where T",href:"#GeometryOps.equals-Union{Tuple{T}, Tuple{T, Any, T, Any}} where T"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[229]||(s[229]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[230]||(s[230]=a('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.
',3))]),i("details",Ls,[i("summary",null,[s[231]||(s[231]=i("a",{id:"GeometryOps.flip-Tuple{Any}",href:"#GeometryOps.flip-Tuple{Any}"},[i("span",{class:"jlbinding"},"GeometryOps.flip")],-1)),s[232]||(s[232]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[233]||(s[233]=a('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, [T::Type])
Return a list of intersection tuple points between two geometries. If no intersection points exist, returns an empty list.
Example
+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_points(line1, line2)
+
+**output**
+
+1-element Vector{Tuple{Float64, Float64}}: (125.58375366067548, -14.83572303404496)
+
+
+[source](https://github.com/JuliaGeo/GeometryOps.jl/blob/9f0e22db49f7b49d5352750e9f69705f13c06d64/src/methods/clipping/intersection.jl#L177-L195)
+
+</details>
+
+<details class='jldocstring custom-block' open>
+<summary><a id='GeometryOps.intersects-Tuple{Any, Any}' href='#GeometryOps.intersects-Tuple{Any, Any}'><span class="jlbinding">GeometryOps.intersects</span></a> <Badge type="info" class="jlObjectType jlMethod" text="Method" /></summary>
+
+
+
+\`\`\`julia
+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
if the line goes clockwise, or false
if the line goes counter-clockwise. "Going clockwise" means, mathematically,
Example
julia> import GeoInterface as GI, GeometryOps as GO
+julia> ring = GI.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]);
+julia> 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.
',3))]),i("details",Us,[i("summary",null,[s[255]||(s[255]=i("a",{id:"GeometryOps.overlaps-Tuple{GeoInterface.LineTrait, Any, GeoInterface.LineTrait, Any}",href:"#GeometryOps.overlaps-Tuple{GeoInterface.LineTrait, Any, GeoInterface.LineTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[256]||(s[256]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[257]||(s[257]=a('overlaps(::GI.LineTrait, line1, ::GI.LineTrait, line)::Bool
If the lines overlap, meaning that they are collinear but each have one endpoint outside of the other line, return true. Else false.
',3))]),i("details",Hs,[i("summary",null,[s[258]||(s[258]=i("a",{id:"GeometryOps.overlaps-Tuple{GeoInterface.MultiPointTrait, Any, GeoInterface.MultiPointTrait, Any}",href:"#GeometryOps.overlaps-Tuple{GeoInterface.MultiPointTrait, Any, GeoInterface.MultiPointTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[259]||(s[259]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[260]||(s[260]=a(`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.
`,3))]),i("details",Ns,[i("summary",null,[s[261]||(s[261]=i("a",{id:"GeometryOps.overlaps-Tuple{GeoInterface.MultiPolygonTrait, Any, GeoInterface.MultiPolygonTrait, Any}",href:"#GeometryOps.overlaps-Tuple{GeoInterface.MultiPolygonTrait, Any, GeoInterface.MultiPolygonTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[262]||(s[262]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[263]||(s[263]=a(`overlaps(
+ ::GI.MultiPolygonTrait, polys1,
+ ::GI.MultiPolygonTrait, polys2,
+)::Bool
Return true if at least one pair of polygons from multipolygons overlap. Else false.
`,3))]),i("details",Ws,[i("summary",null,[s[264]||(s[264]=i("a",{id:"GeometryOps.overlaps-Tuple{GeoInterface.MultiPolygonTrait, Any, GeoInterface.PolygonTrait, Any}",href:"#GeometryOps.overlaps-Tuple{GeoInterface.MultiPolygonTrait, Any, GeoInterface.PolygonTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[265]||(s[265]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[266]||(s[266]=a(`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.
`,3))]),i("details",zs,[i("summary",null,[s[267]||(s[267]=i("a",{id:"GeometryOps.overlaps-Tuple{GeoInterface.PolygonTrait, Any, GeoInterface.MultiPolygonTrait, Any}",href:"#GeometryOps.overlaps-Tuple{GeoInterface.PolygonTrait, Any, GeoInterface.MultiPolygonTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[268]||(s[268]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[269]||(s[269]=a(`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.
`,3))]),i("details",Zs,[i("summary",null,[s[270]||(s[270]=i("a",{id:"GeometryOps.overlaps-Tuple{GeoInterface.PolygonTrait, Any, GeoInterface.PolygonTrait, Any}",href:"#GeometryOps.overlaps-Tuple{GeoInterface.PolygonTrait, Any, GeoInterface.PolygonTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[271]||(s[271]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[272]||(s[272]=a(`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.
`,3))]),i("details",_s,[i("summary",null,[s[273]||(s[273]=i("a",{id:"GeometryOps.overlaps-Tuple{Union{GeoInterface.LineStringTrait, GeoInterface.Wrappers.LinearRing}, Any, Union{GeoInterface.LineStringTrait, GeoInterface.Wrappers.LinearRing}, Any}",href:"#GeometryOps.overlaps-Tuple{Union{GeoInterface.LineStringTrait, GeoInterface.Wrappers.LinearRing}, Any, Union{GeoInterface.LineStringTrait, GeoInterface.Wrappers.LinearRing}, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[274]||(s[274]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[275]||(s[275]=a(`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.
`,3))]),i("details",Ks,[i("summary",null,[s[276]||(s[276]=i("a",{id:"GeometryOps.polygon_to_line-Tuple{Any}",href:"#GeometryOps.polygon_to_line-Tuple{Any}"},[i("span",{class:"jlbinding"},"GeometryOps.polygon_to_line")],-1)),s[277]||(s[277]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[278]||(s[278]=a(`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);
segmentize([method = Planar()], 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::Manifold = Planar()
: The method to use for segmentizing the geometry. At the moment, only Planar
(assumes a flat plane) and Geodesic
(assumes geometry on the ellipsoidal Earth and uses Vincenty's formulae) are available.
geom
: The geometry to segmentize. Must be a LineString
, LinearRing
, Polygon
, MultiPolygon
, or GeometryCollection
, or some vector or table of those.
max_distance::Real
: The maximum distance between vertices in the geometry. Beware: for Planar
, this is in the units of the geometry, but for Geodesic
and Spherical
it's in units of the radius of the sphere.
Returns a geometry of similar type to the input geometry, but resampled.
',6))]),i("details",Ys,[i("summary",null,[s[285]||(s[285]=i("a",{id:"GeometryOps.signed_area-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.signed_area-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.signed_area")],-1)),s[286]||(s[286]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[287]||(s[287]=a(`signed_area(geom, [T = Float64])::T
Returns the signed area of a single geometry, based on winding order. This is computed slightly 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.
`,5))]),i("details",si,[i("summary",null,[s[288]||(s[288]=i("a",{id:"GeometryOps.signed_distance-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.signed_distance-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.signed_distance")],-1)),s[289]||(s[289]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[290]||(s[290]=a('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.
',4))]),i("details",ii,[i("summary",null,[s[291]||(s[291]=i("a",{id:"GeometryOps.simplify-Tuple{GeometryOps.SimplifyAlg, Any}",href:"#GeometryOps.simplify-Tuple{GeometryOps.SimplifyAlg, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.simplify")],-1)),s[292]||(s[292]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[293]||(s[293]=a(`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 decreasing 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/9f0e22db49f7b49d5352750e9f69705f13c06d64/src/methods/barycentric.jl#L289-L305)
+
+</details>
+
+<details class='jldocstring custom-block' open>
+<summary><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'><span class="jlbinding">GeometryOps.to_edges</span></a> <Badge type="info" class="jlObjectType jlMethod" text="Method" /></summary>
+
+
+
+\`\`\`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 geometry's 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 actually 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]]]
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:
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:
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!
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
@example accurate GI.Polygon.(GO.flatten(Union{GI.LineStringTrait, GI.LinearRingTrait}, all_adm0) |> collect .|> x -> [x]) .|> GO.signed_area |> sum_kbn \`\`\`
`,16)]))}const E=i(n,[["render",l]]);export{g as __pageData,E as default}; diff --git a/previews/PR239/assets/experiments_accurate_accumulators.md.C1U7KOjF.lean.js b/previews/PR239/assets/experiments_accurate_accumulators.md.C1U7KOjF.lean.js new file mode 100644 index 000000000..eb05109cf --- /dev/null +++ b/previews/PR239/assets/experiments_accurate_accumulators.md.C1U7KOjF.lean.js @@ -0,0 +1,6 @@ +import{_ as i,c as a,a5 as t,o as e}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"Accurate accumulation","description":"","frontmatter":{},"headers":[],"relativePath":"experiments/accurate_accumulators.md","filePath":"experiments/accurate_accumulators.md","lastUpdated":null}'),n={name:"experiments/accurate_accumulators.md"};function l(h,s,p,k,d,r){return e(),a("div",null,s[0]||(s[0]=[t(`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!
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
@example accurate GI.Polygon.(GO.flatten(Union{GI.LineStringTrait, GI.LinearRingTrait}, all_adm0) |> collect .|> x -> [x]) .|> GO.signed_area |> sum_kbn \`\`\`
`,16)]))}const E=i(n,[["render",l]]);export{g as __pageData,E as default}; diff --git a/previews/PR239/assets/experiments_predicates.md.C2d7Qctz.js b/previews/PR239/assets/experiments_predicates.md.C2d7Qctz.js new file mode 100644 index 000000000..eec97f2c7 --- /dev/null +++ b/previews/PR239/assets/experiments_predicates.md.C2d7Qctz.js @@ -0,0 +1,98 @@ +import{_ as i,c as a,a5 as h,o as k}from"./chunks/framework.onQNwZ2I.js";const n="/GeometryOps.jl/previews/PR239/assets/ievlkku.5DNJaCuT.png",y=JSON.parse('{"title":"Predicates","description":"","frontmatter":{},"headers":[],"relativePath":"experiments/predicates.md","filePath":"experiments/predicates.md","lastUpdated":null}'),p={name:"experiments/predicates.md"};function l(t,s,E,e,r,d){return k(),a("div",null,s[0]||(s[0]=[h(`Exact vs fast predicates
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
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
+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)
Exact vs fast predicates
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
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
+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)
GeometryOps exposes functions like 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.
Below, we'll describe some of the foundational paradigms of GeometryOps, and why you should care!
apply
The 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)
Functionally, it's similar to 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
The 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.
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 Prepared
geometry with several preparations (sorted edge lists, rtrees, monotone chains, etc.)
GeometryOps exposes functions like 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.
Below, we'll describe some of the foundational paradigms of GeometryOps, and why you should care!
apply
The 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)
Functionally, it's similar to 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
The 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.
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 Prepared
geometry with several preparations (sorted edge lists, rtrees, monotone chains, etc.)
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).
It is recommended for consistency that f
returns GeoInterface geometries unless there is a performance/conversion overhead to doing that.
target
in set operations? In polygon set operations like 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.
We use the 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.
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.
_True
and _False
(or BoolsAsTypes
) 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 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 about
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).
It is recommended for consistency that f
returns GeoInterface geometries unless there is a performance/conversion overhead to doing that.
target
in set operations? In polygon set operations like 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.
We use the 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.
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.
_True
and _False
(or BoolsAsTypes
) 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 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 about
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!
GeometryOps' docs are divided into three main sections: tutorials, explanations and source code.
Documentation and examples for many functions can be found in the source code section, since we use literate programming in GeometryOps.
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!
GeometryOps' docs are divided into three main sections: tutorials, explanations and source code.
Documentation and examples for many functions can be found in the source code section, since we use literate programming in GeometryOps.
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!
apply
paradigm Note
See the Primitive Functions page for more information on this.
The apply
function allows you to decompose a given collection of geometries down to a certain level, and then operate on it.
Functionally, it's similar to 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
)!
GeoInterface.Wrapper
thing? Write a comment about GeoInterface.Wrapper and why it helps in type stability to guarantee a particular return type.
',13)]))}const u=a(r,[["render",n]]);export{m as __pageData,u as default}; diff --git a/previews/PR239/assets/introduction.md.FQTtykEQ.lean.js b/previews/PR239/assets/introduction.md.FQTtykEQ.lean.js new file mode 100644 index 000000000..68fcb53ae --- /dev/null +++ b/previews/PR239/assets/introduction.md.FQTtykEQ.lean.js @@ -0,0 +1 @@ +import{_ as a,c as t,a5 as o,o as i}from"./chunks/framework.onQNwZ2I.js";const m=JSON.parse('{"title":"Introduction","description":"","frontmatter":{},"headers":[],"relativePath":"introduction.md","filePath":"introduction.md","lastUpdated":null}'),r={name:"introduction.md"};function n(c,e,s,p,l,d){return i(),t("div",null,e[0]||(e[0]=[o('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!
apply
paradigm Note
See the Primitive Functions page for more information on this.
The apply
function allows you to decompose a given collection of geometries down to a certain level, and then operate on it.
Functionally, it's similar to 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
)!
GeoInterface.Wrapper
thing? Write a comment about GeoInterface.Wrapper and why it helps in type stability to guarantee a particular return type.
',13)]))}const u=a(r,[["render",n]]);export{m as __pageData,u as default}; diff --git a/previews/PR239/assets/ivowqmu.Cx40vhB3.png b/previews/PR239/assets/ivowqmu.Cx40vhB3.png new file mode 100644 index 000000000..f60db5413 Binary files /dev/null and b/previews/PR239/assets/ivowqmu.Cx40vhB3.png differ diff --git a/previews/PR239/assets/izslanu.Cm_V2Vs0.png b/previews/PR239/assets/izslanu.Cm_V2Vs0.png new file mode 100644 index 000000000..528e61b4a Binary files /dev/null and b/previews/PR239/assets/izslanu.Cm_V2Vs0.png differ diff --git a/previews/PR239/assets/jsdldah.C3SxJ3x-.png b/previews/PR239/assets/jsdldah.C3SxJ3x-.png new file mode 100644 index 000000000..5261ec3dc Binary files /dev/null and b/previews/PR239/assets/jsdldah.C3SxJ3x-.png differ diff --git a/previews/PR239/assets/kbbskga.BT_oJ_KS.png b/previews/PR239/assets/kbbskga.BT_oJ_KS.png new file mode 100644 index 000000000..98b078ad7 Binary files /dev/null and b/previews/PR239/assets/kbbskga.BT_oJ_KS.png differ diff --git a/previews/PR239/assets/kbzwnsa.DC3TvBOO.png b/previews/PR239/assets/kbzwnsa.DC3TvBOO.png new file mode 100644 index 000000000..924b99908 Binary files /dev/null and b/previews/PR239/assets/kbzwnsa.DC3TvBOO.png differ diff --git a/previews/PR239/assets/kguctvv.Cb0_DiYE.png b/previews/PR239/assets/kguctvv.Cb0_DiYE.png new file mode 100644 index 000000000..6a6cda100 Binary files /dev/null and b/previews/PR239/assets/kguctvv.Cb0_DiYE.png differ diff --git a/previews/PR239/assets/kojydpc.3UVIT8DR.png b/previews/PR239/assets/kojydpc.3UVIT8DR.png new file mode 100644 index 000000000..108afaca3 Binary files /dev/null and b/previews/PR239/assets/kojydpc.3UVIT8DR.png differ diff --git a/previews/PR239/assets/krkgpps.4wfjCtJV.png b/previews/PR239/assets/krkgpps.4wfjCtJV.png new file mode 100644 index 000000000..8fe6ad743 Binary files /dev/null and b/previews/PR239/assets/krkgpps.4wfjCtJV.png differ diff --git a/previews/PR239/assets/mtwzvwz.D9AE7i2o.png b/previews/PR239/assets/mtwzvwz.D9AE7i2o.png new file mode 100644 index 000000000..9219565ca Binary files /dev/null and b/previews/PR239/assets/mtwzvwz.D9AE7i2o.png differ diff --git a/previews/PR239/assets/oafsxip.-VpeHhXX.png b/previews/PR239/assets/oafsxip.-VpeHhXX.png new file mode 100644 index 000000000..bc1c05436 Binary files /dev/null and b/previews/PR239/assets/oafsxip.-VpeHhXX.png differ diff --git a/previews/PR239/assets/pkevosp.Dig-DWOQ.png b/previews/PR239/assets/pkevosp.Dig-DWOQ.png new file mode 100644 index 000000000..13977e4c3 Binary files /dev/null and b/previews/PR239/assets/pkevosp.Dig-DWOQ.png differ diff --git a/previews/PR239/assets/rleiuex.3sfpQl2i.png b/previews/PR239/assets/rleiuex.3sfpQl2i.png new file mode 100644 index 000000000..e91a0338c Binary files /dev/null and b/previews/PR239/assets/rleiuex.3sfpQl2i.png differ diff --git a/previews/PR239/assets/rzkakxn._0R9BbFk.png b/previews/PR239/assets/rzkakxn._0R9BbFk.png new file mode 100644 index 000000000..6752dc80d Binary files /dev/null and b/previews/PR239/assets/rzkakxn._0R9BbFk.png differ diff --git a/previews/PR239/assets/source_GeometryOps.md.D_V6MfpH.js b/previews/PR239/assets/source_GeometryOps.md.D_V6MfpH.js new file mode 100644 index 000000000..363b65baa --- /dev/null +++ b/previews/PR239/assets/source_GeometryOps.md.D_V6MfpH.js @@ -0,0 +1,85 @@ +import{_ as i,c as a,a5 as n,o as l}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"GeometryOps.jl","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOps.md","filePath":"source/GeometryOps.md","lastUpdated":null}'),t={name:"source/GeometryOps.md"};function h(p,s,k,e,E,r){return l(),a("div",null,s[0]||(s[0]=[n(`module GeometryOps
+
+import GeometryOpsCore
+import GeometryOpsCore:
+ TraitTarget,
+ Manifold, Planar, Spherical, Geodesic,
+ BoolsAsTypes, _True, _False, _booltype,
+ apply, applyreduce,
+ flatten, reconstruct, rebuild, unwrap, _linearring,
+ APPLY_KEYWORDS, THREADED_KEYWORD, CRS_KEYWORD, CALC_EXTENT_KEYWORD
+
+export TraitTarget, Manifold, Planar, Spherical, Geodesic, apply, applyreduce, flatten, reconstruct, rebuild, unwrap
+
+using GeoInterface
+using GeometryBasics
+using LinearAlgebra, Statistics
+
+import Tables, DataAPI
+import GeometryBasics.StaticArrays
+import DelaunayTriangulation # for convex hull and triangulation
+import ExactPredicates
+import Base.@kwdef
+import 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("types.jl")
+include("primitives.jl")
+include("utils.jl")
+include("not_implemented_yet.jl")
+
+include("methods/angles.jl")
+include("methods/area.jl")
+include("methods/barycentric.jl")
+include("methods/buffer.jl")
+include("methods/centroid.jl")
+include("methods/convex_hull.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")
Import all names from GeoInterface and Extents, so users can do 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__()
Handle all available errors!
Base.Experimental.register_error_hint(_reproject_error_hinter, MethodError)
+ Base.Experimental.register_error_hint(_geodesic_segments_error_hinter, MethodError)
+ Base.Experimental.register_error_hint(_buffer_error_hinter, MethodError)
+end
+
+end
This page was generated using Literate.jl.
`,8)]))}const o=i(t,[["render",h]]);export{g as __pageData,o as default}; diff --git a/previews/PR239/assets/source_GeometryOps.md.D_V6MfpH.lean.js b/previews/PR239/assets/source_GeometryOps.md.D_V6MfpH.lean.js new file mode 100644 index 000000000..363b65baa --- /dev/null +++ b/previews/PR239/assets/source_GeometryOps.md.D_V6MfpH.lean.js @@ -0,0 +1,85 @@ +import{_ as i,c as a,a5 as n,o as l}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"GeometryOps.jl","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOps.md","filePath":"source/GeometryOps.md","lastUpdated":null}'),t={name:"source/GeometryOps.md"};function h(p,s,k,e,E,r){return l(),a("div",null,s[0]||(s[0]=[n(`module GeometryOps
+
+import GeometryOpsCore
+import GeometryOpsCore:
+ TraitTarget,
+ Manifold, Planar, Spherical, Geodesic,
+ BoolsAsTypes, _True, _False, _booltype,
+ apply, applyreduce,
+ flatten, reconstruct, rebuild, unwrap, _linearring,
+ APPLY_KEYWORDS, THREADED_KEYWORD, CRS_KEYWORD, CALC_EXTENT_KEYWORD
+
+export TraitTarget, Manifold, Planar, Spherical, Geodesic, apply, applyreduce, flatten, reconstruct, rebuild, unwrap
+
+using GeoInterface
+using GeometryBasics
+using LinearAlgebra, Statistics
+
+import Tables, DataAPI
+import GeometryBasics.StaticArrays
+import DelaunayTriangulation # for convex hull and triangulation
+import ExactPredicates
+import Base.@kwdef
+import 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("types.jl")
+include("primitives.jl")
+include("utils.jl")
+include("not_implemented_yet.jl")
+
+include("methods/angles.jl")
+include("methods/area.jl")
+include("methods/barycentric.jl")
+include("methods/buffer.jl")
+include("methods/centroid.jl")
+include("methods/convex_hull.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")
Import all names from GeoInterface and Extents, so users can do 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__()
Handle all available errors!
Base.Experimental.register_error_hint(_reproject_error_hinter, MethodError)
+ Base.Experimental.register_error_hint(_geodesic_segments_error_hinter, MethodError)
+ Base.Experimental.register_error_hint(_buffer_error_hinter, MethodError)
+end
+
+end
This page was generated using Literate.jl.
`,8)]))}const o=i(t,[["render",h]]);export{g as __pageData,o as default}; diff --git a/previews/PR239/assets/source_GeometryOpsFlexiJoinsExt_GeometryOpsFlexiJoinsExt.md.BtiH8and.js b/previews/PR239/assets/source_GeometryOpsFlexiJoinsExt_GeometryOpsFlexiJoinsExt.md.BtiH8and.js new file mode 100644 index 000000000..88f619662 --- /dev/null +++ b/previews/PR239/assets/source_GeometryOpsFlexiJoinsExt_GeometryOpsFlexiJoinsExt.md.BtiH8and.js @@ -0,0 +1,16 @@ +import{_ as i,c as a,a5 as h,o as t}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOpsFlexiJoinsExt/GeometryOpsFlexiJoinsExt.md","filePath":"source/GeometryOpsFlexiJoinsExt/GeometryOpsFlexiJoinsExt.md","lastUpdated":null}'),n={name:"source/GeometryOpsFlexiJoinsExt/GeometryOpsFlexiJoinsExt.md"};function e(k,s,p,l,d,r){return t(),a("div",null,s[0]||(s[0]=[h(`module GeometryOpsFlexiJoinsExt
+
+using GeometryOps
+using FlexiJoins
+
+import GeometryOps as GO, GeoInterface as GI
+using SortTileRecursiveTree, Tables
This module defines the FlexiJoins APIs for GeometryOps' boolean comparison functions, taken from DE-9IM.
First, we define the joining modes (Tree, NestedLoopFast) that the GO DE-9IM functions support.
const GO_DE9IM_FUNCS = Union{typeof(GO.contains), typeof(GO.within), typeof(GO.intersects), typeof(GO.disjoint), typeof(GO.touches), typeof(GO.crosses), typeof(GO.overlaps), typeof(GO.covers), typeof(GO.coveredby), typeof(GO.equals)}
NestedLoopFast is the naive fallback method
FlexiJoins.supports_mode(::FlexiJoins.Mode.NestedLoopFast, ::FlexiJoins.ByPred{F}, datas) where F <: GO_DE9IM_FUNCS = true
This method allows you to cache a tree, which we do by using an STRtree. TODO: wrap GO predicate functions in a TreeJoiner
struct or something, to indicate that we want to use trees, since they can be slower in some situations.
FlexiJoins.supports_mode(::FlexiJoins.Mode.Tree, ::FlexiJoins.ByPred{F}, datas) where F <: GO_DE9IM_FUNCS = true
Nested loop support is simple, and needs no further support. However, for trees, we need to define how the tree is prepared and how it is used. This is done by defining the prepare_for_join
function to return an STRTree, and by defining the findmatchix
function as querying that tree before checking intersections.
In theory, one could extract the tree from e.g a GeoPackage or some future GeoDataFrame.
FlexiJoins.prepare_for_join(::FlexiJoins.Mode.Tree, X, cond::FlexiJoins.ByPred{<: GO_DE9IM_FUNCS}) = (X, SortTileRecursiveTree.STRtree(map(cond.Rf, X)))
+function FlexiJoins.findmatchix(::FlexiJoins.Mode.Tree, cond::FlexiJoins.ByPred{F}, ix_a, a, (B, tree)::Tuple, multi::typeof(identity)) where F <: GO_DE9IM_FUNCS
Implementation note: here, a
is a row, and b
is the full table. We extract the relevant columns using cond.Lf and cond.Rf.
idxs = SortTileRecursiveTree.query(tree, cond.Lf(a))
+ intersecting_idxs = filter!(idxs) do idx
+ cond.pred(cond.Lf(a), cond.Rf(B[idx]))
+ end
+ return intersecting_idxs
+end
Finally, for completeness, we define the swap_sides
function for those predicates which are defined as inversions.
FlexiJoins.swap_sides(::typeof(GO.contains)) = GO.within
+FlexiJoins.swap_sides(::typeof(GO.within)) = GO.contains
+FlexiJoins.swap_sides(::typeof(GO.coveredby)) = GO.covers
+FlexiJoins.swap_sides(::typeof(GO.covers)) = GO.coveredby
That's a wrap, folks!
end
This page was generated using Literate.jl.
`,19)]))}const y=i(n,[["render",e]]);export{g as __pageData,y as default}; diff --git a/previews/PR239/assets/source_GeometryOpsFlexiJoinsExt_GeometryOpsFlexiJoinsExt.md.BtiH8and.lean.js b/previews/PR239/assets/source_GeometryOpsFlexiJoinsExt_GeometryOpsFlexiJoinsExt.md.BtiH8and.lean.js new file mode 100644 index 000000000..88f619662 --- /dev/null +++ b/previews/PR239/assets/source_GeometryOpsFlexiJoinsExt_GeometryOpsFlexiJoinsExt.md.BtiH8and.lean.js @@ -0,0 +1,16 @@ +import{_ as i,c as a,a5 as h,o as t}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOpsFlexiJoinsExt/GeometryOpsFlexiJoinsExt.md","filePath":"source/GeometryOpsFlexiJoinsExt/GeometryOpsFlexiJoinsExt.md","lastUpdated":null}'),n={name:"source/GeometryOpsFlexiJoinsExt/GeometryOpsFlexiJoinsExt.md"};function e(k,s,p,l,d,r){return t(),a("div",null,s[0]||(s[0]=[h(`module GeometryOpsFlexiJoinsExt
+
+using GeometryOps
+using FlexiJoins
+
+import GeometryOps as GO, GeoInterface as GI
+using SortTileRecursiveTree, Tables
This module defines the FlexiJoins APIs for GeometryOps' boolean comparison functions, taken from DE-9IM.
First, we define the joining modes (Tree, NestedLoopFast) that the GO DE-9IM functions support.
const GO_DE9IM_FUNCS = Union{typeof(GO.contains), typeof(GO.within), typeof(GO.intersects), typeof(GO.disjoint), typeof(GO.touches), typeof(GO.crosses), typeof(GO.overlaps), typeof(GO.covers), typeof(GO.coveredby), typeof(GO.equals)}
NestedLoopFast is the naive fallback method
FlexiJoins.supports_mode(::FlexiJoins.Mode.NestedLoopFast, ::FlexiJoins.ByPred{F}, datas) where F <: GO_DE9IM_FUNCS = true
This method allows you to cache a tree, which we do by using an STRtree. TODO: wrap GO predicate functions in a TreeJoiner
struct or something, to indicate that we want to use trees, since they can be slower in some situations.
FlexiJoins.supports_mode(::FlexiJoins.Mode.Tree, ::FlexiJoins.ByPred{F}, datas) where F <: GO_DE9IM_FUNCS = true
Nested loop support is simple, and needs no further support. However, for trees, we need to define how the tree is prepared and how it is used. This is done by defining the prepare_for_join
function to return an STRTree, and by defining the findmatchix
function as querying that tree before checking intersections.
In theory, one could extract the tree from e.g a GeoPackage or some future GeoDataFrame.
FlexiJoins.prepare_for_join(::FlexiJoins.Mode.Tree, X, cond::FlexiJoins.ByPred{<: GO_DE9IM_FUNCS}) = (X, SortTileRecursiveTree.STRtree(map(cond.Rf, X)))
+function FlexiJoins.findmatchix(::FlexiJoins.Mode.Tree, cond::FlexiJoins.ByPred{F}, ix_a, a, (B, tree)::Tuple, multi::typeof(identity)) where F <: GO_DE9IM_FUNCS
Implementation note: here, a
is a row, and b
is the full table. We extract the relevant columns using cond.Lf and cond.Rf.
idxs = SortTileRecursiveTree.query(tree, cond.Lf(a))
+ intersecting_idxs = filter!(idxs) do idx
+ cond.pred(cond.Lf(a), cond.Rf(B[idx]))
+ end
+ return intersecting_idxs
+end
Finally, for completeness, we define the swap_sides
function for those predicates which are defined as inversions.
FlexiJoins.swap_sides(::typeof(GO.contains)) = GO.within
+FlexiJoins.swap_sides(::typeof(GO.within)) = GO.contains
+FlexiJoins.swap_sides(::typeof(GO.coveredby)) = GO.covers
+FlexiJoins.swap_sides(::typeof(GO.covers)) = GO.coveredby
That's a wrap, folks!
end
This page was generated using Literate.jl.
`,19)]))}const y=i(n,[["render",e]]);export{g as __pageData,y as default}; diff --git a/previews/PR239/assets/source_GeometryOpsLibGEOSExt_GeometryOpsLibGEOSExt.md.BX97MBsi.js b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_GeometryOpsLibGEOSExt.md.BX97MBsi.js new file mode 100644 index 000000000..2f1f59f92 --- /dev/null +++ b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_GeometryOpsLibGEOSExt.md.BX97MBsi.js @@ -0,0 +1,31 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const o=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOpsLibGEOSExt/GeometryOpsLibGEOSExt.md","filePath":"source/GeometryOpsLibGEOSExt/GeometryOpsLibGEOSExt.md","lastUpdated":null}'),e={name:"source/GeometryOpsLibGEOSExt/GeometryOpsLibGEOSExt.md"};function l(h,s,p,k,r,E){return t(),a("div",null,s[0]||(s[0]=[n(`module GeometryOpsLibGEOSExt
+
+import GeometryOps as GO, LibGEOS as LG
+import GeoInterface as GI
+
+import GeometryOps: GEOS, enforce, _True, _False, _booltype
+
+using GeometryOps
The filter statement is required because in Julia, each module has its own versions of these functions, which serve to evaluate or include code inside the scope of the module. However, if you import those from another module (which you would with all=true
), that creates an ambiguity which causes a warning during precompile/load time. In order to avoid this, we filter out these special functions.
for name in filter(!in((:var"#eval", :eval, :var"#include", :include)), names(GeometryOps))
+ @eval import GeometryOps: $name
+end
+
+"""
+ _wrap(geom; crs, calc_extent)
+
+Wraps \`geom\` in a GI wrapper geometry of its geometry trait. This allows us
+to attach CRS and extent info to geometry types which otherwise could not hold
+those, like LibGEOS and WKB geometries.
+
+Returns a GI wrapper geometry, for which \`parent(result) == geom\`.
+"""
+function _wrap(geom; crs=GI.crs(geom), calc_extent = true)
+ return GI.geointerface_geomtype(GI.geomtrait(geom))(geom; crs, extent = GI.extent(geom, calc_extent))
+end
+
+include("buffer.jl")
+include("segmentize.jl")
+include("simplify.jl")
+
+include("simple_overrides.jl")
+
+end
This page was generated using Literate.jl.
`,5)]))}const g=i(e,[["render",l]]);export{o as __pageData,g as default}; diff --git a/previews/PR239/assets/source_GeometryOpsLibGEOSExt_GeometryOpsLibGEOSExt.md.BX97MBsi.lean.js b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_GeometryOpsLibGEOSExt.md.BX97MBsi.lean.js new file mode 100644 index 000000000..2f1f59f92 --- /dev/null +++ b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_GeometryOpsLibGEOSExt.md.BX97MBsi.lean.js @@ -0,0 +1,31 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const o=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOpsLibGEOSExt/GeometryOpsLibGEOSExt.md","filePath":"source/GeometryOpsLibGEOSExt/GeometryOpsLibGEOSExt.md","lastUpdated":null}'),e={name:"source/GeometryOpsLibGEOSExt/GeometryOpsLibGEOSExt.md"};function l(h,s,p,k,r,E){return t(),a("div",null,s[0]||(s[0]=[n(`module GeometryOpsLibGEOSExt
+
+import GeometryOps as GO, LibGEOS as LG
+import GeoInterface as GI
+
+import GeometryOps: GEOS, enforce, _True, _False, _booltype
+
+using GeometryOps
The filter statement is required because in Julia, each module has its own versions of these functions, which serve to evaluate or include code inside the scope of the module. However, if you import those from another module (which you would with all=true
), that creates an ambiguity which causes a warning during precompile/load time. In order to avoid this, we filter out these special functions.
for name in filter(!in((:var"#eval", :eval, :var"#include", :include)), names(GeometryOps))
+ @eval import GeometryOps: $name
+end
+
+"""
+ _wrap(geom; crs, calc_extent)
+
+Wraps \`geom\` in a GI wrapper geometry of its geometry trait. This allows us
+to attach CRS and extent info to geometry types which otherwise could not hold
+those, like LibGEOS and WKB geometries.
+
+Returns a GI wrapper geometry, for which \`parent(result) == geom\`.
+"""
+function _wrap(geom; crs=GI.crs(geom), calc_extent = true)
+ return GI.geointerface_geomtype(GI.geomtrait(geom))(geom; crs, extent = GI.extent(geom, calc_extent))
+end
+
+include("buffer.jl")
+include("segmentize.jl")
+include("simplify.jl")
+
+include("simple_overrides.jl")
+
+end
This page was generated using Literate.jl.
`,5)]))}const g=i(e,[["render",l]]);export{o as __pageData,g as default}; diff --git a/previews/PR239/assets/source_GeometryOpsLibGEOSExt_buffer.md.BIbTaNIz.js b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_buffer.md.BIbTaNIz.js new file mode 100644 index 000000000..0703c9d6a --- /dev/null +++ b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_buffer.md.BIbTaNIz.js @@ -0,0 +1,31 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const d=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOpsLibGEOSExt/buffer.md","filePath":"source/GeometryOpsLibGEOSExt/buffer.md","lastUpdated":null}'),k={name:"source/GeometryOpsLibGEOSExt/buffer.md"};function t(l,s,p,e,E,r){return h(),a("div",null,s[0]||(s[0]=[n(`const _GEOS_CAPSTYLE_LOOKUP = Dict{Symbol, LG.GEOSBufCapStyles}(
+ :round => LG.GEOSBUF_CAP_ROUND,
+ :flat => LG.GEOSBUF_CAP_FLAT,
+ :square => LG.GEOSBUF_CAP_SQUARE,
+)
+
+const _GEOS_JOINSTYLE_LOOKUP = Dict{Symbol, LG.GEOSBufJoinStyles}(
+ :round => LG.GEOSBUF_JOIN_ROUND,
+ :mitre => LG.GEOSBUF_JOIN_MITRE,
+ :bevel => LG.GEOSBUF_JOIN_BEVEL,
+)
+
+to_cap_style(style::Symbol) = _GEOS_CAPSTYLE_LOOKUP[style]
+to_cap_style(style::LG.GEOSBufCapStyles) = style
+to_cap_style(num::Integer) = num
+
+to_join_style(style::Symbol) = _GEOS_JOINSTYLE_LOOKUP[style]
+to_join_style(style::LG.GEOSBufJoinStyles) = style
+to_join_style(num::Integer) = num
+
+function GO.buffer(alg::GEOS, geometry, distance; calc_extent = true, kwargs...)
The reason we use apply here is so that this also works with featurecollections, tables, vectors of geometries, etc!
return apply(TraitTarget{GI.AbstractGeometryTrait}(), geometry; kwargs...) do geom
+ newgeom = LG.bufferWithStyle(
+ GI.convert(LG, geom), distance;
+ quadsegs = get(alg, :quadsegs, 8),
+ endCapStyle = to_cap_style(get(alg, :endCapStyle, :round)),
+ joinStyle = to_join_style(get(alg, :joinStyle, :round)),
+ mitreLimit = get(alg, :mitreLimit, 5.0),
+ )
+ return _wrap(newgeom; crs = GI.crs(geom), calc_extent)
+ end
+end
This page was generated using Literate.jl.
`,5)]))}const y=i(k,[["render",t]]);export{d as __pageData,y as default}; diff --git a/previews/PR239/assets/source_GeometryOpsLibGEOSExt_buffer.md.BIbTaNIz.lean.js b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_buffer.md.BIbTaNIz.lean.js new file mode 100644 index 000000000..0703c9d6a --- /dev/null +++ b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_buffer.md.BIbTaNIz.lean.js @@ -0,0 +1,31 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const d=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOpsLibGEOSExt/buffer.md","filePath":"source/GeometryOpsLibGEOSExt/buffer.md","lastUpdated":null}'),k={name:"source/GeometryOpsLibGEOSExt/buffer.md"};function t(l,s,p,e,E,r){return h(),a("div",null,s[0]||(s[0]=[n(`const _GEOS_CAPSTYLE_LOOKUP = Dict{Symbol, LG.GEOSBufCapStyles}(
+ :round => LG.GEOSBUF_CAP_ROUND,
+ :flat => LG.GEOSBUF_CAP_FLAT,
+ :square => LG.GEOSBUF_CAP_SQUARE,
+)
+
+const _GEOS_JOINSTYLE_LOOKUP = Dict{Symbol, LG.GEOSBufJoinStyles}(
+ :round => LG.GEOSBUF_JOIN_ROUND,
+ :mitre => LG.GEOSBUF_JOIN_MITRE,
+ :bevel => LG.GEOSBUF_JOIN_BEVEL,
+)
+
+to_cap_style(style::Symbol) = _GEOS_CAPSTYLE_LOOKUP[style]
+to_cap_style(style::LG.GEOSBufCapStyles) = style
+to_cap_style(num::Integer) = num
+
+to_join_style(style::Symbol) = _GEOS_JOINSTYLE_LOOKUP[style]
+to_join_style(style::LG.GEOSBufJoinStyles) = style
+to_join_style(num::Integer) = num
+
+function GO.buffer(alg::GEOS, geometry, distance; calc_extent = true, kwargs...)
The reason we use apply here is so that this also works with featurecollections, tables, vectors of geometries, etc!
return apply(TraitTarget{GI.AbstractGeometryTrait}(), geometry; kwargs...) do geom
+ newgeom = LG.bufferWithStyle(
+ GI.convert(LG, geom), distance;
+ quadsegs = get(alg, :quadsegs, 8),
+ endCapStyle = to_cap_style(get(alg, :endCapStyle, :round)),
+ joinStyle = to_join_style(get(alg, :joinStyle, :round)),
+ mitreLimit = get(alg, :mitreLimit, 5.0),
+ )
+ return _wrap(newgeom; crs = GI.crs(geom), calc_extent)
+ end
+end
This page was generated using Literate.jl.
`,5)]))}const y=i(k,[["render",t]]);export{d as __pageData,y as default}; diff --git a/previews/PR239/assets/source_GeometryOpsLibGEOSExt_segmentize.md.C7teKqJd.js b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_segmentize.md.C7teKqJd.js new file mode 100644 index 000000000..ecd9353d1 --- /dev/null +++ b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_segmentize.md.C7teKqJd.js @@ -0,0 +1,21 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"Segmentize","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOpsLibGEOSExt/segmentize.md","filePath":"source/GeometryOpsLibGEOSExt/segmentize.md","lastUpdated":null}'),e={name:"source/GeometryOpsLibGEOSExt/segmentize.md"};function h(k,s,l,p,r,E){return t(),a("div",null,s[0]||(s[0]=[n(`import GeometryOps: segmentize, apply
This file implements the LibGEOS segmentization method for GeometryOps.
function _segmentize_geos(geom::LG.AbstractGeometry, max_distance)
+ context = LG.get_context(geom)
+ result = LG.GEOSDensify_r(context, geom, max_distance)
+ if result == C_NULL
+ error("LibGEOS: Error in GEOSDensify")
+ end
+ return LG.geomFromGEOS(result, context)
+end
+
+_segmentize_geos(geom, max_distance) = _segmentize_geos(GI.convert(LG, geom), max_distance)
+
+function _wrap_and_segmentize_geos(geom, max_distance)
+ _wrap(_segmentize_geos(geom, max_distance); crs = GI.crs(geom), calc_extent = false)
+end
2 behaviours:
enforce: enforce the presence of a kwargs
fetch: fetch the value of a kwargs, or return a default value
@inline function GO.segmentize(alg::GEOS, geom; threaded::Union{Bool, GO.BoolsAsTypes} = _False())
+ max_distance = enforce(alg, :max_distance, GO.segmentize)
+ return GO.apply(
+ Base.Fix2(_wrap_and_segmentize_geos, max_distance),
TODO: should this just be a target on GI.AbstractGeometryTrait()? But Geos doesn't support eg RectangleTrait Maybe we need an abstract trait GI.AbstractWKBGeomTrait
?
GO.TraitTarget(GI.GeometryCollectionTrait(), GI.MultiPolygonTrait(), GI.PolygonTrait(), GI.MultiLineStringTrait(), GI.LineStringTrait(), GI.LinearRingTrait(), GI.MultiPointTrait(), GI.PointTrait()),
+ geom;
+ threaded
+ )
+end
This page was generated using Literate.jl.
`,11)]))}const y=i(e,[["render",h]]);export{g as __pageData,y as default}; diff --git a/previews/PR239/assets/source_GeometryOpsLibGEOSExt_segmentize.md.C7teKqJd.lean.js b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_segmentize.md.C7teKqJd.lean.js new file mode 100644 index 000000000..ecd9353d1 --- /dev/null +++ b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_segmentize.md.C7teKqJd.lean.js @@ -0,0 +1,21 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"Segmentize","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOpsLibGEOSExt/segmentize.md","filePath":"source/GeometryOpsLibGEOSExt/segmentize.md","lastUpdated":null}'),e={name:"source/GeometryOpsLibGEOSExt/segmentize.md"};function h(k,s,l,p,r,E){return t(),a("div",null,s[0]||(s[0]=[n(`import GeometryOps: segmentize, apply
This file implements the LibGEOS segmentization method for GeometryOps.
function _segmentize_geos(geom::LG.AbstractGeometry, max_distance)
+ context = LG.get_context(geom)
+ result = LG.GEOSDensify_r(context, geom, max_distance)
+ if result == C_NULL
+ error("LibGEOS: Error in GEOSDensify")
+ end
+ return LG.geomFromGEOS(result, context)
+end
+
+_segmentize_geos(geom, max_distance) = _segmentize_geos(GI.convert(LG, geom), max_distance)
+
+function _wrap_and_segmentize_geos(geom, max_distance)
+ _wrap(_segmentize_geos(geom, max_distance); crs = GI.crs(geom), calc_extent = false)
+end
2 behaviours:
enforce: enforce the presence of a kwargs
fetch: fetch the value of a kwargs, or return a default value
@inline function GO.segmentize(alg::GEOS, geom; threaded::Union{Bool, GO.BoolsAsTypes} = _False())
+ max_distance = enforce(alg, :max_distance, GO.segmentize)
+ return GO.apply(
+ Base.Fix2(_wrap_and_segmentize_geos, max_distance),
TODO: should this just be a target on GI.AbstractGeometryTrait()? But Geos doesn't support eg RectangleTrait Maybe we need an abstract trait GI.AbstractWKBGeomTrait
?
GO.TraitTarget(GI.GeometryCollectionTrait(), GI.MultiPolygonTrait(), GI.PolygonTrait(), GI.MultiLineStringTrait(), GI.LineStringTrait(), GI.LinearRingTrait(), GI.MultiPointTrait(), GI.PointTrait()),
+ geom;
+ threaded
+ )
+end
This page was generated using Literate.jl.
`,11)]))}const y=i(e,[["render",h]]);export{g as __pageData,y as default}; diff --git a/previews/PR239/assets/source_GeometryOpsLibGEOSExt_simple_overrides.md.PL-RH0TF.js b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_simple_overrides.md.PL-RH0TF.js new file mode 100644 index 000000000..821c4e635 --- /dev/null +++ b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_simple_overrides.md.PL-RH0TF.js @@ -0,0 +1,46 @@ +import{_ as i,c as a,a5 as h,o as n}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"Simple overrides","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOpsLibGEOSExt/simple_overrides.md","filePath":"source/GeometryOpsLibGEOSExt/simple_overrides.md","lastUpdated":null}'),t={name:"source/GeometryOpsLibGEOSExt/simple_overrides.md"};function k(l,s,e,p,r,d){return n(),a("div",null,s[0]||(s[0]=[h(`This file contains simple overrides for GEOS, essentially only those functions which have direct counterparts in LG and only require conversion before calling.
function GO.difference(::GEOS, geom_a, geom_b; target=nothing, calc_extent = false)
+ return _wrap(LG.difference(GI.convert(LG, geom_a), GI.convert(LG, geom_b)); crs = GI.crs(geom_a), calc_extent)
+end
function GO.union(::GEOS, geom_a, geom_b; target=nothing, calc_extent = false)
+ return _wrap(LG.union(GI.convert(LG, geom_a), GI.convert(LG, geom_b)); crs = GI.crs(geom_a), calc_extent)
+end
function GO.intersection(::GEOS, geom_a, geom_b; target=nothing, calc_extent = false)
+ return _wrap(LG.intersection(GI.convert(LG, geom_a), GI.convert(LG, geom_b)); crs = GI.crs(geom_a), calc_extent)
+end
function GO.symdifference(::GEOS, geom_a, geom_b; target=nothing, calc_extent = false)
+ return _wrap(LG.symmetric_difference(GI.convert(LG, geom_a), GI.convert(LG, geom_b)); crs = GI.crs(geom_a), calc_extent)
+end
function GO.equals(::GEOS, geom_a, geom_b)
+ return LG.equals(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.disjoint(::GEOS, geom_a, geom_b)
+ return LG.disjoint(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.touches(::GEOS, geom_a, geom_b)
+ return LG.touches(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.crosses(::GEOS, geom_a, geom_b)
+ return LG.crosses(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.within(::GEOS, geom_a, geom_b)
+ return LG.within(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.contains(::GEOS, geom_a, geom_b)
+ return LG.contains(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.overlaps(::GEOS, geom_a, geom_b)
+ return LG.overlaps(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.covers(::GEOS, geom_a, geom_b)
+ return LG.covers(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.coveredby(::GEOS, geom_a, geom_b)
+ return LG.coveredby(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.intersects(::GEOS, geom_a, geom_b)
+ return LG.intersects(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.convex_hull(::GEOS, geoms)
+ chull = LG.convexhull(
+ LG.MultiPoint(
+ collect(
+ GO.flatten(
+ x -> GI.convert(LG.Point, x),
+ GI.PointTrait,
+ geoms
+ )
+ )
+ )
+ );
+ return _wrap(
+ chull;
+ crs = GI.crs(geoms),
+ calc_extent = false
+ )
+end
This page was generated using Literate.jl.
`,36)]))}const o=i(t,[["render",k]]);export{g as __pageData,o as default}; diff --git a/previews/PR239/assets/source_GeometryOpsLibGEOSExt_simple_overrides.md.PL-RH0TF.lean.js b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_simple_overrides.md.PL-RH0TF.lean.js new file mode 100644 index 000000000..821c4e635 --- /dev/null +++ b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_simple_overrides.md.PL-RH0TF.lean.js @@ -0,0 +1,46 @@ +import{_ as i,c as a,a5 as h,o as n}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"Simple overrides","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOpsLibGEOSExt/simple_overrides.md","filePath":"source/GeometryOpsLibGEOSExt/simple_overrides.md","lastUpdated":null}'),t={name:"source/GeometryOpsLibGEOSExt/simple_overrides.md"};function k(l,s,e,p,r,d){return n(),a("div",null,s[0]||(s[0]=[h(`This file contains simple overrides for GEOS, essentially only those functions which have direct counterparts in LG and only require conversion before calling.
function GO.difference(::GEOS, geom_a, geom_b; target=nothing, calc_extent = false)
+ return _wrap(LG.difference(GI.convert(LG, geom_a), GI.convert(LG, geom_b)); crs = GI.crs(geom_a), calc_extent)
+end
function GO.union(::GEOS, geom_a, geom_b; target=nothing, calc_extent = false)
+ return _wrap(LG.union(GI.convert(LG, geom_a), GI.convert(LG, geom_b)); crs = GI.crs(geom_a), calc_extent)
+end
function GO.intersection(::GEOS, geom_a, geom_b; target=nothing, calc_extent = false)
+ return _wrap(LG.intersection(GI.convert(LG, geom_a), GI.convert(LG, geom_b)); crs = GI.crs(geom_a), calc_extent)
+end
function GO.symdifference(::GEOS, geom_a, geom_b; target=nothing, calc_extent = false)
+ return _wrap(LG.symmetric_difference(GI.convert(LG, geom_a), GI.convert(LG, geom_b)); crs = GI.crs(geom_a), calc_extent)
+end
function GO.equals(::GEOS, geom_a, geom_b)
+ return LG.equals(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.disjoint(::GEOS, geom_a, geom_b)
+ return LG.disjoint(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.touches(::GEOS, geom_a, geom_b)
+ return LG.touches(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.crosses(::GEOS, geom_a, geom_b)
+ return LG.crosses(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.within(::GEOS, geom_a, geom_b)
+ return LG.within(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.contains(::GEOS, geom_a, geom_b)
+ return LG.contains(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.overlaps(::GEOS, geom_a, geom_b)
+ return LG.overlaps(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.covers(::GEOS, geom_a, geom_b)
+ return LG.covers(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.coveredby(::GEOS, geom_a, geom_b)
+ return LG.coveredby(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.intersects(::GEOS, geom_a, geom_b)
+ return LG.intersects(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end
function GO.convex_hull(::GEOS, geoms)
+ chull = LG.convexhull(
+ LG.MultiPoint(
+ collect(
+ GO.flatten(
+ x -> GI.convert(LG.Point, x),
+ GI.PointTrait,
+ geoms
+ )
+ )
+ )
+ );
+ return _wrap(
+ chull;
+ crs = GI.crs(geoms),
+ calc_extent = false
+ )
+end
This page was generated using Literate.jl.
`,36)]))}const o=i(t,[["render",k]]);export{g as __pageData,o as default}; diff --git a/previews/PR239/assets/source_GeometryOpsLibGEOSExt_simplify.md.DqcZcvcS.js b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_simplify.md.DqcZcvcS.js new file mode 100644 index 000000000..fb6d1f2ff --- /dev/null +++ b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_simplify.md.DqcZcvcS.js @@ -0,0 +1,29 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOpsLibGEOSExt/simplify.md","filePath":"source/GeometryOpsLibGEOSExt/simplify.md","lastUpdated":null}'),k={name:"source/GeometryOpsLibGEOSExt/simplify.md"};function l(t,s,p,e,E,r){return h(),a("div",null,s[0]||(s[0]=[n(`Address potential ambiguities
GO._simplify(::GI.PointTrait, ::GO.GEOS, geom; kw...) = geom
+GO._simplify(::GI.MultiPointTrait, ::GO.GEOS, geom; kw...) = geom
+
+function GO._simplify(::GI.AbstractGeometryTrait, alg::GO.GEOS, geom; kwargs...)
+ method = get(alg, :method, :TopologyPreserve)
+ @assert haskey(alg.params, :tol) """
+ The \`:tol\` parameter is required for the GEOS algorithm in \`simplify\`,
+ but it was not provided.
+
+ Provide it by passing \`GEOS(; tol = ...,) as the algorithm.
+ """
+ tol = alg.params.tol
+ if method == :TopologyPreserve
+ return LG.topologyPreserveSimplify(GI.convert(LG, geom), tol)
+ elseif method == :DouglasPeucker
+ return LG.simplify(GI.convert(LG, geom), tol)
+ else
+ error("Invalid method passed to \`GO.simplify(GEOS(...), ...)\`: $method. Please use :TopologyPreserve or :DouglasPeucker")
+ end
+end
+
+function GO._simplify(trait::GI.AbstractCurveTrait, alg::GO.GEOS, geom; kw...)
+ Base.invoke(
+ GO._simplify,
+ Tuple{GI.AbstractGeometryTrait, GO.GEOS, typeof(geom)},
+ trait, alg, geom;
+ kw...
+ )
+end
This page was generated using Literate.jl.
`,4)]))}const y=i(k,[["render",l]]);export{g as __pageData,y as default}; diff --git a/previews/PR239/assets/source_GeometryOpsLibGEOSExt_simplify.md.DqcZcvcS.lean.js b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_simplify.md.DqcZcvcS.lean.js new file mode 100644 index 000000000..fb6d1f2ff --- /dev/null +++ b/previews/PR239/assets/source_GeometryOpsLibGEOSExt_simplify.md.DqcZcvcS.lean.js @@ -0,0 +1,29 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOpsLibGEOSExt/simplify.md","filePath":"source/GeometryOpsLibGEOSExt/simplify.md","lastUpdated":null}'),k={name:"source/GeometryOpsLibGEOSExt/simplify.md"};function l(t,s,p,e,E,r){return h(),a("div",null,s[0]||(s[0]=[n(`Address potential ambiguities
GO._simplify(::GI.PointTrait, ::GO.GEOS, geom; kw...) = geom
+GO._simplify(::GI.MultiPointTrait, ::GO.GEOS, geom; kw...) = geom
+
+function GO._simplify(::GI.AbstractGeometryTrait, alg::GO.GEOS, geom; kwargs...)
+ method = get(alg, :method, :TopologyPreserve)
+ @assert haskey(alg.params, :tol) """
+ The \`:tol\` parameter is required for the GEOS algorithm in \`simplify\`,
+ but it was not provided.
+
+ Provide it by passing \`GEOS(; tol = ...,) as the algorithm.
+ """
+ tol = alg.params.tol
+ if method == :TopologyPreserve
+ return LG.topologyPreserveSimplify(GI.convert(LG, geom), tol)
+ elseif method == :DouglasPeucker
+ return LG.simplify(GI.convert(LG, geom), tol)
+ else
+ error("Invalid method passed to \`GO.simplify(GEOS(...), ...)\`: $method. Please use :TopologyPreserve or :DouglasPeucker")
+ end
+end
+
+function GO._simplify(trait::GI.AbstractCurveTrait, alg::GO.GEOS, geom; kw...)
+ Base.invoke(
+ GO._simplify,
+ Tuple{GI.AbstractGeometryTrait, GO.GEOS, typeof(geom)},
+ trait, alg, geom;
+ kw...
+ )
+end
This page was generated using Literate.jl.
`,4)]))}const y=i(k,[["render",l]]);export{g as __pageData,y as default}; diff --git a/previews/PR239/assets/source_GeometryOpsProjExt_GeometryOpsProjExt.md.CF-5wcua.js b/previews/PR239/assets/source_GeometryOpsProjExt_GeometryOpsProjExt.md.CF-5wcua.js new file mode 100644 index 000000000..7e3861f42 --- /dev/null +++ b/previews/PR239/assets/source_GeometryOpsProjExt_GeometryOpsProjExt.md.CF-5wcua.js @@ -0,0 +1,8 @@ +import{_ as e,c as a,a5 as i,o as t}from"./chunks/framework.onQNwZ2I.js";const c=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOpsProjExt/GeometryOpsProjExt.md","filePath":"source/GeometryOpsProjExt/GeometryOpsProjExt.md","lastUpdated":null}'),n={name:"source/GeometryOpsProjExt/GeometryOpsProjExt.md"};function p(l,s,r,o,h,k){return t(),a("div",null,s[0]||(s[0]=[i(`module GeometryOpsProjExt
+
+using GeometryOps, Proj
+
+include("reproject.jl")
+include("segmentize.jl")
+
+end
This page was generated using Literate.jl.
`,3)]))}const E=e(n,[["render",p]]);export{c as __pageData,E as default}; diff --git a/previews/PR239/assets/source_GeometryOpsProjExt_GeometryOpsProjExt.md.CF-5wcua.lean.js b/previews/PR239/assets/source_GeometryOpsProjExt_GeometryOpsProjExt.md.CF-5wcua.lean.js new file mode 100644 index 000000000..7e3861f42 --- /dev/null +++ b/previews/PR239/assets/source_GeometryOpsProjExt_GeometryOpsProjExt.md.CF-5wcua.lean.js @@ -0,0 +1,8 @@ +import{_ as e,c as a,a5 as i,o as t}from"./chunks/framework.onQNwZ2I.js";const c=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOpsProjExt/GeometryOpsProjExt.md","filePath":"source/GeometryOpsProjExt/GeometryOpsProjExt.md","lastUpdated":null}'),n={name:"source/GeometryOpsProjExt/GeometryOpsProjExt.md"};function p(l,s,r,o,h,k){return t(),a("div",null,s[0]||(s[0]=[i(`module GeometryOpsProjExt
+
+using GeometryOps, Proj
+
+include("reproject.jl")
+include("segmentize.jl")
+
+end
This page was generated using Literate.jl.
`,3)]))}const E=e(n,[["render",p]]);export{c as __pageData,E as default}; diff --git a/previews/PR239/assets/source_GeometryOpsProjExt_reproject.md.Cm7Q7Ebj.js b/previews/PR239/assets/source_GeometryOpsProjExt_reproject.md.Cm7Q7Ebj.js new file mode 100644 index 000000000..ffaa90130 --- /dev/null +++ b/previews/PR239/assets/source_GeometryOpsProjExt_reproject.md.Cm7Q7Ebj.js @@ -0,0 +1,44 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const d=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOpsProjExt/reproject.md","filePath":"source/GeometryOpsProjExt/reproject.md","lastUpdated":null}'),k={name:"source/GeometryOpsProjExt/reproject.md"};function t(p,s,l,e,r,E){return h(),a("div",null,s[0]||(s[0]=[n(`import GeometryOps: GI, GeoInterface, reproject, apply, transform, _is3d, _True, _False
+import Proj
+
+function reproject(geom;
+ source_crs=nothing, target_crs=nothing, transform=nothing, kw...
+)
+ if isnothing(transform)
+ if isnothing(source_crs)
+ source_crs = if GI.trait(geom) isa Nothing && geom isa AbstractArray
+ GeoInterface.crs(first(geom))
+ else
+ GeoInterface.crs(geom)
+ end
+ end
If its still nothing, error
isnothing(source_crs) && throw(ArgumentError("geom has no crs attached. Pass a \`source_crs\` keyword"))
Otherwise reproject
reproject(geom, source_crs, target_crs; kw...)
+ else
+ reproject(geom, transform; kw...)
+ end
+end
+function reproject(geom, source_crs, target_crs;
+ time=Inf,
+ always_xy=true,
+ transform=nothing,
+ kw...
+)
+ transform = if isnothing(transform)
+ s = source_crs isa Proj.CRS ? source_crs : convert(String, source_crs)
+ t = target_crs isa Proj.CRS ? target_crs : convert(String, target_crs)
+ Proj.Transformation(s, t; always_xy)
+ else
+ transform
+ end
+ reproject(geom, transform; time, target_crs, kw...)
+end
+function reproject(geom, transform::Proj.Transformation; time=Inf, target_crs=nothing, kw...)
+ if _is3d(geom)
+ return apply(GI.PointTrait(), geom; crs=target_crs, kw...) do p
+ transform(GI.x(p), GI.y(p), GI.z(p))
+ end
+ else
+ return apply(GI.PointTrait(), geom; crs=target_crs, kw...) do p
+ transform(GI.x(p), GI.y(p))
+ end
+ end
+end
This page was generated using Literate.jl.
`,7)]))}const y=i(k,[["render",t]]);export{d as __pageData,y as default}; diff --git a/previews/PR239/assets/source_GeometryOpsProjExt_reproject.md.Cm7Q7Ebj.lean.js b/previews/PR239/assets/source_GeometryOpsProjExt_reproject.md.Cm7Q7Ebj.lean.js new file mode 100644 index 000000000..ffaa90130 --- /dev/null +++ b/previews/PR239/assets/source_GeometryOpsProjExt_reproject.md.Cm7Q7Ebj.lean.js @@ -0,0 +1,44 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const d=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOpsProjExt/reproject.md","filePath":"source/GeometryOpsProjExt/reproject.md","lastUpdated":null}'),k={name:"source/GeometryOpsProjExt/reproject.md"};function t(p,s,l,e,r,E){return h(),a("div",null,s[0]||(s[0]=[n(`import GeometryOps: GI, GeoInterface, reproject, apply, transform, _is3d, _True, _False
+import Proj
+
+function reproject(geom;
+ source_crs=nothing, target_crs=nothing, transform=nothing, kw...
+)
+ if isnothing(transform)
+ if isnothing(source_crs)
+ source_crs = if GI.trait(geom) isa Nothing && geom isa AbstractArray
+ GeoInterface.crs(first(geom))
+ else
+ GeoInterface.crs(geom)
+ end
+ end
If its still nothing, error
isnothing(source_crs) && throw(ArgumentError("geom has no crs attached. Pass a \`source_crs\` keyword"))
Otherwise reproject
reproject(geom, source_crs, target_crs; kw...)
+ else
+ reproject(geom, transform; kw...)
+ end
+end
+function reproject(geom, source_crs, target_crs;
+ time=Inf,
+ always_xy=true,
+ transform=nothing,
+ kw...
+)
+ transform = if isnothing(transform)
+ s = source_crs isa Proj.CRS ? source_crs : convert(String, source_crs)
+ t = target_crs isa Proj.CRS ? target_crs : convert(String, target_crs)
+ Proj.Transformation(s, t; always_xy)
+ else
+ transform
+ end
+ reproject(geom, transform; time, target_crs, kw...)
+end
+function reproject(geom, transform::Proj.Transformation; time=Inf, target_crs=nothing, kw...)
+ if _is3d(geom)
+ return apply(GI.PointTrait(), geom; crs=target_crs, kw...) do p
+ transform(GI.x(p), GI.y(p), GI.z(p))
+ end
+ else
+ return apply(GI.PointTrait(), geom; crs=target_crs, kw...) do p
+ transform(GI.x(p), GI.y(p))
+ end
+ end
+end
This page was generated using Literate.jl.
`,7)]))}const y=i(k,[["render",t]]);export{d as __pageData,y as default}; diff --git a/previews/PR239/assets/source_GeometryOpsProjExt_segmentize.md.DOG0-rtA.js b/previews/PR239/assets/source_GeometryOpsProjExt_segmentize.md.DOG0-rtA.js new file mode 100644 index 000000000..92742bf54 --- /dev/null +++ b/previews/PR239/assets/source_GeometryOpsProjExt_segmentize.md.DOG0-rtA.js @@ -0,0 +1,31 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOpsProjExt/segmentize.md","filePath":"source/GeometryOpsProjExt/segmentize.md","lastUpdated":null}'),t={name:"source/GeometryOpsProjExt/segmentize.md"};function k(e,s,l,p,d,r){return h(),a("div",null,s[0]||(s[0]=[n(`This holds the segmentize
geodesic functionality.
import GeometryOps: GeodesicSegments, _segmentize, _fill_linear_kernel!
+import Proj
+
+function GeometryOps.GeodesicSegments(; max_distance, equatorial_radius::Real=6378137, flattening::Real=1/298.257223563, geodesic::Proj.geod_geodesic = Proj.geod_geodesic(equatorial_radius, flattening))
+ return GeometryOps.GeodesicSegments{Proj.geod_geodesic}(geodesic, max_distance)
+end
This is the same method as in transformations/segmentize.jl
, but it constructs a Proj geodesic line every time. Maybe this should be better...
function _segmentize(method::Geodesic, geom, ::Union{GI.LineStringTrait, GI.LinearRingTrait}; max_distance)
+ proj_geodesic = Proj.geod_geodesic(method.semimajor_axis #= same thing as equatorial radius =#, 1/method.inv_flattening)
+ 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; max_distance, proj_geodesic)
+ x1, y1 = x2, y2
+ end
+ return rebuild(geom, new_coords)
+end
+
+function GeometryOps._fill_linear_kernel!(method::Geodesic, new_coords::Vector, x1, y1, x2, y2; max_distance, proj_geodesic)
+ geod_line = Proj.geod_inverseline(proj_geodesic, y1, x1, y2, x2)
This is the distance in meters computed between the two points. It's s13
because geod_inverseline
sets point 3 to the second input point.
distance = geod_line.s13
+ if distance > max_distance
+ n_segments = ceil(Int, distance / max_distance)
+ for i in 1:(n_segments - 1)
+ y, x, _ = Proj.geod_position(geod_line, i / n_segments * distance)
+ push!(new_coords, (x, y))
+ end
+ end
End the line with the original coordinate, to avoid any multiplication errors.
push!(new_coords, (x2, y2))
+ return nothing
+end
This page was generated using Literate.jl.
`,10)]))}const y=i(t,[["render",k]]);export{g as __pageData,y as default}; diff --git a/previews/PR239/assets/source_GeometryOpsProjExt_segmentize.md.DOG0-rtA.lean.js b/previews/PR239/assets/source_GeometryOpsProjExt_segmentize.md.DOG0-rtA.lean.js new file mode 100644 index 000000000..92742bf54 --- /dev/null +++ b/previews/PR239/assets/source_GeometryOpsProjExt_segmentize.md.DOG0-rtA.lean.js @@ -0,0 +1,31 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/GeometryOpsProjExt/segmentize.md","filePath":"source/GeometryOpsProjExt/segmentize.md","lastUpdated":null}'),t={name:"source/GeometryOpsProjExt/segmentize.md"};function k(e,s,l,p,d,r){return h(),a("div",null,s[0]||(s[0]=[n(`This holds the segmentize
geodesic functionality.
import GeometryOps: GeodesicSegments, _segmentize, _fill_linear_kernel!
+import Proj
+
+function GeometryOps.GeodesicSegments(; max_distance, equatorial_radius::Real=6378137, flattening::Real=1/298.257223563, geodesic::Proj.geod_geodesic = Proj.geod_geodesic(equatorial_radius, flattening))
+ return GeometryOps.GeodesicSegments{Proj.geod_geodesic}(geodesic, max_distance)
+end
This is the same method as in transformations/segmentize.jl
, but it constructs a Proj geodesic line every time. Maybe this should be better...
function _segmentize(method::Geodesic, geom, ::Union{GI.LineStringTrait, GI.LinearRingTrait}; max_distance)
+ proj_geodesic = Proj.geod_geodesic(method.semimajor_axis #= same thing as equatorial radius =#, 1/method.inv_flattening)
+ 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; max_distance, proj_geodesic)
+ x1, y1 = x2, y2
+ end
+ return rebuild(geom, new_coords)
+end
+
+function GeometryOps._fill_linear_kernel!(method::Geodesic, new_coords::Vector, x1, y1, x2, y2; max_distance, proj_geodesic)
+ geod_line = Proj.geod_inverseline(proj_geodesic, y1, x1, y2, x2)
This is the distance in meters computed between the two points. It's s13
because geod_inverseline
sets point 3 to the second input point.
distance = geod_line.s13
+ if distance > max_distance
+ n_segments = ceil(Int, distance / max_distance)
+ for i in 1:(n_segments - 1)
+ y, x, _ = Proj.geod_position(geod_line, i / n_segments * distance)
+ push!(new_coords, (x, y))
+ end
+ end
End the line with the original coordinate, to avoid any multiplication errors.
push!(new_coords, (x2, y2))
+ return nothing
+end
This page was generated using Literate.jl.
`,10)]))}const y=i(t,[["render",k]]);export{g as __pageData,y as default}; diff --git a/previews/PR239/assets/source_methods_angles.md.B9lhXIGg.js b/previews/PR239/assets/source_methods_angles.md.B9lhXIGg.js new file mode 100644 index 000000000..5c17fafc8 --- /dev/null +++ b/previews/PR239/assets/source_methods_angles.md.B9lhXIGg.js @@ -0,0 +1,124 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const l="/GeometryOps.jl/previews/PR239/assets/pkevosp.Dig-DWOQ.png",y=JSON.parse('{"title":"Angles","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/angles.md","filePath":"source/methods/angles.md","lastUpdated":null}'),k={name:"source/methods/angles.md"};function t(p,s,e,r,E,g){return h(),a("div",null,s[0]||(s[0]=[n(`export angles
Angles are the angles formed by a given geometries line segments, if it has line segments.
To provide an example, consider this rectangle:
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()))
This is clearly a rectangle, with angles of 90 degrees.
GO.angles(rect) # [90, 90, 90, 90]
4-element Vector{Float64}:
+ 90.0
+ 90.0
+ 90.0
+ 90.0
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!
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 polygon 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
Points and single line segments have no angles
_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 polygon, 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
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
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
Loop through the curve and find each of the angels
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 the last point of geometry should be the same as the first, calculate closing angle
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 needed, calculate first angle corresponding to the first point
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
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.
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
This page was generated using Literate.jl.
`,27)]))}const F=i(k,[["render",t]]);export{y as __pageData,F as default}; diff --git a/previews/PR239/assets/source_methods_angles.md.B9lhXIGg.lean.js b/previews/PR239/assets/source_methods_angles.md.B9lhXIGg.lean.js new file mode 100644 index 000000000..5c17fafc8 --- /dev/null +++ b/previews/PR239/assets/source_methods_angles.md.B9lhXIGg.lean.js @@ -0,0 +1,124 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const l="/GeometryOps.jl/previews/PR239/assets/pkevosp.Dig-DWOQ.png",y=JSON.parse('{"title":"Angles","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/angles.md","filePath":"source/methods/angles.md","lastUpdated":null}'),k={name:"source/methods/angles.md"};function t(p,s,e,r,E,g){return h(),a("div",null,s[0]||(s[0]=[n(`export angles
Angles are the angles formed by a given geometries line segments, if it has line segments.
To provide an example, consider this rectangle:
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()))
This is clearly a rectangle, with angles of 90 degrees.
GO.angles(rect) # [90, 90, 90, 90]
4-element Vector{Float64}:
+ 90.0
+ 90.0
+ 90.0
+ 90.0
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!
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 polygon 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
Points and single line segments have no angles
_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 polygon, 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
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
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
Loop through the curve and find each of the angels
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 the last point of geometry should be the same as the first, calculate closing angle
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 needed, calculate first angle corresponding to the first point
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
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.
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
This page was generated using Literate.jl.
`,27)]))}const F=i(k,[["render",t]]);export{y as __pageData,F as default}; diff --git a/previews/PR239/assets/source_methods_area.md.42EUHlu6.js b/previews/PR239/assets/source_methods_area.md.42EUHlu6.js new file mode 100644 index 000000000..f8c65c212 --- /dev/null +++ b/previews/PR239/assets/source_methods_area.md.42EUHlu6.js @@ -0,0 +1,87 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/pkevosp.Dig-DWOQ.png",e="/GeometryOps.jl/previews/PR239/assets/dytnfok.CULn5saZ.png",y=JSON.parse('{"title":"Area and signed area","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/area.md","filePath":"source/methods/area.md","lastUpdated":null}'),l={name:"source/methods/area.md"};function p(k,s,r,d,g,E){return h(),a("div",null,s[0]||(s[0]=[n(`export area, signed_area
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:
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()))
This is clearly a rectangle, etc. But now let's look at how the points look:
lines!(
+ collect(GI.getpoint(rect));
+ color = 1:GI.npoint(rect), linewidth = 10.0)
+f
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 positive area.
GO.signed_area(rect) # -1.0
-1.0
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
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 slightly 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)
Points, MultiPoints, Curves, MultiCurves
_area(::Type{T}, ::GI.AbstractGeometryTrait, geom) where T = zero(T)
+
+_signed_area(::Type{T}, ::GI.AbstractGeometryTrait, geom) where T = zero(T)
LibGEOS treats linear rings as zero area. I disagree with that but we should probably maintain compatibility...
_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)
Polygons
_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
Remove hole areas from total
for hole in GI.gethole(poly)
+ area -= abs(_signed_area(T, hole))
+ end
Winding of exterior ring determines sign
return area * sign(s_area)
+end
One term of the shoelace area formula
_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
Integrate the area under the curve
for p2 in GI.getpoint(geom)
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.
if first
+ p1 = pfirst = p2
+ first = false
+ continue
+ end
Accumulate the area into area
area += _area_component(p1, p2)
+ p1 = p2
+ end
Complete the last edge. If the first and last where the same this will be zero
p2 = pfirst
+ area += _area_component(p1, p2)
+ return T(area / 2)
+end
This page was generated using Literate.jl.
`,40)]))}const F=i(l,[["render",p]]);export{y as __pageData,F as default}; diff --git a/previews/PR239/assets/source_methods_area.md.42EUHlu6.lean.js b/previews/PR239/assets/source_methods_area.md.42EUHlu6.lean.js new file mode 100644 index 000000000..f8c65c212 --- /dev/null +++ b/previews/PR239/assets/source_methods_area.md.42EUHlu6.lean.js @@ -0,0 +1,87 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/pkevosp.Dig-DWOQ.png",e="/GeometryOps.jl/previews/PR239/assets/dytnfok.CULn5saZ.png",y=JSON.parse('{"title":"Area and signed area","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/area.md","filePath":"source/methods/area.md","lastUpdated":null}'),l={name:"source/methods/area.md"};function p(k,s,r,d,g,E){return h(),a("div",null,s[0]||(s[0]=[n(`export area, signed_area
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:
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()))
This is clearly a rectangle, etc. But now let's look at how the points look:
lines!(
+ collect(GI.getpoint(rect));
+ color = 1:GI.npoint(rect), linewidth = 10.0)
+f
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 positive area.
GO.signed_area(rect) # -1.0
-1.0
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
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 slightly 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)
Points, MultiPoints, Curves, MultiCurves
_area(::Type{T}, ::GI.AbstractGeometryTrait, geom) where T = zero(T)
+
+_signed_area(::Type{T}, ::GI.AbstractGeometryTrait, geom) where T = zero(T)
LibGEOS treats linear rings as zero area. I disagree with that but we should probably maintain compatibility...
_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)
Polygons
_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
Remove hole areas from total
for hole in GI.gethole(poly)
+ area -= abs(_signed_area(T, hole))
+ end
Winding of exterior ring determines sign
return area * sign(s_area)
+end
One term of the shoelace area formula
_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
Integrate the area under the curve
for p2 in GI.getpoint(geom)
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.
if first
+ p1 = pfirst = p2
+ first = false
+ continue
+ end
Accumulate the area into area
area += _area_component(p1, p2)
+ p1 = p2
+ end
Complete the last edge. If the first and last where the same this will be zero
p2 = pfirst
+ area += _area_component(p1, p2)
+ return T(area / 2)
+end
This page was generated using Literate.jl.
`,40)]))}const F=i(l,[["render",p]]);export{y as __pageData,F as default}; diff --git a/previews/PR239/assets/source_methods_barycentric.md.BiNAbdvJ.js b/previews/PR239/assets/source_methods_barycentric.md.BiNAbdvJ.js new file mode 100644 index 000000000..8fcc33d0e --- /dev/null +++ b/previews/PR239/assets/source_methods_barycentric.md.BiNAbdvJ.js @@ -0,0 +1,415 @@ +import{_ as k,c as n,a5 as t,j as s,a,o as h}from"./chunks/framework.onQNwZ2I.js";const l="/GeometryOps.jl/previews/PR239/assets/cwpogcp.pAYw0Yqf.png",m=JSON.parse('{"title":"Barycentric coordinates","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/barycentric.md","filePath":"source/methods/barycentric.md","lastUpdated":null}'),p={name:"source/methods/barycentric.md"},e={class:"MathJax",jax:"SVG",style:{direction:"ltr",position:"relative"}},E={style:{overflow:"visible","min-height":"1px","min-width":"1px","vertical-align":"-0.566ex"},xmlns:"http://www.w3.org/2000/svg",width:"10.692ex",height:"2.262ex",role:"img",focusable:"false",viewBox:"0 -750 4726 1000","aria-hidden":"true"},r={class:"MathJax",jax:"SVG",style:{direction:"ltr",position:"relative"}},d={style:{overflow:"visible","min-height":"1px","min-width":"1px","vertical-align":"-0.025ex"},xmlns:"http://www.w3.org/2000/svg",width:"1.357ex",height:"1.025ex",role:"img",focusable:"false",viewBox:"0 -442 600 453","aria-hidden":"true"},g={class:"MathJax",jax:"SVG",style:{direction:"ltr",position:"relative"}},y={style:{overflow:"visible","min-height":"1px","min-width":"1px","vertical-align":"-0.025ex"},xmlns:"http://www.w3.org/2000/svg",width:"1.357ex",height:"1.025ex",role:"img",focusable:"false",viewBox:"0 -442 600 453","aria-hidden":"true"},F={class:"MathJax",jax:"SVG",style:{direction:"ltr",position:"relative"}},o={style:{overflow:"visible","min-height":"1px","min-width":"1px","vertical-align":"-0.566ex"},xmlns:"http://www.w3.org/2000/svg",width:"14.876ex",height:"2.262ex",role:"img",focusable:"false",viewBox:"0 -750 6575.4 1000","aria-hidden":"true"};function C(c,i,B,A,D,u){return h(),n("div",null,[i[14]||(i[14]=t(`export barycentric_coordinates, barycentric_coordinates!, barycentric_interpolate
+export MeanValue
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.
`,4)),s("p",null,[i[2]||(i[2]=a("In the case of a triangle, barycentric coordinates are a set of three numbers ")),s("mjx-container",e,[(h(),n("svg",E,i[0]||(i[0]=[t('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.
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(
+ Point2d.(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
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.
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)
This is the GeoInterface-compatible 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.
+"""
+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)
This is the GeoInterface-compatible method.
"""
+ 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)
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.
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)
This method is the one which supports GeoInterface.
"""
+ 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
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.
"""
+ _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
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...
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
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!
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.
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:l_exterior
Increment counters + set variables
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ᵢ
Updates - first the interpolated value,
interpolated_value += values[current_index] * wᵢ
then the accumulators for total weight and current index.
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
This page was generated using Literate.jl.
`,35))])}const b=k(p,[["render",C]]);export{m as __pageData,b as default}; diff --git a/previews/PR239/assets/source_methods_barycentric.md.BiNAbdvJ.lean.js b/previews/PR239/assets/source_methods_barycentric.md.BiNAbdvJ.lean.js new file mode 100644 index 000000000..8fcc33d0e --- /dev/null +++ b/previews/PR239/assets/source_methods_barycentric.md.BiNAbdvJ.lean.js @@ -0,0 +1,415 @@ +import{_ as k,c as n,a5 as t,j as s,a,o as h}from"./chunks/framework.onQNwZ2I.js";const l="/GeometryOps.jl/previews/PR239/assets/cwpogcp.pAYw0Yqf.png",m=JSON.parse('{"title":"Barycentric coordinates","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/barycentric.md","filePath":"source/methods/barycentric.md","lastUpdated":null}'),p={name:"source/methods/barycentric.md"},e={class:"MathJax",jax:"SVG",style:{direction:"ltr",position:"relative"}},E={style:{overflow:"visible","min-height":"1px","min-width":"1px","vertical-align":"-0.566ex"},xmlns:"http://www.w3.org/2000/svg",width:"10.692ex",height:"2.262ex",role:"img",focusable:"false",viewBox:"0 -750 4726 1000","aria-hidden":"true"},r={class:"MathJax",jax:"SVG",style:{direction:"ltr",position:"relative"}},d={style:{overflow:"visible","min-height":"1px","min-width":"1px","vertical-align":"-0.025ex"},xmlns:"http://www.w3.org/2000/svg",width:"1.357ex",height:"1.025ex",role:"img",focusable:"false",viewBox:"0 -442 600 453","aria-hidden":"true"},g={class:"MathJax",jax:"SVG",style:{direction:"ltr",position:"relative"}},y={style:{overflow:"visible","min-height":"1px","min-width":"1px","vertical-align":"-0.025ex"},xmlns:"http://www.w3.org/2000/svg",width:"1.357ex",height:"1.025ex",role:"img",focusable:"false",viewBox:"0 -442 600 453","aria-hidden":"true"},F={class:"MathJax",jax:"SVG",style:{direction:"ltr",position:"relative"}},o={style:{overflow:"visible","min-height":"1px","min-width":"1px","vertical-align":"-0.566ex"},xmlns:"http://www.w3.org/2000/svg",width:"14.876ex",height:"2.262ex",role:"img",focusable:"false",viewBox:"0 -750 6575.4 1000","aria-hidden":"true"};function C(c,i,B,A,D,u){return h(),n("div",null,[i[14]||(i[14]=t(`export barycentric_coordinates, barycentric_coordinates!, barycentric_interpolate
+export MeanValue
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.
`,4)),s("p",null,[i[2]||(i[2]=a("In the case of a triangle, barycentric coordinates are a set of three numbers ")),s("mjx-container",e,[(h(),n("svg",E,i[0]||(i[0]=[t('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.
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(
+ Point2d.(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
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.
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)
This is the GeoInterface-compatible 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.
+"""
+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)
This is the GeoInterface-compatible method.
"""
+ 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)
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.
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)
This method is the one which supports GeoInterface.
"""
+ 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
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.
"""
+ _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
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...
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
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!
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.
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:l_exterior
Increment counters + set variables
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ᵢ
Updates - first the interpolated value,
interpolated_value += values[current_index] * wᵢ
then the accumulators for total weight and current index.
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
This page was generated using Literate.jl.
`,35))])}const b=k(p,[["render",C]]);export{m as __pageData,b as default}; diff --git a/previews/PR239/assets/source_methods_buffer.md.Wvk7coQv.js b/previews/PR239/assets/source_methods_buffer.md.Wvk7coQv.js new file mode 100644 index 000000000..dde6ba167 --- /dev/null +++ b/previews/PR239/assets/source_methods_buffer.md.Wvk7coQv.js @@ -0,0 +1,11 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"Buffer","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/buffer.md","filePath":"source/methods/buffer.md","lastUpdated":null}'),e={name:"source/methods/buffer.md"};function h(k,s,p,l,r,d){return t(),a("div",null,s[0]||(s[0]=[n(`Buffering a geometry means computing the region distance
away from it, and returning that region as the new geometry.
As of now, we only support GEOS
as the backend, meaning that LibGEOS must be loaded.
function buffer(geometry, distance; kwargs...)
+ buffered = buffer(GEOS(; kwargs...), geometry, distance)
+ return tuples(buffered)
+end
Below is an error handler similar to the others we have for e.g. segmentize, which checks if there is a method error for the geos backend.
Add an error hint for buffer
if LibGEOS is not loaded!
function _buffer_error_hinter(io, exc, argtypes, kwargs)
+ if isnothing(Base.get_extension(GeometryOps, :GeometryOpsLibGEOSExt)) && exc.f == buffer && first(argtypes) == GEOS
+ print(io, "\\n\\nThe \`buffer\` method requires the LibGEOS.jl package to be explicitly loaded.\\n")
+ print(io, "You can do this by simply typing ")
+ printstyled(io, "using LibGEOS"; color = :cyan, bold = true)
+ println(io, " in your REPL, \\nor otherwise loading LibGEOS.jl via using or import.")
+ end
+end
This page was generated using Literate.jl.
`,9)]))}const o=i(e,[["render",h]]);export{g as __pageData,o as default}; diff --git a/previews/PR239/assets/source_methods_buffer.md.Wvk7coQv.lean.js b/previews/PR239/assets/source_methods_buffer.md.Wvk7coQv.lean.js new file mode 100644 index 000000000..dde6ba167 --- /dev/null +++ b/previews/PR239/assets/source_methods_buffer.md.Wvk7coQv.lean.js @@ -0,0 +1,11 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"Buffer","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/buffer.md","filePath":"source/methods/buffer.md","lastUpdated":null}'),e={name:"source/methods/buffer.md"};function h(k,s,p,l,r,d){return t(),a("div",null,s[0]||(s[0]=[n(`Buffering a geometry means computing the region distance
away from it, and returning that region as the new geometry.
As of now, we only support GEOS
as the backend, meaning that LibGEOS must be loaded.
function buffer(geometry, distance; kwargs...)
+ buffered = buffer(GEOS(; kwargs...), geometry, distance)
+ return tuples(buffered)
+end
Below is an error handler similar to the others we have for e.g. segmentize, which checks if there is a method error for the geos backend.
Add an error hint for buffer
if LibGEOS is not loaded!
function _buffer_error_hinter(io, exc, argtypes, kwargs)
+ if isnothing(Base.get_extension(GeometryOps, :GeometryOpsLibGEOSExt)) && exc.f == buffer && first(argtypes) == GEOS
+ print(io, "\\n\\nThe \`buffer\` method requires the LibGEOS.jl package to be explicitly loaded.\\n")
+ print(io, "You can do this by simply typing ")
+ printstyled(io, "using LibGEOS"; color = :cyan, bold = true)
+ println(io, " in your REPL, \\nor otherwise loading LibGEOS.jl via using or import.")
+ end
+end
This page was generated using Literate.jl.
`,9)]))}const o=i(e,[["render",h]]);export{g as __pageData,o as default}; diff --git a/previews/PR239/assets/source_methods_centroid.md.CPAUzyVU.js b/previews/PR239/assets/source_methods_centroid.md.CPAUzyVU.js new file mode 100644 index 000000000..32151a8fd --- /dev/null +++ b/previews/PR239/assets/source_methods_centroid.md.CPAUzyVU.js @@ -0,0 +1,93 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/ibdeule.BD0hVfse.png",k="/GeometryOps.jl/previews/PR239/assets/cgnfnmo.DHcwB147.png",o=JSON.parse('{"title":"Centroid","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/centroid.md","filePath":"source/methods/centroid.md","lastUpdated":null}'),l={name:"source/methods/centroid.md"};function p(e,s,r,E,d,g){return h(),a("div",null,s[0]||(s[0]=[n(`export centroid, centroid_and_length, centroid_and_area
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':
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()))
Let's see what the centroid looks like (plotted in red):
cent = GO.centroid(cshape)
+scatter!(GI.x(cent), GI.y(cent), color = :red)
+f
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 available just in case the user also needs the area or length to decrease repeat computation.
"""
+ 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
Initialize starting values
xcentroid = T(0)
+ ycentroid = T(0)
+ length = T(0)
+ point₁ = GI.getpoint(geom, 1)
Loop over line segments of line string
for point₂ in GI.getpoint(geom)
Calculate length of line segment
length_component = sqrt(
+ (GI.x(point₂) - GI.x(point₁))^2 +
+ (GI.y(point₂) - GI.y(point₁))^2
+ )
Accumulate the line segment length into length
length += length_component
Weighted average of line segment centroids
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))
Advance the point buffer by 1 point to move to next line segment
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
Check that the geometry is closed
@assert(
+ GI.getpoint(geom, 1) == GI.getpoint(geom, GI.ngeom(geom)),
+ "centroid_and_area should only be used with closed geometries"
+ )
Initialize starting values
xcentroid = T(0)
+ ycentroid = T(0)
+ area = T(0)
+ point₁ = GI.getpoint(geom, 1)
Loop over line segments of linear ring
for point₂ in GI.getpoint(geom)
+ area_component = GI.x(point₁) * GI.y(point₂) -
+ GI.x(point₂) * GI.y(point₁)
Accumulate the area component into area
area += area_component
Weighted average of centroid components
xcentroid += (GI.x(point₁) + GI.x(point₂)) * area_component
+ ycentroid += (GI.y(point₁) + GI.y(point₂)) * area_component
Advance the point buffer by 1 point
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
Exterior ring's centroid and area
(xcentroid, ycentroid), area = centroid_and_area(GI.getexterior(geom), T)
Weight exterior centroid by area
xcentroid *= area
+ ycentroid *= area
Loop over any holes within the polygon
for hole in GI.gethole(geom)
Hole polygon's centroid and area
(xinterior, yinterior), interior_area = centroid_and_area(hole, T)
Accumulate the area component into area
area -= interior_area
Weighted average of centroid components
xcentroid -= xinterior * interior_area
+ ycentroid -= yinterior * interior_area
+ end
+ xcentroid /= area
+ ycentroid /= area
+ return (xcentroid, ycentroid), area
+end
The 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
This page was generated using Literate.jl.
`,57)]))}const c=i(l,[["render",p]]);export{o as __pageData,c as default}; diff --git a/previews/PR239/assets/source_methods_centroid.md.CPAUzyVU.lean.js b/previews/PR239/assets/source_methods_centroid.md.CPAUzyVU.lean.js new file mode 100644 index 000000000..32151a8fd --- /dev/null +++ b/previews/PR239/assets/source_methods_centroid.md.CPAUzyVU.lean.js @@ -0,0 +1,93 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/ibdeule.BD0hVfse.png",k="/GeometryOps.jl/previews/PR239/assets/cgnfnmo.DHcwB147.png",o=JSON.parse('{"title":"Centroid","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/centroid.md","filePath":"source/methods/centroid.md","lastUpdated":null}'),l={name:"source/methods/centroid.md"};function p(e,s,r,E,d,g){return h(),a("div",null,s[0]||(s[0]=[n(`export centroid, centroid_and_length, centroid_and_area
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':
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()))
Let's see what the centroid looks like (plotted in red):
cent = GO.centroid(cshape)
+scatter!(GI.x(cent), GI.y(cent), color = :red)
+f
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 available just in case the user also needs the area or length to decrease repeat computation.
"""
+ 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
Initialize starting values
xcentroid = T(0)
+ ycentroid = T(0)
+ length = T(0)
+ point₁ = GI.getpoint(geom, 1)
Loop over line segments of line string
for point₂ in GI.getpoint(geom)
Calculate length of line segment
length_component = sqrt(
+ (GI.x(point₂) - GI.x(point₁))^2 +
+ (GI.y(point₂) - GI.y(point₁))^2
+ )
Accumulate the line segment length into length
length += length_component
Weighted average of line segment centroids
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))
Advance the point buffer by 1 point to move to next line segment
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
Check that the geometry is closed
@assert(
+ GI.getpoint(geom, 1) == GI.getpoint(geom, GI.ngeom(geom)),
+ "centroid_and_area should only be used with closed geometries"
+ )
Initialize starting values
xcentroid = T(0)
+ ycentroid = T(0)
+ area = T(0)
+ point₁ = GI.getpoint(geom, 1)
Loop over line segments of linear ring
for point₂ in GI.getpoint(geom)
+ area_component = GI.x(point₁) * GI.y(point₂) -
+ GI.x(point₂) * GI.y(point₁)
Accumulate the area component into area
area += area_component
Weighted average of centroid components
xcentroid += (GI.x(point₁) + GI.x(point₂)) * area_component
+ ycentroid += (GI.y(point₁) + GI.y(point₂)) * area_component
Advance the point buffer by 1 point
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
Exterior ring's centroid and area
(xcentroid, ycentroid), area = centroid_and_area(GI.getexterior(geom), T)
Weight exterior centroid by area
xcentroid *= area
+ ycentroid *= area
Loop over any holes within the polygon
for hole in GI.gethole(geom)
Hole polygon's centroid and area
(xinterior, yinterior), interior_area = centroid_and_area(hole, T)
Accumulate the area component into area
area -= interior_area
Weighted average of centroid components
xcentroid -= xinterior * interior_area
+ ycentroid -= yinterior * interior_area
+ end
+ xcentroid /= area
+ ycentroid /= area
+ return (xcentroid, ycentroid), area
+end
The 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
This page was generated using Literate.jl.
`,57)]))}const c=i(l,[["render",p]]);export{o as __pageData,c as default}; diff --git a/previews/PR239/assets/source_methods_clipping_clipping_processor.md.Ci1IQvdb.js b/previews/PR239/assets/source_methods_clipping_clipping_processor.md.Ci1IQvdb.js new file mode 100644 index 000000000..3ec0b2217 --- /dev/null +++ b/previews/PR239/assets/source_methods_clipping_clipping_processor.md.Ci1IQvdb.js @@ -0,0 +1,508 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"Polygon clipping helpers","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/clipping/clipping_processor.md","filePath":"source/methods/clipping/clipping_processor.md","lastUpdated":null}'),t={name:"source/methods/clipping/clipping_processor.md"};function p(l,s,k,e,E,r){return h(),a("div",null,s[0]||(s[0]=[n(`This file contains the shared helper functions for the polygon clipping functionalities.
This enum defines which side of an edge a point is on
@enum PointEdgeSide left=1 right=2 unknown=3
Constants assigned for readability
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)
Checks equality of two PolyNodes by backing point value, fractional value, and intersection status
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)
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.
function _build_ab_list(::Type{T}, poly_a, poly_b, delay_cross_f::F1, delay_bounce_f::F2; exact) where {T, F1, F2}
Make a list for nodes of each polygon
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)
Flag crossings
_classify_crossing!(T, a_list, b_list; exact)
Flag the entry and exits
_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)
Set node indices and filter a_idx_list to just crossing points
_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)
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 indices 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 indices 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.
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
Loop through points of poly_a
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
Add the first point of the edge to the list of points in a_list
new_point = PolyNode{T}(;point = a_pt1)
+ a_count += 1
+ push!(a_list, new_point)
Find intersections with edges of poly_b
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
Determine if edges intersect and how they intersect
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
Determine if a1 or b1 should be added to a_list
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 lines are collinear and overlapping, a second intersection exists
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
Add intersection points determined above
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
Order intersection points by placement along edge using fracs value
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
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 neighbor value in a_list is now updated.
function _build_b_list(::Type{T}, a_idx_list, a_list, n_b_intrs, poly_b) where T
Sort intersection points by insertion order in b_list
sort!(a_idx_list, by = x-> a_list[x].neighbor + a_list[x].fracs[2])
Initialize needed values and lists
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
Loop over points in poly_b and add each point and intersection point
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
intersection point is vertex of b
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)
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.
function _classify_crossing!(::Type{T}, a_list, b_list; exact) where T
+ napts = length(a_list)
+ nbpts = length(b_list)
start centered on last point
a_prev = a_list[end - 1]
+ curr_pt = a_list[end]
+ i = napts
keep track of unmatched bouncing chains
start_chain_edge, start_chain_idx = unknown, 0
+ unmatched_end_chain_edge, unmatched_end_chain_idx = unknown, 0
+ same_winding = true
loop over list points
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]
determine if any segments are on top of one another
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)
determine which side of a segments the p points are on
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)
no sides overlap
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
end of overlapping chain
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
update end of chain with endpoint and crossing / bouncing tags
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,
+ )
update start of chain with endpoint and crossing / bouncing tags
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
start of overlapping chain
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 we started in the middle of overlapping chain, close chain
if unmatched_end_chain_edge != unknown
+ crossing = unmatched_end_chain_edge != start_chain_edge
update end of chain with endpoint and crossing / bouncing tags
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,
+ )
update start of chain with endpoint and crossing / bouncing tags
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
Check if PolyNode is a vertex of original polygon
_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
Determine side orientation of b_prev and b_next
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
Determines if Q lies to the left or right of the line formed by P1-P2-P3
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
Check if a PolyNode is an intersection point
_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)
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.
function _flag_ent_exit!(::Type{T}, ::GI.LinearRingTrait, poly, pt_list, delay_cross_f, delay_bounce_f; exact) where T
+ npts = length(pt_list)
Find starting index if there is one
next_idx, status = _pt_off_edge_status(T, pt_list, poly, npts; exact)
+ isnothing(next_idx) && return
+ start_idx = next_idx - 1
Loop over points and mark entry and exit status
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
update start of chain point
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)
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.
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)
Loop over points and mark entry and exit status
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}
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: 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)
Keep track of number of processed intersection points
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
Find first unprocessed intersecting point in subject polygon
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]
Set first point in polygon
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)
changed curr_not_intr to curr_not_same_ent_flag
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))."
Traverse polygon either forwards or backwards
idx += step
+ idx = (idx > curr_npoints) ? mod(idx, curr_npoints) : idx
+ idx = (idx == 0) ? curr_npoints : idx
Get current node and add to pt_list
curr = curr_list[idx]
+ push!(pt_list, curr.point)
+ if (curr.crossing || curr.endpoint != not_endpoint)
Keep track of processed intersection points
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
Switch to next list and next point
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 type of polygons that will be made TODO: Increase type options
_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)
For polygons with no crossing intersection points, either one polygon is inside of another, or they are separate 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.
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)
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
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[]
Remove set of holes from all polygons
for i in 1:n_polys
+ n_new_per_poly = 0
+ for curr_hole in Iterators.map(tuples, hole_iterator) # loop through all holes
+ curr_hole = _linearring(curr_hole)
loop through all pieces of original polygon (new pieces added to end of list)
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 completely 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
replace original
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
polygon is completely within hole
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
Remove all polygon that were marked for removal
deleteat!(return_polys, remove_poly_idx)
+ return
+end
_combine_holes!(::Type{T}, new_hole, curr_poly, return_polys)
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 original 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.
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))
Combine any existing holes in curr_poly with 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)
If the holes intersect, combine them into a bigger hole
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
Remove redundant holes
deleteat!(curr_poly.geom, remove_hole_idx)
+ empty!(remove_hole_idx)
If new polygon pieces created, make sure remaining holes are in the correct piece
@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 and reset removing index buffer
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
check if p2 is approximately on the edge formed by p1 and p3 - remove if so
if Predicates.orient(p1, p2, p3; exact = _False()) == 0
+ remove_idx[i - 1] = true
+ end
+ end
+ p1, p2 = p2, p3
+ end
Check if the first point (which is repeated as the last point) is needed
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
Remove unneeded collinear points
deleteat!(ring.geom, remove_idx)
Check if enough points are left to form a polygon
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
This page was generated using Literate.jl.
`,169)]))}const y=i(t,[["render",p]]);export{g as __pageData,y as default}; diff --git a/previews/PR239/assets/source_methods_clipping_clipping_processor.md.Ci1IQvdb.lean.js b/previews/PR239/assets/source_methods_clipping_clipping_processor.md.Ci1IQvdb.lean.js new file mode 100644 index 000000000..3ec0b2217 --- /dev/null +++ b/previews/PR239/assets/source_methods_clipping_clipping_processor.md.Ci1IQvdb.lean.js @@ -0,0 +1,508 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"Polygon clipping helpers","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/clipping/clipping_processor.md","filePath":"source/methods/clipping/clipping_processor.md","lastUpdated":null}'),t={name:"source/methods/clipping/clipping_processor.md"};function p(l,s,k,e,E,r){return h(),a("div",null,s[0]||(s[0]=[n(`This file contains the shared helper functions for the polygon clipping functionalities.
This enum defines which side of an edge a point is on
@enum PointEdgeSide left=1 right=2 unknown=3
Constants assigned for readability
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)
Checks equality of two PolyNodes by backing point value, fractional value, and intersection status
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)
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.
function _build_ab_list(::Type{T}, poly_a, poly_b, delay_cross_f::F1, delay_bounce_f::F2; exact) where {T, F1, F2}
Make a list for nodes of each polygon
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)
Flag crossings
_classify_crossing!(T, a_list, b_list; exact)
Flag the entry and exits
_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)
Set node indices and filter a_idx_list to just crossing points
_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)
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 indices 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 indices 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.
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
Loop through points of poly_a
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
Add the first point of the edge to the list of points in a_list
new_point = PolyNode{T}(;point = a_pt1)
+ a_count += 1
+ push!(a_list, new_point)
Find intersections with edges of poly_b
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
Determine if edges intersect and how they intersect
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
Determine if a1 or b1 should be added to a_list
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 lines are collinear and overlapping, a second intersection exists
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
Add intersection points determined above
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
Order intersection points by placement along edge using fracs value
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
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 neighbor value in a_list is now updated.
function _build_b_list(::Type{T}, a_idx_list, a_list, n_b_intrs, poly_b) where T
Sort intersection points by insertion order in b_list
sort!(a_idx_list, by = x-> a_list[x].neighbor + a_list[x].fracs[2])
Initialize needed values and lists
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
Loop over points in poly_b and add each point and intersection point
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
intersection point is vertex of b
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)
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.
function _classify_crossing!(::Type{T}, a_list, b_list; exact) where T
+ napts = length(a_list)
+ nbpts = length(b_list)
start centered on last point
a_prev = a_list[end - 1]
+ curr_pt = a_list[end]
+ i = napts
keep track of unmatched bouncing chains
start_chain_edge, start_chain_idx = unknown, 0
+ unmatched_end_chain_edge, unmatched_end_chain_idx = unknown, 0
+ same_winding = true
loop over list points
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]
determine if any segments are on top of one another
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)
determine which side of a segments the p points are on
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)
no sides overlap
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
end of overlapping chain
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
update end of chain with endpoint and crossing / bouncing tags
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,
+ )
update start of chain with endpoint and crossing / bouncing tags
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
start of overlapping chain
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 we started in the middle of overlapping chain, close chain
if unmatched_end_chain_edge != unknown
+ crossing = unmatched_end_chain_edge != start_chain_edge
update end of chain with endpoint and crossing / bouncing tags
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,
+ )
update start of chain with endpoint and crossing / bouncing tags
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
Check if PolyNode is a vertex of original polygon
_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
Determine side orientation of b_prev and b_next
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
Determines if Q lies to the left or right of the line formed by P1-P2-P3
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
Check if a PolyNode is an intersection point
_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)
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.
function _flag_ent_exit!(::Type{T}, ::GI.LinearRingTrait, poly, pt_list, delay_cross_f, delay_bounce_f; exact) where T
+ npts = length(pt_list)
Find starting index if there is one
next_idx, status = _pt_off_edge_status(T, pt_list, poly, npts; exact)
+ isnothing(next_idx) && return
+ start_idx = next_idx - 1
Loop over points and mark entry and exit status
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
update start of chain point
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)
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.
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)
Loop over points and mark entry and exit status
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}
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: 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)
Keep track of number of processed intersection points
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
Find first unprocessed intersecting point in subject polygon
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]
Set first point in polygon
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)
changed curr_not_intr to curr_not_same_ent_flag
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))."
Traverse polygon either forwards or backwards
idx += step
+ idx = (idx > curr_npoints) ? mod(idx, curr_npoints) : idx
+ idx = (idx == 0) ? curr_npoints : idx
Get current node and add to pt_list
curr = curr_list[idx]
+ push!(pt_list, curr.point)
+ if (curr.crossing || curr.endpoint != not_endpoint)
Keep track of processed intersection points
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
Switch to next list and next point
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 type of polygons that will be made TODO: Increase type options
_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)
For polygons with no crossing intersection points, either one polygon is inside of another, or they are separate 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.
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)
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
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[]
Remove set of holes from all polygons
for i in 1:n_polys
+ n_new_per_poly = 0
+ for curr_hole in Iterators.map(tuples, hole_iterator) # loop through all holes
+ curr_hole = _linearring(curr_hole)
loop through all pieces of original polygon (new pieces added to end of list)
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 completely 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
replace original
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
polygon is completely within hole
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
Remove all polygon that were marked for removal
deleteat!(return_polys, remove_poly_idx)
+ return
+end
_combine_holes!(::Type{T}, new_hole, curr_poly, return_polys)
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 original 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.
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))
Combine any existing holes in curr_poly with 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)
If the holes intersect, combine them into a bigger hole
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
Remove redundant holes
deleteat!(curr_poly.geom, remove_hole_idx)
+ empty!(remove_hole_idx)
If new polygon pieces created, make sure remaining holes are in the correct piece
@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 and reset removing index buffer
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
check if p2 is approximately on the edge formed by p1 and p3 - remove if so
if Predicates.orient(p1, p2, p3; exact = _False()) == 0
+ remove_idx[i - 1] = true
+ end
+ end
+ p1, p2 = p2, p3
+ end
Check if the first point (which is repeated as the last point) is needed
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
Remove unneeded collinear points
deleteat!(ring.geom, remove_idx)
Check if enough points are left to form a polygon
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
This page was generated using Literate.jl.
`,169)]))}const y=i(t,[["render",p]]);export{g as __pageData,y as default}; diff --git a/previews/PR239/assets/source_methods_clipping_coverage.md.C89Y2dMt.js b/previews/PR239/assets/source_methods_clipping_coverage.md.C89Y2dMt.js new file mode 100644 index 000000000..bb2eab7c0 --- /dev/null +++ b/previews/PR239/assets/source_methods_clipping_coverage.md.C89Y2dMt.js @@ -0,0 +1,223 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const l="/GeometryOps.jl/previews/PR239/assets/kguctvv.Cb0_DiYE.png",y=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/clipping/coverage.md","filePath":"source/methods/clipping/coverage.md","lastUpdated":null}'),k={name:"source/methods/clipping/coverage.md"};function p(t,s,e,E,r,d){return h(),a("div",null,s[0]||(s[0]=[n(`export coverage
Coverage is the amount of geometry area within a bounding box defined by the minimum and maximum x and y-coordinates of that bounding box, or an Extent containing that information.
To provide an example, consider this rectangle:
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
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.
GO.coverage(rect, xmin, xmax, ymin, ymax)
1.0
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
const _COVERAGE_TARGETS = TraitTarget{Union{GI.PolygonTrait,GI.AbstractCurveTrait,GI.MultiPointTrait,GI.PointTrait}}()
Wall types for coverage
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
Points, MultiPoints, Curves, MultiCurves
_coverage(::Type{T}, ::GI.AbstractGeometryTrait, geom, xmin, xmax, ymin, ymax; kwargs...) where T = zero(T)
Polygons
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
Remove hole coverage from total
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
Loop over edges of polygon
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)
Must rotate clockwise for the algorithm to work
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)
Determine if edge points are within the cell
p1_in_cell = _point_in_cell(p1, xmin, xmax, ymin, ymax)
+ p2_in_cell = _point_in_cell(p2, xmin, xmax, ymin, ymax)
If entire line segment is inside cell
if p1_in_cell && p2_in_cell
+ cov_area += _area_component(p1, p2)
+ p1 = p2
+ continue
+ end
If edge passes outside of rectangle, determine which edge segments are added
inter1, inter2 = _line_intersect_cell(T, p1, p2, xmin, xmax, ymin, ymax)
Endpoints of segment within the cell and wall they are on if known
(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
Add edge component
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-point at beginning, close polygon with last out point
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 grid cell is within polygon then the area is grid cell area
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
Returns true of the given point is within the bounding box determined by x and y values
_point_in_cell(p, xmin, xmax, ymin, ymax) = xmin <= GI.x(p) <= xmax && ymin <= GI.y(p) <= ymax
Returns true if b is between a and c, exclusive of the maximum value, else false.
_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
Calculate and check potential intersections
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
Finds point of cell edge between p1 and p2 given which walls they are on
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
From the point to the corner of wall 1
connect_area += _partial_edge_out_area(p1, xmin, xmax, ymin, ymax, wall1)
Any intermediate walls (full length)
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
From the corner of wall 2 to the point
connect_area += _partial_edge_in_area(p2, xmin, xmax, ymin, ymax, wall2)
+ end
+ return connect_area
+end
True if (x1, y1) is clockwise from (x2, y2) on the same wall
_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
This page was generated using Literate.jl.
`,58)]))}const F=i(k,[["render",p]]);export{y as __pageData,F as default}; diff --git a/previews/PR239/assets/source_methods_clipping_coverage.md.C89Y2dMt.lean.js b/previews/PR239/assets/source_methods_clipping_coverage.md.C89Y2dMt.lean.js new file mode 100644 index 000000000..bb2eab7c0 --- /dev/null +++ b/previews/PR239/assets/source_methods_clipping_coverage.md.C89Y2dMt.lean.js @@ -0,0 +1,223 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const l="/GeometryOps.jl/previews/PR239/assets/kguctvv.Cb0_DiYE.png",y=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/clipping/coverage.md","filePath":"source/methods/clipping/coverage.md","lastUpdated":null}'),k={name:"source/methods/clipping/coverage.md"};function p(t,s,e,E,r,d){return h(),a("div",null,s[0]||(s[0]=[n(`export coverage
Coverage is the amount of geometry area within a bounding box defined by the minimum and maximum x and y-coordinates of that bounding box, or an Extent containing that information.
To provide an example, consider this rectangle:
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
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.
GO.coverage(rect, xmin, xmax, ymin, ymax)
1.0
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
const _COVERAGE_TARGETS = TraitTarget{Union{GI.PolygonTrait,GI.AbstractCurveTrait,GI.MultiPointTrait,GI.PointTrait}}()
Wall types for coverage
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
Points, MultiPoints, Curves, MultiCurves
_coverage(::Type{T}, ::GI.AbstractGeometryTrait, geom, xmin, xmax, ymin, ymax; kwargs...) where T = zero(T)
Polygons
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
Remove hole coverage from total
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
Loop over edges of polygon
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)
Must rotate clockwise for the algorithm to work
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)
Determine if edge points are within the cell
p1_in_cell = _point_in_cell(p1, xmin, xmax, ymin, ymax)
+ p2_in_cell = _point_in_cell(p2, xmin, xmax, ymin, ymax)
If entire line segment is inside cell
if p1_in_cell && p2_in_cell
+ cov_area += _area_component(p1, p2)
+ p1 = p2
+ continue
+ end
If edge passes outside of rectangle, determine which edge segments are added
inter1, inter2 = _line_intersect_cell(T, p1, p2, xmin, xmax, ymin, ymax)
Endpoints of segment within the cell and wall they are on if known
(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
Add edge component
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-point at beginning, close polygon with last out point
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 grid cell is within polygon then the area is grid cell area
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
Returns true of the given point is within the bounding box determined by x and y values
_point_in_cell(p, xmin, xmax, ymin, ymax) = xmin <= GI.x(p) <= xmax && ymin <= GI.y(p) <= ymax
Returns true if b is between a and c, exclusive of the maximum value, else false.
_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
Calculate and check potential intersections
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
Finds point of cell edge between p1 and p2 given which walls they are on
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
From the point to the corner of wall 1
connect_area += _partial_edge_out_area(p1, xmin, xmax, ymin, ymax, wall1)
Any intermediate walls (full length)
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
From the corner of wall 2 to the point
connect_area += _partial_edge_in_area(p2, xmin, xmax, ymin, ymax, wall2)
+ end
+ return connect_area
+end
True if (x1, y1) is clockwise from (x2, y2) on the same wall
_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
This page was generated using Literate.jl.
`,58)]))}const F=i(k,[["render",p]]);export{y as __pageData,F as default}; diff --git a/previews/PR239/assets/source_methods_clipping_cut.md.D8NjrBmq.js b/previews/PR239/assets/source_methods_clipping_cut.md.D8NjrBmq.js new file mode 100644 index 000000000..7d4d09979 --- /dev/null +++ b/previews/PR239/assets/source_methods_clipping_cut.md.D8NjrBmq.js @@ -0,0 +1,87 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/oafsxip.-VpeHhXX.png",y=JSON.parse('{"title":"Polygon cutting","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/clipping/cut.md","filePath":"source/methods/clipping/cut.md","lastUpdated":null}'),l={name:"source/methods/clipping/cut.md"};function p(k,s,e,r,E,d){return h(),a("div",null,s[0]||(s[0]=[n(`export cut
The cut function cuts a polygon through a line segment. This is inspired by functions such as Matlab's cutpolygon
function.
To provide an example, consider the following polygon and line:
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
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.
"""
+ 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)
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]]]
+\`\`\`
+"""
+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 an impossible number of intersection points, return original polygon
if n_intr_pts < 2 || isodd(n_intr_pts)
+ return [tuples(poly)]
+ end
Cut polygon by line
cut_coords = _cut(T, ext_poly, line, poly_list, intr_list, n_intr_pts; exact)
Close coords and create polygons
for c in cut_coords
+ push!(c, c[1])
+ end
+ cut_polys = [GI.Polygon([c]) for c in cut_coords]
Add original polygon holes back in
remove_idx = falses(length(cut_polys))
+ _add_holes_to_polys!(T, cut_polys, GI.gethole(poly), remove_idx; exact)
+ return cut_polys
+end
Many types aren't implemented
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 and categorize the intersection points
sort!(intr_list, by = x -> geom_list[x].fracs[2])
+ _flag_ent_exit!(GI.LineTrait(), line, geom_list; exact)
Add first point to output list
return_coords = [[geom_list[1].point]]
+ cross_backs = [(T(Inf),T(Inf))]
+ poly_idx = 1
+ n_polys = 1
Walk around original polygon to find split polygons
for (pt_idx, curr) in enumerate(geom_list)
+ if pt_idx > 1
+ push!(return_coords[poly_idx], curr.point)
+ end
+ if curr.inter
Find cross back point for current polygon
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
Check if current point is a cross back 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
This page was generated using Literate.jl.
`,34)]))}const o=i(l,[["render",p]]);export{y as __pageData,o as default}; diff --git a/previews/PR239/assets/source_methods_clipping_cut.md.D8NjrBmq.lean.js b/previews/PR239/assets/source_methods_clipping_cut.md.D8NjrBmq.lean.js new file mode 100644 index 000000000..7d4d09979 --- /dev/null +++ b/previews/PR239/assets/source_methods_clipping_cut.md.D8NjrBmq.lean.js @@ -0,0 +1,87 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/oafsxip.-VpeHhXX.png",y=JSON.parse('{"title":"Polygon cutting","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/clipping/cut.md","filePath":"source/methods/clipping/cut.md","lastUpdated":null}'),l={name:"source/methods/clipping/cut.md"};function p(k,s,e,r,E,d){return h(),a("div",null,s[0]||(s[0]=[n(`export cut
The cut function cuts a polygon through a line segment. This is inspired by functions such as Matlab's cutpolygon
function.
To provide an example, consider the following polygon and line:
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
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.
"""
+ 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)
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]]]
+\`\`\`
+"""
+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 an impossible number of intersection points, return original polygon
if n_intr_pts < 2 || isodd(n_intr_pts)
+ return [tuples(poly)]
+ end
Cut polygon by line
cut_coords = _cut(T, ext_poly, line, poly_list, intr_list, n_intr_pts; exact)
Close coords and create polygons
for c in cut_coords
+ push!(c, c[1])
+ end
+ cut_polys = [GI.Polygon([c]) for c in cut_coords]
Add original polygon holes back in
remove_idx = falses(length(cut_polys))
+ _add_holes_to_polys!(T, cut_polys, GI.gethole(poly), remove_idx; exact)
+ return cut_polys
+end
Many types aren't implemented
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 and categorize the intersection points
sort!(intr_list, by = x -> geom_list[x].fracs[2])
+ _flag_ent_exit!(GI.LineTrait(), line, geom_list; exact)
Add first point to output list
return_coords = [[geom_list[1].point]]
+ cross_backs = [(T(Inf),T(Inf))]
+ poly_idx = 1
+ n_polys = 1
Walk around original polygon to find split polygons
for (pt_idx, curr) in enumerate(geom_list)
+ if pt_idx > 1
+ push!(return_coords[poly_idx], curr.point)
+ end
+ if curr.inter
Find cross back point for current polygon
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
Check if current point is a cross back 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
This page was generated using Literate.jl.
`,34)]))}const o=i(l,[["render",p]]);export{y as __pageData,o as default}; diff --git a/previews/PR239/assets/source_methods_clipping_difference.md.KRJn9sdE.js b/previews/PR239/assets/source_methods_clipping_difference.md.KRJn9sdE.js new file mode 100644 index 000000000..68fd9cebd --- /dev/null +++ b/previews/PR239/assets/source_methods_clipping_difference.md.KRJn9sdE.js @@ -0,0 +1,166 @@ +import{_ as i,c as a,a5 as n,o as l}from"./chunks/framework.onQNwZ2I.js";const y=JSON.parse('{"title":"Difference Polygon Clipping","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/clipping/difference.md","filePath":"source/methods/clipping/difference.md","lastUpdated":null}'),p={name:"source/methods/clipping/difference.md"};function t(h,s,e,k,r,d){return l(),a("div",null,s[0]||(s[0]=[n(`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)
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]]]
+\`\`\`
+"""
+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
Get the exterior of the polygons
ext_a = GI.getexterior(poly_a)
+ ext_b = GI.getexterior(poly_b)
Find the difference of the exterior of the polygons
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 no crossing points, determine if either poly is inside of the other
if isempty(polys)
+ a_in_b, b_in_a = _find_non_cross_orientation(a_list, b_list, ext_a, ext_b; exact)
add case for if they polygons are the same (all intersection points!) add a find_first check to find first non-inter poly!
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 the original polygons had holes, take that into account.
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 unneeded collinear points on same edge
_remove_collinear_points!(polys, remove_idx, poly_a, poly_b)
+ return polys
+end
#= 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 (default 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))
+ #= Removing intersections of \`multipoly_a\`\` with pieces of \`multipoly_b\`\` - as
+ pieces of \`multipolygon_a\`\` are removed, continue to take difference with new shape
+ \`polys\` =#
+ polys = if i == 1
+ difference(multipoly_a, poly_b; target, fix_multipoly)
+ else
+ difference(GI.MultiPolygon(polys), poly_b; target, fix_multipoly)
+ end
+ #= One multipoly_a has been completely covered (and thus removed) there is no need to
+ continue taking the difference =#
+ isempty(polys) && break
+ end
+ return polys
+end
Many type and target combos aren't implemented
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
This page was generated using Literate.jl.
`,22)]))}const E=i(p,[["render",t]]);export{y as __pageData,E as default}; diff --git a/previews/PR239/assets/source_methods_clipping_difference.md.KRJn9sdE.lean.js b/previews/PR239/assets/source_methods_clipping_difference.md.KRJn9sdE.lean.js new file mode 100644 index 000000000..68fd9cebd --- /dev/null +++ b/previews/PR239/assets/source_methods_clipping_difference.md.KRJn9sdE.lean.js @@ -0,0 +1,166 @@ +import{_ as i,c as a,a5 as n,o as l}from"./chunks/framework.onQNwZ2I.js";const y=JSON.parse('{"title":"Difference Polygon Clipping","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/clipping/difference.md","filePath":"source/methods/clipping/difference.md","lastUpdated":null}'),p={name:"source/methods/clipping/difference.md"};function t(h,s,e,k,r,d){return l(),a("div",null,s[0]||(s[0]=[n(`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)
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]]]
+\`\`\`
+"""
+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
Get the exterior of the polygons
ext_a = GI.getexterior(poly_a)
+ ext_b = GI.getexterior(poly_b)
Find the difference of the exterior of the polygons
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 no crossing points, determine if either poly is inside of the other
if isempty(polys)
+ a_in_b, b_in_a = _find_non_cross_orientation(a_list, b_list, ext_a, ext_b; exact)
add case for if they polygons are the same (all intersection points!) add a find_first check to find first non-inter poly!
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 the original polygons had holes, take that into account.
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 unneeded collinear points on same edge
_remove_collinear_points!(polys, remove_idx, poly_a, poly_b)
+ return polys
+end
#= 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 (default 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))
+ #= Removing intersections of \`multipoly_a\`\` with pieces of \`multipoly_b\`\` - as
+ pieces of \`multipolygon_a\`\` are removed, continue to take difference with new shape
+ \`polys\` =#
+ polys = if i == 1
+ difference(multipoly_a, poly_b; target, fix_multipoly)
+ else
+ difference(GI.MultiPolygon(polys), poly_b; target, fix_multipoly)
+ end
+ #= One multipoly_a has been completely covered (and thus removed) there is no need to
+ continue taking the difference =#
+ isempty(polys) && break
+ end
+ return polys
+end
Many type and target combos aren't implemented
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
This page was generated using Literate.jl.
`,22)]))}const E=i(p,[["render",t]]);export{y as __pageData,E as default}; diff --git a/previews/PR239/assets/source_methods_clipping_intersection.md.bcPofDGt.js b/previews/PR239/assets/source_methods_clipping_intersection.md.bcPofDGt.js new file mode 100644 index 000000000..7d67e5afa --- /dev/null +++ b/previews/PR239/assets/source_methods_clipping_intersection.md.bcPofDGt.js @@ -0,0 +1,383 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"Geometry Intersection","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/clipping/intersection.md","filePath":"source/methods/clipping/intersection.md","lastUpdated":null}'),h={name:"source/methods/clipping/intersection.md"};function l(p,s,k,e,r,E){return t(),a("div",null,s[0]||(s[0]=[n(`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\` (collinear 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)
output
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
Curve-Curve Intersections with target Point
_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}
First we get the exteriors of 'poly_a' and 'poly_b'
ext_a = GI.getexterior(poly_a)
+ ext_b = GI.getexterior(poly_b)
Then we find the intersection of the exteriors
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 the original polygons had holes, take that into account.
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 unneeded collinear points on same edge
_remove_collinear_points!(polys, remove_idx, poly_a, poly_b)
+ return polys
+end
#= 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
+polygon 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
Many type and target combos aren't implemented
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, [T::Type])
+
+Return a list of intersection tuple points between two geometries. If no intersection points
+exist, returns an empty list.
+
+# 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_points(line1, line2)
output
1-element Vector{Tuple{Float64, Float64}}:
+ (125.58375366067548, -14.83572303404496)
+"""
+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, including line
+segments, line strings, linear rings, polygons, and multipolygons. =#
+function _intersection_points(::Type{T}, ::GI.AbstractTrait, a, ::GI.AbstractTrait, b; exact = _True()) where T
Initialize an empty list of points
result = Tuple{T, T}[]
Check if the geometries extents even overlap
Extents.intersects(GI.extent(a), GI.extent(b)) || return result
Create a list of edges from the two input geometries
edges_a, edges_b = map(sort! ∘ to_edges, (a, b))
Loop over pairs of edges and add any unique intersection points to results
for a_edge in edges_a, b_edge in edges_b
+ line_orient, intr1, intr2 = _intersection_point(T, a_edge, b_edge; exact)
+ line_orient == line_out && continue # no intersection points
+ pt1, _ = intr1
+ push!(result, pt1) # if not line_out, there is at least one intersection point
+ if line_orient == line_over # if line_over, there are two intersection points
+ pt2, _ = intr2
+ push!(result, pt2)
+ end
+ end
+ #= TODO: We might be able to just add unique points with checks on the α and β values
+ returned from \`_intersection_point\`, but this would be different for curves vs polygons
+ vs multipolygons depending on if the shape is closed. This then wouldn't allow using the
+ \`to_edges\` functionality. =#
+ unique!(sort!(result))
+ 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
Default answer for no intersection
line_orient = line_out
+ intr1 = ((zero(T), zero(T)), (zero(T), zero(T)))
+ intr2 = intr1
+ no_intr_result = (line_orient, intr1, intr2)
Seperate out line segment points
(a1x, a1y), (a2x, a2y) = _tuple_point(a1, T), _tuple_point(a2, T)
+ (b1x, b1y), (b2x, b2y) = _tuple_point(b1, T), _tuple_point(b2, T)
Check if envelopes of lines intersect
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
Check orientation of two line segments with respect to one another
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
Determine intersection type and intersection point(s)
if a1_orient == a2_orient == b1_orient == b2_orient == 0
Intersection is collinear if all endpoints lie on the same line
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
Intersection is a hinge if the intersection point is an endpoint
line_orient = line_hinge
+ intr1 = _find_hinge_intersection(T, a1, a2, b1, b2, a1_orient, a2_orient, b1_orient)
+ else
Intersection is a cross if there is only one non-endpoint intersection point
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
Define default return for no intersection points
line_orient, intr1, intr2 = no_intr_result
Determine collinear line overlaps
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)
Determine line distances
a_dist, b_dist = distance(a1, a2, T), distance(b1, b2, T)
Set collinear intersection points if they exist
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 calculated 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
First line runs from a to a + Δa
(a1x, a1y), (a2x, a2y) = _tuple_point(a1, T), _tuple_point(a2, T)
+ Δax, Δay = a2x - a1x, a2y - a1y
Second line runs from b to b + Δb
(b1x, b1y), (b2x, b2y) = _tuple_point(b1, T), _tuple_point(b2, T)
+ Δbx, Δby = b2x - b1x, b2y - b1y
Differences between starting points
Δbax = b1x - a1x
+ Δbay = b1y - a1y
+ a_cross_b = Δax * Δby - Δay * Δbx
Determine α value where 0 < α < 1 and β value where 0 < β < 1
α = _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
+ inaccuracies, α and β calculations may yield 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 certainly 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)
Check if point is within segment envelopes and adjust to endpoint if not
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
Find endpoint of either segment that is closest to the opposite segment
function _nearest_endpoint(::Type{T}, a1, a2, b1, b2) where T
Create lines from segments and calculate segment length
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)
Determine distance from a1 to segment b
min_pt, min_dist = a1, distance(a1, b_line, T)
+ α, β = eps(T), _clamped_frac(distance(min_pt, b1, T), b_dist, eps(T))
Determine distance from a2 to segment b
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
Determine distance from b1 to segment a
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
Determine distance from b2 to segment a
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 point with smallest distance
return _tuple_point(min_pt, T), α, β
+end
Return value of x/y clamped between ϵ and 1 - ϵ
_clamped_frac(x::T, y::T, ϵ = zero(T)) where T = clamp(x / y, ϵ, one(T) - ϵ)
This page was generated using Literate.jl.
`,80)]))}const y=i(h,[["render",l]]);export{g as __pageData,y as default}; diff --git a/previews/PR239/assets/source_methods_clipping_intersection.md.bcPofDGt.lean.js b/previews/PR239/assets/source_methods_clipping_intersection.md.bcPofDGt.lean.js new file mode 100644 index 000000000..7d67e5afa --- /dev/null +++ b/previews/PR239/assets/source_methods_clipping_intersection.md.bcPofDGt.lean.js @@ -0,0 +1,383 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"Geometry Intersection","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/clipping/intersection.md","filePath":"source/methods/clipping/intersection.md","lastUpdated":null}'),h={name:"source/methods/clipping/intersection.md"};function l(p,s,k,e,r,E){return t(),a("div",null,s[0]||(s[0]=[n(`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\` (collinear 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)
output
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
Curve-Curve Intersections with target Point
_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}
First we get the exteriors of 'poly_a' and 'poly_b'
ext_a = GI.getexterior(poly_a)
+ ext_b = GI.getexterior(poly_b)
Then we find the intersection of the exteriors
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 the original polygons had holes, take that into account.
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 unneeded collinear points on same edge
_remove_collinear_points!(polys, remove_idx, poly_a, poly_b)
+ return polys
+end
#= 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
+polygon 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
Many type and target combos aren't implemented
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, [T::Type])
+
+Return a list of intersection tuple points between two geometries. If no intersection points
+exist, returns an empty list.
+
+# 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_points(line1, line2)
output
1-element Vector{Tuple{Float64, Float64}}:
+ (125.58375366067548, -14.83572303404496)
+"""
+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, including line
+segments, line strings, linear rings, polygons, and multipolygons. =#
+function _intersection_points(::Type{T}, ::GI.AbstractTrait, a, ::GI.AbstractTrait, b; exact = _True()) where T
Initialize an empty list of points
result = Tuple{T, T}[]
Check if the geometries extents even overlap
Extents.intersects(GI.extent(a), GI.extent(b)) || return result
Create a list of edges from the two input geometries
edges_a, edges_b = map(sort! ∘ to_edges, (a, b))
Loop over pairs of edges and add any unique intersection points to results
for a_edge in edges_a, b_edge in edges_b
+ line_orient, intr1, intr2 = _intersection_point(T, a_edge, b_edge; exact)
+ line_orient == line_out && continue # no intersection points
+ pt1, _ = intr1
+ push!(result, pt1) # if not line_out, there is at least one intersection point
+ if line_orient == line_over # if line_over, there are two intersection points
+ pt2, _ = intr2
+ push!(result, pt2)
+ end
+ end
+ #= TODO: We might be able to just add unique points with checks on the α and β values
+ returned from \`_intersection_point\`, but this would be different for curves vs polygons
+ vs multipolygons depending on if the shape is closed. This then wouldn't allow using the
+ \`to_edges\` functionality. =#
+ unique!(sort!(result))
+ 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
Default answer for no intersection
line_orient = line_out
+ intr1 = ((zero(T), zero(T)), (zero(T), zero(T)))
+ intr2 = intr1
+ no_intr_result = (line_orient, intr1, intr2)
Seperate out line segment points
(a1x, a1y), (a2x, a2y) = _tuple_point(a1, T), _tuple_point(a2, T)
+ (b1x, b1y), (b2x, b2y) = _tuple_point(b1, T), _tuple_point(b2, T)
Check if envelopes of lines intersect
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
Check orientation of two line segments with respect to one another
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
Determine intersection type and intersection point(s)
if a1_orient == a2_orient == b1_orient == b2_orient == 0
Intersection is collinear if all endpoints lie on the same line
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
Intersection is a hinge if the intersection point is an endpoint
line_orient = line_hinge
+ intr1 = _find_hinge_intersection(T, a1, a2, b1, b2, a1_orient, a2_orient, b1_orient)
+ else
Intersection is a cross if there is only one non-endpoint intersection point
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
Define default return for no intersection points
line_orient, intr1, intr2 = no_intr_result
Determine collinear line overlaps
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)
Determine line distances
a_dist, b_dist = distance(a1, a2, T), distance(b1, b2, T)
Set collinear intersection points if they exist
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 calculated 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
First line runs from a to a + Δa
(a1x, a1y), (a2x, a2y) = _tuple_point(a1, T), _tuple_point(a2, T)
+ Δax, Δay = a2x - a1x, a2y - a1y
Second line runs from b to b + Δb
(b1x, b1y), (b2x, b2y) = _tuple_point(b1, T), _tuple_point(b2, T)
+ Δbx, Δby = b2x - b1x, b2y - b1y
Differences between starting points
Δbax = b1x - a1x
+ Δbay = b1y - a1y
+ a_cross_b = Δax * Δby - Δay * Δbx
Determine α value where 0 < α < 1 and β value where 0 < β < 1
α = _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
+ inaccuracies, α and β calculations may yield 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 certainly 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)
Check if point is within segment envelopes and adjust to endpoint if not
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
Find endpoint of either segment that is closest to the opposite segment
function _nearest_endpoint(::Type{T}, a1, a2, b1, b2) where T
Create lines from segments and calculate segment length
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)
Determine distance from a1 to segment b
min_pt, min_dist = a1, distance(a1, b_line, T)
+ α, β = eps(T), _clamped_frac(distance(min_pt, b1, T), b_dist, eps(T))
Determine distance from a2 to segment b
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
Determine distance from b1 to segment a
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
Determine distance from b2 to segment a
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 point with smallest distance
return _tuple_point(min_pt, T), α, β
+end
Return value of x/y clamped between ϵ and 1 - ϵ
_clamped_frac(x::T, y::T, ϵ = zero(T)) where T = clamp(x / y, ϵ, one(T) - ϵ)
This page was generated using Literate.jl.
`,80)]))}const y=i(h,[["render",l]]);export{g as __pageData,y as default}; diff --git a/previews/PR239/assets/source_methods_clipping_predicates.md.BJITr_Uq.js b/previews/PR239/assets/source_methods_clipping_predicates.md.BJITr_Uq.js new file mode 100644 index 000000000..3e7fbf706 --- /dev/null +++ b/previews/PR239/assets/source_methods_clipping_predicates.md.BJITr_Uq.js @@ -0,0 +1,44 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const c=JSON.parse('{"title":"If we want to inject adaptivity, we would do something like:","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/clipping/predicates.md","filePath":"source/methods/clipping/predicates.md","lastUpdated":null}'),h={name:"source/methods/clipping/predicates.md"};function e(p,s,l,k,r,d){return t(),a("div",null,s[0]||(s[0]=[n(`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)
If 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))
If 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))
Exact cross product calculation using ExactPredicates
.
@genpredicate function _cross_exact(a :: 2, b :: 2)
+ group!(a...)
+ group!(b...)
+ ext(a, b)
+ end
If 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
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.
`,13)]))}const g=i(h,[["render",e]]);export{c as __pageData,g as default}; diff --git a/previews/PR239/assets/source_methods_clipping_predicates.md.BJITr_Uq.lean.js b/previews/PR239/assets/source_methods_clipping_predicates.md.BJITr_Uq.lean.js new file mode 100644 index 000000000..3e7fbf706 --- /dev/null +++ b/previews/PR239/assets/source_methods_clipping_predicates.md.BJITr_Uq.lean.js @@ -0,0 +1,44 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const c=JSON.parse('{"title":"If we want to inject adaptivity, we would do something like:","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/clipping/predicates.md","filePath":"source/methods/clipping/predicates.md","lastUpdated":null}'),h={name:"source/methods/clipping/predicates.md"};function e(p,s,l,k,r,d){return t(),a("div",null,s[0]||(s[0]=[n(`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)
If 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))
If 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))
Exact cross product calculation using ExactPredicates
.
@genpredicate function _cross_exact(a :: 2, b :: 2)
+ group!(a...)
+ group!(b...)
+ ext(a, b)
+ end
If 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
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.
`,13)]))}const g=i(h,[["render",e]]);export{c as __pageData,g as default}; diff --git a/previews/PR239/assets/source_methods_clipping_union.md.DlTyWPn1.js b/previews/PR239/assets/source_methods_clipping_union.md.DlTyWPn1.js new file mode 100644 index 000000000..ecc318c2a --- /dev/null +++ b/previews/PR239/assets/source_methods_clipping_union.md.DlTyWPn1.js @@ -0,0 +1,251 @@ +import{_ as i,c as a,a5 as n,o as l}from"./chunks/framework.onQNwZ2I.js";const y=JSON.parse('{"title":"Union Polygon Clipping","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/clipping/union.md","filePath":"source/methods/clipping/union.md","lastUpdated":null}'),h={name:"source/methods/clipping/union.md"};function p(t,s,k,e,r,E){return l(),a("div",null,s[0]||(s[0]=[n(`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)
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]]]
+\`\`\`
+"""
+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
First, I get the exteriors of the two polygons
ext_a = GI.getexterior(poly_a)
+ ext_b = GI.getexterior(poly_b)
Then, I get the union of the exteriors
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)
Check if one polygon totally within other and if so, return the larger polygon
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([_linearring(tuples(ext_b))]))
+ elseif b_in_a
+ push!(polys, GI.Polygon([_linearring(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
the first element is the exterior, the rest are holes
@views append!(polys[1].geom, (GI.getexterior(p) for p in polys[2:end]))
+ keepat!(polys, 1)
+ end
Add in holes
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 unneeded collinear points on same edge
_remove_collinear_points!(polys, [false], poly_a, poly_b)
+ return polys
+end
#= 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
Loop over all holes in both original polygons
for (i, ih) in enumerate(Iterators.flatten((GI.gethole(poly_a), GI.gethole(poly_b))))
+ ih = _linearring(ih)
+ 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 completely 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)
If polygons intersect and form a new polygon, swap out polygon
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
If they don't intersect, poly_b is now a part of the union as its own polygon
push!(polys, tuples(poly_b, T))
+ end
+ end
+ return polys
+end
+
+#= Multipolygon with polygon union is equivalent to taking the union of the polygon 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
Many type and target combos aren't implemented
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
This page was generated using Literate.jl.
`,28)]))}const d=i(h,[["render",p]]);export{y as __pageData,d as default}; diff --git a/previews/PR239/assets/source_methods_clipping_union.md.DlTyWPn1.lean.js b/previews/PR239/assets/source_methods_clipping_union.md.DlTyWPn1.lean.js new file mode 100644 index 000000000..ecc318c2a --- /dev/null +++ b/previews/PR239/assets/source_methods_clipping_union.md.DlTyWPn1.lean.js @@ -0,0 +1,251 @@ +import{_ as i,c as a,a5 as n,o as l}from"./chunks/framework.onQNwZ2I.js";const y=JSON.parse('{"title":"Union Polygon Clipping","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/clipping/union.md","filePath":"source/methods/clipping/union.md","lastUpdated":null}'),h={name:"source/methods/clipping/union.md"};function p(t,s,k,e,r,E){return l(),a("div",null,s[0]||(s[0]=[n(`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)
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]]]
+\`\`\`
+"""
+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
First, I get the exteriors of the two polygons
ext_a = GI.getexterior(poly_a)
+ ext_b = GI.getexterior(poly_b)
Then, I get the union of the exteriors
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)
Check if one polygon totally within other and if so, return the larger polygon
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([_linearring(tuples(ext_b))]))
+ elseif b_in_a
+ push!(polys, GI.Polygon([_linearring(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
the first element is the exterior, the rest are holes
@views append!(polys[1].geom, (GI.getexterior(p) for p in polys[2:end]))
+ keepat!(polys, 1)
+ end
Add in holes
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 unneeded collinear points on same edge
_remove_collinear_points!(polys, [false], poly_a, poly_b)
+ return polys
+end
#= 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
Loop over all holes in both original polygons
for (i, ih) in enumerate(Iterators.flatten((GI.gethole(poly_a), GI.gethole(poly_b))))
+ ih = _linearring(ih)
+ 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 completely 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)
If polygons intersect and form a new polygon, swap out polygon
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
If they don't intersect, poly_b is now a part of the union as its own polygon
push!(polys, tuples(poly_b, T))
+ end
+ end
+ return polys
+end
+
+#= Multipolygon with polygon union is equivalent to taking the union of the polygon 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
Many type and target combos aren't implemented
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
This page was generated using Literate.jl.
`,28)]))}const d=i(h,[["render",p]]);export{y as __pageData,d as default}; diff --git a/previews/PR239/assets/source_methods_convex_hull.md.DkVMD0Lv.js b/previews/PR239/assets/source_methods_convex_hull.md.DkVMD0Lv.js new file mode 100644 index 000000000..10d5d100a --- /dev/null +++ b/previews/PR239/assets/source_methods_convex_hull.md.DkVMD0Lv.js @@ -0,0 +1,57 @@ +import{_ as i,c as a,a5 as n,o as l}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/kbbskga.BT_oJ_KS.png",h="/GeometryOps.jl/previews/PR239/assets/filtvpm.mCtKcWOr.png",e="/GeometryOps.jl/previews/PR239/assets/cvdrlht.HTM0ungm.png",c=JSON.parse('{"title":"Convex hull","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/convex_hull.md","filePath":"source/methods/convex_hull.md","lastUpdated":null}'),p={name:"source/methods/convex_hull.md"};function k(r,s,o,d,E,g){return l(),a("div",null,s[0]||(s[0]=[n(`The convex hull of a set of points is the smallest convex polygon that contains all the points.
GeometryOps.jl provides a number of methods for computing the convex hull of a set of points, usually linked to other Julia packages.
For now, we expose one algorithm, MonotoneChainMethod, which uses the DelaunayTriangulation.jl package. The GEOS()
interface also supports convex hulls.
Future work could include other algorithms, such as Quickhull.jl, or similar, via package extensions.
import GeometryOps as GO, GeoInterface as GI
+using CairoMakie # to plot
+
+points = randn(GO.Point2f, 100)
+f, a, p = plot(points; label = "Points")
+hull_poly = GO.convex_hull(points)
+lines!(a, hull_poly; label = "Convex hull", color = Makie.wong_colors()[2])
+axislegend(a)
+f
import GeometryOps as GO, GeoInterface as GI
+using CairoMakie # to plot
+using NaturalEarth # for data
+
+all_adm0 = naturalearth("admin_0_countries", 110)
+usa = all_adm0.geometry[findfirst(==("USA"), all_adm0.ADM0_A3)]
+f, a, p = lines(usa)
+lines!(a, GO.convex_hull(usa); color = Makie.wong_colors()[2])
+f
The winding order of the monotone chain method is counterclockwise, while the winding order of the GEOS method is clockwise.
GeometryOps' convexity detection says that the GEOS hull is convex, while the monotone chain method hull is not. However, they are both going over the same points (we checked), it's just that the winding order is different.
In reality, both sets are convex, but we need to fix the GeometryOps convexity detector (isconcave
)!
We may also decide at a later date to change the returned winding order of the polygon, but most algorithms are robust to that, and you can always fix
it...
import GeoInterface as GI, GeometryOps as GO, LibGEOS as LG
+using CairoMakie # to plot
+
+points = rand(Point2{Float64}, 100)
+go_hull = GO.convex_hull(GO.MonotoneChainMethod(), points)
+lg_hull = GO.convex_hull(GO.GEOS(), points)
+
+fig = Figure()
+a1, p1 = lines(fig[1, 1], go_hull; color = 1:GI.npoint(go_hull), axis = (; title = "MonotoneChainMethod()"))
+a2, p2 = lines(fig[2, 1], lg_hull; color = 1:GI.npoint(lg_hull), axis = (; title = "GEOS()"))
+cb = Colorbar(fig[1:2, 2], p1; label = "Vertex number")
+fig
"""
+ convex_hull([method], geometries)
+
+Compute the convex hull of the points in \`geometries\`.
+Returns a \`GI.Polygon\` representing the convex hull.
+
+Note that the polygon returned is wound counterclockwise
+as in the Simple Features standard by default. If you
+choose GEOS, the winding order will be inverted.
+
+!!! warning
+ This interface only computes the 2-dimensional convex hull!
+
+ For higher dimensional hulls, use the relevant package (Qhull.jl, Quickhull.jl, or similar).
+"""
+function convex_hull end
+
+"""
+ MonotoneChainMethod()
+
+This is an algorithm for the \`convex_hull\` function.
+
+Uses [\`DelaunayTriangulation.jl\`](https://github.com/JuliaGeometry/DelaunayTriangulation.jl) to compute the convex hull.
+This is a pure Julia algorithm which provides an optimal Delaunay triangulation.
+
+See also \`convex_hull\`
+"""
+struct MonotoneChainMethod end
GrahamScanMethod, etc. can be implemented in GO as well, if someone wants to. If we add an extension on Quickhull.jl, then that would be another algorithm.
convex_hull(geometries) = convex_hull(MonotoneChainMethod(), geometries)
TODO: have this respect the CRS by pulling it out of geometries
.
function convex_hull(::MonotoneChainMethod, geometries)
Extract all points as tuples. We have to collect and allocate here, because DelaunayTriangulation only accepts vectors of point-like geoms.
Cleanest would be to use the iterable from GO.flatten directly, but that would require us to implement the convex hull algorithm directly.
TODO: create a specialized method that extracts only the information required, GeometryBasics points can be passed through directly.
points = collect(flatten(tuples, GI.PointTrait, geometries))
Compute the convex hull using DelTri (shorthand for DelaunayTriangulation.jl).
hull = DelaunayTriangulation.convex_hull(points)
Convert the result to a GI.Polygon
and return it. View would be more efficient here, but re-allocating is cleaner.
point_vec = DelaunayTriangulation.get_points(hull)[DelaunayTriangulation.get_vertices(hull)]
+ return GI.Polygon([GI.LinearRing(point_vec)])
+end
This page was generated using Literate.jl.
`,35)]))}const u=i(p,[["render",k]]);export{c as __pageData,u as default}; diff --git a/previews/PR239/assets/source_methods_convex_hull.md.DkVMD0Lv.lean.js b/previews/PR239/assets/source_methods_convex_hull.md.DkVMD0Lv.lean.js new file mode 100644 index 000000000..10d5d100a --- /dev/null +++ b/previews/PR239/assets/source_methods_convex_hull.md.DkVMD0Lv.lean.js @@ -0,0 +1,57 @@ +import{_ as i,c as a,a5 as n,o as l}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/kbbskga.BT_oJ_KS.png",h="/GeometryOps.jl/previews/PR239/assets/filtvpm.mCtKcWOr.png",e="/GeometryOps.jl/previews/PR239/assets/cvdrlht.HTM0ungm.png",c=JSON.parse('{"title":"Convex hull","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/convex_hull.md","filePath":"source/methods/convex_hull.md","lastUpdated":null}'),p={name:"source/methods/convex_hull.md"};function k(r,s,o,d,E,g){return l(),a("div",null,s[0]||(s[0]=[n(`The convex hull of a set of points is the smallest convex polygon that contains all the points.
GeometryOps.jl provides a number of methods for computing the convex hull of a set of points, usually linked to other Julia packages.
For now, we expose one algorithm, MonotoneChainMethod, which uses the DelaunayTriangulation.jl package. The GEOS()
interface also supports convex hulls.
Future work could include other algorithms, such as Quickhull.jl, or similar, via package extensions.
import GeometryOps as GO, GeoInterface as GI
+using CairoMakie # to plot
+
+points = randn(GO.Point2f, 100)
+f, a, p = plot(points; label = "Points")
+hull_poly = GO.convex_hull(points)
+lines!(a, hull_poly; label = "Convex hull", color = Makie.wong_colors()[2])
+axislegend(a)
+f
import GeometryOps as GO, GeoInterface as GI
+using CairoMakie # to plot
+using NaturalEarth # for data
+
+all_adm0 = naturalearth("admin_0_countries", 110)
+usa = all_adm0.geometry[findfirst(==("USA"), all_adm0.ADM0_A3)]
+f, a, p = lines(usa)
+lines!(a, GO.convex_hull(usa); color = Makie.wong_colors()[2])
+f
The winding order of the monotone chain method is counterclockwise, while the winding order of the GEOS method is clockwise.
GeometryOps' convexity detection says that the GEOS hull is convex, while the monotone chain method hull is not. However, they are both going over the same points (we checked), it's just that the winding order is different.
In reality, both sets are convex, but we need to fix the GeometryOps convexity detector (isconcave
)!
We may also decide at a later date to change the returned winding order of the polygon, but most algorithms are robust to that, and you can always fix
it...
import GeoInterface as GI, GeometryOps as GO, LibGEOS as LG
+using CairoMakie # to plot
+
+points = rand(Point2{Float64}, 100)
+go_hull = GO.convex_hull(GO.MonotoneChainMethod(), points)
+lg_hull = GO.convex_hull(GO.GEOS(), points)
+
+fig = Figure()
+a1, p1 = lines(fig[1, 1], go_hull; color = 1:GI.npoint(go_hull), axis = (; title = "MonotoneChainMethod()"))
+a2, p2 = lines(fig[2, 1], lg_hull; color = 1:GI.npoint(lg_hull), axis = (; title = "GEOS()"))
+cb = Colorbar(fig[1:2, 2], p1; label = "Vertex number")
+fig
"""
+ convex_hull([method], geometries)
+
+Compute the convex hull of the points in \`geometries\`.
+Returns a \`GI.Polygon\` representing the convex hull.
+
+Note that the polygon returned is wound counterclockwise
+as in the Simple Features standard by default. If you
+choose GEOS, the winding order will be inverted.
+
+!!! warning
+ This interface only computes the 2-dimensional convex hull!
+
+ For higher dimensional hulls, use the relevant package (Qhull.jl, Quickhull.jl, or similar).
+"""
+function convex_hull end
+
+"""
+ MonotoneChainMethod()
+
+This is an algorithm for the \`convex_hull\` function.
+
+Uses [\`DelaunayTriangulation.jl\`](https://github.com/JuliaGeometry/DelaunayTriangulation.jl) to compute the convex hull.
+This is a pure Julia algorithm which provides an optimal Delaunay triangulation.
+
+See also \`convex_hull\`
+"""
+struct MonotoneChainMethod end
GrahamScanMethod, etc. can be implemented in GO as well, if someone wants to. If we add an extension on Quickhull.jl, then that would be another algorithm.
convex_hull(geometries) = convex_hull(MonotoneChainMethod(), geometries)
TODO: have this respect the CRS by pulling it out of geometries
.
function convex_hull(::MonotoneChainMethod, geometries)
Extract all points as tuples. We have to collect and allocate here, because DelaunayTriangulation only accepts vectors of point-like geoms.
Cleanest would be to use the iterable from GO.flatten directly, but that would require us to implement the convex hull algorithm directly.
TODO: create a specialized method that extracts only the information required, GeometryBasics points can be passed through directly.
points = collect(flatten(tuples, GI.PointTrait, geometries))
Compute the convex hull using DelTri (shorthand for DelaunayTriangulation.jl).
hull = DelaunayTriangulation.convex_hull(points)
Convert the result to a GI.Polygon
and return it. View would be more efficient here, but re-allocating is cleaner.
point_vec = DelaunayTriangulation.get_points(hull)[DelaunayTriangulation.get_vertices(hull)]
+ return GI.Polygon([GI.LinearRing(point_vec)])
+end
This page was generated using Literate.jl.
`,35)]))}const u=i(p,[["render",k]]);export{c as __pageData,u as default}; diff --git a/previews/PR239/assets/source_methods_distance.md.DLCaGxsq.js b/previews/PR239/assets/source_methods_distance.md.DLCaGxsq.js new file mode 100644 index 000000000..f6d5521c7 --- /dev/null +++ b/previews/PR239/assets/source_methods_distance.md.DLCaGxsq.js @@ -0,0 +1,181 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/tlayvwm.DiwGEg2f.png",k="/GeometryOps.jl/previews/PR239/assets/cphyzje.DuBHk1fh.png",F=JSON.parse('{"title":"Distance and signed distance","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/distance.md","filePath":"source/methods/distance.md","lastUpdated":null}'),p={name:"source/methods/distance.md"};function l(e,s,d,E,r,g){return h(),a("div",null,s[0]||(s[0]=[n(`export distance, signed_distance
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 positive or 0.
To provide an example, consider this rectangle:
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
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 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)
Consider also a heatmap of signed distances around this object:
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
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.
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
Needed for method ambiguity
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
Point-Point, Point-Line, Point-LineString, Point-LinearRing
_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)
Point-Polygon
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
Needed for method ambiguity
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
Point-Geom (just calls _distance)
function _signed_distance(
+ ::Type{T}, ptrait::GI.PointTrait, point, gtrait::GI.AbstractGeometryTrait, geom
+) where T
+ _distance(T, ptrait, point, gtrait, geom)
+end
Point-Polygon
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
negative if point is inside polygon
end
Returns the Euclidean distance between two points.
Base.@propagate_inbounds _euclid_distance(::Type{T}, p1, p2) where T =
+ sqrt(_squared_euclid_distance(T, p1, p2))
Returns the square of the euclidean distance between two points
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),
+ )
Returns the Euclidean distance between two points given their x and y values.
Base.@propagate_inbounds _euclid_distance(::Type{T}, x1, y1, x2, y2) where T =
+ sqrt(_squared_euclid_distance(T, x1, y1, x2, y2))
Returns the squared Euclidean distance between two points given their x and y values.
Base.@propagate_inbounds _squared_euclid_distance(::Type{T}, x1, y1, x2, y2) where T =
+ T((x2 - x1)^2 + (y2 - y1)^2)
Returns the minimum distance from point p0 to the line defined by endpoints p1 and p2.
_distance_line(::Type{T}, p0, p1, p2) where T =
+ sqrt(_squared_distance_line(T, p0, p1, p2))
Returns the squared minimum distance from point p0 to the line defined by endpoints p1 and 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
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.
function _distance_curve(::Type{T}, point, curve; close_curve = false) where T
see if linear ring has explicitly repeated last point in coordinates
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
find minimum distance
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
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.
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
This page was generated using Literate.jl.
`,54)]))}const o=i(p,[["render",l]]);export{F as __pageData,o as default}; diff --git a/previews/PR239/assets/source_methods_distance.md.DLCaGxsq.lean.js b/previews/PR239/assets/source_methods_distance.md.DLCaGxsq.lean.js new file mode 100644 index 000000000..f6d5521c7 --- /dev/null +++ b/previews/PR239/assets/source_methods_distance.md.DLCaGxsq.lean.js @@ -0,0 +1,181 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/tlayvwm.DiwGEg2f.png",k="/GeometryOps.jl/previews/PR239/assets/cphyzje.DuBHk1fh.png",F=JSON.parse('{"title":"Distance and signed distance","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/distance.md","filePath":"source/methods/distance.md","lastUpdated":null}'),p={name:"source/methods/distance.md"};function l(e,s,d,E,r,g){return h(),a("div",null,s[0]||(s[0]=[n(`export distance, signed_distance
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 positive or 0.
To provide an example, consider this rectangle:
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
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 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)
Consider also a heatmap of signed distances around this object:
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
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.
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
Needed for method ambiguity
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
Point-Point, Point-Line, Point-LineString, Point-LinearRing
_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)
Point-Polygon
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
Needed for method ambiguity
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
Point-Geom (just calls _distance)
function _signed_distance(
+ ::Type{T}, ptrait::GI.PointTrait, point, gtrait::GI.AbstractGeometryTrait, geom
+) where T
+ _distance(T, ptrait, point, gtrait, geom)
+end
Point-Polygon
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
negative if point is inside polygon
end
Returns the Euclidean distance between two points.
Base.@propagate_inbounds _euclid_distance(::Type{T}, p1, p2) where T =
+ sqrt(_squared_euclid_distance(T, p1, p2))
Returns the square of the euclidean distance between two points
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),
+ )
Returns the Euclidean distance between two points given their x and y values.
Base.@propagate_inbounds _euclid_distance(::Type{T}, x1, y1, x2, y2) where T =
+ sqrt(_squared_euclid_distance(T, x1, y1, x2, y2))
Returns the squared Euclidean distance between two points given their x and y values.
Base.@propagate_inbounds _squared_euclid_distance(::Type{T}, x1, y1, x2, y2) where T =
+ T((x2 - x1)^2 + (y2 - y1)^2)
Returns the minimum distance from point p0 to the line defined by endpoints p1 and p2.
_distance_line(::Type{T}, p0, p1, p2) where T =
+ sqrt(_squared_distance_line(T, p0, p1, p2))
Returns the squared minimum distance from point p0 to the line defined by endpoints p1 and 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
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.
function _distance_curve(::Type{T}, point, curve; close_curve = false) where T
see if linear ring has explicitly repeated last point in coordinates
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
find minimum distance
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
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.
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
This page was generated using Literate.jl.
`,54)]))}const o=i(p,[["render",l]]);export{F as __pageData,o as default}; diff --git a/previews/PR239/assets/source_methods_equals.md.BsnJrmWL.js b/previews/PR239/assets/source_methods_equals.md.BsnJrmWL.js new file mode 100644 index 000000000..23fd097d8 --- /dev/null +++ b/previews/PR239/assets/source_methods_equals.md.BsnJrmWL.js @@ -0,0 +1,265 @@ +import{_ as i,c as a,a5 as n,o as l}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/hvyhqaw.CgiryX2p.png",F=JSON.parse('{"title":"Equals","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/equals.md","filePath":"source/methods/equals.md","lastUpdated":null}'),h={name:"source/methods/equals.md"};function p(k,s,e,r,d,g){return l(),a("div",null,s[0]||(s[0]=[n(`export equals
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:
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
We can see that the two lines do not share a common set of points and edges in the plot, so they are not equal:
GO.equals(l1, l2) # returns false
false
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 explicitly written in the coordinates. Additionally, geometries and multi-geometries can be equal if the multi-geometry only includes that single geometry.
"""
+ 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)
output
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)
Check if both curves are closed or not
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
How many points in each curve
n1 -= c1_repeat_point ? 1 : 0
+ n2 -= c2_repeat_point ? 1 : 0
+ n1 == n2 || return false
+ n1 == 0 && return true
Find offset between curves
jstart = nothing
+ p1 = GI.getpoint(c1, 1)
+ for i in 1:n2
+ if equals(p1, GI.getpoint(c2, i))
+ jstart = i
+ break
+ end
+ end
no point matches the first point
isnothing(jstart) && return false
found match for only point
n1 == 1 && return true
if isn't closed and first or last point don't match, not same curve
!closed_type1 && (jstart != 1 && jstart != n1) && return false
Check if curves are going in same direction
i = 2
+ j = jstart + 1
+ j -= j > n2 ? n2 : 0
+ same_direction = equals(GI.getpoint(c1, i), GI.getpoint(c2, j))
if only 2 points, we have already compared both
n1 == 2 && return same_direction
Check all remaining points are the same wrapping around line
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 definition.
+"""
+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 definition, 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 definition, 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)
Check if exterior is equal
_equals_curves(
+ GI.getexterior(geom_a), GI.getexterior(geom_b),
+ true, true, # linear rings are closed by definition
+ ) || return false
Check if number of holes are equal
GI.nhole(geom_a) == GI.nhole(geom_b) || return false
Check if holes are equal
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)
Check if same number of polygons
GI.npolygon(geom_a) == GI.npolygon(geom_b) || return false
Check if each polygon has a matching polygon
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
This page was generated using Literate.jl.
`,47)]))}const o=i(h,[["render",p]]);export{F as __pageData,o as default}; diff --git a/previews/PR239/assets/source_methods_equals.md.BsnJrmWL.lean.js b/previews/PR239/assets/source_methods_equals.md.BsnJrmWL.lean.js new file mode 100644 index 000000000..23fd097d8 --- /dev/null +++ b/previews/PR239/assets/source_methods_equals.md.BsnJrmWL.lean.js @@ -0,0 +1,265 @@ +import{_ as i,c as a,a5 as n,o as l}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/hvyhqaw.CgiryX2p.png",F=JSON.parse('{"title":"Equals","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/equals.md","filePath":"source/methods/equals.md","lastUpdated":null}'),h={name:"source/methods/equals.md"};function p(k,s,e,r,d,g){return l(),a("div",null,s[0]||(s[0]=[n(`export equals
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:
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
We can see that the two lines do not share a common set of points and edges in the plot, so they are not equal:
GO.equals(l1, l2) # returns false
false
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 explicitly written in the coordinates. Additionally, geometries and multi-geometries can be equal if the multi-geometry only includes that single geometry.
"""
+ 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)
output
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)
Check if both curves are closed or not
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
How many points in each curve
n1 -= c1_repeat_point ? 1 : 0
+ n2 -= c2_repeat_point ? 1 : 0
+ n1 == n2 || return false
+ n1 == 0 && return true
Find offset between curves
jstart = nothing
+ p1 = GI.getpoint(c1, 1)
+ for i in 1:n2
+ if equals(p1, GI.getpoint(c2, i))
+ jstart = i
+ break
+ end
+ end
no point matches the first point
isnothing(jstart) && return false
found match for only point
n1 == 1 && return true
if isn't closed and first or last point don't match, not same curve
!closed_type1 && (jstart != 1 && jstart != n1) && return false
Check if curves are going in same direction
i = 2
+ j = jstart + 1
+ j -= j > n2 ? n2 : 0
+ same_direction = equals(GI.getpoint(c1, i), GI.getpoint(c2, j))
if only 2 points, we have already compared both
n1 == 2 && return same_direction
Check all remaining points are the same wrapping around line
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 definition.
+"""
+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 definition, 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 definition, 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)
Check if exterior is equal
_equals_curves(
+ GI.getexterior(geom_a), GI.getexterior(geom_b),
+ true, true, # linear rings are closed by definition
+ ) || return false
Check if number of holes are equal
GI.nhole(geom_a) == GI.nhole(geom_b) || return false
Check if holes are equal
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)
Check if same number of polygons
GI.npolygon(geom_a) == GI.npolygon(geom_b) || return false
Check if each polygon has a matching polygon
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
This page was generated using Literate.jl.
`,47)]))}const o=i(h,[["render",p]]);export{F as __pageData,o as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_contains.md.CTaz5DgK.js b/previews/PR239/assets/source_methods_geom_relations_contains.md.CTaz5DgK.js new file mode 100644 index 000000000..098b4f9b5 --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_contains.md.CTaz5DgK.js @@ -0,0 +1,33 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const e="/GeometryOps.jl/previews/PR239/assets/rzkakxn._0R9BbFk.png",E=JSON.parse('{"title":"Contains","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/contains.md","filePath":"source/methods/geom_relations/contains.md","lastUpdated":null}'),h={name:"source/methods/geom_relations/contains.md"};function l(p,s,k,r,o,d){return t(),a("div",null,s[0]||(s[0]=[n(`export contains
The contains function checks if a given geometry completely contains another geometry, or in other words, that the second geometry is completely 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:
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
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.
GO.contains(l1, l2) # returns true
+GO.contains(l2, l1) # returns false
false
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.
"""
+ 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)
output
true
+\`\`\`
+"""
+contains(g1, g2) = GeometryOps.within(g2, g1)
This page was generated using Literate.jl.
`,18)]))}const c=i(h,[["render",l]]);export{E as __pageData,c as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_contains.md.CTaz5DgK.lean.js b/previews/PR239/assets/source_methods_geom_relations_contains.md.CTaz5DgK.lean.js new file mode 100644 index 000000000..098b4f9b5 --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_contains.md.CTaz5DgK.lean.js @@ -0,0 +1,33 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const e="/GeometryOps.jl/previews/PR239/assets/rzkakxn._0R9BbFk.png",E=JSON.parse('{"title":"Contains","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/contains.md","filePath":"source/methods/geom_relations/contains.md","lastUpdated":null}'),h={name:"source/methods/geom_relations/contains.md"};function l(p,s,k,r,o,d){return t(),a("div",null,s[0]||(s[0]=[n(`export contains
The contains function checks if a given geometry completely contains another geometry, or in other words, that the second geometry is completely 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:
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
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.
GO.contains(l1, l2) # returns true
+GO.contains(l2, l1) # returns false
false
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.
"""
+ 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)
output
true
+\`\`\`
+"""
+contains(g1, g2) = GeometryOps.within(g2, g1)
This page was generated using Literate.jl.
`,18)]))}const c=i(h,[["render",l]]);export{E as __pageData,c as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_coveredby.md.BNo4Y8zx.js b/previews/PR239/assets/source_methods_geom_relations_coveredby.md.BNo4Y8zx.js new file mode 100644 index 000000000..b0e7611c0 --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_coveredby.md.BNo4Y8zx.js @@ -0,0 +1,183 @@ +import{_ as i,c as a,a5 as n,o as e}from"./chunks/framework.onQNwZ2I.js";const l="/GeometryOps.jl/previews/PR239/assets/kbzwnsa.DC3TvBOO.png",o=JSON.parse('{"title":"CoveredBy","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/coveredby.md","filePath":"source/methods/geom_relations/coveredby.md","lastUpdated":null}'),h={name:"source/methods/geom_relations/coveredby.md"};function t(p,s,k,r,E,d){return e(),a("div",null,s[0]||(s[0]=[n(`export coveredby
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:
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
As we can see, 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
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 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 g2
The code for the specific implementations is in the geom_geom_processors file.
const 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)
output
true
+\`\`\`
+"""
+coveredby(g1, g2) = _coveredby(trait(g1), g1, trait(g2), g2)
_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))
Point is coveredby another point if those points are equal
_coveredby(
+ ::GI.PointTrait, g1,
+ ::GI.PointTrait, g2,
+) = equals(g1, g2)
Point is coveredby a line/linestring if it is on a line vertex or an edge
_coveredby(
+ ::GI.PointTrait, g1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _point_curve_process(
+ g1, g2;
+ COVEREDBY_ALLOWS...,
+ closed_curve = false,
+)
Point is coveredby a linearring if it is on a vertex or an edge of ring
_coveredby(
+ ::GI.PointTrait, g1,
+ ::GI.LinearRingTrait, g2,
+) = _point_curve_process(
+ g1, g2;
+ COVEREDBY_ALLOWS...,
+ closed_curve = true,
+)
Point is coveredby a polygon if it is inside polygon, including edges/vertices
_coveredby(
+ ::GI.PointTrait, g1,
+ ::GI.PolygonTrait, g2,
+) = _point_polygon_process(
+ g1, g2;
+ COVEREDBY_ALLOWS...,
+ COVEREDBY_EXACT...,
+)
Points cannot cover any geometry other than points
_coveredby(
+ ::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+ ::GI.PointTrait, g2,
+) = false
#= 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, including 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,
+)
#= 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, including hole edges. =#
+_coveredby(
+ ::GI.LinearRingTrait, g1,
+ ::GI.PolygonTrait, g2,
+) = _line_polygon_process(
+ g1, g2;
+ COVEREDBY_ALLOWS...,
+ COVEREDBY_CURVE_REQUIRES...,
+ COVEREDBY_EXACT...,
+ closed_line = true,
+)
#= 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...,
+)
Polygons cannot covered by any curves
_coveredby(
+ ::GI.PolygonTrait, g1,
+ ::GI.AbstractCurveTrait, g2,
+) = false
#= 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 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
This page was generated using Literate.jl.
`,45)]))}const y=i(h,[["render",t]]);export{o as __pageData,y as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_coveredby.md.BNo4Y8zx.lean.js b/previews/PR239/assets/source_methods_geom_relations_coveredby.md.BNo4Y8zx.lean.js new file mode 100644 index 000000000..b0e7611c0 --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_coveredby.md.BNo4Y8zx.lean.js @@ -0,0 +1,183 @@ +import{_ as i,c as a,a5 as n,o as e}from"./chunks/framework.onQNwZ2I.js";const l="/GeometryOps.jl/previews/PR239/assets/kbzwnsa.DC3TvBOO.png",o=JSON.parse('{"title":"CoveredBy","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/coveredby.md","filePath":"source/methods/geom_relations/coveredby.md","lastUpdated":null}'),h={name:"source/methods/geom_relations/coveredby.md"};function t(p,s,k,r,E,d){return e(),a("div",null,s[0]||(s[0]=[n(`export coveredby
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:
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
As we can see, 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
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 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 g2
The code for the specific implementations is in the geom_geom_processors file.
const 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)
output
true
+\`\`\`
+"""
+coveredby(g1, g2) = _coveredby(trait(g1), g1, trait(g2), g2)
_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))
Point is coveredby another point if those points are equal
_coveredby(
+ ::GI.PointTrait, g1,
+ ::GI.PointTrait, g2,
+) = equals(g1, g2)
Point is coveredby a line/linestring if it is on a line vertex or an edge
_coveredby(
+ ::GI.PointTrait, g1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _point_curve_process(
+ g1, g2;
+ COVEREDBY_ALLOWS...,
+ closed_curve = false,
+)
Point is coveredby a linearring if it is on a vertex or an edge of ring
_coveredby(
+ ::GI.PointTrait, g1,
+ ::GI.LinearRingTrait, g2,
+) = _point_curve_process(
+ g1, g2;
+ COVEREDBY_ALLOWS...,
+ closed_curve = true,
+)
Point is coveredby a polygon if it is inside polygon, including edges/vertices
_coveredby(
+ ::GI.PointTrait, g1,
+ ::GI.PolygonTrait, g2,
+) = _point_polygon_process(
+ g1, g2;
+ COVEREDBY_ALLOWS...,
+ COVEREDBY_EXACT...,
+)
Points cannot cover any geometry other than points
_coveredby(
+ ::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+ ::GI.PointTrait, g2,
+) = false
#= 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, including 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,
+)
#= 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, including hole edges. =#
+_coveredby(
+ ::GI.LinearRingTrait, g1,
+ ::GI.PolygonTrait, g2,
+) = _line_polygon_process(
+ g1, g2;
+ COVEREDBY_ALLOWS...,
+ COVEREDBY_CURVE_REQUIRES...,
+ COVEREDBY_EXACT...,
+ closed_line = true,
+)
#= 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...,
+)
Polygons cannot covered by any curves
_coveredby(
+ ::GI.PolygonTrait, g1,
+ ::GI.AbstractCurveTrait, g2,
+) = false
#= 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 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
This page was generated using Literate.jl.
`,45)]))}const y=i(h,[["render",t]]);export{o as __pageData,y as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_covers.md.Ds8aZwNW.js b/previews/PR239/assets/source_methods_geom_relations_covers.md.Ds8aZwNW.js new file mode 100644 index 000000000..9fdf77d59 --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_covers.md.Ds8aZwNW.js @@ -0,0 +1,33 @@ +import{_ as i,c as a,a5 as e,o as n}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/kbzwnsa.DC3TvBOO.png",g=JSON.parse('{"title":"Covers","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/covers.md","filePath":"source/methods/geom_relations/covers.md","lastUpdated":null}'),p={name:"source/methods/geom_relations/covers.md"};function l(h,s,k,r,o,d){return n(),a("div",null,s[0]||(s[0]=[e(`export covers
The covers function checks if a given geometry completely 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:
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
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.
"""
+ 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)
output
true
+\`\`\`
+"""
+covers(g1, g2)::Bool = GeometryOps.coveredby(g2, g1)
This page was generated using Literate.jl.
`,17)]))}const E=i(p,[["render",l]]);export{g as __pageData,E as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_covers.md.Ds8aZwNW.lean.js b/previews/PR239/assets/source_methods_geom_relations_covers.md.Ds8aZwNW.lean.js new file mode 100644 index 000000000..9fdf77d59 --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_covers.md.Ds8aZwNW.lean.js @@ -0,0 +1,33 @@ +import{_ as i,c as a,a5 as e,o as n}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/kbzwnsa.DC3TvBOO.png",g=JSON.parse('{"title":"Covers","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/covers.md","filePath":"source/methods/geom_relations/covers.md","lastUpdated":null}'),p={name:"source/methods/geom_relations/covers.md"};function l(h,s,k,r,o,d){return n(),a("div",null,s[0]||(s[0]=[e(`export covers
The covers function checks if a given geometry completely 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:
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
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.
"""
+ 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)
output
true
+\`\`\`
+"""
+covers(g1, g2)::Bool = GeometryOps.coveredby(g2, g1)
This page was generated using Literate.jl.
`,17)]))}const E=i(p,[["render",l]]);export{g as __pageData,E as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_crosses.md.-nyWl6CC.js b/previews/PR239/assets/source_methods_geom_relations_crosses.md.-nyWl6CC.js new file mode 100644 index 000000000..41b060fb1 --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_crosses.md.-nyWl6CC.js @@ -0,0 +1,120 @@ +import{_ as i,c as a,a5 as n,o as l}from"./chunks/framework.onQNwZ2I.js";const d=JSON.parse('{"title":"Crossing checks","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/crosses.md","filePath":"source/methods/geom_relations/crosses.md","lastUpdated":null}'),p={name:"source/methods/geom_relations/crosses.md"};function h(t,s,k,e,r,E){return l(),a("div",null,s[0]||(s[0]=[n(`"""
+ 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
TODO: Add working example
\`\`\`
+"""
+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
TODO use better predicate for crossing here
cross = dxc * dy1 - dyc * dx1
+ cross != 0 && return false
Will constprop optimise these away?
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
This page was generated using Literate.jl.
`,10)]))}const F=i(p,[["render",h]]);export{d as __pageData,F as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_crosses.md.-nyWl6CC.lean.js b/previews/PR239/assets/source_methods_geom_relations_crosses.md.-nyWl6CC.lean.js new file mode 100644 index 000000000..41b060fb1 --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_crosses.md.-nyWl6CC.lean.js @@ -0,0 +1,120 @@ +import{_ as i,c as a,a5 as n,o as l}from"./chunks/framework.onQNwZ2I.js";const d=JSON.parse('{"title":"Crossing checks","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/crosses.md","filePath":"source/methods/geom_relations/crosses.md","lastUpdated":null}'),p={name:"source/methods/geom_relations/crosses.md"};function h(t,s,k,e,r,E){return l(),a("div",null,s[0]||(s[0]=[n(`"""
+ 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
TODO: Add working example
\`\`\`
+"""
+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
TODO use better predicate for crossing here
cross = dxc * dy1 - dyc * dx1
+ cross != 0 && return false
Will constprop optimise these away?
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
This page was generated using Literate.jl.
`,10)]))}const F=i(p,[["render",h]]);export{d as __pageData,F as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_disjoint.md.DH85oHhz.js b/previews/PR239/assets/source_methods_geom_relations_disjoint.md.DH85oHhz.js new file mode 100644 index 000000000..0986ed48d --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_disjoint.md.DH85oHhz.js @@ -0,0 +1,178 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const h="/GeometryOps.jl/previews/PR239/assets/jsdldah.C3SxJ3x-.png",o=JSON.parse('{"title":"Disjoint","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/disjoint.md","filePath":"source/methods/geom_relations/disjoint.md","lastUpdated":null}'),l={name:"source/methods/geom_relations/disjoint.md"};function p(k,s,e,r,E,g){return t(),a("div",null,s[0]||(s[0]=[n(`export disjoint
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:
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
We can see that none of the edges or vertices of l1 interact with l2 so they are disjoint.
GO.disjoint(l1, l2) # returns true
true
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 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 g2
The code for the specific implementations is in the geom_geom_processors file.
const 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)
output
true
+\`\`\`
+"""
+disjoint(g1, g2) = _disjoint(trait(g1), g1, trait(g2), g2)
_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 is disjoint from another point if the points are not equal.
_disjoint(
+ ::GI.PointTrait, g1,
+ ::GI.PointTrait, g2,
+) = !equals(g1, g2)
Point is disjoint from a linestring if it is not on the line's edges/vertices.
_disjoint(
+ ::GI.PointTrait, g1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _point_curve_process(
+ g1, g2;
+ DISJOINT_ALLOWS...,
+ closed_curve = false,
+)
Point is disjoint from a linearring if it is not on the ring's edges/vertices.
_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)
#= 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 geometry'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)
#= 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 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...,
+)
#= 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 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
This page was generated using Literate.jl.
`,39)]))}const y=i(l,[["render",p]]);export{o as __pageData,y as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_disjoint.md.DH85oHhz.lean.js b/previews/PR239/assets/source_methods_geom_relations_disjoint.md.DH85oHhz.lean.js new file mode 100644 index 000000000..0986ed48d --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_disjoint.md.DH85oHhz.lean.js @@ -0,0 +1,178 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const h="/GeometryOps.jl/previews/PR239/assets/jsdldah.C3SxJ3x-.png",o=JSON.parse('{"title":"Disjoint","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/disjoint.md","filePath":"source/methods/geom_relations/disjoint.md","lastUpdated":null}'),l={name:"source/methods/geom_relations/disjoint.md"};function p(k,s,e,r,E,g){return t(),a("div",null,s[0]||(s[0]=[n(`export disjoint
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:
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
We can see that none of the edges or vertices of l1 interact with l2 so they are disjoint.
GO.disjoint(l1, l2) # returns true
true
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 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 g2
The code for the specific implementations is in the geom_geom_processors file.
const 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)
output
true
+\`\`\`
+"""
+disjoint(g1, g2) = _disjoint(trait(g1), g1, trait(g2), g2)
_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 is disjoint from another point if the points are not equal.
_disjoint(
+ ::GI.PointTrait, g1,
+ ::GI.PointTrait, g2,
+) = !equals(g1, g2)
Point is disjoint from a linestring if it is not on the line's edges/vertices.
_disjoint(
+ ::GI.PointTrait, g1,
+ ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _point_curve_process(
+ g1, g2;
+ DISJOINT_ALLOWS...,
+ closed_curve = false,
+)
Point is disjoint from a linearring if it is not on the ring's edges/vertices.
_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)
#= 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 geometry'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)
#= 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 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...,
+)
#= 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 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
This page was generated using Literate.jl.
`,39)]))}const y=i(l,[["render",p]]);export{o as __pageData,y as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_geom_geom_processors.md.CmpKdMyB.js b/previews/PR239/assets/source_methods_geom_relations_geom_geom_processors.md.CmpKdMyB.js new file mode 100644 index 000000000..869aea8c8 --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_geom_geom_processors.md.CmpKdMyB.js @@ -0,0 +1,437 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"Line-curve interaction","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/geom_geom_processors.md","filePath":"source/methods/geom_relations/geom_geom_processors.md","lastUpdated":null}'),l={name:"source/methods/geom_relations/geom_geom_processors.md"};function t(p,s,k,e,r,E){return h(),a("div",null,s[0]||(s[0]=[n(`#= 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
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.
function _point_curve_process(
+ point, curve;
+ in_allow, on_allow, out_allow,
+ closed_curve = false,
+)
Determine if curve is closed
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
Loop through all curve segments
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
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.
function _point_polygon_process(
+ point, polygon;
+ in_allow, on_allow, out_allow, exact,
+)
Check interaction of geom with polygon's exterior boundary
ext_val = _point_filled_curve_orientation(point, GI.getexterior(polygon); exact)
If a point is outside, it isn't interacting with any holes
ext_val == point_out && return out_allow
if a point is on an external boundary, it isn't interacting with any holes
ext_val == point_on && return on_allow
If geom is within the polygon, need to check interactions with holes
for hole in GI.gethole(polygon)
+ hole_val = _point_filled_curve_orientation(point, hole; exact)
If a point in in a hole, it is outside of the polygon
hole_val == point_in && return out_allow
If a point in on a hole edge, it is on the edge of the polygon
hole_val == point_on && return on_allow
+ end
Point is within external boundary and on in/on any holes
return in_allow
+end
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 boundary 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 requirements, 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.
@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,
+)
Set up requirements
in_req_met = !in_require
+ on_req_met = !on_require
+ out_req_met = !out_require
Determine curve endpoints
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
Loop over each line segment
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))
Loop over each curve segment
for j in (closed_curve ? 1 : 2):nc
+ c_end = _tuple_point(GI.getpoint(curve, j))
Check if line and curve segments meet
seg_val, intr1, _ = _intersection_point(Float64, (l_start, l_end), (c_start, c_end); exact)
If segments are co-linear
if seg_val == line_over
+ !over_allow && return false
at least one point in, meets requirements
in_req_met = true
+ point_val = _point_segment_orientation(l_start, c_start, c_end)
If entire segment isn't covered, consider remaining section
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
Determine location of intersection point on each segment
(_, (α, β)) = 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 needed, determine if hinge actually crosses
if (!cross_allow || !over_allow) && α != 0 && β != 0
Find next pieces of hinge to see if line and curve cross
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
no overlap for a give segment, some of segment must be out of curve
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 completely 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
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 requirements, 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.
@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
Check interaction of line with polygon's exterior boundary
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
If no points within the polygon, the line is disjoint and we are done
!in_curve && return in_req_met && on_req_met && out_req_met
Loop over polygon holes
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 boundary 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
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 requirements, return true. Else, return false.
@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
Check if exterior of poly1 is within poly2
ext1 = GI.getexterior(poly1)
+ ext2 = GI.getexterior(poly2)
Check if exterior of poly1 is in polygon 2
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
if exterior ring isn't in poly2, check if it surrounds poly2
_, _, 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
If interiors interact, check if poly2 interacts with any of poly1's holes
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
If hole isn't in poly2, see if poly2 is in hole
_, _, e2_out_h1 = _line_filled_curve_interactions(
+ ext2, h1;
+ exact, closed_line = true,
+ )
hole encompasses all of poly2
!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
If any of poly2 holes are within poly1, part of poly1 is exterior to poly2
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
Determines if a point is in, on, or out of a segment. If the point is 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.
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.
function _point_segment_orientation(
+ point, start, stop;
+ in::T = point_in, on::T = point_on, out::T = point_out,
+) where {T}
Parse out points
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)
If point is equal to the segment start or end points
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
Determine if point is in, on, or out of a closed curve, which includes the space enclosed by the closed curve.
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.
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 separates 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.
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
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.
function _line_filled_curve_interactions(
+ line, curve;
+ exact, closed_line = false,
+)
+ in_curve = false
+ on_curve = false
+ out_curve = false
Determine number of points in curve and line
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
See if first point is in an acceptable orientation
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
Check for any intersections between line and curve
for i in (closed_line ? 1 : 2):nl
+ l_end = _tuple_point(GI.getpoint(line, i))
+ c_start = _tuple_point(GI.getpoint(curve, nc))
If already interacted with all regions of curve, can stop
in_curve && on_curve && out_curve && break
Check next segment of line against curve
for j in 1:nc
+ c_end = _tuple_point(GI.getpoint(curve, j))
Check if two line and curve segments meet
seg_val, _, _ = _intersection_point(Float64, (l_start, l_end), (c_start, c_end); exact)
+ if seg_val != line_out
If line and curve meet, then at least one point is on boundary
on_curve = true
+ if seg_val == line_cross
When crossing boundary, line is both in and out of curve
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
already checked segment against whole filled curve
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
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 boundaries. 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.
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)
Loop over polygon holes
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
Disjoint extent optimisation: skip work based on geom extent intersection returns Tuple{Bool, Bool} for (skip, returnval)
@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
can't tell anything about this case
false, false
+ elseif out_allow # && ext_disjoint
+ if in_require || on_require
+ true, false
+ else
+ true, true
+ end
+ else # !out_allow && ext_disjoint
points not allowed in exterior, but geoms are disjoint
true, false
+ end
+ return skip, returnval
+end
This page was generated using Literate.jl.
`,142)]))}const y=i(l,[["render",t]]);export{g as __pageData,y as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_geom_geom_processors.md.CmpKdMyB.lean.js b/previews/PR239/assets/source_methods_geom_relations_geom_geom_processors.md.CmpKdMyB.lean.js new file mode 100644 index 000000000..869aea8c8 --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_geom_geom_processors.md.CmpKdMyB.lean.js @@ -0,0 +1,437 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"Line-curve interaction","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/geom_geom_processors.md","filePath":"source/methods/geom_relations/geom_geom_processors.md","lastUpdated":null}'),l={name:"source/methods/geom_relations/geom_geom_processors.md"};function t(p,s,k,e,r,E){return h(),a("div",null,s[0]||(s[0]=[n(`#= 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
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.
function _point_curve_process(
+ point, curve;
+ in_allow, on_allow, out_allow,
+ closed_curve = false,
+)
Determine if curve is closed
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
Loop through all curve segments
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
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.
function _point_polygon_process(
+ point, polygon;
+ in_allow, on_allow, out_allow, exact,
+)
Check interaction of geom with polygon's exterior boundary
ext_val = _point_filled_curve_orientation(point, GI.getexterior(polygon); exact)
If a point is outside, it isn't interacting with any holes
ext_val == point_out && return out_allow
if a point is on an external boundary, it isn't interacting with any holes
ext_val == point_on && return on_allow
If geom is within the polygon, need to check interactions with holes
for hole in GI.gethole(polygon)
+ hole_val = _point_filled_curve_orientation(point, hole; exact)
If a point in in a hole, it is outside of the polygon
hole_val == point_in && return out_allow
If a point in on a hole edge, it is on the edge of the polygon
hole_val == point_on && return on_allow
+ end
Point is within external boundary and on in/on any holes
return in_allow
+end
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 boundary 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 requirements, 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.
@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,
+)
Set up requirements
in_req_met = !in_require
+ on_req_met = !on_require
+ out_req_met = !out_require
Determine curve endpoints
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
Loop over each line segment
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))
Loop over each curve segment
for j in (closed_curve ? 1 : 2):nc
+ c_end = _tuple_point(GI.getpoint(curve, j))
Check if line and curve segments meet
seg_val, intr1, _ = _intersection_point(Float64, (l_start, l_end), (c_start, c_end); exact)
If segments are co-linear
if seg_val == line_over
+ !over_allow && return false
at least one point in, meets requirements
in_req_met = true
+ point_val = _point_segment_orientation(l_start, c_start, c_end)
If entire segment isn't covered, consider remaining section
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
Determine location of intersection point on each segment
(_, (α, β)) = 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 needed, determine if hinge actually crosses
if (!cross_allow || !over_allow) && α != 0 && β != 0
Find next pieces of hinge to see if line and curve cross
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
no overlap for a give segment, some of segment must be out of curve
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 completely 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
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 requirements, 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.
@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
Check interaction of line with polygon's exterior boundary
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
If no points within the polygon, the line is disjoint and we are done
!in_curve && return in_req_met && on_req_met && out_req_met
Loop over polygon holes
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 boundary 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
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 requirements, return true. Else, return false.
@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
Check if exterior of poly1 is within poly2
ext1 = GI.getexterior(poly1)
+ ext2 = GI.getexterior(poly2)
Check if exterior of poly1 is in polygon 2
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
if exterior ring isn't in poly2, check if it surrounds poly2
_, _, 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
If interiors interact, check if poly2 interacts with any of poly1's holes
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
If hole isn't in poly2, see if poly2 is in hole
_, _, e2_out_h1 = _line_filled_curve_interactions(
+ ext2, h1;
+ exact, closed_line = true,
+ )
hole encompasses all of poly2
!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
If any of poly2 holes are within poly1, part of poly1 is exterior to poly2
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
Determines if a point is in, on, or out of a segment. If the point is 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.
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.
function _point_segment_orientation(
+ point, start, stop;
+ in::T = point_in, on::T = point_on, out::T = point_out,
+) where {T}
Parse out points
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)
If point is equal to the segment start or end points
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
Determine if point is in, on, or out of a closed curve, which includes the space enclosed by the closed curve.
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.
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 separates 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.
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
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.
function _line_filled_curve_interactions(
+ line, curve;
+ exact, closed_line = false,
+)
+ in_curve = false
+ on_curve = false
+ out_curve = false
Determine number of points in curve and line
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
See if first point is in an acceptable orientation
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
Check for any intersections between line and curve
for i in (closed_line ? 1 : 2):nl
+ l_end = _tuple_point(GI.getpoint(line, i))
+ c_start = _tuple_point(GI.getpoint(curve, nc))
If already interacted with all regions of curve, can stop
in_curve && on_curve && out_curve && break
Check next segment of line against curve
for j in 1:nc
+ c_end = _tuple_point(GI.getpoint(curve, j))
Check if two line and curve segments meet
seg_val, _, _ = _intersection_point(Float64, (l_start, l_end), (c_start, c_end); exact)
+ if seg_val != line_out
If line and curve meet, then at least one point is on boundary
on_curve = true
+ if seg_val == line_cross
When crossing boundary, line is both in and out of curve
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
already checked segment against whole filled curve
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
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 boundaries. 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.
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)
Loop over polygon holes
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
Disjoint extent optimisation: skip work based on geom extent intersection returns Tuple{Bool, Bool} for (skip, returnval)
@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
can't tell anything about this case
false, false
+ elseif out_allow # && ext_disjoint
+ if in_require || on_require
+ true, false
+ else
+ true, true
+ end
+ else # !out_allow && ext_disjoint
points not allowed in exterior, but geoms are disjoint
true, false
+ end
+ return skip, returnval
+end
This page was generated using Literate.jl.
`,142)]))}const y=i(l,[["render",t]]);export{g as __pageData,y as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_intersects.md.Z6dLYNxj.js b/previews/PR239/assets/source_methods_geom_relations_intersects.md.Z6dLYNxj.js new file mode 100644 index 000000000..d2d54239e --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_intersects.md.Z6dLYNxj.js @@ -0,0 +1,27 @@ +import{_ as i,c as a,a5 as e,o as n}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/sqwzzcg.DeeQUply.png",g=JSON.parse('{"title":"Intersection checks","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/intersects.md","filePath":"source/methods/geom_relations/intersects.md","lastUpdated":null}'),p={name:"source/methods/geom_relations/intersects.md"};function l(h,s,k,r,o,d){return n(),a("div",null,s[0]||(s[0]=[e(`export intersects
intersects
? 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:
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
We can see that they intersect, so we expect intersects to return true, and we can visualize the intersection point in red.
GO.intersects(line1, line2) # true
true
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.
"""
+ 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)
output
true
+\`\`\`
+"""
+intersects(geom1, geom2) = !disjoint(geom1, geom2)
This page was generated using Literate.jl.
`,18)]))}const E=i(p,[["render",l]]);export{g as __pageData,E as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_intersects.md.Z6dLYNxj.lean.js b/previews/PR239/assets/source_methods_geom_relations_intersects.md.Z6dLYNxj.lean.js new file mode 100644 index 000000000..d2d54239e --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_intersects.md.Z6dLYNxj.lean.js @@ -0,0 +1,27 @@ +import{_ as i,c as a,a5 as e,o as n}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/sqwzzcg.DeeQUply.png",g=JSON.parse('{"title":"Intersection checks","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/intersects.md","filePath":"source/methods/geom_relations/intersects.md","lastUpdated":null}'),p={name:"source/methods/geom_relations/intersects.md"};function l(h,s,k,r,o,d){return n(),a("div",null,s[0]||(s[0]=[e(`export intersects
intersects
? 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:
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
We can see that they intersect, so we expect intersects to return true, and we can visualize the intersection point in red.
GO.intersects(line1, line2) # true
true
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.
"""
+ 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)
output
true
+\`\`\`
+"""
+intersects(geom1, geom2) = !disjoint(geom1, geom2)
This page was generated using Literate.jl.
`,18)]))}const E=i(p,[["render",l]]);export{g as __pageData,E as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_overlaps.md.BjDDEk4_.js b/previews/PR239/assets/source_methods_geom_relations_overlaps.md.BjDDEk4_.js new file mode 100644 index 000000000..186cb7e1e --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_overlaps.md.BjDDEk4_.js @@ -0,0 +1,212 @@ +import{_ as i,c as a,a5 as n,o as l}from"./chunks/framework.onQNwZ2I.js";const p="/GeometryOps.jl/previews/PR239/assets/hvyhqaw.CgiryX2p.png",o=JSON.parse('{"title":"Overlaps","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/overlaps.md","filePath":"source/methods/geom_relations/overlaps.md","lastUpdated":null}'),t={name:"source/methods/geom_relations/overlaps.md"};function e(h,s,k,r,d,g){return l(),a("div",null,s[0]||(s[0]=[n(`export overlaps
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 collinear.
To provide an example, consider these two lines:
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
We can see that the two lines overlap in the plot:
GO.overlaps(l1, l2) # true
true
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 automatically 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 respectively, without being contained.
"""
+ 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)
output
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 collinear 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 collinear but each have one endpoint
+outside of the other edge, return true. Else false. =#
+function _overlaps(
+ (a1, a2)::Edge,
+ (b1, b2)::Edge,
+ exact = _False(),
+)
meets in more than one point
seg_val, _, _ = _intersection_point(Float64, (a1, a2), (b1, b2); exact)
one end point is outside of other segment
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. =#
Checks if point is on a segment
function _point_on_seg(point, start, stop)
Parse out points
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
Determine if point is on segment
cross = (x - x1) * Δyl - (y - y1) * Δxl
+ if cross == 0 # point is on line extending to infinity
is line between endpoints
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};
+)
Extents.intersects(to_extent(edges_a), to_extent(edges_b)) || return false
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
Returns true if there is at least one intersection between two edges.
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
This page was generated using Literate.jl.
`,37)]))}const E=i(t,[["render",e]]);export{o as __pageData,E as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_overlaps.md.BjDDEk4_.lean.js b/previews/PR239/assets/source_methods_geom_relations_overlaps.md.BjDDEk4_.lean.js new file mode 100644 index 000000000..186cb7e1e --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_overlaps.md.BjDDEk4_.lean.js @@ -0,0 +1,212 @@ +import{_ as i,c as a,a5 as n,o as l}from"./chunks/framework.onQNwZ2I.js";const p="/GeometryOps.jl/previews/PR239/assets/hvyhqaw.CgiryX2p.png",o=JSON.parse('{"title":"Overlaps","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/overlaps.md","filePath":"source/methods/geom_relations/overlaps.md","lastUpdated":null}'),t={name:"source/methods/geom_relations/overlaps.md"};function e(h,s,k,r,d,g){return l(),a("div",null,s[0]||(s[0]=[n(`export overlaps
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 collinear.
To provide an example, consider these two lines:
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
We can see that the two lines overlap in the plot:
GO.overlaps(l1, l2) # true
true
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 automatically 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 respectively, without being contained.
"""
+ 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)
output
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 collinear 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 collinear but each have one endpoint
+outside of the other edge, return true. Else false. =#
+function _overlaps(
+ (a1, a2)::Edge,
+ (b1, b2)::Edge,
+ exact = _False(),
+)
meets in more than one point
seg_val, _, _ = _intersection_point(Float64, (a1, a2), (b1, b2); exact)
one end point is outside of other segment
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. =#
Checks if point is on a segment
function _point_on_seg(point, start, stop)
Parse out points
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
Determine if point is on segment
cross = (x - x1) * Δyl - (y - y1) * Δxl
+ if cross == 0 # point is on line extending to infinity
is line between endpoints
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};
+)
Extents.intersects(to_extent(edges_a), to_extent(edges_b)) || return false
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
Returns true if there is at least one intersection between two edges.
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
This page was generated using Literate.jl.
`,37)]))}const E=i(t,[["render",e]]);export{o as __pageData,E as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_touches.md.C3wGt7mS.js b/previews/PR239/assets/source_methods_geom_relations_touches.md.C3wGt7mS.js new file mode 100644 index 000000000..99d0ad298 --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_touches.md.C3wGt7mS.js @@ -0,0 +1,174 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const h="/GeometryOps.jl/previews/PR239/assets/eytjybr.BEFUMtlf.png",o=JSON.parse('{"title":"Touches","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/touches.md","filePath":"source/methods/geom_relations/touches.md","lastUpdated":null}'),l={name:"source/methods/geom_relations/touches.md"};function e(p,s,k,r,E,g){return t(),a("div",null,s[0]||(s[0]=[n(`export touches
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 geometry's interior or boundary.
To provide an example, consider these two lines:
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
We can see that these two lines touch only at their endpoints.
GO.touches(l1, l2) # true
true
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 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 g2
The code for the specific implementations is in the geom_geom_processors file.
const 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 geometry's 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)
output
true
+\`\`\`
+"""
+touches(g1, g2)::Bool = _touches(trait(g1), g1, trait(g2), g2)
_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 cannot touch another point as if they are equal, interiors interact
_touches(
+ ::GI.PointTrait, g1,
+ ::GI.PointTrait, g2,
+) = false
Point touches a linestring if it equal to the first of last point of the line
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
Point cannot 'touch' a linearring given that the ring has no boundary points
_touches(
+ ::GI.PointTrait, g1,
+ ::GI.LinearRingTrait, g2,
+) = false
Point touches a polygon if it is on the boundary of that polygon
_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)
#= Linestring touches another line if at least one boundary point interacts with
+the boundary 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,
+)
#= 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 boundary 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 boundary 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,
+)
#= Polygon touches a curve if at least one of the curve boundary points interacts
+with the polygon's boundary 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...,
+)
#= 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 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
This page was generated using Literate.jl.
`,41)]))}const y=i(l,[["render",e]]);export{o as __pageData,y as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_touches.md.C3wGt7mS.lean.js b/previews/PR239/assets/source_methods_geom_relations_touches.md.C3wGt7mS.lean.js new file mode 100644 index 000000000..99d0ad298 --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_touches.md.C3wGt7mS.lean.js @@ -0,0 +1,174 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const h="/GeometryOps.jl/previews/PR239/assets/eytjybr.BEFUMtlf.png",o=JSON.parse('{"title":"Touches","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/touches.md","filePath":"source/methods/geom_relations/touches.md","lastUpdated":null}'),l={name:"source/methods/geom_relations/touches.md"};function e(p,s,k,r,E,g){return t(),a("div",null,s[0]||(s[0]=[n(`export touches
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 geometry's interior or boundary.
To provide an example, consider these two lines:
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
We can see that these two lines touch only at their endpoints.
GO.touches(l1, l2) # true
true
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 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 g2
The code for the specific implementations is in the geom_geom_processors file.
const 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 geometry's 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)
output
true
+\`\`\`
+"""
+touches(g1, g2)::Bool = _touches(trait(g1), g1, trait(g2), g2)
_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 cannot touch another point as if they are equal, interiors interact
_touches(
+ ::GI.PointTrait, g1,
+ ::GI.PointTrait, g2,
+) = false
Point touches a linestring if it equal to the first of last point of the line
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
Point cannot 'touch' a linearring given that the ring has no boundary points
_touches(
+ ::GI.PointTrait, g1,
+ ::GI.LinearRingTrait, g2,
+) = false
Point touches a polygon if it is on the boundary of that polygon
_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)
#= Linestring touches another line if at least one boundary point interacts with
+the boundary 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,
+)
#= 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 boundary 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 boundary 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,
+)
#= Polygon touches a curve if at least one of the curve boundary points interacts
+with the polygon's boundary 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...,
+)
#= 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 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
This page was generated using Literate.jl.
`,41)]))}const y=i(l,[["render",e]]);export{o as __pageData,y as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_within.md.UHYdg8yN.js b/previews/PR239/assets/source_methods_geom_relations_within.md.UHYdg8yN.js new file mode 100644 index 000000000..d8cf62dc9 --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_within.md.UHYdg8yN.js @@ -0,0 +1,193 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/rzkakxn._0R9BbFk.png",o=JSON.parse('{"title":"Within","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/within.md","filePath":"source/methods/geom_relations/within.md","lastUpdated":null}'),l={name:"source/methods/geom_relations/within.md"};function e(p,s,k,r,E,g){return h(),a("div",null,s[0]||(s[0]=[n(`export within
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:
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
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
GO.within(l1, l2) # false
+GO.within(l2, l1) # true
true
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 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 g2
The code for the specific implementations is in the geom_geom_processors file.
const 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)
output
true
+\`\`\`
+"""
+within(g1, g2) = _within(trait(g1), g1, trait(g2), g2)
_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))
Point is within another point if those points are equal.
_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,
+)
Point is within a linearring if it is on a vertex or an edge of that ring.
_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...,
+)
No geometries other than points can be within points
_within(
+ ::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+ ::GI.PointTrait, g2,
+) = false
#= 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,
+)
#= 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,
+)
#= 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...,
+)
Polygons cannot be within any curves
_within(
+ ::GI.PolygonTrait, g1,
+ ::GI.AbstractCurveTrait, g2,
+) = false
#= 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 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
This page was generated using Literate.jl.
`,41)]))}const y=i(l,[["render",e]]);export{o as __pageData,y as default}; diff --git a/previews/PR239/assets/source_methods_geom_relations_within.md.UHYdg8yN.lean.js b/previews/PR239/assets/source_methods_geom_relations_within.md.UHYdg8yN.lean.js new file mode 100644 index 000000000..d8cf62dc9 --- /dev/null +++ b/previews/PR239/assets/source_methods_geom_relations_within.md.UHYdg8yN.lean.js @@ -0,0 +1,193 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/rzkakxn._0R9BbFk.png",o=JSON.parse('{"title":"Within","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/geom_relations/within.md","filePath":"source/methods/geom_relations/within.md","lastUpdated":null}'),l={name:"source/methods/geom_relations/within.md"};function e(p,s,k,r,E,g){return h(),a("div",null,s[0]||(s[0]=[n(`export within
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:
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
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
GO.within(l1, l2) # false
+GO.within(l2, l1) # true
true
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 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 g2
The code for the specific implementations is in the geom_geom_processors file.
const 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)
output
true
+\`\`\`
+"""
+within(g1, g2) = _within(trait(g1), g1, trait(g2), g2)
_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))
Point is within another point if those points are equal.
_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,
+)
Point is within a linearring if it is on a vertex or an edge of that ring.
_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...,
+)
No geometries other than points can be within points
_within(
+ ::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+ ::GI.PointTrait, g2,
+) = false
#= 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,
+)
#= 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,
+)
#= 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...,
+)
Polygons cannot be within any curves
_within(
+ ::GI.PolygonTrait, g1,
+ ::GI.AbstractCurveTrait, g2,
+) = false
#= 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 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
This page was generated using Literate.jl.
`,41)]))}const y=i(l,[["render",e]]);export{o as __pageData,y as default}; diff --git a/previews/PR239/assets/source_methods_orientation.md.Cx8oaEg5.js b/previews/PR239/assets/source_methods_orientation.md.Cx8oaEg5.js new file mode 100644 index 000000000..da812c3ed --- /dev/null +++ b/previews/PR239/assets/source_methods_orientation.md.Cx8oaEg5.js @@ -0,0 +1,100 @@ +import{_ as i,c as a,a5 as n,o as l}from"./chunks/framework.onQNwZ2I.js";const E=JSON.parse('{"title":"Orientation","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/orientation.md","filePath":"source/methods/orientation.md","lastUpdated":null}'),p={name:"source/methods/orientation.md"};function h(t,s,e,k,r,d){return l(),a("div",null,s[0]||(s[0]=[n(`export isclockwise, isconcave
isclockwise
The orientation of a geometry is whether it runs clockwise or counter-clockwise.
This is defined for linestrings, linear rings, or vectors of points.
isconcave
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!
"""
+ isclockwise(line::Union{LineString, Vector{Position}})::Bool
+
+Take a ring and return \`true\` if the line goes clockwise, or \`false\` if the line goes
+counter-clockwise. "Going clockwise" means, mathematically,
+
+\`\`\`math
+\\\\left(\\\\sum_{i=2}^n (x_i - x_{i-1}) \\\\cdot (y_i + y_{i-1})\\\\right) > 0
+\`\`\`
+
+# Example
+
+\`\`\`julia
+julia> import GeoInterface as GI, GeometryOps as GO
+julia> ring = GI.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]);
+julia> GO.isclockwise(ring)
output
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 will be zero for the first point as x is subtracted from itself
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)
output
false
+\`\`\`
+"""
+function isconcave(poly)::Bool
+ sign = false
+
+ exterior = GI.getexterior(poly)
FIXME handle not closed polygons
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
This is commented out.
"""
+ isparallel(line1::LineString, line2::LineString)::Bool
+
+Return \`true\` if each segment of \`line1\` is parallel to the correspondent segment of \`line2\`
+
+## Examples
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
"""
+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
This is actual code:
_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)
This page was generated using Literate.jl.
`,28)]))}const o=i(p,[["render",h]]);export{E as __pageData,o as default}; diff --git a/previews/PR239/assets/source_methods_orientation.md.Cx8oaEg5.lean.js b/previews/PR239/assets/source_methods_orientation.md.Cx8oaEg5.lean.js new file mode 100644 index 000000000..da812c3ed --- /dev/null +++ b/previews/PR239/assets/source_methods_orientation.md.Cx8oaEg5.lean.js @@ -0,0 +1,100 @@ +import{_ as i,c as a,a5 as n,o as l}from"./chunks/framework.onQNwZ2I.js";const E=JSON.parse('{"title":"Orientation","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/orientation.md","filePath":"source/methods/orientation.md","lastUpdated":null}'),p={name:"source/methods/orientation.md"};function h(t,s,e,k,r,d){return l(),a("div",null,s[0]||(s[0]=[n(`export isclockwise, isconcave
isclockwise
The orientation of a geometry is whether it runs clockwise or counter-clockwise.
This is defined for linestrings, linear rings, or vectors of points.
isconcave
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!
"""
+ isclockwise(line::Union{LineString, Vector{Position}})::Bool
+
+Take a ring and return \`true\` if the line goes clockwise, or \`false\` if the line goes
+counter-clockwise. "Going clockwise" means, mathematically,
+
+\`\`\`math
+\\\\left(\\\\sum_{i=2}^n (x_i - x_{i-1}) \\\\cdot (y_i + y_{i-1})\\\\right) > 0
+\`\`\`
+
+# Example
+
+\`\`\`julia
+julia> import GeoInterface as GI, GeometryOps as GO
+julia> ring = GI.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]);
+julia> GO.isclockwise(ring)
output
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 will be zero for the first point as x is subtracted from itself
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)
output
false
+\`\`\`
+"""
+function isconcave(poly)::Bool
+ sign = false
+
+ exterior = GI.getexterior(poly)
FIXME handle not closed polygons
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
This is commented out.
"""
+ isparallel(line1::LineString, line2::LineString)::Bool
+
+Return \`true\` if each segment of \`line1\` is parallel to the correspondent segment of \`line2\`
+
+## Examples
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
"""
+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
This is actual code:
_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)
This page was generated using Literate.jl.
`,28)]))}const o=i(p,[["render",h]]);export{E as __pageData,o as default}; diff --git a/previews/PR239/assets/source_methods_polygonize.md.CCOlggGo.js b/previews/PR239/assets/source_methods_polygonize.md.CCOlggGo.js new file mode 100644 index 000000000..19c0c3b97 --- /dev/null +++ b/previews/PR239/assets/source_methods_polygonize.md.CCOlggGo.js @@ -0,0 +1,289 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"Polygonizing raster data","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/polygonize.md","filePath":"source/methods/polygonize.md","lastUpdated":null}'),l={name:"source/methods/polygonize.md"};function p(t,s,k,e,d,E){return h(),a("div",null,s[0]||(s[0]=[n(`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 polygons 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.
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
\`\`\`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...
+)
Make vectors of pixel bounds
xhalf = step(xs) / 2
+ yhalf = step(ys) / 2
Make bounds ranges first to avoid floating point error making gaps or overlaps
xbounds = range(first(xs) - xhalf; step = step(xs), length = length(xs) + 1)
+ ybounds = range(first(ys) - yhalf; step = step(ys), length = length(ys) + 1)
+ Tx = eltype(xbounds)
+ Ty = eltype(ybounds)
+ xvec = similar(Vector{Tuple{Tx,Tx}}, length(xs))
+ yvec = similar(Vector{Tuple{Ty,Ty}}, length(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"))
Extract the CRS of the array (if it is some kind of geo array / raster)
crs = GI.crs(A)
Define buffers for edges and rings
rings = Vector{T}[]
+
+ strait = true
+ turning = false
Get edges from the array A
edges = _pixel_edges(f, xs, ys, A)
Keep dict keys separately in a vector for performance
edgekeys = collect(keys(edges))
We don't delete keys we just reduce length with nkeys
nkeys = length(edgekeys)
Now create rings from the edges, looping until there are no edge keys left
while nkeys > 0
+ found = false
+ local firstnode, nextnodes, nodestatus
Loop until we find a key that hasn't been removed, decrementing nkeys as we go.
while nkeys > 0
Take the first node from the array
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
If we found nothing this time, we are done
found == false && break
Check if there are one or two lines going through this node and take one of them, then update the status
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
Start a new ring
currentnode = firstnode
+ ring = [currentnode, nextnode]
+ push!(rings, ring)
Loop until we close a the ring and break
while true
Find a node that matches the next node
(c1, c2) = possiblenodes = edges[nextnode]
+ nodestatus = map(!=(typemax(first(firstnode))) ∘ first, possiblenodes)
+ if nodestatus[2]
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.
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
Update edges
edges[nextnode] = (remainingnode, map(typemax, remainingnode))
+ else
Here we simply choose the first (and only valid) node
selectednode = c1
Replace the edge nodes with empty nodes, they will be skipped later
edges[nextnode] = (map(typemax, c1), map(typemax, c1))
Check if we are on a straight line
straightline = currentnode[1] == nextnode[1] == c1[1] ||
+ currentnode[2] == nextnode[2] == c1[2]
+ end
Update the current and next nodes with the next and selected nodes
currentnode, nextnode = nextnode, selectednode
Update the current node or add a new node to the ring
if straightline
replace the last node we don't need it
ring[end] = nextnode
+ else
add a new node, we have turned a corner
push!(ring, nextnode)
+ end
If the ring is closed, break the loop and start a new one
nextnode == firstnode && break
+ end
+ end
Define wrapped LinearRings, with embedded extents so we only calculate them once
linearrings = map(rings) do ring
+ extent = GI.extent(GI.LinearRing(ring))
+ GI.LinearRing(ring; extent, crs)
+ end
Separate exteriors from holes by winding direction
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
Then we add the holes to the polygons they are inside of
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)
Hole is in the exterior, so add it to the polygon
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)
TODO: this really should return an empty MultiPolygon but GeoInterface wrappers cant do that yet, which is not ideal...
@warn "No polgons found, check your data or try another function for \`f\`"
+ return nothing
+ else
Otherwise return a wrapped MultiPolygon
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)
Create one feature per value
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)
Get union of f return values with resolved eltype
values = map(identity, sort!(Base.union(Iterators.map(f, A))))
We ignore pure Bool
return eltype(values) == Bool ? nothing : collect(skipmissing(values))
+end
+
+function update_edge!(dict, key, node)
+ newnodes = (node, map(typemax, node))
Get or write in one go, to skip a hash lookup
existingnodes = get!(() -> newnodes, dict, key)
If we actually fetched an existing node, update it
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}}()
First we collect all the edges around target pixels
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
xs and ys hold pixel bounds
x1, x2 = xs[i]
We check the Von Neumann neighborhood to decide what edges are needed, if any.
(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
This page was generated using Literate.jl.
`,86)]))}const y=i(l,[["render",p]]);export{g as __pageData,y as default}; diff --git a/previews/PR239/assets/source_methods_polygonize.md.CCOlggGo.lean.js b/previews/PR239/assets/source_methods_polygonize.md.CCOlggGo.lean.js new file mode 100644 index 000000000..19c0c3b97 --- /dev/null +++ b/previews/PR239/assets/source_methods_polygonize.md.CCOlggGo.lean.js @@ -0,0 +1,289 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"Polygonizing raster data","description":"","frontmatter":{},"headers":[],"relativePath":"source/methods/polygonize.md","filePath":"source/methods/polygonize.md","lastUpdated":null}'),l={name:"source/methods/polygonize.md"};function p(t,s,k,e,d,E){return h(),a("div",null,s[0]||(s[0]=[n(`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 polygons 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.
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
\`\`\`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...
+)
Make vectors of pixel bounds
xhalf = step(xs) / 2
+ yhalf = step(ys) / 2
Make bounds ranges first to avoid floating point error making gaps or overlaps
xbounds = range(first(xs) - xhalf; step = step(xs), length = length(xs) + 1)
+ ybounds = range(first(ys) - yhalf; step = step(ys), length = length(ys) + 1)
+ Tx = eltype(xbounds)
+ Ty = eltype(ybounds)
+ xvec = similar(Vector{Tuple{Tx,Tx}}, length(xs))
+ yvec = similar(Vector{Tuple{Ty,Ty}}, length(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"))
Extract the CRS of the array (if it is some kind of geo array / raster)
crs = GI.crs(A)
Define buffers for edges and rings
rings = Vector{T}[]
+
+ strait = true
+ turning = false
Get edges from the array A
edges = _pixel_edges(f, xs, ys, A)
Keep dict keys separately in a vector for performance
edgekeys = collect(keys(edges))
We don't delete keys we just reduce length with nkeys
nkeys = length(edgekeys)
Now create rings from the edges, looping until there are no edge keys left
while nkeys > 0
+ found = false
+ local firstnode, nextnodes, nodestatus
Loop until we find a key that hasn't been removed, decrementing nkeys as we go.
while nkeys > 0
Take the first node from the array
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
If we found nothing this time, we are done
found == false && break
Check if there are one or two lines going through this node and take one of them, then update the status
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
Start a new ring
currentnode = firstnode
+ ring = [currentnode, nextnode]
+ push!(rings, ring)
Loop until we close a the ring and break
while true
Find a node that matches the next node
(c1, c2) = possiblenodes = edges[nextnode]
+ nodestatus = map(!=(typemax(first(firstnode))) ∘ first, possiblenodes)
+ if nodestatus[2]
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.
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
Update edges
edges[nextnode] = (remainingnode, map(typemax, remainingnode))
+ else
Here we simply choose the first (and only valid) node
selectednode = c1
Replace the edge nodes with empty nodes, they will be skipped later
edges[nextnode] = (map(typemax, c1), map(typemax, c1))
Check if we are on a straight line
straightline = currentnode[1] == nextnode[1] == c1[1] ||
+ currentnode[2] == nextnode[2] == c1[2]
+ end
Update the current and next nodes with the next and selected nodes
currentnode, nextnode = nextnode, selectednode
Update the current node or add a new node to the ring
if straightline
replace the last node we don't need it
ring[end] = nextnode
+ else
add a new node, we have turned a corner
push!(ring, nextnode)
+ end
If the ring is closed, break the loop and start a new one
nextnode == firstnode && break
+ end
+ end
Define wrapped LinearRings, with embedded extents so we only calculate them once
linearrings = map(rings) do ring
+ extent = GI.extent(GI.LinearRing(ring))
+ GI.LinearRing(ring; extent, crs)
+ end
Separate exteriors from holes by winding direction
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
Then we add the holes to the polygons they are inside of
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)
Hole is in the exterior, so add it to the polygon
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)
TODO: this really should return an empty MultiPolygon but GeoInterface wrappers cant do that yet, which is not ideal...
@warn "No polgons found, check your data or try another function for \`f\`"
+ return nothing
+ else
Otherwise return a wrapped MultiPolygon
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)
Create one feature per value
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)
Get union of f return values with resolved eltype
values = map(identity, sort!(Base.union(Iterators.map(f, A))))
We ignore pure Bool
return eltype(values) == Bool ? nothing : collect(skipmissing(values))
+end
+
+function update_edge!(dict, key, node)
+ newnodes = (node, map(typemax, node))
Get or write in one go, to skip a hash lookup
existingnodes = get!(() -> newnodes, dict, key)
If we actually fetched an existing node, update it
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}}()
First we collect all the edges around target pixels
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
xs and ys hold pixel bounds
x1, x2 = xs[i]
We check the Von Neumann neighborhood to decide what edges are needed, if any.
(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
This page was generated using Literate.jl.
`,86)]))}const y=i(l,[["render",p]]);export{g as __pageData,y as default}; diff --git a/previews/PR239/assets/source_not_implemented_yet.md.BRH4xbkQ.js b/previews/PR239/assets/source_not_implemented_yet.md.BRH4xbkQ.js new file mode 100644 index 000000000..9f702e26d --- /dev/null +++ b/previews/PR239/assets/source_not_implemented_yet.md.BRH4xbkQ.js @@ -0,0 +1,4 @@ +import{_ as i,c as t,a5 as s,o as a}from"./chunks/framework.onQNwZ2I.js";const k=JSON.parse('{"title":"Not implemented yet","description":"","frontmatter":{},"headers":[],"relativePath":"source/not_implemented_yet.md","filePath":"source/not_implemented_yet.md","lastUpdated":null}'),n={name:"source/not_implemented_yet.md"};function l(p,e,h,o,r,d){return a(),t("div",null,e[0]||(e[0]=[s(`All of the functions in this file are not implemented in Julia yet. Some of them may have implementations in LibGEOS which we can use via an extension, but there is no native-Julia implementation for them.
function symdifference end
+function buffer end
+function convexhull end
+function concavehull end
This page was generated using Literate.jl.
`,5)]))}const c=i(n,[["render",l]]);export{k as __pageData,c as default}; diff --git a/previews/PR239/assets/source_not_implemented_yet.md.BRH4xbkQ.lean.js b/previews/PR239/assets/source_not_implemented_yet.md.BRH4xbkQ.lean.js new file mode 100644 index 000000000..9f702e26d --- /dev/null +++ b/previews/PR239/assets/source_not_implemented_yet.md.BRH4xbkQ.lean.js @@ -0,0 +1,4 @@ +import{_ as i,c as t,a5 as s,o as a}from"./chunks/framework.onQNwZ2I.js";const k=JSON.parse('{"title":"Not implemented yet","description":"","frontmatter":{},"headers":[],"relativePath":"source/not_implemented_yet.md","filePath":"source/not_implemented_yet.md","lastUpdated":null}'),n={name:"source/not_implemented_yet.md"};function l(p,e,h,o,r,d){return a(),t("div",null,e[0]||(e[0]=[s(`All of the functions in this file are not implemented in Julia yet. Some of them may have implementations in LibGEOS which we can use via an extension, but there is no native-Julia implementation for them.
function symdifference end
+function buffer end
+function convexhull end
+function concavehull end
This page was generated using Literate.jl.
`,5)]))}const c=i(n,[["render",l]]);export{k as __pageData,c as default}; diff --git a/previews/PR239/assets/source_primitives.md.Dyw7zzpW.js b/previews/PR239/assets/source_primitives.md.Dyw7zzpW.js new file mode 100644 index 000000000..acef8bd21 --- /dev/null +++ b/previews/PR239/assets/source_primitives.md.Dyw7zzpW.js @@ -0,0 +1 @@ +import{_ as a,c as s,j as e,a as r,o as i}from"./chunks/framework.onQNwZ2I.js";const f=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/primitives.md","filePath":"source/primitives.md","lastUpdated":null}'),n={name:"source/primitives.md"};function l(o,t,p,c,d,m){return i(),s("div",null,t[0]||(t[0]=[e("hr",null,null,-1),e("p",null,[e("em",null,[r("This page was generated using "),e("a",{href:"https://github.com/fredrikekre/Literate.jl",target:"_blank",rel:"noreferrer"},"Literate.jl"),r(".")])],-1)]))}const _=a(n,[["render",l]]);export{f as __pageData,_ as default}; diff --git a/previews/PR239/assets/source_primitives.md.Dyw7zzpW.lean.js b/previews/PR239/assets/source_primitives.md.Dyw7zzpW.lean.js new file mode 100644 index 000000000..acef8bd21 --- /dev/null +++ b/previews/PR239/assets/source_primitives.md.Dyw7zzpW.lean.js @@ -0,0 +1 @@ +import{_ as a,c as s,j as e,a as r,o as i}from"./chunks/framework.onQNwZ2I.js";const f=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/primitives.md","filePath":"source/primitives.md","lastUpdated":null}'),n={name:"source/primitives.md"};function l(o,t,p,c,d,m){return i(),s("div",null,t[0]||(t[0]=[e("hr",null,null,-1),e("p",null,[e("em",null,[r("This page was generated using "),e("a",{href:"https://github.com/fredrikekre/Literate.jl",target:"_blank",rel:"noreferrer"},"Literate.jl"),r(".")])],-1)]))}const _=a(n,[["render",l]]);export{f as __pageData,_ as default}; diff --git a/previews/PR239/assets/source_src_GeometryOpsCore.md.BF7KPh3I.js b/previews/PR239/assets/source_src_GeometryOpsCore.md.BF7KPh3I.js new file mode 100644 index 000000000..95056e814 --- /dev/null +++ b/previews/PR239/assets/source_src_GeometryOpsCore.md.BF7KPh3I.js @@ -0,0 +1,25 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/src/GeometryOpsCore.md","filePath":"source/src/GeometryOpsCore.md","lastUpdated":null}'),l={name:"source/src/GeometryOpsCore.md"};function e(p,s,h,k,r,E){return t(),a("div",null,s[0]||(s[0]=[n(`module GeometryOpsCore
+
+using Base.Threads: nthreads, @threads, @spawn
+
+import GeoInterface
+import GeoInterface as GI
+import GeoInterface: Extents
Import all names from GeoInterface and Extents, so users can do 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
+
+using Tables
+using DataAPI
+
+include("keyword_docs.jl")
+include("types.jl")
+
+include("apply.jl")
+include("applyreduce.jl")
+include("other_primitives.jl")
+include("geometry_utils.jl")
+
+end
This page was generated using Literate.jl.
`,5)]))}const y=i(l,[["render",e]]);export{g as __pageData,y as default}; diff --git a/previews/PR239/assets/source_src_GeometryOpsCore.md.BF7KPh3I.lean.js b/previews/PR239/assets/source_src_GeometryOpsCore.md.BF7KPh3I.lean.js new file mode 100644 index 000000000..95056e814 --- /dev/null +++ b/previews/PR239/assets/source_src_GeometryOpsCore.md.BF7KPh3I.lean.js @@ -0,0 +1,25 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/src/GeometryOpsCore.md","filePath":"source/src/GeometryOpsCore.md","lastUpdated":null}'),l={name:"source/src/GeometryOpsCore.md"};function e(p,s,h,k,r,E){return t(),a("div",null,s[0]||(s[0]=[n(`module GeometryOpsCore
+
+using Base.Threads: nthreads, @threads, @spawn
+
+import GeoInterface
+import GeoInterface as GI
+import GeoInterface: Extents
Import all names from GeoInterface and Extents, so users can do 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
+
+using Tables
+using DataAPI
+
+include("keyword_docs.jl")
+include("types.jl")
+
+include("apply.jl")
+include("applyreduce.jl")
+include("other_primitives.jl")
+include("geometry_utils.jl")
+
+end
This page was generated using Literate.jl.
`,5)]))}const y=i(l,[["render",e]]);export{g as __pageData,y as default}; diff --git a/previews/PR239/assets/source_src_apply.md.C1jwRpiI.js b/previews/PR239/assets/source_src_apply.md.C1jwRpiI.js new file mode 100644 index 000000000..683875f0f --- /dev/null +++ b/previews/PR239/assets/source_src_apply.md.C1jwRpiI.js @@ -0,0 +1,144 @@ +import{_ as h,c as l,a5 as a,j as i,a as t,G as n,B as p,o as k}from"./chunks/framework.onQNwZ2I.js";const b=JSON.parse('{"title":"apply","description":"","frontmatter":{},"headers":[],"relativePath":"source/src/apply.md","filePath":"source/src/apply.md","lastUpdated":null}'),r={name:"source/src/apply.md"},d={class:"jldocstring custom-block",open:""},g={class:"jldocstring custom-block",open:""};function o(E,s,c,y,F,u){const e=p("Badge");return k(),l("div",null,[s[6]||(s[6]=a('apply
export apply
This file mainly defines the apply
function.
In general, the idea behind the 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. Then, the geometry or structure is rebuilt.
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:
flipped_geom = GO.apply(GI.PointTrait(), geom) do p\n (GI.y(p), GI.x(p))\nend
As simple as that. There's no need to implement your own decomposition because it's done for you.
Functions like 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.
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.
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
objects
FeatureTrait
objects
AbstractGeometryTrait
objects
apply
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.
The outer recursive functions then progressively rebuild the object using GeoInterface objects matching the original traits.
If 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.
To handle this possibility it may be necessary to make Target
a Union
of traits found at the same level of nesting, and define methods of f
to handle all cases.
Be careful making a union across "levels" of nesting, e.g. Union{FeatureTrait,PolygonTrait}
, as _apply
will just never reach PolygonTrait
when all the polygons are wrapped in a FeatureTrait
object.
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-is
Threading is used at the outermost level possible - over an array, feature collection, or e.g. a MultiPolygonTrait where each PolygonTrait
sub-geometry may be calculated on a different thread.
Currently, threading defaults to 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
Call _apply again with the trait of geom
@inline _apply(f::F, target, geom; kw...) where F =
+ _apply(f, target, GI.trait(geom), geom; kw...)
There is no trait and this is an AbstractArray - so just iterate over it calling _apply on the contents
@inline function _apply(f::F, target, ::Nothing, A::AbstractArray; threaded, kw...) where F
For an Array there is nothing else to do but map _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
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 map
.
@inline function _apply(f::F, target, ::Nothing, iterable::IterableType; threaded, kw...) where {F, IterableType}
Try the Tables.jl interface first
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)
We extract the geometry column and run apply
on it.
geometry_column = first(GI.geometrycolumns(iterable))
+ new_geometry = _apply(f, target, Tables.getcolumn(iterable, geometry_column); threaded, kw...)
Then, we obtain the schema of the table,
old_schema = Tables.schema(iterable)
filter the geometry column out,
new_names = filter(Base.Fix1(!==, geometry_column), old_schema.names)
and try to rebuild the same table as the best type - either the original type of iterable
, or a named tuple which is the default fallback.
result = Tables.materializer(iterable)(
+ merge(
+ NamedTuple{(geometry_column,), Base.Tuple{typeof(new_geometry)}}((new_geometry,)),
+ NamedTuple(Iterators.map(_get_col_pair, new_names))
+ )
+ )
Finally, we ensure that metadata is propagated correctly. This can only happen if the original table supports metadata reads, and the result supports metadata writes.
if DataAPI.metadatasupport(typeof(result)).write
Copy over all metadata from the original table to the new table, if the original table supports metadata reading.
if DataAPI.metadatasupport(IterableType).read
+ for (key, (value, style)) in DataAPI.metadata(iterable; style = true)
Default styles are not preserved on data transformation, so we must skip them!
style == :default && continue
We assume that any other style is preserved.
DataAPI.metadata!(result, key, value; style)
+ end
+ end
We don't usually care about the original table's metadata for GEOINTERFACE namespaced keys, so we should set the crs and geometrycolumns metadata if they are present. Ensure that GEOINTERFACE:geometrycolumns
and GEOINTERFACE:crs
are set!
mdk = DataAPI.metadatakeys(result)
If the user has asked for geometry columns to persist, they would be here, so we don't need to set them.
if !("GEOINTERFACE:geometrycolumns" in mdk)
If the geometry columns are not already set, we need to set them.
DataAPI.metadata!(result, "GEOINTERFACE:geometrycolumns", (geometry_column,); style = :default)
+ end
Force reset CRS always, since you can pass crs
to apply
.
new_crs = if haskey(kw, :crs)
+ kw[:crs]
+ else
+ GI.crs(iterable) # this will automatically check \`GEOINTERFACE:crs\` unless the type has a specialized implementation.
+ end
+
+ DataAPI.metadata!(result, "GEOINTERFACE:crs", new_crs; style = :default)
+ end
+
+ return result
+end
Rewrap all FeatureCollectionTrait feature collections as GI.FeatureCollection Maybe use threads to call _apply on component features
@inline function _apply(f::F, target, ::GI.FeatureCollectionTrait, fc;
+ crs=GI.crs(fc), calc_extent=_False(), threaded
+) where F
Run _apply on all 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
Calculate the extent of the features
extent = mapreduce(GI.extent, Extents.union, features)
Return a FeatureCollection with features, crs and calculated extent
return GI.FeatureCollection(features; crs, extent)
+ else
Return a FeatureCollection with features and crs
return GI.FeatureCollection(features; crs)
+ end
+end
Rewrap all FeatureTrait features as GI.Feature, keeping the properties
@inline function _apply(f::F, target, ::GI.FeatureTrait, feature;
+ crs=GI.crs(feature), calc_extent=_False(), threaded
+) where F
Run _apply on the contained geometry
geometry = _apply(f, target, GI.geometry(feature); crs, calc_extent, threaded)
Get the feature properties
properties = GI.properties(feature)
+ if calc_extent isa _True
Calculate the extent of the geometry
extent = GI.extent(geometry)
Return a new Feature with the new geometry and calculated extent, but the original properties and crs
return GI.Feature(geometry; properties, crs, extent)
+ else
Return a new Feature with the new geometry, but the original properties and crs
return GI.Feature(geometry; properties, crs)
+ end
+end
Reconstruct nested geometries, maybe using threads to call _apply on component geoms
@inline function _apply(f::F, target, trait, geom;
+ crs=GI.crs(geom), calc_extent=_False(), threaded
+)::(GI.geointerface_geomtype(trait)) where F
Map _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
+@inline function _apply(f::F, target::TraitTarget{<:PointTrait}, trait::GI.PolygonTrait, geom;
+ crs=GI.crs(geom), calc_extent=_False(), threaded
+)::(GI.geointerface_geomtype(trait)) where F
We need to force rebuilding a LinearRing not a LineString
geoms = _maptasks(1:GI.ngeom(geom), threaded) do i
+ lr = GI.getgeom(geom, i)
+ points = map(GI.getgeom(lr)) do p
+ _apply(f, target, p; crs, calc_extent, threaded=_False())
+ end
+ _linearring(_apply_inner(lr, points, crs, calc_extent))
+ end
+ return _apply_inner(geom, geoms, crs, calc_extent)
+end
+function _apply_inner(geom, geoms, crs, calc_extent::_True)
Calculate the extent of the sub geometries
extent = mapreduce(GI.extent, Extents.union, geoms)
Return a new geometry of the same trait as geom
, holding the new geoms
with crs
and calculated extent
return rebuild(geom, geoms; crs, extent)
+end
+function _apply_inner(geom, geoms, crs, calc_extent::_False)
Return a new geometry of the same trait as geom
, holding the new geoms
with crs
return rebuild(geom, geoms; crs)
+end
Fail loudly if we hit PointTrait without running 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)
Define some specific cases of this match to avoid method ambiguity
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
+
+
+### \`_maptasks\` - flexible, threaded \`map\`
+
+using Base.Threads: nthreads, @threads, @spawn
Threading utility, modified Mason Protters threading PSA run f
over ntasks, where f receives an AbstractArray/range of linear indices
@inline function _maptasks(f::F, taskrange, threaded::_True)::Vector where F
+ ntasks = length(taskrange)
Customize this as needed. More tasks have more overhead, but better load balancing
tasks_per_thread = 2
+ chunk_size = max(1, ntasks ÷ (tasks_per_thread * nthreads()))
partition the range into chunks
task_chunks = Iterators.partition(taskrange, chunk_size)
Map over the chunks
tasks = map(task_chunks) do chunk
Spawn a task to process this chunk
@spawn begin
Where we map f
over the chunk indices
map(f, chunk)
+ end
+ end
Finally we join the results into a new vector
return mapreduce(fetch, vcat, tasks)
+end
Here we use the compiler directive @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
This page was generated using Literate.jl.
`,107))])}const m=h(r,[["render",o]]);export{b as __pageData,m as default}; diff --git a/previews/PR239/assets/source_src_apply.md.C1jwRpiI.lean.js b/previews/PR239/assets/source_src_apply.md.C1jwRpiI.lean.js new file mode 100644 index 000000000..683875f0f --- /dev/null +++ b/previews/PR239/assets/source_src_apply.md.C1jwRpiI.lean.js @@ -0,0 +1,144 @@ +import{_ as h,c as l,a5 as a,j as i,a as t,G as n,B as p,o as k}from"./chunks/framework.onQNwZ2I.js";const b=JSON.parse('{"title":"apply","description":"","frontmatter":{},"headers":[],"relativePath":"source/src/apply.md","filePath":"source/src/apply.md","lastUpdated":null}'),r={name:"source/src/apply.md"},d={class:"jldocstring custom-block",open:""},g={class:"jldocstring custom-block",open:""};function o(E,s,c,y,F,u){const e=p("Badge");return k(),l("div",null,[s[6]||(s[6]=a('apply
export apply
This file mainly defines the apply
function.
In general, the idea behind the 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. Then, the geometry or structure is rebuilt.
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:
flipped_geom = GO.apply(GI.PointTrait(), geom) do p\n (GI.y(p), GI.x(p))\nend
As simple as that. There's no need to implement your own decomposition because it's done for you.
Functions like 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.
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.
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
objects
FeatureTrait
objects
AbstractGeometryTrait
objects
apply
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.
The outer recursive functions then progressively rebuild the object using GeoInterface objects matching the original traits.
If 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.
To handle this possibility it may be necessary to make Target
a Union
of traits found at the same level of nesting, and define methods of f
to handle all cases.
Be careful making a union across "levels" of nesting, e.g. Union{FeatureTrait,PolygonTrait}
, as _apply
will just never reach PolygonTrait
when all the polygons are wrapped in a FeatureTrait
object.
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-is
Threading is used at the outermost level possible - over an array, feature collection, or e.g. a MultiPolygonTrait where each PolygonTrait
sub-geometry may be calculated on a different thread.
Currently, threading defaults to 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
Call _apply again with the trait of geom
@inline _apply(f::F, target, geom; kw...) where F =
+ _apply(f, target, GI.trait(geom), geom; kw...)
There is no trait and this is an AbstractArray - so just iterate over it calling _apply on the contents
@inline function _apply(f::F, target, ::Nothing, A::AbstractArray; threaded, kw...) where F
For an Array there is nothing else to do but map _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
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 map
.
@inline function _apply(f::F, target, ::Nothing, iterable::IterableType; threaded, kw...) where {F, IterableType}
Try the Tables.jl interface first
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)
We extract the geometry column and run apply
on it.
geometry_column = first(GI.geometrycolumns(iterable))
+ new_geometry = _apply(f, target, Tables.getcolumn(iterable, geometry_column); threaded, kw...)
Then, we obtain the schema of the table,
old_schema = Tables.schema(iterable)
filter the geometry column out,
new_names = filter(Base.Fix1(!==, geometry_column), old_schema.names)
and try to rebuild the same table as the best type - either the original type of iterable
, or a named tuple which is the default fallback.
result = Tables.materializer(iterable)(
+ merge(
+ NamedTuple{(geometry_column,), Base.Tuple{typeof(new_geometry)}}((new_geometry,)),
+ NamedTuple(Iterators.map(_get_col_pair, new_names))
+ )
+ )
Finally, we ensure that metadata is propagated correctly. This can only happen if the original table supports metadata reads, and the result supports metadata writes.
if DataAPI.metadatasupport(typeof(result)).write
Copy over all metadata from the original table to the new table, if the original table supports metadata reading.
if DataAPI.metadatasupport(IterableType).read
+ for (key, (value, style)) in DataAPI.metadata(iterable; style = true)
Default styles are not preserved on data transformation, so we must skip them!
style == :default && continue
We assume that any other style is preserved.
DataAPI.metadata!(result, key, value; style)
+ end
+ end
We don't usually care about the original table's metadata for GEOINTERFACE namespaced keys, so we should set the crs and geometrycolumns metadata if they are present. Ensure that GEOINTERFACE:geometrycolumns
and GEOINTERFACE:crs
are set!
mdk = DataAPI.metadatakeys(result)
If the user has asked for geometry columns to persist, they would be here, so we don't need to set them.
if !("GEOINTERFACE:geometrycolumns" in mdk)
If the geometry columns are not already set, we need to set them.
DataAPI.metadata!(result, "GEOINTERFACE:geometrycolumns", (geometry_column,); style = :default)
+ end
Force reset CRS always, since you can pass crs
to apply
.
new_crs = if haskey(kw, :crs)
+ kw[:crs]
+ else
+ GI.crs(iterable) # this will automatically check \`GEOINTERFACE:crs\` unless the type has a specialized implementation.
+ end
+
+ DataAPI.metadata!(result, "GEOINTERFACE:crs", new_crs; style = :default)
+ end
+
+ return result
+end
Rewrap all FeatureCollectionTrait feature collections as GI.FeatureCollection Maybe use threads to call _apply on component features
@inline function _apply(f::F, target, ::GI.FeatureCollectionTrait, fc;
+ crs=GI.crs(fc), calc_extent=_False(), threaded
+) where F
Run _apply on all 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
Calculate the extent of the features
extent = mapreduce(GI.extent, Extents.union, features)
Return a FeatureCollection with features, crs and calculated extent
return GI.FeatureCollection(features; crs, extent)
+ else
Return a FeatureCollection with features and crs
return GI.FeatureCollection(features; crs)
+ end
+end
Rewrap all FeatureTrait features as GI.Feature, keeping the properties
@inline function _apply(f::F, target, ::GI.FeatureTrait, feature;
+ crs=GI.crs(feature), calc_extent=_False(), threaded
+) where F
Run _apply on the contained geometry
geometry = _apply(f, target, GI.geometry(feature); crs, calc_extent, threaded)
Get the feature properties
properties = GI.properties(feature)
+ if calc_extent isa _True
Calculate the extent of the geometry
extent = GI.extent(geometry)
Return a new Feature with the new geometry and calculated extent, but the original properties and crs
return GI.Feature(geometry; properties, crs, extent)
+ else
Return a new Feature with the new geometry, but the original properties and crs
return GI.Feature(geometry; properties, crs)
+ end
+end
Reconstruct nested geometries, maybe using threads to call _apply on component geoms
@inline function _apply(f::F, target, trait, geom;
+ crs=GI.crs(geom), calc_extent=_False(), threaded
+)::(GI.geointerface_geomtype(trait)) where F
Map _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
+@inline function _apply(f::F, target::TraitTarget{<:PointTrait}, trait::GI.PolygonTrait, geom;
+ crs=GI.crs(geom), calc_extent=_False(), threaded
+)::(GI.geointerface_geomtype(trait)) where F
We need to force rebuilding a LinearRing not a LineString
geoms = _maptasks(1:GI.ngeom(geom), threaded) do i
+ lr = GI.getgeom(geom, i)
+ points = map(GI.getgeom(lr)) do p
+ _apply(f, target, p; crs, calc_extent, threaded=_False())
+ end
+ _linearring(_apply_inner(lr, points, crs, calc_extent))
+ end
+ return _apply_inner(geom, geoms, crs, calc_extent)
+end
+function _apply_inner(geom, geoms, crs, calc_extent::_True)
Calculate the extent of the sub geometries
extent = mapreduce(GI.extent, Extents.union, geoms)
Return a new geometry of the same trait as geom
, holding the new geoms
with crs
and calculated extent
return rebuild(geom, geoms; crs, extent)
+end
+function _apply_inner(geom, geoms, crs, calc_extent::_False)
Return a new geometry of the same trait as geom
, holding the new geoms
with crs
return rebuild(geom, geoms; crs)
+end
Fail loudly if we hit PointTrait without running 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)
Define some specific cases of this match to avoid method ambiguity
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
+
+
+### \`_maptasks\` - flexible, threaded \`map\`
+
+using Base.Threads: nthreads, @threads, @spawn
Threading utility, modified Mason Protters threading PSA run f
over ntasks, where f receives an AbstractArray/range of linear indices
@inline function _maptasks(f::F, taskrange, threaded::_True)::Vector where F
+ ntasks = length(taskrange)
Customize this as needed. More tasks have more overhead, but better load balancing
tasks_per_thread = 2
+ chunk_size = max(1, ntasks ÷ (tasks_per_thread * nthreads()))
partition the range into chunks
task_chunks = Iterators.partition(taskrange, chunk_size)
Map over the chunks
tasks = map(task_chunks) do chunk
Spawn a task to process this chunk
@spawn begin
Where we map f
over the chunk indices
map(f, chunk)
+ end
+ end
Finally we join the results into a new vector
return mapreduce(fetch, vcat, tasks)
+end
Here we use the compiler directive @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
This page was generated using Literate.jl.
`,107))])}const m=h(r,[["render",o]]);export{b as __pageData,m as default}; diff --git a/previews/PR239/assets/source_src_applyreduce.md.vbUWBm8w.js b/previews/PR239/assets/source_src_applyreduce.md.vbUWBm8w.js new file mode 100644 index 000000000..5c5d859a2 --- /dev/null +++ b/previews/PR239/assets/source_src_applyreduce.md.vbUWBm8w.js @@ -0,0 +1,72 @@ +import{_ as i,c as a,a5 as t,o as n}from"./chunks/framework.onQNwZ2I.js";const E=JSON.parse('{"title":"applyreduce","description":"","frontmatter":{},"headers":[],"relativePath":"source/src/applyreduce.md","filePath":"source/src/applyreduce.md","lastUpdated":null}'),h={name:"source/src/applyreduce.md"};function e(p,s,l,k,r,d){return n(),a("div",null,s[0]||(s[0]=[t(`applyreduce
export applyreduce
This file mainly defines the applyreduce
function.
This performs apply
, but then reduces the result after flattening instead of rebuilding the geometry.
In general, the idea behind the 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.
centroid
, area
and distance
have been implemented using the applyreduce
framework.
"""
+ 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)
Maybe use threads reducing over arrays
@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
Try to applyreduce over iterables
@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
Try to mapreduce
the iterable as-is
mapreduce(applyreduce_iterable, op, iterable; init)
+ end
+ end
+end
In this case, we don't reconstruct the table, but only operate on the geometry column.
function _applyreduce_table(f::F, op::O, target, iterable::IterableType; threaded, init) where {F, O, IterableType}
We extract the geometry column and run applyreduce
on it.
geometry_column = first(GI.geometrycolumns(iterable))
+ return _applyreduce(f, op, target, Tables.getcolumn(iterable, geometry_column); threaded, init)
+end
If 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}
We extract the geometry column and run 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
Maybe use threads reducing over features of feature collections
@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
Features just applyreduce to their geometry
@inline _applyreduce(f::F, op::O, target, ::GI.FeatureTrait, feature; threaded, init) where {F, O} =
+ _applyreduce(f, op, target, GI.geometry(feature); threaded, init)
Maybe use threads over components of nested geometries
@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
Don't thread over points it won't pay off
@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
Apply f to the target
@inline function _applyreduce(f::F, op::O, ::TraitTarget{Target}, ::Trait, x; kw...) where {F,O,Target,Trait<:Target}
+ f(x)
+end
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
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
+
+### \`_mapreducetasks\` - flexible, threaded mapreduce
+
+import Base.Threads: nthreads, @threads, @spawn
Threading utility, modified Mason Protters threading PSA run f
over ntasks, where f receives an AbstractArray/range of linear indices
WARNING: this will not work for mean/median - only ops where grouping is possible. That's because the implementation operates in chunks, and not globally.
If you absolutely need a single chunk, then threaded = false
will always decompose to straight mapreduce
without grouping.
@inline function _mapreducetasks(f::F, op, taskrange, threaded::_True; init) where F
+ ntasks = length(taskrange)
Customize this as needed. More tasks have more overhead, but better load balancing
tasks_per_thread = 2
+ chunk_size = max(1, ntasks ÷ (tasks_per_thread * nthreads()))
partition the range into chunks
task_chunks = Iterators.partition(taskrange, chunk_size)
Map over the chunks
tasks = map(task_chunks) do chunk
Spawn a task to process this chunk
@spawn begin
Where we map f
over the chunk indices
mapreduce(f, op, chunk; init)
+ end
+ end
Finally we join the results into a new vector
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
This page was generated using Literate.jl.
`,51)]))}const y=i(h,[["render",e]]);export{E as __pageData,y as default}; diff --git a/previews/PR239/assets/source_src_applyreduce.md.vbUWBm8w.lean.js b/previews/PR239/assets/source_src_applyreduce.md.vbUWBm8w.lean.js new file mode 100644 index 000000000..5c5d859a2 --- /dev/null +++ b/previews/PR239/assets/source_src_applyreduce.md.vbUWBm8w.lean.js @@ -0,0 +1,72 @@ +import{_ as i,c as a,a5 as t,o as n}from"./chunks/framework.onQNwZ2I.js";const E=JSON.parse('{"title":"applyreduce","description":"","frontmatter":{},"headers":[],"relativePath":"source/src/applyreduce.md","filePath":"source/src/applyreduce.md","lastUpdated":null}'),h={name:"source/src/applyreduce.md"};function e(p,s,l,k,r,d){return n(),a("div",null,s[0]||(s[0]=[t(`applyreduce
export applyreduce
This file mainly defines the applyreduce
function.
This performs apply
, but then reduces the result after flattening instead of rebuilding the geometry.
In general, the idea behind the 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.
centroid
, area
and distance
have been implemented using the applyreduce
framework.
"""
+ 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)
Maybe use threads reducing over arrays
@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
Try to applyreduce over iterables
@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
Try to mapreduce
the iterable as-is
mapreduce(applyreduce_iterable, op, iterable; init)
+ end
+ end
+end
In this case, we don't reconstruct the table, but only operate on the geometry column.
function _applyreduce_table(f::F, op::O, target, iterable::IterableType; threaded, init) where {F, O, IterableType}
We extract the geometry column and run applyreduce
on it.
geometry_column = first(GI.geometrycolumns(iterable))
+ return _applyreduce(f, op, target, Tables.getcolumn(iterable, geometry_column); threaded, init)
+end
If 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}
We extract the geometry column and run 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
Maybe use threads reducing over features of feature collections
@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
Features just applyreduce to their geometry
@inline _applyreduce(f::F, op::O, target, ::GI.FeatureTrait, feature; threaded, init) where {F, O} =
+ _applyreduce(f, op, target, GI.geometry(feature); threaded, init)
Maybe use threads over components of nested geometries
@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
Don't thread over points it won't pay off
@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
Apply f to the target
@inline function _applyreduce(f::F, op::O, ::TraitTarget{Target}, ::Trait, x; kw...) where {F,O,Target,Trait<:Target}
+ f(x)
+end
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
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
+
+### \`_mapreducetasks\` - flexible, threaded mapreduce
+
+import Base.Threads: nthreads, @threads, @spawn
Threading utility, modified Mason Protters threading PSA run f
over ntasks, where f receives an AbstractArray/range of linear indices
WARNING: this will not work for mean/median - only ops where grouping is possible. That's because the implementation operates in chunks, and not globally.
If you absolutely need a single chunk, then threaded = false
will always decompose to straight mapreduce
without grouping.
@inline function _mapreducetasks(f::F, op, taskrange, threaded::_True; init) where F
+ ntasks = length(taskrange)
Customize this as needed. More tasks have more overhead, but better load balancing
tasks_per_thread = 2
+ chunk_size = max(1, ntasks ÷ (tasks_per_thread * nthreads()))
partition the range into chunks
task_chunks = Iterators.partition(taskrange, chunk_size)
Map over the chunks
tasks = map(task_chunks) do chunk
Spawn a task to process this chunk
@spawn begin
Where we map f
over the chunk indices
mapreduce(f, op, chunk; init)
+ end
+ end
Finally we join the results into a new vector
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
This page was generated using Literate.jl.
`,51)]))}const y=i(h,[["render",e]]);export{E as __pageData,y as default}; diff --git a/previews/PR239/assets/source_src_geometry_utils.md.CpDbCg3A.js b/previews/PR239/assets/source_src_geometry_utils.md.CpDbCg3A.js new file mode 100644 index 000000000..ffd71c493 --- /dev/null +++ b/previews/PR239/assets/source_src_geometry_utils.md.CpDbCg3A.js @@ -0,0 +1,2 @@ +import{_ as i,c as a,a5 as t,o as e}from"./chunks/framework.onQNwZ2I.js";const E=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/src/geometry_utils.md","filePath":"source/src/geometry_utils.md","lastUpdated":null}'),n={name:"source/src/geometry_utils.md"};function h(l,s,k,p,r,g){return e(),a("div",null,s[0]||(s[0]=[t(`_linearring(geom::GI.LineString) = GI.LinearRing(parent(geom); extent=geom.extent, crs=geom.crs)
+_linearring(geom::GI.LinearRing) = geom
This page was generated using Literate.jl.
`,3)]))}const o=i(n,[["render",h]]);export{E as __pageData,o as default}; diff --git a/previews/PR239/assets/source_src_geometry_utils.md.CpDbCg3A.lean.js b/previews/PR239/assets/source_src_geometry_utils.md.CpDbCg3A.lean.js new file mode 100644 index 000000000..ffd71c493 --- /dev/null +++ b/previews/PR239/assets/source_src_geometry_utils.md.CpDbCg3A.lean.js @@ -0,0 +1,2 @@ +import{_ as i,c as a,a5 as t,o as e}from"./chunks/framework.onQNwZ2I.js";const E=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/src/geometry_utils.md","filePath":"source/src/geometry_utils.md","lastUpdated":null}'),n={name:"source/src/geometry_utils.md"};function h(l,s,k,p,r,g){return e(),a("div",null,s[0]||(s[0]=[t(`_linearring(geom::GI.LineString) = GI.LinearRing(parent(geom); extent=geom.extent, crs=geom.crs)
+_linearring(geom::GI.LinearRing) = geom
This page was generated using Literate.jl.
`,3)]))}const o=i(n,[["render",h]]);export{E as __pageData,o as default}; diff --git a/previews/PR239/assets/source_src_keyword_docs.md.BxLB2oH3.js b/previews/PR239/assets/source_src_keyword_docs.md.BxLB2oH3.js new file mode 100644 index 000000000..7b174f887 --- /dev/null +++ b/previews/PR239/assets/source_src_keyword_docs.md.BxLB2oH3.js @@ -0,0 +1 @@ +import{_ as a,c as i,a5 as t,o as e}from"./chunks/framework.onQNwZ2I.js";const c=JSON.parse('{"title":"Keyword docs","description":"","frontmatter":{},"headers":[],"relativePath":"source/src/keyword_docs.md","filePath":"source/src/keyword_docs.md","lastUpdated":null}'),n={name:"source/src/keyword_docs.md"};function l(h,s,o,r,p,d){return e(),i("div",null,s[0]||(s[0]=[t('This file defines common keyword documentation, that can be spliced into docstrings.
const THREADED_KEYWORD = "- `threaded`: `true` or `false`. Whether to use multithreading. Defaults to `false`."\nconst CRS_KEYWORD = "- `crs`: The CRS to attach to geometries. Defaults to `nothing`."\nconst CALC_EXTENT_KEYWORD = "- `calc_extent`: `true` or `false`. Whether to calculate the extent. Defaults to `false`."\n\nconst APPLY_KEYWORDS = """\n$THREADED_KEYWORD\n$CRS_KEYWORD\n$CALC_EXTENT_KEYWORD\n"""
This page was generated using Literate.jl.
',5)]))}const u=a(n,[["render",l]]);export{c as __pageData,u as default}; diff --git a/previews/PR239/assets/source_src_keyword_docs.md.BxLB2oH3.lean.js b/previews/PR239/assets/source_src_keyword_docs.md.BxLB2oH3.lean.js new file mode 100644 index 000000000..7b174f887 --- /dev/null +++ b/previews/PR239/assets/source_src_keyword_docs.md.BxLB2oH3.lean.js @@ -0,0 +1 @@ +import{_ as a,c as i,a5 as t,o as e}from"./chunks/framework.onQNwZ2I.js";const c=JSON.parse('{"title":"Keyword docs","description":"","frontmatter":{},"headers":[],"relativePath":"source/src/keyword_docs.md","filePath":"source/src/keyword_docs.md","lastUpdated":null}'),n={name:"source/src/keyword_docs.md"};function l(h,s,o,r,p,d){return e(),i("div",null,s[0]||(s[0]=[t('This file defines common keyword documentation, that can be spliced into docstrings.
const THREADED_KEYWORD = "- `threaded`: `true` or `false`. Whether to use multithreading. Defaults to `false`."\nconst CRS_KEYWORD = "- `crs`: The CRS to attach to geometries. Defaults to `nothing`."\nconst CALC_EXTENT_KEYWORD = "- `calc_extent`: `true` or `false`. Whether to calculate the extent. Defaults to `false`."\n\nconst APPLY_KEYWORDS = """\n$THREADED_KEYWORD\n$CRS_KEYWORD\n$CALC_EXTENT_KEYWORD\n"""
This page was generated using Literate.jl.
',5)]))}const u=a(n,[["render",l]]);export{c as __pageData,u as default}; diff --git a/previews/PR239/assets/source_src_other_primitives.md.BVzBrNUT.js b/previews/PR239/assets/source_src_other_primitives.md.BVzBrNUT.js new file mode 100644 index 000000000..9d232a003 --- /dev/null +++ b/previews/PR239/assets/source_src_other_primitives.md.BVzBrNUT.js @@ -0,0 +1,131 @@ +import{_ as k,c as l,j as i,a,G as h,a5 as t,B as p,o as e}from"./chunks/framework.onQNwZ2I.js";const A=JSON.parse('{"title":"Other primitives (unwrap, flatten, etc)","description":"","frontmatter":{},"headers":[],"relativePath":"source/src/other_primitives.md","filePath":"source/src/other_primitives.md","lastUpdated":null}'),r={name:"source/src/other_primitives.md"},E={class:"jldocstring custom-block",open:""},d={class:"jldocstring custom-block",open:""},g={class:"jldocstring custom-block",open:""},y={class:"jldocstring custom-block",open:""};function F(o,s,c,C,u,m){const n=p("Badge");return e(),l("div",null,[s[12]||(s[12]=i("h1",{id:"Other-primitives-(unwrap,-flatten,-etc)",tabindex:"-1"},[a("Other primitives (unwrap, flatten, etc) "),i("a",{class:"header-anchor",href:"#Other-primitives-(unwrap,-flatten,-etc)","aria-label":'Permalink to "Other primitives (unwrap, flatten, etc) {#Other-primitives-(unwrap,-flatten,-etc)}"'},"")],-1)),s[13]||(s[13]=i("p",null,"This file defines the following primitives:",-1)),i("details",E,[i("summary",null,[s[0]||(s[0]=i("a",{id:"GeometryOpsCore.unwrap-source-src-other_primitives",href:"#GeometryOpsCore.unwrap-source-src-other_primitives"},[i("span",{class:"jlbinding"},"GeometryOpsCore.unwrap")],-1)),s[1]||(s[1]=a()),h(n,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[2]||(s[2]=t(`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.
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.
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
.
Usually used in combination with flatten
.
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)
',5))]),s[14]||(s[14]=t(`"""
+ 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)
Add dispatch argument for trait
unwrap(f, target::Type, geom) = unwrap(f, target, GI.trait(geom), geom)
Try to unwrap over iterables
unwrap(f, target::Type, ::Nothing, iterable) =
+ map(x -> unwrap(f, target, x), iterable)
Rewrap feature collections
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))
Apply f to the target geometry
unwrap(f, ::Type{Target}, ::Trait, geom) where {Target,Trait<:Target} = f(geom)
Fail if we hit PointTrait
unwrap(f, target::Type, trait::GI.PointTrait, geom) =
+ throw(ArgumentError("target $target not found, but reached a \`PointTrait\` leaf"))
Specific cases to avoid method ambiguity
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)
Try to flatten over iterables
function _flatten(f, ::Type{Target}, ::Nothing, iterable) where Target
+ if Tables.istable(iterable)
+ column = Tables.getcolumn(iterable, first(GI.geometrycolumns(iterable)))
+ Iterators.map(x -> _flatten(f, Target, x), column) |> Iterators.flatten
+ else
+ Iterators.map(x -> _flatten(f, Target, x), iterable) |> Iterators.flatten
+ end
+end
Flatten feature collections
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))
Apply f to the target geometry
_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)))
Fail if we hit PointTrait without running f
_flatten(f, ::Type{Target}, trait::GI.PointTrait, geom) where Target =
+ throw(ArgumentError("target $Target not found, but reached a \`PointTrait\` leaf"))
Specific cases to avoid method ambiguity
_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\`.
+
+Usually 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)
Try to reconstruct over iterables
function _reconstruct(::Type{Target}, ::Nothing, iterable, components, iter) where Target
+ vect = map(iterable) do x
iter is updated by _reconstruct here
obj, iter = _reconstruct(Target, x, components, iter)
+ obj
+ end
+ return vect, iter
+end
Reconstruct feature collections
function _reconstruct(::Type{Target}, ::GI.FeatureCollectionTrait, fc, components, iter) where Target
+ features = map(GI.getfeature(fc)) do feature
iter is updated by _reconstruct here
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
iter is updated by _reconstruct here
subgeom1, iter = _reconstruct(Target, GI.trait(subgeom), subgeom, components, iter)
+ subgeom1
+ end
+ return rebuild(geom, geoms), iter
+end
Apply f to the target geometry
_reconstruct(::Type{Target}, ::Trait, geom, components, iter) where {Target,Trait<:Target} =
+ iterate(components, iter)
Specific cases to avoid method ambiguity
_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)
Fail if we hit PointTrait without running f
_reconstruct(::Type{Target}, trait::GI.PointTrait, geom, components, iter) where Target =
+ throw(ArgumentError("target $Target not found, but reached a \`PointTrait\` leaf"))
+
+"""
+ 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)
+ haveZ = (GI.is3d(child) for child in child_geoms)
+ haveM = (GI.ismeasured(child) for child in child_geoms)
+
+ consistentZ = length(child_geoms) == 1 ? true : all(==(first(haveZ)), haveZ)
+ consistentM = length(child_geoms) == 1 ? true : all(==(first(haveM)), haveM)
+
+ if !consistentZ || !consistentM
+ @show consistentZ consistentM
+ @show GI.is3d.(child_geoms)
+ @show GI.ismeasured.(child_geoms)
+ throw(ArgumentError("child geometries do not have consistent 3d or measure attributes."))
+ end
+
+ hasZ = first(haveZ)
+ hasM = first(haveM)
+
+ return T{hasZ,hasM}(child_geoms; crs, extent)
+end
This page was generated using Literate.jl.
`,41))])}const B=k(r,[["render",F]]);export{A as __pageData,B as default}; diff --git a/previews/PR239/assets/source_src_other_primitives.md.BVzBrNUT.lean.js b/previews/PR239/assets/source_src_other_primitives.md.BVzBrNUT.lean.js new file mode 100644 index 000000000..9d232a003 --- /dev/null +++ b/previews/PR239/assets/source_src_other_primitives.md.BVzBrNUT.lean.js @@ -0,0 +1,131 @@ +import{_ as k,c as l,j as i,a,G as h,a5 as t,B as p,o as e}from"./chunks/framework.onQNwZ2I.js";const A=JSON.parse('{"title":"Other primitives (unwrap, flatten, etc)","description":"","frontmatter":{},"headers":[],"relativePath":"source/src/other_primitives.md","filePath":"source/src/other_primitives.md","lastUpdated":null}'),r={name:"source/src/other_primitives.md"},E={class:"jldocstring custom-block",open:""},d={class:"jldocstring custom-block",open:""},g={class:"jldocstring custom-block",open:""},y={class:"jldocstring custom-block",open:""};function F(o,s,c,C,u,m){const n=p("Badge");return e(),l("div",null,[s[12]||(s[12]=i("h1",{id:"Other-primitives-(unwrap,-flatten,-etc)",tabindex:"-1"},[a("Other primitives (unwrap, flatten, etc) "),i("a",{class:"header-anchor",href:"#Other-primitives-(unwrap,-flatten,-etc)","aria-label":'Permalink to "Other primitives (unwrap, flatten, etc) {#Other-primitives-(unwrap,-flatten,-etc)}"'},"")],-1)),s[13]||(s[13]=i("p",null,"This file defines the following primitives:",-1)),i("details",E,[i("summary",null,[s[0]||(s[0]=i("a",{id:"GeometryOpsCore.unwrap-source-src-other_primitives",href:"#GeometryOpsCore.unwrap-source-src-other_primitives"},[i("span",{class:"jlbinding"},"GeometryOpsCore.unwrap")],-1)),s[1]||(s[1]=a()),h(n,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[2]||(s[2]=t(`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.
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.
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
.
Usually used in combination with flatten
.
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)
',5))]),s[14]||(s[14]=t(`"""
+ 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)
Add dispatch argument for trait
unwrap(f, target::Type, geom) = unwrap(f, target, GI.trait(geom), geom)
Try to unwrap over iterables
unwrap(f, target::Type, ::Nothing, iterable) =
+ map(x -> unwrap(f, target, x), iterable)
Rewrap feature collections
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))
Apply f to the target geometry
unwrap(f, ::Type{Target}, ::Trait, geom) where {Target,Trait<:Target} = f(geom)
Fail if we hit PointTrait
unwrap(f, target::Type, trait::GI.PointTrait, geom) =
+ throw(ArgumentError("target $target not found, but reached a \`PointTrait\` leaf"))
Specific cases to avoid method ambiguity
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)
Try to flatten over iterables
function _flatten(f, ::Type{Target}, ::Nothing, iterable) where Target
+ if Tables.istable(iterable)
+ column = Tables.getcolumn(iterable, first(GI.geometrycolumns(iterable)))
+ Iterators.map(x -> _flatten(f, Target, x), column) |> Iterators.flatten
+ else
+ Iterators.map(x -> _flatten(f, Target, x), iterable) |> Iterators.flatten
+ end
+end
Flatten feature collections
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))
Apply f to the target geometry
_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)))
Fail if we hit PointTrait without running f
_flatten(f, ::Type{Target}, trait::GI.PointTrait, geom) where Target =
+ throw(ArgumentError("target $Target not found, but reached a \`PointTrait\` leaf"))
Specific cases to avoid method ambiguity
_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\`.
+
+Usually 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)
Try to reconstruct over iterables
function _reconstruct(::Type{Target}, ::Nothing, iterable, components, iter) where Target
+ vect = map(iterable) do x
iter is updated by _reconstruct here
obj, iter = _reconstruct(Target, x, components, iter)
+ obj
+ end
+ return vect, iter
+end
Reconstruct feature collections
function _reconstruct(::Type{Target}, ::GI.FeatureCollectionTrait, fc, components, iter) where Target
+ features = map(GI.getfeature(fc)) do feature
iter is updated by _reconstruct here
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
iter is updated by _reconstruct here
subgeom1, iter = _reconstruct(Target, GI.trait(subgeom), subgeom, components, iter)
+ subgeom1
+ end
+ return rebuild(geom, geoms), iter
+end
Apply f to the target geometry
_reconstruct(::Type{Target}, ::Trait, geom, components, iter) where {Target,Trait<:Target} =
+ iterate(components, iter)
Specific cases to avoid method ambiguity
_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)
Fail if we hit PointTrait without running f
_reconstruct(::Type{Target}, trait::GI.PointTrait, geom, components, iter) where Target =
+ throw(ArgumentError("target $Target not found, but reached a \`PointTrait\` leaf"))
+
+"""
+ 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)
+ haveZ = (GI.is3d(child) for child in child_geoms)
+ haveM = (GI.ismeasured(child) for child in child_geoms)
+
+ consistentZ = length(child_geoms) == 1 ? true : all(==(first(haveZ)), haveZ)
+ consistentM = length(child_geoms) == 1 ? true : all(==(first(haveM)), haveM)
+
+ if !consistentZ || !consistentM
+ @show consistentZ consistentM
+ @show GI.is3d.(child_geoms)
+ @show GI.ismeasured.(child_geoms)
+ throw(ArgumentError("child geometries do not have consistent 3d or measure attributes."))
+ end
+
+ hasZ = first(haveZ)
+ hasM = first(haveM)
+
+ return T{hasZ,hasM}(child_geoms; crs, extent)
+end
This page was generated using Literate.jl.
`,41))])}const B=k(r,[["render",F]]);export{A as __pageData,B as default}; diff --git a/previews/PR239/assets/source_src_types.md.BulrghgC.js b/previews/PR239/assets/source_src_types.md.BulrghgC.js new file mode 100644 index 000000000..21ac874a0 --- /dev/null +++ b/previews/PR239/assets/source_src_types.md.BulrghgC.js @@ -0,0 +1,111 @@ +import{_ as a,c as i,a5 as n,o as e}from"./chunks/framework.onQNwZ2I.js";const c=JSON.parse('{"title":"Types","description":"","frontmatter":{},"headers":[],"relativePath":"source/src/types.md","filePath":"source/src/types.md","lastUpdated":null}'),t={name:"source/src/types.md"};function l(p,s,h,k,r,o){return e(),i("div",null,s[0]||(s[0]=[n(`This defines core types that the GeometryOps ecosystem uses, and that are usable in more than just GeometryOps.
Manifold
A manifold is mathematically defined as a topological space that resembles Euclidean space locally.
In GeometryOps (and geodesy more generally), there are three manifolds we care about:
Planar
: the 2d plane, a completely Euclidean manifold
Spherical
: the unit sphere, but one where areas are multiplied by the radius of the Earth. This is not Euclidean globally, but all map projections attempt to represent the sphere on the Euclidean 2D plane to varying degrees of success.
Geodesic
: the ellipsoid, the closest we can come to representing the Earth by a simple geometric shape. Parametrized by semimajor_axis
and inv_flattening
.
Generally, we aim to have Linear
and Spherical
be operable everywhere, whereas Geodesic
will only apply in specific circumstances. Currently, those circumstances are area
and segmentize
, but this could be extended with time and https://github.com/JuliaGeo/SphericalGeodesics.jl.
export Planar, Spherical, Geodesic
+export TraitTarget
+export BoolsAsTypes, _True, _False, _booltype
+
+"""
+ abstract type Manifold
+
+A manifold is mathematically defined as a topological space that resembles Euclidean space locally.
+
+We use the manifold definition to define the space in which an operation should be performed, or where a geometry lies.
+
+Currently we have \`Planar\`, \`Spherical\`, and \`Geodesic\` manifolds.
+"""
+abstract type Manifold end
+
+"""
+ Planar()
+
+A planar manifold refers to the 2D Euclidean plane.
+
+Z coordinates may be accepted but will not influence geometry calculations, which
+are done purely on 2D geometry. This is the standard "2.5D" model used by e.g. GEOS.
+"""
+struct Planar <: Manifold
+end
+
+"""
+ Spherical(; radius)
+
+A spherical manifold means that the geometry is on the 3-sphere (but is represented by 2-D longitude and latitude).
+
+By default, the radius is defined as the mean radius of the Earth, \`6371008.8 m\`.
+
+# Extended help
+
+!!! note
+ The traditional definition of spherical coordinates in physics and mathematics,
+ \`\`r, \\\\theta, \\\\phi\`\`, uses the _colatitude_, that measures angular displacement from the \`z\`-axis.
+
+ Here, we use the geographic definition of longitude and latitude, meaning
+ that \`lon\` is longitude between -180 and 180, and \`lat\` is latitude between
+ \`-90\` (south pole) and \`90\` (north pole).
+"""
+Base.@kwdef struct Spherical{T} <: Manifold
+ radius::T = 6371008.8
+end
+
+"""
+ Geodesic(; semimajor_axis, inv_flattening)
+
+A geodesic manifold means that the geometry is on a 3-dimensional ellipsoid, parameterized by \`semimajor_axis\` (\`\`a\`\` in mathematical parlance)
+and \`inv_flattening\` (\`\`1/f\`\`).
+
+Usually, this is only relevant for area and segmentization calculations. It becomes more relevant as one grows closer to the poles (or equator).
+"""
+Base.@kwdef struct Geodesic{T} <: Manifold
+ semimajor_axis::T = 6378137.0
+ inv_flattening::T = 298.257223563
+end
TraitTarget
This struct holds a trait parameter or a union of trait parameters. It's essentially a way to construct unions.
"""
+ 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(...))
There are also type based constructors available, but that's not advised.
TraitTarget(GI.PointTrait)
+TraitTarget(Union{GI.LineStringTrait, GI.LinearRingTrait})
etc.
\`\`\`
+
+"""
+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)...}}()
+
+
+Base.in(::Trait, ::TraitTarget{Target}) where {Trait <: GI.AbstractTrait, Target} = Trait <: Target
BoolsAsTypes
In apply
and applyreduce
, we pass threading
and calc_extent
as types, not simple boolean values.
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 _booltype(::Bool)
method for this reason as well.
TODO: should we switch to Static.jl
?
"""
+ abstract type BoolsAsTypes
+
+"""
+abstract type BoolsAsTypes end
+
+"""
+ struct _True <: BoolsAsTypes
+
+A struct that means \`true\`.
+"""
+struct _True <: BoolsAsTypes end
+
+"""
+ struct _False <: BoolsAsTypes
+
+A struct that means \`false\`.
+"""
+struct _False <: BoolsAsTypes end
+
+"""
+ _booltype(x)
+
+Returns a \`BoolsAsTypes\` from \`x\`, whether it's a boolean or a BoolsAsTypes.
+"""
+function _booltype end
+
+@inline _booltype(x::Bool)::BoolsAsTypes = x ? _True() : _False()
+@inline _booltype(x::BoolsAsTypes)::BoolsAsTypes = x
This page was generated using Literate.jl.
`,24)]))}const F=a(t,[["render",l]]);export{c as __pageData,F as default}; diff --git a/previews/PR239/assets/source_src_types.md.BulrghgC.lean.js b/previews/PR239/assets/source_src_types.md.BulrghgC.lean.js new file mode 100644 index 000000000..21ac874a0 --- /dev/null +++ b/previews/PR239/assets/source_src_types.md.BulrghgC.lean.js @@ -0,0 +1,111 @@ +import{_ as a,c as i,a5 as n,o as e}from"./chunks/framework.onQNwZ2I.js";const c=JSON.parse('{"title":"Types","description":"","frontmatter":{},"headers":[],"relativePath":"source/src/types.md","filePath":"source/src/types.md","lastUpdated":null}'),t={name:"source/src/types.md"};function l(p,s,h,k,r,o){return e(),i("div",null,s[0]||(s[0]=[n(`This defines core types that the GeometryOps ecosystem uses, and that are usable in more than just GeometryOps.
Manifold
A manifold is mathematically defined as a topological space that resembles Euclidean space locally.
In GeometryOps (and geodesy more generally), there are three manifolds we care about:
Planar
: the 2d plane, a completely Euclidean manifold
Spherical
: the unit sphere, but one where areas are multiplied by the radius of the Earth. This is not Euclidean globally, but all map projections attempt to represent the sphere on the Euclidean 2D plane to varying degrees of success.
Geodesic
: the ellipsoid, the closest we can come to representing the Earth by a simple geometric shape. Parametrized by semimajor_axis
and inv_flattening
.
Generally, we aim to have Linear
and Spherical
be operable everywhere, whereas Geodesic
will only apply in specific circumstances. Currently, those circumstances are area
and segmentize
, but this could be extended with time and https://github.com/JuliaGeo/SphericalGeodesics.jl.
export Planar, Spherical, Geodesic
+export TraitTarget
+export BoolsAsTypes, _True, _False, _booltype
+
+"""
+ abstract type Manifold
+
+A manifold is mathematically defined as a topological space that resembles Euclidean space locally.
+
+We use the manifold definition to define the space in which an operation should be performed, or where a geometry lies.
+
+Currently we have \`Planar\`, \`Spherical\`, and \`Geodesic\` manifolds.
+"""
+abstract type Manifold end
+
+"""
+ Planar()
+
+A planar manifold refers to the 2D Euclidean plane.
+
+Z coordinates may be accepted but will not influence geometry calculations, which
+are done purely on 2D geometry. This is the standard "2.5D" model used by e.g. GEOS.
+"""
+struct Planar <: Manifold
+end
+
+"""
+ Spherical(; radius)
+
+A spherical manifold means that the geometry is on the 3-sphere (but is represented by 2-D longitude and latitude).
+
+By default, the radius is defined as the mean radius of the Earth, \`6371008.8 m\`.
+
+# Extended help
+
+!!! note
+ The traditional definition of spherical coordinates in physics and mathematics,
+ \`\`r, \\\\theta, \\\\phi\`\`, uses the _colatitude_, that measures angular displacement from the \`z\`-axis.
+
+ Here, we use the geographic definition of longitude and latitude, meaning
+ that \`lon\` is longitude between -180 and 180, and \`lat\` is latitude between
+ \`-90\` (south pole) and \`90\` (north pole).
+"""
+Base.@kwdef struct Spherical{T} <: Manifold
+ radius::T = 6371008.8
+end
+
+"""
+ Geodesic(; semimajor_axis, inv_flattening)
+
+A geodesic manifold means that the geometry is on a 3-dimensional ellipsoid, parameterized by \`semimajor_axis\` (\`\`a\`\` in mathematical parlance)
+and \`inv_flattening\` (\`\`1/f\`\`).
+
+Usually, this is only relevant for area and segmentization calculations. It becomes more relevant as one grows closer to the poles (or equator).
+"""
+Base.@kwdef struct Geodesic{T} <: Manifold
+ semimajor_axis::T = 6378137.0
+ inv_flattening::T = 298.257223563
+end
TraitTarget
This struct holds a trait parameter or a union of trait parameters. It's essentially a way to construct unions.
"""
+ 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(...))
There are also type based constructors available, but that's not advised.
TraitTarget(GI.PointTrait)
+TraitTarget(Union{GI.LineStringTrait, GI.LinearRingTrait})
etc.
\`\`\`
+
+"""
+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)...}}()
+
+
+Base.in(::Trait, ::TraitTarget{Target}) where {Trait <: GI.AbstractTrait, Target} = Trait <: Target
BoolsAsTypes
In apply
and applyreduce
, we pass threading
and calc_extent
as types, not simple boolean values.
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 _booltype(::Bool)
method for this reason as well.
TODO: should we switch to Static.jl
?
"""
+ abstract type BoolsAsTypes
+
+"""
+abstract type BoolsAsTypes end
+
+"""
+ struct _True <: BoolsAsTypes
+
+A struct that means \`true\`.
+"""
+struct _True <: BoolsAsTypes end
+
+"""
+ struct _False <: BoolsAsTypes
+
+A struct that means \`false\`.
+"""
+struct _False <: BoolsAsTypes end
+
+"""
+ _booltype(x)
+
+Returns a \`BoolsAsTypes\` from \`x\`, whether it's a boolean or a BoolsAsTypes.
+"""
+function _booltype end
+
+@inline _booltype(x::Bool)::BoolsAsTypes = x ? _True() : _False()
+@inline _booltype(x::BoolsAsTypes)::BoolsAsTypes = x
This page was generated using Literate.jl.
`,24)]))}const F=a(t,[["render",l]]);export{c as __pageData,F as default}; diff --git a/previews/PR239/assets/source_transformations_correction_closed_ring.md.DlzJm4wM.js b/previews/PR239/assets/source_transformations_correction_closed_ring.md.DlzJm4wM.js new file mode 100644 index 000000000..95f4acd86 --- /dev/null +++ b/previews/PR239/assets/source_transformations_correction_closed_ring.md.DlzJm4wM.js @@ -0,0 +1,30 @@ +import{_ as l,c as a,a5 as n,j as i,a as t,o as e}from"./chunks/framework.onQNwZ2I.js";const m=JSON.parse('{"title":"Closed Rings","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/correction/closed_ring.md","filePath":"source/transformations/correction/closed_ring.md","lastUpdated":null}'),h={name:"source/transformations/correction/closed_ring.md"},p={class:"MathJax",jax:"SVG",style:{direction:"ltr",position:"relative"}},k={style:{overflow:"visible","min-height":"1px","min-width":"1px","vertical-align":"-0.025ex"},xmlns:"http://www.w3.org/2000/svg",width:"1.357ex",height:"1.025ex",role:"img",focusable:"false",viewBox:"0 -442 600 453","aria-hidden":"true"},r={class:"MathJax",jax:"SVG",style:{direction:"ltr",position:"relative"}},o={style:{overflow:"visible","min-height":"1px","min-width":"1px","vertical-align":"-0.186ex"},xmlns:"http://www.w3.org/2000/svg",width:"5.254ex",height:"1.692ex",role:"img",focusable:"false",viewBox:"0 -666 2322.4 748","aria-hidden":"true"};function d(g,s,c,E,y,u){return e(),a("div",null,[s[7]||(s[7]=n(`export ClosedRing
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:
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)
even though it will look correct when visualized, and indeed appears correct.
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)
"""
+ 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))
the ring is closed, all hail the ring
return ring
+ else
Assemble the ring as a vector
tups = tuples.(GI.getpoint(ring))
Close the ring
push!(tups, tups[1])
Return an actual ring
return GI.LinearRing(tups)
+ end
+end
This page was generated using Literate.jl.
`,12))])}const C=l(h,[["render",d]]);export{m as __pageData,C as default}; diff --git a/previews/PR239/assets/source_transformations_correction_closed_ring.md.DlzJm4wM.lean.js b/previews/PR239/assets/source_transformations_correction_closed_ring.md.DlzJm4wM.lean.js new file mode 100644 index 000000000..95f4acd86 --- /dev/null +++ b/previews/PR239/assets/source_transformations_correction_closed_ring.md.DlzJm4wM.lean.js @@ -0,0 +1,30 @@ +import{_ as l,c as a,a5 as n,j as i,a as t,o as e}from"./chunks/framework.onQNwZ2I.js";const m=JSON.parse('{"title":"Closed Rings","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/correction/closed_ring.md","filePath":"source/transformations/correction/closed_ring.md","lastUpdated":null}'),h={name:"source/transformations/correction/closed_ring.md"},p={class:"MathJax",jax:"SVG",style:{direction:"ltr",position:"relative"}},k={style:{overflow:"visible","min-height":"1px","min-width":"1px","vertical-align":"-0.025ex"},xmlns:"http://www.w3.org/2000/svg",width:"1.357ex",height:"1.025ex",role:"img",focusable:"false",viewBox:"0 -442 600 453","aria-hidden":"true"},r={class:"MathJax",jax:"SVG",style:{direction:"ltr",position:"relative"}},o={style:{overflow:"visible","min-height":"1px","min-width":"1px","vertical-align":"-0.186ex"},xmlns:"http://www.w3.org/2000/svg",width:"5.254ex",height:"1.692ex",role:"img",focusable:"false",viewBox:"0 -666 2322.4 748","aria-hidden":"true"};function d(g,s,c,E,y,u){return e(),a("div",null,[s[7]||(s[7]=n(`export ClosedRing
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:
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)
even though it will look correct when visualized, and indeed appears correct.
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)
"""
+ 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))
the ring is closed, all hail the ring
return ring
+ else
Assemble the ring as a vector
tups = tuples.(GI.getpoint(ring))
Close the ring
push!(tups, tups[1])
Return an actual ring
return GI.LinearRing(tups)
+ end
+end
This page was generated using Literate.jl.
`,12))])}const C=l(h,[["render",d]]);export{m as __pageData,C as default}; diff --git a/previews/PR239/assets/source_transformations_correction_geometry_correction.md.DSGv3LiX.js b/previews/PR239/assets/source_transformations_correction_geometry_correction.md.DSGv3LiX.js new file mode 100644 index 000000000..e79621afd --- /dev/null +++ b/previews/PR239/assets/source_transformations_correction_geometry_correction.md.DSGv3LiX.js @@ -0,0 +1,31 @@ +import{_ as r,c as o,a5 as e,j as i,a as t,G as n,B as l,o as p}from"./chunks/framework.onQNwZ2I.js";const G=JSON.parse('{"title":"Geometry Corrections","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/correction/geometry_correction.md","filePath":"source/transformations/correction/geometry_correction.md","lastUpdated":null}'),h={name:"source/transformations/correction/geometry_correction.md"},k={class:"jldocstring custom-block",open:""},c={class:"jldocstring custom-block",open:""},d={class:"jldocstring custom-block",open:""},y={class:"jldocstring custom-block",open:""},g={class:"jldocstring custom-block",open:""};function m(E,s,u,F,f,C){const a=l("Badge");return p(),o("div",null,[s[15]||(s[15]=e('export fix
This file simply defines the GeometryCorrection
abstract type, and the interface that any GeometryCorrection
must implement.
A geometry correction is a transformation that is applied to a geometry to correct it in some way.
For example, a ClosedRing
correction might be applied to a Polygon
to ensure that its exterior ring is closed.
All 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).
See below for the full interface specification.
',8)),i("details",k,[i("summary",null,[s[0]||(s[0]=i("a",{id:"GeometryOps.GeometryCorrection-source-transformations-correction-geometry_correction",href:"#GeometryOps.GeometryCorrection-source-transformations-correction-geometry_correction"},[i("span",{class:"jlbinding"},"GeometryOps.GeometryCorrection")],-1)),s[1]||(s[1]=t()),n(a,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[2]||(s[2]=e('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.
Any geometry correction must implement the interface as given above.
"""
+ 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
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
.
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.
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
.
export fix
This file simply defines the GeometryCorrection
abstract type, and the interface that any GeometryCorrection
must implement.
A geometry correction is a transformation that is applied to a geometry to correct it in some way.
For example, a ClosedRing
correction might be applied to a Polygon
to ensure that its exterior ring is closed.
All 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).
See below for the full interface specification.
',8)),i("details",k,[i("summary",null,[s[0]||(s[0]=i("a",{id:"GeometryOps.GeometryCorrection-source-transformations-correction-geometry_correction",href:"#GeometryOps.GeometryCorrection-source-transformations-correction-geometry_correction"},[i("span",{class:"jlbinding"},"GeometryOps.GeometryCorrection")],-1)),s[1]||(s[1]=t()),n(a,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[2]||(s[2]=e('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.
Any geometry correction must implement the interface as given above.
"""
+ 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
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
.
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.
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
.
export UnionIntersectingPolygons
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 requirement 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 UnionIntersectingPolygons
correction.
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:
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)
given that the two sub-polygons are the exact same shape.
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)
You can see that the the multipolygon now only contains one sub-polygon, rather than the two identical ones provided.
"""
+ 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
Combine any sub-polygons that intersect
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
Break apart any sub-polygons that intersect
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
This page was generated using Literate.jl.
`,22)]))}const d=i(t,[["render",h]]);export{E as __pageData,d as default}; diff --git a/previews/PR239/assets/source_transformations_correction_intersecting_polygons.md.CLhMqjHy.lean.js b/previews/PR239/assets/source_transformations_correction_intersecting_polygons.md.CLhMqjHy.lean.js new file mode 100644 index 000000000..440caed24 --- /dev/null +++ b/previews/PR239/assets/source_transformations_correction_intersecting_polygons.md.CLhMqjHy.lean.js @@ -0,0 +1,97 @@ +import{_ as i,c as a,a5 as n,o as l}from"./chunks/framework.onQNwZ2I.js";const E=JSON.parse('{"title":"Intersecting Polygons","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/correction/intersecting_polygons.md","filePath":"source/transformations/correction/intersecting_polygons.md","lastUpdated":null}'),t={name:"source/transformations/correction/intersecting_polygons.md"};function h(p,s,e,k,r,g){return l(),a("div",null,s[0]||(s[0]=[n(`export UnionIntersectingPolygons
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 requirement 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 UnionIntersectingPolygons
correction.
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:
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)
given that the two sub-polygons are the exact same shape.
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)
You can see that the the multipolygon now only contains one sub-polygon, rather than the two identical ones provided.
"""
+ 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
Combine any sub-polygons that intersect
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
Break apart any sub-polygons that intersect
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
This page was generated using Literate.jl.
`,22)]))}const d=i(t,[["render",h]]);export{E as __pageData,d as default}; diff --git a/previews/PR239/assets/source_transformations_extent.md.CrI7_2Lj.js b/previews/PR239/assets/source_transformations_extent.md.CrI7_2Lj.js new file mode 100644 index 000000000..6452b6fb3 --- /dev/null +++ b/previews/PR239/assets/source_transformations_extent.md.CrI7_2Lj.js @@ -0,0 +1,13 @@ +import{_ as e,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const k=JSON.parse('{"title":"Extent embedding","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/extent.md","filePath":"source/transformations/extent.md","lastUpdated":null}'),i={name:"source/transformations/extent.md"};function l(p,s,r,h,d,o){return t(),a("div",null,s[0]||(s[0]=[n(`"""
+ 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_KEYWORD
+$CRS_KEYWORD
+"""
+embed_extent(x; threaded=false, crs=nothing) =
+ apply(identity, GI.PointTrait(), x; calc_extent=true, threaded, crs)
This page was generated using Literate.jl.
`,6)]))}const g=e(i,[["render",l]]);export{k as __pageData,g as default}; diff --git a/previews/PR239/assets/source_transformations_extent.md.CrI7_2Lj.lean.js b/previews/PR239/assets/source_transformations_extent.md.CrI7_2Lj.lean.js new file mode 100644 index 000000000..6452b6fb3 --- /dev/null +++ b/previews/PR239/assets/source_transformations_extent.md.CrI7_2Lj.lean.js @@ -0,0 +1,13 @@ +import{_ as e,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const k=JSON.parse('{"title":"Extent embedding","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/extent.md","filePath":"source/transformations/extent.md","lastUpdated":null}'),i={name:"source/transformations/extent.md"};function l(p,s,r,h,d,o){return t(),a("div",null,s[0]||(s[0]=[n(`"""
+ 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_KEYWORD
+$CRS_KEYWORD
+"""
+embed_extent(x; threaded=false, crs=nothing) =
+ apply(identity, GI.PointTrait(), x; calc_extent=true, threaded, crs)
This page was generated using Literate.jl.
`,6)]))}const g=e(i,[["render",l]]);export{k as __pageData,g as default}; diff --git a/previews/PR239/assets/source_transformations_flip.md.CbHFKr9B.js b/previews/PR239/assets/source_transformations_flip.md.CbHFKr9B.js new file mode 100644 index 000000000..d979792ae --- /dev/null +++ b/previews/PR239/assets/source_transformations_flip.md.CbHFKr9B.js @@ -0,0 +1,22 @@ +import{_ as i,c as a,a5 as n,o as p}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"Coordinate flipping","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/flip.md","filePath":"source/transformations/flip.md","lastUpdated":null}'),t={name:"source/transformations/flip.md"};function l(h,s,e,k,r,d){return p(),a("div",null,s[0]||(s[0]=[n(`This is a simple example of how to use the 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
This page was generated using Literate.jl.
`,5)]))}const E=i(t,[["render",l]]);export{g as __pageData,E as default}; diff --git a/previews/PR239/assets/source_transformations_flip.md.CbHFKr9B.lean.js b/previews/PR239/assets/source_transformations_flip.md.CbHFKr9B.lean.js new file mode 100644 index 000000000..d979792ae --- /dev/null +++ b/previews/PR239/assets/source_transformations_flip.md.CbHFKr9B.lean.js @@ -0,0 +1,22 @@ +import{_ as i,c as a,a5 as n,o as p}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"Coordinate flipping","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/flip.md","filePath":"source/transformations/flip.md","lastUpdated":null}'),t={name:"source/transformations/flip.md"};function l(h,s,e,k,r,d){return p(),a("div",null,s[0]||(s[0]=[n(`This is a simple example of how to use the 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
This page was generated using Literate.jl.
`,5)]))}const E=i(t,[["render",l]]);export{g as __pageData,E as default}; diff --git a/previews/PR239/assets/source_transformations_forcedims.md._lugiyHg.js b/previews/PR239/assets/source_transformations_forcedims.md._lugiyHg.js new file mode 100644 index 000000000..8d62b14bb --- /dev/null +++ b/previews/PR239/assets/source_transformations_forcedims.md._lugiyHg.js @@ -0,0 +1,5 @@ +import{_ as i,c as a,a5 as t,o as n}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/forcedims.md","filePath":"source/transformations/forcedims.md","lastUpdated":null}'),e={name:"source/transformations/forcedims.md"};function p(h,s,l,k,r,d){return n(),a("div",null,s[0]||(s[0]=[t(`function forcexy(geom)
+ return GO.apply(GO.GI.PointTrait(), geom) do point
+ (GI.x(point), GI.y(point))
+ end
+end
This page was generated using Literate.jl.
`,3)]))}const E=i(e,[["render",p]]);export{g as __pageData,E as default}; diff --git a/previews/PR239/assets/source_transformations_forcedims.md._lugiyHg.lean.js b/previews/PR239/assets/source_transformations_forcedims.md._lugiyHg.lean.js new file mode 100644 index 000000000..8d62b14bb --- /dev/null +++ b/previews/PR239/assets/source_transformations_forcedims.md._lugiyHg.lean.js @@ -0,0 +1,5 @@ +import{_ as i,c as a,a5 as t,o as n}from"./chunks/framework.onQNwZ2I.js";const g=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/forcedims.md","filePath":"source/transformations/forcedims.md","lastUpdated":null}'),e={name:"source/transformations/forcedims.md"};function p(h,s,l,k,r,d){return n(),a("div",null,s[0]||(s[0]=[t(`function forcexy(geom)
+ return GO.apply(GO.GI.PointTrait(), geom) do point
+ (GI.x(point), GI.y(point))
+ end
+end
This page was generated using Literate.jl.
`,3)]))}const E=i(e,[["render",p]]);export{g as __pageData,E as default}; diff --git a/previews/PR239/assets/source_transformations_reproject.md.DZgumE25.js b/previews/PR239/assets/source_transformations_reproject.md.DZgumE25.js new file mode 100644 index 000000000..891cb589b --- /dev/null +++ b/previews/PR239/assets/source_transformations_reproject.md.DZgumE25.js @@ -0,0 +1 @@ +import{_ as i,c as a,a5 as e,o as n}from"./chunks/framework.onQNwZ2I.js";const c=JSON.parse('{"title":"Geometry reprojection","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/reproject.md","filePath":"source/transformations/reproject.md","lastUpdated":null}'),t={name:"source/transformations/reproject.md"};function p(l,s,r,h,k,o){return n(),a("div",null,s[0]||(s[0]=[e('export reproject
This file is pretty simple - it simply reprojects a geometry pointwise from one CRS to another. It uses the Proj
package for the transformation, but this could be moved to an extension if needed.
Note that the actual implementation is in the GeometryOpsProjExt
extension module.
This works using the 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 reference system, as a GeoFormatTypes.jl object or a string.\n- `target_crs`: the target coordinate reference 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 retrievable 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
We also inject a method error handler, which prints a suggestion if the Proj extension is not loaded.
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
This page was generated using Literate.jl.
',11)]))}const g=i(t,[["render",p]]);export{c as __pageData,g as default}; diff --git a/previews/PR239/assets/source_transformations_reproject.md.DZgumE25.lean.js b/previews/PR239/assets/source_transformations_reproject.md.DZgumE25.lean.js new file mode 100644 index 000000000..891cb589b --- /dev/null +++ b/previews/PR239/assets/source_transformations_reproject.md.DZgumE25.lean.js @@ -0,0 +1 @@ +import{_ as i,c as a,a5 as e,o as n}from"./chunks/framework.onQNwZ2I.js";const c=JSON.parse('{"title":"Geometry reprojection","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/reproject.md","filePath":"source/transformations/reproject.md","lastUpdated":null}'),t={name:"source/transformations/reproject.md"};function p(l,s,r,h,k,o){return n(),a("div",null,s[0]||(s[0]=[e('export reproject
This file is pretty simple - it simply reprojects a geometry pointwise from one CRS to another. It uses the Proj
package for the transformation, but this could be moved to an extension if needed.
Note that the actual implementation is in the GeometryOpsProjExt
extension module.
This works using the 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 reference system, as a GeoFormatTypes.jl object or a string.\n- `target_crs`: the target coordinate reference 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 retrievable 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
We also inject a method error handler, which prints a suggestion if the Proj extension is not loaded.
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
This page was generated using Literate.jl.
',11)]))}const g=i(t,[["render",p]]);export{c as __pageData,g as default}; diff --git a/previews/PR239/assets/source_transformations_segmentize.md.DQ-jJDsI.js b/previews/PR239/assets/source_transformations_segmentize.md.DQ-jJDsI.js new file mode 100644 index 000000000..83598bc44 --- /dev/null +++ b/previews/PR239/assets/source_transformations_segmentize.md.DQ-jJDsI.js @@ -0,0 +1,161 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/xnfkjof.D5-bot8v.png",l="/GeometryOps.jl/previews/PR239/assets/eozeurm.DGf_4PiA.png",o=JSON.parse('{"title":"Segmentize","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/segmentize.md","filePath":"source/transformations/segmentize.md","lastUpdated":null}'),k={name:"source/transformations/segmentize.md"};function e(p,s,r,d,E,g){return h(),a("div",null,s[0]||(s[0]=[n(`export segmentize
+export LinearSegments, GeodesicSegments
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.
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)
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.
using Proj # required to activate the \`GeodesicSegments\` method!
+geodesic = GO.segmentize(GO.GeodesicSegments(max_distance = 1000), rectangle)
+length(GI.getpoint(geodesic) |> collect)
3585
This has a lot of points! It's important to keep in mind that the max_distance
is in meters, so this is a very fine-grained segmentation.
Now, let's see what they look like! To make this fair, we'll use approximately the same number of points for both.
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
There are two methods available for segmentizing geometries at the moment:
Missing docstring.
Missing docstring for LinearSegments
. Check Documenter's build log for details.
Missing docstring.
Missing docstring for GeodesicSegments
. Check Documenter's build log for details.
We benchmark our method against LibGEOS's 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
Add an error hint for GeodesicSegments if Proj is not loaded!
function _geodesic_segments_error_hinter(io, exc, argtypes, kwargs)
+ if isnothing(Base.get_extension(GeometryOps, :GeometryOpsProjExt)) && exc.f == GeodesicSegments
+ print(io, "\\n\\nThe \`Geodesic\` 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
"""
+ segmentize([method = Planar()], 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::Manifold = Planar()\`: The method to use for segmentizing the geometry. At the moment, only \`Planar\` (assumes a flat plane) and \`Geodesic\` (assumes geometry on the ellipsoidal Earth and uses Vincenty's formulae) are available.
+- \`geom\`: The geometry to segmentize. Must be a \`LineString\`, \`LinearRing\`, \`Polygon\`, \`MultiPolygon\`, or \`GeometryCollection\`, or some vector or table of those.
+- \`max_distance::Real\`: The maximum distance between vertices in the geometry. **Beware: for \`Planar\`, this is in the units of the geometry, but for \`Geodesic\` and \`Spherical\` it's in units of the radius of the sphere.**
+
+Returns a geometry of similar type to the input geometry, but resampled.
+"""
+function segmentize(geom; max_distance, threaded::Union{Bool, BoolsAsTypes} = _False())
+ return segmentize(Planar(), geom; max_distance, threaded = _booltype(threaded))
+end
allow three-arg method as well, just in case
segmentize(geom, max_distance::Real; threaded = _False()) = segmentize(Planar(), geom, max_distance; threaded)
+segmentize(method::Manifold, geom, max_distance::Real; threaded = _False()) = segmentize(Planar(), geom; max_distance, threaded)
generic implementation
function segmentize(method::Manifold, geom; max_distance, threaded::Union{Bool, BoolsAsTypes} = _False())
+ @assert max_distance > 0 "\`max_distance\` should be positive and nonzero! Found $(method.max_distance)."
+ _segmentize_function(geom) = _segmentize(method, geom, GI.trait(geom); max_distance)
+ return apply(_segmentize_function, TraitTarget(GI.LinearRingTrait(), GI.LineStringTrait()), geom; threaded)
+end
+
+function segmentize(method::SegmentizeMethod, geom; threaded::Union{Bool, BoolsAsTypes} = _False())
+ @warn "\`segmentize(method::$(typeof(method)), geom) is deprecated; use \`segmentize($(method isa LinearSegments ? "Planar()" : "Geodesic()"), geom; max_distance, threaded) instead!" maxlog=3
+ @assert method.max_distance > 0 "\`max_distance\` should be positive and nonzero! Found $(method.max_distance)."
+ new_method = method isa LinearSegments ? Planar() : Geodesic()
+ segmentize(new_method, geom; max_distance = method.max_distance, 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{Planar, Spherical}, geom, T::Union{GI.LineStringTrait, GI.LinearRingTrait}; max_distance)
+ 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; max_distance)
+ x1, y1 = x2, y2
+ end
+ return rebuild(geom, new_coords)
+end
+
+function _fill_linear_kernel!(::Planar, new_coords::Vector, x1, y1, x2, y2; max_distance)
+ dx, dy = x2 - x1, y2 - y1
+ distance = hypot(dx, dy) # this is a more stable way to compute the Euclidean distance
+ if distance > max_distance
+ n_segments = ceil(Int, distance / max_distance)
+ for i in 1:(n_segments - 1)
+ t = i / n_segments
+ push!(new_coords, (x1 + t * dx, y1 + t * dy))
+ end
+ end
End the line with the original coordinate, to avoid any multiplication errors.
push!(new_coords, (x2, y2))
+ return nothing
+end
Note
The
_fill_linear_kernel
definition forGeodesicSegments
is in theGeometryOpsProjExt
extension module, in thesegmentize.jl
file.
This page was generated using Literate.jl.
`,39)]))}const F=i(k,[["render",e]]);export{o as __pageData,F as default}; diff --git a/previews/PR239/assets/source_transformations_segmentize.md.DQ-jJDsI.lean.js b/previews/PR239/assets/source_transformations_segmentize.md.DQ-jJDsI.lean.js new file mode 100644 index 000000000..83598bc44 --- /dev/null +++ b/previews/PR239/assets/source_transformations_segmentize.md.DQ-jJDsI.lean.js @@ -0,0 +1,161 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/xnfkjof.D5-bot8v.png",l="/GeometryOps.jl/previews/PR239/assets/eozeurm.DGf_4PiA.png",o=JSON.parse('{"title":"Segmentize","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/segmentize.md","filePath":"source/transformations/segmentize.md","lastUpdated":null}'),k={name:"source/transformations/segmentize.md"};function e(p,s,r,d,E,g){return h(),a("div",null,s[0]||(s[0]=[n(`export segmentize
+export LinearSegments, GeodesicSegments
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.
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)
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.
using Proj # required to activate the \`GeodesicSegments\` method!
+geodesic = GO.segmentize(GO.GeodesicSegments(max_distance = 1000), rectangle)
+length(GI.getpoint(geodesic) |> collect)
3585
This has a lot of points! It's important to keep in mind that the max_distance
is in meters, so this is a very fine-grained segmentation.
Now, let's see what they look like! To make this fair, we'll use approximately the same number of points for both.
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
There are two methods available for segmentizing geometries at the moment:
Missing docstring.
Missing docstring for LinearSegments
. Check Documenter's build log for details.
Missing docstring.
Missing docstring for GeodesicSegments
. Check Documenter's build log for details.
We benchmark our method against LibGEOS's 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
Add an error hint for GeodesicSegments if Proj is not loaded!
function _geodesic_segments_error_hinter(io, exc, argtypes, kwargs)
+ if isnothing(Base.get_extension(GeometryOps, :GeometryOpsProjExt)) && exc.f == GeodesicSegments
+ print(io, "\\n\\nThe \`Geodesic\` 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
"""
+ segmentize([method = Planar()], 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::Manifold = Planar()\`: The method to use for segmentizing the geometry. At the moment, only \`Planar\` (assumes a flat plane) and \`Geodesic\` (assumes geometry on the ellipsoidal Earth and uses Vincenty's formulae) are available.
+- \`geom\`: The geometry to segmentize. Must be a \`LineString\`, \`LinearRing\`, \`Polygon\`, \`MultiPolygon\`, or \`GeometryCollection\`, or some vector or table of those.
+- \`max_distance::Real\`: The maximum distance between vertices in the geometry. **Beware: for \`Planar\`, this is in the units of the geometry, but for \`Geodesic\` and \`Spherical\` it's in units of the radius of the sphere.**
+
+Returns a geometry of similar type to the input geometry, but resampled.
+"""
+function segmentize(geom; max_distance, threaded::Union{Bool, BoolsAsTypes} = _False())
+ return segmentize(Planar(), geom; max_distance, threaded = _booltype(threaded))
+end
allow three-arg method as well, just in case
segmentize(geom, max_distance::Real; threaded = _False()) = segmentize(Planar(), geom, max_distance; threaded)
+segmentize(method::Manifold, geom, max_distance::Real; threaded = _False()) = segmentize(Planar(), geom; max_distance, threaded)
generic implementation
function segmentize(method::Manifold, geom; max_distance, threaded::Union{Bool, BoolsAsTypes} = _False())
+ @assert max_distance > 0 "\`max_distance\` should be positive and nonzero! Found $(method.max_distance)."
+ _segmentize_function(geom) = _segmentize(method, geom, GI.trait(geom); max_distance)
+ return apply(_segmentize_function, TraitTarget(GI.LinearRingTrait(), GI.LineStringTrait()), geom; threaded)
+end
+
+function segmentize(method::SegmentizeMethod, geom; threaded::Union{Bool, BoolsAsTypes} = _False())
+ @warn "\`segmentize(method::$(typeof(method)), geom) is deprecated; use \`segmentize($(method isa LinearSegments ? "Planar()" : "Geodesic()"), geom; max_distance, threaded) instead!" maxlog=3
+ @assert method.max_distance > 0 "\`max_distance\` should be positive and nonzero! Found $(method.max_distance)."
+ new_method = method isa LinearSegments ? Planar() : Geodesic()
+ segmentize(new_method, geom; max_distance = method.max_distance, 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{Planar, Spherical}, geom, T::Union{GI.LineStringTrait, GI.LinearRingTrait}; max_distance)
+ 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; max_distance)
+ x1, y1 = x2, y2
+ end
+ return rebuild(geom, new_coords)
+end
+
+function _fill_linear_kernel!(::Planar, new_coords::Vector, x1, y1, x2, y2; max_distance)
+ dx, dy = x2 - x1, y2 - y1
+ distance = hypot(dx, dy) # this is a more stable way to compute the Euclidean distance
+ if distance > max_distance
+ n_segments = ceil(Int, distance / max_distance)
+ for i in 1:(n_segments - 1)
+ t = i / n_segments
+ push!(new_coords, (x1 + t * dx, y1 + t * dy))
+ end
+ end
End the line with the original coordinate, to avoid any multiplication errors.
push!(new_coords, (x2, y2))
+ return nothing
+end
Note
The
_fill_linear_kernel
definition forGeodesicSegments
is in theGeometryOpsProjExt
extension module, in thesegmentize.jl
file.
This page was generated using Literate.jl.
`,39)]))}const F=i(k,[["render",e]]);export{o as __pageData,F as default}; diff --git a/previews/PR239/assets/source_transformations_simplify.md.CnWLEC8c.js b/previews/PR239/assets/source_transformations_simplify.md.CnWLEC8c.js new file mode 100644 index 000000000..b2b60e12a --- /dev/null +++ b/previews/PR239/assets/source_transformations_simplify.md.CnWLEC8c.js @@ -0,0 +1,490 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const l="/GeometryOps.jl/previews/PR239/assets/evhqnlv.Bglvb-jp.png",k="/GeometryOps.jl/previews/PR239/assets/tsgqxnn.B94PsR1K.png",t="/GeometryOps.jl/previews/PR239/assets/umfmfiz.Da39FuJg.png",p="/GeometryOps.jl/previews/PR239/assets/twuyjor.DYrZOYql.png",c=JSON.parse('{"title":"Geometry simplification","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/simplify.md","filePath":"source/transformations/simplify.md","lastUpdated":null}'),e={name:"source/transformations/simplify.md"};function E(r,s,d,g,y,F){return h(),a("div",null,s[0]||(s[0]=[n(`This file holds implementations for the RadialDistance, Douglas-Peucker, and Visvalingam-Whyatt algorithms for simplifying geometries (specifically for polygons and lines).
The GEOS extension also allows for GEOS's topology preserving simplification as well as Douglas-Peucker simplification implemented in GEOS. Call this by passing GEOS(; method = :TopologyPreserve)
or GEOS(; method = :DouglasPeucker)
to the algorithm.
A quick and dirty example is:
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
We benchmark these methods against LibGEOS's 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
+using NaturalEarth
+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 = NaturalEarth.naturalearth("admin_0_countries", 10)
+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")
This is the complex polygon we'll be benchmarking.
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 decreasing 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.
+$APPLY_KEYWORDS
+
+
+Keywords for DouglasPeucker are allowed when no algorithm is specified:
+
+$DOUGLAS_PEUCKER_KEYWORDS
Example
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)
output
6
+\`\`\`
+"""
+simplify(alg::SimplifyAlg, data; kw...) = _simplify(alg, data; kw...)
+simplify(alg::GEOS, data; kw...) = _simplify(alg, data; kw...)
Default algorithm is DouglasPeucker
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 simplification to all curves, multipoints, and
+points, reconstructing everything else around them. =#
+function _simplify(alg::Union{SimplifyAlg, GEOS}, 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
"""
+ 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 unnecessary 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)
square tolerance for reduced computation
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
"""
+ 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 unnecessary 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)
square tolerance for reduced computation
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
Determine stopping criteria
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)
Set up queue
queue = Vector{Tuple{Int, Int, Int, Float64}}()
+ queue_idx, queue_dist = 0, zero(Float64)
+ len_queue = 0
Set up results vector
results = Vector{Int}(undef, max_points + (preserve_endpoint ? 0 : 1))
+ results[1], results[2] = 1, npoints
Loop through points until stopping criteria are fulfilled
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)
Add next point to results
i += 1
+ results[i] = max_idx
Determine which point to add next by checking left and right of point
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)
Add and remove values from queue
if queue_dist > left_dist && queue_dist > right_dist
Value in queue is next value to add to results
start_idx, max_idx, end_idx, max_dist = queue[queue_idx]
Add left and/or right values to queue or delete used queue value
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
Determine new maximum queue value
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
Check start/endpoint distance to other points to see if it meets criteria
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)
Remove start point and replace with second point
if endpt_dist < max_tol
+ results[i] = results[2]
+ sorted_results = @view results[2:i]
+ end
+ else
Remove start point and add point with maximum distance still remaining
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 connecting 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
"""
+ 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 unnecessary 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)
double tolerance for reduced computation
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
Calculates double the area of a triangle given its vertices
_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]))
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
Check SimplifyAlgs inputs to make sure they are valid for below algorithms
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
This page was generated using Literate.jl.
`,71)]))}const C=i(e,[["render",E]]);export{c as __pageData,C as default}; diff --git a/previews/PR239/assets/source_transformations_simplify.md.CnWLEC8c.lean.js b/previews/PR239/assets/source_transformations_simplify.md.CnWLEC8c.lean.js new file mode 100644 index 000000000..b2b60e12a --- /dev/null +++ b/previews/PR239/assets/source_transformations_simplify.md.CnWLEC8c.lean.js @@ -0,0 +1,490 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const l="/GeometryOps.jl/previews/PR239/assets/evhqnlv.Bglvb-jp.png",k="/GeometryOps.jl/previews/PR239/assets/tsgqxnn.B94PsR1K.png",t="/GeometryOps.jl/previews/PR239/assets/umfmfiz.Da39FuJg.png",p="/GeometryOps.jl/previews/PR239/assets/twuyjor.DYrZOYql.png",c=JSON.parse('{"title":"Geometry simplification","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/simplify.md","filePath":"source/transformations/simplify.md","lastUpdated":null}'),e={name:"source/transformations/simplify.md"};function E(r,s,d,g,y,F){return h(),a("div",null,s[0]||(s[0]=[n(`This file holds implementations for the RadialDistance, Douglas-Peucker, and Visvalingam-Whyatt algorithms for simplifying geometries (specifically for polygons and lines).
The GEOS extension also allows for GEOS's topology preserving simplification as well as Douglas-Peucker simplification implemented in GEOS. Call this by passing GEOS(; method = :TopologyPreserve)
or GEOS(; method = :DouglasPeucker)
to the algorithm.
A quick and dirty example is:
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
We benchmark these methods against LibGEOS's 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
+using NaturalEarth
+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 = NaturalEarth.naturalearth("admin_0_countries", 10)
+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")
This is the complex polygon we'll be benchmarking.
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 decreasing 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.
+$APPLY_KEYWORDS
+
+
+Keywords for DouglasPeucker are allowed when no algorithm is specified:
+
+$DOUGLAS_PEUCKER_KEYWORDS
Example
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)
output
6
+\`\`\`
+"""
+simplify(alg::SimplifyAlg, data; kw...) = _simplify(alg, data; kw...)
+simplify(alg::GEOS, data; kw...) = _simplify(alg, data; kw...)
Default algorithm is DouglasPeucker
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 simplification to all curves, multipoints, and
+points, reconstructing everything else around them. =#
+function _simplify(alg::Union{SimplifyAlg, GEOS}, 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
"""
+ 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 unnecessary 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)
square tolerance for reduced computation
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
"""
+ 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 unnecessary 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)
square tolerance for reduced computation
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
Determine stopping criteria
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)
Set up queue
queue = Vector{Tuple{Int, Int, Int, Float64}}()
+ queue_idx, queue_dist = 0, zero(Float64)
+ len_queue = 0
Set up results vector
results = Vector{Int}(undef, max_points + (preserve_endpoint ? 0 : 1))
+ results[1], results[2] = 1, npoints
Loop through points until stopping criteria are fulfilled
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)
Add next point to results
i += 1
+ results[i] = max_idx
Determine which point to add next by checking left and right of point
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)
Add and remove values from queue
if queue_dist > left_dist && queue_dist > right_dist
Value in queue is next value to add to results
start_idx, max_idx, end_idx, max_dist = queue[queue_idx]
Add left and/or right values to queue or delete used queue value
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
Determine new maximum queue value
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
Check start/endpoint distance to other points to see if it meets criteria
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)
Remove start point and replace with second point
if endpt_dist < max_tol
+ results[i] = results[2]
+ sorted_results = @view results[2:i]
+ end
+ else
Remove start point and add point with maximum distance still remaining
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 connecting 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
"""
+ 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 unnecessary 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)
double tolerance for reduced computation
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
Calculates double the area of a triangle given its vertices
_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]))
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
Check SimplifyAlgs inputs to make sure they are valid for below algorithms
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
This page was generated using Literate.jl.
`,71)]))}const C=i(e,[["render",E]]);export{c as __pageData,C as default}; diff --git a/previews/PR239/assets/source_transformations_transform.md.BWclfchr.js b/previews/PR239/assets/source_transformations_transform.md.BWclfchr.js new file mode 100644 index 000000000..4ece6c0b6 --- /dev/null +++ b/previews/PR239/assets/source_transformations_transform.md.BWclfchr.js @@ -0,0 +1,55 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const F=JSON.parse('{"title":"Pointwise transformation","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/transform.md","filePath":"source/transformations/transform.md","lastUpdated":null}'),l={name:"source/transformations/transform.md"};function e(p,s,h,r,k,o){return t(),a("div",null,s[0]||(s[0]=[n(`"""
+ 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 actually 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
This page was generated using Literate.jl.
`,4)]))}const c=i(l,[["render",e]]);export{F as __pageData,c as default}; diff --git a/previews/PR239/assets/source_transformations_transform.md.BWclfchr.lean.js b/previews/PR239/assets/source_transformations_transform.md.BWclfchr.lean.js new file mode 100644 index 000000000..4ece6c0b6 --- /dev/null +++ b/previews/PR239/assets/source_transformations_transform.md.BWclfchr.lean.js @@ -0,0 +1,55 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const F=JSON.parse('{"title":"Pointwise transformation","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/transform.md","filePath":"source/transformations/transform.md","lastUpdated":null}'),l={name:"source/transformations/transform.md"};function e(p,s,h,r,k,o){return t(),a("div",null,s[0]||(s[0]=[n(`"""
+ 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 actually 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
This page was generated using Literate.jl.
`,4)]))}const c=i(l,[["render",e]]);export{F as __pageData,c as default}; diff --git a/previews/PR239/assets/source_transformations_tuples.md.BFXQMbxF.js b/previews/PR239/assets/source_transformations_tuples.md.BFXQMbxF.js new file mode 100644 index 000000000..42ae74220 --- /dev/null +++ b/previews/PR239/assets/source_transformations_tuples.md.BFXQMbxF.js @@ -0,0 +1,19 @@ +import{_ as a,c as n,a5 as i,o as e}from"./chunks/framework.onQNwZ2I.js";const d=JSON.parse('{"title":"Tuple conversion","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/tuples.md","filePath":"source/transformations/tuples.md","lastUpdated":null}'),t={name:"source/transformations/tuples.md"};function p(l,s,r,o,h,k){return e(),n("div",null,s[0]||(s[0]=[i(`"""
+ 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
$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
This page was generated using Literate.jl.
`,6)]))}const F=a(t,[["render",p]]);export{d as __pageData,F as default}; diff --git a/previews/PR239/assets/source_transformations_tuples.md.BFXQMbxF.lean.js b/previews/PR239/assets/source_transformations_tuples.md.BFXQMbxF.lean.js new file mode 100644 index 000000000..42ae74220 --- /dev/null +++ b/previews/PR239/assets/source_transformations_tuples.md.BFXQMbxF.lean.js @@ -0,0 +1,19 @@ +import{_ as a,c as n,a5 as i,o as e}from"./chunks/framework.onQNwZ2I.js";const d=JSON.parse('{"title":"Tuple conversion","description":"","frontmatter":{},"headers":[],"relativePath":"source/transformations/tuples.md","filePath":"source/transformations/tuples.md","lastUpdated":null}'),t={name:"source/transformations/tuples.md"};function p(l,s,r,o,h,k){return e(),n("div",null,s[0]||(s[0]=[i(`"""
+ 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
$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
This page was generated using Literate.jl.
`,6)]))}const F=a(t,[["render",p]]);export{d as __pageData,F as default}; diff --git a/previews/PR239/assets/source_types.md.BzRqHlPc.js b/previews/PR239/assets/source_types.md.BzRqHlPc.js new file mode 100644 index 000000000..9d5cfae61 --- /dev/null +++ b/previews/PR239/assets/source_types.md.BzRqHlPc.js @@ -0,0 +1,38 @@ +import{_ as i,c as a,a5 as n,o as e}from"./chunks/framework.onQNwZ2I.js";const E=JSON.parse('{"title":"Types","description":"","frontmatter":{},"headers":[],"relativePath":"source/types.md","filePath":"source/types.md","lastUpdated":null}'),t={name:"source/types.md"};function l(h,s,p,k,r,d){return e(),a("div",null,s[0]||(s[0]=[n(`This file defines some fundamental types used in GeometryOps.
Warning
Unlike in other Julia packages, only some types are defined in this file, not all. This is because we define types in the files where they are used, to make it easier to understand the code.
export GEOS
GEOS
GEOS
is a struct which instructs the method it's passed to as an algorithm to use the appropriate GEOS function via LibGEOS.jl
for the operation.
It's generally a lot slower than the native Julia implementations, but it's useful for two reasons:
Functionality which doesn't exist in GeometryOps can be accessed through the GeometryOps API, but use GEOS in the backend until someone implements a native Julia version.
It's a good way to test the correctness of the native implementations.
"""
+ GEOS(; params...)
+
+A struct which instructs the method it's passed to as an algorithm
+to use the appropriate GEOS function via \`LibGEOS.jl\` for the operation.
+
+Dispatch is generally carried out using the names of the keyword arguments.
+For example, \`segmentize\` will only accept a \`GEOS\` struct with only a
+\`max_distance\` keyword, and no other.
+
+It's generally a lot slower than the native Julia implementations, since
+it must convert to the LibGEOS implementation and back - so be warned!
+"""
+struct GEOS
+ params::NamedTuple
+end
+
+function GEOS(; params...)
+ nt = NamedTuple(params)
+ return GEOS(nt)
+end
These are definitions for convenience, so we don't have to type out alg.params
every time.
Base.get(alg::GEOS, key, value) = Base.get(alg.params, key, value)
+Base.get(f::Function, alg::GEOS, key) = Base.get(f, alg.params, key)
+
+"""
+ enforce(alg::GO.GEOS, kw::Symbol, f)
+
+Enforce the presence of a keyword argument in a \`GEOS\` algorithm, and return \`alg.params[kw]\`.
+
+Throws an error if the key is not present, and mentions \`f\` in the error message (since there isn't
+a good way to get the name of the function that called this method).
+"""
+function enforce(alg::GEOS, kw::Symbol, f)
+ if haskey(alg.params, kw)
+ return alg.params[kw]
+ else
+ error("$(f) requires a \`$(kw)\` keyword argument to the \`GEOS\` algorithm, which was not provided.")
+ end
+end
This page was generated using Literate.jl.
`,13)]))}const g=i(t,[["render",l]]);export{E as __pageData,g as default}; diff --git a/previews/PR239/assets/source_types.md.BzRqHlPc.lean.js b/previews/PR239/assets/source_types.md.BzRqHlPc.lean.js new file mode 100644 index 000000000..9d5cfae61 --- /dev/null +++ b/previews/PR239/assets/source_types.md.BzRqHlPc.lean.js @@ -0,0 +1,38 @@ +import{_ as i,c as a,a5 as n,o as e}from"./chunks/framework.onQNwZ2I.js";const E=JSON.parse('{"title":"Types","description":"","frontmatter":{},"headers":[],"relativePath":"source/types.md","filePath":"source/types.md","lastUpdated":null}'),t={name:"source/types.md"};function l(h,s,p,k,r,d){return e(),a("div",null,s[0]||(s[0]=[n(`This file defines some fundamental types used in GeometryOps.
Warning
Unlike in other Julia packages, only some types are defined in this file, not all. This is because we define types in the files where they are used, to make it easier to understand the code.
export GEOS
GEOS
GEOS
is a struct which instructs the method it's passed to as an algorithm to use the appropriate GEOS function via LibGEOS.jl
for the operation.
It's generally a lot slower than the native Julia implementations, but it's useful for two reasons:
Functionality which doesn't exist in GeometryOps can be accessed through the GeometryOps API, but use GEOS in the backend until someone implements a native Julia version.
It's a good way to test the correctness of the native implementations.
"""
+ GEOS(; params...)
+
+A struct which instructs the method it's passed to as an algorithm
+to use the appropriate GEOS function via \`LibGEOS.jl\` for the operation.
+
+Dispatch is generally carried out using the names of the keyword arguments.
+For example, \`segmentize\` will only accept a \`GEOS\` struct with only a
+\`max_distance\` keyword, and no other.
+
+It's generally a lot slower than the native Julia implementations, since
+it must convert to the LibGEOS implementation and back - so be warned!
+"""
+struct GEOS
+ params::NamedTuple
+end
+
+function GEOS(; params...)
+ nt = NamedTuple(params)
+ return GEOS(nt)
+end
These are definitions for convenience, so we don't have to type out alg.params
every time.
Base.get(alg::GEOS, key, value) = Base.get(alg.params, key, value)
+Base.get(f::Function, alg::GEOS, key) = Base.get(f, alg.params, key)
+
+"""
+ enforce(alg::GO.GEOS, kw::Symbol, f)
+
+Enforce the presence of a keyword argument in a \`GEOS\` algorithm, and return \`alg.params[kw]\`.
+
+Throws an error if the key is not present, and mentions \`f\` in the error message (since there isn't
+a good way to get the name of the function that called this method).
+"""
+function enforce(alg::GEOS, kw::Symbol, f)
+ if haskey(alg.params, kw)
+ return alg.params[kw]
+ else
+ error("$(f) requires a \`$(kw)\` keyword argument to the \`GEOS\` algorithm, which was not provided.")
+ end
+end
This page was generated using Literate.jl.
`,13)]))}const g=i(t,[["render",l]]);export{E as __pageData,g as default}; diff --git a/previews/PR239/assets/source_utils.md.Cy9C-nnj.js b/previews/PR239/assets/source_utils.md.Cy9C-nnj.js new file mode 100644 index 000000000..494fb3f1f --- /dev/null +++ b/previews/PR239/assets/source_utils.md.Cy9C-nnj.js @@ -0,0 +1,120 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const d=JSON.parse('{"title":"Utility functions","description":"","frontmatter":{},"headers":[],"relativePath":"source/utils.md","filePath":"source/utils.md","lastUpdated":null}'),p={name:"source/utils.md"};function l(h,s,k,e,r,F){return t(),a("div",null,s[0]||(s[0]=[n(`_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
Examples
\`\`\`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)
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)
+\`\`\`
+"""
+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
This page was generated using Literate.jl.
`,8)]))}const E=i(p,[["render",l]]);export{d as __pageData,E as default}; diff --git a/previews/PR239/assets/source_utils.md.Cy9C-nnj.lean.js b/previews/PR239/assets/source_utils.md.Cy9C-nnj.lean.js new file mode 100644 index 000000000..494fb3f1f --- /dev/null +++ b/previews/PR239/assets/source_utils.md.Cy9C-nnj.lean.js @@ -0,0 +1,120 @@ +import{_ as i,c as a,a5 as n,o as t}from"./chunks/framework.onQNwZ2I.js";const d=JSON.parse('{"title":"Utility functions","description":"","frontmatter":{},"headers":[],"relativePath":"source/utils.md","filePath":"source/utils.md","lastUpdated":null}'),p={name:"source/utils.md"};function l(h,s,k,e,r,F){return t(),a("div",null,s[0]||(s[0]=[n(`_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
Examples
\`\`\`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)
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)
+\`\`\`
+"""
+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
This page was generated using Literate.jl.
`,8)]))}const E=i(p,[["render",l]]);export{d as __pageData,E as default}; diff --git a/previews/PR239/assets/sqwzzcg.DeeQUply.png b/previews/PR239/assets/sqwzzcg.DeeQUply.png new file mode 100644 index 000000000..8a60753d0 Binary files /dev/null and b/previews/PR239/assets/sqwzzcg.DeeQUply.png differ diff --git a/previews/PR239/assets/style.BIJ4ByRG.css b/previews/PR239/assets/style.BIJ4ByRG.css new file mode 100644 index 000000000..9f292cd28 --- /dev/null +++ b/previews/PR239/assets/style.BIJ4ByRG.css @@ -0,0 +1 @@ +@import"https://fonts.googleapis.com/css?family=Space+Mono:regular,italic,700,700italic";@import"https://fonts.googleapis.com/css?family=Space+Grotesk:regular,italic,700,700italic";@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/GeometryOps.jl/previews/PR239/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/GeometryOps.jl/previews/PR239/assets/inter-roman-cyrillic.C5lxZ8CY.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/GeometryOps.jl/previews/PR239/assets/inter-roman-greek-ext.CqjqNYQ-.woff2) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/GeometryOps.jl/previews/PR239/assets/inter-roman-greek.BBVDIX6e.woff2) format("woff2");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/GeometryOps.jl/previews/PR239/assets/inter-roman-vietnamese.BjW4sHH5.woff2) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/GeometryOps.jl/previews/PR239/assets/inter-roman-latin-ext.4ZJIpNVo.woff2) format("woff2");unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/GeometryOps.jl/previews/PR239/assets/inter-roman-latin.Di8DUHzh.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/GeometryOps.jl/previews/PR239/assets/inter-italic-cyrillic-ext.r48I6akx.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/GeometryOps.jl/previews/PR239/assets/inter-italic-cyrillic.By2_1cv3.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/GeometryOps.jl/previews/PR239/assets/inter-italic-greek-ext.1u6EdAuj.woff2) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/GeometryOps.jl/previews/PR239/assets/inter-italic-greek.DJ8dCoTZ.woff2) format("woff2");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/GeometryOps.jl/previews/PR239/assets/inter-italic-vietnamese.BSbpV94h.woff2) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/GeometryOps.jl/previews/PR239/assets/inter-italic-latin-ext.CN1xVJS-.woff2) format("woff2");unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/GeometryOps.jl/previews/PR239/assets/inter-italic-latin.C2AdPX0b.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Punctuation SC;font-weight:400;src:local("PingFang SC Regular"),local("Noto Sans CJK SC"),local("Microsoft YaHei");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}@font-face{font-family:Punctuation SC;font-weight:500;src:local("PingFang SC Medium"),local("Noto Sans CJK SC"),local("Microsoft YaHei");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}@font-face{font-family:Punctuation SC;font-weight:600;src:local("PingFang SC Semibold"),local("Noto Sans CJK SC Bold"),local("Microsoft YaHei Bold");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}@font-face{font-family:Punctuation SC;font-weight:700;src:local("PingFang SC Semibold"),local("Noto Sans CJK SC Bold"),local("Microsoft YaHei Bold");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}:root{--vp-c-white: #ffffff;--vp-c-black: #000000;--vp-c-neutral: var(--vp-c-black);--vp-c-neutral-inverse: var(--vp-c-white)}.dark{--vp-c-neutral: var(--vp-c-white);--vp-c-neutral-inverse: var(--vp-c-black)}:root{--vp-c-gray-1: #dddde3;--vp-c-gray-2: #e4e4e9;--vp-c-gray-3: #ebebef;--vp-c-gray-soft: rgba(142, 150, 170, .14);--vp-c-indigo-1: #3451b2;--vp-c-indigo-2: #3a5ccc;--vp-c-indigo-3: #5672cd;--vp-c-indigo-soft: rgba(100, 108, 255, .14);--vp-c-purple-1: #6f42c1;--vp-c-purple-2: #7e4cc9;--vp-c-purple-3: #8e5cd9;--vp-c-purple-soft: rgba(159, 122, 234, .14);--vp-c-green-1: #18794e;--vp-c-green-2: #299764;--vp-c-green-3: #30a46c;--vp-c-green-soft: rgba(16, 185, 129, .14);--vp-c-yellow-1: #915930;--vp-c-yellow-2: #946300;--vp-c-yellow-3: #9f6a00;--vp-c-yellow-soft: rgba(234, 179, 8, .14);--vp-c-red-1: #b8272c;--vp-c-red-2: #d5393e;--vp-c-red-3: #e0575b;--vp-c-red-soft: rgba(244, 63, 94, .14);--vp-c-sponsor: #db2777}.dark{--vp-c-gray-1: #515c67;--vp-c-gray-2: #414853;--vp-c-gray-3: #32363f;--vp-c-gray-soft: rgba(101, 117, 133, .16);--vp-c-indigo-1: #a8b1ff;--vp-c-indigo-2: #5c73e7;--vp-c-indigo-3: #3e63dd;--vp-c-indigo-soft: rgba(100, 108, 255, .16);--vp-c-purple-1: #c8abfa;--vp-c-purple-2: #a879e6;--vp-c-purple-3: #8e5cd9;--vp-c-purple-soft: rgba(159, 122, 234, .16);--vp-c-green-1: #3dd68c;--vp-c-green-2: #30a46c;--vp-c-green-3: #298459;--vp-c-green-soft: rgba(16, 185, 129, .16);--vp-c-yellow-1: #f9b44e;--vp-c-yellow-2: #da8b17;--vp-c-yellow-3: #a46a0a;--vp-c-yellow-soft: rgba(234, 179, 8, .16);--vp-c-red-1: #f66f81;--vp-c-red-2: #f14158;--vp-c-red-3: #b62a3c;--vp-c-red-soft: rgba(244, 63, 94, .16)}:root{--vp-c-bg: #ffffff;--vp-c-bg-alt: #f6f6f7;--vp-c-bg-elv: #ffffff;--vp-c-bg-soft: #f6f6f7}.dark{--vp-c-bg: #1b1b1f;--vp-c-bg-alt: #161618;--vp-c-bg-elv: #202127;--vp-c-bg-soft: #202127}:root{--vp-c-border: #c2c2c4;--vp-c-divider: #e2e2e3;--vp-c-gutter: #e2e2e3}.dark{--vp-c-border: #3c3f44;--vp-c-divider: #2e2e32;--vp-c-gutter: #000000}:root{--vp-c-text-1: rgba(60, 60, 67);--vp-c-text-2: rgba(60, 60, 67, .78);--vp-c-text-3: rgba(60, 60, 67, .56)}.dark{--vp-c-text-1: rgba(255, 255, 245, .86);--vp-c-text-2: rgba(235, 235, 245, .6);--vp-c-text-3: rgba(235, 235, 245, .38)}:root{--vp-c-default-1: var(--vp-c-gray-1);--vp-c-default-2: var(--vp-c-gray-2);--vp-c-default-3: var(--vp-c-gray-3);--vp-c-default-soft: var(--vp-c-gray-soft);--vp-c-brand-1: var(--vp-c-indigo-1);--vp-c-brand-2: var(--vp-c-indigo-2);--vp-c-brand-3: var(--vp-c-indigo-3);--vp-c-brand-soft: var(--vp-c-indigo-soft);--vp-c-brand: var(--vp-c-brand-1);--vp-c-tip-1: var(--vp-c-brand-1);--vp-c-tip-2: var(--vp-c-brand-2);--vp-c-tip-3: var(--vp-c-brand-3);--vp-c-tip-soft: var(--vp-c-brand-soft);--vp-c-note-1: var(--vp-c-brand-1);--vp-c-note-2: var(--vp-c-brand-2);--vp-c-note-3: var(--vp-c-brand-3);--vp-c-note-soft: var(--vp-c-brand-soft);--vp-c-success-1: var(--vp-c-green-1);--vp-c-success-2: var(--vp-c-green-2);--vp-c-success-3: var(--vp-c-green-3);--vp-c-success-soft: var(--vp-c-green-soft);--vp-c-important-1: var(--vp-c-purple-1);--vp-c-important-2: var(--vp-c-purple-2);--vp-c-important-3: var(--vp-c-purple-3);--vp-c-important-soft: var(--vp-c-purple-soft);--vp-c-warning-1: var(--vp-c-yellow-1);--vp-c-warning-2: var(--vp-c-yellow-2);--vp-c-warning-3: var(--vp-c-yellow-3);--vp-c-warning-soft: var(--vp-c-yellow-soft);--vp-c-danger-1: var(--vp-c-red-1);--vp-c-danger-2: var(--vp-c-red-2);--vp-c-danger-3: var(--vp-c-red-3);--vp-c-danger-soft: var(--vp-c-red-soft);--vp-c-caution-1: var(--vp-c-red-1);--vp-c-caution-2: var(--vp-c-red-2);--vp-c-caution-3: var(--vp-c-red-3);--vp-c-caution-soft: var(--vp-c-red-soft)}:root{--vp-font-family-base: "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--vp-font-family-mono: ui-monospace, "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", monospace;font-optical-sizing:auto}:root:where(:lang(zh)){--vp-font-family-base: "Punctuation SC", "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"}:root{--vp-shadow-1: 0 1px 2px rgba(0, 0, 0, .04), 0 1px 2px rgba(0, 0, 0, .06);--vp-shadow-2: 0 3px 12px rgba(0, 0, 0, .07), 0 1px 4px rgba(0, 0, 0, .07);--vp-shadow-3: 0 12px 32px rgba(0, 0, 0, .1), 0 2px 6px rgba(0, 0, 0, .08);--vp-shadow-4: 0 14px 44px rgba(0, 0, 0, .12), 0 3px 9px rgba(0, 0, 0, .12);--vp-shadow-5: 0 18px 56px rgba(0, 0, 0, .16), 0 4px 12px rgba(0, 0, 0, .16)}:root{--vp-z-index-footer: 10;--vp-z-index-local-nav: 20;--vp-z-index-nav: 30;--vp-z-index-layout-top: 40;--vp-z-index-backdrop: 50;--vp-z-index-sidebar: 60}@media (min-width: 960px){:root{--vp-z-index-sidebar: 25}}:root{--vp-layout-max-width: 1440px}:root{--vp-header-anchor-symbol: "#"}:root{--vp-code-line-height: 1.7;--vp-code-font-size: .875em;--vp-code-color: var(--vp-c-brand-1);--vp-code-link-color: var(--vp-c-brand-1);--vp-code-link-hover-color: var(--vp-c-brand-2);--vp-code-bg: var(--vp-c-default-soft);--vp-code-block-color: var(--vp-c-text-2);--vp-code-block-bg: var(--vp-c-bg-alt);--vp-code-block-divider-color: var(--vp-c-gutter);--vp-code-lang-color: var(--vp-c-text-3);--vp-code-line-highlight-color: var(--vp-c-default-soft);--vp-code-line-number-color: var(--vp-c-text-3);--vp-code-line-diff-add-color: var(--vp-c-success-soft);--vp-code-line-diff-add-symbol-color: var(--vp-c-success-1);--vp-code-line-diff-remove-color: var(--vp-c-danger-soft);--vp-code-line-diff-remove-symbol-color: var(--vp-c-danger-1);--vp-code-line-warning-color: var(--vp-c-warning-soft);--vp-code-line-error-color: var(--vp-c-danger-soft);--vp-code-copy-code-border-color: var(--vp-c-divider);--vp-code-copy-code-bg: var(--vp-c-bg-soft);--vp-code-copy-code-hover-border-color: var(--vp-c-divider);--vp-code-copy-code-hover-bg: var(--vp-c-bg);--vp-code-copy-code-active-text: var(--vp-c-text-2);--vp-code-copy-copied-text-content: "Copied";--vp-code-tab-divider: var(--vp-code-block-divider-color);--vp-code-tab-text-color: var(--vp-c-text-2);--vp-code-tab-bg: var(--vp-code-block-bg);--vp-code-tab-hover-text-color: var(--vp-c-text-1);--vp-code-tab-active-text-color: var(--vp-c-text-1);--vp-code-tab-active-bar-color: var(--vp-c-brand-1)}:root{--vp-button-brand-border: transparent;--vp-button-brand-text: var(--vp-c-white);--vp-button-brand-bg: var(--vp-c-brand-3);--vp-button-brand-hover-border: transparent;--vp-button-brand-hover-text: var(--vp-c-white);--vp-button-brand-hover-bg: var(--vp-c-brand-2);--vp-button-brand-active-border: transparent;--vp-button-brand-active-text: var(--vp-c-white);--vp-button-brand-active-bg: var(--vp-c-brand-1);--vp-button-alt-border: transparent;--vp-button-alt-text: var(--vp-c-text-1);--vp-button-alt-bg: var(--vp-c-default-3);--vp-button-alt-hover-border: transparent;--vp-button-alt-hover-text: var(--vp-c-text-1);--vp-button-alt-hover-bg: var(--vp-c-default-2);--vp-button-alt-active-border: transparent;--vp-button-alt-active-text: var(--vp-c-text-1);--vp-button-alt-active-bg: var(--vp-c-default-1);--vp-button-sponsor-border: var(--vp-c-text-2);--vp-button-sponsor-text: var(--vp-c-text-2);--vp-button-sponsor-bg: transparent;--vp-button-sponsor-hover-border: var(--vp-c-sponsor);--vp-button-sponsor-hover-text: var(--vp-c-sponsor);--vp-button-sponsor-hover-bg: transparent;--vp-button-sponsor-active-border: var(--vp-c-sponsor);--vp-button-sponsor-active-text: var(--vp-c-sponsor);--vp-button-sponsor-active-bg: transparent}:root{--vp-custom-block-font-size: 14px;--vp-custom-block-code-font-size: 13px;--vp-custom-block-info-border: transparent;--vp-custom-block-info-text: var(--vp-c-text-1);--vp-custom-block-info-bg: var(--vp-c-default-soft);--vp-custom-block-info-code-bg: var(--vp-c-default-soft);--vp-custom-block-note-border: transparent;--vp-custom-block-note-text: var(--vp-c-text-1);--vp-custom-block-note-bg: var(--vp-c-default-soft);--vp-custom-block-note-code-bg: var(--vp-c-default-soft);--vp-custom-block-tip-border: transparent;--vp-custom-block-tip-text: var(--vp-c-text-1);--vp-custom-block-tip-bg: var(--vp-c-tip-soft);--vp-custom-block-tip-code-bg: var(--vp-c-tip-soft);--vp-custom-block-important-border: transparent;--vp-custom-block-important-text: var(--vp-c-text-1);--vp-custom-block-important-bg: var(--vp-c-important-soft);--vp-custom-block-important-code-bg: var(--vp-c-important-soft);--vp-custom-block-warning-border: transparent;--vp-custom-block-warning-text: var(--vp-c-text-1);--vp-custom-block-warning-bg: var(--vp-c-warning-soft);--vp-custom-block-warning-code-bg: var(--vp-c-warning-soft);--vp-custom-block-danger-border: transparent;--vp-custom-block-danger-text: var(--vp-c-text-1);--vp-custom-block-danger-bg: var(--vp-c-danger-soft);--vp-custom-block-danger-code-bg: var(--vp-c-danger-soft);--vp-custom-block-caution-border: transparent;--vp-custom-block-caution-text: var(--vp-c-text-1);--vp-custom-block-caution-bg: var(--vp-c-caution-soft);--vp-custom-block-caution-code-bg: var(--vp-c-caution-soft);--vp-custom-block-details-border: var(--vp-custom-block-info-border);--vp-custom-block-details-text: var(--vp-custom-block-info-text);--vp-custom-block-details-bg: var(--vp-custom-block-info-bg);--vp-custom-block-details-code-bg: var(--vp-custom-block-info-code-bg)}:root{--vp-input-border-color: var(--vp-c-border);--vp-input-bg-color: var(--vp-c-bg-alt);--vp-input-switch-bg-color: var(--vp-c-default-soft)}:root{--vp-nav-height: 64px;--vp-nav-bg-color: var(--vp-c-bg);--vp-nav-screen-bg-color: var(--vp-c-bg);--vp-nav-logo-height: 24px}.hide-nav{--vp-nav-height: 0px}.hide-nav .VPSidebar{--vp-nav-height: 22px}:root{--vp-local-nav-bg-color: var(--vp-c-bg)}:root{--vp-sidebar-width: 272px;--vp-sidebar-bg-color: var(--vp-c-bg-alt)}:root{--vp-backdrop-bg-color: rgba(0, 0, 0, .6)}:root{--vp-home-hero-name-color: var(--vp-c-brand-1);--vp-home-hero-name-background: transparent;--vp-home-hero-image-background-image: none;--vp-home-hero-image-filter: none}:root{--vp-badge-info-border: transparent;--vp-badge-info-text: var(--vp-c-text-2);--vp-badge-info-bg: var(--vp-c-default-soft);--vp-badge-tip-border: transparent;--vp-badge-tip-text: var(--vp-c-tip-1);--vp-badge-tip-bg: var(--vp-c-tip-soft);--vp-badge-warning-border: transparent;--vp-badge-warning-text: var(--vp-c-warning-1);--vp-badge-warning-bg: var(--vp-c-warning-soft);--vp-badge-danger-border: transparent;--vp-badge-danger-text: var(--vp-c-danger-1);--vp-badge-danger-bg: var(--vp-c-danger-soft)}:root{--vp-carbon-ads-text-color: var(--vp-c-text-1);--vp-carbon-ads-poweredby-color: var(--vp-c-text-2);--vp-carbon-ads-bg-color: var(--vp-c-bg-soft);--vp-carbon-ads-hover-text-color: var(--vp-c-brand-1);--vp-carbon-ads-hover-poweredby-color: var(--vp-c-text-1)}:root{--vp-local-search-bg: var(--vp-c-bg);--vp-local-search-result-bg: var(--vp-c-bg);--vp-local-search-result-border: var(--vp-c-divider);--vp-local-search-result-selected-bg: var(--vp-c-bg);--vp-local-search-result-selected-border: var(--vp-c-brand-1);--vp-local-search-highlight-bg: var(--vp-c-brand-1);--vp-local-search-highlight-text: var(--vp-c-neutral-inverse)}@media (prefers-reduced-motion: reduce){*,:before,:after{animation-delay:-1ms!important;animation-duration:1ms!important;animation-iteration-count:1!important;background-attachment:initial!important;scroll-behavior:auto!important;transition-duration:0s!important;transition-delay:0s!important}}*,:before,:after{box-sizing:border-box}html{line-height:1.4;font-size:16px;-webkit-text-size-adjust:100%}html.dark{color-scheme:dark}body{margin:0;width:100%;min-width:320px;min-height:100vh;line-height:24px;font-family:var(--vp-font-family-base);font-size:16px;font-weight:400;color:var(--vp-c-text-1);background-color:var(--vp-c-bg);font-synthesis:style;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}main{display:block}h1,h2,h3,h4,h5,h6{margin:0;line-height:24px;font-size:16px;font-weight:400}p{margin:0}strong,b{font-weight:600}a,area,button,[role=button],input,label,select,summary,textarea{touch-action:manipulation}a{color:inherit;text-decoration:inherit}ol,ul{list-style:none;margin:0;padding:0}blockquote{margin:0}pre,code,kbd,samp{font-family:var(--vp-font-family-mono)}img,svg,video,canvas,audio,iframe,embed,object{display:block}figure{margin:0}img,video{max-width:100%;height:auto}button,input,optgroup,select,textarea{border:0;padding:0;line-height:inherit;color:inherit}button{padding:0;font-family:inherit;background-color:transparent;background-image:none}button:enabled,[role=button]:enabled{cursor:pointer}button:focus,button:focus-visible{outline:1px dotted;outline:4px auto -webkit-focus-ring-color}button:focus:not(:focus-visible){outline:none!important}input:focus,textarea:focus,select:focus{outline:none}table{border-collapse:collapse}input{background-color:transparent}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:var(--vp-c-text-3)}input::-ms-input-placeholder,textarea::-ms-input-placeholder{color:var(--vp-c-text-3)}input::placeholder,textarea::placeholder{color:var(--vp-c-text-3)}input::-webkit-outer-spin-button,input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}input[type=number]{-moz-appearance:textfield}textarea{resize:vertical}select{-webkit-appearance:none}fieldset{margin:0;padding:0}h1,h2,h3,h4,h5,h6,li,p{overflow-wrap:break-word}vite-error-overlay{z-index:9999}mjx-container{overflow-x:auto}mjx-container>svg{display:inline-block;margin:auto}[class^=vpi-],[class*=" vpi-"],.vp-icon{width:1em;height:1em}[class^=vpi-].bg,[class*=" vpi-"].bg,.vp-icon.bg{background-size:100% 100%;background-color:transparent}[class^=vpi-]:not(.bg),[class*=" vpi-"]:not(.bg),.vp-icon:not(.bg){-webkit-mask:var(--icon) no-repeat;mask:var(--icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit}.vpi-align-left{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M21 6H3M15 12H3M17 18H3'/%3E%3C/svg%3E")}.vpi-arrow-right,.vpi-arrow-down,.vpi-arrow-left,.vpi-arrow-up{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14M12 5l7 7-7 7'/%3E%3C/svg%3E")}.vpi-chevron-right,.vpi-chevron-down,.vpi-chevron-left,.vpi-chevron-up{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E")}.vpi-chevron-down,.vpi-arrow-down{transform:rotate(90deg)}.vpi-chevron-left,.vpi-arrow-left{transform:rotate(180deg)}.vpi-chevron-up,.vpi-arrow-up{transform:rotate(-90deg)}.vpi-square-pen{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7'/%3E%3Cpath d='M18.375 2.625a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4Z'/%3E%3C/svg%3E")}.vpi-plus{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14M12 5v14'/%3E%3C/svg%3E")}.vpi-sun{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='4'/%3E%3Cpath d='M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41'/%3E%3C/svg%3E")}.vpi-moon{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z'/%3E%3C/svg%3E")}.vpi-more-horizontal{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='1'/%3E%3Ccircle cx='19' cy='12' r='1'/%3E%3Ccircle cx='5' cy='12' r='1'/%3E%3C/svg%3E")}.vpi-languages{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m5 8 6 6M4 14l6-6 2-3M2 5h12M7 2h1M22 22l-5-10-5 10M14 18h6'/%3E%3C/svg%3E")}.vpi-heart{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z'/%3E%3C/svg%3E")}.vpi-search{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E")}.vpi-layout-list{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='7' height='7' x='3' y='3' rx='1'/%3E%3Crect width='7' height='7' x='3' y='14' rx='1'/%3E%3Cpath d='M14 4h7M14 9h7M14 15h7M14 20h7'/%3E%3C/svg%3E")}.vpi-delete{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M20 5H9l-7 7 7 7h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2ZM18 9l-6 6M12 9l6 6'/%3E%3C/svg%3E")}.vpi-corner-down-left{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m9 10-5 5 5 5'/%3E%3Cpath d='M20 4v7a4 4 0 0 1-4 4H4'/%3E%3C/svg%3E")}:root{--vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E");--vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E")}.visually-hidden{position:absolute;width:1px;height:1px;white-space:nowrap;clip:rect(0 0 0 0);clip-path:inset(50%);overflow:hidden}.custom-block{border:1px solid transparent;border-radius:8px;padding:16px 16px 8px;line-height:24px;font-size:var(--vp-custom-block-font-size);color:var(--vp-c-text-2)}.custom-block.info{border-color:var(--vp-custom-block-info-border);color:var(--vp-custom-block-info-text);background-color:var(--vp-custom-block-info-bg)}.custom-block.info a,.custom-block.info code{color:var(--vp-c-brand-1)}.custom-block.info a:hover,.custom-block.info a:hover>code{color:var(--vp-c-brand-2)}.custom-block.info code{background-color:var(--vp-custom-block-info-code-bg)}.custom-block.note{border-color:var(--vp-custom-block-note-border);color:var(--vp-custom-block-note-text);background-color:var(--vp-custom-block-note-bg)}.custom-block.note a,.custom-block.note code{color:var(--vp-c-brand-1)}.custom-block.note a:hover,.custom-block.note a:hover>code{color:var(--vp-c-brand-2)}.custom-block.note code{background-color:var(--vp-custom-block-note-code-bg)}.custom-block.tip{border-color:var(--vp-custom-block-tip-border);color:var(--vp-custom-block-tip-text);background-color:var(--vp-custom-block-tip-bg)}.custom-block.tip a,.custom-block.tip code{color:var(--vp-c-tip-1)}.custom-block.tip a:hover,.custom-block.tip a:hover>code{color:var(--vp-c-tip-2)}.custom-block.tip code{background-color:var(--vp-custom-block-tip-code-bg)}.custom-block.important{border-color:var(--vp-custom-block-important-border);color:var(--vp-custom-block-important-text);background-color:var(--vp-custom-block-important-bg)}.custom-block.important a,.custom-block.important code{color:var(--vp-c-important-1)}.custom-block.important a:hover,.custom-block.important a:hover>code{color:var(--vp-c-important-2)}.custom-block.important code{background-color:var(--vp-custom-block-important-code-bg)}.custom-block.warning{border-color:var(--vp-custom-block-warning-border);color:var(--vp-custom-block-warning-text);background-color:var(--vp-custom-block-warning-bg)}.custom-block.warning a,.custom-block.warning code{color:var(--vp-c-warning-1)}.custom-block.warning a:hover,.custom-block.warning a:hover>code{color:var(--vp-c-warning-2)}.custom-block.warning code{background-color:var(--vp-custom-block-warning-code-bg)}.custom-block.danger{border-color:var(--vp-custom-block-danger-border);color:var(--vp-custom-block-danger-text);background-color:var(--vp-custom-block-danger-bg)}.custom-block.danger a,.custom-block.danger code{color:var(--vp-c-danger-1)}.custom-block.danger a:hover,.custom-block.danger a:hover>code{color:var(--vp-c-danger-2)}.custom-block.danger code{background-color:var(--vp-custom-block-danger-code-bg)}.custom-block.caution{border-color:var(--vp-custom-block-caution-border);color:var(--vp-custom-block-caution-text);background-color:var(--vp-custom-block-caution-bg)}.custom-block.caution a,.custom-block.caution code{color:var(--vp-c-caution-1)}.custom-block.caution a:hover,.custom-block.caution a:hover>code{color:var(--vp-c-caution-2)}.custom-block.caution code{background-color:var(--vp-custom-block-caution-code-bg)}.custom-block.details{border-color:var(--vp-custom-block-details-border);color:var(--vp-custom-block-details-text);background-color:var(--vp-custom-block-details-bg)}.custom-block.details a{color:var(--vp-c-brand-1)}.custom-block.details a:hover,.custom-block.details a:hover>code{color:var(--vp-c-brand-2)}.custom-block.details code{background-color:var(--vp-custom-block-details-code-bg)}.custom-block-title{font-weight:600}.custom-block p+p{margin:8px 0}.custom-block.details summary{margin:0 0 8px;font-weight:700;cursor:pointer;-webkit-user-select:none;user-select:none}.custom-block.details summary+p{margin:8px 0}.custom-block a{color:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px;transition:opacity .25s}.custom-block a:hover{opacity:.75}.custom-block code{font-size:var(--vp-custom-block-code-font-size)}.custom-block.custom-block th,.custom-block.custom-block blockquote>p{font-size:var(--vp-custom-block-font-size);color:inherit}.dark .vp-code span{color:var(--shiki-dark, inherit)}html:not(.dark) .vp-code span{color:var(--shiki-light, inherit)}.vp-code-group{margin-top:16px}.vp-code-group .tabs{position:relative;display:flex;margin-right:-24px;margin-left:-24px;padding:0 12px;background-color:var(--vp-code-tab-bg);overflow-x:auto;overflow-y:hidden;box-shadow:inset 0 -1px var(--vp-code-tab-divider)}@media (min-width: 640px){.vp-code-group .tabs{margin-right:0;margin-left:0;border-radius:8px 8px 0 0}}.vp-code-group .tabs input{position:fixed;opacity:0;pointer-events:none}.vp-code-group .tabs label{position:relative;display:inline-block;border-bottom:1px solid transparent;padding:0 12px;line-height:48px;font-size:14px;font-weight:500;color:var(--vp-code-tab-text-color);white-space:nowrap;cursor:pointer;transition:color .25s}.vp-code-group .tabs label:after{position:absolute;right:8px;bottom:-1px;left:8px;z-index:1;height:2px;border-radius:2px;content:"";background-color:transparent;transition:background-color .25s}.vp-code-group label:hover{color:var(--vp-code-tab-hover-text-color)}.vp-code-group input:checked+label{color:var(--vp-code-tab-active-text-color)}.vp-code-group input:checked+label:after{background-color:var(--vp-code-tab-active-bar-color)}.vp-code-group div[class*=language-],.vp-block{display:none;margin-top:0!important;border-top-left-radius:0!important;border-top-right-radius:0!important}.vp-code-group div[class*=language-].active,.vp-block.active{display:block}.vp-block{padding:20px 24px}.vp-doc h1,.vp-doc h2,.vp-doc h3,.vp-doc h4,.vp-doc h5,.vp-doc h6{position:relative;font-weight:600;outline:none}.vp-doc h1{letter-spacing:-.02em;line-height:40px;font-size:28px}.vp-doc h2{margin:48px 0 16px;border-top:1px solid var(--vp-c-divider);padding-top:24px;letter-spacing:-.02em;line-height:32px;font-size:24px}.vp-doc h3{margin:32px 0 0;letter-spacing:-.01em;line-height:28px;font-size:20px}.vp-doc h4{margin:24px 0 0;letter-spacing:-.01em;line-height:24px;font-size:18px}.vp-doc .header-anchor{position:absolute;top:0;left:0;margin-left:-.87em;font-weight:500;-webkit-user-select:none;user-select:none;opacity:0;text-decoration:none;transition:color .25s,opacity .25s}.vp-doc .header-anchor:before{content:var(--vp-header-anchor-symbol)}.vp-doc h1:hover .header-anchor,.vp-doc h1 .header-anchor:focus,.vp-doc h2:hover .header-anchor,.vp-doc h2 .header-anchor:focus,.vp-doc h3:hover .header-anchor,.vp-doc h3 .header-anchor:focus,.vp-doc h4:hover .header-anchor,.vp-doc h4 .header-anchor:focus,.vp-doc h5:hover .header-anchor,.vp-doc h5 .header-anchor:focus,.vp-doc h6:hover .header-anchor,.vp-doc h6 .header-anchor:focus{opacity:1}@media (min-width: 768px){.vp-doc h1{letter-spacing:-.02em;line-height:40px;font-size:32px}}.vp-doc h2 .header-anchor{top:24px}.vp-doc p,.vp-doc summary{margin:16px 0}.vp-doc p{line-height:28px}.vp-doc blockquote{margin:16px 0;border-left:2px solid var(--vp-c-divider);padding-left:16px;transition:border-color .5s;color:var(--vp-c-text-2)}.vp-doc blockquote>p{margin:0;font-size:16px;transition:color .5s}.vp-doc a{font-weight:500;color:var(--vp-c-brand-1);text-decoration:underline;text-underline-offset:2px;transition:color .25s,opacity .25s}.vp-doc a:hover{color:var(--vp-c-brand-2)}.vp-doc strong{font-weight:600}.vp-doc ul,.vp-doc ol{padding-left:1.25rem;margin:16px 0}.vp-doc ul{list-style:disc}.vp-doc ol{list-style:decimal}.vp-doc li+li{margin-top:8px}.vp-doc li>ol,.vp-doc li>ul{margin:8px 0 0}.vp-doc table{display:block;border-collapse:collapse;margin:20px 0;overflow-x:auto}.vp-doc tr{background-color:var(--vp-c-bg);border-top:1px solid var(--vp-c-divider);transition:background-color .5s}.vp-doc tr:nth-child(2n){background-color:var(--vp-c-bg-soft)}.vp-doc th,.vp-doc td{border:1px solid var(--vp-c-divider);padding:8px 16px}.vp-doc th{text-align:left;font-size:14px;font-weight:600;color:var(--vp-c-text-2);background-color:var(--vp-c-bg-soft)}.vp-doc td{font-size:14px}.vp-doc hr{margin:16px 0;border:none;border-top:1px solid var(--vp-c-divider)}.vp-doc .custom-block{margin:16px 0}.vp-doc .custom-block p{margin:8px 0;line-height:24px}.vp-doc .custom-block p:first-child{margin:0}.vp-doc .custom-block div[class*=language-]{margin:8px 0;border-radius:8px}.vp-doc .custom-block div[class*=language-] code{font-weight:400;background-color:transparent}.vp-doc .custom-block .vp-code-group .tabs{margin:0;border-radius:8px 8px 0 0}.vp-doc :not(pre,h1,h2,h3,h4,h5,h6)>code{font-size:var(--vp-code-font-size);color:var(--vp-code-color)}.vp-doc :not(pre)>code{border-radius:4px;padding:3px 6px;background-color:var(--vp-code-bg);transition:color .25s,background-color .5s}.vp-doc a>code{color:var(--vp-code-link-color)}.vp-doc a:hover>code{color:var(--vp-code-link-hover-color)}.vp-doc h1>code,.vp-doc h2>code,.vp-doc h3>code,.vp-doc h4>code{font-size:.9em}.vp-doc div[class*=language-],.vp-block{position:relative;margin:16px -24px;background-color:var(--vp-code-block-bg);overflow-x:auto;transition:background-color .5s}@media (min-width: 640px){.vp-doc div[class*=language-],.vp-block{border-radius:8px;margin:16px 0}}@media (max-width: 639px){.vp-doc li div[class*=language-]{border-radius:8px 0 0 8px}}.vp-doc div[class*=language-]+div[class*=language-],.vp-doc div[class$=-api]+div[class*=language-],.vp-doc div[class*=language-]+div[class$=-api]>div[class*=language-]{margin-top:-8px}.vp-doc [class*=language-] pre,.vp-doc [class*=language-] code{direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}.vp-doc [class*=language-] pre{position:relative;z-index:1;margin:0;padding:20px 0;background:transparent;overflow-x:auto}.vp-doc [class*=language-] code{display:block;padding:0 24px;width:fit-content;min-width:100%;line-height:var(--vp-code-line-height);font-size:var(--vp-code-font-size);color:var(--vp-code-block-color);transition:color .5s}.vp-doc [class*=language-] code .highlighted{background-color:var(--vp-code-line-highlight-color);transition:background-color .5s;margin:0 -24px;padding:0 24px;width:calc(100% + 48px);display:inline-block}.vp-doc [class*=language-] code .highlighted.error{background-color:var(--vp-code-line-error-color)}.vp-doc [class*=language-] code .highlighted.warning{background-color:var(--vp-code-line-warning-color)}.vp-doc [class*=language-] code .diff{transition:background-color .5s;margin:0 -24px;padding:0 24px;width:calc(100% + 48px);display:inline-block}.vp-doc [class*=language-] code .diff:before{position:absolute;left:10px}.vp-doc [class*=language-] .has-focused-lines .line:not(.has-focus){filter:blur(.095rem);opacity:.4;transition:filter .35s,opacity .35s}.vp-doc [class*=language-] .has-focused-lines .line:not(.has-focus){opacity:.7;transition:filter .35s,opacity .35s}.vp-doc [class*=language-]:hover .has-focused-lines .line:not(.has-focus){filter:blur(0);opacity:1}.vp-doc [class*=language-] code .diff.remove{background-color:var(--vp-code-line-diff-remove-color);opacity:.7}.vp-doc [class*=language-] code .diff.remove:before{content:"-";color:var(--vp-code-line-diff-remove-symbol-color)}.vp-doc [class*=language-] code .diff.add{background-color:var(--vp-code-line-diff-add-color)}.vp-doc [class*=language-] code .diff.add:before{content:"+";color:var(--vp-code-line-diff-add-symbol-color)}.vp-doc div[class*=language-].line-numbers-mode{padding-left:32px}.vp-doc .line-numbers-wrapper{position:absolute;top:0;bottom:0;left:0;z-index:3;border-right:1px solid var(--vp-code-block-divider-color);padding-top:20px;width:32px;text-align:center;font-family:var(--vp-font-family-mono);line-height:var(--vp-code-line-height);font-size:var(--vp-code-font-size);color:var(--vp-code-line-number-color);transition:border-color .5s,color .5s}.vp-doc [class*=language-]>button.copy{direction:ltr;position:absolute;top:12px;right:12px;z-index:3;border:1px solid var(--vp-code-copy-code-border-color);border-radius:4px;width:40px;height:40px;background-color:var(--vp-code-copy-code-bg);opacity:0;cursor:pointer;background-image:var(--vp-icon-copy);background-position:50%;background-size:20px;background-repeat:no-repeat;transition:border-color .25s,background-color .25s,opacity .25s}.vp-doc [class*=language-]:hover>button.copy,.vp-doc [class*=language-]>button.copy:focus{opacity:1}.vp-doc [class*=language-]>button.copy:hover,.vp-doc [class*=language-]>button.copy.copied{border-color:var(--vp-code-copy-code-hover-border-color);background-color:var(--vp-code-copy-code-hover-bg)}.vp-doc [class*=language-]>button.copy.copied,.vp-doc [class*=language-]>button.copy:hover.copied{border-radius:0 4px 4px 0;background-color:var(--vp-code-copy-code-hover-bg);background-image:var(--vp-icon-copied)}.vp-doc [class*=language-]>button.copy.copied:before,.vp-doc [class*=language-]>button.copy:hover.copied:before{position:relative;top:-1px;transform:translate(calc(-100% - 1px));display:flex;justify-content:center;align-items:center;border:1px solid var(--vp-code-copy-code-hover-border-color);border-right:0;border-radius:4px 0 0 4px;padding:0 10px;width:fit-content;height:40px;text-align:center;font-size:12px;font-weight:500;color:var(--vp-code-copy-code-active-text);background-color:var(--vp-code-copy-code-hover-bg);white-space:nowrap;content:var(--vp-code-copy-copied-text-content)}.vp-doc [class*=language-]>span.lang{position:absolute;top:2px;right:8px;z-index:2;font-size:12px;font-weight:500;-webkit-user-select:none;user-select:none;color:var(--vp-code-lang-color);transition:color .4s,opacity .4s}.vp-doc [class*=language-]:hover>button.copy+span.lang,.vp-doc [class*=language-]>button.copy:focus+span.lang{opacity:0}.vp-doc .VPTeamMembers{margin-top:24px}.vp-doc .VPTeamMembers.small.count-1 .container{margin:0!important;max-width:calc((100% - 24px)/2)!important}.vp-doc .VPTeamMembers.small.count-2 .container,.vp-doc .VPTeamMembers.small.count-3 .container{max-width:100%!important}.vp-doc .VPTeamMembers.medium.count-1 .container{margin:0!important;max-width:calc((100% - 24px)/2)!important}:is(.vp-external-link-icon,.vp-doc a[href*="://"],.vp-doc a[target=_blank]):not(.no-icon):after{display:inline-block;margin-top:-1px;margin-left:4px;width:11px;height:11px;background:currentColor;color:var(--vp-c-text-3);flex-shrink:0;--icon: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M0 0h24v24H0V0z' fill='none' /%3E%3Cpath d='M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z' /%3E%3C/svg%3E");-webkit-mask-image:var(--icon);mask-image:var(--icon)}.vp-external-link-icon:after{content:""}.external-link-icon-enabled :is(.vp-doc a[href*="://"],.vp-doc a[target=_blank]):after{content:"";color:currentColor}.vp-sponsor{border-radius:16px;overflow:hidden}.vp-sponsor.aside{border-radius:12px}.vp-sponsor-section+.vp-sponsor-section{margin-top:4px}.vp-sponsor-tier{margin:0 0 4px!important;text-align:center;letter-spacing:1px!important;line-height:24px;width:100%;font-weight:600;color:var(--vp-c-text-2);background-color:var(--vp-c-bg-soft)}.vp-sponsor.normal .vp-sponsor-tier{padding:13px 0 11px;font-size:14px}.vp-sponsor.aside .vp-sponsor-tier{padding:9px 0 7px;font-size:12px}.vp-sponsor-grid+.vp-sponsor-tier{margin-top:4px}.vp-sponsor-grid{display:flex;flex-wrap:wrap;gap:4px}.vp-sponsor-grid.xmini .vp-sponsor-grid-link{height:64px}.vp-sponsor-grid.xmini .vp-sponsor-grid-image{max-width:64px;max-height:22px}.vp-sponsor-grid.mini .vp-sponsor-grid-link{height:72px}.vp-sponsor-grid.mini .vp-sponsor-grid-image{max-width:96px;max-height:24px}.vp-sponsor-grid.small .vp-sponsor-grid-link{height:96px}.vp-sponsor-grid.small .vp-sponsor-grid-image{max-width:96px;max-height:24px}.vp-sponsor-grid.medium .vp-sponsor-grid-link{height:112px}.vp-sponsor-grid.medium .vp-sponsor-grid-image{max-width:120px;max-height:36px}.vp-sponsor-grid.big .vp-sponsor-grid-link{height:184px}.vp-sponsor-grid.big .vp-sponsor-grid-image{max-width:192px;max-height:56px}.vp-sponsor-grid[data-vp-grid="2"] .vp-sponsor-grid-item{width:calc((100% - 4px)/2)}.vp-sponsor-grid[data-vp-grid="3"] .vp-sponsor-grid-item{width:calc((100% - 4px * 2) / 3)}.vp-sponsor-grid[data-vp-grid="4"] .vp-sponsor-grid-item{width:calc((100% - 12px)/4)}.vp-sponsor-grid[data-vp-grid="5"] .vp-sponsor-grid-item{width:calc((100% - 16px)/5)}.vp-sponsor-grid[data-vp-grid="6"] .vp-sponsor-grid-item{width:calc((100% - 4px * 5) / 6)}.vp-sponsor-grid-item{flex-shrink:0;width:100%;background-color:var(--vp-c-bg-soft);transition:background-color .25s}.vp-sponsor-grid-item:hover{background-color:var(--vp-c-default-soft)}.vp-sponsor-grid-item:hover .vp-sponsor-grid-image{filter:grayscale(0) invert(0)}.vp-sponsor-grid-item.empty:hover{background-color:var(--vp-c-bg-soft)}.dark .vp-sponsor-grid-item:hover{background-color:var(--vp-c-white)}.dark .vp-sponsor-grid-item.empty:hover{background-color:var(--vp-c-bg-soft)}.vp-sponsor-grid-link{display:flex}.vp-sponsor-grid-box{display:flex;justify-content:center;align-items:center;width:100%}.vp-sponsor-grid-image{max-width:100%;filter:grayscale(1);transition:filter .25s}.dark .vp-sponsor-grid-image{filter:grayscale(1) invert(1)}.VPBadge{display:inline-block;margin-left:2px;border:1px solid transparent;border-radius:12px;padding:0 10px;line-height:22px;font-size:12px;font-weight:500;transform:translateY(-2px)}.VPBadge.small{padding:0 6px;line-height:18px;font-size:10px;transform:translateY(-8px)}.VPDocFooter .VPBadge{display:none}.vp-doc h1>.VPBadge{margin-top:4px;vertical-align:top}.vp-doc h2>.VPBadge{margin-top:3px;padding:0 8px;vertical-align:top}.vp-doc h3>.VPBadge{vertical-align:middle}.vp-doc h4>.VPBadge,.vp-doc h5>.VPBadge,.vp-doc h6>.VPBadge{vertical-align:middle;line-height:18px}.VPBadge.info{border-color:var(--vp-badge-info-border);color:var(--vp-badge-info-text);background-color:var(--vp-badge-info-bg)}.VPBadge.tip{border-color:var(--vp-badge-tip-border);color:var(--vp-badge-tip-text);background-color:var(--vp-badge-tip-bg)}.VPBadge.warning{border-color:var(--vp-badge-warning-border);color:var(--vp-badge-warning-text);background-color:var(--vp-badge-warning-bg)}.VPBadge.danger{border-color:var(--vp-badge-danger-border);color:var(--vp-badge-danger-text);background-color:var(--vp-badge-danger-bg)}.VPBackdrop[data-v-b06cdb19]{position:fixed;top:0;right:0;bottom:0;left:0;z-index:var(--vp-z-index-backdrop);background:var(--vp-backdrop-bg-color);transition:opacity .5s}.VPBackdrop.fade-enter-from[data-v-b06cdb19],.VPBackdrop.fade-leave-to[data-v-b06cdb19]{opacity:0}.VPBackdrop.fade-leave-active[data-v-b06cdb19]{transition-duration:.25s}@media (min-width: 1280px){.VPBackdrop[data-v-b06cdb19]{display:none}}.NotFound[data-v-951cab6c]{padding:64px 24px 96px;text-align:center}@media (min-width: 768px){.NotFound[data-v-951cab6c]{padding:96px 32px 168px}}.code[data-v-951cab6c]{line-height:64px;font-size:64px;font-weight:600}.title[data-v-951cab6c]{padding-top:12px;letter-spacing:2px;line-height:20px;font-size:20px;font-weight:700}.divider[data-v-951cab6c]{margin:24px auto 18px;width:64px;height:1px;background-color:var(--vp-c-divider)}.quote[data-v-951cab6c]{margin:0 auto;max-width:256px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.action[data-v-951cab6c]{padding-top:20px}.link[data-v-951cab6c]{display:inline-block;border:1px solid var(--vp-c-brand-1);border-radius:16px;padding:3px 16px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:border-color .25s,color .25s}.link[data-v-951cab6c]:hover{border-color:var(--vp-c-brand-2);color:var(--vp-c-brand-2)}.root[data-v-3f927ebe]{position:relative;z-index:1}.nested[data-v-3f927ebe]{padding-right:16px;padding-left:16px}.outline-link[data-v-3f927ebe]{display:block;line-height:32px;font-size:14px;font-weight:400;color:var(--vp-c-text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .5s}.outline-link[data-v-3f927ebe]:hover,.outline-link.active[data-v-3f927ebe]{color:var(--vp-c-text-1);transition:color .25s}.outline-link.nested[data-v-3f927ebe]{padding-left:13px}.VPDocAsideOutline[data-v-b38bf2ff]{display:none}.VPDocAsideOutline.has-outline[data-v-b38bf2ff]{display:block}.content[data-v-b38bf2ff]{position:relative;border-left:1px solid var(--vp-c-divider);padding-left:16px;font-size:13px;font-weight:500}.outline-marker[data-v-b38bf2ff]{position:absolute;top:32px;left:-1px;z-index:0;opacity:0;width:2px;border-radius:2px;height:18px;background-color:var(--vp-c-brand-1);transition:top .25s cubic-bezier(0,1,.5,1),background-color .5s,opacity .25s}.outline-title[data-v-b38bf2ff]{line-height:32px;font-size:14px;font-weight:600}.VPDocAside[data-v-6d7b3c46]{display:flex;flex-direction:column;flex-grow:1}.spacer[data-v-6d7b3c46]{flex-grow:1}.VPDocAside[data-v-6d7b3c46] .spacer+.VPDocAsideSponsors,.VPDocAside[data-v-6d7b3c46] .spacer+.VPDocAsideCarbonAds{margin-top:24px}.VPDocAside[data-v-6d7b3c46] .VPDocAsideSponsors+.VPDocAsideCarbonAds{margin-top:16px}.VPLastUpdated[data-v-475f71b8]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 640px){.VPLastUpdated[data-v-475f71b8]{line-height:32px;font-size:14px;font-weight:500}}.VPDocFooter[data-v-4f9813fa]{margin-top:64px}.edit-info[data-v-4f9813fa]{padding-bottom:18px}@media (min-width: 640px){.edit-info[data-v-4f9813fa]{display:flex;justify-content:space-between;align-items:center;padding-bottom:14px}}.edit-link-button[data-v-4f9813fa]{display:flex;align-items:center;border:0;line-height:32px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:color .25s}.edit-link-button[data-v-4f9813fa]:hover{color:var(--vp-c-brand-2)}.edit-link-icon[data-v-4f9813fa]{margin-right:8px}.prev-next[data-v-4f9813fa]{border-top:1px solid var(--vp-c-divider);padding-top:24px;display:grid;grid-row-gap:8px}@media (min-width: 640px){.prev-next[data-v-4f9813fa]{grid-template-columns:repeat(2,1fr);grid-column-gap:16px}}.pager-link[data-v-4f9813fa]{display:block;border:1px solid var(--vp-c-divider);border-radius:8px;padding:11px 16px 13px;width:100%;height:100%;transition:border-color .25s}.pager-link[data-v-4f9813fa]:hover{border-color:var(--vp-c-brand-1)}.pager-link.next[data-v-4f9813fa]{margin-left:auto;text-align:right}.desc[data-v-4f9813fa]{display:block;line-height:20px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.title[data-v-4f9813fa]{display:block;line-height:20px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:color .25s}.VPDoc[data-v-83890dd9]{padding:32px 24px 96px;width:100%}@media (min-width: 768px){.VPDoc[data-v-83890dd9]{padding:48px 32px 128px}}@media (min-width: 960px){.VPDoc[data-v-83890dd9]{padding:48px 32px 0}.VPDoc:not(.has-sidebar) .container[data-v-83890dd9]{display:flex;justify-content:center;max-width:992px}.VPDoc:not(.has-sidebar) .content[data-v-83890dd9]{max-width:752px}}@media (min-width: 1280px){.VPDoc .container[data-v-83890dd9]{display:flex;justify-content:center}.VPDoc .aside[data-v-83890dd9]{display:block}}@media (min-width: 1440px){.VPDoc:not(.has-sidebar) .content[data-v-83890dd9]{max-width:784px}.VPDoc:not(.has-sidebar) .container[data-v-83890dd9]{max-width:1104px}}.container[data-v-83890dd9]{margin:0 auto;width:100%}.aside[data-v-83890dd9]{position:relative;display:none;order:2;flex-grow:1;padding-left:32px;width:100%;max-width:256px}.left-aside[data-v-83890dd9]{order:1;padding-left:unset;padding-right:32px}.aside-container[data-v-83890dd9]{position:fixed;top:0;padding-top:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 48px);width:224px;height:100vh;overflow-x:hidden;overflow-y:auto;scrollbar-width:none}.aside-container[data-v-83890dd9]::-webkit-scrollbar{display:none}.aside-curtain[data-v-83890dd9]{position:fixed;bottom:0;z-index:10;width:224px;height:32px;background:linear-gradient(transparent,var(--vp-c-bg) 70%)}.aside-content[data-v-83890dd9]{display:flex;flex-direction:column;min-height:calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px));padding-bottom:32px}.content[data-v-83890dd9]{position:relative;margin:0 auto;width:100%}@media (min-width: 960px){.content[data-v-83890dd9]{padding:0 32px 128px}}@media (min-width: 1280px){.content[data-v-83890dd9]{order:1;margin:0;min-width:640px}}.content-container[data-v-83890dd9]{margin:0 auto}.VPDoc.has-aside .content-container[data-v-83890dd9]{max-width:688px}.VPButton[data-v-906d7fb4]{display:inline-block;border:1px solid transparent;text-align:center;font-weight:600;white-space:nowrap;transition:color .25s,border-color .25s,background-color .25s}.VPButton[data-v-906d7fb4]:active{transition:color .1s,border-color .1s,background-color .1s}.VPButton.medium[data-v-906d7fb4]{border-radius:20px;padding:0 20px;line-height:38px;font-size:14px}.VPButton.big[data-v-906d7fb4]{border-radius:24px;padding:0 24px;line-height:46px;font-size:16px}.VPButton.brand[data-v-906d7fb4]{border-color:var(--vp-button-brand-border);color:var(--vp-button-brand-text);background-color:var(--vp-button-brand-bg)}.VPButton.brand[data-v-906d7fb4]:hover{border-color:var(--vp-button-brand-hover-border);color:var(--vp-button-brand-hover-text);background-color:var(--vp-button-brand-hover-bg)}.VPButton.brand[data-v-906d7fb4]:active{border-color:var(--vp-button-brand-active-border);color:var(--vp-button-brand-active-text);background-color:var(--vp-button-brand-active-bg)}.VPButton.alt[data-v-906d7fb4]{border-color:var(--vp-button-alt-border);color:var(--vp-button-alt-text);background-color:var(--vp-button-alt-bg)}.VPButton.alt[data-v-906d7fb4]:hover{border-color:var(--vp-button-alt-hover-border);color:var(--vp-button-alt-hover-text);background-color:var(--vp-button-alt-hover-bg)}.VPButton.alt[data-v-906d7fb4]:active{border-color:var(--vp-button-alt-active-border);color:var(--vp-button-alt-active-text);background-color:var(--vp-button-alt-active-bg)}.VPButton.sponsor[data-v-906d7fb4]{border-color:var(--vp-button-sponsor-border);color:var(--vp-button-sponsor-text);background-color:var(--vp-button-sponsor-bg)}.VPButton.sponsor[data-v-906d7fb4]:hover{border-color:var(--vp-button-sponsor-hover-border);color:var(--vp-button-sponsor-hover-text);background-color:var(--vp-button-sponsor-hover-bg)}.VPButton.sponsor[data-v-906d7fb4]:active{border-color:var(--vp-button-sponsor-active-border);color:var(--vp-button-sponsor-active-text);background-color:var(--vp-button-sponsor-active-bg)}html:not(.dark) .VPImage.dark[data-v-35a7d0b8]{display:none}.dark .VPImage.light[data-v-35a7d0b8]{display:none}.VPHero[data-v-955009fc]{margin-top:calc((var(--vp-nav-height) + var(--vp-layout-top-height, 0px)) * -1);padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px) 24px 48px}@media (min-width: 640px){.VPHero[data-v-955009fc]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 48px 64px}}@media (min-width: 960px){.VPHero[data-v-955009fc]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 64px 64px}}.container[data-v-955009fc]{display:flex;flex-direction:column;margin:0 auto;max-width:1152px}@media (min-width: 960px){.container[data-v-955009fc]{flex-direction:row}}.main[data-v-955009fc]{position:relative;z-index:10;order:2;flex-grow:1;flex-shrink:0}.VPHero.has-image .container[data-v-955009fc]{text-align:center}@media (min-width: 960px){.VPHero.has-image .container[data-v-955009fc]{text-align:left}}@media (min-width: 960px){.main[data-v-955009fc]{order:1;width:calc((100% / 3) * 2)}.VPHero.has-image .main[data-v-955009fc]{max-width:592px}}.name[data-v-955009fc],.text[data-v-955009fc]{max-width:392px;letter-spacing:-.4px;line-height:40px;font-size:32px;font-weight:700;white-space:pre-wrap}.VPHero.has-image .name[data-v-955009fc],.VPHero.has-image .text[data-v-955009fc]{margin:0 auto}.name[data-v-955009fc]{color:var(--vp-home-hero-name-color)}.clip[data-v-955009fc]{background:var(--vp-home-hero-name-background);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:var(--vp-home-hero-name-color)}@media (min-width: 640px){.name[data-v-955009fc],.text[data-v-955009fc]{max-width:576px;line-height:56px;font-size:48px}}@media (min-width: 960px){.name[data-v-955009fc],.text[data-v-955009fc]{line-height:64px;font-size:56px}.VPHero.has-image .name[data-v-955009fc],.VPHero.has-image .text[data-v-955009fc]{margin:0}}.tagline[data-v-955009fc]{padding-top:8px;max-width:392px;line-height:28px;font-size:18px;font-weight:500;white-space:pre-wrap;color:var(--vp-c-text-2)}.VPHero.has-image .tagline[data-v-955009fc]{margin:0 auto}@media (min-width: 640px){.tagline[data-v-955009fc]{padding-top:12px;max-width:576px;line-height:32px;font-size:20px}}@media (min-width: 960px){.tagline[data-v-955009fc]{line-height:36px;font-size:24px}.VPHero.has-image .tagline[data-v-955009fc]{margin:0}}.actions[data-v-955009fc]{display:flex;flex-wrap:wrap;margin:-6px;padding-top:24px}.VPHero.has-image .actions[data-v-955009fc]{justify-content:center}@media (min-width: 640px){.actions[data-v-955009fc]{padding-top:32px}}@media (min-width: 960px){.VPHero.has-image .actions[data-v-955009fc]{justify-content:flex-start}}.action[data-v-955009fc]{flex-shrink:0;padding:6px}.image[data-v-955009fc]{order:1;margin:-76px -24px -48px}@media (min-width: 640px){.image[data-v-955009fc]{margin:-108px -24px -48px}}@media (min-width: 960px){.image[data-v-955009fc]{flex-grow:1;order:2;margin:0;min-height:100%}}.image-container[data-v-955009fc]{position:relative;margin:0 auto;width:320px;height:320px}@media (min-width: 640px){.image-container[data-v-955009fc]{width:392px;height:392px}}@media (min-width: 960px){.image-container[data-v-955009fc]{display:flex;justify-content:center;align-items:center;width:100%;height:100%;transform:translate(-32px,-32px)}}.image-bg[data-v-955009fc]{position:absolute;top:50%;left:50%;border-radius:50%;width:192px;height:192px;background-image:var(--vp-home-hero-image-background-image);filter:var(--vp-home-hero-image-filter);transform:translate(-50%,-50%)}@media (min-width: 640px){.image-bg[data-v-955009fc]{width:256px;height:256px}}@media (min-width: 960px){.image-bg[data-v-955009fc]{width:320px;height:320px}}[data-v-955009fc] .image-src{position:absolute;top:50%;left:50%;max-width:192px;max-height:192px;transform:translate(-50%,-50%)}@media (min-width: 640px){[data-v-955009fc] .image-src{max-width:256px;max-height:256px}}@media (min-width: 960px){[data-v-955009fc] .image-src{max-width:320px;max-height:320px}}.VPFeature[data-v-f5e9645b]{display:block;border:1px solid var(--vp-c-bg-soft);border-radius:12px;height:100%;background-color:var(--vp-c-bg-soft);transition:border-color .25s,background-color .25s}.VPFeature.link[data-v-f5e9645b]:hover{border-color:var(--vp-c-brand-1)}.box[data-v-f5e9645b]{display:flex;flex-direction:column;padding:24px;height:100%}.box[data-v-f5e9645b]>.VPImage{margin-bottom:20px}.icon[data-v-f5e9645b]{display:flex;justify-content:center;align-items:center;margin-bottom:20px;border-radius:6px;background-color:var(--vp-c-default-soft);width:48px;height:48px;font-size:24px;transition:background-color .25s}.title[data-v-f5e9645b]{line-height:24px;font-size:16px;font-weight:600}.details[data-v-f5e9645b]{flex-grow:1;padding-top:8px;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.link-text[data-v-f5e9645b]{padding-top:8px}.link-text-value[data-v-f5e9645b]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.link-text-icon[data-v-f5e9645b]{margin-left:6px}.VPFeatures[data-v-d0a190d7]{position:relative;padding:0 24px}@media (min-width: 640px){.VPFeatures[data-v-d0a190d7]{padding:0 48px}}@media (min-width: 960px){.VPFeatures[data-v-d0a190d7]{padding:0 64px}}.container[data-v-d0a190d7]{margin:0 auto;max-width:1152px}.items[data-v-d0a190d7]{display:flex;flex-wrap:wrap;margin:-8px}.item[data-v-d0a190d7]{padding:8px;width:100%}@media (min-width: 640px){.item.grid-2[data-v-d0a190d7],.item.grid-4[data-v-d0a190d7],.item.grid-6[data-v-d0a190d7]{width:50%}}@media (min-width: 768px){.item.grid-2[data-v-d0a190d7],.item.grid-4[data-v-d0a190d7]{width:50%}.item.grid-3[data-v-d0a190d7],.item.grid-6[data-v-d0a190d7]{width:calc(100% / 3)}}@media (min-width: 960px){.item.grid-4[data-v-d0a190d7]{width:25%}}.container[data-v-7a48a447]{margin:auto;width:100%;max-width:1280px;padding:0 24px}@media (min-width: 640px){.container[data-v-7a48a447]{padding:0 48px}}@media (min-width: 960px){.container[data-v-7a48a447]{width:100%;padding:0 64px}}.vp-doc[data-v-7a48a447] .VPHomeSponsors,.vp-doc[data-v-7a48a447] .VPTeamPage{margin-left:var(--vp-offset, calc(50% - 50vw) );margin-right:var(--vp-offset, calc(50% - 50vw) )}.vp-doc[data-v-7a48a447] .VPHomeSponsors h2{border-top:none;letter-spacing:normal}.vp-doc[data-v-7a48a447] .VPHomeSponsors a,.vp-doc[data-v-7a48a447] .VPTeamPage a{text-decoration:none}.VPHome[data-v-cbb6ec48]{margin-bottom:96px}@media (min-width: 768px){.VPHome[data-v-cbb6ec48]{margin-bottom:128px}}.VPContent[data-v-91765379]{flex-grow:1;flex-shrink:0;margin:var(--vp-layout-top-height, 0px) auto 0;width:100%}.VPContent.is-home[data-v-91765379]{width:100%;max-width:100%}.VPContent.has-sidebar[data-v-91765379]{margin:0}@media (min-width: 960px){.VPContent[data-v-91765379]{padding-top:var(--vp-nav-height)}.VPContent.has-sidebar[data-v-91765379]{margin:var(--vp-layout-top-height, 0px) 0 0;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPContent.has-sidebar[data-v-91765379]{padding-right:calc((100vw - var(--vp-layout-max-width)) / 2);padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.VPFooter[data-v-c970a860]{position:relative;z-index:var(--vp-z-index-footer);border-top:1px solid var(--vp-c-gutter);padding:32px 24px;background-color:var(--vp-c-bg)}.VPFooter.has-sidebar[data-v-c970a860]{display:none}.VPFooter[data-v-c970a860] a{text-decoration-line:underline;text-underline-offset:2px;transition:color .25s}.VPFooter[data-v-c970a860] a:hover{color:var(--vp-c-text-1)}@media (min-width: 768px){.VPFooter[data-v-c970a860]{padding:32px}}.container[data-v-c970a860]{margin:0 auto;max-width:var(--vp-layout-max-width);text-align:center}.message[data-v-c970a860],.copyright[data-v-c970a860]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.VPLocalNavOutlineDropdown[data-v-bc9dc845]{padding:12px 20px 11px}@media (min-width: 960px){.VPLocalNavOutlineDropdown[data-v-bc9dc845]{padding:12px 36px 11px}}.VPLocalNavOutlineDropdown button[data-v-bc9dc845]{display:block;font-size:12px;font-weight:500;line-height:24px;color:var(--vp-c-text-2);transition:color .5s;position:relative}.VPLocalNavOutlineDropdown button[data-v-bc9dc845]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPLocalNavOutlineDropdown button.open[data-v-bc9dc845]{color:var(--vp-c-text-1)}.icon[data-v-bc9dc845]{display:inline-block;vertical-align:middle;margin-left:2px;font-size:14px;transform:rotate(0);transition:transform .25s}@media (min-width: 960px){.VPLocalNavOutlineDropdown button[data-v-bc9dc845]{font-size:14px}.icon[data-v-bc9dc845]{font-size:16px}}.open>.icon[data-v-bc9dc845]{transform:rotate(90deg)}.items[data-v-bc9dc845]{position:absolute;top:40px;right:16px;left:16px;display:grid;gap:1px;border:1px solid var(--vp-c-border);border-radius:8px;background-color:var(--vp-c-gutter);max-height:calc(var(--vp-vh, 100vh) - 86px);overflow:hidden auto;box-shadow:var(--vp-shadow-3)}@media (min-width: 960px){.items[data-v-bc9dc845]{right:auto;left:calc(var(--vp-sidebar-width) + 32px);width:320px}}.header[data-v-bc9dc845]{background-color:var(--vp-c-bg-soft)}.top-link[data-v-bc9dc845]{display:block;padding:0 16px;line-height:48px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.outline[data-v-bc9dc845]{padding:8px 0;background-color:var(--vp-c-bg-soft)}.flyout-enter-active[data-v-bc9dc845]{transition:all .2s ease-out}.flyout-leave-active[data-v-bc9dc845]{transition:all .15s ease-in}.flyout-enter-from[data-v-bc9dc845],.flyout-leave-to[data-v-bc9dc845]{opacity:0;transform:translateY(-16px)}.VPLocalNav[data-v-070ab83d]{position:sticky;top:0;left:0;z-index:var(--vp-z-index-local-nav);border-bottom:1px solid var(--vp-c-gutter);padding-top:var(--vp-layout-top-height, 0px);width:100%;background-color:var(--vp-local-nav-bg-color)}.VPLocalNav.fixed[data-v-070ab83d]{position:fixed}@media (min-width: 960px){.VPLocalNav[data-v-070ab83d]{top:var(--vp-nav-height)}.VPLocalNav.has-sidebar[data-v-070ab83d]{padding-left:var(--vp-sidebar-width)}.VPLocalNav.empty[data-v-070ab83d]{display:none}}@media (min-width: 1280px){.VPLocalNav[data-v-070ab83d]{display:none}}@media (min-width: 1440px){.VPLocalNav.has-sidebar[data-v-070ab83d]{padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.container[data-v-070ab83d]{display:flex;justify-content:space-between;align-items:center}.menu[data-v-070ab83d]{display:flex;align-items:center;padding:12px 24px 11px;line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.menu[data-v-070ab83d]:hover{color:var(--vp-c-text-1);transition:color .25s}@media (min-width: 768px){.menu[data-v-070ab83d]{padding:0 32px}}@media (min-width: 960px){.menu[data-v-070ab83d]{display:none}}.menu-icon[data-v-070ab83d]{margin-right:8px;font-size:14px}.VPOutlineDropdown[data-v-070ab83d]{padding:12px 24px 11px}@media (min-width: 768px){.VPOutlineDropdown[data-v-070ab83d]{padding:12px 32px 11px}}.VPSwitch[data-v-4a1c76db]{position:relative;border-radius:11px;display:block;width:40px;height:22px;flex-shrink:0;border:1px solid var(--vp-input-border-color);background-color:var(--vp-input-switch-bg-color);transition:border-color .25s!important}.VPSwitch[data-v-4a1c76db]:hover{border-color:var(--vp-c-brand-1)}.check[data-v-4a1c76db]{position:absolute;top:1px;left:1px;width:18px;height:18px;border-radius:50%;background-color:var(--vp-c-neutral-inverse);box-shadow:var(--vp-shadow-1);transition:transform .25s!important}.icon[data-v-4a1c76db]{position:relative;display:block;width:18px;height:18px;border-radius:50%;overflow:hidden}.icon[data-v-4a1c76db] [class^=vpi-]{position:absolute;top:3px;left:3px;width:12px;height:12px;color:var(--vp-c-text-2)}.dark .icon[data-v-4a1c76db] [class^=vpi-]{color:var(--vp-c-text-1);transition:opacity .25s!important}.sun[data-v-e40a8bb6]{opacity:1}.moon[data-v-e40a8bb6],.dark .sun[data-v-e40a8bb6]{opacity:0}.dark .moon[data-v-e40a8bb6]{opacity:1}.dark .VPSwitchAppearance[data-v-e40a8bb6] .check{transform:translate(18px)}.VPNavBarAppearance[data-v-af096f4a]{display:none}@media (min-width: 1280px){.VPNavBarAppearance[data-v-af096f4a]{display:flex;align-items:center}}.VPMenuGroup+.VPMenuLink[data-v-acbfed09]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.link[data-v-acbfed09]{display:block;border-radius:6px;padding:0 12px;line-height:32px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);white-space:nowrap;transition:background-color .25s,color .25s}.link[data-v-acbfed09]:hover{color:var(--vp-c-brand-1);background-color:var(--vp-c-default-soft)}.link.active[data-v-acbfed09]{color:var(--vp-c-brand-1)}.VPMenuGroup[data-v-48c802d0]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.VPMenuGroup[data-v-48c802d0]:first-child{margin-top:0;border-top:0;padding-top:0}.VPMenuGroup+.VPMenuGroup[data-v-48c802d0]{margin-top:12px;border-top:1px solid var(--vp-c-divider)}.title[data-v-48c802d0]{padding:0 12px;line-height:32px;font-size:14px;font-weight:600;color:var(--vp-c-text-2);white-space:nowrap;transition:color .25s}.VPMenu[data-v-7dd3104a]{border-radius:12px;padding:12px;min-width:128px;border:1px solid var(--vp-c-divider);background-color:var(--vp-c-bg-elv);box-shadow:var(--vp-shadow-3);transition:background-color .5s;max-height:calc(100vh - var(--vp-nav-height));overflow-y:auto}.VPMenu[data-v-7dd3104a] .group{margin:0 -12px;padding:0 12px 12px}.VPMenu[data-v-7dd3104a] .group+.group{border-top:1px solid var(--vp-c-divider);padding:11px 12px 12px}.VPMenu[data-v-7dd3104a] .group:last-child{padding-bottom:0}.VPMenu[data-v-7dd3104a] .group+.item{border-top:1px solid var(--vp-c-divider);padding:11px 16px 0}.VPMenu[data-v-7dd3104a] .item{padding:0 16px;white-space:nowrap}.VPMenu[data-v-7dd3104a] .label{flex-grow:1;line-height:28px;font-size:12px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.VPMenu[data-v-7dd3104a] .action{padding-left:24px}.VPFlyout[data-v-04f5c5e9]{position:relative}.VPFlyout[data-v-04f5c5e9]:hover{color:var(--vp-c-brand-1);transition:color .25s}.VPFlyout:hover .text[data-v-04f5c5e9]{color:var(--vp-c-text-2)}.VPFlyout:hover .icon[data-v-04f5c5e9]{fill:var(--vp-c-text-2)}.VPFlyout.active .text[data-v-04f5c5e9]{color:var(--vp-c-brand-1)}.VPFlyout.active:hover .text[data-v-04f5c5e9]{color:var(--vp-c-brand-2)}.button[aria-expanded=false]+.menu[data-v-04f5c5e9]{opacity:0;visibility:hidden;transform:translateY(0)}.VPFlyout:hover .menu[data-v-04f5c5e9],.button[aria-expanded=true]+.menu[data-v-04f5c5e9]{opacity:1;visibility:visible;transform:translateY(0)}.button[data-v-04f5c5e9]{display:flex;align-items:center;padding:0 12px;height:var(--vp-nav-height);color:var(--vp-c-text-1);transition:color .5s}.text[data-v-04f5c5e9]{display:flex;align-items:center;line-height:var(--vp-nav-height);font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.option-icon[data-v-04f5c5e9]{margin-right:0;font-size:16px}.text-icon[data-v-04f5c5e9]{margin-left:4px;font-size:14px}.icon[data-v-04f5c5e9]{font-size:20px;transition:fill .25s}.menu[data-v-04f5c5e9]{position:absolute;top:calc(var(--vp-nav-height) / 2 + 20px);right:0;opacity:0;visibility:hidden;transition:opacity .25s,visibility .25s,transform .25s}.VPSocialLink[data-v-d26d30cb]{display:flex;justify-content:center;align-items:center;width:36px;height:36px;color:var(--vp-c-text-2);transition:color .5s}.VPSocialLink[data-v-d26d30cb]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPSocialLink[data-v-d26d30cb]>svg,.VPSocialLink[data-v-d26d30cb]>[class^=vpi-social-]{width:20px;height:20px;fill:currentColor}.VPSocialLinks[data-v-ee7a9424]{display:flex;justify-content:center}.VPNavBarExtra[data-v-925effce]{display:none;margin-right:-12px}@media (min-width: 768px){.VPNavBarExtra[data-v-925effce]{display:block}}@media (min-width: 1280px){.VPNavBarExtra[data-v-925effce]{display:none}}.trans-title[data-v-925effce]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.item.appearance[data-v-925effce],.item.social-links[data-v-925effce]{display:flex;align-items:center;padding:0 12px}.item.appearance[data-v-925effce]{min-width:176px}.appearance-action[data-v-925effce]{margin-right:-2px}.social-links-list[data-v-925effce]{margin:-4px -8px}.VPNavBarHamburger[data-v-5dea55bf]{display:flex;justify-content:center;align-items:center;width:48px;height:var(--vp-nav-height)}@media (min-width: 768px){.VPNavBarHamburger[data-v-5dea55bf]{display:none}}.container[data-v-5dea55bf]{position:relative;width:16px;height:14px;overflow:hidden}.VPNavBarHamburger:hover .top[data-v-5dea55bf]{top:0;left:0;transform:translate(4px)}.VPNavBarHamburger:hover .middle[data-v-5dea55bf]{top:6px;left:0;transform:translate(0)}.VPNavBarHamburger:hover .bottom[data-v-5dea55bf]{top:12px;left:0;transform:translate(8px)}.VPNavBarHamburger.active .top[data-v-5dea55bf]{top:6px;transform:translate(0) rotate(225deg)}.VPNavBarHamburger.active .middle[data-v-5dea55bf]{top:6px;transform:translate(16px)}.VPNavBarHamburger.active .bottom[data-v-5dea55bf]{top:6px;transform:translate(0) rotate(135deg)}.VPNavBarHamburger.active:hover .top[data-v-5dea55bf],.VPNavBarHamburger.active:hover .middle[data-v-5dea55bf],.VPNavBarHamburger.active:hover .bottom[data-v-5dea55bf]{background-color:var(--vp-c-text-2);transition:top .25s,background-color .25s,transform .25s}.top[data-v-5dea55bf],.middle[data-v-5dea55bf],.bottom[data-v-5dea55bf]{position:absolute;width:16px;height:2px;background-color:var(--vp-c-text-1);transition:top .25s,background-color .5s,transform .25s}.top[data-v-5dea55bf]{top:0;left:0;transform:translate(0)}.middle[data-v-5dea55bf]{top:6px;left:0;transform:translate(8px)}.bottom[data-v-5dea55bf]{top:12px;left:0;transform:translate(4px)}.VPNavBarMenuLink[data-v-956ec74c]{display:flex;align-items:center;padding:0 12px;line-height:var(--vp-nav-height);font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.VPNavBarMenuLink.active[data-v-956ec74c],.VPNavBarMenuLink[data-v-956ec74c]:hover{color:var(--vp-c-brand-1)}.VPNavBarMenu[data-v-e6d46098]{display:none}@media (min-width: 768px){.VPNavBarMenu[data-v-e6d46098]{display:flex}}/*! @docsearch/css 3.8.0 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */:root{--docsearch-primary-color:#5468ff;--docsearch-text-color:#1c1e21;--docsearch-spacing:12px;--docsearch-icon-stroke-width:1.4;--docsearch-highlight-color:var(--docsearch-primary-color);--docsearch-muted-color:#969faf;--docsearch-container-background:rgba(101,108,133,.8);--docsearch-logo-color:#5468ff;--docsearch-modal-width:560px;--docsearch-modal-height:600px;--docsearch-modal-background:#f5f6f7;--docsearch-modal-shadow:inset 1px 1px 0 0 hsla(0,0%,100%,.5),0 3px 8px 0 #555a64;--docsearch-searchbox-height:56px;--docsearch-searchbox-background:#ebedf0;--docsearch-searchbox-focus-background:#fff;--docsearch-searchbox-shadow:inset 0 0 0 2px var(--docsearch-primary-color);--docsearch-hit-height:56px;--docsearch-hit-color:#444950;--docsearch-hit-active-color:#fff;--docsearch-hit-background:#fff;--docsearch-hit-shadow:0 1px 3px 0 #d4d9e1;--docsearch-key-gradient:linear-gradient(-225deg,#d5dbe4,#f8f8f8);--docsearch-key-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 2px 1px rgba(30,35,90,.4);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 1px 0 rgba(30,35,90,.4);--docsearch-footer-height:44px;--docsearch-footer-background:#fff;--docsearch-footer-shadow:0 -1px 0 0 #e0e3e8,0 -3px 6px 0 rgba(69,98,155,.12)}html[data-theme=dark]{--docsearch-text-color:#f5f6f7;--docsearch-container-background:rgba(9,10,17,.8);--docsearch-modal-background:#15172a;--docsearch-modal-shadow:inset 1px 1px 0 0 #2c2e40,0 3px 8px 0 #000309;--docsearch-searchbox-background:#090a11;--docsearch-searchbox-focus-background:#000;--docsearch-hit-color:#bec3c9;--docsearch-hit-shadow:none;--docsearch-hit-background:#090a11;--docsearch-key-gradient:linear-gradient(-26.5deg,#565872,#31355b);--docsearch-key-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 2px 2px 0 rgba(3,4,9,.3);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 1px 1px 0 #0304094d;--docsearch-footer-background:#1e2136;--docsearch-footer-shadow:inset 0 1px 0 0 rgba(73,76,106,.5),0 -4px 8px 0 rgba(0,0,0,.2);--docsearch-logo-color:#fff;--docsearch-muted-color:#7f8497}.DocSearch-Button{align-items:center;background:var(--docsearch-searchbox-background);border:0;border-radius:40px;color:var(--docsearch-muted-color);cursor:pointer;display:flex;font-weight:500;height:36px;justify-content:space-between;margin:0 0 0 16px;padding:0 8px;-webkit-user-select:none;user-select:none}.DocSearch-Button:active,.DocSearch-Button:focus,.DocSearch-Button:hover{background:var(--docsearch-searchbox-focus-background);box-shadow:var(--docsearch-searchbox-shadow);color:var(--docsearch-text-color);outline:none}.DocSearch-Button-Container{align-items:center;display:flex}.DocSearch-Search-Icon{stroke-width:1.6}.DocSearch-Button .DocSearch-Search-Icon{color:var(--docsearch-text-color)}.DocSearch-Button-Placeholder{font-size:1rem;padding:0 12px 0 6px}.DocSearch-Button-Keys{display:flex;min-width:calc(40px + .8em)}.DocSearch-Button-Key{align-items:center;background:var(--docsearch-key-gradient);border:0;border-radius:3px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 2px;position:relative;top:-1px;width:20px}.DocSearch-Button-Key--pressed{box-shadow:var(--docsearch-key-pressed-shadow);transform:translate3d(0,1px,0)}@media (max-width:768px){.DocSearch-Button-Keys,.DocSearch-Button-Placeholder{display:none}}.DocSearch--active{overflow:hidden!important}.DocSearch-Container,.DocSearch-Container *{box-sizing:border-box}.DocSearch-Container{background-color:var(--docsearch-container-background);height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:200}.DocSearch-Container a{text-decoration:none}.DocSearch-Link{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;font:inherit;margin:0;padding:0}.DocSearch-Modal{background:var(--docsearch-modal-background);border-radius:6px;box-shadow:var(--docsearch-modal-shadow);flex-direction:column;margin:60px auto auto;max-width:var(--docsearch-modal-width);position:relative}.DocSearch-SearchBar{display:flex;padding:var(--docsearch-spacing) var(--docsearch-spacing) 0}.DocSearch-Form{align-items:center;background:var(--docsearch-searchbox-focus-background);border-radius:4px;box-shadow:var(--docsearch-searchbox-shadow);display:flex;height:var(--docsearch-searchbox-height);margin:0;padding:0 var(--docsearch-spacing);position:relative;width:100%}.DocSearch-Input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:0;color:var(--docsearch-text-color);flex:1;font:inherit;font-size:1.2em;height:100%;outline:none;padding:0 0 0 8px;width:80%}.DocSearch-Input::placeholder{color:var(--docsearch-muted-color);opacity:1}.DocSearch-Input::-webkit-search-cancel-button,.DocSearch-Input::-webkit-search-decoration,.DocSearch-Input::-webkit-search-results-button,.DocSearch-Input::-webkit-search-results-decoration{display:none}.DocSearch-LoadingIndicator,.DocSearch-MagnifierLabel,.DocSearch-Reset{margin:0;padding:0}.DocSearch-MagnifierLabel,.DocSearch-Reset{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}.DocSearch-Container--Stalled .DocSearch-MagnifierLabel,.DocSearch-LoadingIndicator{display:none}.DocSearch-Container--Stalled .DocSearch-LoadingIndicator{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Reset{animation:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;right:0;stroke-width:var(--docsearch-icon-stroke-width)}}.DocSearch-Reset{animation:fade-in .1s ease-in forwards;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;padding:2px;right:0;stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Reset[hidden]{display:none}.DocSearch-Reset:hover{color:var(--docsearch-highlight-color)}.DocSearch-LoadingIndicator svg,.DocSearch-MagnifierLabel svg{height:24px;width:24px}.DocSearch-Cancel{display:none}.DocSearch-Dropdown{max-height:calc(var(--docsearch-modal-height) - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height));min-height:var(--docsearch-spacing);overflow-y:auto;overflow-y:overlay;padding:0 var(--docsearch-spacing);scrollbar-color:var(--docsearch-muted-color) var(--docsearch-modal-background);scrollbar-width:thin}.DocSearch-Dropdown::-webkit-scrollbar{width:12px}.DocSearch-Dropdown::-webkit-scrollbar-track{background:transparent}.DocSearch-Dropdown::-webkit-scrollbar-thumb{background-color:var(--docsearch-muted-color);border:3px solid var(--docsearch-modal-background);border-radius:20px}.DocSearch-Dropdown ul{list-style:none;margin:0;padding:0}.DocSearch-Label{font-size:.75em;line-height:1.6em}.DocSearch-Help,.DocSearch-Label{color:var(--docsearch-muted-color)}.DocSearch-Help{font-size:.9em;margin:0;-webkit-user-select:none;user-select:none}.DocSearch-Title{font-size:1.2em}.DocSearch-Logo a{display:flex}.DocSearch-Logo svg{color:var(--docsearch-logo-color);margin-left:8px}.DocSearch-Hits:last-of-type{margin-bottom:24px}.DocSearch-Hits mark{background:none;color:var(--docsearch-highlight-color)}.DocSearch-HitsFooter{color:var(--docsearch-muted-color);display:flex;font-size:.85em;justify-content:center;margin-bottom:var(--docsearch-spacing);padding:var(--docsearch-spacing)}.DocSearch-HitsFooter a{border-bottom:1px solid;color:inherit}.DocSearch-Hit{border-radius:4px;display:flex;padding-bottom:4px;position:relative}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--deleting{transition:none}}.DocSearch-Hit--deleting{opacity:0;transition:all .25s linear}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--favoriting{transition:none}}.DocSearch-Hit--favoriting{transform:scale(0);transform-origin:top center;transition:all .25s linear;transition-delay:.25s}.DocSearch-Hit a{background:var(--docsearch-hit-background);border-radius:4px;box-shadow:var(--docsearch-hit-shadow);display:block;padding-left:var(--docsearch-spacing);width:100%}.DocSearch-Hit-source{background:var(--docsearch-modal-background);color:var(--docsearch-highlight-color);font-size:.85em;font-weight:600;line-height:32px;margin:0 -4px;padding:8px 4px 0;position:sticky;top:0;z-index:10}.DocSearch-Hit-Tree{color:var(--docsearch-muted-color);height:var(--docsearch-hit-height);opacity:.5;stroke-width:var(--docsearch-icon-stroke-width);width:24px}.DocSearch-Hit[aria-selected=true] a{background-color:var(--docsearch-highlight-color)}.DocSearch-Hit[aria-selected=true] mark{text-decoration:underline}.DocSearch-Hit-Container{align-items:center;color:var(--docsearch-hit-color);display:flex;flex-direction:row;height:var(--docsearch-hit-height);padding:0 var(--docsearch-spacing) 0 0}.DocSearch-Hit-icon{height:20px;width:20px}.DocSearch-Hit-action,.DocSearch-Hit-icon{color:var(--docsearch-muted-color);stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Hit-action{align-items:center;display:flex;height:22px;width:22px}.DocSearch-Hit-action svg{display:block;height:18px;width:18px}.DocSearch-Hit-action+.DocSearch-Hit-action{margin-left:6px}.DocSearch-Hit-action-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:inherit;cursor:pointer;padding:2px}svg.DocSearch-Hit-Select-Icon{display:none}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Select-Icon{display:block}.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:#0003;transition:background-color .1s ease-in}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{transition:none}}.DocSearch-Hit-action-button:focus path,.DocSearch-Hit-action-button:hover path{fill:#fff}.DocSearch-Hit-content-wrapper{display:flex;flex:1 1 auto;flex-direction:column;font-weight:500;justify-content:center;line-height:1.2em;margin:0 8px;overflow-x:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap;width:80%}.DocSearch-Hit-title{font-size:.9em}.DocSearch-Hit-path{color:var(--docsearch-muted-color);font-size:.75em}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Tree,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-action,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-icon,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-path,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-text,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-title,.DocSearch-Hit[aria-selected=true] mark{color:var(--docsearch-hit-active-color)!important}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:#0003;transition:none}}.DocSearch-ErrorScreen,.DocSearch-NoResults,.DocSearch-StartScreen{font-size:.9em;margin:0 auto;padding:36px 0;text-align:center;width:80%}.DocSearch-Screen-Icon{color:var(--docsearch-muted-color);padding-bottom:12px}.DocSearch-NoResults-Prefill-List{display:inline-block;padding-bottom:24px;text-align:left}.DocSearch-NoResults-Prefill-List ul{display:inline-block;padding:8px 0 0}.DocSearch-NoResults-Prefill-List li{list-style-position:inside;list-style-type:"» "}.DocSearch-Prefill{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:1em;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;font-size:1em;font-weight:700;padding:0}.DocSearch-Prefill:focus,.DocSearch-Prefill:hover{outline:none;text-decoration:underline}.DocSearch-Footer{align-items:center;background:var(--docsearch-footer-background);border-radius:0 0 8px 8px;box-shadow:var(--docsearch-footer-shadow);display:flex;flex-direction:row-reverse;flex-shrink:0;height:var(--docsearch-footer-height);justify-content:space-between;padding:0 var(--docsearch-spacing);position:relative;-webkit-user-select:none;user-select:none;width:100%;z-index:300}.DocSearch-Commands{color:var(--docsearch-muted-color);display:flex;list-style:none;margin:0;padding:0}.DocSearch-Commands li{align-items:center;display:flex}.DocSearch-Commands li:not(:last-of-type){margin-right:.8em}.DocSearch-Commands-Key{align-items:center;background:var(--docsearch-key-gradient);border:0;border-radius:2px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 1px;width:20px}.DocSearch-VisuallyHiddenForAccessibility{clip:rect(0 0 0 0);clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}@media (max-width:768px){:root{--docsearch-spacing:10px;--docsearch-footer-height:40px}.DocSearch-Dropdown{height:100%}.DocSearch-Container{height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);position:absolute}.DocSearch-Footer{border-radius:0;bottom:0;position:absolute}.DocSearch-Hit-content-wrapper{display:flex;position:relative;width:80%}.DocSearch-Modal{border-radius:0;box-shadow:none;height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);margin:0;max-width:100%;width:100%}.DocSearch-Dropdown{max-height:calc(var(--docsearch-vh, 1vh)*100 - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height))}.DocSearch-Cancel{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;flex:none;font:inherit;font-size:1em;font-weight:500;margin-left:var(--docsearch-spacing);outline:none;overflow:hidden;padding:0;-webkit-user-select:none;user-select:none;white-space:nowrap}.DocSearch-Commands,.DocSearch-Hit-Tree{display:none}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}[class*=DocSearch]{--docsearch-primary-color: var(--vp-c-brand-1);--docsearch-highlight-color: var(--docsearch-primary-color);--docsearch-text-color: var(--vp-c-text-1);--docsearch-muted-color: var(--vp-c-text-2);--docsearch-searchbox-shadow: none;--docsearch-searchbox-background: transparent;--docsearch-searchbox-focus-background: transparent;--docsearch-key-gradient: transparent;--docsearch-key-shadow: none;--docsearch-modal-background: var(--vp-c-bg-soft);--docsearch-footer-background: var(--vp-c-bg)}.dark [class*=DocSearch]{--docsearch-modal-shadow: none;--docsearch-footer-shadow: none;--docsearch-logo-color: var(--vp-c-text-2);--docsearch-hit-background: var(--vp-c-default-soft);--docsearch-hit-color: var(--vp-c-text-2);--docsearch-hit-shadow: none}.DocSearch-Button{display:flex;justify-content:center;align-items:center;margin:0;padding:0;width:48px;height:55px;background:transparent;transition:border-color .25s}.DocSearch-Button:hover{background:transparent}.DocSearch-Button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}.DocSearch-Button-Key--pressed{transform:none;box-shadow:none}.DocSearch-Button:focus:not(:focus-visible){outline:none!important}@media (min-width: 768px){.DocSearch-Button{justify-content:flex-start;border:1px solid transparent;border-radius:8px;padding:0 10px 0 12px;width:100%;height:40px;background-color:var(--vp-c-bg-alt)}.DocSearch-Button:hover{border-color:var(--vp-c-brand-1);background:var(--vp-c-bg-alt)}}.DocSearch-Button .DocSearch-Button-Container{display:flex;align-items:center}.DocSearch-Button .DocSearch-Search-Icon{position:relative;width:16px;height:16px;color:var(--vp-c-text-1);fill:currentColor;transition:color .5s}.DocSearch-Button:hover .DocSearch-Search-Icon{color:var(--vp-c-text-1)}@media (min-width: 768px){.DocSearch-Button .DocSearch-Search-Icon{top:1px;margin-right:8px;width:14px;height:14px;color:var(--vp-c-text-2)}}.DocSearch-Button .DocSearch-Button-Placeholder{display:none;margin-top:2px;padding:0 16px 0 0;font-size:13px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.DocSearch-Button:hover .DocSearch-Button-Placeholder{color:var(--vp-c-text-1)}@media (min-width: 768px){.DocSearch-Button .DocSearch-Button-Placeholder{display:inline-block}}.DocSearch-Button .DocSearch-Button-Keys{direction:ltr;display:none;min-width:auto}@media (min-width: 768px){.DocSearch-Button .DocSearch-Button-Keys{display:flex;align-items:center}}.DocSearch-Button .DocSearch-Button-Key{display:block;margin:2px 0 0;border:1px solid var(--vp-c-divider);border-right:none;border-radius:4px 0 0 4px;padding-left:6px;min-width:0;width:auto;height:22px;line-height:22px;font-family:var(--vp-font-family-base);font-size:12px;font-weight:500;transition:color .5s,border-color .5s}.DocSearch-Button .DocSearch-Button-Key+.DocSearch-Button-Key{border-right:1px solid var(--vp-c-divider);border-left:none;border-radius:0 4px 4px 0;padding-left:2px;padding-right:6px}.DocSearch-Button .DocSearch-Button-Key:first-child{font-size:0!important}.DocSearch-Button .DocSearch-Button-Key:first-child:after{content:"Ctrl";font-size:12px;letter-spacing:normal;color:var(--docsearch-muted-color)}.mac .DocSearch-Button .DocSearch-Button-Key:first-child:after{content:"⌘"}.DocSearch-Button .DocSearch-Button-Key:first-child>*{display:none}.DocSearch-Search-Icon{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' stroke-width='1.6' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='m14.386 14.386 4.088 4.088-4.088-4.088A7.533 7.533 0 1 1 3.733 3.733a7.533 7.533 0 0 1 10.653 10.653z'/%3E%3C/svg%3E")}.VPNavBarSearch{display:flex;align-items:center}@media (min-width: 768px){.VPNavBarSearch{flex-grow:1;padding-left:24px}}@media (min-width: 960px){.VPNavBarSearch{padding-left:32px}}.dark .DocSearch-Footer{border-top:1px solid var(--vp-c-divider)}.DocSearch-Form{border:1px solid var(--vp-c-brand-1);background-color:var(--vp-c-white)}.dark .DocSearch-Form{background-color:var(--vp-c-default-soft)}.DocSearch-Screen-Icon>svg{margin:auto}.VPNavBarSocialLinks[data-v-164c457f]{display:none}@media (min-width: 1280px){.VPNavBarSocialLinks[data-v-164c457f]{display:flex;align-items:center}}.title[data-v-0f4f798b]{display:flex;align-items:center;border-bottom:1px solid transparent;width:100%;height:var(--vp-nav-height);font-size:16px;font-weight:600;color:var(--vp-c-text-1);transition:opacity .25s}@media (min-width: 960px){.title[data-v-0f4f798b]{flex-shrink:0}.VPNavBarTitle.has-sidebar .title[data-v-0f4f798b]{border-bottom-color:var(--vp-c-divider)}}[data-v-0f4f798b] .logo{margin-right:8px;height:var(--vp-nav-logo-height)}.VPNavBarTranslations[data-v-c80d9ad0]{display:none}@media (min-width: 1280px){.VPNavBarTranslations[data-v-c80d9ad0]{display:flex;align-items:center}}.title[data-v-c80d9ad0]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.VPNavBar[data-v-822684d1]{position:relative;height:var(--vp-nav-height);pointer-events:none;white-space:nowrap;transition:background-color .25s}.VPNavBar.screen-open[data-v-822684d1]{transition:none;background-color:var(--vp-nav-bg-color);border-bottom:1px solid var(--vp-c-divider)}.VPNavBar[data-v-822684d1]:not(.home){background-color:var(--vp-nav-bg-color)}@media (min-width: 960px){.VPNavBar[data-v-822684d1]:not(.home){background-color:transparent}.VPNavBar[data-v-822684d1]:not(.has-sidebar):not(.home.top){background-color:var(--vp-nav-bg-color)}}.wrapper[data-v-822684d1]{padding:0 8px 0 24px}@media (min-width: 768px){.wrapper[data-v-822684d1]{padding:0 32px}}@media (min-width: 960px){.VPNavBar.has-sidebar .wrapper[data-v-822684d1]{padding:0}}.container[data-v-822684d1]{display:flex;justify-content:space-between;margin:0 auto;max-width:calc(var(--vp-layout-max-width) - 64px);height:var(--vp-nav-height);pointer-events:none}.container>.title[data-v-822684d1],.container>.content[data-v-822684d1]{pointer-events:none}.container[data-v-822684d1] *{pointer-events:auto}@media (min-width: 960px){.VPNavBar.has-sidebar .container[data-v-822684d1]{max-width:100%}}.title[data-v-822684d1]{flex-shrink:0;height:calc(var(--vp-nav-height) - 1px);transition:background-color .5s}@media (min-width: 960px){.VPNavBar.has-sidebar .title[data-v-822684d1]{position:absolute;top:0;left:0;z-index:2;padding:0 32px;width:var(--vp-sidebar-width);height:var(--vp-nav-height);background-color:transparent}}@media (min-width: 1440px){.VPNavBar.has-sidebar .title[data-v-822684d1]{padding-left:max(32px,calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));width:calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px)}}.content[data-v-822684d1]{flex-grow:1}@media (min-width: 960px){.VPNavBar.has-sidebar .content[data-v-822684d1]{position:relative;z-index:1;padding-right:32px;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .content[data-v-822684d1]{padding-right:calc((100vw - var(--vp-layout-max-width)) / 2 + 32px);padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.content-body[data-v-822684d1]{display:flex;justify-content:flex-end;align-items:center;height:var(--vp-nav-height);transition:background-color .5s}@media (min-width: 960px){.VPNavBar:not(.home.top) .content-body[data-v-822684d1]{position:relative;background-color:var(--vp-nav-bg-color)}.VPNavBar:not(.has-sidebar):not(.home.top) .content-body[data-v-822684d1]{background-color:transparent}}@media (max-width: 767px){.content-body[data-v-822684d1]{column-gap:.5rem}}.menu+.translations[data-v-822684d1]:before,.menu+.appearance[data-v-822684d1]:before,.menu+.social-links[data-v-822684d1]:before,.translations+.appearance[data-v-822684d1]:before,.appearance+.social-links[data-v-822684d1]:before{margin-right:8px;margin-left:8px;width:1px;height:24px;background-color:var(--vp-c-divider);content:""}.menu+.appearance[data-v-822684d1]:before,.translations+.appearance[data-v-822684d1]:before{margin-right:16px}.appearance+.social-links[data-v-822684d1]:before{margin-left:16px}.social-links[data-v-822684d1]{margin-right:-8px}.divider[data-v-822684d1]{width:100%;height:1px}@media (min-width: 960px){.VPNavBar.has-sidebar .divider[data-v-822684d1]{padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .divider[data-v-822684d1]{padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.divider-line[data-v-822684d1]{width:100%;height:1px;transition:background-color .5s}.VPNavBar:not(.home) .divider-line[data-v-822684d1]{background-color:var(--vp-c-gutter)}@media (min-width: 960px){.VPNavBar:not(.home.top) .divider-line[data-v-822684d1]{background-color:var(--vp-c-gutter)}.VPNavBar:not(.has-sidebar):not(.home.top) .divider[data-v-822684d1]{background-color:var(--vp-c-gutter)}}.VPNavScreenAppearance[data-v-ffb44008]{display:flex;justify-content:space-between;align-items:center;border-radius:8px;padding:12px 14px 12px 16px;background-color:var(--vp-c-bg-soft)}.text[data-v-ffb44008]{line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.VPNavScreenMenuLink[data-v-735512b8]{display:block;border-bottom:1px solid var(--vp-c-divider);padding:12px 0 11px;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:border-color .25s,color .25s}.VPNavScreenMenuLink[data-v-735512b8]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupLink[data-v-372ae7c0]{display:block;margin-left:12px;line-height:32px;font-size:14px;font-weight:400;color:var(--vp-c-text-1);transition:color .25s}.VPNavScreenMenuGroupLink[data-v-372ae7c0]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupSection[data-v-4b8941ac]{display:block}.title[data-v-4b8941ac]{line-height:32px;font-size:13px;font-weight:700;color:var(--vp-c-text-2);transition:color .25s}.VPNavScreenMenuGroup[data-v-875057a5]{border-bottom:1px solid var(--vp-c-divider);height:48px;overflow:hidden;transition:border-color .5s}.VPNavScreenMenuGroup .items[data-v-875057a5]{visibility:hidden}.VPNavScreenMenuGroup.open .items[data-v-875057a5]{visibility:visible}.VPNavScreenMenuGroup.open[data-v-875057a5]{padding-bottom:10px;height:auto}.VPNavScreenMenuGroup.open .button[data-v-875057a5]{padding-bottom:6px;color:var(--vp-c-brand-1)}.VPNavScreenMenuGroup.open .button-icon[data-v-875057a5]{transform:rotate(45deg)}.button[data-v-875057a5]{display:flex;justify-content:space-between;align-items:center;padding:12px 4px 11px 0;width:100%;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.button[data-v-875057a5]:hover{color:var(--vp-c-brand-1)}.button-icon[data-v-875057a5]{transition:transform .25s}.group[data-v-875057a5]:first-child{padding-top:0}.group+.group[data-v-875057a5],.group+.item[data-v-875057a5]{padding-top:4px}.VPNavScreenTranslations[data-v-362991c2]{height:24px;overflow:hidden}.VPNavScreenTranslations.open[data-v-362991c2]{height:auto}.title[data-v-362991c2]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-text-1)}.icon[data-v-362991c2]{font-size:16px}.icon.lang[data-v-362991c2]{margin-right:8px}.icon.chevron[data-v-362991c2]{margin-left:4px}.list[data-v-362991c2]{padding:4px 0 0 24px}.link[data-v-362991c2]{line-height:32px;font-size:13px;color:var(--vp-c-text-1)}.VPNavScreen[data-v-833aabba]{position:fixed;top:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px));right:0;bottom:0;left:0;padding:0 32px;width:100%;background-color:var(--vp-nav-screen-bg-color);overflow-y:auto;transition:background-color .25s;pointer-events:auto}.VPNavScreen.fade-enter-active[data-v-833aabba],.VPNavScreen.fade-leave-active[data-v-833aabba]{transition:opacity .25s}.VPNavScreen.fade-enter-active .container[data-v-833aabba],.VPNavScreen.fade-leave-active .container[data-v-833aabba]{transition:transform .25s ease}.VPNavScreen.fade-enter-from[data-v-833aabba],.VPNavScreen.fade-leave-to[data-v-833aabba]{opacity:0}.VPNavScreen.fade-enter-from .container[data-v-833aabba],.VPNavScreen.fade-leave-to .container[data-v-833aabba]{transform:translateY(-8px)}@media (min-width: 768px){.VPNavScreen[data-v-833aabba]{display:none}}.container[data-v-833aabba]{margin:0 auto;padding:24px 0 96px;max-width:288px}.menu+.translations[data-v-833aabba],.menu+.appearance[data-v-833aabba],.translations+.appearance[data-v-833aabba]{margin-top:24px}.menu+.social-links[data-v-833aabba]{margin-top:16px}.appearance+.social-links[data-v-833aabba]{margin-top:16px}.VPNav[data-v-f1e365da]{position:relative;top:var(--vp-layout-top-height, 0px);left:0;z-index:var(--vp-z-index-nav);width:100%;pointer-events:none;transition:background-color .5s}@media (min-width: 960px){.VPNav[data-v-f1e365da]{position:fixed}}.VPSidebarItem.level-0[data-v-196b2e5f]{padding-bottom:24px}.VPSidebarItem.collapsed.level-0[data-v-196b2e5f]{padding-bottom:10px}.item[data-v-196b2e5f]{position:relative;display:flex;width:100%}.VPSidebarItem.collapsible>.item[data-v-196b2e5f]{cursor:pointer}.indicator[data-v-196b2e5f]{position:absolute;top:6px;bottom:6px;left:-17px;width:2px;border-radius:2px;transition:background-color .25s}.VPSidebarItem.level-2.is-active>.item>.indicator[data-v-196b2e5f],.VPSidebarItem.level-3.is-active>.item>.indicator[data-v-196b2e5f],.VPSidebarItem.level-4.is-active>.item>.indicator[data-v-196b2e5f],.VPSidebarItem.level-5.is-active>.item>.indicator[data-v-196b2e5f]{background-color:var(--vp-c-brand-1)}.link[data-v-196b2e5f]{display:flex;align-items:center;flex-grow:1}.text[data-v-196b2e5f]{flex-grow:1;padding:4px 0;line-height:24px;font-size:14px;transition:color .25s}.VPSidebarItem.level-0 .text[data-v-196b2e5f]{font-weight:700;color:var(--vp-c-text-1)}.VPSidebarItem.level-1 .text[data-v-196b2e5f],.VPSidebarItem.level-2 .text[data-v-196b2e5f],.VPSidebarItem.level-3 .text[data-v-196b2e5f],.VPSidebarItem.level-4 .text[data-v-196b2e5f],.VPSidebarItem.level-5 .text[data-v-196b2e5f]{font-weight:500;color:var(--vp-c-text-2)}.VPSidebarItem.level-0.is-link>.item>.link:hover .text[data-v-196b2e5f],.VPSidebarItem.level-1.is-link>.item>.link:hover .text[data-v-196b2e5f],.VPSidebarItem.level-2.is-link>.item>.link:hover .text[data-v-196b2e5f],.VPSidebarItem.level-3.is-link>.item>.link:hover .text[data-v-196b2e5f],.VPSidebarItem.level-4.is-link>.item>.link:hover .text[data-v-196b2e5f],.VPSidebarItem.level-5.is-link>.item>.link:hover .text[data-v-196b2e5f]{color:var(--vp-c-brand-1)}.VPSidebarItem.level-0.has-active>.item>.text[data-v-196b2e5f],.VPSidebarItem.level-1.has-active>.item>.text[data-v-196b2e5f],.VPSidebarItem.level-2.has-active>.item>.text[data-v-196b2e5f],.VPSidebarItem.level-3.has-active>.item>.text[data-v-196b2e5f],.VPSidebarItem.level-4.has-active>.item>.text[data-v-196b2e5f],.VPSidebarItem.level-5.has-active>.item>.text[data-v-196b2e5f],.VPSidebarItem.level-0.has-active>.item>.link>.text[data-v-196b2e5f],.VPSidebarItem.level-1.has-active>.item>.link>.text[data-v-196b2e5f],.VPSidebarItem.level-2.has-active>.item>.link>.text[data-v-196b2e5f],.VPSidebarItem.level-3.has-active>.item>.link>.text[data-v-196b2e5f],.VPSidebarItem.level-4.has-active>.item>.link>.text[data-v-196b2e5f],.VPSidebarItem.level-5.has-active>.item>.link>.text[data-v-196b2e5f]{color:var(--vp-c-text-1)}.VPSidebarItem.level-0.is-active>.item .link>.text[data-v-196b2e5f],.VPSidebarItem.level-1.is-active>.item .link>.text[data-v-196b2e5f],.VPSidebarItem.level-2.is-active>.item .link>.text[data-v-196b2e5f],.VPSidebarItem.level-3.is-active>.item .link>.text[data-v-196b2e5f],.VPSidebarItem.level-4.is-active>.item .link>.text[data-v-196b2e5f],.VPSidebarItem.level-5.is-active>.item .link>.text[data-v-196b2e5f]{color:var(--vp-c-brand-1)}.caret[data-v-196b2e5f]{display:flex;justify-content:center;align-items:center;margin-right:-7px;width:32px;height:32px;color:var(--vp-c-text-3);cursor:pointer;transition:color .25s;flex-shrink:0}.item:hover .caret[data-v-196b2e5f]{color:var(--vp-c-text-2)}.item:hover .caret[data-v-196b2e5f]:hover{color:var(--vp-c-text-1)}.caret-icon[data-v-196b2e5f]{font-size:18px;transform:rotate(90deg);transition:transform .25s}.VPSidebarItem.collapsed .caret-icon[data-v-196b2e5f]{transform:rotate(0)}.VPSidebarItem.level-1 .items[data-v-196b2e5f],.VPSidebarItem.level-2 .items[data-v-196b2e5f],.VPSidebarItem.level-3 .items[data-v-196b2e5f],.VPSidebarItem.level-4 .items[data-v-196b2e5f],.VPSidebarItem.level-5 .items[data-v-196b2e5f]{border-left:1px solid var(--vp-c-divider);padding-left:16px}.VPSidebarItem.collapsed .items[data-v-196b2e5f]{display:none}.no-transition[data-v-9e426adc] .caret-icon{transition:none}.group+.group[data-v-9e426adc]{border-top:1px solid var(--vp-c-divider);padding-top:10px}@media (min-width: 960px){.group[data-v-9e426adc]{padding-top:10px;width:calc(var(--vp-sidebar-width) - 64px)}}.VPSidebar[data-v-18756405]{position:fixed;top:var(--vp-layout-top-height, 0px);bottom:0;left:0;z-index:var(--vp-z-index-sidebar);padding:32px 32px 96px;width:calc(100vw - 64px);max-width:320px;background-color:var(--vp-sidebar-bg-color);opacity:0;box-shadow:var(--vp-c-shadow-3);overflow-x:hidden;overflow-y:auto;transform:translate(-100%);transition:opacity .5s,transform .25s ease;overscroll-behavior:contain}.VPSidebar.open[data-v-18756405]{opacity:1;visibility:visible;transform:translate(0);transition:opacity .25s,transform .5s cubic-bezier(.19,1,.22,1)}.dark .VPSidebar[data-v-18756405]{box-shadow:var(--vp-shadow-1)}@media (min-width: 960px){.VPSidebar[data-v-18756405]{padding-top:var(--vp-nav-height);width:var(--vp-sidebar-width);max-width:100%;background-color:var(--vp-sidebar-bg-color);opacity:1;visibility:visible;box-shadow:none;transform:translate(0)}}@media (min-width: 1440px){.VPSidebar[data-v-18756405]{padding-left:max(32px,calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));width:calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px)}}@media (min-width: 960px){.curtain[data-v-18756405]{position:sticky;top:-64px;left:0;z-index:1;margin-top:calc(var(--vp-nav-height) * -1);margin-right:-32px;margin-left:-32px;height:var(--vp-nav-height);background-color:var(--vp-sidebar-bg-color)}}.nav[data-v-18756405]{outline:0}.VPSkipLink[data-v-c3508ec8]{top:8px;left:8px;padding:8px 16px;z-index:999;border-radius:8px;font-size:12px;font-weight:700;text-decoration:none;color:var(--vp-c-brand-1);box-shadow:var(--vp-shadow-3);background-color:var(--vp-c-bg)}.VPSkipLink[data-v-c3508ec8]:focus{height:auto;width:auto;clip:auto;clip-path:none}@media (min-width: 1280px){.VPSkipLink[data-v-c3508ec8]{top:14px;left:16px}}.Layout[data-v-a9a9e638]{display:flex;flex-direction:column;min-height:100vh}.VPHomeSponsors[data-v-db81191c]{border-top:1px solid var(--vp-c-gutter);padding-top:88px!important}.VPHomeSponsors[data-v-db81191c]{margin:96px 0}@media (min-width: 768px){.VPHomeSponsors[data-v-db81191c]{margin:128px 0}}.VPHomeSponsors[data-v-db81191c]{padding:0 24px}@media (min-width: 768px){.VPHomeSponsors[data-v-db81191c]{padding:0 48px}}@media (min-width: 960px){.VPHomeSponsors[data-v-db81191c]{padding:0 64px}}.container[data-v-db81191c]{margin:0 auto;max-width:1152px}.love[data-v-db81191c]{margin:0 auto;width:fit-content;font-size:28px;color:var(--vp-c-text-3)}.icon[data-v-db81191c]{display:inline-block}.message[data-v-db81191c]{margin:0 auto;padding-top:10px;max-width:320px;text-align:center;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}.sponsors[data-v-db81191c]{padding-top:32px}.action[data-v-db81191c]{padding-top:40px;text-align:center}.VPTeamPage[data-v-c2f8e101]{margin:96px 0}@media (min-width: 768px){.VPTeamPage[data-v-c2f8e101]{margin:128px 0}}.VPHome .VPTeamPageTitle[data-v-c2f8e101-s]{border-top:1px solid var(--vp-c-gutter);padding-top:88px!important}.VPTeamPageSection+.VPTeamPageSection[data-v-c2f8e101-s],.VPTeamMembers+.VPTeamPageSection[data-v-c2f8e101-s]{margin-top:64px}.VPTeamMembers+.VPTeamMembers[data-v-c2f8e101-s]{margin-top:24px}@media (min-width: 768px){.VPTeamPageTitle+.VPTeamPageSection[data-v-c2f8e101-s]{margin-top:16px}.VPTeamPageSection+.VPTeamPageSection[data-v-c2f8e101-s],.VPTeamMembers+.VPTeamPageSection[data-v-c2f8e101-s]{margin-top:96px}}.VPTeamMembers[data-v-c2f8e101-s]{padding:0 24px}@media (min-width: 768px){.VPTeamMembers[data-v-c2f8e101-s]{padding:0 48px}}@media (min-width: 960px){.VPTeamMembers[data-v-c2f8e101-s]{padding:0 64px}}.VPTeamPageTitle[data-v-e277e15c]{padding:48px 32px;text-align:center}@media (min-width: 768px){.VPTeamPageTitle[data-v-e277e15c]{padding:64px 48px 48px}}@media (min-width: 960px){.VPTeamPageTitle[data-v-e277e15c]{padding:80px 64px 48px}}.title[data-v-e277e15c]{letter-spacing:0;line-height:44px;font-size:36px;font-weight:500}@media (min-width: 768px){.title[data-v-e277e15c]{letter-spacing:-.5px;line-height:56px;font-size:48px}}.lead[data-v-e277e15c]{margin:0 auto;max-width:512px;padding-top:12px;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 768px){.lead[data-v-e277e15c]{max-width:592px;letter-spacing:.15px;line-height:28px;font-size:20px}}.VPTeamPageSection[data-v-d43bc49d]{padding:0 32px}@media (min-width: 768px){.VPTeamPageSection[data-v-d43bc49d]{padding:0 48px}}@media (min-width: 960px){.VPTeamPageSection[data-v-d43bc49d]{padding:0 64px}}.title[data-v-d43bc49d]{position:relative;margin:0 auto;max-width:1152px;text-align:center;color:var(--vp-c-text-2)}.title-line[data-v-d43bc49d]{position:absolute;top:16px;left:0;width:100%;height:1px;background-color:var(--vp-c-divider)}.title-text[data-v-d43bc49d]{position:relative;display:inline-block;padding:0 24px;letter-spacing:0;line-height:32px;font-size:20px;font-weight:500;background-color:var(--vp-c-bg)}.lead[data-v-d43bc49d]{margin:0 auto;max-width:480px;padding-top:12px;text-align:center;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}.members[data-v-d43bc49d]{padding-top:40px}.VPTeamMembersItem[data-v-f9987cb6]{display:flex;flex-direction:column;gap:2px;border-radius:12px;width:100%;height:100%;overflow:hidden}.VPTeamMembersItem.small .profile[data-v-f9987cb6]{padding:32px}.VPTeamMembersItem.small .data[data-v-f9987cb6]{padding-top:20px}.VPTeamMembersItem.small .avatar[data-v-f9987cb6]{width:64px;height:64px}.VPTeamMembersItem.small .name[data-v-f9987cb6]{line-height:24px;font-size:16px}.VPTeamMembersItem.small .affiliation[data-v-f9987cb6]{padding-top:4px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .desc[data-v-f9987cb6]{padding-top:12px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .links[data-v-f9987cb6]{margin:0 -16px -20px;padding:10px 0 0}.VPTeamMembersItem.medium .profile[data-v-f9987cb6]{padding:48px 32px}.VPTeamMembersItem.medium .data[data-v-f9987cb6]{padding-top:24px;text-align:center}.VPTeamMembersItem.medium .avatar[data-v-f9987cb6]{width:96px;height:96px}.VPTeamMembersItem.medium .name[data-v-f9987cb6]{letter-spacing:.15px;line-height:28px;font-size:20px}.VPTeamMembersItem.medium .affiliation[data-v-f9987cb6]{padding-top:4px;font-size:16px}.VPTeamMembersItem.medium .desc[data-v-f9987cb6]{padding-top:16px;max-width:288px;font-size:16px}.VPTeamMembersItem.medium .links[data-v-f9987cb6]{margin:0 -16px -12px;padding:16px 12px 0}.profile[data-v-f9987cb6]{flex-grow:1;background-color:var(--vp-c-bg-soft)}.data[data-v-f9987cb6]{text-align:center}.avatar[data-v-f9987cb6]{position:relative;flex-shrink:0;margin:0 auto;border-radius:50%;box-shadow:var(--vp-shadow-3)}.avatar-img[data-v-f9987cb6]{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:50%;object-fit:cover}.name[data-v-f9987cb6]{margin:0;font-weight:600}.affiliation[data-v-f9987cb6]{margin:0;font-weight:500;color:var(--vp-c-text-2)}.org.link[data-v-f9987cb6]{color:var(--vp-c-text-2);transition:color .25s}.org.link[data-v-f9987cb6]:hover{color:var(--vp-c-brand-1)}.desc[data-v-f9987cb6]{margin:0 auto}.desc[data-v-f9987cb6] a{font-weight:500;color:var(--vp-c-brand-1);text-decoration-style:dotted;transition:color .25s}.links[data-v-f9987cb6]{display:flex;justify-content:center;height:56px}.sp-link[data-v-f9987cb6]{display:flex;justify-content:center;align-items:center;text-align:center;padding:16px;font-size:14px;font-weight:500;color:var(--vp-c-sponsor);background-color:var(--vp-c-bg-soft);transition:color .25s,background-color .25s}.sp .sp-link.link[data-v-f9987cb6]:hover,.sp .sp-link.link[data-v-f9987cb6]:focus{outline:none;color:var(--vp-c-white);background-color:var(--vp-c-sponsor)}.sp-icon[data-v-f9987cb6]{margin-right:8px;font-size:16px}.VPTeamMembers.small .container[data-v-fba19bad]{grid-template-columns:repeat(auto-fit,minmax(224px,1fr))}.VPTeamMembers.small.count-1 .container[data-v-fba19bad]{max-width:276px}.VPTeamMembers.small.count-2 .container[data-v-fba19bad]{max-width:576px}.VPTeamMembers.small.count-3 .container[data-v-fba19bad]{max-width:876px}.VPTeamMembers.medium .container[data-v-fba19bad]{grid-template-columns:repeat(auto-fit,minmax(256px,1fr))}@media (min-width: 375px){.VPTeamMembers.medium .container[data-v-fba19bad]{grid-template-columns:repeat(auto-fit,minmax(288px,1fr))}}.VPTeamMembers.medium.count-1 .container[data-v-fba19bad]{max-width:368px}.VPTeamMembers.medium.count-2 .container[data-v-fba19bad]{max-width:760px}.container[data-v-fba19bad]{display:grid;gap:24px;margin:0 auto;max-width:1152px}:root{--vp-plugin-tabs-tab-text-color: var(--vp-c-text-2);--vp-plugin-tabs-tab-active-text-color: var(--vp-c-text-1);--vp-plugin-tabs-tab-hover-text-color: var(--vp-c-text-1);--vp-plugin-tabs-tab-bg: var(--vp-c-bg-soft);--vp-plugin-tabs-tab-divider: var(--vp-c-divider);--vp-plugin-tabs-tab-active-bar-color: var(--vp-c-brand-1)}.plugin-tabs{margin:16px 0;background-color:var(--vp-plugin-tabs-tab-bg);border-radius:8px}.plugin-tabs--tab-list{position:relative;padding:0 12px;overflow-x:auto;overflow-y:hidden}.plugin-tabs--tab-list:after{content:"";position:absolute;bottom:0;left:0;right:0;height:2px;background-color:var(--vp-plugin-tabs-tab-divider)}.plugin-tabs--tab{position:relative;padding:0 12px;line-height:48px;border-bottom:2px solid transparent;color:var(--vp-plugin-tabs-tab-text-color);font-size:14px;font-weight:500;white-space:nowrap;transition:color .25s}.plugin-tabs--tab[aria-selected=true]{color:var(--vp-plugin-tabs-tab-active-text-color)}.plugin-tabs--tab:hover{color:var(--vp-plugin-tabs-tab-hover-text-color)}.plugin-tabs--tab:after{content:"";position:absolute;bottom:-2px;left:8px;right:8px;height:2px;background-color:transparent;transition:background-color .25s;z-index:1}.plugin-tabs--tab[aria-selected=true]:after{background-color:var(--vp-plugin-tabs-tab-active-bar-color)}.plugin-tabs--content[data-v-9b0d03d2]{padding:16px}.plugin-tabs--content[data-v-9b0d03d2]>:first-child:first-child{margin-top:0}.plugin-tabs--content[data-v-9b0d03d2]>:last-child:last-child{margin-bottom:0}.plugin-tabs--content[data-v-9b0d03d2]>div[class*=language-]{border-radius:8px;margin:16px 0}:root:not(.dark) .plugin-tabs--content[data-v-9b0d03d2] div[class*=language-]{background-color:var(--vp-c-bg)}.VPHero .clip{white-space:pre;max-width:500px}:root{--vp-font-family-base: "Barlow", "Inter var experimental", "Inter var", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;--vp-font-family-mono: "Space Mono", Menlo, Monaco, Consolas, "Courier New", monospace}:root{--julia-blue: #4063D8;--julia-purple: #9558B2;--julia-red: #CB3C33;--julia-green: #389826;--vp-c-brand: #389826;--vp-c-brand-light: #3dd027;--vp-c-brand-lighter: #9499ff;--vp-c-brand-lightest: #bcc0ff;--vp-c-brand-dark: #535bf2;--vp-c-brand-darker: #454ce1;--vp-c-brand-dimm: #212425}:root{--vp-button-brand-border: var(--vp-c-brand-light);--vp-button-brand-text: var(--vp-c-white);--vp-button-brand-bg: var(--vp-c-brand);--vp-button-brand-hover-border: var(--vp-c-brand-light);--vp-button-brand-hover-text: var(--vp-c-white);--vp-button-brand-hover-bg: var(--vp-c-brand-light);--vp-button-brand-active-border: var(--vp-c-brand-light);--vp-button-brand-active-text: var(--vp-c-white);--vp-button-brand-active-bg: var(--vp-button-brand-bg)}:root{--vp-home-hero-name-color: transparent;--vp-home-hero-name-background: -webkit-linear-gradient( 120deg, #459c55 30%, #dccc50 );--vp-home-hero-image-background-image: linear-gradient( -45deg, #9558B2 30%, #389826 30%, #CB3C33 );--vp-home-hero-image-filter: blur(40px)}@media (min-width: 640px){:root{--vp-home-hero-image-filter: blur(56px)}}@media (min-width: 960px){:root{--vp-home-hero-image-filter: blur(72px)}}:root.dark{--vp-custom-block-tip-border: var(--vp-c-brand);--vp-custom-block-tip-text: var(--vp-c-brand-lightest);--vp-custom-block-tip-bg: var(--vp-c-brand-dimm);--vp-c-black: hsl(220 20% 9%);--vp-c-black-pure: hsl(220, 24%, 4%);--vp-c-black-soft: hsl(220 16% 13%);--vp-c-black-mute: hsl(220 14% 17%);--vp-c-gray: hsl(220 8% 56%);--vp-c-gray-dark-1: hsl(220 10% 39%);--vp-c-gray-dark-2: hsl(220 12% 28%);--vp-c-gray-dark-3: hsl(220 12% 23%);--vp-c-gray-dark-4: hsl(220 14% 17%);--vp-c-gray-dark-5: hsl(220 16% 13%);--vp-custom-block-info-bg: hsl(220 14% 17%)}.DocSearch{--docsearch-primary-color: var(--vp-c-brand) !important}mjx-container>svg{display:block;margin:auto}mjx-container{padding:.5rem 0}mjx-container{display:inline-block;margin:auto 2px -2px}mjx-container>svg{margin:auto;display:inline-block}:root{--vp-c-brand-1: #CB3C33;--vp-c-brand-2: #CB3C33;--vp-c-brand-3: #CB3C33;--vp-c-sponsor: #ca2971;--vitest-c-sponsor-hover: #c13071}.dark{--vp-c-brand-1: #91dd33;--vp-c-brand-2: #91dd33;--vp-c-brand-3: #91dd33;--vp-c-sponsor: #91dd33;--vitest-c-sponsor-hover: #e51370}.VPLocalSearchBox[data-v-42e65fb9]{position:fixed;z-index:100;top:0;right:0;bottom:0;left:0;display:flex}.backdrop[data-v-42e65fb9]{position:absolute;top:0;right:0;bottom:0;left:0;background:var(--vp-backdrop-bg-color);transition:opacity .5s}.shell[data-v-42e65fb9]{position:relative;padding:12px;margin:64px auto;display:flex;flex-direction:column;gap:16px;background:var(--vp-local-search-bg);width:min(100vw - 60px,900px);height:min-content;max-height:min(100vh - 128px,900px);border-radius:6px}@media (max-width: 767px){.shell[data-v-42e65fb9]{margin:0;width:100vw;height:100vh;max-height:none;border-radius:0}}.search-bar[data-v-42e65fb9]{border:1px solid var(--vp-c-divider);border-radius:4px;display:flex;align-items:center;padding:0 12px;cursor:text}@media (max-width: 767px){.search-bar[data-v-42e65fb9]{padding:0 8px}}.search-bar[data-v-42e65fb9]:focus-within{border-color:var(--vp-c-brand-1)}.local-search-icon[data-v-42e65fb9]{display:block;font-size:18px}.navigate-icon[data-v-42e65fb9]{display:block;font-size:14px}.search-icon[data-v-42e65fb9]{margin:8px}@media (max-width: 767px){.search-icon[data-v-42e65fb9]{display:none}}.search-input[data-v-42e65fb9]{padding:6px 12px;font-size:inherit;width:100%}@media (max-width: 767px){.search-input[data-v-42e65fb9]{padding:6px 4px}}.search-actions[data-v-42e65fb9]{display:flex;gap:4px}@media (any-pointer: coarse){.search-actions[data-v-42e65fb9]{gap:8px}}@media (min-width: 769px){.search-actions.before[data-v-42e65fb9]{display:none}}.search-actions button[data-v-42e65fb9]{padding:8px}.search-actions button[data-v-42e65fb9]:not([disabled]):hover,.toggle-layout-button.detailed-list[data-v-42e65fb9]{color:var(--vp-c-brand-1)}.search-actions button.clear-button[data-v-42e65fb9]:disabled{opacity:.37}.search-keyboard-shortcuts[data-v-42e65fb9]{font-size:.8rem;opacity:75%;display:flex;flex-wrap:wrap;gap:16px;line-height:14px}.search-keyboard-shortcuts span[data-v-42e65fb9]{display:flex;align-items:center;gap:4px}@media (max-width: 767px){.search-keyboard-shortcuts[data-v-42e65fb9]{display:none}}.search-keyboard-shortcuts kbd[data-v-42e65fb9]{background:#8080801a;border-radius:4px;padding:3px 6px;min-width:24px;display:inline-block;text-align:center;vertical-align:middle;border:1px solid rgba(128,128,128,.15);box-shadow:0 2px 2px #0000001a}.results[data-v-42e65fb9]{display:flex;flex-direction:column;gap:6px;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain}.result[data-v-42e65fb9]{display:flex;align-items:center;gap:8px;border-radius:4px;transition:none;line-height:1rem;border:solid 2px var(--vp-local-search-result-border);outline:none}.result>div[data-v-42e65fb9]{margin:12px;width:100%;overflow:hidden}@media (max-width: 767px){.result>div[data-v-42e65fb9]{margin:8px}}.titles[data-v-42e65fb9]{display:flex;flex-wrap:wrap;gap:4px;position:relative;z-index:1001;padding:2px 0}.title[data-v-42e65fb9]{display:flex;align-items:center;gap:4px}.title.main[data-v-42e65fb9]{font-weight:500}.title-icon[data-v-42e65fb9]{opacity:.5;font-weight:500;color:var(--vp-c-brand-1)}.title svg[data-v-42e65fb9]{opacity:.5}.result.selected[data-v-42e65fb9]{--vp-local-search-result-bg: var(--vp-local-search-result-selected-bg);border-color:var(--vp-local-search-result-selected-border)}.excerpt-wrapper[data-v-42e65fb9]{position:relative}.excerpt[data-v-42e65fb9]{opacity:50%;pointer-events:none;max-height:140px;overflow:hidden;position:relative;margin-top:4px}.result.selected .excerpt[data-v-42e65fb9]{opacity:1}.excerpt[data-v-42e65fb9] *{font-size:.8rem!important;line-height:130%!important}.titles[data-v-42e65fb9] mark,.excerpt[data-v-42e65fb9] mark{background-color:var(--vp-local-search-highlight-bg);color:var(--vp-local-search-highlight-text);border-radius:2px;padding:0 2px}.excerpt[data-v-42e65fb9] .vp-code-group .tabs{display:none}.excerpt[data-v-42e65fb9] .vp-code-group div[class*=language-]{border-radius:8px!important}.excerpt-gradient-bottom[data-v-42e65fb9]{position:absolute;bottom:-1px;left:0;width:100%;height:8px;background:linear-gradient(transparent,var(--vp-local-search-result-bg));z-index:1000}.excerpt-gradient-top[data-v-42e65fb9]{position:absolute;top:-1px;left:0;width:100%;height:8px;background:linear-gradient(var(--vp-local-search-result-bg),transparent);z-index:1000}.result.selected .titles[data-v-42e65fb9],.result.selected .title-icon[data-v-42e65fb9]{color:var(--vp-c-brand-1)!important}.no-results[data-v-42e65fb9]{font-size:.9rem;text-align:center;padding:12px}svg[data-v-42e65fb9]{flex:none} diff --git a/previews/PR239/assets/tfqfmef.Dz86q2IX.png b/previews/PR239/assets/tfqfmef.Dz86q2IX.png new file mode 100644 index 000000000..4009c1abb Binary files /dev/null and b/previews/PR239/assets/tfqfmef.Dz86q2IX.png differ diff --git a/previews/PR239/assets/tlayvwm.DiwGEg2f.png b/previews/PR239/assets/tlayvwm.DiwGEg2f.png new file mode 100644 index 000000000..b9cc2026e Binary files /dev/null and b/previews/PR239/assets/tlayvwm.DiwGEg2f.png differ diff --git a/previews/PR239/assets/tsgqxnn.B94PsR1K.png b/previews/PR239/assets/tsgqxnn.B94PsR1K.png new file mode 100644 index 000000000..65bec08ee Binary files /dev/null and b/previews/PR239/assets/tsgqxnn.B94PsR1K.png differ diff --git a/previews/PR239/assets/tstphwa.DTKLkKh_.png b/previews/PR239/assets/tstphwa.DTKLkKh_.png new file mode 100644 index 000000000..ca49f1a1f Binary files /dev/null and b/previews/PR239/assets/tstphwa.DTKLkKh_.png differ diff --git a/previews/PR239/assets/tutorials_creating_geometry.md.BW0vmesq.js b/previews/PR239/assets/tutorials_creating_geometry.md.BW0vmesq.js new file mode 100644 index 000000000..9097582fd --- /dev/null +++ b/previews/PR239/assets/tutorials_creating_geometry.md.BW0vmesq.js @@ -0,0 +1,89 @@ +import{_ as i,c as a,a5 as e,o as t}from"./chunks/framework.onQNwZ2I.js";const n="/GeometryOps.jl/previews/PR239/assets/tfqfmef.Dz86q2IX.png",l="/GeometryOps.jl/previews/PR239/assets/ivowqmu.Cx40vhB3.png",p="/GeometryOps.jl/previews/PR239/assets/xetmrwv.0OJvb21A.png",h="/GeometryOps.jl/previews/PR239/assets/cthossd.DaovVbE6.png",o="/GeometryOps.jl/previews/PR239/assets/bnkpkoa.rOsRk89v.png",k="/GeometryOps.jl/previews/PR239/assets/krkgpps.4wfjCtJV.png",r="/GeometryOps.jl/previews/PR239/assets/rleiuex.3sfpQl2i.png",g="/GeometryOps.jl/previews/PR239/assets/fyfzmss.Dab1-ETk.png",d="/GeometryOps.jl/previews/PR239/assets/mtwzvwz.D9AE7i2o.png",E="/GeometryOps.jl/previews/PR239/assets/tstphwa.DTKLkKh_.png",c="/GeometryOps.jl/previews/PR239/assets/wjwdigs.0f3Lq4Lw.png",G=JSON.parse('{"title":"Creating Geometry","description":"","frontmatter":{},"headers":[],"relativePath":"tutorials/creating_geometry.md","filePath":"tutorials/creating_geometry.md","lastUpdated":null}'),y={name:"tutorials/creating_geometry.md"};function F(u,s,f,C,m,b){return t(),a("div",null,s[0]||(s[0]=[e(`In this tutorial, we're going to:
Plot geometries on a map using GeoMakie
and coordinate reference system (CRS
)
Create geospatial geometries with embedded coordinate reference system information
Save geospatial geometries to common geospatial file formats
First, we load some required packages.
# Geospatial packages from Julia
+import GeoInterface as GI
+import GeometryOps as GO
+import GeoFormatTypes as GFT
+using GeoJSON # to load some data
+# Packages for coordinate transformation and projection
+import CoordinateTransformations
+import Proj
+# Plotting
+using CairoMakie
+using GeoMakie
Let's start by making a single Point
.
point = GI.Point(0, 0)
GeoInterface.Wrappers.Point{false, false, Tuple{Int64, Int64}, Nothing}((0, 0), nothing)
Now, let's plot our point.
fig, ax, plt = plot(point)
Let's create a set of points, and have a bit more fun with plotting.
x = [-5, 0, 5, 0];
+y = [0, -5, 0, 5];
+points = GI.Point.(zip(x,y));
+plot!(ax, points; marker = '✈', markersize = 30)
+fig
Point
s can be combined into a single MultiPoint
geometry.
x = [-5, -5, 5, 5];
+y = [-5, 5, 5, -5];
+multipoint = GI.MultiPoint(GI.Point.(zip(x, y)));
+plot!(ax, multipoint; marker = '☁', markersize = 30)
+fig
Let's create a LineString
connecting two points.
p1 = GI.Point.(-5, 0);
+p2 = GI.Point.(5, 0);
+line = GI.LineString([p1,p2])
+plot!(ax, line; color = :red)
+fig
Now, let's create a line connecting multiple points (i.e. a LineString
). This time we get a bit more fancy with point creation.
r = 2;
+k = 10;
+ϴ = 0:0.01:2pi;
+x = r .* (k + 1) .* cos.(ϴ) .- r .* cos.((k + 1) .* ϴ);
+y = r .* (k + 1) .* sin.(ϴ) .- r .* sin.((k + 1) .* ϴ);
+lines = GI.LineString(GI.Point.(zip(x,y)));
+plot!(ax, lines; linewidth = 5)
+fig
We can also create a single LinearRing
trait, the building block of a polygon. A LinearRing
is simply a LineString
with the same beginning and endpoint, i.e., an arbitrary closed shape composed of point pairs.
A LinearRing
is composed of a series of points.
ring1 = GI.LinearRing(GI.getpoint(lines));
GeoInterface.Wrappers.LinearRing{false, false, Vector{GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}[GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.0, 0.0), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.010987813253244, 0.0004397316773170068), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.043805248003498, 0.0035114210915891397), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.098016055420953, 0.011814947665167774), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.172899020101585, 0.027886421973952302), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.267456684570245, 0.05416726609360478), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.380427415579764, 0.09297443860091348), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.51030066635026, 0.1464721641710074), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.655335250260467, 0.21664550952386064), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.813580405100698, 0.30527612515520186), nothing) … GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.866418416586406, -0.3376428491230612), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.704405820024185, -0.24279488312757858), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.55494217175954, -0.16692537029320365), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.420040147662014, -0.10832215707812454), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.30151010318639, -0.0650624499034016), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.200938172182195, -0.03503632062070827), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.119667078681967, -0.01597247419241532), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.058779893613323, -0.005465967083412071), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.019086932781654, -0.0010075412835199304), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.001115954499138, -1.4219350464667047e-5), nothing)], nothing, nothing)
Now, let's make the LinearRing
into a Polygon
.
polygon1 = GI.Polygon([ring1]);
GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.LinearRing{false, false, Vector{GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}[GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.0, 0.0), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.010987813253244, 0.0004397316773170068), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.043805248003498, 0.0035114210915891397), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.098016055420953, 0.011814947665167774), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.172899020101585, 0.027886421973952302), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.267456684570245, 0.05416726609360478), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.380427415579764, 0.09297443860091348), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.51030066635026, 0.1464721641710074), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.655335250260467, 0.21664550952386064), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.813580405100698, 0.30527612515520186), nothing) … GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.866418416586406, -0.3376428491230612), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.704405820024185, -0.24279488312757858), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.55494217175954, -0.16692537029320365), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.420040147662014, -0.10832215707812454), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.30151010318639, -0.0650624499034016), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.200938172182195, -0.03503632062070827), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.119667078681967, -0.01597247419241532), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.058779893613323, -0.005465967083412071), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.019086932781654, -0.0010075412835199304), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.001115954499138, -1.4219350464667047e-5), nothing)], nothing, nothing)], nothing, nothing)
Now, we can use GeometryOps and CoordinateTransformations to shift polygon1
up, to avoid plotting over our earlier results. This is done through the GeometryOps.transform function.
xoffset = 0.;
+yoffset = 50.;
+f = CoordinateTransformations.Translation(xoffset, yoffset);
+polygon1 = GO.transform(f, polygon1);
+plot!(polygon1)
+fig
Polygons can contain "holes". The first LinearRing
in a polygon is the exterior, and all subsequent LinearRing
s are treated as holes in the leading LinearRing
.
GeoInterface
offers the GI.getexterior(poly)
and GI.gethole(poly)
methods to get the exterior ring and an iterable of holes, respectively.
hole = GI.LinearRing(GI.getpoint(multipoint))
+polygon2 = GI.Polygon([ring1, hole])
GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, T, Nothing, Nothing} where T}, Nothing, Nothing}(GeoInterface.Wrappers.LinearRing{false, false, T, Nothing, Nothing} where T[GeoInterface.Wrappers.LinearRing{false, false, Vector{GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}[GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.0, 0.0), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.010987813253244, 0.0004397316773170068), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.043805248003498, 0.0035114210915891397), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.098016055420953, 0.011814947665167774), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.172899020101585, 0.027886421973952302), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.267456684570245, 0.05416726609360478), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.380427415579764, 0.09297443860091348), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.51030066635026, 0.1464721641710074), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.655335250260467, 0.21664550952386064), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.813580405100698, 0.30527612515520186), nothing) … GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.866418416586406, -0.3376428491230612), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.704405820024185, -0.24279488312757858), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.55494217175954, -0.16692537029320365), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.420040147662014, -0.10832215707812454), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.30151010318639, -0.0650624499034016), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.200938172182195, -0.03503632062070827), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.119667078681967, -0.01597247419241532), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.058779893613323, -0.005465967083412071), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.019086932781654, -0.0010075412835199304), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.001115954499138, -1.4219350464667047e-5), nothing)], nothing, nothing), GeoInterface.Wrappers.LinearRing{false, false, Vector{GeoInterface.Wrappers.Point{false, false, Tuple{Int64, Int64}, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.Point{false, false, Tuple{Int64, Int64}, Nothing}[GeoInterface.Wrappers.Point{false, false, Tuple{Int64, Int64}, Nothing}((-5, -5), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Int64, Int64}, Nothing}((-5, 5), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Int64, Int64}, Nothing}((5, 5), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Int64, Int64}, Nothing}((5, -5), nothing)], nothing, nothing)], nothing, nothing)
Shift polygon2
to the right, to avoid plotting over our earlier results.
xoffset = 50.;
+yoffset = 0.;
+f = CoordinateTransformations.Translation(xoffset, yoffset);
+polygon2 = GO.transform(f, polygon2);
+plot!(polygon2)
+fig
Polygon
s can also be grouped together as a MultiPolygon
.
r = 5;
+x = cos.(reverse(ϴ)) .* r .+ xoffset;
+y = sin.(reverse(ϴ)) .* r .+ yoffset;
+ring2 = GI.LinearRing(GI.Point.(zip(x,y)));
+polygon3 = GI.Polygon([ring2]);
+multipolygon = GI.MultiPolygon([polygon2, polygon3])
GeoInterface.Wrappers.MultiPolygon{false, false, Vector{GeoInterface.Wrappers.Polygon{false, false, T, Nothing, Nothing} where T}, Nothing, Nothing}(GeoInterface.Wrappers.Polygon{false, false, T, Nothing, Nothing} where T[GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}(StaticArraysCore.SVector{2, Float64}[[70.0, 0.0], [70.01098781325325, 0.0004397316773170068], [70.0438052480035, 0.0035114210915891397], [70.09801605542096, 0.011814947665167774], [70.17289902010158, 0.027886421973952302], [70.26745668457025, 0.05416726609360478], [70.38042741557976, 0.09297443860091348], [70.51030066635026, 0.1464721641710074], [70.65533525026046, 0.21664550952386064], [70.8135804051007, 0.30527612515520186] … [70.86641841658641, -0.3376428491230612], [70.70440582002419, -0.24279488312757858], [70.55494217175954, -0.16692537029320365], [70.42004014766201, -0.10832215707812454], [70.30151010318639, -0.0650624499034016], [70.20093817218219, -0.03503632062070827], [70.11966707868197, -0.01597247419241532], [70.05877989361332, -0.005465967083412071], [70.01908693278165, -0.0010075412835199304], [70.00111595449914, -1.4219350464667047e-5]], nothing, nothing), GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}(StaticArraysCore.SVector{2, Float64}[[45.0, -5.0], [45.0, 5.0], [55.0, 5.0], [55.0, -5.0]], nothing, nothing)], nothing, nothing), GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.LinearRing{false, false, Vector{GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}[GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.999974634566875, -0.01592650896568995), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.999565375483215, -0.06592462566760626), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.99865616402829, -0.11591614996189725), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.997247091122496, -0.16589608273778408), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.99533829767195, -0.2158594260436434), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.99292997455441, -0.2658011835867806), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.990022362600165, -0.31571636123306385), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.98661575256801, -0.3655999675063154), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.98271048511609, -0.41544701408748197), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.9783069507679, -0.46525251631344455), nothing) … GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.97976366505997, 0.4493927459900552), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.9840085315131, 0.3995734698458635), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.9877550012664, 0.3497142366876638), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.991002699676024, 0.299820032397223), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.99375130197483, 0.24989584635339165), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.99600053330489, 0.1999466709331708), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.997750168744936, 0.1499775010124783), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.99900003333289, 0.0999933334666654), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.999750002083324, 0.049999166670833324), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((55.0, 0.0), nothing)], nothing, nothing)], nothing, nothing)], nothing, nothing)
Shift multipolygon
up, to avoid plotting over our earlier results.
xoffset = 0.;
+yoffset = 50.;
+f = CoordinateTransformations.Translation(xoffset, yoffset);
+multipolygon = GO.transform(f, multipolygon);
+plot!(multipolygon)
+fig
Great, now we can make Points
, MultiPoints
, Lines
, LineStrings
, Polygons
(with holes), and MultiPolygons
and modify them using [CoordinateTransformations
] and [GeometryOps
].
GeoMakie
and coordinate reference system (CRS
) In geospatial sciences we often have data in one Coordinate Reference System (CRS) (source
) and would like to display it in different (destination
) CRS
. GeoMakie
allows us to do this by automatically projecting from source
to destination
CRS.
Here, our source
CRS is common geographic (i.e. coordinates of latitude and longitude), WGS84.
source_crs1 = GFT.EPSG(4326)
GeoFormatTypes.EPSG{1}((4326,))
Now let's pick a destination
CRS for displaying our map. Here we'll pick natearth2.
destination_crs = "+proj=natearth2"
"+proj=natearth2"
Let's add land area for context. First, download and open the Natural Earth global land polygons at 110 m resolution.GeoMakie
ships with this particular dataset, so we will access it from there.
land_path = GeoMakie.assetpath("ne_110m_land.geojson")
"/home/runner/.julia/packages/GeoMakie/t8Vkb/assets/ne_110m_land.geojson"
Note
Natural Earth has lots of other datasets, and there is a Julia package that provides an interface to it called NaturalEarth.jl.
Read the land MultiPolygon
s as a GeoJSON.FeatureCollection
.
land_geo = GeoJSON.read(land_path)
FeatureCollection with 127 Features
We then need to create a figure with a GeoAxis
that can handle the projection between source
and destination
CRS. For GeoMakie, source
is the CRS of the input and dest
is the CRS you want to visualize in.
fig = Figure(size=(1000, 500));
+ga = GeoAxis(
+ fig[1, 1];
+ source = source_crs1,
+ dest = destination_crs,
+ xticklabelsvisible = false,
+ yticklabelsvisible = false,
+);
Plot land
for context.
poly!(ga, land_geo, color=:black)
+fig
Now let's plot a Polygon
like before, but this time with a CRS that differs from our source
data
plot!(multipolygon; color = :green)
+fig
But what if we want to plot geometries with a different source
CRS on the same figure?
To show how to do this let's create a geometry with coordinates in UTM (Universal Transverse Mercator) zone 10N EPSG:32610.
source_crs2 = GFT.EPSG(32610)
GeoFormatTypes.EPSG{1}((32610,))
Create a polygon (we're working in meters now, not latitude and longitude)
r = 1000000;
+ϴ = 0:0.01:2pi;
+x = r .* cos.(ϴ).^3 .+ 500000;
+y = r .* sin.(ϴ) .^ 3 .+5000000;
629-element Vector{Float64}:
+ 5.0e6
+ 5.0e6
+ 5.00001e6
+ ⋮
+ 5.0e6
+ 5.0e6
Now create a LinearRing
from Points
ring3 = GI.LinearRing(Point.(zip(x, y)))
GeoInterface.Wrappers.LinearRing{false, false, Vector{Point{2, Float64}}, Nothing, Nothing}(Point{2, Float64}[[1.5e6, 5.0e6], [1.4998500087497458e6, 5.000000999950001e6], [1.4994001399837343e6, 5.000007998400139e6], [1.4986507085647392e6, 5.000026987852369e6], [1.4976022389592e6, 5.000063948817746e6], [1.4962554647802354e6, 5.000124843834609e6], [1.4946113281484335e6, 5.000215611503127e6], [1.4926709788709967e6, 5.000342160541625e6], [1.4904357734399722e6, 5.000510363870095e6], [1.4879072738504685e6, 5.0007260527263e6] … [1.4870405593989636e6, 4.999194331880103e6], [1.4896621210021754e6, 4.999426363321033e6], [1.491990928929295e6, 4.999609061508909e6], [1.4940253560034204e6, 4.999748243174828e6], [1.4957639801366436e6, 4.999849768598615e6], [1.497205585568957e6, 4.999919535736425e6], [1.4983491639274692e6, 4.999963474314044e6], [1.4991939151049731e6, 4.999987539891298e6], [1.4997392479570867e6, 4.999997707902938e6], [1.499984780817334e6, 4.999999967681458e6]], nothing, nothing)
Now create a Polygon
from the LineRing
polygon3 = GI.Polygon([ring3])
GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{Point{2, Float64}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.LinearRing{false, false, Vector{Point{2, Float64}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{Point{2, Float64}}, Nothing, Nothing}(Point{2, Float64}[[1.5e6, 5.0e6], [1.4998500087497458e6, 5.000000999950001e6], [1.4994001399837343e6, 5.000007998400139e6], [1.4986507085647392e6, 5.000026987852369e6], [1.4976022389592e6, 5.000063948817746e6], [1.4962554647802354e6, 5.000124843834609e6], [1.4946113281484335e6, 5.000215611503127e6], [1.4926709788709967e6, 5.000342160541625e6], [1.4904357734399722e6, 5.000510363870095e6], [1.4879072738504685e6, 5.0007260527263e6] … [1.4870405593989636e6, 4.999194331880103e6], [1.4896621210021754e6, 4.999426363321033e6], [1.491990928929295e6, 4.999609061508909e6], [1.4940253560034204e6, 4.999748243174828e6], [1.4957639801366436e6, 4.999849768598615e6], [1.497205585568957e6, 4.999919535736425e6], [1.4983491639274692e6, 4.999963474314044e6], [1.4991939151049731e6, 4.999987539891298e6], [1.4997392479570867e6, 4.999997707902938e6], [1.499984780817334e6, 4.999999967681458e6]], nothing, nothing)], nothing, nothing)
Now plot on the existing GeoAxis.
Note
The keyword argument source
is used to specify the source CRS
of that particular plot, when plotting on an existing GeoAxis
.
plot!(ga,polygon3; color=:red, source = source_crs2)
+fig
Great, we can make geometries and plot them on a map... now let's export the data to common geospatial data formats. To do this we now need to create geometries with embedded CRS
information, making it a geospatial geometry. All that's needed is to include ; crs = crs
as a keyword argument when constructing the geometry.
Let's do this for a new Polygon
r = 3;
+k = 7;
+ϴ = 0:0.01:2pi;
+x = r .* (k + 1) .* cos.(ϴ) .- r .* cos.((k + 1) .* ϴ);
+y = r .* (k + 1) .* sin.(ϴ) .- r .* sin.((k + 1) .* ϴ);
+ring4 = GI.LinearRing(Point.(zip(x, y)))
GeoInterface.Wrappers.LinearRing{false, false, Vector{Point{2, Float64}}, Nothing, Nothing}(Point{2, Float64}[[21.0, 0.0], [21.00839489109211, 0.00025191811248184703], [21.033518309870985, 0.0020133807972559925], [21.075186885419612, 0.006784125578492062], [21.13309630561615, 0.016044338630866517], [21.206823267470536, 0.031245035570328428], [21.29582819010705, 0.053798628882221644], [21.39945867303846, 0.08506974233813636], [21.516953677609987, 0.12636633117296836], [21.64744840486518, 0.17893116483784577] … [21.69159119078359, -0.19823293781563178], [21.557153362189904, -0.14182952335952814], [21.43541888381864, -0.09707519809793252], [21.327284472232776, -0.06274967861547665], [21.233544778745394, -0.03756486776283019], [21.15488729606723, -0.020173244847778715], [21.091887951911644, -0.0091766360295773], [21.045007417743918, -0.0031353088009582475], [21.01458815628695, -0.0005773323690041465], [21.00085222666982, -8.14404531208901e-6]], nothing, nothing)
But this time when we create the Polygon
we need to specify the CRS
at the time of creation, making it a geospatial polygon
geopoly1 = GI.Polygon([ring4], crs = source_crs1)
GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{Point{2, Float64}}, Nothing, Nothing}}, Nothing, GeoFormatTypes.EPSG{1}}(GeoInterface.Wrappers.LinearRing{false, false, Vector{Point{2, Float64}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{Point{2, Float64}}, Nothing, Nothing}(Point{2, Float64}[[21.0, 0.0], [21.00839489109211, 0.00025191811248184703], [21.033518309870985, 0.0020133807972559925], [21.075186885419612, 0.006784125578492062], [21.13309630561615, 0.016044338630866517], [21.206823267470536, 0.031245035570328428], [21.29582819010705, 0.053798628882221644], [21.39945867303846, 0.08506974233813636], [21.516953677609987, 0.12636633117296836], [21.64744840486518, 0.17893116483784577] … [21.69159119078359, -0.19823293781563178], [21.557153362189904, -0.14182952335952814], [21.43541888381864, -0.09707519809793252], [21.327284472232776, -0.06274967861547665], [21.233544778745394, -0.03756486776283019], [21.15488729606723, -0.020173244847778715], [21.091887951911644, -0.0091766360295773], [21.045007417743918, -0.0031353088009582475], [21.01458815628695, -0.0005773323690041465], [21.00085222666982, -8.14404531208901e-6]], nothing, nothing)], nothing, GeoFormatTypes.EPSG{1}((4326,)))
Note
It is good practice to only include CRS information with the highest-level geometry. Not doing so can bloat the memory footprint of the geometry. CRS information can be included at the individual Point
level but is discouraged.
And let's create second Polygon
by shifting the first using CoordinateTransformations
xoffset = 20.;
+yoffset = -25.;
+f = CoordinateTransformations.Translation(xoffset, yoffset);
+geopoly2 = GO.transform(f, geopoly1);
GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, GeoFormatTypes.EPSG{1}}}, Nothing, GeoFormatTypes.EPSG{1}}(GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, GeoFormatTypes.EPSG{1}}[GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, GeoFormatTypes.EPSG{1}}(StaticArraysCore.SVector{2, Float64}[[41.0, -25.0], [41.00839489109211, -24.999748081887518], [41.033518309870985, -24.997986619202745], [41.07518688541961, -24.99321587442151], [41.13309630561615, -24.983955661369134], [41.20682326747054, -24.96875496442967], [41.295828190107045, -24.946201371117777], [41.39945867303846, -24.914930257661865], [41.51695367760999, -24.873633668827033], [41.64744840486518, -24.821068835162155] … [41.69159119078359, -25.198232937815632], [41.55715336218991, -25.14182952335953], [41.43541888381864, -25.097075198097933], [41.327284472232776, -25.062749678615475], [41.2335447787454, -25.037564867762832], [41.15488729606723, -25.02017324484778], [41.091887951911644, -25.009176636029576], [41.04500741774392, -25.003135308800957], [41.01458815628695, -25.000577332369005], [41.00085222666982, -25.000008144045314]], nothing, GeoFormatTypes.EPSG{1}((4326,)))], nothing, GeoFormatTypes.EPSG{1}((4326,)))
Typically, you'll also want to include attributes with your geometries. Attributes are simply data that are attributed to each geometry. The easiest way to do this is to create a table with a :geometry
column. Let's do this using DataFrames
.
using DataFrames
+df = DataFrame(geometry=[geopoly1, geopoly2])
Now let's add a couple of attributes to the geometries. We do this using DataFrames' !
mutation syntax that allows you to add a new column to an existing data frame.
df[!,:id] = ["a", "b"]
+df[!, :name] = ["polygon 1", "polygon 2"]
+df
There are Julia packages for most commonly used geographic data formats. Below, we show how to export that data to each of these.
We begin with GeoJSON, which is a JSON format for geospatial feature collections. It's human-readable and widely supported by most web-based and desktop geospatial libraries.
import GeoJSON
+fn = "shapes.json"
+GeoJSON.write(fn, df)
"shapes.json"
Now, let's save as a Shapefile
. Shapefiles are actually a set of files (usually 4) that hold geometry information, a CRS, and additional attribute information as a separate table. When you give Shapefile.write
a file name, it will write 4 files of the same name but with different extensions.
import Shapefile
+fn = "shapes.shp"
+Shapefile.write(fn, df)
20340
Now, let's save as a GeoParquet
. GeoParquet is a geospatial extension to the Parquet format, which is a high-performance data store. It's great for storing large amounts of data in a single file.
import GeoParquet
+fn = "shapes.parquet"
+GeoParquet.write(fn, df, (:geometry,))
"shapes.parquet"
Finally, if there's no Julia-native package that can write data to your desired format (e.g. .gpkg
, .gml
, etc), you can use GeoDataFrames
. This package uses the GDAL library under the hood which supports writing to nearly all geospatial formats.
import GeoDataFrames
+fn = "shapes.gpkg"
+GeoDataFrames.write(fn, df)
"shapes.gpkg"
And there we go, you can now create mapped geometries from scratch, manipulate them, plot them on a map, and save them in multiple geospatial data formats.
`,120)]))}const A=i(y,[["render",F]]);export{G as __pageData,A as default}; diff --git a/previews/PR239/assets/tutorials_creating_geometry.md.BW0vmesq.lean.js b/previews/PR239/assets/tutorials_creating_geometry.md.BW0vmesq.lean.js new file mode 100644 index 000000000..9097582fd --- /dev/null +++ b/previews/PR239/assets/tutorials_creating_geometry.md.BW0vmesq.lean.js @@ -0,0 +1,89 @@ +import{_ as i,c as a,a5 as e,o as t}from"./chunks/framework.onQNwZ2I.js";const n="/GeometryOps.jl/previews/PR239/assets/tfqfmef.Dz86q2IX.png",l="/GeometryOps.jl/previews/PR239/assets/ivowqmu.Cx40vhB3.png",p="/GeometryOps.jl/previews/PR239/assets/xetmrwv.0OJvb21A.png",h="/GeometryOps.jl/previews/PR239/assets/cthossd.DaovVbE6.png",o="/GeometryOps.jl/previews/PR239/assets/bnkpkoa.rOsRk89v.png",k="/GeometryOps.jl/previews/PR239/assets/krkgpps.4wfjCtJV.png",r="/GeometryOps.jl/previews/PR239/assets/rleiuex.3sfpQl2i.png",g="/GeometryOps.jl/previews/PR239/assets/fyfzmss.Dab1-ETk.png",d="/GeometryOps.jl/previews/PR239/assets/mtwzvwz.D9AE7i2o.png",E="/GeometryOps.jl/previews/PR239/assets/tstphwa.DTKLkKh_.png",c="/GeometryOps.jl/previews/PR239/assets/wjwdigs.0f3Lq4Lw.png",G=JSON.parse('{"title":"Creating Geometry","description":"","frontmatter":{},"headers":[],"relativePath":"tutorials/creating_geometry.md","filePath":"tutorials/creating_geometry.md","lastUpdated":null}'),y={name:"tutorials/creating_geometry.md"};function F(u,s,f,C,m,b){return t(),a("div",null,s[0]||(s[0]=[e(`In this tutorial, we're going to:
Plot geometries on a map using GeoMakie
and coordinate reference system (CRS
)
Create geospatial geometries with embedded coordinate reference system information
Save geospatial geometries to common geospatial file formats
First, we load some required packages.
# Geospatial packages from Julia
+import GeoInterface as GI
+import GeometryOps as GO
+import GeoFormatTypes as GFT
+using GeoJSON # to load some data
+# Packages for coordinate transformation and projection
+import CoordinateTransformations
+import Proj
+# Plotting
+using CairoMakie
+using GeoMakie
Let's start by making a single Point
.
point = GI.Point(0, 0)
GeoInterface.Wrappers.Point{false, false, Tuple{Int64, Int64}, Nothing}((0, 0), nothing)
Now, let's plot our point.
fig, ax, plt = plot(point)
Let's create a set of points, and have a bit more fun with plotting.
x = [-5, 0, 5, 0];
+y = [0, -5, 0, 5];
+points = GI.Point.(zip(x,y));
+plot!(ax, points; marker = '✈', markersize = 30)
+fig
Point
s can be combined into a single MultiPoint
geometry.
x = [-5, -5, 5, 5];
+y = [-5, 5, 5, -5];
+multipoint = GI.MultiPoint(GI.Point.(zip(x, y)));
+plot!(ax, multipoint; marker = '☁', markersize = 30)
+fig
Let's create a LineString
connecting two points.
p1 = GI.Point.(-5, 0);
+p2 = GI.Point.(5, 0);
+line = GI.LineString([p1,p2])
+plot!(ax, line; color = :red)
+fig
Now, let's create a line connecting multiple points (i.e. a LineString
). This time we get a bit more fancy with point creation.
r = 2;
+k = 10;
+ϴ = 0:0.01:2pi;
+x = r .* (k + 1) .* cos.(ϴ) .- r .* cos.((k + 1) .* ϴ);
+y = r .* (k + 1) .* sin.(ϴ) .- r .* sin.((k + 1) .* ϴ);
+lines = GI.LineString(GI.Point.(zip(x,y)));
+plot!(ax, lines; linewidth = 5)
+fig
We can also create a single LinearRing
trait, the building block of a polygon. A LinearRing
is simply a LineString
with the same beginning and endpoint, i.e., an arbitrary closed shape composed of point pairs.
A LinearRing
is composed of a series of points.
ring1 = GI.LinearRing(GI.getpoint(lines));
GeoInterface.Wrappers.LinearRing{false, false, Vector{GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}[GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.0, 0.0), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.010987813253244, 0.0004397316773170068), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.043805248003498, 0.0035114210915891397), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.098016055420953, 0.011814947665167774), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.172899020101585, 0.027886421973952302), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.267456684570245, 0.05416726609360478), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.380427415579764, 0.09297443860091348), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.51030066635026, 0.1464721641710074), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.655335250260467, 0.21664550952386064), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.813580405100698, 0.30527612515520186), nothing) … GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.866418416586406, -0.3376428491230612), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.704405820024185, -0.24279488312757858), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.55494217175954, -0.16692537029320365), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.420040147662014, -0.10832215707812454), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.30151010318639, -0.0650624499034016), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.200938172182195, -0.03503632062070827), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.119667078681967, -0.01597247419241532), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.058779893613323, -0.005465967083412071), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.019086932781654, -0.0010075412835199304), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.001115954499138, -1.4219350464667047e-5), nothing)], nothing, nothing)
Now, let's make the LinearRing
into a Polygon
.
polygon1 = GI.Polygon([ring1]);
GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.LinearRing{false, false, Vector{GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}[GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.0, 0.0), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.010987813253244, 0.0004397316773170068), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.043805248003498, 0.0035114210915891397), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.098016055420953, 0.011814947665167774), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.172899020101585, 0.027886421973952302), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.267456684570245, 0.05416726609360478), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.380427415579764, 0.09297443860091348), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.51030066635026, 0.1464721641710074), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.655335250260467, 0.21664550952386064), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.813580405100698, 0.30527612515520186), nothing) … GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.866418416586406, -0.3376428491230612), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.704405820024185, -0.24279488312757858), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.55494217175954, -0.16692537029320365), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.420040147662014, -0.10832215707812454), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.30151010318639, -0.0650624499034016), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.200938172182195, -0.03503632062070827), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.119667078681967, -0.01597247419241532), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.058779893613323, -0.005465967083412071), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.019086932781654, -0.0010075412835199304), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.001115954499138, -1.4219350464667047e-5), nothing)], nothing, nothing)], nothing, nothing)
Now, we can use GeometryOps and CoordinateTransformations to shift polygon1
up, to avoid plotting over our earlier results. This is done through the GeometryOps.transform function.
xoffset = 0.;
+yoffset = 50.;
+f = CoordinateTransformations.Translation(xoffset, yoffset);
+polygon1 = GO.transform(f, polygon1);
+plot!(polygon1)
+fig
Polygons can contain "holes". The first LinearRing
in a polygon is the exterior, and all subsequent LinearRing
s are treated as holes in the leading LinearRing
.
GeoInterface
offers the GI.getexterior(poly)
and GI.gethole(poly)
methods to get the exterior ring and an iterable of holes, respectively.
hole = GI.LinearRing(GI.getpoint(multipoint))
+polygon2 = GI.Polygon([ring1, hole])
GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, T, Nothing, Nothing} where T}, Nothing, Nothing}(GeoInterface.Wrappers.LinearRing{false, false, T, Nothing, Nothing} where T[GeoInterface.Wrappers.LinearRing{false, false, Vector{GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}[GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.0, 0.0), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.010987813253244, 0.0004397316773170068), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.043805248003498, 0.0035114210915891397), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.098016055420953, 0.011814947665167774), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.172899020101585, 0.027886421973952302), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.267456684570245, 0.05416726609360478), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.380427415579764, 0.09297443860091348), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.51030066635026, 0.1464721641710074), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.655335250260467, 0.21664550952386064), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.813580405100698, 0.30527612515520186), nothing) … GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.866418416586406, -0.3376428491230612), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.704405820024185, -0.24279488312757858), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.55494217175954, -0.16692537029320365), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.420040147662014, -0.10832215707812454), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.30151010318639, -0.0650624499034016), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.200938172182195, -0.03503632062070827), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.119667078681967, -0.01597247419241532), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.058779893613323, -0.005465967083412071), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.019086932781654, -0.0010075412835199304), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((20.001115954499138, -1.4219350464667047e-5), nothing)], nothing, nothing), GeoInterface.Wrappers.LinearRing{false, false, Vector{GeoInterface.Wrappers.Point{false, false, Tuple{Int64, Int64}, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.Point{false, false, Tuple{Int64, Int64}, Nothing}[GeoInterface.Wrappers.Point{false, false, Tuple{Int64, Int64}, Nothing}((-5, -5), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Int64, Int64}, Nothing}((-5, 5), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Int64, Int64}, Nothing}((5, 5), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Int64, Int64}, Nothing}((5, -5), nothing)], nothing, nothing)], nothing, nothing)
Shift polygon2
to the right, to avoid plotting over our earlier results.
xoffset = 50.;
+yoffset = 0.;
+f = CoordinateTransformations.Translation(xoffset, yoffset);
+polygon2 = GO.transform(f, polygon2);
+plot!(polygon2)
+fig
Polygon
s can also be grouped together as a MultiPolygon
.
r = 5;
+x = cos.(reverse(ϴ)) .* r .+ xoffset;
+y = sin.(reverse(ϴ)) .* r .+ yoffset;
+ring2 = GI.LinearRing(GI.Point.(zip(x,y)));
+polygon3 = GI.Polygon([ring2]);
+multipolygon = GI.MultiPolygon([polygon2, polygon3])
GeoInterface.Wrappers.MultiPolygon{false, false, Vector{GeoInterface.Wrappers.Polygon{false, false, T, Nothing, Nothing} where T}, Nothing, Nothing}(GeoInterface.Wrappers.Polygon{false, false, T, Nothing, Nothing} where T[GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}(StaticArraysCore.SVector{2, Float64}[[70.0, 0.0], [70.01098781325325, 0.0004397316773170068], [70.0438052480035, 0.0035114210915891397], [70.09801605542096, 0.011814947665167774], [70.17289902010158, 0.027886421973952302], [70.26745668457025, 0.05416726609360478], [70.38042741557976, 0.09297443860091348], [70.51030066635026, 0.1464721641710074], [70.65533525026046, 0.21664550952386064], [70.8135804051007, 0.30527612515520186] … [70.86641841658641, -0.3376428491230612], [70.70440582002419, -0.24279488312757858], [70.55494217175954, -0.16692537029320365], [70.42004014766201, -0.10832215707812454], [70.30151010318639, -0.0650624499034016], [70.20093817218219, -0.03503632062070827], [70.11966707868197, -0.01597247419241532], [70.05877989361332, -0.005465967083412071], [70.01908693278165, -0.0010075412835199304], [70.00111595449914, -1.4219350464667047e-5]], nothing, nothing), GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, Nothing}(StaticArraysCore.SVector{2, Float64}[[45.0, -5.0], [45.0, 5.0], [55.0, 5.0], [55.0, -5.0]], nothing, nothing)], nothing, nothing), GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.LinearRing{false, false, Vector{GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}[GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.999974634566875, -0.01592650896568995), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.999565375483215, -0.06592462566760626), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.99865616402829, -0.11591614996189725), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.997247091122496, -0.16589608273778408), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.99533829767195, -0.2158594260436434), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.99292997455441, -0.2658011835867806), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.990022362600165, -0.31571636123306385), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.98661575256801, -0.3655999675063154), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.98271048511609, -0.41544701408748197), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.9783069507679, -0.46525251631344455), nothing) … GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.97976366505997, 0.4493927459900552), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.9840085315131, 0.3995734698458635), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.9877550012664, 0.3497142366876638), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.991002699676024, 0.299820032397223), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.99375130197483, 0.24989584635339165), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.99600053330489, 0.1999466709331708), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.997750168744936, 0.1499775010124783), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.99900003333289, 0.0999933334666654), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((54.999750002083324, 0.049999166670833324), nothing), GeoInterface.Wrappers.Point{false, false, Tuple{Float64, Float64}, Nothing}((55.0, 0.0), nothing)], nothing, nothing)], nothing, nothing)], nothing, nothing)
Shift multipolygon
up, to avoid plotting over our earlier results.
xoffset = 0.;
+yoffset = 50.;
+f = CoordinateTransformations.Translation(xoffset, yoffset);
+multipolygon = GO.transform(f, multipolygon);
+plot!(multipolygon)
+fig
Great, now we can make Points
, MultiPoints
, Lines
, LineStrings
, Polygons
(with holes), and MultiPolygons
and modify them using [CoordinateTransformations
] and [GeometryOps
].
GeoMakie
and coordinate reference system (CRS
) In geospatial sciences we often have data in one Coordinate Reference System (CRS) (source
) and would like to display it in different (destination
) CRS
. GeoMakie
allows us to do this by automatically projecting from source
to destination
CRS.
Here, our source
CRS is common geographic (i.e. coordinates of latitude and longitude), WGS84.
source_crs1 = GFT.EPSG(4326)
GeoFormatTypes.EPSG{1}((4326,))
Now let's pick a destination
CRS for displaying our map. Here we'll pick natearth2.
destination_crs = "+proj=natearth2"
"+proj=natearth2"
Let's add land area for context. First, download and open the Natural Earth global land polygons at 110 m resolution.GeoMakie
ships with this particular dataset, so we will access it from there.
land_path = GeoMakie.assetpath("ne_110m_land.geojson")
"/home/runner/.julia/packages/GeoMakie/t8Vkb/assets/ne_110m_land.geojson"
Note
Natural Earth has lots of other datasets, and there is a Julia package that provides an interface to it called NaturalEarth.jl.
Read the land MultiPolygon
s as a GeoJSON.FeatureCollection
.
land_geo = GeoJSON.read(land_path)
FeatureCollection with 127 Features
We then need to create a figure with a GeoAxis
that can handle the projection between source
and destination
CRS. For GeoMakie, source
is the CRS of the input and dest
is the CRS you want to visualize in.
fig = Figure(size=(1000, 500));
+ga = GeoAxis(
+ fig[1, 1];
+ source = source_crs1,
+ dest = destination_crs,
+ xticklabelsvisible = false,
+ yticklabelsvisible = false,
+);
Plot land
for context.
poly!(ga, land_geo, color=:black)
+fig
Now let's plot a Polygon
like before, but this time with a CRS that differs from our source
data
plot!(multipolygon; color = :green)
+fig
But what if we want to plot geometries with a different source
CRS on the same figure?
To show how to do this let's create a geometry with coordinates in UTM (Universal Transverse Mercator) zone 10N EPSG:32610.
source_crs2 = GFT.EPSG(32610)
GeoFormatTypes.EPSG{1}((32610,))
Create a polygon (we're working in meters now, not latitude and longitude)
r = 1000000;
+ϴ = 0:0.01:2pi;
+x = r .* cos.(ϴ).^3 .+ 500000;
+y = r .* sin.(ϴ) .^ 3 .+5000000;
629-element Vector{Float64}:
+ 5.0e6
+ 5.0e6
+ 5.00001e6
+ ⋮
+ 5.0e6
+ 5.0e6
Now create a LinearRing
from Points
ring3 = GI.LinearRing(Point.(zip(x, y)))
GeoInterface.Wrappers.LinearRing{false, false, Vector{Point{2, Float64}}, Nothing, Nothing}(Point{2, Float64}[[1.5e6, 5.0e6], [1.4998500087497458e6, 5.000000999950001e6], [1.4994001399837343e6, 5.000007998400139e6], [1.4986507085647392e6, 5.000026987852369e6], [1.4976022389592e6, 5.000063948817746e6], [1.4962554647802354e6, 5.000124843834609e6], [1.4946113281484335e6, 5.000215611503127e6], [1.4926709788709967e6, 5.000342160541625e6], [1.4904357734399722e6, 5.000510363870095e6], [1.4879072738504685e6, 5.0007260527263e6] … [1.4870405593989636e6, 4.999194331880103e6], [1.4896621210021754e6, 4.999426363321033e6], [1.491990928929295e6, 4.999609061508909e6], [1.4940253560034204e6, 4.999748243174828e6], [1.4957639801366436e6, 4.999849768598615e6], [1.497205585568957e6, 4.999919535736425e6], [1.4983491639274692e6, 4.999963474314044e6], [1.4991939151049731e6, 4.999987539891298e6], [1.4997392479570867e6, 4.999997707902938e6], [1.499984780817334e6, 4.999999967681458e6]], nothing, nothing)
Now create a Polygon
from the LineRing
polygon3 = GI.Polygon([ring3])
GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{Point{2, Float64}}, Nothing, Nothing}}, Nothing, Nothing}(GeoInterface.Wrappers.LinearRing{false, false, Vector{Point{2, Float64}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{Point{2, Float64}}, Nothing, Nothing}(Point{2, Float64}[[1.5e6, 5.0e6], [1.4998500087497458e6, 5.000000999950001e6], [1.4994001399837343e6, 5.000007998400139e6], [1.4986507085647392e6, 5.000026987852369e6], [1.4976022389592e6, 5.000063948817746e6], [1.4962554647802354e6, 5.000124843834609e6], [1.4946113281484335e6, 5.000215611503127e6], [1.4926709788709967e6, 5.000342160541625e6], [1.4904357734399722e6, 5.000510363870095e6], [1.4879072738504685e6, 5.0007260527263e6] … [1.4870405593989636e6, 4.999194331880103e6], [1.4896621210021754e6, 4.999426363321033e6], [1.491990928929295e6, 4.999609061508909e6], [1.4940253560034204e6, 4.999748243174828e6], [1.4957639801366436e6, 4.999849768598615e6], [1.497205585568957e6, 4.999919535736425e6], [1.4983491639274692e6, 4.999963474314044e6], [1.4991939151049731e6, 4.999987539891298e6], [1.4997392479570867e6, 4.999997707902938e6], [1.499984780817334e6, 4.999999967681458e6]], nothing, nothing)], nothing, nothing)
Now plot on the existing GeoAxis.
Note
The keyword argument source
is used to specify the source CRS
of that particular plot, when plotting on an existing GeoAxis
.
plot!(ga,polygon3; color=:red, source = source_crs2)
+fig
Great, we can make geometries and plot them on a map... now let's export the data to common geospatial data formats. To do this we now need to create geometries with embedded CRS
information, making it a geospatial geometry. All that's needed is to include ; crs = crs
as a keyword argument when constructing the geometry.
Let's do this for a new Polygon
r = 3;
+k = 7;
+ϴ = 0:0.01:2pi;
+x = r .* (k + 1) .* cos.(ϴ) .- r .* cos.((k + 1) .* ϴ);
+y = r .* (k + 1) .* sin.(ϴ) .- r .* sin.((k + 1) .* ϴ);
+ring4 = GI.LinearRing(Point.(zip(x, y)))
GeoInterface.Wrappers.LinearRing{false, false, Vector{Point{2, Float64}}, Nothing, Nothing}(Point{2, Float64}[[21.0, 0.0], [21.00839489109211, 0.00025191811248184703], [21.033518309870985, 0.0020133807972559925], [21.075186885419612, 0.006784125578492062], [21.13309630561615, 0.016044338630866517], [21.206823267470536, 0.031245035570328428], [21.29582819010705, 0.053798628882221644], [21.39945867303846, 0.08506974233813636], [21.516953677609987, 0.12636633117296836], [21.64744840486518, 0.17893116483784577] … [21.69159119078359, -0.19823293781563178], [21.557153362189904, -0.14182952335952814], [21.43541888381864, -0.09707519809793252], [21.327284472232776, -0.06274967861547665], [21.233544778745394, -0.03756486776283019], [21.15488729606723, -0.020173244847778715], [21.091887951911644, -0.0091766360295773], [21.045007417743918, -0.0031353088009582475], [21.01458815628695, -0.0005773323690041465], [21.00085222666982, -8.14404531208901e-6]], nothing, nothing)
But this time when we create the Polygon
we need to specify the CRS
at the time of creation, making it a geospatial polygon
geopoly1 = GI.Polygon([ring4], crs = source_crs1)
GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{Point{2, Float64}}, Nothing, Nothing}}, Nothing, GeoFormatTypes.EPSG{1}}(GeoInterface.Wrappers.LinearRing{false, false, Vector{Point{2, Float64}}, Nothing, Nothing}[GeoInterface.Wrappers.LinearRing{false, false, Vector{Point{2, Float64}}, Nothing, Nothing}(Point{2, Float64}[[21.0, 0.0], [21.00839489109211, 0.00025191811248184703], [21.033518309870985, 0.0020133807972559925], [21.075186885419612, 0.006784125578492062], [21.13309630561615, 0.016044338630866517], [21.206823267470536, 0.031245035570328428], [21.29582819010705, 0.053798628882221644], [21.39945867303846, 0.08506974233813636], [21.516953677609987, 0.12636633117296836], [21.64744840486518, 0.17893116483784577] … [21.69159119078359, -0.19823293781563178], [21.557153362189904, -0.14182952335952814], [21.43541888381864, -0.09707519809793252], [21.327284472232776, -0.06274967861547665], [21.233544778745394, -0.03756486776283019], [21.15488729606723, -0.020173244847778715], [21.091887951911644, -0.0091766360295773], [21.045007417743918, -0.0031353088009582475], [21.01458815628695, -0.0005773323690041465], [21.00085222666982, -8.14404531208901e-6]], nothing, nothing)], nothing, GeoFormatTypes.EPSG{1}((4326,)))
Note
It is good practice to only include CRS information with the highest-level geometry. Not doing so can bloat the memory footprint of the geometry. CRS information can be included at the individual Point
level but is discouraged.
And let's create second Polygon
by shifting the first using CoordinateTransformations
xoffset = 20.;
+yoffset = -25.;
+f = CoordinateTransformations.Translation(xoffset, yoffset);
+geopoly2 = GO.transform(f, geopoly1);
GeoInterface.Wrappers.Polygon{false, false, Vector{GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, GeoFormatTypes.EPSG{1}}}, Nothing, GeoFormatTypes.EPSG{1}}(GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, GeoFormatTypes.EPSG{1}}[GeoInterface.Wrappers.LinearRing{false, false, Vector{StaticArraysCore.SVector{2, Float64}}, Nothing, GeoFormatTypes.EPSG{1}}(StaticArraysCore.SVector{2, Float64}[[41.0, -25.0], [41.00839489109211, -24.999748081887518], [41.033518309870985, -24.997986619202745], [41.07518688541961, -24.99321587442151], [41.13309630561615, -24.983955661369134], [41.20682326747054, -24.96875496442967], [41.295828190107045, -24.946201371117777], [41.39945867303846, -24.914930257661865], [41.51695367760999, -24.873633668827033], [41.64744840486518, -24.821068835162155] … [41.69159119078359, -25.198232937815632], [41.55715336218991, -25.14182952335953], [41.43541888381864, -25.097075198097933], [41.327284472232776, -25.062749678615475], [41.2335447787454, -25.037564867762832], [41.15488729606723, -25.02017324484778], [41.091887951911644, -25.009176636029576], [41.04500741774392, -25.003135308800957], [41.01458815628695, -25.000577332369005], [41.00085222666982, -25.000008144045314]], nothing, GeoFormatTypes.EPSG{1}((4326,)))], nothing, GeoFormatTypes.EPSG{1}((4326,)))
Typically, you'll also want to include attributes with your geometries. Attributes are simply data that are attributed to each geometry. The easiest way to do this is to create a table with a :geometry
column. Let's do this using DataFrames
.
using DataFrames
+df = DataFrame(geometry=[geopoly1, geopoly2])
Now let's add a couple of attributes to the geometries. We do this using DataFrames' !
mutation syntax that allows you to add a new column to an existing data frame.
df[!,:id] = ["a", "b"]
+df[!, :name] = ["polygon 1", "polygon 2"]
+df
There are Julia packages for most commonly used geographic data formats. Below, we show how to export that data to each of these.
We begin with GeoJSON, which is a JSON format for geospatial feature collections. It's human-readable and widely supported by most web-based and desktop geospatial libraries.
import GeoJSON
+fn = "shapes.json"
+GeoJSON.write(fn, df)
"shapes.json"
Now, let's save as a Shapefile
. Shapefiles are actually a set of files (usually 4) that hold geometry information, a CRS, and additional attribute information as a separate table. When you give Shapefile.write
a file name, it will write 4 files of the same name but with different extensions.
import Shapefile
+fn = "shapes.shp"
+Shapefile.write(fn, df)
20340
Now, let's save as a GeoParquet
. GeoParquet is a geospatial extension to the Parquet format, which is a high-performance data store. It's great for storing large amounts of data in a single file.
import GeoParquet
+fn = "shapes.parquet"
+GeoParquet.write(fn, df, (:geometry,))
"shapes.parquet"
Finally, if there's no Julia-native package that can write data to your desired format (e.g. .gpkg
, .gml
, etc), you can use GeoDataFrames
. This package uses the GDAL library under the hood which supports writing to nearly all geospatial formats.
import GeoDataFrames
+fn = "shapes.gpkg"
+GeoDataFrames.write(fn, df)
"shapes.gpkg"
And there we go, you can now create mapped geometries from scratch, manipulate them, plot them on a map, and save them in multiple geospatial data formats.
`,120)]))}const A=i(y,[["render",F]]);export{G as __pageData,A as default}; diff --git a/previews/PR239/assets/tutorials_geodesic_paths.md.BlU0MlUq.js b/previews/PR239/assets/tutorials_geodesic_paths.md.BlU0MlUq.js new file mode 100644 index 000000000..020af8870 --- /dev/null +++ b/previews/PR239/assets/tutorials_geodesic_paths.md.BlU0MlUq.js @@ -0,0 +1,11 @@ +import{_ as i,c as a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const t="/GeometryOps.jl/previews/PR239/assets/chihpsx.CPClNl7F.png",o=JSON.parse('{"title":"Geodesic paths","description":"","frontmatter":{},"headers":[],"relativePath":"tutorials/geodesic_paths.md","filePath":"tutorials/geodesic_paths.md","lastUpdated":null}'),p={name:"tutorials/geodesic_paths.md"};function k(l,s,e,E,r,d){return h(),a("div",null,s[0]||(s[0]=[n(`Geodesic paths are paths computed on an ellipsoid, as opposed to a plane.
import GeometryOps as GO, GeoInterface as GI
+using CairoMakie, GeoMakie
+
+
+IAH = (-95.358421, 29.749907)
+AMS = (4.897070, 52.377956)
+
+
+fig, ga, _cp = lines(GeoMakie.coastlines(); axis = (; type = GeoAxis))
+lines!(ga, GO.segmentize(GO.GeodesicSegments(; max_distance = 100_000), GI.LineString([IAH, AMS])); color = Makie.wong_colors()[2])
+fig
Geodesic paths are paths computed on an ellipsoid, as opposed to a plane.
import GeometryOps as GO, GeoInterface as GI
+using CairoMakie, GeoMakie
+
+
+IAH = (-95.358421, 29.749907)
+AMS = (4.897070, 52.377956)
+
+
+fig, ga, _cp = lines(GeoMakie.coastlines(); axis = (; type = GeoAxis))
+lines!(ga, GO.segmentize(GO.GeodesicSegments(; max_distance = 100_000), GI.LineString([IAH, AMS])); color = Makie.wong_colors()[2])
+fig
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 by_pred
joining method. This allows the user to specify a predicate in the following manner, for any kind of table join operation:
using FlexiJoins
+innerjoin((table1, table1),
+ by_pred(:table1_column, predicate_function, :table2_column) # & add other conditions here
+)
+leftjoin((table1, table1),
+ by_pred(:table1_column, predicate_function, :table2_column) # & add other conditions here
+)
+rightjoin((table1, table1),
+ by_pred(:table1_column, predicate_function, :table2_column) # & add other conditions here
+)
+outerjoin((table1, table1),
+ by_pred(:table1_column, predicate_function, :table2_column) # & add other conditions here
+)
We have enabled the use of all of GeometryOps' boolean comparisons here. These are:
GO.contains, GO.within, GO.intersects, GO.touches, GO.crosses, GO.disjoint, GO.overlaps, GO.covers, GO.coveredby, GO.equals
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 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.
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.
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))
Here, the upper polygon is blue, and the lower polygon is red. Keep this in mind!
Now, we generate the points.
points = tuple.(rand(1000), rand(1000))
+points_df = DataFrame(geometry = points)
+scatter!(points_df.geometry)
+f
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
@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
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.
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))
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:
my_predicate_function = <(5) ∘ abs ∘ GO.distance
You would need to define FlexiJoins.supports_mode
on your predicate:
FlexiJoins.supports_mode(
+ ::FlexiJoins.Mode.NestedLoopFast,
+ ::FlexiJoins.ByPred{typeof(my_predicate_function)},
+ datas
+) = true
This will enable FlexiJoins to support your custom function, when it's passed to by_pred(:geometry, my_predicate_function, :geometry)
.
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 by_pred
joining method. This allows the user to specify a predicate in the following manner, for any kind of table join operation:
using FlexiJoins
+innerjoin((table1, table1),
+ by_pred(:table1_column, predicate_function, :table2_column) # & add other conditions here
+)
+leftjoin((table1, table1),
+ by_pred(:table1_column, predicate_function, :table2_column) # & add other conditions here
+)
+rightjoin((table1, table1),
+ by_pred(:table1_column, predicate_function, :table2_column) # & add other conditions here
+)
+outerjoin((table1, table1),
+ by_pred(:table1_column, predicate_function, :table2_column) # & add other conditions here
+)
We have enabled the use of all of GeometryOps' boolean comparisons here. These are:
GO.contains, GO.within, GO.intersects, GO.touches, GO.crosses, GO.disjoint, GO.overlaps, GO.covers, GO.coveredby, GO.equals
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 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.
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.
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))
Here, the upper polygon is blue, and the lower polygon is red. Keep this in mind!
Now, we generate the points.
points = tuple.(rand(1000), rand(1000))
+points_df = DataFrame(geometry = points)
+scatter!(points_df.geometry)
+f
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
@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
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.
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))
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:
my_predicate_function = <(5) ∘ abs ∘ GO.distance
You would need to define FlexiJoins.supports_mode
on your predicate:
FlexiJoins.supports_mode(
+ ::FlexiJoins.Mode.NestedLoopFast,
+ ::FlexiJoins.ByPred{typeof(my_predicate_function)},
+ datas
+) = true
This will enable FlexiJoins to support your custom function, when it's passed to by_pred(:geometry, my_predicate_function, :geometry)
.
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!
GeometryOps' docs are divided into three main sections: tutorials, explanations and source code.
Documentation and examples for many functions can be found in the source code section, since we use literate programming in GeometryOps.