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 @@ + + + + + + 404 | GeometryOps.jl + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/previews/PR239/api.html b/previews/PR239/api.html new file mode 100644 index 000000000..811994a92 --- /dev/null +++ b/previews/PR239/api.html @@ -0,0 +1,405 @@ + + + + + + Full GeometryOps API documentation | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Full GeometryOps API documentation

Warning

This page is still very much WIP!

Documentation for GeometryOps's full API (only for reference!).

apply and associated functions

GeometryOpsCore.apply Function
julia
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:

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

source

GeometryOpsCore.applyreduce Function
julia
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.

source

GeometryOps.reproject Function
julia
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.

source

GeometryOps.transform Function
julia
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)

source

General geometry methods

OGC methods

GeometryOps.contains Function
julia
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

julia
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

source

GeometryOps.coveredby Function
julia
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

julia
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

source

GeometryOps.covers Function
julia
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

julia
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

source

GeometryOps.crosses Function
julia
 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

source

GeometryOps.disjoint Function
julia
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

julia
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

source

GeometryOps.intersects Function
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

julia
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

source

GeometryOps.overlaps Function
julia
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

julia
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

source

julia
overlaps(::GI.AbstractTrait, geom1, ::GI.AbstractTrait, geom2)::Bool

For any non-specified pair, all have non-matching dimensions, return false.

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
overlaps(
+    ::GI.MultiPolygonTrait, polys1,
+    ::GI.MultiPolygonTrait, polys2,
+)::Bool

Return true if at least one pair of polygons from multipolygons overlap. Else false.

source

GeometryOps.touches Function
julia
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

julia
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

source

GeometryOps.within Function
julia
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

julia
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

source

Other general methods

GeometryOps.equals Function
julia
equals(geom1, geom2)::Bool

Compare two Geometries return true if they are the same geometry.

Examples

julia
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

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
equals(::GI.MultiPointTrait, mp1, ::GI.MultiPointTrait, mp2)::Bool

Two multipoints are equal if they share the same set of points.

source

julia
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.

source

julia
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

source

julia
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

source

julia
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.

source

julia
equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool

Two polygons are equal if they share the same exterior edge and holes.

source

julia
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.

source

julia
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.

source

julia
equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool

Two multipolygons are equal if they share the same set of polygons.

source

GeometryOps.centroid Function
julia
centroid(geom, [T=Float64])::Tuple{T, T}

Returns the centroid of a given line segment, linear ring, polygon, or mutlipolygon.

source

GeometryOps.distance Function
julia
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.

source

GeometryOps.signed_distance Function
julia
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.

source

GeometryOps.area Function
julia
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.

source

GeometryOps.signed_area Function
julia
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.

source

GeometryOps.angles Function
julia
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.

source

GeometryOps.embed_extent Function
julia
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.

source

Barycentric coordinates

GeometryOps.barycentric_coordinates Function
julia
barycentric_coordinates(method = MeanValue(), polygon, point)

Returns the barycentric coordinates of point in polygon using the barycentric coordinate method method.

source

GeometryOps.barycentric_coordinates! Function
julia
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.

source

GeometryOps.barycentric_interpolate Function
julia
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).

source

Other methods

GeometryOps.AbstractBarycentricCoordinateMethod Type
julia
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.

source

GeometryOps.ClosedRing Type
julia
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.

source

GeometryOps.DiffIntersectingPolygons Type
julia
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.

source

GeometryOps.DouglasPeucker Type
julia
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.

source

GeometryOps.GEOS Type
julia
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!

source

GeometryOps.GeodesicSegments Type
julia
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.

source

GeometryOps.GeometryCorrection Type
julia
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.

source

GeometryOps.LineOrientation Type
julia
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).

source

GeometryOps.LinearSegments Type
julia
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.

source

GeometryOps.MeanValue Type
julia
MeanValue() <: AbstractBarycentricCoordinateMethod

This method calculates barycentric coordinates using the mean value method.

References

source

GeometryOps.MonotoneChainMethod Type
julia
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

source

GeometryOps.PointOrientation Type
julia
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.

source

GeometryOps.RadialDistance Type
julia
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.

source

GeometryOps.SimplifyAlg Type
julia
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.

source

GeometryOps.UnionIntersectingPolygons Type
julia
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.

source

GeometryOps.VisvalingamWhyatt Type
julia
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.

source

GeometryOps._det Method
julia
_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]

source

GeometryOps._equals_curves Method
julia
_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)

source

GeometryOps.angles Method
julia
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.

source

GeometryOps.area Method
julia
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.

source

GeometryOps.barycentric_coordinates! Method
julia
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.

source

GeometryOps.barycentric_coordinates Method
julia
barycentric_coordinates(method = MeanValue(), polygon, point)

Returns the barycentric coordinates of point in polygon using the barycentric coordinate method method.

source

GeometryOps.barycentric_interpolate Method
julia
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).

source

GeometryOps.centroid Method
julia
centroid(geom, [T=Float64])::Tuple{T, T}

Returns the centroid of a given line segment, linear ring, polygon, or mutlipolygon.

source

GeometryOps.centroid_and_area Method
julia
centroid_and_area(geom, [T=Float64])::(::Tuple{T, T}, ::Real)

Returns the centroid and area of a given geometry.

source

GeometryOps.centroid_and_length Method
julia
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.

source

GeometryOps.contains Method
julia
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

julia
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

source

GeometryOps.convex_hull Function
julia
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).

source

GeometryOps.coverage Method
julia
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.

source

GeometryOps.coveredby Method
julia
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

julia
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

source

GeometryOps.covers Method
julia
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

julia
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

source

GeometryOps.crosses Method
julia
 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

source

GeometryOps.cut Method
julia
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

julia
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]]]

source

GeometryOps.difference Method
julia
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

julia
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]]]

source

GeometryOps.disjoint Method
julia
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

julia
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

source

GeometryOps.distance Method
julia
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.

source

GeometryOps.embed_extent Method
julia
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.

source

GeometryOps.enforce Method
julia
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).

source

GeometryOps.equals Method
julia
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.

source

GeometryOps.equals Method
julia
equals(geom1, geom2)::Bool

Compare two Geometries return true if they are the same geometry.

Examples

julia
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

source

GeometryOps.equals Method
julia
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.

source

GeometryOps.equals Method
julia
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

source

GeometryOps.equals Method
julia
equals(::GI.MultiPointTrait, mp1, ::GI.MultiPointTrait, mp2)::Bool

Two multipoints are equal if they share the same set of points.

source

GeometryOps.equals Method
julia
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.

source

GeometryOps.equals Method
julia
equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool

Two multipolygons are equal if they share the same set of polygons.

source

GeometryOps.equals Method
julia
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.

source

GeometryOps.equals Method
julia
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.

source

GeometryOps.equals Method
julia
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.

source

GeometryOps.equals Method
julia
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.

source

GeometryOps.equals Method
julia
equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool

Two polygons are equal if they share the same exterior edge and holes.

source

GeometryOps.equals Method
julia
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

source

GeometryOps.equals Method
julia
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.

source

GeometryOps.equals Method
julia
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.

source

GeometryOps.flip Method
julia
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.

source

GeometryOps.intersection Method
julia
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

julia
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]

source

GeometryOps.intersection_points Method
julia
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

+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

julia
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

source

GeometryOps.isclockwise Method
julia
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,

(i=2n(xixi1)(yi+yi1))>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

source

GeometryOps.isconcave Method
julia
isconcave(poly::Polygon)::Bool

Take a polygon and return true or false as to whether it is concave or not.

Examples

julia
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

source

GeometryOps.overlaps Method
julia
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

julia
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

source

GeometryOps.overlaps Method
julia
overlaps(::GI.AbstractTrait, geom1, ::GI.AbstractTrait, geom2)::Bool

For any non-specified pair, all have non-matching dimensions, return false.

source

GeometryOps.overlaps Method
julia
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.

source

GeometryOps.overlaps Method
julia
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.

source

GeometryOps.overlaps Method
julia
overlaps(
+    ::GI.MultiPolygonTrait, polys1,
+    ::GI.MultiPolygonTrait, polys2,
+)::Bool

Return true if at least one pair of polygons from multipolygons overlap. Else false.

source

GeometryOps.overlaps Method
julia
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.

source

GeometryOps.overlaps Method
julia
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.

source

GeometryOps.overlaps Method
julia
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.

source

GeometryOps.overlaps Method
julia
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.

source

GeometryOps.polygon_to_line Method
julia
polygon_to_line(poly::Polygon)

Converts a Polygon to LineString or MultiLineString

Examples

julia
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)

source

GeometryOps.polygonize Method
julia
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 Features 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);

source

GeometryOps.segmentize Method
julia
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.

source

GeometryOps.signed_area Method
julia
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.

source

GeometryOps.signed_distance Method
julia
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.

source

GeometryOps.simplify Method
julia
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:

julia
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

source

GeometryOps.t_value Method
julia
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.

t=det(s,s)rr+ss

+
+[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.

source

GeometryOps.touches Method
julia
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

julia
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

source

GeometryOps.transform Method
julia
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)

source

GeometryOps.tuples Method
julia
tuples(obj)

Convert all points in obj to Tuples, 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.

source

GeometryOps.union Method
julia
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

julia
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]]]

source

GeometryOps.weighted_mean Method
julia
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.

source

GeometryOps.within Method
julia
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

julia
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

source


  1. K. Hormann and N. Sukumar. Generalized Barycentric Coordinates in Computer Graphics and Computational Mechanics. Taylor & Fancis, CRC Press, 2017. ↩︎

+ + + + \ No newline at end of file diff --git a/previews/PR239/assets/api.md.BkfD-VZg.js b/previews/PR239/assets/api.md.BkfD-VZg.js new file mode 100644 index 000000000..952041e56 --- /dev/null +++ b/previews/PR239/assets/api.md.BkfD-VZg.js @@ -0,0 +1,381 @@ +import{_ as h,c as l,a5 as a,j as i,a as e,G as n,B as k,o as p}from"./chunks/framework.onQNwZ2I.js";const mi=JSON.parse('{"title":"Full GeometryOps API documentation","description":"","frontmatter":{},"headers":[],"relativePath":"api.md","filePath":"api.md","lastUpdated":null}'),r={name:"api.md"},o={class:"jldocstring custom-block",open:""},d={class:"jldocstring custom-block",open:""},g={class:"jldocstring custom-block",open:""},y={class:"jldocstring custom-block",open:""},E={class:"jldocstring custom-block",open:""},c={class:"jldocstring custom-block",open:""},u={class:"jldocstring custom-block",open:""},m={class:"jldocstring custom-block",open:""},F={class:"jldocstring custom-block",open:""},C={class:"jldocstring custom-block",open:""},b={class:"jldocstring custom-block",open:""},f={class:"jldocstring custom-block",open:""},T={class:"jldocstring custom-block",open:""},G={class:"jldocstring custom-block",open:""},v={class:"jldocstring custom-block",open:""},A={class:"jldocstring custom-block",open:""},j={class:"jldocstring custom-block",open:""},B={class:"jldocstring custom-block",open:""},O={class:"jldocstring custom-block",open:""},D={class:"jldocstring custom-block",open:""},Q={class:"jldocstring custom-block",open:""},x={class:"jldocstring custom-block",open:""},w={class:"jldocstring custom-block",open:""},L={class:"jldocstring custom-block",open:""},I={class:"jldocstring custom-block",open:""},M={class:"jldocstring custom-block",open:""},P={class:"jldocstring custom-block",open:""},q={class:"jldocstring custom-block",open:""},R={class:"jldocstring custom-block",open:""},S={class:"jldocstring custom-block",open:""},V={class:"jldocstring custom-block",open:""},J={class:"jldocstring custom-block",open:""},U={class:"jldocstring custom-block",open:""},H={class:"jldocstring custom-block",open:""},N={class:"jldocstring custom-block",open:""},W={class:"jldocstring custom-block",open:""},z={class:"jldocstring custom-block",open:""},Z={class:"jldocstring custom-block",open:""},_={class:"jldocstring custom-block",open:""},K={class:"jldocstring custom-block",open:""},X={class:"jldocstring custom-block",open:""},$={class:"jldocstring custom-block",open:""},Y={class:"jldocstring custom-block",open:""},ss={class:"jldocstring custom-block",open:""},is={class:"jldocstring custom-block",open:""},as={class:"jldocstring custom-block",open:""},es={class:"jldocstring custom-block",open:""},ts={class:"jldocstring custom-block",open:""},ns={class:"jldocstring custom-block",open:""},ls={class:"jldocstring custom-block",open:""},ps={class:"jldocstring custom-block",open:""},hs={class:"jldocstring custom-block",open:""},ks={class:"jldocstring custom-block",open:""},rs={class:"jldocstring custom-block",open:""},os={class:"jldocstring custom-block",open:""},ds={class:"jldocstring custom-block",open:""},gs={class:"jldocstring custom-block",open:""},ys={class:"jldocstring custom-block",open:""},Es={class:"jldocstring custom-block",open:""},cs={class:"jldocstring custom-block",open:""},us={class:"jldocstring custom-block",open:""},ms={class:"jldocstring custom-block",open:""},Fs={class:"jldocstring custom-block",open:""},Cs={class:"jldocstring custom-block",open:""},bs={class:"jldocstring custom-block",open:""},fs={class:"jldocstring custom-block",open:""},Ts={class:"jldocstring custom-block",open:""},Gs={class:"jldocstring custom-block",open:""},vs={class:"jldocstring custom-block",open:""},As={class:"jldocstring custom-block",open:""},js={class:"jldocstring custom-block",open:""},Bs={class:"jldocstring custom-block",open:""},Os={class:"jldocstring custom-block",open:""},Ds={class:"jldocstring custom-block",open:""},Qs={class:"jldocstring custom-block",open:""},xs={class:"jldocstring custom-block",open:""},ws={class:"jldocstring custom-block",open:""},Ls={class:"jldocstring custom-block",open:""},Is={class:"jldocstring custom-block",open:""},Ms={class:"jldocstring custom-block",open:""},Ps={class:"jldocstring custom-block",open:""},qs={class:"MathJax",jax:"SVG",display:"true",style:{direction:"ltr",display:"block","text-align":"center",margin:"1em 0",position:"relative"}},Rs={style:{overflow:"visible","min-height":"1px","min-width":"1px","vertical-align":"-2.827ex"},xmlns:"http://www.w3.org/2000/svg",width:"33.539ex",height:"6.785ex",role:"img",focusable:"false",viewBox:"0 -1749.5 14824.1 2999","aria-hidden":"true"},Ss={class:"jldocstring custom-block",open:""},Vs={class:"jldocstring custom-block",open:""},Js={class:"jldocstring custom-block",open:""},Us={class:"jldocstring custom-block",open:""},Hs={class:"jldocstring custom-block",open:""},Ns={class:"jldocstring custom-block",open:""},Ws={class:"jldocstring custom-block",open:""},zs={class:"jldocstring custom-block",open:""},Zs={class:"jldocstring custom-block",open:""},_s={class:"jldocstring custom-block",open:""},Ks={class:"jldocstring custom-block",open:""},Xs={class:"jldocstring custom-block",open:""},$s={class:"jldocstring custom-block",open:""},Ys={class:"jldocstring custom-block",open:""},si={class:"jldocstring custom-block",open:""},ii={class:"jldocstring custom-block",open:""},ai={class:"jldocstring custom-block",open:""},ei={class:"MathJax",jax:"SVG",display:"true",style:{direction:"ltr",display:"block","text-align":"center",margin:"1em 0",position:"relative"}},ti={style:{overflow:"visible","min-height":"1px","min-width":"1px","vertical-align":"-2.059ex"},xmlns:"http://www.w3.org/2000/svg",width:"27.746ex",height:"5.362ex",role:"img",focusable:"false",viewBox:"0 -1460 12263.9 2370","aria-hidden":"true"},ni={class:"jldocstring custom-block",open:""},li={class:"jldocstring custom-block",open:""},pi={class:"jldocstring custom-block",open:""},hi={class:"jldocstring custom-block",open:""},ki={class:"jldocstring custom-block",open:""},ri={class:"jldocstring custom-block",open:""};function oi(di,s,gi,yi,Ei,ci){const t=k("Badge");return p(),l("div",null,[s[318]||(s[318]=a('

Full GeometryOps API documentation

Warning

This page is still very much WIP!

Documentation for GeometryOps's full API (only for reference!).

apply and associated functions

',5)),i("details",o,[i("summary",null,[s[0]||(s[0]=i("a",{id:"GeometryOpsCore.apply",href:"#GeometryOpsCore.apply"},[i("span",{class:"jlbinding"},"GeometryOpsCore.apply")],-1)),s[1]||(s[1]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[2]||(s[2]=a(`
julia
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.

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

source

`,10))]),i("details",d,[i("summary",null,[s[3]||(s[3]=i("a",{id:"GeometryOpsCore.applyreduce",href:"#GeometryOpsCore.applyreduce"},[i("span",{class:"jlbinding"},"GeometryOpsCore.applyreduce")],-1)),s[4]||(s[4]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[5]||(s[5]=a('
julia
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.

source

',5))]),i("details",g,[i("summary",null,[s[6]||(s[6]=i("a",{id:"GeometryOps.reproject",href:"#GeometryOps.reproject"},[i("span",{class:"jlbinding"},"GeometryOps.reproject")],-1)),s[7]||(s[7]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[8]||(s[8]=a(`
julia
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

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

source

`,10))]),i("details",y,[i("summary",null,[s[9]||(s[9]=i("a",{id:"GeometryOps.transform",href:"#GeometryOps.transform"},[i("span",{class:"jlbinding"},"GeometryOps.transform")],-1)),s[10]||(s[10]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[11]||(s[11]=a(`
julia
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)

source

`,9))]),s[319]||(s[319]=i("h2",{id:"General-geometry-methods",tabindex:"-1"},[e("General geometry methods "),i("a",{class:"header-anchor",href:"#General-geometry-methods","aria-label":'Permalink to "General geometry methods {#General-geometry-methods}"'},"​")],-1)),s[320]||(s[320]=i("h3",{id:"OGC-methods",tabindex:"-1"},[e("OGC methods "),i("a",{class:"header-anchor",href:"#OGC-methods","aria-label":'Permalink to "OGC methods {#OGC-methods}"'},"​")],-1)),i("details",E,[i("summary",null,[s[12]||(s[12]=i("a",{id:"GeometryOps.contains",href:"#GeometryOps.contains"},[i("span",{class:"jlbinding"},"GeometryOps.contains")],-1)),s[13]||(s[13]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[14]||(s[14]=a(`
julia
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

julia
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

source

`,6))]),i("details",c,[i("summary",null,[s[15]||(s[15]=i("a",{id:"GeometryOps.coveredby",href:"#GeometryOps.coveredby"},[i("span",{class:"jlbinding"},"GeometryOps.coveredby")],-1)),s[16]||(s[16]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[17]||(s[17]=a(`
julia
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

julia
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

source

`,6))]),i("details",u,[i("summary",null,[s[18]||(s[18]=i("a",{id:"GeometryOps.covers",href:"#GeometryOps.covers"},[i("span",{class:"jlbinding"},"GeometryOps.covers")],-1)),s[19]||(s[19]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[20]||(s[20]=a(`
julia
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

julia
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

source

`,6))]),i("details",m,[i("summary",null,[s[21]||(s[21]=i("a",{id:"GeometryOps.crosses",href:"#GeometryOps.crosses"},[i("span",{class:"jlbinding"},"GeometryOps.crosses")],-1)),s[22]||(s[22]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[23]||(s[23]=a(`
julia
 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

source

`,6))]),i("details",F,[i("summary",null,[s[24]||(s[24]=i("a",{id:"GeometryOps.disjoint",href:"#GeometryOps.disjoint"},[i("span",{class:"jlbinding"},"GeometryOps.disjoint")],-1)),s[25]||(s[25]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[26]||(s[26]=a(`
julia
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

julia
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

source

`,6))]),i("details",C,[i("summary",null,[s[27]||(s[27]=i("a",{id:"GeometryOps.intersects",href:"#GeometryOps.intersects"},[i("span",{class:"jlbinding"},"GeometryOps.intersects")],-1)),s[28]||(s[28]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[29]||(s[29]=a(`
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

julia
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

source

`,6))]),i("details",b,[i("summary",null,[s[30]||(s[30]=i("a",{id:"GeometryOps.overlaps",href:"#GeometryOps.overlaps"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[31]||(s[31]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[32]||(s[32]=a(`
julia
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

julia
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

source

julia
overlaps(::GI.AbstractTrait, geom1, ::GI.AbstractTrait, geom2)::Bool

For any non-specified pair, all have non-matching dimensions, return false.

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
overlaps(
+    ::GI.MultiPolygonTrait, polys1,
+    ::GI.MultiPolygonTrait, polys2,
+)::Bool

Return true if at least one pair of polygons from multipolygons overlap. Else false.

source

`,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(`
julia
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

julia
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

source

`,5))]),i("details",T,[i("summary",null,[s[36]||(s[36]=i("a",{id:"GeometryOps.within",href:"#GeometryOps.within"},[i("span",{class:"jlbinding"},"GeometryOps.within")],-1)),s[37]||(s[37]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[38]||(s[38]=a(`
julia
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

julia
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

source

`,6))]),s[321]||(s[321]=i("h3",{id:"Other-general-methods",tabindex:"-1"},[e("Other general methods "),i("a",{class:"header-anchor",href:"#Other-general-methods","aria-label":'Permalink to "Other general methods {#Other-general-methods}"'},"​")],-1)),i("details",G,[i("summary",null,[s[39]||(s[39]=i("a",{id:"GeometryOps.equals",href:"#GeometryOps.equals"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[40]||(s[40]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[41]||(s[41]=a(`
julia
equals(geom1, geom2)::Bool

Compare two Geometries return true if they are the same geometry.

Examples

julia
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

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
equals(::GI.MultiPointTrait, mp1, ::GI.MultiPointTrait, mp2)::Bool

Two multipoints are equal if they share the same set of points.

source

julia
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.

source

julia
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

source

julia
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

source

julia
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.

source

julia
equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool

Two polygons are equal if they share the same exterior edge and holes.

source

julia
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.

source

julia
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.

source

julia
equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool

Two multipolygons are equal if they share the same set of polygons.

source

`,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('
julia
centroid(geom, [T=Float64])::Tuple{T, T}

Returns the centroid of a given line segment, linear ring, polygon, or mutlipolygon.

source

',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('
julia
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.

source

',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('
julia
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.

source

',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(`
julia
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.

source

`,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(`
julia
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.

source

`,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(`
julia
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.

source

`,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('
julia
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

source

',6))]),s[322]||(s[322]=i("h2",{id:"Barycentric-coordinates",tabindex:"-1"},[e("Barycentric coordinates "),i("a",{class:"header-anchor",href:"#Barycentric-coordinates","aria-label":'Permalink to "Barycentric coordinates {#Barycentric-coordinates}"'},"​")],-1)),i("details",x,[i("summary",null,[s[63]||(s[63]=i("a",{id:"GeometryOps.barycentric_coordinates",href:"#GeometryOps.barycentric_coordinates"},[i("span",{class:"jlbinding"},"GeometryOps.barycentric_coordinates")],-1)),s[64]||(s[64]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[65]||(s[65]=a('
julia
barycentric_coordinates(method = MeanValue(), polygon, point)

Returns the barycentric coordinates of point in polygon using the barycentric coordinate method method.

source

',3))]),i("details",w,[i("summary",null,[s[66]||(s[66]=i("a",{id:"GeometryOps.barycentric_coordinates!",href:"#GeometryOps.barycentric_coordinates!"},[i("span",{class:"jlbinding"},"GeometryOps.barycentric_coordinates!")],-1)),s[67]||(s[67]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[68]||(s[68]=a('
julia
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.

source

',5))]),i("details",L,[i("summary",null,[s[69]||(s[69]=i("a",{id:"GeometryOps.barycentric_interpolate",href:"#GeometryOps.barycentric_interpolate"},[i("span",{class:"jlbinding"},"GeometryOps.barycentric_interpolate")],-1)),s[70]||(s[70]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[71]||(s[71]=a('
julia
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).

source

',5))]),s[323]||(s[323]=i("h2",{id:"Other-methods",tabindex:"-1"},[e("Other methods "),i("a",{class:"header-anchor",href:"#Other-methods","aria-label":'Permalink to "Other methods {#Other-methods}"'},"​")],-1)),i("details",I,[i("summary",null,[s[72]||(s[72]=i("a",{id:"GeometryOps.AbstractBarycentricCoordinateMethod",href:"#GeometryOps.AbstractBarycentricCoordinateMethod"},[i("span",{class:"jlbinding"},"GeometryOps.AbstractBarycentricCoordinateMethod")],-1)),s[73]||(s[73]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[74]||(s[74]=a('
julia
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:

The rest of the methods will be implemented in terms of these, and have efficient dispatches for broadcasting.

source

',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('
julia
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.

source

',5))]),i("details",P,[i("summary",null,[s[78]||(s[78]=i("a",{id:"GeometryOps.DiffIntersectingPolygons",href:"#GeometryOps.DiffIntersectingPolygons"},[i("span",{class:"jlbinding"},"GeometryOps.DiffIntersectingPolygons")],-1)),s[79]||(s[79]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[80]||(s[80]=a('
julia
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.

source

',3))]),i("details",q,[i("summary",null,[s[81]||(s[81]=i("a",{id:"GeometryOps.DouglasPeucker",href:"#GeometryOps.DouglasPeucker"},[i("span",{class:"jlbinding"},"GeometryOps.DouglasPeucker")],-1)),s[82]||(s[82]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[83]||(s[83]=a(`
julia
DouglasPeucker <: SimplifyAlg
+
+DouglasPeucker(; number, ratio, tol)

Simplifies geometries by removing points below tol distance from the line between its neighboring points.

Keywords

Note: user input tol is squared to avoid unnecessary computation in algorithm.

source

`,6))]),i("details",R,[i("summary",null,[s[84]||(s[84]=i("a",{id:"GeometryOps.GEOS",href:"#GeometryOps.GEOS"},[i("span",{class:"jlbinding"},"GeometryOps.GEOS")],-1)),s[85]||(s[85]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[86]||(s[86]=a('
julia
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!

source

',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('
julia
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

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.

source

',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('
julia
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.

source

',5))]),i("details",J,[i("summary",null,[s[93]||(s[93]=i("a",{id:"GeometryOps.LineOrientation",href:"#GeometryOps.LineOrientation"},[i("span",{class:"jlbinding"},"GeometryOps.LineOrientation")],-1)),s[94]||(s[94]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[95]||(s[95]=a('
julia
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).

source

',3))]),i("details",U,[i("summary",null,[s[96]||(s[96]=i("a",{id:"GeometryOps.LinearSegments",href:"#GeometryOps.LinearSegments"},[i("span",{class:"jlbinding"},"GeometryOps.LinearSegments")],-1)),s[97]||(s[97]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[98]||(s[98]=a('
julia
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.

source

',4))]),i("details",H,[i("summary",null,[s[99]||(s[99]=i("a",{id:"GeometryOps.MeanValue",href:"#GeometryOps.MeanValue"},[i("span",{class:"jlbinding"},"GeometryOps.MeanValue")],-1)),s[100]||(s[100]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[101]||(s[101]=a('
julia
MeanValue() <: AbstractBarycentricCoordinateMethod

This method calculates barycentric coordinates using the mean value method.

References

source

',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('
julia
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

source

',5))]),i("details",W,[i("summary",null,[s[105]||(s[105]=i("a",{id:"GeometryOps.PointOrientation",href:"#GeometryOps.PointOrientation"},[i("span",{class:"jlbinding"},"GeometryOps.PointOrientation")],-1)),s[106]||(s[106]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[107]||(s[107]=a('
julia
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.

source

',3))]),i("details",z,[i("summary",null,[s[108]||(s[108]=i("a",{id:"GeometryOps.RadialDistance",href:"#GeometryOps.RadialDistance"},[i("span",{class:"jlbinding"},"GeometryOps.RadialDistance")],-1)),s[109]||(s[109]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[110]||(s[110]=a('
julia
RadialDistance <: SimplifyAlg

Simplifies geometries by removing points less than tol distance from the line between its neighboring points.

Keywords

Note: user input tol is squared to avoid unnecessary computation in algorithm.

source

',6))]),i("details",Z,[i("summary",null,[s[111]||(s[111]=i("a",{id:"GeometryOps.SimplifyAlg",href:"#GeometryOps.SimplifyAlg"},[i("span",{class:"jlbinding"},"GeometryOps.SimplifyAlg")],-1)),s[112]||(s[112]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[113]||(s[113]=a('
julia
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.

source

',6))]),i("details",_,[i("summary",null,[s[114]||(s[114]=i("a",{id:"GeometryOps.UnionIntersectingPolygons",href:"#GeometryOps.UnionIntersectingPolygons"},[i("span",{class:"jlbinding"},"GeometryOps.UnionIntersectingPolygons")],-1)),s[115]||(s[115]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[116]||(s[116]=a('
julia
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.

source

',4))]),i("details",K,[i("summary",null,[s[117]||(s[117]=i("a",{id:"GeometryOps.VisvalingamWhyatt",href:"#GeometryOps.VisvalingamWhyatt"},[i("span",{class:"jlbinding"},"GeometryOps.VisvalingamWhyatt")],-1)),s[118]||(s[118]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[119]||(s[119]=a(`
julia
VisvalingamWhyatt <: SimplifyAlg
+
+VisvalingamWhyatt(; kw...)

Simplifies geometries by removing points below tol distance from the line between its neighboring points.

Keywords

Note: user input tol is doubled to avoid unnecessary computation in algorithm.

source

`,6))]),i("details",X,[i("summary",null,[s[120]||(s[120]=i("a",{id:"GeometryOps._det-Union{Tuple{T2}, Tuple{T1}, Tuple{Union{Tuple{T1, T1}, StaticArraysCore.StaticArray{Tuple{2}, T1, 1}}, Union{Tuple{T2, T2}, StaticArraysCore.StaticArray{Tuple{2}, T2, 1}}}} where {T1<:Real, T2<:Real}",href:"#GeometryOps._det-Union{Tuple{T2}, Tuple{T1}, Tuple{Union{Tuple{T1, T1}, StaticArraysCore.StaticArray{Tuple{2}, T1, 1}}, Union{Tuple{T2, T2}, StaticArraysCore.StaticArray{Tuple{2}, T2, 1}}}} where {T1<:Real, T2<:Real}"},[i("span",{class:"jlbinding"},"GeometryOps._det")],-1)),s[121]||(s[121]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[122]||(s[122]=a('
julia
_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]

source

',5))]),i("details",$,[i("summary",null,[s[123]||(s[123]=i("a",{id:"GeometryOps._equals_curves-NTuple{4, Any}",href:"#GeometryOps._equals_curves-NTuple{4, Any}"},[i("span",{class:"jlbinding"},"GeometryOps._equals_curves")],-1)),s[124]||(s[124]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[125]||(s[125]=a('
julia
_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)

source

',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(`
julia
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.

source

`,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(`
julia
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.

source

`,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('
julia
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.

source

',5))]),i("details",as,[i("summary",null,[s[135]||(s[135]=i("a",{id:"GeometryOps.barycentric_coordinates-Tuple{GeometryOps.AbstractBarycentricCoordinateMethod, Any, Any}",href:"#GeometryOps.barycentric_coordinates-Tuple{GeometryOps.AbstractBarycentricCoordinateMethod, Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.barycentric_coordinates")],-1)),s[136]||(s[136]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[137]||(s[137]=a('
julia
barycentric_coordinates(method = MeanValue(), polygon, point)

Returns the barycentric coordinates of point in polygon using the barycentric coordinate method method.

source

',3))]),i("details",es,[i("summary",null,[s[138]||(s[138]=i("a",{id:"GeometryOps.barycentric_interpolate-Union{Tuple{V}, Tuple{GeometryOps.AbstractBarycentricCoordinateMethod, Any, AbstractVector{V}, Any}} where V",href:"#GeometryOps.barycentric_interpolate-Union{Tuple{V}, Tuple{GeometryOps.AbstractBarycentricCoordinateMethod, Any, AbstractVector{V}, Any}} where V"},[i("span",{class:"jlbinding"},"GeometryOps.barycentric_interpolate")],-1)),s[139]||(s[139]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[140]||(s[140]=a('
julia
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).

source

',5))]),i("details",ts,[i("summary",null,[s[141]||(s[141]=i("a",{id:"GeometryOps.centroid-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T",href:"#GeometryOps.centroid-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T"},[i("span",{class:"jlbinding"},"GeometryOps.centroid")],-1)),s[142]||(s[142]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[143]||(s[143]=a('
julia
centroid(geom, [T=Float64])::Tuple{T, T}

Returns the centroid of a given line segment, linear ring, polygon, or mutlipolygon.

source

',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('
julia
centroid_and_area(geom, [T=Float64])::(::Tuple{T, T}, ::Real)

Returns the centroid and area of a given geometry.

source

',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('
julia
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.

source

',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(`
julia
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

julia
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

source

`,6))]),i("details",hs,[i("summary",null,[s[153]||(s[153]=i("a",{id:"GeometryOps.convex_hull",href:"#GeometryOps.convex_hull"},[i("span",{class:"jlbinding"},"GeometryOps.convex_hull")],-1)),s[154]||(s[154]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[155]||(s[155]=a('
julia
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).

source

',5))]),i("details",ks,[i("summary",null,[s[156]||(s[156]=i("a",{id:"GeometryOps.coverage-Union{Tuple{T}, NTuple{5, Any}, Tuple{Any, Any, Any, Any, Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.coverage-Union{Tuple{T}, NTuple{5, Any}, Tuple{Any, Any, Any, Any, Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.coverage")],-1)),s[157]||(s[157]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[158]||(s[158]=a('
julia
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:

Result will be of type T, where T is an optional argument with a default value of Float64.

source

',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(`
julia
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

julia
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

source

`,6))]),i("details",os,[i("summary",null,[s[162]||(s[162]=i("a",{id:"GeometryOps.covers-Tuple{Any, Any}",href:"#GeometryOps.covers-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.covers")],-1)),s[163]||(s[163]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[164]||(s[164]=a(`
julia
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

julia
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

source

`,6))]),i("details",ds,[i("summary",null,[s[165]||(s[165]=i("a",{id:"GeometryOps.crosses-Tuple{Any, Any}",href:"#GeometryOps.crosses-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.crosses")],-1)),s[166]||(s[166]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[167]||(s[167]=a(`
julia
 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

source

`,6))]),i("details",gs,[i("summary",null,[s[168]||(s[168]=i("a",{id:"GeometryOps.cut-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.cut-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.cut")],-1)),s[169]||(s[169]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[170]||(s[170]=a(`
julia
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

julia
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]]]

source

`,6))]),i("details",ys,[i("summary",null,[s[171]||(s[171]=i("a",{id:"GeometryOps.difference-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.difference-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.difference")],-1)),s[172]||(s[172]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[173]||(s[173]=a(`
julia
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

julia
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]]]

source

`,5))]),i("details",Es,[i("summary",null,[s[174]||(s[174]=i("a",{id:"GeometryOps.disjoint-Tuple{Any, Any}",href:"#GeometryOps.disjoint-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.disjoint")],-1)),s[175]||(s[175]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[176]||(s[176]=a(`
julia
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

julia
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

source

`,6))]),i("details",cs,[i("summary",null,[s[177]||(s[177]=i("a",{id:"GeometryOps.distance-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.distance-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.distance")],-1)),s[178]||(s[178]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[179]||(s[179]=a('
julia
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.

source

',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('
julia
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

source

',6))]),i("details",ms,[i("summary",null,[s[183]||(s[183]=i("a",{id:"GeometryOps.enforce-Tuple{GEOS, Symbol, Any}",href:"#GeometryOps.enforce-Tuple{GEOS, Symbol, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.enforce")],-1)),s[184]||(s[184]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[185]||(s[185]=a('
julia
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).

source

',4))]),i("details",Fs,[i("summary",null,[s[186]||(s[186]=i("a",{id:"GeometryOps.equals-NTuple{4, Any}",href:"#GeometryOps.equals-NTuple{4, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[187]||(s[187]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[188]||(s[188]=a('
julia
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.

source

',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(`
julia
equals(geom1, geom2)::Bool

Compare two Geometries return true if they are the same geometry.

Examples

julia
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

source

`,5))]),i("details",bs,[i("summary",null,[s[192]||(s[192]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.LinearRingTrait, Any, GeoInterface.LinearRingTrait, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.LinearRingTrait, Any, GeoInterface.LinearRingTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[193]||(s[193]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[194]||(s[194]=a(`
julia
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.

source

`,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(`
julia
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

source

`,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('
julia
equals(::GI.MultiPointTrait, mp1, ::GI.MultiPointTrait, mp2)::Bool

Two multipoints are equal if they share the same set of points.

source

',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('
julia
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.

source

',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('
julia
equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool

Two multipolygons are equal if they share the same set of polygons.

source

',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('
julia
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.

source

',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('
julia
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.

source

',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('
julia
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.

source

',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('
julia
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.

source

',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('
julia
equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool

Two polygons are equal if they share the same exterior edge and holes.

source

',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(`
julia
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

source

`,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(`
julia
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.

source

`,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('
julia
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.

source

',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('
julia
flip(obj)

Swap all of the x and y coordinates in obj, otherwise keeping the original structure (but not necessarily the original type).

Keywords

source

',5))]),i("details",Is,[i("summary",null,[s[234]||(s[234]=i("a",{id:"GeometryOps.intersection-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.intersection-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.intersection")],-1)),s[235]||(s[235]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[236]||(s[236]=a(`
julia
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

julia
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]

source

`,5))]),i("details",Ms,[i("summary",null,[s[237]||(s[237]=i("a",{id:"GeometryOps.intersection_points-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.intersection_points-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.intersection_points")],-1)),s[238]||(s[238]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[239]||(s[239]=a(`
julia
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

+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

julia
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

source

`,9))]),i("details",Ps,[i("summary",null,[s[240]||(s[240]=i("a",{id:"GeometryOps.isclockwise-Tuple{Any}",href:"#GeometryOps.isclockwise-Tuple{Any}"},[i("span",{class:"jlbinding"},"GeometryOps.isclockwise")],-1)),s[241]||(s[241]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[244]||(s[244]=a('
julia
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,

',2)),i("mjx-container",qs,[(p(),l("svg",Rs,s[242]||(s[242]=[a('',1)]))),s[243]||(s[243]=i("mjx-assistive-mml",{unselectable:"on",display:"block",style:{top:"0px",left:"0px",clip:"rect(1px, 1px, 1px, 1px)","-webkit-touch-callout":"none","-webkit-user-select":"none","-khtml-user-select":"none","-moz-user-select":"none","-ms-user-select":"none","user-select":"none",position:"absolute",padding:"1px 0px 0px 0px",border:"0px",display:"block",overflow:"hidden",width:"100%"}},[i("math",{xmlns:"http://www.w3.org/1998/Math/MathML",display:"block"},[i("mrow",{"data-mjx-texclass":"INNER"},[i("mo",{"data-mjx-texclass":"OPEN"},"("),i("munderover",null,[i("mo",{"data-mjx-texclass":"OP"},"∑"),i("mrow",{"data-mjx-texclass":"ORD"},[i("mi",null,"i"),i("mo",null,"="),i("mn",null,"2")]),i("mi",null,"n")]),i("mo",{stretchy:"false"},"("),i("msub",null,[i("mi",null,"x"),i("mi",null,"i")]),i("mo",null,"−"),i("msub",null,[i("mi",null,"x"),i("mrow",{"data-mjx-texclass":"ORD"},[i("mi",null,"i"),i("mo",null,"−"),i("mn",null,"1")])]),i("mo",{stretchy:"false"},")"),i("mo",null,"⋅"),i("mo",{stretchy:"false"},"("),i("msub",null,[i("mi",null,"y"),i("mi",null,"i")]),i("mo",null,"+"),i("msub",null,[i("mi",null,"y"),i("mrow",{"data-mjx-texclass":"ORD"},[i("mi",null,"i"),i("mo",null,"−"),i("mn",null,"1")])]),i("mo",{stretchy:"false"},")"),i("mo",{"data-mjx-texclass":"CLOSE"},")")]),i("mo",null,">"),i("mn",null,"0")])],-1))]),s[245]||(s[245]=a(`

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

source

`,3))]),i("details",Ss,[i("summary",null,[s[246]||(s[246]=i("a",{id:"GeometryOps.isconcave-Tuple{Any}",href:"#GeometryOps.isconcave-Tuple{Any}"},[i("span",{class:"jlbinding"},"GeometryOps.isconcave")],-1)),s[247]||(s[247]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[248]||(s[248]=a(`
julia
isconcave(poly::Polygon)::Bool

Take a polygon and return true or false as to whether it is concave or not.

Examples

julia
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

source

`,5))]),i("details",Vs,[i("summary",null,[s[249]||(s[249]=i("a",{id:"GeometryOps.overlaps-Tuple{Any, Any}",href:"#GeometryOps.overlaps-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[250]||(s[250]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[251]||(s[251]=a(`
julia
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

julia
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

source

`,5))]),i("details",Js,[i("summary",null,[s[252]||(s[252]=i("a",{id:"GeometryOps.overlaps-Tuple{GeoInterface.AbstractTrait, Any, GeoInterface.AbstractTrait, Any}",href:"#GeometryOps.overlaps-Tuple{GeoInterface.AbstractTrait, Any, GeoInterface.AbstractTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[253]||(s[253]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[254]||(s[254]=a('
julia
overlaps(::GI.AbstractTrait, geom1, ::GI.AbstractTrait, geom2)::Bool

For any non-specified pair, all have non-matching dimensions, return false.

source

',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('
julia
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.

source

',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(`
julia
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.

source

`,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(`
julia
overlaps(
+    ::GI.MultiPolygonTrait, polys1,
+    ::GI.MultiPolygonTrait, polys2,
+)::Bool

Return true if at least one pair of polygons from multipolygons overlap. Else false.

source

`,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(`
julia
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.

source

`,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(`
julia
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.

source

`,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(`
julia
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.

source

`,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(`
julia
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.

source

`,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(`
julia
polygon_to_line(poly::Polygon)

Converts a Polygon to LineString or MultiLineString

Examples

julia
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)

source

`,5))]),i("details",Xs,[i("summary",null,[s[279]||(s[279]=i("a",{id:"GeometryOps.polygonize-Tuple{AbstractMatrix{Bool}}",href:"#GeometryOps.polygonize-Tuple{AbstractMatrix{Bool}}"},[i("span",{class:"jlbinding"},"GeometryOps.polygonize")],-1)),s[280]||(s[280]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[281]||(s[281]=a(`
julia
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 Features 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

Example

julia
using GeometryOps
+A = rand(100, 100)
+multipolygon = polygonize(>(0.5), A);

source

`,10))]),i("details",$s,[i("summary",null,[s[282]||(s[282]=i("a",{id:"GeometryOps.segmentize-Tuple{Any}",href:"#GeometryOps.segmentize-Tuple{Any}"},[i("span",{class:"jlbinding"},"GeometryOps.segmentize")],-1)),s[283]||(s[283]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[284]||(s[284]=a('
julia
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

Returns a geometry of similar type to the input geometry, but resampled.

source

',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(`
julia
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.

source

`,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('
julia
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.

source

',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(`
julia
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

Keywords for DouglasPeucker are allowed when no algorithm is specified:

Keywords

Example

Simplify a polygon to have six points:

julia
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

source

`,14))]),i("details",ai,[i("summary",null,[s[294]||(s[294]=i("a",{id:"GeometryOps.t_value-Union{Tuple{T2}, Tuple{T1}, Tuple{N}, Tuple{Union{NTuple{N, T1}, StaticArraysCore.StaticArray{Tuple{N}, T1, 1}}, Union{NTuple{N, T1}, StaticArraysCore.StaticArray{Tuple{N}, T1, 1}}, T2, T2}} where {N, T1<:Real, T2<:Real}",href:"#GeometryOps.t_value-Union{Tuple{T2}, Tuple{T1}, Tuple{N}, Tuple{Union{NTuple{N, T1}, StaticArraysCore.StaticArray{Tuple{N}, T1, 1}}, Union{NTuple{N, T1}, StaticArraysCore.StaticArray{Tuple{N}, T1, 1}}, T2, T2}} where {N, T1<:Real, T2<:Real}"},[i("span",{class:"jlbinding"},"GeometryOps.t_value")],-1)),s[295]||(s[295]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[298]||(s[298]=a('
julia
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.

',3)),i("mjx-container",ei,[(p(),l("svg",ti,s[296]||(s[296]=[a('',1)]))),s[297]||(s[297]=i("mjx-assistive-mml",{unselectable:"on",display:"block",style:{top:"0px",left:"0px",clip:"rect(1px, 1px, 1px, 1px)","-webkit-touch-callout":"none","-webkit-user-select":"none","-khtml-user-select":"none","-moz-user-select":"none","-ms-user-select":"none","user-select":"none",position:"absolute",padding:"1px 0px 0px 0px",border:"0px",display:"block",overflow:"hidden",width:"100%"}},[i("math",{xmlns:"http://www.w3.org/1998/Math/MathML",display:"block"},[i("mi",null,"t"),i("mi",null,"ᵢ"),i("mo",null,"="),i("mfrac",null,[i("mrow",null,[i("mrow",{"data-mjx-texclass":"ORD"},[i("mi",{"data-mjx-auto-op":"false"},"det")]),i("mrow",{"data-mjx-texclass":"INNER"},[i("mo",{"data-mjx-texclass":"OPEN"},"("),i("mi",null,"s"),i("mi",null,"ᵢ"),i("mo",null,","),i("mi",null,"s"),i("mi",null,"ᵢ"),i("mrow",{"data-mjx-texclass":"ORD"},[i("mo",{"data-mjx-pseudoscript":"true"},"₊")]),i("mrow",{"data-mjx-texclass":"ORD"},[i("mo",{"data-mjx-pseudoscript":"true"},"₁")]),i("mo",{"data-mjx-texclass":"CLOSE"},")")])]),i("mrow",null,[i("mi",null,"r"),i("mi",null,"ᵢ"),i("mo",null,"∗"),i("mi",null,"r"),i("mi",null,"ᵢ"),i("mrow",{"data-mjx-texclass":"ORD"},[i("mo",{"data-mjx-pseudoscript":"true"},"₊")]),i("mrow",{"data-mjx-texclass":"ORD"},[i("mo",{"data-mjx-pseudoscript":"true"},"₁")]),i("mo",null,"+"),i("mi",null,"s"),i("mi",null,"ᵢ"),i("mo",null,"⋅"),i("mi",null,"s"),i("mi",null,"ᵢ"),i("mrow",{"data-mjx-texclass":"ORD"},[i("mo",{"data-mjx-pseudoscript":"true"},"₊")]),i("mrow",{"data-mjx-texclass":"ORD"},[i("mo",{"data-mjx-pseudoscript":"true"},"₁")])])])])],-1))]),s[299]||(s[299]=a(`

+
+[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.

source

`,3))]),i("details",ni,[i("summary",null,[s[300]||(s[300]=i("a",{id:"GeometryOps.touches-Tuple{Any, Any}",href:"#GeometryOps.touches-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.touches")],-1)),s[301]||(s[301]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[302]||(s[302]=a(`
julia
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

julia
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

source

`,5))]),i("details",li,[i("summary",null,[s[303]||(s[303]=i("a",{id:"GeometryOps.transform-Tuple{Any, Any}",href:"#GeometryOps.transform-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.transform")],-1)),s[304]||(s[304]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[305]||(s[305]=a(`
julia
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)

source

`,9))]),i("details",pi,[i("summary",null,[s[306]||(s[306]=i("a",{id:"GeometryOps.tuples-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T",href:"#GeometryOps.tuples-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T"},[i("span",{class:"jlbinding"},"GeometryOps.tuples")],-1)),s[307]||(s[307]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[308]||(s[308]=a('
julia
tuples(obj)

Convert all points in obj to Tuples, wherever the are nested.

Returns a similar object or collection of objects using GeoInterface.jl geometries wrapping Tuple points.

Keywords

source

',6))]),i("details",hi,[i("summary",null,[s[309]||(s[309]=i("a",{id:"GeometryOps.union-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.union-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.union")],-1)),s[310]||(s[310]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[311]||(s[311]=a(`
julia
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

julia
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]]]

source

`,6))]),i("details",ki,[i("summary",null,[s[312]||(s[312]=i("a",{id:"GeometryOps.weighted_mean-Union{Tuple{WT}, Tuple{WT, Any, Any}} where WT<:Real",href:"#GeometryOps.weighted_mean-Union{Tuple{WT}, Tuple{WT, Any, Any}} where WT<:Real"},[i("span",{class:"jlbinding"},"GeometryOps.weighted_mean")],-1)),s[313]||(s[313]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[314]||(s[314]=a('
julia
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.

source

',5))]),i("details",ri,[i("summary",null,[s[315]||(s[315]=i("a",{id:"GeometryOps.within-Tuple{Any, Any}",href:"#GeometryOps.within-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.within")],-1)),s[316]||(s[316]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[317]||(s[317]=a(`
julia
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

julia
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

source

`,6))]),s[324]||(s[324]=a('
  1. K. Hormann and N. Sukumar. Generalized Barycentric Coordinates in Computer Graphics and Computational Mechanics. Taylor & Fancis, CRC Press, 2017. ↩︎

',2))])}const Fi=h(r,[["render",oi]]);export{mi as __pageData,Fi as default}; diff --git a/previews/PR239/assets/api.md.BkfD-VZg.lean.js b/previews/PR239/assets/api.md.BkfD-VZg.lean.js new file mode 100644 index 000000000..952041e56 --- /dev/null +++ b/previews/PR239/assets/api.md.BkfD-VZg.lean.js @@ -0,0 +1,381 @@ +import{_ as h,c as l,a5 as a,j as i,a as e,G as n,B as k,o as p}from"./chunks/framework.onQNwZ2I.js";const mi=JSON.parse('{"title":"Full GeometryOps API documentation","description":"","frontmatter":{},"headers":[],"relativePath":"api.md","filePath":"api.md","lastUpdated":null}'),r={name:"api.md"},o={class:"jldocstring custom-block",open:""},d={class:"jldocstring custom-block",open:""},g={class:"jldocstring custom-block",open:""},y={class:"jldocstring custom-block",open:""},E={class:"jldocstring custom-block",open:""},c={class:"jldocstring custom-block",open:""},u={class:"jldocstring custom-block",open:""},m={class:"jldocstring custom-block",open:""},F={class:"jldocstring custom-block",open:""},C={class:"jldocstring custom-block",open:""},b={class:"jldocstring custom-block",open:""},f={class:"jldocstring custom-block",open:""},T={class:"jldocstring custom-block",open:""},G={class:"jldocstring custom-block",open:""},v={class:"jldocstring custom-block",open:""},A={class:"jldocstring custom-block",open:""},j={class:"jldocstring custom-block",open:""},B={class:"jldocstring custom-block",open:""},O={class:"jldocstring custom-block",open:""},D={class:"jldocstring custom-block",open:""},Q={class:"jldocstring custom-block",open:""},x={class:"jldocstring custom-block",open:""},w={class:"jldocstring custom-block",open:""},L={class:"jldocstring custom-block",open:""},I={class:"jldocstring custom-block",open:""},M={class:"jldocstring custom-block",open:""},P={class:"jldocstring custom-block",open:""},q={class:"jldocstring custom-block",open:""},R={class:"jldocstring custom-block",open:""},S={class:"jldocstring custom-block",open:""},V={class:"jldocstring custom-block",open:""},J={class:"jldocstring custom-block",open:""},U={class:"jldocstring custom-block",open:""},H={class:"jldocstring custom-block",open:""},N={class:"jldocstring custom-block",open:""},W={class:"jldocstring custom-block",open:""},z={class:"jldocstring custom-block",open:""},Z={class:"jldocstring custom-block",open:""},_={class:"jldocstring custom-block",open:""},K={class:"jldocstring custom-block",open:""},X={class:"jldocstring custom-block",open:""},$={class:"jldocstring custom-block",open:""},Y={class:"jldocstring custom-block",open:""},ss={class:"jldocstring custom-block",open:""},is={class:"jldocstring custom-block",open:""},as={class:"jldocstring custom-block",open:""},es={class:"jldocstring custom-block",open:""},ts={class:"jldocstring custom-block",open:""},ns={class:"jldocstring custom-block",open:""},ls={class:"jldocstring custom-block",open:""},ps={class:"jldocstring custom-block",open:""},hs={class:"jldocstring custom-block",open:""},ks={class:"jldocstring custom-block",open:""},rs={class:"jldocstring custom-block",open:""},os={class:"jldocstring custom-block",open:""},ds={class:"jldocstring custom-block",open:""},gs={class:"jldocstring custom-block",open:""},ys={class:"jldocstring custom-block",open:""},Es={class:"jldocstring custom-block",open:""},cs={class:"jldocstring custom-block",open:""},us={class:"jldocstring custom-block",open:""},ms={class:"jldocstring custom-block",open:""},Fs={class:"jldocstring custom-block",open:""},Cs={class:"jldocstring custom-block",open:""},bs={class:"jldocstring custom-block",open:""},fs={class:"jldocstring custom-block",open:""},Ts={class:"jldocstring custom-block",open:""},Gs={class:"jldocstring custom-block",open:""},vs={class:"jldocstring custom-block",open:""},As={class:"jldocstring custom-block",open:""},js={class:"jldocstring custom-block",open:""},Bs={class:"jldocstring custom-block",open:""},Os={class:"jldocstring custom-block",open:""},Ds={class:"jldocstring custom-block",open:""},Qs={class:"jldocstring custom-block",open:""},xs={class:"jldocstring custom-block",open:""},ws={class:"jldocstring custom-block",open:""},Ls={class:"jldocstring custom-block",open:""},Is={class:"jldocstring custom-block",open:""},Ms={class:"jldocstring custom-block",open:""},Ps={class:"jldocstring custom-block",open:""},qs={class:"MathJax",jax:"SVG",display:"true",style:{direction:"ltr",display:"block","text-align":"center",margin:"1em 0",position:"relative"}},Rs={style:{overflow:"visible","min-height":"1px","min-width":"1px","vertical-align":"-2.827ex"},xmlns:"http://www.w3.org/2000/svg",width:"33.539ex",height:"6.785ex",role:"img",focusable:"false",viewBox:"0 -1749.5 14824.1 2999","aria-hidden":"true"},Ss={class:"jldocstring custom-block",open:""},Vs={class:"jldocstring custom-block",open:""},Js={class:"jldocstring custom-block",open:""},Us={class:"jldocstring custom-block",open:""},Hs={class:"jldocstring custom-block",open:""},Ns={class:"jldocstring custom-block",open:""},Ws={class:"jldocstring custom-block",open:""},zs={class:"jldocstring custom-block",open:""},Zs={class:"jldocstring custom-block",open:""},_s={class:"jldocstring custom-block",open:""},Ks={class:"jldocstring custom-block",open:""},Xs={class:"jldocstring custom-block",open:""},$s={class:"jldocstring custom-block",open:""},Ys={class:"jldocstring custom-block",open:""},si={class:"jldocstring custom-block",open:""},ii={class:"jldocstring custom-block",open:""},ai={class:"jldocstring custom-block",open:""},ei={class:"MathJax",jax:"SVG",display:"true",style:{direction:"ltr",display:"block","text-align":"center",margin:"1em 0",position:"relative"}},ti={style:{overflow:"visible","min-height":"1px","min-width":"1px","vertical-align":"-2.059ex"},xmlns:"http://www.w3.org/2000/svg",width:"27.746ex",height:"5.362ex",role:"img",focusable:"false",viewBox:"0 -1460 12263.9 2370","aria-hidden":"true"},ni={class:"jldocstring custom-block",open:""},li={class:"jldocstring custom-block",open:""},pi={class:"jldocstring custom-block",open:""},hi={class:"jldocstring custom-block",open:""},ki={class:"jldocstring custom-block",open:""},ri={class:"jldocstring custom-block",open:""};function oi(di,s,gi,yi,Ei,ci){const t=k("Badge");return p(),l("div",null,[s[318]||(s[318]=a('

Full GeometryOps API documentation

Warning

This page is still very much WIP!

Documentation for GeometryOps's full API (only for reference!).

apply and associated functions

',5)),i("details",o,[i("summary",null,[s[0]||(s[0]=i("a",{id:"GeometryOpsCore.apply",href:"#GeometryOpsCore.apply"},[i("span",{class:"jlbinding"},"GeometryOpsCore.apply")],-1)),s[1]||(s[1]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[2]||(s[2]=a(`
julia
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.

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

source

`,10))]),i("details",d,[i("summary",null,[s[3]||(s[3]=i("a",{id:"GeometryOpsCore.applyreduce",href:"#GeometryOpsCore.applyreduce"},[i("span",{class:"jlbinding"},"GeometryOpsCore.applyreduce")],-1)),s[4]||(s[4]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[5]||(s[5]=a('
julia
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.

source

',5))]),i("details",g,[i("summary",null,[s[6]||(s[6]=i("a",{id:"GeometryOps.reproject",href:"#GeometryOps.reproject"},[i("span",{class:"jlbinding"},"GeometryOps.reproject")],-1)),s[7]||(s[7]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[8]||(s[8]=a(`
julia
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

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

source

`,10))]),i("details",y,[i("summary",null,[s[9]||(s[9]=i("a",{id:"GeometryOps.transform",href:"#GeometryOps.transform"},[i("span",{class:"jlbinding"},"GeometryOps.transform")],-1)),s[10]||(s[10]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[11]||(s[11]=a(`
julia
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)

source

`,9))]),s[319]||(s[319]=i("h2",{id:"General-geometry-methods",tabindex:"-1"},[e("General geometry methods "),i("a",{class:"header-anchor",href:"#General-geometry-methods","aria-label":'Permalink to "General geometry methods {#General-geometry-methods}"'},"​")],-1)),s[320]||(s[320]=i("h3",{id:"OGC-methods",tabindex:"-1"},[e("OGC methods "),i("a",{class:"header-anchor",href:"#OGC-methods","aria-label":'Permalink to "OGC methods {#OGC-methods}"'},"​")],-1)),i("details",E,[i("summary",null,[s[12]||(s[12]=i("a",{id:"GeometryOps.contains",href:"#GeometryOps.contains"},[i("span",{class:"jlbinding"},"GeometryOps.contains")],-1)),s[13]||(s[13]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[14]||(s[14]=a(`
julia
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

julia
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

source

`,6))]),i("details",c,[i("summary",null,[s[15]||(s[15]=i("a",{id:"GeometryOps.coveredby",href:"#GeometryOps.coveredby"},[i("span",{class:"jlbinding"},"GeometryOps.coveredby")],-1)),s[16]||(s[16]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[17]||(s[17]=a(`
julia
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

julia
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

source

`,6))]),i("details",u,[i("summary",null,[s[18]||(s[18]=i("a",{id:"GeometryOps.covers",href:"#GeometryOps.covers"},[i("span",{class:"jlbinding"},"GeometryOps.covers")],-1)),s[19]||(s[19]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[20]||(s[20]=a(`
julia
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

julia
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

source

`,6))]),i("details",m,[i("summary",null,[s[21]||(s[21]=i("a",{id:"GeometryOps.crosses",href:"#GeometryOps.crosses"},[i("span",{class:"jlbinding"},"GeometryOps.crosses")],-1)),s[22]||(s[22]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[23]||(s[23]=a(`
julia
 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

source

`,6))]),i("details",F,[i("summary",null,[s[24]||(s[24]=i("a",{id:"GeometryOps.disjoint",href:"#GeometryOps.disjoint"},[i("span",{class:"jlbinding"},"GeometryOps.disjoint")],-1)),s[25]||(s[25]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[26]||(s[26]=a(`
julia
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

julia
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

source

`,6))]),i("details",C,[i("summary",null,[s[27]||(s[27]=i("a",{id:"GeometryOps.intersects",href:"#GeometryOps.intersects"},[i("span",{class:"jlbinding"},"GeometryOps.intersects")],-1)),s[28]||(s[28]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[29]||(s[29]=a(`
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

julia
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

source

`,6))]),i("details",b,[i("summary",null,[s[30]||(s[30]=i("a",{id:"GeometryOps.overlaps",href:"#GeometryOps.overlaps"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[31]||(s[31]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[32]||(s[32]=a(`
julia
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

julia
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

source

julia
overlaps(::GI.AbstractTrait, geom1, ::GI.AbstractTrait, geom2)::Bool

For any non-specified pair, all have non-matching dimensions, return false.

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
overlaps(
+    ::GI.MultiPolygonTrait, polys1,
+    ::GI.MultiPolygonTrait, polys2,
+)::Bool

Return true if at least one pair of polygons from multipolygons overlap. Else false.

source

`,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(`
julia
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

julia
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

source

`,5))]),i("details",T,[i("summary",null,[s[36]||(s[36]=i("a",{id:"GeometryOps.within",href:"#GeometryOps.within"},[i("span",{class:"jlbinding"},"GeometryOps.within")],-1)),s[37]||(s[37]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[38]||(s[38]=a(`
julia
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

julia
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

source

`,6))]),s[321]||(s[321]=i("h3",{id:"Other-general-methods",tabindex:"-1"},[e("Other general methods "),i("a",{class:"header-anchor",href:"#Other-general-methods","aria-label":'Permalink to "Other general methods {#Other-general-methods}"'},"​")],-1)),i("details",G,[i("summary",null,[s[39]||(s[39]=i("a",{id:"GeometryOps.equals",href:"#GeometryOps.equals"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[40]||(s[40]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[41]||(s[41]=a(`
julia
equals(geom1, geom2)::Bool

Compare two Geometries return true if they are the same geometry.

Examples

julia
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

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
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.

source

julia
equals(::GI.MultiPointTrait, mp1, ::GI.MultiPointTrait, mp2)::Bool

Two multipoints are equal if they share the same set of points.

source

julia
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.

source

julia
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

source

julia
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

source

julia
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.

source

julia
equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool

Two polygons are equal if they share the same exterior edge and holes.

source

julia
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.

source

julia
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.

source

julia
equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool

Two multipolygons are equal if they share the same set of polygons.

source

`,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('
julia
centroid(geom, [T=Float64])::Tuple{T, T}

Returns the centroid of a given line segment, linear ring, polygon, or mutlipolygon.

source

',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('
julia
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.

source

',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('
julia
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.

source

',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(`
julia
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.

source

`,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(`
julia
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.

source

`,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(`
julia
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.

source

`,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('
julia
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

source

',6))]),s[322]||(s[322]=i("h2",{id:"Barycentric-coordinates",tabindex:"-1"},[e("Barycentric coordinates "),i("a",{class:"header-anchor",href:"#Barycentric-coordinates","aria-label":'Permalink to "Barycentric coordinates {#Barycentric-coordinates}"'},"​")],-1)),i("details",x,[i("summary",null,[s[63]||(s[63]=i("a",{id:"GeometryOps.barycentric_coordinates",href:"#GeometryOps.barycentric_coordinates"},[i("span",{class:"jlbinding"},"GeometryOps.barycentric_coordinates")],-1)),s[64]||(s[64]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[65]||(s[65]=a('
julia
barycentric_coordinates(method = MeanValue(), polygon, point)

Returns the barycentric coordinates of point in polygon using the barycentric coordinate method method.

source

',3))]),i("details",w,[i("summary",null,[s[66]||(s[66]=i("a",{id:"GeometryOps.barycentric_coordinates!",href:"#GeometryOps.barycentric_coordinates!"},[i("span",{class:"jlbinding"},"GeometryOps.barycentric_coordinates!")],-1)),s[67]||(s[67]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[68]||(s[68]=a('
julia
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.

source

',5))]),i("details",L,[i("summary",null,[s[69]||(s[69]=i("a",{id:"GeometryOps.barycentric_interpolate",href:"#GeometryOps.barycentric_interpolate"},[i("span",{class:"jlbinding"},"GeometryOps.barycentric_interpolate")],-1)),s[70]||(s[70]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[71]||(s[71]=a('
julia
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).

source

',5))]),s[323]||(s[323]=i("h2",{id:"Other-methods",tabindex:"-1"},[e("Other methods "),i("a",{class:"header-anchor",href:"#Other-methods","aria-label":'Permalink to "Other methods {#Other-methods}"'},"​")],-1)),i("details",I,[i("summary",null,[s[72]||(s[72]=i("a",{id:"GeometryOps.AbstractBarycentricCoordinateMethod",href:"#GeometryOps.AbstractBarycentricCoordinateMethod"},[i("span",{class:"jlbinding"},"GeometryOps.AbstractBarycentricCoordinateMethod")],-1)),s[73]||(s[73]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[74]||(s[74]=a('
julia
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:

The rest of the methods will be implemented in terms of these, and have efficient dispatches for broadcasting.

source

',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('
julia
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.

source

',5))]),i("details",P,[i("summary",null,[s[78]||(s[78]=i("a",{id:"GeometryOps.DiffIntersectingPolygons",href:"#GeometryOps.DiffIntersectingPolygons"},[i("span",{class:"jlbinding"},"GeometryOps.DiffIntersectingPolygons")],-1)),s[79]||(s[79]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[80]||(s[80]=a('
julia
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.

source

',3))]),i("details",q,[i("summary",null,[s[81]||(s[81]=i("a",{id:"GeometryOps.DouglasPeucker",href:"#GeometryOps.DouglasPeucker"},[i("span",{class:"jlbinding"},"GeometryOps.DouglasPeucker")],-1)),s[82]||(s[82]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[83]||(s[83]=a(`
julia
DouglasPeucker <: SimplifyAlg
+
+DouglasPeucker(; number, ratio, tol)

Simplifies geometries by removing points below tol distance from the line between its neighboring points.

Keywords

Note: user input tol is squared to avoid unnecessary computation in algorithm.

source

`,6))]),i("details",R,[i("summary",null,[s[84]||(s[84]=i("a",{id:"GeometryOps.GEOS",href:"#GeometryOps.GEOS"},[i("span",{class:"jlbinding"},"GeometryOps.GEOS")],-1)),s[85]||(s[85]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[86]||(s[86]=a('
julia
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!

source

',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('
julia
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

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.

source

',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('
julia
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.

source

',5))]),i("details",J,[i("summary",null,[s[93]||(s[93]=i("a",{id:"GeometryOps.LineOrientation",href:"#GeometryOps.LineOrientation"},[i("span",{class:"jlbinding"},"GeometryOps.LineOrientation")],-1)),s[94]||(s[94]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[95]||(s[95]=a('
julia
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).

source

',3))]),i("details",U,[i("summary",null,[s[96]||(s[96]=i("a",{id:"GeometryOps.LinearSegments",href:"#GeometryOps.LinearSegments"},[i("span",{class:"jlbinding"},"GeometryOps.LinearSegments")],-1)),s[97]||(s[97]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[98]||(s[98]=a('
julia
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.

source

',4))]),i("details",H,[i("summary",null,[s[99]||(s[99]=i("a",{id:"GeometryOps.MeanValue",href:"#GeometryOps.MeanValue"},[i("span",{class:"jlbinding"},"GeometryOps.MeanValue")],-1)),s[100]||(s[100]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[101]||(s[101]=a('
julia
MeanValue() <: AbstractBarycentricCoordinateMethod

This method calculates barycentric coordinates using the mean value method.

References

source

',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('
julia
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

source

',5))]),i("details",W,[i("summary",null,[s[105]||(s[105]=i("a",{id:"GeometryOps.PointOrientation",href:"#GeometryOps.PointOrientation"},[i("span",{class:"jlbinding"},"GeometryOps.PointOrientation")],-1)),s[106]||(s[106]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[107]||(s[107]=a('
julia
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.

source

',3))]),i("details",z,[i("summary",null,[s[108]||(s[108]=i("a",{id:"GeometryOps.RadialDistance",href:"#GeometryOps.RadialDistance"},[i("span",{class:"jlbinding"},"GeometryOps.RadialDistance")],-1)),s[109]||(s[109]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[110]||(s[110]=a('
julia
RadialDistance <: SimplifyAlg

Simplifies geometries by removing points less than tol distance from the line between its neighboring points.

Keywords

Note: user input tol is squared to avoid unnecessary computation in algorithm.

source

',6))]),i("details",Z,[i("summary",null,[s[111]||(s[111]=i("a",{id:"GeometryOps.SimplifyAlg",href:"#GeometryOps.SimplifyAlg"},[i("span",{class:"jlbinding"},"GeometryOps.SimplifyAlg")],-1)),s[112]||(s[112]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[113]||(s[113]=a('
julia
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.

source

',6))]),i("details",_,[i("summary",null,[s[114]||(s[114]=i("a",{id:"GeometryOps.UnionIntersectingPolygons",href:"#GeometryOps.UnionIntersectingPolygons"},[i("span",{class:"jlbinding"},"GeometryOps.UnionIntersectingPolygons")],-1)),s[115]||(s[115]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[116]||(s[116]=a('
julia
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.

source

',4))]),i("details",K,[i("summary",null,[s[117]||(s[117]=i("a",{id:"GeometryOps.VisvalingamWhyatt",href:"#GeometryOps.VisvalingamWhyatt"},[i("span",{class:"jlbinding"},"GeometryOps.VisvalingamWhyatt")],-1)),s[118]||(s[118]=e()),n(t,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[119]||(s[119]=a(`
julia
VisvalingamWhyatt <: SimplifyAlg
+
+VisvalingamWhyatt(; kw...)

Simplifies geometries by removing points below tol distance from the line between its neighboring points.

Keywords

Note: user input tol is doubled to avoid unnecessary computation in algorithm.

source

`,6))]),i("details",X,[i("summary",null,[s[120]||(s[120]=i("a",{id:"GeometryOps._det-Union{Tuple{T2}, Tuple{T1}, Tuple{Union{Tuple{T1, T1}, StaticArraysCore.StaticArray{Tuple{2}, T1, 1}}, Union{Tuple{T2, T2}, StaticArraysCore.StaticArray{Tuple{2}, T2, 1}}}} where {T1<:Real, T2<:Real}",href:"#GeometryOps._det-Union{Tuple{T2}, Tuple{T1}, Tuple{Union{Tuple{T1, T1}, StaticArraysCore.StaticArray{Tuple{2}, T1, 1}}, Union{Tuple{T2, T2}, StaticArraysCore.StaticArray{Tuple{2}, T2, 1}}}} where {T1<:Real, T2<:Real}"},[i("span",{class:"jlbinding"},"GeometryOps._det")],-1)),s[121]||(s[121]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[122]||(s[122]=a('
julia
_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]

source

',5))]),i("details",$,[i("summary",null,[s[123]||(s[123]=i("a",{id:"GeometryOps._equals_curves-NTuple{4, Any}",href:"#GeometryOps._equals_curves-NTuple{4, Any}"},[i("span",{class:"jlbinding"},"GeometryOps._equals_curves")],-1)),s[124]||(s[124]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[125]||(s[125]=a('
julia
_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)

source

',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(`
julia
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.

source

`,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(`
julia
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.

source

`,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('
julia
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.

source

',5))]),i("details",as,[i("summary",null,[s[135]||(s[135]=i("a",{id:"GeometryOps.barycentric_coordinates-Tuple{GeometryOps.AbstractBarycentricCoordinateMethod, Any, Any}",href:"#GeometryOps.barycentric_coordinates-Tuple{GeometryOps.AbstractBarycentricCoordinateMethod, Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.barycentric_coordinates")],-1)),s[136]||(s[136]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[137]||(s[137]=a('
julia
barycentric_coordinates(method = MeanValue(), polygon, point)

Returns the barycentric coordinates of point in polygon using the barycentric coordinate method method.

source

',3))]),i("details",es,[i("summary",null,[s[138]||(s[138]=i("a",{id:"GeometryOps.barycentric_interpolate-Union{Tuple{V}, Tuple{GeometryOps.AbstractBarycentricCoordinateMethod, Any, AbstractVector{V}, Any}} where V",href:"#GeometryOps.barycentric_interpolate-Union{Tuple{V}, Tuple{GeometryOps.AbstractBarycentricCoordinateMethod, Any, AbstractVector{V}, Any}} where V"},[i("span",{class:"jlbinding"},"GeometryOps.barycentric_interpolate")],-1)),s[139]||(s[139]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[140]||(s[140]=a('
julia
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).

source

',5))]),i("details",ts,[i("summary",null,[s[141]||(s[141]=i("a",{id:"GeometryOps.centroid-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T",href:"#GeometryOps.centroid-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T"},[i("span",{class:"jlbinding"},"GeometryOps.centroid")],-1)),s[142]||(s[142]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[143]||(s[143]=a('
julia
centroid(geom, [T=Float64])::Tuple{T, T}

Returns the centroid of a given line segment, linear ring, polygon, or mutlipolygon.

source

',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('
julia
centroid_and_area(geom, [T=Float64])::(::Tuple{T, T}, ::Real)

Returns the centroid and area of a given geometry.

source

',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('
julia
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.

source

',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(`
julia
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

julia
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

source

`,6))]),i("details",hs,[i("summary",null,[s[153]||(s[153]=i("a",{id:"GeometryOps.convex_hull",href:"#GeometryOps.convex_hull"},[i("span",{class:"jlbinding"},"GeometryOps.convex_hull")],-1)),s[154]||(s[154]=e()),n(t,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[155]||(s[155]=a('
julia
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).

source

',5))]),i("details",ks,[i("summary",null,[s[156]||(s[156]=i("a",{id:"GeometryOps.coverage-Union{Tuple{T}, NTuple{5, Any}, Tuple{Any, Any, Any, Any, Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.coverage-Union{Tuple{T}, NTuple{5, Any}, Tuple{Any, Any, Any, Any, Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.coverage")],-1)),s[157]||(s[157]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[158]||(s[158]=a('
julia
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:

Result will be of type T, where T is an optional argument with a default value of Float64.

source

',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(`
julia
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

julia
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

source

`,6))]),i("details",os,[i("summary",null,[s[162]||(s[162]=i("a",{id:"GeometryOps.covers-Tuple{Any, Any}",href:"#GeometryOps.covers-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.covers")],-1)),s[163]||(s[163]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[164]||(s[164]=a(`
julia
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

julia
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

source

`,6))]),i("details",ds,[i("summary",null,[s[165]||(s[165]=i("a",{id:"GeometryOps.crosses-Tuple{Any, Any}",href:"#GeometryOps.crosses-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.crosses")],-1)),s[166]||(s[166]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[167]||(s[167]=a(`
julia
 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

source

`,6))]),i("details",gs,[i("summary",null,[s[168]||(s[168]=i("a",{id:"GeometryOps.cut-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.cut-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.cut")],-1)),s[169]||(s[169]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[170]||(s[170]=a(`
julia
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

julia
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]]]

source

`,6))]),i("details",ys,[i("summary",null,[s[171]||(s[171]=i("a",{id:"GeometryOps.difference-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.difference-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.difference")],-1)),s[172]||(s[172]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[173]||(s[173]=a(`
julia
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

julia
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]]]

source

`,5))]),i("details",Es,[i("summary",null,[s[174]||(s[174]=i("a",{id:"GeometryOps.disjoint-Tuple{Any, Any}",href:"#GeometryOps.disjoint-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.disjoint")],-1)),s[175]||(s[175]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[176]||(s[176]=a(`
julia
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

julia
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

source

`,6))]),i("details",cs,[i("summary",null,[s[177]||(s[177]=i("a",{id:"GeometryOps.distance-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.distance-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.distance")],-1)),s[178]||(s[178]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[179]||(s[179]=a('
julia
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.

source

',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('
julia
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

source

',6))]),i("details",ms,[i("summary",null,[s[183]||(s[183]=i("a",{id:"GeometryOps.enforce-Tuple{GEOS, Symbol, Any}",href:"#GeometryOps.enforce-Tuple{GEOS, Symbol, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.enforce")],-1)),s[184]||(s[184]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[185]||(s[185]=a('
julia
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).

source

',4))]),i("details",Fs,[i("summary",null,[s[186]||(s[186]=i("a",{id:"GeometryOps.equals-NTuple{4, Any}",href:"#GeometryOps.equals-NTuple{4, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[187]||(s[187]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[188]||(s[188]=a('
julia
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.

source

',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(`
julia
equals(geom1, geom2)::Bool

Compare two Geometries return true if they are the same geometry.

Examples

julia
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

source

`,5))]),i("details",bs,[i("summary",null,[s[192]||(s[192]=i("a",{id:"GeometryOps.equals-Tuple{GeoInterface.LinearRingTrait, Any, GeoInterface.LinearRingTrait, Any}",href:"#GeometryOps.equals-Tuple{GeoInterface.LinearRingTrait, Any, GeoInterface.LinearRingTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.equals")],-1)),s[193]||(s[193]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[194]||(s[194]=a(`
julia
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.

source

`,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(`
julia
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

source

`,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('
julia
equals(::GI.MultiPointTrait, mp1, ::GI.MultiPointTrait, mp2)::Bool

Two multipoints are equal if they share the same set of points.

source

',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('
julia
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.

source

',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('
julia
equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool

Two multipolygons are equal if they share the same set of polygons.

source

',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('
julia
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.

source

',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('
julia
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.

source

',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('
julia
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.

source

',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('
julia
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.

source

',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('
julia
equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool

Two polygons are equal if they share the same exterior edge and holes.

source

',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(`
julia
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

source

`,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(`
julia
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.

source

`,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('
julia
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.

source

',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('
julia
flip(obj)

Swap all of the x and y coordinates in obj, otherwise keeping the original structure (but not necessarily the original type).

Keywords

source

',5))]),i("details",Is,[i("summary",null,[s[234]||(s[234]=i("a",{id:"GeometryOps.intersection-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.intersection-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.intersection")],-1)),s[235]||(s[235]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[236]||(s[236]=a(`
julia
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

julia
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]

source

`,5))]),i("details",Ms,[i("summary",null,[s[237]||(s[237]=i("a",{id:"GeometryOps.intersection_points-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.intersection_points-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.intersection_points")],-1)),s[238]||(s[238]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[239]||(s[239]=a(`
julia
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

+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

julia
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

source

`,9))]),i("details",Ps,[i("summary",null,[s[240]||(s[240]=i("a",{id:"GeometryOps.isclockwise-Tuple{Any}",href:"#GeometryOps.isclockwise-Tuple{Any}"},[i("span",{class:"jlbinding"},"GeometryOps.isclockwise")],-1)),s[241]||(s[241]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[244]||(s[244]=a('
julia
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,

',2)),i("mjx-container",qs,[(p(),l("svg",Rs,s[242]||(s[242]=[a('',1)]))),s[243]||(s[243]=i("mjx-assistive-mml",{unselectable:"on",display:"block",style:{top:"0px",left:"0px",clip:"rect(1px, 1px, 1px, 1px)","-webkit-touch-callout":"none","-webkit-user-select":"none","-khtml-user-select":"none","-moz-user-select":"none","-ms-user-select":"none","user-select":"none",position:"absolute",padding:"1px 0px 0px 0px",border:"0px",display:"block",overflow:"hidden",width:"100%"}},[i("math",{xmlns:"http://www.w3.org/1998/Math/MathML",display:"block"},[i("mrow",{"data-mjx-texclass":"INNER"},[i("mo",{"data-mjx-texclass":"OPEN"},"("),i("munderover",null,[i("mo",{"data-mjx-texclass":"OP"},"∑"),i("mrow",{"data-mjx-texclass":"ORD"},[i("mi",null,"i"),i("mo",null,"="),i("mn",null,"2")]),i("mi",null,"n")]),i("mo",{stretchy:"false"},"("),i("msub",null,[i("mi",null,"x"),i("mi",null,"i")]),i("mo",null,"−"),i("msub",null,[i("mi",null,"x"),i("mrow",{"data-mjx-texclass":"ORD"},[i("mi",null,"i"),i("mo",null,"−"),i("mn",null,"1")])]),i("mo",{stretchy:"false"},")"),i("mo",null,"⋅"),i("mo",{stretchy:"false"},"("),i("msub",null,[i("mi",null,"y"),i("mi",null,"i")]),i("mo",null,"+"),i("msub",null,[i("mi",null,"y"),i("mrow",{"data-mjx-texclass":"ORD"},[i("mi",null,"i"),i("mo",null,"−"),i("mn",null,"1")])]),i("mo",{stretchy:"false"},")"),i("mo",{"data-mjx-texclass":"CLOSE"},")")]),i("mo",null,">"),i("mn",null,"0")])],-1))]),s[245]||(s[245]=a(`

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

source

`,3))]),i("details",Ss,[i("summary",null,[s[246]||(s[246]=i("a",{id:"GeometryOps.isconcave-Tuple{Any}",href:"#GeometryOps.isconcave-Tuple{Any}"},[i("span",{class:"jlbinding"},"GeometryOps.isconcave")],-1)),s[247]||(s[247]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[248]||(s[248]=a(`
julia
isconcave(poly::Polygon)::Bool

Take a polygon and return true or false as to whether it is concave or not.

Examples

julia
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

source

`,5))]),i("details",Vs,[i("summary",null,[s[249]||(s[249]=i("a",{id:"GeometryOps.overlaps-Tuple{Any, Any}",href:"#GeometryOps.overlaps-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[250]||(s[250]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[251]||(s[251]=a(`
julia
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

julia
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

source

`,5))]),i("details",Js,[i("summary",null,[s[252]||(s[252]=i("a",{id:"GeometryOps.overlaps-Tuple{GeoInterface.AbstractTrait, Any, GeoInterface.AbstractTrait, Any}",href:"#GeometryOps.overlaps-Tuple{GeoInterface.AbstractTrait, Any, GeoInterface.AbstractTrait, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.overlaps")],-1)),s[253]||(s[253]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[254]||(s[254]=a('
julia
overlaps(::GI.AbstractTrait, geom1, ::GI.AbstractTrait, geom2)::Bool

For any non-specified pair, all have non-matching dimensions, return false.

source

',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('
julia
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.

source

',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(`
julia
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.

source

`,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(`
julia
overlaps(
+    ::GI.MultiPolygonTrait, polys1,
+    ::GI.MultiPolygonTrait, polys2,
+)::Bool

Return true if at least one pair of polygons from multipolygons overlap. Else false.

source

`,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(`
julia
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.

source

`,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(`
julia
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.

source

`,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(`
julia
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.

source

`,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(`
julia
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.

source

`,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(`
julia
polygon_to_line(poly::Polygon)

Converts a Polygon to LineString or MultiLineString

Examples

julia
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)

source

`,5))]),i("details",Xs,[i("summary",null,[s[279]||(s[279]=i("a",{id:"GeometryOps.polygonize-Tuple{AbstractMatrix{Bool}}",href:"#GeometryOps.polygonize-Tuple{AbstractMatrix{Bool}}"},[i("span",{class:"jlbinding"},"GeometryOps.polygonize")],-1)),s[280]||(s[280]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[281]||(s[281]=a(`
julia
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 Features 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

Example

julia
using GeometryOps
+A = rand(100, 100)
+multipolygon = polygonize(>(0.5), A);

source

`,10))]),i("details",$s,[i("summary",null,[s[282]||(s[282]=i("a",{id:"GeometryOps.segmentize-Tuple{Any}",href:"#GeometryOps.segmentize-Tuple{Any}"},[i("span",{class:"jlbinding"},"GeometryOps.segmentize")],-1)),s[283]||(s[283]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[284]||(s[284]=a('
julia
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

Returns a geometry of similar type to the input geometry, but resampled.

source

',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(`
julia
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.

source

`,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('
julia
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.

source

',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(`
julia
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

Keywords for DouglasPeucker are allowed when no algorithm is specified:

Keywords

Example

Simplify a polygon to have six points:

julia
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

source

`,14))]),i("details",ai,[i("summary",null,[s[294]||(s[294]=i("a",{id:"GeometryOps.t_value-Union{Tuple{T2}, Tuple{T1}, Tuple{N}, Tuple{Union{NTuple{N, T1}, StaticArraysCore.StaticArray{Tuple{N}, T1, 1}}, Union{NTuple{N, T1}, StaticArraysCore.StaticArray{Tuple{N}, T1, 1}}, T2, T2}} where {N, T1<:Real, T2<:Real}",href:"#GeometryOps.t_value-Union{Tuple{T2}, Tuple{T1}, Tuple{N}, Tuple{Union{NTuple{N, T1}, StaticArraysCore.StaticArray{Tuple{N}, T1, 1}}, Union{NTuple{N, T1}, StaticArraysCore.StaticArray{Tuple{N}, T1, 1}}, T2, T2}} where {N, T1<:Real, T2<:Real}"},[i("span",{class:"jlbinding"},"GeometryOps.t_value")],-1)),s[295]||(s[295]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[298]||(s[298]=a('
julia
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.

',3)),i("mjx-container",ei,[(p(),l("svg",ti,s[296]||(s[296]=[a('',1)]))),s[297]||(s[297]=i("mjx-assistive-mml",{unselectable:"on",display:"block",style:{top:"0px",left:"0px",clip:"rect(1px, 1px, 1px, 1px)","-webkit-touch-callout":"none","-webkit-user-select":"none","-khtml-user-select":"none","-moz-user-select":"none","-ms-user-select":"none","user-select":"none",position:"absolute",padding:"1px 0px 0px 0px",border:"0px",display:"block",overflow:"hidden",width:"100%"}},[i("math",{xmlns:"http://www.w3.org/1998/Math/MathML",display:"block"},[i("mi",null,"t"),i("mi",null,"ᵢ"),i("mo",null,"="),i("mfrac",null,[i("mrow",null,[i("mrow",{"data-mjx-texclass":"ORD"},[i("mi",{"data-mjx-auto-op":"false"},"det")]),i("mrow",{"data-mjx-texclass":"INNER"},[i("mo",{"data-mjx-texclass":"OPEN"},"("),i("mi",null,"s"),i("mi",null,"ᵢ"),i("mo",null,","),i("mi",null,"s"),i("mi",null,"ᵢ"),i("mrow",{"data-mjx-texclass":"ORD"},[i("mo",{"data-mjx-pseudoscript":"true"},"₊")]),i("mrow",{"data-mjx-texclass":"ORD"},[i("mo",{"data-mjx-pseudoscript":"true"},"₁")]),i("mo",{"data-mjx-texclass":"CLOSE"},")")])]),i("mrow",null,[i("mi",null,"r"),i("mi",null,"ᵢ"),i("mo",null,"∗"),i("mi",null,"r"),i("mi",null,"ᵢ"),i("mrow",{"data-mjx-texclass":"ORD"},[i("mo",{"data-mjx-pseudoscript":"true"},"₊")]),i("mrow",{"data-mjx-texclass":"ORD"},[i("mo",{"data-mjx-pseudoscript":"true"},"₁")]),i("mo",null,"+"),i("mi",null,"s"),i("mi",null,"ᵢ"),i("mo",null,"⋅"),i("mi",null,"s"),i("mi",null,"ᵢ"),i("mrow",{"data-mjx-texclass":"ORD"},[i("mo",{"data-mjx-pseudoscript":"true"},"₊")]),i("mrow",{"data-mjx-texclass":"ORD"},[i("mo",{"data-mjx-pseudoscript":"true"},"₁")])])])])],-1))]),s[299]||(s[299]=a(`

+
+[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.

source

`,3))]),i("details",ni,[i("summary",null,[s[300]||(s[300]=i("a",{id:"GeometryOps.touches-Tuple{Any, Any}",href:"#GeometryOps.touches-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.touches")],-1)),s[301]||(s[301]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[302]||(s[302]=a(`
julia
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

julia
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

source

`,5))]),i("details",li,[i("summary",null,[s[303]||(s[303]=i("a",{id:"GeometryOps.transform-Tuple{Any, Any}",href:"#GeometryOps.transform-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.transform")],-1)),s[304]||(s[304]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[305]||(s[305]=a(`
julia
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)

source

`,9))]),i("details",pi,[i("summary",null,[s[306]||(s[306]=i("a",{id:"GeometryOps.tuples-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T",href:"#GeometryOps.tuples-Union{Tuple{Any}, Tuple{T}, Tuple{Any, Type{T}}} where T"},[i("span",{class:"jlbinding"},"GeometryOps.tuples")],-1)),s[307]||(s[307]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[308]||(s[308]=a('
julia
tuples(obj)

Convert all points in obj to Tuples, wherever the are nested.

Returns a similar object or collection of objects using GeoInterface.jl geometries wrapping Tuple points.

Keywords

source

',6))]),i("details",hi,[i("summary",null,[s[309]||(s[309]=i("a",{id:"GeometryOps.union-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat",href:"#GeometryOps.union-Union{Tuple{T}, Tuple{Any, Any}, Tuple{Any, Any, Type{T}}} where T<:AbstractFloat"},[i("span",{class:"jlbinding"},"GeometryOps.union")],-1)),s[310]||(s[310]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[311]||(s[311]=a(`
julia
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

julia
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]]]

source

`,6))]),i("details",ki,[i("summary",null,[s[312]||(s[312]=i("a",{id:"GeometryOps.weighted_mean-Union{Tuple{WT}, Tuple{WT, Any, Any}} where WT<:Real",href:"#GeometryOps.weighted_mean-Union{Tuple{WT}, Tuple{WT, Any, Any}} where WT<:Real"},[i("span",{class:"jlbinding"},"GeometryOps.weighted_mean")],-1)),s[313]||(s[313]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[314]||(s[314]=a('
julia
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.

source

',5))]),i("details",ri,[i("summary",null,[s[315]||(s[315]=i("a",{id:"GeometryOps.within-Tuple{Any, Any}",href:"#GeometryOps.within-Tuple{Any, Any}"},[i("span",{class:"jlbinding"},"GeometryOps.within")],-1)),s[316]||(s[316]=e()),n(t,{type:"info",class:"jlObjectType jlMethod",text:"Method"})]),s[317]||(s[317]=a(`
julia
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

julia
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

source

`,6))]),s[324]||(s[324]=a('
  1. K. Hormann and N. Sukumar. Generalized Barycentric Coordinates in Computer Graphics and Computational Mechanics. Taylor & Fancis, CRC Press, 2017. ↩︎

',2))])}const Fi=h(r,[["render",oi]]);export{mi as __pageData,Fi as default}; diff --git a/previews/PR239/assets/app.CZ1qv-bZ.js b/previews/PR239/assets/app.CZ1qv-bZ.js new file mode 100644 index 000000000..e1c58d33e --- /dev/null +++ b/previews/PR239/assets/app.CZ1qv-bZ.js @@ -0,0 +1 @@ +import{R as p}from"./chunks/theme.CkNAWswv.js";import{R as o,a6 as u,a7 as c,a8 as l,a9 as f,aa as d,ab as m,ac as h,ad as g,ae as A,af as v,d as P,u as R,v as w,s as y,ag as C,ah as b,ai as E,a4 as S}from"./chunks/framework.onQNwZ2I.js";function i(e){if(e.extends){const a=i(e.extends);return{...a,...e,async enhanceApp(t){a.enhanceApp&&await a.enhanceApp(t),e.enhanceApp&&await e.enhanceApp(t)}}}return e}const s=i(p),T=P({name:"VitePressApp",setup(){const{site:e,lang:a,dir:t}=R();return w(()=>{y(()=>{document.documentElement.lang=a.value,document.documentElement.dir=t.value})}),e.value.router.prefetchLinks&&C(),b(),E(),s.setup&&s.setup(),()=>S(s.Layout)}});async function D(){globalThis.__VITEPRESS__=!0;const e=j(),a=_();a.provide(c,e);const t=l(e.route);return a.provide(f,t),a.component("Content",d),a.component("ClientOnly",m),Object.defineProperties(a.config.globalProperties,{$frontmatter:{get(){return t.frontmatter.value}},$params:{get(){return t.page.value.params}}}),s.enhanceApp&&await s.enhanceApp({app:a,router:e,siteData:h}),{app:a,router:e,data:t}}function _(){return g(T)}function j(){let e=o,a;return A(t=>{let n=v(t),r=null;return n&&(e&&(a=n),(e||a===n)&&(n=n.replace(/\.js$/,".lean.js")),r=import(n)),o&&(e=!1),r},s.NotFound)}o&&D().then(({app:e,router:a,data:t})=>{a.go().then(()=>{u(a.route,t.site),e.mount("#app")})});export{D as createApp}; diff --git a/previews/PR239/assets/bnkpkoa.rOsRk89v.png b/previews/PR239/assets/bnkpkoa.rOsRk89v.png new file mode 100644 index 000000000..9f49cef5a Binary files /dev/null and b/previews/PR239/assets/bnkpkoa.rOsRk89v.png differ diff --git a/previews/PR239/assets/call_notes.md.D1Aj_9mC.js b/previews/PR239/assets/call_notes.md.D1Aj_9mC.js new file mode 100644 index 000000000..23a9a2a60 --- /dev/null +++ b/previews/PR239/assets/call_notes.md.D1Aj_9mC.js @@ -0,0 +1 @@ +import{_ as i,c as l,a5 as t,o}from"./chunks/framework.onQNwZ2I.js";const u=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"call_notes.md","filePath":"call_notes.md","lastUpdated":null}'),a={name:"call_notes.md"};function r(n,e,s,p,c,d){return o(),l("div",null,e[0]||(e[0]=[t('

20th April, 2024

See GeometryOps#114.

29th Feb, 2024

To do

Done

',8)]))}const f=i(a,[["render",r]]);export{u as __pageData,f as default}; diff --git a/previews/PR239/assets/call_notes.md.D1Aj_9mC.lean.js b/previews/PR239/assets/call_notes.md.D1Aj_9mC.lean.js new file mode 100644 index 000000000..23a9a2a60 --- /dev/null +++ b/previews/PR239/assets/call_notes.md.D1Aj_9mC.lean.js @@ -0,0 +1 @@ +import{_ as i,c as l,a5 as t,o}from"./chunks/framework.onQNwZ2I.js";const u=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"call_notes.md","filePath":"call_notes.md","lastUpdated":null}'),a={name:"call_notes.md"};function r(n,e,s,p,c,d){return o(),l("div",null,e[0]||(e[0]=[t('

20th April, 2024

See GeometryOps#114.

29th Feb, 2024

To do

Done

',8)]))}const f=i(a,[["render",r]]);export{u as __pageData,f as default}; diff --git a/previews/PR239/assets/cfoowuw.BEoJ_XVP.png b/previews/PR239/assets/cfoowuw.BEoJ_XVP.png new file mode 100644 index 000000000..31a71c289 Binary files /dev/null and b/previews/PR239/assets/cfoowuw.BEoJ_XVP.png differ diff --git a/previews/PR239/assets/cgnfnmo.DHcwB147.png b/previews/PR239/assets/cgnfnmo.DHcwB147.png new file mode 100644 index 000000000..9595a3cd4 Binary files /dev/null and b/previews/PR239/assets/cgnfnmo.DHcwB147.png differ diff --git a/previews/PR239/assets/chihpsx.CPClNl7F.png b/previews/PR239/assets/chihpsx.CPClNl7F.png new file mode 100644 index 000000000..c741e396f Binary files /dev/null and b/previews/PR239/assets/chihpsx.CPClNl7F.png differ diff --git a/previews/PR239/assets/chunks/@localSearchIndexroot.rs1EPJPu.js b/previews/PR239/assets/chunks/@localSearchIndexroot.rs1EPJPu.js new file mode 100644 index 000000000..6dfd4e840 --- /dev/null +++ b/previews/PR239/assets/chunks/@localSearchIndexroot.rs1EPJPu.js @@ -0,0 +1 @@ +const e='{"documentCount":201,"nextId":201,"documentIds":{"0":"/GeometryOps.jl/previews/PR239/api#Full-GeometryOps-API-documentation","1":"/GeometryOps.jl/previews/PR239/api#apply-and-associated-functions","2":"/GeometryOps.jl/previews/PR239/api#General-geometry-methods","3":"/GeometryOps.jl/previews/PR239/api#OGC-methods","4":"/GeometryOps.jl/previews/PR239/api#Other-general-methods","5":"/GeometryOps.jl/previews/PR239/api#Barycentric-coordinates","6":"/GeometryOps.jl/previews/PR239/api#Other-methods","7":"/GeometryOps.jl/previews/PR239/call_notes#20th-April,-2024","8":"/GeometryOps.jl/previews/PR239/call_notes#29th-Feb,-2024","9":"/GeometryOps.jl/previews/PR239/call_notes#To-do","10":"/GeometryOps.jl/previews/PR239/call_notes#done","11":"/GeometryOps.jl/previews/PR239/experiments/accurate_accumulators#Accurate-accumulation","12":"/GeometryOps.jl/previews/PR239/experiments/predicates#predicates","13":"/GeometryOps.jl/previews/PR239/experiments/predicates#orient","14":"/GeometryOps.jl/previews/PR239/experiments/predicates#dashboard","15":"/GeometryOps.jl/previews/PR239/experiments/predicates#Testing-robust-vs-regular-predicates","16":"/GeometryOps.jl/previews/PR239/experiments/predicates#incircle","17":"/GeometryOps.jl/previews/PR239/explanations/paradigms#paradigms","18":"/GeometryOps.jl/previews/PR239/explanations/paradigms#apply","19":"/GeometryOps.jl/previews/PR239/explanations/paradigms#applyreduce","20":"/GeometryOps.jl/previews/PR239/explanations/paradigms#fix-and-prepare","21":"/GeometryOps.jl/previews/PR239/explanations/peculiarities#peculiarities","22":"/GeometryOps.jl/previews/PR239/explanations/peculiarities#What-does-apply-return-and-why?","23":"/GeometryOps.jl/previews/PR239/explanations/peculiarities#Why-do-you-want-me-to-provide-a-target-in-set-operations?","24":"/GeometryOps.jl/previews/PR239/explanations/peculiarities#_True-and-_False-(or-BoolsAsTypes)","25":"/GeometryOps.jl/previews/PR239/#what-is-geometryops-jl","26":"/GeometryOps.jl/previews/PR239/#how-to-navigate-the-docs","27":"/GeometryOps.jl/previews/PR239/introduction#introduction","28":"/GeometryOps.jl/previews/PR239/introduction#Main-concepts","29":"/GeometryOps.jl/previews/PR239/introduction#The-apply-paradigm","30":"/GeometryOps.jl/previews/PR239/introduction#What\'s-this-GeoInterface.Wrapper-thing?","31":"/GeometryOps.jl/previews/PR239/source/GeometryOps#geometryops-jl","32":"/GeometryOps.jl/previews/PR239/source/GeometryOpsLibGEOSExt/segmentize#segmentize","33":"/GeometryOps.jl/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides#Simple-overrides","34":"/GeometryOps.jl/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides#Polygon-set-operations","35":"/GeometryOps.jl/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides#difference","36":"/GeometryOps.jl/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides#union","37":"/GeometryOps.jl/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides#intersection","38":"/GeometryOps.jl/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides#Symmetric-difference","39":"/GeometryOps.jl/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides#DE-9IM-boolean-methods","40":"/GeometryOps.jl/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides#equals","41":"/GeometryOps.jl/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides#disjoint","42":"/GeometryOps.jl/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides#touches","43":"/GeometryOps.jl/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides#crosses","44":"/GeometryOps.jl/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides#within","45":"/GeometryOps.jl/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides#contains","46":"/GeometryOps.jl/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides#overlaps","47":"/GeometryOps.jl/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides#covers","48":"/GeometryOps.jl/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides#coveredby","49":"/GeometryOps.jl/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides#intersects","50":"/GeometryOps.jl/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides#Convex-hull","51":"/GeometryOps.jl/previews/PR239/source/methods/angles#angles","52":"/GeometryOps.jl/previews/PR239/source/methods/angles#What-is-angles?","53":"/GeometryOps.jl/previews/PR239/source/methods/angles#implementation","54":"/GeometryOps.jl/previews/PR239/source/methods/area#Area-and-signed-area","55":"/GeometryOps.jl/previews/PR239/source/methods/area#What-is-area?-What-is-signed-area?","56":"/GeometryOps.jl/previews/PR239/source/methods/area#implementation","57":"/GeometryOps.jl/previews/PR239/source/methods/barycentric#Barycentric-coordinates","58":"/GeometryOps.jl/previews/PR239/source/methods/barycentric#example","59":"/GeometryOps.jl/previews/PR239/source/methods/barycentric#Barycentric-coordinate-API","60":"/GeometryOps.jl/previews/PR239/source/methods/buffer#buffer","61":"/GeometryOps.jl/previews/PR239/source/methods/centroid#centroid","62":"/GeometryOps.jl/previews/PR239/source/methods/centroid#What-is-the-centroid?","63":"/GeometryOps.jl/previews/PR239/source/methods/centroid#implementation","64":"/GeometryOps.jl/previews/PR239/source/methods/clipping/clipping_processor#Polygon-clipping-helpers","65":"/GeometryOps.jl/previews/PR239/source/methods/clipping/coverage#What-is-coverage?","66":"/GeometryOps.jl/previews/PR239/source/methods/clipping/coverage#implementation","67":"/GeometryOps.jl/previews/PR239/source/methods/clipping/cut#Polygon-cutting","68":"/GeometryOps.jl/previews/PR239/source/methods/clipping/cut#What-is-cut?","69":"/GeometryOps.jl/previews/PR239/source/methods/clipping/cut#implementation","70":"/GeometryOps.jl/previews/PR239/source/methods/clipping/difference#Difference-Polygon-Clipping","71":"/GeometryOps.jl/previews/PR239/source/methods/clipping/difference#Helper-functions-for-Differences-with-Greiner-and-Hormann-Polygon-Clipping","72":"/GeometryOps.jl/previews/PR239/source/methods/clipping/intersection#Geometry-Intersection","73":"/GeometryOps.jl/previews/PR239/source/methods/clipping/intersection#Helper-functions-for-Intersections-with-Greiner-and-Hormann-Polygon-Clipping","74":"/GeometryOps.jl/previews/PR239/source/methods/clipping/predicates#If-we-want-to-inject-adaptivity,-we-would-do-something-like:","75":"/GeometryOps.jl/previews/PR239/source/methods/clipping/union#Union-Polygon-Clipping","76":"/GeometryOps.jl/previews/PR239/source/methods/clipping/union#Helper-functions-for-Unions-with-Greiner-and-Hormann-Polygon-Clipping","77":"/GeometryOps.jl/previews/PR239/source/methods/convex_hull#Convex-hull","78":"/GeometryOps.jl/previews/PR239/source/methods/convex_hull#example","79":"/GeometryOps.jl/previews/PR239/source/methods/convex_hull#Simple-hull","80":"/GeometryOps.jl/previews/PR239/source/methods/convex_hull#Convex-hull-of-the-USA","81":"/GeometryOps.jl/previews/PR239/source/methods/convex_hull#Investigating-the-winding-order","82":"/GeometryOps.jl/previews/PR239/source/methods/convex_hull#implementation","83":"/GeometryOps.jl/previews/PR239/source/methods/distance#Distance-and-signed-distance","84":"/GeometryOps.jl/previews/PR239/source/methods/distance#What-is-distance?-What-is-signed-distance?","85":"/GeometryOps.jl/previews/PR239/source/methods/distance#implementation","86":"/GeometryOps.jl/previews/PR239/source/methods/equals#equals","87":"/GeometryOps.jl/previews/PR239/source/methods/equals#What-is-equals?","88":"/GeometryOps.jl/previews/PR239/source/methods/equals#implementation","89":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/contains#contains","90":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/contains#What-is-contains?","91":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/contains#implementation","92":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/coveredby#coveredby","93":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/coveredby#What-is-coveredby?","94":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/coveredby#implementation","95":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/coveredby#Convert-features-to-geometries","96":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/coveredby#Points-coveredby-geometries","97":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/coveredby#Lines-coveredby-geometries","98":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/coveredby#Rings-covered-by-geometries","99":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/coveredby#Polygons-covered-by-geometries","100":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/coveredby#Geometries-coveredby-multi-geometry/geometry-collections","101":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/coveredby#Multi-geometry/geometry-collections-coveredby-geometries","102":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/covers#covers","103":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/covers#What-is-covers?","104":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/covers#implementation","105":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/crosses#Crossing-checks","106":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/disjoint#disjoint","107":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/disjoint#What-is-disjoint?","108":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/disjoint#implementation","109":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/disjoint#Convert-features-to-geometries","110":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/disjoint#Point-disjoint-geometries","111":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/disjoint#Lines-disjoint-geometries","112":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/disjoint#Rings-disjoint-geometries","113":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/disjoint#Polygon-disjoint-geometries","114":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/disjoint#Geometries-disjoint-multi-geometry/geometry-collections","115":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/disjoint#Multi-geometry/geometry-collections-coveredby-geometries","116":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/geom_geom_processors#Line-curve-interaction","117":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/intersects#Intersection-checks","118":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/intersects#What-is-intersects?","119":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/intersects#implementation","120":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/overlaps#overlaps","121":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/overlaps#What-is-overlaps?","122":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/overlaps#implementation","123":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/touches#touches","124":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/touches#What-is-touches?","125":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/touches#implementation","126":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/touches#Convert-features-to-geometries","127":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/touches#Point-touches-geometries","128":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/touches#Lines-touching-geometries","129":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/touches#Rings-touch-geometries","130":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/touches#Polygons-touch-geometries","131":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/touches#Geometries-touch-multi-geometry/geometry-collections","132":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/touches#Multi-geometry/geometry-collections-cross-geometries","133":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/within#within","134":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/within#What-is-within?","135":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/within#implementation","136":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/within#Convert-features-to-geometries","137":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/within#Points-within-geometries","138":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/within#Lines-within-geometries","139":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/within#Rings-covered-by-geometries","140":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/within#Polygons-within-geometries","141":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/within#Geometries-within-multi-geometry/geometry-collections","142":"/GeometryOps.jl/previews/PR239/source/methods/geom_relations/within#Multi-geometry/geometry-collections-within-geometries","143":"/GeometryOps.jl/previews/PR239/source/methods/orientation#orientation","144":"/GeometryOps.jl/previews/PR239/source/methods/orientation#isclockwise","145":"/GeometryOps.jl/previews/PR239/source/methods/orientation#isconcave","146":"/GeometryOps.jl/previews/PR239/source/methods/polygonize#Polygonizing-raster-data","147":"/GeometryOps.jl/previews/PR239/source/not_implemented_yet#Not-implemented-yet","148":"/GeometryOps.jl/previews/PR239/source/src/apply#apply","149":"/GeometryOps.jl/previews/PR239/source/src/apply#docstrings","150":"/GeometryOps.jl/previews/PR239/source/src/apply#functions","151":"/GeometryOps.jl/previews/PR239/source/src/apply#What-is-apply?","152":"/GeometryOps.jl/previews/PR239/source/src/apply#embedding","153":"/GeometryOps.jl/previews/PR239/source/src/apply#threading","154":"/GeometryOps.jl/previews/PR239/source/src/applyreduce#applyreduce","155":"/GeometryOps.jl/previews/PR239/source/src/keyword_docs#Keyword-docs","156":"/GeometryOps.jl/previews/PR239/source/src/other_primitives#Other-primitives-(unwrap,-flatten,-etc)","157":"/GeometryOps.jl/previews/PR239/source/src/types#types","158":"/GeometryOps.jl/previews/PR239/source/src/types#Manifold","159":"/GeometryOps.jl/previews/PR239/source/src/types#TraitTarget","160":"/GeometryOps.jl/previews/PR239/source/src/types#BoolsAsTypes","161":"/GeometryOps.jl/previews/PR239/source/transformations/correction/closed_ring#Closed-Rings","162":"/GeometryOps.jl/previews/PR239/source/transformations/correction/closed_ring#example","163":"/GeometryOps.jl/previews/PR239/source/transformations/correction/closed_ring#implementation","164":"/GeometryOps.jl/previews/PR239/source/transformations/correction/geometry_correction#Geometry-Corrections","165":"/GeometryOps.jl/previews/PR239/source/transformations/correction/geometry_correction#interface","166":"/GeometryOps.jl/previews/PR239/source/transformations/correction/geometry_correction#Available-corrections","167":"/GeometryOps.jl/previews/PR239/source/transformations/correction/intersecting_polygons#Intersecting-Polygons","168":"/GeometryOps.jl/previews/PR239/source/transformations/correction/intersecting_polygons#example","169":"/GeometryOps.jl/previews/PR239/source/transformations/correction/intersecting_polygons#implementation","170":"/GeometryOps.jl/previews/PR239/source/transformations/extent#Extent-embedding","171":"/GeometryOps.jl/previews/PR239/source/transformations/flip#Coordinate-flipping","172":"/GeometryOps.jl/previews/PR239/source/transformations/reproject#Geometry-reprojection","173":"/GeometryOps.jl/previews/PR239/source/transformations/reproject#Method-error-handling","174":"/GeometryOps.jl/previews/PR239/source/transformations/segmentize#segmentize","175":"/GeometryOps.jl/previews/PR239/source/transformations/segmentize#examples","176":"/GeometryOps.jl/previews/PR239/source/transformations/segmentize#benchmark","177":"/GeometryOps.jl/previews/PR239/source/transformations/segmentize#implementation","178":"/GeometryOps.jl/previews/PR239/source/transformations/simplify#Geometry-simplification","179":"/GeometryOps.jl/previews/PR239/source/transformations/simplify#examples","180":"/GeometryOps.jl/previews/PR239/source/transformations/simplify#benchmark","181":"/GeometryOps.jl/previews/PR239/source/transformations/simplify#Simplify-with-RadialDistance-Algorithm","182":"/GeometryOps.jl/previews/PR239/source/transformations/simplify#Simplify-with-DouglasPeucker-Algorithm","183":"/GeometryOps.jl/previews/PR239/source/transformations/simplify#Simplify-with-VisvalingamWhyatt-Algorithm","184":"/GeometryOps.jl/previews/PR239/source/transformations/simplify#Shared-utils","185":"/GeometryOps.jl/previews/PR239/source/transformations/transform#Pointwise-transformation","186":"/GeometryOps.jl/previews/PR239/source/transformations/tuples#Tuple-conversion","187":"/GeometryOps.jl/previews/PR239/source/types#types","188":"/GeometryOps.jl/previews/PR239/source/types#GEOS","189":"/GeometryOps.jl/previews/PR239/source/utils#Utility-functions","190":"/GeometryOps.jl/previews/PR239/tutorials/creating_geometry#Creating-Geometry","191":"/GeometryOps.jl/previews/PR239/tutorials/creating_geometry#creating-geometry","192":"/GeometryOps.jl/previews/PR239/tutorials/creating_geometry#plot-geometry","193":"/GeometryOps.jl/previews/PR239/tutorials/creating_geometry#geom-crs","194":"/GeometryOps.jl/previews/PR239/tutorials/creating_geometry#attributes","195":"/GeometryOps.jl/previews/PR239/tutorials/creating_geometry#save-geometry","196":"/GeometryOps.jl/previews/PR239/tutorials/geodesic_paths#Geodesic-paths","197":"/GeometryOps.jl/previews/PR239/tutorials/spatial_joins#Spatial-joins","198":"/GeometryOps.jl/previews/PR239/tutorials/spatial_joins#Simple-example","199":"/GeometryOps.jl/previews/PR239/tutorials/spatial_joins#Real-world-example","200":"/GeometryOps.jl/previews/PR239/tutorials/spatial_joins#Enabling-custom-predicates"},"fieldIds":{"title":0,"titles":1,"text":2},"fieldLength":{"0":[4,1,87],"1":[4,4,246],"2":[3,4,1],"3":[2,7,205],"4":[3,7,255],"5":[2,4,81],"6":[2,4,1016],"7":[3,1,55],"8":[3,1,1],"9":[2,3,107],"10":[1,3,17],"11":[2,1,74],"12":[1,1,5],"13":[1,1,128],"14":[1,2,116],"15":[5,2,72],"16":[1,1,1],"17":[1,1,53],"18":[1,1,109],"19":[1,2,35],"20":[3,1,79],"21":[1,1,1],"22":[7,1,71],"23":[13,1,101],"24":[6,1,66],"25":[5,1,72],"26":[5,1,61],"27":[1,1,72],"28":[2,1,1],"29":[3,3,50],"30":[8,3,18],"31":[2,1,146],"32":[1,1,106],"33":[2,1,23],"34":[3,2,1],"35":[1,5,20],"36":[1,5,20],"37":[1,5,20],"38":[2,5,22],"39":[4,2,1],"40":[1,6,13],"41":[1,6,13],"42":[1,6,13],"43":[1,6,13],"44":[1,6,13],"45":[1,6,13],"46":[1,6,13],"47":[1,6,13],"48":[1,6,13],"49":[1,6,13],"50":[2,2,34],"51":[1,1,3],"52":[4,1,57],"53":[1,1,268],"54":[4,1,4],"55":[5,4,100],"56":[1,4,246],"57":[2,1,65],"58":[1,2,211],"59":[3,2,414],"60":[1,1,110],"61":[1,1,6],"62":[5,1,91],"63":[1,1,199],"64":[3,1,610],"65":[4,1,70],"66":[1,1,327],"67":[2,1,3],"68":[4,2,57],"69":[1,2,200],"70":[3,1,214],"71":[10,1,190],"72":[2,1,250],"73":[10,1,480],"74":[11,1,25],"75":[3,1,236],"76":[10,1,284],"77":[2,1,56],"78":[1,2,1],"79":[2,3,32],"80":[5,2,40],"81":[4,2,100],"82":[1,2,166],"83":[4,1,4],"84":[5,4,128],"85":[1,4,260],"86":[1,1,3],"87":[4,1,68],"88":[1,1,265],"89":[1,1,3],"90":[4,1,79],"91":[1,1,79],"92":[1,1,3],"93":[4,1,81],"94":[1,1,128],"95":[4,1,11],"96":[3,1,56],"97":[3,1,46],"98":[4,1,48],"99":[4,1,40],"100":[5,1,40],"101":[5,1,44],"102":[1,1,3],"103":[4,1,67],"104":[1,1,78],"105":[2,1,149],"106":[1,1,3],"107":[4,1,68],"108":[1,1,110],"109":[4,1,10],"110":[3,1,57],"111":[3,1,58],"112":[3,1,47],"113":[3,1,33],"114":[5,1,39],"115":[5,1,44],"116":[3,1,432],"117":[2,1,3],"118":[4,2,80],"119":[1,2,76],"120":[1,1,3],"121":[4,1,82],"122":[1,1,244],"123":[1,1,3],"124":[4,1,70],"125":[1,1,125],"126":[4,1,11],"127":[3,1,69],"128":[3,1,52],"129":[3,1,61],"130":[3,1,40],"131":[5,1,39],"132":[5,1,42],"133":[1,1,3],"134":[4,1,72],"135":[1,1,129],"136":[4,1,11],"137":[3,1,63],"138":[3,1,53],"139":[4,1,53],"140":[3,1,38],"141":[5,1,39],"142":[5,1,42],"143":[1,1,4],"144":[1,1,21],"145":[1,1,206],"146":[3,1,511],"147":[3,1,47],"148":[1,1,114],"149":[1,1,1],"150":[1,2,152],"151":[4,1,114],"152":[2,1,36],"153":[1,1,490],"154":[1,1,291],"155":[2,1,51],"156":[6,1,221],"157":[1,1,17],"158":[1,1,221],"159":[1,1,74],"160":[1,1,98],"161":[2,1,54],"162":[1,2,87],"163":[1,2,83],"164":[2,1,41],"165":[1,2,109],"166":[2,2,106],"167":[2,1,77],"168":[1,2,70],"169":[1,2,135],"170":[2,1,71],"171":[2,1,56],"172":[2,1,132],"173":[3,2,77],"174":[1,1,76],"175":[1,1,147],"176":[1,1,268],"177":[1,1,221],"178":[2,1,44],"179":[1,2,67],"180":[1,2,391],"181":[4,1,75],"182":[4,1,186],"183":[4,1,85],"184":[2,1,149],"185":[2,1,111],"186":[2,1,58],"187":[1,1,37],"188":[1,1,147],"189":[2,1,136],"190":[2,1,60],"191":[4,2,287],"192":[13,2,280],"193":[9,2,199],"194":[7,2,63],"195":[4,2,140],"196":[2,1,52],"197":[2,1,141],"198":[2,2,137],"199":[3,2,117],"200":[3,2,68]},"averageFieldLength":[2.7213930348258706,1.7263681592039801,101.94029850746266],"storedFields":{"0":{"title":"Full GeometryOps API documentation","titles":[]},"1":{"title":"apply and associated functions","titles":["Full GeometryOps API documentation"]},"2":{"title":"General geometry methods","titles":["Full GeometryOps API documentation"]},"3":{"title":"OGC methods","titles":["Full GeometryOps API documentation","General geometry methods"]},"4":{"title":"Other general methods","titles":["Full GeometryOps API documentation","General geometry methods"]},"5":{"title":"Barycentric coordinates","titles":["Full GeometryOps API documentation"]},"6":{"title":"Other methods","titles":["Full GeometryOps API documentation"]},"7":{"title":"20th April, 2024","titles":[]},"8":{"title":"29th Feb, 2024","titles":[]},"9":{"title":"To do","titles":["29th Feb, 2024"]},"10":{"title":"Done","titles":["29th Feb, 2024"]},"11":{"title":"Accurate accumulation","titles":[]},"12":{"title":"Predicates","titles":[]},"13":{"title":"Orient","titles":["Predicates"]},"14":{"title":"Dashboard","titles":["Predicates","Orient"]},"15":{"title":"Testing robust vs regular predicates","titles":["Predicates","Orient"]},"16":{"title":"Incircle","titles":["Predicates"]},"17":{"title":"Paradigms","titles":[]},"18":{"title":"apply","titles":["Paradigms"]},"19":{"title":"applyreduce","titles":["Paradigms","apply"]},"20":{"title":"fix and prepare","titles":["Paradigms"]},"21":{"title":"Peculiarities","titles":[]},"22":{"title":"What does apply return and why?","titles":["Peculiarities"]},"23":{"title":"Why do you want me to provide a target in set operations?","titles":["Peculiarities"]},"24":{"title":"_True and _False (or BoolsAsTypes)","titles":["Peculiarities"]},"25":{"title":"What is GeometryOps.jl?","titles":[]},"26":{"title":"How to navigate the docs","titles":[]},"27":{"title":"Introduction","titles":[]},"28":{"title":"Main concepts","titles":["Introduction"]},"29":{"title":"The apply paradigm","titles":["Introduction","Main concepts"]},"30":{"title":"What's this GeoInterface.Wrapper thing?","titles":["Introduction","Main concepts"]},"31":{"title":"GeometryOps.jl","titles":[]},"32":{"title":"Segmentize","titles":[]},"33":{"title":"Simple overrides","titles":[]},"34":{"title":"Polygon set operations","titles":["Simple overrides"]},"35":{"title":"Difference","titles":["Simple overrides","Polygon set operations"]},"36":{"title":"Union","titles":["Simple overrides","Polygon set operations"]},"37":{"title":"Intersection","titles":["Simple overrides","Polygon set operations"]},"38":{"title":"Symmetric difference","titles":["Simple overrides","Polygon set operations"]},"39":{"title":"DE-9IM boolean methods","titles":["Simple overrides"]},"40":{"title":"Equals","titles":["Simple overrides","DE-9IM boolean methods"]},"41":{"title":"Disjoint","titles":["Simple overrides","DE-9IM boolean methods"]},"42":{"title":"Touches","titles":["Simple overrides","DE-9IM boolean methods"]},"43":{"title":"Crosses","titles":["Simple overrides","DE-9IM boolean methods"]},"44":{"title":"Within","titles":["Simple overrides","DE-9IM boolean methods"]},"45":{"title":"Contains","titles":["Simple overrides","DE-9IM boolean methods"]},"46":{"title":"Overlaps","titles":["Simple overrides","DE-9IM boolean methods"]},"47":{"title":"Covers","titles":["Simple overrides","DE-9IM boolean methods"]},"48":{"title":"CoveredBy","titles":["Simple overrides","DE-9IM boolean methods"]},"49":{"title":"Intersects","titles":["Simple overrides","DE-9IM boolean methods"]},"50":{"title":"Convex hull","titles":["Simple overrides"]},"51":{"title":"Angles","titles":[]},"52":{"title":"What is angles?","titles":["Angles"]},"53":{"title":"Implementation","titles":["Angles"]},"54":{"title":"Area and signed area","titles":[]},"55":{"title":"What is area? What is signed area?","titles":["Area and signed area"]},"56":{"title":"Implementation","titles":["Area and signed area"]},"57":{"title":"Barycentric coordinates","titles":[]},"58":{"title":"Example","titles":["Barycentric coordinates"]},"59":{"title":"Barycentric-coordinate API","titles":["Barycentric coordinates"]},"60":{"title":"Buffer","titles":[]},"61":{"title":"Centroid","titles":[]},"62":{"title":"What is the centroid?","titles":["Centroid"]},"63":{"title":"Implementation","titles":["Centroid"]},"64":{"title":"Polygon clipping helpers","titles":[]},"65":{"title":"What is coverage?","titles":[]},"66":{"title":"Implementation","titles":[]},"67":{"title":"Polygon cutting","titles":[]},"68":{"title":"What is cut?","titles":["Polygon cutting"]},"69":{"title":"Implementation","titles":["Polygon cutting"]},"70":{"title":"Difference Polygon Clipping","titles":[]},"71":{"title":"Helper functions for Differences with Greiner and Hormann Polygon Clipping","titles":[]},"72":{"title":"Geometry Intersection","titles":[]},"73":{"title":"Helper functions for Intersections with Greiner and Hormann Polygon Clipping","titles":[]},"74":{"title":"If we want to inject adaptivity, we would do something like:","titles":[]},"75":{"title":"Union Polygon Clipping","titles":[]},"76":{"title":"Helper functions for Unions with Greiner and Hormann Polygon Clipping","titles":[]},"77":{"title":"Convex hull","titles":[]},"78":{"title":"Example","titles":["Convex hull"]},"79":{"title":"Simple hull","titles":["Convex hull","Example"]},"80":{"title":"Convex hull of the USA","titles":["Convex hull"]},"81":{"title":"Investigating the winding order","titles":["Convex hull"]},"82":{"title":"Implementation","titles":["Convex hull"]},"83":{"title":"Distance and signed distance","titles":[]},"84":{"title":"What is distance? What is signed distance?","titles":["Distance and signed distance"]},"85":{"title":"Implementation","titles":["Distance and signed distance"]},"86":{"title":"Equals","titles":[]},"87":{"title":"What is equals?","titles":["Equals"]},"88":{"title":"Implementation","titles":["Equals"]},"89":{"title":"Contains","titles":[]},"90":{"title":"What is contains?","titles":["Contains"]},"91":{"title":"Implementation","titles":["Contains"]},"92":{"title":"CoveredBy","titles":[]},"93":{"title":"What is coveredby?","titles":["CoveredBy"]},"94":{"title":"Implementation","titles":["CoveredBy"]},"95":{"title":"Convert features to geometries","titles":[]},"96":{"title":"Points coveredby geometries","titles":[]},"97":{"title":"Lines coveredby geometries","titles":[]},"98":{"title":"Rings covered by geometries","titles":[]},"99":{"title":"Polygons covered by geometries","titles":[]},"100":{"title":"Geometries coveredby multi-geometry/geometry collections","titles":[]},"101":{"title":"Multi-geometry/geometry collections coveredby geometries","titles":[]},"102":{"title":"Covers","titles":[]},"103":{"title":"What is covers?","titles":["Covers"]},"104":{"title":"Implementation","titles":["Covers"]},"105":{"title":"Crossing checks","titles":[]},"106":{"title":"Disjoint","titles":[]},"107":{"title":"What is disjoint?","titles":["Disjoint"]},"108":{"title":"Implementation","titles":["Disjoint"]},"109":{"title":"Convert features to geometries","titles":[]},"110":{"title":"Point disjoint geometries","titles":[]},"111":{"title":"Lines disjoint geometries","titles":[]},"112":{"title":"Rings disjoint geometries","titles":[]},"113":{"title":"Polygon disjoint geometries","titles":[]},"114":{"title":"Geometries disjoint multi-geometry/geometry collections","titles":[]},"115":{"title":"Multi-geometry/geometry collections coveredby geometries","titles":[]},"116":{"title":"Line-curve interaction","titles":[]},"117":{"title":"Intersection checks","titles":[]},"118":{"title":"What is intersects?","titles":["Intersection checks"]},"119":{"title":"Implementation","titles":["Intersection checks"]},"120":{"title":"Overlaps","titles":[]},"121":{"title":"What is overlaps?","titles":["Overlaps"]},"122":{"title":"Implementation","titles":["Overlaps"]},"123":{"title":"Touches","titles":[]},"124":{"title":"What is touches?","titles":["Touches"]},"125":{"title":"Implementation","titles":["Touches"]},"126":{"title":"Convert features to geometries","titles":[]},"127":{"title":"Point touches geometries","titles":[]},"128":{"title":"Lines touching geometries","titles":[]},"129":{"title":"Rings touch geometries","titles":[]},"130":{"title":"Polygons touch geometries","titles":[]},"131":{"title":"Geometries touch multi-geometry/geometry collections","titles":[]},"132":{"title":"Multi-geometry/geometry collections cross geometries","titles":[]},"133":{"title":"Within","titles":[]},"134":{"title":"What is within?","titles":["Within"]},"135":{"title":"Implementation","titles":["Within"]},"136":{"title":"Convert features to geometries","titles":[]},"137":{"title":"Points within geometries","titles":[]},"138":{"title":"Lines within geometries","titles":[]},"139":{"title":"Rings covered by geometries","titles":[]},"140":{"title":"Polygons within geometries","titles":[]},"141":{"title":"Geometries within multi-geometry/geometry collections","titles":[]},"142":{"title":"Multi-geometry/geometry collections within geometries","titles":[]},"143":{"title":"Orientation","titles":[]},"144":{"title":"isclockwise","titles":["Orientation"]},"145":{"title":"isconcave","titles":["Orientation"]},"146":{"title":"Polygonizing raster data","titles":[]},"147":{"title":"Not implemented yet","titles":[]},"148":{"title":"apply","titles":[]},"149":{"title":"Docstrings","titles":["apply"]},"150":{"title":"Functions","titles":["apply","Docstrings"]},"151":{"title":"What is apply?","titles":["apply"]},"152":{"title":"Embedding:","titles":["apply"]},"153":{"title":"Threading","titles":["apply"]},"154":{"title":"applyreduce","titles":[]},"155":{"title":"Keyword docs","titles":[]},"156":{"title":"Other primitives (unwrap, flatten, etc)","titles":[]},"157":{"title":"Types","titles":[]},"158":{"title":"Manifold","titles":["Types"]},"159":{"title":"TraitTarget","titles":["Types"]},"160":{"title":"BoolsAsTypes","titles":["Types"]},"161":{"title":"Closed Rings","titles":[]},"162":{"title":"Example","titles":["Closed Rings"]},"163":{"title":"Implementation","titles":["Closed Rings"]},"164":{"title":"Geometry Corrections","titles":[]},"165":{"title":"Interface","titles":["Geometry Corrections"]},"166":{"title":"Available corrections","titles":["Geometry Corrections"]},"167":{"title":"Intersecting Polygons","titles":[]},"168":{"title":"Example","titles":["Intersecting Polygons"]},"169":{"title":"Implementation","titles":["Intersecting Polygons"]},"170":{"title":"Extent embedding","titles":[]},"171":{"title":"Coordinate flipping","titles":[]},"172":{"title":"Geometry reprojection","titles":[]},"173":{"title":"Method error handling","titles":["Geometry reprojection"]},"174":{"title":"Segmentize","titles":[]},"175":{"title":"Examples","titles":["Segmentize"]},"176":{"title":"Benchmark","titles":["Segmentize"]},"177":{"title":"Implementation","titles":["Segmentize"]},"178":{"title":"Geometry simplification","titles":[]},"179":{"title":"Examples","titles":["Geometry simplification"]},"180":{"title":"Benchmark","titles":["Geometry simplification"]},"181":{"title":"Simplify with RadialDistance Algorithm","titles":[]},"182":{"title":"Simplify with DouglasPeucker Algorithm","titles":[]},"183":{"title":"Simplify with VisvalingamWhyatt Algorithm","titles":[]},"184":{"title":"Shared utils","titles":[]},"185":{"title":"Pointwise transformation","titles":[]},"186":{"title":"Tuple conversion","titles":[]},"187":{"title":"Types","titles":[]},"188":{"title":"GEOS","titles":["Types"]},"189":{"title":"Utility functions","titles":[]},"190":{"title":"Creating Geometry","titles":[]},"191":{"title":"Creating and plotting geometries","titles":["Creating Geometry"]},"192":{"title":"Plot geometries on a map using GeoMakie and coordinate reference system (CRS)","titles":["Creating Geometry"]},"193":{"title":"Create geospatial geometries with embedded coordinate reference system information","titles":["Creating Geometry"]},"194":{"title":"Creating a table with attributes and geometry","titles":["Creating Geometry"]},"195":{"title":"Saving your geospatial data","titles":["Creating Geometry"]},"196":{"title":"Geodesic paths","titles":[]},"197":{"title":"Spatial joins","titles":[]},"198":{"title":"Simple example","titles":["Spatial joins"]},"199":{"title":"Real-world example","titles":["Spatial joins"]},"200":{"title":"Enabling custom predicates","titles":["Spatial joins"]}},"dirtCount":0,"index":[["⋮",{"2":{"192":1}}],["θ",{"2":{"191":7,"192":3,"193":5}}],["☁",{"2":{"191":1}}],["✈",{"2":{"191":1}}],["÷",{"2":{"153":1,"154":1}}],["ϵ",{"2":{"73":5}}],["∘",{"2":{"73":1,"122":2,"146":2,"165":1,"180":2,"200":2}}],["⊻",{"2":{"64":1,"71":2}}],["≥",{"2":{"64":1,"116":2,"169":1,"182":1}}],["α≈1",{"2":{"73":1}}],["α≈0",{"2":{"73":1}}],["α2",{"2":{"64":4,"73":2}}],["α",{"2":{"64":3,"73":28,"116":7}}],["α1",{"2":{"64":4,"73":2}}],["β2",{"2":{"64":4,"73":2}}],["β",{"2":{"64":4,"73":28,"116":7}}],["β1",{"2":{"64":4,"73":2}}],["≤",{"2":{"64":5,"66":14,"116":6,"182":1,"184":1,"189":4}}],["^",{"2":{"192":1}}],["^3",{"2":{"192":1}}],["^n",{"2":{"145":1}}],["^2",{"2":{"63":2,"85":2}}],["^hormannpresentation",{"2":{"59":2}}],["∑λ",{"2":{"59":2}}],["∑i=2n",{"2":{"6":1}}],["`$",{"2":{"188":1}}],["`equatorial",{"2":{"176":2}}],["`extents",{"2":{"170":1}}],["`inf`",{"2":{"172":1}}],["`inv",{"2":{"158":1}}],["`intersects`",{"2":{"119":1}}],["`intersectingpolygons`",{"2":{"70":1,"72":1,"75":1}}],["`90`",{"2":{"158":1}}],["`libgeos",{"2":{"188":1}}],["`linearring`",{"2":{"177":1}}],["`linestring`",{"2":{"177":1}}],["`linestringtrait`",{"2":{"165":1}}],["`line2`",{"2":{"145":1}}],["`line1`",{"2":{"145":1}}],["`line",{"2":{"72":4}}],["`lat`",{"2":{"158":1}}],["`lon`",{"2":{"158":1}}],["`z`",{"2":{"158":1}}],["``1",{"2":{"158":1}}],["``a``",{"2":{"158":1}}],["``r",{"2":{"158":1}}],["```jldoctest",{"2":{"69":1,"70":1,"72":1,"73":1,"75":1,"88":1,"91":1,"94":1,"104":1,"108":1,"119":1,"122":1,"125":1,"135":1,"145":1,"180":1}}],["```julia",{"2":{"6":2,"59":1,"105":1,"145":1,"153":1,"159":1,"185":2}}],["```math",{"2":{"59":1,"145":1}}],["```",{"2":{"11":1,"59":3,"69":1,"70":1,"72":1,"75":1,"88":1,"91":1,"94":1,"104":1,"108":1,"119":1,"122":1,"125":1,"135":1,"145":3,"146":11,"153":1,"180":1,"185":2,"189":1}}],["`6371008",{"2":{"158":1}}],["`prefilter",{"2":{"180":1}}],["`proj",{"2":{"172":1,"176":2}}],["`planar`",{"2":{"158":1,"177":2}}],["`polgontrait`",{"2":{"153":1}}],["`polygontrait`",{"2":{"153":1,"165":1}}],["`polygonize`",{"2":{"146":2}}],["`polygon`",{"2":{"59":3,"177":1}}],["`polys`",{"2":{"71":1}}],["`poly",{"2":{"71":3,"73":1,"76":1}}],["`pointrait`",{"2":{"180":1}}],["`pointtrait`",{"2":{"156":3,"165":1}}],["`point",{"2":{"116":3}}],["`point`",{"2":{"59":4,"85":1}}],["`point2f`",{"2":{"58":1}}],["`obj`",{"2":{"156":1,"185":1,"186":1}}],["`op`",{"2":{"154":2}}],["`calc",{"2":{"155":1}}],["`crs`",{"2":{"155":1}}],["`components`",{"2":{"156":1}}],["`collect`",{"2":{"154":1}}],["`covers`",{"2":{"94":1,"104":1}}],["`coveredby`",{"2":{"94":1,"104":1}}],["`contains`",{"2":{"91":1,"135":1}}],["`convex",{"2":{"82":2}}],["`+`",{"2":{"154":1}}],["`alg",{"2":{"188":1}}],["`alg`",{"2":{"184":1}}],["`always",{"2":{"172":1}}],["`application",{"2":{"165":1}}],["`apply`",{"2":{"153":1,"159":1}}],["`abstractgeometrytrait`",{"2":{"156":1}}],["`abstractarray`",{"2":{"156":1}}],["`abstractmatrix`",{"2":{"146":1}}],["`union",{"2":{"146":2}}],["`unionintersectingpolygons`",{"2":{"169":1}}],["`unionintersectingpolygons",{"2":{"70":1,"71":2,"72":1,"73":2,"75":1,"76":2}}],["`ys`",{"2":{"146":1}}],["`flattening`",{"2":{"176":1}}],["`flattening",{"2":{"176":1}}],["`flatten`",{"2":{"156":1}}],["`f",{"2":{"153":1}}],["`featurecollectiontrait`",{"2":{"153":1,"156":1}}],["`featurecollection`",{"2":{"146":2}}],["`featuretrait`",{"2":{"153":2,"156":1}}],["`feature`s",{"2":{"146":1}}],["`f`",{"2":{"146":5,"153":3,"154":1,"156":2,"185":2,"188":1}}],["`false`",{"2":{"145":1,"146":1,"155":4,"160":1,"172":1}}],["`fix",{"2":{"70":2,"71":2,"72":2,"73":2,"75":2,"76":2}}],["`douglaspeucker`",{"2":{"180":1}}],["`d`",{"2":{"172":1}}],["`difference`",{"2":{"169":1}}],["`disjoint`",{"2":{"119":1}}],["`delaunaytriangulation",{"2":{"82":1}}],["`within`",{"2":{"91":1,"135":1}}],["`weight`",{"2":{"59":1}}],["`geos`",{"2":{"188":3}}],["`geodesicsegments`",{"2":{"175":1}}],["`geodesic`",{"2":{"158":1,"176":2,"177":2}}],["`geointerface`",{"2":{"165":1}}],["`geointerface",{"2":{"153":1,"156":2,"172":2}}],["`geom`",{"2":{"85":3,"156":1,"177":1}}],["`geometrycollection`",{"2":{"177":1}}],["`geometrycorrection`",{"2":{"163":1,"165":1,"169":2}}],["`geometry`",{"2":{"153":1,"172":2}}],["`geometrybasics",{"2":{"59":3}}],["`geometries`",{"2":{"82":1}}],["`g1`",{"2":{"85":1}}],["`gi",{"2":{"82":1,"146":1}}],["`tuple",{"2":{"189":1}}],["`tuple`s",{"2":{"186":1}}],["`tuple`",{"2":{"146":1,"186":1}}],["`time`",{"2":{"172":1}}],["`transform`",{"2":{"172":1}}],["`true`",{"2":{"94":1,"105":1,"108":2,"125":1,"135":1,"145":2,"146":1,"155":2,"160":1,"172":1}}],["`threaded`",{"2":{"155":1}}],["`threaded==true`",{"2":{"154":1}}],["`tol`",{"2":{"180":2,"181":3,"182":2,"183":3,"184":4}}],["`to",{"2":{"73":1}}],["`target",{"2":{"172":3}}],["`target`",{"2":{"72":1,"153":2,"154":1,"156":1,"159":1}}],["`tables",{"2":{"153":1}}],["`taget`",{"2":{"70":1,"75":1}}],["`method",{"2":{"177":1}}],["`method`",{"2":{"59":3}}],["`max",{"2":{"176":4,"177":3,"188":1}}],["`map`",{"2":{"153":1}}],["`makie",{"2":{"146":1}}],["`multipointtrait`",{"2":{"153":1,"180":1}}],["`multipolygontrait`",{"2":{"153":1}}],["`multipolygon`",{"2":{"146":3,"177":1}}],["`multipolygon",{"2":{"71":3,"73":3,"76":2}}],["`multipoly",{"2":{"71":7,"73":3,"76":3}}],["`minpoints`",{"2":{"146":2}}],["`number`",{"2":{"180":2,"184":3}}],["`namedtuple`",{"2":{"153":1}}],["`nothing`",{"2":{"64":1,"155":1}}],["`next",{"2":{"64":1}}],["`boolsastypes`",{"2":{"160":1}}],["`bool`",{"2":{"146":2}}],["`buffer`",{"2":{"60":1}}],["`barycentric",{"2":{"59":3}}],["`radialdistance`",{"2":{"180":1}}],["`ratio`",{"2":{"180":2,"184":3}}],["`reproject`",{"2":{"173":1}}],["`rebuild`",{"2":{"156":1}}],["`r`",{"2":{"59":1}}],["`rᵢ`",{"2":{"59":1}}],["`svector`",{"2":{"185":3}}],["`simplifyalg`",{"2":{"180":2}}],["`simplify",{"2":{"180":1}}],["`simplify`",{"2":{"180":2}}],["`segmentize`",{"2":{"188":1}}],["`segmentize",{"2":{"177":2}}],["`semimajor",{"2":{"158":1}}],["`source",{"2":{"172":3}}],["`spherical`",{"2":{"158":1,"177":1}}],["`s`",{"2":{"59":1}}],["`sᵢ`",{"2":{"59":2}}],["`s2`",{"2":{"59":1}}],["`s1`",{"2":{"59":1}}],["`hcat`",{"2":{"59":1}}],["`x`",{"2":{"153":1,"160":1}}],["`xs`",{"2":{"146":1}}],["`x1",{"2":{"59":1}}],["`x1`",{"2":{"59":2}}],["`x2`",{"2":{"59":1}}],["`visvalingamwhyatt`",{"2":{"180":1}}],["`vector",{"2":{"172":1}}],["`vector`",{"2":{"146":1,"153":1}}],["`vᵢ`",{"2":{"59":1}}],["`v`",{"2":{"59":1}}],["`values`",{"2":{"59":1,"146":1}}],["`λs`",{"2":{"59":2}}],["`",{"2":{"59":2,"70":1,"71":2,"72":1,"73":3,"75":1,"76":2,"146":3,"153":2,"154":1,"158":1,"165":2,"172":2,"177":1,"180":3,"188":2,"189":1}}],["λ₁",{"2":{"59":2}}],["λn",{"2":{"57":1}}],["λ3",{"2":{"57":1}}],["λ2",{"2":{"57":2}}],["λ1",{"2":{"57":2}}],["λs",{"2":{"5":3,"6":4,"59":27}}],["π",{"2":{"53":1,"180":1}}],["δbay",{"2":{"73":3}}],["δbax",{"2":{"73":3}}],["δby",{"2":{"73":5}}],["δbx",{"2":{"73":5}}],["δb",{"2":{"73":2}}],["δay",{"2":{"73":5}}],["δax",{"2":{"73":5}}],["δa",{"2":{"73":2}}],["δintrs",{"2":{"64":2}}],["δy2",{"2":{"145":2}}],["δy1",{"2":{"145":2}}],["δyl",{"2":{"122":4}}],["δy",{"2":{"53":8,"66":3,"116":7}}],["δys",{"2":{"53":1}}],["δx2",{"2":{"145":2}}],["δx1",{"2":{"145":2}}],["δxl",{"2":{"122":4}}],["δx",{"2":{"53":9,"66":3,"116":7}}],["∈",{"2":{"14":1}}],["~",{"2":{"14":3}}],["$ratio",{"2":{"184":1}}],["$rectangle",{"2":{"176":2}}],["$number",{"2":{"184":1}}],["$name",{"2":{"31":2}}],["$min",{"2":{"184":1}}],["$douglas",{"2":{"180":1,"182":1}}],["$simplify",{"2":{"180":1,"181":1,"183":1}}],["$lg",{"2":{"176":1}}],["$lin",{"2":{"176":2}}],["$geom",{"2":{"180":8}}],["$geo",{"2":{"176":1}}],["$calc",{"2":{"155":1}}],["$crs",{"2":{"155":1,"170":1}}],["$apply",{"2":{"153":1,"171":1,"172":1,"180":1}}],["$tol",{"2":{"184":1}}],["$threaded",{"2":{"155":1}}],["$t",{"2":{"88":1,"153":2,"154":2}}],["$target",{"2":{"71":1,"73":1,"76":1,"156":3}}],["$trait",{"2":{"69":1,"71":2,"73":2,"76":2}}],["$",{"2":{"13":4,"14":1,"59":4,"64":2,"146":3,"165":4,"177":4,"180":12,"188":1}}],["|=",{"2":{"116":4,"122":1}}],["||",{"2":{"53":2,"64":13,"66":8,"69":1,"72":1,"73":5,"75":1,"88":18,"105":2,"116":8,"122":1,"127":1,"146":7,"156":1,"182":1,"184":3}}],["|",{"2":{"11":4,"116":1}}],["|>",{"2":{"11":8,"13":2,"156":3,"175":1,"176":1,"180":3,"199":2}}],["↩︎",{"2":{"6":1}}],["ᵢᵢᵢ₊₁ᵢᵢ₊₁ᵢᵢ₊₁tᵢ=det",{"2":{"6":1}}],["⋅",{"2":{"6":1,"59":1}}],["qy",{"2":{"13":2,"14":2}}],["qx",{"2":{"13":2,"14":2}}],["q",{"2":{"13":13,"14":12,"64":4}}],["qhull",{"2":{"6":1,"82":1}}],["queue",{"2":{"182":41}}],["questions",{"2":{"73":1}}],["quite",{"2":{"162":1,"199":1}}],["quickhull",{"2":{"6":1,"77":1,"82":2}}],["quick",{"2":{"4":1,"6":1,"59":1,"170":1,"179":1}}],["quality",{"2":{"6":1,"180":1}}],["quantity",{"2":{"6":1,"176":1}}],["quot",{"2":{"1":4,"6":6,"20":2,"64":8,"84":2,"85":2,"88":2,"103":4,"116":10,"150":4,"151":2,"153":2,"154":2,"174":6,"191":2,"198":2}}],["zone",{"2":{"192":1}}],["zoom",{"2":{"14":1}}],["zs",{"2":{"146":5}}],["zip",{"2":{"13":1,"14":1,"191":4,"192":1,"193":1}}],["zeros",{"2":{"59":1,"66":1}}],["zero",{"2":{"4":6,"6":8,"56":11,"63":3,"64":4,"66":11,"73":22,"84":1,"85":3,"145":1,"153":1,"182":4}}],["z",{"2":{"4":1,"5":1,"6":2,"59":3,"88":3,"146":3,"158":1,"171":1,"185":1,"186":1}}],["0e6",{"2":{"192":6}}],["0example",{"2":{"6":1}}],["097075198097933",{"2":{"193":1}}],["09707519809793252",{"2":{"193":2}}],["091887951911644",{"2":{"193":3}}],["0999933334666654",{"2":{"191":1}}],["09801605542096",{"2":{"191":1}}],["098016055420953",{"2":{"191":3}}],["09297443860091348",{"2":{"191":4}}],["09",{"2":{"58":1}}],["08506974233813636",{"2":{"193":2}}],["08",{"2":{"58":1}}],["062749678615475",{"2":{"193":1}}],["06274967861547665",{"2":{"193":2}}],["06592462566760626",{"2":{"191":1}}],["0650624499034016",{"2":{"191":4}}],["06",{"2":{"58":1}}],["02017324484778",{"2":{"193":1}}],["020173244847778715",{"2":{"193":2}}],["027886421973952302",{"2":{"191":4}}],["02",{"2":{"58":3}}],["04500741774392",{"2":{"193":1}}],["045007417743918",{"2":{"193":2}}],["049999166670833324",{"2":{"191":1}}],["0438052480035",{"2":{"191":1}}],["043805248003498",{"2":{"191":3}}],["04",{"2":{"58":6}}],["07518688541961",{"2":{"193":1}}],["075186885419612",{"2":{"193":2}}],["071",{"2":{"175":2,"176":2}}],["07",{"2":{"58":6,"175":6,"176":6}}],["009176636029576",{"2":{"193":1}}],["0091766360295773",{"2":{"193":2}}],["003135308800957",{"2":{"193":1}}],["0031353088009582475",{"2":{"193":2}}],["0035114210915891397",{"2":{"191":4}}],["006784125578492062",{"2":{"193":2}}],["0020133807972559925",{"2":{"193":2}}],["00839489109211",{"2":{"193":3}}],["008696",{"2":{"3":1,"6":3,"72":1,"73":1,"118":1,"119":1}}],["005465967083412071",{"2":{"191":4}}],["00111595449914",{"2":{"191":1}}],["001115954499138",{"2":{"191":3}}],["0010075412835199304",{"2":{"191":4}}],["001",{"2":{"180":1}}],["00085222666982",{"2":{"193":3}}],["000577332369005",{"2":{"193":1}}],["0005773323690041465",{"2":{"193":2}}],["000510363870095e6",{"2":{"192":2}}],["00025191811248184703",{"2":{"193":2}}],["000215611503127e6",{"2":{"192":2}}],["0007260527263e6",{"2":{"192":2}}],["000342160541625e6",{"2":{"192":2}}],["000124843834609e6",{"2":{"192":2}}],["000063948817746e6",{"2":{"192":2}}],["000026987852369e6",{"2":{"192":2}}],["000008144045314",{"2":{"193":1}}],["000007998400139e6",{"2":{"192":2}}],["000000999950001e6",{"2":{"192":2}}],["00001e6",{"2":{"192":1}}],["0004397316773170068",{"2":{"191":4}}],["000",{"2":{"180":1,"196":1}}],["00",{"2":{"58":2}}],["053798628882221644",{"2":{"193":2}}],["05877989361332",{"2":{"191":1}}],["058779893613323",{"2":{"191":3}}],["05416726609360478",{"2":{"191":4}}],["05",{"2":{"58":1}}],["052704767595",{"2":{"15":1}}],["037564867762832",{"2":{"193":1}}],["03756486776283019",{"2":{"193":2}}],["031245035570328428",{"2":{"193":2}}],["033518309870985",{"2":{"193":3}}],["03503632062070827",{"2":{"191":4}}],["03",{"2":{"58":4}}],["01458815628695",{"2":{"193":3}}],["016044338630866517",{"2":{"193":2}}],["01592650896568995",{"2":{"191":1}}],["01597247419241532",{"2":{"191":4}}],["01908693278165",{"2":{"191":1}}],["019086932781654",{"2":{"191":3}}],["01098781325325",{"2":{"191":1}}],["010987813253244",{"2":{"191":3}}],["011814947665167774",{"2":{"191":4}}],["01362848005",{"2":{"15":1}}],["01",{"2":{"14":1,"58":1,"175":1,"176":1,"191":1,"192":1,"193":1}}],["0^",{"2":{"13":2,"14":2}}],["0+2",{"2":{"13":2,"14":2}}],["0",{"2":{"3":35,"4":12,"6":190,"11":1,"13":5,"14":8,"15":32,"52":20,"53":3,"55":9,"56":4,"58":155,"59":2,"62":6,"63":6,"64":49,"65":13,"66":5,"68":22,"69":49,"70":42,"72":2,"73":21,"75":46,"76":2,"80":1,"84":20,"85":2,"87":13,"88":18,"90":16,"93":6,"94":6,"103":6,"104":12,"105":9,"107":15,"116":37,"121":13,"122":9,"124":12,"125":11,"134":16,"145":21,"146":7,"158":1,"162":24,"168":64,"169":4,"175":16,"176":13,"177":2,"180":11,"182":7,"184":4,"189":2,"191":129,"192":2,"193":46,"198":13}}],[">=",{"2":{"59":6,"105":4,"122":1,"184":1}}],[">geometryops",{"2":{"6":2}}],[">",{"2":{"1":1,"6":6,"11":2,"13":1,"15":2,"50":1,"53":1,"59":1,"64":17,"66":3,"69":5,"70":1,"71":1,"73":2,"75":1,"76":3,"84":1,"88":1,"105":8,"116":10,"122":2,"145":4,"146":12,"156":6,"165":1,"169":3,"177":3,"180":1,"182":12,"184":3,"185":1,"189":1,"199":1}}],["914930257661865",{"2":{"193":1}}],["96875496442967",{"2":{"193":1}}],["961329",{"2":{"3":1,"6":3,"72":1,"73":1,"118":1,"119":1}}],["983955661369134",{"2":{"193":1}}],["9833",{"2":{"15":1}}],["9877550012664",{"2":{"191":1}}],["9840085315131",{"2":{"191":1}}],["98271048511609",{"2":{"191":1}}],["98661575256801",{"2":{"191":1}}],["99321587442151",{"2":{"193":1}}],["99375130197483",{"2":{"191":1}}],["997986619202745",{"2":{"193":1}}],["997750168744936",{"2":{"191":1}}],["997247091122496",{"2":{"191":1}}],["99600053330489",{"2":{"191":1}}],["991002699676024",{"2":{"191":1}}],["990022362600165",{"2":{"191":1}}],["99292997455441",{"2":{"191":1}}],["99533829767195",{"2":{"191":1}}],["99865616402829",{"2":{"191":1}}],["999999967681458e6",{"2":{"192":2}}],["999997707902938e6",{"2":{"192":2}}],["999987539891298e6",{"2":{"192":2}}],["999963474314044e6",{"2":{"192":2}}],["999919535736425e6",{"2":{"192":2}}],["999974634566875",{"2":{"191":1}}],["999849768598615e6",{"2":{"192":2}}],["999748081887518",{"2":{"193":1}}],["999748243174828e6",{"2":{"192":2}}],["999750002083324",{"2":{"191":1}}],["999609061508909e6",{"2":{"192":2}}],["999426363321033e6",{"2":{"192":2}}],["999194331880103e6",{"2":{"192":2}}],["99900003333289",{"2":{"191":1}}],["999565375483215",{"2":{"191":1}}],["97976366505997",{"2":{"191":1}}],["9783069507679",{"2":{"191":1}}],["97",{"2":{"58":1}}],["946201371117777",{"2":{"193":1}}],["94",{"2":{"58":1}}],["92",{"2":{"58":1}}],["9im",{"0":{"39":1},"1":{"40":1,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1,"47":1,"48":1,"49":1},"2":{"116":2,"197":1}}],["95770326033",{"2":{"15":1}}],["95",{"2":{"13":1,"14":1,"196":1}}],["900",{"2":{"176":1}}],["90063612163",{"2":{"11":2}}],["90`",{"2":{"158":1}}],["90",{"2":{"52":9,"58":1}}],["909318372607",{"2":{"11":3}}],["9f0e22db49f7b49d5352750e9f69705f13c06d64",{"2":{"6":2}}],["9",{"2":{"1":1,"6":1,"116":2,"145":12,"175":1,"185":1}}],["897070",{"2":{"196":1}}],["873633668827033",{"2":{"193":1}}],["8135804051007",{"2":{"191":1}}],["813580405100698",{"2":{"191":3}}],["88",{"2":{"58":1}}],["86641841658641",{"2":{"191":1}}],["866418416586406",{"2":{"191":3}}],["86",{"2":{"58":1}}],["868447876892",{"2":{"15":2}}],["84",{"2":{"58":1}}],["821068835162155",{"2":{"193":1}}],["82",{"2":{"58":1}}],["800",{"2":{"58":1}}],["80000",{"2":{"15":5}}],["80",{"2":{"58":1}}],["80869813739",{"2":{"15":2}}],["856614689791036e",{"2":{"15":2}}],["83572303404496",{"2":{"6":2,"72":1,"73":1}}],["8",{"2":{"1":3,"6":9,"13":1,"14":1,"70":2,"75":4,"116":1,"146":3,"158":2,"175":1,"185":3,"193":2}}],["karnataka",{"2":{"199":1}}],["kbn",{"2":{"11":2}}],["kinds",{"2":{"23":1}}],["kind",{"2":{"9":1,"146":1,"197":1}}],["k",{"2":{"6":1,"14":6,"59":1,"64":5,"116":5,"191":5,"193":5}}],["kernel",{"2":{"177":4}}],["keepat",{"2":{"75":1,"169":2}}],["keep",{"2":{"64":3,"146":1,"169":14,"175":1,"198":1}}],["keeping",{"2":{"6":1,"153":1,"171":1}}],["keys",{"2":{"146":4,"153":1}}],["key",{"2":{"6":1,"146":4,"153":2,"188":5}}],["keyword",{"0":{"155":1},"2":{"6":8,"23":1,"31":3,"70":1,"72":1,"75":1,"153":1,"155":7,"170":2,"176":2,"188":4,"192":1,"193":1}}],["keywords",{"2":{"1":2,"4":1,"6":10,"31":1,"116":2,"146":1,"153":1,"155":1,"170":1,"171":2,"172":3,"180":8,"181":1,"182":1,"183":1,"184":1,"186":2}}],["known",{"2":{"66":2}}],["know",{"2":{"6":3,"53":2,"64":1,"66":1,"70":1,"72":1,"73":2,"75":1,"76":1,"153":1,"198":1}}],["kwargs",{"2":{"32":2,"60":3,"66":1,"69":1,"70":3,"71":3,"72":4,"73":5,"75":3,"76":5,"165":2,"173":1,"176":1}}],["kwdef",{"2":{"31":1,"64":1,"158":2,"176":1,"181":1,"182":1,"183":1}}],["kw",{"2":{"1":1,"6":10,"116":10,"146":20,"150":1,"153":18,"154":3,"156":2,"171":3,"180":15,"183":1,"185":3,"186":3,"188":6}}],["json",{"2":{"195":3}}],["jstep",{"2":{"88":3}}],["jstart",{"2":{"88":7}}],["joined",{"2":{"198":4}}],["joins",{"0":{"197":1},"1":{"198":1,"199":1,"200":1},"2":{"197":3}}],["joinpath",{"2":{"180":2}}],["join",{"2":{"153":1,"154":1,"197":4,"198":5,"200":1}}],["joining",{"2":{"6":2,"180":1,"197":1}}],["jpn",{"2":{"199":2}}],["jp",{"2":{"88":2}}],["jhole",{"2":{"88":2}}],["jh",{"2":{"76":5}}],["j+1",{"2":{"64":1,"146":1,"184":1}}],["j",{"2":{"64":24,"88":8,"105":9,"116":12,"145":2,"146":8,"184":2}}],["jet",{"2":{"58":1}}],["just",{"2":{"4":1,"6":1,"32":1,"63":1,"64":2,"73":4,"76":1,"81":1,"85":2,"146":1,"151":1,"153":2,"154":1,"157":1,"176":1,"177":1,"197":1}}],["julialand",{"2":{"192":2}}],["julialines",{"2":{"55":1}}],["julialinearsegments",{"2":{"6":1}}],["juliahole",{"2":{"191":1}}],["juliaplot",{"2":{"192":2}}],["juliap1",{"2":{"191":1}}],["juliapoly",{"2":{"192":1}}],["juliapolygon3",{"2":{"192":1}}],["juliapolygon1",{"2":{"191":1}}],["juliapolygonize",{"2":{"6":1}}],["juliapolygon",{"2":{"6":1}}],["juliapoints",{"2":{"198":1}}],["juliapoint",{"2":{"191":1}}],["juliaxoffset",{"2":{"191":3,"193":1}}],["juliax",{"2":{"191":2}}],["juliaxrange",{"2":{"84":1}}],["julia$apply",{"2":{"186":1}}],["julia$threaded",{"2":{"170":1}}],["julia6",{"2":{"180":1}}],["julia```jldoctest",{"2":{"189":1}}],["julia```julia",{"2":{"146":1}}],["julia```",{"2":{"105":1,"159":1}}],["julia1",{"2":{"70":1,"72":1,"73":1,"75":1}}],["julia2",{"2":{"69":1}}],["juliabase",{"2":{"59":1,"85":4,"153":1,"188":1}}],["juliabarycentric",{"2":{"5":3,"6":3}}],["juliafig",{"2":{"191":1,"192":1}}],["juliaflexijoins",{"2":{"200":1}}],["juliaflatten",{"2":{"156":1}}],["juliaflipped",{"2":{"148":1}}],["juliaflip",{"2":{"6":1}}],["juliafalse",{"2":{"145":1}}],["juliafunction",{"2":{"32":1,"35":1,"36":1,"37":1,"38":1,"40":1,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1,"47":1,"48":1,"49":1,"50":1,"53":2,"59":3,"60":2,"63":1,"64":11,"66":2,"69":1,"71":1,"73":2,"76":1,"82":1,"85":8,"116":6,"122":2,"127":1,"147":1,"154":2,"156":4,"173":1,"176":1,"177":1,"184":2}}],["juliafor",{"2":{"31":1,"153":1,"154":1}}],["juliagi",{"2":{"11":2}}],["juliago",{"2":{"11":1,"52":1,"55":1,"65":1,"87":1,"90":1,"93":1,"103":1,"107":1,"118":1,"121":1,"124":1,"134":1,"197":1}}],["juliageopoly1",{"2":{"193":1}}],["juliageointerface",{"2":{"189":1}}],["juliageometry",{"2":{"82":1}}],["juliageo",{"2":{"6":2,"158":1}}],["juliageodesicsegments",{"2":{"6":1}}],["juliageos",{"2":{"6":1}}],["juliaweighted",{"2":{"6":1}}],["juliawithin",{"2":{"3":1,"6":1}}],["juliaunwrap",{"2":{"156":7}}],["juliaunion",{"2":{"6":1}}],["juliaunionintersectingpolygons",{"2":{"6":1,"166":1}}],["juliausing",{"2":{"6":1,"13":1,"14":1,"58":1,"175":2,"176":1,"179":1,"180":1,"194":1,"197":1}}],["juliascatter",{"2":{"198":1}}],["juliasource",{"2":{"192":2}}],["juliasimplify",{"2":{"6":1,"180":3}}],["juliasigned",{"2":{"4":2,"6":2}}],["juliasegmentize",{"2":{"6":1,"177":1}}],["julias1",{"2":{"6":1}}],["juliavisvalingamwhyatt",{"2":{"6":1}}],["juliaring3",{"2":{"192":1}}],["juliaring1",{"2":{"191":1}}],["juliar",{"2":{"191":2,"192":1,"193":1}}],["juliarebuild",{"2":{"156":1}}],["juliareconstruct",{"2":{"156":1}}],["juliareproject",{"2":{"1":1}}],["juliaradialdistance",{"2":{"6":1}}],["juliamy",{"2":{"200":1}}],["juliamultipoly",{"2":{"180":1}}],["juliamodule",{"2":{"31":1}}],["juliamonotonechainmethod",{"2":{"6":1}}],["juliameanvalue",{"2":{"6":1}}],["juliadf",{"2":{"194":1}}],["juliadestination",{"2":{"192":1}}],["juliadouglaspeucker",{"2":{"6":1}}],["juliadifference",{"2":{"6":1}}],["juliadiffintersectingpolygons",{"2":{"6":1,"166":1}}],["juliadistance",{"2":{"4":1,"6":1}}],["juliadisjoint",{"2":{"3":1,"6":1}}],["juliaexport",{"2":{"51":1,"54":1,"57":1,"61":1,"67":1,"70":1,"72":1,"75":1,"83":1,"86":1,"89":1,"92":1,"102":1,"106":1,"117":1,"120":1,"123":1,"133":1,"143":1,"146":1,"148":1,"154":1,"158":1,"161":1,"164":1,"167":1,"172":1,"174":1,"180":1,"187":1}}],["juliaend",{"2":{"85":1}}],["juliaenforce",{"2":{"6":1}}],["juliaenum",{"2":{"6":2}}],["juliaembed",{"2":{"4":1,"6":1}}],["juliaequals",{"2":{"4":15,"6":15,"64":1}}],["juliaaccuratearithmetic",{"2":{"11":2}}],["juliaabstract",{"2":{"6":3,"165":1,"166":1,"176":1}}],["juliaangles",{"2":{"4":1,"6":1}}],["juliaarea",{"2":{"4":1,"6":1}}],["juliaapplyreduce",{"2":{"1":1,"150":1}}],["juliaapply",{"2":{"1":1,"18":1,"150":1}}],["juliacent",{"2":{"62":1}}],["juliacentroid",{"2":{"4":1,"6":3}}],["juliacut",{"2":{"6":1}}],["juliaclosedring",{"2":{"6":1,"166":1}}],["juliaconst",{"2":{"53":1,"56":1,"59":1,"64":1,"66":2,"85":1,"94":1,"108":1,"125":1,"135":1,"155":1}}],["juliaconvex",{"2":{"6":1,"82":1}}],["juliacontains",{"2":{"3":1,"6":1}}],["juliacoverage",{"2":{"6":1}}],["juliacovers",{"2":{"3":1,"6":1}}],["juliacoveredby",{"2":{"3":1,"6":1}}],["juliatraittarget",{"2":{"159":1}}],["juliatransform",{"2":{"1":1,"6":1}}],["juliatrue",{"2":{"88":1,"91":1,"94":1,"104":1,"108":1,"119":1,"122":1,"125":1,"135":1,"145":1}}],["juliatuples",{"2":{"6":1}}],["juliat",{"2":{"6":1}}],["juliatouches",{"2":{"3":1,"6":1}}],["juliaoverlaps",{"2":{"3":9,"6":9}}],["juliainnerjoin",{"2":{"199":1}}],["juliaintersection",{"2":{"6":2}}],["juliaintersects",{"2":{"3":1}}],["juliaisconcave",{"2":{"6":1}}],["juliaisclockwise",{"2":{"6":1}}],["juliaimport",{"2":{"1":1,"3":9,"4":1,"6":17,"11":1,"32":1,"52":1,"55":1,"62":1,"65":1,"68":1,"79":1,"80":1,"81":1,"84":1,"87":1,"90":1,"93":1,"103":1,"107":1,"118":1,"121":1,"124":1,"134":1,"150":1,"162":2,"168":2,"175":1,"195":4,"196":1,"198":1,"199":1}}],["julia",{"2":{"3":1,"6":5,"9":1,"15":1,"25":1,"27":1,"31":1,"32":2,"53":4,"56":10,"59":8,"63":19,"64":60,"66":18,"69":10,"70":6,"71":1,"72":5,"73":29,"75":6,"76":4,"77":1,"82":5,"84":1,"85":4,"88":15,"91":1,"95":1,"96":5,"97":1,"98":1,"99":2,"100":1,"101":1,"104":1,"105":3,"109":1,"110":3,"111":1,"112":1,"113":1,"114":1,"115":1,"116":50,"119":1,"122":7,"126":1,"127":3,"128":1,"129":1,"130":1,"131":1,"132":1,"136":1,"137":3,"138":1,"139":1,"140":2,"141":1,"142":1,"145":9,"146":40,"147":2,"153":44,"154":18,"156":10,"159":1,"160":1,"163":5,"165":1,"169":3,"170":1,"171":1,"172":1,"176":1,"177":2,"180":1,"181":2,"182":15,"183":3,"185":1,"186":1,"187":1,"188":4,"189":1,"190":2,"192":2,"195":2,"198":1}}],["julia>",{"2":{"1":5,"6":7,"145":3,"185":7}}],["juliajulia>",{"2":{"1":2,"6":3}}],["jl`",{"2":{"82":1,"172":1,"188":1}}],["jlmethod",{"2":{"6":2}}],["jlobjecttype",{"2":{"6":2}}],["jlbinding",{"2":{"6":2}}],["jldocstring",{"2":{"6":2}}],["jldoctest",{"2":{"6":1}}],["jl",{"0":{"25":1,"31":1},"2":{"1":10,"4":1,"6":15,"10":2,"11":1,"22":2,"25":2,"27":2,"31":42,"32":1,"50":1,"53":1,"56":1,"59":1,"60":3,"63":1,"64":1,"66":1,"69":1,"71":1,"73":1,"74":1,"76":1,"77":3,"82":6,"85":1,"88":1,"91":1,"101":1,"104":1,"105":1,"115":1,"116":1,"119":1,"122":1,"132":1,"142":1,"145":2,"146":1,"147":1,"153":2,"154":1,"155":1,"156":3,"158":1,"160":2,"163":1,"166":1,"169":1,"170":2,"171":1,"172":4,"173":3,"174":1,"176":3,"177":2,"180":2,"184":1,"185":6,"186":2,"188":2,"189":1,"192":1,"197":1}}],["+5000000",{"2":{"192":1}}],["+proj=natearth2",{"2":{"192":2}}],["+=",{"2":{"56":2,"59":11,"63":6,"64":17,"66":10,"69":1,"88":1,"105":1,"116":5,"145":1,"169":2,"182":4,"189":3}}],["+",{"2":{"1":1,"6":1,"13":1,"14":1,"53":4,"56":1,"59":20,"63":10,"64":15,"66":7,"69":1,"73":10,"85":3,"88":2,"105":2,"116":6,"145":6,"146":2,"150":1,"169":3,"177":2,"182":3,"183":2,"184":2,"191":6,"192":1,"193":4}}],["yticklabelsvisible",{"2":{"192":1}}],["york",{"2":{"199":1}}],["yoffset",{"2":{"191":7,"193":2}}],["your",{"0":{"195":1},"2":{"18":1,"60":1,"146":1,"148":1,"173":1,"176":1,"194":1,"195":1,"200":2}}],["you",{"0":{"23":1},"2":{"1":1,"4":1,"5":2,"6":9,"9":1,"11":1,"17":1,"18":5,"23":1,"29":2,"53":1,"56":1,"59":8,"60":1,"63":1,"70":1,"72":1,"75":1,"81":1,"82":1,"148":1,"153":1,"154":1,"159":1,"162":1,"168":1,"173":1,"175":1,"176":1,"185":1,"192":1,"194":2,"195":3,"198":2,"199":3,"200":4}}],["y=y",{"2":{"189":1}}],["yvec",{"2":{"146":4}}],["ybounds",{"2":{"146":4}}],["yhalf",{"2":{"146":2}}],["ylast",{"2":{"85":3}}],["yfirst",{"2":{"85":5}}],["y0",{"2":{"85":5}}],["yw",{"2":{"66":4}}],["ye",{"2":{"66":4}}],["yet",{"0":{"147":1},"2":{"31":1,"59":1,"69":1,"71":1,"73":1,"76":1,"88":1,"146":1,"147":1,"165":2,"199":1}}],["y2",{"2":{"63":2,"66":18,"85":7,"105":10,"116":5,"122":4,"146":5,"177":6,"189":2}}],["y1",{"2":{"63":2,"66":22,"85":7,"105":11,"116":6,"122":5,"146":5,"177":7,"189":2}}],["yind+1",{"2":{"146":1}}],["yind",{"2":{"146":2}}],["yinterior",{"2":{"63":2}}],["yield",{"2":{"73":1,"176":1}}],["yi+yi−1",{"2":{"6":1}}],["ycentroid",{"2":{"63":13}}],["yrange",{"2":{"58":3,"84":3}}],["yautolimits",{"2":{"58":2}}],["yp2",{"2":{"53":4}}],["ys",{"2":{"6":3,"146":30}}],["ymax",{"2":{"6":1,"65":2,"66":48}}],["ymin",{"2":{"6":1,"65":2,"66":49}}],["y",{"2":{"1":2,"4":1,"6":3,"13":9,"14":10,"53":8,"56":2,"58":8,"62":1,"63":10,"64":6,"65":1,"66":8,"71":6,"73":8,"84":2,"85":7,"88":3,"105":21,"116":16,"122":7,"145":10,"148":2,"150":1,"153":1,"171":4,"172":1,"177":2,"184":1,"185":2,"186":2,"189":7,"191":8,"192":2,"193":2,"197":1}}],["75",{"2":{"84":2,"90":1,"107":1,"134":1}}],["78",{"2":{"58":1}}],["749907",{"2":{"196":1}}],["74",{"2":{"58":1}}],["72",{"2":{"58":1}}],["726711609794",{"2":{"15":1}}],["76",{"2":{"58":1}}],["76085",{"2":{"15":1}}],["768946",{"2":{"3":1,"6":3,"72":1,"73":1,"118":1,"119":1}}],["70440582002419",{"2":{"191":1}}],["704405820024185",{"2":{"191":3}}],["704377648755",{"2":{"15":2}}],["700",{"2":{"14":1}}],["700454",{"2":{"6":1,"179":1,"180":1}}],["701141",{"2":{"6":1,"179":1,"180":1}}],["70",{"2":{"6":20,"58":1,"179":20,"180":20,"191":20}}],["738281",{"2":{"3":1,"6":3,"72":1,"73":1,"118":1,"119":1}}],["7",{"2":{"1":5,"6":4,"116":1,"150":1,"153":1,"175":4,"176":4,"185":4,"193":1}}],["65533525026046",{"2":{"191":1}}],["655335250260467",{"2":{"191":3}}],["659942",{"2":{"6":1,"179":1,"180":1}}],["6378137",{"2":{"158":1}}],["6371008",{"2":{"158":1}}],["639343",{"2":{"6":1,"179":1,"180":1}}],["6area",{"2":{"63":2}}],["66",{"2":{"58":1}}],["668869",{"2":{"6":1,"179":1,"180":1}}],["64744840486518",{"2":{"193":3}}],["64",{"2":{"58":1,"175":2,"176":2}}],["646209",{"2":{"6":1,"179":1,"180":1}}],["629",{"2":{"192":1}}],["62",{"2":{"58":1}}],["624923",{"2":{"6":1,"179":1,"180":1}}],["61366192682",{"2":{"15":1}}],["614624",{"2":{"6":1,"179":1,"180":1}}],["605000000000004",{"2":{"175":2}}],["60",{"2":{"58":1,"175":2}}],["60000",{"2":{"15":3}}],["609817",{"2":{"6":1,"179":1,"180":1}}],["603637",{"2":{"6":2,"179":2,"180":2}}],["68",{"2":{"58":1}}],["682601",{"2":{"6":1,"179":1,"180":1}}],["683975",{"2":{"6":1,"179":1,"180":1}}],["69159119078359",{"2":{"193":3}}],["694274",{"2":{"6":1,"179":1,"180":1}}],["697021",{"2":{"6":1,"179":1,"180":1}}],["6",{"2":{"1":12,"3":4,"6":20,"70":3,"75":3,"116":1,"122":4,"150":3,"153":3,"180":1,"185":9,"193":2}}],["51695367760999",{"2":{"193":1}}],["516953677609987",{"2":{"193":2}}],["51030066635026",{"2":{"191":4}}],["5e6",{"2":{"192":2}}],["55715336218991",{"2":{"193":1}}],["557153362189904",{"2":{"193":2}}],["55",{"2":{"191":3}}],["55494217175954",{"2":{"191":4}}],["57",{"2":{"175":4,"176":4}}],["57725",{"2":{"15":2}}],["5x",{"2":{"153":1}}],["563198",{"2":{"73":1}}],["56",{"2":{"58":1}}],["54",{"2":{"58":1,"191":19}}],["50",{"2":{"58":1,"175":4,"176":4,"191":3}}],["500000",{"2":{"192":1}}],["50000",{"2":{"15":1}}],["500",{"2":{"14":1,"192":1}}],["5d",{"2":{"25":1,"27":1,"158":1}}],["52",{"2":{"58":1,"196":1}}],["52521",{"2":{"15":1}}],["52709",{"2":{"15":2}}],["594711",{"2":{"6":1,"179":1,"180":1}}],["590591",{"2":{"6":1,"179":1,"180":1}}],["595397",{"2":{"6":1,"179":1,"180":1}}],["535",{"2":{"175":4}}],["5355",{"2":{"175":2}}],["53333",{"2":{"15":4}}],["53",{"2":{"6":10,"175":2,"189":10}}],["58",{"2":{"58":1}}],["58059",{"2":{"15":2}}],["587158",{"2":{"6":2,"179":2,"180":2}}],["58375366067548",{"2":{"6":2,"72":1,"73":1}}],["584961",{"2":{"3":1,"6":3,"72":1,"73":1,"118":1,"119":1}}],["5",{"2":{"1":29,"3":4,"4":8,"6":82,"13":1,"58":1,"68":5,"69":8,"70":16,"75":18,"84":8,"88":8,"116":1,"122":4,"146":1,"150":2,"153":2,"175":1,"176":1,"185":27,"191":32,"192":25,"200":1}}],["4983491639274692e6",{"2":{"192":2}}],["4986507085647392e6",{"2":{"192":2}}],["497205585568957e6",{"2":{"192":2}}],["4976022389592e6",{"2":{"192":2}}],["4957639801366436e6",{"2":{"192":2}}],["4940253560034204e6",{"2":{"192":2}}],["4946113281484335e6",{"2":{"192":2}}],["491990928929295e6",{"2":{"192":2}}],["4904357734399722e6",{"2":{"192":2}}],["4926709788709967e6",{"2":{"192":2}}],["4962554647802354e6",{"2":{"192":2}}],["499984780817334e6",{"2":{"192":2}}],["4997392479570867e6",{"2":{"192":2}}],["4991939151049731e6",{"2":{"192":2}}],["4994001399837343e6",{"2":{"192":2}}],["4998500087497458e6",{"2":{"192":2}}],["49",{"2":{"146":1}}],["43541888381864",{"2":{"193":3}}],["4326",{"2":{"192":2,"193":3}}],["43787",{"2":{"15":1}}],["439295815226",{"2":{"15":1}}],["434306",{"2":{"6":1,"179":1,"180":1}}],["4896621210021754e6",{"2":{"192":2}}],["489271",{"2":{"6":4,"189":4}}],["4870405593989636e6",{"2":{"192":2}}],["4879072738504685e6",{"2":{"192":2}}],["484003",{"2":{"145":4}}],["482551",{"2":{"145":4}}],["48268",{"2":{"15":1}}],["48",{"2":{"58":1}}],["48001",{"2":{"15":1}}],["45",{"2":{"58":2,"145":12,"191":2}}],["450",{"2":{"13":1}}],["458369",{"2":{"6":2,"179":2,"180":2}}],["42004014766201",{"2":{"191":1}}],["420040147662014",{"2":{"191":3}}],["4219350464667047e",{"2":{"191":4}}],["42",{"2":{"13":1,"14":1,"58":3}}],["426283",{"2":{"6":1,"179":1,"180":1}}],["400",{"2":{"58":3}}],["40000",{"2":{"15":1}}],["40",{"2":{"14":1,"58":3}}],["406224",{"2":{"6":1,"179":1,"180":1}}],["404504",{"2":{"6":1,"179":1,"180":1}}],["41544701408748197",{"2":{"191":1}}],["41",{"2":{"58":1,"193":20}}],["41878",{"2":{"15":1}}],["414248",{"2":{"6":1,"179":1,"180":1}}],["419406",{"2":{"6":1,"179":1,"180":1}}],["4493927459900552",{"2":{"191":1}}],["44121252392",{"2":{"15":1}}],["44",{"2":{"14":1,"58":2}}],["442901",{"2":{"6":1,"179":1,"180":1}}],["446339",{"2":{"6":1,"179":1,"180":1}}],["477985",{"2":{"145":4}}],["47",{"2":{"58":3}}],["473835",{"2":{"6":1,"179":1,"180":1}}],["472117",{"2":{"6":2,"179":2,"180":2}}],["46525251631344455",{"2":{"191":1}}],["465816",{"2":{"6":1,"179":1,"180":1}}],["46",{"2":{"58":3}}],["468107",{"2":{"6":1,"179":1,"180":1}}],["464547",{"2":{"6":6,"189":6}}],["4",{"2":{"1":11,"3":4,"6":12,"9":2,"13":2,"14":1,"52":1,"66":1,"91":1,"104":1,"108":1,"116":2,"135":1,"145":1,"150":3,"153":3,"175":1,"182":1,"185":8,"192":20,"195":2,"196":1}}],["358421",{"2":{"196":1}}],["3585",{"2":{"175":1}}],["35",{"2":{"58":3}}],["354492",{"2":{"3":1,"6":3,"72":1,"73":1,"118":1,"119":1}}],["38042741557976",{"2":{"191":1}}],["380427415579764",{"2":{"191":3}}],["38",{"2":{"58":3}}],["3655999675063154",{"2":{"191":1}}],["36",{"2":{"58":2}}],["360",{"2":{"53":1}}],["36022",{"2":{"15":1}}],["327284472232776",{"2":{"193":3}}],["32610",{"2":{"192":3}}],["32",{"2":{"58":3}}],["377956",{"2":{"196":1}}],["37",{"2":{"58":5}}],["3497142366876638",{"2":{"191":1}}],["34",{"2":{"58":3}}],["31571636123306385",{"2":{"191":1}}],["31",{"2":{"58":2}}],["30151010318639",{"2":{"191":4}}],["30527612515520186",{"2":{"191":4}}],["300",{"2":{"84":1}}],["30",{"2":{"14":2,"58":3,"191":2}}],["3376428491230612",{"2":{"191":4}}],["3390",{"2":{"116":1}}],["33333333333",{"2":{"15":1}}],["333333333336",{"2":{"15":3}}],["33",{"2":{"6":20,"179":20,"180":20}}],["3d",{"2":{"4":1,"6":1,"59":2,"88":1,"156":1,"174":1}}],["3",{"2":{"1":13,"3":4,"5":1,"6":26,"14":2,"59":13,"62":6,"64":1,"70":6,"75":5,"87":1,"91":1,"104":1,"108":1,"116":2,"121":1,"135":1,"146":7,"150":3,"153":3,"158":2,"168":16,"175":4,"180":3,"182":2,"184":1,"185":11,"192":1,"193":1,"198":1}}],["39945867303846",{"2":{"193":3}}],["3995734698458635",{"2":{"191":1}}],["399918",{"2":{"6":2,"179":2,"180":2}}],["394759",{"2":{"6":1,"179":1,"180":1}}],["392466",{"2":{"6":1,"179":1,"180":1}}],["395332",{"2":{"6":1,"179":1,"180":1}}],["39",{"0":{"30":1},"2":{"0":1,"3":1,"4":5,"6":22,"7":1,"9":2,"17":2,"18":1,"19":1,"23":1,"29":1,"32":1,"53":1,"55":1,"56":1,"57":1,"58":3,"59":2,"62":6,"63":3,"64":28,"66":1,"68":1,"69":1,"71":1,"72":4,"73":1,"76":2,"81":2,"85":4,"88":7,"93":1,"103":2,"110":2,"116":19,"122":1,"124":2,"127":2,"146":3,"148":2,"153":5,"154":4,"159":2,"160":3,"166":6,"167":1,"175":6,"176":1,"178":1,"180":2,"188":6,"190":1,"191":6,"192":6,"193":4,"194":4,"195":5,"197":1,"199":1,"200":2}}],["2pi",{"2":{"191":1,"192":1,"193":1}}],["2nd",{"2":{"73":6}}],["2335447787454",{"2":{"193":1}}],["233544778745394",{"2":{"193":2}}],["23",{"2":{"58":3,"116":1}}],["23699059147",{"2":{"15":1}}],["28",{"2":{"58":2}}],["28083",{"2":{"15":2}}],["2658011835867806",{"2":{"191":1}}],["26745668457025",{"2":{"191":1}}],["267456684570245",{"2":{"191":3}}],["26",{"2":{"58":5,"116":2}}],["24989584635339165",{"2":{"191":1}}],["24279488312757858",{"2":{"191":4}}],["24",{"2":{"58":7,"116":1,"193":9}}],["274364",{"2":{"70":1,"72":1,"75":1}}],["274363",{"2":{"70":1,"72":1,"75":1}}],["27",{"2":{"58":2}}],["275543",{"2":{"6":6,"189":6}}],["2d",{"2":{"25":1,"27":1,"158":4,"174":1}}],["2^",{"2":{"14":1}}],["2158594260436434",{"2":{"191":1}}],["215118",{"2":{"6":4,"189":4}}],["21664550952386064",{"2":{"191":4}}],["21",{"2":{"58":4,"116":2,"193":40}}],["21427",{"2":{"11":5}}],["25",{"2":{"58":3,"90":1,"116":1,"134":1,"193":12}}],["258",{"2":{"11":1}}],["257223563`",{"2":{"176":1}}],["257223563",{"2":{"6":2,"158":1,"176":1}}],["295828190107045",{"2":{"193":1}}],["29582819010705",{"2":{"193":2}}],["299820032397223",{"2":{"191":1}}],["29",{"2":{"58":3,"196":1}}],["29th",{"0":{"8":1},"1":{"9":1,"10":1}}],["298",{"2":{"6":2,"158":1,"176":2}}],["20340",{"2":{"195":1}}],["20682326747054",{"2":{"193":1}}],["206823267470536",{"2":{"193":2}}],["20093817218219",{"2":{"191":1}}],["200938172182195",{"2":{"191":3}}],["2018",{"2":{"116":1}}],["2017",{"2":{"6":1,"59":1}}],["20",{"2":{"58":3,"116":2,"191":60,"193":1}}],["2024",{"0":{"7":1,"8":1},"1":{"9":1,"10":1}}],["20th",{"0":{"7":1}}],["22",{"2":{"58":3,"116":1}}],["22168",{"2":{"3":1,"6":3,"72":1,"73":1,"118":1,"119":1}}],["224758",{"2":{"3":1,"6":3,"72":1,"73":1,"118":1,"119":1}}],["2",{"2":{"1":19,"3":9,"5":1,"6":48,"13":1,"14":1,"15":3,"25":2,"27":2,"32":1,"53":3,"56":1,"58":6,"59":32,"62":2,"63":4,"64":10,"65":6,"66":7,"68":1,"69":2,"73":2,"75":1,"79":1,"80":1,"81":3,"82":1,"84":1,"85":3,"88":4,"91":2,"104":2,"105":2,"107":2,"108":3,"116":11,"135":2,"145":2,"146":23,"150":2,"153":4,"154":1,"158":2,"169":1,"177":1,"180":5,"182":7,"183":7,"184":5,"185":18,"189":11,"191":7,"192":6,"193":10,"194":1,"196":1}}],["1st",{"2":{"73":6}}],["198232937815632",{"2":{"193":1}}],["19823293781563178",{"2":{"193":2}}],["1999466709331708",{"2":{"191":1}}],["1998",{"2":{"70":1,"72":1,"75":1}}],["19",{"2":{"58":2,"116":2}}],["11591614996189725",{"2":{"191":1}}],["11966707868197",{"2":{"191":1}}],["119667078681967",{"2":{"191":3}}],["110m",{"2":{"192":2}}],["110",{"2":{"80":1,"192":1}}],["11",{"2":{"15":2,"58":2,"116":1}}],["1145",{"2":{"70":1,"72":1,"75":1}}],["114",{"2":{"7":1}}],["16589608273778408",{"2":{"191":1}}],["165644",{"2":{"145":2}}],["16692537029320365",{"2":{"191":4}}],["166644",{"2":{"145":2}}],["163434",{"2":{"145":2}}],["169356",{"2":{"145":2}}],["164434",{"2":{"145":2}}],["16111",{"2":{"15":1}}],["16",{"2":{"13":1,"14":1,"58":2,"116":2}}],["180",{"2":{"53":1,"145":1,"158":2}}],["18593721105",{"2":{"15":1}}],["18",{"2":{"13":1,"14":1,"58":3,"116":2}}],["13309630561615",{"2":{"193":3}}],["13401805979",{"2":{"15":2}}],["13",{"2":{"6":3,"58":1,"70":1,"75":2,"116":1}}],["10n",{"2":{"192":1}}],["10832215707812454",{"2":{"191":4}}],["10^9",{"2":{"13":1}}],["1000000",{"2":{"192":1}}],["1000",{"2":{"13":2,"175":2,"192":1,"198":2}}],["100",{"2":{"6":2,"14":3,"79":1,"81":1,"146":2,"196":1}}],["10",{"2":{"6":12,"11":1,"55":1,"58":5,"68":4,"69":10,"70":2,"72":1,"75":2,"87":2,"116":3,"121":2,"176":1,"180":4,"191":1}}],["14182952335953",{"2":{"193":1}}],["14182952335952814",{"2":{"193":2}}],["14404531208901e",{"2":{"193":2}}],["1499775010124783",{"2":{"191":1}}],["1464721641710074",{"2":{"191":4}}],["14",{"2":{"3":1,"6":5,"58":2,"72":2,"73":2,"116":1,"118":1,"119":1,"175":2,"176":2}}],["15488729606723",{"2":{"193":3}}],["15",{"2":{"3":1,"6":4,"58":1,"68":1,"69":1,"72":1,"73":1,"116":1,"118":1,"119":1}}],["17893116483784577",{"2":{"193":2}}],["17289902010158",{"2":{"191":1}}],["172899020101585",{"2":{"191":3}}],["170356",{"2":{"145":2}}],["17",{"2":{"3":1,"6":3,"58":3,"72":1,"73":1,"116":2,"118":1,"119":1}}],["125",{"2":{"6":2,"72":1,"73":1}}],["127",{"2":{"3":1,"6":3,"72":1,"73":1,"118":1,"119":1,"192":1}}],["123",{"2":{"3":1,"6":3,"72":1,"73":1,"118":1,"119":1}}],["12636633117296836",{"2":{"193":2}}],["126",{"2":{"3":1,"6":3,"72":1,"73":1,"118":1,"119":1}}],["12",{"2":{"3":1,"6":3,"58":2,"72":1,"73":1,"116":1,"118":1,"119":1,"199":1}}],["124",{"2":{"3":1,"6":3,"72":1,"73":1,"118":1,"119":1}}],["1",{"2":{"1":8,"3":38,"6":59,"13":1,"14":4,"15":1,"52":4,"53":15,"55":7,"57":2,"58":11,"59":16,"62":4,"63":7,"64":72,"65":9,"66":15,"68":1,"69":10,"71":3,"73":9,"75":4,"76":11,"81":6,"84":7,"85":6,"88":16,"90":2,"91":6,"93":2,"94":2,"103":2,"104":8,"105":12,"107":2,"108":5,"116":38,"122":6,"124":4,"125":5,"127":1,"134":2,"135":6,"145":16,"146":27,"150":2,"153":6,"154":3,"156":3,"162":12,"163":2,"169":9,"176":1,"177":4,"180":8,"182":15,"183":3,"184":15,"185":6,"189":9,"191":8,"192":44,"193":12,"194":1,"198":8,"199":3}}],["nselected",{"2":{"184":3}}],["nmax",{"2":{"184":2}}],["nice",{"2":{"182":1}}],["n+1",{"2":{"162":1}}],["nfeature",{"2":{"153":1,"154":1}}],["nkeys",{"2":{"146":8}}],["nc",{"2":{"116":13}}],["ncoord",{"2":{"88":2}}],["nl",{"2":{"116":11}}],["nhole",{"2":{"64":2,"70":2,"72":2,"75":2,"76":1,"88":2}}],["nbpts",{"2":{"64":2}}],["ngeom",{"2":{"63":1,"153":2,"154":1,"189":1}}],["nt",{"2":{"188":2}}],["ntasks",{"2":{"153":3,"154":3}}],["nthreads",{"2":{"153":2,"154":2}}],["nthe",{"2":{"60":1,"173":1,"176":1}}],["ntuple",{"2":{"59":3,"177":1}}],["n2",{"2":{"59":8,"88":10}}],["n1",{"2":{"59":8,"88":9}}],["np2",{"2":{"105":4}}],["npolygon",{"2":{"71":1,"88":3,"169":2}}],["npoints",{"2":{"53":6,"64":5,"116":3,"176":6,"182":7}}],["npoint",{"2":{"6":1,"53":2,"55":1,"56":1,"66":2,"81":2,"85":1,"88":6,"105":6,"116":6,"127":1,"145":2,"163":1,"176":3,"177":1,"180":14,"184":1,"189":13}}],["npts",{"2":{"64":6,"182":3}}],["np",{"2":{"56":2,"85":5}}],["null",{"2":{"32":1,"176":1}}],["numeric",{"2":{"11":1}}],["numbers",{"2":{"6":1,"7":1,"57":2,"59":1,"176":1}}],["number=6",{"2":{"6":1,"179":1,"180":1}}],["number",{"2":{"6":11,"59":1,"64":2,"69":1,"77":1,"81":1,"84":1,"88":2,"116":2,"174":1,"175":1,"177":1,"180":2,"181":4,"182":7,"183":4,"184":12}}],["n",{"2":{"6":1,"9":1,"57":2,"59":36,"60":2,"64":51,"69":10,"75":4,"76":4,"116":11,"127":2,"145":6,"146":5,"162":1,"169":25,"173":2,"176":2,"177":3,"180":2,"184":7,"189":32}}],["naive",{"2":{"74":1}}],["napts",{"2":{"64":3}}],["navigate",{"0":{"26":1}}],["natearth2",{"2":{"192":1}}],["natural",{"2":{"180":1,"192":2,"197":1}}],["naturalearth",{"2":{"11":2,"80":2,"180":3,"192":1}}],["nature",{"2":{"146":1}}],["native",{"2":{"6":1,"147":1,"188":4,"195":1}}],["nan",{"2":{"9":1}}],["named",{"2":{"153":1}}],["namedtuple",{"2":{"22":2,"153":2,"188":2}}],["name",{"2":{"6":1,"31":2,"180":1,"188":1,"194":1,"195":2,"199":1}}],["namespaced",{"2":{"153":1}}],["names",{"2":{"6":1,"31":3,"153":3,"154":3,"188":1}}],["nodestatus",{"2":{"146":6}}],["nodes",{"2":{"64":1,"146":4}}],["node",{"2":{"64":26,"146":17}}],["north",{"2":{"66":12,"158":1}}],["nor",{"2":{"60":1,"173":1,"176":1}}],["normalized",{"2":{"59":1}}],["normalize",{"2":{"59":1}}],["norm",{"2":{"6":1,"59":22}}],["now",{"2":{"6":1,"13":1,"25":1,"27":1,"55":1,"58":2,"59":4,"60":1,"64":1,"69":1,"76":1,"77":1,"85":1,"145":1,"146":2,"168":1,"174":1,"175":3,"176":1,"180":1,"191":5,"192":6,"193":2,"194":1,"195":3,"198":2}}],["no",{"2":{"6":6,"53":1,"59":2,"64":4,"70":1,"71":1,"72":1,"73":14,"75":1,"88":2,"94":3,"108":3,"116":2,"125":2,"127":1,"129":1,"130":2,"135":2,"137":1,"138":3,"139":3,"140":1,"146":2,"147":1,"148":1,"153":3,"174":1,"176":2,"177":1,"180":1,"188":1,"195":1}}],["nonzero",{"2":{"177":2}}],["none",{"2":{"6":4,"64":1,"69":1,"70":1,"72":1,"75":1,"76":1,"105":4,"107":1}}],["nondimensional",{"2":{"6":1,"176":1}}],["nonintersecting",{"2":{"6":1,"23":1,"166":1,"169":1}}],["non",{"2":{"3":2,"6":2,"22":1,"57":2,"64":15,"70":2,"71":1,"72":1,"73":4,"75":1,"76":2,"84":1,"122":2,"129":1,"151":1,"160":1}}],["note",{"2":{"4":4,"6":11,"19":1,"29":1,"53":1,"56":2,"59":1,"62":2,"63":2,"64":3,"66":1,"69":2,"71":3,"73":5,"76":3,"82":1,"85":1,"88":5,"116":1,"121":1,"122":1,"158":1,"160":1,"172":1,"177":1,"181":1,"182":2,"183":1,"192":2,"193":1}}],["not",{"0":{"147":1},"2":{"1":2,"3":8,"4":1,"6":16,"18":1,"19":1,"22":1,"24":1,"31":1,"53":4,"56":1,"59":4,"60":1,"62":1,"64":18,"69":1,"72":1,"73":7,"76":3,"81":1,"87":2,"88":4,"90":2,"91":1,"93":2,"94":2,"103":1,"104":2,"108":3,"110":5,"111":3,"112":2,"113":2,"116":5,"121":1,"122":2,"125":1,"134":2,"135":2,"137":1,"145":4,"146":2,"147":1,"150":1,"151":1,"152":1,"153":7,"154":4,"156":4,"158":2,"159":1,"160":1,"162":2,"165":2,"168":3,"171":1,"172":1,"173":1,"174":1,"176":2,"177":1,"187":1,"188":2,"192":1,"193":1,"197":1}}],["nothing`",{"2":{"71":2,"73":2,"76":2}}],["nothing",{"2":{"1":34,"4":1,"6":41,"15":3,"64":4,"69":1,"71":2,"72":1,"73":2,"76":2,"88":1,"116":1,"145":9,"146":3,"150":1,"153":3,"154":2,"156":3,"162":24,"165":1,"168":60,"173":1,"177":1,"180":2,"181":6,"182":6,"183":6,"184":1,"185":32,"189":7,"191":252,"192":16,"193":20}}],["ne",{"2":{"192":2}}],["net",{"2":{"165":2}}],["never",{"2":{"151":1,"181":1}}],["nesting",{"2":{"151":2}}],["nestedloopfast",{"2":{"200":1}}],["nested",{"2":{"1":2,"4":1,"6":3,"18":1,"53":1,"150":2,"151":2,"153":2,"154":2,"180":1,"186":1}}],["neumann",{"2":{"146":1}}],["neither",{"2":{"76":1}}],["neighborhood",{"2":{"146":2}}],["neighbor",{"2":{"64":27}}],["neighboring",{"2":{"6":6,"64":1,"180":1,"181":1,"182":1,"183":2}}],["nearly",{"2":{"195":1}}],["nearest",{"2":{"64":1,"73":2}}],["neatly",{"2":{"17":1}}],["nedge",{"2":{"64":2,"189":13}}],["nextnode",{"2":{"146":27}}],["nextnodes",{"2":{"146":6}}],["next",{"2":{"63":1,"64":81,"66":3,"69":4,"116":14,"146":3,"169":13,"182":5}}],["necessarily",{"2":{"6":1,"145":1,"171":1}}],["necessary",{"2":{"6":1,"151":1,"180":1}}],["newfeature",{"2":{"156":2}}],["newnodes",{"2":{"146":2}}],["new",{"2":{"6":1,"60":1,"64":58,"66":9,"70":3,"71":1,"76":16,"116":2,"146":4,"152":1,"153":21,"154":1,"165":2,"166":1,"167":1,"169":19,"177":10,"181":1,"182":2,"183":1,"193":1,"194":1,"199":1}}],["negative",{"2":{"4":3,"6":3,"55":2,"56":1,"57":2,"84":2,"85":3}}],["needs",{"2":{"63":1}}],["needing",{"2":{"4":1,"6":1,"17":1,"170":1}}],["need",{"2":{"1":1,"3":1,"4":2,"5":1,"6":5,"32":1,"59":2,"62":1,"64":2,"71":1,"81":1,"88":4,"103":1,"104":1,"116":1,"146":2,"148":1,"153":3,"154":1,"170":1,"184":1,"185":1,"192":1,"193":2,"200":2}}],["needed",{"2":{"1":2,"6":4,"53":1,"59":1,"64":4,"66":1,"70":1,"72":1,"75":1,"85":2,"116":5,"146":1,"153":1,"154":1,"165":1,"172":3,"193":1}}],["bx",{"2":{"145":2}}],["b2y",{"2":{"73":4}}],["b2x",{"2":{"73":4}}],["b2",{"2":{"73":47,"85":3,"122":6}}],["b``",{"2":{"71":1}}],["b`",{"2":{"71":3,"73":4,"76":4}}],["bigger",{"2":{"64":1}}],["bit",{"2":{"25":1,"27":1,"184":8,"191":2}}],["b1y",{"2":{"73":7}}],["b1x",{"2":{"73":7}}],["b1",{"2":{"64":8,"73":63,"122":6}}],["breaks",{"2":{"169":1}}],["break",{"2":{"64":3,"66":1,"71":2,"88":4,"116":11,"122":1,"146":6,"169":2}}],["brevity",{"2":{"59":1}}],["broadcasting",{"2":{"6":1,"59":1}}],["broken",{"2":{"3":1,"6":1,"105":1}}],["building",{"2":{"191":1}}],["build",{"2":{"64":13,"69":1,"70":1,"72":1,"75":1,"175":2,"183":1,"184":1}}],["built",{"2":{"17":1,"20":1}}],["buffers",{"2":{"146":1}}],["buffered",{"2":{"60":2}}],["buffering",{"2":{"59":1,"60":1}}],["buffer",{"0":{"60":1},"2":{"31":2,"60":5,"63":2,"64":1,"147":1}}],["but",{"2":{"1":2,"3":5,"4":3,"6":12,"18":1,"22":1,"25":1,"27":1,"32":1,"53":1,"55":1,"56":1,"66":1,"73":2,"76":2,"81":2,"82":2,"88":4,"93":2,"116":1,"121":1,"122":5,"124":1,"125":1,"128":2,"129":1,"134":1,"145":1,"146":1,"147":1,"150":2,"151":1,"153":10,"154":3,"156":5,"158":5,"159":1,"171":1,"172":1,"177":2,"180":1,"188":2,"192":2,"193":2,"195":1,"197":1,"198":1,"199":2}}],["black",{"2":{"68":1,"192":1}}],["blue",{"2":{"14":1,"68":1,"87":2,"90":2,"107":2,"121":2,"134":2,"198":2}}],["bloat",{"2":{"193":1}}],["block",{"2":{"6":2,"191":1}}],["blob",{"2":{"6":2,"180":1}}],["balancing",{"2":{"153":1,"154":1}}],["barrier",{"2":{"153":1}}],["barycentric",{"0":{"5":1,"57":1,"59":1},"1":{"58":1,"59":1},"2":{"0":6,"5":10,"6":17,"9":1,"31":1,"57":9,"58":4,"59":52}}],["basic",{"2":{"146":1}}],["base",{"2":{"31":4,"32":1,"59":15,"60":1,"64":2,"146":6,"153":3,"154":3,"158":2,"159":1,"173":1,"176":2,"188":3}}],["based",{"2":{"4":2,"6":2,"19":1,"53":2,"56":2,"58":2,"63":1,"66":1,"85":2,"88":1,"94":1,"108":1,"116":2,"122":1,"125":1,"135":1,"159":1,"195":1,"197":1,"198":1}}],["badge",{"2":{"6":2}}],["backs",{"2":{"69":4}}],["backwards",{"2":{"64":1,"71":1,"73":1,"76":1}}],["backing",{"2":{"64":1}}],["backend",{"2":{"60":2,"188":1}}],["back",{"2":{"6":1,"18":1,"22":1,"23":1,"69":4,"188":1}}],["b",{"2":{"3":2,"4":7,"6":13,"35":2,"36":2,"37":2,"38":2,"40":2,"41":2,"42":2,"43":2,"44":2,"45":2,"46":2,"47":2,"48":2,"49":2,"64":206,"66":9,"70":26,"71":18,"72":26,"73":77,"74":3,"75":27,"76":39,"88":29,"116":2,"122":22,"194":1}}],["box",{"2":{"65":2,"66":3,"198":1}}],["bounce",{"2":{"64":8,"70":1,"71":1,"72":1,"73":1,"75":1,"76":1}}],["bouncings",{"2":{"64":1}}],["bouncing",{"2":{"64":13,"71":3,"73":4,"76":4}}],["bound",{"2":{"74":1}}],["bounding",{"2":{"64":1,"65":2,"66":3}}],["boundscheck",{"2":{"59":14}}],["bounds",{"2":{"6":1,"146":4}}],["boundaries",{"2":{"3":2,"6":2,"103":2,"107":1,"108":1,"116":2,"118":1,"119":1,"146":1}}],["boundary",{"2":{"3":7,"6":7,"84":1,"90":1,"91":1,"93":1,"94":3,"97":4,"104":2,"105":9,"108":2,"110":1,"111":6,"112":3,"116":18,"124":2,"125":4,"127":3,"128":5,"129":3,"130":3,"134":1,"135":3}}],["bold",{"2":{"60":1,"173":1,"176":1}}],["both",{"2":{"3":5,"6":6,"64":2,"73":4,"75":1,"76":1,"81":2,"84":1,"88":3,"91":1,"105":4,"108":1,"116":2,"122":1,"129":1,"135":1,"175":1,"177":1}}],["booltype",{"2":{"31":1,"153":2,"154":1,"158":1,"160":5,"177":1}}],["booleans",{"2":{"64":1,"116":2}}],["boolean",{"0":{"39":1},"1":{"40":1,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1,"47":1,"48":1,"49":1},"2":{"24":2,"64":1,"71":1,"73":1,"76":1,"160":2,"197":1}}],["boolsastypes",{"0":{"24":1,"160":1},"2":{"31":1,"32":1,"158":1,"160":10,"177":3}}],["bools",{"2":{"10":1}}],["bool",{"2":{"3":17,"4":14,"6":40,"32":1,"64":3,"88":17,"91":1,"94":1,"104":2,"105":13,"108":1,"116":2,"119":1,"122":10,"125":2,"135":1,"145":7,"146":6,"160":2,"177":3,"189":5}}],["bypred",{"2":{"200":1}}],["by",{"0":{"98":1,"99":1,"139":1},"2":{"1":3,"3":2,"4":8,"6":29,"7":1,"17":1,"18":2,"19":1,"22":1,"24":1,"25":1,"26":1,"27":1,"52":1,"53":6,"55":1,"56":1,"59":4,"60":1,"62":5,"63":4,"64":14,"65":1,"66":6,"68":1,"69":6,"70":1,"72":1,"73":5,"75":4,"76":3,"82":2,"85":5,"88":8,"91":1,"93":1,"94":1,"98":2,"99":2,"100":1,"101":2,"103":1,"115":2,"116":10,"145":2,"146":4,"153":2,"156":7,"158":7,"166":2,"167":1,"169":3,"171":1,"172":2,"173":1,"174":1,"176":3,"177":1,"178":1,"180":1,"181":1,"182":3,"183":1,"185":1,"191":1,"192":1,"193":1,"195":1,"197":5,"198":1,"199":2,"200":1}}],["berlin",{"2":{"199":1}}],["bearing",{"2":{"145":4}}],["beauty",{"2":{"9":1}}],["better",{"2":{"105":1,"153":1,"154":1,"180":1}}],["between",{"2":{"4":4,"6":18,"23":1,"53":2,"59":13,"64":1,"66":18,"70":1,"71":4,"72":1,"73":11,"75":2,"76":4,"85":8,"88":1,"116":4,"122":4,"146":1,"158":2,"176":3,"177":1,"181":2,"182":2,"183":1,"192":1,"197":2,"198":1}}],["been",{"2":{"71":1,"146":1,"148":2,"154":1,"160":1}}],["because",{"2":{"64":1,"82":1,"148":1,"154":1,"187":1}}],["becomes",{"2":{"158":1}}],["become",{"2":{"22":1}}],["being",{"2":{"53":1,"73":1,"116":1,"122":1,"167":1}}],["behind",{"2":{"25":1,"26":1,"27":1,"58":1,"148":1,"154":1}}],["behaviours",{"2":{"32":1}}],["behaviour",{"2":{"6":1,"18":1,"23":1,"180":1}}],["best",{"2":{"19":1,"153":2,"180":1}}],["benchmarking",{"2":{"176":1,"180":1}}],["benchmarkgroup",{"2":{"176":2,"180":3}}],["benchmark",{"0":{"176":1,"180":1},"2":{"176":1,"180":1}}],["benchmarktools",{"2":{"13":1,"176":1,"180":1}}],["benchmarks",{"2":{"9":2}}],["beginning",{"2":{"66":1,"191":1}}],["begin+1",{"2":{"59":5}}],["begin",{"2":{"9":1,"59":7,"153":1,"154":1,"181":1,"195":1}}],["beware",{"2":{"6":1,"18":1,"177":1}}],["before",{"2":{"6":4,"33":1,"59":1,"70":1,"72":1,"73":1,"75":1,"180":1,"192":1}}],["below",{"2":{"6":2,"17":1,"60":1,"165":1,"182":1,"183":1,"184":1,"195":1}}],["be",{"2":{"1":7,"3":3,"4":11,"5":4,"6":59,"7":1,"9":1,"13":2,"20":3,"23":2,"24":1,"25":3,"26":1,"27":3,"32":1,"53":8,"56":7,"57":2,"58":1,"59":10,"60":2,"62":1,"63":3,"64":14,"65":1,"66":2,"69":1,"70":7,"71":4,"72":7,"73":12,"75":6,"76":5,"82":6,"84":2,"85":4,"88":15,"94":6,"103":2,"104":1,"108":6,"116":21,"122":2,"125":6,"135":6,"137":1,"138":1,"139":1,"140":1,"145":2,"146":1,"150":4,"151":2,"152":2,"153":9,"154":1,"155":1,"156":6,"158":4,"160":1,"162":1,"163":1,"164":1,"165":3,"166":4,"167":2,"169":2,"170":1,"172":3,"173":1,"174":1,"176":8,"177":3,"180":10,"182":1,"184":3,"185":1,"188":2,"191":2,"193":1,"197":4}}],["human",{"2":{"195":1}}],["hull`",{"2":{"82":2}}],["hulls",{"2":{"6":1,"77":1,"82":1}}],["hull",{"0":{"50":1,"77":1,"79":1,"80":1},"1":{"78":1,"79":1,"80":1,"81":1,"82":1},"2":{"0":1,"6":8,"31":2,"50":1,"77":2,"79":4,"80":1,"81":10,"82":15}}],["hypot",{"2":{"177":1}}],["h2",{"2":{"116":6}}],["h1",{"2":{"116":11}}],["hm",{"2":{"58":2}}],["h",{"2":{"53":2,"64":2,"76":8}}],["href=",{"2":{"6":2}}],["https",{"2":{"6":2,"70":1,"72":1,"73":1,"75":1,"82":1,"116":2,"158":1,"182":1}}],["high",{"2":{"195":1}}],["highest",{"2":{"193":1}}],["higher",{"2":{"6":1,"82":1}}],["hit",{"2":{"64":1,"153":1,"154":1,"156":3}}],["hits",{"2":{"18":1}}],["hidedecorations",{"2":{"58":2}}],["hinter",{"2":{"31":3,"60":1,"173":1,"176":1}}],["hint",{"2":{"31":3,"60":1,"176":1}}],["hinge=2",{"2":{"72":1}}],["hinge`",{"2":{"72":1}}],["hinge",{"2":{"6":1,"64":1,"73":13,"116":15}}],["hist",{"2":{"13":1}}],["histogram",{"2":{"13":1}}],["hcat",{"2":{"6":1}}],["heavily",{"2":{"153":1}}],["heatmap",{"2":{"13":5,"14":6,"58":2,"84":2,"146":1}}],["help",{"2":{"158":1,"160":1}}],["helpers",{"0":{"64":1},"2":{"105":1,"122":1}}],["helper",{"0":{"71":1,"73":1,"76":1},"2":{"63":1,"64":1,"69":1}}],["helps",{"2":{"30":1}}],["held",{"2":{"1":1,"150":1,"153":1}}],["here",{"2":{"6":2,"9":1,"13":1,"14":1,"20":1,"59":1,"73":1,"82":2,"105":1,"146":2,"153":2,"156":3,"158":1,"162":1,"176":1,"192":2,"197":5,"198":3}}],["hours",{"2":{"199":1}}],["hood",{"2":{"195":1}}],["hook",{"2":{"6":1,"180":1}}],["home",{"2":{"192":1}}],["horizontal",{"2":{"66":1,"73":1,"146":1,"180":1}}],["hormann",{"0":{"71":1,"73":1,"76":1},"2":{"6":2,"59":2,"64":3,"69":2,"70":1,"72":1,"75":1}}],["how",{"0":{"26":1},"2":{"6":1,"26":2,"55":1,"59":1,"64":1,"73":1,"88":1,"146":2,"156":2,"171":1,"180":1,"192":1,"195":1,"197":2,"198":2,"199":1,"200":1}}],["however",{"2":{"3":1,"6":2,"53":1,"59":1,"63":1,"64":1,"73":1,"81":1,"84":1,"88":1,"90":1,"104":1,"167":1}}],["hole",{"2":{"53":1,"56":3,"58":2,"59":9,"63":4,"64":60,"66":3,"70":6,"72":2,"76":15,"85":3,"97":1,"98":1,"99":1,"116":33,"146":5,"163":2,"191":1}}],["holes=",{"2":{"64":1}}],["holes",{"2":{"4":3,"5":1,"6":4,"9":2,"53":2,"55":1,"59":2,"63":1,"64":19,"69":4,"70":2,"72":2,"75":6,"76":29,"85":3,"88":3,"113":1,"116":11,"137":1,"146":17,"163":2,"191":4}}],["holds",{"2":{"64":1,"159":2,"178":1}}],["holding",{"2":{"6":1,"146":1,"153":2}}],["hold",{"2":{"6":1,"146":1,"160":1,"180":1,"195":1}}],["halign",{"2":{"180":1}}],["half",{"2":{"65":2}}],["hail",{"2":{"163":1}}],["handling",{"0":{"173":1},"2":{"148":1}}],["handler",{"2":{"60":1,"173":1}}],["handle",{"2":{"31":1,"145":1,"151":2,"153":1,"192":1}}],["handled",{"2":{"18":1}}],["hao",{"2":{"116":1}}],["had",{"2":{"70":1,"72":1,"88":1,"160":1,"175":1}}],["happens",{"2":{"64":1}}],["happen",{"2":{"24":1,"153":1}}],["havem",{"2":{"156":4}}],["havez",{"2":{"156":4}}],["have",{"2":{"3":3,"4":9,"6":15,"19":1,"23":1,"33":1,"53":2,"56":3,"59":6,"60":1,"64":4,"73":1,"82":2,"84":1,"85":2,"88":10,"116":5,"121":1,"122":3,"124":1,"125":1,"146":1,"147":1,"148":2,"152":1,"153":1,"154":2,"156":5,"158":2,"160":1,"180":1,"182":1,"188":1,"191":1,"192":1,"197":1,"198":1,"199":3}}],["hasm",{"2":{"156":2}}],["hasz",{"2":{"156":2}}],["haskey",{"2":{"153":1,"188":1}}],["hash",{"2":{"146":1}}],["hasn",{"2":{"146":1}}],["hassle",{"2":{"1":1,"6":1,"185":1}}],["has",{"2":{"1":4,"52":1,"55":2,"59":2,"64":2,"71":2,"73":1,"76":1,"84":1,"85":1,"88":11,"127":1,"145":1,"150":4,"153":6,"161":1,"175":2,"184":1,"192":1}}],["xticklabelsvisible",{"2":{"192":1}}],["xticklabelrotation",{"2":{"13":2}}],["xoffset",{"2":{"191":4,"193":1}}],["x=x",{"2":{"189":1}}],["x`",{"2":{"153":1}}],["xvec",{"2":{"146":4}}],["xbounds",{"2":{"146":4}}],["xhalf",{"2":{"146":2}}],["xlast",{"2":{"85":3}}],["xfirst",{"2":{"85":5}}],["x0",{"2":{"85":5}}],["xn",{"2":{"66":4}}],["xind+1",{"2":{"146":1}}],["xind",{"2":{"146":2}}],["xinterior",{"2":{"63":2}}],["xi−xi−1",{"2":{"6":1}}],["xcentroid",{"2":{"63":13}}],["xrange",{"2":{"58":3,"84":2}}],["xautolimits",{"2":{"58":2}}],["xp2",{"2":{"53":5}}],["x26",{"2":{"53":2,"56":6,"59":1,"60":4,"64":58,"66":36,"70":4,"71":4,"73":24,"76":2,"85":3,"88":14,"100":2,"101":2,"105":48,"114":2,"115":2,"116":170,"122":22,"127":2,"131":2,"132":2,"141":2,"142":2,"145":4,"146":12,"153":2,"165":2,"169":6,"173":2,"176":2,"182":10,"183":2,"189":4,"197":4}}],["x2",{"2":{"6":3,"59":4,"63":2,"66":18,"85":8,"105":10,"116":5,"122":4,"146":5,"177":6,"189":2}}],["x1",{"2":{"6":4,"59":3,"63":2,"66":22,"85":8,"105":11,"116":6,"122":5,"146":5,"177":7,"189":2}}],["xs",{"2":{"6":3,"66":4,"146":30,"189":4}}],["xmax",{"2":{"6":1,"65":2,"66":48}}],["xmin",{"2":{"6":1,"65":2,"66":49}}],["x3c",{"2":{"5":1,"6":28,"31":1,"53":4,"56":2,"59":66,"64":17,"66":11,"69":3,"70":1,"72":1,"73":6,"75":1,"84":1,"85":13,"88":2,"105":33,"116":13,"122":10,"145":1,"146":4,"153":3,"154":2,"156":19,"158":3,"159":4,"160":4,"163":2,"166":3,"169":5,"176":2,"181":2,"182":6,"183":3,"184":9,"200":1}}],["xy`",{"2":{"172":1}}],["xy",{"2":{"1":4,"59":1,"172":3}}],["x",{"2":{"1":4,"4":1,"6":3,"7":1,"9":5,"11":6,"13":11,"14":10,"15":4,"50":2,"53":9,"56":2,"58":8,"62":1,"63":10,"64":21,"65":1,"66":8,"69":6,"71":9,"73":18,"76":9,"84":2,"85":7,"88":3,"105":21,"116":16,"122":7,"145":11,"146":2,"148":2,"150":3,"153":5,"154":4,"156":10,"160":5,"165":2,"170":2,"171":4,"172":1,"177":2,"180":2,"182":2,"184":1,"185":2,"186":2,"189":33,"191":6,"192":2,"193":2,"197":1,"199":2}}],["=>",{"2":{"153":2}}],["=float64",{"2":{"63":3,"72":1,"75":1}}],["=false",{"2":{"53":1}}],["===",{"2":{"105":8,"145":4}}],["==",{"2":{"9":1,"19":1,"32":1,"53":4,"56":2,"59":9,"60":2,"63":1,"64":34,"66":30,"71":1,"73":20,"75":2,"76":1,"80":1,"84":1,"88":16,"116":44,"122":2,"145":3,"146":18,"153":2,"156":4,"163":1,"165":1,"169":1,"173":1,"176":2,"180":1,"184":3}}],["=",{"2":{"1":5,"3":17,"4":7,"5":2,"6":59,"11":1,"13":19,"14":33,"15":18,"31":4,"32":7,"35":2,"36":2,"37":2,"38":2,"50":3,"52":4,"53":56,"55":6,"56":25,"58":35,"59":110,"60":3,"62":6,"63":36,"64":324,"65":6,"66":96,"68":7,"69":29,"70":21,"71":30,"72":18,"73":143,"75":21,"76":78,"79":6,"80":4,"81":14,"82":4,"84":15,"85":52,"87":7,"88":40,"90":7,"91":3,"93":4,"94":23,"95":3,"96":7,"97":14,"98":14,"99":4,"100":2,"101":2,"103":5,"104":3,"105":57,"107":7,"108":18,"109":3,"110":11,"111":17,"112":9,"113":3,"114":2,"115":2,"116":189,"118":3,"119":3,"121":7,"122":40,"124":3,"125":22,"126":3,"127":9,"128":14,"129":10,"130":6,"131":2,"132":2,"134":7,"135":22,"136":3,"137":11,"138":14,"139":14,"140":4,"141":2,"142":2,"145":32,"146":110,"148":1,"150":3,"153":40,"154":20,"155":4,"156":45,"158":3,"159":6,"160":2,"162":2,"163":4,"165":9,"168":3,"169":30,"170":1,"173":2,"175":16,"176":21,"177":25,"178":2,"179":5,"180":60,"181":11,"182":49,"183":7,"184":43,"185":2,"186":1,"188":3,"189":42,"191":45,"192":19,"193":13,"194":3,"195":4,"196":7,"198":12,"199":6,"200":2}}],["utm",{"2":{"192":1}}],["utility",{"0":{"189":1},"2":{"59":1,"153":1,"154":1}}],["utils",{"0":{"184":1},"2":{"31":1}}],["u2",{"2":{"116":4}}],["u1",{"2":{"116":4}}],["update",{"2":{"64":6,"146":10}}],["updated",{"2":{"64":6,"156":3}}],["updates",{"2":{"59":1}}],["up",{"2":{"64":2,"73":1,"116":1,"129":1,"151":1,"168":1,"182":2,"191":2}}],["upper",{"2":{"6":1,"146":1,"198":1}}],["uv",{"2":{"59":1}}],["usage",{"2":{"199":1}}],["usable",{"2":{"157":1}}],["usa",{"0":{"80":1},"2":{"80":4,"180":15,"199":3}}],["us",{"2":{"56":1,"82":1,"192":1,"199":1}}],["usually",{"2":{"26":2,"77":1,"153":1,"156":2,"158":1,"195":1}}],["usual",{"2":{"6":1,"163":1,"166":1}}],["usecases",{"2":{"25":1,"27":1}}],["uses",{"2":{"6":3,"77":1,"82":1,"116":1,"157":1,"158":1,"172":1,"176":1,"177":1,"180":1,"195":1}}],["users",{"2":{"31":1,"148":1,"167":1}}],["user",{"2":{"6":12,"23":1,"63":1,"70":3,"72":3,"75":3,"153":1,"181":1,"182":1,"183":1,"197":1}}],["useful",{"2":{"6":10,"59":1,"146":1,"174":1,"177":1,"180":2,"188":1}}],["used",{"2":{"1":1,"5":1,"6":4,"53":1,"56":1,"57":1,"59":1,"63":2,"64":7,"66":1,"69":1,"73":1,"84":1,"85":1,"88":1,"122":1,"146":2,"150":1,"153":1,"154":1,"156":2,"158":1,"159":1,"161":1,"180":1,"182":2,"187":2,"192":1,"195":1,"197":2,"198":1}}],["use",{"2":{"1":2,"4":1,"5":1,"6":9,"11":1,"20":1,"23":1,"26":1,"59":1,"64":1,"76":1,"82":2,"105":2,"122":1,"146":1,"147":1,"150":1,"153":4,"154":3,"155":1,"158":2,"163":1,"171":1,"175":1,"177":2,"180":1,"182":2,"188":3,"191":1,"195":1,"197":2,"200":2}}],["using",{"0":{"192":1},"2":{"1":4,"4":1,"5":3,"6":11,"11":4,"13":4,"14":1,"15":1,"31":6,"32":1,"50":1,"52":1,"53":1,"55":2,"56":2,"58":4,"59":7,"60":3,"62":2,"63":1,"64":5,"65":2,"66":1,"68":2,"69":1,"71":3,"73":4,"74":1,"76":3,"79":1,"80":2,"81":1,"82":2,"84":2,"85":2,"87":2,"88":1,"90":2,"91":1,"93":2,"94":1,"101":1,"103":2,"104":1,"105":1,"107":2,"108":1,"115":1,"116":1,"118":2,"119":1,"121":2,"122":1,"124":2,"125":1,"132":1,"134":2,"135":1,"142":1,"145":1,"146":5,"147":1,"148":2,"150":1,"151":1,"153":4,"154":2,"155":1,"156":1,"160":1,"163":1,"166":1,"169":1,"170":2,"171":1,"172":1,"173":3,"175":1,"176":5,"177":1,"180":3,"182":1,"184":1,"185":4,"186":2,"188":2,"189":1,"190":4,"191":1,"193":1,"194":2,"196":1,"197":1,"198":4,"199":2}}],["until",{"2":{"146":3,"151":1,"182":1,"188":1}}],["unprocessed",{"2":{"64":1}}],["unknown",{"2":{"64":4,"66":15}}],["unknown=3",{"2":{"64":1}}],["unmatched",{"2":{"64":9,"66":26}}],["understand",{"2":{"187":1}}],["under",{"2":{"56":2,"195":1}}],["undergrad",{"2":{"9":1}}],["undef",{"2":{"53":2,"64":1,"181":1,"182":1,"184":2,"189":2}}],["unwrap",{"0":{"156":1},"2":{"31":2,"156":19}}],["unless",{"2":{"22":1,"64":1,"71":2,"73":2,"76":2,"153":1}}],["unlike",{"2":{"18":1,"187":1}}],["unstable",{"2":{"13":1,"153":1,"160":1}}],["unneeded",{"2":{"6":3,"64":1,"70":2,"72":2,"75":2}}],["unnecessary",{"2":{"6":3,"146":1,"181":1,"182":1,"183":1}}],["universal",{"2":{"192":1}}],["united",{"2":{"180":1}}],["unit",{"2":{"158":1}}],["units",{"2":{"6":2,"177":2}}],["unify",{"2":{"25":1,"27":1}}],["unique",{"2":{"6":2,"73":3,"166":2,"169":2}}],["unioning",{"2":{"76":1}}],["unionintersectingpolygons",{"2":{"0":1,"6":8,"70":1,"71":2,"72":1,"73":2,"75":1,"76":2,"166":2,"167":2,"168":1,"169":4}}],["unions",{"0":{"76":1},"2":{"18":1,"159":1}}],["union",{"0":{"36":1,"75":1},"2":{"0":1,"1":2,"3":2,"4":4,"6":18,"11":3,"23":2,"31":1,"32":1,"36":2,"53":2,"56":1,"59":1,"63":4,"64":7,"66":1,"72":2,"75":18,"76":30,"85":1,"88":8,"96":2,"97":4,"98":1,"100":2,"101":1,"110":2,"111":6,"114":2,"115":1,"122":4,"127":2,"128":4,"129":1,"131":2,"132":1,"137":2,"138":4,"139":1,"141":2,"142":1,"145":1,"146":5,"148":1,"150":2,"151":3,"153":3,"154":3,"159":4,"166":1,"167":1,"169":9,"177":5,"180":2,"181":3,"182":3,"183":3,"189":1}}],["unchanged",{"2":{"1":1,"6":1,"150":1,"153":1,"180":1}}],["gdal",{"2":{"195":1}}],["gml",{"2":{"195":1}}],["gpkg",{"2":{"195":3}}],["gadm",{"2":{"199":4}}],["ga",{"2":{"192":3,"196":2}}],["gaps",{"2":{"146":1}}],["global",{"2":{"192":1}}],["globally",{"2":{"154":1,"158":1}}],["gft",{"2":{"190":1,"192":2}}],["gc",{"2":{"165":6}}],["g",{"2":{"53":3,"56":3,"60":1,"63":3,"66":3,"85":6,"151":1,"153":2,"156":4,"158":1,"180":2,"189":2,"195":1}}],["gb",{"2":{"31":1}}],["guarantee",{"2":{"19":1,"30":1}}],["guaranteed",{"2":{"1":1,"53":1,"150":1,"154":1}}],["grows",{"2":{"158":1}}],["grouped",{"2":{"191":1}}],["groups",{"2":{"6":1,"146":1}}],["grouping",{"2":{"1":1,"150":1,"154":3}}],["grand",{"2":{"199":1}}],["grained",{"2":{"175":1}}],["grahamscanmethod",{"2":{"82":1}}],["graphics",{"2":{"6":1,"59":1}}],["great",{"2":{"191":1,"193":1,"195":1}}],["greater",{"2":{"145":1}}],["greiner",{"0":{"71":1,"73":1,"76":1},"2":{"64":3,"69":2,"70":1,"72":1,"75":1}}],["green",{"2":{"14":1,"192":1}}],["grid",{"2":{"6":1,"58":1,"66":4}}],["g2",{"2":{"3":5,"6":5,"91":4,"94":12,"95":6,"96":9,"97":6,"98":6,"99":3,"100":4,"101":2,"104":3,"105":21,"108":10,"109":6,"110":10,"111":8,"112":4,"113":2,"114":4,"115":2,"125":10,"126":6,"127":10,"128":6,"129":5,"130":4,"131":4,"132":2,"135":10,"136":6,"137":9,"138":6,"139":6,"140":3,"141":4,"142":2}}],["g1",{"2":{"3":5,"4":1,"6":6,"91":4,"94":12,"95":6,"96":9,"97":6,"98":6,"99":3,"100":2,"101":4,"104":3,"105":21,"108":9,"109":6,"110":10,"111":8,"112":4,"113":2,"114":2,"115":4,"125":10,"126":6,"127":9,"128":6,"129":5,"130":4,"131":2,"132":4,"135":10,"136":6,"137":9,"138":6,"139":6,"140":3,"141":2,"142":4}}],["generic",{"2":{"22":1,"177":1}}],["generation",{"2":{"180":2}}],["generated",{"2":{"31":1,"32":1,"50":1,"53":1,"56":1,"59":1,"60":1,"63":1,"64":1,"66":1,"69":1,"71":1,"73":1,"74":1,"76":1,"82":1,"85":1,"88":1,"91":1,"101":1,"104":1,"105":1,"115":1,"116":1,"119":1,"122":1,"132":1,"142":1,"145":1,"146":1,"147":1,"153":1,"154":1,"155":1,"156":1,"160":1,"163":1,"166":1,"169":1,"170":1,"171":1,"173":1,"177":1,"180":1,"184":1,"185":1,"186":1,"188":1,"189":1,"198":1}}],["generate",{"2":{"7":1,"13":2,"180":1,"198":2}}],["generalization",{"2":{"57":1}}],["generalized",{"2":{"6":1,"57":3,"59":1}}],["generalise",{"2":{"6":4,"180":1}}],["generally",{"2":{"6":2,"158":2,"188":3}}],["general",{"0":{"2":1,"4":1},"1":{"3":1,"4":1},"2":{"18":1,"20":1,"25":1,"27":1,"148":1,"154":1,"173":1}}],["getfeature",{"2":{"153":1,"154":1,"156":3,"189":5}}],["getcolumn",{"2":{"153":2,"154":2,"156":1}}],["getgeom",{"2":{"100":1,"101":1,"114":1,"115":1,"122":2,"131":1,"132":1,"141":1,"142":1,"153":3,"154":2,"156":3,"180":3,"189":6}}],["getring",{"2":{"64":1}}],["getindex",{"2":{"59":2}}],["gethole",{"2":{"53":1,"56":1,"63":1,"64":4,"66":1,"69":1,"70":2,"72":2,"76":6,"85":1,"88":2,"116":5,"163":1,"191":1}}],["getexterior",{"2":{"53":1,"56":1,"59":1,"63":1,"64":3,"66":1,"69":1,"70":2,"72":2,"75":3,"76":6,"85":1,"88":2,"116":5,"145":1,"146":1,"163":1,"191":1}}],["getpolygon",{"2":{"71":3,"73":2,"76":2,"88":3}}],["getpoint",{"2":{"52":1,"53":5,"55":2,"56":1,"59":1,"62":1,"63":6,"64":3,"65":2,"66":3,"68":3,"84":1,"85":6,"87":4,"88":13,"90":4,"93":1,"103":1,"105":7,"107":4,"116":28,"118":2,"121":4,"122":2,"124":2,"127":2,"134":4,"145":5,"163":3,"175":4,"177":2,"184":1,"189":3,"191":2}}],["getproperty",{"2":{"13":2,"14":1}}],["get",{"2":{"6":1,"13":1,"32":1,"55":1,"60":1,"64":9,"70":1,"71":1,"72":1,"73":2,"75":2,"82":2,"146":4,"153":3,"154":1,"173":1,"176":3,"180":1,"181":1,"183":1,"184":2,"188":5,"189":1,"191":2,"199":2}}],["geoparquet",{"2":{"195":4}}],["geopoly1",{"2":{"193":1,"194":1}}],["geopoly2",{"2":{"193":1,"194":1}}],["geoaxis",{"2":{"192":4,"196":1}}],["geographic",{"2":{"158":1,"192":1,"195":1}}],["geographiclib",{"2":{"6":1,"176":1}}],["geointeface",{"2":{"151":1}}],["geointerace",{"2":{"22":1}}],["geointerfacemakie",{"2":{"179":1,"198":1,"199":1}}],["geointerface",{"0":{"30":1},"2":{"1":20,"3":9,"4":2,"6":35,"11":1,"13":1,"14":1,"15":1,"22":3,"25":1,"27":1,"30":1,"31":7,"52":1,"53":1,"55":1,"56":1,"59":23,"62":1,"63":1,"64":1,"65":1,"66":1,"68":1,"69":1,"70":1,"72":1,"73":1,"75":1,"79":1,"80":1,"81":1,"84":1,"85":11,"87":1,"88":2,"90":1,"91":2,"93":1,"94":2,"103":1,"104":2,"105":1,"107":1,"108":3,"118":1,"119":2,"121":1,"122":2,"124":1,"125":2,"134":1,"135":2,"145":5,"146":1,"150":3,"151":3,"153":11,"156":5,"162":9,"165":1,"166":1,"168":23,"170":1,"172":2,"175":1,"176":1,"179":1,"180":2,"185":13,"186":1,"189":1,"190":1,"191":122,"192":5,"193":9,"196":1,"198":1,"199":1}}],["geo",{"2":{"146":1,"176":4,"192":2}}],["geotable",{"2":{"29":1}}],["geojson",{"2":{"11":1,"180":2,"190":1,"192":4,"195":3}}],["geodataframes",{"2":{"195":3}}],["geodesy",{"2":{"158":1}}],["geodesic`",{"2":{"176":2}}],["geodesic",{"0":{"196":1},"2":{"6":7,"31":3,"158":6,"175":6,"176":8,"177":3,"196":1}}],["geodesicsegments",{"2":{"0":1,"6":1,"174":1,"175":3,"176":6,"177":1,"196":1}}],["geod",{"2":{"6":2,"176":3}}],["geoformattypes",{"2":{"1":2,"172":2,"190":1,"192":2,"193":8}}],["geomakie",{"0":{"192":1},"2":{"190":2,"192":5,"196":2}}],["geomtype",{"2":{"153":2,"156":1}}],["geoms",{"2":{"50":3,"82":1,"116":1,"153":12,"156":14}}],["geomfromgeos",{"2":{"32":1,"176":1}}],["geom2",{"2":{"3":8,"4":1,"6":9,"85":6,"88":1,"105":6,"108":1,"119":3,"122":6,"125":1,"135":2}}],["geom1",{"2":{"3":8,"4":1,"6":9,"85":6,"88":1,"105":4,"108":1,"119":3,"122":6,"125":1,"135":2}}],["geometrical",{"2":{"197":1}}],["geometric",{"2":{"25":2,"27":2,"62":1,"158":1}}],["geometries",{"0":{"95":1,"96":1,"97":1,"98":1,"99":1,"100":1,"101":1,"109":1,"110":1,"111":1,"112":1,"113":1,"114":1,"115":1,"126":1,"127":1,"128":1,"129":1,"130":1,"131":1,"132":1,"136":1,"137":1,"138":1,"139":1,"140":1,"141":1,"142":1,"191":1,"192":1,"193":1},"2":{"1":6,"3":8,"4":14,"6":55,"18":3,"20":1,"22":3,"25":2,"27":2,"29":4,"52":1,"53":3,"56":4,"63":1,"66":2,"69":1,"70":5,"72":5,"73":4,"75":5,"82":6,"84":1,"85":4,"87":1,"88":7,"91":1,"93":1,"94":1,"105":2,"108":2,"116":1,"118":1,"119":1,"121":2,"122":3,"124":2,"125":2,"135":2,"137":1,"148":1,"150":3,"152":2,"153":6,"154":3,"155":1,"156":11,"170":1,"172":2,"174":3,"175":1,"176":4,"177":1,"178":1,"181":1,"182":1,"183":1,"186":1,"189":1,"190":5,"192":1,"193":2,"194":2,"195":1,"197":5,"198":1,"200":1}}],["geometry=",{"2":{"194":1}}],["geometrybasics",{"2":{"31":3,"58":2,"59":10,"82":1,"84":1,"94":1,"125":1,"135":1}}],["geometrycolumns",{"2":{"153":5,"154":2,"156":1}}],["geometrycollections",{"2":{"197":1}}],["geometrycollectiontrait",{"2":{"23":1,"32":1,"100":1,"101":1,"114":1,"115":1,"131":1,"132":1,"141":1,"142":1}}],["geometrycollection",{"2":{"6":1,"23":1,"199":1}}],["geometrycorrections",{"2":{"165":1}}],["geometrycorrection",{"2":{"0":1,"6":11,"163":2,"164":2,"165":13,"166":11,"169":4}}],["geometry",{"0":{"2":1,"72":1,"100":2,"101":2,"114":2,"115":2,"131":2,"132":2,"141":2,"142":2,"164":1,"172":1,"178":1,"190":1,"194":1},"1":{"3":1,"4":1,"165":1,"166":1,"173":1,"179":1,"180":1,"191":1,"192":1,"193":1,"194":1,"195":1},"2":{"1":10,"3":24,"4":12,"6":67,"9":1,"11":2,"18":4,"20":1,"23":3,"29":1,"31":1,"53":7,"56":4,"60":4,"63":3,"65":1,"66":4,"69":4,"80":1,"84":4,"85":6,"88":8,"90":5,"91":2,"93":4,"94":5,"95":4,"96":1,"100":3,"101":4,"103":4,"104":4,"105":3,"107":2,"108":5,"109":4,"110":2,"111":2,"114":3,"115":4,"116":2,"118":2,"122":3,"124":3,"125":4,"126":4,"127":2,"131":3,"132":4,"134":4,"135":5,"136":4,"141":3,"142":4,"144":1,"148":4,"150":3,"151":2,"153":28,"154":12,"156":10,"158":5,"162":1,"163":1,"164":2,"165":23,"166":6,"170":1,"171":1,"172":6,"174":3,"175":1,"176":5,"177":10,"180":6,"189":6,"191":1,"192":1,"193":4,"194":2,"195":2,"197":1,"198":7,"199":4,"200":2}}],["geometryopsprojext",{"2":{"172":1,"173":1,"176":1,"177":1}}],["geometryopslibgeosext",{"2":{"60":1}}],["geometryopscore",{"2":{"0":2,"1":2,"31":2,"150":2,"156":4}}],["geometryops",{"0":{"0":1,"25":1,"31":1},"1":{"1":1,"2":1,"3":1,"4":1,"5":1,"6":1},"2":{"0":105,"1":4,"3":18,"4":9,"5":3,"6":106,"7":2,"11":1,"13":1,"14":1,"15":1,"17":2,"25":1,"26":4,"27":1,"31":1,"32":2,"52":1,"55":1,"58":5,"60":1,"62":1,"65":1,"68":1,"69":1,"70":1,"72":1,"73":1,"75":1,"77":1,"79":1,"80":1,"81":3,"84":1,"87":1,"88":1,"90":1,"91":2,"93":1,"94":2,"103":1,"104":2,"105":2,"107":1,"108":2,"118":1,"119":1,"121":1,"122":1,"124":1,"125":2,"134":1,"135":2,"145":3,"146":2,"150":1,"153":1,"157":2,"158":1,"162":1,"165":1,"166":4,"168":1,"173":1,"175":1,"176":2,"179":1,"180":2,"185":1,"187":1,"188":2,"189":1,"190":1,"191":3,"196":1,"197":1,"198":2,"199":2}}],["geom",{"2":{"1":7,"4":21,"6":41,"18":3,"31":12,"32":10,"35":5,"36":5,"37":5,"38":5,"40":4,"41":4,"42":4,"43":4,"44":4,"45":4,"46":4,"47":4,"48":4,"49":4,"53":25,"56":17,"63":28,"64":16,"66":6,"69":15,"70":8,"71":2,"72":12,"73":10,"75":9,"76":9,"85":28,"88":45,"94":4,"105":1,"108":4,"116":3,"122":1,"125":4,"135":4,"145":3,"146":1,"148":2,"150":4,"153":34,"154":13,"156":51,"169":11,"171":4,"177":24,"180":36,"184":3,"185":7,"186":4,"189":13,"199":6}}],["geospatial",{"0":{"193":1,"195":1},"2":{"190":5,"192":1,"193":3,"195":5}}],["geoscontext",{"2":{"176":1}}],["geosdensify",{"2":{"32":2,"176":3}}],["geos",{"0":{"188":1},"2":{"0":1,"6":6,"32":8,"33":1,"35":1,"36":1,"37":1,"38":1,"40":1,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1,"47":1,"48":1,"49":1,"50":1,"60":4,"77":1,"81":4,"82":1,"158":1,"178":5,"180":2,"187":1,"188":12}}],["got",{"2":{"184":3}}],["goes",{"2":{"6":2,"145":2}}],["good",{"2":{"6":1,"59":1,"188":2,"193":1}}],["going",{"2":{"4":4,"6":5,"66":2,"81":1,"88":5,"145":1,"146":1,"190":1}}],["go",{"2":{"1":5,"3":17,"4":2,"6":40,"11":9,"13":1,"14":1,"15":6,"18":1,"31":2,"32":5,"35":1,"36":1,"37":1,"38":1,"40":1,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1,"47":1,"48":1,"49":1,"50":2,"52":1,"55":1,"59":1,"62":2,"65":1,"68":2,"69":2,"70":2,"72":2,"73":2,"75":2,"79":3,"80":2,"81":8,"82":2,"84":6,"87":1,"88":2,"90":2,"91":2,"93":1,"94":2,"103":2,"104":2,"105":1,"107":1,"108":2,"118":1,"119":2,"121":1,"122":2,"124":1,"125":2,"134":2,"135":2,"145":6,"146":2,"148":1,"150":2,"153":2,"156":2,"162":3,"168":3,"175":7,"176":8,"179":2,"180":49,"185":3,"188":1,"189":2,"190":1,"191":3,"193":1,"195":1,"196":3,"197":9,"198":2,"199":5,"200":1}}],["gtrait",{"2":{"85":2}}],["gt",{"2":{"1":1,"6":1,"9":2,"10":1,"11":5,"64":3,"145":3,"150":1}}],["gif",{"2":{"182":1}}],["gives",{"2":{"199":1}}],["give",{"2":{"116":1,"195":1}}],["given",{"2":{"4":7,"6":21,"18":2,"29":1,"52":1,"53":1,"56":2,"63":3,"64":8,"66":3,"69":5,"70":1,"71":2,"72":1,"73":2,"75":1,"76":2,"85":8,"88":4,"90":1,"91":1,"94":1,"103":1,"104":1,"108":1,"116":7,"118":1,"119":1,"125":1,"127":1,"135":1,"148":1,"154":1,"165":4,"166":1,"168":1,"174":2,"176":2,"177":2,"183":1}}],["github",{"2":{"6":2,"82":1,"158":1}}],["gis",{"2":{"5":1,"6":1,"25":1,"27":1,"59":1}}],["gi",{"2":{"1":13,"3":42,"4":31,"6":108,"11":8,"13":15,"14":15,"15":8,"18":2,"31":1,"32":12,"35":3,"36":3,"37":3,"38":3,"40":2,"41":2,"42":2,"43":2,"44":2,"45":2,"46":2,"47":2,"48":2,"49":2,"50":3,"52":3,"53":30,"55":5,"56":21,"58":1,"62":5,"63":38,"64":34,"65":5,"66":17,"68":6,"69":13,"70":19,"71":16,"72":25,"73":24,"75":17,"76":36,"79":1,"80":1,"81":3,"82":4,"84":8,"85":42,"87":7,"88":99,"90":7,"91":3,"93":3,"94":4,"95":6,"96":12,"97":10,"98":7,"99":4,"100":8,"101":6,"103":3,"104":3,"105":21,"107":7,"108":2,"109":3,"110":12,"111":14,"112":4,"113":2,"114":8,"115":6,"116":59,"118":5,"119":3,"121":7,"122":51,"124":5,"125":3,"126":6,"127":15,"128":10,"129":7,"130":4,"131":8,"132":6,"134":7,"135":2,"136":6,"137":12,"138":10,"139":7,"140":4,"141":8,"142":6,"145":32,"146":15,"148":4,"150":9,"153":42,"154":25,"156":57,"159":9,"162":2,"163":10,"165":9,"168":3,"169":8,"170":1,"171":5,"175":6,"176":6,"177":13,"179":2,"180":34,"184":4,"185":9,"186":5,"189":55,"190":1,"191":21,"192":2,"193":2,"196":2,"198":5,"199":2}}],["t8vkb",{"2":{"192":1}}],["ty",{"2":{"146":3}}],["typing",{"2":{"60":1,"173":1,"176":1}}],["typically",{"2":{"57":1,"194":1}}],["typemax",{"2":{"85":1,"146":9}}],["typeof",{"2":{"19":1,"59":3,"153":2,"156":1,"159":1,"177":1,"200":1}}],["type=",{"2":{"6":2}}],["type2",{"2":{"6":2,"88":4}}],["type1",{"2":{"6":2,"88":5}}],["types",{"0":{"157":1,"187":1},"1":{"158":1,"159":1,"160":1,"188":1},"2":{"6":4,"23":1,"24":1,"31":1,"59":3,"66":1,"69":1,"116":3,"157":1,"160":1,"180":1,"187":3,"197":1}}],["type",{"2":{"4":11,"5":1,"6":40,"11":1,"22":2,"23":1,"24":2,"30":2,"53":9,"56":11,"58":1,"59":8,"63":8,"64":21,"66":8,"69":6,"70":7,"71":6,"72":8,"73":19,"75":7,"76":5,"85":28,"88":2,"153":5,"156":41,"158":2,"159":3,"160":6,"164":1,"165":5,"166":2,"171":1,"176":1,"177":1,"180":3,"186":1,"188":1,"189":3,"196":1}}],["tx",{"2":{"146":3}}],["tᵢ",{"2":{"59":1}}],["tutorial",{"2":{"190":1,"197":1}}],["tutorials",{"2":{"26":2}}],["tups",{"2":{"163":4}}],["tuplepoint",{"2":{"31":3,"189":1}}],["tuple",{"0":{"186":1},"2":{"4":1,"6":19,"31":2,"59":2,"63":3,"64":5,"66":3,"69":1,"73":31,"116":17,"145":2,"146":5,"153":2,"162":6,"168":13,"175":1,"180":2,"182":1,"184":2,"189":6,"191":99,"198":2}}],["tuples",{"2":{"0":1,"6":2,"31":1,"60":1,"63":1,"64":1,"69":1,"70":3,"71":1,"72":2,"73":1,"75":4,"76":5,"82":2,"163":2,"169":2,"180":2,"186":2,"199":2}}],["turf",{"2":{"145":1}}],["turned",{"2":{"146":1,"153":1}}],["turning",{"2":{"146":8}}],["turn",{"2":{"6":1,"146":1}}],["temporary",{"2":{"64":1}}],["term",{"2":{"56":1}}],["terms",{"2":{"6":1,"59":1}}],["teach",{"2":{"26":1}}],["technically",{"2":{"23":1,"161":1}}],["technique",{"2":{"11":1}}],["tell",{"2":{"18":1,"116":1,"160":1,"200":1}}],["test",{"2":{"64":1,"180":2,"188":1}}],["testing",{"0":{"15":1}}],["tests",{"2":{"9":2}}],["text=",{"2":{"6":2}}],["t2",{"2":{"6":5,"59":47,"95":1,"105":2,"126":1,"136":1}}],["t1",{"2":{"6":6,"59":51,"105":2}}],["t=float64",{"2":{"4":1,"6":3,"63":3}}],["two",{"2":{"3":5,"4":10,"6":23,"23":2,"53":2,"55":1,"59":1,"63":1,"64":6,"66":3,"70":2,"72":1,"73":14,"75":4,"76":3,"85":5,"87":3,"88":12,"90":2,"91":1,"93":1,"103":1,"104":1,"105":1,"107":1,"116":2,"118":2,"119":2,"121":4,"122":5,"124":3,"125":1,"134":2,"146":3,"165":2,"166":1,"168":2,"169":2,"175":1,"188":1,"191":1,"197":3,"198":3}}],["task",{"2":{"153":3,"154":3}}],["tasks",{"2":{"153":5,"154":5}}],["taskrange",{"2":{"153":5,"154":5}}],["tags",{"2":{"64":4}}],["taget",{"2":{"6":2}}],["taylor",{"2":{"6":1,"59":1}}],["table2",{"2":{"197":4}}],["table1",{"2":{"197":12}}],["tables",{"2":{"22":4,"31":1,"153":6,"154":5,"156":2}}],["table",{"0":{"194":1},"2":{"6":2,"18":1,"29":2,"153":13,"154":4,"177":1,"180":1,"194":1,"195":1,"197":2}}],["taking",{"2":{"6":3,"63":1,"70":1,"71":1,"72":1,"73":1,"75":1,"76":1,"167":1}}],["takes",{"2":{"64":3,"197":1}}],["taken",{"2":{"20":1,"58":1}}],["take",{"2":{"1":1,"6":2,"29":1,"64":3,"70":1,"71":1,"72":1,"73":1,"76":1,"145":2,"146":2,"148":1,"154":1,"172":1}}],["target=gi",{"2":{"64":1}}],["target=nothing",{"2":{"35":1,"36":1,"37":1,"38":1,"70":1,"72":1,"75":1}}],["targets",{"2":{"23":1,"53":2,"56":3,"66":3,"85":3}}],["target",{"0":{"23":1},"2":{"1":14,"6":11,"15":3,"22":2,"23":2,"32":1,"59":1,"63":2,"64":1,"70":5,"71":11,"72":6,"73":10,"75":4,"76":13,"146":1,"148":1,"150":8,"151":5,"152":1,"153":34,"154":33,"156":87,"159":3,"169":2,"172":3,"180":2}}],["tilted",{"2":{"66":1}}],["tie",{"2":{"53":1}}],["timings",{"2":{"13":5}}],["timing",{"2":{"13":2}}],["times",{"2":{"4":1,"6":1,"170":1}}],["time",{"2":{"1":5,"13":3,"24":1,"58":1,"146":1,"158":1,"172":4,"188":1,"191":1,"192":1,"193":2,"198":1,"199":1}}],["title",{"2":{"13":2,"58":2,"81":2,"146":1,"176":1,"180":2}}],["tip",{"2":{"1":1,"5":1,"6":1,"59":1,"172":1,"197":1}}],["tree",{"2":{"197":1}}],["treating",{"2":{"180":1}}],["treated",{"2":{"116":5,"191":1}}],["treats",{"2":{"56":1,"85":1}}],["trials",{"2":{"176":2,"180":2}}],["triangles",{"2":{"57":1}}],["triangle",{"2":{"6":1,"57":4,"183":4,"198":1}}],["triangulation",{"2":{"6":1,"31":1,"82":1}}],["trivially",{"2":{"148":1}}],["try",{"2":{"74":3,"146":1,"151":1,"153":3,"154":3,"156":3,"199":1}}],["tr",{"2":{"56":3}}],["trues",{"2":{"169":3}}],["true",{"0":{"24":1},"2":{"1":5,"3":25,"4":3,"6":40,"31":1,"53":7,"56":1,"58":2,"60":1,"64":30,"66":6,"69":1,"70":1,"71":2,"72":1,"73":1,"75":2,"76":3,"85":4,"88":21,"90":1,"91":1,"93":2,"94":6,"96":1,"97":1,"98":4,"100":1,"101":1,"103":2,"104":1,"105":9,"107":2,"108":2,"110":1,"111":1,"112":3,"114":1,"115":1,"116":78,"118":3,"119":1,"121":2,"122":18,"124":2,"125":6,"128":1,"129":1,"131":1,"132":1,"134":2,"135":7,"137":1,"138":1,"139":4,"141":1,"142":1,"145":4,"146":5,"150":2,"153":6,"154":2,"156":2,"158":1,"160":3,"169":1,"173":1,"176":1,"180":1,"197":1,"200":1}}],["traditional",{"2":{"146":1,"158":1}}],["traverse",{"2":{"64":1}}],["traced",{"2":{"64":1}}],["traces",{"2":{"64":1}}],["trace",{"2":{"64":2,"70":1,"72":1,"75":1}}],["track",{"2":{"64":3,"169":2}}],["tracing",{"2":{"6":1,"64":4,"66":1,"71":5,"73":2,"76":2}}],["transverse",{"2":{"192":1}}],["translate",{"2":{"58":2}}],["translation",{"2":{"1":2,"6":2,"180":2,"185":2,"191":3,"193":1}}],["transformations",{"2":{"31":10}}],["transformation",{"0":{"185":1},"2":{"6":1,"146":1,"153":1,"164":1,"172":1,"190":1}}],["transform",{"2":{"0":2,"1":6,"6":3,"15":2,"31":1,"148":1,"172":2,"180":1,"185":4,"191":4,"193":1}}],["trait`",{"2":{"156":1}}],["trait2",{"2":{"85":10,"88":2,"110":2,"111":2,"122":2,"127":2,"129":2,"130":2}}],["trait1",{"2":{"85":12,"88":2,"110":2,"111":2,"122":2,"127":2,"129":2,"130":2}}],["traits",{"2":{"6":1,"18":1,"122":1,"151":2,"159":3,"165":2,"180":2}}],["trait",{"2":{"1":5,"3":2,"4":2,"6":6,"18":7,"20":1,"22":1,"31":1,"32":1,"53":2,"56":5,"59":6,"63":7,"66":2,"69":3,"70":2,"71":2,"72":6,"73":4,"75":2,"76":2,"85":7,"88":11,"94":3,"105":2,"108":3,"116":4,"122":10,"125":3,"135":3,"145":1,"148":1,"150":5,"151":3,"153":19,"154":9,"156":27,"159":8,"165":9,"166":1,"177":2,"180":2,"189":7,"191":1}}],["traittarget",{"0":{"159":1},"2":{"1":2,"31":2,"32":1,"53":1,"56":1,"63":1,"66":1,"70":2,"71":4,"72":3,"73":4,"75":2,"76":4,"85":1,"148":1,"150":2,"153":6,"154":6,"158":1,"159":20,"177":1,"180":1}}],["thus",{"2":{"53":1,"56":1,"64":3,"71":1,"73":1,"76":1}}],["though",{"2":{"20":1,"162":1}}],["those",{"2":{"6":1,"33":1,"53":1,"64":1,"73":1,"96":1,"137":1,"158":1,"177":1}}],["thing",{"0":{"30":1}}],["things",{"2":{"9":1}}],["this",{"0":{"30":1},"2":{"0":1,"1":1,"3":1,"4":6,"5":1,"6":30,"7":1,"18":1,"23":3,"24":2,"25":2,"27":2,"29":1,"31":1,"32":3,"33":1,"50":1,"52":2,"53":6,"55":2,"56":9,"58":3,"59":11,"60":2,"62":1,"63":4,"64":26,"65":1,"66":4,"68":1,"69":5,"71":1,"73":5,"74":1,"75":1,"76":2,"82":5,"84":4,"85":6,"88":4,"90":1,"91":2,"93":3,"94":3,"101":1,"103":1,"104":2,"105":1,"108":3,"115":1,"116":3,"119":2,"121":1,"122":4,"125":3,"132":1,"134":1,"135":3,"142":1,"144":1,"145":3,"146":8,"147":2,"148":2,"151":2,"153":15,"154":7,"155":2,"156":2,"157":1,"158":4,"159":2,"160":4,"161":3,"163":3,"164":1,"165":6,"166":7,"167":4,"169":3,"170":2,"171":2,"172":4,"173":3,"174":3,"175":5,"176":5,"177":5,"178":2,"180":3,"184":9,"185":1,"186":1,"187":3,"188":2,"189":1,"190":1,"191":2,"192":4,"193":3,"194":3,"195":1,"197":3,"198":3,"199":3,"200":1}}],["three",{"2":{"26":1,"57":1,"73":1,"158":1,"177":1}}],["thread",{"2":{"153":3,"154":3}}],["threading",{"0":{"153":1},"2":{"153":5,"154":2,"160":1}}],["threads",{"2":{"1":1,"150":1,"153":5,"154":7}}],["threaded=",{"2":{"153":4,"154":5}}],["threaded=true",{"2":{"153":1}}],["threaded=false",{"2":{"56":1,"63":4,"66":2,"85":8,"153":1,"154":1,"170":1,"180":1}}],["threaded==true",{"2":{"1":1,"150":1,"153":1}}],["threaded",{"2":{"1":3,"4":1,"6":5,"24":1,"31":1,"32":2,"53":2,"56":1,"63":3,"66":3,"85":5,"150":2,"153":25,"154":27,"155":1,"160":2,"170":1,"177":13,"180":1}}],["through",{"2":{"6":4,"53":1,"59":1,"64":3,"66":1,"68":1,"69":2,"82":1,"94":1,"116":5,"135":1,"146":1,"151":1,"153":1,"166":2,"169":2,"182":1,"188":1,"191":1}}],["thrown",{"2":{"151":1}}],["throws",{"2":{"6":1,"188":1}}],["throw",{"2":{"4":1,"6":1,"76":1,"88":1,"146":1,"153":1,"154":1,"156":4}}],["than",{"2":{"1":1,"3":1,"6":10,"11":1,"64":2,"96":1,"105":1,"122":1,"137":1,"145":1,"146":2,"150":1,"152":1,"153":1,"157":1,"166":2,"167":1,"168":1,"169":2,"174":1,"176":2,"177":1,"181":1,"188":2}}],["that",{"2":{"1":1,"3":3,"4":8,"6":40,"9":1,"17":1,"18":4,"19":3,"20":1,"22":3,"25":1,"26":1,"27":1,"53":4,"55":2,"56":4,"59":4,"60":2,"62":2,"63":3,"64":17,"65":3,"66":5,"70":4,"71":5,"72":4,"73":14,"75":3,"76":5,"77":1,"81":3,"82":4,"85":2,"87":1,"88":11,"90":4,"91":1,"93":2,"94":1,"104":1,"107":1,"108":1,"116":3,"118":1,"119":1,"121":2,"122":10,"124":2,"125":2,"127":2,"134":3,"135":1,"137":3,"145":1,"146":4,"148":1,"150":1,"153":5,"154":1,"155":1,"156":4,"157":2,"158":6,"159":1,"160":3,"161":3,"162":2,"163":1,"164":3,"165":2,"166":4,"167":2,"168":3,"169":4,"172":1,"174":2,"175":2,"176":3,"177":2,"180":2,"182":1,"184":1,"188":1,"192":4,"193":1,"194":2,"195":3,"197":1,"198":2}}],["theta",{"2":{"158":1}}],["theorem",{"2":{"85":1}}],["themselves",{"2":{"59":1}}],["them",{"2":{"25":1,"27":1,"64":1,"146":2,"147":2,"153":3,"156":2,"162":2,"167":1,"168":1,"180":1,"191":1,"193":1,"195":3}}],["thereof",{"2":{"148":1,"154":1}}],["therefore",{"2":{"84":1,"161":1}}],["there",{"2":{"6":2,"22":1,"53":1,"59":2,"60":1,"64":3,"66":1,"69":2,"71":1,"73":6,"74":1,"76":1,"116":1,"122":2,"146":3,"147":1,"148":1,"153":4,"158":1,"159":1,"162":1,"175":1,"184":1,"188":1,"192":2,"195":3}}],["then",{"2":{"6":5,"18":1,"22":1,"29":1,"53":3,"59":2,"64":6,"66":1,"70":1,"72":2,"73":6,"74":2,"75":2,"76":2,"82":1,"116":1,"146":2,"148":1,"151":1,"153":2,"154":3,"167":1,"176":2,"180":1,"192":1,"197":1,"198":1}}],["their",{"2":{"3":1,"4":1,"6":2,"59":1,"62":2,"76":1,"85":2,"113":1,"122":1,"124":1,"128":1,"129":1,"138":3,"139":3,"148":1,"154":1,"162":1,"168":1,"170":1}}],["they",{"2":{"3":3,"4":11,"6":24,"20":2,"22":1,"23":1,"24":1,"57":1,"63":2,"64":8,"66":2,"70":2,"72":1,"73":6,"75":2,"76":3,"81":1,"87":3,"88":16,"94":1,"107":1,"111":2,"112":1,"113":1,"116":2,"118":1,"121":2,"122":4,"127":1,"129":1,"130":1,"138":1,"139":1,"146":6,"153":2,"156":2,"161":1,"166":2,"169":2,"175":1,"184":1,"187":1,"198":1}}],["these",{"2":{"1":2,"6":4,"24":1,"53":1,"59":5,"64":3,"76":1,"87":1,"88":1,"90":1,"94":1,"103":1,"105":2,"107":1,"108":1,"118":1,"121":1,"122":1,"124":2,"125":1,"134":1,"135":1,"145":1,"146":3,"150":1,"151":1,"153":2,"172":1,"176":1,"180":2,"188":1,"195":1,"197":1}}],["the",{"0":{"26":1,"29":1,"62":1,"80":1,"81":1},"2":{"1":28,"3":64,"4":112,"5":12,"6":386,"7":5,"9":3,"10":1,"11":3,"17":5,"18":10,"19":2,"20":4,"22":4,"23":5,"24":3,"25":3,"26":4,"27":3,"29":3,"32":3,"52":1,"53":63,"55":12,"56":43,"57":15,"58":12,"59":71,"60":6,"62":6,"63":25,"64":116,"65":7,"66":47,"68":2,"69":7,"70":27,"71":23,"72":27,"73":82,"74":1,"75":29,"76":70,"77":6,"81":11,"82":16,"84":11,"85":81,"87":5,"88":47,"90":9,"91":11,"93":8,"94":25,"97":8,"98":9,"99":3,"100":3,"101":2,"103":4,"104":11,"105":5,"107":2,"108":21,"110":8,"111":6,"112":4,"114":3,"115":2,"116":123,"118":5,"119":6,"121":5,"122":26,"124":5,"125":21,"127":6,"128":10,"129":7,"130":3,"131":3,"132":2,"134":7,"135":24,"137":2,"138":9,"139":9,"140":6,"141":2,"142":2,"144":1,"145":8,"146":47,"147":1,"148":9,"150":14,"151":11,"153":69,"154":23,"155":2,"156":18,"157":1,"158":24,"160":5,"161":5,"162":5,"163":4,"164":2,"165":13,"166":10,"167":4,"168":8,"169":6,"170":1,"171":5,"172":14,"173":2,"174":3,"175":5,"176":26,"177":21,"178":3,"180":18,"181":3,"182":6,"183":3,"184":2,"185":3,"186":1,"187":2,"188":21,"191":10,"192":11,"193":10,"194":2,"195":4,"197":10,"198":17,"199":3}}],["t",{"2":{"0":1,"4":28,"6":54,"9":1,"23":1,"31":8,"32":1,"53":32,"56":41,"59":26,"63":38,"64":74,"66":44,"69":18,"70":12,"71":12,"72":12,"73":156,"75":10,"76":20,"85":87,"88":17,"93":1,"111":1,"116":24,"122":1,"124":1,"128":2,"129":1,"146":14,"153":5,"154":4,"156":2,"158":5,"159":14,"160":2,"166":2,"169":2,"176":3,"177":4,"180":1,"186":7,"188":3,"189":10,"191":8,"199":1}}],["tokyo",{"2":{"199":1}}],["toy",{"2":{"197":1}}],["together",{"2":{"73":1,"76":1,"191":1,"198":1}}],["touching",{"0":{"128":1},"2":{"73":1,"76":1}}],["touch",{"0":{"129":1,"130":1,"131":1},"2":{"64":1,"124":1,"127":2,"129":1,"131":1}}],["touches",{"0":{"42":1,"123":1,"124":1,"127":1},"1":{"124":1,"125":1},"2":{"0":2,"3":3,"6":3,"31":1,"42":2,"123":1,"124":3,"125":11,"126":6,"127":11,"128":15,"129":9,"130":8,"131":3,"132":4,"197":1}}],["totally",{"2":{"75":1}}],["total",{"2":{"56":1,"59":2,"64":2,"66":1}}],["towards",{"2":{"25":1,"27":1}}],["topright",{"2":{"180":1}}],["topologypreserve",{"2":{"178":1}}],["topology",{"2":{"178":1}}],["topological",{"2":{"158":2}}],["top",{"2":{"20":1,"26":1,"64":1,"146":1}}],["took",{"2":{"199":1}}],["tools",{"2":{"17":1}}],["too",{"2":{"6":1,"73":1,"174":1,"177":1}}],["tol^2",{"2":{"181":1,"182":1}}],["tolerances",{"2":{"183":1,"184":29}}],["tolerance",{"2":{"181":1,"182":1,"183":1,"184":17}}],["tol",{"2":{"6":12,"176":2,"180":18,"181":7,"182":15,"183":8,"184":18}}],["todo",{"2":{"3":2,"6":2,"32":1,"64":1,"73":1,"82":2,"105":4,"122":1,"146":1,"153":1,"160":1,"163":1,"180":1}}],["to",{"0":{"9":1,"23":1,"26":1,"74":1,"95":1,"109":1,"126":1,"136":1},"2":{"0":2,"1":22,"3":1,"4":33,"5":4,"6":125,"7":2,"9":3,"10":2,"11":2,"13":1,"17":2,"18":9,"19":1,"20":2,"22":3,"23":4,"24":2,"25":3,"26":2,"27":3,"29":4,"30":1,"52":1,"53":8,"55":1,"56":4,"57":4,"58":1,"59":26,"60":2,"62":2,"63":5,"64":32,"65":1,"66":12,"68":1,"69":3,"70":8,"71":4,"72":8,"73":27,"75":7,"76":11,"77":1,"79":1,"80":1,"81":4,"82":6,"84":6,"85":32,"87":2,"88":17,"90":1,"91":1,"93":2,"94":7,"103":3,"104":1,"105":2,"107":1,"108":7,"116":21,"118":2,"119":1,"121":3,"122":7,"124":1,"125":7,"127":1,"134":1,"135":7,"145":4,"146":16,"148":4,"150":9,"151":6,"152":1,"153":33,"154":11,"155":7,"156":23,"158":7,"159":2,"160":6,"161":2,"162":3,"164":4,"165":8,"166":5,"167":3,"168":1,"169":4,"170":3,"171":1,"172":4,"173":1,"174":3,"175":3,"176":9,"177":7,"178":1,"180":4,"181":1,"182":15,"183":1,"184":2,"185":4,"186":1,"187":2,"188":9,"189":28,"190":4,"191":6,"192":10,"193":6,"194":7,"195":5,"196":1,"197":7,"198":5,"199":2,"200":8}}],["o",{"2":{"154":24}}],["odd",{"2":{"116":1}}],["own",{"2":{"76":1,"148":2}}],["occur",{"2":{"73":1}}],["occurs",{"2":{"73":1}}],["occupied",{"2":{"55":1}}],["old",{"2":{"64":8,"153":3}}],["ourselves",{"2":{"176":1}}],["our",{"2":{"24":1,"176":1,"191":4,"192":3,"198":1}}],["out=3",{"2":{"116":1}}],["out=4",{"2":{"72":1}}],["out`",{"2":{"72":1,"116":1}}],["out",{"2":{"6":3,"64":12,"66":23,"73":6,"76":4,"82":1,"84":5,"94":4,"105":1,"108":3,"116":102,"122":2,"125":4,"135":4,"145":1,"153":1,"177":1,"188":2}}],["outside",{"2":{"3":2,"4":1,"6":3,"64":3,"66":1,"71":2,"73":4,"76":3,"84":2,"85":1,"93":1,"104":1,"107":1,"116":12,"122":3,"140":1}}],["outputs",{"2":{"64":1}}],["output",{"2":{"3":8,"4":1,"6":18,"18":1,"23":1,"69":2,"70":1,"72":1,"73":1,"75":1,"76":2,"88":1,"91":1,"94":1,"104":1,"108":1,"119":1,"122":1,"125":1,"135":1,"145":2,"180":1,"184":1,"189":1}}],["outerjoin",{"2":{"197":1}}],["outermost",{"2":{"153":1}}],["outer",{"2":{"1":1,"150":1,"151":1,"153":2,"198":1}}],["omit",{"2":{"6":1,"176":1}}],["obtain",{"2":{"153":1}}],["obtained",{"2":{"23":1}}],["observable",{"2":{"14":2}}],["obs",{"2":{"14":10}}],["obviously",{"2":{"4":1,"6":1,"170":1}}],["objects",{"2":{"1":4,"4":1,"6":10,"150":4,"151":5,"153":4,"154":1,"156":6,"170":1,"174":1,"180":2,"186":1}}],["object",{"2":{"1":4,"4":1,"5":1,"6":5,"20":1,"59":1,"84":1,"116":4,"150":1,"151":5,"153":1,"156":4,"170":1,"172":3,"176":1,"180":1,"186":1}}],["obj",{"2":{"1":4,"4":1,"6":10,"150":2,"153":1,"154":1,"156":13,"170":1,"171":2,"176":3,"180":3,"185":1,"186":1}}],["others",{"2":{"60":1,"169":1}}],["otherwise",{"2":{"4":2,"6":3,"60":1,"64":1,"71":1,"73":1,"85":2,"146":1,"171":1,"173":1,"176":1,"189":1}}],["other",{"0":{"4":1,"6":1,"156":1},"2":{"3":4,"6":10,"53":1,"64":5,"70":1,"72":1,"73":1,"75":2,"76":3,"77":2,"85":2,"88":2,"90":1,"96":1,"116":8,"118":1,"121":1,"122":4,"124":2,"125":2,"128":1,"137":1,"146":1,"151":1,"153":1,"156":2,"159":1,"166":2,"167":1,"169":2,"180":2,"182":2,"187":1,"188":1,"192":1,"197":4,"200":1}}],["ogc",{"0":{"3":1}}],["over=3",{"2":{"72":1}}],["over`",{"2":{"72":1}}],["overflow",{"2":{"69":1}}],["overhead",{"2":{"22":1,"153":1,"154":1}}],["overrides",{"0":{"33":1},"1":{"34":1,"35":1,"36":1,"37":1,"38":1,"39":1,"40":1,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1,"47":1,"48":1,"49":1,"50":1},"2":{"33":1}}],["override",{"2":{"6":1,"59":1}}],["overlapping",{"2":{"64":11,"73":4,"76":6}}],["overlap",{"2":{"3":4,"6":4,"9":1,"64":2,"73":8,"76":2,"94":1,"103":1,"116":2,"121":5,"122":6,"135":1,"167":1}}],["overlaps",{"0":{"46":1,"120":1,"121":1},"1":{"121":1,"122":1},"2":{"0":10,"3":5,"6":13,"31":1,"46":2,"73":1,"120":1,"121":3,"122":30,"146":1,"197":1}}],["over",{"2":{"1":1,"6":2,"55":2,"56":1,"63":3,"64":5,"66":1,"72":1,"73":11,"76":1,"81":1,"94":1,"105":1,"108":1,"116":15,"122":2,"125":1,"135":1,"150":1,"153":9,"154":11,"156":3,"191":3}}],["opposed",{"2":{"196":1}}],["opposite",{"2":{"3":5,"6":5,"64":2,"71":1,"73":2,"76":1,"91":2,"94":1,"104":2,"119":2,"135":1}}],["ops",{"2":{"154":1}}],["operable",{"2":{"158":1}}],["operates",{"2":{"153":1,"154":1,"161":1,"167":1}}],["operate",{"2":{"18":1,"29":1,"154":1}}],["operations",{"0":{"23":1,"34":1},"1":{"35":1,"36":1,"37":1,"38":1},"2":{"23":1,"26":1,"73":1,"76":1,"148":1}}],["operation",{"2":{"6":3,"64":1,"148":1,"154":1,"158":1,"166":2,"169":2,"188":2,"197":1}}],["open",{"2":{"64":1,"162":1,"192":1}}],["open>",{"2":{"6":2}}],["optimisation",{"2":{"116":1}}],["optimise",{"2":{"105":1}}],["optimal",{"2":{"6":1,"82":1}}],["options",{"2":{"14":1,"64":1}}],["optional",{"2":{"4":5,"6":6,"53":1,"56":2,"66":1,"85":2}}],["op",{"2":{"1":3,"19":2,"63":1,"150":3,"154":35}}],["on=2",{"2":{"116":1}}],["on`",{"2":{"116":1}}],["once",{"2":{"105":1,"122":1,"146":1}}],["onto",{"2":{"85":1}}],["ones",{"2":{"168":1}}],["oneunit",{"2":{"59":1}}],["one",{"2":{"1":1,"3":9,"6":14,"23":1,"53":4,"56":2,"58":1,"59":1,"63":1,"64":5,"66":3,"70":1,"71":1,"72":1,"73":32,"75":2,"76":2,"77":1,"84":2,"93":1,"100":1,"105":1,"107":1,"116":18,"121":1,"122":18,"124":2,"125":2,"128":3,"129":2,"130":2,"131":1,"132":1,"134":1,"135":1,"141":1,"145":1,"146":5,"153":1,"158":2,"168":1,"172":1,"176":1,"184":1,"185":1,"192":1,"197":1}}],["on",{"0":{"192":1},"2":{"1":2,"4":3,"6":9,"9":1,"18":1,"20":1,"23":2,"25":3,"27":3,"29":2,"32":1,"53":5,"56":3,"58":1,"59":2,"63":3,"64":28,"66":8,"69":1,"70":1,"71":5,"72":1,"73":4,"75":1,"76":2,"82":1,"84":1,"85":3,"88":2,"93":1,"94":7,"96":2,"97":3,"98":3,"99":1,"105":4,"108":6,"110":4,"111":1,"112":1,"116":125,"122":9,"125":7,"127":2,"135":7,"137":2,"138":1,"139":1,"146":3,"148":1,"150":2,"153":12,"154":4,"156":2,"158":4,"160":2,"161":1,"163":1,"166":1,"167":1,"174":1,"176":1,"177":1,"182":1,"190":1,"192":3,"193":1,"195":1,"196":1,"197":4,"198":1,"199":2,"200":2}}],["only",{"2":{"0":1,"5":1,"6":10,"23":1,"33":2,"56":2,"58":2,"59":1,"60":1,"63":2,"64":4,"66":3,"69":1,"70":1,"72":1,"73":1,"75":1,"82":3,"85":2,"88":3,"121":3,"122":1,"124":1,"146":2,"153":1,"154":2,"158":2,"168":1,"174":1,"175":1,"177":1,"187":1,"188":2,"193":1,"199":1,"200":1}}],["often",{"2":{"192":1}}],["offers",{"2":{"191":1}}],["offer",{"2":{"176":1}}],["offset",{"2":{"53":8,"88":1}}],["off",{"2":{"4":1,"6":1,"64":4,"88":1,"116":6,"154":1}}],["of",{"0":{"80":1},"2":{"1":7,"3":29,"4":66,"5":5,"6":190,"7":2,"9":4,"17":3,"18":5,"19":1,"20":2,"22":1,"23":2,"24":2,"25":1,"26":1,"27":1,"29":3,"32":2,"52":1,"53":38,"55":5,"56":28,"57":9,"58":1,"59":13,"60":1,"62":3,"63":12,"64":84,"65":5,"66":31,"69":5,"70":12,"71":13,"72":10,"73":48,"75":9,"76":20,"77":5,"81":3,"82":3,"84":6,"85":18,"87":2,"88":19,"90":4,"91":5,"93":6,"94":18,"96":1,"97":5,"98":5,"99":1,"100":2,"101":1,"104":5,"105":1,"107":3,"108":14,"110":1,"111":1,"112":1,"114":2,"115":1,"116":85,"118":1,"119":3,"121":1,"122":13,"124":2,"125":15,"127":3,"128":7,"129":5,"130":1,"131":2,"132":1,"134":4,"135":18,"137":3,"138":6,"139":6,"140":4,"141":1,"142":1,"144":2,"145":3,"146":19,"147":2,"148":3,"150":6,"151":5,"152":2,"153":21,"154":7,"156":2,"158":5,"159":2,"161":1,"162":1,"166":2,"167":4,"169":4,"171":3,"172":1,"174":2,"175":2,"176":5,"177":6,"180":8,"182":4,"183":2,"184":1,"186":1,"188":4,"189":2,"191":6,"192":4,"193":2,"194":1,"195":4,"197":4,"198":4,"199":2}}],["org",{"2":{"70":1,"72":1,"75":1,"116":2,"182":1}}],["organise",{"2":{"10":1}}],["orange",{"2":{"68":1,"84":1,"87":2,"90":2,"107":2,"121":2,"134":2}}],["oro",{"2":{"11":2}}],["originate",{"2":{"76":1}}],["originals",{"2":{"22":1}}],["original",{"2":{"6":4,"18":1,"22":1,"64":10,"69":5,"70":1,"72":1,"76":4,"151":1,"153":7,"165":1,"171":2,"177":1,"179":4,"180":1}}],["orient",{"0":{"13":1},"1":{"14":1,"15":1},"2":{"13":7,"14":12,"64":15,"73":53}}],["orientation",{"0":{"143":1},"1":{"144":1,"145":1},"2":{"6":2,"10":1,"31":1,"56":1,"64":10,"66":2,"70":1,"72":2,"73":2,"75":1,"116":16,"144":1,"180":1}}],["ordered",{"2":{"55":1}}],["order",{"0":{"81":1},"2":{"1":4,"3":1,"4":2,"6":5,"53":1,"55":1,"56":2,"59":1,"64":4,"73":1,"76":1,"81":4,"82":1,"85":2,"88":3,"91":1,"94":1,"104":1,"119":1,"150":2,"153":1,"154":1,"172":2,"180":1,"197":1}}],["or",{"0":{"24":1},"2":{"1":11,"3":3,"4":10,"6":46,"18":1,"20":1,"22":1,"23":1,"25":1,"27":1,"29":2,"31":1,"32":1,"53":4,"56":1,"59":1,"60":1,"62":2,"63":4,"64":24,"65":1,"69":1,"70":1,"71":3,"72":2,"73":9,"75":1,"76":2,"77":1,"82":1,"84":2,"85":4,"88":3,"90":1,"94":1,"96":2,"97":1,"98":1,"99":1,"100":1,"101":1,"107":2,"110":2,"111":3,"112":2,"113":1,"114":1,"115":1,"116":26,"118":2,"119":1,"121":1,"122":2,"124":1,"125":1,"131":1,"132":1,"135":1,"137":2,"141":1,"142":1,"144":2,"145":3,"146":7,"148":4,"150":7,"152":1,"153":8,"154":3,"155":2,"156":3,"158":2,"159":3,"160":1,"161":1,"163":1,"165":5,"166":2,"172":2,"173":1,"174":2,"176":2,"177":4,"178":1,"180":3,"182":2,"184":2,"186":1,"189":2,"197":1,"199":2}}],["ecosystem",{"2":{"157":1}}],["effects",{"2":{"153":2,"154":1}}],["efficiently",{"2":{"20":1}}],["efficient",{"2":{"6":1,"59":1,"70":2,"72":2,"75":2,"82":1,"163":1}}],["e2",{"2":{"116":4}}],["e1",{"2":{"116":8}}],["euclid",{"2":{"66":2,"85":11,"116":1,"181":1}}],["euclidean",{"2":{"4":1,"6":1,"59":13,"85":6,"158":6,"177":1}}],["everything",{"2":{"180":1}}],["everywhere",{"2":{"158":1}}],["every",{"2":{"64":1,"88":2,"151":1,"188":1}}],["evenly",{"2":{"198":1}}],["eventually",{"2":{"64":1}}],["even",{"2":{"56":2,"66":1,"73":1,"85":1,"88":1,"148":1,"162":1,"167":1}}],["evaluated",{"2":{"197":1}}],["eval",{"2":{"31":2,"153":1,"154":1}}],["epsg",{"2":{"192":5,"193":8}}],["eps",{"2":{"53":2,"73":14}}],["eponymous",{"2":{"6":1,"176":1}}],["est",{"2":{"199":1}}],["essentially",{"2":{"33":1,"159":1}}],["especially",{"2":{"6":1,"18":1,"23":1,"146":1}}],["eg",{"2":{"32":1}}],["etc",{"0":{"156":1},"2":{"20":2,"55":1,"82":1,"159":1,"195":1}}],["e",{"2":{"9":1,"22":1,"60":1,"146":1,"151":1,"153":2,"158":1,"191":2,"192":1,"195":1}}],["ellipsoid",{"2":{"158":2,"196":1}}],["ellipsoidal",{"2":{"6":1,"177":1}}],["eltype",{"2":{"146":4}}],["elements",{"2":{"100":1,"101":1,"114":1,"115":1,"122":1,"131":1,"132":1,"141":1,"142":1}}],["element",{"2":{"6":6,"52":1,"64":4,"69":2,"70":1,"72":1,"73":1,"75":3,"175":1,"192":1}}],["elsewhere",{"2":{"69":1}}],["elseif",{"2":{"64":5,"66":7,"70":1,"72":1,"73":14,"75":2,"76":1,"105":3,"116":10,"145":1,"146":2,"169":1,"182":2,"184":2}}],["else",{"2":{"3":6,"6":6,"53":1,"59":2,"64":19,"66":9,"69":1,"71":2,"73":9,"75":1,"76":9,"105":2,"116":17,"122":8,"146":20,"153":6,"154":2,"156":1,"163":1,"171":1,"173":1,"180":2,"182":4,"184":2,"185":1,"186":1,"188":1}}],["empty",{"2":{"4":2,"6":6,"53":2,"64":2,"70":1,"72":1,"73":2,"75":1,"146":2}}],["embedding",{"0":{"152":1,"170":1}}],["embedded",{"0":{"193":1},"2":{"146":1,"152":3,"190":1,"193":1}}],["embed",{"2":{"0":2,"4":1,"6":1,"152":1,"170":2}}],["errors",{"2":{"31":1,"73":1,"177":1}}],["error",{"0":{"173":1},"2":{"4":1,"6":4,"7":1,"18":1,"31":6,"32":2,"59":1,"60":4,"64":2,"73":2,"74":1,"88":2,"146":1,"151":1,"165":2,"173":3,"176":5,"184":4,"188":3,"189":1}}],["edgekeys",{"2":{"146":3}}],["edge",{"2":{"3":1,"4":3,"6":4,"20":1,"31":1,"56":1,"62":1,"64":25,"66":13,"70":1,"72":1,"73":6,"75":1,"85":4,"88":1,"96":2,"111":2,"112":1,"116":7,"122":21,"137":2,"138":1,"139":1,"146":7,"189":2}}],["edges`",{"2":{"73":1}}],["edges",{"2":{"0":1,"4":2,"6":9,"64":12,"66":8,"71":3,"73":10,"76":3,"85":3,"87":2,"88":1,"90":1,"96":1,"97":3,"98":7,"99":3,"107":1,"110":3,"113":1,"116":3,"122":20,"134":1,"137":1,"146":22,"189":29}}],["earlier",{"2":{"191":3}}],["earth",{"2":{"6":3,"25":1,"27":1,"158":3,"176":2,"177":1,"180":1,"192":2,"197":1}}],["easiest",{"2":{"194":1}}],["easier",{"2":{"160":1,"187":1}}],["easily",{"2":{"148":1}}],["east",{"2":{"66":9}}],["easy",{"2":{"1":1,"6":1,"167":1,"185":1}}],["eachindex",{"2":{"53":1,"145":1,"146":3,"153":1,"154":1,"181":1,"184":1}}],["each",{"2":{"3":2,"4":3,"6":6,"9":1,"18":2,"20":1,"53":4,"56":2,"57":4,"59":1,"64":5,"66":1,"73":3,"85":1,"88":2,"94":1,"108":1,"116":6,"121":1,"122":3,"125":1,"135":1,"145":1,"146":1,"153":1,"167":2,"180":1,"194":1,"195":1,"198":2,"199":2,"200":1}}],["equator",{"2":{"158":1}}],["equatorial",{"2":{"6":6,"176":4}}],["equality",{"2":{"64":1,"73":1,"197":1}}],["equal",{"2":{"3":2,"4":16,"6":19,"53":6,"73":1,"84":1,"85":4,"87":3,"88":22,"96":1,"110":1,"116":15,"121":1,"122":2,"127":2,"137":1,"162":1}}],["equals",{"0":{"40":1,"86":1,"87":1},"1":{"87":1,"88":1},"2":{"0":17,"4":3,"6":19,"31":1,"40":2,"53":2,"64":4,"69":2,"73":8,"85":1,"86":1,"87":2,"88":50,"96":1,"110":1,"116":10,"122":2,"127":3,"137":1,"197":1}}],["equivalent",{"2":{"3":1,"4":4,"6":6,"56":1,"64":1,"73":1,"76":1,"88":5,"94":1,"116":1}}],["enable",{"2":{"200":1}}],["enabled",{"2":{"197":1}}],["enabling",{"0":{"200":1}}],["enclosed",{"2":{"116":1}}],["encode",{"2":{"24":1}}],["encompasses",{"2":{"18":1,"116":1}}],["encounters",{"2":{"18":1}}],["en",{"2":{"116":1,"182":1}}],["envelope",{"2":{"73":2}}],["envelopes",{"2":{"73":2}}],["enough",{"2":{"64":1}}],["entirely",{"2":{"116":1}}],["entire",{"2":{"66":1,"116":4}}],["entry",{"2":{"64":11,"71":3,"73":4,"76":3,"146":1}}],["ent",{"2":{"64":19,"69":2}}],["enter",{"2":{"64":4}}],["ensuring",{"2":{"6":1,"174":1,"177":1}}],["ensure",{"2":{"6":3,"59":1,"70":1,"72":1,"75":1,"153":2,"161":2,"164":1}}],["ensures",{"2":{"6":3,"163":1,"166":3,"169":2}}],["enumerate",{"2":{"13":2,"53":1,"64":10,"66":1,"69":1,"71":1,"76":2,"116":1,"146":2,"169":2,"184":1}}],["enum",{"2":{"6":2,"64":3,"72":3,"116":3}}],["endpt",{"2":{"182":3}}],["endpoints",{"2":{"64":3,"66":1,"71":1,"73":6,"76":1,"85":2,"116":11,"122":2,"124":1}}],["endpoint=3",{"2":{"64":1}}],["endpointtype",{"2":{"64":2}}],["endpoint",{"2":{"3":1,"6":2,"64":27,"72":1,"73":16,"85":2,"93":1,"116":3,"122":2,"180":4,"182":4,"191":1}}],["ending",{"2":{"64":1,"66":1}}],["end",{"2":{"1":1,"9":1,"13":4,"14":3,"31":4,"32":4,"35":1,"36":1,"37":1,"38":1,"40":1,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1,"47":1,"48":1,"49":1,"50":1,"53":18,"56":8,"59":35,"60":3,"63":10,"64":130,"66":43,"69":9,"70":8,"71":12,"72":5,"73":31,"74":1,"75":6,"76":24,"82":3,"85":19,"88":20,"100":2,"101":2,"105":20,"114":2,"115":2,"116":104,"122":21,"127":1,"131":2,"132":2,"137":1,"141":2,"142":2,"145":9,"146":44,"147":4,"148":1,"150":1,"153":27,"154":17,"156":15,"158":4,"159":1,"160":4,"161":1,"163":5,"165":3,"169":18,"171":4,"172":1,"173":2,"176":8,"177":9,"180":7,"181":6,"182":30,"183":3,"184":27,"185":4,"186":4,"188":4,"189":19}}],["enforce",{"2":{"0":1,"6":2,"32":3,"188":3}}],["exits",{"2":{"64":1}}],["exit",{"2":{"64":32,"69":2,"71":3,"73":3,"76":2}}],["existingnodes",{"2":{"146":3}}],["existing",{"2":{"64":3,"75":1,"146":1,"192":2,"194":1}}],["exists",{"2":{"64":1,"66":1,"73":1}}],["exist",{"2":{"6":1,"64":1,"73":3,"188":1}}],["excluding",{"2":{"113":1,"116":1,"137":2}}],["exclude",{"2":{"105":9}}],["exclusively",{"2":{"129":1}}],["exclusive",{"2":{"66":1,"73":2}}],["exc",{"2":{"60":2,"173":2,"176":2}}],["excellent",{"2":{"23":1}}],["except",{"2":{"18":1,"19":1}}],["excess",{"2":{"5":1,"6":1,"59":1}}],["exp10",{"2":{"176":1,"180":2}}],["expressed",{"2":{"57":2}}],["express",{"2":{"57":1}}],["experimental",{"2":{"31":3}}],["expect",{"2":{"1":1,"118":1,"172":1}}],["explain",{"2":{"26":1}}],["explanations",{"2":{"26":3}}],["explicitly",{"2":{"18":1,"24":1,"53":2,"56":1,"59":1,"60":1,"85":2,"88":1,"173":1,"176":1}}],["expose",{"2":{"77":1}}],["exposes",{"2":{"17":1}}],["export",{"2":{"31":1,"57":1,"158":2,"174":1,"193":1,"195":1}}],["exponential",{"2":{"9":1}}],["ext2",{"2":{"116":3}}],["ext1",{"2":{"116":3}}],["ext",{"2":{"58":15,"64":10,"66":2,"69":3,"70":8,"72":8,"73":18,"75":8,"76":14,"105":7,"116":7}}],["extrema",{"2":{"146":1,"189":1}}],["extreem",{"2":{"53":10}}],["extracts",{"2":{"82":1}}],["extract",{"2":{"82":1,"146":1,"153":1,"154":2,"180":1}}],["extra",{"2":{"6":3,"64":1,"75":1,"174":1,"176":2,"177":1}}],["external",{"2":{"56":1,"116":2}}],["exteriors",{"2":{"9":1,"72":2,"75":2,"146":1}}],["exterior",{"2":{"3":4,"4":2,"6":10,"53":1,"55":1,"56":1,"58":1,"59":36,"63":2,"64":4,"70":2,"75":3,"76":14,"85":1,"88":2,"90":1,"91":1,"94":3,"104":1,"108":2,"116":9,"125":2,"134":1,"135":3,"138":3,"139":3,"145":6,"146":6,"163":3,"164":1,"166":1,"170":1,"191":2}}],["extending",{"2":{"122":1}}],["extended",{"2":{"85":1,"158":2}}],["extensions",{"2":{"77":1,"195":1}}],["extension",{"2":{"1":1,"6":1,"59":1,"60":1,"82":1,"93":1,"147":1,"172":3,"173":2,"176":1,"177":1,"178":1,"195":1}}],["extent`",{"2":{"155":1,"170":1}}],["extent=true",{"2":{"170":1}}],["extent=nothing",{"2":{"156":1}}],["extent=",{"2":{"153":4}}],["extent=false",{"2":{"153":1,"180":1}}],["extent=gi",{"2":{"146":3}}],["extents",{"2":{"4":3,"6":3,"31":5,"66":1,"73":3,"116":5,"122":1,"146":3,"153":2,"170":2,"189":2}}],["extent",{"0":{"170":1},"2":{"0":2,"1":4,"4":3,"6":9,"24":1,"31":3,"32":1,"35":2,"36":2,"37":2,"38":2,"50":1,"58":1,"65":1,"66":1,"73":10,"116":4,"122":2,"146":12,"150":2,"152":4,"153":29,"155":3,"156":1,"160":1,"170":2,"180":1,"189":7}}],["exactly",{"2":{"184":1}}],["exactpredicates",{"2":{"7":1,"13":2,"14":2,"31":1}}],["exact",{"2":{"3":5,"6":5,"7":2,"12":1,"13":1,"64":36,"66":6,"69":7,"70":5,"72":5,"73":7,"74":1,"75":5,"76":7,"91":2,"94":3,"96":1,"97":3,"98":3,"99":1,"104":2,"105":1,"108":2,"110":1,"111":3,"112":2,"113":1,"116":24,"119":2,"122":3,"125":2,"127":1,"128":3,"129":1,"130":1,"135":3,"137":1,"138":3,"139":3,"140":1,"146":1,"168":1}}],["examples",{"0":{"175":1,"179":1},"2":{"3":8,"4":1,"6":11,"26":2,"88":1,"91":1,"94":1,"104":1,"105":1,"108":1,"122":1,"125":1,"135":1,"145":2,"189":1}}],["example",{"0":{"58":1,"78":1,"162":1,"168":1,"198":1,"199":1},"1":{"79":1},"2":{"1":3,"3":2,"6":11,"11":1,"20":1,"23":2,"52":1,"55":1,"58":1,"62":1,"63":1,"65":1,"68":1,"69":1,"70":1,"72":1,"73":1,"75":1,"84":1,"87":1,"88":1,"90":1,"93":1,"103":1,"105":1,"107":1,"116":1,"118":1,"119":1,"121":1,"124":1,"134":1,"145":1,"146":8,"148":1,"150":2,"153":2,"162":1,"164":1,"167":1,"168":1,"171":1,"179":1,"180":2,"185":1,"188":1,"198":1,"200":1}}],["either",{"2":{"1":2,"3":1,"6":2,"22":1,"25":1,"27":1,"64":9,"70":1,"72":1,"73":2,"75":1,"76":1,"84":1,"94":1,"116":5,"118":1,"124":1,"125":1,"135":1,"146":1,"150":2,"153":3,"165":1,"197":1}}],["lj",{"2":{"146":2}}],["lrs",{"2":{"180":2}}],["lr",{"2":{"146":3,"153":3}}],["lp",{"2":{"116":2}}],["lstart",{"2":{"116":2}}],["ls",{"2":{"116":12}}],["ll",{"2":{"17":1,"58":1,"175":2,"180":1,"192":1,"194":1}}],["l",{"2":{"14":4,"59":6,"105":2,"116":32}}],["lgeos",{"2":{"180":1}}],["lg",{"2":{"13":1,"14":1,"15":2,"32":5,"33":1,"35":3,"36":3,"37":3,"38":3,"40":3,"41":3,"42":3,"43":3,"44":3,"45":3,"46":3,"47":3,"48":3,"49":3,"50":3,"81":4,"176":9,"180":17}}],["l305",{"2":{"6":1}}],["log10",{"2":{"176":2,"180":4}}],["log",{"2":{"175":2}}],["loudly",{"2":{"153":1}}],["location",{"2":{"64":1,"116":6}}],["locally",{"2":{"158":2}}],["local",{"2":{"53":3,"56":1,"64":4,"71":1,"76":1,"146":1}}],["loose",{"2":{"161":1}}],["lookup",{"2":{"146":1,"153":1}}],["looks",{"2":{"62":1}}],["look",{"2":{"55":2,"146":2,"162":1,"175":1}}],["looping",{"2":{"146":1}}],["loop",{"2":{"53":1,"59":2,"63":3,"64":8,"66":1,"73":1,"76":1,"116":5,"146":3,"182":1}}],["lower",{"2":{"6":1,"7":1,"146":1,"198":1}}],["lon",{"2":{"6":2,"175":1,"176":2}}],["longitude",{"2":{"158":3,"192":2}}],["long",{"2":{"6":1,"73":1,"176":1,"199":1}}],["longer",{"2":{"6":3,"174":1,"176":2,"177":1}}],["lots",{"2":{"192":1}}],["lot",{"2":{"6":1,"23":1,"53":1,"56":1,"63":1,"66":1,"85":1,"88":1,"122":1,"148":1,"175":1,"188":2}}],["load",{"2":{"153":1,"154":1,"180":1,"190":2}}],["loading",{"2":{"60":1,"173":1,"176":1}}],["loads",{"2":{"5":1,"6":1,"59":1}}],["loaded",{"2":{"1":1,"60":3,"172":1,"173":2,"176":2}}],["laptop",{"2":{"199":1}}],["land",{"2":{"192":8}}],["lazily",{"2":{"156":2}}],["layers",{"2":{"151":1}}],["label",{"2":{"14":4,"15":2,"79":2,"81":1,"84":1,"146":3,"175":2,"179":2,"180":2}}],["labels",{"2":{"13":2,"146":1}}],["latitude",{"2":{"158":3,"192":2}}],["later",{"2":{"56":1,"81":1,"146":1}}],["lat",{"2":{"6":3,"175":1,"176":3}}],["larger",{"2":{"64":1,"75":1,"184":1}}],["large",{"2":{"6":8,"59":1,"180":2,"195":1,"199":1}}],["lastindex",{"2":{"184":1}}],["last",{"2":{"4":3,"6":3,"9":1,"53":11,"56":2,"58":2,"64":9,"66":4,"85":7,"88":5,"116":20,"127":1,"146":6,"162":1,"169":2,"182":1}}],["lt",{"2":{"6":6,"73":4,"175":1}}],["len",{"2":{"182":7}}],["length",{"2":{"0":1,"5":1,"6":5,"9":1,"18":1,"59":32,"61":1,"62":1,"63":22,"64":11,"66":1,"69":2,"70":2,"72":1,"73":1,"75":1,"76":1,"116":1,"146":16,"153":2,"154":1,"156":2,"169":2,"175":1,"181":1,"182":1,"183":1,"184":7}}],["legend",{"2":{"180":4}}],["le",{"2":{"116":10}}],["leaving",{"2":{"162":1}}],["leaf",{"2":{"153":1,"156":3}}],["leading",{"2":{"191":1}}],["lead",{"2":{"73":1,"162":1}}],["least",{"2":{"3":4,"6":4,"64":1,"73":1,"76":2,"116":14,"122":6,"125":1,"128":3,"129":2,"130":2,"131":1,"132":1,"135":1,"141":1,"145":1}}],["leftjoin",{"2":{"197":1}}],["leftover",{"2":{"64":1}}],["left=1",{"2":{"64":1}}],["left",{"2":{"59":1,"64":5,"145":1,"146":2,"182":17,"184":5}}],["lets",{"2":{"56":1}}],["let",{"2":{"55":1,"62":1,"116":1,"146":3,"175":1,"191":6,"192":4,"193":3,"194":2,"195":2,"200":1}}],["levels",{"2":{"146":1,"151":1,"159":1}}],["level",{"2":{"6":1,"7":1,"9":1,"18":2,"20":1,"29":1,"151":1,"153":2,"161":1,"163":1,"165":4,"166":1,"167":1,"169":2,"193":2}}],["less",{"2":{"3":1,"6":7,"53":1,"56":1,"59":1,"63":1,"66":1,"85":1,"88":1,"105":1,"122":1,"146":1,"180":1,"181":1}}],["l289",{"2":{"6":1}}],["l2",{"2":{"3":4,"4":4,"6":8,"87":4,"88":12,"90":8,"104":2,"107":5,"121":4,"124":3,"125":2,"134":8}}],["l195",{"2":{"6":1}}],["l177",{"2":{"6":1}}],["l1",{"2":{"3":6,"4":4,"6":10,"87":4,"88":12,"90":8,"93":4,"94":2,"103":4,"104":2,"107":5,"121":4,"124":3,"125":2,"134":8}}],["li",{"2":{"146":2}}],["lie",{"2":{"73":1}}],["lies",{"2":{"64":3,"158":1,"198":1}}],["limitations",{"2":{"73":1}}],["limits",{"2":{"58":1}}],["limited",{"2":{"6":1,"174":1,"177":1}}],["library",{"2":{"69":1,"195":1}}],["libraries",{"2":{"56":1,"167":1,"195":1}}],["libgeos",{"2":{"6":2,"13":1,"14":1,"15":1,"23":1,"32":2,"56":1,"60":5,"81":1,"147":1,"176":7,"180":4,"188":2}}],["little",{"2":{"26":1}}],["literate",{"2":{"26":1,"31":1,"32":1,"50":1,"53":1,"56":1,"59":1,"60":1,"63":1,"64":1,"66":1,"69":1,"71":1,"73":1,"74":1,"76":1,"82":1,"85":1,"88":1,"91":1,"101":1,"104":1,"105":1,"115":1,"116":1,"119":1,"122":1,"132":1,"142":1,"145":1,"146":1,"147":1,"153":1,"154":1,"155":1,"156":1,"160":1,"163":1,"166":1,"169":1,"170":1,"171":1,"173":1,"177":1,"184":1,"185":1,"186":1,"188":1,"189":1}}],["lift",{"2":{"14":2}}],["lin",{"2":{"176":5}}],["linked",{"2":{"77":1}}],["linrange",{"2":{"13":2,"14":5,"58":2,"84":1,"146":2,"176":1,"180":2}}],["linering",{"2":{"192":1}}],["linewidth",{"2":{"55":1,"191":1}}],["linesegment",{"2":{"145":2,"177":1}}],["lines",{"0":{"97":1,"111":1,"128":1,"138":1},"2":{"3":1,"4":4,"6":5,"64":3,"68":1,"73":9,"79":1,"80":2,"81":2,"84":1,"87":4,"88":4,"90":3,"93":1,"94":1,"103":2,"105":1,"107":3,"116":2,"118":3,"121":4,"122":2,"124":4,"134":3,"135":1,"146":3,"178":1,"191":4,"196":2}}],["linestrings",{"2":{"4":2,"6":2,"9":1,"88":2,"144":1,"161":1,"191":1}}],["linestringtrait",{"2":{"3":2,"4":4,"6":7,"11":3,"32":1,"53":1,"63":4,"72":2,"85":2,"88":8,"96":1,"97":4,"98":1,"105":6,"110":1,"111":5,"122":4,"127":1,"128":4,"129":1,"137":1,"138":4,"139":1,"151":1,"159":2,"165":2,"166":1,"177":2}}],["linestring",{"2":{"3":5,"4":6,"6":15,"18":1,"20":1,"53":2,"63":2,"85":4,"87":2,"88":2,"90":2,"91":1,"96":1,"97":3,"104":2,"107":2,"108":1,"110":1,"111":4,"116":2,"121":2,"127":1,"128":3,"129":1,"134":2,"135":1,"137":1,"138":4,"139":1,"145":7,"153":2,"154":2,"189":3,"191":5,"196":1}}],["linetrait",{"2":{"3":2,"4":4,"6":6,"53":1,"64":2,"69":2,"72":2,"85":2,"88":8,"96":1,"97":4,"98":1,"110":1,"111":5,"122":4,"127":1,"128":4,"129":1,"137":1,"138":4,"139":1}}],["line2",{"2":{"3":3,"6":7,"72":2,"73":2,"105":5,"118":3,"119":2,"122":3,"145":5}}],["line1",{"2":{"3":4,"6":8,"72":2,"73":2,"105":5,"118":3,"119":2,"122":5,"145":5}}],["linea",{"2":{"1":1,"6":1,"185":1}}],["linearmap",{"2":{"180":1}}],["linearalgebra",{"2":{"31":1}}],["linear",{"2":{"4":7,"6":10,"9":1,"56":2,"59":1,"62":1,"63":3,"66":1,"73":1,"85":5,"88":8,"98":1,"116":2,"128":1,"129":2,"138":1,"144":1,"153":1,"154":1,"158":1,"163":3,"175":5,"176":3,"177":4}}],["linearr",{"2":{"1":1,"6":1,"185":1}}],["linearrings",{"2":{"9":1,"112":1,"146":6,"161":1,"191":1}}],["linearringtrait",{"2":{"4":4,"6":4,"11":3,"32":1,"53":3,"56":2,"63":4,"64":4,"72":2,"85":2,"88":8,"96":1,"97":1,"98":4,"110":1,"111":2,"112":3,"127":1,"128":1,"129":4,"137":1,"138":1,"139":4,"159":2,"165":1,"177":2,"180":1}}],["linearring",{"2":{"1":10,"3":2,"4":1,"6":13,"31":1,"53":2,"63":2,"64":2,"75":2,"76":1,"82":1,"85":1,"96":1,"98":3,"110":1,"111":1,"112":2,"116":2,"122":4,"127":1,"128":1,"129":3,"137":1,"139":4,"145":1,"146":3,"150":2,"153":5,"154":2,"161":1,"162":6,"163":1,"168":13,"180":1,"185":8,"191":24,"192":6,"193":8,"198":2}}],["linearsegments",{"2":{"0":1,"6":1,"174":1,"175":1,"176":3,"177":2}}],["line",{"0":{"116":1},"2":{"0":1,"3":13,"4":9,"6":52,"23":1,"52":2,"53":7,"62":3,"63":10,"64":12,"66":8,"68":6,"69":18,"72":8,"73":67,"76":6,"85":11,"88":5,"91":2,"93":2,"94":1,"96":2,"97":11,"98":8,"103":1,"105":11,"108":2,"110":1,"111":9,"112":4,"116":141,"118":2,"119":2,"121":3,"122":12,"124":2,"125":2,"127":1,"128":10,"129":3,"135":2,"137":2,"138":10,"139":7,"145":6,"146":2,"177":1,"180":1,"181":1,"182":4,"183":1,"189":3,"191":3}}],["lineorientation",{"2":{"0":1,"6":2,"72":2}}],["lists",{"2":{"20":1,"64":3,"122":1}}],["listed",{"2":{"6":1,"53":1,"180":1}}],["list",{"2":{"6":16,"9":1,"53":20,"64":233,"69":20,"70":14,"71":8,"72":13,"73":5,"75":13,"76":1,"199":2}}],["likely",{"2":{"151":1}}],["like",{"0":{"74":1},"2":{"1":2,"6":5,"17":1,"18":2,"19":1,"20":1,"23":1,"24":1,"29":1,"56":1,"58":1,"59":1,"62":1,"66":1,"70":1,"72":1,"75":1,"82":1,"84":1,"85":1,"88":1,"148":2,"150":2,"153":1,"154":2,"159":2,"165":2,"166":1,"174":1,"175":1,"192":2}}],["iah",{"2":{"196":2}}],["image",{"2":{"146":1}}],["impossible",{"2":{"69":1,"121":1}}],["important",{"2":{"175":1}}],["import",{"2":{"1":3,"6":4,"13":2,"14":2,"15":1,"31":9,"52":1,"55":1,"60":1,"62":1,"65":1,"69":1,"70":1,"72":1,"73":1,"75":1,"84":1,"87":1,"88":1,"90":1,"91":1,"93":1,"94":1,"103":1,"104":1,"105":1,"107":1,"108":1,"118":1,"119":1,"121":1,"122":1,"124":1,"125":1,"134":1,"135":1,"145":3,"150":1,"153":2,"154":1,"173":1,"176":2,"179":2,"180":3,"185":2,"189":1,"190":5}}],["implements",{"2":{"32":1,"188":1}}],["implementing",{"2":{"6":1,"180":1}}],["implement",{"2":{"6":1,"17":1,"23":1,"53":1,"56":1,"59":1,"63":1,"66":1,"82":1,"85":1,"88":1,"94":1,"108":1,"122":1,"125":1,"135":1,"148":1,"164":1,"165":3,"166":1}}],["implementation",{"0":{"53":1,"56":1,"63":1,"66":1,"69":1,"82":1,"85":1,"88":1,"91":1,"94":1,"104":1,"108":1,"119":1,"122":1,"125":1,"135":1,"163":1,"169":1,"177":1},"2":{"6":1,"53":3,"56":3,"59":1,"63":3,"66":3,"75":1,"85":3,"88":3,"91":1,"94":2,"104":1,"108":2,"119":1,"122":3,"125":2,"135":2,"146":2,"147":1,"153":1,"154":1,"172":1,"177":1,"180":1,"188":1}}],["implementations",{"2":{"6":1,"94":1,"108":1,"125":1,"135":1,"147":1,"178":1,"188":3}}],["implemented",{"0":{"147":1},"2":{"1":1,"6":2,"31":1,"56":1,"59":3,"69":2,"71":2,"73":2,"76":2,"82":1,"85":2,"147":1,"148":2,"154":1,"165":2,"172":1,"178":1}}],["improvements",{"2":{"9":2,"10":1}}],["improve",{"2":{"4":1,"6":1,"170":1}}],["i=2",{"2":{"145":1}}],["ipoints",{"2":{"116":4}}],["ip",{"2":{"88":2}}],["ipt",{"2":{"64":8}}],["ihole",{"2":{"88":2}}],["ih",{"2":{"76":22}}],["i2",{"2":{"66":2}}],["i1",{"2":{"66":2}}],["ii",{"2":{"64":8}}],["io",{"2":{"60":5,"173":5,"176":5}}],["i+1",{"2":{"59":8,"146":1,"182":2,"184":1}}],["i",{"2":{"9":1,"13":4,"22":1,"53":14,"56":1,"59":16,"64":28,"66":4,"71":2,"75":2,"76":4,"85":2,"88":8,"105":7,"116":28,"145":14,"146":17,"153":8,"154":8,"177":2,"181":3,"182":12,"184":28,"189":2,"191":2,"192":1,"199":3}}],["id",{"2":{"194":1}}],["identical",{"2":{"153":1,"168":1}}],["identity",{"2":{"146":4,"156":2,"170":1}}],["ideal",{"2":{"146":1}}],["idea",{"2":{"6":1,"20":1,"25":1,"27":1,"59":1,"148":1,"154":1}}],["idx`",{"2":{"64":1}}],["idx",{"2":{"53":6,"64":167,"66":9,"69":23,"70":5,"72":5,"75":2,"169":34,"182":51}}],["id=",{"2":{"6":2}}],["ignored",{"2":{"6":1,"146":1}}],["ignore",{"2":{"6":1,"146":2}}],["innerjoin",{"2":{"197":1,"198":1,"199":1}}],["inner",{"2":{"116":6,"153":5}}],["in=1",{"2":{"116":1}}],["in`",{"2":{"116":1}}],["inject",{"0":{"74":1},"2":{"173":1}}],["inaccuracies",{"2":{"73":1}}],["ind",{"2":{"199":2}}],["individual",{"2":{"193":1}}],["indices",{"2":{"64":4,"153":2,"154":2,"184":16}}],["indicates",{"2":{"20":1}}],["indeed",{"2":{"162":1}}],["index",{"2":{"59":8,"64":11,"116":1,"153":1,"182":1}}],["inds",{"2":{"146":3}}],["inplace",{"2":{"59":1}}],["inputs",{"2":{"6":1,"64":1,"88":1,"91":1,"104":1,"119":1,"184":1}}],["input",{"2":{"6":11,"53":1,"69":1,"70":1,"72":1,"73":1,"75":1,"146":1,"148":1,"154":1,"176":3,"177":1,"181":1,"182":1,"183":1,"192":1}}],["inbounds",{"2":{"59":16,"85":5,"116":1}}],["inspiration",{"2":{"69":1}}],["inspired",{"2":{"68":1,"69":2}}],["inside",{"2":{"53":3,"62":1,"64":3,"66":1,"70":1,"71":1,"72":1,"73":1,"75":1,"76":2,"84":3,"85":1,"96":1,"116":1,"134":1,"137":1,"146":2}}],["insertion",{"2":{"64":1}}],["insert",{"2":{"53":1,"182":1}}],["instability",{"2":{"24":2}}],["instantiating",{"2":{"20":1}}],["instead",{"2":{"18":1,"24":1,"154":1,"177":1}}],["instructs",{"2":{"6":1,"188":2}}],["inline",{"2":{"32":1,"116":4,"145":1,"153":12,"154":10,"160":2}}],["init=nothing",{"2":{"154":1}}],["init=typemax",{"2":{"85":2}}],["init=zero",{"2":{"56":1,"66":1}}],["initial",{"2":{"73":2,"176":1}}],["initially",{"2":{"64":1}}],["initialize",{"2":{"59":3,"63":2,"64":1,"73":1}}],["init",{"2":{"19":1,"31":1,"53":1,"63":2,"154":30,"176":4}}],["incorrect",{"2":{"161":1,"162":1}}],["increase",{"2":{"64":1,"116":1}}],["increasing",{"2":{"6":1,"180":1}}],["increment",{"2":{"59":5}}],["including",{"2":{"53":1,"64":2,"73":1,"85":1,"96":1,"97":1,"98":1,"99":1,"116":2}}],["include",{"2":{"31":41,"53":1,"64":1,"77":1,"85":2,"160":1,"180":2,"193":2,"194":1}}],["included",{"2":{"6":2,"76":2,"166":2,"169":2,"193":1}}],["includes",{"2":{"4":2,"6":2,"76":1,"85":2,"88":1,"116":1}}],["incircle",{"0":{"16":1}}],["inv",{"2":{"158":3}}],["investigate",{"2":{"146":1}}],["investigating",{"0":{"81":1}}],["inverted",{"2":{"6":1,"82":1}}],["invalid",{"2":{"20":1,"71":2,"162":1,"167":1,"168":1}}],["invoke",{"2":{"20":1}}],["involved",{"2":{"148":1}}],["involve",{"2":{"20":1}}],["involving",{"2":{"6":3,"70":1,"72":1,"75":1}}],["invocation",{"2":{"18":1}}],["ing",{"2":{"1":1,"6":2,"59":1,"185":1}}],["intr",{"2":{"64":28,"66":9,"69":16,"73":8}}],["intr2",{"2":{"64":2,"73":14}}],["intr1",{"2":{"64":3,"73":21,"116":2}}],["intrs",{"2":{"64":10,"73":5}}],["introduction",{"0":{"27":1},"1":{"28":1,"29":1,"30":1}}],["introducing",{"2":{"24":1}}],["introduces",{"2":{"24":1}}],["int",{"2":{"64":6,"76":7,"105":7,"177":1,"180":1,"182":5,"183":1,"184":1}}],["integrate",{"2":{"56":1}}],["integrating",{"2":{"56":1}}],["integrals",{"2":{"55":1}}],["integral",{"2":{"55":1}}],["intended",{"2":{"6":1,"165":2,"166":1}}],["intermediate",{"2":{"66":1}}],["inter2",{"2":{"66":15}}],["inter1",{"2":{"66":23}}],["interpreted",{"2":{"59":1}}],["interpolation",{"2":{"5":1,"6":1,"58":1,"59":2,"174":1}}],["interpolated",{"2":{"5":3,"6":3,"59":17,"174":1}}],["interpolate",{"2":{"0":2,"5":2,"6":4,"57":1,"58":2,"59":25}}],["interest",{"2":{"59":1,"85":1}}],["internal",{"2":{"58":1}}],["internals",{"2":{"24":1}}],["inter",{"2":{"6":3,"64":21,"69":1,"70":1,"72":5,"73":4}}],["interface",{"0":{"165":1},"2":{"6":3,"20":1,"77":1,"82":1,"153":1,"164":1,"165":4,"166":1,"180":1,"192":1}}],["interacted",{"2":{"116":1}}],["interaction",{"0":{"116":1},"2":{"116":2}}],["interactions",{"2":{"64":1,"76":3,"116":15}}],["interactive",{"2":{"13":1,"14":1}}],["interacting",{"2":{"6":1,"72":1,"116":2}}],["interacts",{"2":{"3":1,"6":1,"116":3,"124":1,"125":1,"128":3,"129":1,"130":1}}],["interact",{"2":{"3":2,"6":2,"75":1,"76":1,"107":1,"116":5,"119":1,"124":1,"125":1,"127":1,"128":2,"129":2,"130":1}}],["interior",{"2":{"3":6,"6":7,"53":9,"55":1,"59":7,"63":5,"64":1,"76":18,"90":1,"91":1,"93":1,"94":3,"97":5,"98":1,"99":2,"103":2,"104":1,"105":1,"108":2,"110":2,"111":6,"112":3,"116":15,"124":1,"125":3,"128":1,"129":2,"130":2,"134":1,"135":3,"140":2,"145":2,"163":1,"166":1}}],["interiors",{"2":{"3":6,"6":7,"59":20,"90":1,"91":1,"93":1,"103":1,"104":1,"107":1,"108":1,"113":1,"116":5,"118":1,"119":1,"124":1,"125":1,"127":1,"128":2,"129":1,"134":1,"135":1,"138":3,"139":3}}],["intersectingpolygons",{"2":{"6":3}}],["intersecting",{"0":{"167":1},"1":{"168":1,"169":1},"2":{"6":4,"31":1,"64":2,"76":2,"166":4,"167":1,"169":4}}],["intersections",{"0":{"73":1},"2":{"64":2,"66":2,"71":1,"72":2,"73":1,"76":1,"116":2}}],["intersection",{"0":{"37":1,"72":1,"117":1},"1":{"118":1,"119":1},"2":{"0":2,"3":3,"6":19,"9":1,"15":4,"23":1,"31":1,"37":2,"64":38,"66":6,"69":3,"70":4,"71":4,"72":15,"73":73,"75":2,"76":2,"105":2,"116":6,"118":1,"122":5}}],["intersect",{"2":{"3":8,"6":11,"64":4,"66":3,"70":2,"72":1,"73":1,"75":1,"76":5,"90":1,"91":2,"93":1,"94":1,"104":1,"108":1,"111":1,"113":1,"116":4,"118":2,"122":1,"134":1,"135":2,"138":3,"139":3,"168":1,"169":4}}],["intersects",{"0":{"49":1,"118":1},"2":{"0":2,"3":3,"6":6,"31":1,"49":2,"64":1,"73":2,"76":2,"105":2,"117":1,"118":4,"119":4,"122":5,"140":1,"169":2,"197":1}}],["into",{"2":{"5":1,"6":5,"17":1,"26":1,"53":1,"56":1,"59":1,"63":4,"64":2,"69":1,"70":1,"72":1,"116":1,"146":4,"153":3,"154":2,"155":1,"156":2,"159":1,"169":1,"180":1,"189":2,"191":2,"197":1}}],["int64",{"2":{"1":6,"6":6,"162":6,"181":1,"182":1,"184":1,"185":6,"191":14}}],["influence",{"2":{"158":1}}],["infinity",{"2":{"116":1,"122":1}}],["info",{"2":{"6":2,"174":1}}],["information",{"0":{"193":1},"2":{"6":1,"29":1,"59":1,"64":2,"65":1,"73":1,"82":1,"116":1,"190":1,"193":3,"195":2}}],["inf",{"2":{"1":1,"9":1,"69":2,"181":1,"184":3}}],["in",{"0":{"23":1},"2":{"1":6,"3":3,"4":1,"5":4,"6":45,"7":1,"9":2,"13":6,"14":2,"17":2,"18":4,"20":1,"23":3,"24":2,"25":1,"26":3,"27":1,"29":1,"30":1,"31":2,"32":1,"33":1,"53":7,"55":1,"56":3,"57":2,"59":22,"60":1,"62":2,"63":5,"64":72,"66":31,"69":6,"70":9,"71":3,"72":5,"73":29,"75":11,"76":28,"81":1,"82":3,"84":6,"85":6,"87":1,"88":17,"90":2,"91":1,"94":10,"97":1,"98":1,"99":1,"100":1,"101":1,"104":1,"105":7,"108":9,"110":1,"111":1,"112":1,"114":1,"115":1,"116":135,"118":2,"119":1,"121":1,"122":11,"124":1,"125":11,"129":1,"131":1,"132":1,"134":1,"135":10,"138":3,"139":3,"141":1,"142":1,"145":4,"146":10,"147":3,"148":1,"150":2,"151":2,"152":1,"153":8,"154":4,"156":10,"157":1,"158":5,"159":1,"160":1,"164":1,"165":1,"166":2,"167":1,"169":7,"170":1,"171":2,"172":3,"173":1,"174":2,"175":2,"176":14,"177":8,"178":1,"180":5,"181":2,"182":4,"183":1,"184":5,"185":2,"186":1,"187":4,"188":4,"189":8,"190":1,"191":2,"192":6,"195":2,"197":4,"198":3,"199":1,"200":1}}],["itererable",{"2":{"189":1}}],["iter",{"2":{"156":31}}],["iterator",{"2":{"64":4,"72":2,"156":4}}],["iterators",{"2":{"13":1,"59":1,"64":5,"66":3,"72":1,"76":2,"146":1,"153":2,"154":3,"156":8,"169":6,"177":1}}],["iterate",{"2":{"59":2,"151":1,"153":1,"156":4}}],["iteration",{"2":{"56":1}}],["iterabletype",{"2":{"153":5,"154":6}}],["iterable",{"2":{"4":1,"6":2,"18":1,"56":1,"66":1,"82":1,"153":17,"154":15,"156":11,"191":1}}],["iterables",{"2":{"1":2,"22":1,"150":2,"153":1,"154":2,"156":3}}],["ith",{"2":{"64":3,"76":7}}],["itself",{"2":{"66":1,"145":1,"151":1}}],["its",{"2":{"5":1,"6":9,"18":1,"55":1,"59":1,"64":2,"66":4,"76":1,"97":1,"156":2,"164":1,"180":1,"181":1,"182":1,"183":3,"197":1}}],["it",{"2":{"1":4,"4":1,"6":14,"9":1,"18":11,"19":2,"20":1,"22":1,"29":3,"30":1,"52":1,"53":1,"55":2,"56":3,"58":1,"60":1,"63":2,"64":2,"65":1,"66":2,"70":1,"71":1,"72":1,"73":4,"75":1,"76":3,"81":2,"82":2,"85":3,"88":1,"93":2,"96":3,"110":3,"116":14,"121":1,"122":1,"127":2,"137":3,"144":1,"145":2,"146":4,"148":3,"150":1,"151":3,"152":1,"153":8,"154":5,"156":8,"158":1,"159":2,"160":2,"162":1,"163":1,"164":1,"166":1,"167":1,"172":5,"175":2,"176":1,"177":1,"180":1,"182":1,"187":1,"188":7,"192":3,"193":3,"195":3,"199":2,"200":2}}],["iff",{"2":{"153":1}}],["if",{"0":{"74":1},"2":{"1":5,"3":17,"4":19,"5":1,"6":68,"18":1,"22":2,"32":1,"52":1,"53":15,"55":1,"56":6,"59":5,"60":3,"63":1,"64":95,"66":36,"69":8,"70":13,"71":12,"72":9,"73":44,"75":10,"76":22,"82":3,"84":2,"85":8,"87":2,"88":35,"90":1,"91":1,"93":1,"94":2,"96":4,"97":3,"98":3,"99":2,"100":1,"101":1,"103":1,"104":1,"105":9,"107":1,"108":3,"110":5,"111":4,"112":2,"113":2,"114":1,"115":1,"116":132,"118":1,"119":1,"121":4,"122":16,"124":1,"125":2,"127":4,"128":3,"129":2,"130":2,"131":1,"132":1,"134":1,"135":2,"137":5,"138":3,"139":3,"140":1,"141":1,"142":1,"145":5,"146":35,"150":3,"151":1,"153":19,"154":6,"156":9,"160":1,"163":1,"165":1,"166":2,"167":1,"169":12,"170":1,"171":1,"172":3,"173":2,"176":6,"177":1,"180":1,"182":12,"184":12,"185":1,"186":1,"188":2,"192":1,"195":1,"198":2,"199":1}}],["isolate",{"2":{"180":1}}],["isodd",{"2":{"69":1}}],["ismeasured",{"2":{"156":2}}],["istable",{"2":{"153":1,"154":1,"156":1}}],["isequal",{"2":{"146":1}}],["iseven",{"2":{"116":1}}],["isempty",{"2":{"56":1,"59":4,"66":1,"70":1,"71":2,"72":1,"146":1,"165":1,"182":1}}],["isparallel",{"2":{"116":1,"145":8}}],["is3d",{"2":{"88":1,"156":2,"171":1,"185":1,"186":1,"189":10}}],["issue",{"2":{"64":1}}],["issues",{"2":{"25":1,"27":1}}],["isa",{"2":{"18":1,"59":9,"153":3,"154":1,"177":2,"189":1}}],["isnothing",{"2":{"60":1,"64":9,"69":1,"71":2,"73":2,"76":2,"88":1,"146":1,"173":1,"176":1,"180":1,"181":1,"182":5,"183":1,"184":6}}],["isn",{"2":{"6":1,"56":1,"64":4,"66":1,"69":1,"71":1,"73":3,"76":2,"85":3,"88":1,"93":1,"116":9,"122":1,"180":1,"188":1}}],["isconcave",{"0":{"145":1},"2":{"0":1,"6":2,"81":1,"143":1,"145":3}}],["isclockwise",{"0":{"144":1},"2":{"0":1,"6":2,"66":1,"143":1,"145":5,"146":2}}],["is",{"0":{"25":1,"52":1,"55":2,"62":1,"65":1,"68":1,"84":2,"87":1,"90":1,"93":1,"103":1,"107":1,"118":1,"121":1,"124":1,"134":1,"151":1},"2":{"0":1,"1":10,"3":8,"4":43,"5":1,"6":103,"9":3,"11":1,"13":1,"14":1,"18":3,"19":1,"20":1,"22":3,"23":2,"25":2,"27":2,"52":1,"53":19,"55":7,"56":19,"57":2,"58":2,"59":15,"60":3,"62":2,"63":5,"64":82,"65":3,"66":22,"68":1,"69":2,"70":4,"71":13,"72":4,"73":40,"75":7,"76":20,"77":1,"81":5,"82":4,"84":8,"85":23,"88":13,"90":2,"91":3,"93":4,"94":4,"96":7,"97":3,"98":3,"99":1,"100":1,"101":1,"104":3,"105":3,"107":1,"108":5,"110":9,"111":4,"112":2,"113":1,"114":1,"115":1,"116":91,"119":2,"121":3,"122":12,"125":4,"127":2,"134":4,"135":5,"137":8,"138":3,"139":3,"140":1,"141":2,"142":1,"144":2,"145":7,"146":11,"147":1,"148":3,"150":3,"151":3,"152":1,"153":18,"154":4,"156":7,"158":11,"159":1,"160":1,"161":7,"162":2,"163":1,"164":3,"165":2,"166":1,"167":4,"168":1,"169":2,"171":1,"172":7,"173":2,"174":3,"175":3,"176":9,"177":8,"179":1,"180":4,"181":1,"182":2,"183":1,"185":2,"187":1,"188":3,"191":4,"192":5,"193":3,"194":1,"195":3,"197":2,"198":5,"199":2}}],["cpu",{"2":{"199":1}}],["cp",{"2":{"196":1}}],["circumstances",{"2":{"158":2}}],["cy",{"2":{"145":2}}],["cyan",{"2":{"60":1,"173":1,"176":1}}],["cx",{"2":{"145":2}}],["cdot",{"2":{"145":1}}],["cs",{"2":{"116":9}}],["cshape",{"2":{"62":3}}],["cw",{"2":{"66":2}}],["cb",{"2":{"58":1,"81":1,"146":1}}],["cgrad",{"2":{"58":1}}],["cgal",{"2":{"58":1}}],["c",{"2":{"32":1,"56":1,"62":1,"66":4,"69":5,"74":3,"116":24,"176":1}}],["cleaner",{"2":{"82":1}}],["cleanest",{"2":{"82":1}}],["clear",{"2":{"65":1}}],["clearly",{"2":{"52":1,"55":1,"84":1}}],["clamped",{"2":{"73":18}}],["clamp",{"2":{"53":1,"73":1}}],["classified",{"2":{"64":1}}],["classify",{"2":{"64":4}}],["class",{"2":{"6":1,"146":1}}],["class=",{"2":{"6":6}}],["clipping",{"0":{"64":1,"70":1,"71":1,"73":1,"75":1,"76":1},"2":{"6":1,"9":1,"31":8,"64":9,"69":3,"70":1,"72":1,"75":1,"167":1}}],["closure",{"2":{"153":2}}],["closing",{"2":{"53":1}}],["closer",{"2":{"158":1}}],["close",{"2":{"53":5,"64":2,"66":1,"69":1,"73":1,"85":9,"146":1,"162":1,"163":4}}],["closest",{"2":{"4":3,"6":3,"73":2,"84":1,"85":6,"158":1}}],["closed2",{"2":{"88":2}}],["closed1",{"2":{"88":2}}],["closed",{"0":{"161":1},"1":{"162":1,"163":1},"2":{"4":4,"6":11,"9":1,"31":1,"53":6,"56":3,"63":3,"64":1,"66":1,"73":1,"76":3,"88":21,"96":2,"97":5,"98":5,"110":2,"111":5,"112":3,"116":52,"128":5,"129":1,"137":3,"138":5,"139":5,"145":1,"146":1,"161":1,"162":1,"163":2,"164":1,"166":1,"191":1}}],["closedring",{"2":{"0":1,"6":1,"161":1,"162":1,"163":4,"164":1,"165":1,"166":1}}],["clockwise",{"2":{"4":1,"6":4,"9":1,"55":1,"56":1,"66":7,"81":1,"144":2,"145":3,"146":1}}],["ceil",{"2":{"177":1}}],["ce",{"2":{"116":11}}],["certainly",{"2":{"73":1}}],["certain",{"2":{"17":1,"18":1,"26":1,"29":1}}],["central",{"2":{"64":1}}],["centroids",{"2":{"62":1,"63":1}}],["centroid",{"0":{"61":1,"62":1},"1":{"62":1,"63":1},"2":{"0":4,"4":2,"6":6,"31":1,"61":3,"62":4,"63":38,"148":1,"154":1,"180":4}}],["cent",{"2":{"62":2}}],["centered",{"2":{"64":1}}],["center",{"2":{"6":1,"62":1,"146":1}}],["cells",{"2":{"146":1}}],["cell",{"2":{"6":4,"65":3,"66":28,"146":2}}],["children",{"2":{"180":1}}],["child",{"2":{"156":19}}],["chunks",{"2":{"153":4,"154":5}}],["chunk",{"2":{"153":6,"154":7}}],["chull",{"2":{"50":2}}],["chose",{"2":{"24":1,"146":1}}],["choose",{"2":{"6":1,"82":1,"146":2}}],["changes",{"2":{"64":1}}],["changed",{"2":{"64":1}}],["change",{"2":{"24":1,"64":1,"81":1}}],["chain=2",{"2":{"64":1}}],["chain=1",{"2":{"64":1}}],["chain",{"2":{"64":66,"71":3,"73":4,"76":4,"81":2}}],["chains",{"2":{"20":1,"64":1}}],["chairmarks",{"2":{"13":1,"176":1,"180":1}}],["checkargs",{"2":{"181":1,"182":1,"183":1,"184":1}}],["checking",{"2":{"88":1,"182":1}}],["checks",{"0":{"105":1,"117":1},"1":{"118":1,"119":1},"2":{"60":1,"64":1,"73":1,"87":1,"90":1,"93":1,"103":1,"107":1,"116":5,"118":1,"121":1,"122":1,"124":1,"134":1,"198":1}}],["check",{"2":{"4":1,"6":1,"7":1,"63":1,"64":8,"66":3,"69":1,"70":1,"73":4,"74":1,"75":1,"88":8,"116":11,"146":5,"153":1,"168":1,"170":1,"175":2,"182":1,"184":1}}],["checked",{"2":{"4":1,"6":1,"64":1,"73":1,"81":1,"116":1,"170":1}}],["c2",{"2":{"6":3,"15":2,"85":3,"88":14,"146":21}}],["c1",{"2":{"6":3,"15":2,"85":4,"88":13,"146":26}}],["ctor",{"2":{"1":1,"6":1,"185":1}}],["categorize",{"2":{"69":1}}],["categorical",{"2":{"58":1}}],["came",{"2":{"69":1}}],["case",{"2":{"53":1,"57":2,"63":1,"64":1,"70":1,"73":2,"76":1,"116":16,"154":1,"177":1,"200":1}}],["cases",{"2":{"6":1,"59":1,"69":2,"116":3,"151":1,"153":1,"154":1,"156":3}}],["cause",{"2":{"18":1,"23":1,"182":1}}],["careful",{"2":{"59":1,"151":1}}],["care",{"2":{"17":1,"153":1,"158":1}}],["carried",{"2":{"6":1,"188":1}}],["cairomakie",{"2":{"13":1,"52":1,"55":1,"58":2,"62":1,"65":1,"68":1,"79":1,"80":1,"81":1,"84":1,"87":1,"90":1,"93":1,"103":1,"107":1,"118":1,"121":1,"124":1,"134":1,"175":1,"176":1,"180":1,"190":1,"196":1,"198":1,"199":1}}],["california",{"2":{"199":1}}],["callable",{"2":{"146":4,"165":1}}],["calls",{"2":{"64":1,"73":1,"76":1,"85":1,"94":1,"108":1,"125":1,"135":1,"151":1,"153":1,"177":1}}],["calling",{"2":{"33":1,"64":2,"153":1}}],["call",{"2":{"18":1,"24":1,"56":1,"63":1,"153":4,"178":1}}],["called",{"2":{"6":2,"63":3,"64":1,"153":1,"163":1,"165":1,"166":1,"188":1,"192":1}}],["calculation",{"2":{"73":1}}],["calculations",{"2":{"6":1,"25":2,"27":2,"73":1,"158":2,"176":1}}],["calculating",{"2":{"4":1,"6":1,"18":1,"64":1,"170":1}}],["calculated",{"2":{"6":1,"62":2,"66":1,"73":2,"85":1,"153":4}}],["calculates",{"2":{"4":2,"6":6,"56":1,"59":2,"66":1,"73":2,"75":1,"85":2,"176":1,"183":1}}],["calculate",{"2":{"1":2,"5":1,"6":5,"11":1,"53":3,"59":3,"63":1,"66":1,"73":1,"116":1,"146":1,"150":1,"153":3,"155":1}}],["calc",{"2":{"1":2,"6":3,"24":1,"31":1,"32":1,"35":2,"36":2,"37":2,"38":2,"50":1,"53":4,"150":1,"152":1,"153":19,"155":1,"160":1,"170":1,"180":2}}],["cache",{"2":{"6":1,"59":1}}],["cant",{"2":{"88":1,"146":1,"189":1}}],["cannot",{"2":{"3":3,"4":2,"6":5,"56":1,"88":1,"96":1,"99":1,"122":2,"125":1,"127":2,"129":1,"140":1}}],["can",{"2":{"1":1,"4":2,"6":14,"7":2,"9":1,"13":1,"14":1,"24":1,"26":1,"31":1,"53":1,"56":1,"57":2,"58":1,"59":3,"60":1,"63":1,"64":3,"66":1,"70":3,"72":3,"73":1,"75":3,"76":1,"81":1,"82":2,"87":1,"88":3,"90":1,"93":1,"105":1,"107":1,"116":23,"118":2,"121":2,"122":3,"124":1,"128":1,"129":1,"134":1,"137":1,"138":1,"139":1,"146":1,"147":1,"148":1,"150":1,"152":1,"153":8,"155":1,"156":2,"158":1,"162":2,"163":1,"166":1,"167":2,"168":1,"170":1,"173":1,"175":1,"176":2,"180":2,"188":1,"191":6,"192":1,"193":3,"195":3,"197":3,"198":3}}],["creation",{"2":{"191":1,"193":1}}],["creating",{"0":{"190":1,"191":1,"194":1},"1":{"191":1,"192":1,"193":1,"194":1,"195":1},"2":{"116":1}}],["creates",{"2":{"7":1,"64":3}}],["create",{"0":{"193":1},"2":{"6":2,"13":2,"14":1,"64":1,"69":1,"73":2,"82":1,"146":2,"153":1,"166":2,"167":3,"169":2,"190":2,"191":4,"192":5,"193":3,"194":1,"195":1,"198":1}}],["created",{"2":{"4":2,"6":2,"64":1,"85":3}}],["criteria",{"2":{"94":2,"108":2,"125":2,"135":2,"182":3}}],["cropping",{"2":{"58":2}}],["cross=1",{"2":{"72":1}}],["cross`",{"2":{"72":1}}],["crossings",{"2":{"64":3,"116":1}}],["crossing",{"0":{"105":1},"2":{"6":2,"64":81,"70":1,"71":7,"72":3,"73":7,"75":1,"76":6,"94":1,"105":1,"116":1,"135":1}}],["cross",{"0":{"132":1},"2":{"6":1,"9":1,"53":3,"64":13,"69":14,"70":2,"71":1,"72":2,"73":10,"74":4,"75":2,"76":1,"94":1,"105":2,"108":1,"116":17,"122":2,"125":1,"135":1,"145":3}}],["crosses",{"0":{"43":1},"2":{"0":2,"3":2,"6":3,"31":1,"43":2,"64":1,"69":1,"105":26,"116":2,"197":1}}],["crc",{"2":{"6":1,"59":1}}],["crs2",{"2":{"192":2}}],["crs1",{"2":{"192":2,"193":1}}],["crs=nothing",{"2":{"153":1,"170":1,"180":1}}],["crs=gi",{"2":{"153":5,"156":3}}],["crs`",{"2":{"153":1,"172":6}}],["crs",{"0":{"192":1},"2":{"1":16,"4":2,"6":8,"31":1,"32":2,"35":2,"36":2,"37":2,"38":2,"50":2,"82":1,"146":10,"150":2,"152":3,"153":36,"155":2,"156":4,"170":1,"172":7,"180":1,"190":1,"192":13,"193":7,"195":1}}],["customize",{"2":{"153":1,"154":1}}],["custom",{"0":{"200":1},"2":{"6":3,"59":1,"200":2}}],["curr^2",{"2":{"53":2}}],["curr",{"2":{"53":8,"64":116,"69":9,"76":3,"169":26}}],["currentnode",{"2":{"146":8}}],["current",{"2":{"53":1,"59":8,"64":3,"69":2,"71":1,"73":1,"76":6,"146":2,"169":3}}],["currently",{"2":{"5":1,"6":3,"23":1,"59":1,"64":1,"69":1,"71":2,"146":1,"153":1,"158":2}}],["curve",{"0":{"116":1},"2":{"3":1,"4":7,"6":18,"53":6,"56":7,"64":5,"66":3,"72":7,"84":1,"85":22,"88":6,"94":2,"96":4,"97":9,"98":9,"108":1,"110":4,"111":6,"112":3,"116":152,"122":1,"125":1,"128":6,"130":3,"135":1,"137":4,"138":6,"139":6}}],["curves",{"2":{"0":1,"3":1,"6":5,"56":3,"66":3,"73":1,"84":1,"85":1,"88":13,"99":1,"116":1,"122":1,"140":1,"180":2}}],["cutpolygon",{"2":{"68":1}}],["cuts",{"2":{"68":1}}],["cutting",{"0":{"67":1},"1":{"68":1,"69":1},"2":{"64":1,"69":2}}],["cut",{"0":{"68":1},"2":{"0":1,"6":6,"31":1,"59":1,"67":1,"68":5,"69":22,"116":1}}],["coastlines",{"2":{"196":1}}],["coarse",{"2":{"6":1,"174":1,"177":1}}],["cos",{"2":{"191":3,"192":1,"193":2}}],["copy",{"2":{"153":1,"165":1,"184":1}}],["coors1",{"2":{"145":2}}],["coors2",{"2":{"145":3}}],["coord",{"2":{"177":6}}],["coords",{"2":{"69":9,"177":8}}],["coordinatetransformations",{"2":{"1":2,"6":2,"180":1,"185":2,"190":1,"191":5,"193":2}}],["coordinate",{"0":{"59":1,"171":1,"192":1,"193":1},"2":{"1":4,"5":5,"6":7,"25":1,"27":1,"58":2,"59":10,"172":4,"177":1,"190":3,"192":1}}],["coordinates",{"0":{"5":1,"57":1},"1":{"58":1,"59":1},"2":{"0":4,"1":1,"4":2,"5":7,"6":20,"56":2,"57":8,"59":25,"64":2,"65":1,"69":2,"70":1,"72":1,"75":1,"85":1,"88":2,"148":1,"158":2,"171":2,"172":1,"175":1,"176":3,"192":2}}],["co",{"2":{"116":2}}],["core",{"2":{"157":1}}],["corner",{"2":{"66":4,"146":1}}],["corners",{"2":{"66":1}}],["correspondent",{"2":{"145":1}}],["correspond",{"2":{"64":1}}],["corresponding",{"2":{"53":3,"71":2}}],["corrected",{"2":{"165":1}}],["correctness",{"2":{"161":1,"188":1}}],["correctly",{"2":{"153":1,"175":1}}],["correcting",{"2":{"20":1,"165":1}}],["corrections",{"0":{"164":1,"166":1},"1":{"165":1,"166":1},"2":{"162":1,"165":6,"168":1}}],["correction",{"2":{"6":10,"31":4,"70":1,"71":2,"72":1,"73":2,"75":1,"76":2,"161":1,"163":2,"164":2,"165":8,"166":7,"167":1,"169":2}}],["correct",{"2":{"6":3,"24":1,"53":1,"56":1,"63":1,"64":1,"66":1,"70":1,"72":1,"75":1,"85":1,"88":1,"94":1,"108":1,"122":1,"125":1,"135":1,"161":1,"162":2,"164":1}}],["cov",{"2":{"66":16}}],["cover",{"2":{"96":1,"100":1}}],["covering",{"2":{"6":2,"103":1,"166":2,"169":2}}],["covered",{"0":{"98":1,"99":1,"139":1},"2":{"3":1,"6":1,"71":1,"76":1,"93":1,"94":1,"98":2,"99":2,"100":1,"101":2,"103":1,"115":2,"116":3,"169":1}}],["coveredby",{"0":{"48":1,"92":1,"93":1,"96":1,"97":1,"100":1,"101":1,"115":1},"1":{"93":1,"94":1},"2":{"0":2,"3":4,"6":4,"31":1,"48":2,"64":1,"92":1,"93":3,"94":10,"95":6,"96":13,"97":15,"98":13,"99":5,"100":2,"101":2,"104":3,"197":1}}],["covers",{"0":{"47":1,"102":1,"103":1},"1":{"103":1,"104":1},"2":{"0":2,"3":5,"6":5,"31":1,"47":2,"102":1,"103":4,"104":5,"146":1,"197":1}}],["coverages",{"2":{"6":1,"66":1}}],["coverage",{"0":{"65":1},"2":{"0":1,"6":2,"31":1,"65":3,"66":16}}],["code",{"2":{"7":1,"10":1,"26":5,"59":1,"94":1,"108":1,"116":1,"125":1,"135":1,"145":1,"160":1,"187":1,"199":1}}],["colatitude",{"2":{"158":1}}],["colname",{"2":{"153":3}}],["col",{"2":{"153":2,"154":1}}],["columns",{"2":{"153":2}}],["column",{"2":{"22":1,"153":9,"154":8,"156":2,"194":2,"197":8}}],["colored",{"2":{"198":1}}],["color=",{"2":{"192":2}}],["colors",{"2":{"79":1,"80":1,"196":1,"198":2}}],["colorrange",{"2":{"58":2,"84":1}}],["colorbar",{"2":{"58":1,"81":1,"84":1,"146":1}}],["colormap",{"2":{"14":1,"58":3,"84":1}}],["color",{"2":{"6":1,"55":1,"58":2,"59":1,"60":1,"62":1,"68":3,"79":1,"80":1,"81":2,"84":2,"87":4,"90":4,"93":1,"103":1,"107":4,"121":4,"134":4,"173":1,"176":1,"191":1,"192":1,"196":1,"198":5}}],["collect",{"2":{"11":3,"13":1,"50":1,"52":1,"55":2,"59":1,"62":1,"65":2,"68":2,"82":2,"84":1,"146":3,"153":2,"154":1,"175":4,"189":2}}],["collections",{"0":{"100":1,"101":1,"114":1,"115":1,"131":1,"132":1,"141":1,"142":1},"2":{"1":2,"6":8,"22":1,"150":2,"152":1,"153":2,"154":2,"156":3,"180":2,"195":1}}],["collection",{"2":{"1":1,"4":7,"6":12,"18":2,"29":2,"53":2,"56":3,"66":2,"85":2,"100":2,"101":2,"114":2,"115":2,"131":2,"132":2,"141":2,"142":2,"148":1,"150":1,"153":3,"154":1,"180":1,"186":1,"189":1}}],["collinear",{"2":{"3":1,"6":2,"64":5,"70":2,"72":3,"73":12,"75":2,"121":1,"122":2}}],["come",{"2":{"158":1}}],["commonly",{"2":{"195":1}}],["common",{"2":{"87":1,"155":1,"177":1,"190":1,"192":1,"193":1}}],["commented",{"2":{"145":1}}],["comments",{"2":{"116":1}}],["comment",{"2":{"30":1}}],["combos",{"2":{"71":1,"73":1,"76":1}}],["combination",{"2":{"64":1,"156":2}}],["combines",{"2":{"63":1}}],["combine",{"2":{"63":2,"64":5,"169":1}}],["combined",{"2":{"6":1,"64":4,"76":1,"166":1,"169":2,"191":1}}],["coming",{"2":{"66":1}}],["com",{"2":{"6":2,"73":1,"82":1,"158":1}}],["compilation",{"2":{"160":1}}],["compiled",{"2":{"24":1}}],["compiler",{"2":{"24":1,"153":2,"160":2}}],["complex",{"2":{"148":1,"180":1,"197":1}}],["complexity",{"2":{"148":1}}],["complete",{"2":{"56":1}}],["completely",{"2":{"1":1,"3":4,"6":4,"64":2,"71":1,"76":1,"90":2,"91":1,"94":1,"103":1,"104":1,"116":1,"135":1,"150":1,"153":1,"158":1}}],["components",{"2":{"62":1,"63":2,"154":1,"156":25}}],["component",{"2":{"56":3,"63":11,"66":8,"73":1,"153":2,"156":2}}],["composed",{"2":{"4":4,"6":5,"88":5,"191":2}}],["comprised",{"2":{"6":3,"70":1,"72":1,"75":1}}],["computing",{"2":{"60":1,"77":1}}],["computational",{"2":{"6":1,"59":1}}],["computation",{"2":{"6":6,"59":1,"63":1,"70":1,"72":1,"75":1,"181":2,"182":2,"183":2}}],["computer",{"2":{"6":1,"59":1}}],["computes",{"2":{"6":1,"82":1}}],["compute",{"2":{"4":1,"6":3,"56":1,"59":1,"82":3,"177":1}}],["computed",{"2":{"4":4,"6":5,"53":1,"56":3,"59":3,"66":1,"196":1}}],["compact",{"2":{"199":3}}],["comparisons",{"2":{"197":1}}],["comparing",{"2":{"76":1,"88":1}}],["compares",{"2":{"146":1}}],["compared",{"2":{"88":1}}],["compare",{"2":{"3":1,"4":1,"6":2,"53":1,"76":1,"88":2,"122":1}}],["compatibility",{"2":{"56":1}}],["compatible",{"2":{"1":3,"22":1,"25":1,"27":1,"53":1,"56":1,"59":2,"63":1,"66":1,"85":1,"88":1,"91":1,"94":1,"104":1,"108":1,"119":1,"122":1,"125":1,"135":1,"150":1,"151":1,"153":1,"172":2}}],["couple",{"2":{"194":1}}],["course",{"2":{"152":1}}],["country",{"2":{"199":8}}],["countries",{"2":{"11":1,"80":1,"180":1}}],["counted",{"2":{"73":2}}],["counters",{"2":{"59":8}}],["counterparts",{"2":{"33":1}}],["counter",{"2":{"6":1,"64":7,"116":1,"144":1,"145":1}}],["counterclockwise",{"2":{"4":1,"6":2,"9":1,"55":2,"56":1,"81":1,"82":1}}],["count",{"2":{"64":16,"146":1,"184":1}}],["couldn",{"2":{"9":1,"160":1}}],["could",{"2":{"4":1,"6":1,"56":3,"73":4,"76":1,"77":1,"85":1,"116":2,"158":1,"172":1}}],["conditions",{"2":{"197":4}}],["connected",{"2":{"116":5}}],["connect",{"2":{"66":11}}],["connecting",{"2":{"53":1,"182":1,"191":2}}],["connections",{"2":{"6":2,"166":2,"169":2}}],["contents",{"2":{"153":1,"161":1}}],["context",{"2":{"32":4,"176":4,"192":2}}],["contours",{"2":{"146":1}}],["contour",{"2":{"146":4}}],["continue",{"2":{"56":1,"64":7,"66":1,"71":2,"73":1,"116":1,"153":1,"165":1,"169":3,"184":1,"199":1}}],["contributions",{"2":{"25":1,"27":1}}],["controlled",{"2":{"24":1}}],["control",{"2":{"23":1}}],["containing",{"2":{"65":1,"198":1}}],["contain",{"2":{"3":1,"6":1,"26":1,"76":1,"90":1,"122":1,"191":1}}],["contained",{"2":{"3":1,"6":1,"9":1,"76":4,"91":1,"103":1,"121":1,"122":1,"153":1,"198":1}}],["contains",{"0":{"45":1,"89":1,"90":1},"1":{"90":1,"91":1},"2":{"0":2,"3":4,"6":4,"26":1,"31":1,"33":1,"45":2,"64":1,"76":1,"77":1,"89":1,"90":5,"91":4,"151":1,"168":1,"197":1,"198":1}}],["consistentm",{"2":{"156":3}}],["consistentz",{"2":{"156":3}}],["consistent",{"2":{"64":1,"148":1,"156":1}}],["consistency",{"2":{"22":1}}],["considered",{"2":{"53":1,"59":1}}],["consider",{"2":{"52":1,"55":1,"62":1,"65":1,"66":3,"68":1,"84":2,"87":1,"90":1,"93":1,"103":1,"107":1,"116":3,"118":1,"121":1,"124":1,"134":1}}],["constprop",{"2":{"105":1}}],["constants",{"2":{"64":1}}],["const",{"2":{"31":4,"64":1,"94":4,"108":3,"125":4,"135":4,"155":3,"180":4}}],["constructing",{"2":{"193":1}}],["constructors",{"2":{"159":2}}],["construct",{"2":{"159":1}}],["constructed",{"2":{"1":1,"20":1,"172":1}}],["constrained",{"2":{"6":3,"70":1,"72":1,"75":1}}],["concepts",{"0":{"28":1},"1":{"29":1,"30":1},"2":{"26":1}}],["concieve",{"2":{"9":1}}],["concavehull",{"2":{"147":1}}],["concave",{"2":{"6":1,"53":2,"62":2,"145":2}}],["convention",{"2":{"62":1}}],["convenience",{"2":{"59":1,"188":1}}],["conversely",{"2":{"64":1}}],["conversion",{"0":{"186":1},"2":{"22":1,"33":1}}],["converted",{"2":{"22":1,"59":3}}],["converts",{"2":{"6":1,"189":1}}],["convert",{"0":{"95":1,"109":1,"126":1,"136":1},"2":{"6":3,"32":1,"35":2,"36":2,"37":2,"38":2,"40":2,"41":2,"42":2,"43":2,"44":2,"45":2,"46":2,"47":2,"48":2,"49":2,"50":1,"59":6,"82":1,"84":1,"146":2,"176":1,"180":3,"186":1,"188":1,"189":1}}],["convexity",{"2":{"81":2}}],["convexhull",{"2":{"50":1,"147":1}}],["convex",{"0":{"50":1,"77":1,"80":1},"1":{"78":1,"79":1,"80":1,"81":1,"82":1},"2":{"0":1,"6":7,"31":2,"50":1,"53":3,"77":4,"79":2,"80":1,"81":4,"82":11,"145":1}}],["vw",{"2":{"180":3}}],["von",{"2":{"146":1}}],["v2",{"2":{"116":9}}],["v1",{"2":{"116":9}}],["v`",{"2":{"59":2}}],["vcat",{"2":{"53":1,"59":1,"153":1}}],["vararg",{"2":{"59":1}}],["varying",{"2":{"158":1}}],["vary",{"2":{"53":1}}],["variables",{"2":{"24":1,"59":8,"91":1,"104":1,"119":1}}],["variable",{"2":{"24":1,"71":2}}],["vals",{"2":{"182":9}}],["valign",{"2":{"180":1}}],["validated",{"2":{"71":2,"73":2,"76":2}}],["validate",{"2":{"9":1}}],["valid",{"2":{"1":1,"6":8,"63":1,"70":2,"72":2,"73":3,"75":2,"146":1,"161":3,"162":1,"167":1,"168":1,"184":1,"185":1}}],["val",{"2":{"53":2,"64":2,"73":8,"116":30,"122":4}}],["values=",{"2":{"146":2}}],["values=sort",{"2":{"146":1}}],["values",{"2":{"1":1,"5":3,"6":15,"53":2,"58":3,"59":40,"63":2,"64":5,"66":5,"73":1,"85":2,"116":4,"146":19,"150":1,"153":2,"160":1,"182":2,"184":1}}],["value",{"2":{"0":1,"4":7,"5":2,"6":15,"14":1,"24":1,"32":2,"53":1,"55":2,"56":4,"59":45,"64":5,"66":2,"73":7,"84":1,"85":3,"146":6,"153":2,"182":11,"188":2,"197":1}}],["vs",{"0":{"15":1},"2":{"12":1,"73":2}}],["vᵢ",{"2":{"6":1}}],["v",{"2":{"5":2,"6":6,"14":4,"59":23,"85":8}}],["visvalingam",{"2":{"178":1}}],["visvalingamwhyatt",{"0":{"183":1},"2":{"0":1,"6":3,"180":4,"183":5}}],["visualized",{"2":{"162":1}}],["visualize",{"2":{"118":1,"192":1}}],["visa",{"2":{"64":1}}],["visited",{"2":{"64":4}}],["view",{"2":{"64":2,"82":1,"146":1,"169":1,"182":3,"199":1}}],["viewport",{"2":{"14":1}}],["views",{"2":{"1":1,"64":1,"75":1,"172":1}}],["vincenty",{"2":{"6":1,"177":1}}],["via",{"2":{"6":1,"60":1,"77":1,"147":1,"173":1,"176":1,"188":2}}],["vec",{"2":{"82":2}}],["vect",{"2":{"156":2}}],["vectypes",{"2":{"59":5}}],["vector",{"2":{"1":12,"4":6,"5":1,"6":46,"18":1,"23":1,"29":1,"52":1,"53":10,"59":14,"64":7,"69":6,"70":4,"71":1,"72":2,"73":3,"75":4,"116":1,"122":2,"145":3,"146":4,"148":1,"150":1,"153":4,"154":3,"162":8,"163":1,"168":22,"174":1,"175":1,"177":2,"181":1,"182":4,"183":1,"184":2,"185":10,"189":15,"191":18,"192":6,"193":9}}],["vectors",{"2":{"1":1,"4":2,"6":3,"22":1,"53":4,"59":3,"64":1,"82":1,"85":1,"144":1,"146":1,"150":1,"153":1,"156":2,"180":1}}],["ve",{"2":{"17":1,"177":1}}],["vein",{"2":{"7":1}}],["version",{"2":{"188":1}}],["versa",{"2":{"64":1}}],["vert",{"2":{"184":21}}],["verts",{"2":{"180":2}}],["vertical",{"2":{"58":1,"66":1,"73":1,"146":1}}],["vertices",{"2":{"6":7,"9":1,"57":4,"59":5,"64":1,"69":2,"82":1,"96":1,"98":5,"107":1,"110":3,"113":1,"116":1,"137":1,"162":1,"174":2,"175":1,"176":3,"177":3,"180":1,"183":1}}],["vertex",{"2":{"5":1,"6":2,"53":1,"57":2,"59":2,"64":19,"73":12,"81":1,"96":2,"111":2,"112":1,"116":1,"137":3}}],["very",{"2":{"0":1,"175":1,"199":2}}],["rd",{"2":{"180":3}}],["rdbu",{"2":{"84":1}}],["rhumb",{"2":{"145":2}}],["runner",{"2":{"192":1}}],["running",{"2":{"153":1,"156":2}}],["run",{"2":{"153":5,"154":3,"199":1}}],["runs",{"2":{"73":2,"144":1}}],["rule",{"2":{"64":1}}],["rules",{"2":{"64":1}}],["rightjoin",{"2":{"197":1}}],["right=2",{"2":{"64":1}}],["right",{"2":{"59":1,"64":4,"69":1,"85":1,"145":1,"182":19,"184":5,"191":1}}],["ring4",{"2":{"193":2}}],["ring3",{"2":{"192":1}}],["ring2",{"2":{"191":2}}],["ring1",{"2":{"191":2}}],["rings",{"0":{"98":1,"112":1,"129":1,"139":1,"161":1},"1":{"162":1,"163":1},"2":{"4":4,"6":6,"9":2,"56":1,"63":1,"64":3,"73":1,"88":7,"144":1,"146":5,"163":1,"166":1,"180":1}}],["ring",{"2":{"4":7,"6":13,"9":1,"31":1,"53":3,"56":2,"59":1,"62":1,"63":4,"64":12,"66":13,"76":3,"85":5,"88":4,"96":1,"97":2,"98":5,"110":1,"112":1,"116":2,"127":1,"128":1,"129":3,"137":1,"138":2,"139":4,"145":3,"146":11,"161":2,"162":1,"163":14,"164":1,"191":1}}],["rtrees",{"2":{"20":1}}],["r+y",{"2":{"13":2,"14":1}}],["r+x",{"2":{"13":2,"14":1}}],["ry",{"2":{"13":3,"14":3}}],["rx",{"2":{"13":3,"14":3}}],["round",{"2":{"180":1,"182":1,"184":1}}],["routines",{"2":{"11":1}}],["row",{"2":{"154":3}}],["rows",{"2":{"154":2}}],["robust",{"0":{"15":1},"2":{"81":1}}],["rotate",{"2":{"66":1}}],["rotation",{"2":{"1":1,"6":1,"185":1}}],["rotations",{"2":{"1":3,"6":3,"185":3}}],["rotmatrix2d",{"2":{"180":1}}],["rotmatrix",{"2":{"1":1,"6":1,"185":1}}],["r",{"2":{"6":1,"9":1,"13":11,"14":12,"32":1,"176":1,"184":2,"191":6,"192":2,"193":4}}],["rᵢ₋₁",{"2":{"59":20}}],["rᵢ∗rᵢ₊₁+sᵢ⋅sᵢ₊₁",{"2":{"6":1}}],["rᵢ₊₁",{"2":{"6":1,"59":29}}],["rᵢ",{"2":{"6":2,"59":49}}],["ramer",{"2":{"182":1}}],["raster",{"0":{"146":1},"2":{"146":4}}],["ray",{"2":{"116":4}}],["raw",{"2":{"18":1}}],["range",{"2":{"13":8,"14":4,"146":3,"153":2,"154":2}}],["ranges",{"2":{"6":1,"146":2}}],["randomly",{"2":{"198":2}}],["random",{"2":{"180":2}}],["randn",{"2":{"79":1}}],["rand",{"2":{"6":1,"81":1,"146":1,"198":2}}],["rather",{"2":{"6":1,"146":1,"168":1}}],["ratio",{"2":{"6":7,"73":1,"176":1,"180":1,"181":4,"182":6,"183":4,"184":11}}],["radii",{"2":{"6":1,"176":1}}],["radius`",{"2":{"176":1}}],["radius",{"2":{"6":6,"59":13,"158":5,"176":4,"177":1}}],["radialdistance",{"0":{"181":1},"2":{"0":1,"6":2,"178":1,"180":4,"181":4}}],["rrayscore",{"2":{"1":1,"6":1,"185":1}}],["rring",{"2":{"1":1,"6":1,"185":1}}],["rewrap",{"2":{"153":2,"156":1}}],["req",{"2":{"116":44}}],["requirement",{"2":{"161":1,"167":1}}],["requirements",{"2":{"116":5}}],["required",{"2":{"82":1,"94":3,"108":3,"125":3,"135":3,"175":1,"190":1}}],["requires",{"2":{"60":1,"88":1,"90":1,"94":2,"97":3,"98":3,"99":1,"108":1,"111":3,"112":2,"113":1,"125":1,"128":3,"129":1,"130":1,"134":1,"135":1,"138":3,"139":3,"140":1,"173":1,"176":1,"188":1}}],["require",{"2":{"33":1,"82":1,"93":2,"94":6,"108":3,"116":32,"125":3,"135":3}}],["requests",{"2":{"25":1,"27":1}}],["reflected",{"2":{"180":3}}],["ref",{"2":{"84":1}}],["refers",{"2":{"158":1}}],["referring",{"2":{"116":1}}],["refer",{"2":{"6":1,"146":1}}],["references",{"2":{"6":1,"59":1}}],["reference",{"0":{"192":1,"193":1},"2":{"0":1,"1":2,"172":2,"190":2,"192":1}}],["reveal",{"2":{"76":1}}],["reveals",{"2":{"76":1}}],["reverse",{"2":{"55":1,"58":1,"59":1,"64":2,"191":2}}],["rev",{"2":{"75":1}}],["render",{"2":{"58":1}}],["rendering",{"2":{"58":3,"59":1}}],["rename",{"2":{"10":1}}],["regardless",{"2":{"73":1,"116":1}}],["regions",{"2":{"71":3,"73":5,"75":1,"76":3,"116":2,"199":1}}],["region",{"2":{"60":2,"73":3,"76":2,"199":1}}],["register",{"2":{"31":3,"59":3}}],["regular",{"0":{"15":1}}],["rebuilding",{"2":{"153":1,"154":1}}],["rebuild",{"2":{"31":2,"151":1,"153":3,"156":11,"177":1,"180":2}}],["rebuilt",{"2":{"1":1,"148":1,"150":1,"153":1,"156":2}}],["readable",{"2":{"195":1}}],["readability",{"2":{"64":1}}],["read",{"2":{"153":1,"192":2}}],["reading",{"2":{"153":1}}],["reads",{"2":{"153":1}}],["reached",{"2":{"153":1,"156":3}}],["reaches",{"2":{"151":1}}],["reach",{"2":{"151":1}}],["reasons",{"2":{"188":1}}],["reason",{"2":{"24":1,"160":1,"161":1,"167":1}}],["real`",{"2":{"176":1,"177":1}}],["reality",{"2":{"81":1}}],["really",{"2":{"56":1,"116":1,"146":1,"160":1}}],["real=1",{"2":{"6":2,"176":2}}],["real=6378137`",{"2":{"176":1}}],["real=6378137",{"2":{"6":2,"176":1}}],["real",{"0":{"199":1},"2":{"5":1,"6":13,"53":1,"59":45,"63":2,"73":1,"176":3,"177":3,"184":5,"197":1}}],["relation",{"2":{"64":2}}],["relations",{"2":{"31":10,"105":1,"122":1}}],["relationship",{"2":{"23":1,"197":2}}],["relative",{"2":{"59":3}}],["relevant",{"2":{"6":1,"10":1,"82":1,"158":2}}],["reducing",{"2":{"154":2}}],["reduced",{"2":{"181":1,"182":1,"183":1}}],["reduces",{"2":{"19":1,"76":1,"154":1}}],["reduce",{"2":{"1":1,"146":1,"150":1,"154":2,"165":1}}],["redundant",{"2":{"64":1}}],["red",{"2":{"14":1,"62":2,"84":1,"93":1,"103":1,"118":1,"191":1,"192":1,"198":2}}],["removal",{"2":{"64":1}}],["removes",{"2":{"64":1,"148":1}}],["removed",{"2":{"64":3,"71":5,"146":1}}],["remove",{"2":{"56":1,"64":33,"66":1,"69":2,"70":5,"72":5,"75":2,"169":2,"181":1,"182":3,"184":4}}],["removing",{"2":{"6":3,"64":1,"71":1,"181":1,"182":1,"183":1}}],["remainingnode",{"2":{"146":3}}],["remaining",{"2":{"64":1,"88":1,"116":2,"182":1}}],["remain",{"2":{"1":1,"6":8,"150":1,"153":1,"180":2}}],["resolution",{"2":{"192":1}}],["resolved",{"2":{"146":1}}],["resembles",{"2":{"158":2}}],["reset",{"2":{"64":1,"153":1}}],["resize",{"2":{"13":1,"14":1,"64":2}}],["resampled",{"2":{"6":1,"177":1}}],["respectively",{"2":{"64":1,"122":1,"191":1}}],["respect",{"2":{"6":2,"72":1,"73":1,"82":1,"116":6}}],["rest",{"2":{"6":1,"59":2,"75":1}}],["resulting",{"2":{"69":1,"76":1,"146":1,"198":1}}],["results",{"2":{"3":2,"6":2,"73":1,"105":1,"122":1,"153":1,"154":1,"162":1,"182":25,"191":3}}],["result",{"2":{"1":2,"3":5,"4":5,"6":11,"19":1,"32":3,"53":1,"56":2,"66":1,"73":15,"82":1,"85":2,"91":1,"94":1,"104":1,"119":1,"135":1,"150":2,"153":9,"154":2,"176":3,"184":6}}],["receives",{"2":{"153":1,"154":1}}],["recent",{"2":{"64":1,"71":1,"73":1,"76":1}}],["recalculate",{"2":{"152":1}}],["recursive",{"2":{"151":1}}],["recursively",{"2":{"4":1,"6":1,"151":1,"170":1}}],["rect",{"2":{"52":3,"55":5,"65":3,"84":7}}],["rectangle",{"2":{"52":2,"55":2,"58":2,"65":2,"66":1,"84":2,"175":5,"176":8,"198":2}}],["rectangletrait",{"2":{"32":1}}],["recommended",{"2":{"22":1}}],["reconstructing",{"2":{"180":1}}],["reconstructed",{"2":{"18":1}}],["reconstruct",{"2":{"1":1,"18":1,"31":2,"150":1,"153":2,"154":1,"156":28}}],["replace",{"2":{"64":1,"146":2,"182":1}}],["replaced",{"2":{"22":1}}],["repl",{"2":{"60":1,"173":1,"176":1}}],["repeat",{"2":{"63":1,"64":3,"88":6}}],["repeating",{"2":{"56":1,"76":1}}],["repeated",{"2":{"4":3,"6":3,"9":1,"53":2,"59":1,"64":2,"76":2,"85":2,"88":4,"116":1}}],["represented",{"2":{"158":1,"198":1}}],["represent",{"2":{"17":1,"59":1,"64":4,"88":1,"158":1}}],["representing",{"2":{"6":2,"71":1,"73":1,"76":1,"82":1,"84":1,"88":1,"158":1,"199":1}}],["represents",{"2":{"6":1,"165":2,"166":1}}],["reprojects",{"2":{"172":1}}],["reprojection",{"0":{"172":1},"1":{"173":1}}],["reproject",{"2":{"0":1,"1":4,"31":2,"148":1,"172":6,"173":2}}],["re",{"2":{"1":1,"6":1,"17":1,"82":1,"185":1,"190":1,"192":1}}],["retrievable",{"2":{"1":1,"172":1}}],["returnval",{"2":{"116":9}}],["returntype",{"2":{"19":1}}],["returning",{"2":{"18":1,"60":1,"71":2}}],["return",{"0":{"22":1},"2":{"1":1,"3":18,"4":2,"6":37,"13":3,"14":3,"23":2,"30":1,"32":3,"35":1,"36":1,"37":1,"38":1,"40":1,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1,"47":1,"48":1,"49":1,"50":1,"53":5,"56":5,"59":20,"60":1,"63":4,"64":36,"66":12,"69":12,"70":5,"71":4,"72":4,"73":18,"75":6,"76":9,"82":2,"85":8,"88":30,"91":1,"94":1,"100":2,"101":2,"104":1,"105":18,"108":2,"114":2,"115":2,"116":71,"118":1,"119":1,"122":28,"125":1,"127":2,"131":2,"132":2,"135":1,"141":2,"142":2,"145":10,"146":12,"153":17,"154":3,"156":6,"161":1,"163":4,"165":6,"166":2,"169":2,"171":2,"172":1,"177":4,"180":3,"181":1,"182":5,"183":3,"184":7,"185":2,"186":2,"188":3,"189":8}}],["returned",{"2":{"1":1,"6":10,"22":2,"23":1,"64":3,"69":1,"70":2,"72":2,"73":2,"75":2,"81":1,"82":1,"146":1,"153":1,"156":2,"160":1,"172":1,"180":1}}],["returns",{"2":{"1":1,"3":5,"4":4,"5":3,"6":23,"18":1,"22":2,"53":1,"56":2,"59":6,"63":3,"64":4,"66":6,"69":1,"70":1,"73":1,"75":1,"82":1,"85":8,"87":1,"90":2,"91":1,"94":1,"103":2,"104":1,"107":1,"116":4,"119":1,"122":2,"135":1,"146":2,"150":1,"153":2,"160":1,"177":1,"186":1,"197":1}}],["phi``",{"2":{"158":1}}],["physics",{"2":{"158":1}}],["psa",{"2":{"153":1,"154":1}}],["pb",{"2":{"105":2}}],["p0",{"2":{"85":9}}],["p3",{"2":{"64":8,"183":4}}],["ptm",{"2":{"145":3}}],["ptj",{"2":{"145":5}}],["pti",{"2":{"145":3}}],["ptrait",{"2":{"85":2}}],["pts",{"2":{"64":22,"69":7}}],["pt",{"2":{"64":114,"69":2,"73":26,"116":8,"182":4}}],["pt2",{"2":{"64":14,"73":2}}],["pt1",{"2":{"64":18,"73":2}}],["pn",{"2":{"127":3}}],["pn2",{"2":{"64":4}}],["pn1",{"2":{"64":4}}],["pfirst",{"2":{"56":3}}],["pu",{"2":{"198":2}}],["purpose",{"2":{"153":1}}],["pure",{"2":{"6":1,"82":1,"146":1}}],["purely",{"2":{"6":1,"18":1,"158":1,"176":1}}],["push",{"2":{"64":15,"69":5,"70":2,"72":2,"73":2,"75":4,"76":5,"146":3,"163":1,"177":3,"182":3}}],["pulling",{"2":{"82":1}}],["pull",{"2":{"25":1,"27":1}}],["public",{"2":{"24":1}}],["pick",{"2":{"192":2}}],["piece",{"2":{"64":6,"169":6}}],["pieces",{"2":{"64":12,"69":1,"71":2,"75":4,"116":1,"169":9}}],["pi",{"2":{"13":2}}],["pixels",{"2":{"146":1}}],["pixel",{"2":{"6":2,"146":7}}],["pythagorean",{"2":{"85":1}}],["py",{"2":{"13":2,"14":2}}],["px",{"2":{"13":2,"14":2}}],["peucker",{"2":{"178":2,"180":3,"182":2}}],["peaks",{"2":{"146":2}}],["peculiarities",{"0":{"21":1},"1":{"22":1,"23":1,"24":1}}],["people",{"2":{"9":1}}],["persist",{"2":{"153":1}}],["performed",{"2":{"158":1,"198":1}}],["performs",{"2":{"59":1,"154":1,"177":1}}],["perform",{"2":{"26":1,"58":1,"59":2,"64":1,"148":1,"154":1,"197":3,"198":2,"200":1}}],["performing",{"2":{"6":3,"23":1,"59":1,"70":1,"72":1,"75":1,"198":1}}],["performance",{"2":{"4":1,"6":2,"22":1,"146":1,"167":1,"170":1,"180":1,"195":1}}],["per",{"2":{"5":2,"6":2,"58":1,"59":2,"64":5,"146":1,"153":2,"154":2,"177":1}}],["pl",{"2":{"198":2}}],["plt",{"2":{"191":1}}],["please",{"2":{"64":1}}],["place",{"2":{"73":1,"197":1}}],["placement",{"2":{"64":1}}],["plan",{"2":{"174":1}}],["plane",{"2":{"6":1,"59":1,"158":3,"177":1,"196":1}}],["planar",{"2":{"6":4,"31":2,"158":5,"177":9}}],["plottable",{"2":{"146":1}}],["plotted",{"2":{"62":1}}],["plotting",{"0":{"191":1},"2":{"6":1,"146":1,"174":1,"177":1,"190":1,"191":4,"192":1}}],["plots",{"2":{"58":2}}],["plot",{"0":{"192":1},"2":{"13":1,"58":3,"79":2,"80":1,"81":1,"87":1,"121":1,"146":1,"176":2,"180":4,"190":2,"191":9,"192":5,"193":1,"195":1,"198":1}}],["plus",{"2":{"5":1,"6":1,"59":1}}],["p2y",{"2":{"189":3}}],["p2x",{"2":{"189":3}}],["p2box",{"2":{"58":1}}],["p2",{"2":{"3":2,"4":2,"6":6,"15":12,"53":12,"56":9,"64":18,"66":19,"75":2,"81":1,"85":15,"88":11,"94":2,"103":2,"116":12,"122":2,"145":3,"175":1,"183":4,"189":3,"191":2}}],["p1y",{"2":{"189":3}}],["p1x",{"2":{"189":3}}],["p1",{"2":{"3":3,"4":2,"6":7,"15":12,"53":21,"56":8,"58":4,"64":9,"66":25,"68":1,"75":2,"81":2,"85":15,"88":14,"93":5,"94":3,"103":5,"116":4,"122":2,"127":3,"145":3,"183":4,"189":3,"191":1}}],["practice",{"2":{"193":1}}],["pred",{"2":{"197":5,"198":1,"199":2,"200":1}}],["predicate",{"2":{"7":1,"105":1,"197":6,"198":1,"200":5}}],["predicates",{"0":{"12":1,"15":1,"200":1},"1":{"13":1,"14":1,"15":1,"16":1},"2":{"7":4,"12":1,"31":1,"64":5,"73":4,"74":3,"116":1,"197":1,"199":1}}],["pretty",{"2":{"172":1}}],["prettytime",{"2":{"13":2}}],["prevent",{"2":{"71":2,"73":2,"76":2}}],["prev^2",{"2":{"53":2}}],["prev",{"2":{"53":14,"64":69,"145":4}}],["previously",{"2":{"175":1}}],["previous",{"2":{"19":1,"53":1,"153":1,"181":3}}],["preparations",{"2":{"20":1}}],["prepared",{"2":{"20":1,"146":2}}],["prepare",{"0":{"20":1},"2":{"17":1,"20":1}}],["precision",{"2":{"11":1}}],["preserve",{"2":{"180":4,"182":3}}],["preserved",{"2":{"153":2}}],["preserving",{"2":{"178":1}}],["presentation",{"2":{"6":1,"59":1}}],["present",{"2":{"6":1,"153":1,"188":1}}],["presence",{"2":{"6":1,"32":1,"188":1}}],["prescribes",{"2":{"20":1}}],["press",{"2":{"6":1,"59":1}}],["pre",{"2":{"6":1,"75":1,"180":1,"182":2}}],["prefilter",{"2":{"6":1,"180":7}}],["protters",{"2":{"153":1,"154":1}}],["progressively",{"2":{"151":1}}],["program",{"2":{"17":1}}],["programming",{"2":{"17":1,"26":1}}],["promote",{"2":{"59":5}}],["property",{"2":{"154":2}}],["properties=gi",{"2":{"156":1}}],["properties=namedtuple",{"2":{"154":1}}],["properties=",{"2":{"146":1}}],["properties",{"2":{"6":1,"153":8,"156":1,"180":1,"184":1}}],["propagated",{"2":{"153":1}}],["propagate",{"2":{"59":16,"85":4}}],["probably",{"2":{"56":1,"153":1}}],["prod",{"2":{"53":4}}],["product",{"2":{"53":1}}],["process",{"2":{"96":3,"97":3,"98":3,"99":1,"105":1,"110":3,"111":3,"112":2,"113":1,"116":11,"127":1,"128":3,"129":1,"130":1,"137":3,"138":3,"139":3,"140":1,"153":1,"154":1,"182":1}}],["processed",{"2":{"64":6}}],["processors",{"2":{"31":1,"94":2,"108":2,"125":2,"135":2}}],["processor",{"2":{"31":1}}],["processing",{"2":{"23":1}}],["profile",{"2":{"9":1}}],["providers",{"2":{"162":1,"168":1}}],["provide",{"0":{"23":1},"2":{"6":6,"52":1,"55":1,"57":1,"62":1,"65":1,"68":1,"70":2,"72":2,"75":2,"84":1,"87":1,"90":1,"93":1,"103":1,"107":1,"116":2,"118":1,"121":1,"124":1,"134":1,"146":1,"184":1}}],["provides",{"2":{"6":1,"77":1,"82":1,"192":1}}],["provided",{"2":{"4":1,"6":3,"11":1,"64":1,"85":1,"88":1,"168":1,"176":2,"188":1}}],["projecting",{"2":{"192":1}}],["projections",{"2":{"158":1}}],["projection",{"2":{"85":2,"190":1,"192":1}}],["project",{"2":{"9":1}}],["projects",{"2":{"9":1}}],["proj",{"2":{"1":2,"6":3,"172":2,"173":4,"175":1,"176":6,"190":1}}],["prints",{"2":{"173":1}}],["printstyled",{"2":{"60":1,"173":1,"176":1}}],["println",{"2":{"60":1,"173":1,"176":1,"180":2}}],["print",{"2":{"60":2,"173":2,"176":2}}],["primitives",{"0":{"156":1},"2":{"31":1,"156":1}}],["primitive",{"2":{"29":1}}],["primarily",{"2":{"25":2,"27":2,"159":1}}],["primary",{"2":{"3":2,"6":3,"94":1,"135":1,"180":1}}],["priority",{"2":{"1":1,"172":1}}],["pay",{"2":{"154":1}}],["paper",{"2":{"116":2}}],["pa",{"2":{"105":2}}],["pathof",{"2":{"180":2}}],["paths",{"0":{"196":1},"2":{"160":1,"196":2}}],["path",{"2":{"55":3,"192":2}}],["parquet",{"2":{"195":3}}],["parent",{"2":{"160":1}}],["parse",{"2":{"116":1,"122":1}}],["part",{"2":{"66":2,"76":2,"116":3,"152":1}}],["partition",{"2":{"153":2,"154":2}}],["partialsort",{"2":{"184":1}}],["partial",{"2":{"66":4}}],["partially",{"2":{"64":2,"76":2}}],["particularly",{"2":{"59":1}}],["particular",{"2":{"30":1,"53":1,"146":1,"192":2}}],["parameters",{"2":{"159":2}}],["parameter",{"2":{"159":3}}],["parameterized",{"2":{"158":1}}],["parametrized",{"2":{"158":1}}],["params",{"2":{"6":2,"188":10}}],["parallel",{"2":{"116":1,"145":1}}],["paradigm",{"0":{"29":1}}],["paradigms",{"0":{"17":1},"1":{"18":1,"19":1,"20":1},"2":{"17":2,"20":1}}],["parlance",{"2":{"5":1,"6":1,"59":1,"158":1}}],["passes",{"2":{"66":2,"116":1}}],["passed",{"2":{"1":2,"6":5,"82":1,"146":1,"156":4,"172":1,"176":2,"185":1,"188":2,"200":1}}],["passable",{"2":{"59":18}}],["passing",{"2":{"18":1,"153":1,"178":1}}],["pass",{"2":{"5":1,"6":3,"18":1,"59":1,"91":1,"104":1,"116":2,"119":1,"153":1,"160":1,"176":1,"180":1}}],["pairs",{"2":{"73":1,"191":1}}],["pair",{"2":{"3":2,"6":2,"66":1,"122":2,"153":2,"154":1}}],["packages",{"2":{"25":1,"27":1,"77":1,"156":2,"161":1,"187":1,"190":3,"192":1,"195":1}}],["package",{"2":{"1":2,"6":1,"22":1,"25":2,"27":2,"60":1,"77":2,"82":1,"146":1,"172":3,"173":1,"176":1,"192":1,"195":2}}],["page",{"2":{"0":1,"9":1,"26":1,"29":1,"31":1,"32":1,"50":1,"53":1,"56":1,"58":1,"59":1,"60":1,"63":1,"64":1,"66":1,"69":1,"71":1,"73":1,"74":1,"76":1,"82":1,"85":1,"88":1,"91":1,"101":1,"104":1,"105":1,"115":1,"116":1,"119":1,"122":1,"132":1,"142":1,"145":1,"146":1,"147":1,"153":1,"154":1,"155":1,"156":1,"160":1,"163":1,"166":1,"169":1,"170":1,"171":1,"173":1,"177":1,"184":1,"185":1,"186":1,"188":1,"189":1}}],["p",{"2":{"1":5,"6":2,"13":13,"14":12,"15":2,"52":1,"55":1,"62":1,"64":5,"65":1,"66":5,"71":2,"75":2,"79":1,"80":1,"84":3,"87":1,"90":1,"93":1,"103":1,"105":4,"107":1,"116":20,"118":1,"121":1,"124":1,"134":1,"145":4,"146":3,"148":3,"150":3,"153":5,"171":7,"175":1,"179":1,"180":1,"184":3,"185":9,"186":7,"189":11,"197":1,"198":1}}],["poylgon",{"2":{"116":1}}],["potential",{"2":{"66":1}}],["potentially",{"2":{"6":2,"64":1,"166":2,"169":2}}],["post",{"2":{"182":2}}],["possibly",{"2":{"153":1}}],["possiblenodes",{"2":{"146":2}}],["possible",{"2":{"6":3,"70":1,"72":1,"75":1,"146":1,"153":1,"154":1}}],["possibility",{"2":{"151":1}}],["possibilities",{"2":{"73":1}}],["position=",{"2":{"180":1}}],["position",{"2":{"6":1,"145":1,"175":1}}],["positive",{"2":{"4":4,"6":4,"55":3,"56":1,"84":3,"85":3,"177":2,"184":1}}],["poles",{"2":{"158":1}}],["pole",{"2":{"158":2}}],["polgons",{"2":{"146":1}}],["polgontrait",{"2":{"1":1,"150":1}}],["polar",{"2":{"6":1,"176":1}}],["polynodes",{"2":{"64":7,"70":1,"72":1,"75":1}}],["polynode",{"2":{"64":36}}],["polypoints",{"2":{"59":46}}],["polys",{"2":{"6":2,"64":39,"68":3,"69":10,"70":14,"71":14,"72":9,"73":6,"75":14,"76":28,"169":26}}],["polys1",{"2":{"3":2,"6":2,"122":6}}],["polys2",{"2":{"3":2,"6":2,"122":6}}],["poly",{"2":{"3":2,"6":16,"15":2,"52":1,"55":1,"56":6,"58":2,"59":3,"62":1,"64":107,"65":2,"66":4,"68":4,"69":20,"70":22,"71":12,"72":15,"73":10,"75":19,"76":79,"79":2,"84":1,"85":3,"88":4,"105":10,"116":21,"122":8,"145":5,"146":5,"169":18,"175":2,"179":2,"180":8,"189":8,"191":2,"198":5}}],["poly2",{"2":{"3":3,"4":2,"6":7,"70":2,"88":2,"116":19,"122":7}}],["poly1",{"2":{"3":3,"4":2,"6":7,"70":2,"88":2,"116":14,"122":7}}],["polygon3",{"2":{"191":2,"192":1}}],["polygon2",{"2":{"191":6}}],["polygon1",{"2":{"191":4}}],["polygonization",{"2":{"146":1}}],["polygonizing",{"0":{"146":1}}],["polygonized",{"2":{"146":1}}],["polygonize",{"2":{"0":1,"6":6,"9":1,"31":1,"146":31}}],["polygon`",{"2":{"59":3,"82":1}}],["polygons",{"0":{"99":1,"130":1,"140":1,"167":1},"1":{"168":1,"169":1},"2":{"3":4,"4":3,"5":1,"6":18,"9":1,"23":5,"31":1,"56":5,"57":1,"59":2,"62":1,"63":1,"64":14,"66":1,"69":2,"70":9,"71":5,"72":3,"73":8,"75":6,"76":19,"84":1,"85":1,"88":5,"99":1,"122":5,"140":1,"145":1,"146":19,"151":1,"162":2,"166":3,"167":2,"168":2,"169":10,"178":1,"180":1,"191":3,"192":1,"198":5,"199":3}}],["polygontrait",{"2":{"1":1,"3":4,"4":6,"6":13,"15":3,"23":1,"32":1,"53":2,"56":3,"59":3,"63":2,"64":2,"66":2,"69":1,"70":5,"71":5,"72":3,"73":5,"75":4,"76":8,"85":2,"88":10,"96":2,"97":1,"98":1,"99":3,"100":1,"105":4,"110":2,"111":2,"112":1,"113":2,"114":1,"122":8,"127":2,"128":1,"129":1,"130":3,"131":1,"137":2,"138":1,"139":1,"140":3,"141":1,"148":1,"150":1,"151":2,"153":2,"154":1,"163":2,"165":2,"166":1,"169":2,"180":2,"189":1}}],["polygon",{"0":{"34":1,"64":1,"67":1,"70":1,"71":1,"73":1,"75":1,"76":1,"113":1},"1":{"35":1,"36":1,"37":1,"38":1,"68":1,"69":1},"2":{"0":1,"1":4,"3":4,"4":21,"5":9,"6":65,"9":2,"11":3,"15":4,"20":1,"23":1,"52":1,"53":5,"55":2,"56":5,"57":5,"58":16,"59":57,"62":2,"63":4,"64":39,"65":3,"66":4,"68":3,"69":10,"70":5,"71":7,"72":5,"73":6,"75":6,"76":34,"77":1,"81":1,"82":3,"84":3,"85":14,"88":12,"94":1,"96":3,"97":3,"98":4,"99":8,"105":1,"110":3,"111":4,"112":4,"113":4,"116":66,"122":4,"125":1,"127":3,"128":4,"129":5,"130":7,"135":1,"137":3,"138":4,"139":4,"140":8,"145":5,"146":4,"150":1,"153":1,"161":4,"162":8,"163":5,"164":1,"166":4,"167":3,"168":12,"169":7,"175":1,"176":4,"179":1,"180":9,"185":3,"189":6,"191":12,"192":5,"193":7,"194":2,"198":6}}],["pointwise",{"0":{"185":1},"2":{"172":1}}],["point1",{"2":{"85":4}}],["point`",{"2":{"73":1}}],["pointedgeside",{"2":{"64":1}}],["point₂",{"2":{"63":13}}],["point₁",{"2":{"63":13}}],["point3s",{"2":{"59":10}}],["point3f",{"2":{"58":1}}],["pointrait",{"2":{"6":1}}],["point2f",{"2":{"58":4,"59":2,"79":1,"84":1}}],["point2d",{"2":{"58":1}}],["point2",{"2":{"6":2,"59":5,"81":1,"85":4}}],["pointtrait",{"2":{"1":1,"4":4,"6":6,"18":1,"32":1,"50":1,"53":2,"56":1,"59":3,"66":1,"72":2,"82":1,"85":17,"88":8,"96":6,"100":1,"110":6,"114":1,"127":6,"131":1,"137":6,"141":1,"148":2,"150":1,"151":1,"153":7,"154":4,"156":12,"159":2,"165":2,"166":1,"170":1,"171":2,"180":2,"185":2,"186":2,"189":1}}],["point",{"0":{"110":1,"127":1},"2":{"1":4,"3":10,"4":37,"5":7,"6":82,"9":2,"20":1,"50":1,"53":12,"56":3,"57":3,"58":1,"59":126,"63":6,"64":97,"66":50,"69":13,"71":9,"72":1,"73":71,"76":9,"82":3,"84":17,"85":73,"88":27,"91":3,"93":1,"94":2,"96":8,"105":21,"108":2,"110":10,"116":138,"118":1,"121":2,"122":21,"124":1,"125":3,"127":10,"128":1,"130":1,"135":4,"137":11,"145":1,"146":3,"150":1,"153":1,"161":1,"162":2,"166":2,"167":1,"169":2,"172":1,"174":1,"180":2,"181":3,"182":9,"183":1,"185":2,"189":4,"191":111,"192":7,"193":8,"198":2}}],["points2",{"2":{"3":1,"6":1,"122":3}}],["points1",{"2":{"3":1,"6":1,"122":3}}],["points",{"0":{"96":1,"137":1},"2":{"0":1,"1":3,"3":1,"4":11,"5":1,"6":53,"9":3,"13":1,"53":2,"55":3,"56":3,"58":8,"59":32,"63":1,"64":55,"66":5,"69":3,"70":5,"71":1,"72":8,"73":28,"75":4,"76":1,"77":3,"79":4,"81":4,"82":6,"84":2,"85":8,"87":2,"88":13,"90":1,"94":6,"96":3,"97":4,"108":6,"110":1,"111":7,"112":3,"116":10,"122":5,"125":5,"127":1,"128":2,"129":4,"130":3,"134":1,"135":5,"137":3,"138":3,"139":3,"140":1,"144":1,"146":3,"153":2,"154":1,"170":2,"172":1,"175":2,"176":2,"180":11,"181":11,"182":26,"183":10,"184":33,"185":2,"186":2,"189":25,"191":8,"192":1,"197":1,"198":11}}],["pointorientation",{"2":{"0":1,"6":2,"116":2}}],["my",{"2":{"199":1,"200":2}}],["m`",{"2":{"158":1}}],["mdk",{"2":{"153":2}}],["moore",{"2":{"146":1}}],["moved",{"2":{"64":1,"172":1}}],["move",{"2":{"63":1,"116":1}}],["mode",{"2":{"200":3}}],["model",{"2":{"158":1,"197":1}}],["modify",{"2":{"191":1}}],["modified",{"2":{"153":1,"154":1}}],["module",{"2":{"172":1,"177":1}}],["modules",{"2":{"6":1,"59":1}}],["mod1",{"2":{"59":5}}],["mod",{"2":{"59":1,"64":1}}],["most",{"2":{"25":1,"27":1,"64":1,"71":1,"73":1,"76":1,"81":1,"116":1,"180":1,"195":2}}],["monotone",{"2":{"20":1,"81":2}}],["monotonechainmethod",{"2":{"0":1,"6":1,"77":1,"81":2,"82":4}}],["moment",{"2":{"6":1,"175":1,"177":1}}],["more",{"2":{"6":3,"7":1,"9":1,"10":1,"11":1,"23":1,"26":1,"29":1,"64":1,"70":1,"72":1,"75":1,"82":1,"116":1,"122":1,"153":2,"154":2,"157":1,"158":2,"163":1,"173":1,"175":1,"177":1,"191":2,"197":1}}],["missing",{"2":{"175":4}}],["missed",{"2":{"146":1}}],["mistakenly",{"2":{"167":1}}],["mid",{"2":{"66":2,"116":3}}],["midpoint",{"2":{"64":2}}],["middle",{"2":{"64":2}}],["mining",{"2":{"199":1}}],["minimal",{"2":{"199":1}}],["minimize",{"2":{"73":1}}],["minimum",{"2":{"4":7,"6":12,"65":1,"66":2,"85":12,"180":1,"181":1,"183":1}}],["mind",{"2":{"175":1,"198":1}}],["minmax",{"2":{"73":4}}],["min",{"2":{"66":1,"73":16,"85":15,"180":1,"182":3,"183":1,"184":28}}],["minus",{"2":{"55":1}}],["minpoints=0",{"2":{"146":1}}],["minpoints",{"2":{"6":2}}],["might",{"2":{"6":3,"25":1,"27":1,"56":1,"70":1,"72":1,"73":1,"75":1,"164":1,"182":1}}],["mixed",{"2":{"6":4,"180":1}}],["m",{"2":{"5":1,"6":1,"15":10,"59":2,"66":6,"145":2,"192":1}}],["mp",{"2":{"105":2}}],["mp1",{"2":{"4":2,"6":2,"88":7}}],["mp2",{"2":{"4":2,"6":2,"88":8}}],["mason",{"2":{"153":1,"154":1}}],["markersize",{"2":{"191":2}}],["marker",{"2":{"191":2}}],["marked",{"2":{"64":6,"71":1,"73":1,"76":1}}],["marking",{"2":{"71":2,"73":2,"76":2}}],["mark",{"2":{"64":2}}],["marks",{"2":{"64":1}}],["mag",{"2":{"53":4}}],["making",{"2":{"64":1,"146":1,"151":1,"168":1,"191":1,"193":2}}],["makie",{"2":{"13":1,"14":1,"52":1,"55":1,"58":3,"62":1,"65":1,"68":4,"79":1,"80":1,"84":1,"87":1,"90":1,"93":1,"103":1,"107":1,"118":1,"121":1,"124":1,"134":1,"146":3,"179":1,"180":1,"196":1}}],["makevalid",{"2":{"180":2}}],["makes",{"2":{"56":1,"64":1,"85":1,"153":1,"162":1,"167":1,"168":1}}],["make",{"2":{"9":1,"25":1,"27":1,"53":1,"64":3,"73":1,"85":1,"122":1,"146":2,"151":1,"163":1,"175":1,"184":1,"187":1,"191":2,"193":1}}],["mainly",{"2":{"59":1,"84":1,"148":1,"154":1}}],["maintain",{"2":{"56":1}}],["main",{"0":{"28":1},"1":{"29":1,"30":1},"2":{"7":1,"26":1,"64":1,"146":1,"153":1,"176":1}}],["manner",{"2":{"197":1}}],["manipulate",{"2":{"195":1}}],["manifolds",{"2":{"158":2}}],["manifold",{"0":{"158":1},"2":{"6":1,"31":2,"158":12,"177":3}}],["manually",{"2":{"161":1}}],["many",{"2":{"4":1,"5":1,"6":2,"23":1,"26":1,"59":1,"64":1,"69":1,"71":1,"73":2,"76":1,"88":1,"162":1,"170":1}}],["mapped",{"2":{"195":1}}],["maptasks`",{"2":{"153":1}}],["maptasks",{"2":{"153":7}}],["mapreducetasks`",{"2":{"154":1}}],["mapreducetasks",{"2":{"154":5}}],["mapreduce",{"2":{"71":1,"146":2,"153":3,"154":7}}],["map",{"0":{"192":1},"2":{"6":1,"13":1,"18":3,"19":1,"29":1,"59":1,"64":1,"73":1,"122":2,"146":17,"153":11,"154":5,"156":10,"158":1,"159":1,"163":1,"180":1,"190":1,"192":1,"193":1,"195":1}}],["matches",{"2":{"88":1,"146":1}}],["match",{"2":{"88":12,"122":3,"146":1,"153":1,"156":2}}],["matching",{"2":{"3":1,"6":1,"22":1,"88":3,"122":1,"151":2}}],["matlab",{"2":{"68":1}}],["materializer`",{"2":{"153":1}}],["materializer",{"2":{"22":1,"153":1}}],["mathematical",{"2":{"158":1}}],["mathematically",{"2":{"6":1,"145":1,"158":2}}],["mathematics",{"2":{"158":1}}],["mathrm",{"2":{"59":1}}],["math",{"2":{"7":1}}],["matrix",{"2":{"6":1,"14":2,"59":1}}],["maxlog=3",{"2":{"177":1}}],["maximal",{"2":{"73":1}}],["maximum",{"2":{"3":1,"6":4,"14":1,"65":1,"66":4,"105":1,"146":1,"176":1,"177":1,"182":4}}],["max",{"2":{"6":9,"32":9,"53":2,"66":1,"146":1,"153":1,"154":1,"175":5,"176":8,"177":21,"182":36,"184":1,"196":1}}],["made",{"2":{"6":2,"63":1,"64":1,"73":1,"129":1,"151":1,"166":1,"169":1,"183":1}}],["maybe",{"2":{"32":1,"116":4,"153":2,"154":3,"156":2}}],["may",{"2":{"1":1,"6":3,"23":2,"24":1,"59":2,"64":1,"73":1,"81":1,"145":1,"147":1,"150":1,"151":1,"153":4,"158":1,"159":1,"167":1,"168":1,"176":1}}],["mercator",{"2":{"192":1}}],["merge",{"2":{"153":1}}],["measures",{"2":{"158":1}}],["measure",{"2":{"156":1,"174":1}}],["meant",{"2":{"26":1}}],["meaning",{"2":{"3":3,"4":1,"6":4,"24":1,"56":1,"60":1,"122":4,"145":1,"158":1}}],["means",{"2":{"3":1,"6":2,"55":1,"56":1,"60":1,"93":1,"116":3,"121":1,"122":1,"145":1,"151":1,"158":2,"160":2}}],["mean",{"2":{"0":1,"6":5,"17":1,"58":2,"59":5,"154":1,"158":1}}],["meanvalue",{"2":{"0":1,"5":2,"6":3,"57":1,"58":2,"59":15}}],["meets",{"2":{"116":9,"122":1,"182":1}}],["meet",{"2":{"73":3,"93":1,"94":1,"108":1,"116":7,"125":1,"135":1}}],["memory",{"2":{"59":1,"193":1}}],["mesh",{"2":{"58":1}}],["message",{"2":{"6":1,"188":1}}],["me",{"0":{"23":1}}],["mentioned",{"2":{"19":1}}],["mentions",{"2":{"6":1,"188":1}}],["menu",{"2":{"14":3}}],["median",{"2":{"13":4,"154":1}}],["mechanics",{"2":{"6":1,"59":1}}],["metadatakeys",{"2":{"153":1}}],["metadatasupport",{"2":{"153":2}}],["metadata",{"2":{"153":11}}],["met",{"2":{"116":44}}],["meters",{"2":{"6":4,"175":1,"176":4,"192":1}}],["methoderror",{"2":{"31":3}}],["methods",{"0":{"2":1,"3":1,"4":1,"6":1,"39":1},"1":{"3":1,"4":1,"40":1,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1,"47":1,"48":1,"49":1},"2":{"1":1,"6":7,"9":1,"25":1,"27":1,"31":27,"59":3,"77":1,"94":1,"108":1,"125":1,"135":1,"146":1,"151":1,"153":1,"156":2,"159":1,"175":1,"180":2,"185":1,"191":1}}],["method",{"0":{"173":1},"2":{"1":1,"4":1,"5":10,"6":30,"24":1,"32":1,"53":1,"56":1,"59":37,"60":2,"63":1,"66":1,"73":1,"76":1,"81":3,"82":2,"85":4,"88":1,"94":2,"108":2,"122":1,"125":2,"135":2,"146":1,"153":1,"154":1,"156":3,"160":1,"172":1,"173":2,"175":2,"176":9,"177":22,"178":2,"188":3,"197":1,"200":1}}],["mutation",{"2":{"194":1}}],["mutlipolygon",{"2":{"4":1,"6":1,"63":1}}],["muladd",{"2":{"59":2}}],["multifloats",{"2":{"13":1,"14":1,"15":1}}],["multifloat",{"2":{"7":1}}],["multilinestringtrait",{"2":{"32":1,"165":1}}],["multilinestring",{"2":{"6":1,"189":2}}],["multi",{"0":{"100":1,"101":1,"114":1,"115":1,"131":1,"132":1,"141":1,"142":1},"2":{"4":2,"6":3,"53":1,"56":1,"66":1,"88":2,"100":1,"101":1,"114":1,"115":1,"131":1,"132":1,"141":1,"142":1}}],["multicurves",{"2":{"56":1,"66":1}}],["multicurve",{"2":{"4":1,"6":1,"56":1}}],["multigeometry",{"2":{"4":2,"6":2,"85":2}}],["multiplication",{"2":{"177":1}}],["multiplied",{"2":{"59":3,"158":1}}],["multiple",{"2":{"4":1,"6":1,"59":1,"116":1,"170":1,"184":1,"191":1,"195":1}}],["multiply",{"2":{"1":1,"6":1,"185":1}}],["multipolys",{"2":{"76":3}}],["multipoly`",{"2":{"70":2,"72":2,"75":2}}],["multipoly",{"2":{"6":9,"15":3,"70":1,"71":27,"72":1,"73":26,"75":1,"76":24,"169":21,"180":9}}],["multipolygon`",{"2":{"146":1}}],["multipolygons",{"2":{"3":1,"4":1,"6":11,"63":1,"70":3,"72":3,"73":3,"75":3,"84":1,"88":1,"122":2,"168":1,"191":1,"192":1}}],["multipolygon",{"2":{"3":2,"4":5,"6":17,"56":2,"62":1,"70":1,"71":7,"72":1,"73":5,"75":1,"76":6,"88":4,"122":2,"146":7,"166":2,"167":7,"168":8,"169":2,"180":3,"191":8,"192":1}}],["multipolygontrait",{"2":{"1":1,"3":4,"4":2,"6":6,"23":1,"32":1,"71":4,"73":4,"76":4,"88":6,"100":1,"101":1,"114":1,"115":1,"122":8,"131":1,"132":1,"141":1,"142":1,"150":1,"153":1,"165":1,"169":4}}],["multipoint",{"2":{"4":5,"6":5,"50":1,"56":1,"88":5,"105":6,"153":1,"154":2,"180":1,"191":5}}],["multipoints",{"2":{"3":2,"4":1,"6":3,"56":1,"66":1,"88":2,"122":2,"180":1,"191":1}}],["multipointtrait",{"2":{"1":1,"3":2,"4":4,"6":7,"32":1,"53":2,"56":1,"66":1,"88":8,"100":1,"101":1,"105":4,"114":1,"115":1,"122":4,"131":1,"132":1,"141":1,"142":1,"150":1,"151":1,"165":1,"180":2,"189":1}}],["multithreading",{"2":{"1":2,"4":1,"6":4,"150":1,"155":1}}],["must",{"2":{"1":1,"3":8,"4":2,"5":1,"6":22,"9":1,"53":1,"59":5,"60":1,"64":2,"66":3,"69":1,"73":2,"88":5,"91":2,"94":1,"103":1,"104":1,"108":1,"116":13,"122":1,"124":1,"125":1,"135":2,"146":1,"153":1,"156":2,"164":1,"165":3,"166":1,"172":1,"176":1,"177":1,"180":1,"184":4,"188":1}}],["much",{"2":{"0":1,"6":3,"70":1,"72":1,"75":1}}],["df",{"2":{"194":3,"195":4,"198":10,"199":12}}],["dp",{"2":{"180":3}}],["dy",{"2":{"145":2,"177":3}}],["dy2",{"2":{"145":2}}],["dy1",{"2":{"105":10,"145":2}}],["dyc",{"2":{"105":2}}],["dx",{"2":{"145":2,"177":3}}],["dx2",{"2":{"145":2}}],["dx1",{"2":{"105":10,"145":2}}],["dxc",{"2":{"105":2}}],["drop",{"2":{"76":1,"169":1,"177":1}}],["driven",{"2":{"25":1,"27":1}}],["driving",{"2":{"25":1,"27":1}}],["duplicated",{"2":{"73":2}}],["during",{"2":{"64":1}}],["due",{"2":{"63":1,"64":1,"73":3}}],["date",{"2":{"81":1}}],["datas",{"2":{"200":1}}],["datasets",{"2":{"192":1,"197":1,"198":1}}],["dataset",{"2":{"192":1,"197":1}}],["datainterpolations",{"2":{"174":1}}],["dataapi",{"2":{"31":1,"153":7}}],["dataaspect",{"2":{"13":1,"14":1,"52":1,"55":1,"58":2,"62":1,"65":1,"84":2,"146":2,"175":1,"180":1}}],["dataframes",{"2":{"194":3,"198":2,"199":1}}],["dataframe",{"2":{"29":1,"194":1,"197":1,"198":5,"199":2}}],["data",{"0":{"146":1,"195":1},"2":{"23":1,"25":1,"27":1,"80":1,"146":2,"153":1,"180":10,"190":1,"192":2,"193":2,"194":2,"195":6,"198":1}}],["dashboard",{"0":{"14":1},"2":{"13":1,"14":1}}],["d",{"2":{"1":2,"5":1,"6":1,"59":1,"158":1,"172":1,"182":3}}],["deu",{"2":{"199":2}}],["demonstrates",{"2":{"198":1}}],["densify",{"2":{"176":3}}],["densifying",{"2":{"176":1}}],["densifies",{"2":{"174":1}}],["denoted",{"2":{"116":1}}],["denotes",{"2":{"64":1}}],["debug",{"2":{"165":1}}],["debugging",{"2":{"59":1,"64":1}}],["derivation",{"2":{"73":1}}],["dealing",{"2":{"66":1}}],["delete",{"2":{"105":1,"122":1,"146":1,"182":1}}],["deleteat",{"2":{"64":6,"182":1,"184":2}}],["deltri",{"2":{"82":1}}],["delayed",{"2":{"64":10,"71":2,"73":2,"76":2}}],["delay",{"2":{"64":14,"70":2,"71":2,"72":2,"73":2,"75":2,"76":2}}],["delaunay",{"2":{"6":1,"82":1}}],["delaunaytriangulation",{"2":{"6":1,"31":1,"77":1,"82":6}}],["deprecated",{"2":{"177":1}}],["depend",{"2":{"64":2}}],["depends",{"2":{"64":1,"69":1}}],["depending",{"2":{"1":1,"23":1,"73":1,"150":1,"153":1}}],["depth",{"2":{"59":2}}],["desktop",{"2":{"195":1}}],["dest",{"2":{"192":2}}],["destination",{"2":{"192":5}}],["desired",{"2":{"75":1,"195":1}}],["despite",{"2":{"53":1}}],["describe",{"2":{"17":1}}],["described",{"2":{"6":1,"59":1,"63":1,"64":1}}],["de",{"0":{"39":1},"1":{"40":1,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1,"47":1,"48":1,"49":1},"2":{"116":2,"197":1}}],["deconstruct",{"2":{"148":1,"154":1}}],["decomposition",{"2":{"18":1,"148":1}}],["decomposing",{"2":{"18":1}}],["decompose",{"2":{"18":2,"29":1,"59":4,"154":1,"161":1}}],["decrementing",{"2":{"146":1}}],["decrease",{"2":{"63":1}}],["decreasing",{"2":{"6":1,"180":1}}],["decide",{"2":{"81":1,"146":1}}],["decision",{"2":{"24":1}}],["degeneracies",{"2":{"9":1}}],["degenerate",{"2":{"6":1,"59":1,"69":2}}],["degrees",{"2":{"6":1,"52":1,"145":1,"158":1,"176":1}}],["defines",{"2":{"64":1,"148":1,"154":1,"155":1,"156":1,"157":1,"164":1,"187":1}}],["define",{"2":{"58":1,"73":2,"87":1,"146":2,"148":1,"151":1,"153":1,"158":1,"187":1,"200":2}}],["defined",{"2":{"4":1,"5":1,"6":3,"7":1,"22":1,"53":2,"59":1,"65":1,"66":4,"73":4,"85":2,"144":1,"153":1,"158":3,"177":1,"187":1}}],["definitions",{"2":{"188":1}}],["definition",{"2":{"4":4,"6":6,"88":8,"93":1,"158":3,"177":1}}],["default",{"2":{"1":2,"4":5,"6":13,"32":1,"53":1,"56":2,"66":1,"70":1,"71":2,"72":1,"73":5,"75":1,"76":2,"82":1,"85":2,"146":5,"153":5,"156":2,"158":1,"172":2,"180":2}}],["defaults",{"2":{"1":6,"4":2,"6":11,"150":3,"153":1,"155":3}}],["deeper",{"2":{"1":1,"150":1,"152":1,"153":2}}],["detrimental",{"2":{"167":1}}],["detector",{"2":{"81":1}}],["detection",{"2":{"81":1,"146":1}}],["determined",{"2":{"64":1,"66":1}}],["determine",{"2":{"64":10,"66":3,"70":2,"72":2,"73":9,"75":2,"94":1,"108":1,"116":9,"122":1,"125":1,"135":1,"182":3,"197":1}}],["determines",{"2":{"56":1,"64":3,"73":1,"116":10}}],["determinant",{"2":{"6":1,"59":1}}],["detail",{"2":{"26":1}}],["details",{"2":{"6":2,"175":2}}],["details>",{"2":{"6":2}}],["det",{"2":{"0":1,"6":2,"59":4}}],["dirname",{"2":{"180":4}}],["dirty",{"2":{"179":1}}],["directive",{"2":{"153":1}}],["direction",{"2":{"6":1,"64":1,"88":5,"146":3}}],["direct",{"2":{"33":1}}],["directly",{"2":{"6":1,"82":3,"146":1,"176":1}}],["dig",{"2":{"153":1}}],["dict",{"2":{"146":5}}],["didn",{"2":{"76":1,"116":1,"160":1}}],["division",{"2":{"58":1}}],["divided",{"2":{"26":1}}],["ditance",{"2":{"4":1,"6":1,"85":1}}],["dimensional",{"2":{"5":1,"6":3,"55":1,"59":1,"82":2,"158":1}}],["dimensions",{"2":{"3":1,"6":1,"122":2}}],["dimension",{"2":{"1":1,"3":4,"6":4,"105":2,"121":1,"122":4,"172":1}}],["discouraged",{"2":{"193":1}}],["discussion",{"2":{"25":1,"27":1,"69":1}}],["distributed",{"2":{"198":2}}],["distinct",{"2":{"73":1,"146":1}}],["dist",{"2":{"73":40,"85":19,"116":2,"176":8,"182":40}}],["distance`",{"2":{"176":3,"177":2,"188":1}}],["distances",{"2":{"73":1,"84":1,"176":1,"181":5}}],["distance",{"0":{"83":2,"84":2},"1":{"84":2,"85":2},"2":{"0":4,"4":26,"6":47,"31":1,"32":9,"59":13,"60":3,"66":3,"73":32,"83":2,"84":17,"85":92,"116":1,"148":1,"154":1,"174":1,"175":5,"176":13,"177":28,"180":1,"181":3,"182":6,"183":1,"196":1,"200":2}}],["disagree",{"2":{"56":1}}],["displacement",{"2":{"158":1}}],["displaying",{"2":{"192":1}}],["display",{"2":{"13":1,"58":1,"192":1}}],["disparate",{"2":{"25":1,"27":1}}],["dispatches",{"2":{"6":1,"53":1,"56":1,"59":1,"63":1,"66":1,"85":1,"88":1,"94":1,"108":1,"122":1,"125":1,"135":1}}],["dispatch",{"2":{"4":1,"6":3,"24":1,"59":1,"88":1,"156":3,"159":1,"188":1}}],["disjoint",{"0":{"41":1,"106":1,"107":1,"110":1,"111":1,"112":1,"113":1,"114":1},"1":{"107":1,"108":1},"2":{"0":2,"3":5,"6":7,"23":1,"31":1,"41":2,"76":2,"106":1,"107":3,"108":11,"109":6,"110":15,"111":18,"112":10,"113":5,"114":4,"115":2,"116":16,"119":3,"166":2,"167":2,"169":7,"197":1}}],["diffs",{"2":{"53":4}}],["diff",{"2":{"6":2,"53":17,"70":5,"71":3,"122":3,"169":8}}],["differs",{"2":{"192":1}}],["differ",{"2":{"4":1,"6":1,"85":1}}],["differently",{"2":{"4":3,"6":4,"53":1,"56":2,"66":1}}],["different",{"2":{"3":1,"4":4,"6":6,"20":1,"23":1,"53":1,"56":4,"64":3,"66":1,"73":2,"81":1,"122":3,"146":1,"151":1,"153":1,"167":2,"192":2,"195":1}}],["differences",{"0":{"71":1},"2":{"73":1,"182":1}}],["difference",{"0":{"35":1,"38":1,"70":1},"2":{"0":1,"6":7,"11":1,"23":1,"31":1,"35":2,"38":1,"64":3,"70":12,"71":14,"73":1,"75":1,"76":3,"166":1,"169":1,"176":1}}],["diffintersectingpolygons",{"2":{"0":1,"6":1,"166":1,"169":4}}],["doi",{"2":{"70":2,"72":2,"75":2,"116":1}}],["doing",{"2":{"17":1,"22":1,"153":1,"193":1}}],["dot",{"2":{"53":2,"59":1}}],["download",{"2":{"192":1}}],["down",{"2":{"18":1,"29":1,"59":1,"156":2}}],["doable",{"2":{"9":1}}],["documenter",{"2":{"175":2}}],["documentation",{"0":{"0":1},"1":{"1":1,"2":1,"3":1,"4":1,"5":1,"6":1},"2":{"0":1,"26":1,"58":1,"155":1}}],["docstring",{"2":{"175":4}}],["docstrings",{"0":{"149":1},"1":{"150":1},"2":{"7":1,"155":1}}],["docs",{"0":{"26":1,"155":1},"2":{"26":1,"146":1}}],["doc",{"2":{"9":1,"10":1}}],["does",{"0":{"22":1},"2":{"7":1,"19":1,"62":1,"73":2,"90":1,"93":3,"116":1}}],["doesn",{"2":{"4":1,"6":2,"32":1,"56":1,"69":1,"88":1,"176":1,"188":1,"199":1}}],["doublets",{"2":{"199":2}}],["double",{"2":{"183":4}}],["doubled",{"2":{"6":1,"183":1}}],["douglas",{"2":{"178":2,"180":2,"182":1}}],["douglaspeucker",{"0":{"182":1},"2":{"0":1,"6":5,"178":1,"180":5,"182":6}}],["done",{"0":{"10":1},"2":{"13":1,"14":1,"116":2,"146":1,"148":1,"158":1,"167":1,"191":1,"197":1}}],["don",{"2":{"4":2,"6":2,"64":4,"70":1,"76":1,"88":6,"111":1,"116":1,"124":1,"128":1,"146":2,"153":3,"154":2,"188":1}}],["do",{"0":{"9":1,"23":1,"74":1},"2":{"1":1,"6":1,"14":1,"23":1,"31":1,"53":1,"56":2,"59":2,"60":1,"63":1,"66":1,"69":1,"76":3,"85":2,"87":1,"88":1,"103":1,"111":2,"112":1,"113":2,"116":1,"146":4,"148":1,"150":1,"153":5,"154":2,"156":5,"162":1,"163":1,"168":1,"171":2,"173":1,"176":1,"180":1,"185":2,"186":2,"192":2,"193":2,"194":3,"198":1,"199":1}}],["aim",{"2":{"158":1}}],["ay",{"2":{"145":2}}],["azimuth",{"2":{"145":2}}],["automatically",{"2":{"122":1,"153":1,"192":1}}],["a3",{"2":{"80":1}}],["against",{"2":{"88":1,"116":2,"176":1,"180":1}}],["again",{"2":{"73":1,"153":1}}],["a``",{"2":{"71":2}}],["a`",{"2":{"71":7,"73":3,"76":2}}],["away",{"2":{"60":1,"105":1,"200":1}}],["a2y",{"2":{"73":4}}],["a2x",{"2":{"73":4}}],["a2",{"2":{"58":4,"73":54,"81":1,"122":6}}],["a1y",{"2":{"73":7}}],["a1x",{"2":{"73":7}}],["a1",{"2":{"58":2,"64":9,"73":66,"81":1,"122":6}}],["ams",{"2":{"196":2}}],["america",{"2":{"180":1}}],["am",{"2":{"116":1}}],["ambiguity",{"2":{"85":2,"153":1,"154":1,"156":3}}],["amounts",{"2":{"195":1}}],["amount",{"2":{"55":1,"65":1}}],["amp",{"2":{"6":1,"9":1}}],["axes",{"2":{"146":6}}],["ax",{"2":{"13":3,"14":3,"145":2,"191":5}}],["axis`",{"2":{"158":1}}],["axislegend",{"2":{"79":1,"175":1,"179":1}}],["axis",{"2":{"13":2,"14":1,"52":1,"55":1,"58":4,"62":1,"65":1,"81":2,"84":1,"146":2,"158":4,"175":1,"180":1,"196":1}}],["axs",{"2":{"13":2}}],["advised",{"2":{"159":1}}],["advance",{"2":{"63":2}}],["adjust",{"2":{"73":1}}],["adjacent",{"2":{"64":1,"71":1,"73":1,"76":1}}],["adaptivity",{"0":{"74":1}}],["adaptive",{"2":{"7":1,"13":3,"14":2,"74":1}}],["adapted",{"2":{"70":1,"72":1,"75":1,"145":1}}],["administrative",{"2":{"199":1}}],["admin",{"2":{"11":1,"80":1,"180":2}}],["adm0",{"2":{"11":7,"80":4}}],["additional",{"2":{"195":1}}],["additionally",{"2":{"64":3,"88":1}}],["addition",{"2":{"58":1,"76":1}}],["adding",{"2":{"4":1,"6":4,"7":1,"76":2,"146":1,"170":1,"174":1,"176":2,"177":1}}],["added",{"2":{"53":1,"64":6,"66":1,"76":2,"156":2,"169":2,"182":1}}],["add",{"2":{"3":1,"6":1,"7":2,"59":1,"60":1,"64":16,"66":1,"69":4,"70":3,"72":1,"73":2,"75":2,"76":9,"82":1,"105":1,"146":4,"156":1,"160":1,"174":1,"176":1,"182":10,"192":1,"194":2,"197":4}}],["average",{"2":{"57":3,"63":4,"73":1}}],["available",{"0":{"166":1},"2":{"6":2,"31":1,"63":1,"159":1,"165":3,"174":1,"175":1,"177":1,"180":1,"184":1}}],["avoid",{"2":{"5":1,"6":7,"59":1,"70":1,"72":1,"73":1,"75":1,"76":1,"146":2,"153":1,"154":1,"156":3,"177":1,"181":1,"182":1,"183":1,"191":3}}],["a>",{"2":{"6":2}}],["achieve",{"2":{"167":1}}],["across",{"2":{"151":1}}],["acos",{"2":{"53":1}}],["activate",{"2":{"175":1}}],["action",{"2":{"20":2}}],["actions",{"2":{"20":2}}],["actual",{"2":{"10":1,"59":1,"145":1,"163":1,"172":1,"176":1}}],["actually",{"2":{"1":1,"6":1,"9":1,"59":4,"73":1,"76":3,"116":1,"146":1,"185":1,"195":1}}],["access",{"2":{"192":1}}],["accessed",{"2":{"188":1}}],["accepted",{"2":{"158":1}}],["acceptable",{"2":{"116":1}}],["accepts",{"2":{"82":1}}],["accept",{"2":{"6":1,"188":1}}],["according",{"2":{"162":1,"167":1,"168":1,"198":1}}],["accordingly",{"2":{"64":1}}],["account",{"2":{"70":1,"72":1}}],["accurary",{"2":{"73":1}}],["accuratearithmetic",{"2":{"11":2}}],["accurate",{"0":{"11":1},"2":{"11":3,"175":1}}],["accumulators",{"2":{"59":1}}],["accumulator",{"2":{"59":1}}],["accumulate",{"2":{"56":1,"63":3}}],["accumulation",{"0":{"11":1},"2":{"59":1}}],["after",{"2":{"6":8,"53":1,"64":3,"153":1,"154":1,"180":2}}],["ab",{"2":{"64":3,"70":1,"72":1,"73":5,"75":1}}],["able",{"2":{"20":1,"73":1}}],["ability",{"2":{"17":1}}],["about",{"2":{"6":1,"24":2,"30":1,"59":2,"116":1,"153":1,"158":1,"161":1}}],["above",{"2":{"4":1,"6":1,"53":1,"63":1,"64":1,"73":1,"76":1,"165":1}}],["abs",{"2":{"53":1,"56":4,"63":1,"66":2,"105":8,"122":2,"146":1,"183":1,"200":1}}],["absolutely",{"2":{"154":1}}],["absolute",{"2":{"4":1,"6":1,"55":1,"56":2}}],["abstractarray",{"2":{"153":4,"154":2,"156":1,"189":2}}],["abstractarrays",{"2":{"151":1}}],["abstractrange",{"2":{"146":6}}],["abstractmulticurvetrait",{"2":{"100":1,"101":1,"114":1,"115":1,"131":1,"132":1,"141":1,"142":1}}],["abstractmatrix",{"2":{"6":5,"146":13}}],["abstractpolygontrait",{"2":{"85":1}}],["abstractcurvetrait",{"2":{"53":1,"56":1,"66":1,"96":1,"99":1,"100":1,"105":1,"110":1,"114":1,"127":1,"130":1,"131":1,"137":1,"140":1,"141":1,"145":1,"180":2,"189":3}}],["abstractwkbgeomtrait",{"2":{"32":1}}],["abstractfloat",{"2":{"31":1,"53":1,"56":2,"64":1,"66":2,"69":1,"70":1,"72":1,"73":1,"75":1,"85":8}}],["abstract",{"2":{"6":3,"32":1,"59":3,"158":2,"160":3,"164":1,"165":4,"166":1,"180":3}}],["abstractvector",{"2":{"5":1,"6":1,"59":26,"146":8}}],["abstractgeometrytrait`",{"2":{"165":1}}],["abstractgeometrytrait",{"2":{"6":2,"32":1,"56":2,"66":1,"85":1,"101":1,"115":1,"132":1,"142":1,"151":1,"156":1,"165":4,"166":2,"189":5}}],["abstractgeometry",{"2":{"3":4,"6":4,"32":1,"91":2,"104":2}}],["abstracttrait",{"2":{"1":2,"3":2,"6":2,"69":1,"71":2,"73":4,"76":2,"122":4,"148":1,"150":2,"153":1,"154":2,"156":11,"159":3}}],["abstractbarycentriccoordinatemethod",{"2":{"0":1,"5":1,"6":7,"59":18}}],["attribute",{"2":{"195":1}}],["attributed",{"2":{"194":1}}],["attributes",{"0":{"194":1},"2":{"156":1,"190":1,"194":3}}],["attempt",{"2":{"158":1}}],["attempts",{"2":{"116":1}}],["attach",{"2":{"1":2,"4":1,"6":4,"150":1,"155":1}}],["atomic",{"2":{"7":1}}],["at",{"2":{"3":4,"5":1,"6":6,"9":1,"18":1,"20":1,"24":1,"26":1,"53":2,"55":1,"56":1,"58":1,"59":2,"64":4,"66":2,"73":8,"76":2,"81":1,"116":17,"122":6,"124":1,"125":1,"128":3,"129":2,"130":2,"131":1,"132":1,"135":1,"141":1,"145":1,"151":1,"153":1,"175":1,"177":1,"182":1,"184":1,"192":1,"193":2}}],["arbitrarily",{"2":{"151":1}}],["arbitrary",{"2":{"57":1,"148":1,"154":1,"191":1}}],["around",{"2":{"58":1,"59":2,"69":1,"84":1,"88":1,"146":1,"180":1,"198":1}}],["argmin",{"2":{"184":1}}],["arg",{"2":{"177":1}}],["argtypes",{"2":{"60":2,"173":1,"176":1}}],["args",{"2":{"13":2}}],["argumenterror",{"2":{"76":1,"146":1,"153":1,"154":1,"156":4}}],["argument",{"2":{"4":5,"6":10,"53":1,"56":2,"63":1,"66":1,"70":1,"72":1,"75":1,"85":2,"153":1,"156":1,"188":2,"192":1,"193":1}}],["arguments",{"2":{"1":1,"3":1,"6":5,"64":1,"94":2,"108":1,"125":1,"135":1,"172":1,"176":2,"177":1,"188":1}}],["arithmetic",{"2":{"11":1}}],["archgdal",{"2":{"23":1}}],["arc",{"2":{"6":1,"176":1}}],["array",{"2":{"4":1,"6":2,"56":1,"66":1,"146":5,"153":5,"154":2,"181":1,"184":1}}],["arrays",{"2":{"1":1,"6":1,"22":1,"146":1,"150":1,"154":2}}],["aren",{"2":{"4":3,"6":5,"69":1,"71":1,"73":2,"76":4,"88":3,"166":2,"169":2}}],["are",{"2":{"1":1,"3":4,"4":18,"5":2,"6":46,"9":2,"20":2,"22":3,"24":1,"25":1,"26":2,"27":1,"52":1,"53":8,"55":1,"56":2,"57":6,"59":11,"62":1,"63":3,"64":31,"66":6,"69":3,"70":3,"71":15,"72":2,"73":16,"75":5,"76":9,"81":3,"84":1,"85":1,"87":3,"88":26,"90":1,"94":7,"96":1,"97":3,"98":3,"99":1,"101":1,"107":1,"108":5,"110":1,"111":1,"112":1,"114":1,"115":1,"116":12,"122":7,"125":5,"127":1,"129":2,"134":1,"135":5,"137":1,"138":3,"139":3,"140":1,"142":1,"145":1,"146":14,"147":1,"150":1,"151":1,"153":6,"156":4,"157":1,"158":4,"159":1,"161":1,"162":1,"163":1,"165":1,"166":3,"167":1,"168":1,"169":2,"170":1,"175":1,"176":1,"177":1,"180":4,"182":1,"184":2,"186":1,"187":2,"188":1,"191":1,"194":2,"195":2,"196":1,"197":3,"198":6,"199":1,"200":1}}],["area2",{"2":{"63":4}}],["area1",{"2":{"63":4}}],["areas",{"2":{"4":2,"6":2,"56":3,"158":1,"183":2}}],["area",{"0":{"54":2,"55":2},"1":{"55":2,"56":2},"2":{"0":5,"4":15,"6":25,"11":6,"31":1,"54":2,"55":9,"56":61,"61":1,"62":3,"63":49,"65":2,"66":44,"75":2,"76":1,"122":1,"148":1,"154":1,"158":2,"166":2,"169":2,"180":2,"183":4,"192":1}}],["asked",{"2":{"153":1}}],["ask",{"2":{"23":1}}],["aspect",{"2":{"13":1,"14":1,"52":1,"55":1,"58":2,"62":1,"65":1,"84":2,"146":2,"175":1,"180":1}}],["assign",{"2":{"190":1}}],["assigned",{"2":{"64":1,"146":7,"198":1}}],["assets",{"2":{"192":1}}],["assetpath",{"2":{"192":1}}],["assemble",{"2":{"163":1}}],["assert",{"2":{"59":23,"63":1,"64":1,"69":1,"71":1,"73":1,"177":2,"189":1}}],["assume",{"2":{"122":2,"153":3,"154":1,"162":1,"169":1}}],["assumed",{"2":{"56":1,"88":1,"116":1}}],["assumes",{"2":{"6":3,"64":1,"85":1,"176":1,"177":2,"184":1}}],["associativity",{"2":{"19":1}}],["associated",{"0":{"1":1},"2":{"57":2}}],["as",{"2":{"1":8,"3":18,"4":6,"5":1,"6":74,"7":1,"11":2,"13":3,"14":3,"15":3,"17":2,"18":2,"20":1,"22":1,"23":2,"24":1,"25":1,"27":1,"52":2,"53":4,"55":2,"56":3,"57":4,"58":1,"59":4,"60":3,"62":2,"63":1,"64":15,"65":2,"66":5,"68":3,"69":5,"70":8,"71":2,"72":7,"73":8,"75":7,"76":5,"77":1,"79":2,"80":2,"81":3,"82":3,"84":3,"85":1,"87":2,"88":2,"90":2,"91":2,"93":3,"94":3,"103":2,"104":2,"105":2,"107":2,"108":3,"116":7,"118":2,"119":2,"121":2,"122":2,"124":2,"125":3,"127":1,"134":2,"135":3,"145":8,"146":3,"148":3,"150":2,"151":2,"152":2,"153":10,"154":4,"156":4,"158":4,"159":2,"160":2,"162":2,"163":2,"165":1,"166":1,"168":2,"170":1,"172":3,"174":2,"175":2,"176":3,"177":1,"178":2,"179":2,"180":7,"182":2,"185":3,"188":2,"189":2,"190":3,"191":2,"192":1,"193":1,"195":3,"196":3,"197":2,"198":3,"199":2}}],["alone",{"2":{"153":1}}],["along",{"2":{"4":4,"6":5,"64":3,"66":2,"73":4,"88":4,"116":1}}],["although",{"2":{"138":1,"139":1}}],["alternate",{"2":{"64":1}}],["alternative",{"2":{"64":1}}],["already",{"2":{"88":1,"116":2,"153":1,"182":1}}],["almost",{"2":{"73":1}}],["alg=nothing",{"2":{"180":1}}],["alg`",{"2":{"180":1}}],["alg",{"2":{"6":4,"32":2,"180":21,"181":3,"182":8,"183":3,"184":7,"188":9}}],["algorithms",{"2":{"6":3,"77":1,"81":1,"162":1,"177":1,"178":1,"180":4,"182":1,"184":1}}],["algorithm",{"0":{"181":1,"182":1,"183":1},"2":{"6":12,"58":1,"64":2,"66":1,"69":3,"70":1,"72":1,"75":1,"77":1,"82":4,"116":1,"178":1,"180":8,"181":1,"182":3,"183":1,"188":4}}],["allocating",{"2":{"82":1}}],["allocations",{"2":{"5":1,"6":1,"59":1}}],["allocate",{"2":{"82":1}}],["allow=",{"2":{"116":1}}],["allows",{"2":{"11":1,"18":1,"23":1,"24":1,"29":1,"94":2,"96":3,"97":3,"98":3,"99":1,"108":2,"110":3,"111":3,"112":2,"113":1,"125":1,"128":1,"129":1,"130":1,"135":3,"137":3,"138":3,"139":3,"140":1,"148":1,"178":1,"192":1,"194":1,"197":1}}],["allowed",{"2":{"6":1,"94":3,"108":3,"116":6,"125":5,"127":1,"128":2,"135":3,"180":1}}],["allow",{"2":{"1":1,"6":1,"23":1,"73":1,"94":7,"105":3,"108":7,"116":73,"125":10,"135":10,"177":1,"185":1}}],["all",{"2":{"1":3,"3":2,"4":3,"6":12,"9":1,"11":7,"22":1,"25":1,"27":1,"31":2,"53":5,"56":3,"59":1,"64":13,"66":3,"70":1,"71":3,"73":3,"76":5,"77":1,"80":3,"82":1,"84":1,"88":2,"90":1,"97":3,"98":3,"101":1,"114":1,"115":1,"116":7,"122":4,"134":1,"142":1,"145":1,"146":3,"147":1,"150":1,"151":2,"152":1,"153":7,"154":1,"156":4,"158":1,"163":1,"165":1,"167":1,"170":1,"171":1,"180":1,"185":2,"186":1,"187":1,"191":1,"193":1,"195":1,"197":1}}],["always",{"2":{"1":7,"4":6,"6":8,"25":1,"27":1,"55":1,"56":4,"66":2,"81":1,"84":1,"85":1,"88":1,"150":2,"153":3,"154":1,"172":4,"197":1}}],["also",{"2":{"1":2,"6":9,"23":1,"53":1,"56":2,"63":2,"64":3,"66":1,"70":1,"72":1,"73":2,"75":1,"77":1,"81":1,"82":1,"84":1,"85":2,"88":3,"122":1,"146":1,"150":1,"151":1,"153":1,"159":1,"163":1,"166":3,"169":2,"173":1,"174":1,"175":1,"176":1,"178":1,"185":1,"191":2,"194":1}}],["a",{"0":{"23":1,"192":1,"194":1},"2":{"1":13,"3":5,"4":79,"5":1,"6":187,"7":3,"9":1,"11":1,"15":2,"17":1,"18":6,"20":3,"22":3,"23":4,"24":2,"25":2,"27":2,"29":2,"30":2,"32":4,"35":3,"36":3,"37":3,"38":3,"40":2,"41":2,"42":2,"43":2,"44":2,"45":2,"46":2,"47":2,"48":2,"49":2,"52":3,"53":22,"55":11,"56":20,"57":13,"58":4,"59":11,"60":2,"62":5,"63":8,"64":281,"65":2,"66":17,"68":3,"69":4,"70":39,"71":30,"72":38,"73":91,"74":3,"75":36,"76":45,"77":3,"79":3,"80":2,"81":1,"82":4,"84":15,"85":27,"87":2,"88":54,"90":2,"93":1,"94":3,"96":5,"97":3,"98":2,"100":2,"101":2,"103":2,"105":1,"107":1,"108":3,"110":4,"111":3,"112":1,"114":2,"115":2,"116":52,"118":2,"121":5,"122":27,"124":2,"125":4,"127":4,"128":2,"129":2,"130":1,"131":2,"132":2,"134":1,"135":3,"137":5,"138":2,"139":2,"141":2,"142":2,"144":1,"145":4,"146":79,"148":4,"150":4,"151":4,"153":27,"154":6,"156":7,"158":11,"159":6,"160":6,"161":6,"162":2,"163":2,"164":5,"165":5,"166":9,"167":8,"169":6,"170":2,"171":3,"172":8,"173":3,"174":5,"175":4,"176":8,"177":10,"179":3,"180":5,"183":3,"184":1,"185":2,"186":1,"188":12,"189":2,"190":1,"191":17,"192":12,"193":5,"194":5,"195":11,"196":1,"197":5,"198":8,"199":3,"200":3}}],["apart",{"2":{"169":1}}],["april",{"0":{"7":1}}],["appears",{"2":{"162":1}}],["append",{"2":{"53":1,"64":5,"70":1,"71":2,"73":2,"75":1,"76":5,"169":2}}],["approximately",{"2":{"64":1,"175":1}}],["approach",{"2":{"19":1}}],["appropriately",{"2":{"198":1}}],["appropriate",{"2":{"6":1,"188":2}}],["applies",{"2":{"151":2}}],["applied",{"2":{"6":1,"20":1,"156":4,"164":2,"165":2,"166":1}}],["application",{"2":{"1":1,"6":2,"150":1,"154":1,"163":1,"165":3,"166":1,"169":2,"174":1,"177":1}}],["apply`",{"2":{"153":1}}],["applys",{"2":{"56":1,"66":1}}],["applyreduce",{"0":{"19":1,"154":1},"2":{"0":1,"1":1,"17":1,"19":2,"29":1,"31":2,"53":1,"56":1,"63":2,"66":1,"85":2,"148":1,"150":1,"154":42,"160":1}}],["apply",{"0":{"1":1,"18":1,"22":1,"29":1,"148":1,"151":1},"1":{"19":1,"149":1,"150":1,"151":1,"152":1,"153":1},"2":{"0":1,"1":4,"6":3,"17":1,"18":5,"20":1,"22":1,"24":1,"29":3,"31":3,"32":2,"148":5,"150":3,"151":4,"152":1,"153":55,"154":5,"155":1,"156":3,"158":1,"160":1,"165":4,"166":1,"170":1,"171":3,"172":1,"176":1,"177":1,"180":2,"185":3,"186":2}}],["apis",{"2":{"17":1}}],["api",{"0":{"0":1,"59":1},"1":{"1":1,"2":1,"3":1,"4":1,"5":1,"6":1},"2":{"0":1,"6":3,"24":1,"59":1,"176":1,"180":1,"188":1}}],["annotation",{"2":{"160":1}}],["annotated",{"2":{"26":1}}],["angular",{"2":{"158":1}}],["angels",{"2":{"53":1}}],["angle",{"2":{"53":35,"145":1}}],["angles",{"0":{"51":1,"52":1},"1":{"52":1,"53":1},"2":{"0":2,"4":10,"6":10,"31":1,"51":1,"52":4,"53":42}}],["answers",{"2":{"167":1}}],["answer",{"2":{"6":3,"70":1,"72":1,"73":1,"75":1}}],["another",{"2":{"3":1,"6":1,"64":2,"73":1,"82":1,"84":1,"90":1,"93":1,"96":1,"98":1,"99":1,"103":1,"107":1,"110":1,"111":1,"112":1,"113":1,"116":1,"118":1,"121":1,"122":1,"124":1,"127":1,"128":1,"129":1,"130":1,"134":1,"137":1,"138":1,"139":1,"140":1,"146":1,"172":1}}],["anonymous",{"2":{"1":1,"6":1,"185":1}}],["an",{"2":{"1":3,"4":11,"5":1,"6":27,"9":1,"13":1,"14":1,"18":1,"20":1,"23":2,"32":1,"52":1,"53":4,"55":1,"56":2,"58":1,"59":3,"60":2,"62":1,"64":15,"65":2,"66":2,"68":1,"69":1,"70":2,"71":5,"72":2,"73":9,"75":2,"76":2,"82":3,"84":2,"85":5,"87":1,"88":1,"90":1,"93":2,"96":2,"103":1,"107":1,"116":13,"118":1,"121":1,"122":1,"124":1,"134":1,"137":2,"138":1,"139":1,"146":3,"147":1,"150":1,"151":1,"152":1,"153":5,"154":2,"156":2,"158":1,"163":1,"170":1,"172":1,"176":1,"185":2,"188":3,"189":1,"191":2,"192":2,"194":1,"196":1,"198":1}}],["anything",{"2":{"58":1,"116":1}}],["any",{"2":{"1":3,"3":1,"4":2,"6":17,"18":1,"24":1,"25":1,"27":1,"29":1,"57":2,"63":1,"64":7,"66":1,"69":1,"71":1,"73":4,"76":1,"85":2,"95":2,"96":1,"99":1,"107":1,"109":2,"110":1,"111":2,"112":1,"113":2,"116":8,"122":2,"126":2,"136":2,"140":1,"146":2,"148":2,"150":1,"153":2,"154":2,"156":2,"163":1,"164":1,"165":3,"166":4,"169":4,"172":2,"174":1,"176":1,"177":1,"182":1,"189":1,"197":3,"198":2}}],["and",{"0":{"1":1,"20":1,"22":1,"24":1,"54":1,"71":1,"73":1,"76":1,"83":1,"191":1,"192":1,"194":1},"1":{"55":1,"56":1,"84":1,"85":1},"2":{"0":2,"1":8,"3":11,"4":18,"6":79,"7":1,"9":2,"17":4,"18":4,"20":2,"22":1,"23":2,"24":3,"25":2,"26":5,"27":2,"29":2,"30":1,"31":2,"32":2,"33":1,"53":13,"55":1,"56":7,"57":2,"58":2,"59":20,"60":1,"61":2,"62":1,"63":29,"64":71,"65":2,"66":18,"68":1,"69":4,"70":4,"71":16,"72":4,"73":38,"75":6,"76":15,"81":1,"82":2,"84":2,"85":13,"87":2,"88":13,"90":3,"91":2,"93":2,"94":3,"97":3,"98":4,"99":1,"103":2,"104":2,"105":1,"108":3,"111":3,"112":1,"113":1,"116":36,"118":1,"121":2,"122":3,"125":2,"129":2,"130":2,"134":3,"135":4,"137":2,"138":3,"139":3,"140":1,"145":2,"146":16,"148":6,"150":5,"151":2,"152":3,"153":16,"154":10,"156":2,"157":1,"158":14,"159":1,"160":3,"161":1,"162":2,"163":1,"164":1,"165":4,"166":2,"167":1,"170":2,"171":2,"172":2,"175":1,"176":5,"177":7,"178":2,"179":1,"180":8,"182":8,"183":1,"184":1,"185":1,"188":4,"190":3,"191":9,"192":7,"193":2,"195":5,"197":2,"198":4,"199":2}}],["snapped",{"2":{"146":1}}],["s3",{"2":{"64":3}}],["scratch",{"2":{"195":1}}],["sciences",{"2":{"192":1}}],["scalefactor",{"2":{"176":3}}],["scattered",{"2":{"198":1}}],["scatter",{"2":{"62":1,"84":2,"87":2,"90":2,"93":1,"103":1,"107":2,"121":2,"134":2,"198":1}}],["schema",{"2":{"153":5,"154":1}}],["scheme",{"2":{"63":1}}],["scenario",{"2":{"116":2}}],["scene",{"2":{"14":1}}],["skipmissing",{"2":{"146":1}}],["skipped",{"2":{"146":1}}],["skip",{"2":{"56":1,"116":14,"146":1,"153":1,"184":4}}],["skygering",{"2":{"7":1}}],["square",{"2":{"85":1,"181":1,"182":1}}],["squared",{"2":{"6":2,"66":2,"85":12,"181":2,"182":7}}],["sqrt",{"2":{"53":2,"63":1,"85":3}}],["sgn",{"2":{"53":5}}],["smallest",{"2":{"53":2,"73":1,"77":1}}],["sᵢ₋₁",{"2":{"59":25}}],["sᵢ₊₁",{"2":{"6":2,"59":41}}],["sᵢ",{"2":{"6":4,"59":46}}],["src",{"2":{"6":2}}],["syntax",{"2":{"194":1}}],["sym10100477",{"2":{"116":1}}],["symdifference",{"2":{"38":1,"147":1}}],["symmetric",{"0":{"38":1},"2":{"38":1}}],["symbol=",{"2":{"105":1}}],["symbol",{"2":{"6":1,"188":2}}],["system",{"0":{"192":1,"193":1},"2":{"1":2,"172":2,"190":2,"192":1}}],["switches",{"2":{"73":1,"76":1}}],["switch",{"2":{"64":1,"76":1,"160":1}}],["switching",{"2":{"6":1,"66":1}}],["swap",{"2":{"6":1,"76":1,"85":2,"171":1}}],["swapped",{"2":{"3":1,"6":1,"91":1,"94":1,"104":1,"105":1,"119":1,"122":1}}],["swapping",{"2":{"1":1,"150":1,"153":1}}],["s2",{"2":{"6":4,"59":6,"64":3}}],["s1",{"2":{"6":3,"59":6,"64":3}}],["saving",{"0":{"195":1}}],["saved",{"2":{"88":1}}],["save",{"2":{"53":1,"190":1,"195":3}}],["samples",{"2":{"13":1}}],["sample",{"2":{"13":1}}],["same",{"2":{"3":2,"4":12,"6":22,"18":1,"53":3,"56":1,"64":15,"66":1,"69":1,"70":3,"72":1,"73":1,"75":1,"76":1,"81":1,"84":1,"87":2,"88":30,"116":2,"121":1,"122":8,"151":1,"153":5,"156":2,"161":1,"166":2,"168":1,"169":2,"175":1,"191":1,"192":1,"195":1}}],["says",{"2":{"81":1}}],["say",{"2":{"6":1,"59":1,"176":1}}],["spliced",{"2":{"155":1}}],["split",{"2":{"69":1}}],["sp",{"2":{"116":2}}],["specify",{"2":{"156":2,"192":1,"193":1,"197":1}}],["specification",{"2":{"162":1,"165":1,"167":1,"168":1}}],["specifically",{"2":{"6":2,"59":3,"178":1,"197":1}}],["specific",{"2":{"64":1,"94":1,"108":1,"125":1,"135":1,"153":1,"154":1,"156":3,"158":1}}],["specified",{"2":{"3":1,"6":2,"64":1,"71":2,"73":2,"76":2,"122":1,"151":1,"180":1}}],["specialized",{"2":{"25":1,"27":1,"82":1,"153":1}}],["sphere",{"2":{"6":1,"158":3,"177":1}}],["sphericalgeodesics",{"2":{"158":1}}],["spherical",{"2":{"6":1,"31":2,"158":7,"177":1}}],["spatial",{"0":{"197":1},"1":{"198":1,"199":1,"200":1},"2":{"197":6,"198":3,"200":1}}],["spawn",{"2":{"153":3,"154":3}}],["span>",{"2":{"6":2}}],["span",{"2":{"6":2,"198":1}}],["space",{"2":{"6":1,"25":1,"27":1,"53":1,"55":1,"116":1,"158":5,"176":1}}],["slow",{"2":{"199":1}}],["slower",{"2":{"6":1,"188":2}}],["slope2",{"2":{"145":2}}],["slope1",{"2":{"145":2}}],["slidergrid",{"2":{"14":1}}],["sliders",{"2":{"14":3}}],["slightly",{"2":{"4":2,"6":2,"56":2}}],["suite",{"2":{"176":5,"180":13}}],["suggestion",{"2":{"173":1}}],["success",{"2":{"158":1}}],["such",{"2":{"4":1,"6":1,"55":1,"68":1,"77":1,"151":1,"170":1}}],["sun",{"2":{"116":1}}],["surrounds",{"2":{"116":1}}],["sure",{"2":{"9":1,"53":1,"64":2,"85":1,"122":1,"184":1}}],["suppose",{"2":{"199":1,"200":1}}],["supports",{"2":{"58":1,"59":1,"77":1,"153":3,"195":1,"200":2}}],["support",{"2":{"32":1,"60":1,"174":1,"200":1}}],["supported",{"2":{"23":1,"195":1}}],["supertype",{"2":{"6":1,"59":1}}],["sukumar",{"2":{"6":1,"59":1}}],["sum=1",{"2":{"59":1}}],["summary>",{"2":{"6":4}}],["sum",{"2":{"4":2,"6":3,"11":6,"55":1,"56":2,"57":2,"59":8,"66":1,"85":2,"145":5,"184":1,"189":4}}],["sublevel",{"2":{"199":1}}],["subsequent",{"2":{"191":1}}],["substituted",{"2":{"1":1,"150":1,"153":1}}],["subgeom1",{"2":{"156":2}}],["subgeom",{"2":{"156":3}}],["subject",{"2":{"64":1}}],["subtype",{"2":{"153":1}}],["subtypes",{"2":{"6":2,"59":2}}],["subtracted",{"2":{"145":1}}],["subtitle",{"2":{"13":1,"58":2,"176":1,"180":2}}],["sub",{"2":{"4":6,"6":7,"53":1,"56":3,"66":1,"71":6,"73":3,"76":4,"85":2,"100":2,"101":2,"114":2,"115":2,"131":2,"132":2,"141":2,"142":2,"153":3,"167":4,"168":2,"169":4}}],["series",{"2":{"191":1}}],["serve",{"2":{"6":1,"59":1}}],["searchsortedfirst",{"2":{"182":1}}],["semimajor",{"2":{"158":3}}],["seg2",{"2":{"145":2}}],["seg1",{"2":{"145":3}}],["seg",{"2":{"116":23,"122":9}}],["segmentation",{"2":{"175":1}}],["segments",{"2":{"23":1,"31":1,"52":2,"53":1,"62":1,"63":2,"64":3,"66":1,"73":6,"116":18,"176":1,"177":3}}],["segmentization",{"2":{"32":1,"158":1,"174":1}}],["segmentizing",{"2":{"6":3,"175":1,"176":2,"177":1}}],["segmentizemethod",{"2":{"176":3,"177":1}}],["segmentized",{"2":{"175":1}}],["segmentizes",{"2":{"174":1}}],["segmentize",{"0":{"32":1,"174":1},"1":{"175":1,"176":1,"177":1},"2":{"0":1,"6":4,"31":1,"32":9,"60":1,"148":1,"158":1,"174":1,"175":5,"176":11,"177":18,"196":1}}],["segment",{"2":{"4":4,"6":7,"53":1,"63":5,"64":6,"66":3,"68":1,"73":17,"85":2,"105":3,"116":43,"122":3,"145":2,"174":1,"176":2,"177":1}}],["seperate",{"2":{"73":1}}],["separates",{"2":{"116":1}}],["separate",{"2":{"64":1,"146":1,"153":1,"160":1,"195":1,"198":1}}],["separately",{"2":{"59":1,"146":1}}],["sense",{"2":{"56":1,"85":1}}],["several",{"2":{"20":2,"56":1,"161":1,"169":1}}],["select",{"2":{"159":1}}],["selected",{"2":{"146":1}}],["selectednode",{"2":{"146":3}}],["selection",{"2":{"14":1}}],["self",{"2":{"9":2}}],["section",{"2":{"26":1,"116":3,"121":1}}],["sections",{"2":{"10":1,"26":1}}],["seconds=1",{"2":{"176":3,"180":8}}],["secondisleft",{"2":{"146":4}}],["secondisstraight",{"2":{"146":7}}],["secondary",{"2":{"3":3,"6":3,"91":1,"94":1,"135":1}}],["second",{"2":{"3":8,"6":9,"64":1,"73":4,"88":1,"90":2,"91":1,"93":1,"94":1,"97":1,"98":1,"99":1,"104":2,"108":2,"116":2,"125":1,"134":1,"135":1,"138":1,"139":1,"140":2,"182":1,"193":1,"197":1}}],["seem",{"2":{"25":1,"27":1}}],["see",{"2":{"6":4,"7":1,"29":1,"62":1,"82":1,"85":1,"87":1,"90":1,"93":1,"107":1,"116":5,"118":1,"121":1,"124":1,"134":1,"146":2,"162":1,"163":1,"165":1,"166":3,"168":1,"169":2,"175":2,"182":1,"198":2}}],["setup=",{"2":{"94":1,"108":1,"125":1,"135":1}}],["sets",{"2":{"64":1,"81":1}}],["setting",{"2":{"23":1}}],["set",{"0":{"23":1,"34":1},"1":{"35":1,"36":1,"37":1,"38":1},"2":{"3":2,"4":7,"6":19,"23":1,"53":1,"57":2,"59":8,"64":3,"70":2,"72":2,"73":7,"75":2,"77":2,"87":2,"88":10,"94":1,"105":1,"108":1,"116":2,"122":1,"125":1,"135":1,"145":1,"146":1,"153":5,"166":2,"169":2,"182":2,"191":1,"195":1,"198":3}}],["sve",{"2":{"1":1,"6":1,"185":1}}],["svector",{"2":{"1":14,"6":14,"64":5,"70":1,"73":2,"76":6,"116":1,"146":1,"185":13,"191":6,"193":4}}],["solution",{"2":{"116":1}}],["south",{"2":{"66":7,"158":1}}],["source",{"2":{"1":10,"3":19,"4":22,"5":3,"6":84,"26":4,"105":2,"150":2,"156":4,"165":1,"166":4,"172":3,"192":13,"193":1}}],["sort",{"2":{"64":4,"69":2,"73":2,"75":2,"116":1,"122":2,"146":1,"182":1}}],["sorted",{"2":{"20":1,"64":1,"182":7,"197":1}}],["someone",{"2":{"82":1,"188":1}}],["something",{"0":{"74":1},"2":{"10":1}}],["some",{"2":{"3":1,"6":4,"9":1,"17":1,"59":4,"88":1,"116":3,"122":1,"146":1,"147":1,"148":1,"151":2,"153":2,"154":1,"164":1,"165":2,"166":1,"177":1,"184":1,"187":2,"190":2,"197":1,"200":1}}],["so",{"2":{"1":1,"4":4,"6":9,"9":1,"17":1,"19":1,"25":1,"27":1,"31":1,"58":1,"59":1,"64":1,"65":1,"73":1,"75":2,"76":2,"84":1,"85":1,"87":1,"88":4,"90":1,"107":1,"116":2,"118":1,"134":1,"146":3,"153":8,"156":2,"174":1,"175":1,"176":3,"177":1,"184":1,"185":1,"188":2,"192":1,"193":1}}],["styles",{"2":{"153":1}}],["style",{"2":{"153":7}}],["step",{"2":{"64":7,"70":1,"71":3,"72":1,"73":3,"75":1,"76":3,"146":6,"189":1}}],["storing",{"2":{"195":1}}],["stored",{"2":{"64":2,"198":1}}],["stores",{"2":{"64":1}}],["store",{"2":{"59":1,"195":1}}],["stopping",{"2":{"182":2}}],["stops",{"2":{"151":1}}],["stop",{"2":{"18":2,"105":3,"116":4,"122":3}}],["stay",{"2":{"76":1}}],["stackoverflow",{"2":{"73":1}}],["stack",{"2":{"69":1}}],["states",{"2":{"180":1}}],["state",{"2":{"75":1,"199":9}}],["status",{"2":{"64":31,"71":4,"73":5,"76":4,"146":1}}],["static",{"2":{"160":1}}],["staticarray",{"2":{"59":1}}],["staticarrays",{"2":{"31":1,"64":5,"70":1,"73":2,"76":6,"116":1,"146":1,"185":2}}],["staticarraysco",{"2":{"1":1,"6":1,"185":1}}],["staticarrayscore",{"2":{"1":10,"6":10,"59":1,"185":10,"191":6,"193":4}}],["statica",{"2":{"1":1,"6":1,"185":1}}],["statistics",{"2":{"13":2,"31":1}}],["stability",{"2":{"23":1,"30":1}}],["stable",{"2":{"13":1,"24":1,"177":1}}],["stage",{"2":{"7":1}}],["standardized",{"2":{"116":1}}],["standards",{"2":{"116":1}}],["standard",{"2":{"6":1,"82":1,"158":1}}],["started",{"2":{"64":1}}],["starting",{"2":{"63":2,"64":1,"66":1,"73":1,"169":4}}],["startvalue",{"2":{"14":4}}],["start",{"2":{"6":1,"18":1,"53":7,"59":1,"64":76,"66":17,"71":4,"73":4,"76":5,"88":1,"105":4,"116":44,"122":3,"137":1,"146":2,"161":1,"182":16,"191":1}}],["straightline",{"2":{"146":3}}],["straight",{"2":{"146":6,"154":1}}],["strait",{"2":{"146":1}}],["structs",{"2":{"20":1,"165":1}}],["structures",{"2":{"148":1}}],["structure",{"2":{"6":1,"148":1,"156":2,"171":1}}],["struct",{"2":{"6":2,"59":2,"64":2,"82":1,"158":3,"159":3,"160":6,"163":1,"169":2,"176":2,"181":1,"182":1,"183":1,"188":4}}],["strings",{"2":{"6":1,"63":1,"73":1}}],["string",{"2":{"1":2,"14":1,"62":1,"63":1,"172":2}}],["still",{"2":{"0":1,"56":1,"63":1,"64":3,"93":1,"182":1}}],["shp",{"2":{"195":1}}],["ships",{"2":{"192":1}}],["shifting",{"2":{"193":1}}],["shift",{"2":{"191":3}}],["shewchuck",{"2":{"7":1}}],["short",{"2":{"153":1}}],["shorthand",{"2":{"82":1}}],["show",{"2":{"9":1,"11":1,"13":1,"14":1,"58":1,"156":3,"192":1,"195":1,"197":2}}],["shoelace",{"2":{"4":1,"6":1,"56":2,"66":1}}],["shouldn",{"2":{"73":1}}],["should",{"2":{"1":1,"4":1,"6":12,"17":1,"18":1,"20":2,"25":1,"27":1,"32":1,"53":3,"56":1,"63":1,"64":3,"65":1,"88":1,"116":4,"146":2,"150":1,"153":2,"156":2,"158":1,"160":1,"162":1,"165":4,"166":2,"177":2,"180":2}}],["sharing",{"2":{"107":1}}],["shares",{"2":{"73":1}}],["share",{"2":{"4":7,"6":8,"87":2,"88":8,"111":2,"112":1,"113":1,"122":1,"130":1}}],["shared",{"0":{"184":1},"2":{"3":1,"6":1,"64":2,"73":2,"122":3}}],["shapes",{"2":{"85":1,"195":7}}],["shape",{"2":{"62":1,"71":1,"73":1,"87":1,"158":1,"168":1,"191":1}}],["shaped",{"2":{"58":1}}],["shapefiles",{"2":{"195":1}}],["shapefile",{"2":{"29":1,"195":4}}],["shallower",{"2":{"1":1,"150":1,"153":1}}],["sites",{"2":{"199":1}}],["sides",{"2":{"64":3,"162":1}}],["side",{"2":{"53":4,"64":26}}],["signals",{"2":{"152":1}}],["sign",{"2":{"13":2,"14":2,"53":7,"56":2,"145":3}}],["signed",{"0":{"54":1,"55":1,"83":1,"84":1},"1":{"55":1,"56":1,"84":1,"85":1},"2":{"0":4,"4":15,"6":18,"11":3,"54":1,"55":5,"56":23,"66":3,"83":1,"84":7,"85":19}}],["six",{"2":{"6":1,"180":1}}],["size=",{"2":{"192":1}}],["sizehint",{"2":{"64":2,"71":1,"177":1}}],["size",{"2":{"6":4,"13":2,"14":5,"58":1,"76":1,"146":2,"153":2,"154":2,"180":1}}],["sin",{"2":{"191":3,"192":1,"193":2}}],["singed",{"2":{"85":1}}],["singular",{"2":{"73":1}}],["singlepoly",{"2":{"180":6}}],["single",{"2":{"4":6,"6":10,"23":1,"53":2,"56":1,"66":1,"88":5,"121":2,"146":2,"154":1,"166":2,"167":1,"169":2,"191":3,"195":1}}],["since",{"2":{"1":1,"6":2,"26":1,"53":1,"56":1,"63":1,"66":2,"75":1,"85":1,"88":1,"116":1,"122":2,"129":1,"153":1,"160":1,"172":1,"188":2,"199":1}}],["simulation",{"2":{"23":1}}],["simultaneously",{"2":{"20":1}}],["simply",{"2":{"7":1,"55":1,"60":1,"64":1,"73":1,"76":1,"91":1,"104":1,"119":1,"146":1,"164":1,"172":1,"173":1,"176":1,"191":1,"194":1}}],["simpler",{"2":{"6":1,"146":1}}],["simple",{"0":{"33":1,"79":1,"198":1},"1":{"34":1,"35":1,"36":1,"37":1,"38":1,"39":1,"40":1,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1,"47":1,"48":1,"49":1,"50":1},"2":{"6":3,"33":1,"59":1,"82":1,"148":3,"158":1,"160":1,"171":1,"172":1,"179":2,"180":2}}],["simplifier",{"2":{"180":4}}],["simplified",{"2":{"23":1,"179":1}}],["simplifies",{"2":{"6":3,"181":1,"182":1,"183":1}}],["simplification",{"0":{"178":1},"1":{"179":1,"180":1},"2":{"6":2,"178":2,"180":3}}],["simplifying",{"2":{"178":1}}],["simplify",{"0":{"181":1,"182":1,"183":1},"2":{"0":1,"6":15,"9":1,"31":1,"64":1,"148":1,"179":1,"180":41,"181":1,"182":2,"183":1}}],["simplifyalgs",{"2":{"184":1}}],["simplifyalg",{"2":{"0":1,"6":8,"180":5,"181":2,"182":2,"183":2}}],["similarly",{"2":{"148":1}}],["similar",{"2":{"1":1,"6":3,"18":1,"25":1,"27":1,"29":1,"60":1,"77":1,"82":1,"146":2,"150":1,"153":1,"176":2,"177":1,"186":1}}],["s",{"0":{"30":1},"2":{"0":1,"3":1,"6":11,"7":1,"9":1,"18":1,"19":1,"29":1,"53":1,"55":1,"56":4,"57":1,"58":2,"59":5,"62":3,"63":3,"64":3,"66":1,"68":1,"71":1,"73":2,"76":3,"81":1,"85":1,"88":1,"103":2,"110":3,"111":4,"112":2,"116":9,"122":1,"124":1,"125":1,"130":1,"146":5,"148":2,"153":2,"154":1,"159":2,"160":2,"163":1,"166":4,"167":1,"169":3,"175":4,"176":1,"177":2,"178":1,"180":2,"184":4,"188":6,"191":6,"192":4,"193":4,"194":2,"195":5,"200":2}}],["fn",{"2":{"195":8}}],["f``",{"2":{"158":1}}],["fc",{"2":{"153":4,"154":5,"156":13,"180":3,"189":14}}],["fj",{"2":{"146":2}}],["f2",{"2":{"64":2}}],["f1",{"2":{"64":2}}],["f64",{"2":{"13":2,"14":2}}],["few",{"2":{"162":1}}],["fetched",{"2":{"146":1}}],["fetch",{"2":{"32":2,"153":1,"154":1}}],["feb",{"0":{"8":1},"1":{"9":1,"10":1}}],["featurecollection",{"2":{"6":2,"11":1,"18":1,"146":4,"153":6,"156":1,"192":2}}],["featurecollectiontrait",{"2":{"1":1,"150":1,"151":1,"153":3,"154":2,"156":10,"189":5}}],["features",{"0":{"95":1,"109":1,"126":1,"136":1},"2":{"1":1,"6":2,"11":1,"22":1,"82":1,"146":3,"150":1,"152":1,"153":11,"154":6,"156":2,"192":1}}],["featuretrait",{"2":{"1":2,"95":4,"105":2,"109":4,"126":4,"136":4,"150":2,"151":3,"153":3,"154":3,"156":10,"189":5}}],["feature",{"2":{"1":6,"4":1,"6":4,"18":2,"22":1,"56":1,"66":1,"146":2,"148":1,"150":6,"152":1,"153":22,"154":6,"156":22,"180":2,"189":2,"195":1}}],["fra",{"2":{"199":2}}],["frame",{"2":{"194":1}}],["framework",{"2":{"148":4,"154":2}}],["fracs",{"2":{"64":20,"69":1}}],["frac",{"2":{"59":1,"64":2,"73":17,"180":2}}],["fractional",{"2":{"64":1,"73":3}}],["fractions",{"2":{"64":1,"73":4}}],["fraction",{"2":{"6":4,"73":2,"85":1,"180":1}}],["front",{"2":{"53":1}}],["from",{"2":{"1":3,"3":4,"4":16,"6":26,"7":1,"11":1,"20":1,"22":1,"26":1,"31":1,"56":2,"58":1,"59":4,"60":1,"63":1,"64":6,"66":11,"69":1,"70":1,"71":3,"72":1,"73":11,"75":1,"76":6,"82":1,"85":25,"108":2,"110":5,"111":4,"112":2,"113":1,"114":2,"116":6,"122":2,"145":2,"146":7,"153":1,"156":6,"158":1,"160":1,"169":1,"172":4,"174":1,"180":1,"181":1,"182":3,"183":1,"189":1,"190":1,"192":5,"195":1,"197":1,"198":1,"200":1}}],["footprint",{"2":{"193":1}}],["foldable",{"2":{"153":2,"154":1}}],["follows",{"2":{"64":2,"94":1,"108":1,"125":1,"135":1,"146":1}}],["followed",{"2":{"26":1}}],["following",{"2":{"6":1,"59":1,"68":1,"156":1,"162":1,"168":1,"180":1,"197":1}}],["focusing",{"2":{"25":1,"27":1}}],["foundational",{"2":{"17":1}}],["found",{"2":{"6":7,"26":1,"66":1,"69":1,"70":2,"72":2,"73":1,"75":2,"88":1,"146":5,"151":3,"153":2,"154":1,"156":5,"177":2}}],["forward",{"2":{"73":1}}],["forwards",{"2":{"64":1,"71":1,"76":1}}],["formats",{"2":{"190":1,"193":1,"195":3}}],["format",{"2":{"69":1,"195":3}}],["form",{"2":{"18":1,"64":3,"73":8,"76":3,"153":1}}],["formed",{"2":{"4":2,"6":3,"52":1,"53":4,"59":1,"64":8,"75":1,"76":1,"182":1}}],["formulae",{"2":{"6":1,"177":1}}],["formula",{"2":{"4":1,"6":1,"56":2,"66":1}}],["force",{"2":{"1":1,"153":3,"172":1,"180":1}}],["for",{"0":{"71":1,"73":1,"76":1},"2":{"0":2,"1":3,"3":1,"4":4,"5":3,"6":46,"7":4,"9":2,"13":5,"14":1,"18":1,"20":1,"22":1,"23":5,"25":3,"26":1,"27":3,"29":1,"31":2,"32":1,"33":1,"53":5,"56":8,"57":1,"58":1,"59":19,"60":3,"63":7,"64":35,"66":11,"69":5,"70":2,"71":3,"72":1,"73":7,"75":1,"76":9,"77":2,"80":1,"82":3,"84":2,"85":7,"88":13,"94":3,"100":1,"101":1,"103":1,"105":6,"108":2,"114":1,"115":1,"116":22,"121":1,"122":11,"125":2,"131":1,"132":1,"135":3,"141":1,"142":1,"144":1,"145":5,"146":13,"147":1,"148":3,"150":1,"153":7,"154":1,"156":3,"158":1,"159":1,"160":2,"161":3,"162":2,"164":1,"165":5,"167":3,"168":1,"169":5,"170":1,"172":3,"174":4,"175":7,"176":6,"177":10,"178":4,"180":12,"181":2,"182":2,"183":1,"184":6,"188":5,"189":7,"190":1,"192":4,"193":1,"195":3,"197":2,"199":2,"200":1}}],["fi",{"2":{"146":2}}],["fine",{"2":{"175":1}}],["final",{"2":{"76":1,"165":4,"176":1}}],["finally",{"2":{"58":2,"73":1,"146":1,"153":3,"154":1,"195":1}}],["findmin",{"2":{"184":1}}],["findmax",{"2":{"180":1,"182":1}}],["findall",{"2":{"165":1}}],["finding",{"2":{"73":1}}],["findfirst",{"2":{"64":6,"69":2,"80":1,"180":1}}],["findnext",{"2":{"64":3}}],["findlast",{"2":{"64":2}}],["findprev",{"2":{"64":2}}],["finds",{"2":{"64":1,"66":1}}],["find",{"2":{"53":6,"56":1,"64":12,"66":3,"69":2,"70":4,"72":2,"73":8,"75":1,"85":3,"88":1,"116":7,"146":2,"182":5,"199":1}}],["finish",{"2":{"9":1}}],["fill",{"2":{"64":1,"146":1,"177":3}}],["filled",{"2":{"64":5,"66":3,"84":1,"85":2,"116":16}}],["files",{"2":{"187":1,"195":2}}],["file",{"2":{"26":1,"32":1,"33":1,"64":1,"94":3,"108":3,"125":3,"135":3,"146":1,"147":1,"148":1,"154":1,"155":1,"156":1,"164":1,"172":1,"177":1,"178":1,"187":2,"190":1,"195":2}}],["filters",{"2":{"64":1}}],["filtering",{"2":{"6":1,"180":1}}],["filter",{"2":{"4":1,"6":2,"64":2,"153":2,"154":1,"169":2,"170":1,"180":1}}],["fit",{"2":{"17":1}}],["field",{"2":{"13":2,"64":2}}],["figure",{"2":{"13":1,"14":1,"55":1,"58":2,"81":1,"192":3}}],["fig",{"2":{"13":6,"14":6,"81":5,"191":7,"192":4,"196":2}}],["fix1",{"2":{"153":1,"154":1}}],["fixme",{"2":{"145":1}}],["fix2",{"2":{"32":1,"64":2}}],["fixed",{"2":{"6":3,"70":1,"72":1,"75":1}}],["fix",{"0":{"20":1},"2":{"6":9,"9":3,"15":3,"17":1,"20":1,"70":1,"71":11,"72":1,"73":11,"75":1,"76":10,"81":2,"162":1,"164":1,"165":1,"168":1}}],["firstisright",{"2":{"146":2}}],["firstisleft",{"2":{"146":4}}],["firstisstraight",{"2":{"146":4}}],["firstnode",{"2":{"146":9}}],["first",{"2":{"3":9,"6":10,"53":14,"56":7,"58":1,"59":9,"60":1,"63":1,"64":17,"66":2,"69":1,"70":2,"72":1,"73":5,"75":3,"85":8,"88":4,"90":2,"91":2,"93":1,"94":2,"97":1,"98":1,"99":1,"104":2,"108":3,"116":22,"122":1,"125":2,"127":1,"134":1,"135":2,"138":1,"139":1,"140":2,"145":1,"146":16,"153":3,"154":2,"156":6,"162":1,"177":3,"181":1,"182":2,"189":2,"190":1,"191":1,"192":1,"193":1,"197":1,"198":1}}],["flexijoins",{"2":{"197":2,"198":3,"199":1,"200":5}}],["flexible",{"2":{"153":1,"154":1}}],["flags",{"2":{"64":5}}],["flag",{"2":{"64":12,"69":1}}],["flattened",{"2":{"18":1}}],["flatten",{"0":{"156":1},"2":{"11":3,"13":1,"18":1,"31":2,"50":1,"59":1,"64":2,"66":3,"72":1,"76":1,"82":2,"105":1,"156":33,"169":1}}],["flattening`",{"2":{"158":1}}],["flattening",{"2":{"6":4,"154":1,"158":3,"176":2}}],["flat",{"2":{"6":2,"177":1,"184":2,"189":1}}],["floating",{"2":{"6":1,"64":1,"73":5,"146":2}}],["float",{"2":{"6":3,"70":1,"72":1,"75":1}}],["float64x2",{"2":{"13":6,"14":6,"15":2}}],["float64",{"2":{"1":6,"4":10,"6":30,"13":1,"52":1,"53":3,"56":6,"63":1,"66":4,"69":2,"70":2,"72":1,"73":3,"75":1,"81":1,"85":12,"116":4,"122":2,"145":4,"162":6,"168":26,"175":2,"176":2,"177":1,"181":4,"182":9,"183":2,"184":6,"185":6,"186":1,"189":8,"191":190,"192":7,"193":10}}],["flipping",{"0":{"171":1},"2":{"171":1}}],["flipped",{"2":{"1":2,"150":2,"153":2}}],["flipaxis",{"2":{"58":1}}],["flip",{"2":{"0":1,"6":1,"31":1,"148":2,"153":1,"171":2}}],["fancy",{"2":{"191":1}}],["fancis",{"2":{"6":1,"59":1}}],["fair",{"2":{"175":1}}],["fail",{"2":{"1":1,"6":1,"150":1,"153":2,"154":1,"156":3,"176":1}}],["fallback",{"2":{"153":1}}],["falses",{"2":{"64":2,"69":1,"70":1,"72":1}}],["false",{"0":{"24":1},"2":{"1":29,"3":7,"4":3,"6":51,"31":1,"32":2,"35":1,"36":1,"37":1,"38":1,"50":1,"53":2,"56":1,"58":5,"64":26,"66":5,"69":1,"70":1,"71":3,"73":1,"75":4,"85":2,"87":2,"88":27,"90":2,"94":8,"96":2,"97":4,"98":1,"99":1,"100":1,"101":1,"103":2,"105":11,"108":9,"110":1,"111":4,"114":1,"115":1,"116":39,"122":22,"125":8,"127":3,"128":4,"129":1,"131":1,"132":1,"134":1,"135":7,"137":2,"138":4,"139":1,"140":1,"141":1,"142":1,"145":10,"146":5,"150":4,"153":11,"154":7,"158":1,"160":3,"162":16,"168":44,"169":4,"177":5,"180":1,"184":2,"185":20,"189":2,"191":242,"192":12,"193":18,"197":1}}],["fashion",{"2":{"55":1}}],["faster",{"2":{"59":1,"153":1}}],["fast",{"2":{"12":1}}],["f",{"2":{"1":12,"6":14,"15":1,"18":3,"19":1,"22":2,"52":1,"55":2,"58":4,"60":1,"62":2,"64":19,"65":2,"68":2,"70":2,"71":2,"72":2,"73":2,"75":2,"76":2,"79":2,"80":2,"84":5,"87":2,"90":2,"93":2,"103":2,"107":2,"116":7,"118":2,"121":2,"124":2,"134":2,"146":30,"150":7,"151":2,"153":61,"154":64,"156":52,"173":1,"175":2,"176":1,"179":2,"180":1,"184":6,"185":6,"188":5,"189":16,"191":6,"193":2,"198":3}}],["fulfilled",{"2":{"182":1}}],["fully",{"2":{"6":1,"64":4,"69":1,"122":4}}],["full",{"0":{"0":1},"1":{"1":1,"2":1,"3":1,"4":1,"5":1,"6":1},"2":{"0":1,"66":5,"165":1,"199":4}}],["furthest",{"2":{"146":1}}],["further",{"2":{"62":1,"153":1}}],["furthermore",{"2":{"3":2,"6":5,"64":1,"70":1,"72":1,"75":1,"94":1,"135":1}}],["fun",{"2":{"191":1}}],["fundamental",{"2":{"26":1,"187":1}}],["func",{"2":{"13":5}}],["funcs",{"2":{"13":2,"14":3}}],["functionality",{"2":{"73":1,"171":1,"172":1,"177":1,"188":1}}],["functionalities",{"2":{"64":1}}],["functionally",{"2":{"1":1,"18":1,"29":1,"150":1,"153":1}}],["function",{"2":{"1":4,"4":1,"6":10,"7":2,"9":1,"13":3,"14":2,"18":2,"29":1,"31":1,"32":2,"53":5,"56":3,"59":13,"63":6,"64":18,"66":7,"68":2,"69":4,"70":3,"71":3,"72":2,"73":7,"74":1,"75":2,"76":4,"82":2,"85":6,"87":1,"88":8,"90":1,"93":1,"94":1,"100":1,"101":1,"103":1,"105":5,"107":1,"108":1,"114":1,"115":1,"116":9,"118":1,"121":1,"122":7,"124":1,"125":1,"131":1,"132":1,"134":1,"135":1,"141":1,"142":1,"145":4,"146":16,"147":3,"148":1,"150":2,"151":2,"153":15,"154":11,"156":5,"160":1,"163":2,"165":7,"166":2,"169":2,"171":2,"172":1,"174":2,"176":2,"177":7,"180":3,"181":2,"182":3,"183":2,"184":7,"185":3,"186":1,"188":6,"189":12,"191":1,"197":4,"200":4}}],["functions",{"0":{"1":1,"71":1,"73":1,"76":1,"150":1,"189":1},"2":{"6":1,"9":3,"17":1,"26":1,"29":1,"33":1,"56":1,"59":1,"63":1,"64":2,"66":1,"68":1,"116":1,"147":1,"148":1,"151":1,"165":2,"166":1}}],["future",{"2":{"23":1,"77":1,"85":1,"174":2}}],["wgs84",{"2":{"192":1}}],["wglmakie",{"2":{"14":1}}],["wₜₒₜ",{"2":{"59":8}}],["wᵢ",{"2":{"59":18}}],["wt",{"2":{"59":3}}],["w",{"2":{"13":13,"14":7,"85":4,"146":1}}],["wrong",{"2":{"167":1,"184":1}}],["writing",{"2":{"195":1}}],["written",{"2":{"88":1}}],["writes",{"2":{"153":1}}],["write",{"2":{"7":1,"30":1,"146":1,"153":1,"195":7}}],["wrap",{"2":{"4":1,"6":1,"32":3,"35":1,"36":1,"37":1,"38":1,"50":1,"170":1,"176":1}}],["wrapped",{"2":{"22":1,"146":2,"151":1,"153":1}}],["wrapper",{"0":{"30":1},"2":{"30":1,"53":1,"56":1,"63":1,"66":1,"85":1,"88":1,"94":1,"108":1,"122":1,"125":1,"135":1}}],["wrappers`",{"2":{"156":1}}],["wrappers",{"2":{"1":10,"6":11,"22":1,"145":2,"146":1,"156":1,"162":8,"163":1,"168":22,"175":1,"176":1,"180":1,"185":10,"189":1,"191":121,"192":5,"193":9}}],["wrappergeometry`",{"2":{"172":1}}],["wrappergeometry",{"2":{"1":1}}],["wrapping",{"2":{"1":1,"6":1,"88":1,"172":1,"186":1}}],["web",{"2":{"195":1}}],["west",{"2":{"66":8}}],["were",{"2":{"64":1,"146":1,"198":1}}],["welcome",{"2":{"25":1,"27":1}}],["well",{"2":{"17":1,"64":1,"66":1,"82":1,"160":1,"174":1,"177":1,"178":1}}],["we",{"0":{"74":2},"2":{"7":1,"11":1,"13":1,"17":3,"19":1,"23":3,"24":1,"25":2,"26":1,"27":2,"32":1,"53":1,"55":2,"56":2,"58":4,"59":8,"60":2,"63":1,"64":7,"66":4,"71":8,"72":2,"73":3,"76":1,"77":1,"81":3,"82":2,"85":1,"87":1,"88":5,"90":1,"91":1,"93":1,"94":1,"104":1,"107":1,"108":1,"116":2,"118":3,"119":1,"121":1,"122":2,"124":1,"125":1,"134":1,"135":1,"146":21,"147":1,"151":1,"153":19,"154":6,"156":3,"158":6,"160":4,"161":1,"173":1,"174":2,"175":2,"176":3,"177":1,"180":5,"184":2,"187":1,"188":1,"190":2,"191":4,"192":6,"193":4,"194":1,"195":3,"197":3,"198":7}}],["weighting",{"2":{"62":2,"63":1}}],["weights",{"2":{"57":4}}],["weight",{"2":{"6":5,"59":14,"63":1}}],["weighted",{"2":{"0":1,"6":2,"57":3,"59":4,"63":4}}],["walk",{"2":{"69":1}}],["wall2",{"2":{"66":7}}],["wall1",{"2":{"66":12}}],["walls",{"2":{"66":3}}],["wall",{"2":{"66":69}}],["wachspress",{"2":{"59":1}}],["wasincreasing",{"2":{"146":10}}],["wasn",{"2":{"64":1}}],["was",{"2":{"31":1,"32":1,"50":1,"53":1,"56":1,"58":1,"59":4,"60":1,"63":1,"64":1,"66":1,"69":1,"70":1,"71":3,"72":1,"73":2,"74":1,"75":1,"76":3,"82":1,"85":1,"88":1,"91":1,"101":1,"104":1,"105":1,"115":1,"116":1,"119":1,"122":1,"132":1,"142":1,"145":1,"146":1,"147":1,"151":1,"153":1,"154":1,"155":1,"156":1,"160":1,"163":1,"166":1,"169":1,"170":1,"171":1,"173":1,"175":1,"177":1,"184":1,"185":1,"186":1,"188":2,"189":1}}],["wanted",{"2":{"200":1}}],["wants",{"2":{"82":1,"154":1}}],["want",{"0":{"23":1,"74":1},"2":{"13":1,"18":1,"23":1,"59":1,"64":1,"145":1,"192":2,"194":1,"199":1,"200":1}}],["ways",{"2":{"167":1}}],["way",{"2":{"6":1,"17":1,"18":1,"24":1,"29":1,"57":1,"148":1,"159":1,"164":1,"177":1,"188":2,"194":1}}],["warn",{"2":{"146":2,"177":1}}],["warned",{"2":{"6":1,"188":1}}],["warning",{"2":{"0":1,"5":1,"6":3,"24":1,"59":1,"82":1,"154":1,"176":1,"187":1,"199":1}}],["wong",{"2":{"79":1,"80":1,"196":1}}],["won",{"2":{"23":1,"64":1,"154":1}}],["wouldn",{"2":{"73":1}}],["would",{"0":{"74":1},"2":{"6":3,"23":1,"70":1,"72":1,"73":1,"75":1,"82":4,"146":1,"153":1,"160":2,"192":1,"199":1,"200":1}}],["wound",{"2":{"6":1,"82":1}}],["world",{"0":{"199":1},"2":{"197":1}}],["worrying",{"2":{"24":1}}],["words",{"2":{"3":1,"6":1,"90":1,"118":1,"124":1,"125":1}}],["workflow",{"2":{"105":1,"122":1}}],["workflows",{"2":{"23":1,"25":1,"27":1}}],["works",{"2":{"26":1,"172":1,"174":1}}],["working",{"2":{"3":1,"6":1,"105":1,"192":1}}],["work",{"2":{"1":3,"6":2,"9":1,"53":1,"56":2,"63":1,"66":2,"69":2,"77":1,"85":1,"88":1,"116":1,"122":1,"145":1,"150":1,"153":1,"154":1,"172":1,"185":1,"199":1}}],["whole",{"2":{"116":1}}],["whose",{"2":{"3":1,"6":1,"105":1}}],["white",{"2":{"58":1}}],["while",{"2":{"53":1,"62":1,"63":1,"64":5,"73":2,"81":2,"84":1,"88":1,"105":1,"116":1,"146":3,"169":1,"182":1,"184":2,"198":1}}],["whichever",{"2":{"6":1,"180":1}}],["which",{"2":{"1":1,"4":2,"5":1,"6":7,"7":1,"11":1,"13":1,"14":1,"17":1,"18":4,"20":1,"23":2,"33":1,"55":1,"56":1,"57":1,"58":2,"59":5,"60":1,"64":11,"66":2,"73":2,"77":1,"82":1,"88":2,"116":3,"146":3,"147":1,"148":1,"153":1,"158":2,"159":1,"161":2,"162":1,"165":1,"167":1,"168":1,"173":1,"174":1,"175":1,"176":3,"177":2,"180":1,"182":2,"185":1,"188":4,"195":3,"197":3,"198":6,"200":1}}],["what",{"0":{"22":1,"25":1,"30":1,"52":1,"55":2,"62":1,"65":1,"68":1,"84":2,"87":1,"90":1,"93":1,"103":1,"107":1,"118":1,"121":1,"124":1,"134":1,"151":1},"2":{"13":1,"14":1,"24":1,"62":1,"145":1,"146":1,"160":1,"175":1,"192":1,"199":1}}],["whatever",{"2":{"1":1,"22":1,"150":1,"153":1}}],["whyatt",{"2":{"178":1}}],["why",{"0":{"22":1,"23":1},"2":{"9":1,"17":1,"30":1,"56":1}}],["wheel",{"2":{"17":1}}],["whether",{"2":{"1":4,"4":1,"6":8,"144":1,"145":1,"150":2,"155":2,"160":1,"161":1}}],["when",{"2":{"1":1,"4":2,"5":1,"6":4,"18":1,"20":1,"23":2,"24":1,"56":1,"59":3,"63":1,"64":2,"71":5,"73":6,"76":7,"116":1,"146":1,"150":1,"151":3,"153":1,"162":1,"165":1,"170":2,"180":1,"192":1,"193":2,"195":1,"200":1}}],["whereas",{"2":{"158":1}}],["wherever",{"2":{"6":1,"186":1}}],["where",{"2":{"1":2,"4":5,"6":10,"20":1,"31":2,"53":8,"56":12,"57":2,"59":24,"63":8,"64":14,"66":8,"69":5,"70":2,"71":5,"72":3,"73":18,"75":2,"76":9,"85":25,"88":1,"116":9,"146":3,"150":1,"153":18,"154":18,"156":17,"158":2,"159":5,"167":1,"172":1,"175":1,"186":1,"187":1,"189":3,"191":4}}],["widely",{"2":{"195":1}}],["widths",{"2":{"14":1}}],["wiki",{"2":{"116":1,"182":1}}],["wikipedia",{"2":{"116":1,"182":2}}],["wind",{"2":{"4":1,"6":2,"56":1,"88":1}}],["winding",{"0":{"81":1},"2":{"4":2,"6":3,"56":3,"64":7,"81":4,"82":1,"88":1,"146":1}}],["without",{"2":{"1":2,"6":1,"17":1,"24":1,"64":1,"107":1,"122":1,"153":1,"154":1,"156":2,"172":1,"185":1}}],["with",{"0":{"71":1,"73":1,"76":1,"181":1,"182":1,"183":1,"193":1,"194":1},"2":{"1":5,"3":5,"4":7,"6":22,"11":1,"20":1,"22":2,"23":1,"52":1,"53":2,"56":7,"57":4,"58":1,"59":2,"63":1,"64":14,"66":6,"71":8,"72":5,"73":15,"75":1,"76":12,"84":1,"85":2,"94":1,"107":1,"111":1,"116":28,"118":1,"121":2,"122":5,"124":1,"125":1,"128":3,"129":2,"130":2,"140":1,"146":7,"148":1,"150":3,"151":1,"153":12,"154":3,"156":4,"158":1,"160":1,"162":1,"167":1,"170":1,"172":1,"174":1,"177":2,"180":2,"182":2,"183":1,"185":1,"188":1,"190":1,"191":4,"192":6,"193":2,"194":2,"195":2,"197":1,"198":1,"199":1}}],["within",{"0":{"44":1,"133":1,"134":1,"137":1,"138":1,"140":1,"141":1,"142":1},"1":{"134":1,"135":1},"2":{"0":2,"3":9,"4":3,"5":1,"6":13,"7":1,"9":1,"31":1,"44":2,"53":2,"56":1,"57":3,"59":1,"63":1,"64":9,"65":2,"66":5,"71":1,"73":2,"75":1,"76":6,"84":1,"85":5,"90":2,"91":3,"93":2,"110":1,"116":9,"121":1,"122":9,"133":1,"134":6,"135":11,"136":6,"137":14,"138":15,"139":15,"140":7,"141":4,"142":4,"148":1,"197":1,"198":4,"199":2}}],["will",{"2":{"1":8,"4":7,"5":1,"6":38,"11":1,"18":3,"23":2,"24":1,"53":4,"56":3,"59":2,"63":1,"64":5,"66":1,"69":1,"70":4,"71":4,"72":4,"73":4,"75":4,"76":4,"82":1,"84":2,"85":4,"88":1,"105":1,"145":1,"146":2,"150":3,"151":1,"152":2,"153":4,"154":3,"156":6,"158":2,"162":1,"166":2,"167":1,"169":2,"172":3,"174":2,"176":3,"180":2,"182":1,"184":1,"185":2,"188":1,"192":1,"195":1,"197":2,"199":1,"200":1}}],["wip",{"2":{"0":1}}]],"serializationVersion":2}';export{e as default}; diff --git a/previews/PR239/assets/chunks/VPLocalSearchBox.B2uKxNr_.js b/previews/PR239/assets/chunks/VPLocalSearchBox.B2uKxNr_.js new file mode 100644 index 000000000..1e698e021 --- /dev/null +++ b/previews/PR239/assets/chunks/VPLocalSearchBox.B2uKxNr_.js @@ -0,0 +1,8 @@ +var Ft=Object.defineProperty;var Ot=(a,e,t)=>e in a?Ft(a,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):a[e]=t;var Ae=(a,e,t)=>Ot(a,typeof e!="symbol"?e+"":e,t);import{V as Ct,p as ie,h as me,aj as tt,ak as Rt,al as At,q as $e,am as Mt,d as Lt,D as xe,an as st,ao as Dt,ap as zt,s as Pt,aq as jt,v as Me,P as he,O as _e,ar as Vt,as as $t,W as Bt,R as Wt,$ as Kt,o as H,b as Jt,j as _,a0 as Ut,k as L,at as qt,au as Gt,av as Ht,c as Z,n as nt,e as Se,C as it,F as rt,a as fe,t as pe,aw as Qt,ax as at,ay as Yt,a9 as Zt,af as Xt,az as es,_ as ts}from"./framework.onQNwZ2I.js";import{u as ss,c as ns}from"./theme.CkNAWswv.js";const is={root:()=>Ct(()=>import("./@localSearchIndexroot.rs1EPJPu.js"),[])};/*! +* tabbable 6.2.0 +* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE +*/var mt=["input:not([inert])","select:not([inert])","textarea:not([inert])","a[href]:not([inert])","button:not([inert])","[tabindex]:not(slot):not([inert])","audio[controls]:not([inert])","video[controls]:not([inert])",'[contenteditable]:not([contenteditable="false"]):not([inert])',"details>summary:first-of-type:not([inert])","details:not([inert])"],Ne=mt.join(","),gt=typeof Element>"u",ae=gt?function(){}:Element.prototype.matches||Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector,Fe=!gt&&Element.prototype.getRootNode?function(a){var e;return a==null||(e=a.getRootNode)===null||e===void 0?void 0:e.call(a)}:function(a){return a==null?void 0:a.ownerDocument},Oe=function a(e,t){var s;t===void 0&&(t=!0);var n=e==null||(s=e.getAttribute)===null||s===void 0?void 0:s.call(e,"inert"),r=n===""||n==="true",i=r||t&&e&&a(e.parentNode);return i},rs=function(e){var t,s=e==null||(t=e.getAttribute)===null||t===void 0?void 0:t.call(e,"contenteditable");return s===""||s==="true"},bt=function(e,t,s){if(Oe(e))return[];var n=Array.prototype.slice.apply(e.querySelectorAll(Ne));return t&&ae.call(e,Ne)&&n.unshift(e),n=n.filter(s),n},yt=function a(e,t,s){for(var n=[],r=Array.from(e);r.length;){var i=r.shift();if(!Oe(i,!1))if(i.tagName==="SLOT"){var o=i.assignedElements(),l=o.length?o:i.children,c=a(l,!0,s);s.flatten?n.push.apply(n,c):n.push({scopeParent:i,candidates:c})}else{var h=ae.call(i,Ne);h&&s.filter(i)&&(t||!e.includes(i))&&n.push(i);var m=i.shadowRoot||typeof s.getShadowRoot=="function"&&s.getShadowRoot(i),f=!Oe(m,!1)&&(!s.shadowRootFilter||s.shadowRootFilter(i));if(m&&f){var b=a(m===!0?i.children:m.children,!0,s);s.flatten?n.push.apply(n,b):n.push({scopeParent:i,candidates:b})}else r.unshift.apply(r,i.children)}}return n},wt=function(e){return!isNaN(parseInt(e.getAttribute("tabindex"),10))},re=function(e){if(!e)throw new Error("No node provided");return e.tabIndex<0&&(/^(AUDIO|VIDEO|DETAILS)$/.test(e.tagName)||rs(e))&&!wt(e)?0:e.tabIndex},as=function(e,t){var s=re(e);return s<0&&t&&!wt(e)?0:s},os=function(e,t){return e.tabIndex===t.tabIndex?e.documentOrder-t.documentOrder:e.tabIndex-t.tabIndex},xt=function(e){return e.tagName==="INPUT"},ls=function(e){return xt(e)&&e.type==="hidden"},cs=function(e){var t=e.tagName==="DETAILS"&&Array.prototype.slice.apply(e.children).some(function(s){return s.tagName==="SUMMARY"});return t},us=function(e,t){for(var s=0;ssummary:first-of-type"),i=r?e.parentElement:e;if(ae.call(i,"details:not([open]) *"))return!0;if(!s||s==="full"||s==="legacy-full"){if(typeof n=="function"){for(var o=e;e;){var l=e.parentElement,c=Fe(e);if(l&&!l.shadowRoot&&n(l)===!0)return ot(e);e.assignedSlot?e=e.assignedSlot:!l&&c!==e.ownerDocument?e=c.host:e=l}e=o}if(ps(e))return!e.getClientRects().length;if(s!=="legacy-full")return!0}else if(s==="non-zero-area")return ot(e);return!1},ms=function(e){if(/^(INPUT|BUTTON|SELECT|TEXTAREA)$/.test(e.tagName))for(var t=e.parentElement;t;){if(t.tagName==="FIELDSET"&&t.disabled){for(var s=0;s=0)},bs=function a(e){var t=[],s=[];return e.forEach(function(n,r){var i=!!n.scopeParent,o=i?n.scopeParent:n,l=as(o,i),c=i?a(n.candidates):o;l===0?i?t.push.apply(t,c):t.push(o):s.push({documentOrder:r,tabIndex:l,item:n,isScope:i,content:c})}),s.sort(os).reduce(function(n,r){return r.isScope?n.push.apply(n,r.content):n.push(r.content),n},[]).concat(t)},ys=function(e,t){t=t||{};var s;return t.getShadowRoot?s=yt([e],t.includeContainer,{filter:Be.bind(null,t),flatten:!1,getShadowRoot:t.getShadowRoot,shadowRootFilter:gs}):s=bt(e,t.includeContainer,Be.bind(null,t)),bs(s)},ws=function(e,t){t=t||{};var s;return t.getShadowRoot?s=yt([e],t.includeContainer,{filter:Ce.bind(null,t),flatten:!0,getShadowRoot:t.getShadowRoot}):s=bt(e,t.includeContainer,Ce.bind(null,t)),s},oe=function(e,t){if(t=t||{},!e)throw new Error("No node provided");return ae.call(e,Ne)===!1?!1:Be(t,e)},xs=mt.concat("iframe").join(","),Le=function(e,t){if(t=t||{},!e)throw new Error("No node provided");return ae.call(e,xs)===!1?!1:Ce(t,e)};/*! +* focus-trap 7.6.2 +* @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE +*/function We(a,e){(e==null||e>a.length)&&(e=a.length);for(var t=0,s=Array(e);t0){var s=e[e.length-1];s!==t&&s.pause()}var n=e.indexOf(t);n===-1||e.splice(n,1),e.push(t)},deactivateTrap:function(e,t){var s=e.indexOf(t);s!==-1&&e.splice(s,1),e.length>0&&e[e.length-1].unpause()}},Os=function(e){return e.tagName&&e.tagName.toLowerCase()==="input"&&typeof e.select=="function"},Cs=function(e){return(e==null?void 0:e.key)==="Escape"||(e==null?void 0:e.key)==="Esc"||(e==null?void 0:e.keyCode)===27},ge=function(e){return(e==null?void 0:e.key)==="Tab"||(e==null?void 0:e.keyCode)===9},Rs=function(e){return ge(e)&&!e.shiftKey},As=function(e){return ge(e)&&e.shiftKey},dt=function(e){return setTimeout(e,0)},ve=function(e){for(var t=arguments.length,s=new Array(t>1?t-1:0),n=1;n1&&arguments[1]!==void 0?arguments[1]:{},g=d.hasFallback,T=g===void 0?!1:g,k=d.params,O=k===void 0?[]:k,S=r[u];if(typeof S=="function"&&(S=S.apply(void 0,Is(O))),S===!0&&(S=void 0),!S){if(S===void 0||S===!1)return S;throw new Error("`".concat(u,"` was specified but was not a node, or did not return a node"))}var C=S;if(typeof S=="string"){try{C=s.querySelector(S)}catch(v){throw new Error("`".concat(u,'` appears to be an invalid selector; error="').concat(v.message,'"'))}if(!C&&!T)throw new Error("`".concat(u,"` as selector refers to no known node"))}return C},m=function(){var u=h("initialFocus",{hasFallback:!0});if(u===!1)return!1;if(u===void 0||u&&!Le(u,r.tabbableOptions))if(c(s.activeElement)>=0)u=s.activeElement;else{var d=i.tabbableGroups[0],g=d&&d.firstTabbableNode;u=g||h("fallbackFocus")}else u===null&&(u=h("fallbackFocus"));if(!u)throw new Error("Your focus-trap needs to have at least one focusable element");return u},f=function(){if(i.containerGroups=i.containers.map(function(u){var d=ys(u,r.tabbableOptions),g=ws(u,r.tabbableOptions),T=d.length>0?d[0]:void 0,k=d.length>0?d[d.length-1]:void 0,O=g.find(function(v){return oe(v)}),S=g.slice().reverse().find(function(v){return oe(v)}),C=!!d.find(function(v){return re(v)>0});return{container:u,tabbableNodes:d,focusableNodes:g,posTabIndexesFound:C,firstTabbableNode:T,lastTabbableNode:k,firstDomTabbableNode:O,lastDomTabbableNode:S,nextTabbableNode:function(p){var E=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0,F=d.indexOf(p);return F<0?E?g.slice(g.indexOf(p)+1).find(function(z){return oe(z)}):g.slice(0,g.indexOf(p)).reverse().find(function(z){return oe(z)}):d[F+(E?1:-1)]}}}),i.tabbableGroups=i.containerGroups.filter(function(u){return u.tabbableNodes.length>0}),i.tabbableGroups.length<=0&&!h("fallbackFocus"))throw new Error("Your focus-trap must have at least one container with at least one tabbable node in it at all times");if(i.containerGroups.find(function(u){return u.posTabIndexesFound})&&i.containerGroups.length>1)throw new Error("At least one node with a positive tabindex was found in one of your focus-trap's multiple containers. Positive tabindexes are only supported in single-container focus-traps.")},b=function(u){var d=u.activeElement;if(d)return d.shadowRoot&&d.shadowRoot.activeElement!==null?b(d.shadowRoot):d},y=function(u){if(u!==!1&&u!==b(document)){if(!u||!u.focus){y(m());return}u.focus({preventScroll:!!r.preventScroll}),i.mostRecentlyFocusedNode=u,Os(u)&&u.select()}},x=function(u){var d=h("setReturnFocus",{params:[u]});return d||(d===!1?!1:u)},w=function(u){var d=u.target,g=u.event,T=u.isBackward,k=T===void 0?!1:T;d=d||Ee(g),f();var O=null;if(i.tabbableGroups.length>0){var S=c(d,g),C=S>=0?i.containerGroups[S]:void 0;if(S<0)k?O=i.tabbableGroups[i.tabbableGroups.length-1].lastTabbableNode:O=i.tabbableGroups[0].firstTabbableNode;else if(k){var v=i.tabbableGroups.findIndex(function(j){var I=j.firstTabbableNode;return d===I});if(v<0&&(C.container===d||Le(d,r.tabbableOptions)&&!oe(d,r.tabbableOptions)&&!C.nextTabbableNode(d,!1))&&(v=S),v>=0){var p=v===0?i.tabbableGroups.length-1:v-1,E=i.tabbableGroups[p];O=re(d)>=0?E.lastTabbableNode:E.lastDomTabbableNode}else ge(g)||(O=C.nextTabbableNode(d,!1))}else{var F=i.tabbableGroups.findIndex(function(j){var I=j.lastTabbableNode;return d===I});if(F<0&&(C.container===d||Le(d,r.tabbableOptions)&&!oe(d,r.tabbableOptions)&&!C.nextTabbableNode(d))&&(F=S),F>=0){var z=F===i.tabbableGroups.length-1?0:F+1,P=i.tabbableGroups[z];O=re(d)>=0?P.firstTabbableNode:P.firstDomTabbableNode}else ge(g)||(O=C.nextTabbableNode(d))}}else O=h("fallbackFocus");return O},R=function(u){var d=Ee(u);if(!(c(d,u)>=0)){if(ve(r.clickOutsideDeactivates,u)){o.deactivate({returnFocus:r.returnFocusOnDeactivate});return}ve(r.allowOutsideClick,u)||u.preventDefault()}},A=function(u){var d=Ee(u),g=c(d,u)>=0;if(g||d instanceof Document)g&&(i.mostRecentlyFocusedNode=d);else{u.stopImmediatePropagation();var T,k=!0;if(i.mostRecentlyFocusedNode)if(re(i.mostRecentlyFocusedNode)>0){var O=c(i.mostRecentlyFocusedNode),S=i.containerGroups[O].tabbableNodes;if(S.length>0){var C=S.findIndex(function(v){return v===i.mostRecentlyFocusedNode});C>=0&&(r.isKeyForward(i.recentNavEvent)?C+1=0&&(T=S[C-1],k=!1))}}else i.containerGroups.some(function(v){return v.tabbableNodes.some(function(p){return re(p)>0})})||(k=!1);else k=!1;k&&(T=w({target:i.mostRecentlyFocusedNode,isBackward:r.isKeyBackward(i.recentNavEvent)})),y(T||i.mostRecentlyFocusedNode||m())}i.recentNavEvent=void 0},J=function(u){var d=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!1;i.recentNavEvent=u;var g=w({event:u,isBackward:d});g&&(ge(u)&&u.preventDefault(),y(g))},Q=function(u){(r.isKeyForward(u)||r.isKeyBackward(u))&&J(u,r.isKeyBackward(u))},W=function(u){Cs(u)&&ve(r.escapeDeactivates,u)!==!1&&(u.preventDefault(),o.deactivate())},V=function(u){var d=Ee(u);c(d,u)>=0||ve(r.clickOutsideDeactivates,u)||ve(r.allowOutsideClick,u)||(u.preventDefault(),u.stopImmediatePropagation())},$=function(){if(i.active)return ut.activateTrap(n,o),i.delayInitialFocusTimer=r.delayInitialFocus?dt(function(){y(m())}):y(m()),s.addEventListener("focusin",A,!0),s.addEventListener("mousedown",R,{capture:!0,passive:!1}),s.addEventListener("touchstart",R,{capture:!0,passive:!1}),s.addEventListener("click",V,{capture:!0,passive:!1}),s.addEventListener("keydown",Q,{capture:!0,passive:!1}),s.addEventListener("keydown",W),o},be=function(){if(i.active)return s.removeEventListener("focusin",A,!0),s.removeEventListener("mousedown",R,!0),s.removeEventListener("touchstart",R,!0),s.removeEventListener("click",V,!0),s.removeEventListener("keydown",Q,!0),s.removeEventListener("keydown",W),o},M=function(u){var d=u.some(function(g){var T=Array.from(g.removedNodes);return T.some(function(k){return k===i.mostRecentlyFocusedNode})});d&&y(m())},U=typeof window<"u"&&"MutationObserver"in window?new MutationObserver(M):void 0,q=function(){U&&(U.disconnect(),i.active&&!i.paused&&i.containers.map(function(u){U.observe(u,{subtree:!0,childList:!0})}))};return o={get active(){return i.active},get paused(){return i.paused},activate:function(u){if(i.active)return this;var d=l(u,"onActivate"),g=l(u,"onPostActivate"),T=l(u,"checkCanFocusTrap");T||f(),i.active=!0,i.paused=!1,i.nodeFocusedBeforeActivation=s.activeElement,d==null||d();var k=function(){T&&f(),$(),q(),g==null||g()};return T?(T(i.containers.concat()).then(k,k),this):(k(),this)},deactivate:function(u){if(!i.active)return this;var d=ct({onDeactivate:r.onDeactivate,onPostDeactivate:r.onPostDeactivate,checkCanReturnFocus:r.checkCanReturnFocus},u);clearTimeout(i.delayInitialFocusTimer),i.delayInitialFocusTimer=void 0,be(),i.active=!1,i.paused=!1,q(),ut.deactivateTrap(n,o);var g=l(d,"onDeactivate"),T=l(d,"onPostDeactivate"),k=l(d,"checkCanReturnFocus"),O=l(d,"returnFocus","returnFocusOnDeactivate");g==null||g();var S=function(){dt(function(){O&&y(x(i.nodeFocusedBeforeActivation)),T==null||T()})};return O&&k?(k(x(i.nodeFocusedBeforeActivation)).then(S,S),this):(S(),this)},pause:function(u){if(i.paused||!i.active)return this;var d=l(u,"onPause"),g=l(u,"onPostPause");return i.paused=!0,d==null||d(),be(),q(),g==null||g(),this},unpause:function(u){if(!i.paused||!i.active)return this;var d=l(u,"onUnpause"),g=l(u,"onPostUnpause");return i.paused=!1,d==null||d(),f(),$(),q(),g==null||g(),this},updateContainerElements:function(u){var d=[].concat(u).filter(Boolean);return i.containers=d.map(function(g){return typeof g=="string"?s.querySelector(g):g}),i.active&&f(),q(),this}},o.updateContainerElements(e),o};function Ds(a,e={}){let t;const{immediate:s,...n}=e,r=ie(!1),i=ie(!1),o=f=>t&&t.activate(f),l=f=>t&&t.deactivate(f),c=()=>{t&&(t.pause(),i.value=!0)},h=()=>{t&&(t.unpause(),i.value=!1)},m=me(()=>{const f=tt(a);return(Array.isArray(f)?f:[f]).map(b=>{const y=tt(b);return typeof y=="string"?y:Rt(y)}).filter(At)});return $e(m,f=>{f.length&&(t=Ls(f,{...n,onActivate(){r.value=!0,e.onActivate&&e.onActivate()},onDeactivate(){r.value=!1,e.onDeactivate&&e.onDeactivate()}}),s&&o())},{flush:"post"}),Mt(()=>l()),{hasFocus:r,isPaused:i,activate:o,deactivate:l,pause:c,unpause:h}}class ce{constructor(e,t=!0,s=[],n=5e3){this.ctx=e,this.iframes=t,this.exclude=s,this.iframesTimeout=n}static matches(e,t){const s=typeof t=="string"?[t]:t,n=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.oMatchesSelector||e.webkitMatchesSelector;if(n){let r=!1;return s.every(i=>n.call(e,i)?(r=!0,!1):!0),r}else return!1}getContexts(){let e,t=[];return typeof this.ctx>"u"||!this.ctx?e=[]:NodeList.prototype.isPrototypeOf(this.ctx)?e=Array.prototype.slice.call(this.ctx):Array.isArray(this.ctx)?e=this.ctx:typeof this.ctx=="string"?e=Array.prototype.slice.call(document.querySelectorAll(this.ctx)):e=[this.ctx],e.forEach(s=>{const n=t.filter(r=>r.contains(s)).length>0;t.indexOf(s)===-1&&!n&&t.push(s)}),t}getIframeContents(e,t,s=()=>{}){let n;try{const r=e.contentWindow;if(n=r.document,!r||!n)throw new Error("iframe inaccessible")}catch{s()}n&&t(n)}isIframeBlank(e){const t="about:blank",s=e.getAttribute("src").trim();return e.contentWindow.location.href===t&&s!==t&&s}observeIframeLoad(e,t,s){let n=!1,r=null;const i=()=>{if(!n){n=!0,clearTimeout(r);try{this.isIframeBlank(e)||(e.removeEventListener("load",i),this.getIframeContents(e,t,s))}catch{s()}}};e.addEventListener("load",i),r=setTimeout(i,this.iframesTimeout)}onIframeReady(e,t,s){try{e.contentWindow.document.readyState==="complete"?this.isIframeBlank(e)?this.observeIframeLoad(e,t,s):this.getIframeContents(e,t,s):this.observeIframeLoad(e,t,s)}catch{s()}}waitForIframes(e,t){let s=0;this.forEachIframe(e,()=>!0,n=>{s++,this.waitForIframes(n.querySelector("html"),()=>{--s||t()})},n=>{n||t()})}forEachIframe(e,t,s,n=()=>{}){let r=e.querySelectorAll("iframe"),i=r.length,o=0;r=Array.prototype.slice.call(r);const l=()=>{--i<=0&&n(o)};i||l(),r.forEach(c=>{ce.matches(c,this.exclude)?l():this.onIframeReady(c,h=>{t(c)&&(o++,s(h)),l()},l)})}createIterator(e,t,s){return document.createNodeIterator(e,t,s,!1)}createInstanceOnIframe(e){return new ce(e.querySelector("html"),this.iframes)}compareNodeIframe(e,t,s){const n=e.compareDocumentPosition(s),r=Node.DOCUMENT_POSITION_PRECEDING;if(n&r)if(t!==null){const i=t.compareDocumentPosition(s),o=Node.DOCUMENT_POSITION_FOLLOWING;if(i&o)return!0}else return!0;return!1}getIteratorNode(e){const t=e.previousNode();let s;return t===null?s=e.nextNode():s=e.nextNode()&&e.nextNode(),{prevNode:t,node:s}}checkIframeFilter(e,t,s,n){let r=!1,i=!1;return n.forEach((o,l)=>{o.val===s&&(r=l,i=o.handled)}),this.compareNodeIframe(e,t,s)?(r===!1&&!i?n.push({val:s,handled:!0}):r!==!1&&!i&&(n[r].handled=!0),!0):(r===!1&&n.push({val:s,handled:!1}),!1)}handleOpenIframes(e,t,s,n){e.forEach(r=>{r.handled||this.getIframeContents(r.val,i=>{this.createInstanceOnIframe(i).forEachNode(t,s,n)})})}iterateThroughNodes(e,t,s,n,r){const i=this.createIterator(t,e,n);let o=[],l=[],c,h,m=()=>({prevNode:h,node:c}=this.getIteratorNode(i),c);for(;m();)this.iframes&&this.forEachIframe(t,f=>this.checkIframeFilter(c,h,f,o),f=>{this.createInstanceOnIframe(f).forEachNode(e,b=>l.push(b),n)}),l.push(c);l.forEach(f=>{s(f)}),this.iframes&&this.handleOpenIframes(o,e,s,n),r()}forEachNode(e,t,s,n=()=>{}){const r=this.getContexts();let i=r.length;i||n(),r.forEach(o=>{const l=()=>{this.iterateThroughNodes(e,o,t,s,()=>{--i<=0&&n()})};this.iframes?this.waitForIframes(o,l):l()})}}let zs=class{constructor(e){this.ctx=e,this.ie=!1;const t=window.navigator.userAgent;(t.indexOf("MSIE")>-1||t.indexOf("Trident")>-1)&&(this.ie=!0)}set opt(e){this._opt=Object.assign({},{element:"",className:"",exclude:[],iframes:!1,iframesTimeout:5e3,separateWordSearch:!0,diacritics:!0,synonyms:{},accuracy:"partially",acrossElements:!1,caseSensitive:!1,ignoreJoiners:!1,ignoreGroups:0,ignorePunctuation:[],wildcards:"disabled",each:()=>{},noMatch:()=>{},filter:()=>!0,done:()=>{},debug:!1,log:window.console},e)}get opt(){return this._opt}get iterator(){return new ce(this.ctx,this.opt.iframes,this.opt.exclude,this.opt.iframesTimeout)}log(e,t="debug"){const s=this.opt.log;this.opt.debug&&typeof s=="object"&&typeof s[t]=="function"&&s[t](`mark.js: ${e}`)}escapeStr(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}createRegExp(e){return this.opt.wildcards!=="disabled"&&(e=this.setupWildcardsRegExp(e)),e=this.escapeStr(e),Object.keys(this.opt.synonyms).length&&(e=this.createSynonymsRegExp(e)),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),this.opt.diacritics&&(e=this.createDiacriticsRegExp(e)),e=this.createMergedBlanksRegExp(e),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.createJoinersRegExp(e)),this.opt.wildcards!=="disabled"&&(e=this.createWildcardsRegExp(e)),e=this.createAccuracyRegExp(e),e}createSynonymsRegExp(e){const t=this.opt.synonyms,s=this.opt.caseSensitive?"":"i",n=this.opt.ignoreJoiners||this.opt.ignorePunctuation.length?"\0":"";for(let r in t)if(t.hasOwnProperty(r)){const i=t[r],o=this.opt.wildcards!=="disabled"?this.setupWildcardsRegExp(r):this.escapeStr(r),l=this.opt.wildcards!=="disabled"?this.setupWildcardsRegExp(i):this.escapeStr(i);o!==""&&l!==""&&(e=e.replace(new RegExp(`(${this.escapeStr(o)}|${this.escapeStr(l)})`,`gm${s}`),n+`(${this.processSynomyms(o)}|${this.processSynomyms(l)})`+n))}return e}processSynomyms(e){return(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),e}setupWildcardsRegExp(e){return e=e.replace(/(?:\\)*\?/g,t=>t.charAt(0)==="\\"?"?":""),e.replace(/(?:\\)*\*/g,t=>t.charAt(0)==="\\"?"*":"")}createWildcardsRegExp(e){let t=this.opt.wildcards==="withSpaces";return e.replace(/\u0001/g,t?"[\\S\\s]?":"\\S?").replace(/\u0002/g,t?"[\\S\\s]*?":"\\S*")}setupIgnoreJoinersRegExp(e){return e.replace(/[^(|)\\]/g,(t,s,n)=>{let r=n.charAt(s+1);return/[(|)\\]/.test(r)||r===""?t:t+"\0"})}createJoinersRegExp(e){let t=[];const s=this.opt.ignorePunctuation;return Array.isArray(s)&&s.length&&t.push(this.escapeStr(s.join(""))),this.opt.ignoreJoiners&&t.push("\\u00ad\\u200b\\u200c\\u200d"),t.length?e.split(/\u0000+/).join(`[${t.join("")}]*`):e}createDiacriticsRegExp(e){const t=this.opt.caseSensitive?"":"i",s=this.opt.caseSensitive?["aàáảãạăằắẳẵặâầấẩẫậäåāą","AÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ","cçćč","CÇĆČ","dđď","DĐĎ","eèéẻẽẹêềếểễệëěēę","EÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ","iìíỉĩịîïī","IÌÍỈĨỊÎÏĪ","lł","LŁ","nñňń","NÑŇŃ","oòóỏõọôồốổỗộơởỡớờợöøō","OÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ","rř","RŘ","sšśșş","SŠŚȘŞ","tťțţ","TŤȚŢ","uùúủũụưừứửữựûüůū","UÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ","yýỳỷỹỵÿ","YÝỲỶỸỴŸ","zžżź","ZŽŻŹ"]:["aàáảãạăằắẳẵặâầấẩẫậäåāąAÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ","cçćčCÇĆČ","dđďDĐĎ","eèéẻẽẹêềếểễệëěēęEÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ","iìíỉĩịîïīIÌÍỈĨỊÎÏĪ","lłLŁ","nñňńNÑŇŃ","oòóỏõọôồốổỗộơởỡớờợöøōOÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ","rřRŘ","sšśșşSŠŚȘŞ","tťțţTŤȚŢ","uùúủũụưừứửữựûüůūUÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ","yýỳỷỹỵÿYÝỲỶỸỴŸ","zžżźZŽŻŹ"];let n=[];return e.split("").forEach(r=>{s.every(i=>{if(i.indexOf(r)!==-1){if(n.indexOf(i)>-1)return!1;e=e.replace(new RegExp(`[${i}]`,`gm${t}`),`[${i}]`),n.push(i)}return!0})}),e}createMergedBlanksRegExp(e){return e.replace(/[\s]+/gmi,"[\\s]+")}createAccuracyRegExp(e){const t="!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~¡¿";let s=this.opt.accuracy,n=typeof s=="string"?s:s.value,r=typeof s=="string"?[]:s.limiters,i="";switch(r.forEach(o=>{i+=`|${this.escapeStr(o)}`}),n){case"partially":default:return`()(${e})`;case"complementary":return i="\\s"+(i||this.escapeStr(t)),`()([^${i}]*${e}[^${i}]*)`;case"exactly":return`(^|\\s${i})(${e})(?=$|\\s${i})`}}getSeparatedKeywords(e){let t=[];return e.forEach(s=>{this.opt.separateWordSearch?s.split(" ").forEach(n=>{n.trim()&&t.indexOf(n)===-1&&t.push(n)}):s.trim()&&t.indexOf(s)===-1&&t.push(s)}),{keywords:t.sort((s,n)=>n.length-s.length),length:t.length}}isNumeric(e){return Number(parseFloat(e))==e}checkRanges(e){if(!Array.isArray(e)||Object.prototype.toString.call(e[0])!=="[object Object]")return this.log("markRanges() will only accept an array of objects"),this.opt.noMatch(e),[];const t=[];let s=0;return e.sort((n,r)=>n.start-r.start).forEach(n=>{let{start:r,end:i,valid:o}=this.callNoMatchOnInvalidRanges(n,s);o&&(n.start=r,n.length=i-r,t.push(n),s=i)}),t}callNoMatchOnInvalidRanges(e,t){let s,n,r=!1;return e&&typeof e.start<"u"?(s=parseInt(e.start,10),n=s+parseInt(e.length,10),this.isNumeric(e.start)&&this.isNumeric(e.length)&&n-t>0&&n-s>0?r=!0:(this.log(`Ignoring invalid or overlapping range: ${JSON.stringify(e)}`),this.opt.noMatch(e))):(this.log(`Ignoring invalid range: ${JSON.stringify(e)}`),this.opt.noMatch(e)),{start:s,end:n,valid:r}}checkWhitespaceRanges(e,t,s){let n,r=!0,i=s.length,o=t-i,l=parseInt(e.start,10)-o;return l=l>i?i:l,n=l+parseInt(e.length,10),n>i&&(n=i,this.log(`End range automatically set to the max value of ${i}`)),l<0||n-l<0||l>i||n>i?(r=!1,this.log(`Invalid range: ${JSON.stringify(e)}`),this.opt.noMatch(e)):s.substring(l,n).replace(/\s+/g,"")===""&&(r=!1,this.log("Skipping whitespace only range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:l,end:n,valid:r}}getTextNodes(e){let t="",s=[];this.iterator.forEachNode(NodeFilter.SHOW_TEXT,n=>{s.push({start:t.length,end:(t+=n.textContent).length,node:n})},n=>this.matchesExclude(n.parentNode)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT,()=>{e({value:t,nodes:s})})}matchesExclude(e){return ce.matches(e,this.opt.exclude.concat(["script","style","title","head","html"]))}wrapRangeInTextNode(e,t,s){const n=this.opt.element?this.opt.element:"mark",r=e.splitText(t),i=r.splitText(s-t);let o=document.createElement(n);return o.setAttribute("data-markjs","true"),this.opt.className&&o.setAttribute("class",this.opt.className),o.textContent=r.textContent,r.parentNode.replaceChild(o,r),i}wrapRangeInMappedTextNode(e,t,s,n,r){e.nodes.every((i,o)=>{const l=e.nodes[o+1];if(typeof l>"u"||l.start>t){if(!n(i.node))return!1;const c=t-i.start,h=(s>i.end?i.end:s)-i.start,m=e.value.substr(0,i.start),f=e.value.substr(h+i.start);if(i.node=this.wrapRangeInTextNode(i.node,c,h),e.value=m+f,e.nodes.forEach((b,y)=>{y>=o&&(e.nodes[y].start>0&&y!==o&&(e.nodes[y].start-=h),e.nodes[y].end-=h)}),s-=h,r(i.node.previousSibling,i.start),s>i.end)t=i.end;else return!1}return!0})}wrapMatches(e,t,s,n,r){const i=t===0?0:t+1;this.getTextNodes(o=>{o.nodes.forEach(l=>{l=l.node;let c;for(;(c=e.exec(l.textContent))!==null&&c[i]!=="";){if(!s(c[i],l))continue;let h=c.index;if(i!==0)for(let m=1;m{let l;for(;(l=e.exec(o.value))!==null&&l[i]!=="";){let c=l.index;if(i!==0)for(let m=1;ms(l[i],m),(m,f)=>{e.lastIndex=f,n(m)})}r()})}wrapRangeFromIndex(e,t,s,n){this.getTextNodes(r=>{const i=r.value.length;e.forEach((o,l)=>{let{start:c,end:h,valid:m}=this.checkWhitespaceRanges(o,i,r.value);m&&this.wrapRangeInMappedTextNode(r,c,h,f=>t(f,o,r.value.substring(c,h),l),f=>{s(f,o)})}),n()})}unwrapMatches(e){const t=e.parentNode;let s=document.createDocumentFragment();for(;e.firstChild;)s.appendChild(e.removeChild(e.firstChild));t.replaceChild(s,e),this.ie?this.normalizeTextNode(t):t.normalize()}normalizeTextNode(e){if(e){if(e.nodeType===3)for(;e.nextSibling&&e.nextSibling.nodeType===3;)e.nodeValue+=e.nextSibling.nodeValue,e.parentNode.removeChild(e.nextSibling);else this.normalizeTextNode(e.firstChild);this.normalizeTextNode(e.nextSibling)}}markRegExp(e,t){this.opt=t,this.log(`Searching with expression "${e}"`);let s=0,n="wrapMatches";const r=i=>{s++,this.opt.each(i)};this.opt.acrossElements&&(n="wrapMatchesAcrossElements"),this[n](e,this.opt.ignoreGroups,(i,o)=>this.opt.filter(o,i,s),r,()=>{s===0&&this.opt.noMatch(e),this.opt.done(s)})}mark(e,t){this.opt=t;let s=0,n="wrapMatches";const{keywords:r,length:i}=this.getSeparatedKeywords(typeof e=="string"?[e]:e),o=this.opt.caseSensitive?"":"i",l=c=>{let h=new RegExp(this.createRegExp(c),`gm${o}`),m=0;this.log(`Searching with expression "${h}"`),this[n](h,1,(f,b)=>this.opt.filter(b,c,s,m),f=>{m++,s++,this.opt.each(f)},()=>{m===0&&this.opt.noMatch(c),r[i-1]===c?this.opt.done(s):l(r[r.indexOf(c)+1])})};this.opt.acrossElements&&(n="wrapMatchesAcrossElements"),i===0?this.opt.done(s):l(r[0])}markRanges(e,t){this.opt=t;let s=0,n=this.checkRanges(e);n&&n.length?(this.log("Starting to mark with the following ranges: "+JSON.stringify(n)),this.wrapRangeFromIndex(n,(r,i,o,l)=>this.opt.filter(r,i,o,l),(r,i)=>{s++,this.opt.each(r,i)},()=>{this.opt.done(s)})):this.opt.done(s)}unmark(e){this.opt=e;let t=this.opt.element?this.opt.element:"*";t+="[data-markjs]",this.opt.className&&(t+=`.${this.opt.className}`),this.log(`Removal selector "${t}"`),this.iterator.forEachNode(NodeFilter.SHOW_ELEMENT,s=>{this.unwrapMatches(s)},s=>{const n=ce.matches(s,t),r=this.matchesExclude(s);return!n||r?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT},this.opt.done)}};function Ps(a){const e=new zs(a);return this.mark=(t,s)=>(e.mark(t,s),this),this.markRegExp=(t,s)=>(e.markRegExp(t,s),this),this.markRanges=(t,s)=>(e.markRanges(t,s),this),this.unmark=t=>(e.unmark(t),this),this}function ke(a,e,t,s){function n(r){return r instanceof t?r:new t(function(i){i(r)})}return new(t||(t=Promise))(function(r,i){function o(h){try{c(s.next(h))}catch(m){i(m)}}function l(h){try{c(s.throw(h))}catch(m){i(m)}}function c(h){h.done?r(h.value):n(h.value).then(o,l)}c((s=s.apply(a,[])).next())})}const js="ENTRIES",_t="KEYS",St="VALUES",D="";class De{constructor(e,t){const s=e._tree,n=Array.from(s.keys());this.set=e,this._type=t,this._path=n.length>0?[{node:s,keys:n}]:[]}next(){const e=this.dive();return this.backtrack(),e}dive(){if(this._path.length===0)return{done:!0,value:void 0};const{node:e,keys:t}=le(this._path);if(le(t)===D)return{done:!1,value:this.result()};const s=e.get(le(t));return this._path.push({node:s,keys:Array.from(s.keys())}),this.dive()}backtrack(){if(this._path.length===0)return;const e=le(this._path).keys;e.pop(),!(e.length>0)&&(this._path.pop(),this.backtrack())}key(){return this.set._prefix+this._path.map(({keys:e})=>le(e)).filter(e=>e!==D).join("")}value(){return le(this._path).node.get(D)}result(){switch(this._type){case St:return this.value();case _t:return this.key();default:return[this.key(),this.value()]}}[Symbol.iterator](){return this}}const le=a=>a[a.length-1],Vs=(a,e,t)=>{const s=new Map;if(e===void 0)return s;const n=e.length+1,r=n+t,i=new Uint8Array(r*n).fill(t+1);for(let o=0;o{const l=r*i;e:for(const c of a.keys())if(c===D){const h=n[l-1];h<=t&&s.set(o,[a.get(c),h])}else{let h=r;for(let m=0;mt)continue e}Et(a.get(c),e,t,s,n,h,i,o+c)}};class X{constructor(e=new Map,t=""){this._size=void 0,this._tree=e,this._prefix=t}atPrefix(e){if(!e.startsWith(this._prefix))throw new Error("Mismatched prefix");const[t,s]=Re(this._tree,e.slice(this._prefix.length));if(t===void 0){const[n,r]=qe(s);for(const i of n.keys())if(i!==D&&i.startsWith(r)){const o=new Map;return o.set(i.slice(r.length),n.get(i)),new X(o,e)}}return new X(t,e)}clear(){this._size=void 0,this._tree.clear()}delete(e){return this._size=void 0,$s(this._tree,e)}entries(){return new De(this,js)}forEach(e){for(const[t,s]of this)e(t,s,this)}fuzzyGet(e,t){return Vs(this._tree,e,t)}get(e){const t=Ke(this._tree,e);return t!==void 0?t.get(D):void 0}has(e){const t=Ke(this._tree,e);return t!==void 0&&t.has(D)}keys(){return new De(this,_t)}set(e,t){if(typeof e!="string")throw new Error("key must be a string");return this._size=void 0,ze(this._tree,e).set(D,t),this}get size(){if(this._size)return this._size;this._size=0;const e=this.entries();for(;!e.next().done;)this._size+=1;return this._size}update(e,t){if(typeof e!="string")throw new Error("key must be a string");this._size=void 0;const s=ze(this._tree,e);return s.set(D,t(s.get(D))),this}fetch(e,t){if(typeof e!="string")throw new Error("key must be a string");this._size=void 0;const s=ze(this._tree,e);let n=s.get(D);return n===void 0&&s.set(D,n=t()),n}values(){return new De(this,St)}[Symbol.iterator](){return this.entries()}static from(e){const t=new X;for(const[s,n]of e)t.set(s,n);return t}static fromObject(e){return X.from(Object.entries(e))}}const Re=(a,e,t=[])=>{if(e.length===0||a==null)return[a,t];for(const s of a.keys())if(s!==D&&e.startsWith(s))return t.push([a,s]),Re(a.get(s),e.slice(s.length),t);return t.push([a,e]),Re(void 0,"",t)},Ke=(a,e)=>{if(e.length===0||a==null)return a;for(const t of a.keys())if(t!==D&&e.startsWith(t))return Ke(a.get(t),e.slice(t.length))},ze=(a,e)=>{const t=e.length;e:for(let s=0;a&&s{const[t,s]=Re(a,e);if(t!==void 0){if(t.delete(D),t.size===0)Tt(s);else if(t.size===1){const[n,r]=t.entries().next().value;It(s,n,r)}}},Tt=a=>{if(a.length===0)return;const[e,t]=qe(a);if(e.delete(t),e.size===0)Tt(a.slice(0,-1));else if(e.size===1){const[s,n]=e.entries().next().value;s!==D&&It(a.slice(0,-1),s,n)}},It=(a,e,t)=>{if(a.length===0)return;const[s,n]=qe(a);s.set(n+e,t),s.delete(n)},qe=a=>a[a.length-1],Ge="or",kt="and",Bs="and_not";class ue{constructor(e){if((e==null?void 0:e.fields)==null)throw new Error('MiniSearch: option "fields" must be provided');const t=e.autoVacuum==null||e.autoVacuum===!0?Ve:e.autoVacuum;this._options=Object.assign(Object.assign(Object.assign({},je),e),{autoVacuum:t,searchOptions:Object.assign(Object.assign({},ht),e.searchOptions||{}),autoSuggestOptions:Object.assign(Object.assign({},qs),e.autoSuggestOptions||{})}),this._index=new X,this._documentCount=0,this._documentIds=new Map,this._idToShortId=new Map,this._fieldIds={},this._fieldLength=new Map,this._avgFieldLength=[],this._nextId=0,this._storedFields=new Map,this._dirtCount=0,this._currentVacuum=null,this._enqueuedVacuum=null,this._enqueuedVacuumConditions=Ue,this.addFields(this._options.fields)}add(e){const{extractField:t,tokenize:s,processTerm:n,fields:r,idField:i}=this._options,o=t(e,i);if(o==null)throw new Error(`MiniSearch: document does not have ID field "${i}"`);if(this._idToShortId.has(o))throw new Error(`MiniSearch: duplicate ID ${o}`);const l=this.addDocumentId(o);this.saveStoredFields(l,e);for(const c of r){const h=t(e,c);if(h==null)continue;const m=s(h.toString(),c),f=this._fieldIds[c],b=new Set(m).size;this.addFieldLength(l,f,this._documentCount-1,b);for(const y of m){const x=n(y,c);if(Array.isArray(x))for(const w of x)this.addTerm(f,l,w);else x&&this.addTerm(f,l,x)}}}addAll(e){for(const t of e)this.add(t)}addAllAsync(e,t={}){const{chunkSize:s=10}=t,n={chunk:[],promise:Promise.resolve()},{chunk:r,promise:i}=e.reduce(({chunk:o,promise:l},c,h)=>(o.push(c),(h+1)%s===0?{chunk:[],promise:l.then(()=>new Promise(m=>setTimeout(m,0))).then(()=>this.addAll(o))}:{chunk:o,promise:l}),n);return i.then(()=>this.addAll(r))}remove(e){const{tokenize:t,processTerm:s,extractField:n,fields:r,idField:i}=this._options,o=n(e,i);if(o==null)throw new Error(`MiniSearch: document does not have ID field "${i}"`);const l=this._idToShortId.get(o);if(l==null)throw new Error(`MiniSearch: cannot remove document with ID ${o}: it is not in the index`);for(const c of r){const h=n(e,c);if(h==null)continue;const m=t(h.toString(),c),f=this._fieldIds[c],b=new Set(m).size;this.removeFieldLength(l,f,this._documentCount,b);for(const y of m){const x=s(y,c);if(Array.isArray(x))for(const w of x)this.removeTerm(f,l,w);else x&&this.removeTerm(f,l,x)}}this._storedFields.delete(l),this._documentIds.delete(l),this._idToShortId.delete(o),this._fieldLength.delete(l),this._documentCount-=1}removeAll(e){if(e)for(const t of e)this.remove(t);else{if(arguments.length>0)throw new Error("Expected documents to be present. Omit the argument to remove all documents.");this._index=new X,this._documentCount=0,this._documentIds=new Map,this._idToShortId=new Map,this._fieldLength=new Map,this._avgFieldLength=[],this._storedFields=new Map,this._nextId=0}}discard(e){const t=this._idToShortId.get(e);if(t==null)throw new Error(`MiniSearch: cannot discard document with ID ${e}: it is not in the index`);this._idToShortId.delete(e),this._documentIds.delete(t),this._storedFields.delete(t),(this._fieldLength.get(t)||[]).forEach((s,n)=>{this.removeFieldLength(t,n,this._documentCount,s)}),this._fieldLength.delete(t),this._documentCount-=1,this._dirtCount+=1,this.maybeAutoVacuum()}maybeAutoVacuum(){if(this._options.autoVacuum===!1)return;const{minDirtFactor:e,minDirtCount:t,batchSize:s,batchWait:n}=this._options.autoVacuum;this.conditionalVacuum({batchSize:s,batchWait:n},{minDirtCount:t,minDirtFactor:e})}discardAll(e){const t=this._options.autoVacuum;try{this._options.autoVacuum=!1;for(const s of e)this.discard(s)}finally{this._options.autoVacuum=t}this.maybeAutoVacuum()}replace(e){const{idField:t,extractField:s}=this._options,n=s(e,t);this.discard(n),this.add(e)}vacuum(e={}){return this.conditionalVacuum(e)}conditionalVacuum(e,t){return this._currentVacuum?(this._enqueuedVacuumConditions=this._enqueuedVacuumConditions&&t,this._enqueuedVacuum!=null?this._enqueuedVacuum:(this._enqueuedVacuum=this._currentVacuum.then(()=>{const s=this._enqueuedVacuumConditions;return this._enqueuedVacuumConditions=Ue,this.performVacuuming(e,s)}),this._enqueuedVacuum)):this.vacuumConditionsMet(t)===!1?Promise.resolve():(this._currentVacuum=this.performVacuuming(e),this._currentVacuum)}performVacuuming(e,t){return ke(this,void 0,void 0,function*(){const s=this._dirtCount;if(this.vacuumConditionsMet(t)){const n=e.batchSize||Je.batchSize,r=e.batchWait||Je.batchWait;let i=1;for(const[o,l]of this._index){for(const[c,h]of l)for(const[m]of h)this._documentIds.has(m)||(h.size<=1?l.delete(c):h.delete(m));this._index.get(o).size===0&&this._index.delete(o),i%n===0&&(yield new Promise(c=>setTimeout(c,r))),i+=1}this._dirtCount-=s}yield null,this._currentVacuum=this._enqueuedVacuum,this._enqueuedVacuum=null})}vacuumConditionsMet(e){if(e==null)return!0;let{minDirtCount:t,minDirtFactor:s}=e;return t=t||Ve.minDirtCount,s=s||Ve.minDirtFactor,this.dirtCount>=t&&this.dirtFactor>=s}get isVacuuming(){return this._currentVacuum!=null}get dirtCount(){return this._dirtCount}get dirtFactor(){return this._dirtCount/(1+this._documentCount+this._dirtCount)}has(e){return this._idToShortId.has(e)}getStoredFields(e){const t=this._idToShortId.get(e);if(t!=null)return this._storedFields.get(t)}search(e,t={}){const{searchOptions:s}=this._options,n=Object.assign(Object.assign({},s),t),r=this.executeQuery(e,t),i=[];for(const[o,{score:l,terms:c,match:h}]of r){const m=c.length||1,f={id:this._documentIds.get(o),score:l*m,terms:Object.keys(h),queryTerms:c,match:h};Object.assign(f,this._storedFields.get(o)),(n.filter==null||n.filter(f))&&i.push(f)}return e===ue.wildcard&&n.boostDocument==null||i.sort(pt),i}autoSuggest(e,t={}){t=Object.assign(Object.assign({},this._options.autoSuggestOptions),t);const s=new Map;for(const{score:r,terms:i}of this.search(e,t)){const o=i.join(" "),l=s.get(o);l!=null?(l.score+=r,l.count+=1):s.set(o,{score:r,terms:i,count:1})}const n=[];for(const[r,{score:i,terms:o,count:l}]of s)n.push({suggestion:r,terms:o,score:i/l});return n.sort(pt),n}get documentCount(){return this._documentCount}get termCount(){return this._index.size}static loadJSON(e,t){if(t==null)throw new Error("MiniSearch: loadJSON should be given the same options used when serializing the index");return this.loadJS(JSON.parse(e),t)}static loadJSONAsync(e,t){return ke(this,void 0,void 0,function*(){if(t==null)throw new Error("MiniSearch: loadJSON should be given the same options used when serializing the index");return this.loadJSAsync(JSON.parse(e),t)})}static getDefault(e){if(je.hasOwnProperty(e))return Pe(je,e);throw new Error(`MiniSearch: unknown option "${e}"`)}static loadJS(e,t){const{index:s,documentIds:n,fieldLength:r,storedFields:i,serializationVersion:o}=e,l=this.instantiateMiniSearch(e,t);l._documentIds=Te(n),l._fieldLength=Te(r),l._storedFields=Te(i);for(const[c,h]of l._documentIds)l._idToShortId.set(h,c);for(const[c,h]of s){const m=new Map;for(const f of Object.keys(h)){let b=h[f];o===1&&(b=b.ds),m.set(parseInt(f,10),Te(b))}l._index.set(c,m)}return l}static loadJSAsync(e,t){return ke(this,void 0,void 0,function*(){const{index:s,documentIds:n,fieldLength:r,storedFields:i,serializationVersion:o}=e,l=this.instantiateMiniSearch(e,t);l._documentIds=yield Ie(n),l._fieldLength=yield Ie(r),l._storedFields=yield Ie(i);for(const[h,m]of l._documentIds)l._idToShortId.set(m,h);let c=0;for(const[h,m]of s){const f=new Map;for(const b of Object.keys(m)){let y=m[b];o===1&&(y=y.ds),f.set(parseInt(b,10),yield Ie(y))}++c%1e3===0&&(yield Nt(0)),l._index.set(h,f)}return l})}static instantiateMiniSearch(e,t){const{documentCount:s,nextId:n,fieldIds:r,averageFieldLength:i,dirtCount:o,serializationVersion:l}=e;if(l!==1&&l!==2)throw new Error("MiniSearch: cannot deserialize an index created with an incompatible version");const c=new ue(t);return c._documentCount=s,c._nextId=n,c._idToShortId=new Map,c._fieldIds=r,c._avgFieldLength=i,c._dirtCount=o||0,c._index=new X,c}executeQuery(e,t={}){if(e===ue.wildcard)return this.executeWildcardQuery(t);if(typeof e!="string"){const f=Object.assign(Object.assign(Object.assign({},t),e),{queries:void 0}),b=e.queries.map(y=>this.executeQuery(y,f));return this.combineResults(b,f.combineWith)}const{tokenize:s,processTerm:n,searchOptions:r}=this._options,i=Object.assign(Object.assign({tokenize:s,processTerm:n},r),t),{tokenize:o,processTerm:l}=i,m=o(e).flatMap(f=>l(f)).filter(f=>!!f).map(Us(i)).map(f=>this.executeQuerySpec(f,i));return this.combineResults(m,i.combineWith)}executeQuerySpec(e,t){const s=Object.assign(Object.assign({},this._options.searchOptions),t),n=(s.fields||this._options.fields).reduce((x,w)=>Object.assign(Object.assign({},x),{[w]:Pe(s.boost,w)||1}),{}),{boostDocument:r,weights:i,maxFuzzy:o,bm25:l}=s,{fuzzy:c,prefix:h}=Object.assign(Object.assign({},ht.weights),i),m=this._index.get(e.term),f=this.termResults(e.term,e.term,1,e.termBoost,m,n,r,l);let b,y;if(e.prefix&&(b=this._index.atPrefix(e.term)),e.fuzzy){const x=e.fuzzy===!0?.2:e.fuzzy,w=x<1?Math.min(o,Math.round(e.term.length*x)):x;w&&(y=this._index.fuzzyGet(e.term,w))}if(b)for(const[x,w]of b){const R=x.length-e.term.length;if(!R)continue;y==null||y.delete(x);const A=h*x.length/(x.length+.3*R);this.termResults(e.term,x,A,e.termBoost,w,n,r,l,f)}if(y)for(const x of y.keys()){const[w,R]=y.get(x);if(!R)continue;const A=c*x.length/(x.length+R);this.termResults(e.term,x,A,e.termBoost,w,n,r,l,f)}return f}executeWildcardQuery(e){const t=new Map,s=Object.assign(Object.assign({},this._options.searchOptions),e);for(const[n,r]of this._documentIds){const i=s.boostDocument?s.boostDocument(r,"",this._storedFields.get(n)):1;t.set(n,{score:i,terms:[],match:{}})}return t}combineResults(e,t=Ge){if(e.length===0)return new Map;const s=t.toLowerCase(),n=Ws[s];if(!n)throw new Error(`Invalid combination operator: ${t}`);return e.reduce(n)||new Map}toJSON(){const e=[];for(const[t,s]of this._index){const n={};for(const[r,i]of s)n[r]=Object.fromEntries(i);e.push([t,n])}return{documentCount:this._documentCount,nextId:this._nextId,documentIds:Object.fromEntries(this._documentIds),fieldIds:this._fieldIds,fieldLength:Object.fromEntries(this._fieldLength),averageFieldLength:this._avgFieldLength,storedFields:Object.fromEntries(this._storedFields),dirtCount:this._dirtCount,index:e,serializationVersion:2}}termResults(e,t,s,n,r,i,o,l,c=new Map){if(r==null)return c;for(const h of Object.keys(i)){const m=i[h],f=this._fieldIds[h],b=r.get(f);if(b==null)continue;let y=b.size;const x=this._avgFieldLength[f];for(const w of b.keys()){if(!this._documentIds.has(w)){this.removeTerm(f,w,t),y-=1;continue}const R=o?o(this._documentIds.get(w),t,this._storedFields.get(w)):1;if(!R)continue;const A=b.get(w),J=this._fieldLength.get(w)[f],Q=Js(A,y,this._documentCount,J,x,l),W=s*n*m*R*Q,V=c.get(w);if(V){V.score+=W,Gs(V.terms,e);const $=Pe(V.match,t);$?$.push(h):V.match[t]=[h]}else c.set(w,{score:W,terms:[e],match:{[t]:[h]}})}}return c}addTerm(e,t,s){const n=this._index.fetch(s,vt);let r=n.get(e);if(r==null)r=new Map,r.set(t,1),n.set(e,r);else{const i=r.get(t);r.set(t,(i||0)+1)}}removeTerm(e,t,s){if(!this._index.has(s)){this.warnDocumentChanged(t,e,s);return}const n=this._index.fetch(s,vt),r=n.get(e);r==null||r.get(t)==null?this.warnDocumentChanged(t,e,s):r.get(t)<=1?r.size<=1?n.delete(e):r.delete(t):r.set(t,r.get(t)-1),this._index.get(s).size===0&&this._index.delete(s)}warnDocumentChanged(e,t,s){for(const n of Object.keys(this._fieldIds))if(this._fieldIds[n]===t){this._options.logger("warn",`MiniSearch: document with ID ${this._documentIds.get(e)} has changed before removal: term "${s}" was not present in field "${n}". Removing a document after it has changed can corrupt the index!`,"version_conflict");return}}addDocumentId(e){const t=this._nextId;return this._idToShortId.set(e,t),this._documentIds.set(t,e),this._documentCount+=1,this._nextId+=1,t}addFields(e){for(let t=0;tObject.prototype.hasOwnProperty.call(a,e)?a[e]:void 0,Ws={[Ge]:(a,e)=>{for(const t of e.keys()){const s=a.get(t);if(s==null)a.set(t,e.get(t));else{const{score:n,terms:r,match:i}=e.get(t);s.score=s.score+n,s.match=Object.assign(s.match,i),ft(s.terms,r)}}return a},[kt]:(a,e)=>{const t=new Map;for(const s of e.keys()){const n=a.get(s);if(n==null)continue;const{score:r,terms:i,match:o}=e.get(s);ft(n.terms,i),t.set(s,{score:n.score+r,terms:n.terms,match:Object.assign(n.match,o)})}return t},[Bs]:(a,e)=>{for(const t of e.keys())a.delete(t);return a}},Ks={k:1.2,b:.7,d:.5},Js=(a,e,t,s,n,r)=>{const{k:i,b:o,d:l}=r;return Math.log(1+(t-e+.5)/(e+.5))*(l+a*(i+1)/(a+i*(1-o+o*s/n)))},Us=a=>(e,t,s)=>{const n=typeof a.fuzzy=="function"?a.fuzzy(e,t,s):a.fuzzy||!1,r=typeof a.prefix=="function"?a.prefix(e,t,s):a.prefix===!0,i=typeof a.boostTerm=="function"?a.boostTerm(e,t,s):1;return{term:e,fuzzy:n,prefix:r,termBoost:i}},je={idField:"id",extractField:(a,e)=>a[e],tokenize:a=>a.split(Hs),processTerm:a=>a.toLowerCase(),fields:void 0,searchOptions:void 0,storeFields:[],logger:(a,e)=>{typeof(console==null?void 0:console[a])=="function"&&console[a](e)},autoVacuum:!0},ht={combineWith:Ge,prefix:!1,fuzzy:!1,maxFuzzy:6,boost:{},weights:{fuzzy:.45,prefix:.375},bm25:Ks},qs={combineWith:kt,prefix:(a,e,t)=>e===t.length-1},Je={batchSize:1e3,batchWait:10},Ue={minDirtFactor:.1,minDirtCount:20},Ve=Object.assign(Object.assign({},Je),Ue),Gs=(a,e)=>{a.includes(e)||a.push(e)},ft=(a,e)=>{for(const t of e)a.includes(t)||a.push(t)},pt=({score:a},{score:e})=>e-a,vt=()=>new Map,Te=a=>{const e=new Map;for(const t of Object.keys(a))e.set(parseInt(t,10),a[t]);return e},Ie=a=>ke(void 0,void 0,void 0,function*(){const e=new Map;let t=0;for(const s of Object.keys(a))e.set(parseInt(s,10),a[s]),++t%1e3===0&&(yield Nt(0));return e}),Nt=a=>new Promise(e=>setTimeout(e,a)),Hs=/[\n\r\p{Z}\p{P}]+/u;class Qs{constructor(e=10){Ae(this,"max");Ae(this,"cache");this.max=e,this.cache=new Map}get(e){let t=this.cache.get(e);return t!==void 0&&(this.cache.delete(e),this.cache.set(e,t)),t}set(e,t){this.cache.has(e)?this.cache.delete(e):this.cache.size===this.max&&this.cache.delete(this.first()),this.cache.set(e,t)}first(){return this.cache.keys().next().value}clear(){this.cache.clear()}}const Ys=["aria-owns"],Zs={class:"shell"},Xs=["title"],en={class:"search-actions before"},tn=["title"],sn=["aria-activedescendant","aria-controls","placeholder"],nn={class:"search-actions"},rn=["title"],an=["disabled","title"],on=["id","role","aria-labelledby"],ln=["id","aria-selected"],cn=["href","aria-label","onMouseenter","onFocusin","data-index"],un={class:"titles"},dn=["innerHTML"],hn={class:"title main"},fn=["innerHTML"],pn={key:0,class:"excerpt-wrapper"},vn={key:0,class:"excerpt",inert:""},mn=["innerHTML"],gn={key:0,class:"no-results"},bn={class:"search-keyboard-shortcuts"},yn=["aria-label"],wn=["aria-label"],xn=["aria-label"],_n=["aria-label"],Sn=Lt({__name:"VPLocalSearchBox",emits:["close"],setup(a,{emit:e}){var S,C;const t=e,s=xe(),n=xe(),r=xe(is),i=ss(),{activate:o}=Ds(s,{immediate:!0,allowOutsideClick:!0,clickOutsideDeactivates:!0,escapeDeactivates:!0}),{localeIndex:l,theme:c}=i,h=st(async()=>{var v,p,E,F,z,P,j,I,K;return at(ue.loadJSON((E=await((p=(v=r.value)[l.value])==null?void 0:p.call(v)))==null?void 0:E.default,{fields:["title","titles","text"],storeFields:["title","titles"],searchOptions:{fuzzy:.2,prefix:!0,boost:{title:4,text:2,titles:1},...((F=c.value.search)==null?void 0:F.provider)==="local"&&((P=(z=c.value.search.options)==null?void 0:z.miniSearch)==null?void 0:P.searchOptions)},...((j=c.value.search)==null?void 0:j.provider)==="local"&&((K=(I=c.value.search.options)==null?void 0:I.miniSearch)==null?void 0:K.options)}))}),f=me(()=>{var v,p;return((v=c.value.search)==null?void 0:v.provider)==="local"&&((p=c.value.search.options)==null?void 0:p.disableQueryPersistence)===!0}).value?ie(""):Dt("vitepress:local-search-filter",""),b=zt("vitepress:local-search-detailed-list",((S=c.value.search)==null?void 0:S.provider)==="local"&&((C=c.value.search.options)==null?void 0:C.detailedView)===!0),y=me(()=>{var v,p,E;return((v=c.value.search)==null?void 0:v.provider)==="local"&&(((p=c.value.search.options)==null?void 0:p.disableDetailedView)===!0||((E=c.value.search.options)==null?void 0:E.detailedView)===!1)}),x=me(()=>{var p,E,F,z,P,j,I;const v=((p=c.value.search)==null?void 0:p.options)??c.value.algolia;return((P=(z=(F=(E=v==null?void 0:v.locales)==null?void 0:E[l.value])==null?void 0:F.translations)==null?void 0:z.button)==null?void 0:P.buttonText)||((I=(j=v==null?void 0:v.translations)==null?void 0:j.button)==null?void 0:I.buttonText)||"Search"});Pt(()=>{y.value&&(b.value=!1)});const w=xe([]),R=ie(!1);$e(f,()=>{R.value=!1});const A=st(async()=>{if(n.value)return at(new Ps(n.value))},null),J=new Qs(16);jt(()=>[h.value,f.value,b.value],async([v,p,E],F,z)=>{var ee,ye,He,Qe;(F==null?void 0:F[0])!==v&&J.clear();let P=!1;if(z(()=>{P=!0}),!v)return;w.value=v.search(p).slice(0,16),R.value=!0;const j=E?await Promise.all(w.value.map(B=>Q(B.id))):[];if(P)return;for(const{id:B,mod:te}of j){const se=B.slice(0,B.indexOf("#"));let Y=J.get(se);if(Y)continue;Y=new Map,J.set(se,Y);const G=te.default??te;if(G!=null&&G.render||G!=null&&G.setup){const ne=Yt(G);ne.config.warnHandler=()=>{},ne.provide(Zt,i),Object.defineProperties(ne.config.globalProperties,{$frontmatter:{get(){return i.frontmatter.value}},$params:{get(){return i.page.value.params}}});const Ye=document.createElement("div");ne.mount(Ye),Ye.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach(de=>{var et;const we=(et=de.querySelector("a"))==null?void 0:et.getAttribute("href"),Ze=(we==null?void 0:we.startsWith("#"))&&we.slice(1);if(!Ze)return;let Xe="";for(;(de=de.nextElementSibling)&&!/^h[1-6]$/i.test(de.tagName);)Xe+=de.outerHTML;Y.set(Ze,Xe)}),ne.unmount()}if(P)return}const I=new Set;if(w.value=w.value.map(B=>{const[te,se]=B.id.split("#"),Y=J.get(te),G=(Y==null?void 0:Y.get(se))??"";for(const ne in B.match)I.add(ne);return{...B,text:G}}),await he(),P)return;await new Promise(B=>{var te;(te=A.value)==null||te.unmark({done:()=>{var se;(se=A.value)==null||se.markRegExp(k(I),{done:B})}})});const K=((ee=s.value)==null?void 0:ee.querySelectorAll(".result .excerpt"))??[];for(const B of K)(ye=B.querySelector('mark[data-markjs="true"]'))==null||ye.scrollIntoView({block:"center"});(Qe=(He=n.value)==null?void 0:He.firstElementChild)==null||Qe.scrollIntoView({block:"start"})},{debounce:200,immediate:!0});async function Q(v){const p=Xt(v.slice(0,v.indexOf("#")));try{if(!p)throw new Error(`Cannot find file for id: ${v}`);return{id:v,mod:await import(p)}}catch(E){return console.error(E),{id:v,mod:{}}}}const W=ie(),V=me(()=>{var v;return((v=f.value)==null?void 0:v.length)<=0});function $(v=!0){var p,E;(p=W.value)==null||p.focus(),v&&((E=W.value)==null||E.select())}Me(()=>{$()});function be(v){v.pointerType==="mouse"&&$()}const M=ie(-1),U=ie(!0);$e(w,v=>{M.value=v.length?0:-1,q()});function q(){he(()=>{const v=document.querySelector(".result.selected");v==null||v.scrollIntoView({block:"nearest"})})}_e("ArrowUp",v=>{v.preventDefault(),M.value--,M.value<0&&(M.value=w.value.length-1),U.value=!0,q()}),_e("ArrowDown",v=>{v.preventDefault(),M.value++,M.value>=w.value.length&&(M.value=0),U.value=!0,q()});const N=Vt();_e("Enter",v=>{if(v.isComposing||v.target instanceof HTMLButtonElement&&v.target.type!=="submit")return;const p=w.value[M.value];if(v.target instanceof HTMLInputElement&&!p){v.preventDefault();return}p&&(N.go(p.id),t("close"))}),_e("Escape",()=>{t("close")});const d=ns({modal:{displayDetails:"Display detailed list",resetButtonTitle:"Reset search",backButtonTitle:"Close search",noResultsText:"No results for",footer:{selectText:"to select",selectKeyAriaLabel:"enter",navigateText:"to navigate",navigateUpKeyAriaLabel:"up arrow",navigateDownKeyAriaLabel:"down arrow",closeText:"to close",closeKeyAriaLabel:"escape"}}});Me(()=>{window.history.pushState(null,"",null)}),$t("popstate",v=>{v.preventDefault(),t("close")});const g=Bt(Wt?document.body:null);Me(()=>{he(()=>{g.value=!0,he().then(()=>o())})}),Kt(()=>{g.value=!1});function T(){f.value="",he().then(()=>$(!1))}function k(v){return new RegExp([...v].sort((p,E)=>E.length-p.length).map(p=>`(${es(p)})`).join("|"),"gi")}function O(v){var F;if(!U.value)return;const p=(F=v.target)==null?void 0:F.closest(".result"),E=Number.parseInt(p==null?void 0:p.dataset.index);E>=0&&E!==M.value&&(M.value=E),U.value=!1}return(v,p)=>{var E,F,z,P,j;return H(),Jt(Qt,{to:"body"},[_("div",{ref_key:"el",ref:s,role:"button","aria-owns":(E=w.value)!=null&&E.length?"localsearch-list":void 0,"aria-expanded":"true","aria-haspopup":"listbox","aria-labelledby":"localsearch-label",class:"VPLocalSearchBox"},[_("div",{class:"backdrop",onClick:p[0]||(p[0]=I=>v.$emit("close"))}),_("div",Zs,[_("form",{class:"search-bar",onPointerup:p[4]||(p[4]=I=>be(I)),onSubmit:p[5]||(p[5]=Ut(()=>{},["prevent"]))},[_("label",{title:x.value,id:"localsearch-label",for:"localsearch-input"},p[7]||(p[7]=[_("span",{"aria-hidden":"true",class:"vpi-search search-icon local-search-icon"},null,-1)]),8,Xs),_("div",en,[_("button",{class:"back-button",title:L(d)("modal.backButtonTitle"),onClick:p[1]||(p[1]=I=>v.$emit("close"))},p[8]||(p[8]=[_("span",{class:"vpi-arrow-left local-search-icon"},null,-1)]),8,tn)]),qt(_("input",{ref_key:"searchInput",ref:W,"onUpdate:modelValue":p[2]||(p[2]=I=>Ht(f)?f.value=I:null),"aria-activedescendant":M.value>-1?"localsearch-item-"+M.value:void 0,"aria-autocomplete":"both","aria-controls":(F=w.value)!=null&&F.length?"localsearch-list":void 0,"aria-labelledby":"localsearch-label",autocapitalize:"off",autocomplete:"off",autocorrect:"off",class:"search-input",id:"localsearch-input",enterkeyhint:"go",maxlength:"64",placeholder:x.value,spellcheck:"false",type:"search"},null,8,sn),[[Gt,L(f)]]),_("div",nn,[y.value?Se("",!0):(H(),Z("button",{key:0,class:nt(["toggle-layout-button",{"detailed-list":L(b)}]),type:"button",title:L(d)("modal.displayDetails"),onClick:p[3]||(p[3]=I=>M.value>-1&&(b.value=!L(b)))},p[9]||(p[9]=[_("span",{class:"vpi-layout-list local-search-icon"},null,-1)]),10,rn)),_("button",{class:"clear-button",type:"reset",disabled:V.value,title:L(d)("modal.resetButtonTitle"),onClick:T},p[10]||(p[10]=[_("span",{class:"vpi-delete local-search-icon"},null,-1)]),8,an)])],32),_("ul",{ref_key:"resultsEl",ref:n,id:(z=w.value)!=null&&z.length?"localsearch-list":void 0,role:(P=w.value)!=null&&P.length?"listbox":void 0,"aria-labelledby":(j=w.value)!=null&&j.length?"localsearch-label":void 0,class:"results",onMousemove:O},[(H(!0),Z(rt,null,it(w.value,(I,K)=>(H(),Z("li",{key:I.id,id:"localsearch-item-"+K,"aria-selected":M.value===K?"true":"false",role:"option"},[_("a",{href:I.id,class:nt(["result",{selected:M.value===K}]),"aria-label":[...I.titles,I.title].join(" > "),onMouseenter:ee=>!U.value&&(M.value=K),onFocusin:ee=>M.value=K,onClick:p[6]||(p[6]=ee=>v.$emit("close")),"data-index":K},[_("div",null,[_("div",un,[p[12]||(p[12]=_("span",{class:"title-icon"},"#",-1)),(H(!0),Z(rt,null,it(I.titles,(ee,ye)=>(H(),Z("span",{key:ye,class:"title"},[_("span",{class:"text",innerHTML:ee},null,8,dn),p[11]||(p[11]=_("span",{class:"vpi-chevron-right local-search-icon"},null,-1))]))),128)),_("span",hn,[_("span",{class:"text",innerHTML:I.title},null,8,fn)])]),L(b)?(H(),Z("div",pn,[I.text?(H(),Z("div",vn,[_("div",{class:"vp-doc",innerHTML:I.text},null,8,mn)])):Se("",!0),p[13]||(p[13]=_("div",{class:"excerpt-gradient-bottom"},null,-1)),p[14]||(p[14]=_("div",{class:"excerpt-gradient-top"},null,-1))])):Se("",!0)])],42,cn)],8,ln))),128)),L(f)&&!w.value.length&&R.value?(H(),Z("li",gn,[fe(pe(L(d)("modal.noResultsText"))+' "',1),_("strong",null,pe(L(f)),1),p[15]||(p[15]=fe('" '))])):Se("",!0)],40,on),_("div",bn,[_("span",null,[_("kbd",{"aria-label":L(d)("modal.footer.navigateUpKeyAriaLabel")},p[16]||(p[16]=[_("span",{class:"vpi-arrow-up navigate-icon"},null,-1)]),8,yn),_("kbd",{"aria-label":L(d)("modal.footer.navigateDownKeyAriaLabel")},p[17]||(p[17]=[_("span",{class:"vpi-arrow-down navigate-icon"},null,-1)]),8,wn),fe(" "+pe(L(d)("modal.footer.navigateText")),1)]),_("span",null,[_("kbd",{"aria-label":L(d)("modal.footer.selectKeyAriaLabel")},p[18]||(p[18]=[_("span",{class:"vpi-corner-down-left navigate-icon"},null,-1)]),8,xn),fe(" "+pe(L(d)("modal.footer.selectText")),1)]),_("span",null,[_("kbd",{"aria-label":L(d)("modal.footer.closeKeyAriaLabel")},"esc",8,_n),fe(" "+pe(L(d)("modal.footer.closeText")),1)])])])],8,Ys)])}}}),Fn=ts(Sn,[["__scopeId","data-v-42e65fb9"]]);export{Fn as default}; diff --git a/previews/PR239/assets/chunks/framework.onQNwZ2I.js b/previews/PR239/assets/chunks/framework.onQNwZ2I.js new file mode 100644 index 000000000..d11bd04b5 --- /dev/null +++ b/previews/PR239/assets/chunks/framework.onQNwZ2I.js @@ -0,0 +1,18 @@ +/** +* @vue/shared v3.5.13 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**//*! #__NO_SIDE_EFFECTS__ */function Ns(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const Z={},Et=[],ke=()=>{},Ko=()=>!1,en=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),Fs=e=>e.startsWith("onUpdate:"),ae=Object.assign,Hs=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},qo=Object.prototype.hasOwnProperty,z=(e,t)=>qo.call(e,t),W=Array.isArray,Tt=e=>In(e)==="[object Map]",ii=e=>In(e)==="[object Set]",q=e=>typeof e=="function",re=e=>typeof e=="string",Xe=e=>typeof e=="symbol",ne=e=>e!==null&&typeof e=="object",oi=e=>(ne(e)||q(e))&&q(e.then)&&q(e.catch),li=Object.prototype.toString,In=e=>li.call(e),Go=e=>In(e).slice(8,-1),ci=e=>In(e)==="[object Object]",$s=e=>re(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Ct=Ns(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Nn=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},Yo=/-(\w)/g,Le=Nn(e=>e.replace(Yo,(t,n)=>n?n.toUpperCase():"")),Xo=/\B([A-Z])/g,st=Nn(e=>e.replace(Xo,"-$1").toLowerCase()),Fn=Nn(e=>e.charAt(0).toUpperCase()+e.slice(1)),_n=Nn(e=>e?`on${Fn(e)}`:""),tt=(e,t)=>!Object.is(e,t),bn=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:s,value:n})},vs=e=>{const t=parseFloat(e);return isNaN(t)?e:t},Jo=e=>{const t=re(e)?Number(e):NaN;return isNaN(t)?e:t};let ar;const Hn=()=>ar||(ar=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Ds(e){if(W(e)){const t={};for(let n=0;n{if(n){const s=n.split(Qo);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function js(e){let t="";if(re(e))t=e;else if(W(e))for(let n=0;n!!(e&&e.__v_isRef===!0),sl=e=>re(e)?e:e==null?"":W(e)||ne(e)&&(e.toString===li||!q(e.toString))?ui(e)?sl(e.value):JSON.stringify(e,di,2):String(e),di=(e,t)=>ui(t)?di(e,t.value):Tt(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,r],i)=>(n[zn(s,i)+" =>"]=r,n),{})}:ii(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>zn(n))}:Xe(t)?zn(t):ne(t)&&!W(t)&&!ci(t)?String(t):t,zn=(e,t="")=>{var n;return Xe(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/** +* @vue/reactivity v3.5.13 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let we;class rl{constructor(t=!1){this.detached=t,this._active=!0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.parent=we,!t&&we&&(this.index=(we.scopes||(we.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes)for(t=0,n=this.scopes.length;t0)return;if(jt){let t=jt;for(jt=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;Dt;){let t=Dt;for(Dt=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(s){e||(e=s)}t=n}}if(e)throw e}function yi(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function vi(e){let t,n=e.depsTail,s=n;for(;s;){const r=s.prevDep;s.version===-1?(s===n&&(n=r),ks(s),ol(s)):t=s,s.dep.activeLink=s.prevActiveLink,s.prevActiveLink=void 0,s=r}e.deps=t,e.depsTail=n}function _s(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(_i(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function _i(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Kt))return;e.globalVersion=Kt;const t=e.dep;if(e.flags|=2,t.version>0&&!e.isSSR&&e.deps&&!_s(e)){e.flags&=-3;return}const n=te,s=Ne;te=e,Ne=!0;try{yi(e);const r=e.fn(e._value);(t.version===0||tt(r,e._value))&&(e._value=r,t.version++)}catch(r){throw t.version++,r}finally{te=n,Ne=s,vi(e),e.flags&=-3}}function ks(e,t=!1){const{dep:n,prevSub:s,nextSub:r}=e;if(s&&(s.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=s,e.nextSub=void 0),n.subs===e&&(n.subs=s,!s&&n.computed)){n.computed.flags&=-5;for(let i=n.computed.deps;i;i=i.nextDep)ks(i,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function ol(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let Ne=!0;const bi=[];function rt(){bi.push(Ne),Ne=!1}function it(){const e=bi.pop();Ne=e===void 0?!0:e}function fr(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=te;te=void 0;try{t()}finally{te=n}}}let Kt=0;class ll{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class $n{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0}track(t){if(!te||!Ne||te===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==te)n=this.activeLink=new ll(te,this),te.deps?(n.prevDep=te.depsTail,te.depsTail.nextDep=n,te.depsTail=n):te.deps=te.depsTail=n,wi(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const s=n.nextDep;s.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=s),n.prevDep=te.depsTail,n.nextDep=void 0,te.depsTail.nextDep=n,te.depsTail=n,te.deps===n&&(te.deps=s)}return n}trigger(t){this.version++,Kt++,this.notify(t)}notify(t){Vs();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{Us()}}}function wi(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let s=t.deps;s;s=s.nextDep)wi(s)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const Cn=new WeakMap,dt=Symbol(""),bs=Symbol(""),qt=Symbol("");function me(e,t,n){if(Ne&&te){let s=Cn.get(e);s||Cn.set(e,s=new Map);let r=s.get(n);r||(s.set(n,r=new $n),r.map=s,r.key=n),r.track()}}function Ge(e,t,n,s,r,i){const o=Cn.get(e);if(!o){Kt++;return}const l=c=>{c&&c.trigger()};if(Vs(),t==="clear")o.forEach(l);else{const c=W(e),f=c&&$s(n);if(c&&n==="length"){const a=Number(s);o.forEach((h,y)=>{(y==="length"||y===qt||!Xe(y)&&y>=a)&&l(h)})}else switch((n!==void 0||o.has(void 0))&&l(o.get(n)),f&&l(o.get(qt)),t){case"add":c?f&&l(o.get("length")):(l(o.get(dt)),Tt(e)&&l(o.get(bs)));break;case"delete":c||(l(o.get(dt)),Tt(e)&&l(o.get(bs)));break;case"set":Tt(e)&&l(o.get(dt));break}}Us()}function cl(e,t){const n=Cn.get(e);return n&&n.get(t)}function _t(e){const t=J(e);return t===e?t:(me(t,"iterate",qt),Pe(e)?t:t.map(ye))}function Dn(e){return me(e=J(e),"iterate",qt),e}const al={__proto__:null,[Symbol.iterator](){return Zn(this,Symbol.iterator,ye)},concat(...e){return _t(this).concat(...e.map(t=>W(t)?_t(t):t))},entries(){return Zn(this,"entries",e=>(e[1]=ye(e[1]),e))},every(e,t){return We(this,"every",e,t,void 0,arguments)},filter(e,t){return We(this,"filter",e,t,n=>n.map(ye),arguments)},find(e,t){return We(this,"find",e,t,ye,arguments)},findIndex(e,t){return We(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return We(this,"findLast",e,t,ye,arguments)},findLastIndex(e,t){return We(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return We(this,"forEach",e,t,void 0,arguments)},includes(...e){return es(this,"includes",e)},indexOf(...e){return es(this,"indexOf",e)},join(e){return _t(this).join(e)},lastIndexOf(...e){return es(this,"lastIndexOf",e)},map(e,t){return We(this,"map",e,t,void 0,arguments)},pop(){return Ft(this,"pop")},push(...e){return Ft(this,"push",e)},reduce(e,...t){return ur(this,"reduce",e,t)},reduceRight(e,...t){return ur(this,"reduceRight",e,t)},shift(){return Ft(this,"shift")},some(e,t){return We(this,"some",e,t,void 0,arguments)},splice(...e){return Ft(this,"splice",e)},toReversed(){return _t(this).toReversed()},toSorted(e){return _t(this).toSorted(e)},toSpliced(...e){return _t(this).toSpliced(...e)},unshift(...e){return Ft(this,"unshift",e)},values(){return Zn(this,"values",ye)}};function Zn(e,t,n){const s=Dn(e),r=s[t]();return s!==e&&!Pe(e)&&(r._next=r.next,r.next=()=>{const i=r._next();return i.value&&(i.value=n(i.value)),i}),r}const fl=Array.prototype;function We(e,t,n,s,r,i){const o=Dn(e),l=o!==e&&!Pe(e),c=o[t];if(c!==fl[t]){const h=c.apply(e,i);return l?ye(h):h}let f=n;o!==e&&(l?f=function(h,y){return n.call(this,ye(h),y,e)}:n.length>2&&(f=function(h,y){return n.call(this,h,y,e)}));const a=c.call(o,f,s);return l&&r?r(a):a}function ur(e,t,n,s){const r=Dn(e);let i=n;return r!==e&&(Pe(e)?n.length>3&&(i=function(o,l,c){return n.call(this,o,l,c,e)}):i=function(o,l,c){return n.call(this,o,ye(l),c,e)}),r[t](i,...s)}function es(e,t,n){const s=J(e);me(s,"iterate",qt);const r=s[t](...n);return(r===-1||r===!1)&&Ks(n[0])?(n[0]=J(n[0]),s[t](...n)):r}function Ft(e,t,n=[]){rt(),Vs();const s=J(e)[t].apply(e,n);return Us(),it(),s}const ul=Ns("__proto__,__v_isRef,__isVue"),Si=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Xe));function dl(e){Xe(e)||(e=String(e));const t=J(this);return me(t,"has",e),t.hasOwnProperty(e)}class xi{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,s){if(n==="__v_skip")return t.__v_skip;const r=this._isReadonly,i=this._isShallow;if(n==="__v_isReactive")return!r;if(n==="__v_isReadonly")return r;if(n==="__v_isShallow")return i;if(n==="__v_raw")return s===(r?i?Sl:Ai:i?Ci:Ti).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const o=W(t);if(!r){let c;if(o&&(c=al[n]))return c;if(n==="hasOwnProperty")return dl}const l=Reflect.get(t,n,fe(t)?t:s);return(Xe(n)?Si.has(n):ul(n))||(r||me(t,"get",n),i)?l:fe(l)?o&&$s(n)?l:l.value:ne(l)?r?Vn(l):jn(l):l}}class Ei extends xi{constructor(t=!1){super(!1,t)}set(t,n,s,r){let i=t[n];if(!this._isShallow){const c=yt(i);if(!Pe(s)&&!yt(s)&&(i=J(i),s=J(s)),!W(t)&&fe(i)&&!fe(s))return c?!1:(i.value=s,!0)}const o=W(t)&&$s(n)?Number(n)e,cn=e=>Reflect.getPrototypeOf(e);function yl(e,t,n){return function(...s){const r=this.__v_raw,i=J(r),o=Tt(i),l=e==="entries"||e===Symbol.iterator&&o,c=e==="keys"&&o,f=r[e](...s),a=n?ws:t?Ss:ye;return!t&&me(i,"iterate",c?bs:dt),{next(){const{value:h,done:y}=f.next();return y?{value:h,done:y}:{value:l?[a(h[0]),a(h[1])]:a(h),done:y}},[Symbol.iterator](){return this}}}}function an(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function vl(e,t){const n={get(r){const i=this.__v_raw,o=J(i),l=J(r);e||(tt(r,l)&&me(o,"get",r),me(o,"get",l));const{has:c}=cn(o),f=t?ws:e?Ss:ye;if(c.call(o,r))return f(i.get(r));if(c.call(o,l))return f(i.get(l));i!==o&&i.get(r)},get size(){const r=this.__v_raw;return!e&&me(J(r),"iterate",dt),Reflect.get(r,"size",r)},has(r){const i=this.__v_raw,o=J(i),l=J(r);return e||(tt(r,l)&&me(o,"has",r),me(o,"has",l)),r===l?i.has(r):i.has(r)||i.has(l)},forEach(r,i){const o=this,l=o.__v_raw,c=J(l),f=t?ws:e?Ss:ye;return!e&&me(c,"iterate",dt),l.forEach((a,h)=>r.call(i,f(a),f(h),o))}};return ae(n,e?{add:an("add"),set:an("set"),delete:an("delete"),clear:an("clear")}:{add(r){!t&&!Pe(r)&&!yt(r)&&(r=J(r));const i=J(this);return cn(i).has.call(i,r)||(i.add(r),Ge(i,"add",r,r)),this},set(r,i){!t&&!Pe(i)&&!yt(i)&&(i=J(i));const o=J(this),{has:l,get:c}=cn(o);let f=l.call(o,r);f||(r=J(r),f=l.call(o,r));const a=c.call(o,r);return o.set(r,i),f?tt(i,a)&&Ge(o,"set",r,i):Ge(o,"add",r,i),this},delete(r){const i=J(this),{has:o,get:l}=cn(i);let c=o.call(i,r);c||(r=J(r),c=o.call(i,r)),l&&l.call(i,r);const f=i.delete(r);return c&&Ge(i,"delete",r,void 0),f},clear(){const r=J(this),i=r.size!==0,o=r.clear();return i&&Ge(r,"clear",void 0,void 0),o}}),["keys","values","entries",Symbol.iterator].forEach(r=>{n[r]=yl(r,e,t)}),n}function Bs(e,t){const n=vl(e,t);return(s,r,i)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?s:Reflect.get(z(n,r)&&r in s?n:s,r,i)}const _l={get:Bs(!1,!1)},bl={get:Bs(!1,!0)},wl={get:Bs(!0,!1)};const Ti=new WeakMap,Ci=new WeakMap,Ai=new WeakMap,Sl=new WeakMap;function xl(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function El(e){return e.__v_skip||!Object.isExtensible(e)?0:xl(Go(e))}function jn(e){return yt(e)?e:Ws(e,!1,pl,_l,Ti)}function Tl(e){return Ws(e,!1,ml,bl,Ci)}function Vn(e){return Ws(e,!0,gl,wl,Ai)}function Ws(e,t,n,s,r){if(!ne(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const i=r.get(e);if(i)return i;const o=El(e);if(o===0)return e;const l=new Proxy(e,o===2?s:n);return r.set(e,l),l}function ht(e){return yt(e)?ht(e.__v_raw):!!(e&&e.__v_isReactive)}function yt(e){return!!(e&&e.__v_isReadonly)}function Pe(e){return!!(e&&e.__v_isShallow)}function Ks(e){return e?!!e.__v_raw:!1}function J(e){const t=e&&e.__v_raw;return t?J(t):e}function wn(e){return!z(e,"__v_skip")&&Object.isExtensible(e)&&ai(e,"__v_skip",!0),e}const ye=e=>ne(e)?jn(e):e,Ss=e=>ne(e)?Vn(e):e;function fe(e){return e?e.__v_isRef===!0:!1}function oe(e){return Ri(e,!1)}function qs(e){return Ri(e,!0)}function Ri(e,t){return fe(e)?e:new Cl(e,t)}class Cl{constructor(t,n){this.dep=new $n,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:J(t),this._value=n?t:ye(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,s=this.__v_isShallow||Pe(t)||yt(t);t=s?t:J(t),tt(t,n)&&(this._rawValue=t,this._value=s?t:ye(t),this.dep.trigger())}}function Oi(e){return fe(e)?e.value:e}const Al={get:(e,t,n)=>t==="__v_raw"?e:Oi(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const r=e[t];return fe(r)&&!fe(n)?(r.value=n,!0):Reflect.set(e,t,n,s)}};function Mi(e){return ht(e)?e:new Proxy(e,Al)}class Rl{constructor(t){this.__v_isRef=!0,this._value=void 0;const n=this.dep=new $n,{get:s,set:r}=t(n.track.bind(n),n.trigger.bind(n));this._get=s,this._set=r}get value(){return this._value=this._get()}set value(t){this._set(t)}}function Ol(e){return new Rl(e)}class Ml{constructor(t,n,s){this._object=t,this._key=n,this._defaultValue=s,this.__v_isRef=!0,this._value=void 0}get value(){const t=this._object[this._key];return this._value=t===void 0?this._defaultValue:t}set value(t){this._object[this._key]=t}get dep(){return cl(J(this._object),this._key)}}class Pl{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0,this._value=void 0}get value(){return this._value=this._getter()}}function Ll(e,t,n){return fe(e)?e:q(e)?new Pl(e):ne(e)&&arguments.length>1?Il(e,t,n):oe(e)}function Il(e,t,n){const s=e[t];return fe(s)?s:new Ml(e,t,n)}class Nl{constructor(t,n,s){this.fn=t,this.setter=n,this._value=void 0,this.dep=new $n(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Kt-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=s}notify(){if(this.flags|=16,!(this.flags&8)&&te!==this)return mi(this,!0),!0}get value(){const t=this.dep.track();return _i(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function Fl(e,t,n=!1){let s,r;return q(e)?s=e:(s=e.get,r=e.set),new Nl(s,r,n)}const fn={},An=new WeakMap;let ft;function Hl(e,t=!1,n=ft){if(n){let s=An.get(n);s||An.set(n,s=[]),s.push(e)}}function $l(e,t,n=Z){const{immediate:s,deep:r,once:i,scheduler:o,augmentJob:l,call:c}=n,f=g=>r?g:Pe(g)||r===!1||r===0?Ye(g,1):Ye(g);let a,h,y,v,S=!1,_=!1;if(fe(e)?(h=()=>e.value,S=Pe(e)):ht(e)?(h=()=>f(e),S=!0):W(e)?(_=!0,S=e.some(g=>ht(g)||Pe(g)),h=()=>e.map(g=>{if(fe(g))return g.value;if(ht(g))return f(g);if(q(g))return c?c(g,2):g()})):q(e)?t?h=c?()=>c(e,2):e:h=()=>{if(y){rt();try{y()}finally{it()}}const g=ft;ft=a;try{return c?c(e,3,[v]):e(v)}finally{ft=g}}:h=ke,t&&r){const g=h,O=r===!0?1/0:r;h=()=>Ye(g(),O)}const K=hi(),N=()=>{a.stop(),K&&K.active&&Hs(K.effects,a)};if(i&&t){const g=t;t=(...O)=>{g(...O),N()}}let j=_?new Array(e.length).fill(fn):fn;const p=g=>{if(!(!(a.flags&1)||!a.dirty&&!g))if(t){const O=a.run();if(r||S||(_?O.some((F,$)=>tt(F,j[$])):tt(O,j))){y&&y();const F=ft;ft=a;try{const $=[O,j===fn?void 0:_&&j[0]===fn?[]:j,v];c?c(t,3,$):t(...$),j=O}finally{ft=F}}}else a.run()};return l&&l(p),a=new pi(h),a.scheduler=o?()=>o(p,!1):p,v=g=>Hl(g,!1,a),y=a.onStop=()=>{const g=An.get(a);if(g){if(c)c(g,4);else for(const O of g)O();An.delete(a)}},t?s?p(!0):j=a.run():o?o(p.bind(null,!0),!0):a.run(),N.pause=a.pause.bind(a),N.resume=a.resume.bind(a),N.stop=N,N}function Ye(e,t=1/0,n){if(t<=0||!ne(e)||e.__v_skip||(n=n||new Set,n.has(e)))return e;if(n.add(e),t--,fe(e))Ye(e.value,t,n);else if(W(e))for(let s=0;s{Ye(s,t,n)});else if(ci(e)){for(const s in e)Ye(e[s],t,n);for(const s of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,s)&&Ye(e[s],t,n)}return e}/** +* @vue/runtime-core v3.5.13 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function tn(e,t,n,s){try{return s?e(...s):e()}catch(r){nn(r,t,n)}}function He(e,t,n,s){if(q(e)){const r=tn(e,t,n,s);return r&&oi(r)&&r.catch(i=>{nn(i,t,n)}),r}if(W(e)){const r=[];for(let i=0;i>>1,r=Se[s],i=Gt(r);i=Gt(n)?Se.push(e):Se.splice(jl(t),0,e),e.flags|=1,Li()}}function Li(){Rn||(Rn=Pi.then(Ii))}function Vl(e){W(e)?At.push(...e):Qe&&e.id===-1?Qe.splice(wt+1,0,e):e.flags&1||(At.push(e),e.flags|=1),Li()}function dr(e,t,n=Ve+1){for(;nGt(n)-Gt(s));if(At.length=0,Qe){Qe.push(...t);return}for(Qe=t,wt=0;wte.id==null?e.flags&2?-1:1/0:e.id;function Ii(e){try{for(Ve=0;Ve{s._d&&Ar(-1);const i=Mn(t);let o;try{o=e(...r)}finally{Mn(i),s._d&&Ar(1)}return o};return s._n=!0,s._c=!0,s._d=!0,s}function _f(e,t){if(de===null)return e;const n=Gn(de),s=e.dirs||(e.dirs=[]);for(let r=0;re.__isTeleport,Vt=e=>e&&(e.disabled||e.disabled===""),hr=e=>e&&(e.defer||e.defer===""),pr=e=>typeof SVGElement<"u"&&e instanceof SVGElement,gr=e=>typeof MathMLElement=="function"&&e instanceof MathMLElement,xs=(e,t)=>{const n=e&&e.to;return re(n)?t?t(n):null:n},$i={name:"Teleport",__isTeleport:!0,process(e,t,n,s,r,i,o,l,c,f){const{mc:a,pc:h,pbc:y,o:{insert:v,querySelector:S,createText:_,createComment:K}}=f,N=Vt(t.props);let{shapeFlag:j,children:p,dynamicChildren:g}=t;if(e==null){const O=t.el=_(""),F=t.anchor=_("");v(O,n,s),v(F,n,s);const $=(R,b)=>{j&16&&(r&&r.isCE&&(r.ce._teleportTarget=R),a(p,R,b,r,i,o,l,c))},V=()=>{const R=t.target=xs(t.props,S),b=Di(R,t,_,v);R&&(o!=="svg"&&pr(R)?o="svg":o!=="mathml"&&gr(R)&&(o="mathml"),N||($(R,b),Sn(t,!1)))};N&&($(n,F),Sn(t,!0)),hr(t.props)?be(()=>{V(),t.el.__isMounted=!0},i):V()}else{if(hr(t.props)&&!e.el.__isMounted){be(()=>{$i.process(e,t,n,s,r,i,o,l,c,f),delete e.el.__isMounted},i);return}t.el=e.el,t.targetStart=e.targetStart;const O=t.anchor=e.anchor,F=t.target=e.target,$=t.targetAnchor=e.targetAnchor,V=Vt(e.props),R=V?n:F,b=V?O:$;if(o==="svg"||pr(F)?o="svg":(o==="mathml"||gr(F))&&(o="mathml"),g?(y(e.dynamicChildren,g,R,r,i,o,l),Qs(e,t,!0)):c||h(e,t,R,b,r,i,o,l,!1),N)V?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):un(t,n,O,f,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){const I=t.target=xs(t.props,S);I&&un(t,I,null,f,0)}else V&&un(t,F,$,f,1);Sn(t,N)}},remove(e,t,n,{um:s,o:{remove:r}},i){const{shapeFlag:o,children:l,anchor:c,targetStart:f,targetAnchor:a,target:h,props:y}=e;if(h&&(r(f),r(a)),i&&r(c),o&16){const v=i||!Vt(y);for(let S=0;S{e.isMounted=!0}),Ki(()=>{e.isUnmounting=!0}),e}const Re=[Function,Array],ji={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:Re,onEnter:Re,onAfterEnter:Re,onEnterCancelled:Re,onBeforeLeave:Re,onLeave:Re,onAfterLeave:Re,onLeaveCancelled:Re,onBeforeAppear:Re,onAppear:Re,onAfterAppear:Re,onAppearCancelled:Re},Vi=e=>{const t=e.subTree;return t.component?Vi(t.component):t},Wl={name:"BaseTransition",props:ji,setup(e,{slots:t}){const n=qn(),s=Bl();return()=>{const r=t.default&&Bi(t.default(),!0);if(!r||!r.length)return;const i=Ui(r),o=J(e),{mode:l}=o;if(s.isLeaving)return ts(i);const c=mr(i);if(!c)return ts(i);let f=Es(c,o,s,n,h=>f=h);c.type!==ve&&Yt(c,f);let a=n.subTree&&mr(n.subTree);if(a&&a.type!==ve&&!ut(c,a)&&Vi(n).type!==ve){let h=Es(a,o,s,n);if(Yt(a,h),l==="out-in"&&c.type!==ve)return s.isLeaving=!0,h.afterLeave=()=>{s.isLeaving=!1,n.job.flags&8||n.update(),delete h.afterLeave,a=void 0},ts(i);l==="in-out"&&c.type!==ve?h.delayLeave=(y,v,S)=>{const _=ki(s,a);_[String(a.key)]=a,y[Ze]=()=>{v(),y[Ze]=void 0,delete f.delayedLeave,a=void 0},f.delayedLeave=()=>{S(),delete f.delayedLeave,a=void 0}}:a=void 0}else a&&(a=void 0);return i}}};function Ui(e){let t=e[0];if(e.length>1){for(const n of e)if(n.type!==ve){t=n;break}}return t}const Kl=Wl;function ki(e,t){const{leavingVNodes:n}=e;let s=n.get(t.type);return s||(s=Object.create(null),n.set(t.type,s)),s}function Es(e,t,n,s,r){const{appear:i,mode:o,persisted:l=!1,onBeforeEnter:c,onEnter:f,onAfterEnter:a,onEnterCancelled:h,onBeforeLeave:y,onLeave:v,onAfterLeave:S,onLeaveCancelled:_,onBeforeAppear:K,onAppear:N,onAfterAppear:j,onAppearCancelled:p}=t,g=String(e.key),O=ki(n,e),F=(R,b)=>{R&&He(R,s,9,b)},$=(R,b)=>{const I=b[1];F(R,b),W(R)?R.every(x=>x.length<=1)&&I():R.length<=1&&I()},V={mode:o,persisted:l,beforeEnter(R){let b=c;if(!n.isMounted)if(i)b=K||c;else return;R[Ze]&&R[Ze](!0);const I=O[g];I&&ut(e,I)&&I.el[Ze]&&I.el[Ze](),F(b,[R])},enter(R){let b=f,I=a,x=h;if(!n.isMounted)if(i)b=N||f,I=j||a,x=p||h;else return;let B=!1;const se=R[dn]=le=>{B||(B=!0,le?F(x,[R]):F(I,[R]),V.delayedLeave&&V.delayedLeave(),R[dn]=void 0)};b?$(b,[R,se]):se()},leave(R,b){const I=String(e.key);if(R[dn]&&R[dn](!0),n.isUnmounting)return b();F(y,[R]);let x=!1;const B=R[Ze]=se=>{x||(x=!0,b(),se?F(_,[R]):F(S,[R]),R[Ze]=void 0,O[I]===e&&delete O[I])};O[I]=e,v?$(v,[R,B]):B()},clone(R){const b=Es(R,t,n,s,r);return r&&r(b),b}};return V}function ts(e){if(sn(e))return e=nt(e),e.children=null,e}function mr(e){if(!sn(e))return Hi(e.type)&&e.children?Ui(e.children):e;const{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&q(n.default))return n.default()}}function Yt(e,t){e.shapeFlag&6&&e.component?(e.transition=t,Yt(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Bi(e,t=!1,n){let s=[],r=0;for(let i=0;i1)for(let i=0;iXt(S,t&&(W(t)?t[_]:t),n,s,r));return}if(pt(s)&&!r){s.shapeFlag&512&&s.type.__asyncResolved&&s.component.subTree.component&&Xt(e,t,n,s.component.subTree);return}const i=s.shapeFlag&4?Gn(s.component):s.el,o=r?null:i,{i:l,r:c}=e,f=t&&t.r,a=l.refs===Z?l.refs={}:l.refs,h=l.setupState,y=J(h),v=h===Z?()=>!1:S=>z(y,S);if(f!=null&&f!==c&&(re(f)?(a[f]=null,v(f)&&(h[f]=null)):fe(f)&&(f.value=null)),q(c))tn(c,l,12,[o,a]);else{const S=re(c),_=fe(c);if(S||_){const K=()=>{if(e.f){const N=S?v(c)?h[c]:a[c]:c.value;r?W(N)&&Hs(N,i):W(N)?N.includes(i)||N.push(i):S?(a[c]=[i],v(c)&&(h[c]=a[c])):(c.value=[i],e.k&&(a[e.k]=c.value))}else S?(a[c]=o,v(c)&&(h[c]=o)):_&&(c.value=o,e.k&&(a[e.k]=o))};o?(K.id=-1,be(K,n)):K()}}}let yr=!1;const bt=()=>{yr||(console.error("Hydration completed but contains mismatches."),yr=!0)},ql=e=>e.namespaceURI.includes("svg")&&e.tagName!=="foreignObject",Gl=e=>e.namespaceURI.includes("MathML"),hn=e=>{if(e.nodeType===1){if(ql(e))return"svg";if(Gl(e))return"mathml"}},xt=e=>e.nodeType===8;function Yl(e){const{mt:t,p:n,o:{patchProp:s,createText:r,nextSibling:i,parentNode:o,remove:l,insert:c,createComment:f}}=e,a=(p,g)=>{if(!g.hasChildNodes()){n(null,p,g),On(),g._vnode=p;return}h(g.firstChild,p,null,null,null),On(),g._vnode=p},h=(p,g,O,F,$,V=!1)=>{V=V||!!g.dynamicChildren;const R=xt(p)&&p.data==="[",b=()=>_(p,g,O,F,$,R),{type:I,ref:x,shapeFlag:B,patchFlag:se}=g;let le=p.nodeType;g.el=p,se===-2&&(V=!1,g.dynamicChildren=null);let U=null;switch(I){case gt:le!==3?g.children===""?(c(g.el=r(""),o(p),p),U=p):U=b():(p.data!==g.children&&(bt(),p.data=g.children),U=i(p));break;case ve:j(p)?(U=i(p),N(g.el=p.content.firstChild,p,O)):le!==8||R?U=b():U=i(p);break;case kt:if(R&&(p=i(p),le=p.nodeType),le===1||le===3){U=p;const Y=!g.children.length;for(let D=0;D{V=V||!!g.dynamicChildren;const{type:R,props:b,patchFlag:I,shapeFlag:x,dirs:B,transition:se}=g,le=R==="input"||R==="option";if(le||I!==-1){B&&Ue(g,null,O,"created");let U=!1;if(j(p)){U=co(null,se)&&O&&O.vnode.props&&O.vnode.props.appear;const D=p.content.firstChild;U&&se.beforeEnter(D),N(D,p,O),g.el=p=D}if(x&16&&!(b&&(b.innerHTML||b.textContent))){let D=v(p.firstChild,g,p,O,F,$,V);for(;D;){pn(p,1)||bt();const he=D;D=D.nextSibling,l(he)}}else if(x&8){let D=g.children;D[0]===` +`&&(p.tagName==="PRE"||p.tagName==="TEXTAREA")&&(D=D.slice(1)),p.textContent!==D&&(pn(p,0)||bt(),p.textContent=g.children)}if(b){if(le||!V||I&48){const D=p.tagName.includes("-");for(const he in b)(le&&(he.endsWith("value")||he==="indeterminate")||en(he)&&!Ct(he)||he[0]==="."||D)&&s(p,he,null,b[he],void 0,O)}else if(b.onClick)s(p,"onClick",null,b.onClick,void 0,O);else if(I&4&&ht(b.style))for(const D in b.style)b.style[D]}let Y;(Y=b&&b.onVnodeBeforeMount)&&Oe(Y,O,g),B&&Ue(g,null,O,"beforeMount"),((Y=b&&b.onVnodeMounted)||B||U)&&go(()=>{Y&&Oe(Y,O,g),U&&se.enter(p),B&&Ue(g,null,O,"mounted")},F)}return p.nextSibling},v=(p,g,O,F,$,V,R)=>{R=R||!!g.dynamicChildren;const b=g.children,I=b.length;for(let x=0;x{const{slotScopeIds:R}=g;R&&($=$?$.concat(R):R);const b=o(p),I=v(i(p),g,b,O,F,$,V);return I&&xt(I)&&I.data==="]"?i(g.anchor=I):(bt(),c(g.anchor=f("]"),b,I),I)},_=(p,g,O,F,$,V)=>{if(pn(p.parentElement,1)||bt(),g.el=null,V){const I=K(p);for(;;){const x=i(p);if(x&&x!==I)l(x);else break}}const R=i(p),b=o(p);return l(p),n(null,g,b,R,O,F,hn(b),$),O&&(O.vnode.el=g.el,ho(O,g.el)),R},K=(p,g="[",O="]")=>{let F=0;for(;p;)if(p=i(p),p&&xt(p)&&(p.data===g&&F++,p.data===O)){if(F===0)return i(p);F--}return p},N=(p,g,O)=>{const F=g.parentNode;F&&F.replaceChild(p,g);let $=O;for(;$;)$.vnode.el===g&&($.vnode.el=$.subTree.el=p),$=$.parent},j=p=>p.nodeType===1&&p.tagName==="TEMPLATE";return[a,h]}const vr="data-allow-mismatch",Xl={0:"text",1:"children",2:"class",3:"style",4:"attribute"};function pn(e,t){if(t===0||t===1)for(;e&&!e.hasAttribute(vr);)e=e.parentElement;const n=e&&e.getAttribute(vr);if(n==null)return!1;if(n==="")return!0;{const s=n.split(",");return t===0&&s.includes("children")?!0:n.split(",").includes(Xl[t])}}Hn().requestIdleCallback;Hn().cancelIdleCallback;function Jl(e,t){if(xt(e)&&e.data==="["){let n=1,s=e.nextSibling;for(;s;){if(s.nodeType===1){if(t(s)===!1)break}else if(xt(s))if(s.data==="]"){if(--n===0)break}else s.data==="["&&n++;s=s.nextSibling}}else t(e)}const pt=e=>!!e.type.__asyncLoader;/*! #__NO_SIDE_EFFECTS__ */function wf(e){q(e)&&(e={loader:e});const{loader:t,loadingComponent:n,errorComponent:s,delay:r=200,hydrate:i,timeout:o,suspensible:l=!0,onError:c}=e;let f=null,a,h=0;const y=()=>(h++,f=null,v()),v=()=>{let S;return f||(S=f=t().catch(_=>{if(_=_ instanceof Error?_:new Error(String(_)),c)return new Promise((K,N)=>{c(_,()=>K(y()),()=>N(_),h+1)});throw _}).then(_=>S!==f&&f?f:(_&&(_.__esModule||_[Symbol.toStringTag]==="Module")&&(_=_.default),a=_,_)))};return Ys({name:"AsyncComponentWrapper",__asyncLoader:v,__asyncHydrate(S,_,K){const N=i?()=>{const j=i(K,p=>Jl(S,p));j&&(_.bum||(_.bum=[])).push(j)}:K;a?N():v().then(()=>!_.isUnmounted&&N())},get __asyncResolved(){return a},setup(){const S=ue;if(Xs(S),a)return()=>ns(a,S);const _=p=>{f=null,nn(p,S,13,!s)};if(l&&S.suspense||Mt)return v().then(p=>()=>ns(p,S)).catch(p=>(_(p),()=>s?ce(s,{error:p}):null));const K=oe(!1),N=oe(),j=oe(!!r);return r&&setTimeout(()=>{j.value=!1},r),o!=null&&setTimeout(()=>{if(!K.value&&!N.value){const p=new Error(`Async component timed out after ${o}ms.`);_(p),N.value=p}},o),v().then(()=>{K.value=!0,S.parent&&sn(S.parent.vnode)&&S.parent.update()}).catch(p=>{_(p),N.value=p}),()=>{if(K.value&&a)return ns(a,S);if(N.value&&s)return ce(s,{error:N.value});if(n&&!j.value)return ce(n)}}})}function ns(e,t){const{ref:n,props:s,children:r,ce:i}=t.vnode,o=ce(e,s,r);return o.ref=n,o.ce=i,delete t.vnode.ce,o}const sn=e=>e.type.__isKeepAlive;function zl(e,t){Wi(e,"a",t)}function Ql(e,t){Wi(e,"da",t)}function Wi(e,t,n=ue){const s=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(kn(t,s,n),n){let r=n.parent;for(;r&&r.parent;)sn(r.parent.vnode)&&Zl(s,t,n,r),r=r.parent}}function Zl(e,t,n,s){const r=kn(t,e,s,!0);Bn(()=>{Hs(s[t],r)},n)}function kn(e,t,n=ue,s=!1){if(n){const r=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...o)=>{rt();const l=rn(n),c=He(t,n,e,o);return l(),it(),c});return s?r.unshift(i):r.push(i),i}}const Je=e=>(t,n=ue)=>{(!Mt||e==="sp")&&kn(e,(...s)=>t(...s),n)},ec=Je("bm"),Lt=Je("m"),tc=Je("bu"),nc=Je("u"),Ki=Je("bum"),Bn=Je("um"),sc=Je("sp"),rc=Je("rtg"),ic=Je("rtc");function oc(e,t=ue){kn("ec",e,t)}const qi="components";function Sf(e,t){return Yi(qi,e,!0,t)||e}const Gi=Symbol.for("v-ndc");function xf(e){return re(e)?Yi(qi,e,!1)||e:e||Gi}function Yi(e,t,n=!0,s=!1){const r=de||ue;if(r){const i=r.type;{const l=Wc(i,!1);if(l&&(l===t||l===Le(t)||l===Fn(Le(t))))return i}const o=_r(r[e]||i[e],t)||_r(r.appContext[e],t);return!o&&s?i:o}}function _r(e,t){return e&&(e[t]||e[Le(t)]||e[Fn(Le(t))])}function Ef(e,t,n,s){let r;const i=n,o=W(e);if(o||re(e)){const l=o&&ht(e);let c=!1;l&&(c=!Pe(e),e=Dn(e)),r=new Array(e.length);for(let f=0,a=e.length;ft(l,c,void 0,i));else{const l=Object.keys(e);r=new Array(l.length);for(let c=0,f=l.length;czt(t)?!(t.type===ve||t.type===xe&&!Xi(t.children)):!0)?e:null}function Cf(e,t){const n={};for(const s in e)n[/[A-Z]/.test(s)?`on:${s}`:_n(s)]=e[s];return n}const Ts=e=>e?bo(e)?Gn(e):Ts(e.parent):null,Ut=ae(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Ts(e.parent),$root:e=>Ts(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Js(e),$forceUpdate:e=>e.f||(e.f=()=>{Gs(e.update)}),$nextTick:e=>e.n||(e.n=Un.bind(e.proxy)),$watch:e=>Rc.bind(e)}),ss=(e,t)=>e!==Z&&!e.__isScriptSetup&&z(e,t),lc={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:s,data:r,props:i,accessCache:o,type:l,appContext:c}=e;let f;if(t[0]!=="$"){const v=o[t];if(v!==void 0)switch(v){case 1:return s[t];case 2:return r[t];case 4:return n[t];case 3:return i[t]}else{if(ss(s,t))return o[t]=1,s[t];if(r!==Z&&z(r,t))return o[t]=2,r[t];if((f=e.propsOptions[0])&&z(f,t))return o[t]=3,i[t];if(n!==Z&&z(n,t))return o[t]=4,n[t];Cs&&(o[t]=0)}}const a=Ut[t];let h,y;if(a)return t==="$attrs"&&me(e.attrs,"get",""),a(e);if((h=l.__cssModules)&&(h=h[t]))return h;if(n!==Z&&z(n,t))return o[t]=4,n[t];if(y=c.config.globalProperties,z(y,t))return y[t]},set({_:e},t,n){const{data:s,setupState:r,ctx:i}=e;return ss(r,t)?(r[t]=n,!0):s!==Z&&z(s,t)?(s[t]=n,!0):z(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:r,propsOptions:i}},o){let l;return!!n[o]||e!==Z&&z(e,o)||ss(t,o)||(l=i[0])&&z(l,o)||z(s,o)||z(Ut,o)||z(r.config.globalProperties,o)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:z(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function Af(){return cc().slots}function cc(){const e=qn();return e.setupContext||(e.setupContext=So(e))}function br(e){return W(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let Cs=!0;function ac(e){const t=Js(e),n=e.proxy,s=e.ctx;Cs=!1,t.beforeCreate&&wr(t.beforeCreate,e,"bc");const{data:r,computed:i,methods:o,watch:l,provide:c,inject:f,created:a,beforeMount:h,mounted:y,beforeUpdate:v,updated:S,activated:_,deactivated:K,beforeDestroy:N,beforeUnmount:j,destroyed:p,unmounted:g,render:O,renderTracked:F,renderTriggered:$,errorCaptured:V,serverPrefetch:R,expose:b,inheritAttrs:I,components:x,directives:B,filters:se}=t;if(f&&fc(f,s,null),o)for(const Y in o){const D=o[Y];q(D)&&(s[Y]=D.bind(n))}if(r){const Y=r.call(n,n);ne(Y)&&(e.data=jn(Y))}if(Cs=!0,i)for(const Y in i){const D=i[Y],he=q(D)?D.bind(n,n):q(D.get)?D.get.bind(n,n):ke,on=!q(D)&&q(D.set)?D.set.bind(n):ke,ot=ie({get:he,set:on});Object.defineProperty(s,Y,{enumerable:!0,configurable:!0,get:()=>ot.value,set:De=>ot.value=De})}if(l)for(const Y in l)Ji(l[Y],s,n,Y);if(c){const Y=q(c)?c.call(n):c;Reflect.ownKeys(Y).forEach(D=>{mc(D,Y[D])})}a&&wr(a,e,"c");function U(Y,D){W(D)?D.forEach(he=>Y(he.bind(n))):D&&Y(D.bind(n))}if(U(ec,h),U(Lt,y),U(tc,v),U(nc,S),U(zl,_),U(Ql,K),U(oc,V),U(ic,F),U(rc,$),U(Ki,j),U(Bn,g),U(sc,R),W(b))if(b.length){const Y=e.exposed||(e.exposed={});b.forEach(D=>{Object.defineProperty(Y,D,{get:()=>n[D],set:he=>n[D]=he})})}else e.exposed||(e.exposed={});O&&e.render===ke&&(e.render=O),I!=null&&(e.inheritAttrs=I),x&&(e.components=x),B&&(e.directives=B),R&&Xs(e)}function fc(e,t,n=ke){W(e)&&(e=As(e));for(const s in e){const r=e[s];let i;ne(r)?"default"in r?i=Ot(r.from||s,r.default,!0):i=Ot(r.from||s):i=Ot(r),fe(i)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>i.value,set:o=>i.value=o}):t[s]=i}}function wr(e,t,n){He(W(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function Ji(e,t,n,s){let r=s.includes(".")?fo(n,s):()=>n[s];if(re(e)){const i=t[e];q(i)&&Fe(r,i)}else if(q(e))Fe(r,e.bind(n));else if(ne(e))if(W(e))e.forEach(i=>Ji(i,t,n,s));else{const i=q(e.handler)?e.handler.bind(n):t[e.handler];q(i)&&Fe(r,i,e)}}function Js(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:i,config:{optionMergeStrategies:o}}=e.appContext,l=i.get(t);let c;return l?c=l:!r.length&&!n&&!s?c=t:(c={},r.length&&r.forEach(f=>Pn(c,f,o,!0)),Pn(c,t,o)),ne(t)&&i.set(t,c),c}function Pn(e,t,n,s=!1){const{mixins:r,extends:i}=t;i&&Pn(e,i,n,!0),r&&r.forEach(o=>Pn(e,o,n,!0));for(const o in t)if(!(s&&o==="expose")){const l=uc[o]||n&&n[o];e[o]=l?l(e[o],t[o]):t[o]}return e}const uc={data:Sr,props:xr,emits:xr,methods:$t,computed:$t,beforeCreate:_e,created:_e,beforeMount:_e,mounted:_e,beforeUpdate:_e,updated:_e,beforeDestroy:_e,beforeUnmount:_e,destroyed:_e,unmounted:_e,activated:_e,deactivated:_e,errorCaptured:_e,serverPrefetch:_e,components:$t,directives:$t,watch:hc,provide:Sr,inject:dc};function Sr(e,t){return t?e?function(){return ae(q(e)?e.call(this,this):e,q(t)?t.call(this,this):t)}:t:e}function dc(e,t){return $t(As(e),As(t))}function As(e){if(W(e)){const t={};for(let n=0;n1)return n&&q(t)?t.call(s&&s.proxy):t}}const Qi={},Zi=()=>Object.create(Qi),eo=e=>Object.getPrototypeOf(e)===Qi;function yc(e,t,n,s=!1){const r={},i=Zi();e.propsDefaults=Object.create(null),to(e,t,r,i);for(const o in e.propsOptions[0])o in r||(r[o]=void 0);n?e.props=s?r:Tl(r):e.type.props?e.props=r:e.props=i,e.attrs=i}function vc(e,t,n,s){const{props:r,attrs:i,vnode:{patchFlag:o}}=e,l=J(r),[c]=e.propsOptions;let f=!1;if((s||o>0)&&!(o&16)){if(o&8){const a=e.vnode.dynamicProps;for(let h=0;h{c=!0;const[y,v]=no(h,t,!0);ae(o,y),v&&l.push(...v)};!n&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}if(!i&&!c)return ne(e)&&s.set(e,Et),Et;if(W(i))for(let a=0;ae[0]==="_"||e==="$stable",zs=e=>W(e)?e.map(Me):[Me(e)],bc=(e,t,n)=>{if(t._n)return t;const s=Ul((...r)=>zs(t(...r)),n);return s._c=!1,s},ro=(e,t,n)=>{const s=e._ctx;for(const r in e){if(so(r))continue;const i=e[r];if(q(i))t[r]=bc(r,i,s);else if(i!=null){const o=zs(i);t[r]=()=>o}}},io=(e,t)=>{const n=zs(t);e.slots.default=()=>n},oo=(e,t,n)=>{for(const s in t)(n||s!=="_")&&(e[s]=t[s])},wc=(e,t,n)=>{const s=e.slots=Zi();if(e.vnode.shapeFlag&32){const r=t._;r?(oo(s,t,n),n&&ai(s,"_",r,!0)):ro(t,s)}else t&&io(e,t)},Sc=(e,t,n)=>{const{vnode:s,slots:r}=e;let i=!0,o=Z;if(s.shapeFlag&32){const l=t._;l?n&&l===1?i=!1:oo(r,t,n):(i=!t.$stable,ro(t,r)),o=t}else t&&(io(e,t),o={default:1});if(i)for(const l in r)!so(l)&&o[l]==null&&delete r[l]},be=go;function xc(e){return lo(e)}function Ec(e){return lo(e,Yl)}function lo(e,t){const n=Hn();n.__VUE__=!0;const{insert:s,remove:r,patchProp:i,createElement:o,createText:l,createComment:c,setText:f,setElementText:a,parentNode:h,nextSibling:y,setScopeId:v=ke,insertStaticContent:S}=e,_=(u,d,m,T=null,w=null,E=null,P=void 0,M=null,A=!!d.dynamicChildren)=>{if(u===d)return;u&&!ut(u,d)&&(T=ln(u),De(u,w,E,!0),u=null),d.patchFlag===-2&&(A=!1,d.dynamicChildren=null);const{type:C,ref:k,shapeFlag:L}=d;switch(C){case gt:K(u,d,m,T);break;case ve:N(u,d,m,T);break;case kt:u==null&&j(d,m,T,P);break;case xe:x(u,d,m,T,w,E,P,M,A);break;default:L&1?O(u,d,m,T,w,E,P,M,A):L&6?B(u,d,m,T,w,E,P,M,A):(L&64||L&128)&&C.process(u,d,m,T,w,E,P,M,A,vt)}k!=null&&w&&Xt(k,u&&u.ref,E,d||u,!d)},K=(u,d,m,T)=>{if(u==null)s(d.el=l(d.children),m,T);else{const w=d.el=u.el;d.children!==u.children&&f(w,d.children)}},N=(u,d,m,T)=>{u==null?s(d.el=c(d.children||""),m,T):d.el=u.el},j=(u,d,m,T)=>{[u.el,u.anchor]=S(u.children,d,m,T,u.el,u.anchor)},p=({el:u,anchor:d},m,T)=>{let w;for(;u&&u!==d;)w=y(u),s(u,m,T),u=w;s(d,m,T)},g=({el:u,anchor:d})=>{let m;for(;u&&u!==d;)m=y(u),r(u),u=m;r(d)},O=(u,d,m,T,w,E,P,M,A)=>{d.type==="svg"?P="svg":d.type==="math"&&(P="mathml"),u==null?F(d,m,T,w,E,P,M,A):R(u,d,w,E,P,M,A)},F=(u,d,m,T,w,E,P,M)=>{let A,C;const{props:k,shapeFlag:L,transition:H,dirs:G}=u;if(A=u.el=o(u.type,E,k&&k.is,k),L&8?a(A,u.children):L&16&&V(u.children,A,null,T,w,rs(u,E),P,M),G&&Ue(u,null,T,"created"),$(A,u,u.scopeId,P,T),k){for(const ee in k)ee!=="value"&&!Ct(ee)&&i(A,ee,null,k[ee],E,T);"value"in k&&i(A,"value",null,k.value,E),(C=k.onVnodeBeforeMount)&&Oe(C,T,u)}G&&Ue(u,null,T,"beforeMount");const X=co(w,H);X&&H.beforeEnter(A),s(A,d,m),((C=k&&k.onVnodeMounted)||X||G)&&be(()=>{C&&Oe(C,T,u),X&&H.enter(A),G&&Ue(u,null,T,"mounted")},w)},$=(u,d,m,T,w)=>{if(m&&v(u,m),T)for(let E=0;E{for(let C=A;C{const M=d.el=u.el;let{patchFlag:A,dynamicChildren:C,dirs:k}=d;A|=u.patchFlag&16;const L=u.props||Z,H=d.props||Z;let G;if(m&<(m,!1),(G=H.onVnodeBeforeUpdate)&&Oe(G,m,d,u),k&&Ue(d,u,m,"beforeUpdate"),m&<(m,!0),(L.innerHTML&&H.innerHTML==null||L.textContent&&H.textContent==null)&&a(M,""),C?b(u.dynamicChildren,C,M,m,T,rs(d,w),E):P||D(u,d,M,null,m,T,rs(d,w),E,!1),A>0){if(A&16)I(M,L,H,m,w);else if(A&2&&L.class!==H.class&&i(M,"class",null,H.class,w),A&4&&i(M,"style",L.style,H.style,w),A&8){const X=d.dynamicProps;for(let ee=0;ee{G&&Oe(G,m,d,u),k&&Ue(d,u,m,"updated")},T)},b=(u,d,m,T,w,E,P)=>{for(let M=0;M{if(d!==m){if(d!==Z)for(const E in d)!Ct(E)&&!(E in m)&&i(u,E,d[E],null,w,T);for(const E in m){if(Ct(E))continue;const P=m[E],M=d[E];P!==M&&E!=="value"&&i(u,E,M,P,w,T)}"value"in m&&i(u,"value",d.value,m.value,w)}},x=(u,d,m,T,w,E,P,M,A)=>{const C=d.el=u?u.el:l(""),k=d.anchor=u?u.anchor:l("");let{patchFlag:L,dynamicChildren:H,slotScopeIds:G}=d;G&&(M=M?M.concat(G):G),u==null?(s(C,m,T),s(k,m,T),V(d.children||[],m,k,w,E,P,M,A)):L>0&&L&64&&H&&u.dynamicChildren?(b(u.dynamicChildren,H,m,w,E,P,M),(d.key!=null||w&&d===w.subTree)&&Qs(u,d,!0)):D(u,d,m,k,w,E,P,M,A)},B=(u,d,m,T,w,E,P,M,A)=>{d.slotScopeIds=M,u==null?d.shapeFlag&512?w.ctx.activate(d,m,T,P,A):se(d,m,T,w,E,P,A):le(u,d,A)},se=(u,d,m,T,w,E,P)=>{const M=u.component=Vc(u,T,w);if(sn(u)&&(M.ctx.renderer=vt),Uc(M,!1,P),M.asyncDep){if(w&&w.registerDep(M,U,P),!u.el){const A=M.subTree=ce(ve);N(null,A,d,m)}}else U(M,u,d,m,w,E,P)},le=(u,d,m)=>{const T=d.component=u.component;if(Ic(u,d,m))if(T.asyncDep&&!T.asyncResolved){Y(T,d,m);return}else T.next=d,T.update();else d.el=u.el,T.vnode=d},U=(u,d,m,T,w,E,P)=>{const M=()=>{if(u.isMounted){let{next:L,bu:H,u:G,parent:X,vnode:ee}=u;{const Te=ao(u);if(Te){L&&(L.el=ee.el,Y(u,L,P)),Te.asyncDep.then(()=>{u.isUnmounted||M()});return}}let Q=L,Ee;lt(u,!1),L?(L.el=ee.el,Y(u,L,P)):L=ee,H&&bn(H),(Ee=L.props&&L.props.onVnodeBeforeUpdate)&&Oe(Ee,X,L,ee),lt(u,!0);const pe=is(u),Ie=u.subTree;u.subTree=pe,_(Ie,pe,h(Ie.el),ln(Ie),u,w,E),L.el=pe.el,Q===null&&ho(u,pe.el),G&&be(G,w),(Ee=L.props&&L.props.onVnodeUpdated)&&be(()=>Oe(Ee,X,L,ee),w)}else{let L;const{el:H,props:G}=d,{bm:X,m:ee,parent:Q,root:Ee,type:pe}=u,Ie=pt(d);if(lt(u,!1),X&&bn(X),!Ie&&(L=G&&G.onVnodeBeforeMount)&&Oe(L,Q,d),lt(u,!0),H&&Jn){const Te=()=>{u.subTree=is(u),Jn(H,u.subTree,u,w,null)};Ie&&pe.__asyncHydrate?pe.__asyncHydrate(H,u,Te):Te()}else{Ee.ce&&Ee.ce._injectChildStyle(pe);const Te=u.subTree=is(u);_(null,Te,m,T,u,w,E),d.el=Te.el}if(ee&&be(ee,w),!Ie&&(L=G&&G.onVnodeMounted)){const Te=d;be(()=>Oe(L,Q,Te),w)}(d.shapeFlag&256||Q&&pt(Q.vnode)&&Q.vnode.shapeFlag&256)&&u.a&&be(u.a,w),u.isMounted=!0,d=m=T=null}};u.scope.on();const A=u.effect=new pi(M);u.scope.off();const C=u.update=A.run.bind(A),k=u.job=A.runIfDirty.bind(A);k.i=u,k.id=u.uid,A.scheduler=()=>Gs(k),lt(u,!0),C()},Y=(u,d,m)=>{d.component=u;const T=u.vnode.props;u.vnode=d,u.next=null,vc(u,d.props,T,m),Sc(u,d.children,m),rt(),dr(u),it()},D=(u,d,m,T,w,E,P,M,A=!1)=>{const C=u&&u.children,k=u?u.shapeFlag:0,L=d.children,{patchFlag:H,shapeFlag:G}=d;if(H>0){if(H&128){on(C,L,m,T,w,E,P,M,A);return}else if(H&256){he(C,L,m,T,w,E,P,M,A);return}}G&8?(k&16&&It(C,w,E),L!==C&&a(m,L)):k&16?G&16?on(C,L,m,T,w,E,P,M,A):It(C,w,E,!0):(k&8&&a(m,""),G&16&&V(L,m,T,w,E,P,M,A))},he=(u,d,m,T,w,E,P,M,A)=>{u=u||Et,d=d||Et;const C=u.length,k=d.length,L=Math.min(C,k);let H;for(H=0;Hk?It(u,w,E,!0,!1,L):V(d,m,T,w,E,P,M,A,L)},on=(u,d,m,T,w,E,P,M,A)=>{let C=0;const k=d.length;let L=u.length-1,H=k-1;for(;C<=L&&C<=H;){const G=u[C],X=d[C]=A?et(d[C]):Me(d[C]);if(ut(G,X))_(G,X,m,null,w,E,P,M,A);else break;C++}for(;C<=L&&C<=H;){const G=u[L],X=d[H]=A?et(d[H]):Me(d[H]);if(ut(G,X))_(G,X,m,null,w,E,P,M,A);else break;L--,H--}if(C>L){if(C<=H){const G=H+1,X=GH)for(;C<=L;)De(u[C],w,E,!0),C++;else{const G=C,X=C,ee=new Map;for(C=X;C<=H;C++){const Ce=d[C]=A?et(d[C]):Me(d[C]);Ce.key!=null&&ee.set(Ce.key,C)}let Q,Ee=0;const pe=H-X+1;let Ie=!1,Te=0;const Nt=new Array(pe);for(C=0;C=pe){De(Ce,w,E,!0);continue}let je;if(Ce.key!=null)je=ee.get(Ce.key);else for(Q=X;Q<=H;Q++)if(Nt[Q-X]===0&&ut(Ce,d[Q])){je=Q;break}je===void 0?De(Ce,w,E,!0):(Nt[je-X]=C+1,je>=Te?Te=je:Ie=!0,_(Ce,d[je],m,null,w,E,P,M,A),Ee++)}const lr=Ie?Tc(Nt):Et;for(Q=lr.length-1,C=pe-1;C>=0;C--){const Ce=X+C,je=d[Ce],cr=Ce+1{const{el:E,type:P,transition:M,children:A,shapeFlag:C}=u;if(C&6){ot(u.component.subTree,d,m,T);return}if(C&128){u.suspense.move(d,m,T);return}if(C&64){P.move(u,d,m,vt);return}if(P===xe){s(E,d,m);for(let L=0;LM.enter(E),w);else{const{leave:L,delayLeave:H,afterLeave:G}=M,X=()=>s(E,d,m),ee=()=>{L(E,()=>{X(),G&&G()})};H?H(E,X,ee):ee()}else s(E,d,m)},De=(u,d,m,T=!1,w=!1)=>{const{type:E,props:P,ref:M,children:A,dynamicChildren:C,shapeFlag:k,patchFlag:L,dirs:H,cacheIndex:G}=u;if(L===-2&&(w=!1),M!=null&&Xt(M,null,m,u,!0),G!=null&&(d.renderCache[G]=void 0),k&256){d.ctx.deactivate(u);return}const X=k&1&&H,ee=!pt(u);let Q;if(ee&&(Q=P&&P.onVnodeBeforeUnmount)&&Oe(Q,d,u),k&6)Wo(u.component,m,T);else{if(k&128){u.suspense.unmount(m,T);return}X&&Ue(u,null,d,"beforeUnmount"),k&64?u.type.remove(u,d,m,vt,T):C&&!C.hasOnce&&(E!==xe||L>0&&L&64)?It(C,d,m,!1,!0):(E===xe&&L&384||!w&&k&16)&&It(A,d,m),T&&ir(u)}(ee&&(Q=P&&P.onVnodeUnmounted)||X)&&be(()=>{Q&&Oe(Q,d,u),X&&Ue(u,null,d,"unmounted")},m)},ir=u=>{const{type:d,el:m,anchor:T,transition:w}=u;if(d===xe){Bo(m,T);return}if(d===kt){g(u);return}const E=()=>{r(m),w&&!w.persisted&&w.afterLeave&&w.afterLeave()};if(u.shapeFlag&1&&w&&!w.persisted){const{leave:P,delayLeave:M}=w,A=()=>P(m,E);M?M(u.el,E,A):A()}else E()},Bo=(u,d)=>{let m;for(;u!==d;)m=y(u),r(u),u=m;r(d)},Wo=(u,d,m)=>{const{bum:T,scope:w,job:E,subTree:P,um:M,m:A,a:C}=u;Tr(A),Tr(C),T&&bn(T),w.stop(),E&&(E.flags|=8,De(P,u,d,m)),M&&be(M,d),be(()=>{u.isUnmounted=!0},d),d&&d.pendingBranch&&!d.isUnmounted&&u.asyncDep&&!u.asyncResolved&&u.suspenseId===d.pendingId&&(d.deps--,d.deps===0&&d.resolve())},It=(u,d,m,T=!1,w=!1,E=0)=>{for(let P=E;P{if(u.shapeFlag&6)return ln(u.component.subTree);if(u.shapeFlag&128)return u.suspense.next();const d=y(u.anchor||u.el),m=d&&d[Fi];return m?y(m):d};let Yn=!1;const or=(u,d,m)=>{u==null?d._vnode&&De(d._vnode,null,null,!0):_(d._vnode||null,u,d,null,null,null,m),d._vnode=u,Yn||(Yn=!0,dr(),On(),Yn=!1)},vt={p:_,um:De,m:ot,r:ir,mt:se,mc:V,pc:D,pbc:b,n:ln,o:e};let Xn,Jn;return t&&([Xn,Jn]=t(vt)),{render:or,hydrate:Xn,createApp:gc(or,Xn)}}function rs({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function lt({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function co(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function Qs(e,t,n=!1){const s=e.children,r=t.children;if(W(s)&&W(r))for(let i=0;i>1,e[n[l]]0&&(t[s]=n[i-1]),n[i]=s)}}for(i=n.length,o=n[i-1];i-- >0;)n[i]=o,o=t[o];return n}function ao(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:ao(t)}function Tr(e){if(e)for(let t=0;tOt(Cc);function Zs(e,t){return Wn(e,null,t)}function Rf(e,t){return Wn(e,null,{flush:"post"})}function Fe(e,t,n){return Wn(e,t,n)}function Wn(e,t,n=Z){const{immediate:s,deep:r,flush:i,once:o}=n,l=ae({},n),c=t&&s||!t&&i!=="post";let f;if(Mt){if(i==="sync"){const v=Ac();f=v.__watcherHandles||(v.__watcherHandles=[])}else if(!c){const v=()=>{};return v.stop=ke,v.resume=ke,v.pause=ke,v}}const a=ue;l.call=(v,S,_)=>He(v,a,S,_);let h=!1;i==="post"?l.scheduler=v=>{be(v,a&&a.suspense)}:i!=="sync"&&(h=!0,l.scheduler=(v,S)=>{S?v():Gs(v)}),l.augmentJob=v=>{t&&(v.flags|=4),h&&(v.flags|=2,a&&(v.id=a.uid,v.i=a))};const y=$l(e,t,l);return Mt&&(f?f.push(y):c&&y()),y}function Rc(e,t,n){const s=this.proxy,r=re(e)?e.includes(".")?fo(s,e):()=>s[e]:e.bind(s,s);let i;q(t)?i=t:(i=t.handler,n=t);const o=rn(this),l=Wn(r,i.bind(s),n);return o(),l}function fo(e,t){const n=t.split(".");return()=>{let s=e;for(let r=0;rt==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Le(t)}Modifiers`]||e[`${st(t)}Modifiers`];function Mc(e,t,...n){if(e.isUnmounted)return;const s=e.vnode.props||Z;let r=n;const i=t.startsWith("update:"),o=i&&Oc(s,t.slice(7));o&&(o.trim&&(r=n.map(a=>re(a)?a.trim():a)),o.number&&(r=n.map(vs)));let l,c=s[l=_n(t)]||s[l=_n(Le(t))];!c&&i&&(c=s[l=_n(st(t))]),c&&He(c,e,6,r);const f=s[l+"Once"];if(f){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,He(f,e,6,r)}}function uo(e,t,n=!1){const s=t.emitsCache,r=s.get(e);if(r!==void 0)return r;const i=e.emits;let o={},l=!1;if(!q(e)){const c=f=>{const a=uo(f,t,!0);a&&(l=!0,ae(o,a))};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}return!i&&!l?(ne(e)&&s.set(e,null),null):(W(i)?i.forEach(c=>o[c]=null):ae(o,i),ne(e)&&s.set(e,o),o)}function Kn(e,t){return!e||!en(t)?!1:(t=t.slice(2).replace(/Once$/,""),z(e,t[0].toLowerCase()+t.slice(1))||z(e,st(t))||z(e,t))}function is(e){const{type:t,vnode:n,proxy:s,withProxy:r,propsOptions:[i],slots:o,attrs:l,emit:c,render:f,renderCache:a,props:h,data:y,setupState:v,ctx:S,inheritAttrs:_}=e,K=Mn(e);let N,j;try{if(n.shapeFlag&4){const g=r||s,O=g;N=Me(f.call(O,g,a,h,v,y,S)),j=l}else{const g=t;N=Me(g.length>1?g(h,{attrs:l,slots:o,emit:c}):g(h,null)),j=t.props?l:Pc(l)}}catch(g){Bt.length=0,nn(g,e,1),N=ce(ve)}let p=N;if(j&&_!==!1){const g=Object.keys(j),{shapeFlag:O}=p;g.length&&O&7&&(i&&g.some(Fs)&&(j=Lc(j,i)),p=nt(p,j,!1,!0))}return n.dirs&&(p=nt(p,null,!1,!0),p.dirs=p.dirs?p.dirs.concat(n.dirs):n.dirs),n.transition&&Yt(p,n.transition),N=p,Mn(K),N}const Pc=e=>{let t;for(const n in e)(n==="class"||n==="style"||en(n))&&((t||(t={}))[n]=e[n]);return t},Lc=(e,t)=>{const n={};for(const s in e)(!Fs(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function Ic(e,t,n){const{props:s,children:r,component:i}=e,{props:o,children:l,patchFlag:c}=t,f=i.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return s?Cr(s,o,f):!!o;if(c&8){const a=t.dynamicProps;for(let h=0;he.__isSuspense;function go(e,t){t&&t.pendingBranch?W(e)?t.effects.push(...e):t.effects.push(e):Vl(e)}const xe=Symbol.for("v-fgt"),gt=Symbol.for("v-txt"),ve=Symbol.for("v-cmt"),kt=Symbol.for("v-stc"),Bt=[];let Ae=null;function Os(e=!1){Bt.push(Ae=e?null:[])}function Nc(){Bt.pop(),Ae=Bt[Bt.length-1]||null}let Jt=1;function Ar(e,t=!1){Jt+=e,e<0&&Ae&&t&&(Ae.hasOnce=!0)}function mo(e){return e.dynamicChildren=Jt>0?Ae||Et:null,Nc(),Jt>0&&Ae&&Ae.push(e),e}function Of(e,t,n,s,r,i){return mo(vo(e,t,n,s,r,i,!0))}function Ms(e,t,n,s,r){return mo(ce(e,t,n,s,r,!0))}function zt(e){return e?e.__v_isVNode===!0:!1}function ut(e,t){return e.type===t.type&&e.key===t.key}const yo=({key:e})=>e??null,xn=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?re(e)||fe(e)||q(e)?{i:de,r:e,k:t,f:!!n}:e:null);function vo(e,t=null,n=null,s=0,r=null,i=e===xe?0:1,o=!1,l=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&yo(t),ref:t&&xn(t),scopeId:Ni,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:s,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:de};return l?(er(c,n),i&128&&e.normalize(c)):n&&(c.shapeFlag|=re(n)?8:16),Jt>0&&!o&&Ae&&(c.patchFlag>0||i&6)&&c.patchFlag!==32&&Ae.push(c),c}const ce=Fc;function Fc(e,t=null,n=null,s=0,r=null,i=!1){if((!e||e===Gi)&&(e=ve),zt(e)){const l=nt(e,t,!0);return n&&er(l,n),Jt>0&&!i&&Ae&&(l.shapeFlag&6?Ae[Ae.indexOf(e)]=l:Ae.push(l)),l.patchFlag=-2,l}if(Kc(e)&&(e=e.__vccOpts),t){t=Hc(t);let{class:l,style:c}=t;l&&!re(l)&&(t.class=js(l)),ne(c)&&(Ks(c)&&!W(c)&&(c=ae({},c)),t.style=Ds(c))}const o=re(e)?1:po(e)?128:Hi(e)?64:ne(e)?4:q(e)?2:0;return vo(e,t,n,s,r,o,i,!0)}function Hc(e){return e?Ks(e)||eo(e)?ae({},e):e:null}function nt(e,t,n=!1,s=!1){const{props:r,ref:i,patchFlag:o,children:l,transition:c}=e,f=t?$c(r||{},t):r,a={__v_isVNode:!0,__v_skip:!0,type:e.type,props:f,key:f&&yo(f),ref:t&&t.ref?n&&i?W(i)?i.concat(xn(t)):[i,xn(t)]:xn(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==xe?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&nt(e.ssContent),ssFallback:e.ssFallback&&nt(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&s&&Yt(a,c.clone(a)),a}function _o(e=" ",t=0){return ce(gt,null,e,t)}function Mf(e,t){const n=ce(kt,null,e);return n.staticCount=t,n}function Pf(e="",t=!1){return t?(Os(),Ms(ve,null,e)):ce(ve,null,e)}function Me(e){return e==null||typeof e=="boolean"?ce(ve):W(e)?ce(xe,null,e.slice()):zt(e)?et(e):ce(gt,null,String(e))}function et(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:nt(e)}function er(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(W(t))n=16;else if(typeof t=="object")if(s&65){const r=t.default;r&&(r._c&&(r._d=!1),er(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!eo(t)?t._ctx=de:r===3&&de&&(de.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else q(t)?(t={default:t,_ctx:de},n=32):(t=String(t),s&64?(n=16,t=[_o(t)]):n=8);e.children=t,e.shapeFlag|=n}function $c(...e){const t={};for(let n=0;nue||de;let Ln,Ps;{const e=Hn(),t=(n,s)=>{let r;return(r=e[n])||(r=e[n]=[]),r.push(s),i=>{r.length>1?r.forEach(o=>o(i)):r[0](i)}};Ln=t("__VUE_INSTANCE_SETTERS__",n=>ue=n),Ps=t("__VUE_SSR_SETTERS__",n=>Mt=n)}const rn=e=>{const t=ue;return Ln(e),e.scope.on(),()=>{e.scope.off(),Ln(t)}},Rr=()=>{ue&&ue.scope.off(),Ln(null)};function bo(e){return e.vnode.shapeFlag&4}let Mt=!1;function Uc(e,t=!1,n=!1){t&&Ps(t);const{props:s,children:r}=e.vnode,i=bo(e);yc(e,s,i,t),wc(e,r,n);const o=i?kc(e,t):void 0;return t&&Ps(!1),o}function kc(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,lc);const{setup:s}=n;if(s){rt();const r=e.setupContext=s.length>1?So(e):null,i=rn(e),o=tn(s,e,0,[e.props,r]),l=oi(o);if(it(),i(),(l||e.sp)&&!pt(e)&&Xs(e),l){if(o.then(Rr,Rr),t)return o.then(c=>{Or(e,c,t)}).catch(c=>{nn(c,e,0)});e.asyncDep=o}else Or(e,o,t)}else wo(e,t)}function Or(e,t,n){q(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:ne(t)&&(e.setupState=Mi(t)),wo(e,n)}let Mr;function wo(e,t,n){const s=e.type;if(!e.render){if(!t&&Mr&&!s.render){const r=s.template||Js(e).template;if(r){const{isCustomElement:i,compilerOptions:o}=e.appContext.config,{delimiters:l,compilerOptions:c}=s,f=ae(ae({isCustomElement:i,delimiters:l},o),c);s.render=Mr(r,f)}}e.render=s.render||ke}{const r=rn(e);rt();try{ac(e)}finally{it(),r()}}}const Bc={get(e,t){return me(e,"get",""),e[t]}};function So(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,Bc),slots:e.slots,emit:e.emit,expose:t}}function Gn(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Mi(wn(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Ut)return Ut[n](e)},has(t,n){return n in t||n in Ut}})):e.proxy}function Wc(e,t=!0){return q(e)?e.displayName||e.name:e.name||t&&e.__name}function Kc(e){return q(e)&&"__vccOpts"in e}const ie=(e,t)=>Fl(e,t,Mt);function Ls(e,t,n){const s=arguments.length;return s===2?ne(t)&&!W(t)?zt(t)?ce(e,null,[t]):ce(e,t):ce(e,null,t):(s>3?n=Array.prototype.slice.call(arguments,2):s===3&&zt(n)&&(n=[n]),ce(e,t,n))}const qc="3.5.13";/** +* @vue/runtime-dom v3.5.13 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let Is;const Pr=typeof window<"u"&&window.trustedTypes;if(Pr)try{Is=Pr.createPolicy("vue",{createHTML:e=>e})}catch{}const xo=Is?e=>Is.createHTML(e):e=>e,Gc="http://www.w3.org/2000/svg",Yc="http://www.w3.org/1998/Math/MathML",qe=typeof document<"u"?document:null,Lr=qe&&qe.createElement("template"),Xc={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t==="svg"?qe.createElementNS(Gc,e):t==="mathml"?qe.createElementNS(Yc,e):n?qe.createElement(e,{is:n}):qe.createElement(e);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>qe.createTextNode(e),createComment:e=>qe.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>qe.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,i){const o=n?n.previousSibling:t.lastChild;if(r&&(r===i||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===i||!(r=r.nextSibling)););else{Lr.innerHTML=xo(s==="svg"?`${e}`:s==="mathml"?`${e}`:e);const l=Lr.content;if(s==="svg"||s==="mathml"){const c=l.firstChild;for(;c.firstChild;)l.appendChild(c.firstChild);l.removeChild(c)}t.insertBefore(l,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},ze="transition",Ht="animation",Qt=Symbol("_vtc"),Eo={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},Jc=ae({},ji,Eo),zc=e=>(e.displayName="Transition",e.props=Jc,e),Lf=zc((e,{slots:t})=>Ls(Kl,Qc(e),t)),ct=(e,t=[])=>{W(e)?e.forEach(n=>n(...t)):e&&e(...t)},Ir=e=>e?W(e)?e.some(t=>t.length>1):e.length>1:!1;function Qc(e){const t={};for(const x in e)x in Eo||(t[x]=e[x]);if(e.css===!1)return t;const{name:n="v",type:s,duration:r,enterFromClass:i=`${n}-enter-from`,enterActiveClass:o=`${n}-enter-active`,enterToClass:l=`${n}-enter-to`,appearFromClass:c=i,appearActiveClass:f=o,appearToClass:a=l,leaveFromClass:h=`${n}-leave-from`,leaveActiveClass:y=`${n}-leave-active`,leaveToClass:v=`${n}-leave-to`}=e,S=Zc(r),_=S&&S[0],K=S&&S[1],{onBeforeEnter:N,onEnter:j,onEnterCancelled:p,onLeave:g,onLeaveCancelled:O,onBeforeAppear:F=N,onAppear:$=j,onAppearCancelled:V=p}=t,R=(x,B,se,le)=>{x._enterCancelled=le,at(x,B?a:l),at(x,B?f:o),se&&se()},b=(x,B)=>{x._isLeaving=!1,at(x,h),at(x,v),at(x,y),B&&B()},I=x=>(B,se)=>{const le=x?$:j,U=()=>R(B,x,se);ct(le,[B,U]),Nr(()=>{at(B,x?c:i),Ke(B,x?a:l),Ir(le)||Fr(B,s,_,U)})};return ae(t,{onBeforeEnter(x){ct(N,[x]),Ke(x,i),Ke(x,o)},onBeforeAppear(x){ct(F,[x]),Ke(x,c),Ke(x,f)},onEnter:I(!1),onAppear:I(!0),onLeave(x,B){x._isLeaving=!0;const se=()=>b(x,B);Ke(x,h),x._enterCancelled?(Ke(x,y),Dr()):(Dr(),Ke(x,y)),Nr(()=>{x._isLeaving&&(at(x,h),Ke(x,v),Ir(g)||Fr(x,s,K,se))}),ct(g,[x,se])},onEnterCancelled(x){R(x,!1,void 0,!0),ct(p,[x])},onAppearCancelled(x){R(x,!0,void 0,!0),ct(V,[x])},onLeaveCancelled(x){b(x),ct(O,[x])}})}function Zc(e){if(e==null)return null;if(ne(e))return[os(e.enter),os(e.leave)];{const t=os(e);return[t,t]}}function os(e){return Jo(e)}function Ke(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[Qt]||(e[Qt]=new Set)).add(t)}function at(e,t){t.split(/\s+/).forEach(s=>s&&e.classList.remove(s));const n=e[Qt];n&&(n.delete(t),n.size||(e[Qt]=void 0))}function Nr(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let ea=0;function Fr(e,t,n,s){const r=e._endId=++ea,i=()=>{r===e._endId&&s()};if(n!=null)return setTimeout(i,n);const{type:o,timeout:l,propCount:c}=ta(e,t);if(!o)return s();const f=o+"end";let a=0;const h=()=>{e.removeEventListener(f,y),i()},y=v=>{v.target===e&&++a>=c&&h()};setTimeout(()=>{a(n[S]||"").split(", "),r=s(`${ze}Delay`),i=s(`${ze}Duration`),o=Hr(r,i),l=s(`${Ht}Delay`),c=s(`${Ht}Duration`),f=Hr(l,c);let a=null,h=0,y=0;t===ze?o>0&&(a=ze,h=o,y=i.length):t===Ht?f>0&&(a=Ht,h=f,y=c.length):(h=Math.max(o,f),a=h>0?o>f?ze:Ht:null,y=a?a===ze?i.length:c.length:0);const v=a===ze&&/\b(transform|all)(,|$)/.test(s(`${ze}Property`).toString());return{type:a,timeout:h,propCount:y,hasTransform:v}}function Hr(e,t){for(;e.length$r(n)+$r(e[s])))}function $r(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function Dr(){return document.body.offsetHeight}function na(e,t,n){const s=e[Qt];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const jr=Symbol("_vod"),sa=Symbol("_vsh"),ra=Symbol(""),ia=/(^|;)\s*display\s*:/;function oa(e,t,n){const s=e.style,r=re(n);let i=!1;if(n&&!r){if(t)if(re(t))for(const o of t.split(";")){const l=o.slice(0,o.indexOf(":")).trim();n[l]==null&&En(s,l,"")}else for(const o in t)n[o]==null&&En(s,o,"");for(const o in n)o==="display"&&(i=!0),En(s,o,n[o])}else if(r){if(t!==n){const o=s[ra];o&&(n+=";"+o),s.cssText=n,i=ia.test(n)}}else t&&e.removeAttribute("style");jr in e&&(e[jr]=i?s.display:"",e[sa]&&(s.display="none"))}const Vr=/\s*!important$/;function En(e,t,n){if(W(n))n.forEach(s=>En(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=la(e,t);Vr.test(n)?e.setProperty(st(s),n.replace(Vr,""),"important"):e[s]=n}}const Ur=["Webkit","Moz","ms"],ls={};function la(e,t){const n=ls[t];if(n)return n;let s=Le(t);if(s!=="filter"&&s in e)return ls[t]=s;s=Fn(s);for(let r=0;rcs||(ua.then(()=>cs=0),cs=Date.now());function ha(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;He(pa(s,n.value),t,5,[s])};return n.value=e,n.attached=da(),n}function pa(e,t){if(W(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>r=>!r._stopped&&s&&s(r))}else return t}const Gr=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,ga=(e,t,n,s,r,i)=>{const o=r==="svg";t==="class"?na(e,s,o):t==="style"?oa(e,n,s):en(t)?Fs(t)||aa(e,t,n,s,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):ma(e,t,s,o))?(Wr(e,t,s),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Br(e,t,s,o,i,t!=="value")):e._isVueCE&&(/[A-Z]/.test(t)||!re(s))?Wr(e,Le(t),s,i,t):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),Br(e,t,s,o))};function ma(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&Gr(t)&&q(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return Gr(t)&&re(n)?!1:t in e}const Yr=e=>{const t=e.props["onUpdate:modelValue"]||!1;return W(t)?n=>bn(t,n):t};function ya(e){e.target.composing=!0}function Xr(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const as=Symbol("_assign"),If={created(e,{modifiers:{lazy:t,trim:n,number:s}},r){e[as]=Yr(r);const i=s||r.props&&r.props.type==="number";St(e,t?"change":"input",o=>{if(o.target.composing)return;let l=e.value;n&&(l=l.trim()),i&&(l=vs(l)),e[as](l)}),n&&St(e,"change",()=>{e.value=e.value.trim()}),t||(St(e,"compositionstart",ya),St(e,"compositionend",Xr),St(e,"change",Xr))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:s,trim:r,number:i}},o){if(e[as]=Yr(o),e.composing)return;const l=(i||e.type==="number")&&!/^0\d/.test(e.value)?vs(e.value):e.value,c=t??"";l!==c&&(document.activeElement===e&&e.type!=="range"&&(s&&t===n||r&&e.value.trim()===c)||(e.value=c))}},va=["ctrl","shift","alt","meta"],_a={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>va.some(n=>e[`${n}Key`]&&!t.includes(n))},Nf=(e,t)=>{const n=e._withMods||(e._withMods={}),s=t.join(".");return n[s]||(n[s]=(r,...i)=>{for(let o=0;o{const n=e._withKeys||(e._withKeys={}),s=t.join(".");return n[s]||(n[s]=r=>{if(!("key"in r))return;const i=st(r.key);if(t.some(o=>o===i||ba[o]===i))return e(r)})},To=ae({patchProp:ga},Xc);let Wt,Jr=!1;function wa(){return Wt||(Wt=xc(To))}function Sa(){return Wt=Jr?Wt:Ec(To),Jr=!0,Wt}const Hf=(...e)=>{const t=wa().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=Ao(s);if(!r)return;const i=t._component;!q(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.nodeType===1&&(r.textContent="");const o=n(r,!1,Co(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),o},t},$f=(...e)=>{const t=Sa().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=Ao(s);if(r)return n(r,!0,Co(r))},t};function Co(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Ao(e){return re(e)?document.querySelector(e):e}const Df=(e,t)=>{const n=e.__vccOpts||e;for(const[s,r]of t)n[s]=r;return n},xa=window.__VP_SITE_DATA__;function tr(e){return hi()?(il(e),!0):!1}function Be(e){return typeof e=="function"?e():Oi(e)}const Ro=typeof window<"u"&&typeof document<"u";typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const jf=e=>e!=null,Ea=Object.prototype.toString,Ta=e=>Ea.call(e)==="[object Object]",Zt=()=>{},zr=Ca();function Ca(){var e,t;return Ro&&((e=window==null?void 0:window.navigator)==null?void 0:e.userAgent)&&(/iP(?:ad|hone|od)/.test(window.navigator.userAgent)||((t=window==null?void 0:window.navigator)==null?void 0:t.maxTouchPoints)>2&&/iPad|Macintosh/.test(window==null?void 0:window.navigator.userAgent))}function Aa(e,t){function n(...s){return new Promise((r,i)=>{Promise.resolve(e(()=>t.apply(this,s),{fn:t,thisArg:this,args:s})).then(r).catch(i)})}return n}const Oo=e=>e();function Ra(e,t={}){let n,s,r=Zt;const i=l=>{clearTimeout(l),r(),r=Zt};return l=>{const c=Be(e),f=Be(t.maxWait);return n&&i(n),c<=0||f!==void 0&&f<=0?(s&&(i(s),s=null),Promise.resolve(l())):new Promise((a,h)=>{r=t.rejectOnCancel?h:a,f&&!s&&(s=setTimeout(()=>{n&&i(n),s=null,a(l())},f)),n=setTimeout(()=>{s&&i(s),s=null,a(l())},c)})}}function Oa(e=Oo){const t=oe(!0);function n(){t.value=!1}function s(){t.value=!0}const r=(...i)=>{t.value&&e(...i)};return{isActive:Vn(t),pause:n,resume:s,eventFilter:r}}function Ma(e){return qn()}function Mo(...e){if(e.length!==1)return Ll(...e);const t=e[0];return typeof t=="function"?Vn(Ol(()=>({get:t,set:Zt}))):oe(t)}function Po(e,t,n={}){const{eventFilter:s=Oo,...r}=n;return Fe(e,Aa(s,t),r)}function Pa(e,t,n={}){const{eventFilter:s,...r}=n,{eventFilter:i,pause:o,resume:l,isActive:c}=Oa(s);return{stop:Po(e,t,{...r,eventFilter:i}),pause:o,resume:l,isActive:c}}function nr(e,t=!0,n){Ma()?Lt(e,n):t?e():Un(e)}function Vf(e,t,n={}){const{debounce:s=0,maxWait:r=void 0,...i}=n;return Po(e,t,{...i,eventFilter:Ra(s,{maxWait:r})})}function Uf(e,t,n){let s;fe(n)?s={evaluating:n}:s={};const{lazy:r=!1,evaluating:i=void 0,shallow:o=!0,onError:l=Zt}=s,c=oe(!r),f=o?qs(t):oe(t);let a=0;return Zs(async h=>{if(!c.value)return;a++;const y=a;let v=!1;i&&Promise.resolve().then(()=>{i.value=!0});try{const S=await e(_=>{h(()=>{i&&(i.value=!1),v||_()})});y===a&&(f.value=S)}catch(S){l(S)}finally{i&&y===a&&(i.value=!1),v=!0}}),r?ie(()=>(c.value=!0,f.value)):f}const $e=Ro?window:void 0;function Lo(e){var t;const n=Be(e);return(t=n==null?void 0:n.$el)!=null?t:n}function Pt(...e){let t,n,s,r;if(typeof e[0]=="string"||Array.isArray(e[0])?([n,s,r]=e,t=$e):[t,n,s,r]=e,!t)return Zt;Array.isArray(n)||(n=[n]),Array.isArray(s)||(s=[s]);const i=[],o=()=>{i.forEach(a=>a()),i.length=0},l=(a,h,y,v)=>(a.addEventListener(h,y,v),()=>a.removeEventListener(h,y,v)),c=Fe(()=>[Lo(t),Be(r)],([a,h])=>{if(o(),!a)return;const y=Ta(h)?{...h}:h;i.push(...n.flatMap(v=>s.map(S=>l(a,v,S,y))))},{immediate:!0,flush:"post"}),f=()=>{c(),o()};return tr(f),f}function La(e){return typeof e=="function"?e:typeof e=="string"?t=>t.key===e:Array.isArray(e)?t=>e.includes(t.key):()=>!0}function kf(...e){let t,n,s={};e.length===3?(t=e[0],n=e[1],s=e[2]):e.length===2?typeof e[1]=="object"?(t=!0,n=e[0],s=e[1]):(t=e[0],n=e[1]):(t=!0,n=e[0]);const{target:r=$e,eventName:i="keydown",passive:o=!1,dedupe:l=!1}=s,c=La(t);return Pt(r,i,a=>{a.repeat&&Be(l)||c(a)&&n(a)},o)}function Ia(){const e=oe(!1),t=qn();return t&&Lt(()=>{e.value=!0},t),e}function Na(e){const t=Ia();return ie(()=>(t.value,!!e()))}function Io(e,t={}){const{window:n=$e}=t,s=Na(()=>n&&"matchMedia"in n&&typeof n.matchMedia=="function");let r;const i=oe(!1),o=f=>{i.value=f.matches},l=()=>{r&&("removeEventListener"in r?r.removeEventListener("change",o):r.removeListener(o))},c=Zs(()=>{s.value&&(l(),r=n.matchMedia(Be(e)),"addEventListener"in r?r.addEventListener("change",o):r.addListener(o),i.value=r.matches)});return tr(()=>{c(),l(),r=void 0}),i}const gn=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},mn="__vueuse_ssr_handlers__",Fa=Ha();function Ha(){return mn in gn||(gn[mn]=gn[mn]||{}),gn[mn]}function No(e,t){return Fa[e]||t}function sr(e){return Io("(prefers-color-scheme: dark)",e)}function $a(e){return e==null?"any":e instanceof Set?"set":e instanceof Map?"map":e instanceof Date?"date":typeof e=="boolean"?"boolean":typeof e=="string"?"string":typeof e=="object"?"object":Number.isNaN(e)?"any":"number"}const Da={boolean:{read:e=>e==="true",write:e=>String(e)},object:{read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},number:{read:e=>Number.parseFloat(e),write:e=>String(e)},any:{read:e=>e,write:e=>String(e)},string:{read:e=>e,write:e=>String(e)},map:{read:e=>new Map(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e.entries()))},set:{read:e=>new Set(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e))},date:{read:e=>new Date(e),write:e=>e.toISOString()}},Qr="vueuse-storage";function rr(e,t,n,s={}){var r;const{flush:i="pre",deep:o=!0,listenToStorageChanges:l=!0,writeDefaults:c=!0,mergeDefaults:f=!1,shallow:a,window:h=$e,eventFilter:y,onError:v=b=>{console.error(b)},initOnMounted:S}=s,_=(a?qs:oe)(typeof t=="function"?t():t);if(!n)try{n=No("getDefaultStorage",()=>{var b;return(b=$e)==null?void 0:b.localStorage})()}catch(b){v(b)}if(!n)return _;const K=Be(t),N=$a(K),j=(r=s.serializer)!=null?r:Da[N],{pause:p,resume:g}=Pa(_,()=>F(_.value),{flush:i,deep:o,eventFilter:y});h&&l&&nr(()=>{n instanceof Storage?Pt(h,"storage",V):Pt(h,Qr,R),S&&V()}),S||V();function O(b,I){if(h){const x={key:e,oldValue:b,newValue:I,storageArea:n};h.dispatchEvent(n instanceof Storage?new StorageEvent("storage",x):new CustomEvent(Qr,{detail:x}))}}function F(b){try{const I=n.getItem(e);if(b==null)O(I,null),n.removeItem(e);else{const x=j.write(b);I!==x&&(n.setItem(e,x),O(I,x))}}catch(I){v(I)}}function $(b){const I=b?b.newValue:n.getItem(e);if(I==null)return c&&K!=null&&n.setItem(e,j.write(K)),K;if(!b&&f){const x=j.read(I);return typeof f=="function"?f(x,K):N==="object"&&!Array.isArray(x)?{...K,...x}:x}else return typeof I!="string"?I:j.read(I)}function V(b){if(!(b&&b.storageArea!==n)){if(b&&b.key==null){_.value=K;return}if(!(b&&b.key!==e)){p();try{(b==null?void 0:b.newValue)!==j.write(_.value)&&(_.value=$(b))}catch(I){v(I)}finally{b?Un(g):g()}}}}function R(b){V(b.detail)}return _}const ja="*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}";function Va(e={}){const{selector:t="html",attribute:n="class",initialValue:s="auto",window:r=$e,storage:i,storageKey:o="vueuse-color-scheme",listenToStorageChanges:l=!0,storageRef:c,emitAuto:f,disableTransition:a=!0}=e,h={auto:"",light:"light",dark:"dark",...e.modes||{}},y=sr({window:r}),v=ie(()=>y.value?"dark":"light"),S=c||(o==null?Mo(s):rr(o,s,i,{window:r,listenToStorageChanges:l})),_=ie(()=>S.value==="auto"?v.value:S.value),K=No("updateHTMLAttrs",(g,O,F)=>{const $=typeof g=="string"?r==null?void 0:r.document.querySelector(g):Lo(g);if(!$)return;const V=new Set,R=new Set;let b=null;if(O==="class"){const x=F.split(/\s/g);Object.values(h).flatMap(B=>(B||"").split(/\s/g)).filter(Boolean).forEach(B=>{x.includes(B)?V.add(B):R.add(B)})}else b={key:O,value:F};if(V.size===0&&R.size===0&&b===null)return;let I;a&&(I=r.document.createElement("style"),I.appendChild(document.createTextNode(ja)),r.document.head.appendChild(I));for(const x of V)$.classList.add(x);for(const x of R)$.classList.remove(x);b&&$.setAttribute(b.key,b.value),a&&(r.getComputedStyle(I).opacity,document.head.removeChild(I))});function N(g){var O;K(t,n,(O=h[g])!=null?O:g)}function j(g){e.onChanged?e.onChanged(g,N):N(g)}Fe(_,j,{flush:"post",immediate:!0}),nr(()=>j(_.value));const p=ie({get(){return f?S.value:_.value},set(g){S.value=g}});try{return Object.assign(p,{store:S,system:v,state:_})}catch{return p}}function Ua(e={}){const{valueDark:t="dark",valueLight:n="",window:s=$e}=e,r=Va({...e,onChanged:(l,c)=>{var f;e.onChanged?(f=e.onChanged)==null||f.call(e,l==="dark",c,l):c(l)},modes:{dark:t,light:n}}),i=ie(()=>r.system?r.system.value:sr({window:s}).value?"dark":"light");return ie({get(){return r.value==="dark"},set(l){const c=l?"dark":"light";i.value===c?r.value="auto":r.value=c}})}function fs(e){return typeof Window<"u"&&e instanceof Window?e.document.documentElement:typeof Document<"u"&&e instanceof Document?e.documentElement:e}function Bf(e,t,n={}){const{window:s=$e}=n;return rr(e,t,s==null?void 0:s.localStorage,n)}function Fo(e){const t=window.getComputedStyle(e);if(t.overflowX==="scroll"||t.overflowY==="scroll"||t.overflowX==="auto"&&e.clientWidth1?!0:(t.preventDefault&&t.preventDefault(),!1)}const us=new WeakMap;function Wf(e,t=!1){const n=oe(t);let s=null,r="";Fe(Mo(e),l=>{const c=fs(Be(l));if(c){const f=c;if(us.get(f)||us.set(f,f.style.overflow),f.style.overflow!=="hidden"&&(r=f.style.overflow),f.style.overflow==="hidden")return n.value=!0;if(n.value)return f.style.overflow="hidden"}},{immediate:!0});const i=()=>{const l=fs(Be(e));!l||n.value||(zr&&(s=Pt(l,"touchmove",c=>{ka(c)},{passive:!1})),l.style.overflow="hidden",n.value=!0)},o=()=>{const l=fs(Be(e));!l||!n.value||(zr&&(s==null||s()),l.style.overflow=r,us.delete(l),n.value=!1)};return tr(o),ie({get(){return n.value},set(l){l?i():o()}})}function Kf(e,t,n={}){const{window:s=$e}=n;return rr(e,t,s==null?void 0:s.sessionStorage,n)}function qf(e={}){const{window:t=$e,behavior:n="auto"}=e;if(!t)return{x:oe(0),y:oe(0)};const s=oe(t.scrollX),r=oe(t.scrollY),i=ie({get(){return s.value},set(l){scrollTo({left:l,behavior:n})}}),o=ie({get(){return r.value},set(l){scrollTo({top:l,behavior:n})}});return Pt(t,"scroll",()=>{s.value=t.scrollX,r.value=t.scrollY},{capture:!1,passive:!0}),{x:i,y:o}}function Gf(e={}){const{window:t=$e,initialWidth:n=Number.POSITIVE_INFINITY,initialHeight:s=Number.POSITIVE_INFINITY,listenOrientation:r=!0,includeScrollbar:i=!0,type:o="inner"}=e,l=oe(n),c=oe(s),f=()=>{t&&(o==="outer"?(l.value=t.outerWidth,c.value=t.outerHeight):i?(l.value=t.innerWidth,c.value=t.innerHeight):(l.value=t.document.documentElement.clientWidth,c.value=t.document.documentElement.clientHeight))};if(f(),nr(f),Pt("resize",f,{passive:!0}),r){const a=Io("(orientation: portrait)");Fe(a,()=>f())}return{width:l,height:c}}const ds={BASE_URL:"/GeometryOps.jl/previews/PR239/",DEV:!1,MODE:"production",PROD:!0,SSR:!1};var hs={};const Ho=/^(?:[a-z]+:|\/\/)/i,Ba="vitepress-theme-appearance",Wa=/#.*$/,Ka=/[?#].*$/,qa=/(?:(^|\/)index)?\.(?:md|html)$/,ge=typeof document<"u",$o={relativePath:"404.md",filePath:"",title:"404",description:"Not Found",headers:[],frontmatter:{sidebar:!1,layout:"page"},lastUpdated:0,isNotFound:!0};function Ga(e,t,n=!1){if(t===void 0)return!1;if(e=Zr(`/${e}`),n)return new RegExp(t).test(e);if(Zr(t)!==e)return!1;const s=t.match(Wa);return s?(ge?location.hash:"")===s[0]:!0}function Zr(e){return decodeURI(e).replace(Ka,"").replace(qa,"$1")}function Ya(e){return Ho.test(e)}function Xa(e,t){return Object.keys((e==null?void 0:e.locales)||{}).find(n=>n!=="root"&&!Ya(n)&&Ga(t,`/${n}/`,!0))||"root"}function Ja(e,t){var s,r,i,o,l,c,f;const n=Xa(e,t);return Object.assign({},e,{localeIndex:n,lang:((s=e.locales[n])==null?void 0:s.lang)??e.lang,dir:((r=e.locales[n])==null?void 0:r.dir)??e.dir,title:((i=e.locales[n])==null?void 0:i.title)??e.title,titleTemplate:((o=e.locales[n])==null?void 0:o.titleTemplate)??e.titleTemplate,description:((l=e.locales[n])==null?void 0:l.description)??e.description,head:jo(e.head,((c=e.locales[n])==null?void 0:c.head)??[]),themeConfig:{...e.themeConfig,...(f=e.locales[n])==null?void 0:f.themeConfig}})}function Do(e,t){const n=t.title||e.title,s=t.titleTemplate??e.titleTemplate;if(typeof s=="string"&&s.includes(":title"))return s.replace(/:title/g,n);const r=za(e.title,s);return n===r.slice(3)?n:`${n}${r}`}function za(e,t){return t===!1?"":t===!0||t===void 0?` | ${e}`:e===t?"":` | ${t}`}function Qa(e,t){const[n,s]=t;if(n!=="meta")return!1;const r=Object.entries(s)[0];return r==null?!1:e.some(([i,o])=>i===n&&o[r[0]]===r[1])}function jo(e,t){return[...e.filter(n=>!Qa(t,n)),...t]}const Za=/[\u0000-\u001F"#$&*+,:;<=>?[\]^`{|}\u007F]/g,ef=/^[a-z]:/i;function ei(e){const t=ef.exec(e),n=t?t[0]:"";return n+e.slice(n.length).replace(Za,"_").replace(/(^|\/)_+(?=[^/]*$)/,"$1")}const ps=new Set;function tf(e){if(ps.size===0){const n=typeof process=="object"&&(hs==null?void 0:hs.VITE_EXTRA_EXTENSIONS)||(ds==null?void 0:ds.VITE_EXTRA_EXTENSIONS)||"";("3g2,3gp,aac,ai,apng,au,avif,bin,bmp,cer,class,conf,crl,css,csv,dll,doc,eps,epub,exe,gif,gz,ics,ief,jar,jpe,jpeg,jpg,js,json,jsonld,m4a,man,mid,midi,mjs,mov,mp2,mp3,mp4,mpe,mpeg,mpg,mpp,oga,ogg,ogv,ogx,opus,otf,p10,p7c,p7m,p7s,pdf,png,ps,qt,roff,rtf,rtx,ser,svg,t,tif,tiff,tr,ts,tsv,ttf,txt,vtt,wav,weba,webm,webp,woff,woff2,xhtml,xml,yaml,yml,zip"+(n&&typeof n=="string"?","+n:"")).split(",").forEach(s=>ps.add(s))}const t=e.split(".").pop();return t==null||!ps.has(t.toLowerCase())}function Yf(e){return e.replace(/[|\\{}()[\]^$+*?.]/g,"\\$&").replace(/-/g,"\\x2d")}const nf=Symbol(),mt=qs(xa);function Xf(e){const t=ie(()=>Ja(mt.value,e.data.relativePath)),n=t.value.appearance,s=n==="force-dark"?oe(!0):n==="force-auto"?sr():n?Ua({storageKey:Ba,initialValue:()=>n==="dark"?"dark":"auto",...typeof n=="object"?n:{}}):oe(!1),r=oe(ge?location.hash:"");return ge&&window.addEventListener("hashchange",()=>{r.value=location.hash}),Fe(()=>e.data,()=>{r.value=ge?location.hash:""}),{site:t,theme:ie(()=>t.value.themeConfig),page:ie(()=>e.data),frontmatter:ie(()=>e.data.frontmatter),params:ie(()=>e.data.params),lang:ie(()=>t.value.lang),dir:ie(()=>e.data.frontmatter.dir||t.value.dir),localeIndex:ie(()=>t.value.localeIndex||"root"),title:ie(()=>Do(t.value,e.data)),description:ie(()=>e.data.description||t.value.description),isDark:s,hash:ie(()=>r.value)}}function sf(){const e=Ot(nf);if(!e)throw new Error("vitepress data not properly injected in app");return e}function rf(e,t){return`${e}${t}`.replace(/\/+/g,"/")}function ti(e){return Ho.test(e)||!e.startsWith("/")?e:rf(mt.value.base,e)}function of(e){let t=e.replace(/\.html$/,"");if(t=decodeURIComponent(t),t=t.replace(/\/$/,"/index"),ge){const n="/GeometryOps.jl/previews/PR239/";t=ei(t.slice(n.length).replace(/\//g,"_")||"index")+".md";let s=__VP_HASH_MAP__[t.toLowerCase()];if(s||(t=t.endsWith("_index.md")?t.slice(0,-9)+".md":t.slice(0,-3)+"_index.md",s=__VP_HASH_MAP__[t.toLowerCase()]),!s)return null;t=`${n}assets/${t}.${s}.js`}else t=`./${ei(t.slice(1).replace(/\//g,"_"))}.md.js`;return t}let Tn=[];function Jf(e){Tn.push(e),Bn(()=>{Tn=Tn.filter(t=>t!==e)})}function lf(){let e=mt.value.scrollOffset,t=0,n=24;if(typeof e=="object"&&"padding"in e&&(n=e.padding,e=e.selector),typeof e=="number")t=e;else if(typeof e=="string")t=ni(e,n);else if(Array.isArray(e))for(const s of e){const r=ni(s,n);if(r){t=r;break}}return t}function ni(e,t){const n=document.querySelector(e);if(!n)return 0;const s=n.getBoundingClientRect().bottom;return s<0?0:s+t}const cf=Symbol(),Vo="http://a.com",af=()=>({path:"/",component:null,data:$o});function zf(e,t){const n=jn(af()),s={route:n,go:r};async function r(l=ge?location.href:"/"){var c,f;l=gs(l),await((c=s.onBeforeRouteChange)==null?void 0:c.call(s,l))!==!1&&(ge&&l!==gs(location.href)&&(history.replaceState({scrollPosition:window.scrollY},""),history.pushState({},"",l)),await o(l),await((f=s.onAfterRouteChanged)==null?void 0:f.call(s,l)))}let i=null;async function o(l,c=0,f=!1){var y,v;if(await((y=s.onBeforePageLoad)==null?void 0:y.call(s,l))===!1)return;const a=new URL(l,Vo),h=i=a.pathname;try{let S=await e(h);if(!S)throw new Error(`Page not found: ${h}`);if(i===h){i=null;const{default:_,__pageData:K}=S;if(!_)throw new Error(`Invalid route component: ${_}`);await((v=s.onAfterPageLoad)==null?void 0:v.call(s,l)),n.path=ge?h:ti(h),n.component=wn(_),n.data=wn(K),ge&&Un(()=>{let N=mt.value.base+K.relativePath.replace(/(?:(^|\/)index)?\.md$/,"$1");if(!mt.value.cleanUrls&&!N.endsWith("/")&&(N+=".html"),N!==a.pathname&&(a.pathname=N,l=N+a.search+a.hash,history.replaceState({},"",l)),a.hash&&!c){let j=null;try{j=document.getElementById(decodeURIComponent(a.hash).slice(1))}catch(p){console.warn(p)}if(j){si(j,a.hash);return}}window.scrollTo(0,c)})}}catch(S){if(!/fetch|Page not found/.test(S.message)&&!/^\/404(\.html|\/)?$/.test(l)&&console.error(S),!f)try{const _=await fetch(mt.value.base+"hashmap.json");window.__VP_HASH_MAP__=await _.json(),await o(l,c,!0);return}catch{}if(i===h){i=null,n.path=ge?h:ti(h),n.component=t?wn(t):null;const _=ge?h.replace(/(^|\/)$/,"$1index").replace(/(\.html)?$/,".md").replace(/^\//,""):"404.md";n.data={...$o,relativePath:_}}}}return ge&&(history.state===null&&history.replaceState({},""),window.addEventListener("click",l=>{if(l.defaultPrevented||!(l.target instanceof Element)||l.target.closest("button")||l.button!==0||l.ctrlKey||l.shiftKey||l.altKey||l.metaKey)return;const c=l.target.closest("a");if(!c||c.closest(".vp-raw")||c.hasAttribute("download")||c.hasAttribute("target"))return;const f=c.getAttribute("href")??(c instanceof SVGAElement?c.getAttribute("xlink:href"):null);if(f==null)return;const{href:a,origin:h,pathname:y,hash:v,search:S}=new URL(f,c.baseURI),_=new URL(location.href);h===_.origin&&tf(y)&&(l.preventDefault(),y===_.pathname&&S===_.search?(v!==_.hash&&(history.pushState({},"",a),window.dispatchEvent(new HashChangeEvent("hashchange",{oldURL:_.href,newURL:a}))),v?si(c,v,c.classList.contains("header-anchor")):window.scrollTo(0,0)):r(a))},{capture:!0}),window.addEventListener("popstate",async l=>{var c;l.state!==null&&(await o(gs(location.href),l.state&&l.state.scrollPosition||0),(c=s.onAfterRouteChanged)==null||c.call(s,location.href))}),window.addEventListener("hashchange",l=>{l.preventDefault()})),s}function ff(){const e=Ot(cf);if(!e)throw new Error("useRouter() is called without provider.");return e}function Uo(){return ff().route}function si(e,t,n=!1){let s=null;try{s=e.classList.contains("header-anchor")?e:document.getElementById(decodeURIComponent(t).slice(1))}catch(r){console.warn(r)}if(s){let r=function(){!n||Math.abs(o-window.scrollY)>window.innerHeight?window.scrollTo(0,o):window.scrollTo({left:0,top:o,behavior:"smooth"})};const i=parseInt(window.getComputedStyle(s).paddingTop,10),o=window.scrollY+s.getBoundingClientRect().top-lf()+i;requestAnimationFrame(r)}}function gs(e){const t=new URL(e,Vo);return t.pathname=t.pathname.replace(/(^|\/)index(\.html)?$/,"$1"),mt.value.cleanUrls?t.pathname=t.pathname.replace(/\.html$/,""):!t.pathname.endsWith("/")&&!t.pathname.endsWith(".html")&&(t.pathname+=".html"),t.pathname+t.search+t.hash}const yn=()=>Tn.forEach(e=>e()),Qf=Ys({name:"VitePressContent",props:{as:{type:[Object,String],default:"div"}},setup(e){const t=Uo(),{frontmatter:n,site:s}=sf();return Fe(n,yn,{deep:!0,flush:"post"}),()=>Ls(e.as,s.value.contentProps??{style:{position:"relative"}},[t.component?Ls(t.component,{onVnodeMounted:yn,onVnodeUpdated:yn,onVnodeUnmounted:yn}):"404 Page Not Found"])}}),uf="modulepreload",df=function(e){return"/GeometryOps.jl/previews/PR239/"+e},ri={},Zf=function(t,n,s){let r=Promise.resolve();if(n&&n.length>0){document.getElementsByTagName("link");const o=document.querySelector("meta[property=csp-nonce]"),l=(o==null?void 0:o.nonce)||(o==null?void 0:o.getAttribute("nonce"));r=Promise.allSettled(n.map(c=>{if(c=df(c),c in ri)return;ri[c]=!0;const f=c.endsWith(".css"),a=f?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${c}"]${a}`))return;const h=document.createElement("link");if(h.rel=f?"stylesheet":uf,f||(h.as="script"),h.crossOrigin="",h.href=c,l&&h.setAttribute("nonce",l),document.head.appendChild(h),f)return new Promise((y,v)=>{h.addEventListener("load",y),h.addEventListener("error",()=>v(new Error(`Unable to preload CSS for ${c}`)))})}))}function i(o){const l=new Event("vite:preloadError",{cancelable:!0});if(l.payload=o,window.dispatchEvent(l),!l.defaultPrevented)throw o}return r.then(o=>{for(const l of o||[])l.status==="rejected"&&i(l.reason);return t().catch(i)})},eu=Ys({setup(e,{slots:t}){const n=oe(!1);return Lt(()=>{n.value=!0}),()=>n.value&&t.default?t.default():null}});function tu(){ge&&window.addEventListener("click",e=>{var n;const t=e.target;if(t.matches(".vp-code-group input")){const s=(n=t.parentElement)==null?void 0:n.parentElement;if(!s)return;const r=Array.from(s.querySelectorAll("input")).indexOf(t);if(r<0)return;const i=s.querySelector(".blocks");if(!i)return;const o=Array.from(i.children).find(f=>f.classList.contains("active"));if(!o)return;const l=i.children[r];if(!l||o===l)return;o.classList.remove("active"),l.classList.add("active");const c=s==null?void 0:s.querySelector(`label[for="${t.id}"]`);c==null||c.scrollIntoView({block:"nearest"})}})}function nu(){if(ge){const e=new WeakMap;window.addEventListener("click",t=>{var s;const n=t.target;if(n.matches('div[class*="language-"] > button.copy')){const r=n.parentElement,i=(s=n.nextElementSibling)==null?void 0:s.nextElementSibling;if(!r||!i)return;const o=/language-(shellscript|shell|bash|sh|zsh)/.test(r.className),l=[".vp-copy-ignore",".diff.remove"],c=i.cloneNode(!0);c.querySelectorAll(l.join(",")).forEach(a=>a.remove());let f=c.textContent||"";o&&(f=f.replace(/^ *(\$|>) /gm,"").trim()),hf(f).then(()=>{n.classList.add("copied"),clearTimeout(e.get(n));const a=setTimeout(()=>{n.classList.remove("copied"),n.blur(),e.delete(n)},2e3);e.set(n,a)})}})}}async function hf(e){try{return navigator.clipboard.writeText(e)}catch{const t=document.createElement("textarea"),n=document.activeElement;t.value=e,t.setAttribute("readonly",""),t.style.contain="strict",t.style.position="absolute",t.style.left="-9999px",t.style.fontSize="12pt";const s=document.getSelection(),r=s?s.rangeCount>0&&s.getRangeAt(0):null;document.body.appendChild(t),t.select(),t.selectionStart=0,t.selectionEnd=e.length,document.execCommand("copy"),document.body.removeChild(t),r&&(s.removeAllRanges(),s.addRange(r)),n&&n.focus()}}function su(e,t){let n=!0,s=[];const r=i=>{if(n){n=!1,i.forEach(l=>{const c=ms(l);for(const f of document.head.children)if(f.isEqualNode(c)){s.push(f);return}});return}const o=i.map(ms);s.forEach((l,c)=>{const f=o.findIndex(a=>a==null?void 0:a.isEqualNode(l??null));f!==-1?delete o[f]:(l==null||l.remove(),delete s[c])}),o.forEach(l=>l&&document.head.appendChild(l)),s=[...s,...o].filter(Boolean)};Zs(()=>{const i=e.data,o=t.value,l=i&&i.description,c=i&&i.frontmatter.head||[],f=Do(o,i);f!==document.title&&(document.title=f);const a=l||o.description;let h=document.querySelector("meta[name=description]");h?h.getAttribute("content")!==a&&h.setAttribute("content",a):ms(["meta",{name:"description",content:a}]),r(jo(o.head,gf(c)))})}function ms([e,t,n]){const s=document.createElement(e);for(const r in t)s.setAttribute(r,t[r]);return n&&(s.innerHTML=n),e==="script"&&t.async==null&&(s.async=!1),s}function pf(e){return e[0]==="meta"&&e[1]&&e[1].name==="description"}function gf(e){return e.filter(t=>!pf(t))}const ys=new Set,ko=()=>document.createElement("link"),mf=e=>{const t=ko();t.rel="prefetch",t.href=e,document.head.appendChild(t)},yf=e=>{const t=new XMLHttpRequest;t.open("GET",e,t.withCredentials=!0),t.send()};let vn;const vf=ge&&(vn=ko())&&vn.relList&&vn.relList.supports&&vn.relList.supports("prefetch")?mf:yf;function ru(){if(!ge||!window.IntersectionObserver)return;let e;if((e=navigator.connection)&&(e.saveData||/2g/.test(e.effectiveType)))return;const t=window.requestIdleCallback||setTimeout;let n=null;const s=()=>{n&&n.disconnect(),n=new IntersectionObserver(i=>{i.forEach(o=>{if(o.isIntersecting){const l=o.target;n.unobserve(l);const{pathname:c}=l;if(!ys.has(c)){ys.add(c);const f=of(c);f&&vf(f)}}})}),t(()=>{document.querySelectorAll("#app a").forEach(i=>{const{hostname:o,pathname:l}=new URL(i.href instanceof SVGAnimatedString?i.href.animVal:i.href,i.baseURI),c=l.match(/\.\w+$/);c&&c[0]!==".html"||i.target!=="_blank"&&o===location.hostname&&(l!==location.pathname?n.observe(i):ys.add(l))})})};Lt(s);const r=Uo();Fe(()=>r.path,s),Bn(()=>{n&&n.disconnect()})}export{Ki as $,lf as A,Sf as B,Ef as C,qs as D,Jf as E,xe as F,ce as G,xf as H,Ho as I,Uo as J,$c as K,Ot as L,Gf as M,Ds as N,kf as O,Un as P,qf as Q,ge as R,Vn as S,Lf as T,wf as U,Zf as V,Wf as W,mc as X,Ff as Y,Cf as Z,Df as _,_o as a,Nf as a0,Af as a1,jn as a2,Ll as a3,Ls as a4,Mf as a5,su as a6,cf as a7,Xf as a8,nf as a9,Qf as aa,eu as ab,mt as ac,$f as ad,zf as ae,of as af,ru as ag,nu as ah,tu as ai,Be as aj,Lo as ak,jf as al,tr as am,Uf as an,Kf as ao,Bf as ap,Vf as aq,ff as ar,Pt as as,_f as at,If as au,fe as av,bf as aw,wn as ax,Hf as ay,Yf as az,Ms as b,Of as c,Ys as d,Pf as e,tf as f,ti as g,ie as h,Ya as i,vo as j,Oi as k,Ga as l,Io as m,js as n,Os as o,oe as p,Fe as q,Tf as r,Zs as s,sl as t,sf as u,Lt as v,Ul as w,Bn as x,Rf as y,nc as z}; diff --git a/previews/PR239/assets/chunks/theme.CkNAWswv.js b/previews/PR239/assets/chunks/theme.CkNAWswv.js new file mode 100644 index 000000000..f4de653df --- /dev/null +++ b/previews/PR239/assets/chunks/theme.CkNAWswv.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/chunks/VPLocalSearchBox.B2uKxNr_.js","assets/chunks/framework.onQNwZ2I.js"])))=>i.map(i=>d[i]); +import{d as m,o as a,c as u,r as c,n as I,a as z,t as w,b as g,w as f,e as h,T as de,_ as $,u as Ge,i as je,f as ze,g as pe,h as y,j as v,k as i,l as K,m as re,p as T,q as F,s as Z,v as O,x as ve,y as fe,z as Ke,A as Re,B as R,F as M,C as H,D as Ve,E as x,G as k,H as E,I as Te,J as ee,K as j,L as q,M as We,N as Ne,O as ie,P as he,Q as we,R as te,S as qe,U as Je,V as Ye,W as Ie,X as me,Y as Xe,Z as Qe,$ as Ze,a0 as xe,a1 as Me,a2 as et,a3 as tt,a4 as nt}from"./framework.onQNwZ2I.js";const st=m({__name:"VPBadge",props:{text:{},type:{default:"tip"}},setup(o){return(e,t)=>(a(),u("span",{class:I(["VPBadge",e.type])},[c(e.$slots,"default",{},()=>[z(w(e.text),1)])],2))}}),ot={key:0,class:"VPBackdrop"},at=m({__name:"VPBackdrop",props:{show:{type:Boolean}},setup(o){return(e,t)=>(a(),g(de,{name:"fade"},{default:f(()=>[e.show?(a(),u("div",ot)):h("",!0)]),_:1}))}}),rt=$(at,[["__scopeId","data-v-b06cdb19"]]),V=Ge;function it(o,e){let t,s=!1;return()=>{t&&clearTimeout(t),s?t=setTimeout(o,e):(o(),(s=!0)&&setTimeout(()=>s=!1,e))}}function le(o){return/^\//.test(o)?o:`/${o}`}function _e(o){const{pathname:e,search:t,hash:s,protocol:n}=new URL(o,"http://a.com");if(je(o)||o.startsWith("#")||!n.startsWith("http")||!ze(e))return o;const{site:r}=V(),l=e.endsWith("/")||e.endsWith(".html")?o:o.replace(/(?:(^\.+)\/)?.*$/,`$1${e.replace(/(\.md)?$/,r.value.cleanUrls?"":".html")}${t}${s}`);return pe(l)}function Y({correspondingLink:o=!1}={}){const{site:e,localeIndex:t,page:s,theme:n,hash:r}=V(),l=y(()=>{var p,b;return{label:(p=e.value.locales[t.value])==null?void 0:p.label,link:((b=e.value.locales[t.value])==null?void 0:b.link)||(t.value==="root"?"/":`/${t.value}/`)}});return{localeLinks:y(()=>Object.entries(e.value.locales).flatMap(([p,b])=>l.value.label===b.label?[]:{text:b.label,link:lt(b.link||(p==="root"?"/":`/${p}/`),n.value.i18nRouting!==!1&&o,s.value.relativePath.slice(l.value.link.length-1),!e.value.cleanUrls)+r.value})),currentLang:l}}function lt(o,e,t,s){return e?o.replace(/\/$/,"")+le(t.replace(/(^|\/)index\.md$/,"$1").replace(/\.md$/,s?".html":"")):o}const ct={class:"NotFound"},ut={class:"code"},dt={class:"title"},pt={class:"quote"},vt={class:"action"},ft=["href","aria-label"],ht=m({__name:"NotFound",setup(o){const{theme:e}=V(),{currentLang:t}=Y();return(s,n)=>{var r,l,d,p,b;return a(),u("div",ct,[v("p",ut,w(((r=i(e).notFound)==null?void 0:r.code)??"404"),1),v("h1",dt,w(((l=i(e).notFound)==null?void 0:l.title)??"PAGE NOT FOUND"),1),n[0]||(n[0]=v("div",{class:"divider"},null,-1)),v("blockquote",pt,w(((d=i(e).notFound)==null?void 0:d.quote)??"But if you don't change your direction, and if you keep looking, you may end up where you are heading."),1),v("div",vt,[v("a",{class:"link",href:i(pe)(i(t).link),"aria-label":((p=i(e).notFound)==null?void 0:p.linkLabel)??"go to home"},w(((b=i(e).notFound)==null?void 0:b.linkText)??"Take me home"),9,ft)])])}}}),mt=$(ht,[["__scopeId","data-v-951cab6c"]]);function Ae(o,e){if(Array.isArray(o))return X(o);if(o==null)return[];e=le(e);const t=Object.keys(o).sort((n,r)=>r.split("/").length-n.split("/").length).find(n=>e.startsWith(le(n))),s=t?o[t]:[];return Array.isArray(s)?X(s):X(s.items,s.base)}function _t(o){const e=[];let t=0;for(const s in o){const n=o[s];if(n.items){t=e.push(n);continue}e[t]||e.push({items:[]}),e[t].items.push(n)}return e}function bt(o){const e=[];function t(s){for(const n of s)n.text&&n.link&&e.push({text:n.text,link:n.link,docFooterText:n.docFooterText}),n.items&&t(n.items)}return t(o),e}function ce(o,e){return Array.isArray(e)?e.some(t=>ce(o,t)):K(o,e.link)?!0:e.items?ce(o,e.items):!1}function X(o,e){return[...o].map(t=>{const s={...t},n=s.base||e;return n&&s.link&&(s.link=n+s.link),s.items&&(s.items=X(s.items,n)),s})}function U(){const{frontmatter:o,page:e,theme:t}=V(),s=re("(min-width: 960px)"),n=T(!1),r=y(()=>{const C=t.value.sidebar,N=e.value.relativePath;return C?Ae(C,N):[]}),l=T(r.value);F(r,(C,N)=>{JSON.stringify(C)!==JSON.stringify(N)&&(l.value=r.value)});const d=y(()=>o.value.sidebar!==!1&&l.value.length>0&&o.value.layout!=="home"),p=y(()=>b?o.value.aside==null?t.value.aside==="left":o.value.aside==="left":!1),b=y(()=>o.value.layout==="home"?!1:o.value.aside!=null?!!o.value.aside:t.value.aside!==!1),L=y(()=>d.value&&s.value),_=y(()=>d.value?_t(l.value):[]);function P(){n.value=!0}function S(){n.value=!1}function A(){n.value?S():P()}return{isOpen:n,sidebar:l,sidebarGroups:_,hasSidebar:d,hasAside:b,leftAside:p,isSidebarEnabled:L,open:P,close:S,toggle:A}}function kt(o,e){let t;Z(()=>{t=o.value?document.activeElement:void 0}),O(()=>{window.addEventListener("keyup",s)}),ve(()=>{window.removeEventListener("keyup",s)});function s(n){n.key==="Escape"&&o.value&&(e(),t==null||t.focus())}}function gt(o){const{page:e,hash:t}=V(),s=T(!1),n=y(()=>o.value.collapsed!=null),r=y(()=>!!o.value.link),l=T(!1),d=()=>{l.value=K(e.value.relativePath,o.value.link)};F([e,o,t],d),O(d);const p=y(()=>l.value?!0:o.value.items?ce(e.value.relativePath,o.value.items):!1),b=y(()=>!!(o.value.items&&o.value.items.length));Z(()=>{s.value=!!(n.value&&o.value.collapsed)}),fe(()=>{(l.value||p.value)&&(s.value=!1)});function L(){n.value&&(s.value=!s.value)}return{collapsed:s,collapsible:n,isLink:r,isActiveLink:l,hasActiveLink:p,hasChildren:b,toggle:L}}function $t(){const{hasSidebar:o}=U(),e=re("(min-width: 960px)"),t=re("(min-width: 1280px)");return{isAsideEnabled:y(()=>!t.value&&!e.value?!1:o.value?t.value:e.value)}}const ue=[];function Ce(o){return typeof o.outline=="object"&&!Array.isArray(o.outline)&&o.outline.label||o.outlineTitle||"On this page"}function be(o){const e=[...document.querySelectorAll(".VPDoc :where(h1,h2,h3,h4,h5,h6)")].filter(t=>t.id&&t.hasChildNodes()).map(t=>{const s=Number(t.tagName[1]);return{element:t,title:yt(t),link:"#"+t.id,level:s}});return Pt(e,o)}function yt(o){let e="";for(const t of o.childNodes)if(t.nodeType===1){if(t.classList.contains("VPBadge")||t.classList.contains("header-anchor")||t.classList.contains("ignore-header"))continue;e+=t.textContent}else t.nodeType===3&&(e+=t.textContent);return e.trim()}function Pt(o,e){if(e===!1)return[];const t=(typeof e=="object"&&!Array.isArray(e)?e.level:e)||2,[s,n]=typeof t=="number"?[t,t]:t==="deep"?[2,6]:t;return Vt(o,s,n)}function St(o,e){const{isAsideEnabled:t}=$t(),s=it(r,100);let n=null;O(()=>{requestAnimationFrame(r),window.addEventListener("scroll",s)}),Ke(()=>{l(location.hash)}),ve(()=>{window.removeEventListener("scroll",s)});function r(){if(!t.value)return;const d=window.scrollY,p=window.innerHeight,b=document.body.offsetHeight,L=Math.abs(d+p-b)<1,_=ue.map(({element:S,link:A})=>({link:A,top:Lt(S)})).filter(({top:S})=>!Number.isNaN(S)).sort((S,A)=>S.top-A.top);if(!_.length){l(null);return}if(d<1){l(null);return}if(L){l(_[_.length-1].link);return}let P=null;for(const{link:S,top:A}of _){if(A>d+Re()+4)break;P=S}l(P)}function l(d){n&&n.classList.remove("active"),d==null?n=null:n=o.value.querySelector(`a[href="${decodeURIComponent(d)}"]`);const p=n;p?(p.classList.add("active"),e.value.style.top=p.offsetTop+39+"px",e.value.style.opacity="1"):(e.value.style.top="33px",e.value.style.opacity="0")}}function Lt(o){let e=0;for(;o!==document.body;){if(o===null)return NaN;e+=o.offsetTop,o=o.offsetParent}return e}function Vt(o,e,t){ue.length=0;const s=[],n=[];return o.forEach(r=>{const l={...r,children:[]};let d=n[n.length-1];for(;d&&d.level>=l.level;)n.pop(),d=n[n.length-1];if(l.element.classList.contains("ignore-header")||d&&"shouldIgnore"in d){n.push({level:l.level,shouldIgnore:!0});return}l.level>t||l.level{const n=R("VPDocOutlineItem",!0);return a(),u("ul",{class:I(["VPDocOutlineItem",t.root?"root":"nested"])},[(a(!0),u(M,null,H(t.headers,({children:r,link:l,title:d})=>(a(),u("li",null,[v("a",{class:"outline-link",href:l,onClick:e,title:d},w(d),9,Tt),r!=null&&r.length?(a(),g(n,{key:0,headers:r},null,8,["headers"])):h("",!0)]))),256))],2)}}}),He=$(Nt,[["__scopeId","data-v-3f927ebe"]]),wt={class:"content"},It={"aria-level":"2",class:"outline-title",id:"doc-outline-aria-label",role:"heading"},Mt=m({__name:"VPDocAsideOutline",setup(o){const{frontmatter:e,theme:t}=V(),s=Ve([]);x(()=>{s.value=be(e.value.outline??t.value.outline)});const n=T(),r=T();return St(n,r),(l,d)=>(a(),u("nav",{"aria-labelledby":"doc-outline-aria-label",class:I(["VPDocAsideOutline",{"has-outline":s.value.length>0}]),ref_key:"container",ref:n},[v("div",wt,[v("div",{class:"outline-marker",ref_key:"marker",ref:r},null,512),v("div",It,w(i(Ce)(i(t))),1),k(He,{headers:s.value,root:!0},null,8,["headers"])])],2))}}),At=$(Mt,[["__scopeId","data-v-b38bf2ff"]]),Ct={class:"VPDocAsideCarbonAds"},Ht=m({__name:"VPDocAsideCarbonAds",props:{carbonAds:{}},setup(o){const e=()=>null;return(t,s)=>(a(),u("div",Ct,[k(i(e),{"carbon-ads":t.carbonAds},null,8,["carbon-ads"])]))}}),Bt={class:"VPDocAside"},Et=m({__name:"VPDocAside",setup(o){const{theme:e}=V();return(t,s)=>(a(),u("div",Bt,[c(t.$slots,"aside-top",{},void 0,!0),c(t.$slots,"aside-outline-before",{},void 0,!0),k(At),c(t.$slots,"aside-outline-after",{},void 0,!0),s[0]||(s[0]=v("div",{class:"spacer"},null,-1)),c(t.$slots,"aside-ads-before",{},void 0,!0),i(e).carbonAds?(a(),g(Ht,{key:0,"carbon-ads":i(e).carbonAds},null,8,["carbon-ads"])):h("",!0),c(t.$slots,"aside-ads-after",{},void 0,!0),c(t.$slots,"aside-bottom",{},void 0,!0)]))}}),Dt=$(Et,[["__scopeId","data-v-6d7b3c46"]]);function Ft(){const{theme:o,page:e}=V();return y(()=>{const{text:t="Edit this page",pattern:s=""}=o.value.editLink||{};let n;return typeof s=="function"?n=s(e.value):n=s.replace(/:path/g,e.value.filePath),{url:n,text:t}})}function Ot(){const{page:o,theme:e,frontmatter:t}=V();return y(()=>{var b,L,_,P,S,A,C,N;const s=Ae(e.value.sidebar,o.value.relativePath),n=bt(s),r=Ut(n,B=>B.link.replace(/[?#].*$/,"")),l=r.findIndex(B=>K(o.value.relativePath,B.link)),d=((b=e.value.docFooter)==null?void 0:b.prev)===!1&&!t.value.prev||t.value.prev===!1,p=((L=e.value.docFooter)==null?void 0:L.next)===!1&&!t.value.next||t.value.next===!1;return{prev:d?void 0:{text:(typeof t.value.prev=="string"?t.value.prev:typeof t.value.prev=="object"?t.value.prev.text:void 0)??((_=r[l-1])==null?void 0:_.docFooterText)??((P=r[l-1])==null?void 0:P.text),link:(typeof t.value.prev=="object"?t.value.prev.link:void 0)??((S=r[l-1])==null?void 0:S.link)},next:p?void 0:{text:(typeof t.value.next=="string"?t.value.next:typeof t.value.next=="object"?t.value.next.text:void 0)??((A=r[l+1])==null?void 0:A.docFooterText)??((C=r[l+1])==null?void 0:C.text),link:(typeof t.value.next=="object"?t.value.next.link:void 0)??((N=r[l+1])==null?void 0:N.link)}}})}function Ut(o,e){const t=new Set;return o.filter(s=>{const n=e(s);return t.has(n)?!1:t.add(n)})}const D=m({__name:"VPLink",props:{tag:{},href:{},noIcon:{type:Boolean},target:{},rel:{}},setup(o){const e=o,t=y(()=>e.tag??(e.href?"a":"span")),s=y(()=>e.href&&Te.test(e.href)||e.target==="_blank");return(n,r)=>(a(),g(E(t.value),{class:I(["VPLink",{link:n.href,"vp-external-link-icon":s.value,"no-icon":n.noIcon}]),href:n.href?i(_e)(n.href):void 0,target:n.target??(s.value?"_blank":void 0),rel:n.rel??(s.value?"noreferrer":void 0)},{default:f(()=>[c(n.$slots,"default")]),_:3},8,["class","href","target","rel"]))}}),Gt={class:"VPLastUpdated"},jt=["datetime"],zt=m({__name:"VPDocFooterLastUpdated",setup(o){const{theme:e,page:t,lang:s}=V(),n=y(()=>new Date(t.value.lastUpdated)),r=y(()=>n.value.toISOString()),l=T("");return O(()=>{Z(()=>{var d,p,b;l.value=new Intl.DateTimeFormat((p=(d=e.value.lastUpdated)==null?void 0:d.formatOptions)!=null&&p.forceLocale?s.value:void 0,((b=e.value.lastUpdated)==null?void 0:b.formatOptions)??{dateStyle:"short",timeStyle:"short"}).format(n.value)})}),(d,p)=>{var b;return a(),u("p",Gt,[z(w(((b=i(e).lastUpdated)==null?void 0:b.text)||i(e).lastUpdatedText||"Last updated")+": ",1),v("time",{datetime:r.value},w(l.value),9,jt)])}}}),Kt=$(zt,[["__scopeId","data-v-475f71b8"]]),Rt={key:0,class:"VPDocFooter"},Wt={key:0,class:"edit-info"},qt={key:0,class:"edit-link"},Jt={key:1,class:"last-updated"},Yt={key:1,class:"prev-next","aria-labelledby":"doc-footer-aria-label"},Xt={class:"pager"},Qt=["innerHTML"],Zt=["innerHTML"],xt={class:"pager"},en=["innerHTML"],tn=["innerHTML"],nn=m({__name:"VPDocFooter",setup(o){const{theme:e,page:t,frontmatter:s}=V(),n=Ft(),r=Ot(),l=y(()=>e.value.editLink&&s.value.editLink!==!1),d=y(()=>t.value.lastUpdated),p=y(()=>l.value||d.value||r.value.prev||r.value.next);return(b,L)=>{var _,P,S,A;return p.value?(a(),u("footer",Rt,[c(b.$slots,"doc-footer-before",{},void 0,!0),l.value||d.value?(a(),u("div",Wt,[l.value?(a(),u("div",qt,[k(D,{class:"edit-link-button",href:i(n).url,"no-icon":!0},{default:f(()=>[L[0]||(L[0]=v("span",{class:"vpi-square-pen edit-link-icon"},null,-1)),z(" "+w(i(n).text),1)]),_:1},8,["href"])])):h("",!0),d.value?(a(),u("div",Jt,[k(Kt)])):h("",!0)])):h("",!0),(_=i(r).prev)!=null&&_.link||(P=i(r).next)!=null&&P.link?(a(),u("nav",Yt,[L[1]||(L[1]=v("span",{class:"visually-hidden",id:"doc-footer-aria-label"},"Pager",-1)),v("div",Xt,[(S=i(r).prev)!=null&&S.link?(a(),g(D,{key:0,class:"pager-link prev",href:i(r).prev.link},{default:f(()=>{var C;return[v("span",{class:"desc",innerHTML:((C=i(e).docFooter)==null?void 0:C.prev)||"Previous page"},null,8,Qt),v("span",{class:"title",innerHTML:i(r).prev.text},null,8,Zt)]}),_:1},8,["href"])):h("",!0)]),v("div",xt,[(A=i(r).next)!=null&&A.link?(a(),g(D,{key:0,class:"pager-link next",href:i(r).next.link},{default:f(()=>{var C;return[v("span",{class:"desc",innerHTML:((C=i(e).docFooter)==null?void 0:C.next)||"Next page"},null,8,en),v("span",{class:"title",innerHTML:i(r).next.text},null,8,tn)]}),_:1},8,["href"])):h("",!0)])])):h("",!0)])):h("",!0)}}}),sn=$(nn,[["__scopeId","data-v-4f9813fa"]]),on={class:"container"},an={class:"aside-container"},rn={class:"aside-content"},ln={class:"content"},cn={class:"content-container"},un={class:"main"},dn=m({__name:"VPDoc",setup(o){const{theme:e}=V(),t=ee(),{hasSidebar:s,hasAside:n,leftAside:r}=U(),l=y(()=>t.path.replace(/[./]+/g,"_").replace(/_html$/,""));return(d,p)=>{const b=R("Content");return a(),u("div",{class:I(["VPDoc",{"has-sidebar":i(s),"has-aside":i(n)}])},[c(d.$slots,"doc-top",{},void 0,!0),v("div",on,[i(n)?(a(),u("div",{key:0,class:I(["aside",{"left-aside":i(r)}])},[p[0]||(p[0]=v("div",{class:"aside-curtain"},null,-1)),v("div",an,[v("div",rn,[k(Dt,null,{"aside-top":f(()=>[c(d.$slots,"aside-top",{},void 0,!0)]),"aside-bottom":f(()=>[c(d.$slots,"aside-bottom",{},void 0,!0)]),"aside-outline-before":f(()=>[c(d.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":f(()=>[c(d.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":f(()=>[c(d.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":f(()=>[c(d.$slots,"aside-ads-after",{},void 0,!0)]),_:3})])])],2)):h("",!0),v("div",ln,[v("div",cn,[c(d.$slots,"doc-before",{},void 0,!0),v("main",un,[k(b,{class:I(["vp-doc",[l.value,i(e).externalLinkIcon&&"external-link-icon-enabled"]])},null,8,["class"])]),k(sn,null,{"doc-footer-before":f(()=>[c(d.$slots,"doc-footer-before",{},void 0,!0)]),_:3}),c(d.$slots,"doc-after",{},void 0,!0)])])]),c(d.$slots,"doc-bottom",{},void 0,!0)],2)}}}),pn=$(dn,[["__scopeId","data-v-83890dd9"]]),vn=m({__name:"VPButton",props:{tag:{},size:{default:"medium"},theme:{default:"brand"},text:{},href:{},target:{},rel:{}},setup(o){const e=o,t=y(()=>e.href&&Te.test(e.href)),s=y(()=>e.tag||(e.href?"a":"button"));return(n,r)=>(a(),g(E(s.value),{class:I(["VPButton",[n.size,n.theme]]),href:n.href?i(_e)(n.href):void 0,target:e.target??(t.value?"_blank":void 0),rel:e.rel??(t.value?"noreferrer":void 0)},{default:f(()=>[z(w(n.text),1)]),_:1},8,["class","href","target","rel"]))}}),fn=$(vn,[["__scopeId","data-v-906d7fb4"]]),hn=["src","alt"],mn=m({inheritAttrs:!1,__name:"VPImage",props:{image:{},alt:{}},setup(o){return(e,t)=>{const s=R("VPImage",!0);return e.image?(a(),u(M,{key:0},[typeof e.image=="string"||"src"in e.image?(a(),u("img",j({key:0,class:"VPImage"},typeof e.image=="string"?e.$attrs:{...e.image,...e.$attrs},{src:i(pe)(typeof e.image=="string"?e.image:e.image.src),alt:e.alt??(typeof e.image=="string"?"":e.image.alt||"")}),null,16,hn)):(a(),u(M,{key:1},[k(s,j({class:"dark",image:e.image.dark,alt:e.image.alt},e.$attrs),null,16,["image","alt"]),k(s,j({class:"light",image:e.image.light,alt:e.image.alt},e.$attrs),null,16,["image","alt"])],64))],64)):h("",!0)}}}),Q=$(mn,[["__scopeId","data-v-35a7d0b8"]]),_n={class:"container"},bn={class:"main"},kn={key:0,class:"name"},gn=["innerHTML"],$n=["innerHTML"],yn=["innerHTML"],Pn={key:0,class:"actions"},Sn={key:0,class:"image"},Ln={class:"image-container"},Vn=m({__name:"VPHero",props:{name:{},text:{},tagline:{},image:{},actions:{}},setup(o){const e=q("hero-image-slot-exists");return(t,s)=>(a(),u("div",{class:I(["VPHero",{"has-image":t.image||i(e)}])},[v("div",_n,[v("div",bn,[c(t.$slots,"home-hero-info-before",{},void 0,!0),c(t.$slots,"home-hero-info",{},()=>[t.name?(a(),u("h1",kn,[v("span",{innerHTML:t.name,class:"clip"},null,8,gn)])):h("",!0),t.text?(a(),u("p",{key:1,innerHTML:t.text,class:"text"},null,8,$n)):h("",!0),t.tagline?(a(),u("p",{key:2,innerHTML:t.tagline,class:"tagline"},null,8,yn)):h("",!0)],!0),c(t.$slots,"home-hero-info-after",{},void 0,!0),t.actions?(a(),u("div",Pn,[(a(!0),u(M,null,H(t.actions,n=>(a(),u("div",{key:n.link,class:"action"},[k(fn,{tag:"a",size:"medium",theme:n.theme,text:n.text,href:n.link,target:n.target,rel:n.rel},null,8,["theme","text","href","target","rel"])]))),128))])):h("",!0),c(t.$slots,"home-hero-actions-after",{},void 0,!0)]),t.image||i(e)?(a(),u("div",Sn,[v("div",Ln,[s[0]||(s[0]=v("div",{class:"image-bg"},null,-1)),c(t.$slots,"home-hero-image",{},()=>[t.image?(a(),g(Q,{key:0,class:"image-src",image:t.image},null,8,["image"])):h("",!0)],!0)])])):h("",!0)])],2))}}),Tn=$(Vn,[["__scopeId","data-v-955009fc"]]),Nn=m({__name:"VPHomeHero",setup(o){const{frontmatter:e}=V();return(t,s)=>i(e).hero?(a(),g(Tn,{key:0,class:"VPHomeHero",name:i(e).hero.name,text:i(e).hero.text,tagline:i(e).hero.tagline,image:i(e).hero.image,actions:i(e).hero.actions},{"home-hero-info-before":f(()=>[c(t.$slots,"home-hero-info-before")]),"home-hero-info":f(()=>[c(t.$slots,"home-hero-info")]),"home-hero-info-after":f(()=>[c(t.$slots,"home-hero-info-after")]),"home-hero-actions-after":f(()=>[c(t.$slots,"home-hero-actions-after")]),"home-hero-image":f(()=>[c(t.$slots,"home-hero-image")]),_:3},8,["name","text","tagline","image","actions"])):h("",!0)}}),wn={class:"box"},In={key:0,class:"icon"},Mn=["innerHTML"],An=["innerHTML"],Cn=["innerHTML"],Hn={key:4,class:"link-text"},Bn={class:"link-text-value"},En=m({__name:"VPFeature",props:{icon:{},title:{},details:{},link:{},linkText:{},rel:{},target:{}},setup(o){return(e,t)=>(a(),g(D,{class:"VPFeature",href:e.link,rel:e.rel,target:e.target,"no-icon":!0,tag:e.link?"a":"div"},{default:f(()=>[v("article",wn,[typeof e.icon=="object"&&e.icon.wrap?(a(),u("div",In,[k(Q,{image:e.icon,alt:e.icon.alt,height:e.icon.height||48,width:e.icon.width||48},null,8,["image","alt","height","width"])])):typeof e.icon=="object"?(a(),g(Q,{key:1,image:e.icon,alt:e.icon.alt,height:e.icon.height||48,width:e.icon.width||48},null,8,["image","alt","height","width"])):e.icon?(a(),u("div",{key:2,class:"icon",innerHTML:e.icon},null,8,Mn)):h("",!0),v("h2",{class:"title",innerHTML:e.title},null,8,An),e.details?(a(),u("p",{key:3,class:"details",innerHTML:e.details},null,8,Cn)):h("",!0),e.linkText?(a(),u("div",Hn,[v("p",Bn,[z(w(e.linkText)+" ",1),t[0]||(t[0]=v("span",{class:"vpi-arrow-right link-text-icon"},null,-1))])])):h("",!0)])]),_:1},8,["href","rel","target","tag"]))}}),Dn=$(En,[["__scopeId","data-v-f5e9645b"]]),Fn={key:0,class:"VPFeatures"},On={class:"container"},Un={class:"items"},Gn=m({__name:"VPFeatures",props:{features:{}},setup(o){const e=o,t=y(()=>{const s=e.features.length;if(s){if(s===2)return"grid-2";if(s===3)return"grid-3";if(s%3===0)return"grid-6";if(s>3)return"grid-4"}else return});return(s,n)=>s.features?(a(),u("div",Fn,[v("div",On,[v("div",Un,[(a(!0),u(M,null,H(s.features,r=>(a(),u("div",{key:r.title,class:I(["item",[t.value]])},[k(Dn,{icon:r.icon,title:r.title,details:r.details,link:r.link,"link-text":r.linkText,rel:r.rel,target:r.target},null,8,["icon","title","details","link","link-text","rel","target"])],2))),128))])])])):h("",!0)}}),jn=$(Gn,[["__scopeId","data-v-d0a190d7"]]),zn=m({__name:"VPHomeFeatures",setup(o){const{frontmatter:e}=V();return(t,s)=>i(e).features?(a(),g(jn,{key:0,class:"VPHomeFeatures",features:i(e).features},null,8,["features"])):h("",!0)}}),Kn=m({__name:"VPHomeContent",setup(o){const{width:e}=We({initialWidth:0,includeScrollbar:!1});return(t,s)=>(a(),u("div",{class:"vp-doc container",style:Ne(i(e)?{"--vp-offset":`calc(50% - ${i(e)/2}px)`}:{})},[c(t.$slots,"default",{},void 0,!0)],4))}}),Rn=$(Kn,[["__scopeId","data-v-7a48a447"]]),Wn={class:"VPHome"},qn=m({__name:"VPHome",setup(o){const{frontmatter:e}=V();return(t,s)=>{const n=R("Content");return a(),u("div",Wn,[c(t.$slots,"home-hero-before",{},void 0,!0),k(Nn,null,{"home-hero-info-before":f(()=>[c(t.$slots,"home-hero-info-before",{},void 0,!0)]),"home-hero-info":f(()=>[c(t.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-info-after":f(()=>[c(t.$slots,"home-hero-info-after",{},void 0,!0)]),"home-hero-actions-after":f(()=>[c(t.$slots,"home-hero-actions-after",{},void 0,!0)]),"home-hero-image":f(()=>[c(t.$slots,"home-hero-image",{},void 0,!0)]),_:3}),c(t.$slots,"home-hero-after",{},void 0,!0),c(t.$slots,"home-features-before",{},void 0,!0),k(zn),c(t.$slots,"home-features-after",{},void 0,!0),i(e).markdownStyles!==!1?(a(),g(Rn,{key:0},{default:f(()=>[k(n)]),_:1})):(a(),g(n,{key:1}))])}}}),Jn=$(qn,[["__scopeId","data-v-cbb6ec48"]]),Yn={},Xn={class:"VPPage"};function Qn(o,e){const t=R("Content");return a(),u("div",Xn,[c(o.$slots,"page-top"),k(t),c(o.$slots,"page-bottom")])}const Zn=$(Yn,[["render",Qn]]),xn=m({__name:"VPContent",setup(o){const{page:e,frontmatter:t}=V(),{hasSidebar:s}=U();return(n,r)=>(a(),u("div",{class:I(["VPContent",{"has-sidebar":i(s),"is-home":i(t).layout==="home"}]),id:"VPContent"},[i(e).isNotFound?c(n.$slots,"not-found",{key:0},()=>[k(mt)],!0):i(t).layout==="page"?(a(),g(Zn,{key:1},{"page-top":f(()=>[c(n.$slots,"page-top",{},void 0,!0)]),"page-bottom":f(()=>[c(n.$slots,"page-bottom",{},void 0,!0)]),_:3})):i(t).layout==="home"?(a(),g(Jn,{key:2},{"home-hero-before":f(()=>[c(n.$slots,"home-hero-before",{},void 0,!0)]),"home-hero-info-before":f(()=>[c(n.$slots,"home-hero-info-before",{},void 0,!0)]),"home-hero-info":f(()=>[c(n.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-info-after":f(()=>[c(n.$slots,"home-hero-info-after",{},void 0,!0)]),"home-hero-actions-after":f(()=>[c(n.$slots,"home-hero-actions-after",{},void 0,!0)]),"home-hero-image":f(()=>[c(n.$slots,"home-hero-image",{},void 0,!0)]),"home-hero-after":f(()=>[c(n.$slots,"home-hero-after",{},void 0,!0)]),"home-features-before":f(()=>[c(n.$slots,"home-features-before",{},void 0,!0)]),"home-features-after":f(()=>[c(n.$slots,"home-features-after",{},void 0,!0)]),_:3})):i(t).layout&&i(t).layout!=="doc"?(a(),g(E(i(t).layout),{key:3})):(a(),g(pn,{key:4},{"doc-top":f(()=>[c(n.$slots,"doc-top",{},void 0,!0)]),"doc-bottom":f(()=>[c(n.$slots,"doc-bottom",{},void 0,!0)]),"doc-footer-before":f(()=>[c(n.$slots,"doc-footer-before",{},void 0,!0)]),"doc-before":f(()=>[c(n.$slots,"doc-before",{},void 0,!0)]),"doc-after":f(()=>[c(n.$slots,"doc-after",{},void 0,!0)]),"aside-top":f(()=>[c(n.$slots,"aside-top",{},void 0,!0)]),"aside-outline-before":f(()=>[c(n.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":f(()=>[c(n.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":f(()=>[c(n.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":f(()=>[c(n.$slots,"aside-ads-after",{},void 0,!0)]),"aside-bottom":f(()=>[c(n.$slots,"aside-bottom",{},void 0,!0)]),_:3}))],2))}}),es=$(xn,[["__scopeId","data-v-91765379"]]),ts={class:"container"},ns=["innerHTML"],ss=["innerHTML"],os=m({__name:"VPFooter",setup(o){const{theme:e,frontmatter:t}=V(),{hasSidebar:s}=U();return(n,r)=>i(e).footer&&i(t).footer!==!1?(a(),u("footer",{key:0,class:I(["VPFooter",{"has-sidebar":i(s)}])},[v("div",ts,[i(e).footer.message?(a(),u("p",{key:0,class:"message",innerHTML:i(e).footer.message},null,8,ns)):h("",!0),i(e).footer.copyright?(a(),u("p",{key:1,class:"copyright",innerHTML:i(e).footer.copyright},null,8,ss)):h("",!0)])],2)):h("",!0)}}),as=$(os,[["__scopeId","data-v-c970a860"]]);function rs(){const{theme:o,frontmatter:e}=V(),t=Ve([]),s=y(()=>t.value.length>0);return x(()=>{t.value=be(e.value.outline??o.value.outline)}),{headers:t,hasLocalNav:s}}const is={class:"menu-text"},ls={class:"header"},cs={class:"outline"},us=m({__name:"VPLocalNavOutlineDropdown",props:{headers:{},navHeight:{}},setup(o){const e=o,{theme:t}=V(),s=T(!1),n=T(0),r=T(),l=T();function d(_){var P;(P=r.value)!=null&&P.contains(_.target)||(s.value=!1)}F(s,_=>{if(_){document.addEventListener("click",d);return}document.removeEventListener("click",d)}),ie("Escape",()=>{s.value=!1}),x(()=>{s.value=!1});function p(){s.value=!s.value,n.value=window.innerHeight+Math.min(window.scrollY-e.navHeight,0)}function b(_){_.target.classList.contains("outline-link")&&(l.value&&(l.value.style.transition="none"),he(()=>{s.value=!1}))}function L(){s.value=!1,window.scrollTo({top:0,left:0,behavior:"smooth"})}return(_,P)=>(a(),u("div",{class:"VPLocalNavOutlineDropdown",style:Ne({"--vp-vh":n.value+"px"}),ref_key:"main",ref:r},[_.headers.length>0?(a(),u("button",{key:0,onClick:p,class:I({open:s.value})},[v("span",is,w(i(Ce)(i(t))),1),P[0]||(P[0]=v("span",{class:"vpi-chevron-right icon"},null,-1))],2)):(a(),u("button",{key:1,onClick:L},w(i(t).returnToTopLabel||"Return to top"),1)),k(de,{name:"flyout"},{default:f(()=>[s.value?(a(),u("div",{key:0,ref_key:"items",ref:l,class:"items",onClick:b},[v("div",ls,[v("a",{class:"top-link",href:"#",onClick:L},w(i(t).returnToTopLabel||"Return to top"),1)]),v("div",cs,[k(He,{headers:_.headers},null,8,["headers"])])],512)):h("",!0)]),_:1})],4))}}),ds=$(us,[["__scopeId","data-v-bc9dc845"]]),ps={class:"container"},vs=["aria-expanded"],fs={class:"menu-text"},hs=m({__name:"VPLocalNav",props:{open:{type:Boolean}},emits:["open-menu"],setup(o){const{theme:e,frontmatter:t}=V(),{hasSidebar:s}=U(),{headers:n}=rs(),{y:r}=we(),l=T(0);O(()=>{l.value=parseInt(getComputedStyle(document.documentElement).getPropertyValue("--vp-nav-height"))}),x(()=>{n.value=be(t.value.outline??e.value.outline)});const d=y(()=>n.value.length===0),p=y(()=>d.value&&!s.value),b=y(()=>({VPLocalNav:!0,"has-sidebar":s.value,empty:d.value,fixed:p.value}));return(L,_)=>i(t).layout!=="home"&&(!p.value||i(r)>=l.value)?(a(),u("div",{key:0,class:I(b.value)},[v("div",ps,[i(s)?(a(),u("button",{key:0,class:"menu","aria-expanded":L.open,"aria-controls":"VPSidebarNav",onClick:_[0]||(_[0]=P=>L.$emit("open-menu"))},[_[1]||(_[1]=v("span",{class:"vpi-align-left menu-icon"},null,-1)),v("span",fs,w(i(e).sidebarMenuLabel||"Menu"),1)],8,vs)):h("",!0),k(ds,{headers:i(n),navHeight:l.value},null,8,["headers","navHeight"])])],2)):h("",!0)}}),ms=$(hs,[["__scopeId","data-v-070ab83d"]]);function _s(){const o=T(!1);function e(){o.value=!0,window.addEventListener("resize",n)}function t(){o.value=!1,window.removeEventListener("resize",n)}function s(){o.value?t():e()}function n(){window.outerWidth>=768&&t()}const r=ee();return F(()=>r.path,t),{isScreenOpen:o,openScreen:e,closeScreen:t,toggleScreen:s}}const bs={},ks={class:"VPSwitch",type:"button",role:"switch"},gs={class:"check"},$s={key:0,class:"icon"};function ys(o,e){return a(),u("button",ks,[v("span",gs,[o.$slots.default?(a(),u("span",$s,[c(o.$slots,"default",{},void 0,!0)])):h("",!0)])])}const Ps=$(bs,[["render",ys],["__scopeId","data-v-4a1c76db"]]),Ss=m({__name:"VPSwitchAppearance",setup(o){const{isDark:e,theme:t}=V(),s=q("toggle-appearance",()=>{e.value=!e.value}),n=T("");return fe(()=>{n.value=e.value?t.value.lightModeSwitchTitle||"Switch to light theme":t.value.darkModeSwitchTitle||"Switch to dark theme"}),(r,l)=>(a(),g(Ps,{title:n.value,class:"VPSwitchAppearance","aria-checked":i(e),onClick:i(s)},{default:f(()=>l[0]||(l[0]=[v("span",{class:"vpi-sun sun"},null,-1),v("span",{class:"vpi-moon moon"},null,-1)])),_:1},8,["title","aria-checked","onClick"]))}}),ke=$(Ss,[["__scopeId","data-v-e40a8bb6"]]),Ls={key:0,class:"VPNavBarAppearance"},Vs=m({__name:"VPNavBarAppearance",setup(o){const{site:e}=V();return(t,s)=>i(e).appearance&&i(e).appearance!=="force-dark"&&i(e).appearance!=="force-auto"?(a(),u("div",Ls,[k(ke)])):h("",!0)}}),Ts=$(Vs,[["__scopeId","data-v-af096f4a"]]),ge=T();let Be=!1,ae=0;function Ns(o){const e=T(!1);if(te){!Be&&ws(),ae++;const t=F(ge,s=>{var n,r,l;s===o.el.value||(n=o.el.value)!=null&&n.contains(s)?(e.value=!0,(r=o.onFocus)==null||r.call(o)):(e.value=!1,(l=o.onBlur)==null||l.call(o))});ve(()=>{t(),ae--,ae||Is()})}return qe(e)}function ws(){document.addEventListener("focusin",Ee),Be=!0,ge.value=document.activeElement}function Is(){document.removeEventListener("focusin",Ee)}function Ee(){ge.value=document.activeElement}const Ms={class:"VPMenuLink"},As=["innerHTML"],Cs=m({__name:"VPMenuLink",props:{item:{}},setup(o){const{page:e}=V();return(t,s)=>(a(),u("div",Ms,[k(D,{class:I({active:i(K)(i(e).relativePath,t.item.activeMatch||t.item.link,!!t.item.activeMatch)}),href:t.item.link,target:t.item.target,rel:t.item.rel,"no-icon":t.item.noIcon},{default:f(()=>[v("span",{innerHTML:t.item.text},null,8,As)]),_:1},8,["class","href","target","rel","no-icon"])]))}}),ne=$(Cs,[["__scopeId","data-v-acbfed09"]]),Hs={class:"VPMenuGroup"},Bs={key:0,class:"title"},Es=m({__name:"VPMenuGroup",props:{text:{},items:{}},setup(o){return(e,t)=>(a(),u("div",Hs,[e.text?(a(),u("p",Bs,w(e.text),1)):h("",!0),(a(!0),u(M,null,H(e.items,s=>(a(),u(M,null,["link"in s?(a(),g(ne,{key:0,item:s},null,8,["item"])):h("",!0)],64))),256))]))}}),Ds=$(Es,[["__scopeId","data-v-48c802d0"]]),Fs={class:"VPMenu"},Os={key:0,class:"items"},Us=m({__name:"VPMenu",props:{items:{}},setup(o){return(e,t)=>(a(),u("div",Fs,[e.items?(a(),u("div",Os,[(a(!0),u(M,null,H(e.items,s=>(a(),u(M,{key:JSON.stringify(s)},["link"in s?(a(),g(ne,{key:0,item:s},null,8,["item"])):"component"in s?(a(),g(E(s.component),j({key:1,ref_for:!0},s.props),null,16)):(a(),g(Ds,{key:2,text:s.text,items:s.items},null,8,["text","items"]))],64))),128))])):h("",!0),c(e.$slots,"default",{},void 0,!0)]))}}),Gs=$(Us,[["__scopeId","data-v-7dd3104a"]]),js=["aria-expanded","aria-label"],zs={key:0,class:"text"},Ks=["innerHTML"],Rs={key:1,class:"vpi-more-horizontal icon"},Ws={class:"menu"},qs=m({__name:"VPFlyout",props:{icon:{},button:{},label:{},items:{}},setup(o){const e=T(!1),t=T();Ns({el:t,onBlur:s});function s(){e.value=!1}return(n,r)=>(a(),u("div",{class:"VPFlyout",ref_key:"el",ref:t,onMouseenter:r[1]||(r[1]=l=>e.value=!0),onMouseleave:r[2]||(r[2]=l=>e.value=!1)},[v("button",{type:"button",class:"button","aria-haspopup":"true","aria-expanded":e.value,"aria-label":n.label,onClick:r[0]||(r[0]=l=>e.value=!e.value)},[n.button||n.icon?(a(),u("span",zs,[n.icon?(a(),u("span",{key:0,class:I([n.icon,"option-icon"])},null,2)):h("",!0),n.button?(a(),u("span",{key:1,innerHTML:n.button},null,8,Ks)):h("",!0),r[3]||(r[3]=v("span",{class:"vpi-chevron-down text-icon"},null,-1))])):(a(),u("span",Rs))],8,js),v("div",Ws,[k(Gs,{items:n.items},{default:f(()=>[c(n.$slots,"default",{},void 0,!0)]),_:3},8,["items"])])],544))}}),$e=$(qs,[["__scopeId","data-v-04f5c5e9"]]),Js=["href","aria-label","innerHTML"],Ys=m({__name:"VPSocialLink",props:{icon:{},link:{},ariaLabel:{}},setup(o){const e=o,t=T();O(async()=>{var r;await he();const n=(r=t.value)==null?void 0:r.children[0];n instanceof HTMLElement&&n.className.startsWith("vpi-social-")&&(getComputedStyle(n).maskImage||getComputedStyle(n).webkitMaskImage)==="none"&&n.style.setProperty("--icon",`url('https://api.iconify.design/simple-icons/${e.icon}.svg')`)});const s=y(()=>typeof e.icon=="object"?e.icon.svg:``);return(n,r)=>(a(),u("a",{ref_key:"el",ref:t,class:"VPSocialLink no-icon",href:n.link,"aria-label":n.ariaLabel??(typeof n.icon=="string"?n.icon:""),target:"_blank",rel:"noopener",innerHTML:s.value},null,8,Js))}}),Xs=$(Ys,[["__scopeId","data-v-d26d30cb"]]),Qs={class:"VPSocialLinks"},Zs=m({__name:"VPSocialLinks",props:{links:{}},setup(o){return(e,t)=>(a(),u("div",Qs,[(a(!0),u(M,null,H(e.links,({link:s,icon:n,ariaLabel:r})=>(a(),g(Xs,{key:s,icon:n,link:s,ariaLabel:r},null,8,["icon","link","ariaLabel"]))),128))]))}}),ye=$(Zs,[["__scopeId","data-v-ee7a9424"]]),xs={key:0,class:"group translations"},eo={class:"trans-title"},to={key:1,class:"group"},no={class:"item appearance"},so={class:"label"},oo={class:"appearance-action"},ao={key:2,class:"group"},ro={class:"item social-links"},io=m({__name:"VPNavBarExtra",setup(o){const{site:e,theme:t}=V(),{localeLinks:s,currentLang:n}=Y({correspondingLink:!0}),r=y(()=>s.value.length&&n.value.label||e.value.appearance||t.value.socialLinks);return(l,d)=>r.value?(a(),g($e,{key:0,class:"VPNavBarExtra",label:"extra navigation"},{default:f(()=>[i(s).length&&i(n).label?(a(),u("div",xs,[v("p",eo,w(i(n).label),1),(a(!0),u(M,null,H(i(s),p=>(a(),g(ne,{key:p.link,item:p},null,8,["item"]))),128))])):h("",!0),i(e).appearance&&i(e).appearance!=="force-dark"&&i(e).appearance!=="force-auto"?(a(),u("div",to,[v("div",no,[v("p",so,w(i(t).darkModeSwitchLabel||"Appearance"),1),v("div",oo,[k(ke)])])])):h("",!0),i(t).socialLinks?(a(),u("div",ao,[v("div",ro,[k(ye,{class:"social-links-list",links:i(t).socialLinks},null,8,["links"])])])):h("",!0)]),_:1})):h("",!0)}}),lo=$(io,[["__scopeId","data-v-925effce"]]),co=["aria-expanded"],uo=m({__name:"VPNavBarHamburger",props:{active:{type:Boolean}},emits:["click"],setup(o){return(e,t)=>(a(),u("button",{type:"button",class:I(["VPNavBarHamburger",{active:e.active}]),"aria-label":"mobile navigation","aria-expanded":e.active,"aria-controls":"VPNavScreen",onClick:t[0]||(t[0]=s=>e.$emit("click"))},t[1]||(t[1]=[v("span",{class:"container"},[v("span",{class:"top"}),v("span",{class:"middle"}),v("span",{class:"bottom"})],-1)]),10,co))}}),po=$(uo,[["__scopeId","data-v-5dea55bf"]]),vo=["innerHTML"],fo=m({__name:"VPNavBarMenuLink",props:{item:{}},setup(o){const{page:e}=V();return(t,s)=>(a(),g(D,{class:I({VPNavBarMenuLink:!0,active:i(K)(i(e).relativePath,t.item.activeMatch||t.item.link,!!t.item.activeMatch)}),href:t.item.link,target:t.item.target,rel:t.item.rel,"no-icon":t.item.noIcon,tabindex:"0"},{default:f(()=>[v("span",{innerHTML:t.item.text},null,8,vo)]),_:1},8,["class","href","target","rel","no-icon"]))}}),ho=$(fo,[["__scopeId","data-v-956ec74c"]]),mo=m({__name:"VPNavBarMenuGroup",props:{item:{}},setup(o){const e=o,{page:t}=V(),s=r=>"component"in r?!1:"link"in r?K(t.value.relativePath,r.link,!!e.item.activeMatch):r.items.some(s),n=y(()=>s(e.item));return(r,l)=>(a(),g($e,{class:I({VPNavBarMenuGroup:!0,active:i(K)(i(t).relativePath,r.item.activeMatch,!!r.item.activeMatch)||n.value}),button:r.item.text,items:r.item.items},null,8,["class","button","items"]))}}),_o={key:0,"aria-labelledby":"main-nav-aria-label",class:"VPNavBarMenu"},bo=m({__name:"VPNavBarMenu",setup(o){const{theme:e}=V();return(t,s)=>i(e).nav?(a(),u("nav",_o,[s[0]||(s[0]=v("span",{id:"main-nav-aria-label",class:"visually-hidden"}," Main Navigation ",-1)),(a(!0),u(M,null,H(i(e).nav,n=>(a(),u(M,{key:JSON.stringify(n)},["link"in n?(a(),g(ho,{key:0,item:n},null,8,["item"])):"component"in n?(a(),g(E(n.component),j({key:1,ref_for:!0},n.props),null,16)):(a(),g(mo,{key:2,item:n},null,8,["item"]))],64))),128))])):h("",!0)}}),ko=$(bo,[["__scopeId","data-v-e6d46098"]]);function go(o){const{localeIndex:e,theme:t}=V();function s(n){var A,C,N;const r=n.split("."),l=(A=t.value.search)==null?void 0:A.options,d=l&&typeof l=="object",p=d&&((N=(C=l.locales)==null?void 0:C[e.value])==null?void 0:N.translations)||null,b=d&&l.translations||null;let L=p,_=b,P=o;const S=r.pop();for(const B of r){let G=null;const W=P==null?void 0:P[B];W&&(G=P=W);const se=_==null?void 0:_[B];se&&(G=_=se);const oe=L==null?void 0:L[B];oe&&(G=L=oe),W||(P=G),se||(_=G),oe||(L=G)}return(L==null?void 0:L[S])??(_==null?void 0:_[S])??(P==null?void 0:P[S])??""}return s}const $o=["aria-label"],yo={class:"DocSearch-Button-Container"},Po={class:"DocSearch-Button-Placeholder"},Pe=m({__name:"VPNavBarSearchButton",setup(o){const t=go({button:{buttonText:"Search",buttonAriaLabel:"Search"}});return(s,n)=>(a(),u("button",{type:"button",class:"DocSearch DocSearch-Button","aria-label":i(t)("button.buttonAriaLabel")},[v("span",yo,[n[0]||(n[0]=v("span",{class:"vp-icon DocSearch-Search-Icon"},null,-1)),v("span",Po,w(i(t)("button.buttonText")),1)]),n[1]||(n[1]=v("span",{class:"DocSearch-Button-Keys"},[v("kbd",{class:"DocSearch-Button-Key"}),v("kbd",{class:"DocSearch-Button-Key"},"K")],-1))],8,$o))}}),So={class:"VPNavBarSearch"},Lo={id:"local-search"},Vo={key:1,id:"docsearch"},To=m({__name:"VPNavBarSearch",setup(o){const e=Je(()=>Ye(()=>import("./VPLocalSearchBox.B2uKxNr_.js"),__vite__mapDeps([0,1]))),t=()=>null,{theme:s}=V(),n=T(!1),r=T(!1);O(()=>{});function l(){n.value||(n.value=!0,setTimeout(d,16))}function d(){const _=new Event("keydown");_.key="k",_.metaKey=!0,window.dispatchEvent(_),setTimeout(()=>{document.querySelector(".DocSearch-Modal")||d()},16)}function p(_){const P=_.target,S=P.tagName;return P.isContentEditable||S==="INPUT"||S==="SELECT"||S==="TEXTAREA"}const b=T(!1);ie("k",_=>{(_.ctrlKey||_.metaKey)&&(_.preventDefault(),b.value=!0)}),ie("/",_=>{p(_)||(_.preventDefault(),b.value=!0)});const L="local";return(_,P)=>{var S;return a(),u("div",So,[i(L)==="local"?(a(),u(M,{key:0},[b.value?(a(),g(i(e),{key:0,onClose:P[0]||(P[0]=A=>b.value=!1)})):h("",!0),v("div",Lo,[k(Pe,{onClick:P[1]||(P[1]=A=>b.value=!0)})])],64)):i(L)==="algolia"?(a(),u(M,{key:1},[n.value?(a(),g(i(t),{key:0,algolia:((S=i(s).search)==null?void 0:S.options)??i(s).algolia,onVnodeBeforeMount:P[2]||(P[2]=A=>r.value=!0)},null,8,["algolia"])):h("",!0),r.value?h("",!0):(a(),u("div",Vo,[k(Pe,{onClick:l})]))],64)):h("",!0)])}}}),No=m({__name:"VPNavBarSocialLinks",setup(o){const{theme:e}=V();return(t,s)=>i(e).socialLinks?(a(),g(ye,{key:0,class:"VPNavBarSocialLinks",links:i(e).socialLinks},null,8,["links"])):h("",!0)}}),wo=$(No,[["__scopeId","data-v-164c457f"]]),Io=["href","rel","target"],Mo=["innerHTML"],Ao={key:2},Co=m({__name:"VPNavBarTitle",setup(o){const{site:e,theme:t}=V(),{hasSidebar:s}=U(),{currentLang:n}=Y(),r=y(()=>{var p;return typeof t.value.logoLink=="string"?t.value.logoLink:(p=t.value.logoLink)==null?void 0:p.link}),l=y(()=>{var p;return typeof t.value.logoLink=="string"||(p=t.value.logoLink)==null?void 0:p.rel}),d=y(()=>{var p;return typeof t.value.logoLink=="string"||(p=t.value.logoLink)==null?void 0:p.target});return(p,b)=>(a(),u("div",{class:I(["VPNavBarTitle",{"has-sidebar":i(s)}])},[v("a",{class:"title",href:r.value??i(_e)(i(n).link),rel:l.value,target:d.value},[c(p.$slots,"nav-bar-title-before",{},void 0,!0),i(t).logo?(a(),g(Q,{key:0,class:"logo",image:i(t).logo},null,8,["image"])):h("",!0),i(t).siteTitle?(a(),u("span",{key:1,innerHTML:i(t).siteTitle},null,8,Mo)):i(t).siteTitle===void 0?(a(),u("span",Ao,w(i(e).title),1)):h("",!0),c(p.$slots,"nav-bar-title-after",{},void 0,!0)],8,Io)],2))}}),Ho=$(Co,[["__scopeId","data-v-0f4f798b"]]),Bo={class:"items"},Eo={class:"title"},Do=m({__name:"VPNavBarTranslations",setup(o){const{theme:e}=V(),{localeLinks:t,currentLang:s}=Y({correspondingLink:!0});return(n,r)=>i(t).length&&i(s).label?(a(),g($e,{key:0,class:"VPNavBarTranslations",icon:"vpi-languages",label:i(e).langMenuLabel||"Change language"},{default:f(()=>[v("div",Bo,[v("p",Eo,w(i(s).label),1),(a(!0),u(M,null,H(i(t),l=>(a(),g(ne,{key:l.link,item:l},null,8,["item"]))),128))])]),_:1},8,["label"])):h("",!0)}}),Fo=$(Do,[["__scopeId","data-v-c80d9ad0"]]),Oo={class:"wrapper"},Uo={class:"container"},Go={class:"title"},jo={class:"content"},zo={class:"content-body"},Ko=m({__name:"VPNavBar",props:{isScreenOpen:{type:Boolean}},emits:["toggle-screen"],setup(o){const e=o,{y:t}=we(),{hasSidebar:s}=U(),{frontmatter:n}=V(),r=T({});return fe(()=>{r.value={"has-sidebar":s.value,home:n.value.layout==="home",top:t.value===0,"screen-open":e.isScreenOpen}}),(l,d)=>(a(),u("div",{class:I(["VPNavBar",r.value])},[v("div",Oo,[v("div",Uo,[v("div",Go,[k(Ho,null,{"nav-bar-title-before":f(()=>[c(l.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":f(()=>[c(l.$slots,"nav-bar-title-after",{},void 0,!0)]),_:3})]),v("div",jo,[v("div",zo,[c(l.$slots,"nav-bar-content-before",{},void 0,!0),k(To,{class:"search"}),k(ko,{class:"menu"}),k(Fo,{class:"translations"}),k(Ts,{class:"appearance"}),k(wo,{class:"social-links"}),k(lo,{class:"extra"}),c(l.$slots,"nav-bar-content-after",{},void 0,!0),k(po,{class:"hamburger",active:l.isScreenOpen,onClick:d[0]||(d[0]=p=>l.$emit("toggle-screen"))},null,8,["active"])])])])]),d[1]||(d[1]=v("div",{class:"divider"},[v("div",{class:"divider-line"})],-1))],2))}}),Ro=$(Ko,[["__scopeId","data-v-822684d1"]]),Wo={key:0,class:"VPNavScreenAppearance"},qo={class:"text"},Jo=m({__name:"VPNavScreenAppearance",setup(o){const{site:e,theme:t}=V();return(s,n)=>i(e).appearance&&i(e).appearance!=="force-dark"&&i(e).appearance!=="force-auto"?(a(),u("div",Wo,[v("p",qo,w(i(t).darkModeSwitchLabel||"Appearance"),1),k(ke)])):h("",!0)}}),Yo=$(Jo,[["__scopeId","data-v-ffb44008"]]),Xo=["innerHTML"],Qo=m({__name:"VPNavScreenMenuLink",props:{item:{}},setup(o){const e=q("close-screen");return(t,s)=>(a(),g(D,{class:"VPNavScreenMenuLink",href:t.item.link,target:t.item.target,rel:t.item.rel,"no-icon":t.item.noIcon,onClick:i(e)},{default:f(()=>[v("span",{innerHTML:t.item.text},null,8,Xo)]),_:1},8,["href","target","rel","no-icon","onClick"]))}}),Zo=$(Qo,[["__scopeId","data-v-735512b8"]]),xo=["innerHTML"],ea=m({__name:"VPNavScreenMenuGroupLink",props:{item:{}},setup(o){const e=q("close-screen");return(t,s)=>(a(),g(D,{class:"VPNavScreenMenuGroupLink",href:t.item.link,target:t.item.target,rel:t.item.rel,"no-icon":t.item.noIcon,onClick:i(e)},{default:f(()=>[v("span",{innerHTML:t.item.text},null,8,xo)]),_:1},8,["href","target","rel","no-icon","onClick"]))}}),De=$(ea,[["__scopeId","data-v-372ae7c0"]]),ta={class:"VPNavScreenMenuGroupSection"},na={key:0,class:"title"},sa=m({__name:"VPNavScreenMenuGroupSection",props:{text:{},items:{}},setup(o){return(e,t)=>(a(),u("div",ta,[e.text?(a(),u("p",na,w(e.text),1)):h("",!0),(a(!0),u(M,null,H(e.items,s=>(a(),g(De,{key:s.text,item:s},null,8,["item"]))),128))]))}}),oa=$(sa,[["__scopeId","data-v-4b8941ac"]]),aa=["aria-controls","aria-expanded"],ra=["innerHTML"],ia=["id"],la={key:0,class:"item"},ca={key:1,class:"item"},ua={key:2,class:"group"},da=m({__name:"VPNavScreenMenuGroup",props:{text:{},items:{}},setup(o){const e=o,t=T(!1),s=y(()=>`NavScreenGroup-${e.text.replace(" ","-").toLowerCase()}`);function n(){t.value=!t.value}return(r,l)=>(a(),u("div",{class:I(["VPNavScreenMenuGroup",{open:t.value}])},[v("button",{class:"button","aria-controls":s.value,"aria-expanded":t.value,onClick:n},[v("span",{class:"button-text",innerHTML:r.text},null,8,ra),l[0]||(l[0]=v("span",{class:"vpi-plus button-icon"},null,-1))],8,aa),v("div",{id:s.value,class:"items"},[(a(!0),u(M,null,H(r.items,d=>(a(),u(M,{key:JSON.stringify(d)},["link"in d?(a(),u("div",la,[k(De,{item:d},null,8,["item"])])):"component"in d?(a(),u("div",ca,[(a(),g(E(d.component),j({ref_for:!0},d.props,{"screen-menu":""}),null,16))])):(a(),u("div",ua,[k(oa,{text:d.text,items:d.items},null,8,["text","items"])]))],64))),128))],8,ia)],2))}}),pa=$(da,[["__scopeId","data-v-875057a5"]]),va={key:0,class:"VPNavScreenMenu"},fa=m({__name:"VPNavScreenMenu",setup(o){const{theme:e}=V();return(t,s)=>i(e).nav?(a(),u("nav",va,[(a(!0),u(M,null,H(i(e).nav,n=>(a(),u(M,{key:JSON.stringify(n)},["link"in n?(a(),g(Zo,{key:0,item:n},null,8,["item"])):"component"in n?(a(),g(E(n.component),j({key:1,ref_for:!0},n.props,{"screen-menu":""}),null,16)):(a(),g(pa,{key:2,text:n.text||"",items:n.items},null,8,["text","items"]))],64))),128))])):h("",!0)}}),ha=m({__name:"VPNavScreenSocialLinks",setup(o){const{theme:e}=V();return(t,s)=>i(e).socialLinks?(a(),g(ye,{key:0,class:"VPNavScreenSocialLinks",links:i(e).socialLinks},null,8,["links"])):h("",!0)}}),ma={class:"list"},_a=m({__name:"VPNavScreenTranslations",setup(o){const{localeLinks:e,currentLang:t}=Y({correspondingLink:!0}),s=T(!1);function n(){s.value=!s.value}return(r,l)=>i(e).length&&i(t).label?(a(),u("div",{key:0,class:I(["VPNavScreenTranslations",{open:s.value}])},[v("button",{class:"title",onClick:n},[l[0]||(l[0]=v("span",{class:"vpi-languages icon lang"},null,-1)),z(" "+w(i(t).label)+" ",1),l[1]||(l[1]=v("span",{class:"vpi-chevron-down icon chevron"},null,-1))]),v("ul",ma,[(a(!0),u(M,null,H(i(e),d=>(a(),u("li",{key:d.link,class:"item"},[k(D,{class:"link",href:d.link},{default:f(()=>[z(w(d.text),1)]),_:2},1032,["href"])]))),128))])],2)):h("",!0)}}),ba=$(_a,[["__scopeId","data-v-362991c2"]]),ka={class:"container"},ga=m({__name:"VPNavScreen",props:{open:{type:Boolean}},setup(o){const e=T(null),t=Ie(te?document.body:null);return(s,n)=>(a(),g(de,{name:"fade",onEnter:n[0]||(n[0]=r=>t.value=!0),onAfterLeave:n[1]||(n[1]=r=>t.value=!1)},{default:f(()=>[s.open?(a(),u("div",{key:0,class:"VPNavScreen",ref_key:"screen",ref:e,id:"VPNavScreen"},[v("div",ka,[c(s.$slots,"nav-screen-content-before",{},void 0,!0),k(fa,{class:"menu"}),k(ba,{class:"translations"}),k(Yo,{class:"appearance"}),k(ha,{class:"social-links"}),c(s.$slots,"nav-screen-content-after",{},void 0,!0)])],512)):h("",!0)]),_:3}))}}),$a=$(ga,[["__scopeId","data-v-833aabba"]]),ya={key:0,class:"VPNav"},Pa=m({__name:"VPNav",setup(o){const{isScreenOpen:e,closeScreen:t,toggleScreen:s}=_s(),{frontmatter:n}=V(),r=y(()=>n.value.navbar!==!1);return me("close-screen",t),Z(()=>{te&&document.documentElement.classList.toggle("hide-nav",!r.value)}),(l,d)=>r.value?(a(),u("header",ya,[k(Ro,{"is-screen-open":i(e),onToggleScreen:i(s)},{"nav-bar-title-before":f(()=>[c(l.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":f(()=>[c(l.$slots,"nav-bar-title-after",{},void 0,!0)]),"nav-bar-content-before":f(()=>[c(l.$slots,"nav-bar-content-before",{},void 0,!0)]),"nav-bar-content-after":f(()=>[c(l.$slots,"nav-bar-content-after",{},void 0,!0)]),_:3},8,["is-screen-open","onToggleScreen"]),k($a,{open:i(e)},{"nav-screen-content-before":f(()=>[c(l.$slots,"nav-screen-content-before",{},void 0,!0)]),"nav-screen-content-after":f(()=>[c(l.$slots,"nav-screen-content-after",{},void 0,!0)]),_:3},8,["open"])])):h("",!0)}}),Sa=$(Pa,[["__scopeId","data-v-f1e365da"]]),La=["role","tabindex"],Va={key:1,class:"items"},Ta=m({__name:"VPSidebarItem",props:{item:{},depth:{}},setup(o){const e=o,{collapsed:t,collapsible:s,isLink:n,isActiveLink:r,hasActiveLink:l,hasChildren:d,toggle:p}=gt(y(()=>e.item)),b=y(()=>d.value?"section":"div"),L=y(()=>n.value?"a":"div"),_=y(()=>d.value?e.depth+2===7?"p":`h${e.depth+2}`:"p"),P=y(()=>n.value?void 0:"button"),S=y(()=>[[`level-${e.depth}`],{collapsible:s.value},{collapsed:t.value},{"is-link":n.value},{"is-active":r.value},{"has-active":l.value}]);function A(N){"key"in N&&N.key!=="Enter"||!e.item.link&&p()}function C(){e.item.link&&p()}return(N,B)=>{const G=R("VPSidebarItem",!0);return a(),g(E(b.value),{class:I(["VPSidebarItem",S.value])},{default:f(()=>[N.item.text?(a(),u("div",j({key:0,class:"item",role:P.value},Qe(N.item.items?{click:A,keydown:A}:{},!0),{tabindex:N.item.items&&0}),[B[1]||(B[1]=v("div",{class:"indicator"},null,-1)),N.item.link?(a(),g(D,{key:0,tag:L.value,class:"link",href:N.item.link,rel:N.item.rel,target:N.item.target},{default:f(()=>[(a(),g(E(_.value),{class:"text",innerHTML:N.item.text},null,8,["innerHTML"]))]),_:1},8,["tag","href","rel","target"])):(a(),g(E(_.value),{key:1,class:"text",innerHTML:N.item.text},null,8,["innerHTML"])),N.item.collapsed!=null&&N.item.items&&N.item.items.length?(a(),u("div",{key:2,class:"caret",role:"button","aria-label":"toggle section",onClick:C,onKeydown:Xe(C,["enter"]),tabindex:"0"},B[0]||(B[0]=[v("span",{class:"vpi-chevron-right caret-icon"},null,-1)]),32)):h("",!0)],16,La)):h("",!0),N.item.items&&N.item.items.length?(a(),u("div",Va,[N.depth<5?(a(!0),u(M,{key:0},H(N.item.items,W=>(a(),g(G,{key:W.text,item:W,depth:N.depth+1},null,8,["item","depth"]))),128)):h("",!0)])):h("",!0)]),_:1},8,["class"])}}}),Na=$(Ta,[["__scopeId","data-v-196b2e5f"]]),wa=m({__name:"VPSidebarGroup",props:{items:{}},setup(o){const e=T(!0);let t=null;return O(()=>{t=setTimeout(()=>{t=null,e.value=!1},300)}),Ze(()=>{t!=null&&(clearTimeout(t),t=null)}),(s,n)=>(a(!0),u(M,null,H(s.items,r=>(a(),u("div",{key:r.text,class:I(["group",{"no-transition":e.value}])},[k(Na,{item:r,depth:0},null,8,["item"])],2))),128))}}),Ia=$(wa,[["__scopeId","data-v-9e426adc"]]),Ma={class:"nav",id:"VPSidebarNav","aria-labelledby":"sidebar-aria-label",tabindex:"-1"},Aa=m({__name:"VPSidebar",props:{open:{type:Boolean}},setup(o){const{sidebarGroups:e,hasSidebar:t}=U(),s=o,n=T(null),r=Ie(te?document.body:null);F([s,n],()=>{var d;s.open?(r.value=!0,(d=n.value)==null||d.focus()):r.value=!1},{immediate:!0,flush:"post"});const l=T(0);return F(e,()=>{l.value+=1},{deep:!0}),(d,p)=>i(t)?(a(),u("aside",{key:0,class:I(["VPSidebar",{open:d.open}]),ref_key:"navEl",ref:n,onClick:p[0]||(p[0]=xe(()=>{},["stop"]))},[p[2]||(p[2]=v("div",{class:"curtain"},null,-1)),v("nav",Ma,[p[1]||(p[1]=v("span",{class:"visually-hidden",id:"sidebar-aria-label"}," Sidebar Navigation ",-1)),c(d.$slots,"sidebar-nav-before",{},void 0,!0),(a(),g(Ia,{items:i(e),key:l.value},null,8,["items"])),c(d.$slots,"sidebar-nav-after",{},void 0,!0)])],2)):h("",!0)}}),Ca=$(Aa,[["__scopeId","data-v-18756405"]]),Ha=m({__name:"VPSkipLink",setup(o){const e=ee(),t=T();F(()=>e.path,()=>t.value.focus());function s({target:n}){const r=document.getElementById(decodeURIComponent(n.hash).slice(1));if(r){const l=()=>{r.removeAttribute("tabindex"),r.removeEventListener("blur",l)};r.setAttribute("tabindex","-1"),r.addEventListener("blur",l),r.focus(),window.scrollTo(0,0)}}return(n,r)=>(a(),u(M,null,[v("span",{ref_key:"backToTop",ref:t,tabindex:"-1"},null,512),v("a",{href:"#VPContent",class:"VPSkipLink visually-hidden",onClick:s}," Skip to content ")],64))}}),Ba=$(Ha,[["__scopeId","data-v-c3508ec8"]]),Ea=m({__name:"Layout",setup(o){const{isOpen:e,open:t,close:s}=U(),n=ee();F(()=>n.path,s),kt(e,s);const{frontmatter:r}=V(),l=Me(),d=y(()=>!!l["home-hero-image"]);return me("hero-image-slot-exists",d),(p,b)=>{const L=R("Content");return i(r).layout!==!1?(a(),u("div",{key:0,class:I(["Layout",i(r).pageClass])},[c(p.$slots,"layout-top",{},void 0,!0),k(Ba),k(rt,{class:"backdrop",show:i(e),onClick:i(s)},null,8,["show","onClick"]),k(Sa,null,{"nav-bar-title-before":f(()=>[c(p.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":f(()=>[c(p.$slots,"nav-bar-title-after",{},void 0,!0)]),"nav-bar-content-before":f(()=>[c(p.$slots,"nav-bar-content-before",{},void 0,!0)]),"nav-bar-content-after":f(()=>[c(p.$slots,"nav-bar-content-after",{},void 0,!0)]),"nav-screen-content-before":f(()=>[c(p.$slots,"nav-screen-content-before",{},void 0,!0)]),"nav-screen-content-after":f(()=>[c(p.$slots,"nav-screen-content-after",{},void 0,!0)]),_:3}),k(ms,{open:i(e),onOpenMenu:i(t)},null,8,["open","onOpenMenu"]),k(Ca,{open:i(e)},{"sidebar-nav-before":f(()=>[c(p.$slots,"sidebar-nav-before",{},void 0,!0)]),"sidebar-nav-after":f(()=>[c(p.$slots,"sidebar-nav-after",{},void 0,!0)]),_:3},8,["open"]),k(es,null,{"page-top":f(()=>[c(p.$slots,"page-top",{},void 0,!0)]),"page-bottom":f(()=>[c(p.$slots,"page-bottom",{},void 0,!0)]),"not-found":f(()=>[c(p.$slots,"not-found",{},void 0,!0)]),"home-hero-before":f(()=>[c(p.$slots,"home-hero-before",{},void 0,!0)]),"home-hero-info-before":f(()=>[c(p.$slots,"home-hero-info-before",{},void 0,!0)]),"home-hero-info":f(()=>[c(p.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-info-after":f(()=>[c(p.$slots,"home-hero-info-after",{},void 0,!0)]),"home-hero-actions-after":f(()=>[c(p.$slots,"home-hero-actions-after",{},void 0,!0)]),"home-hero-image":f(()=>[c(p.$slots,"home-hero-image",{},void 0,!0)]),"home-hero-after":f(()=>[c(p.$slots,"home-hero-after",{},void 0,!0)]),"home-features-before":f(()=>[c(p.$slots,"home-features-before",{},void 0,!0)]),"home-features-after":f(()=>[c(p.$slots,"home-features-after",{},void 0,!0)]),"doc-footer-before":f(()=>[c(p.$slots,"doc-footer-before",{},void 0,!0)]),"doc-before":f(()=>[c(p.$slots,"doc-before",{},void 0,!0)]),"doc-after":f(()=>[c(p.$slots,"doc-after",{},void 0,!0)]),"doc-top":f(()=>[c(p.$slots,"doc-top",{},void 0,!0)]),"doc-bottom":f(()=>[c(p.$slots,"doc-bottom",{},void 0,!0)]),"aside-top":f(()=>[c(p.$slots,"aside-top",{},void 0,!0)]),"aside-bottom":f(()=>[c(p.$slots,"aside-bottom",{},void 0,!0)]),"aside-outline-before":f(()=>[c(p.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":f(()=>[c(p.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":f(()=>[c(p.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":f(()=>[c(p.$slots,"aside-ads-after",{},void 0,!0)]),_:3}),k(as),c(p.$slots,"layout-bottom",{},void 0,!0)],2)):(a(),g(L,{key:1}))}}}),Da=$(Ea,[["__scopeId","data-v-a9a9e638"]]),Se={Layout:Da,enhanceApp:({app:o})=>{o.component("Badge",st)}},Fa=o=>{if(typeof document>"u")return{stabilizeScrollPosition:n=>async(...r)=>n(...r)};const e=document.documentElement;return{stabilizeScrollPosition:s=>async(...n)=>{const r=s(...n),l=o.value;if(!l)return r;const d=l.offsetTop-e.scrollTop;return await he(),e.scrollTop=l.offsetTop-d,r}}},Fe="vitepress:tabSharedState",J=typeof localStorage<"u"?localStorage:null,Oe="vitepress:tabsSharedState",Oa=()=>{const o=J==null?void 0:J.getItem(Oe);if(o)try{return JSON.parse(o)}catch{}return{}},Ua=o=>{J&&J.setItem(Oe,JSON.stringify(o))},Ga=o=>{const e=et({});F(()=>e.content,(t,s)=>{t&&s&&Ua(t)},{deep:!0}),o.provide(Fe,e)},ja=(o,e)=>{const t=q(Fe);if(!t)throw new Error("[vitepress-plugin-tabs] TabsSharedState should be injected");O(()=>{t.content||(t.content=Oa())});const s=T(),n=y({get(){var p;const l=e.value,d=o.value;if(l){const b=(p=t.content)==null?void 0:p[l];if(b&&d.includes(b))return b}else{const b=s.value;if(b)return b}return d[0]},set(l){const d=e.value;d?t.content&&(t.content[d]=l):s.value=l}});return{selected:n,select:l=>{n.value=l}}};let Le=0;const za=()=>(Le++,""+Le);function Ka(){const o=Me();return y(()=>{var s;const t=(s=o.default)==null?void 0:s.call(o);return t?t.filter(n=>typeof n.type=="object"&&"__name"in n.type&&n.type.__name==="PluginTabsTab"&&n.props).map(n=>{var r;return(r=n.props)==null?void 0:r.label}):[]})}const Ue="vitepress:tabSingleState",Ra=o=>{me(Ue,o)},Wa=()=>{const o=q(Ue);if(!o)throw new Error("[vitepress-plugin-tabs] TabsSingleState should be injected");return o},qa={class:"plugin-tabs"},Ja=["id","aria-selected","aria-controls","tabindex","onClick"],Ya=m({__name:"PluginTabs",props:{sharedStateKey:{}},setup(o){const e=o,t=Ka(),{selected:s,select:n}=ja(t,tt(e,"sharedStateKey")),r=T(),{stabilizeScrollPosition:l}=Fa(r),d=l(n),p=T([]),b=_=>{var A;const P=t.value.indexOf(s.value);let S;_.key==="ArrowLeft"?S=P>=1?P-1:t.value.length-1:_.key==="ArrowRight"&&(S=P(a(),u("div",qa,[v("div",{ref_key:"tablist",ref:r,class:"plugin-tabs--tab-list",role:"tablist",onKeydown:b},[(a(!0),u(M,null,H(i(t),S=>(a(),u("button",{id:`tab-${S}-${i(L)}`,ref_for:!0,ref_key:"buttonRefs",ref:p,key:S,role:"tab",class:"plugin-tabs--tab","aria-selected":S===i(s),"aria-controls":`panel-${S}-${i(L)}`,tabindex:S===i(s)?0:-1,onClick:()=>i(d)(S)},w(S),9,Ja))),128))],544),c(_.$slots,"default")]))}}),Xa=["id","aria-labelledby"],Qa=m({__name:"PluginTabsTab",props:{label:{}},setup(o){const{uid:e,selected:t}=Wa();return(s,n)=>i(t)===s.label?(a(),u("div",{key:0,id:`panel-${s.label}-${i(e)}`,class:"plugin-tabs--content",role:"tabpanel",tabindex:"0","aria-labelledby":`tab-${s.label}-${i(e)}`},[c(s.$slots,"default",{},void 0,!0)],8,Xa)):h("",!0)}}),Za=$(Qa,[["__scopeId","data-v-9b0d03d2"]]),xa=o=>{Ga(o),o.component("PluginTabs",Ya),o.component("PluginTabsTab",Za)},tr={extends:Se,Layout(){return nt(Se.Layout,null,{})},enhanceApp({app:o,router:e,siteData:t}){xa(o)}};export{tr as R,go as c,V as u}; diff --git a/previews/PR239/assets/cphyzje.DuBHk1fh.png b/previews/PR239/assets/cphyzje.DuBHk1fh.png new file mode 100644 index 000000000..c49ce334a Binary files /dev/null and b/previews/PR239/assets/cphyzje.DuBHk1fh.png differ diff --git a/previews/PR239/assets/cthossd.DaovVbE6.png b/previews/PR239/assets/cthossd.DaovVbE6.png new file mode 100644 index 000000000..ea37ba57f Binary files /dev/null and b/previews/PR239/assets/cthossd.DaovVbE6.png differ diff --git a/previews/PR239/assets/cvdrlht.HTM0ungm.png b/previews/PR239/assets/cvdrlht.HTM0ungm.png new file mode 100644 index 000000000..10392c56f Binary files /dev/null and b/previews/PR239/assets/cvdrlht.HTM0ungm.png differ diff --git a/previews/PR239/assets/cwpogcp.pAYw0Yqf.png b/previews/PR239/assets/cwpogcp.pAYw0Yqf.png new file mode 100644 index 000000000..5fb95f2cc Binary files /dev/null and b/previews/PR239/assets/cwpogcp.pAYw0Yqf.png differ diff --git a/previews/PR239/assets/dytnfok.CULn5saZ.png b/previews/PR239/assets/dytnfok.CULn5saZ.png new file mode 100644 index 000000000..c834125d6 Binary files /dev/null and b/previews/PR239/assets/dytnfok.CULn5saZ.png differ diff --git a/previews/PR239/assets/eozeurm.DGf_4PiA.png b/previews/PR239/assets/eozeurm.DGf_4PiA.png new file mode 100644 index 000000000..7c21ba073 Binary files /dev/null and b/previews/PR239/assets/eozeurm.DGf_4PiA.png differ diff --git a/previews/PR239/assets/evhqnlv.Bglvb-jp.png b/previews/PR239/assets/evhqnlv.Bglvb-jp.png new file mode 100644 index 000000000..07de50819 Binary files /dev/null and b/previews/PR239/assets/evhqnlv.Bglvb-jp.png differ diff --git a/previews/PR239/assets/experiments_accurate_accumulators.md.C1U7KOjF.js b/previews/PR239/assets/experiments_accurate_accumulators.md.C1U7KOjF.js new file mode 100644 index 000000000..eb05109cf --- /dev/null +++ b/previews/PR239/assets/experiments_accurate_accumulators.md.C1U7KOjF.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 accumulation

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!

julia
import GeometryOps as GO, GeoInterface as GI
+using GeoJSON
+using AccurateArithmetic
+using NaturalEarth
+
+all_adm0 = naturalearth("admin_0_countries", 10)
FeatureCollection with 258 Features
julia
GO.area(all_adm0)
21427.909318372607
julia
AccurateArithmetic.sum_oro(GO.area.(all_adm0.geometry))
21427.909318372607
julia
AccurateArithmetic.sum_kbn(GO.area.(all_adm0.geometry))
21427.909318372607
julia
GI.Polygon.(GO.flatten(Union{GI.LineStringTrait, GI.LinearRingTrait}, all_adm0) |> collect .|> x -> [x]) .|> GO.signed_area |> sum
-21427.90063612163
julia
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 accumulation

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!

julia
import GeometryOps as GO, GeoInterface as GI
+using GeoJSON
+using AccurateArithmetic
+using NaturalEarth
+
+all_adm0 = naturalearth("admin_0_countries", 10)
FeatureCollection with 258 Features
julia
GO.area(all_adm0)
21427.909318372607
julia
AccurateArithmetic.sum_oro(GO.area.(all_adm0.geometry))
21427.909318372607
julia
AccurateArithmetic.sum_kbn(GO.area.(all_adm0.geometry))
21427.909318372607
julia
GI.Polygon.(GO.flatten(Union{GI.LineStringTrait, GI.LinearRingTrait}, all_adm0) |> collect .|> x -> [x]) .|> GO.signed_area |> sum
-21427.90063612163
julia
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(`

Predicates

Exact vs fast predicates

Orient

julia
using CairoMakie
+import GeometryOps as GO, GeoInterface as GI, LibGEOS as LG
+import ExactPredicates
+using MultiFloats
+using Chairmarks: @be
+using BenchmarkTools: prettytime
+using Statistics
+
+function orient_f64(p, q, r)
+    return sign((GI.x(p) - GI.x(r))*(GI.y(q) - GI.y(r)) - (GI.y(p) - GI.y(r))*(GI.x(q) - GI.x(r)))
+end
+
+function orient_adaptive(p, q, r)
+    px, py = Float64x2(GI.x(p)), Float64x2(GI.y(p))
+    qx, qy = Float64x2(GI.x(q)), Float64x2(GI.y(q))
+    rx, ry = Float64x2(GI.x(r)), Float64x2(GI.y(r))
+    return sign((px - rx)*(qy - ry) - (py - ry)*(qx - rx))
+end
+# Create an interactive Makie dashboard which can show what is done here
+labels = ["Float64", "Adaptive", "Exact"]
+funcs = [orient_f64, orient_adaptive, ExactPredicates.orient]
+fig = Figure()
+axs = [Axis(fig[1, i]; aspect = DataAspect(), xticklabelrotation = pi/4, title) for (i, title) in enumerate(labels)]
+w, r, q, p = 42.0, 0.95, 18.0, 16.8
+function generate_heatmap_args(func, w, r, q, p, heatmap_size = 1000)
+    w_range = LinRange(0, 0+2.0^(-w), heatmap_size)
+    orient_field = [func((p, p), (q, q), (r+x, r+y)) for x in w_range, y in w_range]
+    return (w_range, w_range, orient_field)
+end
+for (i, (ax, func)) in enumerate(zip(axs, funcs))
+    heatmap!(ax, generate_heatmap_args(func, w, r, q, p)...)
+    # now get timing
+    w_range = LinRange(0, 0+2.0^(-w), 5) # for timing - we want to sample stable + unstable points
+    @time timings = [@be $(func)($((p, p)), $((q, q)), $((r+x, r+y))) for x in w_range, y in w_range]
+    median_timings = map.(x -> getproperty(x, :time), getproperty.(timings, :samples)) |> Iterators.flatten |> collect
+    ax.subtitle = prettytime(Statistics.median(median_timings)*10^9)
+    # create time histogram plot
+    # hist(fig[2, i], median_timings; axis = (; xticklabelrotation = pi/4))
+    display(fig)
+end
+resize!(fig, 1000, 450)
+fig

Dashboard

julia
using WGLMakie
+import GeometryOps as GO, GeoInterface as GI, LibGEOS as LG
+import ExactPredicates
+using MultiFloats
+
+function orient_f64(p, q, r)
+    return sign((GI.x(p) - GI.x(r))*(GI.y(q) - GI.y(r)) - (GI.y(p) - GI.y(r))*(GI.x(q) - GI.x(r)))
+end
+
+function orient_adaptive(p, q, r)
+    px, py = Float64x2(GI.x(p)), Float64x2(GI.y(p))
+    qx, qy = Float64x2(GI.x(q)), Float64x2(GI.y(q))
+    rx, ry = Float64x2(GI.x(r)), Float64x2(GI.y(r))
+    return sign((px - rx)*(qy - ry) - (py - ry)*(qx - rx))
+end
+# Create an interactive Makie dashboard which can show what is done here
+fig = Figure()
+ax = Axis(fig[1, 1]; aspect = DataAspect())
+sliders = SliderGrid(fig[2, 1],
+        (label = L"w = 2^{-v} (zoom)", range = LinRange(40, 44, 100), startvalue = 42),
+        (label = L"r = (x, y),~ x, y ∈ v + [0..w)", range = 0:0.01:3, startvalue = 0.95),
+        (label = L"q = (k, k),~ k = v", range = LinRange(0, 30, 100), startvalue = 18),
+        (label = L"p = (k, k),~ k = v", range = LinRange(0, 30, 100), startvalue = 16.8),
+)
+orient_funcs = [orient_f64, orient_adaptive, ExactPredicates.orient]
+menu = Menu(fig[3, 1], options = zip(string.(orient_funcs), orient_funcs))
+w_obs, r_obs, q_obs, p_obs = getproperty.(sliders.sliders, :value)
+orient_obs = menu.selection
+
+heatmap_size = @lift maximum(widths($(ax.scene.viewport)))*4
+
+matrix_observable = lift(orient_obs, w_obs, r_obs, q_obs, p_obs, heatmap_size) do orient, w, r, q, p, heatmap_size
+    return [orient((p, p), (q, q), (r+x, r+y)) for x in LinRange(0, 0+2.0^(-w), heatmap_size), y in LinRange(0, 0+2.0^(-w), heatmap_size)]
+end
+heatmap!(ax, matrix_observable; colormap = [:red, :green, :blue])
+resize!(fig, 500, 700)
+fig

Testing robust vs regular predicates

julia

+import GeoInterface as GI, GeometryOps as GO, LibGEOS as LG
+using MultiFloats
+c1 = [[-28083.868447876892, -58059.13401805979], [-9833.052704767595, -48001.726711609794], [-16111.439295815226, -2.856614689791036e-11], [-76085.95770326033, -2.856614689791036e-11], [-28083.868447876892, -58059.13401805979]]
+c2 = [[-53333.333333333336, 0.0], [0.0, 0.0], [0.0, -80000.0], [-60000.0, -80000.0], [-53333.333333333336, 0.0]]
+
+p1 = GI.Polygon([c1])
+p2 = GI.Polygon([c2])
+GO.intersection(p1, p2; target = GI.PolygonTrait(), fix_multipoly = nothing)
+
+p1_m, p2_m = GO.transform(x -> (Float64x2.(x)), [p1, p2])
+GO.intersection(p1_m, p2_m; target = GI.PolygonTrait(), fix_multipoly = nothing)
+
+p1 = GI.Polygon([[[-57725.80869813739, -52709.704377648755], [-53333.333333333336, 0.0], [-41878.01362848005, 0.0], [-36022.23699059147, -43787.61366192682], [-48268.44121252392, -52521.18593721105], [-57725.80869813739, -52709.704377648755]]])
+p2 = GI.Polygon([[[-60000.0, 80000.0], [0.0, 80000.0], [0.0, 0.0], [-53333.33333333333, 0.0], [-50000.0, 40000.0], [-60000.0, 80000.0]]])
+p1_m, p2_m = GO.transform(x -> (Float64x2.(x)), [p1, p2])
+f, a, p__1 = poly(p1; label = "p1")
+p__2 = poly!(a, p2; label = "p2")
+
+GO.intersection(p1_m, p2_m; target = GI.PolygonTrait(), fix_multipoly = nothing)
+LG.intersection(p1_m, p2_m)

Incircle

`,10)]))}const F=i(p,[["render",l]]);export{y as __pageData,F as default}; diff --git a/previews/PR239/assets/experiments_predicates.md.C2d7Qctz.lean.js b/previews/PR239/assets/experiments_predicates.md.C2d7Qctz.lean.js new file mode 100644 index 000000000..eec97f2c7 --- /dev/null +++ b/previews/PR239/assets/experiments_predicates.md.C2d7Qctz.lean.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(`

Predicates

Exact vs fast predicates

Orient

julia
using CairoMakie
+import GeometryOps as GO, GeoInterface as GI, LibGEOS as LG
+import ExactPredicates
+using MultiFloats
+using Chairmarks: @be
+using BenchmarkTools: prettytime
+using Statistics
+
+function orient_f64(p, q, r)
+    return sign((GI.x(p) - GI.x(r))*(GI.y(q) - GI.y(r)) - (GI.y(p) - GI.y(r))*(GI.x(q) - GI.x(r)))
+end
+
+function orient_adaptive(p, q, r)
+    px, py = Float64x2(GI.x(p)), Float64x2(GI.y(p))
+    qx, qy = Float64x2(GI.x(q)), Float64x2(GI.y(q))
+    rx, ry = Float64x2(GI.x(r)), Float64x2(GI.y(r))
+    return sign((px - rx)*(qy - ry) - (py - ry)*(qx - rx))
+end
+# Create an interactive Makie dashboard which can show what is done here
+labels = ["Float64", "Adaptive", "Exact"]
+funcs = [orient_f64, orient_adaptive, ExactPredicates.orient]
+fig = Figure()
+axs = [Axis(fig[1, i]; aspect = DataAspect(), xticklabelrotation = pi/4, title) for (i, title) in enumerate(labels)]
+w, r, q, p = 42.0, 0.95, 18.0, 16.8
+function generate_heatmap_args(func, w, r, q, p, heatmap_size = 1000)
+    w_range = LinRange(0, 0+2.0^(-w), heatmap_size)
+    orient_field = [func((p, p), (q, q), (r+x, r+y)) for x in w_range, y in w_range]
+    return (w_range, w_range, orient_field)
+end
+for (i, (ax, func)) in enumerate(zip(axs, funcs))
+    heatmap!(ax, generate_heatmap_args(func, w, r, q, p)...)
+    # now get timing
+    w_range = LinRange(0, 0+2.0^(-w), 5) # for timing - we want to sample stable + unstable points
+    @time timings = [@be $(func)($((p, p)), $((q, q)), $((r+x, r+y))) for x in w_range, y in w_range]
+    median_timings = map.(x -> getproperty(x, :time), getproperty.(timings, :samples)) |> Iterators.flatten |> collect
+    ax.subtitle = prettytime(Statistics.median(median_timings)*10^9)
+    # create time histogram plot
+    # hist(fig[2, i], median_timings; axis = (; xticklabelrotation = pi/4))
+    display(fig)
+end
+resize!(fig, 1000, 450)
+fig

Dashboard

julia
using WGLMakie
+import GeometryOps as GO, GeoInterface as GI, LibGEOS as LG
+import ExactPredicates
+using MultiFloats
+
+function orient_f64(p, q, r)
+    return sign((GI.x(p) - GI.x(r))*(GI.y(q) - GI.y(r)) - (GI.y(p) - GI.y(r))*(GI.x(q) - GI.x(r)))
+end
+
+function orient_adaptive(p, q, r)
+    px, py = Float64x2(GI.x(p)), Float64x2(GI.y(p))
+    qx, qy = Float64x2(GI.x(q)), Float64x2(GI.y(q))
+    rx, ry = Float64x2(GI.x(r)), Float64x2(GI.y(r))
+    return sign((px - rx)*(qy - ry) - (py - ry)*(qx - rx))
+end
+# Create an interactive Makie dashboard which can show what is done here
+fig = Figure()
+ax = Axis(fig[1, 1]; aspect = DataAspect())
+sliders = SliderGrid(fig[2, 1],
+        (label = L"w = 2^{-v} (zoom)", range = LinRange(40, 44, 100), startvalue = 42),
+        (label = L"r = (x, y),~ x, y ∈ v + [0..w)", range = 0:0.01:3, startvalue = 0.95),
+        (label = L"q = (k, k),~ k = v", range = LinRange(0, 30, 100), startvalue = 18),
+        (label = L"p = (k, k),~ k = v", range = LinRange(0, 30, 100), startvalue = 16.8),
+)
+orient_funcs = [orient_f64, orient_adaptive, ExactPredicates.orient]
+menu = Menu(fig[3, 1], options = zip(string.(orient_funcs), orient_funcs))
+w_obs, r_obs, q_obs, p_obs = getproperty.(sliders.sliders, :value)
+orient_obs = menu.selection
+
+heatmap_size = @lift maximum(widths($(ax.scene.viewport)))*4
+
+matrix_observable = lift(orient_obs, w_obs, r_obs, q_obs, p_obs, heatmap_size) do orient, w, r, q, p, heatmap_size
+    return [orient((p, p), (q, q), (r+x, r+y)) for x in LinRange(0, 0+2.0^(-w), heatmap_size), y in LinRange(0, 0+2.0^(-w), heatmap_size)]
+end
+heatmap!(ax, matrix_observable; colormap = [:red, :green, :blue])
+resize!(fig, 500, 700)
+fig

Testing robust vs regular predicates

julia

+import GeoInterface as GI, GeometryOps as GO, LibGEOS as LG
+using MultiFloats
+c1 = [[-28083.868447876892, -58059.13401805979], [-9833.052704767595, -48001.726711609794], [-16111.439295815226, -2.856614689791036e-11], [-76085.95770326033, -2.856614689791036e-11], [-28083.868447876892, -58059.13401805979]]
+c2 = [[-53333.333333333336, 0.0], [0.0, 0.0], [0.0, -80000.0], [-60000.0, -80000.0], [-53333.333333333336, 0.0]]
+
+p1 = GI.Polygon([c1])
+p2 = GI.Polygon([c2])
+GO.intersection(p1, p2; target = GI.PolygonTrait(), fix_multipoly = nothing)
+
+p1_m, p2_m = GO.transform(x -> (Float64x2.(x)), [p1, p2])
+GO.intersection(p1_m, p2_m; target = GI.PolygonTrait(), fix_multipoly = nothing)
+
+p1 = GI.Polygon([[[-57725.80869813739, -52709.704377648755], [-53333.333333333336, 0.0], [-41878.01362848005, 0.0], [-36022.23699059147, -43787.61366192682], [-48268.44121252392, -52521.18593721105], [-57725.80869813739, -52709.704377648755]]])
+p2 = GI.Polygon([[[-60000.0, 80000.0], [0.0, 80000.0], [0.0, 0.0], [-53333.33333333333, 0.0], [-50000.0, 40000.0], [-60000.0, 80000.0]]])
+p1_m, p2_m = GO.transform(x -> (Float64x2.(x)), [p1, p2])
+f, a, p__1 = poly(p1; label = "p1")
+p__2 = poly!(a, p2; label = "p2")
+
+GO.intersection(p1_m, p2_m; target = GI.PolygonTrait(), fix_multipoly = nothing)
+LG.intersection(p1_m, p2_m)

Incircle

`,10)]))}const F=i(p,[["render",l]]);export{y as __pageData,F as default}; diff --git a/previews/PR239/assets/explanations_crs.md.BdkIxIey.js b/previews/PR239/assets/explanations_crs.md.BdkIxIey.js new file mode 100644 index 000000000..f01c24a96 --- /dev/null +++ b/previews/PR239/assets/explanations_crs.md.BdkIxIey.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.onQNwZ2I.js";const _=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"explanations/crs.md","filePath":"explanations/crs.md","lastUpdated":null}'),s={name:"explanations/crs.md"};function n(r,o,c,p,i,l){return a(),t("div")}const m=e(s,[["render",n]]);export{_ as __pageData,m as default}; diff --git a/previews/PR239/assets/explanations_crs.md.BdkIxIey.lean.js b/previews/PR239/assets/explanations_crs.md.BdkIxIey.lean.js new file mode 100644 index 000000000..f01c24a96 --- /dev/null +++ b/previews/PR239/assets/explanations_crs.md.BdkIxIey.lean.js @@ -0,0 +1 @@ +import{_ as e,c as t,o as a}from"./chunks/framework.onQNwZ2I.js";const _=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"explanations/crs.md","filePath":"explanations/crs.md","lastUpdated":null}'),s={name:"explanations/crs.md"};function n(r,o,c,p,i,l){return a(),t("div")}const m=e(s,[["render",n]]);export{_ as __pageData,m as default}; diff --git a/previews/PR239/assets/explanations_paradigms.md.B5COLxNa.js b/previews/PR239/assets/explanations_paradigms.md.B5COLxNa.js new file mode 100644 index 000000000..ebf2c4ccd --- /dev/null +++ b/previews/PR239/assets/explanations_paradigms.md.B5COLxNa.js @@ -0,0 +1 @@ +import{_ as a,c as t,a5 as o,o as i}from"./chunks/framework.onQNwZ2I.js";const u=JSON.parse('{"title":"Paradigms","description":"","frontmatter":{},"headers":[],"relativePath":"explanations/paradigms.md","filePath":"explanations/paradigms.md","lastUpdated":null}'),r={name:"explanations/paradigms.md"};function s(n,e,d,p,c,l){return i(),t("div",null,e[0]||(e[0]=[o('

Paradigms

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:

julia
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 reduces 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.)

',14)]))}const m=a(r,[["render",s]]);export{u as __pageData,m as default}; diff --git a/previews/PR239/assets/explanations_paradigms.md.B5COLxNa.lean.js b/previews/PR239/assets/explanations_paradigms.md.B5COLxNa.lean.js new file mode 100644 index 000000000..ebf2c4ccd --- /dev/null +++ b/previews/PR239/assets/explanations_paradigms.md.B5COLxNa.lean.js @@ -0,0 +1 @@ +import{_ as a,c as t,a5 as o,o as i}from"./chunks/framework.onQNwZ2I.js";const u=JSON.parse('{"title":"Paradigms","description":"","frontmatter":{},"headers":[],"relativePath":"explanations/paradigms.md","filePath":"explanations/paradigms.md","lastUpdated":null}'),r={name:"explanations/paradigms.md"};function s(n,e,d,p,c,l){return i(),t("div",null,e[0]||(e[0]=[o('

Paradigms

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:

julia
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 reduces 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.)

',14)]))}const m=a(r,[["render",s]]);export{u as __pageData,m as default}; diff --git a/previews/PR239/assets/explanations_peculiarities.md.Cphx4jNo.js b/previews/PR239/assets/explanations_peculiarities.md.Cphx4jNo.js new file mode 100644 index 000000000..8dd61995b --- /dev/null +++ b/previews/PR239/assets/explanations_peculiarities.md.Cphx4jNo.js @@ -0,0 +1 @@ +import{_ as o,c as t,a5 as a,o as r}from"./chunks/framework.onQNwZ2I.js";const u=JSON.parse('{"title":"Peculiarities","description":"","frontmatter":{},"headers":[],"relativePath":"explanations/peculiarities.md","filePath":"explanations/peculiarities.md","lastUpdated":null}'),n={name:"explanations/peculiarities.md"};function i(s,e,l,d,c,p){return r(),t("div",null,e[0]||(e[0]=[a('

Peculiarities

What does apply return and why?

apply returns the target geometries returned by f, whatever type/package they are from, but geometries, features or feature collections that wrapped the target are replaced with GeoInterace.jl wrappers with matching GeoInterface.trait to the originals. All non-geointerface iterables become Arrays. 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.

Why do you want me to provide a 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

',11)]))}const y=o(n,[["render",i]]);export{u as __pageData,y as default}; diff --git a/previews/PR239/assets/explanations_peculiarities.md.Cphx4jNo.lean.js b/previews/PR239/assets/explanations_peculiarities.md.Cphx4jNo.lean.js new file mode 100644 index 000000000..8dd61995b --- /dev/null +++ b/previews/PR239/assets/explanations_peculiarities.md.Cphx4jNo.lean.js @@ -0,0 +1 @@ +import{_ as o,c as t,a5 as a,o as r}from"./chunks/framework.onQNwZ2I.js";const u=JSON.parse('{"title":"Peculiarities","description":"","frontmatter":{},"headers":[],"relativePath":"explanations/peculiarities.md","filePath":"explanations/peculiarities.md","lastUpdated":null}'),n={name:"explanations/peculiarities.md"};function i(s,e,l,d,c,p){return r(),t("div",null,e[0]||(e[0]=[a('

Peculiarities

What does apply return and why?

apply returns the target geometries returned by f, whatever type/package they are from, but geometries, features or feature collections that wrapped the target are replaced with GeoInterace.jl wrappers with matching GeoInterface.trait to the originals. All non-geointerface iterables become Arrays. 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.

Why do you want me to provide a 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

',11)]))}const y=o(n,[["render",i]]);export{u as __pageData,y as default}; diff --git a/previews/PR239/assets/explanations_winding_order.md.0V2FnJmC.js b/previews/PR239/assets/explanations_winding_order.md.0V2FnJmC.js new file mode 100644 index 000000000..bbc6c7dd5 --- /dev/null +++ b/previews/PR239/assets/explanations_winding_order.md.0V2FnJmC.js @@ -0,0 +1 @@ +import{_ as e,c as n,o as t}from"./chunks/framework.onQNwZ2I.js";const l=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"explanations/winding_order.md","filePath":"explanations/winding_order.md","lastUpdated":null}'),a={name:"explanations/winding_order.md"};function r(o,i,d,s,c,p){return t(),n("div")}const m=e(a,[["render",r]]);export{l as __pageData,m as default}; diff --git a/previews/PR239/assets/explanations_winding_order.md.0V2FnJmC.lean.js b/previews/PR239/assets/explanations_winding_order.md.0V2FnJmC.lean.js new file mode 100644 index 000000000..bbc6c7dd5 --- /dev/null +++ b/previews/PR239/assets/explanations_winding_order.md.0V2FnJmC.lean.js @@ -0,0 +1 @@ +import{_ as e,c as n,o as t}from"./chunks/framework.onQNwZ2I.js";const l=JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"explanations/winding_order.md","filePath":"explanations/winding_order.md","lastUpdated":null}'),a={name:"explanations/winding_order.md"};function r(o,i,d,s,c,p){return t(),n("div")}const m=e(a,[["render",r]]);export{l as __pageData,m as default}; diff --git a/previews/PR239/assets/eytjybr.BEFUMtlf.png b/previews/PR239/assets/eytjybr.BEFUMtlf.png new file mode 100644 index 000000000..b400f44a5 Binary files /dev/null and b/previews/PR239/assets/eytjybr.BEFUMtlf.png differ diff --git a/previews/PR239/assets/filtvpm.mCtKcWOr.png b/previews/PR239/assets/filtvpm.mCtKcWOr.png new file mode 100644 index 000000000..c76dcbee9 Binary files /dev/null and b/previews/PR239/assets/filtvpm.mCtKcWOr.png differ diff --git a/previews/PR239/assets/fyfzmss.Dab1-ETk.png b/previews/PR239/assets/fyfzmss.Dab1-ETk.png new file mode 100644 index 000000000..7c32623e0 Binary files /dev/null and b/previews/PR239/assets/fyfzmss.Dab1-ETk.png differ diff --git a/previews/PR239/assets/hvyhqaw.CgiryX2p.png b/previews/PR239/assets/hvyhqaw.CgiryX2p.png new file mode 100644 index 000000000..dcf01fb21 Binary files /dev/null and b/previews/PR239/assets/hvyhqaw.CgiryX2p.png differ diff --git a/previews/PR239/assets/ibdeule.BD0hVfse.png b/previews/PR239/assets/ibdeule.BD0hVfse.png new file mode 100644 index 000000000..212d1e6a0 Binary files /dev/null and b/previews/PR239/assets/ibdeule.BD0hVfse.png differ diff --git a/previews/PR239/assets/ievlkku.5DNJaCuT.png b/previews/PR239/assets/ievlkku.5DNJaCuT.png new file mode 100644 index 000000000..450196afd Binary files /dev/null and b/previews/PR239/assets/ievlkku.5DNJaCuT.png differ diff --git a/previews/PR239/assets/index.md.C-gmTcRS.js b/previews/PR239/assets/index.md.C-gmTcRS.js new file mode 100644 index 000000000..0fe9ce310 --- /dev/null +++ b/previews/PR239/assets/index.md.C-gmTcRS.js @@ -0,0 +1 @@ +import{_ as t,c as a,a5 as o,o as i}from"./chunks/framework.onQNwZ2I.js";const m=JSON.parse('{"title":"What is GeometryOps.jl?","description":"","frontmatter":{"layout":"home","hero":{"name":"GeometryOps.jl","text":"","tagline":"Blazing fast geometry operations in pure Julia","image":{"src":"/logo.png","alt":"GeometryOps"},"actions":[{"theme":"brand","text":"Introduction","link":"/introduction"},{"theme":"alt","text":"View on Github","link":"https://github.com/JuliaGeo/GeometryOps.jl"},{"theme":"alt","text":"API Reference","link":"/api"}]},"features":[{"icon":"\\"Julia","title":"Pure Julia code","details":"Fast, understandable, extensible functions","link":"/introduction"},{"icon":"","title":"Literate programming","details":"Documented source code with examples!","link":"/source/methods/clipping/cut"},{"icon":"","title":"Full integration with GeoInterface","details":"Use any GeoInterface.jl-compatible geometry","link":"https://juliageo.org/GeoInterface.jl/stable"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":null}'),n={name:"index.md"};function s(r,e,l,c,d,h){return i(),a("div",null,e[0]||(e[0]=[o('

What is GeometryOps.jl?

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!

How to navigate the docs

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.

  • Tutorials are meant to teach the fundamental concepts behind GeometryOps, and how to perform certain operations.
  • Explanations usually contain little code, and explain in more detail how GeometryOps works.
  • Source code usually contains explanations and examples at the top of the page, followed by annotated source code from that file.
',2)]))}const u=t(n,[["render",s]]);export{m as __pageData,u as default}; diff --git a/previews/PR239/assets/index.md.C-gmTcRS.lean.js b/previews/PR239/assets/index.md.C-gmTcRS.lean.js new file mode 100644 index 000000000..0fe9ce310 --- /dev/null +++ b/previews/PR239/assets/index.md.C-gmTcRS.lean.js @@ -0,0 +1 @@ +import{_ as t,c as a,a5 as o,o as i}from"./chunks/framework.onQNwZ2I.js";const m=JSON.parse('{"title":"What is GeometryOps.jl?","description":"","frontmatter":{"layout":"home","hero":{"name":"GeometryOps.jl","text":"","tagline":"Blazing fast geometry operations in pure Julia","image":{"src":"/logo.png","alt":"GeometryOps"},"actions":[{"theme":"brand","text":"Introduction","link":"/introduction"},{"theme":"alt","text":"View on Github","link":"https://github.com/JuliaGeo/GeometryOps.jl"},{"theme":"alt","text":"API Reference","link":"/api"}]},"features":[{"icon":"\\"Julia","title":"Pure Julia code","details":"Fast, understandable, extensible functions","link":"/introduction"},{"icon":"","title":"Literate programming","details":"Documented source code with examples!","link":"/source/methods/clipping/cut"},{"icon":"","title":"Full integration with GeoInterface","details":"Use any GeoInterface.jl-compatible geometry","link":"https://juliageo.org/GeoInterface.jl/stable"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":null}'),n={name:"index.md"};function s(r,e,l,c,d,h){return i(),a("div",null,e[0]||(e[0]=[o('

What is GeometryOps.jl?

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!

How to navigate the docs

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.

  • Tutorials are meant to teach the fundamental concepts behind GeometryOps, and how to perform certain operations.
  • Explanations usually contain little code, and explain in more detail how GeometryOps works.
  • Source code usually contains explanations and examples at the top of the page, followed by annotated source code from that file.
',2)]))}const u=t(n,[["render",s]]);export{m as __pageData,u as default}; diff --git a/previews/PR239/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 b/previews/PR239/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 new file mode 100644 index 000000000..b6b603d59 Binary files /dev/null and b/previews/PR239/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 differ diff --git a/previews/PR239/assets/inter-italic-cyrillic.By2_1cv3.woff2 b/previews/PR239/assets/inter-italic-cyrillic.By2_1cv3.woff2 new file mode 100644 index 000000000..def40a4f6 Binary files /dev/null and b/previews/PR239/assets/inter-italic-cyrillic.By2_1cv3.woff2 differ diff --git a/previews/PR239/assets/inter-italic-greek-ext.1u6EdAuj.woff2 b/previews/PR239/assets/inter-italic-greek-ext.1u6EdAuj.woff2 new file mode 100644 index 000000000..e070c3d30 Binary files /dev/null and b/previews/PR239/assets/inter-italic-greek-ext.1u6EdAuj.woff2 differ diff --git a/previews/PR239/assets/inter-italic-greek.DJ8dCoTZ.woff2 b/previews/PR239/assets/inter-italic-greek.DJ8dCoTZ.woff2 new file mode 100644 index 000000000..a3c16ca40 Binary files /dev/null and b/previews/PR239/assets/inter-italic-greek.DJ8dCoTZ.woff2 differ diff --git a/previews/PR239/assets/inter-italic-latin-ext.CN1xVJS-.woff2 b/previews/PR239/assets/inter-italic-latin-ext.CN1xVJS-.woff2 new file mode 100644 index 000000000..2210a899e Binary files /dev/null and b/previews/PR239/assets/inter-italic-latin-ext.CN1xVJS-.woff2 differ diff --git a/previews/PR239/assets/inter-italic-latin.C2AdPX0b.woff2 b/previews/PR239/assets/inter-italic-latin.C2AdPX0b.woff2 new file mode 100644 index 000000000..790d62dc7 Binary files /dev/null and b/previews/PR239/assets/inter-italic-latin.C2AdPX0b.woff2 differ diff --git a/previews/PR239/assets/inter-italic-vietnamese.BSbpV94h.woff2 b/previews/PR239/assets/inter-italic-vietnamese.BSbpV94h.woff2 new file mode 100644 index 000000000..1eec0775a Binary files /dev/null and b/previews/PR239/assets/inter-italic-vietnamese.BSbpV94h.woff2 differ diff --git a/previews/PR239/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 b/previews/PR239/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 new file mode 100644 index 000000000..2cfe61536 Binary files /dev/null and b/previews/PR239/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 differ diff --git a/previews/PR239/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 b/previews/PR239/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 new file mode 100644 index 000000000..e3886dd14 Binary files /dev/null and b/previews/PR239/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 differ diff --git a/previews/PR239/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 b/previews/PR239/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 new file mode 100644 index 000000000..36d67487d Binary files /dev/null and b/previews/PR239/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 differ diff --git a/previews/PR239/assets/inter-roman-greek.BBVDIX6e.woff2 b/previews/PR239/assets/inter-roman-greek.BBVDIX6e.woff2 new file mode 100644 index 000000000..2bed1e85e Binary files /dev/null and b/previews/PR239/assets/inter-roman-greek.BBVDIX6e.woff2 differ diff --git a/previews/PR239/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 b/previews/PR239/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 new file mode 100644 index 000000000..9a8d1e2b5 Binary files /dev/null and b/previews/PR239/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 differ diff --git a/previews/PR239/assets/inter-roman-latin.Di8DUHzh.woff2 b/previews/PR239/assets/inter-roman-latin.Di8DUHzh.woff2 new file mode 100644 index 000000000..07d3c53ae Binary files /dev/null and b/previews/PR239/assets/inter-roman-latin.Di8DUHzh.woff2 differ diff --git a/previews/PR239/assets/inter-roman-vietnamese.BjW4sHH5.woff2 b/previews/PR239/assets/inter-roman-vietnamese.BjW4sHH5.woff2 new file mode 100644 index 000000000..57bdc22ae Binary files /dev/null and b/previews/PR239/assets/inter-roman-vietnamese.BjW4sHH5.woff2 differ diff --git a/previews/PR239/assets/introduction.md.FQTtykEQ.js b/previews/PR239/assets/introduction.md.FQTtykEQ.js new file mode 100644 index 000000000..68fcb53ae --- /dev/null +++ b/previews/PR239/assets/introduction.md.FQTtykEQ.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('

Introduction

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!

Main concepts

The 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)!

What's this 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('

Introduction

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!

Main concepts

The 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)!

What's this 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(`

GeometryOps.jl

julia
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.

julia
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!

julia
    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(`

GeometryOps.jl

julia
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.

julia
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!

julia
    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(`
julia
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.

julia
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

julia
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.

julia
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.

julia
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.

julia
    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.

julia
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!

julia
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(`
julia
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.

julia
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

julia
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.

julia
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.

julia
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.

julia
    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.

julia
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!

julia
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(`
julia
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.

julia
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(`
julia
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.

julia
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(`
julia
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!

julia
    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(`
julia
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!

julia
    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(`

Segmentize

julia
import GeometryOps: segmentize, apply

This file implements the LibGEOS segmentization method for GeometryOps.

julia
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

julia
@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?

julia
        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(`

Segmentize

julia
import GeometryOps: segmentize, apply

This file implements the LibGEOS segmentization method for GeometryOps.

julia
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

julia
@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?

julia
        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(`

Simple overrides

This file contains simple overrides for GEOS, essentially only those functions which have direct counterparts in LG and only require conversion before calling.

Polygon set operations

Difference

julia
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

Union

julia
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

Intersection

julia
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

Symmetric difference

julia
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

DE-9IM boolean methods

Equals

julia
function GO.equals(::GEOS, geom_a, geom_b)
+    return LG.equals(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Disjoint

julia
function GO.disjoint(::GEOS, geom_a, geom_b)
+    return LG.disjoint(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Touches

julia
function GO.touches(::GEOS, geom_a, geom_b)
+    return LG.touches(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Crosses

julia
function GO.crosses(::GEOS, geom_a, geom_b)
+    return LG.crosses(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Within

julia
function GO.within(::GEOS, geom_a, geom_b)
+    return LG.within(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Contains

julia
function GO.contains(::GEOS, geom_a, geom_b)
+    return LG.contains(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Overlaps

julia
function GO.overlaps(::GEOS, geom_a, geom_b)
+    return LG.overlaps(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Covers

julia
function GO.covers(::GEOS, geom_a, geom_b)
+    return LG.covers(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

CoveredBy

julia
function GO.coveredby(::GEOS, geom_a, geom_b)
+    return LG.coveredby(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Intersects

julia
function GO.intersects(::GEOS, geom_a, geom_b)
+    return LG.intersects(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Convex hull

julia
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(`

Simple overrides

This file contains simple overrides for GEOS, essentially only those functions which have direct counterparts in LG and only require conversion before calling.

Polygon set operations

Difference

julia
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

Union

julia
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

Intersection

julia
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

Symmetric difference

julia
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

DE-9IM boolean methods

Equals

julia
function GO.equals(::GEOS, geom_a, geom_b)
+    return LG.equals(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Disjoint

julia
function GO.disjoint(::GEOS, geom_a, geom_b)
+    return LG.disjoint(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Touches

julia
function GO.touches(::GEOS, geom_a, geom_b)
+    return LG.touches(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Crosses

julia
function GO.crosses(::GEOS, geom_a, geom_b)
+    return LG.crosses(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Within

julia
function GO.within(::GEOS, geom_a, geom_b)
+    return LG.within(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Contains

julia
function GO.contains(::GEOS, geom_a, geom_b)
+    return LG.contains(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Overlaps

julia
function GO.overlaps(::GEOS, geom_a, geom_b)
+    return LG.overlaps(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Covers

julia
function GO.covers(::GEOS, geom_a, geom_b)
+    return LG.covers(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

CoveredBy

julia
function GO.coveredby(::GEOS, geom_a, geom_b)
+    return LG.coveredby(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Intersects

julia
function GO.intersects(::GEOS, geom_a, geom_b)
+    return LG.intersects(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Convex hull

julia
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

julia
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

julia
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(`
julia
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(`
julia
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(`
julia
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

julia
        isnothing(source_crs) && throw(ArgumentError("geom has no crs attached. Pass a \`source_crs\` keyword"))

Otherwise reproject

julia
        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(`
julia
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

julia
        isnothing(source_crs) && throw(ArgumentError("geom has no crs attached. Pass a \`source_crs\` keyword"))

Otherwise reproject

julia
        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.

julia
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...

julia
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.

julia
    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.

julia
    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.

julia
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...

julia
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.

julia
    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.

julia
    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(`

Angles

julia
export angles

What is angles?

Angles are the angles formed by a given geometries line segments, if it has line segments.

To provide an example, consider this rectangle:

julia
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.

julia
GO.angles(rect)  # [90, 90, 90, 90]
4-element Vector{Float64}:
+ 90.0
+ 90.0
+ 90.0
+ 90.0

Implementation

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!

julia
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

julia
_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

julia
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

julia
    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

julia
    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

julia
    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.

julia
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(`

Angles

julia
export angles

What is angles?

Angles are the angles formed by a given geometries line segments, if it has line segments.

To provide an example, consider this rectangle:

julia
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.

julia
GO.angles(rect)  # [90, 90, 90, 90]
4-element Vector{Float64}:
+ 90.0
+ 90.0
+ 90.0
+ 90.0

Implementation

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!

julia
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

julia
_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

julia
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

julia
    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

julia
    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

julia
    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.

julia
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(`

Area and signed area

julia
export area, signed_area

What is area? What is 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:

julia
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:

julia
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.

julia
GO.signed_area(rect)  # -1.0
-1.0

Implementation

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

julia
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

julia
_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...

julia
_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

julia
_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

julia
    for hole in GI.gethole(poly)
+        area -= abs(_signed_area(T, hole))
+    end

Winding of exterior ring determines sign

julia
    return area * sign(s_area)
+end

One term of the shoelace area formula

julia
_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

julia
    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.

julia
        if first
+            p1 = pfirst = p2
+            first = false
+            continue
+        end

Accumulate the area into area

julia
        area += _area_component(p1, p2)
+        p1 = p2
+    end

Complete the last edge. If the first and last where the same this will be zero

julia
    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(`

Area and signed area

julia
export area, signed_area

What is area? What is 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:

julia
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:

julia
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.

julia
GO.signed_area(rect)  # -1.0
-1.0

Implementation

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

julia
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

julia
_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...

julia
_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

julia
_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

julia
    for hole in GI.gethole(poly)
+        area -= abs(_signed_area(T, hole))
+    end

Winding of exterior ring determines sign

julia
    return area * sign(s_area)
+end

One term of the shoelace area formula

julia
_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

julia
    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.

julia
        if first
+            p1 = pfirst = p2
+            first = false
+            continue
+        end

Accumulate the area into area

julia
        area += _area_component(p1, p2)
+        p1 = p2
+    end

Complete the last edge. If the first and last where the same this will be zero

julia
    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(`

Barycentric coordinates

julia
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('',1)]))),i[1]||(i[1]=s("mjx-assistive-mml",{unselectable:"on",display:"inline",style:{top:"0px",left:"0px",clip:"rect(1px, 1px, 1px, 1px)","-webkit-touch-callout":"none","-webkit-user-select":"none","-khtml-user-select":"none","-moz-user-select":"none","-ms-user-select":"none","user-select":"none",position:"absolute",padding:"1px 0px 0px 0px",border:"0px",display:"block",width:"auto",overflow:"hidden"}},[s("math",{xmlns:"http://www.w3.org/1998/Math/MathML"},[s("mo",{stretchy:"false"},"("),s("msub",null,[s("mi",null,"λ"),s("mn",null,"1")]),s("mo",null,","),s("msub",null,[s("mi",null,"λ"),s("mn",null,"2")]),s("mo",null,","),s("msub",null,[s("mi",null,"λ"),s("mn",null,"3")]),s("mo",{stretchy:"false"},")")])],-1))]),i[3]||(i[3]=a(", each associated with a vertex of the triangle. Any point within the triangle can be expressed as a weighted average of the vertices, where the weights are the barycentric coordinates. The weights sum to 1, and each is non-negative."))]),s("p",null,[i[10]||(i[10]=a("For a polygon with ")),s("mjx-container",r,[(h(),n("svg",d,i[4]||(i[4]=[s("g",{stroke:"currentColor",fill:"currentColor","stroke-width":"0",transform:"scale(1,-1)"},[s("g",{"data-mml-node":"math"},[s("g",{"data-mml-node":"mi"},[s("path",{"data-c":"1D45B",d:"M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z",style:{"stroke-width":"3"}})])])],-1)]))),i[5]||(i[5]=s("mjx-assistive-mml",{unselectable:"on",display:"inline",style:{top:"0px",left:"0px",clip:"rect(1px, 1px, 1px, 1px)","-webkit-touch-callout":"none","-webkit-user-select":"none","-khtml-user-select":"none","-moz-user-select":"none","-ms-user-select":"none","user-select":"none",position:"absolute",padding:"1px 0px 0px 0px",border:"0px",display:"block",width:"auto",overflow:"hidden"}},[s("math",{xmlns:"http://www.w3.org/1998/Math/MathML"},[s("mi",null,"n")])],-1))]),i[11]||(i[11]=a(" vertices, generalized barycentric coordinates are a set of ")),s("mjx-container",g,[(h(),n("svg",y,i[6]||(i[6]=[s("g",{stroke:"currentColor",fill:"currentColor","stroke-width":"0",transform:"scale(1,-1)"},[s("g",{"data-mml-node":"math"},[s("g",{"data-mml-node":"mi"},[s("path",{"data-c":"1D45B",d:"M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z",style:{"stroke-width":"3"}})])])],-1)]))),i[7]||(i[7]=s("mjx-assistive-mml",{unselectable:"on",display:"inline",style:{top:"0px",left:"0px",clip:"rect(1px, 1px, 1px, 1px)","-webkit-touch-callout":"none","-webkit-user-select":"none","-khtml-user-select":"none","-moz-user-select":"none","-ms-user-select":"none","user-select":"none",position:"absolute",padding:"1px 0px 0px 0px",border:"0px",display:"block",width:"auto",overflow:"hidden"}},[s("math",{xmlns:"http://www.w3.org/1998/Math/MathML"},[s("mi",null,"n")])],-1))]),i[12]||(i[12]=a(" numbers ")),s("mjx-container",F,[(h(),n("svg",o,i[8]||(i[8]=[t('',1)]))),i[9]||(i[9]=s("mjx-assistive-mml",{unselectable:"on",display:"inline",style:{top:"0px",left:"0px",clip:"rect(1px, 1px, 1px, 1px)","-webkit-touch-callout":"none","-webkit-user-select":"none","-khtml-user-select":"none","-moz-user-select":"none","-ms-user-select":"none","user-select":"none",position:"absolute",padding:"1px 0px 0px 0px",border:"0px",display:"block",width:"auto",overflow:"hidden"}},[s("math",{xmlns:"http://www.w3.org/1998/Math/MathML"},[s("mo",{stretchy:"false"},"("),s("msub",null,[s("mi",null,"λ"),s("mn",null,"1")]),s("mo",null,","),s("msub",null,[s("mi",null,"λ"),s("mn",null,"2")]),s("mo",null,","),s("mo",null,"."),s("mo",null,"."),s("mo",null,"."),s("mo",null,","),s("msub",null,[s("mi",null,"λ"),s("mi",null,"n")]),s("mo",{stretchy:"false"},")")])],-1))]),i[13]||(i[13]=a(", each associated with a vertex of the polygon. Any point within the polygon can be expressed as a weighted average of the vertices, where the weights are the generalized barycentric coordinates."))]),i[15]||(i[15]=t(`

As with the triangle case, the weights sum to 1, and each is non-negative.

Example

This example was taken from this page of CGAL's documentation.

julia
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

Barycentric-coordinate API

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.

julia
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.

julia
"""
+    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.

julia
"""
+    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.

julia
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.

julia
"""
+    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.

julia
"""
+    _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
julia
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...

julia
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!

julia
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.

julia
    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

julia
        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,

julia
        interpolated_value += values[current_index] * wᵢ

then the accumulators for total weight and current index.

julia
        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(`

Barycentric coordinates

julia
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('',1)]))),i[1]||(i[1]=s("mjx-assistive-mml",{unselectable:"on",display:"inline",style:{top:"0px",left:"0px",clip:"rect(1px, 1px, 1px, 1px)","-webkit-touch-callout":"none","-webkit-user-select":"none","-khtml-user-select":"none","-moz-user-select":"none","-ms-user-select":"none","user-select":"none",position:"absolute",padding:"1px 0px 0px 0px",border:"0px",display:"block",width:"auto",overflow:"hidden"}},[s("math",{xmlns:"http://www.w3.org/1998/Math/MathML"},[s("mo",{stretchy:"false"},"("),s("msub",null,[s("mi",null,"λ"),s("mn",null,"1")]),s("mo",null,","),s("msub",null,[s("mi",null,"λ"),s("mn",null,"2")]),s("mo",null,","),s("msub",null,[s("mi",null,"λ"),s("mn",null,"3")]),s("mo",{stretchy:"false"},")")])],-1))]),i[3]||(i[3]=a(", each associated with a vertex of the triangle. Any point within the triangle can be expressed as a weighted average of the vertices, where the weights are the barycentric coordinates. The weights sum to 1, and each is non-negative."))]),s("p",null,[i[10]||(i[10]=a("For a polygon with ")),s("mjx-container",r,[(h(),n("svg",d,i[4]||(i[4]=[s("g",{stroke:"currentColor",fill:"currentColor","stroke-width":"0",transform:"scale(1,-1)"},[s("g",{"data-mml-node":"math"},[s("g",{"data-mml-node":"mi"},[s("path",{"data-c":"1D45B",d:"M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z",style:{"stroke-width":"3"}})])])],-1)]))),i[5]||(i[5]=s("mjx-assistive-mml",{unselectable:"on",display:"inline",style:{top:"0px",left:"0px",clip:"rect(1px, 1px, 1px, 1px)","-webkit-touch-callout":"none","-webkit-user-select":"none","-khtml-user-select":"none","-moz-user-select":"none","-ms-user-select":"none","user-select":"none",position:"absolute",padding:"1px 0px 0px 0px",border:"0px",display:"block",width:"auto",overflow:"hidden"}},[s("math",{xmlns:"http://www.w3.org/1998/Math/MathML"},[s("mi",null,"n")])],-1))]),i[11]||(i[11]=a(" vertices, generalized barycentric coordinates are a set of ")),s("mjx-container",g,[(h(),n("svg",y,i[6]||(i[6]=[s("g",{stroke:"currentColor",fill:"currentColor","stroke-width":"0",transform:"scale(1,-1)"},[s("g",{"data-mml-node":"math"},[s("g",{"data-mml-node":"mi"},[s("path",{"data-c":"1D45B",d:"M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z",style:{"stroke-width":"3"}})])])],-1)]))),i[7]||(i[7]=s("mjx-assistive-mml",{unselectable:"on",display:"inline",style:{top:"0px",left:"0px",clip:"rect(1px, 1px, 1px, 1px)","-webkit-touch-callout":"none","-webkit-user-select":"none","-khtml-user-select":"none","-moz-user-select":"none","-ms-user-select":"none","user-select":"none",position:"absolute",padding:"1px 0px 0px 0px",border:"0px",display:"block",width:"auto",overflow:"hidden"}},[s("math",{xmlns:"http://www.w3.org/1998/Math/MathML"},[s("mi",null,"n")])],-1))]),i[12]||(i[12]=a(" numbers ")),s("mjx-container",F,[(h(),n("svg",o,i[8]||(i[8]=[t('',1)]))),i[9]||(i[9]=s("mjx-assistive-mml",{unselectable:"on",display:"inline",style:{top:"0px",left:"0px",clip:"rect(1px, 1px, 1px, 1px)","-webkit-touch-callout":"none","-webkit-user-select":"none","-khtml-user-select":"none","-moz-user-select":"none","-ms-user-select":"none","user-select":"none",position:"absolute",padding:"1px 0px 0px 0px",border:"0px",display:"block",width:"auto",overflow:"hidden"}},[s("math",{xmlns:"http://www.w3.org/1998/Math/MathML"},[s("mo",{stretchy:"false"},"("),s("msub",null,[s("mi",null,"λ"),s("mn",null,"1")]),s("mo",null,","),s("msub",null,[s("mi",null,"λ"),s("mn",null,"2")]),s("mo",null,","),s("mo",null,"."),s("mo",null,"."),s("mo",null,"."),s("mo",null,","),s("msub",null,[s("mi",null,"λ"),s("mi",null,"n")]),s("mo",{stretchy:"false"},")")])],-1))]),i[13]||(i[13]=a(", each associated with a vertex of the polygon. Any point within the polygon can be expressed as a weighted average of the vertices, where the weights are the generalized barycentric coordinates."))]),i[15]||(i[15]=t(`

As with the triangle case, the weights sum to 1, and each is non-negative.

Example

This example was taken from this page of CGAL's documentation.

julia
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

Barycentric-coordinate API

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.

julia
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.

julia
"""
+    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.

julia
"""
+    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.

julia
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.

julia
"""
+    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.

julia
"""
+    _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
julia
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...

julia
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!

julia
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.

julia
    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

julia
        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,

julia
        interpolated_value += values[current_index] * wᵢ

then the accumulators for total weight and current index.

julia
        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(`

Buffer

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.

julia
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!

julia
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(`

Buffer

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.

julia
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!

julia
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(`

Centroid

julia
export centroid, centroid_and_length, centroid_and_area

What is the centroid?

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':

julia
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):

julia
cent = GO.centroid(cshape)
+scatter!(GI.x(cent), GI.y(cent), color = :red)
+f

Implementation

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.

julia
"""
+    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

julia
    xcentroid = T(0)
+    ycentroid = T(0)
+    length = T(0)
+    point₁ = GI.getpoint(geom, 1)

Loop over line segments of line string

julia
    for point₂ in GI.getpoint(geom)

Calculate length of line segment

julia
        length_component = sqrt(
+            (GI.x(point₂) - GI.x(point₁))^2 +
+            (GI.y(point₂) - GI.y(point₁))^2
+        )

Accumulate the line segment length into length

julia
        length += length_component

Weighted average of line segment centroids

julia
        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

julia
        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

julia
    @assert(
+        GI.getpoint(geom, 1) == GI.getpoint(geom, GI.ngeom(geom)),
+        "centroid_and_area should only be used with closed geometries"
+    )

Initialize starting values

julia
    xcentroid = T(0)
+    ycentroid = T(0)
+    area = T(0)
+    point₁ = GI.getpoint(geom, 1)

Loop over line segments of linear ring

julia
    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

julia
        area += area_component

Weighted average of centroid components

julia
        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

julia
        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

julia
    (xcentroid, ycentroid), area = centroid_and_area(GI.getexterior(geom), T)

Weight exterior centroid by area

julia
    xcentroid *= area
+    ycentroid *= area

Loop over any holes within the polygon

julia
    for hole in GI.gethole(geom)

Hole polygon's centroid and area

julia
        (xinterior, yinterior), interior_area = centroid_and_area(hole, T)

Accumulate the area component into area

julia
        area -= interior_area

Weighted average of centroid components

julia
        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.

julia
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(`

Centroid

julia
export centroid, centroid_and_length, centroid_and_area

What is the centroid?

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':

julia
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):

julia
cent = GO.centroid(cshape)
+scatter!(GI.x(cent), GI.y(cent), color = :red)
+f

Implementation

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.

julia
"""
+    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

julia
    xcentroid = T(0)
+    ycentroid = T(0)
+    length = T(0)
+    point₁ = GI.getpoint(geom, 1)

Loop over line segments of line string

julia
    for point₂ in GI.getpoint(geom)

Calculate length of line segment

julia
        length_component = sqrt(
+            (GI.x(point₂) - GI.x(point₁))^2 +
+            (GI.y(point₂) - GI.y(point₁))^2
+        )

Accumulate the line segment length into length

julia
        length += length_component

Weighted average of line segment centroids

julia
        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

julia
        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

julia
    @assert(
+        GI.getpoint(geom, 1) == GI.getpoint(geom, GI.ngeom(geom)),
+        "centroid_and_area should only be used with closed geometries"
+    )

Initialize starting values

julia
    xcentroid = T(0)
+    ycentroid = T(0)
+    area = T(0)
+    point₁ = GI.getpoint(geom, 1)

Loop over line segments of linear ring

julia
    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

julia
        area += area_component

Weighted average of centroid components

julia
        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

julia
        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

julia
    (xcentroid, ycentroid), area = centroid_and_area(GI.getexterior(geom), T)

Weight exterior centroid by area

julia
    xcentroid *= area
+    ycentroid *= area

Loop over any holes within the polygon

julia
    for hole in GI.gethole(geom)

Hole polygon's centroid and area

julia
        (xinterior, yinterior), interior_area = centroid_and_area(hole, T)

Accumulate the area component into area

julia
        area -= interior_area

Weighted average of centroid components

julia
        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.

julia
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(`

Polygon clipping helpers

This file contains the shared helper functions for the polygon clipping functionalities.

This enum defines which side of an edge a point is on

julia
@enum PointEdgeSide left=1 right=2 unknown=3

Constants assigned for readability

julia
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

julia
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.

julia
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

julia
    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

julia
    _classify_crossing!(T, a_list, b_list; exact)

Flag the entry and exits

julia
    _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

julia
    _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.

julia
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

julia
    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

julia
        new_point = PolyNode{T}(;point = a_pt1)
+        a_count += 1
+        push!(a_list, new_point)

Find intersections with edges of poly_b

julia
        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

julia
            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

julia
                    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

julia
                    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

julia
                    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

julia
        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.

julia
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

julia
    sort!(a_idx_list, by = x-> a_list[x].neighbor + a_list[x].fracs[2])

Initialize needed values and lists

julia
    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

julia
    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

julia
                    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.

julia
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

julia
    a_prev = a_list[end - 1]
+    curr_pt = a_list[end]
+    i = napts

keep track of unmatched bouncing chains

julia
    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

julia
    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

julia
            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

julia
            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

julia
            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

julia
            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

julia
                    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

julia
                    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

julia
            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

julia
    if unmatched_end_chain_edge != unknown
+        crossing = unmatched_end_chain_edge != start_chain_edge

update end of chain with endpoint and crossing / bouncing tags

julia
        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

julia
        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

julia
_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

julia
    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

julia
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

julia
_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.

julia
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

julia
    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

julia
    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

julia
                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.

julia
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

julia
    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.

julia
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

julia
    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

julia
        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

julia
        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

julia
            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

julia
                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

julia
                curr = curr_list[idx]
+                push!(pt_list, curr.point)
+                if (curr.crossing || curr.endpoint != not_endpoint)

Keep track of processed intersection points

julia
                    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

julia
            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

julia
_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.

julia
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

julia
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

julia
    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)

julia
            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

julia
                        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

julia
                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

julia
    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.

julia
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

julia
    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

julia
            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

julia
    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

julia
    @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

julia
            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

julia
                    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

julia
            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

julia
            deleteat!(ring.geom, remove_idx)

Check if enough points are left to form a polygon

julia
            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(`

Polygon clipping helpers

This file contains the shared helper functions for the polygon clipping functionalities.

This enum defines which side of an edge a point is on

julia
@enum PointEdgeSide left=1 right=2 unknown=3

Constants assigned for readability

julia
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

julia
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.

julia
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

julia
    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

julia
    _classify_crossing!(T, a_list, b_list; exact)

Flag the entry and exits

julia
    _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

julia
    _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.

julia
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

julia
    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

julia
        new_point = PolyNode{T}(;point = a_pt1)
+        a_count += 1
+        push!(a_list, new_point)

Find intersections with edges of poly_b

julia
        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

julia
            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

julia
                    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

julia
                    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

julia
                    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

julia
        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.

julia
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

julia
    sort!(a_idx_list, by = x-> a_list[x].neighbor + a_list[x].fracs[2])

Initialize needed values and lists

julia
    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

julia
    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

julia
                    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.

julia
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

julia
    a_prev = a_list[end - 1]
+    curr_pt = a_list[end]
+    i = napts

keep track of unmatched bouncing chains

julia
    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

julia
    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

julia
            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

julia
            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

julia
            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

julia
            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

julia
                    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

julia
                    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

julia
            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

julia
    if unmatched_end_chain_edge != unknown
+        crossing = unmatched_end_chain_edge != start_chain_edge

update end of chain with endpoint and crossing / bouncing tags

julia
        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

julia
        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

julia
_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

julia
    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

julia
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

julia
_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.

julia
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

julia
    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

julia
    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

julia
                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.

julia
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

julia
    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.

julia
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

julia
    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

julia
        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

julia
        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

julia
            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

julia
                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

julia
                curr = curr_list[idx]
+                push!(pt_list, curr.point)
+                if (curr.crossing || curr.endpoint != not_endpoint)

Keep track of processed intersection points

julia
                    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

julia
            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

julia
_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.

julia
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

julia
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

julia
    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)

julia
            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

julia
                        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

julia
                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

julia
    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.

julia
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

julia
    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

julia
            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

julia
    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

julia
    @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

julia
            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

julia
                    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

julia
            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

julia
            deleteat!(ring.geom, remove_idx)

Check if enough points are left to form a polygon

julia
            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(`
julia
export coverage

What is 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:

julia
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.

julia
GO.coverage(rect, xmin, xmax, ymin, ymax)
1.0

Implementation

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

julia
const _COVERAGE_TARGETS = TraitTarget{Union{GI.PolygonTrait,GI.AbstractCurveTrait,GI.MultiPointTrait,GI.PointTrait}}()

Wall types for coverage

julia
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

julia
_coverage(::Type{T}, ::GI.AbstractGeometryTrait, geom, xmin, xmax, ymin, ymax; kwargs...) where T = zero(T)

Polygons

julia
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

julia
    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

julia
    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

julia
    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

julia
        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

julia
        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

julia
        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

julia
        (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

julia
        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

julia
    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

julia
    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

julia
_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.

julia
_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

julia
        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

julia
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

julia
        connect_area += _partial_edge_out_area(p1, xmin, xmax, ymin, ymax, wall1)

Any intermediate walls (full length)

julia
        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

julia
        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

julia
_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(`
julia
export coverage

What is 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:

julia
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.

julia
GO.coverage(rect, xmin, xmax, ymin, ymax)
1.0

Implementation

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

julia
const _COVERAGE_TARGETS = TraitTarget{Union{GI.PolygonTrait,GI.AbstractCurveTrait,GI.MultiPointTrait,GI.PointTrait}}()

Wall types for coverage

julia
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

julia
_coverage(::Type{T}, ::GI.AbstractGeometryTrait, geom, xmin, xmax, ymin, ymax; kwargs...) where T = zero(T)

Polygons

julia
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

julia
    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

julia
    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

julia
    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

julia
        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

julia
        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

julia
        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

julia
        (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

julia
        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

julia
    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

julia
    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

julia
_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.

julia
_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

julia
        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

julia
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

julia
        connect_area += _partial_edge_out_area(p1, xmin, xmax, ymin, ymax, wall1)

Any intermediate walls (full length)

julia
        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

julia
        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

julia
_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(`

Polygon cutting

julia
export cut

What is 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:

julia
import GeoInterface as GI, GeometryOps as GO
+using CairoMakie
+using Makie
+
+poly = GI.Polygon([[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0), (0.0, 0.0)]])
+line = GI.Line([(5.0, -5.0), (5.0, 15.0)])
+cut_polys = GO.cut(poly, line)
+
+f, a, p1 = Makie.poly(collect(GI.getpoint(cut_polys[1])); color = (:blue, 0.5))
+Makie.poly!(collect(GI.getpoint(cut_polys[2])); color = (:orange, 0.5))
+Makie.lines!(GI.getpoint(line); color = :black)
+f

Implementation

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.

julia
"""
+    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

julia
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

julia
    if n_intr_pts < 2 || isodd(n_intr_pts)
+        return [tuples(poly)]
+    end

Cut polygon by line

julia
    cut_coords = _cut(T, ext_poly, line, poly_list, intr_list, n_intr_pts; exact)

Close coords and create polygons

julia
    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

julia
    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

julia
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

julia
    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

julia
    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

julia
    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

julia
            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

julia
            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(`

Polygon cutting

julia
export cut

What is 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:

julia
import GeoInterface as GI, GeometryOps as GO
+using CairoMakie
+using Makie
+
+poly = GI.Polygon([[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0), (0.0, 0.0)]])
+line = GI.Line([(5.0, -5.0), (5.0, 15.0)])
+cut_polys = GO.cut(poly, line)
+
+f, a, p1 = Makie.poly(collect(GI.getpoint(cut_polys[1])); color = (:blue, 0.5))
+Makie.poly!(collect(GI.getpoint(cut_polys[2])); color = (:orange, 0.5))
+Makie.lines!(GI.getpoint(line); color = :black)
+f

Implementation

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.

julia
"""
+    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

julia
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

julia
    if n_intr_pts < 2 || isodd(n_intr_pts)
+        return [tuples(poly)]
+    end

Cut polygon by line

julia
    cut_coords = _cut(T, ext_poly, line, poly_list, intr_list, n_intr_pts; exact)

Close coords and create polygons

julia
    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

julia
    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

julia
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

julia
    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

julia
    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

julia
    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

julia
            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

julia
            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(`

Difference Polygon Clipping

julia
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

julia
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

julia
    ext_a = GI.getexterior(poly_a)
+    ext_b = GI.getexterior(poly_b)

Find the difference of the exterior of the polygons

julia
    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

julia
    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!

julia
        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.

julia
    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

julia
    _remove_collinear_points!(polys, remove_idx, poly_a, poly_b)
+    return polys
+end

Helper functions for Differences with Greiner and Hormann Polygon Clipping

julia
#= 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

julia
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(`

Difference Polygon Clipping

julia
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

julia
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

julia
    ext_a = GI.getexterior(poly_a)
+    ext_b = GI.getexterior(poly_b)

Find the difference of the exterior of the polygons

julia
    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

julia
    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!

julia
        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.

julia
    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

julia
    _remove_collinear_points!(polys, remove_idx, poly_a, poly_b)
+    return polys
+end

Helper functions for Differences with Greiner and Hormann Polygon Clipping

julia
#= 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

julia
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(`

Geometry Intersection

julia
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

julia
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

julia
_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'

julia
    ext_a = GI.getexterior(poly_a)
+    ext_b = GI.getexterior(poly_b)

Then we find the intersection of the exteriors

julia
    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.

julia
    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

julia
    _remove_collinear_points!(polys, remove_idx, poly_a, poly_b)
+    return polys
+end

Helper functions for Intersections with Greiner and Hormann Polygon Clipping

julia
#= 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

julia
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

julia
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

julia
    result = Tuple{T, T}[]

Check if the geometries extents even overlap

julia
    Extents.intersects(GI.extent(a), GI.extent(b)) || return result

Create a list of edges from the two input geometries

julia
    edges_a, edges_b = map(sort!  to_edges, (a, b))

Loop over pairs of edges and add any unique intersection points to results

julia
    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

julia
    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

julia
    (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

julia
    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

julia
    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)

julia
    if a1_orient == a2_orient == b1_orient == b2_orient == 0

Intersection is collinear if all endpoints lie on the same line

julia
        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

julia
        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

julia
        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

julia
    line_orient, intr1, intr2 = no_intr_result

Determine collinear line overlaps

julia
    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

julia
    a_dist, b_dist = distance(a1, a2, T), distance(b1, b2, T)

Set collinear intersection points if they exist

julia
    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

julia
    (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

julia
    (b1x, b1y), (b2x, b2y) = _tuple_point(b1, T), _tuple_point(b2, T)
+    Δbx, Δby = b2x - b1x, b2y - b1y

Differences between starting points

julia
    Δbax = b1x - a1x
+    Δbay = b1y - a1y
+    a_cross_b = Δax * Δby - Δay * Δbx

Determine α value where 0 < α < 1 and β value where 0 < β < 1

julia
    α = _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

julia
    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

julia
function _nearest_endpoint(::Type{T}, a1, a2, b1, b2) where T

Create lines from segments and calculate segment length

julia
    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

julia
    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

julia
    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

julia
    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

julia
    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

julia
    return _tuple_point(min_pt, T), α, β
+end

Return value of x/y clamped between ϵ and 1 - ϵ

julia
_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(`

Geometry Intersection

julia
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

julia
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

julia
_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'

julia
    ext_a = GI.getexterior(poly_a)
+    ext_b = GI.getexterior(poly_b)

Then we find the intersection of the exteriors

julia
    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.

julia
    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

julia
    _remove_collinear_points!(polys, remove_idx, poly_a, poly_b)
+    return polys
+end

Helper functions for Intersections with Greiner and Hormann Polygon Clipping

julia
#= 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

julia
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

julia
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

julia
    result = Tuple{T, T}[]

Check if the geometries extents even overlap

julia
    Extents.intersects(GI.extent(a), GI.extent(b)) || return result

Create a list of edges from the two input geometries

julia
    edges_a, edges_b = map(sort!  to_edges, (a, b))

Loop over pairs of edges and add any unique intersection points to results

julia
    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

julia
    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

julia
    (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

julia
    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

julia
    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)

julia
    if a1_orient == a2_orient == b1_orient == b2_orient == 0

Intersection is collinear if all endpoints lie on the same line

julia
        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

julia
        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

julia
        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

julia
    line_orient, intr1, intr2 = no_intr_result

Determine collinear line overlaps

julia
    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

julia
    a_dist, b_dist = distance(a1, a2, T), distance(b1, b2, T)

Set collinear intersection points if they exist

julia
    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

julia
    (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

julia
    (b1x, b1y), (b2x, b2y) = _tuple_point(b1, T), _tuple_point(b2, T)
+    Δbx, Δby = b2x - b1x, b2y - b1y

Differences between starting points

julia
    Δbax = b1x - a1x
+    Δbay = b1y - a1y
+    a_cross_b = Δax * Δby - Δay * Δbx

Determine α value where 0 < α < 1 and β value where 0 < β < 1

julia
    α = _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

julia
    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

julia
function _nearest_endpoint(::Type{T}, a1, a2, b1, b2) where T

Create lines from segments and calculate segment length

julia
    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

julia
    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

julia
    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

julia
    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

julia
    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

julia
    return _tuple_point(min_pt, T), α, β
+end

Return value of x/y clamped between ϵ and 1 - ϵ

julia
_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(`
julia
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.

julia
    _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.

julia
    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.

julia
    @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.

julia
    function _cross(::_False, a, b)
+        c_t1 = GI.x(a) * GI.y(b)
+        c_t2 = GI.y(a) * GI.x(b)
+        c_val = if isapprox(c_t1, c_t2)
+            0
+        else
+            sign(c_t1 - c_t2)
+        end
+        return c_val
+    end
+
+end
+
+import .Predicates

If we want to inject adaptivity, we would do something like:

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(`
julia
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.

julia
    _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.

julia
    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.

julia
    @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.

julia
    function _cross(::_False, a, b)
+        c_t1 = GI.x(a) * GI.y(b)
+        c_t2 = GI.y(a) * GI.x(b)
+        c_val = if isapprox(c_t1, c_t2)
+            0
+        else
+            sign(c_t1 - c_t2)
+        end
+        return c_val
+    end
+
+end
+
+import .Predicates

If we want to inject adaptivity, we would do something like:

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(`

Union Polygon Clipping

julia
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

julia
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

julia
    ext_a = GI.getexterior(poly_a)
+    ext_b = GI.getexterior(poly_b)

Then, I get the union of the exteriors

julia
    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

julia
    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

julia
        @views append!(polys[1].geom, (GI.getexterior(p) for p in polys[2:end]))
+        keepat!(polys, 1)
+    end

Add in holes

julia
    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

julia
    _remove_collinear_points!(polys, [false], poly_a, poly_b)
+    return polys
+end

Helper functions for Unions with Greiner and Hormann Polygon Clipping

julia
#= 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

julia
        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

julia
            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

julia
            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

julia
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(`

Union Polygon Clipping

julia
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

julia
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

julia
    ext_a = GI.getexterior(poly_a)
+    ext_b = GI.getexterior(poly_b)

Then, I get the union of the exteriors

julia
    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

julia
    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

julia
        @views append!(polys[1].geom, (GI.getexterior(p) for p in polys[2:end]))
+        keepat!(polys, 1)
+    end

Add in holes

julia
    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

julia
    _remove_collinear_points!(polys, [false], poly_a, poly_b)
+    return polys
+end

Helper functions for Unions with Greiner and Hormann Polygon Clipping

julia
#= 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

julia
        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

julia
            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

julia
            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

julia
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(`

Convex hull

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.

Example

Simple hull

julia
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

Convex hull of the USA

julia
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

Investigating the winding order

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

julia
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

Implementation

julia
"""
+    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.

julia
convex_hull(geometries) = convex_hull(MonotoneChainMethod(), geometries)

TODO: have this respect the CRS by pulling it out of geometries.

julia
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.

julia
    points = collect(flatten(tuples, GI.PointTrait, geometries))

Compute the convex hull using DelTri (shorthand for DelaunayTriangulation.jl).

julia
    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.

julia
    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(`

Convex hull

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.

Example

Simple hull

julia
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

Convex hull of the USA

julia
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

Investigating the winding order

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

julia
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

Implementation

julia
"""
+    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.

julia
convex_hull(geometries) = convex_hull(MonotoneChainMethod(), geometries)

TODO: have this respect the CRS by pulling it out of geometries.

julia
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.

julia
    points = collect(flatten(tuples, GI.PointTrait, geometries))

Compute the convex hull using DelTri (shorthand for DelaunayTriangulation.jl).

julia
    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.

julia
    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(`

Distance and signed distance

julia
export distance, signed_distance

What is distance? What is 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:

julia
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.

julia
(
+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:

julia
xrange = yrange = LinRange(-0.5, 1.5, 300)
+f, a, p = heatmap(xrange, yrange, GO.signed_distance.(Point2f.(xrange, yrange'), Ref(rect)); colormap = :RdBu, colorrange = (-0.75, 0.75))
+a.aspect = DataAspect(); Colorbar(f[1, 2], p, label = "Signed distance"); lines!(a, GI.convert(GO.GeometryBasics, rect)); f

Implementation

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.

julia
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

julia
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

julia
_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

julia
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

julia
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)

julia
function _signed_distance(
+    ::Type{T}, ptrait::GI.PointTrait, point, gtrait::GI.AbstractGeometryTrait, geom
+) where T
+    _distance(T, ptrait, point, gtrait, geom)
+end

Point-Polygon

julia
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

julia
end

Returns the Euclidean distance between two points.

julia
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

julia
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.

julia
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.

julia
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.

julia
_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.

julia
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.

julia
function _distance_curve(::Type{T}, point, curve; close_curve = false) where T

see if linear ring has explicitly repeated last point in coordinates

julia
    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

julia
    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.

julia
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(`

Distance and signed distance

julia
export distance, signed_distance

What is distance? What is 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:

julia
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.

julia
(
+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:

julia
xrange = yrange = LinRange(-0.5, 1.5, 300)
+f, a, p = heatmap(xrange, yrange, GO.signed_distance.(Point2f.(xrange, yrange'), Ref(rect)); colormap = :RdBu, colorrange = (-0.75, 0.75))
+a.aspect = DataAspect(); Colorbar(f[1, 2], p, label = "Signed distance"); lines!(a, GI.convert(GO.GeometryBasics, rect)); f

Implementation

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.

julia
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

julia
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

julia
_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

julia
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

julia
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)

julia
function _signed_distance(
+    ::Type{T}, ptrait::GI.PointTrait, point, gtrait::GI.AbstractGeometryTrait, geom
+) where T
+    _distance(T, ptrait, point, gtrait, geom)
+end

Point-Polygon

julia
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

julia
end

Returns the Euclidean distance between two points.

julia
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

julia
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.

julia
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.

julia
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.

julia
_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.

julia
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.

julia
function _distance_curve(::Type{T}, point, curve; close_curve = false) where T

see if linear ring has explicitly repeated last point in coordinates

julia
    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

julia
    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.

julia
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(`

Equals

julia
export equals

What is 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:

julia
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:

julia
GO.equals(l1, l2)  # returns false
false

Implementation

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.

julia
"""
+    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

julia
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

julia
    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

julia
    n1 -= c1_repeat_point ? 1 : 0
+    n2 -= c2_repeat_point ? 1 : 0
+    n1 == n2 || return false
+    n1 == 0 && return true

Find offset between curves

julia
    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

julia
    isnothing(jstart) && return false

found match for only point

julia
    n1 == 1 && return true

if isn't closed and first or last point don't match, not same curve

julia
    !closed_type1 && (jstart != 1 && jstart != n1) && return false

Check if curves are going in same direction

julia
    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

julia
    n1 == 2 && return same_direction

Check all remaining points are the same wrapping around line

julia
    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

julia
    _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

julia
    GI.nhole(geom_a) == GI.nhole(geom_b) || return false

Check if holes are equal

julia
    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

julia
    GI.npolygon(geom_a) == GI.npolygon(geom_b) || return false

Check if each polygon has a matching polygon

julia
    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(`

Equals

julia
export equals

What is 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:

julia
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:

julia
GO.equals(l1, l2)  # returns false
false

Implementation

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.

julia
"""
+    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

julia
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

julia
    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

julia
    n1 -= c1_repeat_point ? 1 : 0
+    n2 -= c2_repeat_point ? 1 : 0
+    n1 == n2 || return false
+    n1 == 0 && return true

Find offset between curves

julia
    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

julia
    isnothing(jstart) && return false

found match for only point

julia
    n1 == 1 && return true

if isn't closed and first or last point don't match, not same curve

julia
    !closed_type1 && (jstart != 1 && jstart != n1) && return false

Check if curves are going in same direction

julia
    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

julia
    n1 == 2 && return same_direction

Check all remaining points are the same wrapping around line

julia
    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

julia
    _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

julia
    GI.nhole(geom_a) == GI.nhole(geom_b) || return false

Check if holes are equal

julia
    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

julia
    GI.npolygon(geom_a) == GI.npolygon(geom_b) || return false

Check if each polygon has a matching polygon

julia
    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(`

Contains

julia
export contains

What is 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:

julia
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.

julia
GO.contains(l1, l2)  # returns true
+GO.contains(l2, l1)  # returns false
false

Implementation

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.

julia
"""
+    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

julia
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(`

Contains

julia
export contains

What is 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:

julia
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.

julia
GO.contains(l1, l2)  # returns true
+GO.contains(l2, l1)  # returns false
false

Implementation

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.

julia
"""
+    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

julia
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(`

CoveredBy

julia
export coveredby

What is 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:

julia
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.

julia
GO.coveredby(p1, l1)  # true
true

Implementation

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.

julia
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

julia
true
+\`\`\`
+"""
+coveredby(g1, g2) = _coveredby(trait(g1), g1, trait(g2), g2)

Convert features to geometries

julia
_coveredby(::GI.FeatureTrait, g1, ::Any, g2) = coveredby(GI.geometry(g1), g2)
+_coveredby(::Any, g1, t2::GI.FeatureTrait, g2) = coveredby(g1, GI.geometry(g2))
+_coveredby(::FeatureTrait, g1, ::FeatureTrait, g2) = coveredby(GI.geometry(g1), GI.geometry(g2))

Points coveredby geometries

Point is coveredby another point if those points are equal

julia
_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

julia
_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

julia
_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

julia
_coveredby(
+    ::GI.PointTrait, g1,
+    ::GI.PolygonTrait, g2,
+) = _point_polygon_process(
+    g1, g2;
+    COVEREDBY_ALLOWS...,
+    COVEREDBY_EXACT...,
+)

Points cannot cover any geometry other than points

julia
_coveredby(
+    ::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    ::GI.PointTrait, g2,
+) = false

Lines coveredby geometries

julia
#= 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,
+)

Rings covered by geometries

julia
#= 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,
+)

Polygons covered by geometries

julia
#= 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

julia
_coveredby(
+    ::GI.PolygonTrait, g1,
+    ::GI.AbstractCurveTrait, g2,
+) = false

Geometries coveredby multi-geometry/geometry collections

julia
#= Geometry is covered by a multi-geometry or a collection if one of the elements
+of the collection cover the geometry. =#
+function _coveredby(
+    ::Union{GI.PointTrait, GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    ::Union{
+        GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+        GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+    }, g2,
+)
+    for sub_g2 in GI.getgeom(g2)
+        coveredby(g1, sub_g2) && return true
+    end
+    return false
+end

Multi-geometry/geometry collections coveredby geometries

julia
#= 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(`

CoveredBy

julia
export coveredby

What is 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:

julia
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.

julia
GO.coveredby(p1, l1)  # true
true

Implementation

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.

julia
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

julia
true
+\`\`\`
+"""
+coveredby(g1, g2) = _coveredby(trait(g1), g1, trait(g2), g2)

Convert features to geometries

julia
_coveredby(::GI.FeatureTrait, g1, ::Any, g2) = coveredby(GI.geometry(g1), g2)
+_coveredby(::Any, g1, t2::GI.FeatureTrait, g2) = coveredby(g1, GI.geometry(g2))
+_coveredby(::FeatureTrait, g1, ::FeatureTrait, g2) = coveredby(GI.geometry(g1), GI.geometry(g2))

Points coveredby geometries

Point is coveredby another point if those points are equal

julia
_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

julia
_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

julia
_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

julia
_coveredby(
+    ::GI.PointTrait, g1,
+    ::GI.PolygonTrait, g2,
+) = _point_polygon_process(
+    g1, g2;
+    COVEREDBY_ALLOWS...,
+    COVEREDBY_EXACT...,
+)

Points cannot cover any geometry other than points

julia
_coveredby(
+    ::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    ::GI.PointTrait, g2,
+) = false

Lines coveredby geometries

julia
#= 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,
+)

Rings covered by geometries

julia
#= 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,
+)

Polygons covered by geometries

julia
#= 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

julia
_coveredby(
+    ::GI.PolygonTrait, g1,
+    ::GI.AbstractCurveTrait, g2,
+) = false

Geometries coveredby multi-geometry/geometry collections

julia
#= Geometry is covered by a multi-geometry or a collection if one of the elements
+of the collection cover the geometry. =#
+function _coveredby(
+    ::Union{GI.PointTrait, GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    ::Union{
+        GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+        GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+    }, g2,
+)
+    for sub_g2 in GI.getgeom(g2)
+        coveredby(g1, sub_g2) && return true
+    end
+    return false
+end

Multi-geometry/geometry collections coveredby geometries

julia
#= 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(`

Covers

julia
export covers

What is 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:

julia
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

julia
GO.covers(l1, p1)  # returns true
+GO.covers(p1, l1)  # returns false
false

Implementation

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.

julia
"""
+    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

julia
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(`

Covers

julia
export covers

What is 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:

julia
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

julia
GO.covers(l1, p1)  # returns true
+GO.covers(p1, l1)  # returns false
false

Implementation

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.

julia
"""
+    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

julia
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(`

Crossing checks

julia
"""
+     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

julia
\`\`\`
+"""
+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

julia
    cross = dxc * dy1 - dyc * dx1
+    cross != 0 && return false

Will constprop optimise these away?

julia
    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(`

Crossing checks

julia
"""
+     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

julia
\`\`\`
+"""
+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

julia
    cross = dxc * dy1 - dyc * dx1
+    cross != 0 && return false

Will constprop optimise these away?

julia
    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(`

Disjoint

julia
export disjoint

What is 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:

julia
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.

julia
GO.disjoint(l1, l2)  # returns true
true

Implementation

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.

julia
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

julia
true
+\`\`\`
+"""
+disjoint(g1, g2) = _disjoint(trait(g1), g1, trait(g2), g2)

Convert features to geometries

julia
_disjoint(::FeatureTrait, g1, ::Any, g2) = disjoint(GI.geometry(g1), g2)
+_disjoint(::Any, g1, ::FeatureTrait, g2) = disjoint(g1, geometry(g2))
+_disjoint(::FeatureTrait, g1, ::FeatureTrait, g2) = disjoint(GI.geometry(g1), GI.geometry(g2))

Point disjoint geometries

Point is disjoint from another point if the points are not equal.

julia
_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.

julia
_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.

julia
_disjoint(
+    ::GI.PointTrait, g1,
+    ::GI.LinearRingTrait, g2,
+) = _point_curve_process(
+    g1, g2;
+    DISJOINT_ALLOWS...,
+    closed_curve = true,
+)
+
+#= Point is disjoint from a polygon if it is not on any edges, vertices, or
+within the polygon's interior. =#
+_disjoint(
+    ::GI.PointTrait, g1,
+    ::GI.PolygonTrait, g2,
+) = _point_polygon_process(
+    g1, g2;
+    DISJOINT_ALLOWS...,
+    DISJOINT_EXACT...,
+)
+
+#= Geometry is disjoint from a point if the point is not in the interior or on
+the boundary of the geometry. =#
+_disjoint(
+    trait1::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    trait2::GI.PointTrait, g2,
+) = _disjoint(trait2, g2, trait1, g1)

Lines disjoint geometries

julia
#= 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)

Rings disjoint geometries

julia
#= Linearrings is disjoint from another linearring if they do not share any
+interior edge/vertex points or boundary points.=#
+_disjoint(
+    ::GI.LinearRingTrait, g1,
+    ::GI.LinearRingTrait, g2,
+) = _line_curve_process(
+    g1, g2;
+    DISJOINT_CURVE_ALLOWS...,
+    DISJOINT_REQUIRES...,
+    DISJOINT_EXACT...,
+    closed_line = true,
+    closed_curve = true,
+)
+
+#= Linearring is disjoint from a polygon if the interior and boundary points of
+the ring are not in the polygon's interior or on the polygon's boundary. =#
+_disjoint(
+    ::GI.LinearRingTrait, g1,
+    ::GI.PolygonTrait, g2,
+) = _line_polygon_process(
+    g1, g2;
+    DISJOINT_ALLOWS...,
+    DISJOINT_REQUIRES...,
+    DISJOINT_EXACT...,
+    closed_line = true,
+)

Polygon disjoint geometries

julia
#= Polygon is disjoint from another polygon if they do not share any edges or
+vertices and if their interiors do not intersect, excluding any holes. =#
+_disjoint(
+    ::GI.PolygonTrait, g1,
+    ::GI.PolygonTrait, g2,
+) = _polygon_polygon_process(
+    g1, g2;
+    DISJOINT_ALLOWS...,
+    DISJOINT_REQUIRES...,
+    DISJOINT_EXACT...,
+)

Geometries disjoint multi-geometry/geometry collections

julia
#= Geometry is disjoint from a multi-geometry or a collection if all of the
+elements of the collection are disjoint from the geometry. =#
+function _disjoint(
+    ::Union{GI.PointTrait, GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    ::Union{
+        GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+        GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+    }, g2,
+)
+    for sub_g2 in GI.getgeom(g2)
+        !disjoint(g1, sub_g2) && return false
+    end
+    return true
+end

Multi-geometry/geometry collections coveredby geometries

julia
#= 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(`

Disjoint

julia
export disjoint

What is 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:

julia
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.

julia
GO.disjoint(l1, l2)  # returns true
true

Implementation

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.

julia
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

julia
true
+\`\`\`
+"""
+disjoint(g1, g2) = _disjoint(trait(g1), g1, trait(g2), g2)

Convert features to geometries

julia
_disjoint(::FeatureTrait, g1, ::Any, g2) = disjoint(GI.geometry(g1), g2)
+_disjoint(::Any, g1, ::FeatureTrait, g2) = disjoint(g1, geometry(g2))
+_disjoint(::FeatureTrait, g1, ::FeatureTrait, g2) = disjoint(GI.geometry(g1), GI.geometry(g2))

Point disjoint geometries

Point is disjoint from another point if the points are not equal.

julia
_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.

julia
_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.

julia
_disjoint(
+    ::GI.PointTrait, g1,
+    ::GI.LinearRingTrait, g2,
+) = _point_curve_process(
+    g1, g2;
+    DISJOINT_ALLOWS...,
+    closed_curve = true,
+)
+
+#= Point is disjoint from a polygon if it is not on any edges, vertices, or
+within the polygon's interior. =#
+_disjoint(
+    ::GI.PointTrait, g1,
+    ::GI.PolygonTrait, g2,
+) = _point_polygon_process(
+    g1, g2;
+    DISJOINT_ALLOWS...,
+    DISJOINT_EXACT...,
+)
+
+#= Geometry is disjoint from a point if the point is not in the interior or on
+the boundary of the geometry. =#
+_disjoint(
+    trait1::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    trait2::GI.PointTrait, g2,
+) = _disjoint(trait2, g2, trait1, g1)

Lines disjoint geometries

julia
#= 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)

Rings disjoint geometries

julia
#= Linearrings is disjoint from another linearring if they do not share any
+interior edge/vertex points or boundary points.=#
+_disjoint(
+    ::GI.LinearRingTrait, g1,
+    ::GI.LinearRingTrait, g2,
+) = _line_curve_process(
+    g1, g2;
+    DISJOINT_CURVE_ALLOWS...,
+    DISJOINT_REQUIRES...,
+    DISJOINT_EXACT...,
+    closed_line = true,
+    closed_curve = true,
+)
+
+#= Linearring is disjoint from a polygon if the interior and boundary points of
+the ring are not in the polygon's interior or on the polygon's boundary. =#
+_disjoint(
+    ::GI.LinearRingTrait, g1,
+    ::GI.PolygonTrait, g2,
+) = _line_polygon_process(
+    g1, g2;
+    DISJOINT_ALLOWS...,
+    DISJOINT_REQUIRES...,
+    DISJOINT_EXACT...,
+    closed_line = true,
+)

Polygon disjoint geometries

julia
#= Polygon is disjoint from another polygon if they do not share any edges or
+vertices and if their interiors do not intersect, excluding any holes. =#
+_disjoint(
+    ::GI.PolygonTrait, g1,
+    ::GI.PolygonTrait, g2,
+) = _polygon_polygon_process(
+    g1, g2;
+    DISJOINT_ALLOWS...,
+    DISJOINT_REQUIRES...,
+    DISJOINT_EXACT...,
+)

Geometries disjoint multi-geometry/geometry collections

julia
#= Geometry is disjoint from a multi-geometry or a collection if all of the
+elements of the collection are disjoint from the geometry. =#
+function _disjoint(
+    ::Union{GI.PointTrait, GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    ::Union{
+        GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+        GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+    }, g2,
+)
+    for sub_g2 in GI.getgeom(g2)
+        !disjoint(g1, sub_g2) && return false
+    end
+    return true
+end

Multi-geometry/geometry collections coveredby geometries

julia
#= 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(`

Line-curve interaction

julia
#= 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.

julia
function _point_curve_process(
+    point, curve;
+    in_allow, on_allow, out_allow,
+    closed_curve = false,
+)

Determine if curve is closed

julia
    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

julia
    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.

julia
function _point_polygon_process(
+    point, polygon;
+    in_allow, on_allow, out_allow, exact,
+)

Check interaction of geom with polygon's exterior boundary

julia
    ext_val = _point_filled_curve_orientation(point, GI.getexterior(polygon); exact)

If a point is outside, it isn't interacting with any holes

julia
    ext_val == point_out && return out_allow

if a point is on an external boundary, it isn't interacting with any holes

julia
    ext_val == point_on && return on_allow

If geom is within the polygon, need to check interactions with holes

julia
    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

julia
        hole_val == point_in && return out_allow

If a point in on a hole edge, it is on the edge of the polygon

julia
        hole_val == point_on && return on_allow
+    end

Point is within external boundary and on in/on any holes

julia
    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.

julia
@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

julia
    in_req_met = !in_require
+    on_req_met = !on_require
+    out_req_met = !out_require

Determine curve endpoints

julia
    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

julia
    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

julia
        for j in (closed_curve ? 1 : 2):nc
+            c_end = _tuple_point(GI.getpoint(curve, j))

Check if line and curve segments meet

julia
            seg_val, intr1, _ = _intersection_point(Float64, (l_start, l_end), (c_start, c_end); exact)

If segments are co-linear

julia
            if seg_val == line_over
+                !over_allow && return false

at least one point in, meets requirements

julia
                in_req_met = true
+                point_val = _point_segment_orientation(l_start, c_start, c_end)

If entire segment isn't covered, consider remaining section

julia
                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

julia
                    (_, (α, β)) = 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

julia
                        if (!cross_allow || !over_allow) && α != 0 && β != 0

Find next pieces of hinge to see if line and curve cross

julia
                            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

julia
                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.

julia
@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

julia
    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

julia
    !in_curve && return in_req_met && on_req_met && out_req_met

Loop over polygon holes

julia
    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.

julia
@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

julia
    ext1 = GI.getexterior(poly1)
+    ext2 = GI.getexterior(poly2)

Check if exterior of poly1 is in polygon 2

julia
    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

julia
        _, _, 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

julia
    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

julia
            _, _, e2_out_h1 = _line_filled_curve_interactions(
+                ext2, h1;
+                exact, closed_line = true,
+            )

hole encompasses all of poly2

julia
            !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

julia
    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.

julia
function _point_segment_orientation(
+    point, start, stop;
+    in::T = point_in, on::T = point_on, out::T = point_out,
+) where {T}

Parse out points

julia
    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

julia
        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.

julia
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.

julia
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

julia
    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

julia
    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

julia
    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

julia
        in_curve && on_curve && out_curve && break

Check next segment of line against curve

julia
        for j in 1:nc
+            c_end = _tuple_point(GI.getpoint(curve, j))

Check if two line and curve segments meet

julia
            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

julia
                on_curve = true
+                if seg_val == line_cross

When crossing boundary, line is both in and out of curve

julia
                    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

julia
                        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.

julia
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

julia
    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)

julia
@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

julia
        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

julia
        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(`

Line-curve interaction

julia
#= 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.

julia
function _point_curve_process(
+    point, curve;
+    in_allow, on_allow, out_allow,
+    closed_curve = false,
+)

Determine if curve is closed

julia
    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

julia
    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.

julia
function _point_polygon_process(
+    point, polygon;
+    in_allow, on_allow, out_allow, exact,
+)

Check interaction of geom with polygon's exterior boundary

julia
    ext_val = _point_filled_curve_orientation(point, GI.getexterior(polygon); exact)

If a point is outside, it isn't interacting with any holes

julia
    ext_val == point_out && return out_allow

if a point is on an external boundary, it isn't interacting with any holes

julia
    ext_val == point_on && return on_allow

If geom is within the polygon, need to check interactions with holes

julia
    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

julia
        hole_val == point_in && return out_allow

If a point in on a hole edge, it is on the edge of the polygon

julia
        hole_val == point_on && return on_allow
+    end

Point is within external boundary and on in/on any holes

julia
    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.

julia
@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

julia
    in_req_met = !in_require
+    on_req_met = !on_require
+    out_req_met = !out_require

Determine curve endpoints

julia
    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

julia
    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

julia
        for j in (closed_curve ? 1 : 2):nc
+            c_end = _tuple_point(GI.getpoint(curve, j))

Check if line and curve segments meet

julia
            seg_val, intr1, _ = _intersection_point(Float64, (l_start, l_end), (c_start, c_end); exact)

If segments are co-linear

julia
            if seg_val == line_over
+                !over_allow && return false

at least one point in, meets requirements

julia
                in_req_met = true
+                point_val = _point_segment_orientation(l_start, c_start, c_end)

If entire segment isn't covered, consider remaining section

julia
                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

julia
                    (_, (α, β)) = 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

julia
                        if (!cross_allow || !over_allow) && α != 0 && β != 0

Find next pieces of hinge to see if line and curve cross

julia
                            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

julia
                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.

julia
@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

julia
    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

julia
    !in_curve && return in_req_met && on_req_met && out_req_met

Loop over polygon holes

julia
    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.

julia
@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

julia
    ext1 = GI.getexterior(poly1)
+    ext2 = GI.getexterior(poly2)

Check if exterior of poly1 is in polygon 2

julia
    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

julia
        _, _, 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

julia
    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

julia
            _, _, e2_out_h1 = _line_filled_curve_interactions(
+                ext2, h1;
+                exact, closed_line = true,
+            )

hole encompasses all of poly2

julia
            !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

julia
    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.

julia
function _point_segment_orientation(
+    point, start, stop;
+    in::T = point_in, on::T = point_on, out::T = point_out,
+) where {T}

Parse out points

julia
    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

julia
        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.

julia
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.

julia
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

julia
    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

julia
    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

julia
    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

julia
        in_curve && on_curve && out_curve && break

Check next segment of line against curve

julia
        for j in 1:nc
+            c_end = _tuple_point(GI.getpoint(curve, j))

Check if two line and curve segments meet

julia
            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

julia
                on_curve = true
+                if seg_val == line_cross

When crossing boundary, line is both in and out of curve

julia
                    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

julia
                        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.

julia
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

julia
    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)

julia
@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

julia
        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

julia
        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(`

Intersection checks

julia
export intersects

What is 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:

julia
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.

julia
GO.intersects(line1, line2)  # true
true

Implementation

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.

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
+
+\`\`\`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

julia
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(`

Intersection checks

julia
export intersects

What is 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:

julia
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.

julia
GO.intersects(line1, line2)  # true
true

Implementation

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.

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
+
+\`\`\`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

julia
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(`

Overlaps

julia
export overlaps

What is 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:

julia
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:

julia
GO.overlaps(l1, l2)  # true
true

Implementation

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.

julia
"""
+    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

julia
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

julia
    seg_val, _, _ = _intersection_point(Float64, (a1, a2), (b1, b2); exact)

one end point is outside of other segment

julia
    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

julia
function _point_on_seg(point, start, stop)

Parse out points

julia
    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

julia
    cross = (x - x1) * Δyl - (y - y1) * Δxl
+    if cross == 0  # point is on line extending to infinity

is line between endpoints

julia
        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

julia
    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.

julia
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(`

Overlaps

julia
export overlaps

What is 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:

julia
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:

julia
GO.overlaps(l1, l2)  # true
true

Implementation

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.

julia
"""
+    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

julia
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

julia
    seg_val, _, _ = _intersection_point(Float64, (a1, a2), (b1, b2); exact)

one end point is outside of other segment

julia
    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

julia
function _point_on_seg(point, start, stop)

Parse out points

julia
    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

julia
    cross = (x - x1) * Δyl - (y - y1) * Δxl
+    if cross == 0  # point is on line extending to infinity

is line between endpoints

julia
        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

julia
    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.

julia
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(`

Touches

julia
export touches

What is 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:

julia
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.

julia
GO.touches(l1, l2)  # true
true

Implementation

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.

julia
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

julia
true
+\`\`\`
+"""
+touches(g1, g2)::Bool = _touches(trait(g1), g1, trait(g2), g2)

Convert features to geometries

julia
_touches(::GI.FeatureTrait, g1, ::Any, g2) = touches(GI.geometry(g1), g2)
+_touches(::Any, g1, t2::GI.FeatureTrait, g2) = touches(g1, GI.geometry(g2))
+_touches(::FeatureTrait, g1, ::FeatureTrait, g2) = touches(GI.geometry(g1), GI.geometry(g2))

Point touches geometries

Point cannot touch another point as if they are equal, interiors interact

julia
_touches(
+    ::GI.PointTrait, g1,
+    ::GI.PointTrait, g2,
+) = false

Point touches a linestring if it equal to the first of last point of the line

julia
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

julia
_touches(
+    ::GI.PointTrait, g1,
+    ::GI.LinearRingTrait, g2,
+) = false

Point touches a polygon if it is on the boundary of that polygon

julia
_touches(
+    ::GI.PointTrait, g1,
+    ::GI.PolygonTrait, g2,
+) = _point_polygon_process(
+    g1, g2;
+    TOUCHES_POINT_ALLOWED...,
+    TOUCHES_EXACT...,
+)
+
+#= Geometry touches a point if the point is on the geometry boundary. =#
+_touches(
+    trait1::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    trait2::GI.PointTrait, g2,
+) = _touches(trait2, g2, trait1, g1)

Lines touching geometries

julia
#= 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,
+)

Rings touch geometries

julia
#= 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,
+)

Polygons touch geometries

julia
#= 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...,
+)

Geometries touch multi-geometry/geometry collections

julia
#= Geometry touch a multi-geometry or a collection if the geometry touches at
+least one of the elements of the collection. =#
+function _touches(
+    ::Union{GI.PointTrait, GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    ::Union{
+        GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+        GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+    }, g2,
+)
+    for sub_g2 in GI.getgeom(g2)
+        !touches(g1, sub_g2) && return false
+    end
+    return true
+end

Multi-geometry/geometry collections cross geometries

julia
#= 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(`

Touches

julia
export touches

What is 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:

julia
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.

julia
GO.touches(l1, l2)  # true
true

Implementation

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.

julia
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

julia
true
+\`\`\`
+"""
+touches(g1, g2)::Bool = _touches(trait(g1), g1, trait(g2), g2)

Convert features to geometries

julia
_touches(::GI.FeatureTrait, g1, ::Any, g2) = touches(GI.geometry(g1), g2)
+_touches(::Any, g1, t2::GI.FeatureTrait, g2) = touches(g1, GI.geometry(g2))
+_touches(::FeatureTrait, g1, ::FeatureTrait, g2) = touches(GI.geometry(g1), GI.geometry(g2))

Point touches geometries

Point cannot touch another point as if they are equal, interiors interact

julia
_touches(
+    ::GI.PointTrait, g1,
+    ::GI.PointTrait, g2,
+) = false

Point touches a linestring if it equal to the first of last point of the line

julia
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

julia
_touches(
+    ::GI.PointTrait, g1,
+    ::GI.LinearRingTrait, g2,
+) = false

Point touches a polygon if it is on the boundary of that polygon

julia
_touches(
+    ::GI.PointTrait, g1,
+    ::GI.PolygonTrait, g2,
+) = _point_polygon_process(
+    g1, g2;
+    TOUCHES_POINT_ALLOWED...,
+    TOUCHES_EXACT...,
+)
+
+#= Geometry touches a point if the point is on the geometry boundary. =#
+_touches(
+    trait1::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    trait2::GI.PointTrait, g2,
+) = _touches(trait2, g2, trait1, g1)

Lines touching geometries

julia
#= 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,
+)

Rings touch geometries

julia
#= 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,
+)

Polygons touch geometries

julia
#= 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...,
+)

Geometries touch multi-geometry/geometry collections

julia
#= Geometry touch a multi-geometry or a collection if the geometry touches at
+least one of the elements of the collection. =#
+function _touches(
+    ::Union{GI.PointTrait, GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    ::Union{
+        GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+        GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+    }, g2,
+)
+    for sub_g2 in GI.getgeom(g2)
+        !touches(g1, sub_g2) && return false
+    end
+    return true
+end

Multi-geometry/geometry collections cross geometries

julia
#= 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(`

Within

julia
export within

What is 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:

julia
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

julia
GO.within(l1, l2)  # false
+GO.within(l2, l1)  # true
true

Implementation

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.

julia
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

julia
true
+\`\`\`
+"""
+within(g1, g2) = _within(trait(g1), g1, trait(g2), g2)

Convert features to geometries

julia
_within(::GI.FeatureTrait, g1, ::Any, g2) = within(GI.geometry(g1), g2)
+_within(::Any, g1, t2::GI.FeatureTrait, g2) = within(g1, GI.geometry(g2))
+_within(::FeatureTrait, g1, ::FeatureTrait, g2) = within(GI.geometry(g1), GI.geometry(g2))

Points within geometries

Point is within another point if those points are equal.

julia
_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.

julia
_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

julia
_within(
+    ::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    ::GI.PointTrait, g2,
+) = false

Lines within geometries

julia
#= Linestring is within another linestring if their interiors intersect and no
+points of the first line are in the exterior of the second line. =#
+_within(
+    ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+    ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _line_curve_process(
+    g1, g2;
+    WITHIN_CURVE_ALLOWS...,
+    WITHIN_REQUIRES...,
+    WITHIN_EXACT...,
+    closed_line = false,
+    closed_curve = false,
+)
+
+#= Linestring is within a linear ring if their interiors intersect and no points
+of the line are in the exterior of the ring. =#
+_within(
+    ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+    ::GI.LinearRingTrait, g2,
+) = _line_curve_process(
+    g1, g2;
+    WITHIN_CURVE_ALLOWS...,
+    WITHIN_REQUIRES...,
+    WITHIN_EXACT...,
+    closed_line = false,
+    closed_curve = true,
+)
+
+#= Linestring is within a polygon if their interiors intersect and no points of
+the line are in the exterior of the polygon, although they can be on an edge. =#
+_within(
+    ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+    ::GI.PolygonTrait, g2,
+) = _line_polygon_process(
+    g1, g2;
+    WITHIN_POLYGON_ALLOWS...,
+    WITHIN_REQUIRES...,
+    WITHIN_EXACT...,
+    closed_line = false,
+)

Rings covered by geometries

julia
#= Linearring is within a linestring if their interiors intersect and no points
+of the ring are in the exterior of the line. =#
+_within(
+    ::GI.LinearRingTrait, g1,
+    ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _line_curve_process(
+    g1, g2;
+    WITHIN_CURVE_ALLOWS...,
+    WITHIN_REQUIRES...,
+    WITHIN_EXACT...,
+    closed_line = true,
+    closed_curve = false,
+)
+
+#= Linearring is within another linearring if their interiors intersect and no
+points of the first ring are in the exterior of the second ring. =#
+_within(
+    ::GI.LinearRingTrait, g1,
+    ::GI.LinearRingTrait, g2,
+) = _line_curve_process(
+    g1, g2;
+    WITHIN_CURVE_ALLOWS...,
+    WITHIN_REQUIRES...,
+    WITHIN_EXACT...,
+    closed_line = true,
+    closed_curve = true,
+)
+
+#= Linearring is within a polygon if their interiors intersect and no points of
+the ring are in the exterior of the polygon, although they can be on an edge. =#
+_within(
+    ::GI.LinearRingTrait, g1,
+    ::GI.PolygonTrait, g2,
+) = _line_polygon_process(
+    g1, g2;
+    WITHIN_POLYGON_ALLOWS...,
+    WITHIN_REQUIRES...,
+    WITHIN_EXACT...,
+    closed_line = true,
+)

Polygons within geometries

julia
#= 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

julia
_within(
+    ::GI.PolygonTrait, g1,
+    ::GI.AbstractCurveTrait, g2,
+) = false

Geometries within multi-geometry/geometry collections

julia
#= Geometry is within a multi-geometry or a collection if the geometry is within
+at least one of the collection elements. =#
+function _within(
+    ::Union{GI.PointTrait, GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    ::Union{
+        GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+        GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+    }, g2,
+)
+    for sub_g2 in GI.getgeom(g2)
+        within(g1, sub_g2) && return true
+    end
+    return false
+end

Multi-geometry/geometry collections within geometries

julia
#= 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(`

Within

julia
export within

What is 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:

julia
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

julia
GO.within(l1, l2)  # false
+GO.within(l2, l1)  # true
true

Implementation

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.

julia
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

julia
true
+\`\`\`
+"""
+within(g1, g2) = _within(trait(g1), g1, trait(g2), g2)

Convert features to geometries

julia
_within(::GI.FeatureTrait, g1, ::Any, g2) = within(GI.geometry(g1), g2)
+_within(::Any, g1, t2::GI.FeatureTrait, g2) = within(g1, GI.geometry(g2))
+_within(::FeatureTrait, g1, ::FeatureTrait, g2) = within(GI.geometry(g1), GI.geometry(g2))

Points within geometries

Point is within another point if those points are equal.

julia
_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.

julia
_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

julia
_within(
+    ::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    ::GI.PointTrait, g2,
+) = false

Lines within geometries

julia
#= Linestring is within another linestring if their interiors intersect and no
+points of the first line are in the exterior of the second line. =#
+_within(
+    ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+    ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _line_curve_process(
+    g1, g2;
+    WITHIN_CURVE_ALLOWS...,
+    WITHIN_REQUIRES...,
+    WITHIN_EXACT...,
+    closed_line = false,
+    closed_curve = false,
+)
+
+#= Linestring is within a linear ring if their interiors intersect and no points
+of the line are in the exterior of the ring. =#
+_within(
+    ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+    ::GI.LinearRingTrait, g2,
+) = _line_curve_process(
+    g1, g2;
+    WITHIN_CURVE_ALLOWS...,
+    WITHIN_REQUIRES...,
+    WITHIN_EXACT...,
+    closed_line = false,
+    closed_curve = true,
+)
+
+#= Linestring is within a polygon if their interiors intersect and no points of
+the line are in the exterior of the polygon, although they can be on an edge. =#
+_within(
+    ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+    ::GI.PolygonTrait, g2,
+) = _line_polygon_process(
+    g1, g2;
+    WITHIN_POLYGON_ALLOWS...,
+    WITHIN_REQUIRES...,
+    WITHIN_EXACT...,
+    closed_line = false,
+)

Rings covered by geometries

julia
#= Linearring is within a linestring if their interiors intersect and no points
+of the ring are in the exterior of the line. =#
+_within(
+    ::GI.LinearRingTrait, g1,
+    ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _line_curve_process(
+    g1, g2;
+    WITHIN_CURVE_ALLOWS...,
+    WITHIN_REQUIRES...,
+    WITHIN_EXACT...,
+    closed_line = true,
+    closed_curve = false,
+)
+
+#= Linearring is within another linearring if their interiors intersect and no
+points of the first ring are in the exterior of the second ring. =#
+_within(
+    ::GI.LinearRingTrait, g1,
+    ::GI.LinearRingTrait, g2,
+) = _line_curve_process(
+    g1, g2;
+    WITHIN_CURVE_ALLOWS...,
+    WITHIN_REQUIRES...,
+    WITHIN_EXACT...,
+    closed_line = true,
+    closed_curve = true,
+)
+
+#= Linearring is within a polygon if their interiors intersect and no points of
+the ring are in the exterior of the polygon, although they can be on an edge. =#
+_within(
+    ::GI.LinearRingTrait, g1,
+    ::GI.PolygonTrait, g2,
+) = _line_polygon_process(
+    g1, g2;
+    WITHIN_POLYGON_ALLOWS...,
+    WITHIN_REQUIRES...,
+    WITHIN_EXACT...,
+    closed_line = true,
+)

Polygons within geometries

julia
#= 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

julia
_within(
+    ::GI.PolygonTrait, g1,
+    ::GI.AbstractCurveTrait, g2,
+) = false

Geometries within multi-geometry/geometry collections

julia
#= Geometry is within a multi-geometry or a collection if the geometry is within
+at least one of the collection elements. =#
+function _within(
+    ::Union{GI.PointTrait, GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    ::Union{
+        GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+        GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+    }, g2,
+)
+    for sub_g2 in GI.getgeom(g2)
+        within(g1, sub_g2) && return true
+    end
+    return false
+end

Multi-geometry/geometry collections within geometries

julia
#= 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(`

Orientation

julia
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!

julia
"""
+    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

julia
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

julia
        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

julia
false
+\`\`\`
+"""
+function isconcave(poly)::Bool
+    sign = false
+
+    exterior = GI.getexterior(poly)

FIXME handle not closed polygons

julia
    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.

julia
"""
+    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:

julia
_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(`

Orientation

julia
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!

julia
"""
+    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

julia
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

julia
        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

julia
false
+\`\`\`
+"""
+function isconcave(poly)::Bool
+    sign = false
+
+    exterior = GI.getexterior(poly)

FIXME handle not closed polygons

julia
    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.

julia
"""
+    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:

julia
_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(`

Polygonizing raster data

julia
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

julia
- \`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
\`\`\`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

julia
    xhalf = step(xs) / 2
+    yhalf = step(ys) / 2

Make bounds ranges first to avoid floating point error making gaps or overlaps

julia
    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)

julia
    crs = GI.crs(A)

Define buffers for edges and rings

julia
    rings = Vector{T}[]
+
+    strait = true
+    turning = false

Get edges from the array A

julia
    edges = _pixel_edges(f, xs, ys, A)

Keep dict keys separately in a vector for performance

julia
    edgekeys = collect(keys(edges))

We don't delete keys we just reduce length with nkeys

julia
    nkeys = length(edgekeys)

Now create rings from the edges, looping until there are no edge keys left

julia
    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.

julia
        while nkeys > 0

Take the first node from the array

julia
            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

julia
        found == false && break

Check if there are one or two lines going through this node and take one of them, then update the status

julia
        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

julia
        currentnode = firstnode
+        ring = [currentnode, nextnode]
+        push!(rings, ring)

Loop until we close a the ring and break

julia
        while true

Find a node that matches the next node

julia
            (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.

julia
                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

julia
                edges[nextnode] = (remainingnode, map(typemax, remainingnode))
+            else

Here we simply choose the first (and only valid) node

julia
                selectednode = c1

Replace the edge nodes with empty nodes, they will be skipped later

julia
                edges[nextnode] = (map(typemax, c1), map(typemax, c1))

Check if we are on a straight line

julia
                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

julia
            currentnode, nextnode = nextnode, selectednode

Update the current node or add a new node to the ring

julia
            if straightline

replace the last node we don't need it

julia
                ring[end] = nextnode
+            else

add a new node, we have turned a corner

julia
                push!(ring, nextnode)
+            end

If the ring is closed, break the loop and start a new one

julia
            nextnode == firstnode && break
+        end
+    end

Define wrapped LinearRings, with embedded extents so we only calculate them once

julia
    linearrings = map(rings) do ring
+        extent = GI.extent(GI.LinearRing(ring))
+        GI.LinearRing(ring; extent, crs)
+    end

Separate exteriors from holes by winding direction

julia
    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

julia
    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

julia
                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...

julia
        @warn "No polgons found, check your data or try another function for \`f\`"
+        return nothing
+    else

Otherwise return a wrapped MultiPolygon

julia
        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

julia
    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

julia
    values = map(identity, sort!(Base.union(Iterators.map(f, A))))

We ignore pure Bool

julia
    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

julia
    existingnodes = get!(() -> newnodes, dict, key)

If we actually fetched an existing node, update it

julia
    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

julia
    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

julia
                x1, x2 = xs[i]

We check the Von Neumann neighborhood to decide what edges are needed, if any.

julia
                (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(`

Polygonizing raster data

julia
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

julia
- \`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
\`\`\`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

julia
    xhalf = step(xs) / 2
+    yhalf = step(ys) / 2

Make bounds ranges first to avoid floating point error making gaps or overlaps

julia
    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)

julia
    crs = GI.crs(A)

Define buffers for edges and rings

julia
    rings = Vector{T}[]
+
+    strait = true
+    turning = false

Get edges from the array A

julia
    edges = _pixel_edges(f, xs, ys, A)

Keep dict keys separately in a vector for performance

julia
    edgekeys = collect(keys(edges))

We don't delete keys we just reduce length with nkeys

julia
    nkeys = length(edgekeys)

Now create rings from the edges, looping until there are no edge keys left

julia
    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.

julia
        while nkeys > 0

Take the first node from the array

julia
            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

julia
        found == false && break

Check if there are one or two lines going through this node and take one of them, then update the status

julia
        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

julia
        currentnode = firstnode
+        ring = [currentnode, nextnode]
+        push!(rings, ring)

Loop until we close a the ring and break

julia
        while true

Find a node that matches the next node

julia
            (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.

julia
                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

julia
                edges[nextnode] = (remainingnode, map(typemax, remainingnode))
+            else

Here we simply choose the first (and only valid) node

julia
                selectednode = c1

Replace the edge nodes with empty nodes, they will be skipped later

julia
                edges[nextnode] = (map(typemax, c1), map(typemax, c1))

Check if we are on a straight line

julia
                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

julia
            currentnode, nextnode = nextnode, selectednode

Update the current node or add a new node to the ring

julia
            if straightline

replace the last node we don't need it

julia
                ring[end] = nextnode
+            else

add a new node, we have turned a corner

julia
                push!(ring, nextnode)
+            end

If the ring is closed, break the loop and start a new one

julia
            nextnode == firstnode && break
+        end
+    end

Define wrapped LinearRings, with embedded extents so we only calculate them once

julia
    linearrings = map(rings) do ring
+        extent = GI.extent(GI.LinearRing(ring))
+        GI.LinearRing(ring; extent, crs)
+    end

Separate exteriors from holes by winding direction

julia
    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

julia
    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

julia
                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...

julia
        @warn "No polgons found, check your data or try another function for \`f\`"
+        return nothing
+    else

Otherwise return a wrapped MultiPolygon

julia
        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

julia
    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

julia
    values = map(identity, sort!(Base.union(Iterators.map(f, A))))

We ignore pure Bool

julia
    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

julia
    existingnodes = get!(() -> newnodes, dict, key)

If we actually fetched an existing node, update it

julia
    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

julia
    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

julia
                x1, x2 = xs[i]

We check the Von Neumann neighborhood to decide what edges are needed, if any.

julia
                (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(`

Not implemented yet

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.

julia
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(`

Not implemented yet

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.

julia
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(`
julia
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.

julia
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(`
julia
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.

julia
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

julia
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:

julia
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.

Docstrings

Functions

',11)),i("details",d,[i("summary",null,[s[0]||(s[0]=i("a",{id:"GeometryOpsCore.apply-source-src-apply",href:"#GeometryOpsCore.apply-source-src-apply"},[i("span",{class:"jlbinding"},"GeometryOpsCore.apply")],-1)),s[1]||(s[1]=t()),n(e,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[2]||(s[2]=a(`
julia
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:

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

source

`,10))]),i("details",g,[i("summary",null,[s[3]||(s[3]=i("a",{id:"GeometryOpsCore.applyreduce-source-src-apply",href:"#GeometryOpsCore.applyreduce-source-src-apply"},[i("span",{class:"jlbinding"},"GeometryOpsCore.applyreduce")],-1)),s[4]||(s[4]=t()),n(e,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[5]||(s[5]=a('
julia
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.

source

',5))]),s[7]||(s[7]=a(`

What is apply?

apply applies some function to every geometry matching the Target GeoInterface trait, in some arbitrarily nested object made up of:

  • AbstractArrays (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.

Embedding:

extent and crs can be embedded in all geometries, features, and feature collections as part of apply. Geometries deeper than Target will of course not have new extent or crs embedded.

  • calc_extent signals to recalculate an Extent and embed it.

  • crs will be embedded as-is

Threading

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.

julia
"""
+    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

julia
@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

julia
@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

julia
    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.

julia
@inline function _apply(f::F, target, ::Nothing, iterable::IterableType; threaded, kw...) where {F, IterableType}

Try the Tables.jl interface first

julia
    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

julia
            _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.

julia
    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,

julia
    old_schema = Tables.schema(iterable)

filter the geometry column out,

julia
    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.

julia
    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.

julia
    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.

julia
        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!

julia
                style == :default && continue

We assume that any other style is preserved.

julia
                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!

julia
        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.

julia
        if !("GEOINTERFACE:geometrycolumns" in mdk)

If the geometry columns are not already set, we need to set them.

julia
            DataAPI.metadata!(result, "GEOINTERFACE:geometrycolumns", (geometry_column,); style = :default)
+        end

Force reset CRS always, since you can pass crs to apply.

julia
        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

julia
@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

julia
    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

julia
        extent = mapreduce(GI.extent, Extents.union, features)

Return a FeatureCollection with features, crs and calculated extent

julia
        return GI.FeatureCollection(features; crs, extent)
+    else

Return a FeatureCollection with features and crs

julia
        return GI.FeatureCollection(features; crs)
+    end
+end

Rewrap all FeatureTrait features as GI.Feature, keeping the properties

julia
@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

julia
    geometry = _apply(f, target, GI.geometry(feature); crs, calc_extent, threaded)

Get the feature properties

julia
    properties = GI.properties(feature)
+    if calc_extent isa _True

Calculate the extent of the geometry

julia
        extent = GI.extent(geometry)

Return a new Feature with the new geometry and calculated extent, but the original properties and crs

julia
        return GI.Feature(geometry; properties, crs, extent)
+    else

Return a new Feature with the new geometry, but the original properties and crs

julia
        return GI.Feature(geometry; properties, crs)
+    end
+end

Reconstruct nested geometries, maybe using threads to call _apply on component geoms

julia
@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

julia
    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

julia
    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

julia
    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

julia
    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

julia
    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.

julia
_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

julia
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

julia
@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

julia
    tasks_per_thread = 2
+    chunk_size = max(1, ntasks ÷ (tasks_per_thread * nthreads()))

partition the range into chunks

julia
    task_chunks = Iterators.partition(taskrange, chunk_size)

Map over the chunks

julia
    tasks = map(task_chunks) do chunk

Spawn a task to process this chunk

julia
        @spawn begin

Where we map f over the chunk indices

julia
            map(f, chunk)
+        end
+    end

Finally we join the results into a new vector

julia
    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!

julia
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

julia
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:

julia
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.

Docstrings

Functions

',11)),i("details",d,[i("summary",null,[s[0]||(s[0]=i("a",{id:"GeometryOpsCore.apply-source-src-apply",href:"#GeometryOpsCore.apply-source-src-apply"},[i("span",{class:"jlbinding"},"GeometryOpsCore.apply")],-1)),s[1]||(s[1]=t()),n(e,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[2]||(s[2]=a(`
julia
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:

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

source

`,10))]),i("details",g,[i("summary",null,[s[3]||(s[3]=i("a",{id:"GeometryOpsCore.applyreduce-source-src-apply",href:"#GeometryOpsCore.applyreduce-source-src-apply"},[i("span",{class:"jlbinding"},"GeometryOpsCore.applyreduce")],-1)),s[4]||(s[4]=t()),n(e,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[5]||(s[5]=a('
julia
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.

source

',5))]),s[7]||(s[7]=a(`

What is apply?

apply applies some function to every geometry matching the Target GeoInterface trait, in some arbitrarily nested object made up of:

  • AbstractArrays (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.

Embedding:

extent and crs can be embedded in all geometries, features, and feature collections as part of apply. Geometries deeper than Target will of course not have new extent or crs embedded.

  • calc_extent signals to recalculate an Extent and embed it.

  • crs will be embedded as-is

Threading

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.

julia
"""
+    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

julia
@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

julia
@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

julia
    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.

julia
@inline function _apply(f::F, target, ::Nothing, iterable::IterableType; threaded, kw...) where {F, IterableType}

Try the Tables.jl interface first

julia
    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

julia
            _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.

julia
    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,

julia
    old_schema = Tables.schema(iterable)

filter the geometry column out,

julia
    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.

julia
    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.

julia
    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.

julia
        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!

julia
                style == :default && continue

We assume that any other style is preserved.

julia
                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!

julia
        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.

julia
        if !("GEOINTERFACE:geometrycolumns" in mdk)

If the geometry columns are not already set, we need to set them.

julia
            DataAPI.metadata!(result, "GEOINTERFACE:geometrycolumns", (geometry_column,); style = :default)
+        end

Force reset CRS always, since you can pass crs to apply.

julia
        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

julia
@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

julia
    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

julia
        extent = mapreduce(GI.extent, Extents.union, features)

Return a FeatureCollection with features, crs and calculated extent

julia
        return GI.FeatureCollection(features; crs, extent)
+    else

Return a FeatureCollection with features and crs

julia
        return GI.FeatureCollection(features; crs)
+    end
+end

Rewrap all FeatureTrait features as GI.Feature, keeping the properties

julia
@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

julia
    geometry = _apply(f, target, GI.geometry(feature); crs, calc_extent, threaded)

Get the feature properties

julia
    properties = GI.properties(feature)
+    if calc_extent isa _True

Calculate the extent of the geometry

julia
        extent = GI.extent(geometry)

Return a new Feature with the new geometry and calculated extent, but the original properties and crs

julia
        return GI.Feature(geometry; properties, crs, extent)
+    else

Return a new Feature with the new geometry, but the original properties and crs

julia
        return GI.Feature(geometry; properties, crs)
+    end
+end

Reconstruct nested geometries, maybe using threads to call _apply on component geoms

julia
@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

julia
    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

julia
    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

julia
    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

julia
    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

julia
    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.

julia
_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

julia
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

julia
@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

julia
    tasks_per_thread = 2
+    chunk_size = max(1, ntasks ÷ (tasks_per_thread * nthreads()))

partition the range into chunks

julia
    task_chunks = Iterators.partition(taskrange, chunk_size)

Map over the chunks

julia
    tasks = map(task_chunks) do chunk

Spawn a task to process this chunk

julia
        @spawn begin

Where we map f over the chunk indices

julia
            map(f, chunk)
+        end
+    end

Finally we join the results into a new vector

julia
    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!

julia
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

julia
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.

julia
"""
+    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

julia
@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

julia
@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

julia
            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.

julia
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.

julia
    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.Features.

julia
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.

julia
    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

julia
@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

julia
@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

julia
@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

julia
@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

julia
@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

julia
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.

julia
@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

julia
    tasks_per_thread = 2
+    chunk_size = max(1, ntasks ÷ (tasks_per_thread * nthreads()))

partition the range into chunks

julia
    task_chunks = Iterators.partition(taskrange, chunk_size)

Map over the chunks

julia
    tasks = map(task_chunks) do chunk

Spawn a task to process this chunk

julia
        @spawn begin

Where we map f over the chunk indices

julia
            mapreduce(f, op, chunk; init)
+        end
+    end

Finally we join the results into a new vector

julia
    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

julia
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.

julia
"""
+    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

julia
@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

julia
@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

julia
            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.

julia
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.

julia
    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.Features.

julia
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.

julia
    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

julia
@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

julia
@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

julia
@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

julia
@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

julia
@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

julia
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.

julia
@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

julia
    tasks_per_thread = 2
+    chunk_size = max(1, ntasks ÷ (tasks_per_thread * nthreads()))

partition the range into chunks

julia
    task_chunks = Iterators.partition(taskrange, chunk_size)

Map over the chunks

julia
    tasks = map(task_chunks) do chunk

Spawn a task to process this chunk

julia
        @spawn begin

Where we map f over the chunk indices

julia
            mapreduce(f, op, chunk; init)
+        end
+    end

Finally we join the results into a new vector

julia
    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(`
julia
_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(`
julia
_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('

Keyword docs

This file defines common keyword documentation, that can be spliced into docstrings.

julia
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('

Keyword docs

This file defines common keyword documentation, that can be spliced into docstrings.

julia
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(`
julia
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.

source

`,4))]),i("details",d,[i("summary",null,[s[3]||(s[3]=i("a",{id:"GeometryOpsCore.flatten-source-src-other_primitives",href:"#GeometryOpsCore.flatten-source-src-other_primitives"},[i("span",{class:"jlbinding"},"GeometryOpsCore.flatten")],-1)),s[4]||(s[4]=a()),h(n,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[5]||(s[5]=t(`
julia
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.

source

`,4))]),i("details",g,[i("summary",null,[s[6]||(s[6]=i("a",{id:"GeometryOpsCore.reconstruct-source-src-other_primitives",href:"#GeometryOpsCore.reconstruct-source-src-other_primitives"},[i("span",{class:"jlbinding"},"GeometryOpsCore.reconstruct")],-1)),s[7]||(s[7]=a()),h(n,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[8]||(s[8]=t('
julia
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.

source

',5))]),i("details",y,[i("summary",null,[s[9]||(s[9]=i("a",{id:"GeometryOpsCore.rebuild-source-src-other_primitives",href:"#GeometryOpsCore.rebuild-source-src-other_primitives"},[i("span",{class:"jlbinding"},"GeometryOpsCore.rebuild")],-1)),s[10]||(s[10]=a()),h(n,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[11]||(s[11]=t('
julia
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)

source

',5))]),s[14]||(s[14]=t(`
julia
"""
+    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

julia
unwrap(f, target::Type, geom) = unwrap(f, target, GI.trait(geom), geom)

Try to unwrap over iterables

julia
unwrap(f, target::Type, ::Nothing, iterable) =
+    map(x -> unwrap(f, target, x), iterable)

Rewrap feature collections

julia
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

julia
unwrap(f, ::Type{Target}, ::Trait, geom) where {Target,Trait<:Target} = f(geom)

Fail if we hit PointTrait

julia
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

julia
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

julia
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

julia
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

julia
_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

julia
_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

julia
_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

julia
function _reconstruct(::Type{Target}, ::Nothing, iterable, components, iter) where Target
+    vect = map(iterable) do x

iter is updated by _reconstruct here

julia
        obj, iter = _reconstruct(Target, x, components, iter)
+        obj
+    end
+    return vect, iter
+end

Reconstruct feature collections

julia
function _reconstruct(::Type{Target}, ::GI.FeatureCollectionTrait, fc, components, iter) where Target
+    features = map(GI.getfeature(fc)) do feature

iter is updated by _reconstruct here

julia
        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

julia
        subgeom1, iter = _reconstruct(Target, GI.trait(subgeom), subgeom, components, iter)
+        subgeom1
+    end
+    return rebuild(geom, geoms), iter
+end

Apply f to the target geometry

julia
_reconstruct(::Type{Target}, ::Trait, geom, components, iter) where {Target,Trait<:Target} =
+    iterate(components, iter)

Specific cases to avoid method ambiguity

julia
_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

julia
_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(`
julia
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.

source

`,4))]),i("details",d,[i("summary",null,[s[3]||(s[3]=i("a",{id:"GeometryOpsCore.flatten-source-src-other_primitives",href:"#GeometryOpsCore.flatten-source-src-other_primitives"},[i("span",{class:"jlbinding"},"GeometryOpsCore.flatten")],-1)),s[4]||(s[4]=a()),h(n,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[5]||(s[5]=t(`
julia
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.

source

`,4))]),i("details",g,[i("summary",null,[s[6]||(s[6]=i("a",{id:"GeometryOpsCore.reconstruct-source-src-other_primitives",href:"#GeometryOpsCore.reconstruct-source-src-other_primitives"},[i("span",{class:"jlbinding"},"GeometryOpsCore.reconstruct")],-1)),s[7]||(s[7]=a()),h(n,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[8]||(s[8]=t('
julia
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.

source

',5))]),i("details",y,[i("summary",null,[s[9]||(s[9]=i("a",{id:"GeometryOpsCore.rebuild-source-src-other_primitives",href:"#GeometryOpsCore.rebuild-source-src-other_primitives"},[i("span",{class:"jlbinding"},"GeometryOpsCore.rebuild")],-1)),s[10]||(s[10]=a()),h(n,{type:"info",class:"jlObjectType jlFunction",text:"Function"})]),s[11]||(s[11]=t('
julia
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)

source

',5))]),s[14]||(s[14]=t(`
julia
"""
+    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

julia
unwrap(f, target::Type, geom) = unwrap(f, target, GI.trait(geom), geom)

Try to unwrap over iterables

julia
unwrap(f, target::Type, ::Nothing, iterable) =
+    map(x -> unwrap(f, target, x), iterable)

Rewrap feature collections

julia
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

julia
unwrap(f, ::Type{Target}, ::Trait, geom) where {Target,Trait<:Target} = f(geom)

Fail if we hit PointTrait

julia
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

julia
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

julia
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

julia
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

julia
_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

julia
_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

julia
_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

julia
function _reconstruct(::Type{Target}, ::Nothing, iterable, components, iter) where Target
+    vect = map(iterable) do x

iter is updated by _reconstruct here

julia
        obj, iter = _reconstruct(Target, x, components, iter)
+        obj
+    end
+    return vect, iter
+end

Reconstruct feature collections

julia
function _reconstruct(::Type{Target}, ::GI.FeatureCollectionTrait, fc, components, iter) where Target
+    features = map(GI.getfeature(fc)) do feature

iter is updated by _reconstruct here

julia
        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

julia
        subgeom1, iter = _reconstruct(Target, GI.trait(subgeom), subgeom, components, iter)
+        subgeom1
+    end
+    return rebuild(geom, geoms), iter
+end

Apply f to the target geometry

julia
_reconstruct(::Type{Target}, ::Trait, geom, components, iter) where {Target,Trait<:Target} =
+    iterate(components, iter)

Specific cases to avoid method ambiguity

julia
_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

julia
_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(`

Types

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.

julia
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.

julia
"""
+    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.

julia
TraitTarget(GI.PointTrait)
+TraitTarget(Union{GI.LineStringTrait, GI.LinearRingTrait})

etc.

julia
\`\`\`
+
+"""
+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?

julia
"""
+    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(`

Types

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.

julia
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.

julia
"""
+    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.

julia
TraitTarget(GI.PointTrait)
+TraitTarget(Union{GI.LineStringTrait, GI.LinearRingTrait})

etc.

julia
\`\`\`
+
+"""
+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?

julia
"""
+    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(`

Closed Rings

julia
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.

Example

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:

julia
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.

julia
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)
`,12)),i("p",null,[s[4]||(s[4]=t("You can see that the last point of the ring here is equal to the first point. For a polygon with ")),i("mjx-container",p,[(e(),a("svg",k,s[0]||(s[0]=[i("g",{stroke:"currentColor",fill:"currentColor","stroke-width":"0",transform:"scale(1,-1)"},[i("g",{"data-mml-node":"math"},[i("g",{"data-mml-node":"mi"},[i("path",{"data-c":"1D45B",d:"M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z",style:{"stroke-width":"3"}})])])],-1)]))),s[1]||(s[1]=i("mjx-assistive-mml",{unselectable:"on",display:"inline",style:{top:"0px",left:"0px",clip:"rect(1px, 1px, 1px, 1px)","-webkit-touch-callout":"none","-webkit-user-select":"none","-khtml-user-select":"none","-moz-user-select":"none","-ms-user-select":"none","user-select":"none",position:"absolute",padding:"1px 0px 0px 0px",border:"0px",display:"block",width:"auto",overflow:"hidden"}},[i("math",{xmlns:"http://www.w3.org/1998/Math/MathML"},[i("mi",null,"n")])],-1))]),s[5]||(s[5]=t(" sides, there should be ")),i("mjx-container",r,[(e(),a("svg",o,s[2]||(s[2]=[n('',1)]))),s[3]||(s[3]=i("mjx-assistive-mml",{unselectable:"on",display:"inline",style:{top:"0px",left:"0px",clip:"rect(1px, 1px, 1px, 1px)","-webkit-touch-callout":"none","-webkit-user-select":"none","-khtml-user-select":"none","-moz-user-select":"none","-ms-user-select":"none","user-select":"none",position:"absolute",padding:"1px 0px 0px 0px",border:"0px",display:"block",width:"auto",overflow:"hidden"}},[i("math",{xmlns:"http://www.w3.org/1998/Math/MathML"},[i("mi",null,"n"),i("mo",null,"+"),i("mn",null,"1")])],-1))]),s[6]||(s[6]=t(" vertices."))]),s[8]||(s[8]=n(`

Implementation

julia
"""
+    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

julia
        return ring
+    else

Assemble the ring as a vector

julia
        tups = tuples.(GI.getpoint(ring))

Close the ring

julia
        push!(tups, tups[1])

Return an actual ring

julia
        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(`

Closed Rings

julia
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.

Example

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:

julia
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.

julia
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)
`,12)),i("p",null,[s[4]||(s[4]=t("You can see that the last point of the ring here is equal to the first point. For a polygon with ")),i("mjx-container",p,[(e(),a("svg",k,s[0]||(s[0]=[i("g",{stroke:"currentColor",fill:"currentColor","stroke-width":"0",transform:"scale(1,-1)"},[i("g",{"data-mml-node":"math"},[i("g",{"data-mml-node":"mi"},[i("path",{"data-c":"1D45B",d:"M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z",style:{"stroke-width":"3"}})])])],-1)]))),s[1]||(s[1]=i("mjx-assistive-mml",{unselectable:"on",display:"inline",style:{top:"0px",left:"0px",clip:"rect(1px, 1px, 1px, 1px)","-webkit-touch-callout":"none","-webkit-user-select":"none","-khtml-user-select":"none","-moz-user-select":"none","-ms-user-select":"none","user-select":"none",position:"absolute",padding:"1px 0px 0px 0px",border:"0px",display:"block",width:"auto",overflow:"hidden"}},[i("math",{xmlns:"http://www.w3.org/1998/Math/MathML"},[i("mi",null,"n")])],-1))]),s[5]||(s[5]=t(" sides, there should be ")),i("mjx-container",r,[(e(),a("svg",o,s[2]||(s[2]=[n('',1)]))),s[3]||(s[3]=i("mjx-assistive-mml",{unselectable:"on",display:"inline",style:{top:"0px",left:"0px",clip:"rect(1px, 1px, 1px, 1px)","-webkit-touch-callout":"none","-webkit-user-select":"none","-khtml-user-select":"none","-moz-user-select":"none","-ms-user-select":"none","user-select":"none",position:"absolute",padding:"1px 0px 0px 0px",border:"0px",display:"block",width:"auto",overflow:"hidden"}},[i("math",{xmlns:"http://www.w3.org/1998/Math/MathML"},[i("mi",null,"n"),i("mo",null,"+"),i("mn",null,"1")])],-1))]),s[6]||(s[6]=t(" vertices."))]),s[8]||(s[8]=n(`

Implementation

julia
"""
+    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

julia
        return ring
+    else

Assemble the ring as a vector

julia
        tups = tuples.(GI.getpoint(ring))

Close the ring

julia
        push!(tups, tups[1])

Return an actual ring

julia
        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('

Geometry Corrections

julia
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.

Interface

All GeometryCorrections 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('
julia
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.

source

',5))]),s[16]||(s[16]=e(`

Any geometry correction must implement the interface as given above.

julia
"""
+    abstract type GeometryCorrection
+
+This abstract type represents a geometry correction.
+
+# Interface
+
+Any \`GeometryCorrection\` must implement two functions:
+    * \`application_level(::GeometryCorrection)::AbstractGeometryTrait\`: This function should return the \`GeoInterface\` trait that the correction is intended to be applied to, like \`PointTrait\` or \`LineStringTrait\` or \`PolygonTrait\`.
+    * \`(::GeometryCorrection)(::AbstractGeometryTrait, geometry)::(some_geometry)\`: This function should apply the correction to the given geometry, and return a new geometry.
+"""
+abstract type GeometryCorrection end
+
+application_level(gc::GeometryCorrection) = error("Not implemented yet for $(gc)")
+
+(gc::GeometryCorrection)(geometry) = gc(GI.trait(geometry), geometry)
+
+(gc::GeometryCorrection)(trait::GI.AbstractGeometryTrait, geometry) = error("Not implemented yet for $(gc) and $(trait).")
+
+function fix(geometry; corrections = GeometryCorrection[ClosedRing(),], kwargs...)
+    traits = application_level.(corrections)
+    final_geometry = geometry
+    for Trait in (GI.PointTrait, GI.MultiPointTrait, GI.LineStringTrait, GI.LinearRingTrait, GI.MultiLineStringTrait, GI.PolygonTrait, GI.MultiPolygonTrait)
+        available_corrections = findall(x -> x == Trait, traits)
+        isempty(available_corrections) && continue
+        @debug "Correcting for $(Trait)"
+        net_function = reduce(, corrections[available_corrections])
+        final_geometry = apply(net_function, Trait, final_geometry; kwargs...)
+    end
+    return final_geometry
+end

Available corrections

`,3)),i("details",c,[i("summary",null,[s[3]||(s[3]=i("a",{id:"GeometryOps.ClosedRing-source-transformations-correction-geometry_correction",href:"#GeometryOps.ClosedRing-source-transformations-correction-geometry_correction"},[i("span",{class:"jlbinding"},"GeometryOps.ClosedRing")],-1)),s[4]||(s[4]=t()),n(a,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[5]||(s[5]=e('
julia
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.

source

',5))]),i("details",d,[i("summary",null,[s[6]||(s[6]=i("a",{id:"GeometryOps.DiffIntersectingPolygons-source-transformations-correction-geometry_correction",href:"#GeometryOps.DiffIntersectingPolygons-source-transformations-correction-geometry_correction"},[i("span",{class:"jlbinding"},"GeometryOps.DiffIntersectingPolygons")],-1)),s[7]||(s[7]=t()),n(a,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[8]||(s[8]=e('
julia
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.

source

',3))]),i("details",y,[i("summary",null,[s[9]||(s[9]=i("a",{id:"GeometryOps.GeometryCorrection-source-transformations-correction-geometry_correction-2",href:"#GeometryOps.GeometryCorrection-source-transformations-correction-geometry_correction-2"},[i("span",{class:"jlbinding"},"GeometryOps.GeometryCorrection")],-1)),s[10]||(s[10]=t()),n(a,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[11]||(s[11]=e('
julia
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.

source

',5))]),i("details",g,[i("summary",null,[s[12]||(s[12]=i("a",{id:"GeometryOps.UnionIntersectingPolygons-source-transformations-correction-geometry_correction",href:"#GeometryOps.UnionIntersectingPolygons-source-transformations-correction-geometry_correction"},[i("span",{class:"jlbinding"},"GeometryOps.UnionIntersectingPolygons")],-1)),s[13]||(s[13]=t()),n(a,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[14]||(s[14]=e('
julia
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.

source

',4))]),s[17]||(s[17]=i("hr",null,null,-1)),s[18]||(s[18]=i("p",null,[i("em",null,[t("This page was generated using "),i("a",{href:"https://github.com/fredrikekre/Literate.jl",target:"_blank",rel:"noreferrer"},"Literate.jl"),t(".")])],-1))])}const A=r(h,[["render",m]]);export{G as __pageData,A as default}; diff --git a/previews/PR239/assets/source_transformations_correction_geometry_correction.md.DSGv3LiX.lean.js b/previews/PR239/assets/source_transformations_correction_geometry_correction.md.DSGv3LiX.lean.js new file mode 100644 index 000000000..e79621afd --- /dev/null +++ b/previews/PR239/assets/source_transformations_correction_geometry_correction.md.DSGv3LiX.lean.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('

Geometry Corrections

julia
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.

Interface

All GeometryCorrections 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('
julia
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.

source

',5))]),s[16]||(s[16]=e(`

Any geometry correction must implement the interface as given above.

julia
"""
+    abstract type GeometryCorrection
+
+This abstract type represents a geometry correction.
+
+# Interface
+
+Any \`GeometryCorrection\` must implement two functions:
+    * \`application_level(::GeometryCorrection)::AbstractGeometryTrait\`: This function should return the \`GeoInterface\` trait that the correction is intended to be applied to, like \`PointTrait\` or \`LineStringTrait\` or \`PolygonTrait\`.
+    * \`(::GeometryCorrection)(::AbstractGeometryTrait, geometry)::(some_geometry)\`: This function should apply the correction to the given geometry, and return a new geometry.
+"""
+abstract type GeometryCorrection end
+
+application_level(gc::GeometryCorrection) = error("Not implemented yet for $(gc)")
+
+(gc::GeometryCorrection)(geometry) = gc(GI.trait(geometry), geometry)
+
+(gc::GeometryCorrection)(trait::GI.AbstractGeometryTrait, geometry) = error("Not implemented yet for $(gc) and $(trait).")
+
+function fix(geometry; corrections = GeometryCorrection[ClosedRing(),], kwargs...)
+    traits = application_level.(corrections)
+    final_geometry = geometry
+    for Trait in (GI.PointTrait, GI.MultiPointTrait, GI.LineStringTrait, GI.LinearRingTrait, GI.MultiLineStringTrait, GI.PolygonTrait, GI.MultiPolygonTrait)
+        available_corrections = findall(x -> x == Trait, traits)
+        isempty(available_corrections) && continue
+        @debug "Correcting for $(Trait)"
+        net_function = reduce(, corrections[available_corrections])
+        final_geometry = apply(net_function, Trait, final_geometry; kwargs...)
+    end
+    return final_geometry
+end

Available corrections

`,3)),i("details",c,[i("summary",null,[s[3]||(s[3]=i("a",{id:"GeometryOps.ClosedRing-source-transformations-correction-geometry_correction",href:"#GeometryOps.ClosedRing-source-transformations-correction-geometry_correction"},[i("span",{class:"jlbinding"},"GeometryOps.ClosedRing")],-1)),s[4]||(s[4]=t()),n(a,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[5]||(s[5]=e('
julia
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.

source

',5))]),i("details",d,[i("summary",null,[s[6]||(s[6]=i("a",{id:"GeometryOps.DiffIntersectingPolygons-source-transformations-correction-geometry_correction",href:"#GeometryOps.DiffIntersectingPolygons-source-transformations-correction-geometry_correction"},[i("span",{class:"jlbinding"},"GeometryOps.DiffIntersectingPolygons")],-1)),s[7]||(s[7]=t()),n(a,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[8]||(s[8]=e('
julia
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.

source

',3))]),i("details",y,[i("summary",null,[s[9]||(s[9]=i("a",{id:"GeometryOps.GeometryCorrection-source-transformations-correction-geometry_correction-2",href:"#GeometryOps.GeometryCorrection-source-transformations-correction-geometry_correction-2"},[i("span",{class:"jlbinding"},"GeometryOps.GeometryCorrection")],-1)),s[10]||(s[10]=t()),n(a,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[11]||(s[11]=e('
julia
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.

source

',5))]),i("details",g,[i("summary",null,[s[12]||(s[12]=i("a",{id:"GeometryOps.UnionIntersectingPolygons-source-transformations-correction-geometry_correction",href:"#GeometryOps.UnionIntersectingPolygons-source-transformations-correction-geometry_correction"},[i("span",{class:"jlbinding"},"GeometryOps.UnionIntersectingPolygons")],-1)),s[13]||(s[13]=t()),n(a,{type:"info",class:"jlObjectType jlType",text:"Type"})]),s[14]||(s[14]=e('
julia
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.

source

',4))]),s[17]||(s[17]=i("hr",null,null,-1)),s[18]||(s[18]=i("p",null,[i("em",null,[t("This page was generated using "),i("a",{href:"https://github.com/fredrikekre/Literate.jl",target:"_blank",rel:"noreferrer"},"Literate.jl"),t(".")])],-1))])}const A=r(h,[["render",m]]);export{G as __pageData,A as default}; diff --git a/previews/PR239/assets/source_transformations_correction_intersecting_polygons.md.CLhMqjHy.js b/previews/PR239/assets/source_transformations_correction_intersecting_polygons.md.CLhMqjHy.js new file mode 100644 index 000000000..440caed24 --- /dev/null +++ b/previews/PR239/assets/source_transformations_correction_intersecting_polygons.md.CLhMqjHy.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(`

Intersecting Polygons

julia
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.

Example

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:

julia
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.

julia
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.

Implementation

julia
"""
+    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

julia
        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

julia
        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(`

Intersecting Polygons

julia
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.

Example

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:

julia
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.

julia
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.

Implementation

julia
"""
+    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

julia
        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

julia
        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(`

Extent embedding

julia
"""
+    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

julia
$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(`

Extent embedding

julia
"""
+    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

julia
$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(`

Coordinate flipping

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.

julia
"""
+    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(`

Coordinate flipping

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.

julia
"""
+    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(`
julia
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(`
julia
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('

Geometry reprojection

julia
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.

julia
"""\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

Method error handling

We also inject a method error handler, which prints a suggestion if the Proj extension is not loaded.

julia
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('

Geometry reprojection

julia
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.

julia
"""\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

Method error handling

We also inject a method error handler, which prints a suggestion if the Proj extension is not loaded.

julia
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(`

Segmentize

julia
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.

Examples

julia
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.

julia
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.

julia
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.

Benchmark

We benchmark our method against LibGEOS's GEOSDensify method, which is a similar method for densifying geometries.

julia
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))
julia
# 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)

julia
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!

julia
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

Implementation

julia
"""
+    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

julia
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

julia
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.

julia
    push!(new_coords, (x2, y2))
+    return nothing
+end

Note

The _fill_linear_kernel definition for GeodesicSegments is in the GeometryOpsProjExt extension module, in the segmentize.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(`

Segmentize

julia
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.

Examples

julia
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.

julia
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.

julia
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.

Benchmark

We benchmark our method against LibGEOS's GEOSDensify method, which is a similar method for densifying geometries.

julia
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))
julia
# 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)

julia
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!

julia
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

Implementation

julia
"""
+    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

julia
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

julia
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.

julia
    push!(new_coords, (x2, y2))
+    return nothing
+end

Note

The _fill_linear_kernel definition for GeodesicSegments is in the GeometryOpsProjExt extension module, in the segmentize.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(`

Geometry simplification

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.

Examples

A quick and dirty example is:

julia
using Makie, GeoInterfaceMakie
+import GeoInterface as GI
+import GeometryOps as GO
+
+original = GI.Polygon([[[-70.603637, -33.399918], [-70.614624, -33.395332], [-70.639343, -33.392466], [-70.659942, -33.394759], [-70.683975, -33.404504], [-70.697021, -33.419406], [-70.701141, -33.434306], [-70.700454, -33.446339], [-70.694274, -33.458369], [-70.682601, -33.465816], [-70.668869, -33.472117], [-70.646209, -33.473835], [-70.624923, -33.472117], [-70.609817, -33.468107], [-70.595397, -33.458369], [-70.587158, -33.442901], [-70.587158, -33.426283], [-70.590591, -33.414248], [-70.594711, -33.406224], [-70.603637, -33.399918]]])
+
+simple = GO.simplify(original; number=6)
+
+f, a, p = poly(original; label = "Original")
+poly!(simple; label = "Simplified")
+axislegend(a)
+f

Benchmark

We benchmark these methods against LibGEOS's simplify implementation, which uses the Douglas-Peucker algorithm.

julia
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.

julia
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)

julia
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)

julia
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

julia
- \`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

julia
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

julia
6
+\`\`\`
+"""
+simplify(alg::SimplifyAlg, data; kw...) = _simplify(alg, data; kw...)
+simplify(alg::GEOS, data; kw...) = _simplify(alg, data; kw...)

Default algorithm is DouglasPeucker

julia
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

Simplify with RadialDistance Algorithm

julia
"""
+    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

julia
        tol = isnothing(tol) ? tol : tol^2
+        new(number, ratio, tol)
+    end
+end
+
+function _simplify(alg::RadialDistance, points::Vector, _)
+    previous = first(points)
+    distances = Array{Float64}(undef, length(points))
+    for i in eachindex(points)
+        point = points[i]
+        distances[i] = _squared_euclid_distance(Float64, point, previous)
+        previous = point
+    end
+    # Never remove the end points
+    distances[begin] = distances[end] = Inf
+    return _get_points(alg, points, distances)
+end

Simplify with DouglasPeucker Algorithm

julia
"""
+    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

julia
        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

julia
    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

julia
    queue = Vector{Tuple{Int, Int, Int, Float64}}()
+    queue_idx, queue_dist = 0, zero(Float64)
+    len_queue = 0

Set up results vector

julia
    results = Vector{Int}(undef, max_points + (preserve_endpoint ? 0 : 1))
+    results[1], results[2] = 1, npoints

Loop through points until stopping criteria are fulfilled

julia
    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

julia
        i += 1
+        results[i] = max_idx

Determine which point to add next by checking left and right of point

julia
        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

julia
        if queue_dist > left_dist && queue_dist > right_dist

Value in queue is next value to add to results

julia
            start_idx, max_idx, end_idx, max_dist = queue[queue_idx]

Add left and/or right values to queue or delete used queue value

julia
            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

julia
            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

julia
        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

julia
            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

julia
            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

Simplify with VisvalingamWhyatt Algorithm

julia
"""
+    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

julia
        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

julia
_triangle_double_area(p1, p2, p3) =
+    abs(p1[1] * (p2[2] - p3[2]) + p2[1] * (p3[2] - p1[2]) + p3[1] * (p1[2] - p2[2]))

Shared utils

julia
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

julia
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(`

Geometry simplification

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.

Examples

A quick and dirty example is:

julia
using Makie, GeoInterfaceMakie
+import GeoInterface as GI
+import GeometryOps as GO
+
+original = GI.Polygon([[[-70.603637, -33.399918], [-70.614624, -33.395332], [-70.639343, -33.392466], [-70.659942, -33.394759], [-70.683975, -33.404504], [-70.697021, -33.419406], [-70.701141, -33.434306], [-70.700454, -33.446339], [-70.694274, -33.458369], [-70.682601, -33.465816], [-70.668869, -33.472117], [-70.646209, -33.473835], [-70.624923, -33.472117], [-70.609817, -33.468107], [-70.595397, -33.458369], [-70.587158, -33.442901], [-70.587158, -33.426283], [-70.590591, -33.414248], [-70.594711, -33.406224], [-70.603637, -33.399918]]])
+
+simple = GO.simplify(original; number=6)
+
+f, a, p = poly(original; label = "Original")
+poly!(simple; label = "Simplified")
+axislegend(a)
+f

Benchmark

We benchmark these methods against LibGEOS's simplify implementation, which uses the Douglas-Peucker algorithm.

julia
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.

julia
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)

julia
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)

julia
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

julia
- \`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

julia
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

julia
6
+\`\`\`
+"""
+simplify(alg::SimplifyAlg, data; kw...) = _simplify(alg, data; kw...)
+simplify(alg::GEOS, data; kw...) = _simplify(alg, data; kw...)

Default algorithm is DouglasPeucker

julia
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

Simplify with RadialDistance Algorithm

julia
"""
+    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

julia
        tol = isnothing(tol) ? tol : tol^2
+        new(number, ratio, tol)
+    end
+end
+
+function _simplify(alg::RadialDistance, points::Vector, _)
+    previous = first(points)
+    distances = Array{Float64}(undef, length(points))
+    for i in eachindex(points)
+        point = points[i]
+        distances[i] = _squared_euclid_distance(Float64, point, previous)
+        previous = point
+    end
+    # Never remove the end points
+    distances[begin] = distances[end] = Inf
+    return _get_points(alg, points, distances)
+end

Simplify with DouglasPeucker Algorithm

julia
"""
+    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

julia
        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

julia
    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

julia
    queue = Vector{Tuple{Int, Int, Int, Float64}}()
+    queue_idx, queue_dist = 0, zero(Float64)
+    len_queue = 0

Set up results vector

julia
    results = Vector{Int}(undef, max_points + (preserve_endpoint ? 0 : 1))
+    results[1], results[2] = 1, npoints

Loop through points until stopping criteria are fulfilled

julia
    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

julia
        i += 1
+        results[i] = max_idx

Determine which point to add next by checking left and right of point

julia
        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

julia
        if queue_dist > left_dist && queue_dist > right_dist

Value in queue is next value to add to results

julia
            start_idx, max_idx, end_idx, max_dist = queue[queue_idx]

Add left and/or right values to queue or delete used queue value

julia
            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

julia
            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

julia
        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

julia
            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

julia
            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

Simplify with VisvalingamWhyatt Algorithm

julia
"""
+    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

julia
        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

julia
_triangle_double_area(p1, p2, p3) =
+    abs(p1[1] * (p2[2] - p3[2]) + p2[1] * (p3[2] - p1[2]) + p3[1] * (p1[2] - p2[2]))

Shared utils

julia
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

julia
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(`

Pointwise transformation

julia
"""
+    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(`

Pointwise transformation

julia
"""
+    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(`

Tuple conversion

julia
"""
+    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

julia
$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(`

Tuple conversion

julia
"""
+    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

julia
$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(`

Types

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.

julia
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:

  1. 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.

  2. It's a good way to test the correctness of the native implementations.

julia
"""
+    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.

julia
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(`

Types

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.

julia
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:

  1. 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.

  2. It's a good way to test the correctness of the native implementations.

julia
"""
+    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.

julia
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(`

Utility functions

julia
_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

julia
\`\`\`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

julia
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(`

Utility functions

julia
_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

julia
\`\`\`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

julia
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(`

Creating Geometry

In this tutorial, we're going to:

  1. Create and plot geometries

  2. Plot geometries on a map using GeoMakie and coordinate reference system (CRS)

  3. Create geospatial geometries with embedded coordinate reference system information

  4. Assign attributes to geospatial geometries

  5. Save geospatial geometries to common geospatial file formats

First, we load some required packages.

julia
# 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

Creating and plotting geometries

Let's start by making a single Point.

julia
point = GI.Point(0, 0)
GeoInterface.Wrappers.Point{false, false, Tuple{Int64, Int64}, Nothing}((0, 0), nothing)

Now, let's plot our point.

julia
fig, ax, plt = plot(point)

Let's create a set of points, and have a bit more fun with plotting.

julia
x = [-5, 0, 5, 0];
+y = [0, -5, 0, 5];
+points = GI.Point.(zip(x,y));
+plot!(ax, points; marker = '✈', markersize = 30)
+fig

Points can be combined into a single MultiPoint geometry.

julia
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.

julia
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.

julia
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.

julia
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.

julia
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.

julia
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 LinearRings 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.

julia
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.

julia
xoffset = 50.;
+yoffset = 0.;
+f = CoordinateTransformations.Translation(xoffset, yoffset);
+polygon2 = GO.transform(f, polygon2);
+plot!(polygon2)
+fig

Polygons can also be grouped together as a MultiPolygon.

julia
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.

julia
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].

Plot geometries on a map using 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.

julia
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.

julia
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.

julia
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 MultiPolygons as a GeoJSON.FeatureCollection.

julia
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.

julia
fig = Figure(size=(1000, 500));
+ga = GeoAxis(
+    fig[1, 1];
+    source = source_crs1,
+    dest = destination_crs,
+    xticklabelsvisible = false,
+    yticklabelsvisible = false,
+);

Plot land for context.

julia
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

julia
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.

julia
source_crs2 = GFT.EPSG(32610)
GeoFormatTypes.EPSG{1}((32610,))

Create a polygon (we're working in meters now, not latitude and longitude)

julia
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

julia
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

julia
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.

julia
plot!(ga,polygon3; color=:red, source = source_crs2)
+fig

Create geospatial geometries with embedded coordinate reference system information

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

julia
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

julia
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

julia
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,)))

Creating a table with attributes and geometry

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.

julia
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.

julia
df[!,:id] = ["a", "b"]
+df[!, :name] = ["polygon 1", "polygon 2"]
+df

Saving your geospatial data

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.

julia
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.

julia
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.

julia
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.

julia
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(`

Creating Geometry

In this tutorial, we're going to:

  1. Create and plot geometries

  2. Plot geometries on a map using GeoMakie and coordinate reference system (CRS)

  3. Create geospatial geometries with embedded coordinate reference system information

  4. Assign attributes to geospatial geometries

  5. Save geospatial geometries to common geospatial file formats

First, we load some required packages.

julia
# 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

Creating and plotting geometries

Let's start by making a single Point.

julia
point = GI.Point(0, 0)
GeoInterface.Wrappers.Point{false, false, Tuple{Int64, Int64}, Nothing}((0, 0), nothing)

Now, let's plot our point.

julia
fig, ax, plt = plot(point)

Let's create a set of points, and have a bit more fun with plotting.

julia
x = [-5, 0, 5, 0];
+y = [0, -5, 0, 5];
+points = GI.Point.(zip(x,y));
+plot!(ax, points; marker = '✈', markersize = 30)
+fig

Points can be combined into a single MultiPoint geometry.

julia
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.

julia
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.

julia
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.

julia
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.

julia
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.

julia
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 LinearRings 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.

julia
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.

julia
xoffset = 50.;
+yoffset = 0.;
+f = CoordinateTransformations.Translation(xoffset, yoffset);
+polygon2 = GO.transform(f, polygon2);
+plot!(polygon2)
+fig

Polygons can also be grouped together as a MultiPolygon.

julia
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.

julia
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].

Plot geometries on a map using 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.

julia
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.

julia
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.

julia
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 MultiPolygons as a GeoJSON.FeatureCollection.

julia
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.

julia
fig = Figure(size=(1000, 500));
+ga = GeoAxis(
+    fig[1, 1];
+    source = source_crs1,
+    dest = destination_crs,
+    xticklabelsvisible = false,
+    yticklabelsvisible = false,
+);

Plot land for context.

julia
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

julia
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.

julia
source_crs2 = GFT.EPSG(32610)
GeoFormatTypes.EPSG{1}((32610,))

Create a polygon (we're working in meters now, not latitude and longitude)

julia
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

julia
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

julia
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.

julia
plot!(ga,polygon3; color=:red, source = source_crs2)
+fig

Create geospatial geometries with embedded coordinate reference system information

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

julia
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

julia
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

julia
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,)))

Creating a table with attributes and geometry

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.

julia
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.

julia
df[!,:id] = ["a", "b"]
+df[!, :name] = ["polygon 1", "polygon 2"]
+df

Saving your geospatial data

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.

julia
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.

julia
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.

julia
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.

julia
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

Geodesic paths are paths computed on an ellipsoid, as opposed to a plane.

julia
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

',4)]))}const y=i(p,[["render",k]]);export{o as __pageData,y as default}; diff --git a/previews/PR239/assets/tutorials_geodesic_paths.md.BlU0MlUq.lean.js b/previews/PR239/assets/tutorials_geodesic_paths.md.BlU0MlUq.lean.js new file mode 100644 index 000000000..020af8870 --- /dev/null +++ b/previews/PR239/assets/tutorials_geodesic_paths.md.BlU0MlUq.lean.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

Geodesic paths are paths computed on an ellipsoid, as opposed to a plane.

julia
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

',4)]))}const y=i(p,[["render",k]]);export{o as __pageData,y as default}; diff --git a/previews/PR239/assets/tutorials_spatial_joins.md.B67GghbG.js b/previews/PR239/assets/tutorials_spatial_joins.md.B67GghbG.js new file mode 100644 index 000000000..2a0e89af6 --- /dev/null +++ b/previews/PR239/assets/tutorials_spatial_joins.md.B67GghbG.js @@ -0,0 +1,52 @@ +import{_ as l,c as t,j as i,a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const e="/GeometryOps.jl/previews/PR239/assets/kojydpc.3UVIT8DR.png",p="/GeometryOps.jl/previews/PR239/assets/izslanu.Cm_V2Vs0.png",k="/GeometryOps.jl/previews/PR239/assets/cfoowuw.BEoJ_XVP.png",C=JSON.parse('{"title":"Spatial joins","description":"","frontmatter":{},"headers":[],"relativePath":"tutorials/spatial_joins.md","filePath":"tutorials/spatial_joins.md","lastUpdated":null}'),r={name:"tutorials/spatial_joins.md"},E={class:"MathJax",jax:"SVG",style:{direction:"ltr",position:"relative"}},d={style:{overflow:"visible","min-height":"1px","min-width":"1px","vertical-align":"-0.566ex"},xmlns:"http://www.w3.org/2000/svg",width:"6.307ex",height:"2.262ex",role:"img",focusable:"false",viewBox:"0 -750 2787.7 1000","aria-hidden":"true"};function o(g,s,y,F,c,u){return h(),t("div",null,[s[12]||(s[12]=i("h1",{id:"Spatial-joins",tabindex:"-1"},[a("Spatial joins "),i("a",{class:"header-anchor",href:"#Spatial-joins","aria-label":'Permalink to "Spatial joins {#Spatial-joins}"'},"​")],-1)),i("p",null,[s[2]||(s[2]=a("Spatial joins are ")),s[3]||(s[3]=i("a",{href:"https://www.geeksforgeeks.org/sql-join-set-1-inner-left-right-and-full-joins/",target:"_blank",rel:"noreferrer"},"table joins",-1)),s[4]||(s[4]=a(" which are based not on equality, but on some predicate ")),i("mjx-container",E,[(h(),t("svg",d,s[0]||(s[0]=[n('',1)]))),s[1]||(s[1]=i("mjx-assistive-mml",{unselectable:"on",display:"inline",style:{top:"0px",left:"0px",clip:"rect(1px, 1px, 1px, 1px)","-webkit-touch-callout":"none","-webkit-user-select":"none","-khtml-user-select":"none","-moz-user-select":"none","-ms-user-select":"none","user-select":"none",position:"absolute",padding:"1px 0px 0px 0px",border:"0px",display:"block",width:"auto",overflow:"hidden"}},[i("math",{xmlns:"http://www.w3.org/1998/Math/MathML"},[i("mi",null,"p"),i("mo",{stretchy:"false"},"("),i("mi",null,"x"),i("mo",null,","),i("mi",null,"y"),i("mo",{stretchy:"false"},")")])],-1))]),s[5]||(s[5]=a(", which takes two geometries, and returns a value of either ")),s[6]||(s[6]=i("code",null,"true",-1)),s[7]||(s[7]=a(" or ")),s[8]||(s[8]=i("code",null,"false",-1)),s[9]||(s[9]=a(". For geometries, the ")),s[10]||(s[10]=i("a",{href:"https://en.wikipedia.org/wiki/DE-9IM",target:"_blank",rel:"noreferrer"},[i("code",null,"DE-9IM")],-1)),s[11]||(s[11]=a(" spatial relationship model is used to determine the spatial relationship between two geometries."))]),s[13]||(s[13]=n(`

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:

julia
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:

julia
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.

Simple example

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.

julia
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.

julia
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

julia
@time joined_df = FlexiJoins.innerjoin(
+    (points_df, poly_df),
+    by_pred(:geometry, GO.within, :geometry)
+)
julia
scatter!(a, joined_df.geometry; color = joined_df.color)
+f

Here, you can see that the colors were assigned appropriately to the scattered points!

Real-world example

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.

julia
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]]
julia
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).

Enabling custom predicates

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:

julia
my_predicate_function = <(5)  abs  GO.distance

You would need to define FlexiJoins.supports_mode on your predicate:

julia
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).

`,37))])}const T=l(r,[["render",o]]);export{C as __pageData,T as default}; diff --git a/previews/PR239/assets/tutorials_spatial_joins.md.B67GghbG.lean.js b/previews/PR239/assets/tutorials_spatial_joins.md.B67GghbG.lean.js new file mode 100644 index 000000000..2a0e89af6 --- /dev/null +++ b/previews/PR239/assets/tutorials_spatial_joins.md.B67GghbG.lean.js @@ -0,0 +1,52 @@ +import{_ as l,c as t,j as i,a,a5 as n,o as h}from"./chunks/framework.onQNwZ2I.js";const e="/GeometryOps.jl/previews/PR239/assets/kojydpc.3UVIT8DR.png",p="/GeometryOps.jl/previews/PR239/assets/izslanu.Cm_V2Vs0.png",k="/GeometryOps.jl/previews/PR239/assets/cfoowuw.BEoJ_XVP.png",C=JSON.parse('{"title":"Spatial joins","description":"","frontmatter":{},"headers":[],"relativePath":"tutorials/spatial_joins.md","filePath":"tutorials/spatial_joins.md","lastUpdated":null}'),r={name:"tutorials/spatial_joins.md"},E={class:"MathJax",jax:"SVG",style:{direction:"ltr",position:"relative"}},d={style:{overflow:"visible","min-height":"1px","min-width":"1px","vertical-align":"-0.566ex"},xmlns:"http://www.w3.org/2000/svg",width:"6.307ex",height:"2.262ex",role:"img",focusable:"false",viewBox:"0 -750 2787.7 1000","aria-hidden":"true"};function o(g,s,y,F,c,u){return h(),t("div",null,[s[12]||(s[12]=i("h1",{id:"Spatial-joins",tabindex:"-1"},[a("Spatial joins "),i("a",{class:"header-anchor",href:"#Spatial-joins","aria-label":'Permalink to "Spatial joins {#Spatial-joins}"'},"​")],-1)),i("p",null,[s[2]||(s[2]=a("Spatial joins are ")),s[3]||(s[3]=i("a",{href:"https://www.geeksforgeeks.org/sql-join-set-1-inner-left-right-and-full-joins/",target:"_blank",rel:"noreferrer"},"table joins",-1)),s[4]||(s[4]=a(" which are based not on equality, but on some predicate ")),i("mjx-container",E,[(h(),t("svg",d,s[0]||(s[0]=[n('',1)]))),s[1]||(s[1]=i("mjx-assistive-mml",{unselectable:"on",display:"inline",style:{top:"0px",left:"0px",clip:"rect(1px, 1px, 1px, 1px)","-webkit-touch-callout":"none","-webkit-user-select":"none","-khtml-user-select":"none","-moz-user-select":"none","-ms-user-select":"none","user-select":"none",position:"absolute",padding:"1px 0px 0px 0px",border:"0px",display:"block",width:"auto",overflow:"hidden"}},[i("math",{xmlns:"http://www.w3.org/1998/Math/MathML"},[i("mi",null,"p"),i("mo",{stretchy:"false"},"("),i("mi",null,"x"),i("mo",null,","),i("mi",null,"y"),i("mo",{stretchy:"false"},")")])],-1))]),s[5]||(s[5]=a(", which takes two geometries, and returns a value of either ")),s[6]||(s[6]=i("code",null,"true",-1)),s[7]||(s[7]=a(" or ")),s[8]||(s[8]=i("code",null,"false",-1)),s[9]||(s[9]=a(". For geometries, the ")),s[10]||(s[10]=i("a",{href:"https://en.wikipedia.org/wiki/DE-9IM",target:"_blank",rel:"noreferrer"},[i("code",null,"DE-9IM")],-1)),s[11]||(s[11]=a(" spatial relationship model is used to determine the spatial relationship between two geometries."))]),s[13]||(s[13]=n(`

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:

julia
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:

julia
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.

Simple example

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.

julia
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.

julia
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

julia
@time joined_df = FlexiJoins.innerjoin(
+    (points_df, poly_df),
+    by_pred(:geometry, GO.within, :geometry)
+)
julia
scatter!(a, joined_df.geometry; color = joined_df.color)
+f

Here, you can see that the colors were assigned appropriately to the scattered points!

Real-world example

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.

julia
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]]
julia
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).

Enabling custom predicates

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:

julia
my_predicate_function = <(5)  abs  GO.distance

You would need to define FlexiJoins.supports_mode on your predicate:

julia
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).

`,37))])}const T=l(r,[["render",o]]);export{C as __pageData,T as default}; diff --git a/previews/PR239/assets/twuyjor.DYrZOYql.png b/previews/PR239/assets/twuyjor.DYrZOYql.png new file mode 100644 index 000000000..45c3ba24a Binary files /dev/null and b/previews/PR239/assets/twuyjor.DYrZOYql.png differ diff --git a/previews/PR239/assets/umfmfiz.Da39FuJg.png b/previews/PR239/assets/umfmfiz.Da39FuJg.png new file mode 100644 index 000000000..f3f00422a Binary files /dev/null and b/previews/PR239/assets/umfmfiz.Da39FuJg.png differ diff --git a/previews/PR239/assets/wjwdigs.0f3Lq4Lw.png b/previews/PR239/assets/wjwdigs.0f3Lq4Lw.png new file mode 100644 index 000000000..32330974a Binary files /dev/null and b/previews/PR239/assets/wjwdigs.0f3Lq4Lw.png differ diff --git a/previews/PR239/assets/xetmrwv.0OJvb21A.png b/previews/PR239/assets/xetmrwv.0OJvb21A.png new file mode 100644 index 000000000..38ed06078 Binary files /dev/null and b/previews/PR239/assets/xetmrwv.0OJvb21A.png differ diff --git a/previews/PR239/assets/xnfkjof.D5-bot8v.png b/previews/PR239/assets/xnfkjof.D5-bot8v.png new file mode 100644 index 000000000..d68832de0 Binary files /dev/null and b/previews/PR239/assets/xnfkjof.D5-bot8v.png differ diff --git a/previews/PR239/call_notes.html b/previews/PR239/call_notes.html new file mode 100644 index 000000000..ff97cf5c8 --- /dev/null +++ b/previews/PR239/call_notes.html @@ -0,0 +1,25 @@ + + + + + + GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

20th April, 2024

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

29th Feb, 2024

To do

  • [ ] 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

Done

  • Rename bools.jl to something more relevant to the actual code -> orientation.jl

  • Doc improvements:

    • organise sections
+ + + + \ No newline at end of file diff --git a/previews/PR239/experiments/accurate_accumulators.html b/previews/PR239/experiments/accurate_accumulators.html new file mode 100644 index 000000000..aa422d317 --- /dev/null +++ b/previews/PR239/experiments/accurate_accumulators.html @@ -0,0 +1,30 @@ + + + + + + Accurate accumulation | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Accurate accumulation

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!

julia
import GeometryOps as GO, GeoInterface as GI
+using GeoJSON
+using AccurateArithmetic
+using NaturalEarth
+
+all_adm0 = naturalearth("admin_0_countries", 10)
FeatureCollection with 258 Features
julia
GO.area(all_adm0)
21427.909318372607
julia
AccurateArithmetic.sum_oro(GO.area.(all_adm0.geometry))
21427.909318372607
julia
AccurateArithmetic.sum_kbn(GO.area.(all_adm0.geometry))
21427.909318372607
julia
GI.Polygon.(GO.flatten(Union{GI.LineStringTrait, GI.LinearRingTrait}, all_adm0) |> collect .|> x -> [x]) .|> GO.signed_area |> sum
-21427.90063612163
julia
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 ```

+ + + + \ No newline at end of file diff --git a/previews/PR239/experiments/predicates.html b/previews/PR239/experiments/predicates.html new file mode 100644 index 000000000..0422e9739 --- /dev/null +++ b/previews/PR239/experiments/predicates.html @@ -0,0 +1,122 @@ + + + + + + Predicates | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Predicates

Exact vs fast predicates

Orient

julia
using CairoMakie
+import GeometryOps as GO, GeoInterface as GI, LibGEOS as LG
+import ExactPredicates
+using MultiFloats
+using Chairmarks: @be
+using BenchmarkTools: prettytime
+using Statistics
+
+function orient_f64(p, q, r)
+    return sign((GI.x(p) - GI.x(r))*(GI.y(q) - GI.y(r)) - (GI.y(p) - GI.y(r))*(GI.x(q) - GI.x(r)))
+end
+
+function orient_adaptive(p, q, r)
+    px, py = Float64x2(GI.x(p)), Float64x2(GI.y(p))
+    qx, qy = Float64x2(GI.x(q)), Float64x2(GI.y(q))
+    rx, ry = Float64x2(GI.x(r)), Float64x2(GI.y(r))
+    return sign((px - rx)*(qy - ry) - (py - ry)*(qx - rx))
+end
+# Create an interactive Makie dashboard which can show what is done here
+labels = ["Float64", "Adaptive", "Exact"]
+funcs = [orient_f64, orient_adaptive, ExactPredicates.orient]
+fig = Figure()
+axs = [Axis(fig[1, i]; aspect = DataAspect(), xticklabelrotation = pi/4, title) for (i, title) in enumerate(labels)]
+w, r, q, p = 42.0, 0.95, 18.0, 16.8
+function generate_heatmap_args(func, w, r, q, p, heatmap_size = 1000)
+    w_range = LinRange(0, 0+2.0^(-w), heatmap_size)
+    orient_field = [func((p, p), (q, q), (r+x, r+y)) for x in w_range, y in w_range]
+    return (w_range, w_range, orient_field)
+end
+for (i, (ax, func)) in enumerate(zip(axs, funcs))
+    heatmap!(ax, generate_heatmap_args(func, w, r, q, p)...)
+    # now get timing
+    w_range = LinRange(0, 0+2.0^(-w), 5) # for timing - we want to sample stable + unstable points
+    @time timings = [@be $(func)($((p, p)), $((q, q)), $((r+x, r+y))) for x in w_range, y in w_range]
+    median_timings = map.(x -> getproperty(x, :time), getproperty.(timings, :samples)) |> Iterators.flatten |> collect
+    ax.subtitle = prettytime(Statistics.median(median_timings)*10^9)
+    # create time histogram plot
+    # hist(fig[2, i], median_timings; axis = (; xticklabelrotation = pi/4))
+    display(fig)
+end
+resize!(fig, 1000, 450)
+fig

Dashboard

julia
using WGLMakie
+import GeometryOps as GO, GeoInterface as GI, LibGEOS as LG
+import ExactPredicates
+using MultiFloats
+
+function orient_f64(p, q, r)
+    return sign((GI.x(p) - GI.x(r))*(GI.y(q) - GI.y(r)) - (GI.y(p) - GI.y(r))*(GI.x(q) - GI.x(r)))
+end
+
+function orient_adaptive(p, q, r)
+    px, py = Float64x2(GI.x(p)), Float64x2(GI.y(p))
+    qx, qy = Float64x2(GI.x(q)), Float64x2(GI.y(q))
+    rx, ry = Float64x2(GI.x(r)), Float64x2(GI.y(r))
+    return sign((px - rx)*(qy - ry) - (py - ry)*(qx - rx))
+end
+# Create an interactive Makie dashboard which can show what is done here
+fig = Figure()
+ax = Axis(fig[1, 1]; aspect = DataAspect())
+sliders = SliderGrid(fig[2, 1],
+        (label = L"w = 2^{-v} (zoom)", range = LinRange(40, 44, 100), startvalue = 42),
+        (label = L"r = (x, y),~ x, y ∈ v + [0..w)", range = 0:0.01:3, startvalue = 0.95),
+        (label = L"q = (k, k),~ k = v", range = LinRange(0, 30, 100), startvalue = 18),
+        (label = L"p = (k, k),~ k = v", range = LinRange(0, 30, 100), startvalue = 16.8),
+)
+orient_funcs = [orient_f64, orient_adaptive, ExactPredicates.orient]
+menu = Menu(fig[3, 1], options = zip(string.(orient_funcs), orient_funcs))
+w_obs, r_obs, q_obs, p_obs = getproperty.(sliders.sliders, :value)
+orient_obs = menu.selection
+
+heatmap_size = @lift maximum(widths($(ax.scene.viewport)))*4
+
+matrix_observable = lift(orient_obs, w_obs, r_obs, q_obs, p_obs, heatmap_size) do orient, w, r, q, p, heatmap_size
+    return [orient((p, p), (q, q), (r+x, r+y)) for x in LinRange(0, 0+2.0^(-w), heatmap_size), y in LinRange(0, 0+2.0^(-w), heatmap_size)]
+end
+heatmap!(ax, matrix_observable; colormap = [:red, :green, :blue])
+resize!(fig, 500, 700)
+fig

Testing robust vs regular predicates

julia

+import GeoInterface as GI, GeometryOps as GO, LibGEOS as LG
+using MultiFloats
+c1 = [[-28083.868447876892, -58059.13401805979], [-9833.052704767595, -48001.726711609794], [-16111.439295815226, -2.856614689791036e-11], [-76085.95770326033, -2.856614689791036e-11], [-28083.868447876892, -58059.13401805979]]
+c2 = [[-53333.333333333336, 0.0], [0.0, 0.0], [0.0, -80000.0], [-60000.0, -80000.0], [-53333.333333333336, 0.0]]
+
+p1 = GI.Polygon([c1])
+p2 = GI.Polygon([c2])
+GO.intersection(p1, p2; target = GI.PolygonTrait(), fix_multipoly = nothing)
+
+p1_m, p2_m = GO.transform(x -> (Float64x2.(x)), [p1, p2])
+GO.intersection(p1_m, p2_m; target = GI.PolygonTrait(), fix_multipoly = nothing)
+
+p1 = GI.Polygon([[[-57725.80869813739, -52709.704377648755], [-53333.333333333336, 0.0], [-41878.01362848005, 0.0], [-36022.23699059147, -43787.61366192682], [-48268.44121252392, -52521.18593721105], [-57725.80869813739, -52709.704377648755]]])
+p2 = GI.Polygon([[[-60000.0, 80000.0], [0.0, 80000.0], [0.0, 0.0], [-53333.33333333333, 0.0], [-50000.0, 40000.0], [-60000.0, 80000.0]]])
+p1_m, p2_m = GO.transform(x -> (Float64x2.(x)), [p1, p2])
+f, a, p__1 = poly(p1; label = "p1")
+p__2 = poly!(a, p2; label = "p2")
+
+GO.intersection(p1_m, p2_m; target = GI.PolygonTrait(), fix_multipoly = nothing)
+LG.intersection(p1_m, p2_m)

Incircle

+ + + + \ No newline at end of file diff --git a/previews/PR239/explanations/crs.html b/previews/PR239/explanations/crs.html new file mode 100644 index 000000000..bb56b9874 --- /dev/null +++ b/previews/PR239/explanations/crs.html @@ -0,0 +1,25 @@ + + + + + + GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/previews/PR239/explanations/paradigms.html b/previews/PR239/explanations/paradigms.html new file mode 100644 index 000000000..7d5be27f4 --- /dev/null +++ b/previews/PR239/explanations/paradigms.html @@ -0,0 +1,25 @@ + + + + + + Paradigms | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Paradigms

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:

julia
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 reduces 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.)

+ + + + \ No newline at end of file diff --git a/previews/PR239/explanations/peculiarities.html b/previews/PR239/explanations/peculiarities.html new file mode 100644 index 000000000..7770e104e --- /dev/null +++ b/previews/PR239/explanations/peculiarities.html @@ -0,0 +1,25 @@ + + + + + + Peculiarities | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Peculiarities

What does apply return and why?

apply returns the target geometries returned by f, whatever type/package they are from, but geometries, features or feature collections that wrapped the target are replaced with GeoInterace.jl wrappers with matching GeoInterface.trait to the originals. All non-geointerface iterables become Arrays. 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.

Why do you want me to provide a 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

+ + + + \ No newline at end of file diff --git a/previews/PR239/explanations/winding_order.html b/previews/PR239/explanations/winding_order.html new file mode 100644 index 000000000..d503ae661 --- /dev/null +++ b/previews/PR239/explanations/winding_order.html @@ -0,0 +1,25 @@ + + + + + + GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/previews/PR239/favicon.ico b/previews/PR239/favicon.ico new file mode 100644 index 000000000..468a7af73 Binary files /dev/null and b/previews/PR239/favicon.ico differ diff --git a/previews/PR239/hashmap.json b/previews/PR239/hashmap.json new file mode 100644 index 000000000..5fee5a780 --- /dev/null +++ b/previews/PR239/hashmap.json @@ -0,0 +1 @@ +{"api.md":"BkfD-VZg","call_notes.md":"D1Aj_9mC","experiments_accurate_accumulators.md":"C1U7KOjF","experiments_predicates.md":"C2d7Qctz","explanations_crs.md":"BdkIxIey","explanations_paradigms.md":"B5COLxNa","explanations_peculiarities.md":"Cphx4jNo","explanations_winding_order.md":"0V2FnJmC","index.md":"C-gmTcRS","introduction.md":"FQTtykEQ","source_geometryops.md":"D_V6MfpH","source_geometryopsflexijoinsext_geometryopsflexijoinsext.md":"BtiH8and","source_geometryopslibgeosext_buffer.md":"BIbTaNIz","source_geometryopslibgeosext_geometryopslibgeosext.md":"BX97MBsi","source_geometryopslibgeosext_segmentize.md":"C7teKqJd","source_geometryopslibgeosext_simple_overrides.md":"PL-RH0TF","source_geometryopslibgeosext_simplify.md":"DqcZcvcS","source_geometryopsprojext_geometryopsprojext.md":"CF-5wcua","source_geometryopsprojext_reproject.md":"Cm7Q7Ebj","source_geometryopsprojext_segmentize.md":"DOG0-rtA","source_methods_angles.md":"B9lhXIGg","source_methods_area.md":"42EUHlu6","source_methods_barycentric.md":"BiNAbdvJ","source_methods_buffer.md":"Wvk7coQv","source_methods_centroid.md":"CPAUzyVU","source_methods_clipping_clipping_processor.md":"Ci1IQvdb","source_methods_clipping_coverage.md":"C89Y2dMt","source_methods_clipping_cut.md":"D8NjrBmq","source_methods_clipping_difference.md":"KRJn9sdE","source_methods_clipping_intersection.md":"bcPofDGt","source_methods_clipping_predicates.md":"BJITr_Uq","source_methods_clipping_union.md":"DlTyWPn1","source_methods_convex_hull.md":"DkVMD0Lv","source_methods_distance.md":"DLCaGxsq","source_methods_equals.md":"BsnJrmWL","source_methods_geom_relations_contains.md":"CTaz5DgK","source_methods_geom_relations_coveredby.md":"BNo4Y8zx","source_methods_geom_relations_covers.md":"Ds8aZwNW","source_methods_geom_relations_crosses.md":"-nyWl6CC","source_methods_geom_relations_disjoint.md":"DH85oHhz","source_methods_geom_relations_geom_geom_processors.md":"CmpKdMyB","source_methods_geom_relations_intersects.md":"Z6dLYNxj","source_methods_geom_relations_overlaps.md":"BjDDEk4_","source_methods_geom_relations_touches.md":"C3wGt7mS","source_methods_geom_relations_within.md":"UHYdg8yN","source_methods_orientation.md":"Cx8oaEg5","source_methods_polygonize.md":"CCOlggGo","source_not_implemented_yet.md":"BRH4xbkQ","source_primitives.md":"Dyw7zzpW","source_src_apply.md":"C1jwRpiI","source_src_applyreduce.md":"vbUWBm8w","source_src_geometry_utils.md":"CpDbCg3A","source_src_geometryopscore.md":"BF7KPh3I","source_src_keyword_docs.md":"BxLB2oH3","source_src_other_primitives.md":"BVzBrNUT","source_src_types.md":"BulrghgC","source_transformations_correction_closed_ring.md":"DlzJm4wM","source_transformations_correction_geometry_correction.md":"DSGv3LiX","source_transformations_correction_intersecting_polygons.md":"CLhMqjHy","source_transformations_extent.md":"CrI7_2Lj","source_transformations_flip.md":"CbHFKr9B","source_transformations_forcedims.md":"_lugiyHg","source_transformations_reproject.md":"DZgumE25","source_transformations_segmentize.md":"DQ-jJDsI","source_transformations_simplify.md":"CnWLEC8c","source_transformations_transform.md":"BWclfchr","source_transformations_tuples.md":"BFXQMbxF","source_types.md":"BzRqHlPc","source_utils.md":"Cy9C-nnj","tutorials_creating_geometry.md":"BW0vmesq","tutorials_geodesic_paths.md":"BlU0MlUq","tutorials_spatial_joins.md":"B67GghbG"} diff --git a/previews/PR239/index.html b/previews/PR239/index.html new file mode 100644 index 000000000..c1320ec82 --- /dev/null +++ b/previews/PR239/index.html @@ -0,0 +1,25 @@ + + + + + + What is GeometryOps.jl? | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

GeometryOps.jl

Blazing fast geometry operations in pure Julia

GeometryOps

What is GeometryOps.jl?

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!

How to navigate the docs

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.

  • Tutorials are meant to teach the fundamental concepts behind GeometryOps, and how to perform certain operations.
  • Explanations usually contain little code, and explain in more detail how GeometryOps works.
  • Source code usually contains explanations and examples at the top of the page, followed by annotated source code from that file.
+ + + + \ No newline at end of file diff --git a/previews/PR239/introduction.html b/previews/PR239/introduction.html new file mode 100644 index 000000000..7fa3333df --- /dev/null +++ b/previews/PR239/introduction.html @@ -0,0 +1,25 @@ + + + + + + Introduction | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Introduction

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!

Main concepts

The 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)!

What's this GeoInterface.Wrapper thing?

Write a comment about GeoInterface.Wrapper and why it helps in type stability to guarantee a particular return type.

+ + + + \ No newline at end of file diff --git a/previews/PR239/logo.png b/previews/PR239/logo.png new file mode 100644 index 000000000..8a3120119 Binary files /dev/null and b/previews/PR239/logo.png differ diff --git a/previews/PR239/siteinfo.js b/previews/PR239/siteinfo.js new file mode 100644 index 000000000..317f8612b --- /dev/null +++ b/previews/PR239/siteinfo.js @@ -0,0 +1 @@ +var DOCUMENTER_CURRENT_VERSION = "previews/PR239"; diff --git a/previews/PR239/source/GeometryOps.html b/previews/PR239/source/GeometryOps.html new file mode 100644 index 000000000..a577d7637 --- /dev/null +++ b/previews/PR239/source/GeometryOps.html @@ -0,0 +1,109 @@ + + + + + + GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

GeometryOps.jl

julia
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.

julia
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!

julia
    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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/GeometryOpsFlexiJoinsExt/GeometryOpsFlexiJoinsExt.html b/previews/PR239/source/GeometryOpsFlexiJoinsExt/GeometryOpsFlexiJoinsExt.html new file mode 100644 index 000000000..806cea8ff --- /dev/null +++ b/previews/PR239/source/GeometryOpsFlexiJoinsExt/GeometryOpsFlexiJoinsExt.html @@ -0,0 +1,40 @@ + + + + + + GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content
julia
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.

julia
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

julia
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.

julia
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.

julia
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.

julia
    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.

julia
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!

julia
end

This page was generated using Literate.jl.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/GeometryOpsLibGEOSExt/GeometryOpsLibGEOSExt.html b/previews/PR239/source/GeometryOpsLibGEOSExt/GeometryOpsLibGEOSExt.html new file mode 100644 index 000000000..1e9a9a17c --- /dev/null +++ b/previews/PR239/source/GeometryOpsLibGEOSExt/GeometryOpsLibGEOSExt.html @@ -0,0 +1,55 @@ + + + + + + GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content
julia
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.

julia
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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/GeometryOpsLibGEOSExt/buffer.html b/previews/PR239/source/GeometryOpsLibGEOSExt/buffer.html new file mode 100644 index 000000000..ad2e5754d --- /dev/null +++ b/previews/PR239/source/GeometryOpsLibGEOSExt/buffer.html @@ -0,0 +1,55 @@ + + + + + + GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content
julia
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!

julia
    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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/GeometryOpsLibGEOSExt/segmentize.html b/previews/PR239/source/GeometryOpsLibGEOSExt/segmentize.html new file mode 100644 index 000000000..8abf37461 --- /dev/null +++ b/previews/PR239/source/GeometryOpsLibGEOSExt/segmentize.html @@ -0,0 +1,45 @@ + + + + + + Segmentize | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Segmentize

julia
import GeometryOps: segmentize, apply

This file implements the LibGEOS segmentization method for GeometryOps.

julia
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

julia
@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?

julia
        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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides.html b/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides.html new file mode 100644 index 000000000..3d459fe5b --- /dev/null +++ b/previews/PR239/source/GeometryOpsLibGEOSExt/simple_overrides.html @@ -0,0 +1,70 @@ + + + + + + Simple overrides | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Simple overrides

This file contains simple overrides for GEOS, essentially only those functions which have direct counterparts in LG and only require conversion before calling.

Polygon set operations

Difference

julia
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

Union

julia
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

Intersection

julia
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

Symmetric difference

julia
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

DE-9IM boolean methods

Equals

julia
function GO.equals(::GEOS, geom_a, geom_b)
+    return LG.equals(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Disjoint

julia
function GO.disjoint(::GEOS, geom_a, geom_b)
+    return LG.disjoint(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Touches

julia
function GO.touches(::GEOS, geom_a, geom_b)
+    return LG.touches(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Crosses

julia
function GO.crosses(::GEOS, geom_a, geom_b)
+    return LG.crosses(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Within

julia
function GO.within(::GEOS, geom_a, geom_b)
+    return LG.within(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Contains

julia
function GO.contains(::GEOS, geom_a, geom_b)
+    return LG.contains(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Overlaps

julia
function GO.overlaps(::GEOS, geom_a, geom_b)
+    return LG.overlaps(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Covers

julia
function GO.covers(::GEOS, geom_a, geom_b)
+    return LG.covers(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

CoveredBy

julia
function GO.coveredby(::GEOS, geom_a, geom_b)
+    return LG.coveredby(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Intersects

julia
function GO.intersects(::GEOS, geom_a, geom_b)
+    return LG.intersects(GI.convert(LG, geom_a), GI.convert(LG, geom_b))
+end

Convex hull

julia
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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/GeometryOpsLibGEOSExt/simplify.html b/previews/PR239/source/GeometryOpsLibGEOSExt/simplify.html new file mode 100644 index 000000000..3dd3a3b62 --- /dev/null +++ b/previews/PR239/source/GeometryOpsLibGEOSExt/simplify.html @@ -0,0 +1,53 @@ + + + + + + GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Address potential ambiguities

julia
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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/GeometryOpsProjExt/GeometryOpsProjExt.html b/previews/PR239/source/GeometryOpsProjExt/GeometryOpsProjExt.html new file mode 100644 index 000000000..a8a77eceb --- /dev/null +++ b/previews/PR239/source/GeometryOpsProjExt/GeometryOpsProjExt.html @@ -0,0 +1,32 @@ + + + + + + GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content
julia
module GeometryOpsProjExt
+
+using GeometryOps, Proj
+
+include("reproject.jl")
+include("segmentize.jl")
+
+end

This page was generated using Literate.jl.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/GeometryOpsProjExt/reproject.html b/previews/PR239/source/GeometryOpsProjExt/reproject.html new file mode 100644 index 000000000..5183717c9 --- /dev/null +++ b/previews/PR239/source/GeometryOpsProjExt/reproject.html @@ -0,0 +1,68 @@ + + + + + + GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content
julia
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

julia
        isnothing(source_crs) && throw(ArgumentError("geom has no crs attached. Pass a `source_crs` keyword"))

Otherwise reproject

julia
        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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/GeometryOpsProjExt/segmentize.html b/previews/PR239/source/GeometryOpsProjExt/segmentize.html new file mode 100644 index 000000000..8bed57560 --- /dev/null +++ b/previews/PR239/source/GeometryOpsProjExt/segmentize.html @@ -0,0 +1,55 @@ + + + + + + GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

This holds the segmentize geodesic functionality.

julia
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...

julia
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.

julia
    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.

julia
    push!(new_coords, (x2, y2))
+    return nothing
+end

This page was generated using Literate.jl.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/angles.html b/previews/PR239/source/methods/angles.html new file mode 100644 index 000000000..1a96542e8 --- /dev/null +++ b/previews/PR239/source/methods/angles.html @@ -0,0 +1,148 @@ + + + + + + Angles | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Angles

julia
export angles

What is angles?

Angles are the angles formed by a given geometries line segments, if it has line segments.

To provide an example, consider this rectangle:

julia
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.

julia
GO.angles(rect)  # [90, 90, 90, 90]
4-element Vector{Float64}:
+ 90.0
+ 90.0
+ 90.0
+ 90.0

Implementation

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!

julia
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

julia
_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

julia
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

julia
    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

julia
    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

julia
    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.

julia
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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/area.html b/previews/PR239/source/methods/area.html new file mode 100644 index 000000000..12fbd98c5 --- /dev/null +++ b/previews/PR239/source/methods/area.html @@ -0,0 +1,111 @@ + + + + + + Area and signed area | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Area and signed area

julia
export area, signed_area

What is area? What is 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:

julia
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:

julia
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.

julia
GO.signed_area(rect)  # -1.0
-1.0

Implementation

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

julia
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

julia
_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...

julia
_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

julia
_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

julia
    for hole in GI.gethole(poly)
+        area -= abs(_signed_area(T, hole))
+    end

Winding of exterior ring determines sign

julia
    return area * sign(s_area)
+end

One term of the shoelace area formula

julia
_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

julia
    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.

julia
        if first
+            p1 = pfirst = p2
+            first = false
+            continue
+        end

Accumulate the area into area

julia
        area += _area_component(p1, p2)
+        p1 = p2
+    end

Complete the last edge. If the first and last where the same this will be zero

julia
    p2 = pfirst
+    area += _area_component(p1, p2)
+    return T(area / 2)
+end

This page was generated using Literate.jl.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/barycentric.html b/previews/PR239/source/methods/barycentric.html new file mode 100644 index 000000000..49f17aed0 --- /dev/null +++ b/previews/PR239/source/methods/barycentric.html @@ -0,0 +1,439 @@ + + + + + + Barycentric coordinates | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Barycentric coordinates

julia
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.

In the case of a triangle, barycentric coordinates are a set of three numbers (λ1,λ2,λ3), each associated with a vertex of the triangle. Any point within the triangle can be expressed as a weighted average of the vertices, where the weights are the barycentric coordinates. The weights sum to 1, and each is non-negative.

For a polygon with n vertices, generalized barycentric coordinates are a set of n numbers (λ1,λ2,...,λn), each associated with a vertex of the polygon. Any point within the polygon can be expressed as a weighted average of the vertices, where the weights are the generalized barycentric coordinates.

As with the triangle case, the weights sum to 1, and each is non-negative.

Example

This example was taken from this page of CGAL's documentation.

julia
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

Barycentric-coordinate API

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.

julia
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.

julia
"""
+    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.

julia
"""
+    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.

julia
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.

julia
"""
+    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.

julia
"""
+    _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
julia
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...

julia
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!

julia
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.

julia
    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

julia
        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,

julia
        interpolated_value += values[current_index] * wᵢ

then the accumulators for total weight and current index.

julia
        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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/buffer.html b/previews/PR239/source/methods/buffer.html new file mode 100644 index 000000000..7c97d5d89 --- /dev/null +++ b/previews/PR239/source/methods/buffer.html @@ -0,0 +1,35 @@ + + + + + + Buffer | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Buffer

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.

julia
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!

julia
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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/centroid.html b/previews/PR239/source/methods/centroid.html new file mode 100644 index 000000000..447649a24 --- /dev/null +++ b/previews/PR239/source/methods/centroid.html @@ -0,0 +1,117 @@ + + + + + + Centroid | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Centroid

julia
export centroid, centroid_and_length, centroid_and_area

What is the centroid?

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':

julia
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):

julia
cent = GO.centroid(cshape)
+scatter!(GI.x(cent), GI.y(cent), color = :red)
+f

Implementation

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.

julia
"""
+    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

julia
    xcentroid = T(0)
+    ycentroid = T(0)
+    length = T(0)
+    point₁ = GI.getpoint(geom, 1)

Loop over line segments of line string

julia
    for point₂ in GI.getpoint(geom)

Calculate length of line segment

julia
        length_component = sqrt(
+            (GI.x(point₂) - GI.x(point₁))^2 +
+            (GI.y(point₂) - GI.y(point₁))^2
+        )

Accumulate the line segment length into length

julia
        length += length_component

Weighted average of line segment centroids

julia
        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

julia
        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

julia
    @assert(
+        GI.getpoint(geom, 1) == GI.getpoint(geom, GI.ngeom(geom)),
+        "centroid_and_area should only be used with closed geometries"
+    )

Initialize starting values

julia
    xcentroid = T(0)
+    ycentroid = T(0)
+    area = T(0)
+    point₁ = GI.getpoint(geom, 1)

Loop over line segments of linear ring

julia
    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

julia
        area += area_component

Weighted average of centroid components

julia
        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

julia
        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

julia
    (xcentroid, ycentroid), area = centroid_and_area(GI.getexterior(geom), T)

Weight exterior centroid by area

julia
    xcentroid *= area
+    ycentroid *= area

Loop over any holes within the polygon

julia
    for hole in GI.gethole(geom)

Hole polygon's centroid and area

julia
        (xinterior, yinterior), interior_area = centroid_and_area(hole, T)

Accumulate the area component into area

julia
        area -= interior_area

Weighted average of centroid components

julia
        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.

julia
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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/clipping/clipping_processor.html b/previews/PR239/source/methods/clipping/clipping_processor.html new file mode 100644 index 000000000..7dd28d678 --- /dev/null +++ b/previews/PR239/source/methods/clipping/clipping_processor.html @@ -0,0 +1,532 @@ + + + + + + Polygon clipping helpers | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Polygon clipping helpers

This file contains the shared helper functions for the polygon clipping functionalities.

This enum defines which side of an edge a point is on

julia
@enum PointEdgeSide left=1 right=2 unknown=3

Constants assigned for readability

julia
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

julia
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.

julia
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

julia
    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

julia
    _classify_crossing!(T, a_list, b_list; exact)

Flag the entry and exits

julia
    _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

julia
    _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.

julia
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

julia
    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

julia
        new_point = PolyNode{T}(;point = a_pt1)
+        a_count += 1
+        push!(a_list, new_point)

Find intersections with edges of poly_b

julia
        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

julia
            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

julia
                    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

julia
                    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

julia
                    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

julia
        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.

julia
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

julia
    sort!(a_idx_list, by = x-> a_list[x].neighbor + a_list[x].fracs[2])

Initialize needed values and lists

julia
    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

julia
    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

julia
                    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.

julia
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

julia
    a_prev = a_list[end - 1]
+    curr_pt = a_list[end]
+    i = napts

keep track of unmatched bouncing chains

julia
    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

julia
    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

julia
            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

julia
            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

julia
            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

julia
            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

julia
                    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

julia
                    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

julia
            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

julia
    if unmatched_end_chain_edge != unknown
+        crossing = unmatched_end_chain_edge != start_chain_edge

update end of chain with endpoint and crossing / bouncing tags

julia
        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

julia
        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

julia
_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

julia
    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

julia
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

julia
_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.

julia
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

julia
    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

julia
    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

julia
                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.

julia
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

julia
    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.

julia
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

julia
    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

julia
        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

julia
        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

julia
            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

julia
                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

julia
                curr = curr_list[idx]
+                push!(pt_list, curr.point)
+                if (curr.crossing || curr.endpoint != not_endpoint)

Keep track of processed intersection points

julia
                    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

julia
            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

julia
_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.

julia
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

julia
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

julia
    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)

julia
            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

julia
                        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

julia
                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

julia
    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.

julia
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

julia
    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

julia
            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

julia
    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

julia
    @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

julia
            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

julia
                    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

julia
            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

julia
            deleteat!(ring.geom, remove_idx)

Check if enough points are left to form a polygon

julia
            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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/clipping/coverage.html b/previews/PR239/source/methods/clipping/coverage.html new file mode 100644 index 000000000..8fa9f13e4 --- /dev/null +++ b/previews/PR239/source/methods/clipping/coverage.html @@ -0,0 +1,247 @@ + + + + + + GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content
julia
export coverage

What is 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:

julia
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.

julia
GO.coverage(rect, xmin, xmax, ymin, ymax)
1.0

Implementation

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

julia
const _COVERAGE_TARGETS = TraitTarget{Union{GI.PolygonTrait,GI.AbstractCurveTrait,GI.MultiPointTrait,GI.PointTrait}}()

Wall types for coverage

julia
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

julia
_coverage(::Type{T}, ::GI.AbstractGeometryTrait, geom, xmin, xmax, ymin, ymax; kwargs...) where T = zero(T)

Polygons

julia
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

julia
    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

julia
    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

julia
    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

julia
        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

julia
        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

julia
        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

julia
        (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

julia
        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

julia
    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

julia
    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

julia
_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.

julia
_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

julia
        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

julia
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

julia
        connect_area += _partial_edge_out_area(p1, xmin, xmax, ymin, ymax, wall1)

Any intermediate walls (full length)

julia
        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

julia
        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

julia
_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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/clipping/cut.html b/previews/PR239/source/methods/clipping/cut.html new file mode 100644 index 000000000..a7fe6f764 --- /dev/null +++ b/previews/PR239/source/methods/clipping/cut.html @@ -0,0 +1,111 @@ + + + + + + Polygon cutting | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Polygon cutting

julia
export cut

What is 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:

julia
import GeoInterface as GI, GeometryOps as GO
+using CairoMakie
+using Makie
+
+poly = GI.Polygon([[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0), (0.0, 0.0)]])
+line = GI.Line([(5.0, -5.0), (5.0, 15.0)])
+cut_polys = GO.cut(poly, line)
+
+f, a, p1 = Makie.poly(collect(GI.getpoint(cut_polys[1])); color = (:blue, 0.5))
+Makie.poly!(collect(GI.getpoint(cut_polys[2])); color = (:orange, 0.5))
+Makie.lines!(GI.getpoint(line); color = :black)
+f

Implementation

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.

julia
"""
+    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

julia
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

julia
    if n_intr_pts < 2 || isodd(n_intr_pts)
+        return [tuples(poly)]
+    end

Cut polygon by line

julia
    cut_coords = _cut(T, ext_poly, line, poly_list, intr_list, n_intr_pts; exact)

Close coords and create polygons

julia
    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

julia
    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

julia
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

julia
    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

julia
    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

julia
    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

julia
            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

julia
            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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/clipping/difference.html b/previews/PR239/source/methods/clipping/difference.html new file mode 100644 index 000000000..77d7376c8 --- /dev/null +++ b/previews/PR239/source/methods/clipping/difference.html @@ -0,0 +1,190 @@ + + + + + + Difference Polygon Clipping | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Difference Polygon Clipping

julia
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

julia
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

julia
    ext_a = GI.getexterior(poly_a)
+    ext_b = GI.getexterior(poly_b)

Find the difference of the exterior of the polygons

julia
    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

julia
    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!

julia
        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.

julia
    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

julia
    _remove_collinear_points!(polys, remove_idx, poly_a, poly_b)
+    return polys
+end

Helper functions for Differences with Greiner and Hormann Polygon Clipping

julia
#= 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

julia
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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/clipping/intersection.html b/previews/PR239/source/methods/clipping/intersection.html new file mode 100644 index 000000000..6b1f13a8f --- /dev/null +++ b/previews/PR239/source/methods/clipping/intersection.html @@ -0,0 +1,407 @@ + + + + + + Geometry Intersection | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Geometry Intersection

julia
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

julia
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

julia
_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'

julia
    ext_a = GI.getexterior(poly_a)
+    ext_b = GI.getexterior(poly_b)

Then we find the intersection of the exteriors

julia
    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.

julia
    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

julia
    _remove_collinear_points!(polys, remove_idx, poly_a, poly_b)
+    return polys
+end

Helper functions for Intersections with Greiner and Hormann Polygon Clipping

julia
#= 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

julia
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

julia
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

julia
    result = Tuple{T, T}[]

Check if the geometries extents even overlap

julia
    Extents.intersects(GI.extent(a), GI.extent(b)) || return result

Create a list of edges from the two input geometries

julia
    edges_a, edges_b = map(sort!  to_edges, (a, b))

Loop over pairs of edges and add any unique intersection points to results

julia
    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

julia
    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

julia
    (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

julia
    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

julia
    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)

julia
    if a1_orient == a2_orient == b1_orient == b2_orient == 0

Intersection is collinear if all endpoints lie on the same line

julia
        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

julia
        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

julia
        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

julia
    line_orient, intr1, intr2 = no_intr_result

Determine collinear line overlaps

julia
    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

julia
    a_dist, b_dist = distance(a1, a2, T), distance(b1, b2, T)

Set collinear intersection points if they exist

julia
    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

julia
    (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

julia
    (b1x, b1y), (b2x, b2y) = _tuple_point(b1, T), _tuple_point(b2, T)
+    Δbx, Δby = b2x - b1x, b2y - b1y

Differences between starting points

julia
    Δbax = b1x - a1x
+    Δbay = b1y - a1y
+    a_cross_b = Δax * Δby - Δay * Δbx

Determine α value where 0 < α < 1 and β value where 0 < β < 1

julia
    α = _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

julia
    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

julia
function _nearest_endpoint(::Type{T}, a1, a2, b1, b2) where T

Create lines from segments and calculate segment length

julia
    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

julia
    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

julia
    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

julia
    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

julia
    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

julia
    return _tuple_point(min_pt, T), α, β
+end

Return value of x/y clamped between ϵ and 1 - ϵ

julia
_clamped_frac(x::T, y::T, ϵ = zero(T)) where T = clamp(x / y, ϵ, one(T) - ϵ)

This page was generated using Literate.jl.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/clipping/predicates.html b/previews/PR239/source/methods/clipping/predicates.html new file mode 100644 index 000000000..dc8c722ae --- /dev/null +++ b/previews/PR239/source/methods/clipping/predicates.html @@ -0,0 +1,68 @@ + + + + + + If we want to inject adaptivity, we would do something like: | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content
julia
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.

julia
    _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.

julia
    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.

julia
    @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.

julia
    function _cross(::_False, a, b)
+        c_t1 = GI.x(a) * GI.y(b)
+        c_t2 = GI.y(a) * GI.x(b)
+        c_val = if isapprox(c_t1, c_t2)
+            0
+        else
+            sign(c_t1 - c_t2)
+        end
+        return c_val
+    end
+
+end
+
+import .Predicates

If we want to inject adaptivity, we would do something like:

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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/clipping/union.html b/previews/PR239/source/methods/clipping/union.html new file mode 100644 index 000000000..4252dd52b --- /dev/null +++ b/previews/PR239/source/methods/clipping/union.html @@ -0,0 +1,275 @@ + + + + + + Union Polygon Clipping | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Union Polygon Clipping

julia
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

julia
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

julia
    ext_a = GI.getexterior(poly_a)
+    ext_b = GI.getexterior(poly_b)

Then, I get the union of the exteriors

julia
    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

julia
    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

julia
        @views append!(polys[1].geom, (GI.getexterior(p) for p in polys[2:end]))
+        keepat!(polys, 1)
+    end

Add in holes

julia
    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

julia
    _remove_collinear_points!(polys, [false], poly_a, poly_b)
+    return polys
+end

Helper functions for Unions with Greiner and Hormann Polygon Clipping

julia
#= 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

julia
        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

julia
            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

julia
            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

julia
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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/convex_hull.html b/previews/PR239/source/methods/convex_hull.html new file mode 100644 index 000000000..5d71e52ba --- /dev/null +++ b/previews/PR239/source/methods/convex_hull.html @@ -0,0 +1,81 @@ + + + + + + Convex hull | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Convex hull

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.

Example

Simple hull

julia
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

Convex hull of the USA

julia
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

Investigating the winding order

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

julia
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

Implementation

julia
"""
+    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.

julia
convex_hull(geometries) = convex_hull(MonotoneChainMethod(), geometries)

TODO: have this respect the CRS by pulling it out of geometries.

julia
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.

julia
    points = collect(flatten(tuples, GI.PointTrait, geometries))

Compute the convex hull using DelTri (shorthand for DelaunayTriangulation.jl).

julia
    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.

julia
    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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/distance.html b/previews/PR239/source/methods/distance.html new file mode 100644 index 000000000..ef340e664 --- /dev/null +++ b/previews/PR239/source/methods/distance.html @@ -0,0 +1,205 @@ + + + + + + Distance and signed distance | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Distance and signed distance

julia
export distance, signed_distance

What is distance? What is 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:

julia
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.

julia
(
+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:

julia
xrange = yrange = LinRange(-0.5, 1.5, 300)
+f, a, p = heatmap(xrange, yrange, GO.signed_distance.(Point2f.(xrange, yrange'), Ref(rect)); colormap = :RdBu, colorrange = (-0.75, 0.75))
+a.aspect = DataAspect(); Colorbar(f[1, 2], p, label = "Signed distance"); lines!(a, GI.convert(GO.GeometryBasics, rect)); f

Implementation

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.

julia
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

julia
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

julia
_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

julia
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

julia
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)

julia
function _signed_distance(
+    ::Type{T}, ptrait::GI.PointTrait, point, gtrait::GI.AbstractGeometryTrait, geom
+) where T
+    _distance(T, ptrait, point, gtrait, geom)
+end

Point-Polygon

julia
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

julia
end

Returns the Euclidean distance between two points.

julia
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

julia
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.

julia
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.

julia
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.

julia
_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.

julia
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.

julia
function _distance_curve(::Type{T}, point, curve; close_curve = false) where T

see if linear ring has explicitly repeated last point in coordinates

julia
    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

julia
    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.

julia
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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/equals.html b/previews/PR239/source/methods/equals.html new file mode 100644 index 000000000..ad8c967b6 --- /dev/null +++ b/previews/PR239/source/methods/equals.html @@ -0,0 +1,289 @@ + + + + + + Equals | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Equals

julia
export equals

What is 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:

julia
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:

julia
GO.equals(l1, l2)  # returns false
false

Implementation

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.

julia
"""
+    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

julia
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

julia
    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

julia
    n1 -= c1_repeat_point ? 1 : 0
+    n2 -= c2_repeat_point ? 1 : 0
+    n1 == n2 || return false
+    n1 == 0 && return true

Find offset between curves

julia
    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

julia
    isnothing(jstart) && return false

found match for only point

julia
    n1 == 1 && return true

if isn't closed and first or last point don't match, not same curve

julia
    !closed_type1 && (jstart != 1 && jstart != n1) && return false

Check if curves are going in same direction

julia
    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

julia
    n1 == 2 && return same_direction

Check all remaining points are the same wrapping around line

julia
    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

julia
    _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

julia
    GI.nhole(geom_a) == GI.nhole(geom_b) || return false

Check if holes are equal

julia
    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

julia
    GI.npolygon(geom_a) == GI.npolygon(geom_b) || return false

Check if each polygon has a matching polygon

julia
    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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/geom_relations/contains.html b/previews/PR239/source/methods/geom_relations/contains.html new file mode 100644 index 000000000..16ce2df2b --- /dev/null +++ b/previews/PR239/source/methods/geom_relations/contains.html @@ -0,0 +1,57 @@ + + + + + + Contains | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Contains

julia
export contains

What is 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:

julia
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.

julia
GO.contains(l1, l2)  # returns true
+GO.contains(l2, l1)  # returns false
false

Implementation

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.

julia
"""
+    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

julia
true
+```
+"""
+contains(g1, g2) = GeometryOps.within(g2, g1)

This page was generated using Literate.jl.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/geom_relations/coveredby.html b/previews/PR239/source/methods/geom_relations/coveredby.html new file mode 100644 index 000000000..c3e870bc1 --- /dev/null +++ b/previews/PR239/source/methods/geom_relations/coveredby.html @@ -0,0 +1,207 @@ + + + + + + CoveredBy | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

CoveredBy

julia
export coveredby

What is 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:

julia
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.

julia
GO.coveredby(p1, l1)  # true
true

Implementation

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.

julia
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

julia
true
+```
+"""
+coveredby(g1, g2) = _coveredby(trait(g1), g1, trait(g2), g2)

Convert features to geometries

julia
_coveredby(::GI.FeatureTrait, g1, ::Any, g2) = coveredby(GI.geometry(g1), g2)
+_coveredby(::Any, g1, t2::GI.FeatureTrait, g2) = coveredby(g1, GI.geometry(g2))
+_coveredby(::FeatureTrait, g1, ::FeatureTrait, g2) = coveredby(GI.geometry(g1), GI.geometry(g2))

Points coveredby geometries

Point is coveredby another point if those points are equal

julia
_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

julia
_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

julia
_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

julia
_coveredby(
+    ::GI.PointTrait, g1,
+    ::GI.PolygonTrait, g2,
+) = _point_polygon_process(
+    g1, g2;
+    COVEREDBY_ALLOWS...,
+    COVEREDBY_EXACT...,
+)

Points cannot cover any geometry other than points

julia
_coveredby(
+    ::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    ::GI.PointTrait, g2,
+) = false

Lines coveredby geometries

julia
#= 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,
+)

Rings covered by geometries

julia
#= 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,
+)

Polygons covered by geometries

julia
#= 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

julia
_coveredby(
+    ::GI.PolygonTrait, g1,
+    ::GI.AbstractCurveTrait, g2,
+) = false

Geometries coveredby multi-geometry/geometry collections

julia
#= Geometry is covered by a multi-geometry or a collection if one of the elements
+of the collection cover the geometry. =#
+function _coveredby(
+    ::Union{GI.PointTrait, GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    ::Union{
+        GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+        GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+    }, g2,
+)
+    for sub_g2 in GI.getgeom(g2)
+        coveredby(g1, sub_g2) && return true
+    end
+    return false
+end

Multi-geometry/geometry collections coveredby geometries

julia
#= 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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/geom_relations/covers.html b/previews/PR239/source/methods/geom_relations/covers.html new file mode 100644 index 000000000..557e15b1d --- /dev/null +++ b/previews/PR239/source/methods/geom_relations/covers.html @@ -0,0 +1,57 @@ + + + + + + Covers | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Covers

julia
export covers

What is 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:

julia
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

julia
GO.covers(l1, p1)  # returns true
+GO.covers(p1, l1)  # returns false
false

Implementation

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.

julia
"""
+    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

julia
true
+```
+"""
+covers(g1, g2)::Bool = GeometryOps.coveredby(g2, g1)

This page was generated using Literate.jl.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/geom_relations/crosses.html b/previews/PR239/source/methods/geom_relations/crosses.html new file mode 100644 index 000000000..8f08ce933 --- /dev/null +++ b/previews/PR239/source/methods/geom_relations/crosses.html @@ -0,0 +1,144 @@ + + + + + + Crossing checks | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Crossing checks

julia
"""
+     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

julia
```
+"""
+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

julia
    cross = dxc * dy1 - dyc * dx1
+    cross != 0 && return false

Will constprop optimise these away?

julia
    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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/geom_relations/disjoint.html b/previews/PR239/source/methods/geom_relations/disjoint.html new file mode 100644 index 000000000..3b0b82b1b --- /dev/null +++ b/previews/PR239/source/methods/geom_relations/disjoint.html @@ -0,0 +1,202 @@ + + + + + + Disjoint | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Disjoint

julia
export disjoint

What is 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:

julia
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.

julia
GO.disjoint(l1, l2)  # returns true
true

Implementation

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.

julia
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

julia
true
+```
+"""
+disjoint(g1, g2) = _disjoint(trait(g1), g1, trait(g2), g2)

Convert features to geometries

julia
_disjoint(::FeatureTrait, g1, ::Any, g2) = disjoint(GI.geometry(g1), g2)
+_disjoint(::Any, g1, ::FeatureTrait, g2) = disjoint(g1, geometry(g2))
+_disjoint(::FeatureTrait, g1, ::FeatureTrait, g2) = disjoint(GI.geometry(g1), GI.geometry(g2))

Point disjoint geometries

Point is disjoint from another point if the points are not equal.

julia
_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.

julia
_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.

julia
_disjoint(
+    ::GI.PointTrait, g1,
+    ::GI.LinearRingTrait, g2,
+) = _point_curve_process(
+    g1, g2;
+    DISJOINT_ALLOWS...,
+    closed_curve = true,
+)
+
+#= Point is disjoint from a polygon if it is not on any edges, vertices, or
+within the polygon's interior. =#
+_disjoint(
+    ::GI.PointTrait, g1,
+    ::GI.PolygonTrait, g2,
+) = _point_polygon_process(
+    g1, g2;
+    DISJOINT_ALLOWS...,
+    DISJOINT_EXACT...,
+)
+
+#= Geometry is disjoint from a point if the point is not in the interior or on
+the boundary of the geometry. =#
+_disjoint(
+    trait1::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    trait2::GI.PointTrait, g2,
+) = _disjoint(trait2, g2, trait1, g1)

Lines disjoint geometries

julia
#= 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)

Rings disjoint geometries

julia
#= Linearrings is disjoint from another linearring if they do not share any
+interior edge/vertex points or boundary points.=#
+_disjoint(
+    ::GI.LinearRingTrait, g1,
+    ::GI.LinearRingTrait, g2,
+) = _line_curve_process(
+    g1, g2;
+    DISJOINT_CURVE_ALLOWS...,
+    DISJOINT_REQUIRES...,
+    DISJOINT_EXACT...,
+    closed_line = true,
+    closed_curve = true,
+)
+
+#= Linearring is disjoint from a polygon if the interior and boundary points of
+the ring are not in the polygon's interior or on the polygon's boundary. =#
+_disjoint(
+    ::GI.LinearRingTrait, g1,
+    ::GI.PolygonTrait, g2,
+) = _line_polygon_process(
+    g1, g2;
+    DISJOINT_ALLOWS...,
+    DISJOINT_REQUIRES...,
+    DISJOINT_EXACT...,
+    closed_line = true,
+)

Polygon disjoint geometries

julia
#= Polygon is disjoint from another polygon if they do not share any edges or
+vertices and if their interiors do not intersect, excluding any holes. =#
+_disjoint(
+    ::GI.PolygonTrait, g1,
+    ::GI.PolygonTrait, g2,
+) = _polygon_polygon_process(
+    g1, g2;
+    DISJOINT_ALLOWS...,
+    DISJOINT_REQUIRES...,
+    DISJOINT_EXACT...,
+)

Geometries disjoint multi-geometry/geometry collections

julia
#= Geometry is disjoint from a multi-geometry or a collection if all of the
+elements of the collection are disjoint from the geometry. =#
+function _disjoint(
+    ::Union{GI.PointTrait, GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    ::Union{
+        GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+        GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+    }, g2,
+)
+    for sub_g2 in GI.getgeom(g2)
+        !disjoint(g1, sub_g2) && return false
+    end
+    return true
+end

Multi-geometry/geometry collections coveredby geometries

julia
#= 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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/geom_relations/geom_geom_processors.html b/previews/PR239/source/methods/geom_relations/geom_geom_processors.html new file mode 100644 index 000000000..4a17334ae --- /dev/null +++ b/previews/PR239/source/methods/geom_relations/geom_geom_processors.html @@ -0,0 +1,461 @@ + + + + + + Line-curve interaction | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Line-curve interaction

julia
#= 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.

julia
function _point_curve_process(
+    point, curve;
+    in_allow, on_allow, out_allow,
+    closed_curve = false,
+)

Determine if curve is closed

julia
    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

julia
    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.

julia
function _point_polygon_process(
+    point, polygon;
+    in_allow, on_allow, out_allow, exact,
+)

Check interaction of geom with polygon's exterior boundary

julia
    ext_val = _point_filled_curve_orientation(point, GI.getexterior(polygon); exact)

If a point is outside, it isn't interacting with any holes

julia
    ext_val == point_out && return out_allow

if a point is on an external boundary, it isn't interacting with any holes

julia
    ext_val == point_on && return on_allow

If geom is within the polygon, need to check interactions with holes

julia
    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

julia
        hole_val == point_in && return out_allow

If a point in on a hole edge, it is on the edge of the polygon

julia
        hole_val == point_on && return on_allow
+    end

Point is within external boundary and on in/on any holes

julia
    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.

julia
@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

julia
    in_req_met = !in_require
+    on_req_met = !on_require
+    out_req_met = !out_require

Determine curve endpoints

julia
    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

julia
    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

julia
        for j in (closed_curve ? 1 : 2):nc
+            c_end = _tuple_point(GI.getpoint(curve, j))

Check if line and curve segments meet

julia
            seg_val, intr1, _ = _intersection_point(Float64, (l_start, l_end), (c_start, c_end); exact)

If segments are co-linear

julia
            if seg_val == line_over
+                !over_allow && return false

at least one point in, meets requirements

julia
                in_req_met = true
+                point_val = _point_segment_orientation(l_start, c_start, c_end)

If entire segment isn't covered, consider remaining section

julia
                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

julia
                    (_, (α, β)) = 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

julia
                        if (!cross_allow || !over_allow) && α != 0 && β != 0

Find next pieces of hinge to see if line and curve cross

julia
                            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

julia
                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.

julia
@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

julia
    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

julia
    !in_curve && return in_req_met && on_req_met && out_req_met

Loop over polygon holes

julia
    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.

julia
@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

julia
    ext1 = GI.getexterior(poly1)
+    ext2 = GI.getexterior(poly2)

Check if exterior of poly1 is in polygon 2

julia
    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

julia
        _, _, 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

julia
    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

julia
            _, _, e2_out_h1 = _line_filled_curve_interactions(
+                ext2, h1;
+                exact, closed_line = true,
+            )

hole encompasses all of poly2

julia
            !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

julia
    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.

julia
function _point_segment_orientation(
+    point, start, stop;
+    in::T = point_in, on::T = point_on, out::T = point_out,
+) where {T}

Parse out points

julia
    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

julia
        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.

julia
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.

julia
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

julia
    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

julia
    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

julia
    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

julia
        in_curve && on_curve && out_curve && break

Check next segment of line against curve

julia
        for j in 1:nc
+            c_end = _tuple_point(GI.getpoint(curve, j))

Check if two line and curve segments meet

julia
            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

julia
                on_curve = true
+                if seg_val == line_cross

When crossing boundary, line is both in and out of curve

julia
                    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

julia
                        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.

julia
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

julia
    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)

julia
@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

julia
        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

julia
        true, false
+    end
+    return skip, returnval
+end

This page was generated using Literate.jl.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/geom_relations/intersects.html b/previews/PR239/source/methods/geom_relations/intersects.html new file mode 100644 index 000000000..929cdeb67 --- /dev/null +++ b/previews/PR239/source/methods/geom_relations/intersects.html @@ -0,0 +1,51 @@ + + + + + + Intersection checks | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Intersection checks

julia
export intersects

What is 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:

julia
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.

julia
GO.intersects(line1, line2)  # true
true

Implementation

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.

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
+
+```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

julia
true
+```
+"""
+intersects(geom1, geom2) = !disjoint(geom1, geom2)

This page was generated using Literate.jl.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/geom_relations/overlaps.html b/previews/PR239/source/methods/geom_relations/overlaps.html new file mode 100644 index 000000000..7743e0a5e --- /dev/null +++ b/previews/PR239/source/methods/geom_relations/overlaps.html @@ -0,0 +1,236 @@ + + + + + + Overlaps | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Overlaps

julia
export overlaps

What is 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:

julia
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:

julia
GO.overlaps(l1, l2)  # true
true

Implementation

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.

julia
"""
+    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

julia
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

julia
    seg_val, _, _ = _intersection_point(Float64, (a1, a2), (b1, b2); exact)

one end point is outside of other segment

julia
    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

julia
function _point_on_seg(point, start, stop)

Parse out points

julia
    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

julia
    cross = (x - x1) * Δyl - (y - y1) * Δxl
+    if cross == 0  # point is on line extending to infinity

is line between endpoints

julia
        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

julia
    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.

julia
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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/geom_relations/touches.html b/previews/PR239/source/methods/geom_relations/touches.html new file mode 100644 index 000000000..67ad8fec4 --- /dev/null +++ b/previews/PR239/source/methods/geom_relations/touches.html @@ -0,0 +1,198 @@ + + + + + + Touches | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Touches

julia
export touches

What is 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:

julia
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.

julia
GO.touches(l1, l2)  # true
true

Implementation

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.

julia
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

julia
true
+```
+"""
+touches(g1, g2)::Bool = _touches(trait(g1), g1, trait(g2), g2)

Convert features to geometries

julia
_touches(::GI.FeatureTrait, g1, ::Any, g2) = touches(GI.geometry(g1), g2)
+_touches(::Any, g1, t2::GI.FeatureTrait, g2) = touches(g1, GI.geometry(g2))
+_touches(::FeatureTrait, g1, ::FeatureTrait, g2) = touches(GI.geometry(g1), GI.geometry(g2))

Point touches geometries

Point cannot touch another point as if they are equal, interiors interact

julia
_touches(
+    ::GI.PointTrait, g1,
+    ::GI.PointTrait, g2,
+) = false

Point touches a linestring if it equal to the first of last point of the line

julia
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

julia
_touches(
+    ::GI.PointTrait, g1,
+    ::GI.LinearRingTrait, g2,
+) = false

Point touches a polygon if it is on the boundary of that polygon

julia
_touches(
+    ::GI.PointTrait, g1,
+    ::GI.PolygonTrait, g2,
+) = _point_polygon_process(
+    g1, g2;
+    TOUCHES_POINT_ALLOWED...,
+    TOUCHES_EXACT...,
+)
+
+#= Geometry touches a point if the point is on the geometry boundary. =#
+_touches(
+    trait1::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    trait2::GI.PointTrait, g2,
+) = _touches(trait2, g2, trait1, g1)

Lines touching geometries

julia
#= 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,
+)

Rings touch geometries

julia
#= 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,
+)

Polygons touch geometries

julia
#= 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...,
+)

Geometries touch multi-geometry/geometry collections

julia
#= Geometry touch a multi-geometry or a collection if the geometry touches at
+least one of the elements of the collection. =#
+function _touches(
+    ::Union{GI.PointTrait, GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    ::Union{
+        GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+        GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+    }, g2,
+)
+    for sub_g2 in GI.getgeom(g2)
+        !touches(g1, sub_g2) && return false
+    end
+    return true
+end

Multi-geometry/geometry collections cross geometries

julia
#= 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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/geom_relations/within.html b/previews/PR239/source/methods/geom_relations/within.html new file mode 100644 index 000000000..e728e5eea --- /dev/null +++ b/previews/PR239/source/methods/geom_relations/within.html @@ -0,0 +1,217 @@ + + + + + + Within | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Within

julia
export within

What is 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:

julia
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

julia
GO.within(l1, l2)  # false
+GO.within(l2, l1)  # true
true

Implementation

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.

julia
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

julia
true
+```
+"""
+within(g1, g2) = _within(trait(g1), g1, trait(g2), g2)

Convert features to geometries

julia
_within(::GI.FeatureTrait, g1, ::Any, g2) = within(GI.geometry(g1), g2)
+_within(::Any, g1, t2::GI.FeatureTrait, g2) = within(g1, GI.geometry(g2))
+_within(::FeatureTrait, g1, ::FeatureTrait, g2) = within(GI.geometry(g1), GI.geometry(g2))

Points within geometries

Point is within another point if those points are equal.

julia
_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.

julia
_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

julia
_within(
+    ::Union{GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    ::GI.PointTrait, g2,
+) = false

Lines within geometries

julia
#= Linestring is within another linestring if their interiors intersect and no
+points of the first line are in the exterior of the second line. =#
+_within(
+    ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+    ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _line_curve_process(
+    g1, g2;
+    WITHIN_CURVE_ALLOWS...,
+    WITHIN_REQUIRES...,
+    WITHIN_EXACT...,
+    closed_line = false,
+    closed_curve = false,
+)
+
+#= Linestring is within a linear ring if their interiors intersect and no points
+of the line are in the exterior of the ring. =#
+_within(
+    ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+    ::GI.LinearRingTrait, g2,
+) = _line_curve_process(
+    g1, g2;
+    WITHIN_CURVE_ALLOWS...,
+    WITHIN_REQUIRES...,
+    WITHIN_EXACT...,
+    closed_line = false,
+    closed_curve = true,
+)
+
+#= Linestring is within a polygon if their interiors intersect and no points of
+the line are in the exterior of the polygon, although they can be on an edge. =#
+_within(
+    ::Union{GI.LineTrait, GI.LineStringTrait}, g1,
+    ::GI.PolygonTrait, g2,
+) = _line_polygon_process(
+    g1, g2;
+    WITHIN_POLYGON_ALLOWS...,
+    WITHIN_REQUIRES...,
+    WITHIN_EXACT...,
+    closed_line = false,
+)

Rings covered by geometries

julia
#= Linearring is within a linestring if their interiors intersect and no points
+of the ring are in the exterior of the line. =#
+_within(
+    ::GI.LinearRingTrait, g1,
+    ::Union{GI.LineTrait, GI.LineStringTrait}, g2,
+) = _line_curve_process(
+    g1, g2;
+    WITHIN_CURVE_ALLOWS...,
+    WITHIN_REQUIRES...,
+    WITHIN_EXACT...,
+    closed_line = true,
+    closed_curve = false,
+)
+
+#= Linearring is within another linearring if their interiors intersect and no
+points of the first ring are in the exterior of the second ring. =#
+_within(
+    ::GI.LinearRingTrait, g1,
+    ::GI.LinearRingTrait, g2,
+) = _line_curve_process(
+    g1, g2;
+    WITHIN_CURVE_ALLOWS...,
+    WITHIN_REQUIRES...,
+    WITHIN_EXACT...,
+    closed_line = true,
+    closed_curve = true,
+)
+
+#= Linearring is within a polygon if their interiors intersect and no points of
+the ring are in the exterior of the polygon, although they can be on an edge. =#
+_within(
+    ::GI.LinearRingTrait, g1,
+    ::GI.PolygonTrait, g2,
+) = _line_polygon_process(
+    g1, g2;
+    WITHIN_POLYGON_ALLOWS...,
+    WITHIN_REQUIRES...,
+    WITHIN_EXACT...,
+    closed_line = true,
+)

Polygons within geometries

julia
#= 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

julia
_within(
+    ::GI.PolygonTrait, g1,
+    ::GI.AbstractCurveTrait, g2,
+) = false

Geometries within multi-geometry/geometry collections

julia
#= Geometry is within a multi-geometry or a collection if the geometry is within
+at least one of the collection elements. =#
+function _within(
+    ::Union{GI.PointTrait, GI.AbstractCurveTrait, GI.PolygonTrait}, g1,
+    ::Union{
+        GI.MultiPointTrait, GI.AbstractMultiCurveTrait,
+        GI.MultiPolygonTrait, GI.GeometryCollectionTrait,
+    }, g2,
+)
+    for sub_g2 in GI.getgeom(g2)
+        within(g1, sub_g2) && return true
+    end
+    return false
+end

Multi-geometry/geometry collections within geometries

julia
#= 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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/orientation.html b/previews/PR239/source/methods/orientation.html new file mode 100644 index 000000000..a35900cb9 --- /dev/null +++ b/previews/PR239/source/methods/orientation.html @@ -0,0 +1,124 @@ + + + + + + Orientation | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Orientation

julia
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!

julia
"""
+    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

julia
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

julia
        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

julia
false
+```
+"""
+function isconcave(poly)::Bool
+    sign = false
+
+    exterior = GI.getexterior(poly)

FIXME handle not closed polygons

julia
    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.

julia
"""
+    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:

julia
_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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/methods/polygonize.html b/previews/PR239/source/methods/polygonize.html new file mode 100644 index 000000000..453f268ed --- /dev/null +++ b/previews/PR239/source/methods/polygonize.html @@ -0,0 +1,313 @@ + + + + + + Polygonizing raster data | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Polygonizing raster data

julia
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

julia
- `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
```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

julia
    xhalf = step(xs) / 2
+    yhalf = step(ys) / 2

Make bounds ranges first to avoid floating point error making gaps or overlaps

julia
    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)

julia
    crs = GI.crs(A)

Define buffers for edges and rings

julia
    rings = Vector{T}[]
+
+    strait = true
+    turning = false

Get edges from the array A

julia
    edges = _pixel_edges(f, xs, ys, A)

Keep dict keys separately in a vector for performance

julia
    edgekeys = collect(keys(edges))

We don't delete keys we just reduce length with nkeys

julia
    nkeys = length(edgekeys)

Now create rings from the edges, looping until there are no edge keys left

julia
    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.

julia
        while nkeys > 0

Take the first node from the array

julia
            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

julia
        found == false && break

Check if there are one or two lines going through this node and take one of them, then update the status

julia
        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

julia
        currentnode = firstnode
+        ring = [currentnode, nextnode]
+        push!(rings, ring)

Loop until we close a the ring and break

julia
        while true

Find a node that matches the next node

julia
            (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.

julia
                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

julia
                edges[nextnode] = (remainingnode, map(typemax, remainingnode))
+            else

Here we simply choose the first (and only valid) node

julia
                selectednode = c1

Replace the edge nodes with empty nodes, they will be skipped later

julia
                edges[nextnode] = (map(typemax, c1), map(typemax, c1))

Check if we are on a straight line

julia
                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

julia
            currentnode, nextnode = nextnode, selectednode

Update the current node or add a new node to the ring

julia
            if straightline

replace the last node we don't need it

julia
                ring[end] = nextnode
+            else

add a new node, we have turned a corner

julia
                push!(ring, nextnode)
+            end

If the ring is closed, break the loop and start a new one

julia
            nextnode == firstnode && break
+        end
+    end

Define wrapped LinearRings, with embedded extents so we only calculate them once

julia
    linearrings = map(rings) do ring
+        extent = GI.extent(GI.LinearRing(ring))
+        GI.LinearRing(ring; extent, crs)
+    end

Separate exteriors from holes by winding direction

julia
    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

julia
    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

julia
                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...

julia
        @warn "No polgons found, check your data or try another function for `f`"
+        return nothing
+    else

Otherwise return a wrapped MultiPolygon

julia
        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

julia
    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

julia
    values = map(identity, sort!(Base.union(Iterators.map(f, A))))

We ignore pure Bool

julia
    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

julia
    existingnodes = get!(() -> newnodes, dict, key)

If we actually fetched an existing node, update it

julia
    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

julia
    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

julia
                x1, x2 = xs[i]

We check the Von Neumann neighborhood to decide what edges are needed, if any.

julia
                (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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/not_implemented_yet.html b/previews/PR239/source/not_implemented_yet.html new file mode 100644 index 000000000..8279cc9c4 --- /dev/null +++ b/previews/PR239/source/not_implemented_yet.html @@ -0,0 +1,28 @@ + + + + + + Not implemented yet | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Not implemented yet

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.

julia
function symdifference end
+function buffer end
+function convexhull end
+function concavehull end

This page was generated using Literate.jl.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/primitives.html b/previews/PR239/source/primitives.html new file mode 100644 index 000000000..5859d4963 --- /dev/null +++ b/previews/PR239/source/primitives.html @@ -0,0 +1,25 @@ + + + + + + GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content
+ + + + \ No newline at end of file diff --git a/previews/PR239/source/src/GeometryOpsCore.html b/previews/PR239/source/src/GeometryOpsCore.html new file mode 100644 index 000000000..31c588e5a --- /dev/null +++ b/previews/PR239/source/src/GeometryOpsCore.html @@ -0,0 +1,49 @@ + + + + + + GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content
julia
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.

julia
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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/src/apply.html b/previews/PR239/source/src/apply.html new file mode 100644 index 000000000..c92ed3c42 --- /dev/null +++ b/previews/PR239/source/src/apply.html @@ -0,0 +1,170 @@ + + + + + + apply | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

apply

julia
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:

julia
flipped_geom = GO.apply(GI.PointTrait(), geom) do p
+    (GI.y(p), GI.x(p))
+end

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.

Docstrings

Functions

GeometryOpsCore.apply Function
julia
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:

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

source

GeometryOpsCore.applyreduce Function
julia
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.

source

What is apply?

apply applies some function to every geometry matching the Target GeoInterface trait, in some arbitrarily nested object made up of:

  • AbstractArrays (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.

Embedding:

extent and crs can be embedded in all geometries, features, and feature collections as part of apply. Geometries deeper than Target will of course not have new extent or crs embedded.

  • calc_extent signals to recalculate an Extent and embed it.

  • crs will be embedded as-is

Threading

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.

julia
"""
+    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

julia
@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

julia
@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

julia
    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.

julia
@inline function _apply(f::F, target, ::Nothing, iterable::IterableType; threaded, kw...) where {F, IterableType}

Try the Tables.jl interface first

julia
    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

julia
            _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.

julia
    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,

julia
    old_schema = Tables.schema(iterable)

filter the geometry column out,

julia
    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.

julia
    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.

julia
    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.

julia
        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!

julia
                style == :default && continue

We assume that any other style is preserved.

julia
                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!

julia
        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.

julia
        if !("GEOINTERFACE:geometrycolumns" in mdk)

If the geometry columns are not already set, we need to set them.

julia
            DataAPI.metadata!(result, "GEOINTERFACE:geometrycolumns", (geometry_column,); style = :default)
+        end

Force reset CRS always, since you can pass crs to apply.

julia
        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

julia
@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

julia
    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

julia
        extent = mapreduce(GI.extent, Extents.union, features)

Return a FeatureCollection with features, crs and calculated extent

julia
        return GI.FeatureCollection(features; crs, extent)
+    else

Return a FeatureCollection with features and crs

julia
        return GI.FeatureCollection(features; crs)
+    end
+end

Rewrap all FeatureTrait features as GI.Feature, keeping the properties

julia
@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

julia
    geometry = _apply(f, target, GI.geometry(feature); crs, calc_extent, threaded)

Get the feature properties

julia
    properties = GI.properties(feature)
+    if calc_extent isa _True

Calculate the extent of the geometry

julia
        extent = GI.extent(geometry)

Return a new Feature with the new geometry and calculated extent, but the original properties and crs

julia
        return GI.Feature(geometry; properties, crs, extent)
+    else

Return a new Feature with the new geometry, but the original properties and crs

julia
        return GI.Feature(geometry; properties, crs)
+    end
+end

Reconstruct nested geometries, maybe using threads to call _apply on component geoms

julia
@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

julia
    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

julia
    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

julia
    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

julia
    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

julia
    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.

julia
_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

julia
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

julia
@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

julia
    tasks_per_thread = 2
+    chunk_size = max(1, ntasks ÷ (tasks_per_thread * nthreads()))

partition the range into chunks

julia
    task_chunks = Iterators.partition(taskrange, chunk_size)

Map over the chunks

julia
    tasks = map(task_chunks) do chunk

Spawn a task to process this chunk

julia
        @spawn begin

Where we map f over the chunk indices

julia
            map(f, chunk)
+        end
+    end

Finally we join the results into a new vector

julia
    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!

julia
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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/src/applyreduce.html b/previews/PR239/source/src/applyreduce.html new file mode 100644 index 000000000..c6825a5c6 --- /dev/null +++ b/previews/PR239/source/src/applyreduce.html @@ -0,0 +1,96 @@ + + + + + + applyreduce | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

applyreduce

julia
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.

julia
"""
+    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

julia
@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

julia
@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

julia
            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.

julia
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.

julia
    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.Features.

julia
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.

julia
    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

julia
@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

julia
@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

julia
@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

julia
@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

julia
@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

julia
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.

julia
@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

julia
    tasks_per_thread = 2
+    chunk_size = max(1, ntasks ÷ (tasks_per_thread * nthreads()))

partition the range into chunks

julia
    task_chunks = Iterators.partition(taskrange, chunk_size)

Map over the chunks

julia
    tasks = map(task_chunks) do chunk

Spawn a task to process this chunk

julia
        @spawn begin

Where we map f over the chunk indices

julia
            mapreduce(f, op, chunk; init)
+        end
+    end

Finally we join the results into a new vector

julia
    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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/src/geometry_utils.html b/previews/PR239/source/src/geometry_utils.html new file mode 100644 index 000000000..f3b258464 --- /dev/null +++ b/previews/PR239/source/src/geometry_utils.html @@ -0,0 +1,26 @@ + + + + + + GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content
julia
_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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/src/keyword_docs.html b/previews/PR239/source/src/keyword_docs.html new file mode 100644 index 000000000..42cd835da --- /dev/null +++ b/previews/PR239/source/src/keyword_docs.html @@ -0,0 +1,33 @@ + + + + + + Keyword docs | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Keyword docs

This file defines common keyword documentation, that can be spliced into docstrings.

julia
const THREADED_KEYWORD = "- `threaded`: `true` or `false`. Whether to use multithreading. Defaults to `false`."
+const CRS_KEYWORD = "- `crs`: The CRS to attach to geometries. Defaults to `nothing`."
+const CALC_EXTENT_KEYWORD = "- `calc_extent`: `true` or `false`. Whether to calculate the extent. Defaults to `false`."
+
+const APPLY_KEYWORDS = """
+$THREADED_KEYWORD
+$CRS_KEYWORD
+$CALC_EXTENT_KEYWORD
+"""

This page was generated using Literate.jl.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/src/other_primitives.html b/previews/PR239/source/src/other_primitives.html new file mode 100644 index 000000000..6faf67d7f --- /dev/null +++ b/previews/PR239/source/src/other_primitives.html @@ -0,0 +1,155 @@ + + + + + + Other primitives (unwrap, flatten, etc) | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Other primitives (unwrap, flatten, etc)

This file defines the following primitives:

GeometryOpsCore.unwrap Function
julia
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.

source

GeometryOpsCore.flatten Function
julia
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.

source

GeometryOpsCore.reconstruct Function
julia
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.

source

GeometryOpsCore.rebuild Function
julia
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)

source

julia
"""
+    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

julia
unwrap(f, target::Type, geom) = unwrap(f, target, GI.trait(geom), geom)

Try to unwrap over iterables

julia
unwrap(f, target::Type, ::Nothing, iterable) =
+    map(x -> unwrap(f, target, x), iterable)

Rewrap feature collections

julia
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

julia
unwrap(f, ::Type{Target}, ::Trait, geom) where {Target,Trait<:Target} = f(geom)

Fail if we hit PointTrait

julia
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

julia
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

julia
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

julia
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

julia
_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

julia
_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

julia
_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

julia
function _reconstruct(::Type{Target}, ::Nothing, iterable, components, iter) where Target
+    vect = map(iterable) do x

iter is updated by _reconstruct here

julia
        obj, iter = _reconstruct(Target, x, components, iter)
+        obj
+    end
+    return vect, iter
+end

Reconstruct feature collections

julia
function _reconstruct(::Type{Target}, ::GI.FeatureCollectionTrait, fc, components, iter) where Target
+    features = map(GI.getfeature(fc)) do feature

iter is updated by _reconstruct here

julia
        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

julia
        subgeom1, iter = _reconstruct(Target, GI.trait(subgeom), subgeom, components, iter)
+        subgeom1
+    end
+    return rebuild(geom, geoms), iter
+end

Apply f to the target geometry

julia
_reconstruct(::Type{Target}, ::Trait, geom, components, iter) where {Target,Trait<:Target} =
+    iterate(components, iter)

Specific cases to avoid method ambiguity

julia
_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

julia
_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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/src/types.html b/previews/PR239/source/src/types.html new file mode 100644 index 000000000..1e36ed178 --- /dev/null +++ b/previews/PR239/source/src/types.html @@ -0,0 +1,135 @@ + + + + + + Types | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Types

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.

julia
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.

julia
"""
+    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.

julia
TraitTarget(GI.PointTrait)
+TraitTarget(Union{GI.LineStringTrait, GI.LinearRingTrait})

etc.

julia
```
+
+"""
+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?

julia
"""
+    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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/transformations/correction/closed_ring.html b/previews/PR239/source/transformations/correction/closed_ring.html new file mode 100644 index 000000000..9b7b08a6c --- /dev/null +++ b/previews/PR239/source/transformations/correction/closed_ring.html @@ -0,0 +1,54 @@ + + + + + + Closed Rings | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Closed Rings

julia
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.

Example

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:

julia
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.

julia
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)

You can see that the last point of the ring here is equal to the first point. For a polygon with n sides, there should be n+1 vertices.

Implementation

julia
"""
+    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

julia
        return ring
+    else

Assemble the ring as a vector

julia
        tups = tuples.(GI.getpoint(ring))

Close the ring

julia
        push!(tups, tups[1])

Return an actual ring

julia
        return GI.LinearRing(tups)
+    end
+end

This page was generated using Literate.jl.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/transformations/correction/geometry_correction.html b/previews/PR239/source/transformations/correction/geometry_correction.html new file mode 100644 index 000000000..1d8fe8530 --- /dev/null +++ b/previews/PR239/source/transformations/correction/geometry_correction.html @@ -0,0 +1,55 @@ + + + + + + Geometry Corrections | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Geometry Corrections

julia
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.

Interface

All GeometryCorrections 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.

GeometryOps.GeometryCorrection Type
julia
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.

source

Any geometry correction must implement the interface as given above.

julia
"""
+    abstract type GeometryCorrection
+
+This abstract type represents a geometry correction.
+
+# Interface
+
+Any `GeometryCorrection` must implement two functions:
+    * `application_level(::GeometryCorrection)::AbstractGeometryTrait`: This function should return the `GeoInterface` trait that the correction is intended to be applied to, like `PointTrait` or `LineStringTrait` or `PolygonTrait`.
+    * `(::GeometryCorrection)(::AbstractGeometryTrait, geometry)::(some_geometry)`: This function should apply the correction to the given geometry, and return a new geometry.
+"""
+abstract type GeometryCorrection end
+
+application_level(gc::GeometryCorrection) = error("Not implemented yet for $(gc)")
+
+(gc::GeometryCorrection)(geometry) = gc(GI.trait(geometry), geometry)
+
+(gc::GeometryCorrection)(trait::GI.AbstractGeometryTrait, geometry) = error("Not implemented yet for $(gc) and $(trait).")
+
+function fix(geometry; corrections = GeometryCorrection[ClosedRing(),], kwargs...)
+    traits = application_level.(corrections)
+    final_geometry = geometry
+    for Trait in (GI.PointTrait, GI.MultiPointTrait, GI.LineStringTrait, GI.LinearRingTrait, GI.MultiLineStringTrait, GI.PolygonTrait, GI.MultiPolygonTrait)
+        available_corrections = findall(x -> x == Trait, traits)
+        isempty(available_corrections) && continue
+        @debug "Correcting for $(Trait)"
+        net_function = reduce(, corrections[available_corrections])
+        final_geometry = apply(net_function, Trait, final_geometry; kwargs...)
+    end
+    return final_geometry
+end

Available corrections

GeometryOps.ClosedRing Type
julia
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.

source

GeometryOps.DiffIntersectingPolygons Type
julia
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.

source

GeometryOps.GeometryCorrection Type
julia
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.

source

GeometryOps.UnionIntersectingPolygons Type
julia
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.

source


This page was generated using Literate.jl.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/transformations/correction/intersecting_polygons.html b/previews/PR239/source/transformations/correction/intersecting_polygons.html new file mode 100644 index 000000000..2a41cc054 --- /dev/null +++ b/previews/PR239/source/transformations/correction/intersecting_polygons.html @@ -0,0 +1,121 @@ + + + + + + Intersecting Polygons | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Intersecting Polygons

julia
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.

Example

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:

julia
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.

julia
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.

Implementation

julia
"""
+    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

julia
        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

julia
        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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/transformations/extent.html b/previews/PR239/source/transformations/extent.html new file mode 100644 index 000000000..0292594a4 --- /dev/null +++ b/previews/PR239/source/transformations/extent.html @@ -0,0 +1,37 @@ + + + + + + Extent embedding | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Extent embedding

julia
"""
+    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

julia
$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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/transformations/flip.html b/previews/PR239/source/transformations/flip.html new file mode 100644 index 000000000..146352e2b --- /dev/null +++ b/previews/PR239/source/transformations/flip.html @@ -0,0 +1,46 @@ + + + + + + Coordinate flipping | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Coordinate flipping

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.

julia
"""
+    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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/transformations/forcedims.html b/previews/PR239/source/transformations/forcedims.html new file mode 100644 index 000000000..c9f040f09 --- /dev/null +++ b/previews/PR239/source/transformations/forcedims.html @@ -0,0 +1,29 @@ + + + + + + GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content
julia
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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/transformations/reproject.html b/previews/PR239/source/transformations/reproject.html new file mode 100644 index 000000000..57a718e67 --- /dev/null +++ b/previews/PR239/source/transformations/reproject.html @@ -0,0 +1,65 @@ + + + + + + Geometry reprojection | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Geometry reprojection

julia
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.

julia
"""
+    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.
+$APPLY_KEYWORDS
+"""
+function reproject end

Method error handling

We also inject a method error handler, which prints a suggestion if the Proj extension is not loaded.

julia
function _reproject_error_hinter(io, exc, argtypes, kwargs)
+    if isnothing(Base.get_extension(GeometryOps, :GeometryOpsProjExt)) && exc.f == reproject
+        print(io, "\n\nThe `reproject` 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.")
+    else # this is a more general error
+        nothing
+    end
+end

This page was generated using Literate.jl.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/transformations/segmentize.html b/previews/PR239/source/transformations/segmentize.html new file mode 100644 index 000000000..75e01ed13 --- /dev/null +++ b/previews/PR239/source/transformations/segmentize.html @@ -0,0 +1,185 @@ + + + + + + Segmentize | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Segmentize

julia
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.

Examples

julia
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.

julia
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.

julia
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.

Benchmark

We benchmark our method against LibGEOS's GEOSDensify method, which is a similar method for densifying geometries.

julia
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))
julia
# 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)

julia
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!

julia
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

Implementation

julia
"""
+    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

julia
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

julia
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.

julia
    push!(new_coords, (x2, y2))
+    return nothing
+end

Note

The _fill_linear_kernel definition for GeodesicSegments is in the GeometryOpsProjExt extension module, in the segmentize.jl file.


This page was generated using Literate.jl.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/transformations/simplify.html b/previews/PR239/source/transformations/simplify.html new file mode 100644 index 000000000..f7c613c15 --- /dev/null +++ b/previews/PR239/source/transformations/simplify.html @@ -0,0 +1,514 @@ + + + + + + Geometry simplification | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Geometry simplification

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.

Examples

A quick and dirty example is:

julia
using Makie, GeoInterfaceMakie
+import GeoInterface as GI
+import GeometryOps as GO
+
+original = GI.Polygon([[[-70.603637, -33.399918], [-70.614624, -33.395332], [-70.639343, -33.392466], [-70.659942, -33.394759], [-70.683975, -33.404504], [-70.697021, -33.419406], [-70.701141, -33.434306], [-70.700454, -33.446339], [-70.694274, -33.458369], [-70.682601, -33.465816], [-70.668869, -33.472117], [-70.646209, -33.473835], [-70.624923, -33.472117], [-70.609817, -33.468107], [-70.595397, -33.458369], [-70.587158, -33.442901], [-70.587158, -33.426283], [-70.590591, -33.414248], [-70.594711, -33.406224], [-70.603637, -33.399918]]])
+
+simple = GO.simplify(original; number=6)
+
+f, a, p = poly(original; label = "Original")
+poly!(simple; label = "Simplified")
+axislegend(a)
+f

Benchmark

We benchmark these methods against LibGEOS's simplify implementation, which uses the Douglas-Peucker algorithm.

julia
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.

julia
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)

julia
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)

julia
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

julia
- `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

julia
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

julia
6
+```
+"""
+simplify(alg::SimplifyAlg, data; kw...) = _simplify(alg, data; kw...)
+simplify(alg::GEOS, data; kw...) = _simplify(alg, data; kw...)

Default algorithm is DouglasPeucker

julia
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

Simplify with RadialDistance Algorithm

julia
"""
+    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

julia
        tol = isnothing(tol) ? tol : tol^2
+        new(number, ratio, tol)
+    end
+end
+
+function _simplify(alg::RadialDistance, points::Vector, _)
+    previous = first(points)
+    distances = Array{Float64}(undef, length(points))
+    for i in eachindex(points)
+        point = points[i]
+        distances[i] = _squared_euclid_distance(Float64, point, previous)
+        previous = point
+    end
+    # Never remove the end points
+    distances[begin] = distances[end] = Inf
+    return _get_points(alg, points, distances)
+end

Simplify with DouglasPeucker Algorithm

julia
"""
+    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

julia
        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

julia
    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

julia
    queue = Vector{Tuple{Int, Int, Int, Float64}}()
+    queue_idx, queue_dist = 0, zero(Float64)
+    len_queue = 0

Set up results vector

julia
    results = Vector{Int}(undef, max_points + (preserve_endpoint ? 0 : 1))
+    results[1], results[2] = 1, npoints

Loop through points until stopping criteria are fulfilled

julia
    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

julia
        i += 1
+        results[i] = max_idx

Determine which point to add next by checking left and right of point

julia
        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

julia
        if queue_dist > left_dist && queue_dist > right_dist

Value in queue is next value to add to results

julia
            start_idx, max_idx, end_idx, max_dist = queue[queue_idx]

Add left and/or right values to queue or delete used queue value

julia
            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

julia
            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

julia
        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

julia
            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

julia
            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

Simplify with VisvalingamWhyatt Algorithm

julia
"""
+    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

julia
        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

julia
_triangle_double_area(p1, p2, p3) =
+    abs(p1[1] * (p2[2] - p3[2]) + p2[1] * (p3[2] - p1[2]) + p3[1] * (p1[2] - p2[2]))

Shared utils

julia
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

julia
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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/transformations/transform.html b/previews/PR239/source/transformations/transform.html new file mode 100644 index 000000000..0f103b918 --- /dev/null +++ b/previews/PR239/source/transformations/transform.html @@ -0,0 +1,79 @@ + + + + + + Pointwise transformation | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Pointwise transformation

julia
"""
+    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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/transformations/tuples.html b/previews/PR239/source/transformations/tuples.html new file mode 100644 index 000000000..0413ca05c --- /dev/null +++ b/previews/PR239/source/transformations/tuples.html @@ -0,0 +1,43 @@ + + + + + + Tuple conversion | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Tuple conversion

julia
"""
+    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

julia
$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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/types.html b/previews/PR239/source/types.html new file mode 100644 index 000000000..0af974698 --- /dev/null +++ b/previews/PR239/source/types.html @@ -0,0 +1,62 @@ + + + + + + Types | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Types

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.

julia
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:

  1. 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.

  2. It's a good way to test the correctness of the native implementations.

julia
"""
+    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.

julia
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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/source/utils.html b/previews/PR239/source/utils.html new file mode 100644 index 000000000..91c0b6633 --- /dev/null +++ b/previews/PR239/source/utils.html @@ -0,0 +1,144 @@ + + + + + + Utility functions | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Utility functions

julia
_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

julia
```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

julia
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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/tutorials/creating_geometry.html b/previews/PR239/tutorials/creating_geometry.html new file mode 100644 index 000000000..2e8c3b023 --- /dev/null +++ b/previews/PR239/tutorials/creating_geometry.html @@ -0,0 +1,113 @@ + + + + + + Creating Geometry | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Creating Geometry

In this tutorial, we're going to:

  1. Create and plot geometries

  2. Plot geometries on a map using GeoMakie and coordinate reference system (CRS)

  3. Create geospatial geometries with embedded coordinate reference system information

  4. Assign attributes to geospatial geometries

  5. Save geospatial geometries to common geospatial file formats

First, we load some required packages.

julia
# 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

Creating and plotting geometries

Let's start by making a single Point.

julia
point = GI.Point(0, 0)
GeoInterface.Wrappers.Point{false, false, Tuple{Int64, Int64}, Nothing}((0, 0), nothing)

Now, let's plot our point.

julia
fig, ax, plt = plot(point)

Let's create a set of points, and have a bit more fun with plotting.

julia
x = [-5, 0, 5, 0];
+y = [0, -5, 0, 5];
+points = GI.Point.(zip(x,y));
+plot!(ax, points; marker = '✈', markersize = 30)
+fig

Points can be combined into a single MultiPoint geometry.

julia
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.

julia
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.

julia
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.

julia
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.

julia
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.

julia
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 LinearRings 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.

julia
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.

julia
xoffset = 50.;
+yoffset = 0.;
+f = CoordinateTransformations.Translation(xoffset, yoffset);
+polygon2 = GO.transform(f, polygon2);
+plot!(polygon2)
+fig

Polygons can also be grouped together as a MultiPolygon.

julia
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.

julia
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].

Plot geometries on a map using 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.

julia
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.

julia
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.

julia
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 MultiPolygons as a GeoJSON.FeatureCollection.

julia
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.

julia
fig = Figure(size=(1000, 500));
+ga = GeoAxis(
+    fig[1, 1];
+    source = source_crs1,
+    dest = destination_crs,
+    xticklabelsvisible = false,
+    yticklabelsvisible = false,
+);

Plot land for context.

julia
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

julia
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.

julia
source_crs2 = GFT.EPSG(32610)
GeoFormatTypes.EPSG{1}((32610,))

Create a polygon (we're working in meters now, not latitude and longitude)

julia
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

julia
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

julia
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.

julia
plot!(ga,polygon3; color=:red, source = source_crs2)
+fig

Create geospatial geometries with embedded coordinate reference system information

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

julia
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

julia
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

julia
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,)))

Creating a table with attributes and geometry

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.

julia
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.

julia
df[!,:id] = ["a", "b"]
+df[!, :name] = ["polygon 1", "polygon 2"]
+df

Saving your geospatial data

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.

julia
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.

julia
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.

julia
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.

julia
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.

+ + + + \ No newline at end of file diff --git a/previews/PR239/tutorials/geodesic_paths.html b/previews/PR239/tutorials/geodesic_paths.html new file mode 100644 index 000000000..0ce357761 --- /dev/null +++ b/previews/PR239/tutorials/geodesic_paths.html @@ -0,0 +1,35 @@ + + + + + + Geodesic paths | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Geodesic paths

Geodesic paths are paths computed on an ellipsoid, as opposed to a plane.

julia
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

+ + + + \ No newline at end of file diff --git a/previews/PR239/tutorials/spatial_joins.html b/previews/PR239/tutorials/spatial_joins.html new file mode 100644 index 000000000..f113cd50d --- /dev/null +++ b/previews/PR239/tutorials/spatial_joins.html @@ -0,0 +1,76 @@ + + + + + + Spatial joins | GeometryOps.jl + + + + + + + + + + + + + + +
Skip to content

Spatial joins

Spatial joins are table joins which are based not on equality, but on some predicate p(x,y), which takes two geometries, and returns a value of either true or false. For geometries, the DE-9IM spatial relationship model is used to determine the spatial relationship between two geometries.

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:

julia
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:

julia
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.

Simple example

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.

julia
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.

julia
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

julia
@time joined_df = FlexiJoins.innerjoin(
+    (points_df, poly_df),
+    by_pred(:geometry, GO.within, :geometry)
+)
julia
scatter!(a, joined_df.geometry; color = joined_df.color)
+f

Here, you can see that the colors were assigned appropriately to the scattered points!

Real-world example

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.

julia
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]]
julia
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).

Enabling custom predicates

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:

julia
my_predicate_function = <(5)  abs  GO.distance

You would need to define FlexiJoins.supports_mode on your predicate:

julia
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).

+ + + + \ No newline at end of file diff --git a/previews/PR239/vp-icons.css b/previews/PR239/vp-icons.css new file mode 100644 index 000000000..ddc5bd8ed --- /dev/null +++ b/previews/PR239/vp-icons.css @@ -0,0 +1 @@ +.vpi-social-github{--icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E")} \ No newline at end of file