diff --git a/patches/0007-update-functions-parser.patch b/patches/0007-update-functions-parser.patch new file mode 100644 index 0000000..b31a67e --- /dev/null +++ b/patches/0007-update-functions-parser.patch @@ -0,0 +1,218 @@ +diff --git a/pkg/sql/lexbase/encode.go b/pkg/sql/lexbase/encode.go +index 4f7bc03..5b31fd0 100644 +--- a/pkg/sql/lexbase/encode.go ++++ b/pkg/sql/lexbase/encode.go +@@ -51,6 +51,13 @@ const ( + // as Oracle is case insensitive if object name is not quoted. + EncAlwaysQuoted + ++ // EncSkipEscapeString indicates that the string should not be escaped, ++ // and non-ASCII characters are allowed. ++ // More specifically, it means that the tree.DString won't be wrapped ++ // with e'text' as the prefix and suffix, and single quotes within ++ // the string will not be escaped with a backslash. ++ EncSkipEscapeString ++ + // EncFirstFreeFlagBit needs to remain unused; it is used as base + // bit offset for tree.FmtFlags. + EncFirstFreeFlagBit +@@ -147,6 +154,7 @@ func EscapeSQLString(in string) string { + func EncodeSQLStringWithFlags(buf *bytes.Buffer, in string, flags EncodeFlags) { + // See http://www.postgresql.org/docs/9.4/static/sql-syntax-lexical.html + start := 0 ++ skipEscape := flags.HasFlags(EncSkipEscapeString) + escapedString := false + bareStrings := flags.HasFlags(EncBareStrings) + // Loop through each unicode code point. +@@ -165,7 +173,13 @@ func EncodeSQLStringWithFlags(buf *bytes.Buffer, in string, flags EncodeFlags) { + } + } + +- if !escapedString { ++ // If non-ASCII characters are allowed, ++ // skip escaping and write the original UTF-8 character. ++ if r > maxPrintableChar && skipEscape { ++ continue ++ } ++ ++ if !skipEscape && !escapedString { + buf.WriteString("e'") // begin e'xxx' string + escapedString = true + } +@@ -177,17 +191,19 @@ func EncodeSQLStringWithFlags(buf *bytes.Buffer, in string, flags EncodeFlags) { + } else { + start = i + ln + } +- stringencoding.EncodeEscapedChar(buf, in, r, ch, i, '\'') ++ // If we skip escaping, we don't write the slash char before the ++ // quote char, so we will write the quote char directly. ++ stringencoding.EncodeEscapedChar(buf, in, r, ch, i, '\'', !skipEscape) + } + +- quote := !escapedString && !bareStrings ++ quote := !escapedString && !bareStrings && !skipEscape + if quote { + buf.WriteByte('\'') // begin 'xxx' string if nothing was escaped + } + if start < len(in) { + buf.WriteString(in[start:]) + } +- if escapedString || quote { ++ if !skipEscape && (escapedString || quote) { + buf.WriteByte('\'') + } + } +diff --git a/pkg/sql/sem/tree/expr.go b/pkg/sql/sem/tree/expr.go +index 05d64e1..dabff21 100644 +--- a/pkg/sql/sem/tree/expr.go ++++ b/pkg/sql/sem/tree/expr.go +@@ -1364,6 +1364,19 @@ const ( + OrderedSetAgg + ) + ++// onlyNameFunc is the list of function who can be compiled with only the ++// name. This is for PG compatibility, where examples such as `CURRENT_TIMESTAMP()` ++// is not allowed, but `CURRENT_TIMESTAMP` is allowed. ++var onlyNameFunc = map[string]bool{ ++ "current_timestamp": true, ++} ++ ++func isOnlyNameFunc(f *FuncExpr) bool { ++ funcStr := f.Func.String() ++ _, ok := onlyNameFunc[funcStr] ++ return ok ++} ++ + // Format implements the NodeFormatter interface. + func (node *FuncExpr) Format(ctx *FmtCtx) { + var typ string +@@ -1385,41 +1398,43 @@ func (node *FuncExpr) Format(ctx *FmtCtx) { + ctx.FormatNode(&node.Func) + }) + +- ctx.WriteByte('(') +- ctx.WriteString(typ) +- ctx.FormatNode(&node.Exprs) +- if node.AggType == GeneralAgg && len(node.OrderBy) > 0 { +- ctx.WriteByte(' ') +- ctx.FormatNode(&node.OrderBy) +- } +- ctx.WriteByte(')') +- if ctx.HasFlags(FmtParsable) && node.typ != nil { +- if node.fnProps.AmbiguousReturnType { +- // There's no type annotation available for tuples. +- // TODO(jordan,knz): clean this up. AmbiguousReturnType should be set only +- // when we should and can put an annotation here. #28579 +- if node.typ.Family() != types.TupleFamily { +- ctx.WriteString(":::") +- ctx.Buffer.WriteString(node.typ.SQLString()) ++ if !(ctx.HasFlags(FmtFuncOnlyName) && isOnlyNameFunc(node)) { ++ ctx.WriteByte('(') ++ ctx.WriteString(typ) ++ ctx.FormatNode(&node.Exprs) ++ if node.AggType == GeneralAgg && len(node.OrderBy) > 0 { ++ ctx.WriteByte(' ') ++ ctx.FormatNode(&node.OrderBy) ++ } ++ ctx.WriteByte(')') ++ if ctx.HasFlags(FmtParsable) && node.typ != nil { ++ if node.fnProps.AmbiguousReturnType { ++ // There's no type annotation available for tuples. ++ // TODO(jordan,knz): clean this up. AmbiguousReturnType should be set only ++ // when we should and can put an annotation here. #28579 ++ if node.typ.Family() != types.TupleFamily { ++ ctx.WriteString(":::") ++ ctx.Buffer.WriteString(node.typ.SQLString()) ++ } + } + } +- } +- if node.AggType == OrderedSetAgg && len(node.OrderBy) > 0 { +- ctx.WriteString(" WITHIN GROUP (") +- ctx.FormatNode(&node.OrderBy) +- ctx.WriteString(")") +- } +- if node.Filter != nil { +- ctx.WriteString(" FILTER (WHERE ") +- ctx.FormatNode(node.Filter) +- ctx.WriteString(")") +- } +- if window := node.WindowDef; window != nil { +- ctx.WriteString(" OVER ") +- if window.Name != "" { +- ctx.FormatNode(&window.Name) +- } else { +- ctx.FormatNode(window) ++ if node.AggType == OrderedSetAgg && len(node.OrderBy) > 0 { ++ ctx.WriteString(" WITHIN GROUP (") ++ ctx.FormatNode(&node.OrderBy) ++ ctx.WriteString(")") ++ } ++ if node.Filter != nil { ++ ctx.WriteString(" FILTER (WHERE ") ++ ctx.FormatNode(node.Filter) ++ ctx.WriteString(")") ++ } ++ if window := node.WindowDef; window != nil { ++ ctx.WriteString(" OVER ") ++ if window.Name != "" { ++ ctx.FormatNode(&window.Name) ++ } else { ++ ctx.FormatNode(window) ++ } + } + } + } +diff --git a/pkg/sql/sem/tree/format.go b/pkg/sql/sem/tree/format.go +index 6832c7e..df15c15 100644 +--- a/pkg/sql/sem/tree/format.go ++++ b/pkg/sql/sem/tree/format.go +@@ -195,6 +195,10 @@ const ( + // FmtSkipAsOfSystemTimeClauses prevents the formatter from printing AS OF + // SYSTEM TIME clauses. + FmtSkipAsOfSystemTimeClauses ++ ++ // FmtFuncOnlyName instructs the formating for a function only print ++ // its name, without printing the body of the function nor with the brackets. ++ FmtFuncOnlyName + ) + + const genericArityIndicator = "__more__" +diff --git a/pkg/util/stringencoding/string_encoding.go b/pkg/util/stringencoding/string_encoding.go +index fcb7fac..afa0090 100644 +--- a/pkg/util/stringencoding/string_encoding.go ++++ b/pkg/util/stringencoding/string_encoding.go +@@ -73,6 +73,7 @@ func init() { + + // EncodeEscapedChar is used internally to write out a character from a larger + // string that needs to be escaped to a buffer. ++// If slashedQuoteChar is true, it will write a backslash before the quoteChar. + func EncodeEscapedChar( + buf *bytes.Buffer, + entireString string, +@@ -80,6 +81,7 @@ func EncodeEscapedChar( + currentByte byte, + currentIdx int, + quoteChar byte, ++ slashedQuoteChar bool, + ) { + ln := utf8.RuneLen(currentRune) + if currentRune == utf8.RuneError { +@@ -94,10 +96,15 @@ func EncodeEscapedChar( + } else if ln == 1 { + // For single-byte runes, do the same as encodeSQLBytes. + if encodedChar := EncodeMap[currentByte]; encodedChar != DontEscape { +- buf.WriteByte('\\') ++ if slashedQuoteChar { ++ buf.WriteByte('\\') ++ } ++ + buf.WriteByte(encodedChar) + } else if currentByte == quoteChar { +- buf.WriteByte('\\') ++ if slashedQuoteChar { ++ buf.WriteByte('\\') ++ } + buf.WriteByte(quoteChar) + } else { + // Escape non-printable characters. diff --git a/pkg/geo/geodist/geodist.go b/pkg/geo/geodist/geodist.go new file mode 100644 index 0000000..e2cd2aa --- /dev/null +++ b/pkg/geo/geodist/geodist.go @@ -0,0 +1,428 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +// Package geodist finds distances between two geospatial shapes. +package geodist + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/golang/geo/s2" + "github.com/twpayne/go-geom" +) + +// Point is a union of the point types used in geometry and geography representation. +// The interfaces for distance calculation defined below are shared for both representations, +// and this union helps us avoid heap allocations by doing cheap copy-by-value of points. The +// code that peers inside a Point knows which of the two fields is populated. +type Point struct { + GeomPoint geom.Coord + GeogPoint s2.Point +} + +// IsShape implements the geodist.Shape interface. +func (p *Point) IsShape() {} + +// Edge is a struct that represents a connection between two points. +type Edge struct { + V0, V1 Point +} + +// LineString is an interface that represents a geospatial LineString. +type LineString interface { + Edge(i int) Edge + NumEdges() int + Vertex(i int) Point + NumVertexes() int + IsShape() + IsLineString() +} + +// LinearRing is an interface that represents a geospatial LinearRing. +type LinearRing interface { + Edge(i int) Edge + NumEdges() int + Vertex(i int) Point + NumVertexes() int + IsShape() + IsLinearRing() +} + +// shapeWithEdges represents any shape that contains edges. +type shapeWithEdges interface { + Edge(i int) Edge + NumEdges() int +} + +// Polygon is an interface that represents a geospatial Polygon. +type Polygon interface { + LinearRing(i int) LinearRing + NumLinearRings() int + IsShape() + IsPolygon() +} + +// Shape is an interface that represents any Geospatial shape. +type Shape interface { + IsShape() +} + +var _ Shape = (*Point)(nil) +var _ Shape = (LineString)(nil) +var _ Shape = (LinearRing)(nil) +var _ Shape = (Polygon)(nil) + +// DistanceUpdater is a provided hook that has a series of functions that allows +// the caller to maintain the distance value desired. +type DistanceUpdater interface { + // Update updates the distance based on two provided points, + // returning if the function should return early. + Update(a Point, b Point) bool + // OnIntersects is called when two shapes intersects. + OnIntersects(p Point) bool + // Distance returns the distance to return so far. + Distance() float64 + // IsMaxDistance returns whether the updater is looking for maximum distance. + IsMaxDistance() bool + // FlipGeometries is called to flip the order of geometries. + FlipGeometries() +} + +// EdgeCrosser is a provided hook that calculates whether edges intersect. +type EdgeCrosser interface { + // ChainCrossing assumes there is an edge to compare against, and the previous + // point `p0` is the start of the next edge. It will then returns whether (p0, p) + // intersects with the edge and point of intersection if they intersect. + // When complete, point p will become p0. + // Desired usage examples: + // crosser := NewEdgeCrosser(edge.V0, edge.V1, startingP0) + // intersects, _ := crosser.ChainCrossing(p1) + // laterIntersects, _ := crosser.ChainCrossing(p2) + // intersects |= laterIntersects .... + ChainCrossing(p Point) (bool, Point) +} + +// DistanceCalculator contains calculations which allow ShapeDistance to calculate +// the distance between two shapes. +type DistanceCalculator interface { + // DistanceUpdater returns the DistanceUpdater for the current set of calculations. + DistanceUpdater() DistanceUpdater + // NewEdgeCrosser returns a new EdgeCrosser with the given edge initialized to be + // the edge to compare against, and the start point to be the start of the first + // edge to compare against. + NewEdgeCrosser(edge Edge, startPoint Point) EdgeCrosser + // PointIntersectsLinearRing returns whether the point intersects the given linearRing. + PointIntersectsLinearRing(point Point, linearRing LinearRing) bool + // ClosestPointToEdge returns the closest point to the infinite line denoted by + // the edge, and a bool on whether this point lies on the edge segment. + ClosestPointToEdge(edge Edge, point Point) (Point, bool) + // BoundingBoxIntersects returns whether the bounding boxes of the shapes in + // question intersect. + BoundingBoxIntersects() bool +} + +// ShapeDistance returns the distance between two given shapes. +// Distance is defined by the DistanceUpdater provided by the interface. +// It returns whether the function above should return early. +func ShapeDistance(c DistanceCalculator, a Shape, b Shape) (bool, error) { + switch a := a.(type) { + case *Point: + switch b := b.(type) { + case *Point: + return c.DistanceUpdater().Update(*a, *b), nil + case LineString: + return onPointToLineString(c, *a, b), nil + case Polygon: + return onPointToPolygon(c, *a, b), nil + default: + return false, pgerror.Newf(pgcode.InvalidParameterValue, "unknown shape: %T", b) + } + case LineString: + switch b := b.(type) { + case *Point: + c.DistanceUpdater().FlipGeometries() + // defer to restore the order of geometries at the end of the function call. + defer c.DistanceUpdater().FlipGeometries() + return onPointToLineString(c, *b, a), nil + case LineString: + return onShapeEdgesToShapeEdges(c, a, b), nil + case Polygon: + return onLineStringToPolygon(c, a, b), nil + default: + return false, pgerror.Newf(pgcode.InvalidParameterValue, "unknown shape: %T", b) + } + case Polygon: + switch b := b.(type) { + case *Point: + c.DistanceUpdater().FlipGeometries() + // defer to restore the order of geometries at the end of the function call. + defer c.DistanceUpdater().FlipGeometries() + return onPointToPolygon(c, *b, a), nil + case LineString: + c.DistanceUpdater().FlipGeometries() + // defer to restore the order of geometries at the end of the function call. + defer c.DistanceUpdater().FlipGeometries() + return onLineStringToPolygon(c, b, a), nil + case Polygon: + return onPolygonToPolygon(c, a, b), nil + default: + return false, pgerror.Newf(pgcode.InvalidParameterValue, "unknown shape: %T", b) + } + } + return false, pgerror.Newf(pgcode.InvalidParameterValue, "unknown shape: %T", a) +} + +// onPointToEdgesExceptFirstEdgeStart updates the distance against the edges of a shape and a point. +// It will only check the V1 of each edge and assumes the first edge start does not need the distance +// to be computed. +func onPointToEdgesExceptFirstEdgeStart(c DistanceCalculator, a Point, b shapeWithEdges) bool { + for edgeIdx, bNumEdges := 0, b.NumEdges(); edgeIdx < bNumEdges; edgeIdx++ { + edge := b.Edge(edgeIdx) + // Check against all V1 of every edge. + if c.DistanceUpdater().Update(a, edge.V1) { + return true + } + // The max distance between a point and the set of points representing an edge is the + // maximum distance from the point and the pair of end-points of the edge, so we don't + // need to update the distance using the projected point. + if !c.DistanceUpdater().IsMaxDistance() { + // Also project the point to the infinite line of the edge, and compare if the closestPoint + // lies on the edge. + if closestPoint, ok := c.ClosestPointToEdge(edge, a); ok { + if c.DistanceUpdater().Update(a, closestPoint) { + return true + } + } + } + } + return false +} + +// onPointToLineString updates the distance between a point and a polyline. +// Returns true if the calling function should early exit. +func onPointToLineString(c DistanceCalculator, a Point, b LineString) bool { + // Compare the first point, to avoid checking each V0 in the chain afterwards. + if c.DistanceUpdater().Update(a, b.Vertex(0)) { + return true + } + return onPointToEdgesExceptFirstEdgeStart(c, a, b) +} + +// onPointToPolygon updates the distance between a point and a polygon. +// Returns true if the calling function should early exit. +func onPointToPolygon(c DistanceCalculator, a Point, b Polygon) bool { + // MaxDistance: When computing the maximum distance, the cases are: + // - The point P is not contained in the exterior of the polygon G. + // Say vertex V is the vertex of the exterior of the polygon that is + // furthest away from point P (among all the exterior vertices). + // - One can prove that any vertex of the holes will be closer to point P than vertex V. + // Similarly we can prove that any point in the interior of the polygon is closer to P than vertex V. + // Therefore we only need to compare with the exterior. + // - The point P is contained in the exterior and inside a hole of polygon G. + // One can again prove that the furthest point in the polygon from P is one of the vertices of the exterior. + // - The point P is contained in the polygon. One can again prove the same property. + // So we only need to compare with the exterior ring. + // MinDistance: If the exterior ring does not contain the point, we just need to calculate the distance to + // the exterior ring. + // BoundingBoxIntersects: if the bounding box of the shape being calculated does not intersect, + // then we only need to compare the outer loop. + if c.DistanceUpdater().IsMaxDistance() || !c.BoundingBoxIntersects() || + !c.PointIntersectsLinearRing(a, b.LinearRing(0)) { + return onPointToEdgesExceptFirstEdgeStart(c, a, b.LinearRing(0)) + } + // At this point it may be inside a hole. + // If it is in a hole, return the distance to the hole. + for ringIdx := 1; ringIdx < b.NumLinearRings(); ringIdx++ { + ring := b.LinearRing(ringIdx) + if c.PointIntersectsLinearRing(a, ring) { + return onPointToEdgesExceptFirstEdgeStart(c, a, ring) + } + } + + // Otherwise, we are inside the polygon. + return c.DistanceUpdater().OnIntersects(a) +} + +// onShapeEdgesToShapeEdges updates the distance between two shapes by +// only looking at the edges. +// Returns true if the calling function should early exit. +func onShapeEdgesToShapeEdges(c DistanceCalculator, a shapeWithEdges, b shapeWithEdges) bool { + for aEdgeIdx, aNumEdges := 0, a.NumEdges(); aEdgeIdx < aNumEdges; aEdgeIdx++ { + aEdge := a.Edge(aEdgeIdx) + var crosser EdgeCrosser + // MaxDistance: the max distance between 2 edges is the maximum of the distance across + // pairs of vertices chosen from each edge. + // It does not matter whether the edges cross, so we skip this check. + // BoundingBoxIntersects: if the bounding box of the two shapes do not intersect, + // then we don't need to check whether edges intersect either. + if !c.DistanceUpdater().IsMaxDistance() && c.BoundingBoxIntersects() { + crosser = c.NewEdgeCrosser(aEdge, b.Edge(0).V0) + } + for bEdgeIdx, bNumEdges := 0, b.NumEdges(); bEdgeIdx < bNumEdges; bEdgeIdx++ { + bEdge := b.Edge(bEdgeIdx) + if crosser != nil { + // If the edges cross, the distance is 0. + intersects, intersectionPoint := crosser.ChainCrossing(bEdge.V1) + if intersects { + return c.DistanceUpdater().OnIntersects(intersectionPoint) + } + } + + // Check the vertex against the ends of the edges. + if c.DistanceUpdater().Update(aEdge.V0, bEdge.V0) || + c.DistanceUpdater().Update(aEdge.V0, bEdge.V1) || + c.DistanceUpdater().Update(aEdge.V1, bEdge.V0) || + c.DistanceUpdater().Update(aEdge.V1, bEdge.V1) { + return true + } + // Only project vertexes to edges if we are looking at the edges. + if !c.DistanceUpdater().IsMaxDistance() { + if projectVertexToEdge(c, aEdge.V0, bEdge) || + projectVertexToEdge(c, aEdge.V1, bEdge) { + return true + } + c.DistanceUpdater().FlipGeometries() + if projectVertexToEdge(c, bEdge.V0, aEdge) || + projectVertexToEdge(c, bEdge.V1, aEdge) { + // Restore the order of geometries. + c.DistanceUpdater().FlipGeometries() + return true + } + // Restore the order of geometries. + c.DistanceUpdater().FlipGeometries() + } + } + } + return false +} + +// projectVertexToEdge attempts to project the point onto the given edge. +// Returns true if the calling function should early exit. +func projectVertexToEdge(c DistanceCalculator, vertex Point, edge Edge) bool { + // Also check the projection of the vertex onto the edge. + if closestPoint, ok := c.ClosestPointToEdge(edge, vertex); ok { + if c.DistanceUpdater().Update(vertex, closestPoint) { + return true + } + } + return false +} + +// onLineStringToPolygon updates the distance between a polyline and a polygon. +// Returns true if the calling function should early exit. +func onLineStringToPolygon(c DistanceCalculator, a LineString, b Polygon) bool { + // MinDistance: If we know at least one point is outside the exterior ring, then there are two cases: + // * the line is always outside the exterior ring. We only need to compare the line + // against the exterior ring. + // * the line intersects with the exterior ring. + // In both these cases, we can defer to the edge to edge comparison between the line + // and the exterior ring. + // We use the first point of the linestring for this check. + // MaxDistance: the furthest distance from a LineString to a Polygon is always against the + // exterior ring. This follows the reasoning under "onPointToPolygon", but we must now + // check each point in the LineString. + // BoundingBoxIntersects: if the bounding box of the two shapes do not intersect, + // then the distance is always from the LineString to the exterior ring. + if c.DistanceUpdater().IsMaxDistance() || + !c.BoundingBoxIntersects() || + !c.PointIntersectsLinearRing(a.Vertex(0), b.LinearRing(0)) { + return onShapeEdgesToShapeEdges(c, a, b.LinearRing(0)) + } + + // Now we are guaranteed that there is at least one point inside the exterior ring. + // + // For a polygon with no holes, the fact that there is a point inside the exterior + // ring would imply that the distance is zero. + // + // However, when there are holes, it is possible that the distance is non-zero if + // polyline A is completely contained inside a hole. We iterate over the holes and + // compute the distance between the hole and polyline A. + // * If polyline A is within the given distance, we can immediately return. + // * If polyline A does not intersect the hole but there is at least one point inside + // the hole, must be inside that hole and so the distance of this polyline to this hole + // is the distance of this polyline to this polygon. + for ringIdx := 1; ringIdx < b.NumLinearRings(); ringIdx++ { + hole := b.LinearRing(ringIdx) + if onShapeEdgesToShapeEdges(c, a, hole) { + return true + } + for pointIdx := 0; pointIdx < a.NumVertexes(); pointIdx++ { + if c.PointIntersectsLinearRing(a.Vertex(pointIdx), hole) { + return false + } + } + } + + // This means we are inside the exterior ring, and no points are inside a hole. + // This means the point is inside the polygon. + return c.DistanceUpdater().OnIntersects(a.Vertex(0)) +} + +// onPolygonToPolygon updates the distance between two polygons. +// Returns true if the calling function should early exit. +func onPolygonToPolygon(c DistanceCalculator, a Polygon, b Polygon) bool { + aFirstPoint := a.LinearRing(0).Vertex(0) + bFirstPoint := b.LinearRing(0).Vertex(0) + + // MinDistance: + // If there is at least one point on the exterior ring of B that is outside the exterior ring + // of A, then we have one of these two cases: + // * The exterior rings of A and B intersect. The distance can always be found by comparing + // the exterior rings. + // * The exterior rings of A and B never meet. This distance can always be found + // by only comparing the exterior rings. + // If we find the point is inside the exterior ring, A could contain B, so this reasoning + // does not apply. + // + // The same reasoning applies if there is at least one point on the exterior ring of A + // that is outside the exterior ring of B. + // + // As such, we only need to compare the exterior rings if we detect this. + // + // MaxDistance: + // The furthest distance between two polygons is always against the exterior rings of each other. + // This closely follows the reasoning pointed out in "onPointToPolygon". Holes are always located + // inside the exterior ring of a polygon, so the exterior ring will always contain a point + // with a larger max distance. + // BoundingBoxIntersects: if the bounding box of the two shapes do not intersect, + // then the distance is always between the two exterior rings. + if c.DistanceUpdater().IsMaxDistance() || + !c.BoundingBoxIntersects() || + (!c.PointIntersectsLinearRing(bFirstPoint, a.LinearRing(0)) && + !c.PointIntersectsLinearRing(aFirstPoint, b.LinearRing(0))) { + return onShapeEdgesToShapeEdges(c, a.LinearRing(0), b.LinearRing(0)) + } + + // If any point of polygon A is inside a hole of polygon B, then either: + // * A is inside the hole and the closest point can be found by comparing A's outer + // linearRing and the hole in B, or + // * A intersects this hole and the distance is zero, which can also be found by comparing + // A's outer linearRing and the hole in B. + // In this case, we only need to compare the holes of B to contain a single point A. + for ringIdx := 1; ringIdx < b.NumLinearRings(); ringIdx++ { + bHole := b.LinearRing(ringIdx) + if c.PointIntersectsLinearRing(aFirstPoint, bHole) { + return onShapeEdgesToShapeEdges(c, a.LinearRing(0), bHole) + } + } + + // Do the same check for the polygons the other way around. + c.DistanceUpdater().FlipGeometries() + // defer to restore the order of geometries at the end of the function call. + defer c.DistanceUpdater().FlipGeometries() + for ringIdx := 1; ringIdx < a.NumLinearRings(); ringIdx++ { + aHole := a.LinearRing(ringIdx) + if c.PointIntersectsLinearRing(bFirstPoint, aHole) { + return onShapeEdgesToShapeEdges(c, b.LinearRing(0), aHole) + } + } + + // Now we know either a point of the exterior ring A is definitely inside polygon B + // or vice versa. This is an intersection. + if c.PointIntersectsLinearRing(aFirstPoint, b.LinearRing(0)) { + return c.DistanceUpdater().OnIntersects(aFirstPoint) + } + return c.DistanceUpdater().OnIntersects(bFirstPoint) +} diff --git a/pkg/geo/geogen/geogen.go b/pkg/geo/geogen/geogen.go new file mode 100644 index 0000000..2245fe3 --- /dev/null +++ b/pkg/geo/geogen/geogen.go @@ -0,0 +1,355 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +// Package geogen provides utilities for generating various geospatial types. +package geogen + +import ( + "math" + "math/rand" + "sort" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geoprojbase" + "github.com/cockroachdb/errors" + "github.com/twpayne/go-geom" +) + +var validShapeTypes = []geopb.ShapeType{ + geopb.ShapeType_Point, + geopb.ShapeType_LineString, + geopb.ShapeType_Polygon, + geopb.ShapeType_MultiPoint, + geopb.ShapeType_MultiLineString, + geopb.ShapeType_MultiPolygon, + geopb.ShapeType_GeometryCollection, +} + +// RandomShapeType returns a random geometry type. +func RandomShapeType(rng *rand.Rand) geopb.ShapeType { + return validShapeTypes[rng.Intn(len(validShapeTypes))] +} + +var validLayouts = []geom.Layout{ + geom.XY, + geom.XYM, + geom.XYZ, + geom.XYZM, +} + +// RandomLayout randomly chooses a layout if no layout is provided. +func RandomLayout(rng *rand.Rand, layout geom.Layout) geom.Layout { + if layout != geom.NoLayout { + return layout + } + return validLayouts[rng.Intn(len(validLayouts))] +} + +// RandomGeomBounds is a struct for storing the random bounds for each dimension. +type RandomGeomBounds struct { + minX, maxX float64 + minY, maxY float64 + minZ, maxZ float64 + minM, maxM float64 +} + +// MakeRandomGeomBoundsForGeography creates a RandomGeomBounds struct with +// bounds corresponding to a geography. +func MakeRandomGeomBoundsForGeography() RandomGeomBounds { + randomBounds := MakeRandomGeomBounds() + randomBounds.minX, randomBounds.maxX = -180.0, 180.0 + randomBounds.minY, randomBounds.maxY = -90.0, 90.0 + return randomBounds +} + +// MakeRandomGeomBounds creates a RandomGeomBounds struct with +// bounds corresponding to a geometry. +func MakeRandomGeomBounds() RandomGeomBounds { + return RandomGeomBounds{ + minX: -10e9, maxX: 10e9, + minY: -10e9, maxY: 10e9, + minZ: -10e9, maxZ: 10e9, + minM: -10e9, maxM: 10e9, + } +} + +// RandomCoordSlice generates a slice of random coords of length corresponding +// to the given layout. +func RandomCoordSlice(rng *rand.Rand, randomBounds RandomGeomBounds, layout geom.Layout) []float64 { + if layout == geom.NoLayout { + panic(errors.Newf("must specify a layout for RandomCoordSlice")) + } + var coords []float64 + coords = append(coords, RandomCoord(rng, randomBounds.minX, randomBounds.maxX)) + coords = append(coords, RandomCoord(rng, randomBounds.minY, randomBounds.maxY)) + + if layout.ZIndex() != -1 { + coords = append(coords, RandomCoord(rng, randomBounds.minZ, randomBounds.maxZ)) + } + if layout.MIndex() != -1 { + coords = append(coords, RandomCoord(rng, randomBounds.minM, randomBounds.maxM)) + } + + return coords +} + +// RandomCoord generates a random coord in the given bounds. +func RandomCoord(rng *rand.Rand, min float64, max float64) float64 { + return rng.Float64()*(max-min) + min +} + +// RandomValidLinearRingCoords generates a flat float64 array of coordinates that represents +// a completely closed shape that can represent a simple LinearRing. This shape is always valid. +// A LinearRing must have at least 3 points. A point is added at the end to close the ring. +// Implements the algorithm in https://observablehq.com/@tarte0/generate-random-simple-polygon. +func RandomValidLinearRingCoords( + rng *rand.Rand, numPoints int, randomBounds RandomGeomBounds, layout geom.Layout, +) []geom.Coord { + layout = RandomLayout(rng, layout) + if numPoints < 3 { + panic(errors.Newf("need at least 3 points, got %d", numPoints)) + } + // Generate N random points, and find the center. + coords := make([]geom.Coord, numPoints+1) + var centerX, centerY float64 + for i := 0; i < numPoints; i++ { + coords[i] = RandomCoordSlice(rng, randomBounds, layout) + centerX += coords[i].X() + centerY += coords[i].Y() + } + + centerX /= float64(numPoints) + centerY /= float64(numPoints) + + // Sort by the angle of all the points relative to the center. + // Use ascending order of angle to get a CCW loop. + sort.Slice(coords[:numPoints], func(i, j int) bool { + angleI := math.Atan2(coords[i].Y()-centerY, coords[i].X()-centerX) + angleJ := math.Atan2(coords[j].Y()-centerY, coords[j].X()-centerX) + return angleI < angleJ + }) + + // Append the first coordinate to the end. + coords[numPoints] = coords[0] + return coords +} + +// RandomPoint generates a random Point. +func RandomPoint( + rng *rand.Rand, randomBounds RandomGeomBounds, srid geopb.SRID, layout geom.Layout, +) *geom.Point { + layout = RandomLayout(rng, layout) + // 10% chance to generate an empty point. + if rng.Intn(10) == 0 { + return geom.NewPointEmpty(layout).SetSRID(int(srid)) + } + return geom.NewPointFlat(layout, RandomCoordSlice(rng, randomBounds, layout)).SetSRID(int(srid)) +} + +// RandomLineString generates a random LineString. +func RandomLineString( + rng *rand.Rand, randomBounds RandomGeomBounds, srid geopb.SRID, layout geom.Layout, +) *geom.LineString { + layout = RandomLayout(rng, layout) + // 10% chance to generate an empty linestring. + if rng.Intn(10) == 0 { + return geom.NewLineString(layout).SetSRID(int(srid)) + } + numCoords := 3 + rng.Intn(10) + randCoords := RandomValidLinearRingCoords(rng, numCoords, randomBounds, layout) + + // Extract a random substring from the LineString by truncating at the ends. + var minTrunc, maxTrunc int + // Ensure we always have at least two points. + for maxTrunc-minTrunc < 2 { + minTrunc, maxTrunc = rng.Intn(numCoords+1), rng.Intn(numCoords+1) + // Ensure maxTrunc >= minTrunc. + if minTrunc > maxTrunc { + minTrunc, maxTrunc = maxTrunc, minTrunc + } + } + return geom.NewLineString(layout).MustSetCoords(randCoords[minTrunc:maxTrunc]).SetSRID(int(srid)) +} + +// RandomPolygon generates a random Polygon. +func RandomPolygon( + rng *rand.Rand, randomBounds RandomGeomBounds, srid geopb.SRID, layout geom.Layout, +) *geom.Polygon { + layout = RandomLayout(rng, layout) + // 10% chance to generate an empty polygon. + if rng.Intn(10) == 0 { + return geom.NewPolygon(layout).SetSRID(int(srid)) + } + // TODO(otan): generate random holes inside the Polygon. + // Ideas: + // * We can do something like use 4 arbitrary points in the LinearRing to generate a BoundingBox, + // and re-use "PointIntersectsLinearRing" to generate N random points inside the 4 points to form + // a "sub" linear ring inside. + // * Generate a random set of polygons, see which ones they fully cover and use that. + return geom.NewPolygon(layout).MustSetCoords([][]geom.Coord{ + RandomValidLinearRingCoords(rng, 3+rng.Intn(10), randomBounds, layout), + }).SetSRID(int(srid)) +} + +// RandomMultiPoint generates a random MultiPoint. +func RandomMultiPoint( + rng *rand.Rand, randomBounds RandomGeomBounds, srid geopb.SRID, layout geom.Layout, +) *geom.MultiPoint { + layout = RandomLayout(rng, layout) + ret := geom.NewMultiPoint(layout).SetSRID(int(srid)) + // 10% chance to generate an empty multipoint (when num is 0). + num := rng.Intn(10) + for i := 0; i < num; i++ { + // TODO(#62184): permit EMPTY collection item once GEOS is patched + var point *geom.Point + for point == nil || point.Empty() { + point = RandomPoint(rng, randomBounds, srid, layout) + } + if err := ret.Push(point); err != nil { + panic(err) + } + } + return ret +} + +// RandomMultiLineString generates a random MultiLineString. +func RandomMultiLineString( + rng *rand.Rand, randomBounds RandomGeomBounds, srid geopb.SRID, layout geom.Layout, +) *geom.MultiLineString { + layout = RandomLayout(rng, layout) + ret := geom.NewMultiLineString(layout).SetSRID(int(srid)) + // 10% chance to generate an empty multilinestring (when num is 0). + num := rng.Intn(10) + for i := 0; i < num; i++ { + // TODO(#62184): permit EMPTY collection item once GEOS is patched + var lineString *geom.LineString + for lineString == nil || lineString.Empty() { + lineString = RandomLineString(rng, randomBounds, srid, layout) + } + if err := ret.Push(lineString); err != nil { + panic(err) + } + } + return ret +} + +// RandomMultiPolygon generates a random MultiPolygon. +func RandomMultiPolygon( + rng *rand.Rand, randomBounds RandomGeomBounds, srid geopb.SRID, layout geom.Layout, +) *geom.MultiPolygon { + layout = RandomLayout(rng, layout) + ret := geom.NewMultiPolygon(layout).SetSRID(int(srid)) + // 10% chance to generate an empty multipolygon (when num is 0). + num := rng.Intn(10) + for i := 0; i < num; i++ { + // TODO(#62184): permit EMPTY collection item once GEOS is patched + var polygon *geom.Polygon + for polygon == nil || polygon.Empty() { + polygon = RandomPolygon(rng, randomBounds, srid, layout) + } + if err := ret.Push(polygon); err != nil { + panic(err) + } + } + return ret +} + +// RandomGeometryCollection generates a random GeometryCollection. +func RandomGeometryCollection( + rng *rand.Rand, randomBounds RandomGeomBounds, srid geopb.SRID, layout geom.Layout, +) *geom.GeometryCollection { + layout = RandomLayout(rng, layout) + ret := geom.NewGeometryCollection().SetSRID(int(srid)) + if err := ret.SetLayout(layout); err != nil { + panic(err) + } + // 10% chance to generate an empty geometrycollection (when num is 0). + num := rng.Intn(10) + for i := 0; i < num; i++ { + var shape geom.T + needShape := true + // Keep searching for a non GeometryCollection. + for needShape { + shape = RandomGeomT(rng, randomBounds, srid, layout) + _, needShape = shape.(*geom.GeometryCollection) + // TODO(#62184): permit EMPTY collection item once GEOS is patched + if shape.Empty() { + needShape = true + } + } + if err := ret.Push(shape); err != nil { + panic(err) + } + } + return ret +} + +// RandomGeomT generates a random geom.T object with the given layout within the +// given bounds and SRID. +func RandomGeomT( + rng *rand.Rand, randomBounds RandomGeomBounds, srid geopb.SRID, layout geom.Layout, +) geom.T { + layout = RandomLayout(rng, layout) + shapeType := RandomShapeType(rng) + switch shapeType { + case geopb.ShapeType_Point: + return RandomPoint(rng, randomBounds, srid, layout) + case geopb.ShapeType_LineString: + return RandomLineString(rng, randomBounds, srid, layout) + case geopb.ShapeType_Polygon: + return RandomPolygon(rng, randomBounds, srid, layout) + case geopb.ShapeType_MultiPoint: + return RandomMultiPoint(rng, randomBounds, srid, layout) + case geopb.ShapeType_MultiLineString: + return RandomMultiLineString(rng, randomBounds, srid, layout) + case geopb.ShapeType_MultiPolygon: + return RandomMultiPolygon(rng, randomBounds, srid, layout) + case geopb.ShapeType_GeometryCollection: + return RandomGeometryCollection(rng, randomBounds, srid, layout) + } + panic(errors.Newf("unknown shape type: %v", shapeType)) +} + +// RandomGeometry generates a random Geometry with the given SRID. +func RandomGeometry(rng *rand.Rand, srid geopb.SRID) geo.Geometry { + return RandomGeometryWithLayout(rng, srid, geom.NoLayout) +} + +// RandomGeometryWithLayout generates a random Geometry of a given layout with +// the given SRID. +func RandomGeometryWithLayout(rng *rand.Rand, srid geopb.SRID, layout geom.Layout) geo.Geometry { + randomBounds := MakeRandomGeomBounds() + if srid != 0 { + proj, err := geoprojbase.Projection(srid) + if err != nil { + panic(err) + } + randomBounds.minX, randomBounds.maxX = proj.Bounds.MinX, proj.Bounds.MaxX + randomBounds.minY, randomBounds.maxY = proj.Bounds.MinY, proj.Bounds.MaxY + } + ret, err := geo.MakeGeometryFromGeomT(RandomGeomT(rng, randomBounds, srid, layout)) + if err != nil { + panic(err) + } + return ret +} + +// RandomGeography generates a random Geometry with the given SRID. +func RandomGeography(rng *rand.Rand, srid geopb.SRID) geo.Geography { + return RandomGeographyWithLayout(rng, srid, geom.NoLayout) +} + +// RandomGeographyWithLayout generates a random Geometry of a given layout with +// the given SRID. +func RandomGeographyWithLayout(rng *rand.Rand, srid geopb.SRID, layout geom.Layout) geo.Geography { + // TODO(otan): generate geographies that traverse latitude/longitude boundaries. + randomBoundsGeography := MakeRandomGeomBoundsForGeography() + ret, err := geo.MakeGeographyFromGeomT(RandomGeomT(rng, randomBoundsGeography, srid, layout)) + if err != nil { + panic(err) + } + return ret +} diff --git a/pkg/geo/geogfn/azimuth.go b/pkg/geo/geogfn/azimuth.go new file mode 100644 index 0000000..4bd6806 --- /dev/null +++ b/pkg/geo/geogfn/azimuth.go @@ -0,0 +1,68 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geogfn + +import ( + "math" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/golang/geo/s2" + "github.com/twpayne/go-geom" +) + +// Azimuth returns the azimuth in radians of the segment defined by the given point geometries. +// The azimuth is angle is referenced from north, and is positive clockwise. +// North = 0; East = π/2; South = π; West = 3π/2. +// Returns nil if the two points are the same. +// Returns an error if any of the two Geography items are not points. +func Azimuth(a geo.Geography, b geo.Geography) (*float64, error) { + if a.SRID() != b.SRID() { + return nil, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + + aGeomT, err := a.AsGeomT() + if err != nil { + return nil, err + } + + aPoint, ok := aGeomT.(*geom.Point) + if !ok { + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "arguments must be POINT geometries") + } + + bGeomT, err := b.AsGeomT() + if err != nil { + return nil, err + } + + bPoint, ok := bGeomT.(*geom.Point) + if !ok { + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "arguments must be POINT geometries") + } + + if aPoint.Empty() || bPoint.Empty() { + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "cannot call ST_Azimuth with POINT EMPTY") + } + + if aPoint.X() == bPoint.X() && aPoint.Y() == bPoint.Y() { + return nil, nil + } + + s, err := spheroidFromGeography(a) + if err != nil { + return nil, err + } + + _, az1, _ := s.Inverse( + s2.LatLngFromDegrees(aPoint.Y(), aPoint.X()), + s2.LatLngFromDegrees(bPoint.Y(), bPoint.X()), + ) + // Convert to radians. + az1 = az1 * math.Pi / 180 + return &az1, nil +} diff --git a/pkg/geo/geogfn/best_projection.go b/pkg/geo/geogfn/best_projection.go new file mode 100644 index 0000000..21b7c7a --- /dev/null +++ b/pkg/geo/geogfn/best_projection.go @@ -0,0 +1,134 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geogfn + +import ( + "fmt" + "math" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geoprojbase" + "github.com/golang/geo/s1" + "github.com/golang/geo/s2" +) + +// BestGeomProjection translates roughly to the ST_BestSRID function in PostGIS. +// It attempts to find the best projection for a bounding box into an accurate +// geometry-type projection. +// +// The algorithm is described by ST_Buffer/ST_Intersection documentation (paraphrased): +// +// It first determines the best SRID that fits the bounding box of the 2 geography objects (ST_Intersection only). +// It favors a north/south pole projection, then UTM, then LAEA for smaller zones, otherwise falling back +// to web mercator. +// If geography objects are within one half zone UTM but not the same UTM it will pick one of those. +// After the calculation is complete, it will fall back to WGS84 Geography. +func BestGeomProjection(boundingRect s2.Rect) (geoprojbase.Proj4Text, error) { + center := boundingRect.Center() + + latWidth := s1.Angle(boundingRect.Lat.Length()) + lngWidth := s1.Angle(boundingRect.Lng.Length()) + + // Check if these fit either the North Pole or South Pole areas. + // If the center has latitude greater than 70 (an arbitrary polar threshold), and it is + // within the polar ranges, return that. + if center.Lat.Degrees() > 70 && boundingRect.Lo().Lat.Degrees() > 45 { + // See: https://epsg.io/3574. + return getGeomProjection(3574) + } + // Same for south pole. + if center.Lat.Degrees() < -70 && boundingRect.Hi().Lat.Degrees() < -45 { + // See: https://epsg.io/3409 + return getGeomProjection(3409) + } + + // Each UTM zone is 6 degrees wide and distortion is low for geometries that fit within the zone. We use + // UTM if the width is lower than the UTM zone width, even though the geometry may span 2 zones -- using + // the geometry center to pick the UTM zone should result in most of the geometry being in the picked zone. + if lngWidth.Degrees() < 6 { + // Determine the offset of the projection. + // Offset longitude -180 to 0 and divide by 6 to get the zone. + // Note that we treat 180 degree longitudes as offset 59. + // TODO(#geo): do we care about https://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system#Exceptions? + // PostGIS's _ST_BestSRID function doesn't seem to care: . + sridOffset := geopb.SRID(math.Min(math.Floor((center.Lng.Degrees()+180)/6), 59)) + if center.Lat.Degrees() >= 0 { + // Start at the north UTM SRID. + return getGeomProjection(32601 + sridOffset) + } + // Start at the south UTM SRID. + // This should make no difference in end result compared to using the north UTMs, + // but for completeness we do it. + return getGeomProjection(32701 + sridOffset) + } + + // Attempt to fit into LAEA areas if the width is less than 25 degrees (we can go up to 30 + // but want to leave some room for precision issues). + // + // LAEA areas are separated into 3 latitude zones between 0 and 90 and 3 latitude zones + // between -90 and 0. Within each latitude zones, they have different longitude bands: + // * The bands closest to the equator have 12x30 degree longitude zones. + // * The bands in the temperate area 8x45 degree longitude zones. + // * The bands near the poles have 4x90 degree longitude zones. + // + // For each of these bands, we custom define a LAEA area with the center of the LAEA area + // as the lat/lon offset. + // + // See also: https://en.wikipedia.org/wiki/Lambert_azimuthal_equal-area_projection. + if latWidth.Degrees() < 25 { + // Convert lat to a known 30 degree zone.. + // -3 represents [-90, -60), -2 represents [-60, -30) ... and 2 represents [60, 90]. + // (note: 90 is inclusive at the end). + // Treat a 90 degree latitude as band 2. + latZone := math.Min(math.Floor(center.Lat.Degrees()/30), 2) + latZoneCenterDegrees := (latZone * 30) + 15 + // Equator bands - 30 degree zones. + if (latZone == 0 || latZone == -1) && lngWidth.Degrees() <= 30 { + lngZone := math.Floor(center.Lng.Degrees() / 30) + return geoprojbase.MakeProj4Text( + fmt.Sprintf( + "+proj=laea +ellps=WGS84 +datum=WGS84 +lat_0=%g +lon_0=%g +units=m +no_defs", + latZoneCenterDegrees, + (lngZone*30)+15, + ), + ), nil + } + // Temperate bands - 45 degree zones. + if (latZone == -2 || latZone == 1) && lngWidth.Degrees() <= 45 { + lngZone := math.Floor(center.Lng.Degrees() / 45) + return geoprojbase.MakeProj4Text( + fmt.Sprintf( + "+proj=laea +ellps=WGS84 +datum=WGS84 +lat_0=%g +lon_0=%g +units=m +no_defs", + latZoneCenterDegrees, + (lngZone*45)+22.5, + ), + ), nil + } + // Polar bands -- 90 degree zones. + if (latZone == -3 || latZone == 2) && lngWidth.Degrees() <= 90 { + lngZone := math.Floor(center.Lng.Degrees() / 90) + return geoprojbase.MakeProj4Text( + fmt.Sprintf( + "+proj=laea +ellps=WGS84 +datum=WGS84 +lat_0=%g +lon_0=%g +units=m +no_defs", + latZoneCenterDegrees, + (lngZone*90)+45, + ), + ), nil + } + } + + // Default to Web Mercator. + return getGeomProjection(3857) +} + +// getGeomProjection returns the Proj4Text associated with an SRID. +func getGeomProjection(srid geopb.SRID) (geoprojbase.Proj4Text, error) { + proj, err := geoprojbase.Projection(srid) + if err != nil { + return geoprojbase.Proj4Text{}, err + } + return proj.Proj4Text, nil +} diff --git a/pkg/geo/geogfn/covers.go b/pkg/geo/geogfn/covers.go new file mode 100644 index 0000000..1de0397 --- /dev/null +++ b/pkg/geo/geogfn/covers.go @@ -0,0 +1,300 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geogfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/golang/geo/s2" +) + +// Covers returns whether geography A covers geography B. +// +// This calculation is done on the sphere. +// +// Due to minor inaccuracies and lack of certain primitives in S2, +// precision for Covers will be for up to 1cm. +// +// Current limitations (which are also limitations in PostGIS): +// +// - POLYGON/LINESTRING only works as "contains" - if any point of the LINESTRING +// touches the boundary of the polygon, we will return false but should be true - e.g. +// SELECT st_covers( +// 'multipolygon(((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0)), ((1.0 0.0, 2.0 0.0, 2.0 1.0, 1.0 1.0, 1.0 0.0)))', +// 'linestring(0.0 0.0, 1.0 0.0)'::geography +// ); +// +// - Furthermore, LINESTRINGS that are covered in multiple POLYGONs inside +// MULTIPOLYGON but NOT within a single POLYGON in the MULTIPOLYGON +// currently return false but should be true, e.g. +// SELECT st_covers( +// 'multipolygon(((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0)), ((1.0 0.0, 2.0 0.0, 2.0 1.0, 1.0 1.0, 1.0 0.0)))', +// 'linestring(0.0 0.0, 2.0 0.0)'::geography +// ); +func Covers(a geo.Geography, b geo.Geography) (bool, error) { + if a.SRID() != b.SRID() { + return false, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + return covers(a, b) +} + +// covers is the internal calculation for Covers. +func covers(a geo.Geography, b geo.Geography) (bool, error) { + // Rect "contains" is a version of covers. + if !a.BoundingRect().Contains(b.BoundingRect()) { + return false, nil + } + + // Ignore EMPTY regions in a. + aRegions, err := a.AsS2(geo.EmptyBehaviorOmit) + if err != nil { + return false, err + } + // If any of b is empty, we cannot cover it. Error and catch to return false. + bRegions, err := b.AsS2(geo.EmptyBehaviorError) + if err != nil { + if geo.IsEmptyGeometryError(err) { + return false, nil + } + return false, err + } + + // We need to check each region in B is covered by at least + // one region of A. + bRegionsRemaining := make(map[int]struct{}, len(bRegions)) + for i := range bRegions { + bRegionsRemaining[i] = struct{}{} + } + for _, aRegion := range aRegions { + for bRegionIdx := range bRegionsRemaining { + regionCovers, err := regionCovers(aRegion, bRegions[bRegionIdx]) + if err != nil { + return false, err + } + if regionCovers { + delete(bRegionsRemaining, bRegionIdx) + } + } + if len(bRegionsRemaining) == 0 { + return true, nil + } + } + return false, nil +} + +// CoveredBy returns whether geography A is covered by geography B. +// See Covers for limitations. +func CoveredBy(a geo.Geography, b geo.Geography) (bool, error) { + if a.SRID() != b.SRID() { + return false, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + return covers(b, a) +} + +// regionCovers returns whether aRegion completely covers bRegion. +func regionCovers(aRegion s2.Region, bRegion s2.Region) (bool, error) { + switch aRegion := aRegion.(type) { + case s2.Point: + switch bRegion := bRegion.(type) { + case s2.Point: + return aRegion.ContainsPoint(bRegion), nil + case *s2.Polyline: + return false, nil + case *s2.Polygon: + return false, nil + default: + return false, pgerror.Newf(pgcode.InvalidParameterValue, "unknown s2 type of b: %#v", bRegion) + } + case *s2.Polyline: + switch bRegion := bRegion.(type) { + case s2.Point: + return polylineCoversPoint(aRegion, bRegion), nil + case *s2.Polyline: + return polylineCoversPolyline(aRegion, bRegion), nil + case *s2.Polygon: + return false, nil + default: + return false, pgerror.Newf(pgcode.InvalidParameterValue, "unknown s2 type of b: %#v", bRegion) + } + case *s2.Polygon: + switch bRegion := bRegion.(type) { + case s2.Point: + return polygonCoversPoint(aRegion, bRegion), nil + case *s2.Polyline: + return polygonCoversPolyline(aRegion, bRegion), nil + case *s2.Polygon: + return polygonCoversPolygon(aRegion, bRegion), nil + default: + return false, pgerror.Newf(pgcode.InvalidParameterValue, "unknown s2 type of b: %#v", bRegion) + } + } + return false, pgerror.Newf(pgcode.InvalidParameterValue, "unknown s2 type of a: %#v", aRegion) +} + +// polylineCoversPoints returns whether a polyline covers a given point. +func polylineCoversPoint(a *s2.Polyline, b s2.Point) bool { + return a.IntersectsCell(s2.CellFromPoint(b)) +} + +// polylineCoversPointsWithIdx returns whether a polyline covers a given point. +// If true, it will also return an index of the start of the edge where there +// was an intersection. +func polylineCoversPointWithIdx(a *s2.Polyline, b s2.Point) (bool, int) { + for edgeIdx, aNumEdges := 0, a.NumEdges(); edgeIdx < aNumEdges; edgeIdx++ { + if edgeCoversPoint(a.Edge(edgeIdx), b) { + return true, edgeIdx + } + } + return false, -1 +} + +// polygonCoversPoints returns whether a polygon covers a given point. +func polygonCoversPoint(a *s2.Polygon, b s2.Point) bool { + return a.IntersectsCell(s2.CellFromPoint(b)) +} + +// edgeCoversPoint determines whether a given edge contains a point. +func edgeCoversPoint(e s2.Edge, p s2.Point) bool { + return (&s2.Polyline{e.V0, e.V1}).IntersectsCell(s2.CellFromPoint(p)) +} + +// polylineCoversPolyline returns whether polyline a covers polyline b. +func polylineCoversPolyline(a *s2.Polyline, b *s2.Polyline) bool { + if polylineCoversPolylineOrdered(a, b) { + return true + } + // Check reverse ordering works as well. + reversedB := make([]s2.Point, len(*b)) + for i, point := range *b { + reversedB[len(reversedB)-1-i] = point + } + newBAsPolyline := s2.Polyline(reversedB) + return polylineCoversPolylineOrdered(a, &newBAsPolyline) +} + +// polylineCoversPolylineOrdered returns whether a polyline covers a polyline +// in the same ordering. +func polylineCoversPolylineOrdered(a *s2.Polyline, b *s2.Polyline) bool { + aCoversStartOfB, aCoverBStart := polylineCoversPointWithIdx(a, (*b)[0]) + // We must first check that the start of B is contained by A. + if !aCoversStartOfB { + return false + } + + aPoints := *a + bPoints := *b + // We have found "aIdx" which is the first edge in polyline A + // that includes the starting vertex of polyline "B". + // Start checking the covering from this edge. + aIdx := aCoverBStart + bIdx := 0 + + aEdge := s2.Edge{V0: aPoints[aIdx], V1: aPoints[aIdx+1]} + bEdge := s2.Edge{V0: bPoints[bIdx], V1: bPoints[bIdx+1]} + for { + aEdgeCoversBStart := edgeCoversPoint(aEdge, bEdge.V0) + aEdgeCoversBEnd := edgeCoversPoint(aEdge, bEdge.V1) + bEdgeCoversAEnd := edgeCoversPoint(bEdge, aEdge.V1) + if aEdgeCoversBStart && aEdgeCoversBEnd { + // If the edge A fully covers edge B, check the next edge. + bIdx++ + // We are out of edges in B, and A keeps going or stops at the same point. + // This is a covering. + if bIdx == len(bPoints)-1 { + return true + } + bEdge = s2.Edge{V0: bPoints[bIdx], V1: bPoints[bIdx+1]} + // If A and B end at the same place, we need to move A forward. + if bEdgeCoversAEnd { + aIdx++ + if aIdx == len(aPoints)-1 { + // At this point, B extends past A. return false. + return false + } + aEdge = s2.Edge{V0: aPoints[aIdx], V1: aPoints[aIdx+1]} + } + continue + } + + if aEdgeCoversBStart { + // Edge A doesn't cover the end of B, but it does cover the start. + // If B doesn't cover the end of A, we're done. + if !bEdgeCoversAEnd { + return false + } + // If the end of edge B covers the end of A, that means that + // B is possibly longer than A. If that's the case, truncate B + // to be the end of A, and move A forward. + bEdge.V0 = aEdge.V1 + aIdx++ + if aIdx == len(aPoints)-1 { + // At this point, B extends past A. return false. + return false + } + aEdge = s2.Edge{V0: aPoints[aIdx], V1: aPoints[aIdx+1]} + continue + } + + // Otherwise, we're doomed. + // Edge A does not contain edge B. + return false + } +} + +// polygonCoversPolyline returns whether polygon a covers polyline b. +func polygonCoversPolyline(a *s2.Polygon, b *s2.Polyline) bool { + // Check everything of polyline B is in the interior of polygon A. + for _, vertex := range *b { + if !polygonCoversPoint(a, vertex) { + return false + } + } + // Even if every point of polyline B is inside polygon A, they + // may form an edge which goes outside of polygon A and back in + // due to holes and concavities. + // + // As such, check if there are any intersections - if so, + // we do not consider it a covering. + // + // NOTE: this implementation has a limitation where a vertex of the line could + // be on the boundary and still technically be "covered" (using GEOS). + // + // However, PostGIS seems to consider this as non-covering so we can go + // with this for now. + // i.e. ` + // select st_covers( + // 'POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0))'::geography, + // 'LINESTRING(0.0 0.0, 1.0 1.0)'::geography); + // ` returns false, but should be true. This requires some more math to resolve. + return !polygonIntersectsPolylineEdge(a, b) +} + +// polygonIntersectsPolylineEdge returns whether polygon a intersects with +// polyline b by edge. It does not return true if the polyline is completely +// within the polygon. +func polygonIntersectsPolylineEdge(a *s2.Polygon, b *s2.Polyline) bool { + // Avoid using NumEdges / Edge of the Polygon type as it is not O(1). + for _, loop := range a.Loops() { + for loopEdgeIdx, loopNumEdges := 0, loop.NumEdges(); loopEdgeIdx < loopNumEdges; loopEdgeIdx++ { + loopEdge := loop.Edge(loopEdgeIdx) + crosser := s2.NewChainEdgeCrosser(loopEdge.V0, loopEdge.V1, (*b)[0]) + for _, nextVertex := range (*b)[1:] { + if crosser.ChainCrossingSign(nextVertex) != s2.DoNotCross { + return true + } + } + } + } + return false +} + +// polygonCoversPolygon returns whether polygon a intersects with polygon b. +func polygonCoversPolygon(a *s2.Polygon, b *s2.Polygon) bool { + // We can rely on Contains here, as if the boundaries of A and B are on top + // of each other, it is still considered a containment as well as a covering. + return a.Contains(b) +} diff --git a/pkg/geo/geogfn/distance.go b/pkg/geo/geogfn/distance.go new file mode 100644 index 0000000..99c846f --- /dev/null +++ b/pkg/geo/geogfn/distance.go @@ -0,0 +1,413 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geogfn + +import ( + "math" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geodist" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geoprojbase" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/golang/geo/s1" + "github.com/golang/geo/s2" +) + +// SpheroidErrorFraction is an error fraction to compensate for using a sphere +// to calculate the distance for what is actually a spheroid. The distance +// calculation has an error that is bounded by (2 * spheroid.flattening)%. +// This 5% margin is pretty safe. +const SpheroidErrorFraction = 0.05 + +// Distance returns the distance between geographies a and b on a sphere or spheroid. +// Returns a geo.EmptyGeometryError if any of the Geographies are EMPTY. +func Distance( + a geo.Geography, b geo.Geography, useSphereOrSpheroid UseSphereOrSpheroid, +) (float64, error) { + if a.SRID() != b.SRID() { + return 0, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + + aRegions, err := a.AsS2(geo.EmptyBehaviorError) + if err != nil { + return 0, err + } + bRegions, err := b.AsS2(geo.EmptyBehaviorError) + if err != nil { + return 0, err + } + if BoundingBoxHasNaNCoordinates(a) { + return 0, geo.OutOfRangeError() + } + if BoundingBoxHasNaNCoordinates(b) { + return 0, geo.OutOfRangeError() + } + spheroid, err := spheroidFromGeography(a) + if err != nil { + return 0, err + } + return distanceGeographyRegions( + spheroid, + useSphereOrSpheroid, + aRegions, + bRegions, + a.BoundingRect().Intersects(b.BoundingRect()), + 0, /* stopAfter */ + geo.FnInclusive, + ) +} + +// +// Spheroids +// + +// s2GeodistLineString implements geodist.LineString. +type s2GeodistLineString struct { + *s2.Polyline +} + +var _ geodist.LineString = (*s2GeodistLineString)(nil) + +// IsShape implements the geodist.LineString interface. +func (*s2GeodistLineString) IsShape() {} + +// LineString implements the geodist.LineString interface. +func (*s2GeodistLineString) IsLineString() {} + +// Edge implements the geodist.LineString interface. +func (g *s2GeodistLineString) Edge(i int) geodist.Edge { + return geodist.Edge{ + V0: geodist.Point{GeogPoint: (*g.Polyline)[i]}, + V1: geodist.Point{GeogPoint: (*g.Polyline)[i+1]}, + } +} + +// NumEdges implements the geodist.LineString interface. +func (g *s2GeodistLineString) NumEdges() int { + return len(*g.Polyline) - 1 +} + +// Vertex implements the geodist.LineString interface. +func (g *s2GeodistLineString) Vertex(i int) geodist.Point { + return geodist.Point{ + GeogPoint: (*g.Polyline)[i], + } +} + +// NumVertexes implements the geodist.LineString interface. +func (g *s2GeodistLineString) NumVertexes() int { + return len(*g.Polyline) +} + +// s2GeodistLinearRing implements geodist.LinearRing. +type s2GeodistLinearRing struct { + *s2.Loop +} + +var _ geodist.LinearRing = (*s2GeodistLinearRing)(nil) + +// IsShape implements the geodist.LinearRing interface. +func (*s2GeodistLinearRing) IsShape() {} + +// LinearRing implements the geodist.LinearRing interface. +func (*s2GeodistLinearRing) IsLinearRing() {} + +// Edge implements the geodist.LinearRing interface. +func (g *s2GeodistLinearRing) Edge(i int) geodist.Edge { + return geodist.Edge{ + V0: geodist.Point{GeogPoint: g.Loop.Vertex(i)}, + V1: geodist.Point{GeogPoint: g.Loop.Vertex(i + 1)}, + } +} + +// NumEdges implements the geodist.LinearRing interface. +func (g *s2GeodistLinearRing) NumEdges() int { + return g.Loop.NumEdges() +} + +// Vertex implements the geodist.LinearRing interface. +func (g *s2GeodistLinearRing) Vertex(i int) geodist.Point { + return geodist.Point{ + GeogPoint: g.Loop.Vertex(i), + } +} + +// NumVertexes implements the geodist.LinearRing interface. +func (g *s2GeodistLinearRing) NumVertexes() int { + return g.Loop.NumVertices() +} + +// s2GeodistPolygon implements geodist.Polygon. +type s2GeodistPolygon struct { + *s2.Polygon +} + +var _ geodist.Polygon = (*s2GeodistPolygon)(nil) + +// IsShape implements the geodist.Polygon interface. +func (*s2GeodistPolygon) IsShape() {} + +// Polygon implements the geodist.Polygon interface. +func (*s2GeodistPolygon) IsPolygon() {} + +// LinearRing implements the geodist.Polygon interface. +func (g *s2GeodistPolygon) LinearRing(i int) geodist.LinearRing { + return &s2GeodistLinearRing{Loop: g.Polygon.Loop(i)} +} + +// NumLinearRings implements the geodist.Polygon interface. +func (g *s2GeodistPolygon) NumLinearRings() int { + return g.Polygon.NumLoops() +} + +// s2GeodistEdgeCrosser implements geodist.EdgeCrosser. +type s2GeodistEdgeCrosser struct { + *s2.EdgeCrosser +} + +var _ geodist.EdgeCrosser = (*s2GeodistEdgeCrosser)(nil) + +// ChainCrossing implements geodist.EdgeCrosser. +func (c *s2GeodistEdgeCrosser) ChainCrossing(p geodist.Point) (bool, geodist.Point) { + // Returns nil for the intersection point as we don't require the intersection + // point as we do not have to implement ShortestLine in geography. + return c.EdgeCrosser.ChainCrossingSign(p.GeogPoint) != s2.DoNotCross, geodist.Point{} +} + +// distanceGeographyRegions calculates the distance between two sets of regions. +// If inclusive, it will quit if it finds a distance that is less than or equal +// to stopAfter. Otherwise, it will quit if a distance less than stopAfter is +// found. It is not guaranteed to find the absolute minimum distance if +// stopAfter > 0. +// +// !!! SURPRISING BEHAVIOR WARNING FOR SPHEROIDS !!! +// PostGIS evaluates the distance between spheroid regions by computing the min of +// the pair-wise distance between the cross-product of the regions in A and the regions +// in B, where the pair-wise distance is computed as: +// - Find the two closest points between the pairs of regions using the sphere +// for distance calculations. +// - Compute the spheroid distance between the two closest points. +// +// This is technically incorrect, since it is possible that the two closest points on +// the spheroid are different than the two closest points on the sphere. +// See distance_test.go for examples of the "truer" distance values. +// Since we aim to be compatible with PostGIS, we adopt the same approach. +func distanceGeographyRegions( + spheroid geoprojbase.Spheroid, + useSphereOrSpheroid UseSphereOrSpheroid, + aRegions []s2.Region, + bRegions []s2.Region, + boundingBoxIntersects bool, + stopAfter float64, + exclusivity geo.FnExclusivity, +) (float64, error) { + minDistance := math.MaxFloat64 + for _, aRegion := range aRegions { + aGeodist, err := regionToGeodistShape(aRegion) + if err != nil { + return 0, err + } + for _, bRegion := range bRegions { + minDistanceUpdater := newGeographyMinDistanceUpdater( + spheroid, + useSphereOrSpheroid, + stopAfter, + exclusivity, + ) + bGeodist, err := regionToGeodistShape(bRegion) + if err != nil { + return 0, err + } + earlyExit, err := geodist.ShapeDistance( + &geographyDistanceCalculator{ + updater: minDistanceUpdater, + boundingBoxIntersects: boundingBoxIntersects, + }, + aGeodist, + bGeodist, + ) + if err != nil { + return 0, err + } + minDistance = math.Min(minDistance, minDistanceUpdater.Distance()) + if earlyExit { + return minDistance, nil + } + } + } + return minDistance, nil +} + +// geographyMinDistanceUpdater finds the minimum distance using a sphere. +// Methods will return early if it finds a minimum distance <= stopAfterLE. +type geographyMinDistanceUpdater struct { + spheroid geoprojbase.Spheroid + useSphereOrSpheroid UseSphereOrSpheroid + minEdge s2.Edge + minD s1.ChordAngle + stopAfter s1.ChordAngle + exclusivity geo.FnExclusivity +} + +var _ geodist.DistanceUpdater = (*geographyMinDistanceUpdater)(nil) + +// newGeographyMinDistanceUpdater returns a new geographyMinDistanceUpdater with the +// correct arguments set up. +func newGeographyMinDistanceUpdater( + spheroid geoprojbase.Spheroid, + useSphereOrSpheroid UseSphereOrSpheroid, + stopAfter float64, + exclusivity geo.FnExclusivity, +) *geographyMinDistanceUpdater { + multiplier := 1.0 + if useSphereOrSpheroid == UseSpheroid { + // Modify the stopAfterLE distance to be less by the error fraction, since + // we use the sphere to calculate the distance and we want to leave a + // buffer for spheroid distances being slightly off. + multiplier -= SpheroidErrorFraction + } + stopAfterChordAngle := s1.ChordAngleFromAngle(s1.Angle(stopAfter * multiplier / spheroid.SphereRadius())) + return &geographyMinDistanceUpdater{ + spheroid: spheroid, + minD: math.MaxFloat64, + useSphereOrSpheroid: useSphereOrSpheroid, + stopAfter: stopAfterChordAngle, + exclusivity: exclusivity, + } +} + +// Distance implements the DistanceUpdater interface. +func (u *geographyMinDistanceUpdater) Distance() float64 { + // If the distance is zero, avoid the call to spheroidDistance and return early. + if u.minD == 0 { + return 0 + } + if u.useSphereOrSpheroid == UseSpheroid { + return spheroidDistance(u.spheroid, u.minEdge.V0, u.minEdge.V1) + } + return u.minD.Angle().Radians() * u.spheroid.SphereRadius() +} + +// Update implements the geodist.DistanceUpdater interface. +func (u *geographyMinDistanceUpdater) Update(aPoint geodist.Point, bPoint geodist.Point) bool { + a := aPoint.GeogPoint + b := bPoint.GeogPoint + + sphereDistance := s2.ChordAngleBetweenPoints(a, b) + if sphereDistance < u.minD { + u.minD = sphereDistance + u.minEdge = s2.Edge{V0: a, V1: b} + // If we have a threshold, determine if we can stop early. + // If the sphere distance is within range of the stopAfter, we can + // definitively say we've reach the close enough point. + if (u.exclusivity == geo.FnInclusive && u.minD <= u.stopAfter) || + (u.exclusivity == geo.FnExclusive && u.minD < u.stopAfter) { + return true + } + } + return false +} + +// OnIntersects implements the geodist.DistanceUpdater interface. +func (u *geographyMinDistanceUpdater) OnIntersects(p geodist.Point) bool { + u.minD = 0 + return true +} + +// IsMaxDistance implements the geodist.DistanceUpdater interface. +func (u *geographyMinDistanceUpdater) IsMaxDistance() bool { + return false +} + +// FlipGeometries implements the geodist.DistanceUpdater interface. +func (u *geographyMinDistanceUpdater) FlipGeometries() { + // FlipGeometries is unimplemented for geographyMinDistanceUpdater as we don't + // require the order of geometries for calculation of minimum distance. +} + +// geographyDistanceCalculator implements geodist.DistanceCalculator +type geographyDistanceCalculator struct { + updater *geographyMinDistanceUpdater + boundingBoxIntersects bool +} + +var _ geodist.DistanceCalculator = (*geographyDistanceCalculator)(nil) + +// DistanceUpdater implements geodist.DistanceCalculator. +func (c *geographyDistanceCalculator) DistanceUpdater() geodist.DistanceUpdater { + return c.updater +} + +// BoundingBoxIntersects implements geodist.DistanceCalculator. +func (c *geographyDistanceCalculator) BoundingBoxIntersects() bool { + return c.boundingBoxIntersects +} + +// NewEdgeCrosser implements geodist.DistanceCalculator. +func (c *geographyDistanceCalculator) NewEdgeCrosser( + edge geodist.Edge, startPoint geodist.Point, +) geodist.EdgeCrosser { + return &s2GeodistEdgeCrosser{ + EdgeCrosser: s2.NewChainEdgeCrosser( + edge.V0.GeogPoint, + edge.V1.GeogPoint, + startPoint.GeogPoint, + ), + } +} + +// PointIntersectsLinearRing implements geodist.DistanceCalculator. +func (c *geographyDistanceCalculator) PointIntersectsLinearRing( + point geodist.Point, polygon geodist.LinearRing, +) bool { + return polygon.(*s2GeodistLinearRing).ContainsPoint(point.GeogPoint) +} + +// ClosestPointToEdge implements geodist.DistanceCalculator. +// +// ClosestPointToEdge projects the point onto the infinite line represented +// by the edge. This will return the point on the line closest to the edge. +// It will return the closest point on the line, as well as a bool representing +// whether the point that is projected lies directly on the edge as a segment. +// +// For visualization and more, see: Section 6 / Figure 4 of +// "Projective configuration theorems: old wine into new wineskins", Tabachnikov, Serge, 2016/07/16 +func (c *geographyDistanceCalculator) ClosestPointToEdge( + edge geodist.Edge, point geodist.Point, +) (geodist.Point, bool) { + eV0 := edge.V0.GeogPoint + eV1 := edge.V1.GeogPoint + + // Project the point onto the normal of the edge. A great circle passing through + // the normal and the point will intersect with the great circle represented + // by the given edge. + normal := eV0.Vector.Cross(eV1.Vector).Normalize() + // To find the point where the great circle represented by the edge and the + // great circle represented by (normal, point), we project the point + // onto the normal. + normalScaledToPoint := normal.Mul(normal.Dot(point.GeogPoint.Vector)) + // The difference between the point and the projection of the normal when normalized + // should give us a point on the great circle which contains the vertexes of the edge. + closestPoint := s2.Point{Vector: point.GeogPoint.Vector.Sub(normalScaledToPoint).Normalize()} + // We then check whether the given point lies on the geodesic of the edge, + // as the above algorithm only generates a point on the great circle + // represented by the edge. + return geodist.Point{GeogPoint: closestPoint}, (&s2.Polyline{eV0, eV1}).IntersectsCell(s2.CellFromPoint(closestPoint)) +} + +// regionToGeodistShape converts the s2 Region to a geodist object. +func regionToGeodistShape(r s2.Region) (geodist.Shape, error) { + switch r := r.(type) { + case s2.Point: + return &geodist.Point{GeogPoint: r}, nil + case *s2.Polyline: + return &s2GeodistLineString{Polyline: r}, nil + case *s2.Polygon: + return &s2GeodistPolygon{Polygon: r}, nil + } + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "unknown region: %T", r) +} diff --git a/pkg/geo/geogfn/dwithin.go b/pkg/geo/geogfn/dwithin.go new file mode 100644 index 0000000..509b486 --- /dev/null +++ b/pkg/geo/geogfn/dwithin.go @@ -0,0 +1,75 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geogfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/golang/geo/s1" +) + +// DWithin returns whether a is within distance d of b. If A or B contains empty +// Geography objects, this will return false. If inclusive, DWithin is +// equivalent to Distance(a, b) <= d. Otherwise, DWithin is instead equivalent +// to Distance(a, b) < d. +func DWithin( + a geo.Geography, + b geo.Geography, + distance float64, + useSphereOrSpheroid UseSphereOrSpheroid, + exclusivity geo.FnExclusivity, +) (bool, error) { + if a.SRID() != b.SRID() { + return false, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + if distance < 0 { + return false, pgerror.Newf(pgcode.InvalidParameterValue, "dwithin distance cannot be less than zero") + } + spheroid, err := spheroidFromGeography(a) + if err != nil { + return false, err + } + + angleToExpand := s1.Angle(distance / spheroid.SphereRadius()) + if useSphereOrSpheroid == UseSpheroid { + angleToExpand *= (1 + SpheroidErrorFraction) + } + if !a.BoundingCap().Expanded(angleToExpand).Intersects(b.BoundingCap()) { + return false, nil + } + + aRegions, err := a.AsS2(geo.EmptyBehaviorError) + if err != nil { + if geo.IsEmptyGeometryError(err) { + return false, nil + } + return false, err + } + bRegions, err := b.AsS2(geo.EmptyBehaviorError) + if err != nil { + if geo.IsEmptyGeometryError(err) { + return false, nil + } + return false, err + } + maybeClosestDistance, err := distanceGeographyRegions( + spheroid, + useSphereOrSpheroid, + aRegions, + bRegions, + a.BoundingRect().Intersects(b.BoundingRect()), + distance, + exclusivity, + ) + if err != nil { + return false, err + } + if exclusivity == geo.FnExclusive { + return maybeClosestDistance < distance, nil + } + return maybeClosestDistance <= distance, nil +} diff --git a/pkg/geo/geogfn/geogfn.go b/pkg/geo/geogfn/geogfn.go new file mode 100644 index 0000000..6934e56 --- /dev/null +++ b/pkg/geo/geogfn/geogfn.go @@ -0,0 +1,17 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geogfn + +// UseSphereOrSpheroid indicates whether to use a Sphere or Spheroid +// for certain calculations. +type UseSphereOrSpheroid bool + +const ( + // UseSpheroid indicates to use the spheroid for calculations. + UseSpheroid UseSphereOrSpheroid = true + // UseSphere indicates to use the sphere for calculations. + UseSphere UseSphereOrSpheroid = false +) diff --git a/pkg/geo/geogfn/intersects.go b/pkg/geo/geogfn/intersects.go new file mode 100644 index 0000000..ad71adb --- /dev/null +++ b/pkg/geo/geogfn/intersects.go @@ -0,0 +1,132 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geogfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/golang/geo/s2" +) + +// Intersects returns whether geography A intersects geography B. +// This calculation is done on the sphere. +// Precision of intersect measurements is up to 1cm. +func Intersects(a geo.Geography, b geo.Geography) (bool, error) { + if !a.BoundingRect().Intersects(b.BoundingRect()) { + return false, nil + } + if a.SRID() != b.SRID() { + return false, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + + aRegions, err := a.AsS2(geo.EmptyBehaviorOmit) + if err != nil { + return false, err + } + bRegions, err := b.AsS2(geo.EmptyBehaviorOmit) + if err != nil { + return false, err + } + // If any of aRegions intersects any of bRegions, return true. + for _, aRegion := range aRegions { + for _, bRegion := range bRegions { + intersects, err := singleRegionIntersects(aRegion, bRegion) + if err != nil { + return false, err + } + if intersects { + return true, nil + } + } + } + return false, nil +} + +// singleRegionIntersects returns true if aRegion intersects bRegion. +func singleRegionIntersects(aRegion s2.Region, bRegion s2.Region) (bool, error) { + switch aRegion := aRegion.(type) { + case s2.Point: + switch bRegion := bRegion.(type) { + case s2.Point: + return aRegion.IntersectsCell(s2.CellFromPoint(bRegion)), nil + case *s2.Polyline: + return bRegion.IntersectsCell(s2.CellFromPoint(aRegion)), nil + case *s2.Polygon: + return bRegion.IntersectsCell(s2.CellFromPoint(aRegion)), nil + default: + return false, pgerror.Newf(pgcode.InvalidParameterValue, "unknown s2 type of b: %#v", bRegion) + } + case *s2.Polyline: + switch bRegion := bRegion.(type) { + case s2.Point: + return aRegion.IntersectsCell(s2.CellFromPoint(bRegion)), nil + case *s2.Polyline: + return polylineIntersectsPolyline(aRegion, bRegion), nil + case *s2.Polygon: + return polygonIntersectsPolyline(bRegion, aRegion), nil + default: + return false, pgerror.Newf(pgcode.InvalidParameterValue, "unknown s2 type of b: %#v", bRegion) + } + case *s2.Polygon: + switch bRegion := bRegion.(type) { + case s2.Point: + return aRegion.IntersectsCell(s2.CellFromPoint(bRegion)), nil + case *s2.Polyline: + return polygonIntersectsPolyline(aRegion, bRegion), nil + case *s2.Polygon: + return aRegion.Intersects(bRegion), nil + default: + return false, pgerror.Newf(pgcode.InvalidParameterValue, "unknown s2 type of b: %#v", bRegion) + } + } + return false, pgerror.Newf(pgcode.InvalidParameterValue, "unknown s2 type of a: %#v", aRegion) +} + +// polylineIntersectsPolyline returns whether polyline a intersects with +// polyline b. +func polylineIntersectsPolyline(a *s2.Polyline, b *s2.Polyline) bool { + for aEdgeIdx, aNumEdges := 0, a.NumEdges(); aEdgeIdx < aNumEdges; aEdgeIdx++ { + edge := a.Edge(aEdgeIdx) + crosser := s2.NewChainEdgeCrosser(edge.V0, edge.V1, (*b)[0]) + for _, nextVertex := range (*b)[1:] { + crossing := crosser.ChainCrossingSign(nextVertex) + if crossing != s2.DoNotCross { + return true + } + } + } + return false +} + +// polygonIntersectsPolyline returns whether polygon a intersects with +// polyline b. +func polygonIntersectsPolyline(a *s2.Polygon, b *s2.Polyline) bool { + // Check if the polygon contains any vertex of the line b. + for _, vertex := range *b { + if a.IntersectsCell(s2.CellFromPoint(vertex)) { + return true + } + } + // Here the polygon does not contain any vertex of the polyline. + // The polyline can intersect the polygon if a line goes through the polygon + // with both vertexes that are not in the interior of the polygon. + // This technique works for holes touching, or holes touching the exterior + // as the point in which the holes touch is considered an intersection. + for _, loop := range a.Loops() { + for loopEdgeIdx, loopNumEdges := 0, loop.NumEdges(); loopEdgeIdx < loopNumEdges; loopEdgeIdx++ { + loopEdge := loop.Edge(loopEdgeIdx) + crosser := s2.NewChainEdgeCrosser(loopEdge.V0, loopEdge.V1, (*b)[0]) + for _, nextVertex := range (*b)[1:] { + crossing := crosser.ChainCrossingSign(nextVertex) + if crossing != s2.DoNotCross { + return true + } + } + } + } + return false +} diff --git a/pkg/geo/geogfn/segmentize.go b/pkg/geo/geogfn/segmentize.go new file mode 100644 index 0000000..52f5f4f --- /dev/null +++ b/pkg/geo/geogfn/segmentize.go @@ -0,0 +1,107 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geogfn + +import ( + "math" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geosegmentize" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/golang/geo/s2" + "github.com/twpayne/go-geom" +) + +// Segmentize return modified Geography having no segment longer +// that given maximum segment length. +// This works by dividing each segment by a power of 2 to find the +// smallest power less than or equal to the segmentMaxLength. +func Segmentize(geography geo.Geography, segmentMaxLength float64) (geo.Geography, error) { + if math.IsNaN(segmentMaxLength) || math.IsInf(segmentMaxLength, 1 /* sign */) { + return geography, nil + } + geometry, err := geography.AsGeomT() + if err != nil { + return geo.Geography{}, err + } + switch geometry := geometry.(type) { + case *geom.Point, *geom.MultiPoint: + return geography, nil + default: + if segmentMaxLength <= 0 { + return geo.Geography{}, pgerror.Newf(pgcode.InvalidParameterValue, "maximum segment length must be positive") + } + spheroid, err := spheroidFromGeography(geography) + if err != nil { + return geo.Geography{}, err + } + // Convert segmentMaxLength to Angle with respect to earth sphere as + // further calculation is done considering segmentMaxLength as Angle. + segmentMaxAngle := segmentMaxLength / spheroid.SphereRadius() + ret, err := geosegmentize.Segmentize(geometry, segmentMaxAngle, segmentizeCoords) + if err != nil { + return geo.Geography{}, err + } + return geo.MakeGeographyFromGeomT(ret) + } +} + +// segmentizeCoords inserts multiple points between given two coordinates and +// return resultant point as flat []float64. Such that distance between any two +// points is less than given maximum segment's length, the total number of +// segments is the power of 2, and all the segments are of the same length. +// Note: List of points does not consist of end point. +func segmentizeCoords(a geom.Coord, b geom.Coord, segmentMaxAngle float64) ([]float64, error) { + if len(a) != len(b) { + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "cannot segmentize two coordinates of different dimensions") + } + if segmentMaxAngle <= 0 { + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "maximum segment angle must be positive") + } + + // Converted geom.Coord into s2.Point so we can segmentize the coordinates. + pointA := s2.PointFromLatLng(s2.LatLngFromDegrees(a.Y(), a.X())) + pointB := s2.PointFromLatLng(s2.LatLngFromDegrees(b.Y(), b.X())) + + chordAngleBetweenPoints := s2.ChordAngleBetweenPoints(pointA, pointB).Angle().Radians() + // PostGIS' behavior appears to involve cutting this down into segments divisible + // by a power of two. As such, we do not use ceil(chordAngleBetweenPoints/segmentMaxAngle). + // + // This calculation determines the smallest power of 2 that is greater + // than ceil(chordAngleBetweenPoints/segmentMaxAngle). + // + // We can write that power as 2^n in the following inequality + // 2^n >= ceil(chordAngleBetweenPoints/segmentMaxLength) > 2^(n-1). + // We can drop the ceil since 2^n must be an int + // 2^n >= chordAngleBetweenPoints/segmentMaxLength > 2^(n-1). + // Then n = ceil(log2(chordAngleBetweenPoints/segmentMaxLength)). + // Hence numberOfSegmentsToCreate = 2^(ceil(log2(chordAngleBetweenPoints/segmentMaxLength))). + doubleNumberOfSegmentsToCreate := math.Pow(2, math.Ceil(math.Log2(chordAngleBetweenPoints/segmentMaxAngle))) + doubleNumPoints := float64(len(a)) * (1 + doubleNumberOfSegmentsToCreate) + if err := geosegmentize.CheckSegmentizeValidNumPoints(doubleNumPoints, a, b); err != nil { + return nil, err + } + numberOfSegmentsToCreate := int(doubleNumberOfSegmentsToCreate) + numPoints := int(doubleNumPoints) + + allSegmentizedCoordinates := make([]float64, 0, numPoints) + allSegmentizedCoordinates = append(allSegmentizedCoordinates, a.Clone()...) + segmentFraction := 1.0 / float64(numberOfSegmentsToCreate) + for pointInserted := 1; pointInserted < numberOfSegmentsToCreate; pointInserted++ { + newPoint := s2.Interpolate(float64(pointInserted)/float64(numberOfSegmentsToCreate), pointA, pointB) + latLng := s2.LatLngFromPoint(newPoint) + allSegmentizedCoordinates = append(allSegmentizedCoordinates, latLng.Lng.Degrees(), latLng.Lat.Degrees()) + // Linearly interpolate Z and/or M coordinates. + for i := 2; i < len(a); i++ { + allSegmentizedCoordinates = append( + allSegmentizedCoordinates, + a[i]*(1-float64(pointInserted)*segmentFraction)+b[i]*(float64(pointInserted)*segmentFraction), + ) + } + } + return allSegmentizedCoordinates, nil +} diff --git a/pkg/geo/geogfn/spheroid.go b/pkg/geo/geogfn/spheroid.go new file mode 100644 index 0000000..c80bef3 --- /dev/null +++ b/pkg/geo/geogfn/spheroid.go @@ -0,0 +1,29 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geogfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + // Blank import so projections are initialized correctly. + _ "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geographiclib" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geoprojbase" + "github.com/golang/geo/s2" +) + +// spheroidDistance returns the s12 (meter) component of spheroid.Inverse from s2 Points. +func spheroidDistance(s geoprojbase.Spheroid, a s2.Point, b s2.Point) float64 { + inv, _, _ := s.Inverse(s2.LatLngFromPoint(a), s2.LatLngFromPoint(b)) + return inv +} + +// spheroid returns the spheroid represented by the given Geography. +func spheroidFromGeography(g geo.Geography) (geoprojbase.Spheroid, error) { + proj, err := geoprojbase.Projection(g.SRID()) + if err != nil { + return nil, err + } + return proj.Spheroid, nil +} diff --git a/pkg/geo/geogfn/topology_operations.go b/pkg/geo/geogfn/topology_operations.go new file mode 100644 index 0000000..b0bdc7c --- /dev/null +++ b/pkg/geo/geogfn/topology_operations.go @@ -0,0 +1,137 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geogfn + +import ( + "math" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/golang/geo/r3" + "github.com/golang/geo/s2" + "github.com/twpayne/go-geom" +) + +// Centroid returns the Centroid of a given Geography. +// +// NOTE: In the case of (Multi)Polygon Centroid result, it doesn't mirror with +// PostGIS's result. We are using the same algorithm of dividing into triangles. +// However, The PostGIS implementation differs as it cuts triangles in a different +// way - namely, it fixes the first point in the exterior ring as the first point +// of the triangle, whereas we always update the reference point to be +// the first point of the ring when moving from one ring to another. +// +// See: http://jennessent.com/downloads/Graphics_Shapes_Manual_A4.pdf#page=49 +// for more details. +// +// Ideally, both implementations should provide the same result. However, the +// centroid of the triangles is the vectorized mean of all the points, not the +// actual projection in the Spherical surface, which causes a small inaccuracies. +// This inaccuracy will eventually grow if there is a substantial +// number of a triangle with a larger area. +func Centroid(g geo.Geography, useSphereOrSpheroid UseSphereOrSpheroid) (geo.Geography, error) { + geomRepr, err := g.AsGeomT() + if err != nil { + return geo.Geography{}, err + } + if geomRepr.Empty() { + return geo.MakeGeographyFromGeomT(geom.NewGeometryCollection().SetSRID(geomRepr.SRID())) + } + switch geomRepr.(type) { + case *geom.Point, *geom.LineString, *geom.Polygon, *geom.MultiPoint, *geom.MultiLineString, *geom.MultiPolygon: + default: + return geo.Geography{}, pgerror.Newf(pgcode.InvalidParameterValue, "unhandled geography type %s", g.ShapeType().String()) + } + + regions, err := geo.S2RegionsFromGeomT(geomRepr, geo.EmptyBehaviorOmit) + if err != nil { + return geo.Geography{}, err + } + spheroid, err := spheroidFromGeography(g) + if err != nil { + return geo.Geography{}, err + } + + // localWeightedCentroids is the collection of all the centroid corresponds to + // various small regions in which we divide the given region for calculation + // of centroid. The magnitude of each s2.Point.Vector represents + // the weight corresponding to its region. + var localWeightedCentroids []s2.Point + for _, region := range regions { + switch region := region.(type) { + case s2.Point: + localWeightedCentroids = append(localWeightedCentroids, region) + case *s2.Polyline: + // The algorithm used for the calculation of centroid for (Multi)LineString: + // * Split (Multi)LineString in the set of individual edges. + // * Calculate the mid-points and length/angle for all the edges. + // * The centroid of (Multi)LineString will be a weighted average of mid-points + // of all the edges, where each mid-points is weighted by its length/angle. + for edgeIdx, regionNumEdges := 0, region.NumEdges(); edgeIdx < regionNumEdges; edgeIdx++ { + var edgeWeight float64 + eV0 := region.Edge(edgeIdx).V0 + eV1 := region.Edge(edgeIdx).V1 + if useSphereOrSpheroid == UseSpheroid { + edgeWeight = spheroidDistance(spheroid, eV0, eV1) + } else { + edgeWeight = float64(s2.ChordAngleBetweenPoints(eV0, eV1).Angle()) + } + localWeightedCentroids = append(localWeightedCentroids, s2.Point{Vector: eV0.Add(eV1.Vector).Mul(edgeWeight)}) + } + case *s2.Polygon: + // The algorithm used for the calculation of centroid for (Multi)Polygon: + // * Split (Multi)Polygon in the set of individual triangles. + // * Calculate the centroid and signed area (negative area for triangle inside + // the hole) for all the triangle. + // * The centroid of (Multi)Polygon will be a weighted average of the centroid + // of all the triangle, where each centroid is weighted by its area. + for _, loop := range region.Loops() { + triangleVertices := make([]s2.Point, 4) + triangleVertices[0] = loop.Vertex(0) + triangleVertices[3] = loop.Vertex(0) + + for pointIdx := 1; pointIdx+2 < loop.NumVertices(); pointIdx++ { + triangleVertices[1] = loop.Vertex(pointIdx) + triangleVertices[2] = loop.Vertex(pointIdx + 1) + triangleCentroid := s2.PlanarCentroid(triangleVertices[0], triangleVertices[1], triangleVertices[2]) + var area float64 + if useSphereOrSpheroid == UseSpheroid { + area, _ = spheroid.AreaAndPerimeter(triangleVertices[:3]) + } else { + area = s2.LoopFromPoints(triangleVertices).Area() + } + area = area * float64(loop.Sign()) + localWeightedCentroids = append(localWeightedCentroids, s2.Point{Vector: triangleCentroid.Mul(area)}) + } + } + } + } + var centroidVector r3.Vector + for _, point := range localWeightedCentroids { + centroidVector = centroidVector.Add(point.Vector) + } + latLng := s2.LatLngFromPoint(s2.Point{Vector: centroidVector.Normalize()}) + centroid := geom.NewPointFlat(geom.XY, []float64{latLng.Lng.Degrees(), latLng.Lat.Degrees()}).SetSRID(int(g.SRID())) + return geo.MakeGeographyFromGeomT(centroid) +} + +// BoundingBoxHasNaNCoordinates checks if the bounding box of a Geography +// has a NaN coordinate. +func BoundingBoxHasNaNCoordinates(g geo.Geography) bool { + boundingBox := g.BoundingBoxRef() + if boundingBox == nil { + return false + } + // Don't use `:= range []float64{...}` to avoid memory allocation. + isNaN := func(ord float64) bool { + return math.IsNaN(ord) + } + if isNaN(boundingBox.LoX) || isNaN(boundingBox.LoY) || isNaN(boundingBox.HiX) || isNaN(boundingBox.HiY) { + return true + } + return false +} diff --git a/pkg/geo/geogfn/unary_operators.go b/pkg/geo/geogfn/unary_operators.go new file mode 100644 index 0000000..d7fc544 --- /dev/null +++ b/pkg/geo/geogfn/unary_operators.go @@ -0,0 +1,195 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geogfn + +import ( + "math" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geoprojbase" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/golang/geo/s1" + "github.com/golang/geo/s2" + "github.com/twpayne/go-geom" +) + +// Area returns the area of a given Geography. +func Area(g geo.Geography, useSphereOrSpheroid UseSphereOrSpheroid) (float64, error) { + regions, err := g.AsS2(geo.EmptyBehaviorOmit) + if err != nil { + return 0, err + } + spheroid, err := spheroidFromGeography(g) + if err != nil { + return 0, err + } + + var totalArea float64 + for _, region := range regions { + switch region := region.(type) { + case s2.Point, *s2.Polyline: + case *s2.Polygon: + if useSphereOrSpheroid == UseSpheroid { + for _, loop := range region.Loops() { + points := loop.Vertices() + area, _ := spheroid.AreaAndPerimeter(points[:len(points)-1]) + totalArea += float64(loop.Sign()) * area + } + } else { + totalArea += region.Area() + } + default: + return 0, pgerror.Newf(pgcode.InvalidParameterValue, "unknown type: %T", region) + } + } + if useSphereOrSpheroid == UseSphere { + totalArea *= spheroid.SphereRadius() * spheroid.SphereRadius() + } + return totalArea, nil +} + +// Perimeter returns the perimeter of a given Geography. +func Perimeter(g geo.Geography, useSphereOrSpheroid UseSphereOrSpheroid) (float64, error) { + gt, err := g.AsGeomT() + if err != nil { + return 0, err + } + // This check mirrors PostGIS behavior, where GeometryCollections + // of LineStrings include the length for perimeters. + switch gt.(type) { + case *geom.Polygon, *geom.MultiPolygon, *geom.GeometryCollection: + default: + return 0, nil + } + regions, err := geo.S2RegionsFromGeomT(gt, geo.EmptyBehaviorOmit) + if err != nil { + return 0, err + } + spheroid, err := spheroidFromGeography(g) + if err != nil { + return 0, err + } + return length(regions, spheroid, useSphereOrSpheroid) +} + +// Length returns length of a given Geography. +func Length(g geo.Geography, useSphereOrSpheroid UseSphereOrSpheroid) (float64, error) { + gt, err := g.AsGeomT() + if err != nil { + return 0, err + } + // This check mirrors PostGIS behavior, where GeometryCollections + // of Polygons include the perimeters for polygons. + switch gt.(type) { + case *geom.LineString, *geom.MultiLineString, *geom.GeometryCollection: + default: + return 0, nil + } + regions, err := geo.S2RegionsFromGeomT(gt, geo.EmptyBehaviorOmit) + if err != nil { + return 0, err + } + spheroid, err := spheroidFromGeography(g) + if err != nil { + return 0, err + } + return length(regions, spheroid, useSphereOrSpheroid) +} + +// Project returns calculate a projected point given a source point, a distance and a azimuth. +func Project(g geo.Geography, distance float64, azimuth s1.Angle) (geo.Geography, error) { + geomT, err := g.AsGeomT() + if err != nil { + return geo.Geography{}, err + } + + point, ok := geomT.(*geom.Point) + if !ok { + return geo.Geography{}, pgerror.Newf(pgcode.InvalidParameterValue, "ST_Project(geography) is only valid for point inputs") + } + + spheroid, err := spheroidFromGeography(g) + if err != nil { + return geo.Geography{}, err + } + + // Normalize distance to be positive. + if distance < 0.0 { + distance = -distance + azimuth += math.Pi + } + + // Normalize azimuth + azimuth = azimuth.Normalized() + + // Check the distance validity. + if distance > (math.Pi * spheroid.Radius()) { + return geo.Geography{}, pgerror.Newf(pgcode.InvalidParameterValue, "distance must not be greater than %f", math.Pi*spheroid.Radius()) + } + + if point.Empty() { + return geo.Geography{}, pgerror.Newf(pgcode.InvalidParameterValue, "cannot project POINT EMPTY") + } + + // Convert to ta geodetic point. + x := point.X() + y := point.Y() + + projected := spheroid.Project( + s2.LatLngFromDegrees(x, y), + distance, + azimuth, + ) + + ret := geom.NewPointFlat( + geom.XY, + []float64{ + geo.NormalizeLongitudeDegrees(projected.Lng.Degrees()), + geo.NormalizeLatitudeDegrees(projected.Lat.Degrees()), + }, + ).SetSRID(point.SRID()) + return geo.MakeGeographyFromGeomT(ret) +} + +// length returns the sum of the lengths and perimeters in the shapes of the Geography. +// In OGC parlance, length returns both LineString lengths _and_ Polygon perimeters. +func length( + regions []s2.Region, spheroid geoprojbase.Spheroid, useSphereOrSpheroid UseSphereOrSpheroid, +) (float64, error) { + var totalLength float64 + for _, region := range regions { + switch region := region.(type) { + case s2.Point: + case *s2.Polyline: + if useSphereOrSpheroid == UseSpheroid { + totalLength += spheroid.InverseBatch((*region)) + } else { + for edgeIdx, regionNumEdges := 0, region.NumEdges(); edgeIdx < regionNumEdges; edgeIdx++ { + edge := region.Edge(edgeIdx) + totalLength += s2.ChordAngleBetweenPoints(edge.V0, edge.V1).Angle().Radians() + } + } + case *s2.Polygon: + for _, loop := range region.Loops() { + if useSphereOrSpheroid == UseSpheroid { + totalLength += spheroid.InverseBatch(loop.Vertices()) + } else { + for edgeIdx, loopNumEdges := 0, loop.NumEdges(); edgeIdx < loopNumEdges; edgeIdx++ { + edge := loop.Edge(edgeIdx) + totalLength += s2.ChordAngleBetweenPoints(edge.V0, edge.V1).Angle().Radians() + } + } + } + default: + return 0, pgerror.Newf(pgcode.InvalidParameterValue, "unknown type: %T", region) + } + } + if useSphereOrSpheroid == UseSphere { + totalLength *= spheroid.SphereRadius() + } + return totalLength, nil +} diff --git a/pkg/geo/geographiclib/geodesic.c b/pkg/geo/geographiclib/geodesic.c new file mode 100644 index 0000000..02887e1 --- /dev/null +++ b/pkg/geo/geographiclib/geodesic.c @@ -0,0 +1,2114 @@ +/** + * \file geodesic.c + * \brief Implementation of the geodesic routines in C + * + * For the full documentation see geodesic.h. + **********************************************************************/ + +/** @cond SKIP */ + +/* + * This is a C implementation of the geodesic algorithms described in + * + * C. F. F. Karney, + * Algorithms for geodesics, + * J. Geodesy 87, 43--55 (2013); + * https://doi.org/10.1007/s00190-012-0578-z + * Addenda: https://geographiclib.sourceforge.io/geod-addenda.html + * + * See the comments in geodesic.h for documentation. + * + * Copyright (c) Charles Karney (2012-2019) and licensed + * under the MIT/X11 License. For more information, see + * https://geographiclib.sourceforge.io/ + */ + +#include "geodesic.h" +#include +#include +#include + +#if !defined(HAVE_C99_MATH) +#if defined(PROJ_LIB) +/* PROJ requires C99 so HAVE_C99_MATH is implicit */ +#define HAVE_C99_MATH 1 +#else +#define HAVE_C99_MATH 0 +#endif +#endif + +#if !defined(__cplusplus) +#define nullptr 0 +#endif + +#define GEOGRAPHICLIB_GEODESIC_ORDER 6 +#define nA1 GEOGRAPHICLIB_GEODESIC_ORDER +#define nC1 GEOGRAPHICLIB_GEODESIC_ORDER +#define nC1p GEOGRAPHICLIB_GEODESIC_ORDER +#define nA2 GEOGRAPHICLIB_GEODESIC_ORDER +#define nC2 GEOGRAPHICLIB_GEODESIC_ORDER +#define nA3 GEOGRAPHICLIB_GEODESIC_ORDER +#define nA3x nA3 +#define nC3 GEOGRAPHICLIB_GEODESIC_ORDER +#define nC3x ((nC3 * (nC3 - 1)) / 2) +#define nC4 GEOGRAPHICLIB_GEODESIC_ORDER +#define nC4x ((nC4 * (nC4 + 1)) / 2) +#define nC (GEOGRAPHICLIB_GEODESIC_ORDER + 1) + +typedef double real; +typedef int boolx; + +static unsigned init = 0; +static const int FALSE = 0; +static const int TRUE = 1; +static unsigned digits, maxit1, maxit2; +static real epsilon, realmin, pi, degree, NaN, + tiny, tol0, tol1, tol2, tolb, xthresh; + +static void Init() { + if (!init) { + digits = DBL_MANT_DIG; + epsilon = DBL_EPSILON; + realmin = DBL_MIN; +#if defined(M_PI) + pi = M_PI; +#else + pi = atan2(0.0, -1.0); +#endif + maxit1 = 20; + maxit2 = maxit1 + digits + 10; + tiny = sqrt(realmin); + tol0 = epsilon; + /* Increase multiplier in defn of tol1 from 100 to 200 to fix inverse case + * 52.784459512564 0 -52.784459512563990912 179.634407464943777557 + * which otherwise failed for Visual Studio 10 (Release and Debug) */ + tol1 = 200 * tol0; + tol2 = sqrt(tol0); + /* Check on bisection interval */ + tolb = tol0 * tol2; + xthresh = 1000 * tol2; + degree = pi/180; +#if defined(NAN) + NaN = NAN; /* NAN is defined in C99 */ +#else + { + real minus1 = -1; + /* cppcheck-suppress wrongmathcall */ + NaN = sqrt(minus1); + } +#endif + init = 1; + } +} + +enum captype { + CAP_NONE = 0U, + CAP_C1 = 1U<<0, + CAP_C1p = 1U<<1, + CAP_C2 = 1U<<2, + CAP_C3 = 1U<<3, + CAP_C4 = 1U<<4, + CAP_ALL = 0x1FU, + OUT_ALL = 0x7F80U +}; + +#if HAVE_C99_MATH +#define hypotx hypot +/* no need to redirect log1px, since it's only used by atanhx */ +#define atanhx atanh +#define copysignx copysign +#define cbrtx cbrt +#define remainderx remainder +#define remquox remquo +#else +/* Replacements for C99 math functions */ + +static real hypotx(real x, real y) { + x = fabs(x); y = fabs(y); + if (x < y) { + x /= y; /* y is nonzero */ + return y * sqrt(1 + x * x); + } else { + y /= (x != 0 ? x : 1); + return x * sqrt(1 + y * y); + } +} + +static real log1px(real x) { + volatile real + y = 1 + x, + z = y - 1; + /* Here's the explanation for this magic: y = 1 + z, exactly, and z + * approx x, thus log(y)/z (which is nearly constant near z = 0) returns + * a good approximation to the true log(1 + x)/x. The multiplication x * + * (log(y)/z) introduces little additional error. */ + return z == 0 ? x : x * log(y) / z; +} + +static real atanhx(real x) { + real y = fabs(x); /* Enforce odd parity */ + y = log1px(2 * y/(1 - y))/2; + return x > 0 ? y : (x < 0 ? -y : x); /* atanh(-0.0) = -0.0 */ +} + +static real copysignx(real x, real y) { + /* 1/y trick to get the sign of -0.0 */ + return fabs(x) * (y < 0 || (y == 0 && 1/y < 0) ? -1 : 1); +} + +static real cbrtx(real x) { + real y = pow(fabs(x), 1/(real)(3)); /* Return the real cube root */ + return x > 0 ? y : (x < 0 ? -y : x); /* cbrt(-0.0) = -0.0 */ +} + +static real remainderx(real x, real y) { + real z; + y = fabs(y); /* The result doesn't depend on the sign of y */ + z = fmod(x, y); + if (z == 0) + /* This shouldn't be necessary. However, before version 14 (2015), + * Visual Studio had problems dealing with -0.0. Specifically + * VC 10,11,12 and 32-bit compile: fmod(-0.0, 360.0) -> +0.0 + * python 2.7 on Windows 32-bit machines has the same problem. */ + z = copysignx(z, x); + else if (2 * fabs(z) == y) + z -= fmod(x, 2 * y) - z; /* Implement ties to even */ + else if (2 * fabs(z) > y) + z += (z < 0 ? y : -y); /* Fold remaining cases to (-y/2, y/2) */ + return z; +} + +static real remquox(real x, real y, int* n) { + real z = remainderx(x, y); + if (n) { + real + a = remainderx(x, 2 * y), + b = remainderx(x, 4 * y), + c = remainderx(x, 8 * y); + *n = (a > z ? 1 : (a < z ? -1 : 0)); + *n += (b > a ? 2 : (b < a ? -2 : 0)); + *n += (c > b ? 4 : (c < b ? -4 : 0)); + if (y < 0) *n *= -1; + if (y != 0) { + if (x/y > 0 && *n <= 0) + *n += 8; + else if (x/y < 0 && *n >= 0) + *n -= 8; + } + } + return z; +} + +#endif + +static real sq(real x) { return x * x; } + +static real sumx(real u, real v, real* t) { + volatile real s = u + v; + volatile real up = s - v; + volatile real vpp = s - up; + up -= u; + vpp -= v; + if (t) *t = -(up + vpp); + /* error-free sum: + * u + v = s + t + * = round(u + v) + t */ + return s; +} + +static real polyval(int N, const real p[], real x) { + real y = N < 0 ? 0 : *p++; + while (--N >= 0) y = y * x + *p++; + return y; +} + +/* mimic C++ std::min and std::max */ +static real minx(real a, real b) +{ return (b < a) ? b : a; } + +static real maxx(real a, real b) +{ return (a < b) ? b : a; } + +static void swapx(real* x, real* y) +{ real t = *x; *x = *y; *y = t; } + +static void norm2(real* sinx, real* cosx) { + real r = hypotx(*sinx, *cosx); + *sinx /= r; + *cosx /= r; +} + +static real AngNormalize(real x) { + x = remainderx(x, (real)(360)); + return x != -180 ? x : 180; +} + +static real LatFix(real x) +{ return fabs(x) > 90 ? NaN : x; } + +static real AngDiff(real x, real y, real* e) { + real t, d = AngNormalize(sumx(AngNormalize(-x), AngNormalize(y), &t)); + /* Here y - x = d + t (mod 360), exactly, where d is in (-180,180] and + * abs(t) <= eps (eps = 2^-45 for doubles). The only case where the + * addition of t takes the result outside the range (-180,180] is d = 180 + * and t > 0. The case, d = -180 + eps, t = -eps, can't happen, since + * sum would have returned the exact result in such a case (i.e., given t + * = 0). */ + return sumx(d == 180 && t > 0 ? -180 : d, t, e); +} + +static real AngRound(real x) { + const real z = 1/(real)(16); + volatile real y; + if (x == 0) return 0; + y = fabs(x); + /* The compiler mustn't "simplify" z - (z - y) to y */ + y = y < z ? z - (z - y) : y; + return x < 0 ? -y : y; +} + +static void sincosdx(real x, real* sinx, real* cosx) { + /* In order to minimize round-off errors, this function exactly reduces + * the argument to the range [-45, 45] before converting it to radians. */ + real r, s, c; int q; + r = remquox(x, (real)(90), &q); + /* now abs(r) <= 45 */ + r *= degree; + /* Possibly could call the gnu extension sincos */ + s = sin(r); c = cos(r); +#if defined(_MSC_VER) && _MSC_VER < 1900 + /* + * Before version 14 (2015), Visual Studio had problems dealing + * with -0.0. Specifically + * VC 10,11,12 and 32-bit compile: fmod(-0.0, 360.0) -> +0.0 + * VC 12 and 64-bit compile: sin(-0.0) -> +0.0 + * AngNormalize has a similar fix. + * python 2.7 on Windows 32-bit machines has the same problem. + */ + if (x == 0) s = x; +#endif + switch ((unsigned)q & 3U) { + case 0U: *sinx = s; *cosx = c; break; + case 1U: *sinx = c; *cosx = -s; break; + case 2U: *sinx = -s; *cosx = -c; break; + default: *sinx = -c; *cosx = s; break; /* case 3U */ + } + if (x != 0) { *sinx += (real)(0); *cosx += (real)(0); } +} + +static real atan2dx(real y, real x) { + /* In order to minimize round-off errors, this function rearranges the + * arguments so that result of atan2 is in the range [-pi/4, pi/4] before + * converting it to degrees and mapping the result to the correct + * quadrant. */ + int q = 0; real ang; + if (fabs(y) > fabs(x)) { swapx(&x, &y); q = 2; } + if (x < 0) { x = -x; ++q; } + /* here x >= 0 and x >= abs(y), so angle is in [-pi/4, pi/4] */ + ang = atan2(y, x) / degree; + switch (q) { + /* Note that atan2d(-0.0, 1.0) will return -0. However, we expect that + * atan2d will not be called with y = -0. If need be, include + * + * case 0: ang = 0 + ang; break; + */ + case 1: ang = (y >= 0 ? 180 : -180) - ang; break; + case 2: ang = 90 - ang; break; + case 3: ang = -90 + ang; break; + } + return ang; +} + +static void A3coeff(struct geod_geodesic* g); +static void C3coeff(struct geod_geodesic* g); +static void C4coeff(struct geod_geodesic* g); +static real SinCosSeries(boolx sinp, + real sinx, real cosx, + const real c[], int n); +static void Lengths(const struct geod_geodesic* g, + real eps, real sig12, + real ssig1, real csig1, real dn1, + real ssig2, real csig2, real dn2, + real cbet1, real cbet2, + real* ps12b, real* pm12b, real* pm0, + real* pM12, real* pM21, + /* Scratch area of the right size */ + real Ca[]); +static real Astroid(real x, real y); +static real InverseStart(const struct geod_geodesic* g, + real sbet1, real cbet1, real dn1, + real sbet2, real cbet2, real dn2, + real lam12, real slam12, real clam12, + real* psalp1, real* pcalp1, + /* Only updated if return val >= 0 */ + real* psalp2, real* pcalp2, + /* Only updated for short lines */ + real* pdnm, + /* Scratch area of the right size */ + real Ca[]); +static real Lambda12(const struct geod_geodesic* g, + real sbet1, real cbet1, real dn1, + real sbet2, real cbet2, real dn2, + real salp1, real calp1, + real slam120, real clam120, + real* psalp2, real* pcalp2, + real* psig12, + real* pssig1, real* pcsig1, + real* pssig2, real* pcsig2, + real* peps, + real* pdomg12, + boolx diffp, real* pdlam12, + /* Scratch area of the right size */ + real Ca[]); +static real A3f(const struct geod_geodesic* g, real eps); +static void C3f(const struct geod_geodesic* g, real eps, real c[]); +static void C4f(const struct geod_geodesic* g, real eps, real c[]); +static real A1m1f(real eps); +static void C1f(real eps, real c[]); +static void C1pf(real eps, real c[]); +static real A2m1f(real eps); +static void C2f(real eps, real c[]); +static int transit(real lon1, real lon2); +static int transitdirect(real lon1, real lon2); +static void accini(real s[]); +static void acccopy(const real s[], real t[]); +static void accadd(real s[], real y); +static real accsum(const real s[], real y); +static void accneg(real s[]); +static void accrem(real s[], real y); +static real areareduceA(real area[], real area0, + int crossings, boolx reverse, boolx sign); +static real areareduceB(real area, real area0, + int crossings, boolx reverse, boolx sign); + +void geod_init(struct geod_geodesic* g, real a, real f) { + if (!init) Init(); + g->a = a; + g->f = f; + g->f1 = 1 - g->f; + g->e2 = g->f * (2 - g->f); + g->ep2 = g->e2 / sq(g->f1); /* e2 / (1 - e2) */ + g->n = g->f / ( 2 - g->f); + g->b = g->a * g->f1; + g->c2 = (sq(g->a) + sq(g->b) * + (g->e2 == 0 ? 1 : + (g->e2 > 0 ? atanhx(sqrt(g->e2)) : atan(sqrt(-g->e2))) / + sqrt(fabs(g->e2))))/2; /* authalic radius squared */ + /* The sig12 threshold for "really short". Using the auxiliary sphere + * solution with dnm computed at (bet1 + bet2) / 2, the relative error in the + * azimuth consistency check is sig12^2 * abs(f) * min(1, 1-f/2) / 2. (Error + * measured for 1/100 < b/a < 100 and abs(f) >= 1/1000. For a given f and + * sig12, the max error occurs for lines near the pole. If the old rule for + * computing dnm = (dn1 + dn2)/2 is used, then the error increases by a + * factor of 2.) Setting this equal to epsilon gives sig12 = etol2. Here + * 0.1 is a safety factor (error decreased by 100) and max(0.001, abs(f)) + * stops etol2 getting too large in the nearly spherical case. */ + g->etol2 = 0.1 * tol2 / + sqrt( maxx((real)(0.001), fabs(g->f)) * minx((real)(1), 1 - g->f/2) / 2 ); + + A3coeff(g); + C3coeff(g); + C4coeff(g); +} + +static void geod_lineinit_int(struct geod_geodesicline* l, + const struct geod_geodesic* g, + real lat1, real lon1, + real azi1, real salp1, real calp1, + unsigned caps) { + real cbet1, sbet1, eps; + l->a = g->a; + l->f = g->f; + l->b = g->b; + l->c2 = g->c2; + l->f1 = g->f1; + /* If caps is 0 assume the standard direct calculation */ + l->caps = (caps ? caps : GEOD_DISTANCE_IN | GEOD_LONGITUDE) | + /* always allow latitude and azimuth and unrolling of longitude */ + GEOD_LATITUDE | GEOD_AZIMUTH | GEOD_LONG_UNROLL; + + l->lat1 = LatFix(lat1); + l->lon1 = lon1; + l->azi1 = azi1; + l->salp1 = salp1; + l->calp1 = calp1; + + sincosdx(AngRound(l->lat1), &sbet1, &cbet1); sbet1 *= l->f1; + /* Ensure cbet1 = +epsilon at poles */ + norm2(&sbet1, &cbet1); cbet1 = maxx(tiny, cbet1); + l->dn1 = sqrt(1 + g->ep2 * sq(sbet1)); + + /* Evaluate alp0 from sin(alp1) * cos(bet1) = sin(alp0), */ + l->salp0 = l->salp1 * cbet1; /* alp0 in [0, pi/2 - |bet1|] */ + /* Alt: calp0 = hypot(sbet1, calp1 * cbet1). The following + * is slightly better (consider the case salp1 = 0). */ + l->calp0 = hypotx(l->calp1, l->salp1 * sbet1); + /* Evaluate sig with tan(bet1) = tan(sig1) * cos(alp1). + * sig = 0 is nearest northward crossing of equator. + * With bet1 = 0, alp1 = pi/2, we have sig1 = 0 (equatorial line). + * With bet1 = pi/2, alp1 = -pi, sig1 = pi/2 + * With bet1 = -pi/2, alp1 = 0 , sig1 = -pi/2 + * Evaluate omg1 with tan(omg1) = sin(alp0) * tan(sig1). + * With alp0 in (0, pi/2], quadrants for sig and omg coincide. + * No atan2(0,0) ambiguity at poles since cbet1 = +epsilon. + * With alp0 = 0, omg1 = 0 for alp1 = 0, omg1 = pi for alp1 = pi. */ + l->ssig1 = sbet1; l->somg1 = l->salp0 * sbet1; + l->csig1 = l->comg1 = sbet1 != 0 || l->calp1 != 0 ? cbet1 * l->calp1 : 1; + norm2(&l->ssig1, &l->csig1); /* sig1 in (-pi, pi] */ + /* norm2(somg1, comg1); -- don't need to normalize! */ + + l->k2 = sq(l->calp0) * g->ep2; + eps = l->k2 / (2 * (1 + sqrt(1 + l->k2)) + l->k2); + + if (l->caps & CAP_C1) { + real s, c; + l->A1m1 = A1m1f(eps); + C1f(eps, l->C1a); + l->B11 = SinCosSeries(TRUE, l->ssig1, l->csig1, l->C1a, nC1); + s = sin(l->B11); c = cos(l->B11); + /* tau1 = sig1 + B11 */ + l->stau1 = l->ssig1 * c + l->csig1 * s; + l->ctau1 = l->csig1 * c - l->ssig1 * s; + /* Not necessary because C1pa reverts C1a + * B11 = -SinCosSeries(TRUE, stau1, ctau1, C1pa, nC1p); */ + } + + if (l->caps & CAP_C1p) + C1pf(eps, l->C1pa); + + if (l->caps & CAP_C2) { + l->A2m1 = A2m1f(eps); + C2f(eps, l->C2a); + l->B21 = SinCosSeries(TRUE, l->ssig1, l->csig1, l->C2a, nC2); + } + + if (l->caps & CAP_C3) { + C3f(g, eps, l->C3a); + l->A3c = -l->f * l->salp0 * A3f(g, eps); + l->B31 = SinCosSeries(TRUE, l->ssig1, l->csig1, l->C3a, nC3-1); + } + + if (l->caps & CAP_C4) { + C4f(g, eps, l->C4a); + /* Multiplier = a^2 * e^2 * cos(alpha0) * sin(alpha0) */ + l->A4 = sq(l->a) * l->calp0 * l->salp0 * g->e2; + l->B41 = SinCosSeries(FALSE, l->ssig1, l->csig1, l->C4a, nC4); + } + + l->a13 = l->s13 = NaN; +} + +void geod_lineinit(struct geod_geodesicline* l, + const struct geod_geodesic* g, + real lat1, real lon1, real azi1, unsigned caps) { + real salp1, calp1; + azi1 = AngNormalize(azi1); + /* Guard against underflow in salp0 */ + sincosdx(AngRound(azi1), &salp1, &calp1); + geod_lineinit_int(l, g, lat1, lon1, azi1, salp1, calp1, caps); +} + +void geod_gendirectline(struct geod_geodesicline* l, + const struct geod_geodesic* g, + real lat1, real lon1, real azi1, + unsigned flags, real s12_a12, + unsigned caps) { + geod_lineinit(l, g, lat1, lon1, azi1, caps); + geod_gensetdistance(l, flags, s12_a12); +} + +void geod_directline(struct geod_geodesicline* l, + const struct geod_geodesic* g, + real lat1, real lon1, real azi1, + real s12, unsigned caps) { + geod_gendirectline(l, g, lat1, lon1, azi1, GEOD_NOFLAGS, s12, caps); +} + +real geod_genposition(const struct geod_geodesicline* l, + unsigned flags, real s12_a12, + real* plat2, real* plon2, real* pazi2, + real* ps12, real* pm12, + real* pM12, real* pM21, + real* pS12) { + real lat2 = 0, lon2 = 0, azi2 = 0, s12 = 0, + m12 = 0, M12 = 0, M21 = 0, S12 = 0; + /* Avoid warning about uninitialized B12. */ + real sig12, ssig12, csig12, B12 = 0, AB1 = 0; + real omg12, lam12, lon12; + real ssig2, csig2, sbet2, cbet2, somg2, comg2, salp2, calp2, dn2; + unsigned outmask = + (plat2 ? GEOD_LATITUDE : GEOD_NONE) | + (plon2 ? GEOD_LONGITUDE : GEOD_NONE) | + (pazi2 ? GEOD_AZIMUTH : GEOD_NONE) | + (ps12 ? GEOD_DISTANCE : GEOD_NONE) | + (pm12 ? GEOD_REDUCEDLENGTH : GEOD_NONE) | + (pM12 || pM21 ? GEOD_GEODESICSCALE : GEOD_NONE) | + (pS12 ? GEOD_AREA : GEOD_NONE); + + outmask &= l->caps & OUT_ALL; + if (!( TRUE /*Init()*/ && + (flags & GEOD_ARCMODE || (l->caps & (GEOD_DISTANCE_IN & OUT_ALL))) )) + /* Uninitialized or impossible distance calculation requested */ + return NaN; + + if (flags & GEOD_ARCMODE) { + /* Interpret s12_a12 as spherical arc length */ + sig12 = s12_a12 * degree; + sincosdx(s12_a12, &ssig12, &csig12); + } else { + /* Interpret s12_a12 as distance */ + real + tau12 = s12_a12 / (l->b * (1 + l->A1m1)), + s = sin(tau12), + c = cos(tau12); + /* tau2 = tau1 + tau12 */ + B12 = - SinCosSeries(TRUE, + l->stau1 * c + l->ctau1 * s, + l->ctau1 * c - l->stau1 * s, + l->C1pa, nC1p); + sig12 = tau12 - (B12 - l->B11); + ssig12 = sin(sig12); csig12 = cos(sig12); + if (fabs(l->f) > 0.01) { + /* Reverted distance series is inaccurate for |f| > 1/100, so correct + * sig12 with 1 Newton iteration. The following table shows the + * approximate maximum error for a = WGS_a() and various f relative to + * GeodesicExact. + * erri = the error in the inverse solution (nm) + * errd = the error in the direct solution (series only) (nm) + * errda = the error in the direct solution (series + 1 Newton) (nm) + * + * f erri errd errda + * -1/5 12e6 1.2e9 69e6 + * -1/10 123e3 12e6 765e3 + * -1/20 1110 108e3 7155 + * -1/50 18.63 200.9 27.12 + * -1/100 18.63 23.78 23.37 + * -1/150 18.63 21.05 20.26 + * 1/150 22.35 24.73 25.83 + * 1/100 22.35 25.03 25.31 + * 1/50 29.80 231.9 30.44 + * 1/20 5376 146e3 10e3 + * 1/10 829e3 22e6 1.5e6 + * 1/5 157e6 3.8e9 280e6 */ + real serr; + ssig2 = l->ssig1 * csig12 + l->csig1 * ssig12; + csig2 = l->csig1 * csig12 - l->ssig1 * ssig12; + B12 = SinCosSeries(TRUE, ssig2, csig2, l->C1a, nC1); + serr = (1 + l->A1m1) * (sig12 + (B12 - l->B11)) - s12_a12 / l->b; + sig12 = sig12 - serr / sqrt(1 + l->k2 * sq(ssig2)); + ssig12 = sin(sig12); csig12 = cos(sig12); + /* Update B12 below */ + } + } + + /* sig2 = sig1 + sig12 */ + ssig2 = l->ssig1 * csig12 + l->csig1 * ssig12; + csig2 = l->csig1 * csig12 - l->ssig1 * ssig12; + dn2 = sqrt(1 + l->k2 * sq(ssig2)); + if (outmask & (GEOD_DISTANCE | GEOD_REDUCEDLENGTH | GEOD_GEODESICSCALE)) { + if (flags & GEOD_ARCMODE || fabs(l->f) > 0.01) + B12 = SinCosSeries(TRUE, ssig2, csig2, l->C1a, nC1); + AB1 = (1 + l->A1m1) * (B12 - l->B11); + } + /* sin(bet2) = cos(alp0) * sin(sig2) */ + sbet2 = l->calp0 * ssig2; + /* Alt: cbet2 = hypot(csig2, salp0 * ssig2); */ + cbet2 = hypotx(l->salp0, l->calp0 * csig2); + if (cbet2 == 0) + /* I.e., salp0 = 0, csig2 = 0. Break the degeneracy in this case */ + cbet2 = csig2 = tiny; + /* tan(alp0) = cos(sig2)*tan(alp2) */ + salp2 = l->salp0; calp2 = l->calp0 * csig2; /* No need to normalize */ + + if (outmask & GEOD_DISTANCE) + s12 = (flags & GEOD_ARCMODE) ? + l->b * ((1 + l->A1m1) * sig12 + AB1) : + s12_a12; + + if (outmask & GEOD_LONGITUDE) { + real E = copysignx(1, l->salp0); /* east or west going? */ + /* tan(omg2) = sin(alp0) * tan(sig2) */ + somg2 = l->salp0 * ssig2; comg2 = csig2; /* No need to normalize */ + /* omg12 = omg2 - omg1 */ + omg12 = (flags & GEOD_LONG_UNROLL) + ? E * (sig12 + - (atan2( ssig2, csig2) - atan2( l->ssig1, l->csig1)) + + (atan2(E * somg2, comg2) - atan2(E * l->somg1, l->comg1))) + : atan2(somg2 * l->comg1 - comg2 * l->somg1, + comg2 * l->comg1 + somg2 * l->somg1); + lam12 = omg12 + l->A3c * + ( sig12 + (SinCosSeries(TRUE, ssig2, csig2, l->C3a, nC3-1) + - l->B31)); + lon12 = lam12 / degree; + lon2 = (flags & GEOD_LONG_UNROLL) ? l->lon1 + lon12 : + AngNormalize(AngNormalize(l->lon1) + AngNormalize(lon12)); + } + + if (outmask & GEOD_LATITUDE) + lat2 = atan2dx(sbet2, l->f1 * cbet2); + + if (outmask & GEOD_AZIMUTH) + azi2 = atan2dx(salp2, calp2); + + if (outmask & (GEOD_REDUCEDLENGTH | GEOD_GEODESICSCALE)) { + real + B22 = SinCosSeries(TRUE, ssig2, csig2, l->C2a, nC2), + AB2 = (1 + l->A2m1) * (B22 - l->B21), + J12 = (l->A1m1 - l->A2m1) * sig12 + (AB1 - AB2); + if (outmask & GEOD_REDUCEDLENGTH) + /* Add parens around (csig1 * ssig2) and (ssig1 * csig2) to ensure + * accurate cancellation in the case of coincident points. */ + m12 = l->b * ((dn2 * (l->csig1 * ssig2) - l->dn1 * (l->ssig1 * csig2)) + - l->csig1 * csig2 * J12); + if (outmask & GEOD_GEODESICSCALE) { + real t = l->k2 * (ssig2 - l->ssig1) * (ssig2 + l->ssig1) / + (l->dn1 + dn2); + M12 = csig12 + (t * ssig2 - csig2 * J12) * l->ssig1 / l->dn1; + M21 = csig12 - (t * l->ssig1 - l->csig1 * J12) * ssig2 / dn2; + } + } + + if (outmask & GEOD_AREA) { + real + B42 = SinCosSeries(FALSE, ssig2, csig2, l->C4a, nC4); + real salp12, calp12; + if (l->calp0 == 0 || l->salp0 == 0) { + /* alp12 = alp2 - alp1, used in atan2 so no need to normalize */ + salp12 = salp2 * l->calp1 - calp2 * l->salp1; + calp12 = calp2 * l->calp1 + salp2 * l->salp1; + } else { + /* tan(alp) = tan(alp0) * sec(sig) + * tan(alp2-alp1) = (tan(alp2) -tan(alp1)) / (tan(alp2)*tan(alp1)+1) + * = calp0 * salp0 * (csig1-csig2) / (salp0^2 + calp0^2 * csig1*csig2) + * If csig12 > 0, write + * csig1 - csig2 = ssig12 * (csig1 * ssig12 / (1 + csig12) + ssig1) + * else + * csig1 - csig2 = csig1 * (1 - csig12) + ssig12 * ssig1 + * No need to normalize */ + salp12 = l->calp0 * l->salp0 * + (csig12 <= 0 ? l->csig1 * (1 - csig12) + ssig12 * l->ssig1 : + ssig12 * (l->csig1 * ssig12 / (1 + csig12) + l->ssig1)); + calp12 = sq(l->salp0) + sq(l->calp0) * l->csig1 * csig2; + } + S12 = l->c2 * atan2(salp12, calp12) + l->A4 * (B42 - l->B41); + } + + /* In the pattern + * + * if ((outmask & GEOD_XX) && pYY) + * *pYY = YY; + * + * the second check "&& pYY" is redundant. It's there to make the CLang + * static analyzer happy. + */ + if ((outmask & GEOD_LATITUDE) && plat2) + *plat2 = lat2; + if ((outmask & GEOD_LONGITUDE) && plon2) + *plon2 = lon2; + if ((outmask & GEOD_AZIMUTH) && pazi2) + *pazi2 = azi2; + if ((outmask & GEOD_DISTANCE) && ps12) + *ps12 = s12; + if ((outmask & GEOD_REDUCEDLENGTH) && pm12) + *pm12 = m12; + if (outmask & GEOD_GEODESICSCALE) { + if (pM12) *pM12 = M12; + if (pM21) *pM21 = M21; + } + if ((outmask & GEOD_AREA) && pS12) + *pS12 = S12; + + return (flags & GEOD_ARCMODE) ? s12_a12 : sig12 / degree; +} + +void geod_setdistance(struct geod_geodesicline* l, real s13) { + l->s13 = s13; + l->a13 = geod_genposition(l, GEOD_NOFLAGS, l->s13, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, nullptr); +} + +static void geod_setarc(struct geod_geodesicline* l, real a13) { + l->a13 = a13; l->s13 = NaN; + geod_genposition(l, GEOD_ARCMODE, l->a13, nullptr, nullptr, nullptr, &l->s13, + nullptr, nullptr, nullptr, nullptr); +} + +void geod_gensetdistance(struct geod_geodesicline* l, + unsigned flags, real s13_a13) { + (flags & GEOD_ARCMODE) ? + geod_setarc(l, s13_a13) : + geod_setdistance(l, s13_a13); +} + +void geod_position(const struct geod_geodesicline* l, real s12, + real* plat2, real* plon2, real* pazi2) { + geod_genposition(l, FALSE, s12, plat2, plon2, pazi2, + nullptr, nullptr, nullptr, nullptr, nullptr); +} + +real geod_gendirect(const struct geod_geodesic* g, + real lat1, real lon1, real azi1, + unsigned flags, real s12_a12, + real* plat2, real* plon2, real* pazi2, + real* ps12, real* pm12, real* pM12, real* pM21, + real* pS12) { + struct geod_geodesicline l; + unsigned outmask = + (plat2 ? GEOD_LATITUDE : GEOD_NONE) | + (plon2 ? GEOD_LONGITUDE : GEOD_NONE) | + (pazi2 ? GEOD_AZIMUTH : GEOD_NONE) | + (ps12 ? GEOD_DISTANCE : GEOD_NONE) | + (pm12 ? GEOD_REDUCEDLENGTH : GEOD_NONE) | + (pM12 || pM21 ? GEOD_GEODESICSCALE : GEOD_NONE) | + (pS12 ? GEOD_AREA : GEOD_NONE); + + geod_lineinit(&l, g, lat1, lon1, azi1, + /* Automatically supply GEOD_DISTANCE_IN if necessary */ + outmask | + ((flags & GEOD_ARCMODE) ? GEOD_NONE : GEOD_DISTANCE_IN)); + return geod_genposition(&l, flags, s12_a12, + plat2, plon2, pazi2, ps12, pm12, pM12, pM21, pS12); +} + +void geod_direct(const struct geod_geodesic* g, + real lat1, real lon1, real azi1, + real s12, + real* plat2, real* plon2, real* pazi2) { + geod_gendirect(g, lat1, lon1, azi1, GEOD_NOFLAGS, s12, plat2, plon2, pazi2, + nullptr, nullptr, nullptr, nullptr, nullptr); +} + +static real geod_geninverse_int(const struct geod_geodesic* g, + real lat1, real lon1, real lat2, real lon2, + real* ps12, + real* psalp1, real* pcalp1, + real* psalp2, real* pcalp2, + real* pm12, real* pM12, real* pM21, + real* pS12) { + real s12 = 0, m12 = 0, M12 = 0, M21 = 0, S12 = 0; + real lon12, lon12s; + int latsign, lonsign, swapp; + real sbet1, cbet1, sbet2, cbet2, s12x = 0, m12x = 0; + real dn1, dn2, lam12, slam12, clam12; + real a12 = 0, sig12, calp1 = 0, salp1 = 0, calp2 = 0, salp2 = 0; + real Ca[nC]; + boolx meridian; + /* somg12 > 1 marks that it needs to be calculated */ + real omg12 = 0, somg12 = 2, comg12 = 0; + + unsigned outmask = + (ps12 ? GEOD_DISTANCE : GEOD_NONE) | + (pm12 ? GEOD_REDUCEDLENGTH : GEOD_NONE) | + (pM12 || pM21 ? GEOD_GEODESICSCALE : GEOD_NONE) | + (pS12 ? GEOD_AREA : GEOD_NONE); + + outmask &= OUT_ALL; + /* Compute longitude difference (AngDiff does this carefully). Result is + * in [-180, 180] but -180 is only for west-going geodesics. 180 is for + * east-going and meridional geodesics. */ + lon12 = AngDiff(lon1, lon2, &lon12s); + /* Make longitude difference positive. */ + lonsign = lon12 >= 0 ? 1 : -1; + /* If very close to being on the same half-meridian, then make it so. */ + lon12 = lonsign * AngRound(lon12); + lon12s = AngRound((180 - lon12) - lonsign * lon12s); + lam12 = lon12 * degree; + if (lon12 > 90) { + sincosdx(lon12s, &slam12, &clam12); + clam12 = -clam12; + } else + sincosdx(lon12, &slam12, &clam12); + + /* If really close to the equator, treat as on equator. */ + lat1 = AngRound(LatFix(lat1)); + lat2 = AngRound(LatFix(lat2)); + /* Swap points so that point with higher (abs) latitude is point 1 + * If one latitude is a nan, then it becomes lat1. */ + swapp = fabs(lat1) < fabs(lat2) ? -1 : 1; + if (swapp < 0) { + lonsign *= -1; + swapx(&lat1, &lat2); + } + /* Make lat1 <= 0 */ + latsign = lat1 < 0 ? 1 : -1; + lat1 *= latsign; + lat2 *= latsign; + /* Now we have + * + * 0 <= lon12 <= 180 + * -90 <= lat1 <= 0 + * lat1 <= lat2 <= -lat1 + * + * longsign, swapp, latsign register the transformation to bring the + * coordinates to this canonical form. In all cases, 1 means no change was + * made. We make these transformations so that there are few cases to + * check, e.g., on verifying quadrants in atan2. In addition, this + * enforces some symmetries in the results returned. */ + + sincosdx(lat1, &sbet1, &cbet1); sbet1 *= g->f1; + /* Ensure cbet1 = +epsilon at poles */ + norm2(&sbet1, &cbet1); cbet1 = maxx(tiny, cbet1); + + sincosdx(lat2, &sbet2, &cbet2); sbet2 *= g->f1; + /* Ensure cbet2 = +epsilon at poles */ + norm2(&sbet2, &cbet2); cbet2 = maxx(tiny, cbet2); + + /* If cbet1 < -sbet1, then cbet2 - cbet1 is a sensitive measure of the + * |bet1| - |bet2|. Alternatively (cbet1 >= -sbet1), abs(sbet2) + sbet1 is + * a better measure. This logic is used in assigning calp2 in Lambda12. + * Sometimes these quantities vanish and in that case we force bet2 = +/- + * bet1 exactly. An example where is is necessary is the inverse problem + * 48.522876735459 0 -48.52287673545898293 179.599720456223079643 + * which failed with Visual Studio 10 (Release and Debug) */ + + if (cbet1 < -sbet1) { + if (cbet2 == cbet1) + sbet2 = sbet2 < 0 ? sbet1 : -sbet1; + } else { + if (fabs(sbet2) == -sbet1) + cbet2 = cbet1; + } + + dn1 = sqrt(1 + g->ep2 * sq(sbet1)); + dn2 = sqrt(1 + g->ep2 * sq(sbet2)); + + meridian = lat1 == -90 || slam12 == 0; + + if (meridian) { + + /* Endpoints are on a single full meridian, so the geodesic might lie on + * a meridian. */ + + real ssig1, csig1, ssig2, csig2; + calp1 = clam12; salp1 = slam12; /* Head to the target longitude */ + calp2 = 1; salp2 = 0; /* At the target we're heading north */ + + /* tan(bet) = tan(sig) * cos(alp) */ + ssig1 = sbet1; csig1 = calp1 * cbet1; + ssig2 = sbet2; csig2 = calp2 * cbet2; + + /* sig12 = sig2 - sig1 */ + sig12 = atan2(maxx((real)(0), csig1 * ssig2 - ssig1 * csig2), + csig1 * csig2 + ssig1 * ssig2); + Lengths(g, g->n, sig12, ssig1, csig1, dn1, ssig2, csig2, dn2, + cbet1, cbet2, &s12x, &m12x, nullptr, + (outmask & GEOD_GEODESICSCALE) ? &M12 : nullptr, + (outmask & GEOD_GEODESICSCALE) ? &M21 : nullptr, + Ca); + /* Add the check for sig12 since zero length geodesics might yield m12 < + * 0. Test case was + * + * echo 20.001 0 20.001 0 | GeodSolve -i + * + * In fact, we will have sig12 > pi/2 for meridional geodesic which is + * not a shortest path. */ + if (sig12 < 1 || m12x >= 0) { + /* Need at least 2, to handle 90 0 90 180 */ + if (sig12 < 3 * tiny) + sig12 = m12x = s12x = 0; + m12x *= g->b; + s12x *= g->b; + a12 = sig12 / degree; + } else + /* m12 < 0, i.e., prolate and too close to anti-podal */ + meridian = FALSE; + } + + if (!meridian && + sbet1 == 0 && /* and sbet2 == 0 */ + /* Mimic the way Lambda12 works with calp1 = 0 */ + (g->f <= 0 || lon12s >= g->f * 180)) { + + /* Geodesic runs along equator */ + calp1 = calp2 = 0; salp1 = salp2 = 1; + s12x = g->a * lam12; + sig12 = omg12 = lam12 / g->f1; + m12x = g->b * sin(sig12); + if (outmask & GEOD_GEODESICSCALE) + M12 = M21 = cos(sig12); + a12 = lon12 / g->f1; + + } else if (!meridian) { + + /* Now point1 and point2 belong within a hemisphere bounded by a + * meridian and geodesic is neither meridional or equatorial. */ + + /* Figure a starting point for Newton's method */ + real dnm = 0; + sig12 = InverseStart(g, sbet1, cbet1, dn1, sbet2, cbet2, dn2, + lam12, slam12, clam12, + &salp1, &calp1, &salp2, &calp2, &dnm, + Ca); + + if (sig12 >= 0) { + /* Short lines (InverseStart sets salp2, calp2, dnm) */ + s12x = sig12 * g->b * dnm; + m12x = sq(dnm) * g->b * sin(sig12 / dnm); + if (outmask & GEOD_GEODESICSCALE) + M12 = M21 = cos(sig12 / dnm); + a12 = sig12 / degree; + omg12 = lam12 / (g->f1 * dnm); + } else { + + /* Newton's method. This is a straightforward solution of f(alp1) = + * lambda12(alp1) - lam12 = 0 with one wrinkle. f(alp) has exactly one + * root in the interval (0, pi) and its derivative is positive at the + * root. Thus f(alp) is positive for alp > alp1 and negative for alp < + * alp1. During the course of the iteration, a range (alp1a, alp1b) is + * maintained which brackets the root and with each evaluation of + * f(alp) the range is shrunk, if possible. Newton's method is + * restarted whenever the derivative of f is negative (because the new + * value of alp1 is then further from the solution) or if the new + * estimate of alp1 lies outside (0,pi); in this case, the new starting + * guess is taken to be (alp1a + alp1b) / 2. */ + real ssig1 = 0, csig1 = 0, ssig2 = 0, csig2 = 0, eps = 0, domg12 = 0; + unsigned numit = 0; + /* Bracketing range */ + real salp1a = tiny, calp1a = 1, salp1b = tiny, calp1b = -1; + boolx tripn = FALSE; + boolx tripb = FALSE; + for (; numit < maxit2; ++numit) { + /* the WGS84 test set: mean = 1.47, sd = 1.25, max = 16 + * WGS84 and random input: mean = 2.85, sd = 0.60 */ + real dv = 0, + v = Lambda12(g, sbet1, cbet1, dn1, sbet2, cbet2, dn2, salp1, calp1, + slam12, clam12, + &salp2, &calp2, &sig12, &ssig1, &csig1, &ssig2, &csig2, + &eps, &domg12, numit < maxit1, &dv, Ca); + /* 2 * tol0 is approximately 1 ulp for a number in [0, pi]. */ + /* Reversed test to allow escape with NaNs */ + if (tripb || !(fabs(v) >= (tripn ? 8 : 1) * tol0)) break; + /* Update bracketing values */ + if (v > 0 && (numit > maxit1 || calp1/salp1 > calp1b/salp1b)) + { salp1b = salp1; calp1b = calp1; } + else if (v < 0 && (numit > maxit1 || calp1/salp1 < calp1a/salp1a)) + { salp1a = salp1; calp1a = calp1; } + if (numit < maxit1 && dv > 0) { + real + dalp1 = -v/dv; + real + sdalp1 = sin(dalp1), cdalp1 = cos(dalp1), + nsalp1 = salp1 * cdalp1 + calp1 * sdalp1; + if (nsalp1 > 0 && fabs(dalp1) < pi) { + calp1 = calp1 * cdalp1 - salp1 * sdalp1; + salp1 = nsalp1; + norm2(&salp1, &calp1); + /* In some regimes we don't get quadratic convergence because + * slope -> 0. So use convergence conditions based on epsilon + * instead of sqrt(epsilon). */ + tripn = fabs(v) <= 16 * tol0; + continue; + } + } + /* Either dv was not positive or updated value was outside legal + * range. Use the midpoint of the bracket as the next estimate. + * This mechanism is not needed for the WGS84 ellipsoid, but it does + * catch problems with more eccentric ellipsoids. Its efficacy is + * such for the WGS84 test set with the starting guess set to alp1 = + * 90deg: + * the WGS84 test set: mean = 5.21, sd = 3.93, max = 24 + * WGS84 and random input: mean = 4.74, sd = 0.99 */ + salp1 = (salp1a + salp1b)/2; + calp1 = (calp1a + calp1b)/2; + norm2(&salp1, &calp1); + tripn = FALSE; + tripb = (fabs(salp1a - salp1) + (calp1a - calp1) < tolb || + fabs(salp1 - salp1b) + (calp1 - calp1b) < tolb); + } + Lengths(g, eps, sig12, ssig1, csig1, dn1, ssig2, csig2, dn2, + cbet1, cbet2, &s12x, &m12x, nullptr, + (outmask & GEOD_GEODESICSCALE) ? &M12 : nullptr, + (outmask & GEOD_GEODESICSCALE) ? &M21 : nullptr, Ca); + m12x *= g->b; + s12x *= g->b; + a12 = sig12 / degree; + if (outmask & GEOD_AREA) { + /* omg12 = lam12 - domg12 */ + real sdomg12 = sin(domg12), cdomg12 = cos(domg12); + somg12 = slam12 * cdomg12 - clam12 * sdomg12; + comg12 = clam12 * cdomg12 + slam12 * sdomg12; + } + } + } + + if (outmask & GEOD_DISTANCE) + s12 = 0 + s12x; /* Convert -0 to 0 */ + + if (outmask & GEOD_REDUCEDLENGTH) + m12 = 0 + m12x; /* Convert -0 to 0 */ + + if (outmask & GEOD_AREA) { + real + /* From Lambda12: sin(alp1) * cos(bet1) = sin(alp0) */ + salp0 = salp1 * cbet1, + calp0 = hypotx(calp1, salp1 * sbet1); /* calp0 > 0 */ + real alp12; + if (calp0 != 0 && salp0 != 0) { + real + /* From Lambda12: tan(bet) = tan(sig) * cos(alp) */ + ssig1 = sbet1, csig1 = calp1 * cbet1, + ssig2 = sbet2, csig2 = calp2 * cbet2, + k2 = sq(calp0) * g->ep2, + eps = k2 / (2 * (1 + sqrt(1 + k2)) + k2), + /* Multiplier = a^2 * e^2 * cos(alpha0) * sin(alpha0). */ + A4 = sq(g->a) * calp0 * salp0 * g->e2; + real B41, B42; + norm2(&ssig1, &csig1); + norm2(&ssig2, &csig2); + C4f(g, eps, Ca); + B41 = SinCosSeries(FALSE, ssig1, csig1, Ca, nC4); + B42 = SinCosSeries(FALSE, ssig2, csig2, Ca, nC4); + S12 = A4 * (B42 - B41); + } else + /* Avoid problems with indeterminate sig1, sig2 on equator */ + S12 = 0; + + if (!meridian && somg12 > 1) { + somg12 = sin(omg12); comg12 = cos(omg12); + } + + if (!meridian && + /* omg12 < 3/4 * pi */ + comg12 > -(real)(0.7071) && /* Long difference not too big */ + sbet2 - sbet1 < (real)(1.75)) { /* Lat difference not too big */ + /* Use tan(Gamma/2) = tan(omg12/2) + * * (tan(bet1/2)+tan(bet2/2))/(1+tan(bet1/2)*tan(bet2/2)) + * with tan(x/2) = sin(x)/(1+cos(x)) */ + real + domg12 = 1 + comg12, dbet1 = 1 + cbet1, dbet2 = 1 + cbet2; + alp12 = 2 * atan2( somg12 * ( sbet1 * dbet2 + sbet2 * dbet1 ), + domg12 * ( sbet1 * sbet2 + dbet1 * dbet2 ) ); + } else { + /* alp12 = alp2 - alp1, used in atan2 so no need to normalize */ + real + salp12 = salp2 * calp1 - calp2 * salp1, + calp12 = calp2 * calp1 + salp2 * salp1; + /* The right thing appears to happen if alp1 = +/-180 and alp2 = 0, viz + * salp12 = -0 and alp12 = -180. However this depends on the sign + * being attached to 0 correctly. The following ensures the correct + * behavior. */ + if (salp12 == 0 && calp12 < 0) { + salp12 = tiny * calp1; + calp12 = -1; + } + alp12 = atan2(salp12, calp12); + } + S12 += g->c2 * alp12; + S12 *= swapp * lonsign * latsign; + /* Convert -0 to 0 */ + S12 += 0; + } + + /* Convert calp, salp to azimuth accounting for lonsign, swapp, latsign. */ + if (swapp < 0) { + swapx(&salp1, &salp2); + swapx(&calp1, &calp2); + if (outmask & GEOD_GEODESICSCALE) + swapx(&M12, &M21); + } + + salp1 *= swapp * lonsign; calp1 *= swapp * latsign; + salp2 *= swapp * lonsign; calp2 *= swapp * latsign; + + if (psalp1) *psalp1 = salp1; + if (pcalp1) *pcalp1 = calp1; + if (psalp2) *psalp2 = salp2; + if (pcalp2) *pcalp2 = calp2; + + if (outmask & GEOD_DISTANCE) + *ps12 = s12; + if (outmask & GEOD_REDUCEDLENGTH) + *pm12 = m12; + if (outmask & GEOD_GEODESICSCALE) { + if (pM12) *pM12 = M12; + if (pM21) *pM21 = M21; + } + if (outmask & GEOD_AREA) + *pS12 = S12; + + /* Returned value in [0, 180] */ + return a12; +} + +real geod_geninverse(const struct geod_geodesic* g, + real lat1, real lon1, real lat2, real lon2, + real* ps12, real* pazi1, real* pazi2, + real* pm12, real* pM12, real* pM21, real* pS12) { + real salp1, calp1, salp2, calp2, + a12 = geod_geninverse_int(g, lat1, lon1, lat2, lon2, ps12, + &salp1, &calp1, &salp2, &calp2, + pm12, pM12, pM21, pS12); + if (pazi1) *pazi1 = atan2dx(salp1, calp1); + if (pazi2) *pazi2 = atan2dx(salp2, calp2); + return a12; +} + +void geod_inverseline(struct geod_geodesicline* l, + const struct geod_geodesic* g, + real lat1, real lon1, real lat2, real lon2, + unsigned caps) { + real salp1, calp1, + a12 = geod_geninverse_int(g, lat1, lon1, lat2, lon2, nullptr, + &salp1, &calp1, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr), + azi1 = atan2dx(salp1, calp1); + caps = caps ? caps : GEOD_DISTANCE_IN | GEOD_LONGITUDE; + /* Ensure that a12 can be converted to a distance */ + if (caps & (OUT_ALL & GEOD_DISTANCE_IN)) caps |= GEOD_DISTANCE; + geod_lineinit_int(l, g, lat1, lon1, azi1, salp1, calp1, caps); + geod_setarc(l, a12); +} + +void geod_inverse(const struct geod_geodesic* g, + real lat1, real lon1, real lat2, real lon2, + real* ps12, real* pazi1, real* pazi2) { + geod_geninverse(g, lat1, lon1, lat2, lon2, ps12, pazi1, pazi2, + nullptr, nullptr, nullptr, nullptr); +} + +real SinCosSeries(boolx sinp, real sinx, real cosx, const real c[], int n) { + /* Evaluate + * y = sinp ? sum(c[i] * sin( 2*i * x), i, 1, n) : + * sum(c[i] * cos((2*i+1) * x), i, 0, n-1) + * using Clenshaw summation. N.B. c[0] is unused for sin series + * Approx operation count = (n + 5) mult and (2 * n + 2) add */ + real ar, y0, y1; + c += (n + sinp); /* Point to one beyond last element */ + ar = 2 * (cosx - sinx) * (cosx + sinx); /* 2 * cos(2 * x) */ + y0 = (n & 1) ? *--c : 0; y1 = 0; /* accumulators for sum */ + /* Now n is even */ + n /= 2; + while (n--) { + /* Unroll loop x 2, so accumulators return to their original role */ + y1 = ar * y0 - y1 + *--c; + y0 = ar * y1 - y0 + *--c; + } + return sinp + ? 2 * sinx * cosx * y0 /* sin(2 * x) * y0 */ + : cosx * (y0 - y1); /* cos(x) * (y0 - y1) */ +} + +void Lengths(const struct geod_geodesic* g, + real eps, real sig12, + real ssig1, real csig1, real dn1, + real ssig2, real csig2, real dn2, + real cbet1, real cbet2, + real* ps12b, real* pm12b, real* pm0, + real* pM12, real* pM21, + /* Scratch area of the right size */ + real Ca[]) { + real m0 = 0, J12 = 0, A1 = 0, A2 = 0; + real Cb[nC]; + + /* Return m12b = (reduced length)/b; also calculate s12b = distance/b, + * and m0 = coefficient of secular term in expression for reduced length. */ + boolx redlp = pm12b || pm0 || pM12 || pM21; + if (ps12b || redlp) { + A1 = A1m1f(eps); + C1f(eps, Ca); + if (redlp) { + A2 = A2m1f(eps); + C2f(eps, Cb); + m0 = A1 - A2; + A2 = 1 + A2; + } + A1 = 1 + A1; + } + if (ps12b) { + real B1 = SinCosSeries(TRUE, ssig2, csig2, Ca, nC1) - + SinCosSeries(TRUE, ssig1, csig1, Ca, nC1); + /* Missing a factor of b */ + *ps12b = A1 * (sig12 + B1); + if (redlp) { + real B2 = SinCosSeries(TRUE, ssig2, csig2, Cb, nC2) - + SinCosSeries(TRUE, ssig1, csig1, Cb, nC2); + J12 = m0 * sig12 + (A1 * B1 - A2 * B2); + } + } else if (redlp) { + /* Assume here that nC1 >= nC2 */ + int l; + for (l = 1; l <= nC2; ++l) + Cb[l] = A1 * Ca[l] - A2 * Cb[l]; + J12 = m0 * sig12 + (SinCosSeries(TRUE, ssig2, csig2, Cb, nC2) - + SinCosSeries(TRUE, ssig1, csig1, Cb, nC2)); + } + if (pm0) *pm0 = m0; + if (pm12b) + /* Missing a factor of b. + * Add parens around (csig1 * ssig2) and (ssig1 * csig2) to ensure + * accurate cancellation in the case of coincident points. */ + *pm12b = dn2 * (csig1 * ssig2) - dn1 * (ssig1 * csig2) - + csig1 * csig2 * J12; + if (pM12 || pM21) { + real csig12 = csig1 * csig2 + ssig1 * ssig2; + real t = g->ep2 * (cbet1 - cbet2) * (cbet1 + cbet2) / (dn1 + dn2); + if (pM12) + *pM12 = csig12 + (t * ssig2 - csig2 * J12) * ssig1 / dn1; + if (pM21) + *pM21 = csig12 - (t * ssig1 - csig1 * J12) * ssig2 / dn2; + } +} + +real Astroid(real x, real y) { + /* Solve k^4+2*k^3-(x^2+y^2-1)*k^2-2*y^2*k-y^2 = 0 for positive root k. + * This solution is adapted from Geocentric::Reverse. */ + real k; + real + p = sq(x), + q = sq(y), + r = (p + q - 1) / 6; + if ( !(q == 0 && r <= 0) ) { + real + /* Avoid possible division by zero when r = 0 by multiplying equations + * for s and t by r^3 and r, resp. */ + S = p * q / 4, /* S = r^3 * s */ + r2 = sq(r), + r3 = r * r2, + /* The discriminant of the quadratic equation for T3. This is zero on + * the evolute curve p^(1/3)+q^(1/3) = 1 */ + disc = S * (S + 2 * r3); + real u = r; + real v, uv, w; + if (disc >= 0) { + real T3 = S + r3, T; + /* Pick the sign on the sqrt to maximize abs(T3). This minimizes loss + * of precision due to cancellation. The result is unchanged because + * of the way the T is used in definition of u. */ + T3 += T3 < 0 ? -sqrt(disc) : sqrt(disc); /* T3 = (r * t)^3 */ + /* N.B. cbrtx always returns the real root. cbrtx(-8) = -2. */ + T = cbrtx(T3); /* T = r * t */ + /* T can be zero; but then r2 / T -> 0. */ + u += T + (T != 0 ? r2 / T : 0); + } else { + /* T is complex, but the way u is defined the result is real. */ + real ang = atan2(sqrt(-disc), -(S + r3)); + /* There are three possible cube roots. We choose the root which + * avoids cancellation. Note that disc < 0 implies that r < 0. */ + u += 2 * r * cos(ang / 3); + } + v = sqrt(sq(u) + q); /* guaranteed positive */ + /* Avoid loss of accuracy when u < 0. */ + uv = u < 0 ? q / (v - u) : u + v; /* u+v, guaranteed positive */ + w = (uv - q) / (2 * v); /* positive? */ + /* Rearrange expression for k to avoid loss of accuracy due to + * subtraction. Division by 0 not possible because uv > 0, w >= 0. */ + k = uv / (sqrt(uv + sq(w)) + w); /* guaranteed positive */ + } else { /* q == 0 && r <= 0 */ + /* y = 0 with |x| <= 1. Handle this case directly. + * for y small, positive root is k = abs(y)/sqrt(1-x^2) */ + k = 0; + } + return k; +} + +real InverseStart(const struct geod_geodesic* g, + real sbet1, real cbet1, real dn1, + real sbet2, real cbet2, real dn2, + real lam12, real slam12, real clam12, + real* psalp1, real* pcalp1, + /* Only updated if return val >= 0 */ + real* psalp2, real* pcalp2, + /* Only updated for short lines */ + real* pdnm, + /* Scratch area of the right size */ + real Ca[]) { + real salp1 = 0, calp1 = 0, salp2 = 0, calp2 = 0, dnm = 0; + + /* Return a starting point for Newton's method in salp1 and calp1 (function + * value is -1). If Newton's method doesn't need to be used, return also + * salp2 and calp2 and function value is sig12. */ + real + sig12 = -1, /* Return value */ + /* bet12 = bet2 - bet1 in [0, pi); bet12a = bet2 + bet1 in (-pi, 0] */ + sbet12 = sbet2 * cbet1 - cbet2 * sbet1, + cbet12 = cbet2 * cbet1 + sbet2 * sbet1; + real sbet12a; + boolx shortline = cbet12 >= 0 && sbet12 < (real)(0.5) && + cbet2 * lam12 < (real)(0.5); + real somg12, comg12, ssig12, csig12; + sbet12a = sbet2 * cbet1 + cbet2 * sbet1; + if (shortline) { + real sbetm2 = sq(sbet1 + sbet2), omg12; + /* sin((bet1+bet2)/2)^2 + * = (sbet1 + sbet2)^2 / ((sbet1 + sbet2)^2 + (cbet1 + cbet2)^2) */ + sbetm2 /= sbetm2 + sq(cbet1 + cbet2); + dnm = sqrt(1 + g->ep2 * sbetm2); + omg12 = lam12 / (g->f1 * dnm); + somg12 = sin(omg12); comg12 = cos(omg12); + } else { + somg12 = slam12; comg12 = clam12; + } + + salp1 = cbet2 * somg12; + calp1 = comg12 >= 0 ? + sbet12 + cbet2 * sbet1 * sq(somg12) / (1 + comg12) : + sbet12a - cbet2 * sbet1 * sq(somg12) / (1 - comg12); + + ssig12 = hypotx(salp1, calp1); + csig12 = sbet1 * sbet2 + cbet1 * cbet2 * comg12; + + if (shortline && ssig12 < g->etol2) { + /* really short lines */ + salp2 = cbet1 * somg12; + calp2 = sbet12 - cbet1 * sbet2 * + (comg12 >= 0 ? sq(somg12) / (1 + comg12) : 1 - comg12); + norm2(&salp2, &calp2); + /* Set return value */ + sig12 = atan2(ssig12, csig12); + } else if (fabs(g->n) > (real)(0.1) || /* No astroid calc if too eccentric */ + csig12 >= 0 || + ssig12 >= 6 * fabs(g->n) * pi * sq(cbet1)) { + /* Nothing to do, zeroth order spherical approximation is OK */ + } else { + /* Scale lam12 and bet2 to x, y coordinate system where antipodal point + * is at origin and singular point is at y = 0, x = -1. */ + real y, lamscale, betscale; + /* Volatile declaration needed to fix inverse case + * 56.320923501171 0 -56.320923501171 179.664747671772880215 + * which otherwise fails with g++ 4.4.4 x86 -O3 */ + volatile real x; + real lam12x = atan2(-slam12, -clam12); /* lam12 - pi */ + if (g->f >= 0) { /* In fact f == 0 does not get here */ + /* x = dlong, y = dlat */ + { + real + k2 = sq(sbet1) * g->ep2, + eps = k2 / (2 * (1 + sqrt(1 + k2)) + k2); + lamscale = g->f * cbet1 * A3f(g, eps) * pi; + } + betscale = lamscale * cbet1; + + x = lam12x / lamscale; + y = sbet12a / betscale; + } else { /* f < 0 */ + /* x = dlat, y = dlong */ + real + cbet12a = cbet2 * cbet1 - sbet2 * sbet1, + bet12a = atan2(sbet12a, cbet12a); + real m12b, m0; + /* In the case of lon12 = 180, this repeats a calculation made in + * Inverse. */ + Lengths(g, g->n, pi + bet12a, + sbet1, -cbet1, dn1, sbet2, cbet2, dn2, + cbet1, cbet2, nullptr, &m12b, &m0, nullptr, nullptr, Ca); + x = -1 + m12b / (cbet1 * cbet2 * m0 * pi); + betscale = x < -(real)(0.01) ? sbet12a / x : + -g->f * sq(cbet1) * pi; + lamscale = betscale / cbet1; + y = lam12x / lamscale; + } + + if (y > -tol1 && x > -1 - xthresh) { + /* strip near cut */ + if (g->f >= 0) { + salp1 = minx((real)(1), -(real)(x)); calp1 = - sqrt(1 - sq(salp1)); + } else { + calp1 = maxx((real)(x > -tol1 ? 0 : -1), (real)(x)); + salp1 = sqrt(1 - sq(calp1)); + } + } else { + /* Estimate alp1, by solving the astroid problem. + * + * Could estimate alpha1 = theta + pi/2, directly, i.e., + * calp1 = y/k; salp1 = -x/(1+k); for f >= 0 + * calp1 = x/(1+k); salp1 = -y/k; for f < 0 (need to check) + * + * However, it's better to estimate omg12 from astroid and use + * spherical formula to compute alp1. This reduces the mean number of + * Newton iterations for astroid cases from 2.24 (min 0, max 6) to 2.12 + * (min 0 max 5). The changes in the number of iterations are as + * follows: + * + * change percent + * 1 5 + * 0 78 + * -1 16 + * -2 0.6 + * -3 0.04 + * -4 0.002 + * + * The histogram of iterations is (m = number of iterations estimating + * alp1 directly, n = number of iterations estimating via omg12, total + * number of trials = 148605): + * + * iter m n + * 0 148 186 + * 1 13046 13845 + * 2 93315 102225 + * 3 36189 32341 + * 4 5396 7 + * 5 455 1 + * 6 56 0 + * + * Because omg12 is near pi, estimate work with omg12a = pi - omg12 */ + real k = Astroid(x, y); + real + omg12a = lamscale * ( g->f >= 0 ? -x * k/(1 + k) : -y * (1 + k)/k ); + somg12 = sin(omg12a); comg12 = -cos(omg12a); + /* Update spherical estimate of alp1 using omg12 instead of lam12 */ + salp1 = cbet2 * somg12; + calp1 = sbet12a - cbet2 * sbet1 * sq(somg12) / (1 - comg12); + } + } + /* Sanity check on starting guess. Backwards check allows NaN through. */ + if (!(salp1 <= 0)) + norm2(&salp1, &calp1); + else { + salp1 = 1; calp1 = 0; + } + + *psalp1 = salp1; + *pcalp1 = calp1; + if (shortline) + *pdnm = dnm; + if (sig12 >= 0) { + *psalp2 = salp2; + *pcalp2 = calp2; + } + return sig12; +} + +real Lambda12(const struct geod_geodesic* g, + real sbet1, real cbet1, real dn1, + real sbet2, real cbet2, real dn2, + real salp1, real calp1, + real slam120, real clam120, + real* psalp2, real* pcalp2, + real* psig12, + real* pssig1, real* pcsig1, + real* pssig2, real* pcsig2, + real* peps, + real* pdomg12, + boolx diffp, real* pdlam12, + /* Scratch area of the right size */ + real Ca[]) { + real salp2 = 0, calp2 = 0, sig12 = 0, + ssig1 = 0, csig1 = 0, ssig2 = 0, csig2 = 0, eps = 0, + domg12 = 0, dlam12 = 0; + real salp0, calp0; + real somg1, comg1, somg2, comg2, somg12, comg12, lam12; + real B312, eta, k2; + + if (sbet1 == 0 && calp1 == 0) + /* Break degeneracy of equatorial line. This case has already been + * handled. */ + calp1 = -tiny; + + /* sin(alp1) * cos(bet1) = sin(alp0) */ + salp0 = salp1 * cbet1; + calp0 = hypotx(calp1, salp1 * sbet1); /* calp0 > 0 */ + + /* tan(bet1) = tan(sig1) * cos(alp1) + * tan(omg1) = sin(alp0) * tan(sig1) = tan(omg1)=tan(alp1)*sin(bet1) */ + ssig1 = sbet1; somg1 = salp0 * sbet1; + csig1 = comg1 = calp1 * cbet1; + norm2(&ssig1, &csig1); + /* norm2(&somg1, &comg1); -- don't need to normalize! */ + + /* Enforce symmetries in the case abs(bet2) = -bet1. Need to be careful + * about this case, since this can yield singularities in the Newton + * iteration. + * sin(alp2) * cos(bet2) = sin(alp0) */ + salp2 = cbet2 != cbet1 ? salp0 / cbet2 : salp1; + /* calp2 = sqrt(1 - sq(salp2)) + * = sqrt(sq(calp0) - sq(sbet2)) / cbet2 + * and subst for calp0 and rearrange to give (choose positive sqrt + * to give alp2 in [0, pi/2]). */ + calp2 = cbet2 != cbet1 || fabs(sbet2) != -sbet1 ? + sqrt(sq(calp1 * cbet1) + + (cbet1 < -sbet1 ? + (cbet2 - cbet1) * (cbet1 + cbet2) : + (sbet1 - sbet2) * (sbet1 + sbet2))) / cbet2 : + fabs(calp1); + /* tan(bet2) = tan(sig2) * cos(alp2) + * tan(omg2) = sin(alp0) * tan(sig2). */ + ssig2 = sbet2; somg2 = salp0 * sbet2; + csig2 = comg2 = calp2 * cbet2; + norm2(&ssig2, &csig2); + /* norm2(&somg2, &comg2); -- don't need to normalize! */ + + /* sig12 = sig2 - sig1, limit to [0, pi] */ + sig12 = atan2(maxx((real)(0), csig1 * ssig2 - ssig1 * csig2), + csig1 * csig2 + ssig1 * ssig2); + + /* omg12 = omg2 - omg1, limit to [0, pi] */ + somg12 = maxx((real)(0), comg1 * somg2 - somg1 * comg2); + comg12 = comg1 * comg2 + somg1 * somg2; + /* eta = omg12 - lam120 */ + eta = atan2(somg12 * clam120 - comg12 * slam120, + comg12 * clam120 + somg12 * slam120); + k2 = sq(calp0) * g->ep2; + eps = k2 / (2 * (1 + sqrt(1 + k2)) + k2); + C3f(g, eps, Ca); + B312 = (SinCosSeries(TRUE, ssig2, csig2, Ca, nC3-1) - + SinCosSeries(TRUE, ssig1, csig1, Ca, nC3-1)); + domg12 = -g->f * A3f(g, eps) * salp0 * (sig12 + B312); + lam12 = eta + domg12; + + if (diffp) { + if (calp2 == 0) + dlam12 = - 2 * g->f1 * dn1 / sbet1; + else { + Lengths(g, eps, sig12, ssig1, csig1, dn1, ssig2, csig2, dn2, + cbet1, cbet2, nullptr, &dlam12, nullptr, nullptr, nullptr, Ca); + dlam12 *= g->f1 / (calp2 * cbet2); + } + } + + *psalp2 = salp2; + *pcalp2 = calp2; + *psig12 = sig12; + *pssig1 = ssig1; + *pcsig1 = csig1; + *pssig2 = ssig2; + *pcsig2 = csig2; + *peps = eps; + *pdomg12 = domg12; + if (diffp) + *pdlam12 = dlam12; + + return lam12; +} + +real A3f(const struct geod_geodesic* g, real eps) { + /* Evaluate A3 */ + return polyval(nA3 - 1, g->A3x, eps); +} + +void C3f(const struct geod_geodesic* g, real eps, real c[]) { + /* Evaluate C3 coeffs + * Elements c[1] through c[nC3 - 1] are set */ + real mult = 1; + int o = 0, l; + for (l = 1; l < nC3; ++l) { /* l is index of C3[l] */ + int m = nC3 - l - 1; /* order of polynomial in eps */ + mult *= eps; + c[l] = mult * polyval(m, g->C3x + o, eps); + o += m + 1; + } +} + +void C4f(const struct geod_geodesic* g, real eps, real c[]) { + /* Evaluate C4 coeffs + * Elements c[0] through c[nC4 - 1] are set */ + real mult = 1; + int o = 0, l; + for (l = 0; l < nC4; ++l) { /* l is index of C4[l] */ + int m = nC4 - l - 1; /* order of polynomial in eps */ + c[l] = mult * polyval(m, g->C4x + o, eps); + o += m + 1; + mult *= eps; + } +} + +/* The scale factor A1-1 = mean value of (d/dsigma)I1 - 1 */ +real A1m1f(real eps) { + static const real coeff[] = { + /* (1-eps)*A1-1, polynomial in eps2 of order 3 */ + 1, 4, 64, 0, 256, + }; + int m = nA1/2; + real t = polyval(m, coeff, sq(eps)) / coeff[m + 1]; + return (t + eps) / (1 - eps); +} + +/* The coefficients C1[l] in the Fourier expansion of B1 */ +void C1f(real eps, real c[]) { + static const real coeff[] = { + /* C1[1]/eps^1, polynomial in eps2 of order 2 */ + -1, 6, -16, 32, + /* C1[2]/eps^2, polynomial in eps2 of order 2 */ + -9, 64, -128, 2048, + /* C1[3]/eps^3, polynomial in eps2 of order 1 */ + 9, -16, 768, + /* C1[4]/eps^4, polynomial in eps2 of order 1 */ + 3, -5, 512, + /* C1[5]/eps^5, polynomial in eps2 of order 0 */ + -7, 1280, + /* C1[6]/eps^6, polynomial in eps2 of order 0 */ + -7, 2048, + }; + real + eps2 = sq(eps), + d = eps; + int o = 0, l; + for (l = 1; l <= nC1; ++l) { /* l is index of C1p[l] */ + int m = (nC1 - l) / 2; /* order of polynomial in eps^2 */ + c[l] = d * polyval(m, coeff + o, eps2) / coeff[o + m + 1]; + o += m + 2; + d *= eps; + } +} + +/* The coefficients C1p[l] in the Fourier expansion of B1p */ +void C1pf(real eps, real c[]) { + static const real coeff[] = { + /* C1p[1]/eps^1, polynomial in eps2 of order 2 */ + 205, -432, 768, 1536, + /* C1p[2]/eps^2, polynomial in eps2 of order 2 */ + 4005, -4736, 3840, 12288, + /* C1p[3]/eps^3, polynomial in eps2 of order 1 */ + -225, 116, 384, + /* C1p[4]/eps^4, polynomial in eps2 of order 1 */ + -7173, 2695, 7680, + /* C1p[5]/eps^5, polynomial in eps2 of order 0 */ + 3467, 7680, + /* C1p[6]/eps^6, polynomial in eps2 of order 0 */ + 38081, 61440, + }; + real + eps2 = sq(eps), + d = eps; + int o = 0, l; + for (l = 1; l <= nC1p; ++l) { /* l is index of C1p[l] */ + int m = (nC1p - l) / 2; /* order of polynomial in eps^2 */ + c[l] = d * polyval(m, coeff + o, eps2) / coeff[o + m + 1]; + o += m + 2; + d *= eps; + } +} + +/* The scale factor A2-1 = mean value of (d/dsigma)I2 - 1 */ +real A2m1f(real eps) { + static const real coeff[] = { + /* (eps+1)*A2-1, polynomial in eps2 of order 3 */ + -11, -28, -192, 0, 256, + }; + int m = nA2/2; + real t = polyval(m, coeff, sq(eps)) / coeff[m + 1]; + return (t - eps) / (1 + eps); +} + +/* The coefficients C2[l] in the Fourier expansion of B2 */ +void C2f(real eps, real c[]) { + static const real coeff[] = { + /* C2[1]/eps^1, polynomial in eps2 of order 2 */ + 1, 2, 16, 32, + /* C2[2]/eps^2, polynomial in eps2 of order 2 */ + 35, 64, 384, 2048, + /* C2[3]/eps^3, polynomial in eps2 of order 1 */ + 15, 80, 768, + /* C2[4]/eps^4, polynomial in eps2 of order 1 */ + 7, 35, 512, + /* C2[5]/eps^5, polynomial in eps2 of order 0 */ + 63, 1280, + /* C2[6]/eps^6, polynomial in eps2 of order 0 */ + 77, 2048, + }; + real + eps2 = sq(eps), + d = eps; + int o = 0, l; + for (l = 1; l <= nC2; ++l) { /* l is index of C2[l] */ + int m = (nC2 - l) / 2; /* order of polynomial in eps^2 */ + c[l] = d * polyval(m, coeff + o, eps2) / coeff[o + m + 1]; + o += m + 2; + d *= eps; + } +} + +/* The scale factor A3 = mean value of (d/dsigma)I3 */ +void A3coeff(struct geod_geodesic* g) { + static const real coeff[] = { + /* A3, coeff of eps^5, polynomial in n of order 0 */ + -3, 128, + /* A3, coeff of eps^4, polynomial in n of order 1 */ + -2, -3, 64, + /* A3, coeff of eps^3, polynomial in n of order 2 */ + -1, -3, -1, 16, + /* A3, coeff of eps^2, polynomial in n of order 2 */ + 3, -1, -2, 8, + /* A3, coeff of eps^1, polynomial in n of order 1 */ + 1, -1, 2, + /* A3, coeff of eps^0, polynomial in n of order 0 */ + 1, 1, + }; + int o = 0, k = 0, j; + for (j = nA3 - 1; j >= 0; --j) { /* coeff of eps^j */ + int m = nA3 - j - 1 < j ? nA3 - j - 1 : j; /* order of polynomial in n */ + g->A3x[k++] = polyval(m, coeff + o, g->n) / coeff[o + m + 1]; + o += m + 2; + } +} + +/* The coefficients C3[l] in the Fourier expansion of B3 */ +void C3coeff(struct geod_geodesic* g) { + static const real coeff[] = { + /* C3[1], coeff of eps^5, polynomial in n of order 0 */ + 3, 128, + /* C3[1], coeff of eps^4, polynomial in n of order 1 */ + 2, 5, 128, + /* C3[1], coeff of eps^3, polynomial in n of order 2 */ + -1, 3, 3, 64, + /* C3[1], coeff of eps^2, polynomial in n of order 2 */ + -1, 0, 1, 8, + /* C3[1], coeff of eps^1, polynomial in n of order 1 */ + -1, 1, 4, + /* C3[2], coeff of eps^5, polynomial in n of order 0 */ + 5, 256, + /* C3[2], coeff of eps^4, polynomial in n of order 1 */ + 1, 3, 128, + /* C3[2], coeff of eps^3, polynomial in n of order 2 */ + -3, -2, 3, 64, + /* C3[2], coeff of eps^2, polynomial in n of order 2 */ + 1, -3, 2, 32, + /* C3[3], coeff of eps^5, polynomial in n of order 0 */ + 7, 512, + /* C3[3], coeff of eps^4, polynomial in n of order 1 */ + -10, 9, 384, + /* C3[3], coeff of eps^3, polynomial in n of order 2 */ + 5, -9, 5, 192, + /* C3[4], coeff of eps^5, polynomial in n of order 0 */ + 7, 512, + /* C3[4], coeff of eps^4, polynomial in n of order 1 */ + -14, 7, 512, + /* C3[5], coeff of eps^5, polynomial in n of order 0 */ + 21, 2560, + }; + int o = 0, k = 0, l, j; + for (l = 1; l < nC3; ++l) { /* l is index of C3[l] */ + for (j = nC3 - 1; j >= l; --j) { /* coeff of eps^j */ + int m = nC3 - j - 1 < j ? nC3 - j - 1 : j; /* order of polynomial in n */ + g->C3x[k++] = polyval(m, coeff + o, g->n) / coeff[o + m + 1]; + o += m + 2; + } + } +} + +/* The coefficients C4[l] in the Fourier expansion of I4 */ +void C4coeff(struct geod_geodesic* g) { + static const real coeff[] = { + /* C4[0], coeff of eps^5, polynomial in n of order 0 */ + 97, 15015, + /* C4[0], coeff of eps^4, polynomial in n of order 1 */ + 1088, 156, 45045, + /* C4[0], coeff of eps^3, polynomial in n of order 2 */ + -224, -4784, 1573, 45045, + /* C4[0], coeff of eps^2, polynomial in n of order 3 */ + -10656, 14144, -4576, -858, 45045, + /* C4[0], coeff of eps^1, polynomial in n of order 4 */ + 64, 624, -4576, 6864, -3003, 15015, + /* C4[0], coeff of eps^0, polynomial in n of order 5 */ + 100, 208, 572, 3432, -12012, 30030, 45045, + /* C4[1], coeff of eps^5, polynomial in n of order 0 */ + 1, 9009, + /* C4[1], coeff of eps^4, polynomial in n of order 1 */ + -2944, 468, 135135, + /* C4[1], coeff of eps^3, polynomial in n of order 2 */ + 5792, 1040, -1287, 135135, + /* C4[1], coeff of eps^2, polynomial in n of order 3 */ + 5952, -11648, 9152, -2574, 135135, + /* C4[1], coeff of eps^1, polynomial in n of order 4 */ + -64, -624, 4576, -6864, 3003, 135135, + /* C4[2], coeff of eps^5, polynomial in n of order 0 */ + 8, 10725, + /* C4[2], coeff of eps^4, polynomial in n of order 1 */ + 1856, -936, 225225, + /* C4[2], coeff of eps^3, polynomial in n of order 2 */ + -8448, 4992, -1144, 225225, + /* C4[2], coeff of eps^2, polynomial in n of order 3 */ + -1440, 4160, -4576, 1716, 225225, + /* C4[3], coeff of eps^5, polynomial in n of order 0 */ + -136, 63063, + /* C4[3], coeff of eps^4, polynomial in n of order 1 */ + 1024, -208, 105105, + /* C4[3], coeff of eps^3, polynomial in n of order 2 */ + 3584, -3328, 1144, 315315, + /* C4[4], coeff of eps^5, polynomial in n of order 0 */ + -128, 135135, + /* C4[4], coeff of eps^4, polynomial in n of order 1 */ + -2560, 832, 405405, + /* C4[5], coeff of eps^5, polynomial in n of order 0 */ + 128, 99099, + }; + int o = 0, k = 0, l, j; + for (l = 0; l < nC4; ++l) { /* l is index of C4[l] */ + for (j = nC4 - 1; j >= l; --j) { /* coeff of eps^j */ + int m = nC4 - j - 1; /* order of polynomial in n */ + g->C4x[k++] = polyval(m, coeff + o, g->n) / coeff[o + m + 1]; + o += m + 2; + } + } +} + +int transit(real lon1, real lon2) { + real lon12; + /* Return 1 or -1 if crossing prime meridian in east or west direction. + * Otherwise return zero. */ + /* Compute lon12 the same way as Geodesic::Inverse. */ + lon1 = AngNormalize(lon1); + lon2 = AngNormalize(lon2); + lon12 = AngDiff(lon1, lon2, nullptr); + return lon1 <= 0 && lon2 > 0 && lon12 > 0 ? 1 : + (lon2 <= 0 && lon1 > 0 && lon12 < 0 ? -1 : 0); +} + +int transitdirect(real lon1, real lon2) { + /* Compute exactly the parity of + int(ceil(lon2 / 360)) - int(ceil(lon1 / 360)) */ + lon1 = remainderx(lon1, (real)(720)); + lon2 = remainderx(lon2, (real)(720)); + return ( (lon2 <= 0 && lon2 > -360 ? 1 : 0) - + (lon1 <= 0 && lon1 > -360 ? 1 : 0) ); +} + +void accini(real s[]) { + /* Initialize an accumulator; this is an array with two elements. */ + s[0] = s[1] = 0; +} + +void acccopy(const real s[], real t[]) { + /* Copy an accumulator; t = s. */ + t[0] = s[0]; t[1] = s[1]; +} + +void accadd(real s[], real y) { + /* Add y to an accumulator. */ + real u, z = sumx(y, s[1], &u); + s[0] = sumx(z, s[0], &s[1]); + if (s[0] == 0) + s[0] = u; + else + s[1] = s[1] + u; +} + +real accsum(const real s[], real y) { + /* Return accumulator + y (but don't add to accumulator). */ + real t[2]; + acccopy(s, t); + accadd(t, y); + return t[0]; +} + +void accneg(real s[]) { + /* Negate an accumulator. */ + s[0] = -s[0]; s[1] = -s[1]; +} + +void accrem(real s[], real y) { + /* Reduce to [-y/2, y/2]. */ + s[0] = remainderx(s[0], y); + accadd(s, (real)(0)); +} + +void geod_polygon_init(struct geod_polygon* p, boolx polylinep) { + p->polyline = (polylinep != 0); + geod_polygon_clear(p); +} + +void geod_polygon_clear(struct geod_polygon* p) { + p->lat0 = p->lon0 = p->lat = p->lon = NaN; + accini(p->P); + accini(p->A); + p->num = p->crossings = 0; +} + +void geod_polygon_addpoint(const struct geod_geodesic* g, + struct geod_polygon* p, + real lat, real lon) { + lon = AngNormalize(lon); + if (p->num == 0) { + p->lat0 = p->lat = lat; + p->lon0 = p->lon = lon; + } else { + real s12, S12 = 0; /* Initialize S12 to stop Visual Studio warning */ + geod_geninverse(g, p->lat, p->lon, lat, lon, + &s12, nullptr, nullptr, nullptr, nullptr, nullptr, + p->polyline ? nullptr : &S12); + accadd(p->P, s12); + if (!p->polyline) { + accadd(p->A, S12); + p->crossings += transit(p->lon, lon); + } + p->lat = lat; p->lon = lon; + } + ++p->num; +} + +void geod_polygon_addedge(const struct geod_geodesic* g, + struct geod_polygon* p, + real azi, real s) { + if (p->num) { /* Do nothing is num is zero */ + /* Initialize S12 to stop Visual Studio warning. Initialization of lat and + * lon is to make CLang static analyzer happy. */ + real lat = 0, lon = 0, S12 = 0; + geod_gendirect(g, p->lat, p->lon, azi, GEOD_LONG_UNROLL, s, + &lat, &lon, nullptr, + nullptr, nullptr, nullptr, nullptr, + p->polyline ? nullptr : &S12); + accadd(p->P, s); + if (!p->polyline) { + accadd(p->A, S12); + p->crossings += transitdirect(p->lon, lon); + } + p->lat = lat; p->lon = lon; + ++p->num; + } +} + +unsigned geod_polygon_compute(const struct geod_geodesic* g, + const struct geod_polygon* p, + boolx reverse, boolx sign, + real* pA, real* pP) { + real s12, S12, t[2]; + if (p->num < 2) { + if (pP) *pP = 0; + if (!p->polyline && pA) *pA = 0; + return p->num; + } + if (p->polyline) { + if (pP) *pP = p->P[0]; + return p->num; + } + geod_geninverse(g, p->lat, p->lon, p->lat0, p->lon0, + &s12, nullptr, nullptr, nullptr, nullptr, nullptr, &S12); + if (pP) *pP = accsum(p->P, s12); + acccopy(p->A, t); + accadd(t, S12); + if (pA) *pA = areareduceA(t, 4 * pi * g->c2, + p->crossings + transit(p->lon, p->lon0), + reverse, sign); + return p->num; +} + +unsigned geod_polygon_testpoint(const struct geod_geodesic* g, + const struct geod_polygon* p, + real lat, real lon, + boolx reverse, boolx sign, + real* pA, real* pP) { + real perimeter, tempsum; + int crossings, i; + unsigned num = p->num + 1; + if (num == 1) { + if (pP) *pP = 0; + if (!p->polyline && pA) *pA = 0; + return num; + } + perimeter = p->P[0]; + tempsum = p->polyline ? 0 : p->A[0]; + crossings = p->crossings; + for (i = 0; i < (p->polyline ? 1 : 2); ++i) { + real s12, S12 = 0; /* Initialize S12 to stop Visual Studio warning */ + geod_geninverse(g, + i == 0 ? p->lat : lat, i == 0 ? p->lon : lon, + i != 0 ? p->lat0 : lat, i != 0 ? p->lon0 : lon, + &s12, nullptr, nullptr, nullptr, nullptr, nullptr, + p->polyline ? nullptr : &S12); + perimeter += s12; + if (!p->polyline) { + tempsum += S12; + crossings += transit(i == 0 ? p->lon : lon, + i != 0 ? p->lon0 : lon); + } + } + + if (pP) *pP = perimeter; + if (p->polyline) + return num; + + if (pA) *pA = areareduceB(tempsum, 4 * pi * g->c2, crossings, reverse, sign); + return num; +} + +unsigned geod_polygon_testedge(const struct geod_geodesic* g, + const struct geod_polygon* p, + real azi, real s, + boolx reverse, boolx sign, + real* pA, real* pP) { + real perimeter, tempsum; + int crossings; + unsigned num = p->num + 1; + if (num == 1) { /* we don't have a starting point! */ + if (pP) *pP = NaN; + if (!p->polyline && pA) *pA = NaN; + return 0; + } + perimeter = p->P[0] + s; + if (p->polyline) { + if (pP) *pP = perimeter; + return num; + } + + tempsum = p->A[0]; + crossings = p->crossings; + { + /* Initialization of lat, lon, and S12 is to make CLang static analyzer + * happy. */ + real lat = 0, lon = 0, s12, S12 = 0; + geod_gendirect(g, p->lat, p->lon, azi, GEOD_LONG_UNROLL, s, + &lat, &lon, nullptr, + nullptr, nullptr, nullptr, nullptr, &S12); + tempsum += S12; + crossings += transitdirect(p->lon, lon); + geod_geninverse(g, lat, lon, p->lat0, p->lon0, + &s12, nullptr, nullptr, nullptr, nullptr, nullptr, &S12); + perimeter += s12; + tempsum += S12; + crossings += transit(lon, p->lon0); + } + + if (pP) *pP = perimeter; + if (pA) *pA = areareduceB(tempsum, 4 * pi * g->c2, crossings, reverse, sign); + return num; +} + +void geod_polygonarea(const struct geod_geodesic* g, + real lats[], real lons[], int n, + real* pA, real* pP) { + int i; + struct geod_polygon p; + geod_polygon_init(&p, FALSE); + for (i = 0; i < n; ++i) + geod_polygon_addpoint(g, &p, lats[i], lons[i]); + geod_polygon_compute(g, &p, FALSE, TRUE, pA, pP); +} + +real areareduceA(real area[], real area0, + int crossings, boolx reverse, boolx sign) { + accrem(area, area0); + if (crossings & 1) + accadd(area, (area[0] < 0 ? 1 : -1) * area0/2); + /* area is with the clockwise sense. If !reverse convert to + * counter-clockwise convention. */ + if (!reverse) + accneg(area); + /* If sign put area in (-area0/2, area0/2], else put area in [0, area0) */ + if (sign) { + if (area[0] > area0/2) + accadd(area, -area0); + else if (area[0] <= -area0/2) + accadd(area, +area0); + } else { + if (area[0] >= area0) + accadd(area, -area0); + else if (area[0] < 0) + accadd(area, +area0); + } + return 0 + area[0]; +} + +real areareduceB(real area, real area0, + int crossings, boolx reverse, boolx sign) { + area = remainderx(area, area0); + if (crossings & 1) + area += (area < 0 ? 1 : -1) * area0/2; + /* area is with the clockwise sense. If !reverse convert to + * counter-clockwise convention. */ + if (!reverse) + area *= -1; + /* If sign put area in (-area0/2, area0/2], else put area in [0, area0) */ + if (sign) { + if (area > area0/2) + area -= area0; + else if (area <= -area0/2) + area += area0; + } else { + if (area >= area0) + area -= area0; + else if (area < 0) + area += area0; + } + return 0 + area; +} + +/** @endcond */ diff --git a/pkg/geo/geographiclib/geodesic.h b/pkg/geo/geographiclib/geodesic.h new file mode 100644 index 0000000..5d23053 --- /dev/null +++ b/pkg/geo/geographiclib/geodesic.h @@ -0,0 +1,933 @@ +/** + * \file geodesic.h + * \brief API for the geodesic routines in C + * + * This an implementation in C of the geodesic algorithms described in + * - C. F. F. Karney, + * + * Algorithms for geodesics, + * J. Geodesy 87, 43--55 (2013); + * DOI: + * 10.1007/s00190-012-0578-z; + * addenda: + * geod-addenda.html. + * . + * The principal advantages of these algorithms over previous ones (e.g., + * Vincenty, 1975) are + * - accurate to round off for |f| < 1/50; + * - the solution of the inverse problem is always found; + * - differential and integral properties of geodesics are computed. + * + * The shortest path between two points on the ellipsoid at (\e lat1, \e + * lon1) and (\e lat2, \e lon2) is called the geodesic. Its length is + * \e s12 and the geodesic from point 1 to point 2 has forward azimuths + * \e azi1 and \e azi2 at the two end points. + * + * Traditionally two geodesic problems are considered: + * - the direct problem -- given \e lat1, \e lon1, \e s12, and \e azi1, + * determine \e lat2, \e lon2, and \e azi2. This is solved by the function + * geod_direct(). + * - the inverse problem -- given \e lat1, \e lon1, and \e lat2, \e lon2, + * determine \e s12, \e azi1, and \e azi2. This is solved by the function + * geod_inverse(). + * + * The ellipsoid is specified by its equatorial radius \e a (typically in + * meters) and flattening \e f. The routines are accurate to round off with + * double precision arithmetic provided that |f| < 1/50; for the + * WGS84 ellipsoid, the errors are less than 15 nanometers. (Reasonably + * accurate results are obtained for |f| < 1/5.) For a prolate + * ellipsoid, specify \e f < 0. + * + * The routines also calculate several other quantities of interest + * - \e S12 is the area between the geodesic from point 1 to point 2 and the + * equator; i.e., it is the area, measured counter-clockwise, of the + * quadrilateral with corners (\e lat1,\e lon1), (0,\e lon1), (0,\e lon2), + * and (\e lat2,\e lon2). + * - \e m12, the reduced length of the geodesic is defined such that if + * the initial azimuth is perturbed by \e dazi1 (radians) then the + * second point is displaced by \e m12 \e dazi1 in the direction + * perpendicular to the geodesic. On a curved surface the reduced + * length obeys a symmetry relation, \e m12 + \e m21 = 0. On a flat + * surface, we have \e m12 = \e s12. + * - \e M12 and \e M21 are geodesic scales. If two geodesics are + * parallel at point 1 and separated by a small distance \e dt, then + * they are separated by a distance \e M12 \e dt at point 2. \e M21 + * is defined similarly (with the geodesics being parallel to one + * another at point 2). On a flat surface, we have \e M12 = \e M21 + * = 1. + * - \e a12 is the arc length on the auxiliary sphere. This is a + * construct for converting the problem to one in spherical + * trigonometry. \e a12 is measured in degrees. The spherical arc + * length from one equator crossing to the next is always 180°. + * + * If points 1, 2, and 3 lie on a single geodesic, then the following + * addition rules hold: + * - \e s13 = \e s12 + \e s23 + * - \e a13 = \e a12 + \e a23 + * - \e S13 = \e S12 + \e S23 + * - \e m13 = \e m12 \e M23 + \e m23 \e M21 + * - \e M13 = \e M12 \e M23 − (1 − \e M12 \e M21) \e + * m23 / \e m12 + * - \e M31 = \e M32 \e M21 − (1 − \e M23 \e M32) \e + * m12 / \e m23 + * + * The shortest distance returned by the solution of the inverse problem is + * (obviously) uniquely defined. However, in a few special cases there are + * multiple azimuths which yield the same shortest distance. Here is a + * catalog of those cases: + * - \e lat1 = −\e lat2 (with neither point at a pole). If \e azi1 = \e + * azi2, the geodesic is unique. Otherwise there are two geodesics and the + * second one is obtained by setting [\e azi1, \e azi2] → [\e azi2, \e + * azi1], [\e M12, \e M21] → [\e M21, \e M12], \e S12 → −\e + * S12. (This occurs when the longitude difference is near ±180° + * for oblate ellipsoids.) + * - \e lon2 = \e lon1 ± 180° (with neither point at a pole). If \e + * azi1 = 0° or ±180°, the geodesic is unique. Otherwise + * there are two geodesics and the second one is obtained by setting [\e + * azi1, \e azi2] → [−\e azi1, −\e azi2], \e S12 → + * −\e S12. (This occurs when \e lat2 is near −\e lat1 for + * prolate ellipsoids.) + * - Points 1 and 2 at opposite poles. There are infinitely many geodesics + * which can be generated by setting [\e azi1, \e azi2] → [\e azi1, \e + * azi2] + [\e d, −\e d], for arbitrary \e d. (For spheres, this + * prescription applies when points 1 and 2 are antipodal.) + * - \e s12 = 0 (coincident points). There are infinitely many geodesics which + * can be generated by setting [\e azi1, \e azi2] → [\e azi1, \e azi2] + + * [\e d, \e d], for arbitrary \e d. + * + * These routines are a simple transcription of the corresponding C++ classes + * in GeographicLib. The + * "class data" is represented by the structs geod_geodesic, geod_geodesicline, + * geod_polygon and pointers to these objects are passed as initial arguments + * to the member functions. Most of the internal comments have been retained. + * However, in the process of transcription some documentation has been lost + * and the documentation for the C++ classes, GeographicLib::Geodesic, + * GeographicLib::GeodesicLine, and GeographicLib::PolygonAreaT, should be + * consulted. The C++ code remains the "reference implementation". Think + * twice about restructuring the internals of the C code since this may make + * porting fixes from the C++ code more difficult. + * + * Copyright (c) Charles Karney (2012-2019) and licensed + * under the MIT/X11 License. For more information, see + * https://geographiclib.sourceforge.io/ + * + * This library was distributed with + * GeographicLib 1.50. + **********************************************************************/ + +#if !defined(GEODESIC_H) +#define GEODESIC_H 1 + +/** + * The major version of the geodesic library. (This tracks the version of + * GeographicLib.) + **********************************************************************/ +#define GEODESIC_VERSION_MAJOR 1 +/** + * The minor version of the geodesic library. (This tracks the version of + * GeographicLib.) + **********************************************************************/ +#define GEODESIC_VERSION_MINOR 50 +/** + * The patch level of the geodesic library. (This tracks the version of + * GeographicLib.) + **********************************************************************/ +#define GEODESIC_VERSION_PATCH 0 + +/** + * Pack the version components into a single integer. Users should not rely on + * this particular packing of the components of the version number; see the + * documentation for GEODESIC_VERSION, below. + **********************************************************************/ +#define GEODESIC_VERSION_NUM(a,b,c) ((((a) * 10000 + (b)) * 100) + (c)) + +/** + * The version of the geodesic library as a single integer, packed as MMmmmmpp + * where MM is the major version, mmmm is the minor version, and pp is the + * patch level. Users should not rely on this particular packing of the + * components of the version number. Instead they should use a test such as + * @code{.c} + #if GEODESIC_VERSION >= GEODESIC_VERSION_NUM(1,40,0) + ... + #endif + * @endcode + **********************************************************************/ +#define GEODESIC_VERSION \ + GEODESIC_VERSION_NUM(GEODESIC_VERSION_MAJOR, \ + GEODESIC_VERSION_MINOR, \ + GEODESIC_VERSION_PATCH) + +#if !defined(GEOD_DLL) +#if defined(_MSC_VER) +#define GEOD_DLL __declspec(dllexport) +#elif defined(__GNUC__) +#define GEOD_DLL __attribute__ ((visibility("default"))) +#else +#define GEOD_DLL +#endif +#endif + +#if defined(PROJ_RENAME_SYMBOLS) +#include "proj_symbol_rename.h" +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + + /** + * The struct containing information about the ellipsoid. This must be + * initialized by geod_init() before use. + **********************************************************************/ + struct geod_geodesic { + double a; /**< the equatorial radius */ + double f; /**< the flattening */ + /**< @cond SKIP */ + double f1, e2, ep2, n, b, c2, etol2; + double A3x[6], C3x[15], C4x[21]; + /**< @endcond */ + }; + + /** + * The struct containing information about a single geodesic. This must be + * initialized by geod_lineinit(), geod_directline(), geod_gendirectline(), + * or geod_inverseline() before use. + **********************************************************************/ + struct geod_geodesicline { + double lat1; /**< the starting latitude */ + double lon1; /**< the starting longitude */ + double azi1; /**< the starting azimuth */ + double a; /**< the equatorial radius */ + double f; /**< the flattening */ + double salp1; /**< sine of \e azi1 */ + double calp1; /**< cosine of \e azi1 */ + double a13; /**< arc length to reference point */ + double s13; /**< distance to reference point */ + /**< @cond SKIP */ + double b, c2, f1, salp0, calp0, k2, + ssig1, csig1, dn1, stau1, ctau1, somg1, comg1, + A1m1, A2m1, A3c, B11, B21, B31, A4, B41; + double C1a[6+1], C1pa[6+1], C2a[6+1], C3a[6], C4a[6]; + /**< @endcond */ + unsigned caps; /**< the capabilities */ + }; + + /** + * The struct for accumulating information about a geodesic polygon. This is + * used for computing the perimeter and area of a polygon. This must be + * initialized by geod_polygon_init() before use. + **********************************************************************/ + struct geod_polygon { + double lat; /**< the current latitude */ + double lon; /**< the current longitude */ + /**< @cond SKIP */ + double lat0; + double lon0; + double A[2]; + double P[2]; + int polyline; + int crossings; + /**< @endcond */ + unsigned num; /**< the number of points so far */ + }; + + /** + * Initialize a geod_geodesic object. + * + * @param[out] g a pointer to the object to be initialized. + * @param[in] a the equatorial radius (meters). + * @param[in] f the flattening. + **********************************************************************/ + void GEOD_DLL geod_init(struct geod_geodesic* g, double a, double f); + + /** + * Solve the direct geodesic problem. + * + * @param[in] g a pointer to the geod_geodesic object specifying the + * ellipsoid. + * @param[in] lat1 latitude of point 1 (degrees). + * @param[in] lon1 longitude of point 1 (degrees). + * @param[in] azi1 azimuth at point 1 (degrees). + * @param[in] s12 distance from point 1 to point 2 (meters); it can be + * negative. + * @param[out] plat2 pointer to the latitude of point 2 (degrees). + * @param[out] plon2 pointer to the longitude of point 2 (degrees). + * @param[out] pazi2 pointer to the (forward) azimuth at point 2 (degrees). + * + * \e g must have been initialized with a call to geod_init(). \e lat1 + * should be in the range [−90°, 90°]. The values of \e lon2 + * and \e azi2 returned are in the range [−180°, 180°]. Any of + * the "return" arguments \e plat2, etc., may be replaced by 0, if you do not + * need some quantities computed. + * + * If either point is at a pole, the azimuth is defined by keeping the + * longitude fixed, writing \e lat = ±(90° − ε), and + * taking the limit ε → 0+. An arc length greater that 180° + * signifies a geodesic which is not a shortest path. (For a prolate + * ellipsoid, an additional condition is necessary for a shortest path: the + * longitudinal extent must not exceed of 180°.) + * + * Example, determine the point 10000 km NE of JFK: + @code{.c} + struct geod_geodesic g; + double lat, lon; + geod_init(&g, 6378137, 1/298.257223563); + geod_direct(&g, 40.64, -73.78, 45.0, 10e6, &lat, &lon, 0); + printf("%.5f %.5f\n", lat, lon); + @endcode + **********************************************************************/ + void GEOD_DLL geod_direct(const struct geod_geodesic* g, + double lat1, double lon1, double azi1, double s12, + double* plat2, double* plon2, double* pazi2); + + /** + * The general direct geodesic problem. + * + * @param[in] g a pointer to the geod_geodesic object specifying the + * ellipsoid. + * @param[in] lat1 latitude of point 1 (degrees). + * @param[in] lon1 longitude of point 1 (degrees). + * @param[in] azi1 azimuth at point 1 (degrees). + * @param[in] flags bitor'ed combination of geod_flags(); \e flags & + * GEOD_ARCMODE determines the meaning of \e s12_a12 and \e flags & + * GEOD_LONG_UNROLL "unrolls" \e lon2. + * @param[in] s12_a12 if \e flags & GEOD_ARCMODE is 0, this is the distance + * from point 1 to point 2 (meters); otherwise it is the arc length + * from point 1 to point 2 (degrees); it can be negative. + * @param[out] plat2 pointer to the latitude of point 2 (degrees). + * @param[out] plon2 pointer to the longitude of point 2 (degrees). + * @param[out] pazi2 pointer to the (forward) azimuth at point 2 (degrees). + * @param[out] ps12 pointer to the distance from point 1 to point 2 + * (meters). + * @param[out] pm12 pointer to the reduced length of geodesic (meters). + * @param[out] pM12 pointer to the geodesic scale of point 2 relative to + * point 1 (dimensionless). + * @param[out] pM21 pointer to the geodesic scale of point 1 relative to + * point 2 (dimensionless). + * @param[out] pS12 pointer to the area under the geodesic + * (meters2). + * @return \e a12 arc length from point 1 to point 2 (degrees). + * + * \e g must have been initialized with a call to geod_init(). \e lat1 + * should be in the range [−90°, 90°]. The function value \e + * a12 equals \e s12_a12 if \e flags & GEOD_ARCMODE. Any of the "return" + * arguments, \e plat2, etc., may be replaced by 0, if you do not need some + * quantities computed. + * + * With \e flags & GEOD_LONG_UNROLL bit set, the longitude is "unrolled" so + * that the quantity \e lon2 − \e lon1 indicates how many times and in + * what sense the geodesic encircles the ellipsoid. + **********************************************************************/ + double GEOD_DLL geod_gendirect(const struct geod_geodesic* g, + double lat1, double lon1, double azi1, + unsigned flags, double s12_a12, + double* plat2, double* plon2, double* pazi2, + double* ps12, double* pm12, + double* pM12, double* pM21, + double* pS12); + + /** + * Solve the inverse geodesic problem. + * + * @param[in] g a pointer to the geod_geodesic object specifying the + * ellipsoid. + * @param[in] lat1 latitude of point 1 (degrees). + * @param[in] lon1 longitude of point 1 (degrees). + * @param[in] lat2 latitude of point 2 (degrees). + * @param[in] lon2 longitude of point 2 (degrees). + * @param[out] ps12 pointer to the distance from point 1 to point 2 + * (meters). + * @param[out] pazi1 pointer to the azimuth at point 1 (degrees). + * @param[out] pazi2 pointer to the (forward) azimuth at point 2 (degrees). + * + * \e g must have been initialized with a call to geod_init(). \e lat1 and + * \e lat2 should be in the range [−90°, 90°]. The values of + * \e azi1 and \e azi2 returned are in the range [−180°, 180°]. + * Any of the "return" arguments, \e ps12, etc., may be replaced by 0, if you + * do not need some quantities computed. + * + * If either point is at a pole, the azimuth is defined by keeping the + * longitude fixed, writing \e lat = ±(90° − ε), and + * taking the limit ε → 0+. + * + * The solution to the inverse problem is found using Newton's method. If + * this fails to converge (this is very unlikely in geodetic applications + * but does occur for very eccentric ellipsoids), then the bisection method + * is used to refine the solution. + * + * Example, determine the distance between JFK and Singapore Changi Airport: + @code{.c} + struct geod_geodesic g; + double s12; + geod_init(&g, 6378137, 1/298.257223563); + geod_inverse(&g, 40.64, -73.78, 1.36, 103.99, &s12, 0, 0); + printf("%.3f\n", s12); + @endcode + **********************************************************************/ + void GEOD_DLL geod_inverse(const struct geod_geodesic* g, + double lat1, double lon1, + double lat2, double lon2, + double* ps12, double* pazi1, double* pazi2); + + /** + * The general inverse geodesic calculation. + * + * @param[in] g a pointer to the geod_geodesic object specifying the + * ellipsoid. + * @param[in] lat1 latitude of point 1 (degrees). + * @param[in] lon1 longitude of point 1 (degrees). + * @param[in] lat2 latitude of point 2 (degrees). + * @param[in] lon2 longitude of point 2 (degrees). + * @param[out] ps12 pointer to the distance from point 1 to point 2 + * (meters). + * @param[out] pazi1 pointer to the azimuth at point 1 (degrees). + * @param[out] pazi2 pointer to the (forward) azimuth at point 2 (degrees). + * @param[out] pm12 pointer to the reduced length of geodesic (meters). + * @param[out] pM12 pointer to the geodesic scale of point 2 relative to + * point 1 (dimensionless). + * @param[out] pM21 pointer to the geodesic scale of point 1 relative to + * point 2 (dimensionless). + * @param[out] pS12 pointer to the area under the geodesic + * (meters2). + * @return \e a12 arc length from point 1 to point 2 (degrees). + * + * \e g must have been initialized with a call to geod_init(). \e lat1 and + * \e lat2 should be in the range [−90°, 90°]. Any of the + * "return" arguments \e ps12, etc., may be replaced by 0, if you do not need + * some quantities computed. + **********************************************************************/ + double GEOD_DLL geod_geninverse(const struct geod_geodesic* g, + double lat1, double lon1, + double lat2, double lon2, + double* ps12, double* pazi1, double* pazi2, + double* pm12, double* pM12, double* pM21, + double* pS12); + + /** + * Initialize a geod_geodesicline object. + * + * @param[out] l a pointer to the object to be initialized. + * @param[in] g a pointer to the geod_geodesic object specifying the + * ellipsoid. + * @param[in] lat1 latitude of point 1 (degrees). + * @param[in] lon1 longitude of point 1 (degrees). + * @param[in] azi1 azimuth at point 1 (degrees). + * @param[in] caps bitor'ed combination of geod_mask() values specifying the + * capabilities the geod_geodesicline object should possess, i.e., which + * quantities can be returned in calls to geod_position() and + * geod_genposition(). + * + * \e g must have been initialized with a call to geod_init(). \e lat1 + * should be in the range [−90°, 90°]. + * + * The geod_mask values are [see geod_mask()]: + * - \e caps |= GEOD_LATITUDE for the latitude \e lat2; this is + * added automatically, + * - \e caps |= GEOD_LONGITUDE for the latitude \e lon2, + * - \e caps |= GEOD_AZIMUTH for the latitude \e azi2; this is + * added automatically, + * - \e caps |= GEOD_DISTANCE for the distance \e s12, + * - \e caps |= GEOD_REDUCEDLENGTH for the reduced length \e m12, + * - \e caps |= GEOD_GEODESICSCALE for the geodesic scales \e M12 + * and \e M21, + * - \e caps |= GEOD_AREA for the area \e S12, + * - \e caps |= GEOD_DISTANCE_IN permits the length of the + * geodesic to be given in terms of \e s12; without this capability the + * length can only be specified in terms of arc length. + * . + * A value of \e caps = 0 is treated as GEOD_LATITUDE | GEOD_LONGITUDE | + * GEOD_AZIMUTH | GEOD_DISTANCE_IN (to support the solution of the "standard" + * direct problem). + * + * When initialized by this function, point 3 is undefined (l->s13 = l->a13 = + * NaN). + **********************************************************************/ + void GEOD_DLL geod_lineinit(struct geod_geodesicline* l, + const struct geod_geodesic* g, + double lat1, double lon1, double azi1, + unsigned caps); + + /** + * Initialize a geod_geodesicline object in terms of the direct geodesic + * problem. + * + * @param[out] l a pointer to the object to be initialized. + * @param[in] g a pointer to the geod_geodesic object specifying the + * ellipsoid. + * @param[in] lat1 latitude of point 1 (degrees). + * @param[in] lon1 longitude of point 1 (degrees). + * @param[in] azi1 azimuth at point 1 (degrees). + * @param[in] s12 distance from point 1 to point 2 (meters); it can be + * negative. + * @param[in] caps bitor'ed combination of geod_mask() values specifying the + * capabilities the geod_geodesicline object should possess, i.e., which + * quantities can be returned in calls to geod_position() and + * geod_genposition(). + * + * This function sets point 3 of the geod_geodesicline to correspond to point + * 2 of the direct geodesic problem. See geod_lineinit() for more + * information. + **********************************************************************/ + void GEOD_DLL geod_directline(struct geod_geodesicline* l, + const struct geod_geodesic* g, + double lat1, double lon1, + double azi1, double s12, + unsigned caps); + + /** + * Initialize a geod_geodesicline object in terms of the direct geodesic + * problem spacified in terms of either distance or arc length. + * + * @param[out] l a pointer to the object to be initialized. + * @param[in] g a pointer to the geod_geodesic object specifying the + * ellipsoid. + * @param[in] lat1 latitude of point 1 (degrees). + * @param[in] lon1 longitude of point 1 (degrees). + * @param[in] azi1 azimuth at point 1 (degrees). + * @param[in] flags either GEOD_NOFLAGS or GEOD_ARCMODE to determining the + * meaning of the \e s12_a12. + * @param[in] s12_a12 if \e flags = GEOD_NOFLAGS, this is the distance + * from point 1 to point 2 (meters); if \e flags = GEOD_ARCMODE, it is + * the arc length from point 1 to point 2 (degrees); it can be + * negative. + * @param[in] caps bitor'ed combination of geod_mask() values specifying the + * capabilities the geod_geodesicline object should possess, i.e., which + * quantities can be returned in calls to geod_position() and + * geod_genposition(). + * + * This function sets point 3 of the geod_geodesicline to correspond to point + * 2 of the direct geodesic problem. See geod_lineinit() for more + * information. + **********************************************************************/ + void GEOD_DLL geod_gendirectline(struct geod_geodesicline* l, + const struct geod_geodesic* g, + double lat1, double lon1, double azi1, + unsigned flags, double s12_a12, + unsigned caps); + + /** + * Initialize a geod_geodesicline object in terms of the inverse geodesic + * problem. + * + * @param[out] l a pointer to the object to be initialized. + * @param[in] g a pointer to the geod_geodesic object specifying the + * ellipsoid. + * @param[in] lat1 latitude of point 1 (degrees). + * @param[in] lon1 longitude of point 1 (degrees). + * @param[in] lat2 latitude of point 2 (degrees). + * @param[in] lon2 longitude of point 2 (degrees). + * @param[in] caps bitor'ed combination of geod_mask() values specifying the + * capabilities the geod_geodesicline object should possess, i.e., which + * quantities can be returned in calls to geod_position() and + * geod_genposition(). + * + * This function sets point 3 of the geod_geodesicline to correspond to point + * 2 of the inverse geodesic problem. See geod_lineinit() for more + * information. + **********************************************************************/ + void GEOD_DLL geod_inverseline(struct geod_geodesicline* l, + const struct geod_geodesic* g, + double lat1, double lon1, + double lat2, double lon2, + unsigned caps); + + /** + * Compute the position along a geod_geodesicline. + * + * @param[in] l a pointer to the geod_geodesicline object specifying the + * geodesic line. + * @param[in] s12 distance from point 1 to point 2 (meters); it can be + * negative. + * @param[out] plat2 pointer to the latitude of point 2 (degrees). + * @param[out] plon2 pointer to the longitude of point 2 (degrees); requires + * that \e l was initialized with \e caps |= GEOD_LONGITUDE. + * @param[out] pazi2 pointer to the (forward) azimuth at point 2 (degrees). + * + * \e l must have been initialized with a call, e.g., to geod_lineinit(), + * with \e caps |= GEOD_DISTANCE_IN (or \e caps = 0). The values of \e lon2 + * and \e azi2 returned are in the range [−180°, 180°]. Any of + * the "return" arguments \e plat2, etc., may be replaced by 0, if you do not + * need some quantities computed. + * + * Example, compute way points between JFK and Singapore Changi Airport + * the "obvious" way using geod_direct(): + @code{.c} + struct geod_geodesic g; + double s12, azi1, lat[101],lon[101]; + int i; + geod_init(&g, 6378137, 1/298.257223563); + geod_inverse(&g, 40.64, -73.78, 1.36, 103.99, &s12, &azi1, 0); + for (i = 0; i < 101; ++i) { + geod_direct(&g, 40.64, -73.78, azi1, i * s12 * 0.01, lat + i, lon + i, 0); + printf("%.5f %.5f\n", lat[i], lon[i]); + } + @endcode + * A faster way using geod_position(): + @code{.c} + struct geod_geodesic g; + struct geod_geodesicline l; + double lat[101],lon[101]; + int i; + geod_init(&g, 6378137, 1/298.257223563); + geod_inverseline(&l, &g, 40.64, -73.78, 1.36, 103.99, 0); + for (i = 0; i <= 100; ++i) { + geod_position(&l, i * l.s13 * 0.01, lat + i, lon + i, 0); + printf("%.5f %.5f\n", lat[i], lon[i]); + } + @endcode + **********************************************************************/ + void GEOD_DLL geod_position(const struct geod_geodesicline* l, double s12, + double* plat2, double* plon2, double* pazi2); + + /** + * The general position function. + * + * @param[in] l a pointer to the geod_geodesicline object specifying the + * geodesic line. + * @param[in] flags bitor'ed combination of geod_flags(); \e flags & + * GEOD_ARCMODE determines the meaning of \e s12_a12 and \e flags & + * GEOD_LONG_UNROLL "unrolls" \e lon2; if \e flags & GEOD_ARCMODE is 0, + * then \e l must have been initialized with \e caps |= GEOD_DISTANCE_IN. + * @param[in] s12_a12 if \e flags & GEOD_ARCMODE is 0, this is the + * distance from point 1 to point 2 (meters); otherwise it is the + * arc length from point 1 to point 2 (degrees); it can be + * negative. + * @param[out] plat2 pointer to the latitude of point 2 (degrees). + * @param[out] plon2 pointer to the longitude of point 2 (degrees); requires + * that \e l was initialized with \e caps |= GEOD_LONGITUDE. + * @param[out] pazi2 pointer to the (forward) azimuth at point 2 (degrees). + * @param[out] ps12 pointer to the distance from point 1 to point 2 + * (meters); requires that \e l was initialized with \e caps |= + * GEOD_DISTANCE. + * @param[out] pm12 pointer to the reduced length of geodesic (meters); + * requires that \e l was initialized with \e caps |= GEOD_REDUCEDLENGTH. + * @param[out] pM12 pointer to the geodesic scale of point 2 relative to + * point 1 (dimensionless); requires that \e l was initialized with \e caps + * |= GEOD_GEODESICSCALE. + * @param[out] pM21 pointer to the geodesic scale of point 1 relative to + * point 2 (dimensionless); requires that \e l was initialized with \e caps + * |= GEOD_GEODESICSCALE. + * @param[out] pS12 pointer to the area under the geodesic + * (meters2); requires that \e l was initialized with \e caps |= + * GEOD_AREA. + * @return \e a12 arc length from point 1 to point 2 (degrees). + * + * \e l must have been initialized with a call to geod_lineinit() with \e + * caps |= GEOD_DISTANCE_IN. The value \e azi2 returned is in the range + * [−180°, 180°]. Any of the "return" arguments \e plat2, + * etc., may be replaced by 0, if you do not need some quantities + * computed. Requesting a value which \e l is not capable of computing + * is not an error; the corresponding argument will not be altered. + * + * With \e flags & GEOD_LONG_UNROLL bit set, the longitude is "unrolled" so + * that the quantity \e lon2 − \e lon1 indicates how many times and in + * what sense the geodesic encircles the ellipsoid. + * + * Example, compute way points between JFK and Singapore Changi Airport using + * geod_genposition(). In this example, the points are evenly space in arc + * length (and so only approximately equally spaced in distance). This is + * faster than using geod_position() and would be appropriate if drawing the + * path on a map. + @code{.c} + struct geod_geodesic g; + struct geod_geodesicline l; + double lat[101], lon[101]; + int i; + geod_init(&g, 6378137, 1/298.257223563); + geod_inverseline(&l, &g, 40.64, -73.78, 1.36, 103.99, + GEOD_LATITUDE | GEOD_LONGITUDE); + for (i = 0; i <= 100; ++i) { + geod_genposition(&l, GEOD_ARCMODE, i * l.a13 * 0.01, + lat + i, lon + i, 0, 0, 0, 0, 0, 0); + printf("%.5f %.5f\n", lat[i], lon[i]); + } + @endcode + **********************************************************************/ + double GEOD_DLL geod_genposition(const struct geod_geodesicline* l, + unsigned flags, double s12_a12, + double* plat2, double* plon2, double* pazi2, + double* ps12, double* pm12, + double* pM12, double* pM21, + double* pS12); + + /** + * Specify position of point 3 in terms of distance. + * + * @param[in,out] l a pointer to the geod_geodesicline object. + * @param[in] s13 the distance from point 1 to point 3 (meters); it + * can be negative. + * + * This is only useful if the geod_geodesicline object has been constructed + * with \e caps |= GEOD_DISTANCE_IN. + **********************************************************************/ + void GEOD_DLL geod_setdistance(struct geod_geodesicline* l, double s13); + + /** + * Specify position of point 3 in terms of either distance or arc length. + * + * @param[in,out] l a pointer to the geod_geodesicline object. + * @param[in] flags either GEOD_NOFLAGS or GEOD_ARCMODE to determining the + * meaning of the \e s13_a13. + * @param[in] s13_a13 if \e flags = GEOD_NOFLAGS, this is the distance + * from point 1 to point 3 (meters); if \e flags = GEOD_ARCMODE, it is + * the arc length from point 1 to point 3 (degrees); it can be + * negative. + * + * If flags = GEOD_NOFLAGS, this calls geod_setdistance(). If flags = + * GEOD_ARCMODE, the \e s13 is only set if the geod_geodesicline object has + * been constructed with \e caps |= GEOD_DISTANCE. + **********************************************************************/ + void GEOD_DLL geod_gensetdistance(struct geod_geodesicline* l, + unsigned flags, double s13_a13); + + /** + * Initialize a geod_polygon object. + * + * @param[out] p a pointer to the object to be initialized. + * @param[in] polylinep non-zero if a polyline instead of a polygon. + * + * If \e polylinep is zero, then the sequence of vertices and edges added by + * geod_polygon_addpoint() and geod_polygon_addedge() define a polygon and + * the perimeter and area are returned by geod_polygon_compute(). If \e + * polylinep is non-zero, then the vertices and edges define a polyline and + * only the perimeter is returned by geod_polygon_compute(). + * + * The area and perimeter are accumulated at two times the standard floating + * point precision to guard against the loss of accuracy with many-sided + * polygons. At any point you can ask for the perimeter and area so far. + * + * An example of the use of this function is given in the documentation for + * geod_polygon_compute(). + **********************************************************************/ + void GEOD_DLL geod_polygon_init(struct geod_polygon* p, int polylinep); + + /** + * Clear the polygon, allowing a new polygon to be started. + * + * @param[in,out] p a pointer to the object to be cleared. + **********************************************************************/ + void GEOD_DLL geod_polygon_clear(struct geod_polygon* p); + + /** + * Add a point to the polygon or polyline. + * + * @param[in] g a pointer to the geod_geodesic object specifying the + * ellipsoid. + * @param[in,out] p a pointer to the geod_polygon object specifying the + * polygon. + * @param[in] lat the latitude of the point (degrees). + * @param[in] lon the longitude of the point (degrees). + * + * \e g and \e p must have been initialized with calls to geod_init() and + * geod_polygon_init(), respectively. The same \e g must be used for all the + * points and edges in a polygon. \e lat should be in the range + * [−90°, 90°]. + * + * An example of the use of this function is given in the documentation for + * geod_polygon_compute(). + **********************************************************************/ + void GEOD_DLL geod_polygon_addpoint(const struct geod_geodesic* g, + struct geod_polygon* p, + double lat, double lon); + + /** + * Add an edge to the polygon or polyline. + * + * @param[in] g a pointer to the geod_geodesic object specifying the + * ellipsoid. + * @param[in,out] p a pointer to the geod_polygon object specifying the + * polygon. + * @param[in] azi azimuth at current point (degrees). + * @param[in] s distance from current point to next point (meters). + * + * \e g and \e p must have been initialized with calls to geod_init() and + * geod_polygon_init(), respectively. The same \e g must be used for all the + * points and edges in a polygon. This does nothing if no points have been + * added yet. The \e lat and \e lon fields of \e p give the location of the + * new vertex. + **********************************************************************/ + void GEOD_DLL geod_polygon_addedge(const struct geod_geodesic* g, + struct geod_polygon* p, + double azi, double s); + + /** + * Return the results for a polygon. + * + * @param[in] g a pointer to the geod_geodesic object specifying the + * ellipsoid. + * @param[in] p a pointer to the geod_polygon object specifying the polygon. + * @param[in] reverse if non-zero then clockwise (instead of + * counter-clockwise) traversal counts as a positive area. + * @param[in] sign if non-zero then return a signed result for the area if + * the polygon is traversed in the "wrong" direction instead of returning + * the area for the rest of the earth. + * @param[out] pA pointer to the area of the polygon (meters2); + * only set if \e polyline is non-zero in the call to geod_polygon_init(). + * @param[out] pP pointer to the perimeter of the polygon or length of the + * polyline (meters). + * @return the number of points. + * + * The area and perimeter are accumulated at two times the standard floating + * point precision to guard against the loss of accuracy with many-sided + * polygons. Arbitrarily complex polygons are allowed. In the case of + * self-intersecting polygons the area is accumulated "algebraically", e.g., + * the areas of the 2 loops in a figure-8 polygon will partially cancel. + * There's no need to "close" the polygon by repeating the first vertex. Set + * \e pA or \e pP to zero, if you do not want the corresponding quantity + * returned. + * + * More points can be added to the polygon after this call. + * + * Example, compute the perimeter and area of the geodesic triangle with + * vertices (0°N,0°E), (0°N,90°E), (90°N,0°E). + @code{.c} + double A, P; + int n; + struct geod_geodesic g; + struct geod_polygon p; + geod_init(&g, 6378137, 1/298.257223563); + geod_polygon_init(&p, 0); + + geod_polygon_addpoint(&g, &p, 0, 0); + geod_polygon_addpoint(&g, &p, 0, 90); + geod_polygon_addpoint(&g, &p, 90, 0); + n = geod_polygon_compute(&g, &p, 0, 1, &A, &P); + printf("%d %.8f %.3f\n", n, P, A); + @endcode + **********************************************************************/ + unsigned GEOD_DLL geod_polygon_compute(const struct geod_geodesic* g, + const struct geod_polygon* p, + int reverse, int sign, + double* pA, double* pP); + + /** + * Return the results assuming a tentative final test point is added; + * however, the data for the test point is not saved. This lets you report a + * running result for the perimeter and area as the user moves the mouse + * cursor. Ordinary floating point arithmetic is used to accumulate the data + * for the test point; thus the area and perimeter returned are less accurate + * than if geod_polygon_addpoint() and geod_polygon_compute() are used. + * + * @param[in] g a pointer to the geod_geodesic object specifying the + * ellipsoid. + * @param[in] p a pointer to the geod_polygon object specifying the polygon. + * @param[in] lat the latitude of the test point (degrees). + * @param[in] lon the longitude of the test point (degrees). + * @param[in] reverse if non-zero then clockwise (instead of + * counter-clockwise) traversal counts as a positive area. + * @param[in] sign if non-zero then return a signed result for the area if + * the polygon is traversed in the "wrong" direction instead of returning + * the area for the rest of the earth. + * @param[out] pA pointer to the area of the polygon (meters2); + * only set if \e polyline is non-zero in the call to geod_polygon_init(). + * @param[out] pP pointer to the perimeter of the polygon or length of the + * polyline (meters). + * @return the number of points. + * + * \e lat should be in the range [−90°, 90°]. + **********************************************************************/ + unsigned GEOD_DLL geod_polygon_testpoint(const struct geod_geodesic* g, + const struct geod_polygon* p, + double lat, double lon, + int reverse, int sign, + double* pA, double* pP); + + /** + * Return the results assuming a tentative final test point is added via an + * azimuth and distance; however, the data for the test point is not saved. + * This lets you report a running result for the perimeter and area as the + * user moves the mouse cursor. Ordinary floating point arithmetic is used + * to accumulate the data for the test point; thus the area and perimeter + * returned are less accurate than if geod_polygon_addedge() and + * geod_polygon_compute() are used. + * + * @param[in] g a pointer to the geod_geodesic object specifying the + * ellipsoid. + * @param[in] p a pointer to the geod_polygon object specifying the polygon. + * @param[in] azi azimuth at current point (degrees). + * @param[in] s distance from current point to final test point (meters). + * @param[in] reverse if non-zero then clockwise (instead of + * counter-clockwise) traversal counts as a positive area. + * @param[in] sign if non-zero then return a signed result for the area if + * the polygon is traversed in the "wrong" direction instead of returning + * the area for the rest of the earth. + * @param[out] pA pointer to the area of the polygon (meters2); + * only set if \e polyline is non-zero in the call to geod_polygon_init(). + * @param[out] pP pointer to the perimeter of the polygon or length of the + * polyline (meters). + * @return the number of points. + **********************************************************************/ + unsigned GEOD_DLL geod_polygon_testedge(const struct geod_geodesic* g, + const struct geod_polygon* p, + double azi, double s, + int reverse, int sign, + double* pA, double* pP); + + /** + * A simple interface for computing the area of a geodesic polygon. + * + * @param[in] g a pointer to the geod_geodesic object specifying the + * ellipsoid. + * @param[in] lats an array of latitudes of the polygon vertices (degrees). + * @param[in] lons an array of longitudes of the polygon vertices (degrees). + * @param[in] n the number of vertices. + * @param[out] pA pointer to the area of the polygon (meters2). + * @param[out] pP pointer to the perimeter of the polygon (meters). + * + * \e lats should be in the range [−90°, 90°]. + * + * Arbitrarily complex polygons are allowed. In the case self-intersecting + * of polygons the area is accumulated "algebraically", e.g., the areas of + * the 2 loops in a figure-8 polygon will partially cancel. There's no need + * to "close" the polygon by repeating the first vertex. The area returned + * is signed with counter-clockwise traversal being treated as positive. + * + * Example, compute the area of Antarctica: + @code{.c} + double + lats[] = {-72.9, -71.9, -74.9, -74.3, -77.5, -77.4, -71.7, -65.9, -65.7, + -66.6, -66.9, -69.8, -70.0, -71.0, -77.3, -77.9, -74.7}, + lons[] = {-74, -102, -102, -131, -163, 163, 172, 140, 113, + 88, 59, 25, -4, -14, -33, -46, -61}; + struct geod_geodesic g; + double A, P; + geod_init(&g, 6378137, 1/298.257223563); + geod_polygonarea(&g, lats, lons, (sizeof lats) / (sizeof lats[0]), &A, &P); + printf("%.0f %.2f\n", A, P); + @endcode + **********************************************************************/ + void GEOD_DLL geod_polygonarea(const struct geod_geodesic* g, + double lats[], double lons[], int n, + double* pA, double* pP); + + /** + * mask values for the \e caps argument to geod_lineinit(). + **********************************************************************/ + enum geod_mask { + GEOD_NONE = 0U, /**< Calculate nothing */ + GEOD_LATITUDE = 1U<<7 | 0U, /**< Calculate latitude */ + GEOD_LONGITUDE = 1U<<8 | 1U<<3, /**< Calculate longitude */ + GEOD_AZIMUTH = 1U<<9 | 0U, /**< Calculate azimuth */ + GEOD_DISTANCE = 1U<<10 | 1U<<0, /**< Calculate distance */ + GEOD_DISTANCE_IN = 1U<<11 | 1U<<0 | 1U<<1,/**< Allow distance as input */ + GEOD_REDUCEDLENGTH= 1U<<12 | 1U<<0 | 1U<<2,/**< Calculate reduced length */ + GEOD_GEODESICSCALE= 1U<<13 | 1U<<0 | 1U<<2,/**< Calculate geodesic scale */ + GEOD_AREA = 1U<<14 | 1U<<4, /**< Calculate reduced length */ + GEOD_ALL = 0x7F80U| 0x1FU /**< Calculate everything */ + }; + + /** + * flag values for the \e flags argument to geod_gendirect() and + * geod_genposition() + **********************************************************************/ + enum geod_flags { + GEOD_NOFLAGS = 0U, /**< No flags */ + GEOD_ARCMODE = 1U<<0, /**< Position given in terms of arc distance */ + GEOD_LONG_UNROLL = 1U<<15 /**< Unroll the longitude */ + }; + +#if defined(__cplusplus) +} +#endif + +#endif diff --git a/pkg/geo/geographiclib/geographiclib.cc b/pkg/geo/geographiclib/geographiclib.cc new file mode 100644 index 0000000..140c33e --- /dev/null +++ b/pkg/geo/geographiclib/geographiclib.cc @@ -0,0 +1,22 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +#include "geodesic.h" +#include "geographiclib.h" + +void CR_GEOGRAPHICLIB_InverseBatch( + struct geod_geodesic* spheroid, + double lats[], + double lngs[], + int len, + double *result +) { + *result = 0; + for (int i = 0; i < len - 1; i++) { + double s12, az1, az2; + geod_inverse(spheroid, lats[i], lngs[i], lats[i+1], lngs[i+1], &s12, &az1, &az2); + *result += s12; + } +} diff --git a/pkg/geo/geographiclib/geographiclib.go b/pkg/geo/geographiclib/geographiclib.go new file mode 100644 index 0000000..f5c250e --- /dev/null +++ b/pkg/geo/geographiclib/geographiclib.go @@ -0,0 +1,153 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +// Package geographiclib is a wrapper around the GeographicLib library. +package geographiclib + +// #cgo CXXFLAGS: -std=c++14 +// #cgo LDFLAGS: -lm +// +// #include "geodesic.h" +// #include "geographiclib.h" +import "C" + +import ( + "math" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geoprojbase" + "github.com/golang/geo/s1" + "github.com/golang/geo/s2" +) + +func init() { + geoprojbase.MakeSpheroid = func(radius, flattening float64) (geoprojbase.Spheroid, error) { + return NewSpheroid(radius, flattening), nil + } +} + +var ( + // WGS84Spheroid represents the default WGS84 ellipsoid. + WGS84Spheroid = NewSpheroid(6378137, 1/298.257223563) +) + +// Spheroid is an object that can perform geodesic operations +// on a given spheroid. +type Spheroid struct { + cRepr C.struct_geod_geodesic + radius float64 + flattening float64 + sphereRadius float64 +} + +// NewSpheroid creates a spheroid from a radius and flattening. +func NewSpheroid(radius float64, flattening float64) *Spheroid { + minorAxis := radius - radius*flattening + s := &Spheroid{ + radius: radius, + flattening: flattening, + sphereRadius: (radius*2 + minorAxis) / 3, + } + C.geod_init(&s.cRepr, C.double(radius), C.double(flattening)) + return s +} + +// Radius returns the radius of the spheroid. +func (s *Spheroid) Radius() float64 { + return s.radius +} + +// Flattening returns the flattening factor of the spheroid. +func (s *Spheroid) Flattening() float64 { + return s.flattening +} + +// SphereRadius returns the radius of a sphere that fits inside the spheroid. +func (s *Spheroid) SphereRadius() float64 { + return s.sphereRadius +} + +// Inverse solves the geodetic inverse problem on the given spheroid +// (https://en.wikipedia.org/wiki/Geodesy#Geodetic_problems). +// Returns s12 (distance in meters), az1 (azimuth at point 1) and az2 (azimuth at point 2). +func (s *Spheroid) Inverse(a, b s2.LatLng) (s12, az1, az2 float64) { + var retS12, retAZ1, retAZ2 C.double + C.geod_inverse( + &s.cRepr, + C.double(a.Lat.Degrees()), + C.double(a.Lng.Degrees()), + C.double(b.Lat.Degrees()), + C.double(b.Lng.Degrees()), + &retS12, + &retAZ1, + &retAZ2, + ) + return float64(retS12), float64(retAZ1), float64(retAZ2) +} + +// InverseBatch computes the sum of the length of the lines represented +// by the line of points. +// This is intended for use for LineStrings. LinearRings/Polygons should use "AreaAndPerimeter". +// Returns the sum of the s12 (distance in meters) units. +func (s *Spheroid) InverseBatch(points []s2.Point) float64 { + lats := make([]C.double, len(points)) + lngs := make([]C.double, len(points)) + for i, p := range points { + latlng := s2.LatLngFromPoint(p) + lats[i] = C.double(latlng.Lat.Degrees()) + lngs[i] = C.double(latlng.Lng.Degrees()) + } + var result C.double + C.CR_GEOGRAPHICLIB_InverseBatch( + &s.cRepr, + &lats[0], + &lngs[0], + C.int(len(points)), + &result, + ) + return float64(result) +} + +// AreaAndPerimeter computes the area and perimeter of a polygon on a given spheroid. +// The points must never be duplicated (i.e. do not include the "final" point of a Polygon LinearRing). +// Area is in meter^2, Perimeter is in meters. +func (s *Spheroid) AreaAndPerimeter(points []s2.Point) (area float64, perimeter float64) { + lats := make([]C.double, len(points)) + lngs := make([]C.double, len(points)) + for i, p := range points { + latlng := s2.LatLngFromPoint(p) + lats[i] = C.double(latlng.Lat.Degrees()) + lngs[i] = C.double(latlng.Lng.Degrees()) + } + var areaDouble, perimeterDouble C.double + C.geod_polygonarea( + &s.cRepr, + &lats[0], + &lngs[0], + C.int(len(points)), + &areaDouble, + &perimeterDouble, + ) + return float64(areaDouble), float64(perimeterDouble) +} + +// Project returns computes the location of the projected point. +// +// Using the direct geodesic problem from GeographicLib (Karney 2013). +func (s *Spheroid) Project(point s2.LatLng, distance float64, azimuth s1.Angle) s2.LatLng { + var lat, lng C.double + + C.geod_direct( + &s.cRepr, + C.double(point.Lat.Degrees()), + C.double(point.Lng.Degrees()), + C.double(azimuth*180.0/math.Pi), + C.double(distance), + &lat, + &lng, + nil, + ) + + return s2.LatLngFromDegrees(float64(lat), float64(lng)) +} diff --git a/pkg/geo/geographiclib/geographiclib.h b/pkg/geo/geographiclib/geographiclib.h new file mode 100644 index 0000000..6fbe660 --- /dev/null +++ b/pkg/geo/geographiclib/geographiclib.h @@ -0,0 +1,25 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +#include "geodesic.h" + +#if defined(__cplusplus) +extern "C" { +#endif + +// CR_GEOGRAPHICLIB_InverseBatch computes the sum of the length of the lines +// represented by an array of lat/lngs using Inverse from GeographicLib. +// It is batched in C++ to reduce the cgo overheads. +void CR_GEOGRAPHICLIB_InverseBatch( + struct geod_geodesic* spheroid, + double lats[], + double lngs[], + int len, + double *result +); + +#if defined(__cplusplus) +} +#endif diff --git a/pkg/geo/geoindex/geoindex.go b/pkg/geo/geoindex/geoindex.go new file mode 100644 index 0000000..f9350cc --- /dev/null +++ b/pkg/geo/geoindex/geoindex.go @@ -0,0 +1,714 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geoindex + +import ( + "context" + "fmt" + "math" + "sort" + "strings" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geogfn" + // Blank import so projections are initialized correctly. + _ "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geographiclib" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb" + "github.com/golang/geo/s2" + "github.com/twpayne/go-geom" +) + +// RelationshipMap contains all the geospatial functions that can be index- +// accelerated. Each function implies a certain type of geospatial relationship, +// which affects how the index is queried as part of a constrained scan or +// geospatial lookup join. RelationshipMap maps the function name to its +// corresponding relationship (Covers, CoveredBy, DFullyWithin, DWithin or Intersects). +// +// Note that for all of these functions, a geospatial lookup join or constrained +// index scan may produce false positives. Therefore, the original function must +// be called on the output of the index operation to filter the results. +var RelationshipMap = map[string]RelationshipType{ + "st_covers": Covers, + "st_coveredby": CoveredBy, + "st_contains": Covers, + "st_containsproperly": Covers, + "st_crosses": Intersects, + "st_dwithin": DWithin, + "st_dfullywithin": DFullyWithin, + "st_equals": Intersects, + "st_intersects": Intersects, + "st_overlaps": Intersects, + "st_touches": Intersects, + "st_within": CoveredBy, + "st_dwithinexclusive": DWithin, + "st_dfullywithinexclusive": DFullyWithin, +} + +// RelationshipReverseMap contains a default function for each of the +// possible geospatial relationships. +var RelationshipReverseMap = map[RelationshipType]string{ + Covers: "st_covers", + CoveredBy: "st_coveredby", + DWithin: "st_dwithin", + DFullyWithin: "st_dfullywithin", + Intersects: "st_intersects", +} + +// CommuteRelationshipMap is used to determine how the geospatial +// relationship changes if the arguments to the index-accelerated function are +// commuted. +// +// The relationships in the RelationshipMap map above only apply when the +// second argument to the function is the indexed column. If the arguments are +// commuted so that the first argument is the indexed column, the relationship +// may change. +var CommuteRelationshipMap = map[RelationshipType]RelationshipType{ + Covers: CoveredBy, + CoveredBy: Covers, + DWithin: DWithin, + DFullyWithin: DFullyWithin, + Intersects: Intersects, +} + +// Interfaces for accelerating certain spatial operations, by allowing the +// caller to use an externally stored index. +// +// An index interface has methods that specify what to write or read from the +// stored index. It is the caller's responsibility to do the actual writes and +// reads. Index keys are represented using uint64 which implies that the index +// is transforming the 2D space (spherical or planar) to 1D space, say using a +// space-filling curve. The index can return false positives so query +// execution must use an exact filtering step after using the index. The +// current implementations use the S2 geometry library. +// +// The externally stored index must support key-value semantics and range +// queries over keys. The return values from Covers/CoveredBy/Intersects are +// specialized to the computation that needs to be performed: +// - Intersects needs to union (a) all the ranges corresponding to subtrees +// rooted at the covering of g, (b) all the parent nodes of the covering +// of g. The individual entries in (b) are representable as ranges of +// length 1. All of this is represented as UnionKeySpans. Covers, which +// is the shape that g covers, currently delegates to Intersects so +// returns the same. +// +// - CoveredBy, which are the shapes that g is covered-by, needs to compute +// on the paths from each covering node to the root. For example, consider +// a quad-tree approach to dividing the space, where the nodes/cells +// covering g are c53, c61, c64. The corresponding ancestor path sets are +// {c53, c13, c3, c0}, {c61, c15, c3, c0}, {c64, c15, c3, c0}. Let I(c53) +// represent the index entries for c53. Then, +// I(c53) \union I(c13) \union I(c3) \union I(c0) +// represents all the shapes that cover cell c53. Similar union expressions +// can be constructed for all these paths. The computation needs to +// intersect these unions since all these cells need to be covered by a +// shape that covers g. One can extract the common sub-expressions to give +// I(c0) \union I(c3) \union +// ((I(c13) \union I(c53)) \intersection +// (I(c15) \union (I(c61) \intersection I(c64))) +// CoveredBy returns this factored expression in Reverse Polish notation. + +// GeographyIndex is an index over the unit sphere. +type GeographyIndex interface { + // InvertedIndexKeys returns the keys to store this object under when adding + // it to the index. Additionally returns a bounding box, which is non-empty + // iff the key slice is non-empty. + InvertedIndexKeys(c context.Context, g geo.Geography) ([]Key, geopb.BoundingBox, error) + + // Acceleration for topological relationships (see + // https://postgis.net/docs/reference.html#Spatial_Relationships). Distance + // relationships can be accelerated by adjusting g before calling these + // functions. Bounding box operators are not accelerated since we do not + // index the bounding box -- bounding box queries are an implementation + // detail of a particular indexing approach in PostGIS and are not part of + // the OGC or SQL/MM specs. + + // Covers returns the index spans to read and union for the relationship + // ST_Covers(g, x), where x are the indexed geometries. + Covers(c context.Context, g geo.Geography) (UnionKeySpans, error) + + // CoveredBy returns the index entries to read and the expression to compute + // for ST_CoveredBy(g, x), where x are the indexed geometries. + CoveredBy(c context.Context, g geo.Geography) (RPKeyExpr, error) + + // Intersects returns the index spans to read and union for the relationship + // ST_Intersects(g, x), where x are the indexed geometries. + Intersects(c context.Context, g geo.Geography) (UnionKeySpans, error) + + // DWithin returns the index spans to read and union for the relationship + // ST_DWithin(g, x, distanceMeters). That is, there exists a part of + // geometry g that is within distanceMeters of x, where x is an indexed + // geometry. This function assumes a sphere. + DWithin( + c context.Context, g geo.Geography, distanceMeters float64, + useSphereOrSpheroid geogfn.UseSphereOrSpheroid, + ) (UnionKeySpans, error) + + // TestingInnerCovering returns an inner covering of g. + TestingInnerCovering(g geo.Geography) s2.CellUnion + // CoveringGeography returns a Geography which represents the covering of g + // using the index configuration. + CoveringGeography(c context.Context, g geo.Geography) (geo.Geography, error) +} + +// GeometryIndex is an index over 2D cartesian coordinates. +type GeometryIndex interface { + // InvertedIndexKeys returns the keys to store this object under when adding + // it to the index. Additionally returns a bounding box, which is non-empty + // iff the key slice is non-empty. + InvertedIndexKeys(c context.Context, g geo.Geometry) ([]Key, geopb.BoundingBox, error) + + // Acceleration for topological relationships (see + // https://postgis.net/docs/reference.html#Spatial_Relationships). Distance + // relationships can be accelerated by adjusting g before calling these + // functions. Bounding box operators are not accelerated since we do not + // index the bounding box -- bounding box queries are an implementation + // detail of a particular indexing approach in PostGIS and are not part of + // the OGC or SQL/MM specs. + + // Covers returns the index spans to read and union for the relationship + // ST_Covers(g, x), where x are the indexed geometries. + Covers(c context.Context, g geo.Geometry) (UnionKeySpans, error) + + // CoveredBy returns the index entries to read and the expression to compute + // for ST_CoveredBy(g, x), where x are the indexed geometries. + CoveredBy(c context.Context, g geo.Geometry) (RPKeyExpr, error) + + // Intersects returns the index spans to read and union for the relationship + // ST_Intersects(g, x), where x are the indexed geometries. + Intersects(c context.Context, g geo.Geometry) (UnionKeySpans, error) + + // DWithin returns the index spans to read and union for the relationship + // ST_DWithin(g, x, distance). That is, there exists a part of geometry g + // that is within distance units of x, where x is an indexed geometry. + DWithin(c context.Context, g geo.Geometry, distance float64) (UnionKeySpans, error) + + // DFullyWithin returns the index spans to read and union for the + // relationship ST_DFullyWithin(g, x, distance). That is, the maximum distance + // across every pair of points comprising geometries g and x is within distance + // units, where x is an indexed geometry. + DFullyWithin(c context.Context, g geo.Geometry, distance float64) (UnionKeySpans, error) + + // TestingInnerCovering returns an inner covering of g. + TestingInnerCovering(g geo.Geometry) s2.CellUnion + // CoveringGeometry returns a Geometry which represents the covering of g + // using the index configuration. + CoveringGeometry(c context.Context, g geo.Geometry) (geo.Geometry, error) +} + +// RelationshipType stores a type of geospatial relationship query that can +// be accelerated using an index. +type RelationshipType uint8 + +const ( + // Covers corresponds to the relationship in which one geospatial object + // covers another geospatial object. + Covers RelationshipType = (1 << iota) + + // CoveredBy corresponds to the relationship in which one geospatial object + // is covered by another geospatial object. + CoveredBy + + // Intersects corresponds to the relationship in which one geospatial object + // intersects another geospatial object. + Intersects + + // DWithin corresponds to a relationship where there exists a part of one + // geometry within d distance units of the other geometry. + DWithin + + // DFullyWithin corresponds to a relationship where every pair of points in + // two geometries are within d distance units. + DFullyWithin +) + +var geoRelationshipTypeStr = map[RelationshipType]string{ + Covers: "covers", + CoveredBy: "covered by", + Intersects: "intersects", + DWithin: "dwithin", + DFullyWithin: "dfullywithin", +} + +func (gr RelationshipType) String() string { + return geoRelationshipTypeStr[gr] +} + +// Key is one entry under which a geospatial shape is stored on behalf of an +// Index. The index is of the form (Key, Primary Key). +type Key uint64 + +// rpExprElement implements the RPExprElement interface. +func (k Key) rpExprElement() {} + +func (k Key) String() string { + c := s2.CellID(k) + if !c.IsValid() { + return "spilled" + } + var b strings.Builder + b.WriteByte('F') + b.WriteByte("012345"[c.Face()]) + fmt.Fprintf(&b, "/L%d/", c.Level()) + for level := 1; level <= c.Level(); level++ { + b.WriteByte("0123"[c.ChildPosition(level)]) + } + return b.String() +} + +// S2CellID transforms the given key into the S2 CellID. +func (k Key) S2CellID() s2.CellID { + return s2.CellID(k) +} + +// KeySpan represents a range of Keys. +type KeySpan struct { + // Both Start and End are inclusive, i.e., [Start, End]. + Start, End Key +} + +// UnionKeySpans is the set of indexed spans to retrieve and combine via set +// union. The spans are guaranteed to be non-overlapping and sorted in +// increasing order. Duplicate primary keys will not be retrieved by any +// individual key, but they may be present if more than one key is retrieved +// (including duplicates in a single span where End - Start > 1). +type UnionKeySpans []KeySpan + +func (s UnionKeySpans) String() string { + return s.toString(math.MaxInt32) +} + +func (s UnionKeySpans) toString(wrap int) string { + b := newStringBuilderWithWrap(&strings.Builder{}, wrap) + for i, span := range s { + if span.Start == span.End { + fmt.Fprintf(b, "%s", span.Start) + } else { + fmt.Fprintf(b, "[%s, %s]", span.Start, span.End) + } + if i != len(s)-1 { + b.WriteString(", ") + } + b.tryWrap() + } + return b.String() +} + +// RPExprElement is an element in the Reverse Polish notation expression. +// It is implemented by Key and RPSetOperator. +type RPExprElement interface { + rpExprElement() +} + +// RPSetOperator is a set operator in the Reverse Polish notation expression. +type RPSetOperator int + +const ( + // RPSetUnion is the union operator. + RPSetUnion RPSetOperator = iota + 1 + + // RPSetIntersection is the intersection operator. + RPSetIntersection +) + +// rpExprElement implements the RPExprElement interface. +func (o RPSetOperator) rpExprElement() {} + +// RPKeyExpr is an expression to evaluate over primary keys retrieved for +// index keys. If we view each index key as a posting list of primary keys, +// the expression involves union and intersection over the sets represented by +// each posting list. For S2, this expression represents an intersection of +// ancestors of different keys (cell ids) and is likely to contain many common +// keys. This special structure allows us to efficiently and easily eliminate +// common sub-expressions, hence the interface presents the factored +// expression. The expression is represented in Reverse Polish notation. +type RPKeyExpr []RPExprElement + +func (x RPKeyExpr) String() string { + var elements []string + for _, e := range x { + switch elem := e.(type) { + case Key: + elements = append(elements, elem.String()) + case RPSetOperator: + switch elem { + case RPSetUnion: + elements = append(elements, `\U`) + case RPSetIntersection: + elements = append(elements, `\I`) + } + } + } + return strings.Join(elements, " ") +} + +// Helper functions for index implementations that use the S2 geometry +// library. + +// covererInterface provides a covering for a set of regions. +type covererInterface interface { + // covering returns a normalized CellUnion, i.e., it is sorted, and does not + // contain redundancy. + covering(regions []s2.Region) s2.CellUnion +} + +// simpleCovererImpl is an implementation of covererInterface that delegates +// to s2.RegionCoverer. +type simpleCovererImpl struct { + rc *s2.RegionCoverer +} + +var _ covererInterface = simpleCovererImpl{} + +func (rc simpleCovererImpl) covering(regions []s2.Region) s2.CellUnion { + // TODO(sumeer): Add a max cells constraint for the whole covering, + // to respect the index configuration. + var u s2.CellUnion + for _, r := range regions { + u = append(u, rc.rc.Covering(r)...) + } + // Ensure the cells are non-overlapping. + u.Normalize() + return u +} + +// The "inner covering", for shape s, represented by the regions parameter, is +// used to find shapes that contain shape s. A regular covering that includes +// a cell c not completely covered by shape s could result in false negatives, +// since shape x that covers shape s could use a finer cell covering (using +// cells below c). For example, consider a portion of the cell quad-tree +// below: +// +// c0 +// | +// c3 +// | +// +---+---+ +// | | +// c13 c15 +// | | +// c53 +--+--+ +// | | +// c61 c64 +// +// Shape s could have a regular covering c15, c53, where c15 has 4 child cells +// c61..c64, and shape s only intersects wit c61, c64. A different shape x +// that covers shape s may have a covering c61, c64, c53. That is, it has used +// the finer cells c61, c64. If we used both regular coverings it is hard to +// know that x covers g. Hence, we compute the "inner covering" of g (defined +// below). +// +// The interior covering of shape s includes only cells covered by s. This is +// computed by RegionCoverer.InteriorCovering() and is intuitively what we +// need. But the interior covering is naturally empty for points and lines +// (and can be empty for polygons too), and an empty covering is not useful +// for constraining index lookups. We observe that leaf cells that intersect +// shape s can be used in the covering, since the covering of shape x must +// also cover these cells. This allows us to compute non-empty coverings for +// all shapes. Since this is not technically the interior covering, we use the +// term "inner covering". +func innerCovering(rc *s2.RegionCoverer, regions []s2.Region) s2.CellUnion { + var u s2.CellUnion + for _, r := range regions { + switch region := r.(type) { + case s2.Point: + cellID := cellIDCoveringPoint(region, rc.MaxLevel) + u = append(u, cellID) + case *s2.Polyline: + // TODO(sumeer): for long lines could also pick some intermediate + // points along the line. Decide based on experiments. + for _, p := range *region { + cellID := cellIDCoveringPoint(p, rc.MaxLevel) + u = append(u, cellID) + } + case *s2.Polygon: + // Iterate over all exterior points + if region.NumLoops() > 0 { + loop := region.Loop(0) + for _, p := range loop.Vertices() { + cellID := cellIDCoveringPoint(p, rc.MaxLevel) + u = append(u, cellID) + } + // Arbitrary threshold value. This is to avoid computing an expensive + // region covering for regions with small area. + // TODO(sumeer): Improve this heuristic: + // - Area() may be expensive. + // - For large area regions, put an upper bound on the + // level used for cells. + // Decide based on experiments. + const smallPolygonLevelThreshold = 25 + if region.Area() > s2.AvgAreaMetric.Value(smallPolygonLevelThreshold) { + u = append(u, rc.InteriorCovering(region)...) + } + } + default: + panic("bug: code should not be producing unhandled Region type") + } + } + // Ensure the cells are non-overlapping. + u.Normalize() + + // TODO(sumeer): if the number of cells is too many, make the list sparser. + // u[len(u)-1] - u[0] / len(u) is the mean distance between cells. Pick a + // target distance based on the goal to reduce to k cells: target_distance + // := mean_distance * k / len(u) Then iterate over u and for every sequence + // of cells that are within target_distance, replace by median cell or by + // largest cell. Decide based on experiments. + + return u +} + +func cellIDCoveringPoint(point s2.Point, level int) s2.CellID { + cellID := s2.CellFromPoint(point).ID() + if !cellID.IsLeaf() { + panic("bug in S2") + } + return cellID.Parent(level) +} + +// ancestorCells returns the set of cells containing these cells, not +// including the given cells. +// +// TODO(sumeer): use the MinLevel and LevelMod of the RegionCoverer used +// for the index to constrain the ancestors set. +func ancestorCells(cells []s2.CellID) []s2.CellID { + var ancestors []s2.CellID + var seen map[s2.CellID]struct{} + if len(cells) > 1 { + seen = make(map[s2.CellID]struct{}) + } + for _, c := range cells { + for l := c.Level() - 1; l >= 0; l-- { + p := c.Parent(l) + if seen != nil { + if _, ok := seen[p]; ok { + break + } + seen[p] = struct{}{} + } + ancestors = append(ancestors, p) + } + } + return ancestors +} + +// Helper for InvertedIndexKeys. +func invertedIndexKeys(_ context.Context, rc covererInterface, r []s2.Region) []Key { + covering := rc.covering(r) + keys := make([]Key, len(covering)) + for i, cid := range covering { + keys[i] = Key(cid) + } + return keys +} + +// TODO(sumeer): examine RegionCoverer carefully to see if we can strengthen +// the covering invariant, which would increase the efficiency of covers() and +// remove the need for TestingInnerCovering(). +// +// Helper for Covers. +func covers(c context.Context, rc covererInterface, r []s2.Region) UnionKeySpans { + // We use intersects since geometries covered by r may have been indexed + // using cells that are ancestors of the covering of r. We could avoid + // reading ancestors if we had a stronger covering invariant, such as by + // indexing inner coverings. + return intersects(c, rc, r) +} + +// Helper for Intersects. Returns spans in sorted order for convenience of +// scans. +func intersects(_ context.Context, rc covererInterface, r []s2.Region) UnionKeySpans { + covering := rc.covering(r) + return intersectsUsingCovering(covering) +} + +func intersectsUsingCovering(covering s2.CellUnion) UnionKeySpans { + querySpans := make([]KeySpan, len(covering)) + for i, cid := range covering { + querySpans[i] = KeySpan{Start: Key(cid.RangeMin()), End: Key(cid.RangeMax())} + } + for _, cid := range ancestorCells(covering) { + querySpans = append(querySpans, KeySpan{Start: Key(cid), End: Key(cid)}) + } + sort.Slice(querySpans, func(i, j int) bool { return querySpans[i].Start < querySpans[j].Start }) + return querySpans +} + +// Helper for CoveredBy. +func coveredBy(_ context.Context, rc *s2.RegionCoverer, r []s2.Region) RPKeyExpr { + covering := innerCovering(rc, r) + ancestors := ancestorCells(covering) + + // The covering is normalized so no 2 cells are such that one is an ancestor + // of another. Arrange these cells and their ancestors in a quad-tree. Any cell + // with more than one child needs to be unioned with the intersection of the + // expressions corresponding to each child. See the detailed comment in + // generateRPExprForTree(). + + // It is sufficient to represent the tree(s) using presentCells since the ids + // of all possible 4 children of a cell can be computed and checked for + // presence in the map. + presentCells := make(map[s2.CellID]struct{}, len(covering)+len(ancestors)) + for _, c := range covering { + presentCells[c] = struct{}{} + } + for _, c := range ancestors { + presentCells[c] = struct{}{} + } + + // Construct the reverse polish expression. Note that there are up to 6 + // trees corresponding to the 6 faces in S2. The expressions for the + // trees need to be intersected with each other. + expr := make([]RPExprElement, 0, len(presentCells)*2) + numFaces := 0 + for face := 0; face < 6; face++ { + rootID := s2.CellIDFromFace(face) + if _, ok := presentCells[rootID]; !ok { + continue + } + expr = generateRPExprForTree(rootID, presentCells, expr) + numFaces++ + if numFaces > 1 { + expr = append(expr, RPSetIntersection) + } + } + return expr +} + +// The quad-trees stored in presentCells together represent a set expression. +// This expression specifies: +// - the path for each leaf to the root of that quad-tree. The index entries +// on each such path represent the shapes that cover that leaf. Hence these +// index entries for a single path need to be unioned to give the shapes +// that cover the leaf. +// - The full expression specifies the shapes that cover all the leaves, so +// the union expressions for the paths must be intersected with each other. +// +// Reusing an example from earlier in this file, say the quad-tree is: +// +// c0 +// | +// c3 +// | +// +---+---+ +// | | +// c13 c15 +// | | +// c53 +--+--+ +// | | +// c61 c64 +// +// This tree represents the following expression (where I(c) are the index +// entries stored at cell c): +// +// (I(c64) \union I(c15) \union I(c3) \union I(c0)) \intersection +// (I(c61) \union I(c15) \union I(c3) \union I(c0)) \intersection +// (I(c53) \union I(c13) \union I(c3) \union I(c0)) +// +// In this example all the union sub-expressions have the same number of terms +// but that does not need to be true. +// +// The above expression can be factored to eliminate repetition of the +// same cell. The factored expression for this example is: +// +// I(c0) \union I(c3) \union +// ((I(c13) \union I(c53)) \intersection +// (I(c15) \union (I(c61) \intersection I(c64))) +// +// This function generates this factored expression represented in reverse +// polish notation. +// +// One can generate the factored expression in reverse polish notation using +// a post-order traversal of this tree: +// Step A. append the expression for the subtree rooted at c3 +// Step B. append c0 and the union operator +// For Step A: +// - append the expression for the subtree rooted at c13 +// - append the expression for the subtree rooted at c15 +// - append the intersection operator +// - append c13 +// - append the union operator +func generateRPExprForTree( + rootID s2.CellID, presentCells map[s2.CellID]struct{}, expr []RPExprElement, +) []RPExprElement { + expr = append(expr, Key(rootID)) + if rootID.IsLeaf() { + return expr + } + numChildren := 0 + for _, childCellID := range rootID.Children() { + if _, ok := presentCells[childCellID]; !ok { + continue + } + expr = generateRPExprForTree(childCellID, presentCells, expr) + numChildren++ + if numChildren > 1 { + expr = append(expr, RPSetIntersection) + } + } + if numChildren > 0 { + expr = append(expr, RPSetUnion) + } + return expr +} + +// stringBuilderWithWrap is a strings.Builder that approximately wraps at a +// certain number of characters. Newline characters should only be +// written using tryWrap and doWrap. +type stringBuilderWithWrap struct { + *strings.Builder + wrap int + lastWrap int +} + +func newStringBuilderWithWrap(b *strings.Builder, wrap int) *stringBuilderWithWrap { + return &stringBuilderWithWrap{ + Builder: b, + wrap: wrap, + lastWrap: b.Len(), + } +} + +func (b *stringBuilderWithWrap) tryWrap() { + if b.Len()-b.lastWrap > b.wrap { + b.doWrap() + } +} + +func (b *stringBuilderWithWrap) doWrap() { + fmt.Fprintln(b) + b.lastWrap = b.Len() +} + +// DefaultS2Config returns the default S2Config to initialize. +func DefaultS2Config() *geopb.S2Config { + return &geopb.S2Config{ + MinLevel: 0, + MaxLevel: 30, + LevelMod: 1, + MaxCells: 4, + } +} + +func makeGeomTFromKeys( + keys []Key, srid geopb.SRID, xyFromS2Point func(s2.Point) (float64, float64), +) (geom.T, error) { + t := geom.NewMultiPolygon(geom.XY).SetSRID(int(srid)) + for _, key := range keys { + cell := s2.CellFromCellID(key.S2CellID()) + flatCoords := make([]float64, 0, 10) + for i := 0; i < 4; i++ { + x, y := xyFromS2Point(cell.Vertex(i)) + flatCoords = append(flatCoords, x, y) + } + // The last point is the same as the first point. + flatCoords = append(flatCoords, flatCoords[0:2]...) + err := t.Push(geom.NewPolygonFlat(geom.XY, flatCoords, []int{10})) + if err != nil { + return nil, err + } + } + return t, nil +} diff --git a/pkg/geo/geoindex/s2_geography_index.go b/pkg/geo/geoindex/s2_geography_index.go new file mode 100644 index 0000000..67f0cce --- /dev/null +++ b/pkg/geo/geoindex/s2_geography_index.go @@ -0,0 +1,226 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geoindex + +import ( + "context" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geogfn" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geoprojbase" + "github.com/cockroachdb/errors" + "github.com/golang/geo/s1" + "github.com/golang/geo/s2" + "github.com/twpayne/go-geom" +) + +// s2GeographyIndex is an implementation of GeographyIndex that uses the S2 geometry +// library. +type s2GeographyIndex struct { + rc *s2.RegionCoverer +} + +var _ GeographyIndex = (*s2GeographyIndex)(nil) + +// NewS2GeographyIndex returns an index with the given configuration. The +// configuration of an index cannot be changed without rewriting the index +// since deletes could miss some index entries. Currently, reads could use a +// different configuration, but that is subject to change if we manage to +// strengthen the covering invariants (see the todo in covers() in index.go). +func NewS2GeographyIndex(cfg geopb.S2GeographyConfig) GeographyIndex { + // TODO(sumeer): Sanity check cfg. + return &s2GeographyIndex{ + rc: &s2.RegionCoverer{ + MinLevel: int(cfg.S2Config.MinLevel), + MaxLevel: int(cfg.S2Config.MaxLevel), + LevelMod: int(cfg.S2Config.LevelMod), + MaxCells: int(cfg.S2Config.MaxCells), + }, + } +} + +// DefaultGeographyIndexConfig returns a default config for a geography index. +func DefaultGeographyIndexConfig() *geopb.Config { + return &geopb.Config{ + S2Geography: &geopb.S2GeographyConfig{S2Config: DefaultS2Config()}, + } +} + +// geogCovererWithBBoxFallback first computes the covering for the provided +// regions (which were computed using g), and if the covering is too broad +// (contains top-level cells from all faces), falls back to using the bounding +// box of g to compute the covering. +type geogCovererWithBBoxFallback struct { + rc *s2.RegionCoverer + g geo.Geography +} + +var _ covererInterface = geogCovererWithBBoxFallback{} + +func toDeg(radians float64) float64 { + return s1.Angle(radians).Degrees() +} + +func (rc geogCovererWithBBoxFallback) covering(regions []s2.Region) s2.CellUnion { + cu := simpleCovererImpl{rc: rc.rc}.covering(regions) + if isBadGeogCovering(cu) { + bbox := rc.g.SpatialObject().BoundingBox + if bbox == nil { + return cu + } + flatCoords := []float64{ + toDeg(bbox.LoX), toDeg(bbox.LoY), toDeg(bbox.HiX), toDeg(bbox.LoY), + toDeg(bbox.HiX), toDeg(bbox.HiY), toDeg(bbox.LoX), toDeg(bbox.HiY), + toDeg(bbox.LoX), toDeg(bbox.LoY)} + bboxT := geom.NewPolygonFlat(geom.XY, flatCoords, []int{len(flatCoords)}) + bboxRegions, err := geo.S2RegionsFromGeomT(bboxT, geo.EmptyBehaviorOmit) + if err != nil { + return cu + } + bboxCU := simpleCovererImpl{rc: rc.rc}.covering(bboxRegions) + if !isBadGeogCovering(bboxCU) { + cu = bboxCU + } + } + return cu +} + +func isBadGeogCovering(cu s2.CellUnion) bool { + const numFaces = 6 + if len(cu) != numFaces { + return false + } + numFaceCells := 0 + for _, c := range cu { + if c.Level() == 0 { + numFaceCells++ + } + } + return numFaces == numFaceCells +} + +// InvertedIndexKeys implements the GeographyIndex interface. +func (i *s2GeographyIndex) InvertedIndexKeys( + c context.Context, g geo.Geography, +) ([]Key, geopb.BoundingBox, error) { + r, err := g.AsS2(geo.EmptyBehaviorOmit) + if err != nil { + return nil, geopb.BoundingBox{}, err + } + rect := g.BoundingRect() + bbox := geopb.BoundingBox{ + LoX: rect.Lng.Lo, + HiX: rect.Lng.Hi, + LoY: rect.Lat.Lo, + HiY: rect.Lat.Hi, + } + return invertedIndexKeys(c, geogCovererWithBBoxFallback{rc: i.rc, g: g}, r), bbox, nil +} + +// Covers implements the GeographyIndex interface. +func (i *s2GeographyIndex) Covers(c context.Context, g geo.Geography) (UnionKeySpans, error) { + r, err := g.AsS2(geo.EmptyBehaviorOmit) + if err != nil { + return nil, err + } + return covers(c, geogCovererWithBBoxFallback{rc: i.rc, g: g}, r), nil +} + +// CoveredBy implements the GeographyIndex interface. +func (i *s2GeographyIndex) CoveredBy(c context.Context, g geo.Geography) (RPKeyExpr, error) { + r, err := g.AsS2(geo.EmptyBehaviorOmit) + if err != nil { + return nil, err + } + return coveredBy(c, i.rc, r), nil +} + +// Intersects implements the GeographyIndex interface. +func (i *s2GeographyIndex) Intersects(c context.Context, g geo.Geography) (UnionKeySpans, error) { + r, err := g.AsS2(geo.EmptyBehaviorOmit) + if err != nil { + return nil, err + } + return intersects(c, geogCovererWithBBoxFallback{rc: i.rc, g: g}, r), nil +} + +func (i *s2GeographyIndex) DWithin( + _ context.Context, + g geo.Geography, + distanceMeters float64, + useSphereOrSpheroid geogfn.UseSphereOrSpheroid, +) (UnionKeySpans, error) { + projInfo, err := geoprojbase.Projection(g.SRID()) + if err != nil { + return nil, err + } + if projInfo.Spheroid == nil { + return nil, errors.Errorf("projection %d does not have spheroid", g.SRID()) + } + r, err := g.AsS2(geo.EmptyBehaviorOmit) + if err != nil { + return nil, err + } + // The following approach of constructing the covering and then expanding by + // an angle is worse than first expanding the original shape and then + // constructing a covering. However the s2 golang library lacks the c++ + // S2ShapeIndexBufferedRegion, whose GetCellUnionBound() method is what we + // desire. + // + // Construct the cell covering for the shape. + gCovering := geogCovererWithBBoxFallback{rc: i.rc, g: g}.covering(r) + // Convert the distanceMeters to an angle, in order to expand the cell covering + // on the sphere by the angle. + multiplier := 1.0 + if useSphereOrSpheroid == geogfn.UseSpheroid { + // We are using a sphere to calculate an angle on a spheroid, so adjust by the + // error. + multiplier += geogfn.SpheroidErrorFraction + } + angle := s1.Angle(multiplier * distanceMeters / projInfo.Spheroid.SphereRadius()) + // maxLevelDiff puts a bound on the number of cells used after the expansion. + // For example, we do not want expanding a large country by 1km to generate too + // many cells. + const maxLevelDiff = 2 + gCovering.ExpandByRadius(angle, maxLevelDiff) + // Finally, make the expanded covering obey the configuration of the index, which + // is used in the RegionCoverer. + var covering s2.CellUnion + for _, c := range gCovering { + if c.Level() > i.rc.MaxLevel { + c = c.Parent(i.rc.MaxLevel) + } + covering = append(covering, c) + } + covering.Normalize() + return intersectsUsingCovering(covering), nil +} + +func (i *s2GeographyIndex) TestingInnerCovering(g geo.Geography) s2.CellUnion { + r, _ := g.AsS2(geo.EmptyBehaviorOmit) + if r == nil { + return nil + } + return innerCovering(i.rc, r) +} + +func (i *s2GeographyIndex) CoveringGeography( + c context.Context, g geo.Geography, +) (geo.Geography, error) { + keys, _, err := i.InvertedIndexKeys(c, g) + if err != nil { + return geo.Geography{}, err + } + t, err := makeGeomTFromKeys(keys, g.SRID(), func(p s2.Point) (float64, float64) { + latlng := s2.LatLngFromPoint(p) + return latlng.Lng.Degrees(), latlng.Lat.Degrees() + }) + if err != nil { + return geo.Geography{}, err + } + return geo.MakeGeographyFromGeomT(t) +} diff --git a/pkg/geo/geoindex/s2_geometry_index.go b/pkg/geo/geoindex/s2_geometry_index.go new file mode 100644 index 0000000..babdce1 --- /dev/null +++ b/pkg/geo/geoindex/s2_geometry_index.go @@ -0,0 +1,519 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geoindex + +import ( + "context" + "math" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geomfn" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geoprojbase" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geos" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/errors" + "github.com/golang/geo/r3" + "github.com/golang/geo/s2" + "github.com/twpayne/go-geom" +) + +// s2GeometryIndex is an implementation of GeometryIndex that uses the S2 geometry +// library. +type s2GeometryIndex struct { + rc *s2.RegionCoverer + minX, maxX, minY, maxY float64 + deltaX, deltaY float64 +} + +var _ GeometryIndex = (*s2GeometryIndex)(nil) + +// We adjust the clipping bounds to be smaller by this fraction, since using +// the endpoints of face 0 in S2 causes coverings to spill out of that face. +const clippingBoundsDelta = 0.01 + +// NewS2GeometryIndex returns an index with the given configuration. All reads and +// writes on this index must use the same config. Writes must use the same +// config to correctly process deletions. Reads must use the same config since +// the bounds affect when a read needs to look at the exceedsBoundsCellID. +func NewS2GeometryIndex(cfg geopb.S2GeometryConfig) GeometryIndex { + // TODO(sumeer): Sanity check cfg. + return &s2GeometryIndex{ + rc: &s2.RegionCoverer{ + MinLevel: int(cfg.S2Config.MinLevel), + MaxLevel: int(cfg.S2Config.MaxLevel), + LevelMod: int(cfg.S2Config.LevelMod), + MaxCells: int(cfg.S2Config.MaxCells), + }, + minX: cfg.MinX, + maxX: cfg.MaxX, + minY: cfg.MinY, + maxY: cfg.MaxY, + deltaX: clippingBoundsDelta * (cfg.MaxX - cfg.MinX), + deltaY: clippingBoundsDelta * (cfg.MaxY - cfg.MinY), + } +} + +// TODO(sumeer): also support index config with parameters specified by CREATE +// INDEX. + +// DefaultGeometryIndexConfig returns a default config for a geometry index. +func DefaultGeometryIndexConfig() *geopb.Config { + return &geopb.Config{ + S2Geometry: &geopb.S2GeometryConfig{ + // Bounding box similar to the circumference of the earth (~2B meters) + MinX: -(1 << 31), + MaxX: (1 << 31) - 1, + MinY: -(1 << 31), + MaxY: (1 << 31) - 1, + S2Config: DefaultS2Config(), + }, + } +} + +// GeometryIndexConfigForSRID returns a geometry index config for srid. +func GeometryIndexConfigForSRID(srid geopb.SRID) (*geopb.Config, error) { + if srid == 0 { + return DefaultGeometryIndexConfig(), nil + } + p, err := geoprojbase.Projection(srid) + if err != nil { + return nil, err + } + b := p.Bounds + minX, maxX, minY, maxY := b.MinX, b.MaxX, b.MinY, b.MaxY + // There are projections where the min and max are equal e.g. 3571. + // We need to have a valid rectangle as the geometry index bounds. + if maxX-minX < 1 { + maxX++ + } + if maxY-minY < 1 { + maxY++ + } + // We are covering shapes using cells that are square. If we have shapes + // that start off as well-behaved wrt square cells, we do not wish to + // distort them significantly. Hence, we equalize MaxX-MinX and MaxY-MinY + // in the index bounds. + diffX := maxX - minX + diffY := maxY - minY + if diffX > diffY { + adjustment := (diffX - diffY) / 2 + minY -= adjustment + maxY += adjustment + } else { + adjustment := (diffY - diffX) / 2 + minX -= adjustment + maxX += adjustment + } + // Expand the bounds by 2x the clippingBoundsDelta, to + // ensure that shapes touching the bounds don't get + // clipped. + boundsExpansion := 2 * clippingBoundsDelta + deltaX := (maxX - minX) * boundsExpansion + deltaY := (maxY - minY) * boundsExpansion + return &geopb.Config{ + S2Geometry: &geopb.S2GeometryConfig{ + MinX: minX - deltaX, + MaxX: maxX + deltaX, + MinY: minY - deltaY, + MaxY: maxY + deltaY, + S2Config: DefaultS2Config()}, + }, nil +} + +// A cell id unused by S2. We use it to index geometries that exceed the +// configured bounds. +const exceedsBoundsCellID = s2.CellID(^uint64(0)) + +// TODO(sumeer): adjust code to handle precision issues with floating point +// arithmetic. + +// geomCovererWithBBoxFallback first computes the covering for the provided +// regions (which were computed using geom), and if the covering is too +// broad (contains faces other than 0), falls back to using the bounding +// box of geom to compute the covering. +type geomCovererWithBBoxFallback struct { + s *s2GeometryIndex + geom geom.T +} + +var _ covererInterface = geomCovererWithBBoxFallback{} + +func (rc geomCovererWithBBoxFallback) covering(regions []s2.Region) s2.CellUnion { + cu := simpleCovererImpl{rc: rc.s.rc}.covering(regions) + if isBadGeomCovering(cu) { + bbox := geo.BoundingBoxFromGeomTGeometryType(rc.geom) + flatCoords := []float64{ + bbox.LoX, bbox.LoY, bbox.HiX, bbox.LoY, bbox.HiX, bbox.HiY, bbox.LoX, bbox.HiY, + bbox.LoX, bbox.LoY} + bboxT := geom.NewPolygonFlat(geom.XY, flatCoords, []int{len(flatCoords)}) + bboxRegions := rc.s.s2RegionsFromPlanarGeomT(bboxT) + bboxCU := simpleCovererImpl{rc: rc.s.rc}.covering(bboxRegions) + if !isBadGeomCovering(bboxCU) { + cu = bboxCU + } + } + return cu +} + +func isBadGeomCovering(cu s2.CellUnion) bool { + for _, c := range cu { + if c.Face() != 0 { + // Good coverings should not see a face other than 0. + return true + } + } + return false +} + +// InvertedIndexKeys implements the GeometryIndex interface. +func (s *s2GeometryIndex) InvertedIndexKeys( + c context.Context, g geo.Geometry, +) ([]Key, geopb.BoundingBox, error) { + // If the geometry exceeds the bounds, we index the clipped geometry in + // addition to the special cell, so that queries for geometries that don't + // exceed the bounds don't need to query the special cell (which would + // become a hotspot in the key space). + gt, clipped, err := s.convertToGeomTAndTryClip(g) + if err != nil { + return nil, geopb.BoundingBox{}, err + } + var keys []Key + if gt != nil { + r := s.s2RegionsFromPlanarGeomT(gt) + keys = invertedIndexKeys(c, geomCovererWithBBoxFallback{s: s, geom: gt}, r) + } + if clipped { + keys = append(keys, Key(exceedsBoundsCellID)) + } + bbox := geopb.BoundingBox{} + bboxRef := g.BoundingBoxRef() + if bboxRef == nil && len(keys) > 0 { + return keys, bbox, errors.AssertionFailedf("non-empty geometry should have bounding box") + } + if bboxRef != nil { + bbox = *bboxRef + } + return keys, bbox, nil +} + +// Covers implements the GeometryIndex interface. +func (s *s2GeometryIndex) Covers(c context.Context, g geo.Geometry) (UnionKeySpans, error) { + return s.Intersects(c, g) +} + +// CoveredBy implements the GeometryIndex interface. +func (s *s2GeometryIndex) CoveredBy(c context.Context, g geo.Geometry) (RPKeyExpr, error) { + // If the geometry exceeds the bounds, we use the clipped geometry to + // restrict the search within the bounds. + gt, clipped, err := s.convertToGeomTAndTryClip(g) + if err != nil { + return nil, err + } + var expr RPKeyExpr + if gt != nil { + r := s.s2RegionsFromPlanarGeomT(gt) + expr = coveredBy(c, s.rc, r) + } + if clipped { + // Intersect with the shapes that exceed the bounds. + expr = append(expr, Key(exceedsBoundsCellID)) + if len(expr) > 1 { + expr = append(expr, RPSetIntersection) + } + } + return expr, nil +} + +// Intersects implements the GeometryIndex interface. +func (s *s2GeometryIndex) Intersects(c context.Context, g geo.Geometry) (UnionKeySpans, error) { + // If the geometry exceeds the bounds, we use the clipped geometry to + // restrict the search within the bounds. + gt, clipped, err := s.convertToGeomTAndTryClip(g) + if err != nil { + return nil, err + } + var spans UnionKeySpans + if gt != nil { + r := s.s2RegionsFromPlanarGeomT(gt) + spans = intersects(c, geomCovererWithBBoxFallback{s: s, geom: gt}, r) + } + if clipped { + // And lookup all shapes that exceed the bounds. The exceedsBoundsCellID is the largest + // possible key, so appending it maintains the sorted order of spans. + spans = append(spans, KeySpan{Start: Key(exceedsBoundsCellID), End: Key(exceedsBoundsCellID)}) + } + return spans, nil +} + +func (s *s2GeometryIndex) DWithin( + c context.Context, g geo.Geometry, distance float64, +) (UnionKeySpans, error) { + // TODO(sumeer): are the default params the correct thing to use here? + g, err := geomfn.Buffer(g, geomfn.MakeDefaultBufferParams(), distance) + if err != nil { + return nil, err + } + return s.Intersects(c, g) +} + +func (s *s2GeometryIndex) DFullyWithin( + c context.Context, g geo.Geometry, distance float64, +) (UnionKeySpans, error) { + // TODO(sumeer): are the default params the correct thing to use here? + g, err := geomfn.Buffer(g, geomfn.MakeDefaultBufferParams(), distance) + if err != nil { + return nil, err + } + return s.Covers(c, g) +} + +// Converts to geom.T and clips to the rectangle bounds of the index. +func (s *s2GeometryIndex) convertToGeomTAndTryClip(g geo.Geometry) (geom.T, bool, error) { + // Anything with a NaN coordinate should be marked as clipped. + if bbox := g.BoundingBoxRef(); bbox != nil && (math.IsNaN(bbox.LoX) || math.IsNaN(bbox.HiX) || + math.IsNaN(bbox.LoY) || math.IsNaN(bbox.HiY)) { + return nil, false, pgerror.Newf( + pgcode.InvalidParameterValue, + "cannot index a geometry with NaN coordinates", + ) + } + gt, err := g.AsGeomT() + if err != nil { + return nil, false, err + } + if gt.Empty() { + return gt, false, nil + } + clipped := false + if s.geomExceedsBounds(gt) { + // TODO(#63126): Replace workaround for bug in geos.ClipByRect. + g, err = geomfn.ForceLayout(g, geom.XY) + if err != nil { + return nil, false, err + } + clipped = true + clippedEWKB, err := + geos.ClipByRect(g.EWKB(), s.minX+s.deltaX, s.minY+s.deltaY, s.maxX-s.deltaX, s.maxY-s.deltaY) + if err != nil { + return nil, false, err + } + gt = nil + if clippedEWKB != nil { + g, err = geo.ParseGeometryFromEWKBUnsafe(clippedEWKB) + if err != nil { + return nil, false, err + } + gt, err = g.AsGeomT() + if err != nil { + return nil, false, err + } + } + } + return gt, clipped, nil +} + +// Returns true if the point represented by (x, y) exceeds the rectangle +// bounds of the index. +func (s *s2GeometryIndex) xyExceedsBounds(x float64, y float64) bool { + if x < (s.minX+s.deltaX) || x > (s.maxX-s.deltaX) { + return true + } + if y < (s.minY+s.deltaY) || y > (s.maxY-s.deltaY) { + return true + } + return false +} + +// Returns true if g exceeds the rectangle bounds of the index. +func (s *s2GeometryIndex) geomExceedsBounds(g geom.T) bool { + switch repr := g.(type) { + case *geom.Point: + return s.xyExceedsBounds(repr.X(), repr.Y()) + case *geom.LineString: + for i := 0; i < repr.NumCoords(); i++ { + p := repr.Coord(i) + if s.xyExceedsBounds(p.X(), p.Y()) { + return true + } + } + case *geom.Polygon: + if repr.NumLinearRings() > 0 { + lr := repr.LinearRing(0) + for i := 0; i < lr.NumCoords(); i++ { + if s.xyExceedsBounds(lr.Coord(i).X(), lr.Coord(i).Y()) { + return true + } + } + } + case *geom.GeometryCollection: + for _, geom := range repr.Geoms() { + if s.geomExceedsBounds(geom) { + return true + } + } + case *geom.MultiPoint: + for i := 0; i < repr.NumPoints(); i++ { + if s.geomExceedsBounds(repr.Point(i)) { + return true + } + } + case *geom.MultiLineString: + for i := 0; i < repr.NumLineStrings(); i++ { + if s.geomExceedsBounds(repr.LineString(i)) { + return true + } + } + case *geom.MultiPolygon: + for i := 0; i < repr.NumPolygons(); i++ { + if s.geomExceedsBounds(repr.Polygon(i)) { + return true + } + } + } + return false +} + +// stToUV, uvToST, xyzToFace0UV and face0UVToXYZPoint are adapted +// from unexported methods in github.com/golang/geo/s2/stuv.go + +// stToUV converts an s or t value to the corresponding u or v value. +// This is a non-linear transformation from [-1,1] to [-1,1] that +// attempts to make the cell sizes more uniform. +// This uses what the C++ version calls 'the quadratic transform'. +func stToUV(s float64) float64 { + if s >= 0.5 { + return (1 / 3.) * (4*s*s - 1) + } + return (1 / 3.) * (1 - 4*(1-s)*(1-s)) +} + +// uvToST is the inverse of the stToUV transformation. Note that it +// is not always true that uvToST(stToUV(x)) == x due to numerical +// errors. +func uvToST(u float64) float64 { + if u >= 0 { + return 0.5 * math.Sqrt(1+3*u) + } + return 1 - 0.5*math.Sqrt(1-3*u) +} + +// Specialized version of faceUVToXYZ() for face 0 +func face0UVToXYZPoint(u, v float64) s2.Point { + return s2.Point{Vector: r3.Vector{X: 1, Y: u, Z: v}} +} + +// xyzToFace0UV converts a direction vector (not necessarily unit length) to +// (u, v) coordinates on face 0. +func xyzToFace0UV(r s2.Point) (u, v float64) { + return r.Y / r.X, r.Z / r.X +} + +// planarPointToS2Point converts a planar point to an s2.Point. +func (s *s2GeometryIndex) planarPointToS2Point(x float64, y float64) s2.Point { + ss := (x - s.minX) / (s.maxX - s.minX) + tt := (y - s.minY) / (s.maxY - s.minY) + u := stToUV(ss) + v := stToUV(tt) + return face0UVToXYZPoint(u, v) +} + +// s2PointToPlanarPoints converts an s2.Point to a planar point. +func (s *s2GeometryIndex) s2PointToPlanarPoint(p s2.Point) (x, y float64) { + u, v := xyzToFace0UV(p) + ss := uvToST(u) + tt := uvToST(v) + return ss*(s.maxX-s.minX) + s.minX, tt*(s.maxY-s.minY) + s.minY +} + +// TODO(sumeer): this is similar to S2RegionsFromGeomT() but needs to do +// a different point conversion. If these functions do not diverge further, +// and turn out not to be performance critical, merge the two implementations. +func (s *s2GeometryIndex) s2RegionsFromPlanarGeomT(geomRepr geom.T) []s2.Region { + if geomRepr.Empty() { + return nil + } + var regions []s2.Region + switch repr := geomRepr.(type) { + case *geom.Point: + regions = []s2.Region{ + s.planarPointToS2Point(repr.X(), repr.Y()), + } + case *geom.LineString: + points := make([]s2.Point, repr.NumCoords()) + for i := 0; i < repr.NumCoords(); i++ { + p := repr.Coord(i) + points[i] = s.planarPointToS2Point(p.X(), p.Y()) + } + pl := s2.Polyline(points) + regions = []s2.Region{&pl} + case *geom.Polygon: + loops := make([]*s2.Loop, repr.NumLinearRings()) + // The first ring is a "shell". Following rings are "holes". + // All loops must be oriented CCW for S2. + for ringIdx := 0; ringIdx < repr.NumLinearRings(); ringIdx++ { + linearRing := repr.LinearRing(ringIdx) + points := make([]s2.Point, linearRing.NumCoords()) + isCCW := geo.IsLinearRingCCW(linearRing) + for pointIdx := 0; pointIdx < linearRing.NumCoords(); pointIdx++ { + p := linearRing.Coord(pointIdx) + pt := s.planarPointToS2Point(p.X(), p.Y()) + if isCCW { + points[pointIdx] = pt + } else { + points[len(points)-pointIdx-1] = pt + } + } + loops[ringIdx] = s2.LoopFromPoints(points) + } + regions = []s2.Region{ + s2.PolygonFromLoops(loops), + } + case *geom.GeometryCollection: + for _, geom := range repr.Geoms() { + regions = append(regions, s.s2RegionsFromPlanarGeomT(geom)...) + } + case *geom.MultiPoint: + for i := 0; i < repr.NumPoints(); i++ { + regions = append(regions, s.s2RegionsFromPlanarGeomT(repr.Point(i))...) + } + case *geom.MultiLineString: + for i := 0; i < repr.NumLineStrings(); i++ { + regions = append(regions, s.s2RegionsFromPlanarGeomT(repr.LineString(i))...) + } + case *geom.MultiPolygon: + for i := 0; i < repr.NumPolygons(); i++ { + regions = append(regions, s.s2RegionsFromPlanarGeomT(repr.Polygon(i))...) + } + } + return regions +} + +func (s *s2GeometryIndex) TestingInnerCovering(g geo.Geometry) s2.CellUnion { + gt, _, err := s.convertToGeomTAndTryClip(g) + if err != nil || gt == nil { + return nil + } + r := s.s2RegionsFromPlanarGeomT(gt) + return innerCovering(s.rc, r) +} + +func (s *s2GeometryIndex) CoveringGeometry( + c context.Context, g geo.Geometry, +) (geo.Geometry, error) { + keys, _, err := s.InvertedIndexKeys(c, g) + if err != nil { + return geo.Geometry{}, err + } + t, err := makeGeomTFromKeys(keys, g.SRID(), func(p s2.Point) (float64, float64) { + return s.s2PointToPlanarPoint(p) + }) + if err != nil { + return geo.Geometry{}, err + } + return geo.MakeGeometryFromGeomT(t) +} diff --git a/pkg/geo/geoindex/testdata/clip b/pkg/geo/geoindex/testdata/clip new file mode 100644 index 0000000..11743d1 --- /dev/null +++ b/pkg/geo/geoindex/testdata/clip @@ -0,0 +1,28 @@ +geometry +SRID=4004;MULTIPOINT((1.0 5.0), (3.0 4.0)) +---- + +clip xmin=10 ymin=10 xmax=20 ymax=20 +---- +55 => 13 (srid: 4004) + +clip xmin=0 ymin=0 xmax=10 ymax=10 +---- +55 => 55 (srid: 4004) + +geometry +MULTIPOINT((1.0 5.0), (3.0 4.0)) +---- + +clip xmin=10 ymin=10 xmax=20 ymax=20 +---- +51 => 9 (srid: 0) + +clip xmin=0 ymin=0 xmax=10 ymax=10 +---- +51 => 51 (srid: 0) + + +clip xmin=10 ymin=0 xmax=0 ymax=10 +---- +geos error: IllegalArgumentException: Clipping rectangle must be non-empty diff --git a/pkg/geo/geoindex/testdata/s2_geography b/pkg/geo/geoindex/testdata/s2_geography new file mode 100644 index 0000000..cde1f85 --- /dev/null +++ b/pkg/geo/geoindex/testdata/s2_geography @@ -0,0 +1,440 @@ +geometry name=point1 +MULTIPOINT((1.0 5.0), (3.0 4.0)) +---- + +geometry name=shortline1 +LINESTRING (-86.609955 32.703682,-86.609836 32.703659) +---- + +geometry name=nystate +POLYGON((-79.7624 42.5142,-79.0672 42.7783,-78.9313 42.8508,-78.9024 42.9061,-78.9313 42.9554,-78.9656 42.9584,-79.0219 42.9886,-79.0027 43.0568,-79.0727 43.0769,-79.0713 43.1220,-79.0302 43.1441,-79.0576 43.1801,-79.0604 43.2482,-79.0837 43.2812,-79.2004 43.4509,-78.6909 43.6311,-76.7958 43.6321,-76.4978 43.9987,-76.4388 44.0965,-76.3536 44.1349,-76.3124 44.1989,-76.2437 44.2049,-76.1655 44.2413,-76.1353 44.2973,-76.0474 44.3327,-75.9856 44.3553,-75.9196 44.3749,-75.8730 44.3994,-75.8221 44.4308,-75.8098 44.4740,-75.7288 44.5425,-75.5585 44.6647,-75.4088 44.7672,-75.3442 44.8101,-75.3058 44.8383,-75.2399 44.8676,-75.1204 44.9211,-74.9995 44.9609,-74.9899 44.9803,-74.9103 44.9852,-74.8856 45.0017,-74.8306 45.0153,-74.7633 45.0046,-74.7070 45.0027,-74.5642 45.0007,-74.1467 44.9920,-73.7306 45.0037,-73.4203 45.0085,-73.3430 45.0109,-73.3547 44.9874,-73.3379 44.9648,-73.3396 44.9160,-73.3739 44.8354,-73.3324 44.8013,-73.3667 44.7419,-73.3873 44.6139,-73.3736 44.5787,-73.3049 44.4916,-73.2953 44.4289,-73.3365 44.3513,-73.3118 44.2757,-73.3818 44.1980,-73.4079 44.1142,-73.4367 44.0511,-73.4065 44.0165,-73.4079 43.9375,-73.3749 43.8771,-73.3914 43.8167,-73.3557 43.7790,-73.4244 43.6460,-73.4340 43.5893,-73.3969 43.5655,-73.3818 43.6112,-73.3049 43.6271,-73.3063 43.5764,-73.2582 43.5675,-73.2445 43.5227,-73.2582 43.2582,-73.2733 42.9715,-73.2898 42.8004,-73.2664 42.7460,-73.3708 42.4630,-73.5095 42.0840,-73.4903 42.0218,-73.4999 41.8808,-73.5535 41.2953,-73.4834 41.2128,-73.7275 41.1011,-73.6644 41.0237,-73.6578 40.9851,-73.6132 40.9509,-72.4823 41.1869,-72.0950 41.2551,-71.9714 41.3005,-71.9193 41.3108,-71.7915 41.1838,-71.7929 41.1249,-71.7517 41.0462,-72.9465 40.6306,-73.4628 40.5368,-73.8885 40.4887,-73.9490 40.5232,-74.2271 40.4772,-74.2532 40.4861,-74.1866 40.6468,-74.0547 40.6556,-74.0156 40.7618,-73.9421 40.8699,-73.8934 40.9980,-73.9854 41.0343,-74.6274 41.3268,-74.7084 41.3583,-74.7101 41.3811,-74.8265 41.4386,-74.9913 41.5075,-75.0668 41.6000,-75.0366 41.6719,-75.0545 41.7672,-75.1945 41.8808,-75.3552 42.0013,-75.4266 42.0003,-77.0306 42.0013,-79.7250 41.9993,-79.7621 42.0003,-79.7621 42.1827,-79.7621 42.5146,-79.7624 42.5142)) +---- + +geometry name=nys-approx1 +POLYGON((-79 42,-77 44,-78 41,-79 42)) +---- + +geometry name=nys-approx2 +POLYGON((-79 42,-78 41,-77 44,-79 42)) +---- + +geometry name=red-hook-nyc +MULTIPOLYGON(((-74.0126927094665 40.6600480732464,-74.0127064547618 40.6600447465578,-74.0141319802258 40.6609147092529,-74.0145335611922 40.6611344232761,-74.0145358849763 40.6611361779097,-74.0145399285567 40.6611402822267,-74.014541648353 40.6611426319101,-74.0145797021934 40.6612147162775,-74.0145806405216 40.6612173067132,-74.0145817746666 40.661222645325,-74.0145819704835 40.6612253935011,-74.0145789529984 40.6612703205022,-74.0145775882448 40.6612747317321,-74.0145730223663 40.6612824707693,-74.0145698212414 40.6612857985766,-74.0144997485308 40.6613308932157,-74.0144993922351 40.6613311121095,-74.0144986708037 40.6613315348099,-74.014498305668 40.6613317386166,-74.0136647221644 40.6617744175344,-74.0134542384902 40.6618992353571,-74.0134540575192 40.6618993401319,-74.0134536934255 40.6618995458745,-74.013453510303 40.6618996468423,-74.0128195031462 40.6622406226638,-74.012816235274 40.6622417071219,-74.0128095254169 40.6622427033898,-74.0128060834319 40.6622426151996,-74.0127019801943 40.6622216604417,-74.012700315079 40.6622211716785,-74.0126970944194 40.6622199136429,-74.012695538875 40.6622191443704,-74.011062576148 40.6612266089193,-74.0110624213581 40.6612218066015,-74.0127057329214 40.6600718294846,-74.0127094072378 40.6600602461869,-74.0127040330277 40.6600453326751,-74.0126927094665 40.6600480732464))) +---- + +init minlevel=0 maxlevel=30 maxcells=2 +---- + +# NB: Bounding boxes use radian units for longitude and latitude. +index-keys name=nys-approx1 +---- +F4/L5/10321, F4/L5/10322 +BoundingBox: lo_x:-1.378810 hi_x:-1.343904 lo_y:0.715585 hi_y:0.767945 + +index-keys name=nys-approx2 +---- +F4/L5/10321, F4/L5/10322 +BoundingBox: lo_x:-1.378810 hi_x:-1.343904 lo_y:0.715585 hi_y:0.767945 + +index-keys name=point1 +---- +F0/L30/200023210133112000102232313101, F0/L30/200033223021330130001101002333 +BoundingBox: lo_x:0.017453 hi_x:0.052360 lo_y:0.069813 hi_y:0.087266 + +covers name=point1 +---- +F0/L0/, F0/L4/2000, F0/L5/20002, F0/L6/200023, F0/L10/2000232101, +F0/L11/20002321013, F0/L14/20002321013311, F0/L20/20002321013311200010, +F0/L21/200023210133112000102, F0/L22/2000232101331120001022, +F0/L23/20002321013311200010223, F0/L24/200023210133112000102232, +F0/L26/20002321013311200010223231, F0/L30/200023210133112000102232313101, +F0/L29/20002321013311200010223231310, F0/L28/2000232101331120001022323131, +F0/L27/200023210133112000102232313, F0/L25/2000232101331120001022323, +F0/L19/2000232101331120001, F0/L18/200023210133112000, F0/L17/20002321013311200, +F0/L16/2000232101331120, F0/L15/200023210133112, F0/L13/2000232101331, +F0/L12/200023210133, F0/L9/200023210, F0/L8/20002321, F0/L7/2000232, +F0/L5/20003, F0/L6/200033, F0/L7/2000332, F0/L8/20003322, F0/L10/2000332230, +F0/L12/200033223021, F0/L13/2000332230213, F0/L16/2000332230213301, +F0/L26/20003322302133013000110100, F0/L27/200033223021330130001101002, +F0/L28/2000332230213301300011010023, F0/L29/20003322302133013000110100233, +F0/L30/200033223021330130001101002333, F0/L25/2000332230213301300011010, +F0/L24/200033223021330130001101, F0/L23/20003322302133013000110, +F0/L22/2000332230213301300011, F0/L21/200033223021330130001, +F0/L20/20003322302133013000, F0/L19/2000332230213301300, F0/L18/200033223021330130, +F0/L17/20003322302133013, F0/L15/200033223021330, F0/L14/20003322302133, +F0/L11/20003322302, F0/L9/200033223, F0/L3/200, F0/L2/20, F0/L1/2 + +intersects name=point1 +---- +F0/L0/, F0/L4/2000, F0/L5/20002, F0/L6/200023, F0/L10/2000232101, +F0/L11/20002321013, F0/L14/20002321013311, F0/L20/20002321013311200010, +F0/L21/200023210133112000102, F0/L22/2000232101331120001022, +F0/L23/20002321013311200010223, F0/L24/200023210133112000102232, +F0/L26/20002321013311200010223231, F0/L30/200023210133112000102232313101, +F0/L29/20002321013311200010223231310, F0/L28/2000232101331120001022323131, +F0/L27/200023210133112000102232313, F0/L25/2000232101331120001022323, +F0/L19/2000232101331120001, F0/L18/200023210133112000, F0/L17/20002321013311200, +F0/L16/2000232101331120, F0/L15/200023210133112, F0/L13/2000232101331, +F0/L12/200023210133, F0/L9/200023210, F0/L8/20002321, F0/L7/2000232, +F0/L5/20003, F0/L6/200033, F0/L7/2000332, F0/L8/20003322, F0/L10/2000332230, +F0/L12/200033223021, F0/L13/2000332230213, F0/L16/2000332230213301, +F0/L26/20003322302133013000110100, F0/L27/200033223021330130001101002, +F0/L28/2000332230213301300011010023, F0/L29/20003322302133013000110100233, +F0/L30/200033223021330130001101002333, F0/L25/2000332230213301300011010, +F0/L24/200033223021330130001101, F0/L23/20003322302133013000110, +F0/L22/2000332230213301300011, F0/L21/200033223021330130001, +F0/L20/20003322302133013000, F0/L19/2000332230213301300, F0/L18/200033223021330130, +F0/L17/20003322302133013, F0/L15/200033223021330, F0/L14/20003322302133, +F0/L11/20003322302, F0/L9/200033223, F0/L3/200, F0/L2/20, F0/L1/2 + +inner-covering name=point1 +---- +F0/L30/200023210133112000102232313101, F0/L30/200033223021330130001101002333 + +covered-by name=point1 +---- +0: F0/L30/200033223021330130001101002333, F0/L29/20003322302133013000110100233, +F0/L28/2000332230213301300011010023, F0/L27/200033223021330130001101002, +F0/L26/20003322302133013000110100, F0/L25/2000332230213301300011010, +F0/L24/200033223021330130001101, F0/L23/20003322302133013000110, +F0/L22/2000332230213301300011, F0/L21/200033223021330130001, +F0/L20/20003322302133013000, F0/L19/2000332230213301300, F0/L18/200033223021330130, +F0/L17/20003322302133013, F0/L16/2000332230213301, F0/L15/200033223021330, +F0/L14/20003322302133, F0/L13/2000332230213, F0/L12/200033223021, +F0/L11/20003322302, F0/L10/2000332230, F0/L9/200033223, F0/L8/20003322, +F0/L7/2000332, F0/L6/200033, F0/L5/20003, F0/L4/2000, F0/L3/200, +F0/L2/20, F0/L1/2, F0/L0/ +1: F0/L30/200023210133112000102232313101, F0/L29/20002321013311200010223231310, +F0/L28/2000232101331120001022323131, F0/L27/200023210133112000102232313, +F0/L26/20002321013311200010223231, F0/L25/2000232101331120001022323, +F0/L24/200023210133112000102232, F0/L23/20002321013311200010223, +F0/L22/2000232101331120001022, F0/L21/200023210133112000102, +F0/L20/20002321013311200010, F0/L19/2000232101331120001, F0/L18/200023210133112000, +F0/L17/20002321013311200, F0/L16/2000232101331120, F0/L15/200023210133112, +F0/L14/20002321013311, F0/L13/2000232101331, F0/L12/200023210133, +F0/L11/20002321013, F0/L10/2000232101, F0/L9/200023210, F0/L8/20002321, +F0/L7/2000232, F0/L6/200023, F0/L5/20002, F0/L4/2000, F0/L3/200, +F0/L2/20, F0/L1/2, F0/L0/ + +index-keys name=shortline1 +---- +F4/L19/1010131200020223230, F4/L21/101013120002022323312 +BoundingBox: lo_x:-1.511629 hi_x:-1.511627 lo_y:0.570787 hi_y:0.570787 + +covers name=shortline1 +---- +F4/L5/10101, F4/L7/1010131, F4/L11/10101312000, F4/L13/1010131200020, +F4/L14/10101312000202, F4/L15/101013120002022, F4/L16/1010131200020223, +F4/L17/10101312000202232, [F4/L30/101013120002022323000000000000, F4/L30/101013120002022323033333333333], +F4/L18/101013120002022323, F4/L20/10101312000202232331, [F4/L30/101013120002022323312000000000, F4/L30/101013120002022323312333333333], +F4/L19/1010131200020223233, F4/L12/101013120002, F4/L10/1010131200, +F4/L9/101013120, F4/L8/10101312, F4/L6/101013, F4/L4/1010, F4/L3/101, +F4/L2/10, F4/L1/1, F4/L0/ + +intersects name=shortline1 +---- +F4/L5/10101, F4/L7/1010131, F4/L11/10101312000, F4/L13/1010131200020, +F4/L14/10101312000202, F4/L15/101013120002022, F4/L16/1010131200020223, +F4/L17/10101312000202232, [F4/L30/101013120002022323000000000000, F4/L30/101013120002022323033333333333], +F4/L18/101013120002022323, F4/L20/10101312000202232331, [F4/L30/101013120002022323312000000000, F4/L30/101013120002022323312333333333], +F4/L19/1010131200020223233, F4/L12/101013120002, F4/L10/1010131200, +F4/L9/101013120, F4/L8/10101312, F4/L6/101013, F4/L4/1010, F4/L3/101, +F4/L2/10, F4/L1/1, F4/L0/ + +inner-covering name=shortline1 +---- +F4/L30/101013120002022323031030033032, F4/L30/101013120002022323312001023033 + +covered-by name=shortline1 +---- +0: F4/L30/101013120002022323312001023033, F4/L29/10101312000202232331200102303, +F4/L28/1010131200020223233120010230, F4/L27/101013120002022323312001023, +F4/L26/10101312000202232331200102, F4/L25/1010131200020223233120010, +F4/L24/101013120002022323312001, F4/L23/10101312000202232331200, +F4/L22/1010131200020223233120, F4/L21/101013120002022323312, +F4/L20/10101312000202232331, F4/L19/1010131200020223233, F4/L18/101013120002022323, +F4/L17/10101312000202232, F4/L16/1010131200020223, F4/L15/101013120002022, +F4/L14/10101312000202, F4/L13/1010131200020, F4/L12/101013120002, +F4/L11/10101312000, F4/L10/1010131200, F4/L9/101013120, F4/L8/10101312, +F4/L7/1010131, F4/L6/101013, F4/L5/10101, F4/L4/1010, F4/L3/101, +F4/L2/10, F4/L1/1, F4/L0/ +1: F4/L30/101013120002022323031030033032, F4/L29/10101312000202232303103003303, +F4/L28/1010131200020223230310300330, F4/L27/101013120002022323031030033, +F4/L26/10101312000202232303103003, F4/L25/1010131200020223230310300, +F4/L24/101013120002022323031030, F4/L23/10101312000202232303103, +F4/L22/1010131200020223230310, F4/L21/101013120002022323031, +F4/L20/10101312000202232303, F4/L19/1010131200020223230, F4/L18/101013120002022323, +F4/L17/10101312000202232, F4/L16/1010131200020223, F4/L15/101013120002022, +F4/L14/10101312000202, F4/L13/1010131200020, F4/L12/101013120002, +F4/L11/10101312000, F4/L10/1010131200, F4/L9/101013120, F4/L8/10101312, +F4/L7/1010131, F4/L6/101013, F4/L5/10101, F4/L4/1010, F4/L3/101, +F4/L2/10, F4/L1/1, F4/L0/ + +index-keys name=nystate +---- +F2/L2/12, F4/L2/10 +BoundingBox: lo_x:-1.392116 hi_x:-1.252303 lo_y:0.706460 hi_y:0.785665 + +covers name=nystate +---- +F2/L1/1, [F2/L30/120000000000000000000000000000, F2/L30/123333333333333333333333333333], +F2/L0/, [F4/L30/100000000000000000000000000000, F4/L30/103333333333333333333333333333], +F4/L1/1, F4/L0/ + +intersects name=nystate +---- +F2/L1/1, [F2/L30/120000000000000000000000000000, F2/L30/123333333333333333333333333333], +F2/L0/, [F4/L30/100000000000000000000000000000, F4/L30/103333333333333333333333333333], +F4/L1/1, F4/L0/ + +index-keys name=red-hook-nyc +---- +F4/L13/1032010231102, F4/L13/1032010231113 +BoundingBox: lo_x:-1.291798 hi_x:-1.291737 lo_y:0.709652 hi_y:0.709690 + +init minlevel=0 maxlevel=30 maxcells=8 +---- + +index-keys name=nystate +---- +F2/L5/12112, F2/L5/12121, F2/L5/12122, F2/L7/1221111, F4/L4/1001, F4/L4/1032, F4/L5/10330, F4/L5/10331 +BoundingBox: lo_x:-1.392116 hi_x:-1.252303 lo_y:0.706460 hi_y:0.785665 + +index-keys name=red-hook-nyc +---- +F4/L16/1032010231102232, F4/L16/1032010231102233, F4/L15/103201023110230, F4/L17/10320102311132301, F4/L18/103201023111323020, F4/L16/1032010231113233, F4/L17/10320102311133000, F4/L17/10320102311133003 +BoundingBox: lo_x:-1.291798 hi_x:-1.291737 lo_y:0.709652 hi_y:0.709690 + +init minlevel=0 maxlevel=30 maxcells=1 +---- + +index-keys name=point1 +---- +F0/L30/200023210133112000102232313101, F0/L30/200033223021330130001101002333 +BoundingBox: lo_x:0.017453 hi_x:0.052360 lo_y:0.069813 hi_y:0.087266 + +covers name=point1 +---- +F0/L0/, F0/L4/2000, F0/L5/20002, F0/L6/200023, F0/L10/2000232101, +F0/L11/20002321013, F0/L14/20002321013311, F0/L20/20002321013311200010, +F0/L21/200023210133112000102, F0/L22/2000232101331120001022, +F0/L23/20002321013311200010223, F0/L24/200023210133112000102232, +F0/L26/20002321013311200010223231, F0/L30/200023210133112000102232313101, +F0/L29/20002321013311200010223231310, F0/L28/2000232101331120001022323131, +F0/L27/200023210133112000102232313, F0/L25/2000232101331120001022323, +F0/L19/2000232101331120001, F0/L18/200023210133112000, F0/L17/20002321013311200, +F0/L16/2000232101331120, F0/L15/200023210133112, F0/L13/2000232101331, +F0/L12/200023210133, F0/L9/200023210, F0/L8/20002321, F0/L7/2000232, +F0/L5/20003, F0/L6/200033, F0/L7/2000332, F0/L8/20003322, F0/L10/2000332230, +F0/L12/200033223021, F0/L13/2000332230213, F0/L16/2000332230213301, +F0/L26/20003322302133013000110100, F0/L27/200033223021330130001101002, +F0/L28/2000332230213301300011010023, F0/L29/20003322302133013000110100233, +F0/L30/200033223021330130001101002333, F0/L25/2000332230213301300011010, +F0/L24/200033223021330130001101, F0/L23/20003322302133013000110, +F0/L22/2000332230213301300011, F0/L21/200033223021330130001, +F0/L20/20003322302133013000, F0/L19/2000332230213301300, F0/L18/200033223021330130, +F0/L17/20003322302133013, F0/L15/200033223021330, F0/L14/20003322302133, +F0/L11/20003322302, F0/L9/200033223, F0/L3/200, F0/L2/20, F0/L1/2 + +intersects name=point1 +---- +F0/L0/, F0/L4/2000, F0/L5/20002, F0/L6/200023, F0/L10/2000232101, +F0/L11/20002321013, F0/L14/20002321013311, F0/L20/20002321013311200010, +F0/L21/200023210133112000102, F0/L22/2000232101331120001022, +F0/L23/20002321013311200010223, F0/L24/200023210133112000102232, +F0/L26/20002321013311200010223231, F0/L30/200023210133112000102232313101, +F0/L29/20002321013311200010223231310, F0/L28/2000232101331120001022323131, +F0/L27/200023210133112000102232313, F0/L25/2000232101331120001022323, +F0/L19/2000232101331120001, F0/L18/200023210133112000, F0/L17/20002321013311200, +F0/L16/2000232101331120, F0/L15/200023210133112, F0/L13/2000232101331, +F0/L12/200023210133, F0/L9/200023210, F0/L8/20002321, F0/L7/2000232, +F0/L5/20003, F0/L6/200033, F0/L7/2000332, F0/L8/20003322, F0/L10/2000332230, +F0/L12/200033223021, F0/L13/2000332230213, F0/L16/2000332230213301, +F0/L26/20003322302133013000110100, F0/L27/200033223021330130001101002, +F0/L28/2000332230213301300011010023, F0/L29/20003322302133013000110100233, +F0/L30/200033223021330130001101002333, F0/L25/2000332230213301300011010, +F0/L24/200033223021330130001101, F0/L23/20003322302133013000110, +F0/L22/2000332230213301300011, F0/L21/200033223021330130001, +F0/L20/20003322302133013000, F0/L19/2000332230213301300, F0/L18/200033223021330130, +F0/L17/20003322302133013, F0/L15/200033223021330, F0/L14/20003322302133, +F0/L11/20003322302, F0/L9/200033223, F0/L3/200, F0/L2/20, F0/L1/2 + +inner-covering name=point1 +---- +F0/L30/200023210133112000102232313101, F0/L30/200033223021330130001101002333 + +covered-by name=point1 +---- +0: F0/L30/200033223021330130001101002333, F0/L29/20003322302133013000110100233, +F0/L28/2000332230213301300011010023, F0/L27/200033223021330130001101002, +F0/L26/20003322302133013000110100, F0/L25/2000332230213301300011010, +F0/L24/200033223021330130001101, F0/L23/20003322302133013000110, +F0/L22/2000332230213301300011, F0/L21/200033223021330130001, +F0/L20/20003322302133013000, F0/L19/2000332230213301300, F0/L18/200033223021330130, +F0/L17/20003322302133013, F0/L16/2000332230213301, F0/L15/200033223021330, +F0/L14/20003322302133, F0/L13/2000332230213, F0/L12/200033223021, +F0/L11/20003322302, F0/L10/2000332230, F0/L9/200033223, F0/L8/20003322, +F0/L7/2000332, F0/L6/200033, F0/L5/20003, F0/L4/2000, F0/L3/200, +F0/L2/20, F0/L1/2, F0/L0/ +1: F0/L30/200023210133112000102232313101, F0/L29/20002321013311200010223231310, +F0/L28/2000232101331120001022323131, F0/L27/200023210133112000102232313, +F0/L26/20002321013311200010223231, F0/L25/2000232101331120001022323, +F0/L24/200023210133112000102232, F0/L23/20002321013311200010223, +F0/L22/2000232101331120001022, F0/L21/200023210133112000102, +F0/L20/20002321013311200010, F0/L19/2000232101331120001, F0/L18/200023210133112000, +F0/L17/20002321013311200, F0/L16/2000232101331120, F0/L15/200023210133112, +F0/L14/20002321013311, F0/L13/2000232101331, F0/L12/200023210133, +F0/L11/20002321013, F0/L10/2000232101, F0/L9/200023210, F0/L8/20002321, +F0/L7/2000232, F0/L6/200023, F0/L5/20002, F0/L4/2000, F0/L3/200, +F0/L2/20, F0/L1/2, F0/L0/ + +index-keys name=shortline1 +---- +F4/L18/101013120002022323 +BoundingBox: lo_x:-1.511629 hi_x:-1.511627 lo_y:0.570787 hi_y:0.570787 + +covers name=shortline1 +---- +F4/L5/10101, F4/L7/1010131, F4/L11/10101312000, F4/L13/1010131200020, +F4/L14/10101312000202, F4/L15/101013120002022, F4/L16/1010131200020223, +F4/L17/10101312000202232, [F4/L30/101013120002022323000000000000, F4/L30/101013120002022323333333333333], +F4/L12/101013120002, F4/L10/1010131200, F4/L9/101013120, F4/L8/10101312, +F4/L6/101013, F4/L4/1010, F4/L3/101, F4/L2/10, F4/L1/1, F4/L0/ + +intersects name=shortline1 +---- +F4/L5/10101, F4/L7/1010131, F4/L11/10101312000, F4/L13/1010131200020, +F4/L14/10101312000202, F4/L15/101013120002022, F4/L16/1010131200020223, +F4/L17/10101312000202232, [F4/L30/101013120002022323000000000000, F4/L30/101013120002022323333333333333], +F4/L12/101013120002, F4/L10/1010131200, F4/L9/101013120, F4/L8/10101312, +F4/L6/101013, F4/L4/1010, F4/L3/101, F4/L2/10, F4/L1/1, F4/L0/ + +inner-covering name=shortline1 +---- +F4/L30/101013120002022323031030033032, F4/L30/101013120002022323312001023033 + +covered-by name=shortline1 +---- +0: F4/L30/101013120002022323312001023033, F4/L29/10101312000202232331200102303, +F4/L28/1010131200020223233120010230, F4/L27/101013120002022323312001023, +F4/L26/10101312000202232331200102, F4/L25/1010131200020223233120010, +F4/L24/101013120002022323312001, F4/L23/10101312000202232331200, +F4/L22/1010131200020223233120, F4/L21/101013120002022323312, +F4/L20/10101312000202232331, F4/L19/1010131200020223233, F4/L18/101013120002022323, +F4/L17/10101312000202232, F4/L16/1010131200020223, F4/L15/101013120002022, +F4/L14/10101312000202, F4/L13/1010131200020, F4/L12/101013120002, +F4/L11/10101312000, F4/L10/1010131200, F4/L9/101013120, F4/L8/10101312, +F4/L7/1010131, F4/L6/101013, F4/L5/10101, F4/L4/1010, F4/L3/101, +F4/L2/10, F4/L1/1, F4/L0/ +1: F4/L30/101013120002022323031030033032, F4/L29/10101312000202232303103003303, +F4/L28/1010131200020223230310300330, F4/L27/101013120002022323031030033, +F4/L26/10101312000202232303103003, F4/L25/1010131200020223230310300, +F4/L24/101013120002022323031030, F4/L23/10101312000202232303103, +F4/L22/1010131200020223230310, F4/L21/101013120002022323031, +F4/L20/10101312000202232303, F4/L19/1010131200020223230, F4/L18/101013120002022323, +F4/L17/10101312000202232, F4/L16/1010131200020223, F4/L15/101013120002022, +F4/L14/10101312000202, F4/L13/1010131200020, F4/L12/101013120002, +F4/L11/10101312000, F4/L10/1010131200, F4/L9/101013120, F4/L8/10101312, +F4/L7/1010131, F4/L6/101013, F4/L5/10101, F4/L4/1010, F4/L3/101, +F4/L2/10, F4/L1/1, F4/L0/ + +init minlevel=0 maxlevel=4 maxcells=1 +---- + +inner-covering name=point1 +---- +F0/L4/2000 + +inner-covering name=shortline1 +---- +F4/L4/1010 + +init minlevel=0 maxlevel=16 maxcells=2 +---- + +index-keys name=point1 +---- +F0/L16/2000232101331120, F0/L16/2000332230213301 +BoundingBox: lo_x:0.017453 hi_x:0.052360 lo_y:0.069813 hi_y:0.087266 + +d-within distance=2000 name=point1 +---- +F0/L0/, F0/L4/2000, F0/L5/20002, F0/L6/200023, F0/L10/2000232100, +[F0/L30/200023210020000000000000000000, F0/L30/200023210023333333333333333333], +[F0/L30/200023210030000000000000000000, F0/L30/200023210033333333333333333333], +[F0/L30/200023210100000000000000000000, F0/L30/200023210133333333333333333333], +F0/L9/200023210, [F0/L30/200023210200000000000000000000, F0/L30/200023210203333333333333333333], +[F0/L30/200023210210000000000000000000, F0/L30/200023210213333333333333333333], +F0/L10/2000232102, [F0/L30/200023210310000000000000000000, F0/L30/200023210313333333333333333333], +F0/L10/2000232103, F0/L8/20002321, F0/L7/2000232, F0/L5/20003, +F0/L6/200033, F0/L7/2000332, F0/L8/20003322, [F0/L30/200033223000000000000000000000, F0/L30/200033223033333333333333333333], +[F0/L30/200033223100000000000000000000, F0/L30/200033223103333333333333333333], +F0/L10/2000332231, [F0/L30/200033223130000000000000000000, F0/L30/200033223133333333333333333333], +F0/L9/200033223, [F0/L30/200033223200000000000000000000, F0/L30/200033223203333333333333333333], +F0/L10/2000332232, [F0/L30/200033223310000000000000000000, F0/L30/200033223313333333333333333333], +F0/L10/2000332233, [F0/L30/200033223320000000000000000000, F0/L30/200033223323333333333333333333], +F0/L3/200, F0/L2/20, F0/L1/2 + +d-within distance=20000 name=point1 +---- +F0/L0/, F0/L4/2000, [F0/L30/200020110000000000000000000000, F0/L30/200020113333333333333333333333], +F0/L7/2000201, [F0/L30/200020120000000000000000000000, F0/L30/200020123333333333333333333333], +F0/L6/200020, [F0/L30/200020210000000000000000000000, F0/L30/200020213333333333333333333333], +F0/L7/2000202, F0/L5/20002, F0/L7/2000231, [F0/L30/200023120000000000000000000000, F0/L30/200023123333333333333333333333], +[F0/L30/200023130000000000000000000000, F0/L30/200023133333333333333333333333], +F0/L6/200023, [F0/L30/200023200000000000000000000000, F0/L30/200023233333333333333333333333], +[F0/L30/200030110000000000000000000000, F0/L30/200030113333333333333333333333], +F0/L7/2000301, [F0/L30/200030120000000000000000000000, F0/L30/200030123333333333333333333333], +F0/L6/200030, F0/L5/20003, F0/L6/200033, [F0/L30/200033200000000000000000000000, F0/L30/200033233333333333333333333333], +F0/L6/200100, F0/L7/2001003, [F0/L30/200100320000000000000000000000, F0/L30/200100323333333333333333333333], +[F0/L30/200100330000000000000000000000, F0/L30/200100333333333333333333333333], +[F0/L30/200101000000000000000000000000, F0/L30/200101003333333333333333333333], +F0/L7/2001010, F0/L6/200101, F0/L5/20010, F0/L4/2001, F0/L3/200, +F0/L2/20, F0/L1/2 + +index-keys name=nys-approx1 +---- +F4/L5/10321, F4/L5/10322 +BoundingBox: lo_x:-1.378810 hi_x:-1.343904 lo_y:0.715585 hi_y:0.767945 + +intersects name=nys-approx1 +---- +F4/L2/10, F4/L3/103, [F4/L30/103210000000000000000000000000, F4/L30/103213333333333333333333333333], +F4/L4/1032, [F4/L30/103220000000000000000000000000, F4/L30/103223333333333333333333333333], +F4/L1/1, F4/L0/ + +d-within distance=2000 name=nys-approx1 +---- +F2/L1/1, F2/L3/121, F2/L5/12121, F2/L6/121212, [F2/L30/121212200000000000000000000000, F2/L30/121212233333333333333333333333], +F2/L4/1212, [F2/L30/121221100000000000000000000000, F2/L30/121221133333333333333333333333], +F2/L6/121221, [F2/L30/121221200000000000000000000000, F2/L30/121221233333333333333333333333], +F2/L5/12122, [F2/L30/121222100000000000000000000000, F2/L30/121222133333333333333333333333], +F2/L6/121222, [F2/L30/121222200000000000000000000000, F2/L30/121222233333333333333333333333], +F2/L2/12, [F2/L30/122111100000000000000000000000, F2/L30/122111133333333333333333333333], +F2/L6/122111, F2/L5/12211, F2/L4/1221, F2/L3/122, F2/L0/, [F4/L30/100111100000000000000000000000, F4/L30/100111133333333333333333333333], +F4/L6/100111, [F4/L30/100111200000000000000000000000, F4/L30/100111233333333333333333333333], +F4/L5/10011, [F4/L30/100112100000000000000000000000, F4/L30/100112133333333333333333333333], +F4/L6/100112, [F4/L30/100112200000000000000000000000, F4/L30/100112233333333333333333333333], +F4/L4/1001, [F4/L30/100121100000000000000000000000, F4/L30/100121133333333333333333333333], +F4/L6/100121, [F4/L30/100121200000000000000000000000, F4/L30/100121233333333333333333333333], +F4/L5/10012, [F4/L30/100122100000000000000000000000, F4/L30/100122133333333333333333333333], +F4/L6/100122, [F4/L30/100122200000000000000000000000, F4/L30/100122233333333333333333333333], +F4/L3/100, [F4/L30/100211100000000000000000000000, F4/L30/100211133333333333333333333333], +F4/L6/100211, F4/L5/10021, F4/L4/1002, F4/L2/10, F4/L4/1031, +F4/L5/10312, F4/L6/103122, [F4/L30/103122200000000000000000000000, F4/L30/103122233333333333333333333333], +[F4/L30/103122300000000000000000000000, F4/L30/103122333333333333333333333333], +[F4/L30/103123000000000000000000000000, F4/L30/103123033333333333333333333333], +F4/L6/103123, [F4/L30/103123300000000000000000000000, F4/L30/103123333333333333333333333333], +[F4/L30/103130000000000000000000000000, F4/L30/103130033333333333333333333333], +F4/L6/103130, F4/L5/10313, F4/L3/103, F4/L5/10320, F4/L6/103202, +[F4/L30/103202200000000000000000000000, F4/L30/103202233333333333333333333333], +[F4/L30/103202300000000000000000000000, F4/L30/103202333333333333333333333333], +[F4/L30/103203000000000000000000000000, F4/L30/103203033333333333333333333333], +F4/L6/103203, [F4/L30/103203300000000000000000000000, F4/L30/103203333333333333333333333333], +[F4/L30/103210000000000000000000000000, F4/L30/103213333333333333333333333333], +F4/L4/1032, [F4/L30/103220000000000000000000000000, F4/L30/103223333333333333333333333333], +[F4/L30/103230000000000000000000000000, F4/L30/103230033333333333333333333333], +F4/L6/103230, [F4/L30/103230300000000000000000000000, F4/L30/103230333333333333333333333333], +[F4/L30/103231000000000000000000000000, F4/L30/103231033333333333333333333333], +[F4/L30/103231100000000000000000000000, F4/L30/103231133333333333333333333333], +F4/L6/103231, F4/L5/10323, F4/L1/1, F4/L0/ diff --git a/pkg/geo/geoindex/testdata/s2_geometry b/pkg/geo/geoindex/testdata/s2_geometry new file mode 100644 index 0000000..affb9c2 --- /dev/null +++ b/pkg/geo/geoindex/testdata/s2_geometry @@ -0,0 +1,401 @@ +# TODO(sumeer): add more test cases + +geometry name=point1 +MULTIPOINT((12 12), (21 21)) +---- + +geometry name=shortline1 +LINESTRING (11 11,12 12) +---- + +geometry name=line-exceeds-bound +LINESTRING (12 15, 22 23) +---- + +geometry name=poly1 +POLYGON((15 15,16 15.5,15.5 17,15 15)) +---- + +geometry name=nan +0105000000060000000102000000020000002CF565DB1790F041010000000000F87F1C1E119D614AE7C1010000000000F87F010200000003000000809745B2972CF841010000000000F87FE0D03302F1C6D641010000000000F87FF83C7FD5C4ACD641010000000000F87F010200000004000000C4A695BE1FC2FD41010000000000F87F42F59F55FD780042010000000000F87FA07574B08DC0D941010000000000F87FD843A641FF49E741010000000000F87F010200000004000000781D60294113F241010000000000F87F100451CE196BED41010000000000F87F1430818C69A2EF41010000000000F87F00E4B02D00CB9341010000000000F87F01020000000400000040B0B6B74C96A0C1010000000000F87F1CD943F294E9E5C1010000000000F87F709F7E29F32AC4C1010000000000F87F607737DC4662E941010000000000F87F010200000004000000F097AF975517F441010000000000F87F5442BA391710F841010000000000F87FE01C9C5F7602B641010000000000F87F40E714D68300E2C1010000000000F87F +---- + +geometry name=poly1-different-orientation +POLYGON((15 15,15.5 17,16 15.5,15 15)) +---- + +init minlevel=0 maxlevel=30 maxcells=4 minx=10 miny=10 maxx=20 maxy=20 +---- + +# NaN +covers name=nan +---- +cannot index a geometry with NaN coordinates + +index-keys name=nan +---- +cannot index a geometry with NaN coordinates + +intersects name=nan +---- +cannot index a geometry with NaN coordinates + +# The orientation difference between the polygons has no effect on the +# index keys. + +index-keys name=poly1 +---- +F0/L30/022222222222222222222222222222, F0/L30/133333333333333333333333333333, F0/L2/20, F0/L30/311111111111111111111111111111 +BoundingBox: lo_x:15.000000 hi_x:16.000000 lo_y:15.000000 hi_y:17.000000 + +index-keys name=poly1-different-orientation +---- +F0/L30/022222222222222222222222222222, F0/L30/133333333333333333333333333333, F0/L2/20, F0/L30/311111111111111111111111111111 +BoundingBox: lo_x:15.000000 hi_x:16.000000 lo_y:15.000000 hi_y:17.000000 + +# Point exceeds the defined bounds. + +index-keys name=point1 +---- +F0/L30/002200220022002200220022002200, spilled +BoundingBox: lo_x:12.000000 hi_x:21.000000 lo_y:12.000000 hi_y:21.000000 + +covers name=point1 +---- +F0/L2/00, F0/L3/002, F0/L6/002200, F0/L7/0022002, F0/L10/0022002200, +F0/L11/00220022002, F0/L14/00220022002200, F0/L15/002200220022002, +F0/L18/002200220022002200, F0/L19/0022002200220022002, F0/L22/0022002200220022002200, +F0/L23/00220022002200220022002, F0/L26/00220022002200220022002200, +F0/L27/002200220022002200220022002, F0/L30/002200220022002200220022002200, +F0/L29/00220022002200220022002200220, F0/L28/0022002200220022002200220022, +F0/L25/0022002200220022002200220, F0/L24/002200220022002200220022, +F0/L21/002200220022002200220, F0/L20/00220022002200220022, F0/L17/00220022002200220, +F0/L16/0022002200220022, F0/L13/0022002200220, F0/L12/002200220022, +F0/L9/002200220, F0/L8/00220022, F0/L5/00220, F0/L4/0022, F0/L1/0, +F0/L0/, spilled + +intersects name=point1 +---- +F0/L2/00, F0/L3/002, F0/L6/002200, F0/L7/0022002, F0/L10/0022002200, +F0/L11/00220022002, F0/L14/00220022002200, F0/L15/002200220022002, +F0/L18/002200220022002200, F0/L19/0022002200220022002, F0/L22/0022002200220022002200, +F0/L23/00220022002200220022002, F0/L26/00220022002200220022002200, +F0/L27/002200220022002200220022002, F0/L30/002200220022002200220022002200, +F0/L29/00220022002200220022002200220, F0/L28/0022002200220022002200220022, +F0/L25/0022002200220022002200220, F0/L24/002200220022002200220022, +F0/L21/002200220022002200220, F0/L20/00220022002200220022, F0/L17/00220022002200220, +F0/L16/0022002200220022, F0/L13/0022002200220, F0/L12/002200220022, +F0/L9/002200220, F0/L8/00220022, F0/L5/00220, F0/L4/0022, F0/L1/0, +F0/L0/, spilled + +inner-covering name=point1 +---- +F0/L30/002200220022002200220022002200 + +covered-by name=point1 +---- +0: spilled +1: F0/L30/002200220022002200220022002200, F0/L29/00220022002200220022002200220, +F0/L28/0022002200220022002200220022, F0/L27/002200220022002200220022002, +F0/L26/00220022002200220022002200, F0/L25/0022002200220022002200220, +F0/L24/002200220022002200220022, F0/L23/00220022002200220022002, +F0/L22/0022002200220022002200, F0/L21/002200220022002200220, +F0/L20/00220022002200220022, F0/L19/0022002200220022002, F0/L18/002200220022002200, +F0/L17/00220022002200220, F0/L16/0022002200220022, F0/L15/002200220022002, +F0/L14/00220022002200, F0/L13/0022002200220, F0/L12/002200220022, +F0/L11/00220022002, F0/L10/0022002200, F0/L9/002200220, F0/L8/00220022, +F0/L7/0022002, F0/L6/002200, F0/L5/00220, F0/L4/0022, F0/L3/002, +F0/L2/00, F0/L1/0, F0/L0/ + +index-keys name=shortline1 +---- +F0/L5/00022, F0/L3/002, F0/L30/003111111111111111111111111111 +BoundingBox: lo_x:11.000000 hi_x:12.000000 lo_y:11.000000 hi_y:12.000000 + +covers name=shortline1 +---- +F0/L3/000, F0/L4/0002, [F0/L30/000220000000000000000000000000, F0/L30/000223333333333333333333333333], +F0/L2/00, [F0/L30/002000000000000000000000000000, F0/L30/002333333333333333333333333333], +F0/L30/003111111111111111111111111111, F0/L29/00311111111111111111111111111, +F0/L28/0031111111111111111111111111, F0/L27/003111111111111111111111111, +F0/L26/00311111111111111111111111, F0/L25/0031111111111111111111111, +F0/L24/003111111111111111111111, F0/L23/00311111111111111111111, +F0/L22/0031111111111111111111, F0/L21/003111111111111111111, +F0/L20/00311111111111111111, F0/L19/0031111111111111111, F0/L18/003111111111111111, +F0/L17/00311111111111111, F0/L16/0031111111111111, F0/L15/003111111111111, +F0/L14/00311111111111, F0/L13/0031111111111, F0/L12/003111111111, +F0/L11/00311111111, F0/L10/0031111111, F0/L9/003111111, F0/L8/00311111, +F0/L7/0031111, F0/L6/003111, F0/L5/00311, F0/L4/0031, F0/L3/003, +F0/L1/0, F0/L0/ + +intersects name=shortline1 +---- +F0/L3/000, F0/L4/0002, [F0/L30/000220000000000000000000000000, F0/L30/000223333333333333333333333333], +F0/L2/00, [F0/L30/002000000000000000000000000000, F0/L30/002333333333333333333333333333], +F0/L30/003111111111111111111111111111, F0/L29/00311111111111111111111111111, +F0/L28/0031111111111111111111111111, F0/L27/003111111111111111111111111, +F0/L26/00311111111111111111111111, F0/L25/0031111111111111111111111, +F0/L24/003111111111111111111111, F0/L23/00311111111111111111111, +F0/L22/0031111111111111111111, F0/L21/003111111111111111111, +F0/L20/00311111111111111111, F0/L19/0031111111111111111, F0/L18/003111111111111111, +F0/L17/00311111111111111, F0/L16/0031111111111111, F0/L15/003111111111111, +F0/L14/00311111111111, F0/L13/0031111111111, F0/L12/003111111111, +F0/L11/00311111111, F0/L10/0031111111, F0/L9/003111111, F0/L8/00311111, +F0/L7/0031111, F0/L6/003111, F0/L5/00311, F0/L4/0031, F0/L3/003, +F0/L1/0, F0/L0/ + +inner-covering name=shortline1 +---- +F0/L30/000220022002200220022002200220, F0/L30/002200220022002200220022002200 + +covered-by name=shortline1 +---- +0: F0/L30/002200220022002200220022002200, F0/L29/00220022002200220022002200220, +F0/L28/0022002200220022002200220022, F0/L27/002200220022002200220022002, +F0/L26/00220022002200220022002200, F0/L25/0022002200220022002200220, +F0/L24/002200220022002200220022, F0/L23/00220022002200220022002, +F0/L22/0022002200220022002200, F0/L21/002200220022002200220, +F0/L20/00220022002200220022, F0/L19/0022002200220022002, F0/L18/002200220022002200, +F0/L17/00220022002200220, F0/L16/0022002200220022, F0/L15/002200220022002, +F0/L14/00220022002200, F0/L13/0022002200220, F0/L12/002200220022, +F0/L11/00220022002, F0/L10/0022002200, F0/L9/002200220, F0/L8/00220022, +F0/L7/0022002, F0/L6/002200, F0/L5/00220, F0/L4/0022, F0/L3/002, +F0/L2/00, F0/L1/0, F0/L0/ +1: F0/L30/000220022002200220022002200220, F0/L29/00022002200220022002200220022, +F0/L28/0002200220022002200220022002, F0/L27/000220022002200220022002200, +F0/L26/00022002200220022002200220, F0/L25/0002200220022002200220022, +F0/L24/000220022002200220022002, F0/L23/00022002200220022002200, +F0/L22/0002200220022002200220, F0/L21/000220022002200220022, +F0/L20/00022002200220022002, F0/L19/0002200220022002200, F0/L18/000220022002200220, +F0/L17/00022002200220022, F0/L16/0002200220022002, F0/L15/000220022002200, +F0/L14/00022002200220, F0/L13/0002200220022, F0/L12/000220022002, +F0/L11/00022002200, F0/L10/0002200220, F0/L9/000220022, F0/L8/00022002, +F0/L7/0002200, F0/L6/000220, F0/L5/00022, F0/L4/0002, F0/L3/000, +F0/L2/00, F0/L1/0, F0/L0/ + +index-keys name=line-exceeds-bound +---- +F0/L30/030033003300330033003300330033, F0/L1/1, F0/L2/21, F0/L4/2211, spilled +BoundingBox: lo_x:12.000000 hi_x:22.000000 lo_y:15.000000 hi_y:23.000000 + +covers name=line-exceeds-bound +---- +F0/L1/0, F0/L4/0300, F0/L5/03003, F0/L8/03003300, F0/L9/030033003, +F0/L12/030033003300, F0/L13/0300330033003, F0/L16/0300330033003300, +F0/L17/03003300330033003, F0/L20/03003300330033003300, F0/L21/030033003300330033003, +F0/L24/030033003300330033003300, F0/L25/0300330033003300330033003, +F0/L28/0300330033003300330033003300, F0/L29/03003300330033003300330033003, +F0/L30/030033003300330033003300330033, F0/L27/030033003300330033003300330, +F0/L26/03003300330033003300330033, F0/L23/03003300330033003300330, +F0/L22/0300330033003300330033, F0/L19/0300330033003300330, F0/L18/030033003300330033, +F0/L15/030033003300330, F0/L14/03003300330033, F0/L11/03003300330, +F0/L10/0300330033, F0/L7/0300330, F0/L6/030033, F0/L3/030, F0/L2/03, +[F0/L30/100000000000000000000000000000, F0/L30/133333333333333333333333333333], +F0/L0/, [F0/L30/210000000000000000000000000000, F0/L30/213333333333333333333333333333], +F0/L1/2, [F0/L30/221100000000000000000000000000, F0/L30/221133333333333333333333333333], +F0/L3/221, F0/L2/22, spilled + +intersects name=line-exceeds-bound +---- +F0/L1/0, F0/L4/0300, F0/L5/03003, F0/L8/03003300, F0/L9/030033003, +F0/L12/030033003300, F0/L13/0300330033003, F0/L16/0300330033003300, +F0/L17/03003300330033003, F0/L20/03003300330033003300, F0/L21/030033003300330033003, +F0/L24/030033003300330033003300, F0/L25/0300330033003300330033003, +F0/L28/0300330033003300330033003300, F0/L29/03003300330033003300330033003, +F0/L30/030033003300330033003300330033, F0/L27/030033003300330033003300330, +F0/L26/03003300330033003300330033, F0/L23/03003300330033003300330, +F0/L22/0300330033003300330033, F0/L19/0300330033003300330, F0/L18/030033003300330033, +F0/L15/030033003300330, F0/L14/03003300330033, F0/L11/03003300330, +F0/L10/0300330033, F0/L7/0300330, F0/L6/030033, F0/L3/030, F0/L2/03, +[F0/L30/100000000000000000000000000000, F0/L30/133333333333333333333333333333], +F0/L0/, [F0/L30/210000000000000000000000000000, F0/L30/213333333333333333333333333333], +F0/L1/2, [F0/L30/221100000000000000000000000000, F0/L30/221133333333333333333333333333], +F0/L3/221, F0/L2/22, spilled + +inner-covering name=line-exceeds-bound +---- +F0/L30/101100110011001100110011001100, F0/L30/221122301000333301033322223010 + +covered-by name=line-exceeds-bound +---- +0: spilled +1: F0/L30/221122301000333301033322223010, F0/L29/22112230100033330103332222301, +F0/L28/2211223010003333010333222230, F0/L27/221122301000333301033322223, +F0/L26/22112230100033330103332222, F0/L25/2211223010003333010333222, +F0/L24/221122301000333301033322, F0/L23/22112230100033330103332, +F0/L22/2211223010003333010333, F0/L21/221122301000333301033, +F0/L20/22112230100033330103, F0/L19/2211223010003333010, F0/L18/221122301000333301, +F0/L17/22112230100033330, F0/L16/2211223010003333, F0/L15/221122301000333, +F0/L14/22112230100033, F0/L13/2211223010003, F0/L12/221122301000, +F0/L11/22112230100, F0/L10/2211223010, F0/L9/221122301, F0/L8/22112230, +F0/L7/2211223, F0/L6/221122, F0/L5/22112, F0/L4/2211, F0/L3/221, +F0/L2/22, F0/L1/2, F0/L0/ +2: F0/L30/101100110011001100110011001100, F0/L29/10110011001100110011001100110, +F0/L28/1011001100110011001100110011, F0/L27/101100110011001100110011001, +F0/L26/10110011001100110011001100, F0/L25/1011001100110011001100110, +F0/L24/101100110011001100110011, F0/L23/10110011001100110011001, +F0/L22/1011001100110011001100, F0/L21/101100110011001100110, +F0/L20/10110011001100110011, F0/L19/1011001100110011001, F0/L18/101100110011001100, +F0/L17/10110011001100110, F0/L16/1011001100110011, F0/L15/101100110011001, +F0/L14/10110011001100, F0/L13/1011001100110, F0/L12/101100110011, +F0/L11/10110011001, F0/L10/1011001100, F0/L9/101100110, F0/L8/10110011, +F0/L7/1011001, F0/L6/101100, F0/L5/10110, F0/L4/1011, F0/L3/101, +F0/L2/10, F0/L1/1, F0/L0/ + +init minlevel=0 maxlevel=4 maxcells=1 minx=10 miny=10 maxx=20 maxy=20 +---- + +inner-covering name=point1 +---- +F0/L4/0022 + +inner-covering name=shortline1 +---- +F0/L4/0002, F0/L4/0022 + +inner-covering name=line-exceeds-bound +---- +F0/L4/1011, F0/L4/2211 + +init minlevel=0 maxlevel=2 maxcells=1 minx=10 miny=10 maxx=20 maxy=20 +---- + +inner-covering name=point1 +---- +F0/L2/00 + +inner-covering name=shortline1 +---- +F0/L2/00 + +inner-covering name=line-exceeds-bound +---- +F0/L2/10, F0/L2/22 + +init minlevel=0 maxlevel=4 maxcells=2 minx=10 miny=10 maxx=20 maxy=20 +---- + +index-keys name=point1 +---- +F0/L4/0022, spilled +BoundingBox: lo_x:12.000000 hi_x:21.000000 lo_y:12.000000 hi_y:21.000000 + +covers name=point1 +---- +F0/L2/00, F0/L3/002, [F0/L30/002200000000000000000000000000, F0/L30/002233333333333333333333333333], +F0/L1/0, F0/L0/, spilled + +d-within distance=1 name=point1 +---- +[F0/L30/000000000000000000000000000000, F0/L30/033333333333333333333333333333], +F0/L0/, spilled + +# See https://github.com/cockroachdb/cockroach/issues/106954 for skip-arm64. +d-within distance=2 name=point1 skip-arm64 +---- +[F0/L30/000000000000000000000000000000, F0/L30/033333333333333333333333333333], +F0/L0/, F0/L1/2, F0/L2/22, [F0/L30/222000000000000000000000000000, F0/L30/222333333333333333333333333333], +spilled + +# Since s2GeometryIndex.{Covers,Intersects} currently have the same implementation, +# the output of d-fully-within is the same as dwithin. + +d-fully-within distance=1 name=point1 +---- +[F0/L30/000000000000000000000000000000, F0/L30/033333333333333333333333333333], +F0/L0/, spilled + +# See https://github.com/cockroachdb/cockroach/issues/106954 for skip-arm64. +d-fully-within distance=2 name=point1 skip-arm64 +---- +[F0/L30/000000000000000000000000000000, F0/L30/033333333333333333333333333333], +F0/L0/, F0/L1/2, F0/L2/22, [F0/L30/222000000000000000000000000000, F0/L30/222333333333333333333333333333], +spilled + +# Empty shapes + +geometry name=point-empty +POINT EMPTY +---- + +index-keys name=point-empty +---- + +covered-by name=point-empty +---- + +covers name=point-empty +---- + +inner-covering name=point-empty +---- + +geometry name=polygon-empty +POLYGON EMPTY +---- + +index-keys name=polygon-empty +---- + +covered-by name=polygon-empty +---- + +covers name=polygon-empty +---- + +inner-covering name=polygon-empty +---- + +geometry name=coll-empty +GEOMETRYCOLLECTION EMPTY +---- + +index-keys name=coll-empty +---- + +covered-by name=coll-empty +---- + +covers name=coll-empty +---- + +inner-covering name=coll-empty +---- + +# Rough bounds for SRID 26918, used in a data set with geometries that triggered +# bad coverings. If we did not fallback to using the bounding box, these coverings +# would be F0/L0/, F1/L0/, F2/L0/, F3/L0/, F4/L0/, F5/L0/. +init minlevel=0 maxlevel=30 maxcells=4 minx=-9102387 miny=680961 maxx=11819889 maxy=9646533 +---- + +geometry name=troubled-poly1 +MULTIPOLYGON (((586857.9475235254503786563873 4500244.4056084547191858291626, 586890.2225210913456976413727 4500184.1842294149100780487061, 586836.4119185773888602852821 4500155.4893597159534692764282, 586828.247425784822553396225 4500149.819862429983913898468, 586828.2344921003095805644989 4500149.8106948612257838249207, 586828.219571095774881541729 4500149.8045549383386969566345, 586818.8969909736188128590584 4500145.6565522141754627227783, 586818.8891138497274369001389 4500145.6525010699406266212463, 586818.8811029464704915881157 4500145.6504012979567050933838, 586808.8418830885784700512886 4500143.3347355769947171211243, 586808.8288800554582849144936 4500143.3316676989197731018066, 586808.8138911846326664090157 4500143.3314972892403602600098, 586798.6254629108589142560959 4500142.9746762989088892936707, 586798.606457170913927257061 4500142.9744602208957076072693, 586798.5874725470785051584244 4500142.9772946210578083992004, 586788.7675613947212696075439 4500144.4972309721633791923523, 586773.9053534916602075099945 4500232.9436314916238188743591, 586843.7861474975943565368652 4500270.3302134787663817405701, 586857.9475235254503786563873 4500244.4056084547191858291626))) +---- + +geometry name=troubled-poly2 +MULTIPOLYGON (((592585.8247029320336878299713 4527600.0504941008985042572021, 592570.350101865711621940136 4527506.2746569896116852760315, 592565.788726658443920314312 4527515.0605620192363858222961, 592559.4958810925018042325974 4527523.4817722812294960021973, 592551.4831399817485362291336 4527531.0544084021821618080139, 592541.9410741194151341915131 4527537.281961767934262752533, 592531.242556162993423640728 4527541.7387133436277508735657, 592519.9064861471997573971748 4527544.1516027571633458137512, 592508.5146641617175191640854 4527544.4512635143473744392395, 592497.6182677873875945806503 4527542.7740788338705897331238, 592487.654849782935343682766 4527539.4209095872938632965088, 592478.9087759252870455384254 4527534.7839743737131357192993, 592426.5560151999816298484802 4527507.3111610254272818565369, 592391.1302773362258449196815 4527487.8251397302374243736267, 592380.5234924985561519861221 4527483.903793333098292350769, 592380.5135518473107367753983 4527483.8996622655540704727173, 592380.503569008316844701767 4527483.8975952444598078727722, 592369.0481809278717264533043 4527481.7931765085086226463318, 592369.0362646455178037285805 4527481.7910315934568643569946, 592369.0252366602653637528419 4527481.7909055659547448158264, 592357.2036226189229637384415 4527481.7277858173474669456482, 592357.1816224402282387018204 4527481.7275343947112560272217, 592357.1595949504990130662918 4527481.7313001239672303199768, 592345.5120095876045525074005 4527483.7616681559011340141296, 592345.5029671189840883016586 4527483.7635735515505075454712, 592345.4939884119667112827301 4527483.7664096402004361152649, 592334.5448844350175932049751 4527487.7513697473332285881042, 592334.5358300651423633098602 4527487.7543165665119886398315, 592334.5267987564438953995705 4527487.760127955116331577301, 592324.7216157623333856463432 4527493.3917804230004549026489, 592327.3960505314171314239502 4527512.0222137765958905220032, 592331.7675609502475708723068 4527530.52607412729412317276, 592337.8227386228973045945168 4527548.6721113082021474838257, 592345.5072859587380662560463 4527566.2330158911645412445068, 592354.7286294148070737719536 4527582.9927399819716811180115, 592365.3550374577753245830536 4527598.7585767218843102455139, 592377.2264194176532328128815 4527613.3650593571364879608154, 592387.9781726722139865159988 4527629.3583310637623071670532, 592452.8652935600839555263519 4527675.0458535477519035339355, 592492.8839034214615821838379 4527688.4662118805572390556335, 592592.2261793565703555941582 4527693.3977307192981243133545, 592585.8247029320336878299713 4527600.0504941008985042572021))) +---- + +index-keys name=troubled-poly1 +---- +F0/L17/02213031322200022, F0/L18/022130313222001333, F0/L16/0221303132220020, F0/L16/0221303132220031 +BoundingBox: lo_x:586773.905353 hi_x:586890.222521 lo_y:4500142.974460 hi_y:4500270.330213 + +index-keys name=troubled-poly2 +---- +F0/L16/0221303121021212, F0/L16/0221303121021221, F0/L16/0221303121312112, F0/L16/0221303121312121 +BoundingBox: lo_x:592324.721616 hi_x:592592.226179 lo_y:4527481.727534 hi_y:4527693.397731 + +# Index tests for geometries with Z/M coordinates. + +geometry name=3d-line-exceeds-bounds +LINESTRING Z (-1126040592.69383 164687857.663771 -4874040605.86647,-1096532142.92787 -5330670539.80611 7585243782.68975,9489439383.13689 6909804458.78381 8297630805.37724,-6339483118.5989 1323767903.77216 -7534611690.78481) +---- + +init minlevel=0 maxlevel=30 maxcells=4 minx=-2104533975 miny=-2104533975 maxx=2104533975 maxy=2104533975 +---- + +index-keys name=3d-line-exceeds-bounds +---- +F0/L2/00, F0/L3/030, F0/L3/031, F0/L4/1011, F0/L4/3330, F0/L7/3332011, F0/L5/33323, F0/L5/33331, spilled +BoundingBox: lo_x:-6339483118.598900 hi_x:9489439383.136890 lo_y:-5330670539.806110 hi_y:6909804458.783810 diff --git a/pkg/geo/geomfn/add_measure.go b/pkg/geo/geomfn/add_measure.go new file mode 100644 index 0000000..a2380cb --- /dev/null +++ b/pkg/geo/geomfn/add_measure.go @@ -0,0 +1,127 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/twpayne/go-geom" +) + +// AddMeasure takes a LineString or MultiLineString and linearly interpolates measure values for each line. +func AddMeasure(geometry geo.Geometry, start float64, end float64) (geo.Geometry, error) { + t, err := geometry.AsGeomT() + if err != nil { + return geometry, err + } + + switch t := t.(type) { + case *geom.LineString: + newLineString, err := addMeasureToLineString(t, start, end) + if err != nil { + return geometry, err + } + return geo.MakeGeometryFromGeomT(newLineString) + case *geom.MultiLineString: + newMultiLineString, err := addMeasureToMultiLineString(t, start, end) + if err != nil { + return geometry, err + } + return geo.MakeGeometryFromGeomT(newMultiLineString) + default: + // Ideally we should return NULL here, but following PostGIS on this. + return geometry, pgerror.Newf(pgcode.InvalidParameterValue, "input geometry must be LINESTRING or MULTILINESTRING") + } +} + +// addMeasureToMultiLineString takes a MultiLineString and linearly interpolates measure values for each component line. +func addMeasureToMultiLineString( + multiLineString *geom.MultiLineString, start float64, end float64, +) (*geom.MultiLineString, error) { + newMultiLineString := + geom.NewMultiLineString(augmentLayoutWithM(multiLineString.Layout())).SetSRID(multiLineString.SRID()) + + // Create a copy of the MultiLineString with measures added to each component LineString. + for i := 0; i < multiLineString.NumLineStrings(); i++ { + newLineString, err := addMeasureToLineString(multiLineString.LineString(i), start, end) + if err != nil { + return multiLineString, err + } + err = newMultiLineString.Push(newLineString) + if err != nil { + return multiLineString, err + } + } + + return newMultiLineString, nil +} + +// addMeasureToLineString takes a LineString and linearly interpolates measure values. +func addMeasureToLineString( + lineString *geom.LineString, start float64, end float64, +) (*geom.LineString, error) { + newLineString := geom.NewLineString(augmentLayoutWithM(lineString.Layout())).SetSRID(lineString.SRID()) + + if lineString.Empty() { + return newLineString, nil + } + + // Extract the line's current points. + lineCoords := lineString.Coords() + + // Compute the length of the line as the sum of the distances between each pair of points. + // Also, fill in pointMeasures with the partial sums. + prevPoint := lineCoords[0] + lineLength := float64(0) + pointMeasures := make([]float64, lineString.NumCoords()) + for i := 0; i < lineString.NumCoords(); i++ { + curPoint := lineCoords[i] + distBetweenPoints := coordNorm(coordSub(prevPoint, curPoint)) + lineLength += distBetweenPoints + pointMeasures[i] = lineLength + prevPoint = curPoint + } + + // Compute the measures for each point. + for i := 0; i < lineString.NumCoords(); i++ { + // Handle special case where line is zero length. + if lineLength == 0 { + pointMeasures[i] = start + (end-start)*(float64(i)/float64(lineString.NumCoords()-1)) + } else { + pointMeasures[i] = start + (end-start)*(pointMeasures[i]/lineLength) + } + } + + // Replace M value if it exists, otherwise append it to each Coord. + for i := 0; i < lineString.NumCoords(); i++ { + if lineString.Layout().MIndex() == -1 { + lineCoords[i] = append(lineCoords[i], pointMeasures[i]) + } else { + lineCoords[i][lineString.Layout().MIndex()] = pointMeasures[i] + } + } + + // Create a new LineString with the measures tacked on. + _, err := newLineString.SetCoords(lineCoords) + if err != nil { + return lineString, err + } + + return newLineString, nil +} + +// augmentLayoutWithM takes a layout and returns a layout with the M dimension added. +func augmentLayoutWithM(layout geom.Layout) geom.Layout { + switch layout { + case geom.XY, geom.XYM: + return geom.XYM + case geom.XYZ, geom.XYZM: + return geom.XYZM + default: + return layout + } +} diff --git a/pkg/geo/geomfn/affine_transforms.go b/pkg/geo/geomfn/affine_transforms.go new file mode 100644 index 0000000..fe6cbb1 --- /dev/null +++ b/pkg/geo/geomfn/affine_transforms.go @@ -0,0 +1,347 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "math" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/errors" + "github.com/twpayne/go-geom" +) + +// AffineMatrix defines an affine transformation matrix for a geom object. +// It is expected to be of the form: +// +// a b c x_off +// d e f y_off +// g h i z_off +// 0 0 0 1 +// +// Which gets applies onto a coordinate of form: +// +// (x y z 0)^T +// +// With the following transformation: +// +// x' = a*x + b*y + c*z + x_off +// y' = d*x + e*y + f*z + y_off +// z' = g*x + h*y + i*z + z_off +type AffineMatrix [][]float64 + +// Affine applies a 3D affine transformation onto the given geometry. +// See: https://en.wikipedia.org/wiki/Affine_transformation. +func Affine(g geo.Geometry, m AffineMatrix) (geo.Geometry, error) { + if g.Empty() { + return g, nil + } + + t, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + + newT, err := affine(t, m) + if err != nil { + return geo.Geometry{}, err + } + return geo.MakeGeometryFromGeomT(newT) +} + +func affine(t geom.T, m AffineMatrix) (geom.T, error) { + return applyOnCoordsForGeomT(t, func(l geom.Layout, dst, src []float64) error { + var z float64 + if l.ZIndex() != -1 { + z = src[l.ZIndex()] + } + newX := m[0][0]*src[0] + m[0][1]*src[1] + m[0][2]*z + m[0][3] + newY := m[1][0]*src[0] + m[1][1]*src[1] + m[1][2]*z + m[1][3] + newZ := m[2][0]*src[0] + m[2][1]*src[1] + m[2][2]*z + m[2][3] + + dst[0] = newX + dst[1] = newY + if l.ZIndex() != -1 { + dst[2] = newZ + } + if l.MIndex() != -1 { + dst[l.MIndex()] = src[l.MIndex()] + } + return nil + }) +} + +// Translate returns a modified Geometry whose coordinates are incremented +// or decremented by the deltas. +func Translate(g geo.Geometry, deltas []float64) (geo.Geometry, error) { + if g.Empty() { + return g, nil + } + + t, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + + newT, err := translate(t, deltas) + if err != nil { + return geo.Geometry{}, err + } + return geo.MakeGeometryFromGeomT(newT) +} + +func translate(t geom.T, deltas []float64) (geom.T, error) { + if t.Layout().Stride() != len(deltas) { + err := geom.ErrStrideMismatch{ + Got: len(deltas), + Want: t.Layout().Stride(), + } + return nil, pgerror.WithCandidateCode( + errors.Wrap(err, "translating coordinates"), + pgcode.InvalidParameterValue, + ) + } + var zOff float64 + if t.Layout().ZIndex() != -1 { + zOff = deltas[t.Layout().ZIndex()] + } + return affine( + t, + AffineMatrix([][]float64{ + {1, 0, 0, deltas[0]}, + {0, 1, 0, deltas[1]}, + {0, 0, 1, zOff}, + {0, 0, 0, 1}, + }), + ) +} + +// Scale returns a modified Geometry whose coordinates are multiplied by the factors. +// REQUIRES: len(factors) >= 2. +func Scale(g geo.Geometry, factors []float64) (geo.Geometry, error) { + var zFactor float64 + if len(factors) > 2 { + zFactor = factors[2] + } + return Affine( + g, + AffineMatrix([][]float64{ + {factors[0], 0, 0, 0}, + {0, factors[1], 0, 0}, + {0, 0, zFactor, 0}, + {0, 0, 0, 1}, + }), + ) +} + +// ScaleRelativeToOrigin returns a modified Geometry whose coordinates are +// multiplied by the factors relative to the origin. +func ScaleRelativeToOrigin( + g geo.Geometry, factor geo.Geometry, origin geo.Geometry, +) (geo.Geometry, error) { + if g.Empty() { + return g, nil + } + + t, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + + factorG, err := factor.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + + factorPointG, ok := factorG.(*geom.Point) + if !ok { + return geo.Geometry{}, pgerror.Newf(pgcode.InvalidParameterValue, "the scaling factor must be a Point") + } + + originG, err := origin.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + + originPointG, ok := originG.(*geom.Point) + if !ok { + return geo.Geometry{}, pgerror.Newf(pgcode.InvalidParameterValue, "the false origin must be a Point") + } + + if factorG.Stride() != originG.Stride() { + err := geom.ErrStrideMismatch{ + Got: factorG.Stride(), + Want: originG.Stride(), + } + return geo.Geometry{}, pgerror.WithCandidateCode( + errors.Wrap(err, "number of dimensions for the scaling factor and origin must be equal"), + pgcode.InvalidParameterValue, + ) + } + + // This is inconsistent with PostGIS, which allows a POINT EMPTY, but whose + // behavior seems to depend on previous queries in the session, and not + // desirable to reproduce. + if len(originPointG.FlatCoords()) < 2 { + return geo.Geometry{}, pgerror.Newf(pgcode.InvalidParameterValue, "the origin must have at least 2 coordinates") + } + + // Offset by the origin, scale, and translate it back to the origin. + offsetDeltas := make([]float64, 0, 3) + offsetDeltas = append(offsetDeltas, -originPointG.X(), -originPointG.Y()) + if originG.Layout().ZIndex() != -1 { + offsetDeltas = append(offsetDeltas, -originPointG.Z()) + } + retT, err := translate(t, offsetDeltas) + if err != nil { + return geo.Geometry{}, err + } + + var xFactor, yFactor, zFactor float64 + zFactor = 1 + if len(factorPointG.FlatCoords()) < 2 { + // POINT EMPTY results in factors of 0, similar to PostGIS. + xFactor, yFactor = 0, 0 + } else { + xFactor, yFactor = factorPointG.X(), factorPointG.Y() + if factorPointG.Layout().ZIndex() != -1 { + zFactor = factorPointG.Z() + } + } + + retT, err = affine( + retT, + AffineMatrix([][]float64{ + {xFactor, 0, 0, 0}, + {0, yFactor, 0, 0}, + {0, 0, zFactor, 0}, + {0, 0, 0, 1}, + }), + ) + if err != nil { + return geo.Geometry{}, err + } + + for i := range offsetDeltas { + offsetDeltas[i] = -offsetDeltas[i] + } + retT, err = translate(retT, offsetDeltas) + if err != nil { + return geo.Geometry{}, err + } + + return geo.MakeGeometryFromGeomT(retT) +} + +// Rotate returns a modified Geometry whose coordinates are rotated +// around the origin by a rotation angle. +func Rotate(g geo.Geometry, rotRadians float64) (geo.Geometry, error) { + return Affine( + g, + AffineMatrix([][]float64{ + {math.Cos(rotRadians), -math.Sin(rotRadians), 0, 0}, + {math.Sin(rotRadians), math.Cos(rotRadians), 0, 0}, + {0, 0, 1, 0}, + {0, 0, 0, 1}, + }), + ) +} + +// ErrPointOriginEmpty is an error to compare point origin is empty. +var ErrPointOriginEmpty = pgerror.Newf(pgcode.InvalidParameterValue, "origin is an empty point") + +// RotateWithPointOrigin returns a modified Geometry whose coordinates are rotated +// around the pointOrigin by a rotRadians. +func RotateWithPointOrigin( + g geo.Geometry, rotRadians float64, pointOrigin geo.Geometry, +) (geo.Geometry, error) { + if pointOrigin.ShapeType() != geopb.ShapeType_Point { + return g, pgerror.Newf(pgcode.InvalidParameterValue, "origin is not a POINT") + } + t, err := pointOrigin.AsGeomT() + if err != nil { + return g, err + } + if t.Empty() { + return g, ErrPointOriginEmpty + } + + return RotateWithXY(g, rotRadians, t.FlatCoords()[0], t.FlatCoords()[1]) +} + +// RotateWithXY returns a modified Geometry whose coordinates are rotated +// around the X and Y by a rotRadians. +func RotateWithXY(g geo.Geometry, rotRadians, x, y float64) (geo.Geometry, error) { + cos, sin := math.Cos(rotRadians), math.Sin(rotRadians) + return Affine( + g, + AffineMatrix{ + {cos, -sin, 0, x - x*cos + y*sin}, + {sin, cos, 0, y - x*sin - y*cos}, + {0, 0, 1, 0}, + {0, 0, 0, 1}, + }, + ) +} + +// RotateX returns a modified Geometry whose coordinates are rotated about +// the X axis by rotRadians +func RotateX(g geo.Geometry, rotRadians float64) (geo.Geometry, error) { + cos, sin := math.Cos(rotRadians), math.Sin(rotRadians) + return Affine( + g, + AffineMatrix{ + {1, 0, 0, 0}, + {0, cos, -sin, 0}, + {0, sin, cos, 0}, + {0, 0, 0, 1}, + }, + ) +} + +// RotateY returns a modified Geometry whose coordinates are rotated about +// the Y axis by rotRadians +func RotateY(g geo.Geometry, rotRadians float64) (geo.Geometry, error) { + cos, sin := math.Cos(rotRadians), math.Sin(rotRadians) + return Affine( + g, + AffineMatrix{ + {cos, 0, sin, 0}, + {0, 1, 0, 0}, + {-sin, 0, cos, 0}, + {0, 0, 0, 1}, + }, + ) +} + +// RotateZ returns a modified Geometry whose coordinates are rotated about +// the Z axis by rotRadians +func RotateZ(g geo.Geometry, rotRadians float64) (geo.Geometry, error) { + cos, sin := math.Cos(rotRadians), math.Sin(rotRadians) + return Affine( + g, + AffineMatrix{ + {cos, -sin, 0, 0}, + {sin, cos, 0, 0}, + {0, 0, 1, 0}, + {0, 0, 0, 1}, + }, + ) +} + +// TransScale returns a modified Geometry whose coordinates are +// translate by deltaX and deltaY and scale by xFactor and yFactor +func TransScale(g geo.Geometry, deltaX, deltaY, xFactor, yFactor float64) (geo.Geometry, error) { + return Affine(g, + AffineMatrix([][]float64{ + {xFactor, 0, 0, xFactor * deltaX}, + {0, yFactor, 0, yFactor * deltaY}, + {0, 0, 1, 0}, + {0, 0, 0, 1}, + })) +} diff --git a/pkg/geo/geomfn/angle.go b/pkg/geo/geomfn/angle.go new file mode 100644 index 0000000..1b230ce --- /dev/null +++ b/pkg/geo/geomfn/angle.go @@ -0,0 +1,116 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "math" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/twpayne/go-geom" +) + +// Angle calculates the clockwise angle between two vectors given by points +// p1,p2 and p3,p4. If p4 is an empty geometry (of any type, to follow PostGIS +// behavior), it instead calculates the clockwise angle between p2,p1 and p2,p3. +func Angle(g1, g2, g3, g4 geo.Geometry) (*float64, error) { + if g4.Empty() { + g1, g2, g3, g4 = g2, g1, g2, g3 + } + + if g1.SRID() != g2.SRID() { + return nil, geo.NewMismatchingSRIDsError(g1.SpatialObject(), g2.SpatialObject()) + } + if g1.SRID() != g3.SRID() { + return nil, geo.NewMismatchingSRIDsError(g1.SpatialObject(), g3.SpatialObject()) + } + if g1.SRID() != g4.SRID() { + return nil, geo.NewMismatchingSRIDsError(g1.SpatialObject(), g4.SpatialObject()) + } + + t1, err := g1.AsGeomT() + if err != nil { + return nil, err + } + t2, err := g2.AsGeomT() + if err != nil { + return nil, err + } + t3, err := g3.AsGeomT() + if err != nil { + return nil, err + } + t4, err := g4.AsGeomT() + if err != nil { + return nil, err + } + + p1, p1ok := t1.(*geom.Point) + p2, p2ok := t2.(*geom.Point) + p3, p3ok := t3.(*geom.Point) + p4, p4ok := t4.(*geom.Point) + + if !p1ok || !p2ok || !p3ok || !p4ok { + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "arguments must be POINT geometries") + } + if p1.Empty() || p2.Empty() || p3.Empty() || p4.Empty() { + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "received EMPTY geometry") + } + + return angleFromCoords(p1.Coords(), p2.Coords(), p3.Coords(), p4.Coords()), nil +} + +// AngleLineString calculates the clockwise angle between two linestrings, +// treating them as vectors between the start- and endpoints. Type conflicts +// or empty geometries return nil (as opposed to Angle which errors), to +// follow PostGIS behavior. +func AngleLineString(g1, g2 geo.Geometry) (*float64, error) { + if g1.SRID() != g2.SRID() { + return nil, geo.NewMismatchingSRIDsError(g1.SpatialObject(), g2.SpatialObject()) + } + t1, err := g1.AsGeomT() + if err != nil { + return nil, err + } + t2, err := g2.AsGeomT() + if err != nil { + return nil, err + } + l1, l1ok := t1.(*geom.LineString) + l2, l2ok := t2.(*geom.LineString) + if !l1ok || !l2ok || l1.Empty() || l2.Empty() { + return nil, nil // follow PostGIS behavior + } + return angleFromCoords( + l1.Coord(0), l1.Coord(l1.NumCoords()-1), l2.Coord(0), l2.Coord(l2.NumCoords()-1)), nil +} + +// angleFromCoords returns the clockwise angle between the vectors c1,c2 and +// c3,c4. For compatibility with PostGIS, it returns nil if any vectors have +// length 0. +func angleFromCoords(c1, c2, c3, c4 geom.Coord) *float64 { + a := coordSub(c2, c1) + b := coordSub(c4, c3) + if (a.X() == 0 && a.Y() == 0) || (b.X() == 0 && b.Y() == 0) { + return nil + } + // We want the clockwise angle, not the smallest interior angle, so can't use cosine formula. + angle := math.Atan2(-coordDet(a, b), coordDot(a, b)) + // We want the angle in the interval [0,2π), while Atan2 returns [-π,π] + // + // NB: In Go, the literal -0.0 does not produce negative + // zero. However, since IEEE 754 requires that -0.0 == 0.0, + // this code is still correct and we use -0.0 here for + // semantic clarity. + //lint:ignore SA4026 -0.0 used here for clarity + if angle == -0.0 { + angle = 0.0 + } else if angle < 0 { + angle += 2 * math.Pi + } + return &angle +} diff --git a/pkg/geo/geomfn/azimuth.go b/pkg/geo/geomfn/azimuth.go new file mode 100644 index 0000000..a8ea6b3 --- /dev/null +++ b/pkg/geo/geomfn/azimuth.go @@ -0,0 +1,63 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "math" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/twpayne/go-geom" +) + +// Azimuth returns the azimuth in radians of the segment defined by the given point geometries, +// where point a is the reference point. +// The reference direction from which the azimuth is calculated is north, and is positive clockwise. +// i.e. North = 0; East = π/2; South = π; West = 3π/2. +// See https://en.wikipedia.org/wiki/Polar_coordinate_system. +// Returns nil if the two points are the same. +// Returns an error if any of the two Geometry items are not points. +func Azimuth(a geo.Geometry, b geo.Geometry) (*float64, error) { + if a.SRID() != b.SRID() { + return nil, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + + aGeomT, err := a.AsGeomT() + if err != nil { + return nil, err + } + + aPoint, ok := aGeomT.(*geom.Point) + if !ok { + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "arguments must be POINT geometries") + } + + bGeomT, err := b.AsGeomT() + if err != nil { + return nil, err + } + + bPoint, ok := bGeomT.(*geom.Point) + if !ok { + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "arguments must be POINT geometries") + } + + if aPoint.Empty() || bPoint.Empty() { + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "cannot call ST_Azimuth with POINT EMPTY") + } + + if aPoint.X() == bPoint.X() && aPoint.Y() == bPoint.Y() { + return nil, nil + } + + atan := math.Atan2(bPoint.Y()-aPoint.Y(), bPoint.X()-aPoint.X()) + // math.Pi / 2 is North from the atan calculation this is a CCW direction. + // We want to return a CW direction, so subtract atan from math.Pi / 2 to get it into a CW direction. + // Then add 2*math.Pi to ensure a positive azimuth. + azimuth := math.Mod(math.Pi/2-atan+2*math.Pi, 2*math.Pi) + return &azimuth, nil +} diff --git a/pkg/geo/geomfn/bench.sh b/pkg/geo/geomfn/bench.sh new file mode 100755 index 0000000..c1bf87c --- /dev/null +++ b/pkg/geo/geomfn/bench.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +# Copyright 2021 The Cockroach Authors. +# +# Use of this software is governed by the CockroachDB Software License +# included in the /LICENSE file. + +set -eux + +if [ $# -eq 0 ] || [ "$1" == "" ]; then + echo "Usage: ./bench.sh [output-name]" + exit 1 +fi + +echo "output file: $1.txt" + +for i in {1..10} +do + go test -bench=. >> $1.txt.tmp +done + +sed -n '/^Benchmark/p' $1.txt.tmp > $1.txt +rm $1.txt.tmp diff --git a/pkg/geo/geomfn/binary_predicates.go b/pkg/geo/geomfn/binary_predicates.go new file mode 100644 index 0000000..36b00d1 --- /dev/null +++ b/pkg/geo/geomfn/binary_predicates.go @@ -0,0 +1,337 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geos" + "github.com/cockroachdb/errors" + "github.com/twpayne/go-geom" +) + +// Covers returns whether geometry A covers geometry B. +func Covers(a geo.Geometry, b geo.Geometry) (bool, error) { + if a.SRID() != b.SRID() { + return false, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + if !a.CartesianBoundingBox().Covers(b.CartesianBoundingBox()) { + return false, nil + } + + // Optimization for point in polygon calculations. + pointPolygonPair, pointKind, polygonKind := PointKindAndPolygonKind(a, b) + switch pointPolygonPair { + case PointAndPolygon: + // A point cannot cover a polygon. + return false, nil + case PolygonAndPoint: + // Computing whether a polygon covers a point is equivalent + // to computing whether the point is covered by the polygon. + return PointKindCoveredByPolygonKind(pointKind, polygonKind) + } + + return geos.Covers(a.EWKB(), b.EWKB()) +} + +// CoveredBy returns whether geometry A is covered by geometry B. +func CoveredBy(a geo.Geometry, b geo.Geometry) (bool, error) { + if a.SRID() != b.SRID() { + return false, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + if !b.CartesianBoundingBox().Covers(a.CartesianBoundingBox()) { + return false, nil + } + + // Optimization for point in polygon calculations. + pointPolygonPair, pointKind, polygonKind := PointKindAndPolygonKind(a, b) + switch pointPolygonPair { + case PolygonAndPoint: + // A polygon cannot be covered by a point. + return false, nil + case PointAndPolygon: + return PointKindCoveredByPolygonKind(pointKind, polygonKind) + } + + return geos.CoveredBy(a.EWKB(), b.EWKB()) +} + +// Contains returns whether geometry A contains geometry B. +func Contains(a geo.Geometry, b geo.Geometry) (bool, error) { + if a.SRID() != b.SRID() { + return false, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + if !a.CartesianBoundingBox().Covers(b.CartesianBoundingBox()) { + return false, nil + } + + // Optimization for point in polygon calculations. + pointPolygonPair, pointKind, polygonKind := PointKindAndPolygonKind(a, b) + switch pointPolygonPair { + case PointAndPolygon: + // A point cannot contain a polygon. + return false, nil + case PolygonAndPoint: + // Computing whether a polygon contains a point is equivalent + // to computing whether the point is contained within the polygon. + return PointKindWithinPolygonKind(pointKind, polygonKind) + } + + return geos.Contains(a.EWKB(), b.EWKB()) +} + +// ContainsProperly returns whether geometry A properly contains geometry B. +func ContainsProperly(a geo.Geometry, b geo.Geometry) (bool, error) { + if a.SRID() != b.SRID() { + return false, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + if !a.CartesianBoundingBox().Covers(b.CartesianBoundingBox()) { + return false, nil + } + return geos.RelatePattern(a.EWKB(), b.EWKB(), "T**FF*FF*") +} + +// Crosses returns whether geometry A crosses geometry B. +func Crosses(a geo.Geometry, b geo.Geometry) (bool, error) { + if a.SRID() != b.SRID() { + return false, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + if !a.CartesianBoundingBox().Intersects(b.CartesianBoundingBox()) { + return false, nil + } + return geos.Crosses(a.EWKB(), b.EWKB()) +} + +// Disjoint returns whether geometry A is disjoint from geometry B. +func Disjoint(a geo.Geometry, b geo.Geometry) (bool, error) { + if a.SRID() != b.SRID() { + return false, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + return geos.Disjoint(a.EWKB(), b.EWKB()) +} + +// Equals returns whether geometry A equals geometry B. +func Equals(a geo.Geometry, b geo.Geometry) (bool, error) { + if a.SRID() != b.SRID() { + return false, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + // Empty items are equal to each other. + // Do this check before the BoundingBoxIntersects check, as we would otherwise + // return false. + if a.Empty() && b.Empty() { + return true, nil + } + if !a.CartesianBoundingBox().Covers(b.CartesianBoundingBox()) { + return false, nil + } + return geos.Equals(a.EWKB(), b.EWKB()) +} + +// Intersects returns whether geometry A intersects geometry B. +func Intersects(a geo.Geometry, b geo.Geometry) (bool, error) { + if a.SRID() != b.SRID() { + return false, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + if !a.CartesianBoundingBox().Intersects(b.CartesianBoundingBox()) { + return false, nil + } + + // Optimization for point in polygon calculations. + pointPolygonPair, pointKind, polygonKind := PointKindAndPolygonKind(a, b) + switch pointPolygonPair { + case PointAndPolygon, PolygonAndPoint: + return PointKindIntersectsPolygonKind(pointKind, polygonKind) + } + + return geos.Intersects(a.EWKB(), b.EWKB()) +} + +// OrderingEquals returns whether geometry A is equal to B, with all constituents +// and coordinates in the same order. +func OrderingEquals(a, b geo.Geometry) (bool, error) { + if a.SRID() != b.SRID() { + return false, nil + } + aBox, bBox := a.CartesianBoundingBox(), b.CartesianBoundingBox() + switch { + case aBox == nil && bBox == nil: + case aBox == nil || bBox == nil: + return false, nil + case aBox.Compare(bBox) != 0: + return false, nil + } + + geomA, err := a.AsGeomT() + if err != nil { + return false, err + } + geomB, err := b.AsGeomT() + if err != nil { + return false, err + } + return orderingEqualsFromGeomT(geomA, geomB) +} + +// orderingEqualsFromGeomT returns whether geometry A is equal to B. +func orderingEqualsFromGeomT(a, b geom.T) (bool, error) { + if a.Layout() != b.Layout() { + return false, nil + } + switch a := a.(type) { + case *geom.Point: + if b, ok := b.(*geom.Point); ok { + // Point.Coords() panics on empty points + switch { + case a.Empty() && b.Empty(): + return true, nil + case a.Empty() || b.Empty(): + return false, nil + default: + return a.Coords().Equal(b.Layout(), b.Coords()), nil + } + } + case *geom.LineString: + if b, ok := b.(*geom.LineString); ok && a.NumCoords() == b.NumCoords() { + for i := 0; i < a.NumCoords(); i++ { + if !a.Coord(i).Equal(b.Layout(), b.Coord(i)) { + return false, nil + } + } + return true, nil + } + case *geom.Polygon: + if b, ok := b.(*geom.Polygon); ok && a.NumLinearRings() == b.NumLinearRings() { + for i := 0; i < a.NumLinearRings(); i++ { + for j := 0; j < a.LinearRing(i).NumCoords(); j++ { + if !a.LinearRing(i).Coord(j).Equal(b.Layout(), b.LinearRing(i).Coord(j)) { + return false, nil + } + } + } + return true, nil + } + case *geom.MultiPoint: + if b, ok := b.(*geom.MultiPoint); ok && a.NumPoints() == b.NumPoints() { + for i := 0; i < a.NumPoints(); i++ { + if eq, err := orderingEqualsFromGeomT(a.Point(i), b.Point(i)); err != nil || !eq { + return false, err + } + } + return true, nil + } + case *geom.MultiLineString: + if b, ok := b.(*geom.MultiLineString); ok && a.NumLineStrings() == b.NumLineStrings() { + for i := 0; i < a.NumLineStrings(); i++ { + if eq, err := orderingEqualsFromGeomT(a.LineString(i), b.LineString(i)); err != nil || !eq { + return false, err + } + } + return true, nil + } + case *geom.MultiPolygon: + if b, ok := b.(*geom.MultiPolygon); ok && a.NumPolygons() == b.NumPolygons() { + for i := 0; i < a.NumPolygons(); i++ { + if eq, err := orderingEqualsFromGeomT(a.Polygon(i), b.Polygon(i)); err != nil || !eq { + return false, err + } + } + return true, nil + } + case *geom.GeometryCollection: + if b, ok := b.(*geom.GeometryCollection); ok && a.NumGeoms() == b.NumGeoms() { + for i := 0; i < a.NumGeoms(); i++ { + if eq, err := orderingEqualsFromGeomT(a.Geom(i), b.Geom(i)); err != nil || !eq { + return false, err + } + } + return true, nil + } + default: + return false, errors.AssertionFailedf("unknown geometry type: %T", a) + } + return false, nil +} + +// Overlaps returns whether geometry A overlaps geometry B. +func Overlaps(a geo.Geometry, b geo.Geometry) (bool, error) { + if a.SRID() != b.SRID() { + return false, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + if !a.CartesianBoundingBox().Intersects(b.CartesianBoundingBox()) { + return false, nil + } + return geos.Overlaps(a.EWKB(), b.EWKB()) +} + +// PointPolygonOrder represents the order of a point and a polygon +// in an ordered pair of geometries. +type PointPolygonOrder int + +const ( + // NotPointAndPolygon signifies that a pair of geometries is + // not a point and a polygon. + NotPointAndPolygon PointPolygonOrder = iota + // PointAndPolygon signifies that the point appears first + // in an ordered pair of a point and a polygon. + PointAndPolygon + // PolygonAndPoint signifies that the polygon appears first + // in an ordered pair of a point and a polygon. + PolygonAndPoint +) + +// PointKindAndPolygonKind returns whether a pair of geometries contains +// a (multi)point and a (multi)polygon. It is used to determine if the +// point in polygon optimization can be applied. +func PointKindAndPolygonKind( + a geo.Geometry, b geo.Geometry, +) (PointPolygonOrder, geo.Geometry, geo.Geometry) { + switch a.ShapeType2D() { + case geopb.ShapeType_Point, geopb.ShapeType_MultiPoint: + switch b.ShapeType2D() { + case geopb.ShapeType_Polygon, geopb.ShapeType_MultiPolygon: + return PointAndPolygon, a, b + } + case geopb.ShapeType_Polygon, geopb.ShapeType_MultiPolygon: + switch b.ShapeType2D() { + case geopb.ShapeType_Point, geopb.ShapeType_MultiPoint: + return PolygonAndPoint, b, a + } + } + return NotPointAndPolygon, a, b +} + +// Touches returns whether geometry A touches geometry B. +func Touches(a geo.Geometry, b geo.Geometry) (bool, error) { + if a.SRID() != b.SRID() { + return false, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + if !a.CartesianBoundingBox().Intersects(b.CartesianBoundingBox()) { + return false, nil + } + return geos.Touches(a.EWKB(), b.EWKB()) +} + +// Within returns whether geometry A is within geometry B. +func Within(a geo.Geometry, b geo.Geometry) (bool, error) { + if a.SRID() != b.SRID() { + return false, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + if !b.CartesianBoundingBox().Covers(a.CartesianBoundingBox()) { + return false, nil + } + + // Optimization for point in polygon calculations. + pointPolygonPair, pointKind, polygonKind := PointKindAndPolygonKind(a, b) + switch pointPolygonPair { + case PolygonAndPoint: + // A polygon cannot be contained within a point. + return false, nil + case PointAndPolygon: + return PointKindWithinPolygonKind(pointKind, polygonKind) + } + + return geos.Within(a.EWKB(), b.EWKB()) +} diff --git a/pkg/geo/geomfn/buffer.go b/pkg/geo/geomfn/buffer.go new file mode 100644 index 0000000..20e77be --- /dev/null +++ b/pkg/geo/geomfn/buffer.go @@ -0,0 +1,148 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "strconv" + "strings" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geos" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/errors" +) + +// BufferParams is a wrapper around the geos.BufferParams. +type BufferParams struct { + p geos.BufferParams +} + +// MakeDefaultBufferParams returns the default BufferParams/ +func MakeDefaultBufferParams() BufferParams { + return BufferParams{ + p: geos.BufferParams{ + EndCapStyle: geos.BufferParamsEndCapStyleRound, + JoinStyle: geos.BufferParamsJoinStyleRound, + SingleSided: false, + QuadrantSegments: 8, + MitreLimit: 5.0, + }, + } +} + +// WithQuadrantSegments returns a copy of the BufferParams with the quadrantSegments set. +func (b BufferParams) WithQuadrantSegments(quadrantSegments int) BufferParams { + ret := b + ret.p.QuadrantSegments = quadrantSegments + return ret +} + +// ParseBufferParams parses the given buffer params from a SQL string into +// the BufferParams form. +// The string must be of the same format as specified by https://postgis.net/docs/ST_Buffer.html. +// Returns the BufferParams, as well as the modified distance. +func ParseBufferParams(s string, distance float64) (BufferParams, float64, error) { + p := MakeDefaultBufferParams() + fields := strings.Fields(s) + for _, field := range fields { + fParams := strings.Split(field, "=") + if len(fParams) != 2 { + return BufferParams{}, 0, pgerror.Newf(pgcode.InvalidParameterValue, "unknown buffer parameter: %s", fParams) + } + f, val := fParams[0], fParams[1] + switch strings.ToLower(f) { + case "quad_segs": + valInt, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return BufferParams{}, 0, pgerror.WithCandidateCode( + errors.Wrapf(err, "invalid int for %s: %s", f, val), + pgcode.InvalidParameterValue, + ) + } + p.p.QuadrantSegments = int(valInt) + case "endcap": + switch strings.ToLower(val) { + case "round": + p.p.EndCapStyle = geos.BufferParamsEndCapStyleRound + case "flat", "butt": + p.p.EndCapStyle = geos.BufferParamsEndCapStyleFlat + case "square": + p.p.EndCapStyle = geos.BufferParamsEndCapStyleSquare + default: + return BufferParams{}, 0, pgerror.Newf( + pgcode.InvalidParameterValue, + "unknown endcap: %s (accepted: round, flat, square)", + val, + ) + } + case "join": + switch strings.ToLower(val) { + case "round": + p.p.JoinStyle = geos.BufferParamsJoinStyleRound + case "mitre", "miter": + p.p.JoinStyle = geos.BufferParamsJoinStyleMitre + case "bevel": + p.p.JoinStyle = geos.BufferParamsJoinStyleBevel + default: + return BufferParams{}, 0, pgerror.Newf( + pgcode.InvalidParameterValue, + "unknown join: %s (accepted: round, mitre, bevel)", + val, + ) + } + case "mitre_limit", "miter_limit": + valFloat, err := strconv.ParseFloat(val, 64) + if err != nil { + return BufferParams{}, 0, pgerror.WithCandidateCode( + errors.Wrapf(err, "invalid float for %s: %s", f, val), + pgcode.InvalidParameterValue, + ) + } + p.p.MitreLimit = valFloat + case "side": + switch strings.ToLower(val) { + case "both": + p.p.SingleSided = false + case "left": + p.p.SingleSided = true + case "right": + p.p.SingleSided = true + distance *= -1 + default: + return BufferParams{}, 0, pgerror.Newf(pgcode.InvalidParameterValue, "unknown side: %s (accepted: both, left, right)", val) + } + default: + return BufferParams{}, 0, pgerror.Newf(pgcode.InvalidParameterValue, "unknown field: %s (accepted fields: quad_segs, endcap, join, mitre_limit, side)", f) + } + } + return p, distance, nil +} + +// Buffer buffers a given Geometry by the supplied parameters. +func Buffer(g geo.Geometry, params BufferParams, distance float64) (geo.Geometry, error) { + if params.p.QuadrantSegments < 1 { + return geo.Geometry{}, pgerror.Newf( + pgcode.InvalidParameterValue, + "must request at least 1 quadrant segment, requested %d quadrant segments", + params.p.QuadrantSegments, + ) + + } + if params.p.QuadrantSegments > geo.MaxAllowedSplitPoints { + return geo.Geometry{}, pgerror.Newf( + pgcode.InvalidParameterValue, + "attempting to split buffered geometry into too many quadrant segments; requested %d quadrant segments, max %d", + params.p.QuadrantSegments, + geo.MaxAllowedSplitPoints, + ) + } + bufferedGeom, err := geos.Buffer(g.EWKB(), params.p, distance) + if err != nil { + return geo.Geometry{}, err + } + return geo.ParseGeometryFromEWKB(bufferedGeom) +} diff --git a/pkg/geo/geomfn/collections.go b/pkg/geo/geomfn/collections.go new file mode 100644 index 0000000..021cb7b --- /dev/null +++ b/pkg/geo/geomfn/collections.go @@ -0,0 +1,433 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/errors" + "github.com/twpayne/go-geom" +) + +// Collect collects two geometries into a GeometryCollection or multi-type. +// +// This is the binary version of `ST_Collect()`, but since it's not possible to +// have an aggregate and non-aggregate function with the same name (different +// args), this is not used. Code is left behind for when we add support for +// this. Be sure to handle NULL args when adding the builtin for this, where it +// should return the non-NULL arg unused like PostGIS. +func Collect(g1 geo.Geometry, g2 geo.Geometry) (geo.Geometry, error) { + t1, err := g1.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + t2, err := g2.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + + // First, try to generate multi-types + switch t1 := t1.(type) { + case *geom.Point: + if t2, ok := t2.(*geom.Point); ok { + multi := geom.NewMultiPoint(t1.Layout()).SetSRID(t1.SRID()) + if err := multi.Push(t1); err != nil { + return geo.Geometry{}, err + } + if err := multi.Push(t2); err != nil { + return geo.Geometry{}, err + } + return geo.MakeGeometryFromGeomT(multi) + } + case *geom.LineString: + if t2, ok := t2.(*geom.LineString); ok { + multi := geom.NewMultiLineString(t1.Layout()).SetSRID(t1.SRID()) + if err := multi.Push(t1); err != nil { + return geo.Geometry{}, err + } + if err := multi.Push(t2); err != nil { + return geo.Geometry{}, err + } + return geo.MakeGeometryFromGeomT(multi) + } + case *geom.Polygon: + if t2, ok := t2.(*geom.Polygon); ok { + multi := geom.NewMultiPolygon(t1.Layout()).SetSRID(t1.SRID()) + if err := multi.Push(t1); err != nil { + return geo.Geometry{}, err + } + if err := multi.Push(t2); err != nil { + return geo.Geometry{}, err + } + return geo.MakeGeometryFromGeomT(multi) + } + } + + // Otherwise, just put them in a collection + gc := geom.NewGeometryCollection().SetSRID(t1.SRID()) + if err := gc.Push(t1); err != nil { + return geo.Geometry{}, err + } + if err := gc.Push(t2); err != nil { + return geo.Geometry{}, err + } + return geo.MakeGeometryFromGeomT(gc) +} + +// CollectionExtract returns a (multi-)geometry consisting only of the specified type. +// The type can only be point, line, or polygon. +func CollectionExtract(g geo.Geometry, shapeType geopb.ShapeType) (geo.Geometry, error) { + switch shapeType { + case geopb.ShapeType_Point, geopb.ShapeType_LineString, geopb.ShapeType_Polygon: + default: + return geo.Geometry{}, pgerror.Newf( + pgcode.InvalidParameterValue, + "only point, linestring and polygon may be extracted (got %s)", + shapeType, + ) + } + + // If the input is already of the correct (multi-)type, just return it before + // decoding the geom.T below. + if g.ShapeType() == shapeType || g.ShapeType() == shapeType.MultiType() { + return g, nil + } + + t, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + + switch t := t.(type) { + // If the input is not a collection then return an empty geometry of the expected type. + case *geom.Point, *geom.LineString, *geom.Polygon: + switch shapeType { + case geopb.ShapeType_Point: + return geo.MakeGeometryFromGeomT(geom.NewPointEmpty(t.Layout()).SetSRID(t.SRID())) + case geopb.ShapeType_LineString: + return geo.MakeGeometryFromGeomT(geom.NewLineString(t.Layout()).SetSRID(t.SRID())) + case geopb.ShapeType_Polygon: + return geo.MakeGeometryFromGeomT(geom.NewPolygon(t.Layout()).SetSRID(t.SRID())) + default: + return geo.Geometry{}, pgerror.Newf( + pgcode.InvalidParameterValue, + "unexpected shape type %v", + shapeType.String(), + ) + } + + // If the input is a multitype then return an empty multi-geometry of the expected type. + case *geom.MultiPoint, *geom.MultiLineString, *geom.MultiPolygon: + switch shapeType.MultiType() { + case geopb.ShapeType_MultiPoint: + return geo.MakeGeometryFromGeomT(geom.NewMultiPoint(t.Layout()).SetSRID(t.SRID())) + case geopb.ShapeType_MultiLineString: + return geo.MakeGeometryFromGeomT(geom.NewMultiLineString(t.Layout()).SetSRID(t.SRID())) + case geopb.ShapeType_MultiPolygon: + return geo.MakeGeometryFromGeomT(geom.NewMultiPolygon(t.Layout()).SetSRID(t.SRID())) + default: + return geo.Geometry{}, pgerror.Newf( + pgcode.InvalidParameterValue, + "unexpected shape type %v", + shapeType.MultiType().String(), + ) + } + + // If the input is a collection, recursively gather geometries of the right type. + case *geom.GeometryCollection: + // Empty geos.GeometryCollection has NoLayout, while PostGIS uses XY. Returned + // multi-geometries cannot have NoLayout. + layout := t.Layout() + if layout == geom.NoLayout && t.Empty() { + layout = geom.XY + } + iter := geo.NewGeomTIterator(t, geo.EmptyBehaviorOmit) + srid := t.SRID() + + var ( + multi geom.T + err error + ) + switch shapeType { + case geopb.ShapeType_Point: + multi, err = collectionExtractPoints(iter, layout, srid) + case geopb.ShapeType_LineString: + multi, err = collectionExtractLineStrings(iter, layout, srid) + case geopb.ShapeType_Polygon: + multi, err = collectionExtractPolygons(iter, layout, srid) + default: + return geo.Geometry{}, errors.AssertionFailedf("unexpected shape type %v", shapeType.String()) + } + if err != nil { + return geo.Geometry{}, err + } + return geo.MakeGeometryFromGeomT(multi) + + default: + return geo.Geometry{}, errors.AssertionFailedf("unexpected shape type: %T", t) + } +} + +// collectionExtractPoints extracts points from an iterator. +func collectionExtractPoints( + iter geo.GeomTIterator, layout geom.Layout, srid int, +) (*geom.MultiPoint, error) { + points := geom.NewMultiPoint(layout).SetSRID(srid) + for { + if next, hasNext, err := iter.Next(); err != nil { + return nil, err + } else if !hasNext { + break + } else if point, ok := next.(*geom.Point); ok { + if err = points.Push(point); err != nil { + return nil, err + } + } + } + return points, nil +} + +// collectionExtractLineStrings extracts line strings from an iterator. +func collectionExtractLineStrings( + iter geo.GeomTIterator, layout geom.Layout, srid int, +) (*geom.MultiLineString, error) { + lineStrings := geom.NewMultiLineString(layout).SetSRID(srid) + for { + if next, hasNext, err := iter.Next(); err != nil { + return nil, err + } else if !hasNext { + break + } else if lineString, ok := next.(*geom.LineString); ok { + if err = lineStrings.Push(lineString); err != nil { + return nil, err + } + } + } + return lineStrings, nil +} + +// collectionExtractPolygons extracts polygons from an iterator. +func collectionExtractPolygons( + iter geo.GeomTIterator, layout geom.Layout, srid int, +) (*geom.MultiPolygon, error) { + polygons := geom.NewMultiPolygon(layout).SetSRID(srid) + for { + if next, hasNext, err := iter.Next(); err != nil { + return nil, err + } else if !hasNext { + break + } else if polygon, ok := next.(*geom.Polygon); ok { + if err = polygons.Push(polygon); err != nil { + return nil, err + } + } + } + return polygons, nil +} + +// CollectionHomogenize returns the simplest representation of a collection. +func CollectionHomogenize(g geo.Geometry) (geo.Geometry, error) { + t, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + srid := t.SRID() + t, err = collectionHomogenizeGeomT(t) + if err != nil { + return geo.Geometry{}, err + } + if srid != 0 { + geo.AdjustGeomTSRID(t, geopb.SRID(srid)) + } + return geo.MakeGeometryFromGeomT(t) +} + +// collectionHomogenizeGeomT homogenizes a geom.T collection. +func collectionHomogenizeGeomT(t geom.T) (geom.T, error) { + switch t := t.(type) { + case *geom.Point, *geom.LineString, *geom.Polygon: + return t, nil + + case *geom.MultiPoint: + if t.NumPoints() == 1 { + return t.Point(0), nil + } + return t, nil + + case *geom.MultiLineString: + if t.NumLineStrings() == 1 { + return t.LineString(0), nil + } + return t, nil + + case *geom.MultiPolygon: + if t.NumPolygons() == 1 { + return t.Polygon(0), nil + } + return t, nil + + case *geom.GeometryCollection: + layout := t.Layout() + if layout == geom.NoLayout && t.Empty() { + layout = geom.XY + } + points := geom.NewMultiPoint(layout) + linestrings := geom.NewMultiLineString(layout) + polygons := geom.NewMultiPolygon(layout) + iter := geo.NewGeomTIterator(t, geo.EmptyBehaviorOmit) + for { + next, hasNext, err := iter.Next() + if err != nil { + return nil, err + } + if !hasNext { + break + } + switch next := next.(type) { + case *geom.Point: + err = points.Push(next) + case *geom.LineString: + err = linestrings.Push(next) + case *geom.Polygon: + err = polygons.Push(next) + default: + err = errors.AssertionFailedf("encountered unexpected geometry type: %T", next) + } + if err != nil { + return nil, err + } + } + homog := geom.NewGeometryCollection() + switch points.NumPoints() { + case 0: + case 1: + if err := homog.Push(points.Point(0)); err != nil { + return nil, err + } + default: + if err := homog.Push(points); err != nil { + return nil, err + } + } + switch linestrings.NumLineStrings() { + case 0: + case 1: + if err := homog.Push(linestrings.LineString(0)); err != nil { + return nil, err + } + default: + if err := homog.Push(linestrings); err != nil { + return nil, err + } + } + switch polygons.NumPolygons() { + case 0: + case 1: + if err := homog.Push(polygons.Polygon(0)); err != nil { + return nil, err + } + default: + if err := homog.Push(polygons); err != nil { + return nil, err + } + } + + if homog.NumGeoms() == 1 { + return homog.Geom(0), nil + } + return homog, nil + + default: + return nil, errors.AssertionFailedf("unknown geometry type: %T", t) + } +} + +// ForceCollection converts the input into a GeometryCollection. +func ForceCollection(g geo.Geometry) (geo.Geometry, error) { + t, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + t, err = forceCollectionFromGeomT(t) + if err != nil { + return geo.Geometry{}, err + } + return geo.MakeGeometryFromGeomT(t) +} + +// forceCollectionFromGeomT converts a geom.T into a geom.GeometryCollection. +func forceCollectionFromGeomT(t geom.T) (geom.T, error) { + gc := geom.NewGeometryCollection().SetSRID(t.SRID()) + switch t := t.(type) { + case *geom.Point, *geom.LineString, *geom.Polygon: + if err := gc.Push(t); err != nil { + return nil, err + } + case *geom.MultiPoint: + for i := 0; i < t.NumPoints(); i++ { + if err := gc.Push(t.Point(i)); err != nil { + return nil, err + } + } + case *geom.MultiLineString: + for i := 0; i < t.NumLineStrings(); i++ { + if err := gc.Push(t.LineString(i)); err != nil { + return nil, err + } + } + case *geom.MultiPolygon: + for i := 0; i < t.NumPolygons(); i++ { + if err := gc.Push(t.Polygon(i)); err != nil { + return nil, err + } + } + case *geom.GeometryCollection: + gc = t + default: + return nil, errors.AssertionFailedf("unknown geometry type: %T", t) + } + return gc, nil +} + +// Multi converts the given geometry into a new multi-geometry. +func Multi(g geo.Geometry) (geo.Geometry, error) { + t, err := g.AsGeomT() // implicitly clones the input + if err != nil { + return geo.Geometry{}, err + } + switch t := t.(type) { + case *geom.MultiPoint, *geom.MultiLineString, *geom.MultiPolygon, *geom.GeometryCollection: + return geo.MakeGeometryFromGeomT(t) + case *geom.Point: + multi := geom.NewMultiPoint(t.Layout()).SetSRID(t.SRID()) + if !t.Empty() { + if err = multi.Push(t); err != nil { + return geo.Geometry{}, err + } + } + return geo.MakeGeometryFromGeomT(multi) + case *geom.LineString: + multi := geom.NewMultiLineString(t.Layout()).SetSRID(t.SRID()) + if !t.Empty() { + if err = multi.Push(t); err != nil { + return geo.Geometry{}, err + } + } + return geo.MakeGeometryFromGeomT(multi) + case *geom.Polygon: + multi := geom.NewMultiPolygon(t.Layout()).SetSRID(t.SRID()) + if !t.Empty() { + if err = multi.Push(t); err != nil { + return geo.Geometry{}, err + } + } + return geo.MakeGeometryFromGeomT(multi) + default: + return geo.Geometry{}, errors.AssertionFailedf("unknown geometry type: %T", t) + } +} diff --git a/pkg/geo/geomfn/coord.go b/pkg/geo/geomfn/coord.go new file mode 100644 index 0000000..76d810b --- /dev/null +++ b/pkg/geo/geomfn/coord.go @@ -0,0 +1,62 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "math" + + "github.com/twpayne/go-geom" +) + +// coordAdd adds two coordinates and returns a new result. +func coordAdd(a geom.Coord, b geom.Coord) geom.Coord { + return geom.Coord{a.X() + b.X(), a.Y() + b.Y()} +} + +// coordSub subtracts two coordinates and returns a new result. +func coordSub(a geom.Coord, b geom.Coord) geom.Coord { + return geom.Coord{a.X() - b.X(), a.Y() - b.Y()} +} + +// coordMul multiplies a coord by a scalar and returns the new result. +func coordMul(a geom.Coord, s float64) geom.Coord { + return geom.Coord{a.X() * s, a.Y() * s} +} + +// coordDet returns the determinant of the 2x2 matrix formed by the vectors a and b. +func coordDet(a geom.Coord, b geom.Coord) float64 { + return a.X()*b.Y() - b.X()*a.Y() +} + +// coordDot returns the dot product of two coords if the coord was a vector. +func coordDot(a geom.Coord, b geom.Coord) float64 { + return a.X()*b.X() + a.Y()*b.Y() +} + +// coordCross returns the cross product of two coords if the coord was a vector. +func coordCross(a geom.Coord, b geom.Coord) float64 { + return a.X()*b.Y() - a.Y()*b.X() +} + +// coordNorm2 returns the normalization^2 of a coordinate if the coord was a vector. +func coordNorm2(c geom.Coord) float64 { + return coordDot(c, c) +} + +// coordNorm returns the normalization of a coordinate if the coord was a vector. +func coordNorm(c geom.Coord) float64 { + return math.Sqrt(coordNorm2(c)) +} + +// coordEqual returns whether two coordinates are equal. +func coordEqual(a geom.Coord, b geom.Coord) bool { + return a.X() == b.X() && a.Y() == b.Y() +} + +// coordMag2 returns the magnitude^2 of a coordinate if the coord was a vector. +func coordMag2(c geom.Coord) float64 { + return coordDot(c, c) +} diff --git a/pkg/geo/geomfn/de9im.go b/pkg/geo/geomfn/de9im.go new file mode 100644 index 0000000..7864600 --- /dev/null +++ b/pkg/geo/geomfn/de9im.go @@ -0,0 +1,90 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geos" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/cockroachdb-parser/pkg/util" +) + +// Relate returns the DE-9IM relation between A and B. +func Relate(a geo.Geometry, b geo.Geometry) (string, error) { + if a.SRID() != b.SRID() { + return "", geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + return geos.Relate(a.EWKB(), b.EWKB()) +} + +// RelateBoundaryNodeRule returns the DE-9IM relation between A and B using +// the given boundary node rule (as specified by GEOS). +func RelateBoundaryNodeRule(a, b geo.Geometry, bnr int) (string, error) { + if a.SRID() != b.SRID() { + return "", geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + return geos.RelateBoundaryNodeRule(a.EWKB(), b.EWKB(), bnr) +} + +// RelatePattern returns whether the DE-9IM relation between A and B matches. +func RelatePattern(a geo.Geometry, b geo.Geometry, pattern string) (bool, error) { + if a.SRID() != b.SRID() { + return false, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + return geos.RelatePattern(a.EWKB(), b.EWKB(), pattern) +} + +// MatchesDE9IM checks whether the given DE-9IM relation matches the DE-91M pattern. +// Assumes the relation has been computed, and such has no 'T' and '*' characters. +// See: https://en.wikipedia.org/wiki/DE-9IM. +func MatchesDE9IM(relation string, pattern string) (bool, error) { + if len(relation) != 9 { + return false, pgerror.Newf(pgcode.InvalidParameterValue, "relation %q should be of length 9", relation) + } + if len(pattern) != 9 { + return false, pgerror.Newf(pgcode.InvalidParameterValue, "pattern %q should be of length 9", pattern) + } + for i := 0; i < len(relation); i++ { + matches, err := relationByteMatchesPatternByte(relation[i], pattern[i]) + if err != nil { + return false, err + } + if !matches { + return false, nil + } + } + return true, nil +} + +// relationByteMatchesPatternByte matches a single byte of a DE-9IM relation +// against the DE-9IM pattern. +// Pattern matches are as follows: +// - '*': allow anything. +// - '0' / '1' / '2': match exactly. +// - 't'/'T': allow only if the relation is true. This means the relation must be +// '0' (point), '1' (line) or '2' (area) - which is the dimensionality of the +// intersection. +// - 'f'/'F': allow only if relation is also false, which is of the form 'f'/'F'. +func relationByteMatchesPatternByte(r byte, p byte) (bool, error) { + switch util.ToLowerSingleByte(p) { + case '*': + return true, nil + case 't': + if r < '0' || r > '2' { + return false, nil + } + case 'f': + if util.ToLowerSingleByte(r) != 'f' { + return false, nil + } + case '0', '1', '2': + return r == p, nil + default: + return false, pgerror.Newf(pgcode.InvalidParameterValue, "unrecognized pattern character: %s", string(p)) + } + return true, nil +} diff --git a/pkg/geo/geomfn/distance.go b/pkg/geo/geomfn/distance.go new file mode 100644 index 0000000..324ea21 --- /dev/null +++ b/pkg/geo/geomfn/distance.go @@ -0,0 +1,904 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "math" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geodist" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geos" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/errors" + "github.com/twpayne/go-geom" + "github.com/twpayne/go-geom/xy/lineintersector" +) + +// geometricalObjectsOrder allows us to preserve the order of geometrical objects to +// match the start and endpoint in the shortest and longest LineString. +type geometricalObjectsOrder int + +const ( + // geometricalObjectsFlipped represents that the given order of two geometrical + // objects has been flipped. + geometricalObjectsFlipped geometricalObjectsOrder = -1 + // geometricalObjectsNotFlipped represents that the given order of two geometrical + // objects has not been flipped. + geometricalObjectsNotFlipped geometricalObjectsOrder = 1 +) + +// MinDistance returns the minimum distance between geometries A and B. +// This returns a geo.EmptyGeometryError if either A or B is EMPTY. +func MinDistance(a geo.Geometry, b geo.Geometry) (float64, error) { + if a.SRID() != b.SRID() { + return 0, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + return minDistanceInternal(a, b, 0, geo.EmptyBehaviorOmit, geo.FnInclusive) +} + +// MaxDistance returns the maximum distance across every pair of points comprising +// geometries A and B. +func MaxDistance(a geo.Geometry, b geo.Geometry) (float64, error) { + if a.SRID() != b.SRID() { + return 0, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + return maxDistanceInternal(a, b, math.MaxFloat64, geo.EmptyBehaviorOmit, geo.FnInclusive) +} + +// DWithin determines if any part of geometry A is within D units of geometry B. +// If exclusive, DWithin is equivalent to Distance(a, b) < d. Otherwise, DWithin +// is equivalent to Distance(a, b) <= d. +func DWithin( + a geo.Geometry, b geo.Geometry, d float64, exclusivity geo.FnExclusivity, +) (bool, error) { + if a.SRID() != b.SRID() { + return false, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + if d < 0 { + return false, pgerror.Newf(pgcode.InvalidParameterValue, "dwithin distance cannot be less than zero") + } + if !a.CartesianBoundingBox().Buffer(d, d).Intersects(b.CartesianBoundingBox()) { + return false, nil + } + dist, err := minDistanceInternal(a, b, d, geo.EmptyBehaviorError, exclusivity) + if err != nil { + // In case of any empty geometries return false. + if geo.IsEmptyGeometryError(err) { + return false, nil + } + return false, err + } + if exclusivity == geo.FnExclusive { + return dist < d, nil + } + return dist <= d, nil +} + +// DFullyWithin determines whether the maximum distance across every pair of +// points comprising geometries A and B is within D units. If exclusive, +// DFullyWithin is equivalent to MaxDistance(a, b) < d. Otherwise, DFullyWithin +// is equivalent to MaxDistance(a, b) <= d. +func DFullyWithin( + a geo.Geometry, b geo.Geometry, d float64, exclusivity geo.FnExclusivity, +) (bool, error) { + if a.SRID() != b.SRID() { + return false, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + if d < 0 { + return false, pgerror.Newf(pgcode.InvalidParameterValue, "dwithin distance cannot be less than zero") + } + if !a.CartesianBoundingBox().Buffer(d, d).Covers(b.CartesianBoundingBox()) { + return false, nil + } + dist, err := maxDistanceInternal(a, b, d, geo.EmptyBehaviorError, exclusivity) + if err != nil { + // In case of any empty geometries return false. + if geo.IsEmptyGeometryError(err) { + return false, nil + } + return false, err + } + if exclusivity == geo.FnExclusive { + return dist < d, nil + } + return dist <= d, nil +} + +// LongestLineString returns the LineString corresponds to maximum distance across +// every pair of points comprising geometries A and B. +func LongestLineString(a geo.Geometry, b geo.Geometry) (geo.Geometry, error) { + if a.SRID() != b.SRID() { + return geo.Geometry{}, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + u := newGeomMaxDistanceUpdater(math.MaxFloat64, geo.FnInclusive) + return distanceLineStringInternal(a, b, u, geo.EmptyBehaviorOmit) +} + +// ShortestLineString returns the LineString corresponds to minimum distance across +// every pair of points comprising geometries A and B. +func ShortestLineString(a geo.Geometry, b geo.Geometry) (geo.Geometry, error) { + if a.SRID() != b.SRID() { + return geo.Geometry{}, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + u := newGeomMinDistanceUpdater(0 /*stopAfter */, geo.FnInclusive) + return distanceLineStringInternal(a, b, u, geo.EmptyBehaviorOmit) +} + +// distanceLineStringInternal calculates the LineString between two geometries using +// the DistanceCalculator operator. +// If there are any EMPTY Geometry objects, they will be ignored. It will return an +// EmptyGeometryError if A or B contains only EMPTY geometries, even if emptyBehavior +// is set to EmptyBehaviorOmit. +func distanceLineStringInternal( + a geo.Geometry, b geo.Geometry, u geodist.DistanceUpdater, emptyBehavior geo.EmptyBehavior, +) (geo.Geometry, error) { + c := &geomDistanceCalculator{updater: u, boundingBoxIntersects: a.CartesianBoundingBox().Intersects(b.CartesianBoundingBox())} + _, err := distanceInternal(a, b, c, emptyBehavior) + if err != nil { + return geo.Geometry{}, err + } + var coordA, coordB geom.Coord + switch u := u.(type) { + case *geomMaxDistanceUpdater: + coordA = u.coordA + coordB = u.coordB + case *geomMinDistanceUpdater: + coordA = u.coordA + coordB = u.coordB + default: + return geo.Geometry{}, errors.AssertionFailedf("programmer error: unknown behavior") + } + lineCoords := []float64{coordA.X(), coordA.Y(), coordB.X(), coordB.Y()} + lineString := geom.NewLineStringFlat(geom.XY, lineCoords).SetSRID(int(a.SRID())) + return geo.MakeGeometryFromGeomT(lineString) +} + +// maxDistanceInternal finds the maximum distance between two geometries. +// We can re-use the same algorithm as min-distance, allowing skips of checks that involve +// the interiors or intersections as those will always be less then the maximum min-distance. +func maxDistanceInternal( + a geo.Geometry, + b geo.Geometry, + stopAfter float64, + emptyBehavior geo.EmptyBehavior, + exclusivity geo.FnExclusivity, +) (float64, error) { + u := newGeomMaxDistanceUpdater(stopAfter, exclusivity) + c := &geomDistanceCalculator{updater: u, boundingBoxIntersects: a.CartesianBoundingBox().Intersects(b.CartesianBoundingBox())} + return distanceInternal(a, b, c, emptyBehavior) +} + +// minDistanceInternal finds the minimum distance between two geometries. +// This implementation is done in-house, as compared to using GEOS. +func minDistanceInternal( + a geo.Geometry, + b geo.Geometry, + stopAfter float64, + emptyBehavior geo.EmptyBehavior, + exclusivity geo.FnExclusivity, +) (float64, error) { + u := newGeomMinDistanceUpdater(stopAfter, exclusivity) + c := &geomDistanceCalculator{updater: u, boundingBoxIntersects: a.CartesianBoundingBox().Intersects(b.CartesianBoundingBox())} + return distanceInternal(a, b, c, emptyBehavior) +} + +// distanceInternal calculates the distance between two geometries using +// the DistanceCalculator operator. +// If there are any EMPTY Geometry objects, they will be ignored. It will return an +// EmptyGeometryError if A or B contains only EMPTY geometries, even if emptyBehavior +// is set to EmptyBehaviorOmit. +func distanceInternal( + a geo.Geometry, b geo.Geometry, c geodist.DistanceCalculator, emptyBehavior geo.EmptyBehavior, +) (float64, error) { + // If either side has no geoms, then we error out regardless of emptyBehavior. + if a.Empty() || b.Empty() { + return 0, geo.NewEmptyGeometryError() + } + + aGeomT, err := a.AsGeomT() + if err != nil { + return 0, err + } + bGeomT, err := b.AsGeomT() + if err != nil { + return 0, err + } + + // If we early exit, we have to check empty behavior upfront to return + // the appropriate error message. + // This matches PostGIS's behavior for DWithin, which is always false + // if at least one element is empty. + if emptyBehavior == geo.EmptyBehaviorError && + (geo.GeomTContainsEmpty(aGeomT) || geo.GeomTContainsEmpty(bGeomT)) { + return 0, geo.NewEmptyGeometryError() + } + + aIt := geo.NewGeomTIterator(aGeomT, emptyBehavior) + aGeom, aNext, aErr := aIt.Next() + if aErr != nil { + return 0, err + } + for aNext { + aGeodist, err := geomToGeodist(aGeom) + if err != nil { + return 0, err + } + + bIt := geo.NewGeomTIterator(bGeomT, emptyBehavior) + bGeom, bNext, bErr := bIt.Next() + if bErr != nil { + return 0, err + } + for bNext { + bGeodist, err := geomToGeodist(bGeom) + if err != nil { + return 0, err + } + earlyExit, err := geodist.ShapeDistance(c, aGeodist, bGeodist) + if err != nil { + return 0, err + } + if earlyExit { + return c.DistanceUpdater().Distance(), nil + } + + bGeom, bNext, bErr = bIt.Next() + if bErr != nil { + return 0, err + } + } + + aGeom, aNext, aErr = aIt.Next() + if aErr != nil { + return 0, err + } + } + return c.DistanceUpdater().Distance(), nil +} + +// geomToGeodist converts a given geom object to a geodist shape. +func geomToGeodist(g geom.T) (geodist.Shape, error) { + switch g := g.(type) { + case *geom.Point: + return &geodist.Point{GeomPoint: g.Coords()}, nil + case *geom.LineString: + return &geomGeodistLineString{LineString: g}, nil + case *geom.Polygon: + return &geomGeodistPolygon{Polygon: g}, nil + } + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "could not find shape: %T", g) +} + +// geomGeodistLineString implements geodist.LineString. +type geomGeodistLineString struct { + *geom.LineString +} + +var _ geodist.LineString = (*geomGeodistLineString)(nil) + +// IsShape implements the geodist.LineString interface. +func (*geomGeodistLineString) IsShape() {} + +// LineString implements the geodist.LineString interface. +func (*geomGeodistLineString) IsLineString() {} + +// Edge implements the geodist.LineString interface. +func (g *geomGeodistLineString) Edge(i int) geodist.Edge { + return geodist.Edge{ + V0: geodist.Point{GeomPoint: g.LineString.Coord(i)}, + V1: geodist.Point{GeomPoint: g.LineString.Coord(i + 1)}, + } +} + +// NumEdges implements the geodist.LineString interface. +func (g *geomGeodistLineString) NumEdges() int { + return g.LineString.NumCoords() - 1 +} + +// Vertex implements the geodist.LineString interface. +func (g *geomGeodistLineString) Vertex(i int) geodist.Point { + return geodist.Point{GeomPoint: g.LineString.Coord(i)} +} + +// NumVertexes implements the geodist.LineString interface. +func (g *geomGeodistLineString) NumVertexes() int { + return g.LineString.NumCoords() +} + +// geomGeodistLinearRing implements geodist.LinearRing. +type geomGeodistLinearRing struct { + *geom.LinearRing +} + +var _ geodist.LinearRing = (*geomGeodistLinearRing)(nil) + +// IsShape implements the geodist.LinearRing interface. +func (*geomGeodistLinearRing) IsShape() {} + +// LinearRing implements the geodist.LinearRing interface. +func (*geomGeodistLinearRing) IsLinearRing() {} + +// Edge implements the geodist.LinearRing interface. +func (g *geomGeodistLinearRing) Edge(i int) geodist.Edge { + return geodist.Edge{ + V0: geodist.Point{GeomPoint: g.LinearRing.Coord(i)}, + V1: geodist.Point{GeomPoint: g.LinearRing.Coord(i + 1)}, + } +} + +// NumEdges implements the geodist.LinearRing interface. +func (g *geomGeodistLinearRing) NumEdges() int { + return g.LinearRing.NumCoords() - 1 +} + +// Vertex implements the geodist.LinearRing interface. +func (g *geomGeodistLinearRing) Vertex(i int) geodist.Point { + return geodist.Point{GeomPoint: g.LinearRing.Coord(i)} +} + +// NumVertexes implements the geodist.LinearRing interface. +func (g *geomGeodistLinearRing) NumVertexes() int { + return g.LinearRing.NumCoords() +} + +// geomGeodistPolygon implements geodist.Polygon. +type geomGeodistPolygon struct { + *geom.Polygon +} + +var _ geodist.Polygon = (*geomGeodistPolygon)(nil) + +// IsShape implements the geodist.Polygon interface. +func (*geomGeodistPolygon) IsShape() {} + +// Polygon implements the geodist.Polygon interface. +func (*geomGeodistPolygon) IsPolygon() {} + +// LinearRing implements the geodist.Polygon interface. +func (g *geomGeodistPolygon) LinearRing(i int) geodist.LinearRing { + return &geomGeodistLinearRing{LinearRing: g.Polygon.LinearRing(i)} +} + +// NumLinearRings implements the geodist.Polygon interface. +func (g *geomGeodistPolygon) NumLinearRings() int { + return g.Polygon.NumLinearRings() +} + +// geomGeodistEdgeCrosser implements geodist.EdgeCrosser. +type geomGeodistEdgeCrosser struct { + strategy lineintersector.Strategy + edgeV0 geom.Coord + edgeV1 geom.Coord + nextEdgeV0 geom.Coord +} + +var _ geodist.EdgeCrosser = (*geomGeodistEdgeCrosser)(nil) + +// ChainCrossing implements geodist.EdgeCrosser. +func (c *geomGeodistEdgeCrosser) ChainCrossing(p geodist.Point) (bool, geodist.Point) { + nextEdgeV1 := p.GeomPoint + result := lineintersector.LineIntersectsLine( + c.strategy, + c.edgeV0, + c.edgeV1, + c.nextEdgeV0, + nextEdgeV1, + ) + c.nextEdgeV0 = nextEdgeV1 + if result.HasIntersection() { + return true, geodist.Point{GeomPoint: result.Intersection()[0]} + } + return false, geodist.Point{} +} + +// geomMinDistanceUpdater finds the minimum distance using geom calculations. +// And preserve the line's endpoints as geom.Coord which corresponds to minimum +// distance. If inclusive, methods will return early if it finds a minimum +// distance <= stopAfter. Otherwise, methods will return early if it finds a +// minimum distance < stopAfter. +type geomMinDistanceUpdater struct { + currentValue float64 + stopAfter float64 + exclusivity geo.FnExclusivity + // coordA represents the first vertex of the edge that holds the maximum distance. + coordA geom.Coord + // coordB represents the second vertex of the edge that holds the maximum distance. + coordB geom.Coord + + geometricalObjOrder geometricalObjectsOrder +} + +var _ geodist.DistanceUpdater = (*geomMinDistanceUpdater)(nil) + +// newGeomMinDistanceUpdater returns a new geomMinDistanceUpdater with the +// correct arguments set up. +func newGeomMinDistanceUpdater( + stopAfter float64, exclusivity geo.FnExclusivity, +) *geomMinDistanceUpdater { + return &geomMinDistanceUpdater{ + currentValue: math.MaxFloat64, + stopAfter: stopAfter, + exclusivity: exclusivity, + coordA: nil, + coordB: nil, + geometricalObjOrder: geometricalObjectsNotFlipped, + } +} + +// Distance implements the geodist.DistanceUpdater interface. +func (u *geomMinDistanceUpdater) Distance() float64 { + return u.currentValue +} + +// Update implements the geodist.DistanceUpdater interface. +func (u *geomMinDistanceUpdater) Update(aPoint geodist.Point, bPoint geodist.Point) bool { + a := aPoint.GeomPoint + b := bPoint.GeomPoint + + dist := coordNorm(coordSub(a, b)) + if dist < u.currentValue || u.coordA == nil { + u.currentValue = dist + if u.geometricalObjOrder == geometricalObjectsFlipped { + u.coordA = b + u.coordB = a + } else { + u.coordA = a + u.coordB = b + } + if u.exclusivity == geo.FnExclusive { + return dist < u.stopAfter + } + return dist <= u.stopAfter + } + return false +} + +// OnIntersects implements the geodist.DistanceUpdater interface. +func (u *geomMinDistanceUpdater) OnIntersects(p geodist.Point) bool { + u.coordA = p.GeomPoint + u.coordB = p.GeomPoint + u.currentValue = 0 + return true +} + +// IsMaxDistance implements the geodist.DistanceUpdater interface. +func (u *geomMinDistanceUpdater) IsMaxDistance() bool { + return false +} + +// FlipGeometries implements the geodist.DistanceUpdater interface. +func (u *geomMinDistanceUpdater) FlipGeometries() { + u.geometricalObjOrder = -u.geometricalObjOrder +} + +// geomMaxDistanceUpdater finds the maximum distance using geom calculations. +// And preserve the line's endpoints as geom.Coord which corresponds to maximum +// distance. If exclusive, methods will return early if it finds that +// distance >= stopAfter. Otherwise, methods will return early if distance > +// stopAfter. +type geomMaxDistanceUpdater struct { + currentValue float64 + stopAfter float64 + exclusivity geo.FnExclusivity + + // coordA represents the first vertex of the edge that holds the maximum distance. + coordA geom.Coord + // coordB represents the second vertex of the edge that holds the maximum distance. + coordB geom.Coord + + geometricalObjOrder geometricalObjectsOrder +} + +var _ geodist.DistanceUpdater = (*geomMaxDistanceUpdater)(nil) + +// newGeomMaxDistanceUpdater returns a new geomMaxDistanceUpdater with the +// correct arguments set up. currentValue is initially populated with least +// possible value instead of 0 because there may be the case where maximum +// distance is 0 and we may require to find the line for 0 maximum distance. +func newGeomMaxDistanceUpdater( + stopAfter float64, exclusivity geo.FnExclusivity, +) *geomMaxDistanceUpdater { + return &geomMaxDistanceUpdater{ + currentValue: -math.MaxFloat64, + stopAfter: stopAfter, + exclusivity: exclusivity, + coordA: nil, + coordB: nil, + geometricalObjOrder: geometricalObjectsNotFlipped, + } +} + +// Distance implements the geodist.DistanceUpdater interface. +func (u *geomMaxDistanceUpdater) Distance() float64 { + return u.currentValue +} + +// Update implements the geodist.DistanceUpdater interface. +func (u *geomMaxDistanceUpdater) Update(aPoint geodist.Point, bPoint geodist.Point) bool { + a := aPoint.GeomPoint + b := bPoint.GeomPoint + + dist := coordNorm(coordSub(a, b)) + if dist > u.currentValue || u.coordA == nil { + u.currentValue = dist + if u.geometricalObjOrder == geometricalObjectsFlipped { + u.coordA = b + u.coordB = a + } else { + u.coordA = a + u.coordB = b + } + if u.exclusivity == geo.FnExclusive { + return dist >= u.stopAfter + } + return dist > u.stopAfter + } + return false +} + +// OnIntersects implements the geodist.DistanceUpdater interface. +func (u *geomMaxDistanceUpdater) OnIntersects(p geodist.Point) bool { + return false +} + +// IsMaxDistance implements the geodist.DistanceUpdater interface. +func (u *geomMaxDistanceUpdater) IsMaxDistance() bool { + return true +} + +// FlipGeometries implements the geodist.DistanceUpdater interface. +func (u *geomMaxDistanceUpdater) FlipGeometries() { + u.geometricalObjOrder = -u.geometricalObjOrder +} + +// geomDistanceCalculator implements geodist.DistanceCalculator +type geomDistanceCalculator struct { + updater geodist.DistanceUpdater + boundingBoxIntersects bool +} + +var _ geodist.DistanceCalculator = (*geomDistanceCalculator)(nil) + +// DistanceUpdater implements geodist.DistanceCalculator. +func (c *geomDistanceCalculator) DistanceUpdater() geodist.DistanceUpdater { + return c.updater +} + +// BoundingBoxIntersects implements geodist.DistanceCalculator. +func (c *geomDistanceCalculator) BoundingBoxIntersects() bool { + return c.boundingBoxIntersects +} + +// NewEdgeCrosser implements geodist.DistanceCalculator. +func (c *geomDistanceCalculator) NewEdgeCrosser( + edge geodist.Edge, startPoint geodist.Point, +) geodist.EdgeCrosser { + return &geomGeodistEdgeCrosser{ + strategy: &lineintersector.NonRobustLineIntersector{}, + edgeV0: edge.V0.GeomPoint, + edgeV1: edge.V1.GeomPoint, + nextEdgeV0: startPoint.GeomPoint, + } +} + +// side corresponds to the side in which a point is relative to a line. +type pointSide int + +const ( + pointSideLeft pointSide = -1 + pointSideOn pointSide = 0 + pointSideRight pointSide = 1 +) + +// findPointSide finds which side a point is relative to the infinite line +// given by the edge. +// Note this side is relative to the orientation of the line. +func findPointSide(p geom.Coord, eV0 geom.Coord, eV1 geom.Coord) pointSide { + // This is the equivalent of using the point-gradient formula + // and determining the sign, i.e. the sign of + // d = (x-x1)(y2-y1) - (y-y1)(x2-x1) + // where (x1,y1) and (x2,y2) is the edge and (x,y) is the point + sign := (p.X()-eV0.X())*(eV1.Y()-eV0.Y()) - (eV1.X()-eV0.X())*(p.Y()-eV0.Y()) + switch { + case sign == 0: + return pointSideOn + case sign > 0: + return pointSideRight + default: + return pointSideLeft + } +} + +type linearRingSide int + +const ( + outsideLinearRing linearRingSide = -1 + onLinearRing linearRingSide = 0 + insideLinearRing linearRingSide = 1 +) + +// findPointSideOfLinearRing returns whether a point is outside, on, or inside a +// linear ring. +func findPointSideOfLinearRing(point geodist.Point, linearRing geodist.LinearRing) linearRingSide { + // This is done using the winding number algorithm, also known as the + // "non-zero rule". + // See: https://en.wikipedia.org/wiki/Point_in_polygon for intro. + // See: http://geomalgorithms.com/a03-_inclusion.html for algorithm. + // See also: https://en.wikipedia.org/wiki/Winding_number + // See also: https://en.wikipedia.org/wiki/Nonzero-rule + windingNumber := 0 + p := point.GeomPoint + for edgeIdx, numEdges := 0, linearRing.NumEdges(); edgeIdx < numEdges; edgeIdx++ { + e := linearRing.Edge(edgeIdx) + eV0 := e.V0.GeomPoint + eV1 := e.V1.GeomPoint + // Same vertex; none of these checks will pass. + if coordEqual(eV0, eV1) { + continue + } + yMin := math.Min(eV0.Y(), eV1.Y()) + yMax := math.Max(eV0.Y(), eV1.Y()) + // If the edge isn't on the same level as Y, this edge isn't worth considering. + if p.Y() > yMax || p.Y() < yMin { + continue + } + side := findPointSide(p, eV0, eV1) + // If the point is on the line if the edge was infinite, and the point is within the bounds + // of the line segment denoted by the edge, there is a covering. + if side == pointSideOn && (eV0.X() <= p.X() && p.X() <= eV1.X()) { + return onLinearRing + } + // If the point is left of the segment and the line is rising + // we have a circle going CCW, so increment. + // Note we only compare [start, end) as we do not want to double count points + // which are on the same X / Y axis as an edge vertex. + if side == pointSideLeft && eV0.Y() <= p.Y() && p.Y() < eV1.Y() { + windingNumber++ + } + // If the line is to the right of the segment and the + // line is falling, we a have a circle going CW so decrement. + // Note we only compare [start, end) as we do not want to double count points + // which are on the same X / Y axis as an edge vertex. + if side == pointSideRight && eV1.Y() <= p.Y() && p.Y() < eV0.Y() { + windingNumber-- + } + } + if windingNumber != 0 { + return insideLinearRing + } + return outsideLinearRing +} + +// PointIntersectsLinearRing implements geodist.DistanceCalculator. +func (c *geomDistanceCalculator) PointIntersectsLinearRing( + point geodist.Point, linearRing geodist.LinearRing, +) bool { + switch findPointSideOfLinearRing(point, linearRing) { + case insideLinearRing, onLinearRing: + return true + default: + return false + } +} + +// ClosestPointToEdge implements geodist.DistanceCalculator. +func (c *geomDistanceCalculator) ClosestPointToEdge( + e geodist.Edge, p geodist.Point, +) (geodist.Point, bool) { + // Edge is a single point. Closest point must be any edge vertex. + if coordEqual(e.V0.GeomPoint, e.V1.GeomPoint) { + return e.V0, coordEqual(e.V0.GeomPoint, p.GeomPoint) + } + + // From http://www.faqs.org/faqs/graphics/algorithms-faq/, section 1.02 + // + // Let the point be C (Cx,Cy) and the line be AB (Ax,Ay) to (Bx,By). + // Let P be the point of perpendicular projection of C on AB. The parameter + // r, which indicates P's position along AB, is computed by the dot product + // of AC and AB divided by the square of the length of AB: + // + // (1) AC dot AB + // r = --------- + // ||AB||^2 + // + // r has the following meaning: + // + // r=0 P = A + // r=1 P = B + // r<0 P is on the backward extension of AB + // r>1 P is on the forward extension of AB + // 0 1 { + return p, false + } + return geodist.Point{GeomPoint: coordAdd(e.V0.GeomPoint, coordMul(ab, r))}, true +} + +// FrechetDistance calculates the Frechet distance between two geometries. +func FrechetDistance(a, b geo.Geometry) (*float64, error) { + if a.Empty() || b.Empty() { + return nil, nil + } + if a.SRID() != b.SRID() { + return nil, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + distance, err := geos.FrechetDistance(a.EWKB(), b.EWKB()) + if err != nil { + return nil, err + } + return &distance, nil +} + +// FrechetDistanceDensify calculates the Frechet distance between two geometries. +func FrechetDistanceDensify(a, b geo.Geometry, densifyFrac float64) (*float64, error) { + // For Frechet distance, we take <= 0 to disable the densifyFrac parameter. + // This differs from HausdorffDistance, but follows PostGIS behavior. + if densifyFrac <= 0 { + return FrechetDistance(a, b) + } + if a.Empty() || b.Empty() { + return nil, nil + } + if a.SRID() != b.SRID() { + return nil, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + if err := verifyDensifyFrac(densifyFrac); err != nil { + return nil, err + } + distance, err := geos.FrechetDistanceDensify(a.EWKB(), b.EWKB(), densifyFrac) + if err != nil { + return nil, err + } + return &distance, nil +} + +// HausdorffDistance calculates the Hausdorff distance between two geometries. +func HausdorffDistance(a, b geo.Geometry) (*float64, error) { + if a.Empty() || b.Empty() { + return nil, nil + } + if a.SRID() != b.SRID() { + return nil, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + distance, err := geos.HausdorffDistance(a.EWKB(), b.EWKB()) + if err != nil { + return nil, err + } + return &distance, nil +} + +// HausdorffDistanceDensify calculates the Hausdorff distance between two geometries. +func HausdorffDistanceDensify(a, b geo.Geometry, densifyFrac float64) (*float64, error) { + if a.Empty() || b.Empty() { + return nil, nil + } + if a.SRID() != b.SRID() { + return nil, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + if err := verifyDensifyFrac(densifyFrac); err != nil { + return nil, err + } + + distance, err := geos.HausdorffDistanceDensify(a.EWKB(), b.EWKB(), densifyFrac) + if err != nil { + return nil, err + } + return &distance, nil +} + +// ClosestPoint returns the first point located on geometry A on the shortest line between the geometries. +func ClosestPoint(a, b geo.Geometry) (geo.Geometry, error) { + shortestLine, err := ShortestLineString(a, b) + if err != nil { + return geo.Geometry{}, err + } + shortestLineT, err := shortestLine.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + closestPoint, err := geo.MakeGeometryFromPointCoords( + shortestLineT.(*geom.LineString).Coord(0).X(), + shortestLineT.(*geom.LineString).Coord(0).Y(), + ) + if err != nil { + return geo.Geometry{}, err + } + return closestPoint.CloneWithSRID(a.SRID()) +} + +func verifyDensifyFrac(f float64) error { + if f < 0 || f > 1 { + return pgerror.Newf(pgcode.InvalidParameterValue, "fraction must be in range [0, 1], got %f", f) + } + // Very small densifyFrac potentially causes a SIGFPE or generate a large + // amount of memory. Guard against this. + const fracTooSmall = 1e-6 + if f > 0 && f < fracTooSmall { + return pgerror.Newf(pgcode.InvalidParameterValue, "fraction %f is too small, must be at least %f", f, fracTooSmall) + } + return nil +} + +// findPointSideOfPolygon returns whether a point intersects with a polygon. +func findPointSideOfPolygon(point geom.T, polygon geom.T) (linearRingSide, error) { + // Convert point from a geom.T to a *geodist.Point. + _, ok := point.(*geom.Point) + if !ok { + return outsideLinearRing, pgerror.Newf(pgcode.InvalidParameterValue, "first geometry passed to findPointSideOfPolygon must be a point") + } + pointGeodistShape, err := geomToGeodist(point) + if err != nil { + return outsideLinearRing, err + } + pointGeodistPoint, ok := pointGeodistShape.(*geodist.Point) + if !ok { + return outsideLinearRing, pgerror.Newf(pgcode.InvalidParameterValue, "geomToGeodist failed to convert a *geom.Point to a *geodist.Point") + } + + // Convert polygon from a geom.T to a geodist.Polygon. + _, ok = polygon.(*geom.Polygon) + if !ok { + return outsideLinearRing, pgerror.Newf(pgcode.InvalidParameterValue, "second geometry passed to findPointSideOfPolygon must be a polygon") + } + polygonGeodistShape, err := geomToGeodist(polygon) + if err != nil { + return outsideLinearRing, err + } + polygonGeodistPolygon, ok := polygonGeodistShape.(geodist.Polygon) + if !ok { + return outsideLinearRing, pgerror.Newf(pgcode.InvalidParameterValue, "geomToGeodist failed to convert a *geom.Polygon to a geodist.Polygon") + } + + // Point cannot be inside an empty polygon. + if polygonGeodistPolygon.NumLinearRings() == 0 { + return outsideLinearRing, nil + } + + // Find which side the point is relative to the main outer boundary of + // the polygon. If it outside or on the boundary, we can conclude + // that the point is on that side of the overall polygon as well. + mainRing := polygonGeodistPolygon.LinearRing(0) + switch pointSide := findPointSideOfLinearRing(*pointGeodistPoint, mainRing); pointSide { + case insideLinearRing: + case outsideLinearRing, onLinearRing: + return pointSide, nil + default: + return outsideLinearRing, pgerror.Newf(pgcode.InvalidParameterValue, "unknown linearRingSide %d", pointSide) + } + + // If the point is inside the main outer boundary of the polygon, we must + // determine which side it is relative to every hole in the polygon. + // If it is inside any hole, it is outside the polygon. If it is on the + // ring of any hole, it is on the boundary of the polygon. Otherwise, + // it is inside the polygon. + for ringNum := 1; ringNum < polygonGeodistPolygon.NumLinearRings(); ringNum++ { + polygonHole := polygonGeodistPolygon.LinearRing(ringNum) + switch pointSide := findPointSideOfLinearRing(*pointGeodistPoint, polygonHole); pointSide { + case insideLinearRing: + return outsideLinearRing, nil + case onLinearRing: + return onLinearRing, nil + case outsideLinearRing: + continue + default: + return outsideLinearRing, pgerror.Newf(pgcode.InvalidParameterValue, "unknown linearRingSide %d", pointSide) + } + } + + return insideLinearRing, nil +} diff --git a/pkg/geo/geomfn/envelope.go b/pkg/geo/geomfn/envelope.go new file mode 100644 index 0000000..cbfaa5d --- /dev/null +++ b/pkg/geo/geomfn/envelope.go @@ -0,0 +1,18 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + +// Envelope forms an envelope (compliant with the OGC spec) of the given Geometry. +// It uses the bounding box to return a Polygon, but can return a Point or +// Line if the bounding box is degenerate and not a box. +func Envelope(g geo.Geometry) (geo.Geometry, error) { + if g.Empty() { + return g, nil + } + return geo.MakeGeometryFromGeomT(g.CartesianBoundingBox().ToGeomT(g.SRID())) +} diff --git a/pkg/geo/geomfn/flip_coordinates.go b/pkg/geo/geomfn/flip_coordinates.go new file mode 100644 index 0000000..0bade6a --- /dev/null +++ b/pkg/geo/geomfn/flip_coordinates.go @@ -0,0 +1,39 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/twpayne/go-geom" +) + +// FlipCoordinates returns a modified g whose X, Y coordinates are flipped. +func FlipCoordinates(g geo.Geometry) (geo.Geometry, error) { + if g.Empty() { + return g, nil + } + + t, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + + newT, err := applyOnCoordsForGeomT(t, func(l geom.Layout, dst, src []float64) error { + dst[0], dst[1] = src[1], src[0] + if l.ZIndex() != -1 { + dst[l.ZIndex()] = src[l.ZIndex()] + } + if l.MIndex() != -1 { + dst[l.MIndex()] = src[l.MIndex()] + } + return nil + }) + if err != nil { + return geo.Geometry{}, err + } + + return geo.MakeGeometryFromGeomT(newT) +} diff --git a/pkg/geo/geomfn/force_layout.go b/pkg/geo/geomfn/force_layout.go new file mode 100644 index 0000000..8f1cf6b --- /dev/null +++ b/pkg/geo/geomfn/force_layout.go @@ -0,0 +1,156 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/twpayne/go-geom" +) + +// ForceLayout forces a geometry into the given layout. +// If a Z dimension is added, 0 is added as the Z value. +// If an M dimension is added, 0 is added as the M value. +func ForceLayout(g geo.Geometry, layout geom.Layout) (geo.Geometry, error) { + return ForceLayoutWithDefaultZM(g, layout, 0, 0) +} + +// ForceLayoutWithDefaultZ forces a geometry into the given layout. +// If a Z dimension is added, defaultZ is added as the Z value. +// If an M dimension is added, 0 is added as the M value. +func ForceLayoutWithDefaultZ( + g geo.Geometry, layout geom.Layout, defaultZ float64, +) (geo.Geometry, error) { + return ForceLayoutWithDefaultZM(g, layout, defaultZ, 0) +} + +// ForceLayoutWithDefaultM forces a geometry into the given layout. +// If a Z dimension is added, 0 is added as the Z value. +// If an M dimension is added, defaultM is added as the M value. +func ForceLayoutWithDefaultM( + g geo.Geometry, layout geom.Layout, defaultM float64, +) (geo.Geometry, error) { + return ForceLayoutWithDefaultZM(g, layout, 0, defaultM) +} + +// ForceLayoutWithDefaultZM forces a geometry into the given layout. +// If a Z dimension is added, defaultZ is added as the Z value. +// If an M dimension is added, defaultM is added as the M value. +func ForceLayoutWithDefaultZM( + g geo.Geometry, layout geom.Layout, defaultZ float64, defaultM float64, +) (geo.Geometry, error) { + geomT, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + retGeomT, err := forceLayout(geomT, layout, defaultZ, defaultM) + if err != nil { + return geo.Geometry{}, err + } + return geo.MakeGeometryFromGeomT(retGeomT) +} + +// forceLayout forces a geom.T into the given layout and uses +// the specified default Z and M values as needed. +func forceLayout(t geom.T, layout geom.Layout, defaultZ float64, defaultM float64) (geom.T, error) { + if t.Layout() == layout { + return t, nil + } + switch t := t.(type) { + case *geom.GeometryCollection: + ret := geom.NewGeometryCollection().SetSRID(t.SRID()) + if err := ret.SetLayout(layout); err != nil { + return nil, err + } + for i := 0; i < t.NumGeoms(); i++ { + toPush, err := forceLayout(t.Geom(i), layout, defaultZ, defaultM) + if err != nil { + return nil, err + } + if err := ret.Push(toPush); err != nil { + return nil, err + } + } + return ret, nil + case *geom.Point: + return geom.NewPointFlat( + layout, forceFlatCoordsLayout(t, layout, defaultZ, defaultM)).SetSRID(t.SRID()), nil + case *geom.LineString: + return geom.NewLineStringFlat( + layout, forceFlatCoordsLayout(t, layout, defaultZ, defaultM)).SetSRID(t.SRID()), nil + case *geom.Polygon: + return geom.NewPolygonFlat( + layout, + forceFlatCoordsLayout(t, layout, defaultZ, defaultM), + forceEnds(t.Ends(), t.Layout(), layout), + ).SetSRID(t.SRID()), nil + case *geom.MultiPoint: + return geom.NewMultiPointFlat( + layout, + forceFlatCoordsLayout(t, layout, defaultZ, defaultM), + geom.NewMultiPointFlatOptionWithEnds(forceEnds(t.Ends(), t.Layout(), layout)), + ).SetSRID(t.SRID()), nil + case *geom.MultiLineString: + return geom.NewMultiLineStringFlat( + layout, + forceFlatCoordsLayout(t, layout, defaultZ, defaultM), + forceEnds(t.Ends(), t.Layout(), layout), + ).SetSRID(t.SRID()), nil + case *geom.MultiPolygon: + endss := make([][]int, len(t.Endss())) + for i := range t.Endss() { + endss[i] = forceEnds(t.Endss()[i], t.Layout(), layout) + } + return geom.NewMultiPolygonFlat( + layout, + forceFlatCoordsLayout(t, layout, defaultZ, defaultM), + endss, + ).SetSRID(t.SRID()), nil + default: + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "unknown geom.T type: %T", t) + } +} + +// forceEnds forces the Endss layout of a geometry into the new layout. +func forceEnds(ends []int, oldLayout geom.Layout, newLayout geom.Layout) []int { + if oldLayout.Stride() == newLayout.Stride() { + return ends + } + newEnds := make([]int, len(ends)) + for i := range ends { + newEnds[i] = (ends[i] / oldLayout.Stride()) * newLayout.Stride() + } + return newEnds +} + +// forceFlatCoordsLayout forces the flatCoords layout of a geometry into the new layout +// and uses the specified default Z and M values as needed. +func forceFlatCoordsLayout( + t geom.T, layout geom.Layout, defaultZ float64, defaultM float64, +) []float64 { + oldFlatCoords := t.FlatCoords() + newFlatCoords := make([]float64, (len(oldFlatCoords)/t.Stride())*layout.Stride()) + for coordIdx := 0; coordIdx < len(oldFlatCoords)/t.Stride(); coordIdx++ { + newFlatCoords[coordIdx*layout.Stride()] = oldFlatCoords[coordIdx*t.Stride()] + newFlatCoords[coordIdx*layout.Stride()+1] = oldFlatCoords[coordIdx*t.Stride()+1] + if layout.ZIndex() != -1 { + z := defaultZ + if t.Layout().ZIndex() != -1 { + z = oldFlatCoords[coordIdx*t.Stride()+t.Layout().ZIndex()] + } + newFlatCoords[coordIdx*layout.Stride()+layout.ZIndex()] = z + } + if layout.MIndex() != -1 { + m := defaultM + if t.Layout().MIndex() != -1 { + m = oldFlatCoords[coordIdx*t.Stride()+t.Layout().MIndex()] + } + newFlatCoords[coordIdx*layout.Stride()+layout.MIndex()] = m + } + } + return newFlatCoords +} diff --git a/pkg/geo/geomfn/generate_points.go b/pkg/geo/geomfn/generate_points.go new file mode 100644 index 0000000..3549a51 --- /dev/null +++ b/pkg/geo/geomfn/generate_points.go @@ -0,0 +1,221 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "math" + "math/rand" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geos" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/errors" + "github.com/twpayne/go-geom" +) + +// maxAllowedGridSize is the upper bound limit for a generated grid size. +const maxAllowedGridSize = 100 * geo.MaxAllowedSplitPoints + +// ErrGenerateRandomPointsInvalidPoints is returned if we have a negative number of points +// or an empty geometry. +var ErrGenerateRandomPointsInvalidPoints = pgerror.Newf( + pgcode.InvalidParameterValue, + "points must be positive and geometry must not be empty", +) + +// GenerateRandomPoints generates provided number of pseudo-random points for the input area. +func GenerateRandomPoints(g geo.Geometry, nPoints int, rng *rand.Rand) (geo.Geometry, error) { + var generateRandomPointsFunction func(g geo.Geometry, nPoints int, rng *rand.Rand) (*geom.MultiPoint, error) + switch g.ShapeType() { + case geopb.ShapeType_Polygon: + generateRandomPointsFunction = generateRandomPointsFromPolygon + case geopb.ShapeType_MultiPolygon: + generateRandomPointsFunction = generateRandomPointsFromMultiPolygon + default: + return geo.Geometry{}, pgerror.Newf(pgcode.InvalidParameterValue, "unsupported type: %v", g.ShapeType().String()) + } + if nPoints <= 0 { + return geo.Geometry{}, ErrGenerateRandomPointsInvalidPoints + } + if nPoints > geo.MaxAllowedSplitPoints { + return geo.Geometry{}, pgerror.Newf( + pgcode.InvalidParameterValue, + "failed to generate random points, too many points to generate: requires %d points, max %d", + nPoints, + geo.MaxAllowedSplitPoints, + ) + } + empty, err := IsEmpty(g) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "could not check if geometry is empty") + } + if empty { + return geo.Geometry{}, ErrGenerateRandomPointsInvalidPoints + } + mpt, err := generateRandomPointsFunction(g, nPoints, rng) + if err != nil { + return geo.Geometry{}, err + } + srid := g.SRID() + mpt.SetSRID(int(srid)) + out, err := geo.MakeGeometryFromGeomT(mpt) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "could not transform geom.T into geometry") + } + return out, nil +} + +// generateRandomPointsFromPolygon returns a given number of randomly generated points that are within the Polygon provided. +// In order to get more uniformed number of points, it's generating a grid, and then generating a random point within each grid cell. +// Read more: http://lin-ear-th-inking.blogspot.com/2010/05/more-random-points-in-jts.html. +// Rough description of the algorithm: +// 1. Generate the grid cells. The higher number of points to generate and bounding box area/polygon area ratio, the smaller cell size. +// 2. Shuffle the array of grid cells indexes. This way the order in which grid cells are chosen for point generation is random. +// 3. For each grid cell, generate a random point within it. If the point intersects with our geometry, add it to the results. +// 4. When there are enough points in the results, stop. Otherwise go to step 3. +func generateRandomPointsFromPolygon( + g geo.Geometry, nPoints int, rng *rand.Rand, +) (*geom.MultiPoint, error) { + area, err := Area(g) + if err != nil { + return nil, errors.Wrap(err, "could not calculate Polygon area") + } + if area == 0.0 { + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "zero area input Polygon") + } + bbox := g.CartesianBoundingBox() + bboxWidth := bbox.HiX - bbox.LoX + bboxHeight := bbox.HiY - bbox.LoY + bboxArea := bboxHeight * bboxWidth + // Gross up our test set a bit to increase odds of getting coverage in one pass. + sampleNPoints := float64(nPoints) * bboxArea / area + + sampleSqrt := math.Round(math.Sqrt(sampleNPoints)) + if sampleSqrt == 0 { + sampleSqrt = 1 + } + var sampleHeight, sampleWidth int + var sampleCellSize float64 + // Calculate the grids we're going to randomize within. + if bboxWidth > bboxHeight { + sampleWidth = int(sampleSqrt) + sampleHeight = int(math.Ceil(sampleNPoints / float64(sampleWidth))) + sampleCellSize = bboxWidth / float64(sampleWidth) + } else { + sampleHeight = int(sampleSqrt) + sampleWidth = int(math.Ceil(sampleNPoints / float64(sampleHeight))) + sampleCellSize = bboxHeight / float64(sampleHeight) + } + n := sampleHeight * sampleWidth + if n > maxAllowedGridSize { + return nil, pgerror.Newf( + pgcode.InvalidParameterValue, + "generated area is too large: %d, max %d", + n, + maxAllowedGridSize, + ) + } + + // Prepare the polygon for fast true/false testing. + gPrep, err := geos.PrepareGeometry(g.EWKB()) + if err != nil { + return nil, errors.Wrap(err, "could not prepare geometry") + } + defer geos.PreparedGeomDestroy(gPrep) + + // Generate a slice of points - for every cell on a grid store coordinates. + cells := make([]geom.Coord, n) + for i := 0; i < sampleWidth; i++ { + for j := 0; j < sampleHeight; j++ { + cells[i*sampleHeight+j] = geom.Coord{float64(i), float64(j)} + } + } + // Shuffle the points. Without shuffling, the generated point will + // always be adjacent to the previous one (in terms of grid cells). + if n > 1 { + rng.Shuffle(n, func(i int, j int) { + temp := cells[j] + cells[j] = cells[i] + cells[i] = temp + }) + } + results := geom.NewMultiPoint(geom.XY) + // Generate points and test them. + for nPointsGenerated, iterations := 0, 0; nPointsGenerated < nPoints && iterations <= 100; iterations++ { + for _, cell := range cells { + y := bbox.LoY + cell.X()*sampleCellSize + x := bbox.LoX + cell.Y()*sampleCellSize + x += rng.Float64() * sampleCellSize + y += rng.Float64() * sampleCellSize + if x > bbox.HiX || y > bbox.HiY { + continue + } + gpt, err := geo.MakeGeometryFromPointCoords(x, y) + if err != nil { + return nil, errors.Wrap(err, "could not create geometry Point") + } + intersects, err := geos.PreparedIntersects(gPrep, gpt.EWKB()) + if err != nil { + return nil, errors.Wrap(err, "could not check prepared intersection") + } + if intersects { + nPointsGenerated++ + p := geom.NewPointFlat(geom.XY, []float64{x, y}) + srid := g.SRID() + p.SetSRID(int(srid)) + err = results.Push(p) + if err != nil { + return nil, errors.Wrap(err, "could not add point to the results") + } + if nPointsGenerated == nPoints { + return results, nil + } + } + } + } + return results, nil +} + +func generateRandomPointsFromMultiPolygon( + g geo.Geometry, nPoints int, rng *rand.Rand, +) (*geom.MultiPoint, error) { + results := geom.NewMultiPoint(geom.XY) + + area, err := Area(g) + if err != nil { + return nil, errors.Wrap(err, "could not calculate MultiPolygon area") + } + + gt, err := g.AsGeomT() + if err != nil { + return nil, errors.Wrap(err, "could not transform MultiPolygon into geom.T") + } + + gmp := gt.(*geom.MultiPolygon) + for i := 0; i < gmp.NumPolygons(); i++ { + poly := gmp.Polygon(i) + subarea := poly.Area() + subNPoints := int(math.Round(float64(nPoints) * subarea / area)) + if subNPoints > 0 { + g, err := geo.MakeGeometryFromGeomT(poly) + if err != nil { + return nil, errors.Wrap(err, "could not transform geom.T into Geometry") + } + subMPT, err := generateRandomPointsFromPolygon(g, subNPoints, rng) + if err != nil { + return nil, errors.Wrap(err, "error generating points for Polygon") + } + for j := 0; j < subMPT.NumPoints(); j++ { + if err := results.Push(subMPT.Point(j)); err != nil { + return nil, errors.Wrap(err, "could not push point to the results") + } + } + } + } + return results, nil +} diff --git a/pkg/geo/geomfn/geomfn.go b/pkg/geo/geomfn/geomfn.go new file mode 100644 index 0000000..03fdea2 --- /dev/null +++ b/pkg/geo/geomfn/geomfn.go @@ -0,0 +1,174 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +// Package geomfn contains functions that are used for geometry-based builtins. +package geomfn + +import ( + // Blank import so projections are initialized correctly. + _ "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geographiclib" + "github.com/twpayne/go-geom" +) + +// applyCoordFunc applies a function on src to copy onto dst. +// Both slices represent a single Coord within the FlatCoord array. +type applyCoordFunc func(l geom.Layout, dst []float64, src []float64) error + +// applyOnCoords applies the applyCoordFunc on each coordinate, returning +// a new array for the coordinates. +func applyOnCoords(flatCoords []float64, l geom.Layout, f applyCoordFunc) ([]float64, error) { + newCoords := make([]float64, len(flatCoords)) + for i := 0; i < len(flatCoords); i += l.Stride() { + if err := f(l, newCoords[i:i+l.Stride()], flatCoords[i:i+l.Stride()]); err != nil { + return nil, err + } + } + return newCoords, nil +} + +// applyOnCoordsForGeomT applies the applyCoordFunc on each coordinate in the geom.T, +// returning a copied over geom.T. +func applyOnCoordsForGeomT(g geom.T, f applyCoordFunc) (geom.T, error) { + if geomCollection, ok := g.(*geom.GeometryCollection); ok { + return applyOnCoordsForGeometryCollection(geomCollection, f) + } + + newCoords, err := applyOnCoords(g.FlatCoords(), g.Layout(), f) + if err != nil { + return nil, err + } + + switch t := g.(type) { + case *geom.Point: + g = geom.NewPointFlat(t.Layout(), newCoords).SetSRID(g.SRID()) + case *geom.LineString: + g = geom.NewLineStringFlat(t.Layout(), newCoords).SetSRID(g.SRID()) + case *geom.Polygon: + g = geom.NewPolygonFlat(t.Layout(), newCoords, t.Ends()).SetSRID(g.SRID()) + case *geom.MultiPoint: + g = geom.NewMultiPointFlat(t.Layout(), newCoords, geom.NewMultiPointFlatOptionWithEnds(t.Ends())).SetSRID(g.SRID()) + case *geom.MultiLineString: + g = geom.NewMultiLineStringFlat(t.Layout(), newCoords, t.Ends()).SetSRID(g.SRID()) + case *geom.MultiPolygon: + g = geom.NewMultiPolygonFlat(t.Layout(), newCoords, t.Endss()).SetSRID(g.SRID()) + default: + return nil, geom.ErrUnsupportedType{Value: g} + } + + return g, nil +} + +// applyOnCoordsForGeometryCollection applies the applyCoordFunc on each coordinate +// inside a geometry collection, returning a copied over geom.T. +func applyOnCoordsForGeometryCollection( + geomCollection *geom.GeometryCollection, f applyCoordFunc, +) (*geom.GeometryCollection, error) { + res := geom.NewGeometryCollection() + for _, subG := range geomCollection.Geoms() { + subGeom, err := applyOnCoordsForGeomT(subG, f) + if err != nil { + return nil, err + } + + if err := res.Push(subGeom); err != nil { + return nil, err + } + } + return res, nil +} + +// removeConsecutivePointsFromGeomT removes duplicate consecutive points from a given geom.T. +// If the resultant geometry is invalid, it will be EMPTY. +func removeConsecutivePointsFromGeomT(t geom.T) (geom.T, error) { + if t.Empty() { + return t, nil + } + switch t := t.(type) { + case *geom.Point, *geom.MultiPoint: + return t, nil + case *geom.LineString: + newCoords := make([]float64, 0, len(t.FlatCoords())) + newCoords = append(newCoords, t.Coord(0)...) + for i := 1; i < t.NumCoords(); i++ { + if !t.Coord(i).Equal(t.Layout(), t.Coord(i-1)) { + newCoords = append(newCoords, t.Coord(i)...) + } + } + if len(newCoords) < t.Stride()*2 { + newCoords = newCoords[:0] + } + return geom.NewLineStringFlat(t.Layout(), newCoords).SetSRID(t.SRID()), nil + case *geom.Polygon: + ret := geom.NewPolygon(t.Layout()).SetSRID(t.SRID()) + for ringIdx := 0; ringIdx < t.NumLinearRings(); ringIdx++ { + ring := t.LinearRing(ringIdx) + newCoords := make([]float64, 0, len(ring.FlatCoords())) + newCoords = append(newCoords, ring.Coord(0)...) + for i := 1; i < ring.NumCoords(); i++ { + if !ring.Coord(i).Equal(ring.Layout(), ring.Coord(i-1)) { + newCoords = append(newCoords, ring.Coord(i)...) + } + } + if len(newCoords) < t.Stride()*4 { + // If the outer ring is invalid, the polygon should be entirely empty. + if ringIdx == 0 { + return ret, nil + } + // Ignore any holes. + continue + } + if err := ret.Push(geom.NewLinearRingFlat(t.Layout(), newCoords)); err != nil { + return nil, err + } + } + return ret, nil + case *geom.MultiLineString: + ret := geom.NewMultiLineString(t.Layout()).SetSRID(t.SRID()) + for i := 0; i < t.NumLineStrings(); i++ { + ls, err := removeConsecutivePointsFromGeomT(t.LineString(i)) + if err != nil { + return nil, err + } + if ls.Empty() { + continue + } + if err := ret.Push(ls.(*geom.LineString)); err != nil { + return nil, err + } + } + return ret, nil + case *geom.MultiPolygon: + ret := geom.NewMultiPolygon(t.Layout()).SetSRID(t.SRID()) + for i := 0; i < t.NumPolygons(); i++ { + p, err := removeConsecutivePointsFromGeomT(t.Polygon(i)) + if err != nil { + return nil, err + } + if p.Empty() { + continue + } + if err := ret.Push(p.(*geom.Polygon)); err != nil { + return nil, err + } + } + return ret, nil + case *geom.GeometryCollection: + ret := geom.NewGeometryCollection().SetSRID(t.SRID()) + for i := 0; i < t.NumGeoms(); i++ { + g, err := removeConsecutivePointsFromGeomT(t.Geom(i)) + if err != nil { + return nil, err + } + if g.Empty() { + continue + } + if err := ret.Push(g); err != nil { + return nil, err + } + } + return ret, nil + } + return nil, geom.ErrUnsupportedType{Value: t} +} diff --git a/pkg/geo/geomfn/line_crossing_direction.go b/pkg/geo/geomfn/line_crossing_direction.go new file mode 100644 index 0000000..69e759f --- /dev/null +++ b/pkg/geo/geomfn/line_crossing_direction.go @@ -0,0 +1,156 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/twpayne/go-geom" +) + +// lineCrossingDirection corresponds to integer defining particular direction. +type lineCrossingDirection int + +// LineCrossingDirectionValue maps to the return value of +// ST_LineCrossingDirection from PostGIS. +type LineCrossingDirectionValue int + +// Assigning constant integer values to variables for different crossing behavior and directions +// throughout the functions. +const ( + LineNoCross LineCrossingDirectionValue = 0 + LineCrossLeft LineCrossingDirectionValue = -1 + LineCrossRight LineCrossingDirectionValue = 1 + LineMultiCrossToLeft LineCrossingDirectionValue = -2 + LineMultiCrossToRight LineCrossingDirectionValue = 2 + LineMultiCrossToSameFirstLeft LineCrossingDirectionValue = -3 + LineMultiCrossToSameFirstRight LineCrossingDirectionValue = 3 + + leftDir lineCrossingDirection = -1 + noCrossOrCollinear lineCrossingDirection = 0 + rightDir lineCrossingDirection = 1 + + inLeft lineCrossingDirection = -1 + isCollinear lineCrossingDirection = 0 + inRight lineCrossingDirection = 1 +) + +// getPosition takes 3 points and returns the direction of third point from segment formed by first point and second point. +func getPosition(fromSeg, toSeg, givenPoint geom.Coord) lineCrossingDirection { + + // crossProductValueInZ is value of Cross Product of two vectors in z axis direction where + // first vector is from givenPoint to toSeg and second vector is from givenPoint to fromSeg. + crossProductValueInZ := (toSeg.X()-givenPoint.X())*(fromSeg.Y()-givenPoint.Y()) - (toSeg.Y()-givenPoint.Y())*(fromSeg.X()-givenPoint.X()) + + if crossProductValueInZ < 0 { + return inLeft + } + if crossProductValueInZ > 0 { + return inRight + } + return isCollinear +} + +// getSegCrossDirection takes 4 points and returns the crossing direction of second segment over first segment +// first segment is formed by first 2 points whereas second segment is formed by next 2 points. +func getSegCrossDirection(fromSeg1, toSeg1, fromSeg2, toSeg2 geom.Coord) lineCrossingDirection { + posF1FromSeg2 := getPosition(fromSeg2, toSeg2, fromSeg1) + posT1FromSeg2 := getPosition(fromSeg2, toSeg2, toSeg1) + + posF2FromSeg1 := getPosition(fromSeg1, toSeg1, fromSeg2) + posT2FromSeg1 := getPosition(fromSeg1, toSeg1, toSeg2) + + // If both point of any segment is on same side of other segment + // or second point of any segment is collinear with other segment + // then return the value as noCrossOrCollinear else return the direction of segment2 over segment1. + // To avoid double counting, collinearity condition is used with second points only. + if posF1FromSeg2 == posT1FromSeg2 || posF2FromSeg1 == posT2FromSeg1 || + posT1FromSeg2 == isCollinear || posT2FromSeg1 == isCollinear { + return noCrossOrCollinear + } + return posT2FromSeg1 +} + +// LineCrossingDirection takes two lines and returns an integer value defining behavior of crossing of lines: +// 0: lines do not cross, +// -1: line2 crosses line1 from right to left, +// 1: line2 crosses line1 from left to right, +// -2: line2 crosses line1 multiple times from right to left, +// 2: line2 crosses line1 multiple times from left to right, +// -3: line2 crosses line1 multiple times from left to left, +// 3: line2 crosses line1 multiple times from right to Right. +// Note that the top vertex of the segment touching another line does not count as a crossing, +// but the bottom vertex of segment touching another line is considered a crossing. +func LineCrossingDirection(geometry1, geometry2 geo.Geometry) (LineCrossingDirectionValue, error) { + + t1, err1 := geometry1.AsGeomT() + if err1 != nil { + return 0, err1 + } + + t2, err2 := geometry2.AsGeomT() + if err2 != nil { + return 0, err2 + } + + g1, ok1 := t1.(*geom.LineString) + g2, ok2 := t2.(*geom.LineString) + + if !ok1 || !ok2 { + return 0, pgerror.Newf(pgcode.InvalidParameterValue, "arguments must be LINESTRING") + } + + line1, line2 := g1.Coords(), g2.Coords() + + firstSegCrossDirection := noCrossOrCollinear + countDirection := make(map[lineCrossingDirection]int) + + // Iterating segment2 over line2 from tail to head. + for idx2 := 1; idx2 < len(line2); idx2++ { + fromSeg2, toSeg2 := line2[idx2-1], line2[idx2] + + // Iterating segment1 over line1 from tail to head. + for idx1 := 1; idx1 < len(line1); idx1++ { + fromSeg1, toSeg1 := line1[idx1-1], line1[idx1] + + // segCrossDirection is current crossing direction of segment2 over segment1. + segCrossDirection := getSegCrossDirection(fromSeg1, toSeg1, fromSeg2, toSeg2) + // update count of current crossing direction. + countDirection[segCrossDirection]++ + + // firstSegCrossDirection keeps direction of first segment2 which cross over segment1. + if firstSegCrossDirection == noCrossOrCollinear { + firstSegCrossDirection = segCrossDirection + } + } + } + + if countDirection[leftDir] == 0 && countDirection[rightDir] == 0 { + return LineNoCross, nil + } + if countDirection[leftDir] == 1 && countDirection[rightDir] == 0 { + return LineCrossLeft, nil + } + if countDirection[leftDir] == 0 && countDirection[rightDir] == 1 { + return LineCrossRight, nil + } + if countDirection[leftDir]-countDirection[rightDir] == 1 { + return LineMultiCrossToLeft, nil + } + if countDirection[rightDir]-countDirection[leftDir] == 1 { + return LineMultiCrossToRight, nil + } + if countDirection[leftDir] == countDirection[rightDir] { + + if firstSegCrossDirection == leftDir { + return LineMultiCrossToSameFirstLeft, nil + } + return LineMultiCrossToSameFirstRight, nil + } + + return LineNoCross, nil +} diff --git a/pkg/geo/geomfn/linear_reference.go b/pkg/geo/geomfn/linear_reference.go new file mode 100644 index 0000000..b195138 --- /dev/null +++ b/pkg/geo/geomfn/linear_reference.go @@ -0,0 +1,69 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geos" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/twpayne/go-geom" + "github.com/twpayne/go-geom/encoding/ewkb" +) + +// LineInterpolatePoints returns one or more points along the given +// LineString which are at an integral multiples of given fraction of +// LineString's total length. When repeat is set to false, it returns +// the first point. +func LineInterpolatePoints(g geo.Geometry, fraction float64, repeat bool) (geo.Geometry, error) { + if fraction < 0 || fraction > 1 { + return geo.Geometry{}, pgerror.Newf(pgcode.InvalidParameterValue, "fraction %f should be within [0 1] range", fraction) + } + geomRepr, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + switch geomRepr := geomRepr.(type) { + case *geom.LineString: + // In case fraction is greater than 0.5 or equal to 0 or repeat is false, + // then we will have only one interpolated point. + lengthOfLineString := geomRepr.Length() + if repeat && fraction <= 0.5 && fraction != 0 { + numberOfInterpolatedPoints := int(1 / fraction) + if numberOfInterpolatedPoints > geo.MaxAllowedSplitPoints { + return geo.Geometry{}, pgerror.Newf( + pgcode.InvalidParameterValue, + "attempting to interpolate into too many points; requires %d points, max %d", + numberOfInterpolatedPoints, + geo.MaxAllowedSplitPoints, + ) + } + interpolatedPoints := geom.NewMultiPoint(geom.XY).SetSRID(geomRepr.SRID()) + for pointInserted := 1; pointInserted <= numberOfInterpolatedPoints; pointInserted++ { + pointEWKB, err := geos.InterpolateLine(g.EWKB(), float64(pointInserted)*fraction*lengthOfLineString) + if err != nil { + return geo.Geometry{}, err + } + point, err := ewkb.Unmarshal(pointEWKB) + if err != nil { + return geo.Geometry{}, err + } + err = interpolatedPoints.Push(point.(*geom.Point)) + if err != nil { + return geo.Geometry{}, err + } + } + return geo.MakeGeometryFromGeomT(interpolatedPoints) + } + interpolatedPointEWKB, err := geos.InterpolateLine(g.EWKB(), fraction*lengthOfLineString) + if err != nil { + return geo.Geometry{}, err + } + return geo.ParseGeometryFromEWKB(interpolatedPointEWKB) + default: + return geo.Geometry{}, pgerror.Newf(pgcode.InvalidParameterValue, "geometry %s should be LineString", g.ShapeType()) + } +} diff --git a/pkg/geo/geomfn/linestring.go b/pkg/geo/geomfn/linestring.go new file mode 100644 index 0000000..b4978c2 --- /dev/null +++ b/pkg/geo/geomfn/linestring.go @@ -0,0 +1,360 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geos" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/errors" + "github.com/twpayne/go-geom" + "github.com/twpayne/go-geom/encoding/ewkb" +) + +// LineStringFromMultiPoint generates a linestring from a multipoint. +func LineStringFromMultiPoint(g geo.Geometry) (geo.Geometry, error) { + t, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + mp, ok := t.(*geom.MultiPoint) + if !ok { + return geo.Geometry{}, errors.Wrap(geom.ErrUnsupportedType{Value: t}, + "geometry must be a MultiPoint") + } + if mp.NumPoints() == 1 { + return geo.Geometry{}, pgerror.Newf(pgcode.InvalidParameterValue, "a LineString must have at least 2 points") + } + flatCoords := make([]float64, 0, mp.NumCoords()*mp.Stride()) + var prevPoint *geom.Point + for i := 0; i < mp.NumPoints(); i++ { + p := mp.Point(i) + // Empty points in multipoints are replaced by the previous point. + // If the previous point does not exist, we append (0, 0, ...) coordinates. + if p.Empty() { + if prevPoint == nil { + prevPoint = geom.NewPointFlat(mp.Layout(), make([]float64, mp.Stride())) + } + flatCoords = append(flatCoords, prevPoint.FlatCoords()...) + continue + } + flatCoords = append(flatCoords, p.FlatCoords()...) + prevPoint = p + } + lineString := geom.NewLineStringFlat(mp.Layout(), flatCoords).SetSRID(mp.SRID()) + return geo.MakeGeometryFromGeomT(lineString) +} + +// LineMerge merges multilinestring constituents. +func LineMerge(g geo.Geometry) (geo.Geometry, error) { + // Mirrors PostGIS behavior + if g.Empty() { + return g, nil + } + if BoundingBoxHasInfiniteCoordinates(g) { + return geo.Geometry{}, pgerror.Newf(pgcode.InvalidParameterValue, "value out of range: overflow") + } + ret, err := geos.LineMerge(g.EWKB()) + if err != nil { + return geo.Geometry{}, err + } + return geo.ParseGeometryFromEWKB(ret) +} + +// LineLocatePoint returns a float between 0 and 1 representing the location of the +// closest point on LineString to the given Point, as a fraction of total 2d line length. +func LineLocatePoint(line geo.Geometry, point geo.Geometry) (float64, error) { + lineT, err := line.AsGeomT() + if err != nil { + return 0, err + } + lineString, ok := lineT.(*geom.LineString) + if !ok { + return 0, pgerror.Newf(pgcode.InvalidParameterValue, + "first parameter has to be of type LineString") + } + + // compute closest point on line to the given point + closestPoint, err := ClosestPoint(line, point) + if err != nil { + return 0, err + } + closestT, err := closestPoint.AsGeomT() + if err != nil { + return 0, err + } + + p := closestT.(*geom.Point) + lineStart := geom.Coord{lineString.Coord(0).X(), lineString.Coord(0).Y()} + // build new line segment to the closest point we found + lineSegment := geom.NewLineString(geom.XY).MustSetCoords([]geom.Coord{lineStart, p.Coords()}) + + // compute fraction of new line segment compared to total line length + return lineSegment.Length() / lineString.Length(), nil +} + +// AddPoint adds a point to a LineString at the given 0-based index. -1 appends. +func AddPoint(lineString geo.Geometry, index int, point geo.Geometry) (geo.Geometry, error) { + g, err := lineString.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + lineStringG, ok := g.(*geom.LineString) + if !ok { + e := geom.ErrUnsupportedType{Value: g} + return geo.Geometry{}, errors.Wrap(e, "geometry to be modified must be a LineString") + } + + g, err = point.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + + pointG, ok := g.(*geom.Point) + if !ok { + e := geom.ErrUnsupportedType{Value: g} + return geo.Geometry{}, errors.Wrapf(e, "invalid geometry used to add a Point to a LineString") + } + + g, err = addPoint(lineStringG, index, pointG) + if err != nil { + return geo.Geometry{}, err + } + + return geo.MakeGeometryFromGeomT(g) +} + +func addPoint(lineString *geom.LineString, index int, point *geom.Point) (*geom.LineString, error) { + if lineString.Layout() != point.Layout() { + return nil, pgerror.WithCandidateCode( + geom.ErrLayoutMismatch{Got: point.Layout(), Want: lineString.Layout()}, + pgcode.InvalidParameterValue, + ) + } + if point.Empty() { + point = geom.NewPointFlat(point.Layout(), make([]float64, point.Stride())) + } + + coords := lineString.Coords() + + if index > len(coords) { + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "index %d out of range of LineString with %d coordinates", + index, len(coords)) + } else if index == -1 { + index = len(coords) + } else if index < 0 { + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "invalid index %v", index) + } + + // Shift the slice right by one element, then replace the element at the index, to avoid + // allocating an additional slice. + coords = append(coords, geom.Coord{}) + copy(coords[index+1:], coords[index:]) + coords[index] = point.Coords() + + return lineString.SetCoords(coords) +} + +// SetPoint sets the point at the given index of lineString; index is 0-based. +func SetPoint(lineString geo.Geometry, index int, point geo.Geometry) (geo.Geometry, error) { + g, err := lineString.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + + lineStringG, ok := g.(*geom.LineString) + if !ok { + e := geom.ErrUnsupportedType{Value: g} + return geo.Geometry{}, errors.Wrap(e, "geometry to be modified must be a LineString") + } + + g, err = point.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + + pointG, ok := g.(*geom.Point) + if !ok { + e := geom.ErrUnsupportedType{Value: g} + return geo.Geometry{}, errors.Wrapf(e, "invalid geometry used to replace a Point on a LineString") + } + + g, err = setPoint(lineStringG, index, pointG) + if err != nil { + return geo.Geometry{}, err + } + + return geo.MakeGeometryFromGeomT(g) +} + +func setPoint(lineString *geom.LineString, index int, point *geom.Point) (*geom.LineString, error) { + if lineString.Layout() != point.Layout() { + return nil, geom.ErrLayoutMismatch{Got: point.Layout(), Want: lineString.Layout()} + } + if point.Empty() { + point = geom.NewPointFlat(point.Layout(), make([]float64, point.Stride())) + } + + coords := lineString.Coords() + hasNegIndex := index < 0 + + if index >= len(coords) || (hasNegIndex && index*-1 > len(coords)) { + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "index %d out of range of LineString with %d coordinates", index, len(coords)) + } + + if hasNegIndex { + index = len(coords) + index + } + + coords[index].Set(point.Coords()) + + return lineString.SetCoords(coords) +} + +// RemovePoint removes the point at the given index of lineString; index is 0-based. +func RemovePoint(lineString geo.Geometry, index int) (geo.Geometry, error) { + g, err := lineString.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + + lineStringG, ok := g.(*geom.LineString) + if !ok { + e := geom.ErrUnsupportedType{Value: g} + return geo.Geometry{}, errors.Wrap(e, "geometry to be modified must be a LineString") + } + + if lineStringG.NumCoords() == 2 { + return geo.Geometry{}, pgerror.Newf(pgcode.InvalidParameterValue, "cannot remove a point from a LineString with only two Points") + } + + g, err = removePoint(lineStringG, index) + if err != nil { + return geo.Geometry{}, err + } + + return geo.MakeGeometryFromGeomT(g) +} + +func removePoint(lineString *geom.LineString, index int) (*geom.LineString, error) { + coords := lineString.Coords() + + if index >= len(coords) || index < 0 { + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "index %d out of range of LineString with %d coordinates", index, len(coords)) + } + + coords = append(coords[:index], coords[index+1:]...) + + return lineString.SetCoords(coords) +} + +// LineSubstring returns a LineString being a substring by start and end +func LineSubstring(g geo.Geometry, start, end float64) (geo.Geometry, error) { + if start < 0 || start > 1 || end < 0 || end > 1 { + return g, pgerror.Newf(pgcode.InvalidParameterValue, + "start and and must be within 0 and 1") + } + if start > end { + return g, pgerror.Newf(pgcode.InvalidParameterValue, + "end must be greater or equal to the start") + } + + lineT, err := g.AsGeomT() + if err != nil { + return g, err + } + lineString, ok := lineT.(*geom.LineString) + if !ok { + return g, pgerror.Newf(pgcode.InvalidParameterValue, + "geometry has to be of type LineString") + } + if lineString.Empty() { + return geo.MakeGeometryFromGeomT( + geom.NewLineString(geom.XY).SetSRID(lineString.SRID()), + ) + } + if start == end { + return LineInterpolatePoints(g, start, false) + } + + lsLength := lineString.Length() + // A LineString which entirely consistent of the same point has length 0 + // and should return the single point that represents it. + if lsLength == 0 { + return geo.MakeGeometryFromGeomT( + geom.NewPointFlat(geom.XY, lineString.FlatCoords()[0:2]).SetSRID(lineString.SRID()), + ) + } + + var newFlatCoords []float64 + // The algorithm roughly as follows. + // * For each line segment, first find whether we have exceeded the start distance. + // If we have, interpolate the point on that LineString that represents the start point. + // * Keep adding points until we have we reach the segment where the entire LineString + // exceeds the max distance. + // We then interpolate the end point on the last segment of the LineString. + startDistance, endDistance := start*lsLength, end*lsLength + for i := range lineString.Coords() { + currentLineString, err := geom.NewLineString(geom.XY).SetCoords(lineString.Coords()[0 : i+1]) + if err != nil { + return geo.Geometry{}, err + } + // If the current distance exceeds the end distance, find the last point and + // terminate the loop early. + if currentLineString.Length() >= endDistance { + // If we have not added coordinates to the LineString, it means the + // current segment starts and ends on the current line segment. + // Interpolate the start position. + if len(newFlatCoords) == 0 { + coords, err := interpolateFlatCoordsFromDistance(g, startDistance) + if err != nil { + return geo.Geometry{}, err + } + newFlatCoords = append(newFlatCoords, coords...) + } + + coords, err := interpolateFlatCoordsFromDistance(g, endDistance) + if err != nil { + return geo.Geometry{}, err + } + newFlatCoords = append(newFlatCoords, coords...) + break + } + // If we are past the start distance, check if we already have points + // in the LineString. + if currentLineString.Length() >= startDistance { + if len(newFlatCoords) == 0 { + // If this is our first point, interpolate the first point. + coords, err := interpolateFlatCoordsFromDistance(g, startDistance) + if err != nil { + return geo.Geometry{}, err + } + newFlatCoords = append(newFlatCoords, coords...) + } + + // Add the current point if it is not the same as the previous point. + prevCoords := geom.Coord(newFlatCoords[len(newFlatCoords)-2:]) + if !currentLineString.Coord(i).Equal(geom.XY, prevCoords) { + newFlatCoords = append(newFlatCoords, currentLineString.Coord(i)...) + } + } + } + return geo.MakeGeometryFromGeomT(geom.NewLineStringFlat(geom.XY, newFlatCoords).SetSRID(lineString.SRID())) +} + +// interpolateFlatCoordsFromDistance interpolates the geometry at a given distance and returns its flat coordinates. +func interpolateFlatCoordsFromDistance(g geo.Geometry, distance float64) ([]float64, error) { + pointEWKB, err := geos.InterpolateLine(g.EWKB(), distance) + if err != nil { + return []float64{}, err + } + point, err := ewkb.Unmarshal(pointEWKB) + if err != nil { + return []float64{}, err + } + return point.FlatCoords(), nil +} diff --git a/pkg/geo/geomfn/make_geometry.go b/pkg/geo/geomfn/make_geometry.go new file mode 100644 index 0000000..2ece269 --- /dev/null +++ b/pkg/geo/geomfn/make_geometry.go @@ -0,0 +1,95 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/twpayne/go-geom" +) + +// MakePolygon creates a Polygon geometry from linestring and optional inner linestrings. +// Returns errors if geometries are not linestrings. +func MakePolygon(outer geo.Geometry, interior ...geo.Geometry) (geo.Geometry, error) { + layout := geom.XY + outerGeomT, err := outer.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + outerRing, ok := outerGeomT.(*geom.LineString) + if !ok { + return geo.Geometry{}, pgerror.Newf(pgcode.InvalidParameterValue, "argument must be LINESTRING geometries") + } + if outerRing.Empty() { + return geo.Geometry{}, pgerror.Newf(pgcode.InvalidParameterValue, "polygon shell must not be empty") + } + srid := outerRing.SRID() + coords := make([][]geom.Coord, len(interior)+1) + coords[0] = outerRing.Coords() + for i, g := range interior { + interiorRingGeomT, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + interiorRing, ok := interiorRingGeomT.(*geom.LineString) + if !ok { + return geo.Geometry{}, pgerror.Newf(pgcode.InvalidParameterValue, "argument must be LINESTRING geometries") + } + if interiorRing.SRID() != srid { + return geo.Geometry{}, pgerror.Newf(pgcode.InvalidParameterValue, "mixed SRIDs are not allowed") + } + if outerRing.Layout() != interiorRing.Layout() { + return geo.Geometry{}, pgerror.Newf(pgcode.InvalidParameterValue, "mixed dimension rings") + } + coords[i+1] = interiorRing.Coords() + } + + polygon, err := geom.NewPolygon(layout).SetSRID(srid).SetCoords(coords) + if err != nil { + return geo.Geometry{}, err + } + return geo.MakeGeometryFromGeomT(polygon) +} + +// MakePolygonWithSRID is like MakePolygon but also sets the SRID, like ST_Polygon. +func MakePolygonWithSRID(g geo.Geometry, srid int) (geo.Geometry, error) { + polygon, err := MakePolygon(g) + if err != nil { + return geo.Geometry{}, err + } + t, err := polygon.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + geo.AdjustGeomTSRID(t, geopb.SRID(srid)) + return geo.MakeGeometryFromGeomT(t) +} + +// Takes a multilinestring input and converts it to a slice of linestrings to call MakePolygon. +// Returns error if input is not a single multilinestring. +func MakePolygonFromMultiLineString(g geo.Geometry, srid geopb.SRID) (geo.Geometry, error) { + geomT, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + lsCollection, ok := geomT.(*geom.MultiLineString) + if !ok { + return geo.Geometry{}, pgerror.Newf(pgcode.InvalidParameterValue, "argument must be MULTILINESTRING geometry") + } + linestrings := make([]geo.Geometry, lsCollection.NumLineStrings()) + for i := 0; i < lsCollection.NumLineStrings(); i++ { + lineStringT := lsCollection.LineString(i) + geo.AdjustGeomTSRID(lineStringT, srid) + ls, err := geo.MakeGeometryFromGeomT(lineStringT) + if err != nil { + return geo.Geometry{}, err + } + linestrings[i] = ls + } + return MakePolygon(linestrings[0], linestrings[1:]...) +} diff --git a/pkg/geo/geomfn/mvtgeom.go b/pkg/geo/geomfn/mvtgeom.go new file mode 100644 index 0000000..d2c6149 --- /dev/null +++ b/pkg/geo/geomfn/mvtgeom.go @@ -0,0 +1,309 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/errors" + "github.com/twpayne/go-geom" +) + +// AsMVTGeometry returns a geometry converted into Mapbox Vector Tile coordinate +// space. +func AsMVTGeometry( + g geo.Geometry, bounds geo.CartesianBoundingBox, extent int, buffer int, clipGeometry bool, +) (geo.Geometry, error) { + bbox := bounds.BoundingBox + err := validateInputParams(bbox, extent, buffer) + if err != nil { + return geo.Geometry{}, err + } + gt, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to convert geometry to geom.T") + } + if geometrySmallerThanHalfBoundingBox(gt, bbox, extent) { + return geo.Geometry{}, nil + } + + return transformToMVTGeom(gt, g, bbox, extent, buffer, clipGeometry) +} + +func validateInputParams(bbox geopb.BoundingBox, extent int, buffer int) error { + if bbox.HiX-bbox.LoX <= 0 || bbox.HiY-bbox.LoY <= 0 { + return pgerror.New(pgcode.InvalidParameterValue, "geometric bounds are too small") + } + if extent <= 0 { + return pgerror.New(pgcode.InvalidParameterValue, "extent must be greater than 0") + } + if buffer < 0 { + return pgerror.New(pgcode.InvalidParameterValue, "buffer value cannot be negative") + } + return nil +} + +func geometrySmallerThanHalfBoundingBox(gt geom.T, bbox geopb.BoundingBox, extent int) bool { + switch gt := gt.(type) { + // Don't check GeometryCollection so that we don't discard a collection of points. + case *geom.LineString, *geom.MultiLineString, *geom.Polygon, *geom.MultiPolygon: + geometryBoundingBox := gt.Bounds() + geomWidth := geometryBoundingBox.Max(0) - geometryBoundingBox.Min(0) + geomHeight := geometryBoundingBox.Max(1) - geometryBoundingBox.Min(1) + // We use half of the square height and width as limit instead of area so it works properly with lines. + halfBoundsWidth := ((bbox.HiX - bbox.LoX) / float64(extent)) / 2 + halfBoundsHeight := ((bbox.HiY - bbox.LoY) / float64(extent)) / 2 + if geomWidth < halfBoundsWidth && geomHeight < halfBoundsHeight { + return true + } + } + return false +} + +// transformToMVTGeom transforms a geometry into vector tile coordinate space. +func transformToMVTGeom( + gt geom.T, g geo.Geometry, bbox geopb.BoundingBox, extent int, buffer int, clipGeometry bool, +) (geo.Geometry, error) { + basicType, err := getBasicType(gt) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to get geom.T basic type") + } + basicTypeGeometry, err := convertToBasicType(g, basicType) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to convert geometry to basic type") + } + if basicTypeGeometry.Empty() { + return geo.Geometry{}, nil + } + + removeRepeatedPointsGeometry, err := RemoveRepeatedPoints(basicTypeGeometry, 0) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to remove repeated points") + } + // Remove points on straight lines + simplifiedGeometry, empty, err := Simplify(removeRepeatedPointsGeometry, 0, false) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to simplify geometry") + } + if empty { + return geo.Geometry{}, nil + } + + affinedGeometry, err := transformToTileCoordinateSpace(simplifiedGeometry, bbox, extent) + if err != nil { + return geo.Geometry{}, err + } + + snappedGeometry, err := snapToIntegersGrid(affinedGeometry) + if err != nil { + return geo.Geometry{}, err + } + + out, err := clipAndValidateMVTOutput(snappedGeometry, basicType, extent, buffer, clipGeometry) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to clip and validate") + } + if out.Empty() { + return geo.Geometry{}, nil + } + return out, nil +} + +func transformToTileCoordinateSpace( + g geo.Geometry, bbox geopb.BoundingBox, extent int, +) (geo.Geometry, error) { + width := bbox.HiX - bbox.LoX + height := bbox.HiY - bbox.LoY + fx := float64(extent) / width + fy := -float64(extent) / height + affineMatrix := AffineMatrix{ + {fx, 0, 0, -bbox.LoX * fx}, + {0, fy, 0, -bbox.HiY * fy}, + {0, 0, 1, 0}, + {0, 0, 0, 1}, + } + affinedGeometry, err := Affine(g, affineMatrix) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to affine geometry") + } + return affinedGeometry, nil +} + +// clipAndValidateMVTOutput prepares the geometry so that it's clipped (if needed), valid, snapped to grid and it's basic type is the same as the input geometry. +func clipAndValidateMVTOutput( + g geo.Geometry, inputBasicType geopb.ShapeType, extent int, buffer int, clipGeometry bool, +) (geo.Geometry, error) { + var err error + if clipGeometry && !g.Empty() { + bbox := *g.CartesianBoundingBox() + bbox.HiX = overwriteMinusZero(float64(extent) + float64(buffer)) + bbox.HiY = overwriteMinusZero(float64(extent) + float64(buffer)) + bbox.LoX = overwriteMinusZero(-float64(buffer)) + bbox.LoY = overwriteMinusZero(-float64(buffer)) + g, err = ClipByRect(g, bbox) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to clip geometry") + } + } + g, err = snapToGridAndValidate(g, inputBasicType) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to grid and validate") + } + outputGt, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to convert geometry into geom.T") + } + outputBasicType, err := getBasicType(outputGt) + if err != nil { + return geo.Geometry{}, err + } + if inputBasicType != outputBasicType { + return geo.Geometry{}, errors.New("geometry dropped because of output type change") + } + return g, nil +} + +func snapToGridAndValidate(g geo.Geometry, basicType geopb.ShapeType) (geo.Geometry, error) { + var err error + g, err = convertToBasicType(g, basicType) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to convert to basic type") + } + if basicType != geopb.ShapeType_Polygon { + snappedGeometry, err := snapToIntegersGrid(g) + if err != nil { + return geo.Geometry{}, err + } + return snappedGeometry, nil + } + return snapToGridAndValidateBasicPolygon(g, basicType) +} + +// snapToGridAndValidateBasicPolygon snaps to the integer grid and force validation. +// Since snapping to the grid can create an invalid geometry and making it valid can create float values, +// we iterate up to 3 times to generate a valid geometry with integer coordinates. +func snapToGridAndValidateBasicPolygon( + g geo.Geometry, basicType geopb.ShapeType, +) (geo.Geometry, error) { + iterations := 0 + const maxIterations = 3 + valid := false + + var err error + g, err = snapToIntegersGrid(g) + if err != nil { + return geo.Geometry{}, err + } + valid, err = IsValid(g) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to check validity") + } + for !valid && iterations < maxIterations { + g, err = MakeValid(g) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to make valid") + } + + g, err = snapToIntegersGrid(g) + if err != nil { + return geo.Geometry{}, err + } + converted, err := convertToBasicType(g, basicType) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to convert to basic type") + } + g = converted + valid, err = IsValid(g) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to check validity") + } + iterations++ + } + if !valid { + return geo.Geometry{}, nil + } + + gt, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to convert to geom.T") + } + err = forcePolygonOrientation(gt, OrientationCCW) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to force polygon orientation") + } + g, err = geo.MakeGeometryFromGeomT(gt) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to make geometry from geom.T") + } + return g, nil +} + +func snapToIntegersGrid(g geo.Geometry) (geo.Geometry, error) { + offset := geom.Coord{0, 0, 0, 0} + grid := geom.Coord{1, 1, 0, 0} + g, err := SnapToGrid(g, offset, grid) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to snap to grid") + } + return g, nil +} + +func overwriteMinusZero(val float64) float64 { + if val == -0 { + return 0 + } + return val +} + +// For a given geometry, look for the highest dimensional basic type: point, line or polygon. +func getBasicType(gt geom.T) (geopb.ShapeType, error) { + switch gt := gt.(type) { + case *geom.Point: + return geopb.ShapeType_Point, nil + case *geom.LineString: + return geopb.ShapeType_LineString, nil + case *geom.Polygon: + return geopb.ShapeType_Polygon, nil + case *geom.MultiPoint: + return geopb.ShapeType_Point, nil + case *geom.MultiLineString: + return geopb.ShapeType_LineString, nil + case *geom.MultiPolygon: + return geopb.ShapeType_Polygon, nil + case *geom.GeometryCollection: + maxShape := geopb.ShapeType_Unset + for _, subGt := range gt.Geoms() { + shapeType, err := getBasicType(subGt) + if err != nil { + return geopb.ShapeType_Unset, err + } + if shapeType > maxShape { + maxShape = shapeType + } + } + return maxShape, nil + default: + return geopb.ShapeType_Unset, geom.ErrUnsupportedType{Value: gt} + } +} + +func convertToBasicType(g geo.Geometry, basicType geopb.ShapeType) (geo.Geometry, error) { + var err error + if g.ShapeType() == geopb.ShapeType_GeometryCollection { + g, err = CollectionExtract(g, basicType) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to extract collection") + } + } + // If the extracted geometry is a MultiPoint, MultiLineString or MultiPolygon but includes only one sub geometry, we want to return that subgeometry. + out, err := CollectionHomogenize(g) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "failed to homogenize collection") + } + return out, nil +} diff --git a/pkg/geo/geomfn/node.go b/pkg/geo/geomfn/node.go new file mode 100644 index 0000000..74ab2f2 --- /dev/null +++ b/pkg/geo/geomfn/node.go @@ -0,0 +1,223 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geos" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/errors" + geom "github.com/twpayne/go-geom" + "github.com/twpayne/go-geom/xy" +) + +// Node returns a geometry containing a set of LineStrings using the least +// possible number of nodes while preserving all of the input ones. +func Node(g geo.Geometry) (geo.Geometry, error) { + if g.ShapeType() != geopb.ShapeType_LineString && g.ShapeType() != geopb.ShapeType_MultiLineString { + return geo.Geometry{}, pgerror.Newf( + pgcode.InvalidParameterValue, + "geometry type is unsupported. Please pass a LineString or a MultiLineString", + ) + } + + // Return GEOMETRYCOLLECTION EMPTY if it is empty. + if g.Empty() { + return geo.MakeGeometryFromGeomT(geom.NewGeometryCollection().SetSRID(int(g.SRID()))) + } + + res, err := geos.Node(g.EWKB()) + if err != nil { + return geo.Geometry{}, err + } + node, err := geo.ParseGeometryFromEWKB(res) + if err != nil { + return geo.Geometry{}, err + } + + res, err = geos.LineMerge(node.EWKB()) + if err != nil { + return geo.Geometry{}, err + } + lines, err := geo.ParseGeometryFromEWKB(res) + if err != nil { + return geo.Geometry{}, err + } + if lines.ShapeType() == geopb.ShapeType_LineString { + // No nodes found, return a MultiLineString. + return node, nil + } + + glines, err := lines.AsGeomT() + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "error transforming lines") + } + ep, err := extractEndpoints(g) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "error extracting endpoints") + } + var mllines *geom.MultiLineString + switch t := glines.(type) { + case *geom.MultiLineString: + mllines = t + case *geom.GeometryCollection: + if t.Empty() { + t.SetSRID(int(g.SRID())) + return geo.MakeGeometryFromGeomT(t) + } + return geo.Geometry{}, errors.AssertionFailedf("unknown GEOMETRYCOLLECTION: %T", t) + default: + return geo.Geometry{}, errors.AssertionFailedf("unknown LineMerge result type: %T", t) + } + + gep, err := ep.AsGeomT() + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "error transforming endpoints") + } + mpep := gep.(*geom.MultiPoint) + mlout, err := splitLinesByPoints(mllines, mpep) + if err != nil { + return geo.Geometry{}, err + } + mlout.SetSRID(int(g.SRID())) + out, err := geo.MakeGeometryFromGeomT(mlout) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "could not transform output into geometry") + } + return out, nil +} + +// splitLinesByPoints goes through every LineString and tries to split that LineString by any +// of the Points provided. Does not split LineString if the Point is an endpoint for that line. +// Returns MultiLineString consisting of splitted, as well as not splitted provided LineStrings. +func splitLinesByPoints( + mllines *geom.MultiLineString, mpep *geom.MultiPoint, +) (*geom.MultiLineString, error) { + mlout := geom.NewMultiLineString(geom.XY) + splitted := false + var err error + var splitLines []*geom.LineString + for i := 0; i < mllines.NumLineStrings(); i++ { + l := mllines.LineString(i) + for j := 0; j < mpep.NumPoints(); j++ { + p := mpep.Point(j) + splitted, splitLines, err = splitLineByPoint(l, p.Coords()) + if err != nil { + return nil, errors.Wrap(err, "could not split line") + } + if splitted { + err = mlout.Push(splitLines[0]) + if err != nil { + return nil, errors.Wrap(err, "could not construct output geometry") + } + err = mlout.Push(splitLines[1]) + if err != nil { + return nil, errors.Wrap(err, "could not construct output geometry") + } + break + } + } + if !splitted { + err = mlout.Push(l) + if err != nil { + return nil, errors.Wrap(err, "could not construct output geometry") + } + } + } + return mlout, nil +} + +// extractEndpoints extracts the endpoints from geometry provided and returns them as a MultiPoint geometry. +func extractEndpoints(g geo.Geometry) (geo.Geometry, error) { + mp := geom.NewMultiPoint(geom.XY) + + gt, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "error transforming geometry") + } + + switch gt := gt.(type) { + case *geom.LineString: + endpoints := collectEndpoints(gt) + for _, endpoint := range endpoints { + err := mp.Push(endpoint) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "error creating output geometry") + } + } + case *geom.MultiLineString: + for i := 0; i < gt.NumLineStrings(); i++ { + ls := gt.LineString(i) + endpoints := collectEndpoints(ls) + for _, endpoint := range endpoints { + err := mp.Push(endpoint) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "error creating output geometry") + } + } + } + default: + return geo.Geometry{}, pgerror.Newf(pgcode.InvalidParameterValue, "unsupported type: %T", gt) + } + + result, err := geo.MakeGeometryFromGeomT(mp) + if err != nil { + return geo.Geometry{}, errors.Wrap(err, "error creating output geometry") + } + return result, nil +} + +// collectEndpoints returns endpoints of the line provided as a slice of Points. +func collectEndpoints(ls *geom.LineString) []*geom.Point { + coord := ls.Coord(0) + startPoint := geom.NewPointFlat(geom.XY, []float64{coord.X(), coord.Y()}) + coord = ls.Coord(ls.NumCoords() - 1) + endPoint := geom.NewPointFlat(geom.XY, []float64{coord.X(), coord.Y()}) + return []*geom.Point{startPoint, endPoint} +} + +// splitLineByPoint splits the line using the Point provided. +// Returns a bool representing whether the line was splitted or not, and a slice of output LineStrings. +// The Point must be on provided line and not an endpoint, otherwise false is returned along with unsplit line. +func splitLineByPoint(l *geom.LineString, p geom.Coord) (bool, []*geom.LineString, error) { + // Do not split if Point is not on line. + if !xy.IsOnLine(l.Layout(), p, l.FlatCoords()) { + return false, []*geom.LineString{l}, nil + } + // Do not split if Point is the endpoint of the line. + startCoord := l.Coord(0) + endCoord := l.Coord(l.NumCoords() - 1) + if p.Equal(l.Layout(), startCoord) || p.Equal(l.Layout(), endCoord) { + return false, []*geom.LineString{l}, nil + } + // Find where to split the line and group coords. + coordsA := []geom.Coord{} + coordsB := []geom.Coord{} + for i := 1; i < l.NumCoords(); i++ { + if xy.IsPointWithinLineBounds(p, l.Coord(i-1), l.Coord(i)) { + coordsA = append(l.Coords()[0:i], p) + if p.Equal(l.Layout(), l.Coord(i)) { + coordsB = l.Coords()[i:] + } else { + coordsB = append([]geom.Coord{p}, l.Coords()[i:]...) + } + break + } + } + l1 := geom.NewLineString(l.Layout()) + _, err := l1.SetCoords(coordsA) + if err != nil { + return false, []*geom.LineString{}, errors.Wrap(err, "could not set coords") + } + l2 := geom.NewLineString(l.Layout()) + _, err = l2.SetCoords(coordsB) + if err != nil { + return false, []*geom.LineString{}, errors.Wrap(err, "could not set coords") + } + return true, []*geom.LineString{l1, l2}, nil +} diff --git a/pkg/geo/geomfn/orientation.go b/pkg/geo/geomfn/orientation.go new file mode 100644 index 0000000..c6bc7d7 --- /dev/null +++ b/pkg/geo/geomfn/orientation.go @@ -0,0 +1,150 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/twpayne/go-geom" +) + +// Orientation defines an orientation of a shape. +type Orientation int + +const ( + // OrientationCW denotes a clockwise orientation. + OrientationCW Orientation = iota + // OrientationCCW denotes a counter-clockwise orientation + OrientationCCW +) + +// HasPolygonOrientation checks whether a given Geometry have polygons +// that matches the given Orientation. +// Non-Polygon objects +func HasPolygonOrientation(g geo.Geometry, o Orientation) (bool, error) { + t, err := g.AsGeomT() + if err != nil { + return false, err + } + return hasPolygonOrientation(t, o) +} + +func hasPolygonOrientation(g geom.T, o Orientation) (bool, error) { + switch g := g.(type) { + case *geom.Polygon: + for i := 0; i < g.NumLinearRings(); i++ { + isCCW := geo.IsLinearRingCCW(g.LinearRing(i)) + // Interior rings should be the reverse orientation of the exterior ring. + if i > 0 { + isCCW = !isCCW + } + switch o { + case OrientationCW: + if isCCW { + return false, nil + } + case OrientationCCW: + if !isCCW { + return false, nil + } + default: + return false, pgerror.Newf(pgcode.InvalidParameterValue, "unexpected orientation: %v", o) + } + } + return true, nil + case *geom.MultiPolygon: + for i := 0; i < g.NumPolygons(); i++ { + if ret, err := hasPolygonOrientation(g.Polygon(i), o); !ret || err != nil { + return ret, err + } + } + return true, nil + case *geom.GeometryCollection: + for i := 0; i < g.NumGeoms(); i++ { + if ret, err := hasPolygonOrientation(g.Geom(i), o); !ret || err != nil { + return ret, err + } + } + return true, nil + case *geom.Point, *geom.MultiPoint, *geom.LineString, *geom.MultiLineString: + return true, nil + default: + return false, pgerror.Newf(pgcode.InvalidParameterValue, "unhandled geometry type: %T", g) + } +} + +// ForcePolygonOrientation forces orientations within polygons +// to be oriented the prescribed way. +func ForcePolygonOrientation(g geo.Geometry, o Orientation) (geo.Geometry, error) { + t, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + + if err := forcePolygonOrientation(t, o); err != nil { + return geo.Geometry{}, err + } + return geo.MakeGeometryFromGeomT(t) +} + +func forcePolygonOrientation(g geom.T, o Orientation) error { + switch g := g.(type) { + case *geom.Polygon: + for i := 0; i < g.NumLinearRings(); i++ { + isCCW := geo.IsLinearRingCCW(g.LinearRing(i)) + // Interior rings should be the reverse orientation of the exterior ring. + if i > 0 { + isCCW = !isCCW + } + reverse := false + switch o { + case OrientationCW: + if isCCW { + reverse = true + } + case OrientationCCW: + if !isCCW { + reverse = true + } + default: + return pgerror.Newf(pgcode.InvalidParameterValue, "unexpected orientation: %v", o) + } + + if reverse { + // Reverse coordinates from both ends. + // Do this by swapping up to the middle of the array of elements, which guarantees + // each end get swapped. This works for an odd number of elements as well as + // the middle element ends swapping with itself, which is ok. + coords := g.LinearRing(i).FlatCoords() + for cIdx := 0; cIdx < len(coords)/2; cIdx += g.Stride() { + for sIdx := 0; sIdx < g.Stride(); sIdx++ { + coords[cIdx+sIdx], coords[len(coords)-cIdx-g.Stride()+sIdx] = coords[len(coords)-cIdx-g.Stride()+sIdx], coords[cIdx+sIdx] + } + } + } + } + return nil + case *geom.MultiPolygon: + for i := 0; i < g.NumPolygons(); i++ { + if err := forcePolygonOrientation(g.Polygon(i), o); err != nil { + return err + } + } + return nil + case *geom.GeometryCollection: + for i := 0; i < g.NumGeoms(); i++ { + if err := forcePolygonOrientation(g.Geom(i), o); err != nil { + return err + } + } + return nil + case *geom.Point, *geom.MultiPoint, *geom.LineString, *geom.MultiLineString: + return nil + default: + return pgerror.Newf(pgcode.InvalidParameterValue, "unhandled geometry type: %T", g) + } +} diff --git a/pkg/geo/geomfn/point_polygon_optimization.go b/pkg/geo/geomfn/point_polygon_optimization.go new file mode 100644 index 0000000..cb0d63d --- /dev/null +++ b/pkg/geo/geomfn/point_polygon_optimization.go @@ -0,0 +1,221 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/errors" +) + +// PointPolygonControlFlowType signals what control flow to follow. +type PointPolygonControlFlowType int + +const ( + // PPCFCheckNextPolygon signals that the current point should be checked + // against the next polygon. + PPCFCheckNextPolygon PointPolygonControlFlowType = iota + // PPCFSkipToNextPoint signals that the rest of the checking for the current + // point can be skipped. + PPCFSkipToNextPoint + // PPCFReturnTrue signals that the function should exit early and return true. + PPCFReturnTrue +) + +// PointInPolygonEventListener is an interface implemented for each +// binary predicate making use of the point in polygon optimization +// to specify the behavior in pointKindRelatesToPolygonKind. +type PointInPolygonEventListener interface { + // OnPointIntersectsPolygon returns whether the function should exit and + // return true, skip to the next point, or check the current point against + // the next polygon in the case where a point intersects with a polygon. + // The strictlyInside param signifies whether the point is strictly inside + // or on the boundary of the polygon. + OnPointIntersectsPolygon(strictlyInside bool) PointPolygonControlFlowType + // OnPointDoesNotIntersect returns whether the function should early exit and + // return false in the case where a point does not intersect any polygon. + ExitIfPointDoesNotIntersect() bool + // AfterPointPolygonLoops returns the bool to return after the point-polygon + // loops have finished. + AfterPointPolygonLoops() bool +} + +// For Intersects, at least one point must intersect with at least one polygon. +type intersectsPIPEventListener struct{} + +func (el *intersectsPIPEventListener) OnPointIntersectsPolygon( + strictlyInside bool, +) PointPolygonControlFlowType { + // A single intersection is sufficient. + return PPCFReturnTrue +} + +func (el *intersectsPIPEventListener) ExitIfPointDoesNotIntersect() bool { + return false +} + +func (el *intersectsPIPEventListener) AfterPointPolygonLoops() bool { + return false +} + +var _ PointInPolygonEventListener = (*intersectsPIPEventListener)(nil) + +func newIntersectsPIPEventListener() *intersectsPIPEventListener { + return &intersectsPIPEventListener{} +} + +// For CoveredBy, every point must intersect with at least one polygon. +type coveredByPIPEventListener struct { + intersectsOnce bool +} + +func (el *coveredByPIPEventListener) OnPointIntersectsPolygon( + strictlyInside bool, +) PointPolygonControlFlowType { + // If the current point intersects, check the next point. + el.intersectsOnce = true + return PPCFSkipToNextPoint +} + +func (el *coveredByPIPEventListener) ExitIfPointDoesNotIntersect() bool { + // Each point in a (multi)point must intersect a polygon in the + // (multi)point to be covered by it. + return true +} + +func (el *coveredByPIPEventListener) AfterPointPolygonLoops() bool { + return el.intersectsOnce +} + +var _ PointInPolygonEventListener = (*coveredByPIPEventListener)(nil) + +func newCoveredByPIPEventListener() *coveredByPIPEventListener { + return &coveredByPIPEventListener{intersectsOnce: false} +} + +// For Within, every point must intersect with at least one polygon. +type withinPIPEventListener struct { + insideOnce bool +} + +func (el *withinPIPEventListener) OnPointIntersectsPolygon( + strictlyInside bool, +) PointPolygonControlFlowType { + // We can only skip to the next point if we have already seen a point + // that is inside the (multi)polygon. + if el.insideOnce { + return PPCFSkipToNextPoint + } + if strictlyInside { + el.insideOnce = true + return PPCFSkipToNextPoint + } + return PPCFCheckNextPolygon +} + +func (el *withinPIPEventListener) ExitIfPointDoesNotIntersect() bool { + // Each point in a (multi)point must intersect a polygon in the + // (multi)polygon to be contained within it. + return true +} + +func (el *withinPIPEventListener) AfterPointPolygonLoops() bool { + return el.insideOnce +} + +var _ PointInPolygonEventListener = (*withinPIPEventListener)(nil) + +func newWithinPIPEventListener() *withinPIPEventListener { + return &withinPIPEventListener{insideOnce: false} +} + +// PointKindIntersectsPolygonKind returns whether a (multi)point +// and a (multi)polygon intersect. +func PointKindIntersectsPolygonKind( + pointKind geo.Geometry, polygonKind geo.Geometry, +) (bool, error) { + return pointKindRelatesToPolygonKind(pointKind, polygonKind, newIntersectsPIPEventListener()) +} + +// PointKindCoveredByPolygonKind returns whether a (multi)point +// is covered by a (multi)polygon. +func PointKindCoveredByPolygonKind(pointKind geo.Geometry, polygonKind geo.Geometry) (bool, error) { + return pointKindRelatesToPolygonKind(pointKind, polygonKind, newCoveredByPIPEventListener()) +} + +// PointKindWithinPolygonKind returns whether a (multi)point +// is contained within a (multi)polygon. +func PointKindWithinPolygonKind(pointKind geo.Geometry, polygonKind geo.Geometry) (bool, error) { + return pointKindRelatesToPolygonKind(pointKind, polygonKind, newWithinPIPEventListener()) +} + +// pointKindRelatesToPolygonKind returns whether a (multi)point +// and a (multi)polygon have the given relationship. +func pointKindRelatesToPolygonKind( + pointKind geo.Geometry, polygonKind geo.Geometry, eventListener PointInPolygonEventListener, +) (bool, error) { + // Nothing can relate to a NaN coordinate. + if BoundingBoxHasNaNCoordinates(pointKind) || BoundingBoxHasNaNCoordinates(polygonKind) { + return false, nil + } + pointKindBaseT, err := pointKind.AsGeomT() + if err != nil { + return false, err + } + polygonKindBaseT, err := polygonKind.AsGeomT() + if err != nil { + return false, err + } + pointKindIterator := geo.NewGeomTIterator(pointKindBaseT, geo.EmptyBehaviorOmit) + polygonKindIterator := geo.NewGeomTIterator(polygonKindBaseT, geo.EmptyBehaviorOmit) + + // Check whether each point intersects with at least one polygon. + // The behavior for each predicate is dictated by eventListener. +pointOuterLoop: + for { + point, hasPoint, err := pointKindIterator.Next() + if err != nil { + return false, err + } + if !hasPoint { + break + } + // Reset the polygon iterator on each iteration of the point iterator. + polygonKindIterator.Reset() + curIntersects := false + for { + polygon, hasPolygon, err := polygonKindIterator.Next() + if err != nil { + return false, err + } + if !hasPolygon { + break + } + pointSide, err := findPointSideOfPolygon(point, polygon) + if err != nil { + return false, err + } + switch pointSide { + case insideLinearRing, onLinearRing: + curIntersects = true + strictlyInside := pointSide == insideLinearRing + switch eventListener.OnPointIntersectsPolygon(strictlyInside) { + case PPCFCheckNextPolygon: + case PPCFSkipToNextPoint: + continue pointOuterLoop + case PPCFReturnTrue: + return true, nil + } + case outsideLinearRing: + default: + return false, errors.AssertionFailedf("findPointSideOfPolygon returned unknown linearRingSide %d", pointSide) + } + } + if !curIntersects && eventListener.ExitIfPointDoesNotIntersect() { + return false, nil + } + } + return eventListener.AfterPointPolygonLoops(), nil +} diff --git a/pkg/geo/geomfn/remove_repeated_points.go b/pkg/geo/geomfn/remove_repeated_points.go new file mode 100644 index 0000000..fad5d5e --- /dev/null +++ b/pkg/geo/geomfn/remove_repeated_points.go @@ -0,0 +1,114 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/errors" + "github.com/twpayne/go-geom" +) + +// RemoveRepeatedPoints returns the geometry with repeated points removed. +func RemoveRepeatedPoints(g geo.Geometry, tolerance float64) (geo.Geometry, error) { + t, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + // Use the square of the tolerance to avoid taking the square root of distance results. + t, err = removeRepeatedPointsFromGeomT(t, tolerance*tolerance) + if err != nil { + return geo.Geometry{}, err + } + return geo.MakeGeometryFromGeomT(t) +} + +func removeRepeatedPointsFromGeomT(t geom.T, tolerance2 float64) (geom.T, error) { + switch t := t.(type) { + case *geom.Point: + case *geom.LineString: + if coords, modified := removeRepeatedCoords(t.Layout(), t.Coords(), tolerance2, 2); modified { + return t.SetCoords(coords) + } + case *geom.Polygon: + if coords, modified := removeRepeatedCoords2(t.Layout(), t.Coords(), tolerance2, 4); modified { + return t.SetCoords(coords) + } + case *geom.MultiPoint: + if coords, modified := removeRepeatedCoords(t.Layout(), t.Coords(), tolerance2, 0); modified { + return t.SetCoords(coords) + } + case *geom.MultiLineString: + if coords, modified := removeRepeatedCoords2(t.Layout(), t.Coords(), tolerance2, 2); modified { + return t.SetCoords(coords) + } + case *geom.MultiPolygon: + if coords, modified := removeRepeatedCoords3(t.Layout(), t.Coords(), tolerance2, 4); modified { + return t.SetCoords(coords) + } + case *geom.GeometryCollection: + for _, g := range t.Geoms() { + if _, err := removeRepeatedPointsFromGeomT(g, tolerance2); err != nil { + return nil, err + } + } + default: + return nil, errors.AssertionFailedf("unknown geometry type: %T", t) + } + return t, nil +} + +func removeRepeatedCoords( + layout geom.Layout, coords []geom.Coord, tolerance2 float64, minCoords int, +) ([]geom.Coord, bool) { + modified := false + switch tolerance2 { + case 0: + for i := 1; i < len(coords) && len(coords) > minCoords; i++ { + if coords[i].Equal(layout, coords[i-1]) { + coords = append(coords[:i], coords[i+1:]...) + modified = true + i-- + } + } + default: + for i := 1; i < len(coords) && len(coords) > minCoords; i++ { + if coordMag2(coordSub(coords[i], coords[i-1])) <= tolerance2 { + coords = append(coords[:i], coords[i+1:]...) + modified = true + i-- + } + } + } + return coords, modified +} + +func removeRepeatedCoords2( + layout geom.Layout, coords2 [][]geom.Coord, tolerance2 float64, minCoords int, +) ([][]geom.Coord, bool) { + modified := false + for i, coords := range coords2 { + if c, m := removeRepeatedCoords(layout, coords, tolerance2, minCoords); m { + coords2[i] = c + modified = true + } + } + return coords2, modified +} + +func removeRepeatedCoords3( + layout geom.Layout, coords3 [][][]geom.Coord, tolerance2 float64, minCoords int, +) ([][][]geom.Coord, bool) { + modified := false + for i, coords2 := range coords3 { + for j, coords := range coords2 { + if c, m := removeRepeatedCoords(layout, coords, tolerance2, minCoords); m { + coords3[i][j] = c + modified = true + } + } + } + return coords3, modified +} diff --git a/pkg/geo/geomfn/reverse.go b/pkg/geo/geomfn/reverse.go new file mode 100644 index 0000000..5ec26ab --- /dev/null +++ b/pkg/geo/geomfn/reverse.go @@ -0,0 +1,96 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/twpayne/go-geom" +) + +// Reverse returns a modified geometry by reversing the order of its vertexes +func Reverse(geometry geo.Geometry) (geo.Geometry, error) { + g, err := geometry.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + + g, err = reverse(g) + if err != nil { + return geo.Geometry{}, err + } + + return geo.MakeGeometryFromGeomT(g) +} + +func reverse(g geom.T) (geom.T, error) { + if geomCollection, ok := g.(*geom.GeometryCollection); ok { + return reverseCollection(geomCollection) + } + + switch t := g.(type) { + case *geom.Point, *geom.MultiPoint: // cases where reverse does change the order + return g, nil + case *geom.LineString: + g = geom.NewLineStringFlat(t.Layout(), reverseCoords(g.FlatCoords(), g.Stride())).SetSRID(g.SRID()) + case *geom.Polygon: + g = geom.NewPolygonFlat(t.Layout(), reverseCoords(g.FlatCoords(), g.Stride()), t.Ends()).SetSRID(g.SRID()) + case *geom.MultiLineString: + g = geom.NewMultiLineStringFlat(t.Layout(), reverseMulti(g, t.Ends()), t.Ends()).SetSRID(g.SRID()) + case *geom.MultiPolygon: + var ends []int + for _, e := range t.Endss() { + ends = append(ends, e...) + } + g = geom.NewMultiPolygonFlat(t.Layout(), reverseMulti(g, ends), t.Endss()).SetSRID(g.SRID()) + + default: + return nil, geom.ErrUnsupportedType{Value: g} + } + + return g, nil +} + +func reverseCoords(coords []float64, stride int) []float64 { + for i := 0; i < len(coords)/2; i += stride { + for j := 0; j < stride; j++ { + coords[i+j], coords[len(coords)-stride-i+j] = coords[len(coords)-stride-i+j], coords[i+j] + } + } + + return coords +} + +// reverseMulti handles reversing coordinates of MULTI* geometries with nested sub-structures +func reverseMulti(g geom.T, ends []int) []float64 { + coords := g.FlatCoords() + prevEnd := 0 + + for _, end := range ends { + copy( + coords[prevEnd:end], + reverseCoords(coords[prevEnd:end], g.Stride()), + ) + prevEnd = end + } + + return coords +} + +// reverseCollection iterates through a GeometryCollection and calls reverse() on each geometry. +func reverseCollection(geomCollection *geom.GeometryCollection) (*geom.GeometryCollection, error) { + res := geom.NewGeometryCollection() + for _, subG := range geomCollection.Geoms() { + subGeom, err := reverse(subG) + if err != nil { + return nil, err + } + + if err := res.Push(subGeom); err != nil { + return nil, err + } + } + return res, nil +} diff --git a/pkg/geo/geomfn/segmentize.go b/pkg/geo/geomfn/segmentize.go new file mode 100644 index 0000000..d84e66d --- /dev/null +++ b/pkg/geo/geomfn/segmentize.go @@ -0,0 +1,93 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "math" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geosegmentize" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/twpayne/go-geom" +) + +// Segmentize return modified Geometry having no segment longer +// that given maximum segment length. +// This works by inserting the extra points in such a manner that +// minimum number of new segments with equal length is created, +// between given two-points such that each segment has length less +// than or equal to given maximum segment length. +func Segmentize(g geo.Geometry, segmentMaxLength float64) (geo.Geometry, error) { + if math.IsNaN(segmentMaxLength) || math.IsInf(segmentMaxLength, 1 /* sign */) { + return g, nil + } + geometry, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + switch geometry := geometry.(type) { + case *geom.Point, *geom.MultiPoint: + return g, nil + default: + if segmentMaxLength <= 0 { + return geo.Geometry{}, pgerror.Newf(pgcode.InvalidParameterValue, "maximum segment length must be positive") + } + segGeometry, err := geosegmentize.Segmentize(geometry, segmentMaxLength, segmentizeCoords) + if err != nil { + return geo.Geometry{}, err + } + return geo.MakeGeometryFromGeomT(segGeometry) + } +} + +// segmentizeCoords inserts multiple points between given two coordinates and +// return resultant point as flat []float64. Points are inserted in such a +// way that they create minimum number segments of equal length such that each +// segment has a length less than or equal to given maximum segment length. +// Note: List of points does not consist of end point. +func segmentizeCoords(a geom.Coord, b geom.Coord, maxSegmentLength float64) ([]float64, error) { + if len(a) != len(b) { + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "cannot segmentize two coordinates of different dimensions") + } + if maxSegmentLength <= 0 { + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "maximum segment length must be positive") + } + + // Only 2D distance is considered for determining number of segments. + distanceBetweenPoints := math.Sqrt(math.Pow(a.X()-b.X(), 2) + math.Pow(b.Y()-a.Y(), 2)) + + doubleNumberOfSegmentsToCreate := math.Ceil(distanceBetweenPoints / maxSegmentLength) + doubleNumPoints := float64(len(a)) * (1 + doubleNumberOfSegmentsToCreate) + if err := geosegmentize.CheckSegmentizeValidNumPoints(doubleNumPoints, a, b); err != nil { + return nil, err + } + + // numberOfSegmentsToCreate represent the total number of segments + // in which given two coordinates will be divided. + numberOfSegmentsToCreate := int(doubleNumberOfSegmentsToCreate) + numPoints := int(doubleNumPoints) + // segmentFraction represent the fraction of length each segment + // has with respect to total length between two coordinates. + allSegmentizedCoordinates := make([]float64, 0, numPoints) + allSegmentizedCoordinates = append(allSegmentizedCoordinates, a.Clone()...) + segmentFraction := 1.0 / float64(numberOfSegmentsToCreate) + for pointInserted := 1; pointInserted < numberOfSegmentsToCreate; pointInserted++ { + segmentPoint := make([]float64, 0, len(a)) + for i := 0; i < len(a); i++ { + segmentPoint = append( + segmentPoint, + a[i]*(1-float64(pointInserted)*segmentFraction)+b[i]*(float64(pointInserted)*segmentFraction), + ) + } + allSegmentizedCoordinates = append( + allSegmentizedCoordinates, + segmentPoint..., + ) + } + + return allSegmentizedCoordinates, nil +} diff --git a/pkg/geo/geomfn/shift_longitude.go b/pkg/geo/geomfn/shift_longitude.go new file mode 100644 index 0000000..d9c766c --- /dev/null +++ b/pkg/geo/geomfn/shift_longitude.go @@ -0,0 +1,37 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/twpayne/go-geom" +) + +// ShiftLongitude returns a modified version of a geometry in which the longitude (X coordinate) +// of each point is incremented by 360 if it is <0 and decremented by 360 if it is >180. +// The result is only meaningful if the coordinates are in longitude/latitude. +func ShiftLongitude(geometry geo.Geometry) (geo.Geometry, error) { + t, err := geometry.AsGeomT() + if err != nil { + return geometry, err + } + + newT, err := applyOnCoordsForGeomT(t, func(l geom.Layout, dst []float64, src []float64) error { + copy(dst, src) + if src[0] < 0 { + dst[0] += 360 + } else if src[0] > 180 { + dst[0] -= 360 + } + return nil + }) + + if err != nil { + return geometry, err + } + + return geo.MakeGeometryFromGeomT(newT) +} diff --git a/pkg/geo/geomfn/simplify.go b/pkg/geo/geomfn/simplify.go new file mode 100644 index 0000000..caab762 --- /dev/null +++ b/pkg/geo/geomfn/simplify.go @@ -0,0 +1,344 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/errors" + "github.com/twpayne/go-geom" +) + +// Simplify simplifies the given Geometry using the Douglas-Pecker algorithm. +// If preserveCollapsed is specified, we will always return a partial LineString +// or Polygon if the entire shape is collapsed. +// Returns a bool if the geometry should be NULL. +func Simplify( + g geo.Geometry, tolerance float64, preserveCollapsed bool, +) (geo.Geometry, bool, error) { + if g.ShapeType2D() == geopb.ShapeType_Point || + g.ShapeType2D() == geopb.ShapeType_MultiPoint || + g.Empty() { + return g, false, nil + } + // Negative tolerances round to zero, similar to PostGIS. + if tolerance < 0 { + tolerance = 0 + } + t, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, false, err + } + simplifiedT, err := simplify(t, tolerance, preserveCollapsed) + if err != nil { + return geo.Geometry{}, false, err + } + if simplifiedT == nil { + return geo.Geometry{}, true, nil + } + + ret, err := geo.MakeGeometryFromGeomT(simplifiedT) + return ret, false, err +} + +func simplify(t geom.T, tolerance float64, preserveCollapsed bool) (geom.T, error) { + if t.Empty() { + return t, nil + } + switch t := t.(type) { + case *geom.Point, *geom.MultiPoint: + // Though handled by the callee, GeometryCollections can have points/multipoints. + return t, nil + case *geom.LineString: + return simplifySimplifiableShape(t, tolerance, preserveCollapsed) + case *geom.MultiLineString: + ret := geom.NewMultiLineString(t.Layout()).SetSRID(t.SRID()) + for i := 0; i < t.NumLineStrings(); i++ { + appendT, err := simplify(t.LineString(i), tolerance, preserveCollapsed) + if err != nil { + return nil, err + } + if appendT != nil { + if err := ret.Push(appendT.(*geom.LineString)); err != nil { + return nil, err + } + } + } + return ret, nil + case *geom.Polygon: + p := geom.NewPolygon(t.Layout()).SetSRID(t.SRID()) + for i := 0; i < t.NumLinearRings(); i++ { + lrT, err := simplifySimplifiableShape( + t.LinearRing(i), + tolerance, + // We only want to preserve the outer ring. + preserveCollapsed && i == 0, + ) + if err != nil { + return nil, err + } + lr := lrT.(*geom.LinearRing) + if lr.NumCoords() < 4 { + // If we are the exterior ring without at least 4 points, return nil. + if i == 0 { + return nil, nil + } + // Otherwise, ignore this inner ring. + continue + } + // Note it is possible for an inner ring to extend outside of the outer ring. + if err := p.Push(lr); err != nil { + return nil, err + } + } + return p, nil + case *geom.MultiPolygon: + ret := geom.NewMultiPolygon(t.Layout()).SetSRID(t.SRID()) + for i := 0; i < t.NumPolygons(); i++ { + appendT, err := simplify(t.Polygon(i), tolerance, preserveCollapsed) + if err != nil { + return nil, err + } + if appendT != nil { + if err := ret.Push(appendT.(*geom.Polygon)); err != nil { + return nil, err + } + } + } + return ret, nil + case *geom.GeometryCollection: + ret := geom.NewGeometryCollection().SetSRID(t.SRID()) + for _, gcT := range t.Geoms() { + appendT, err := simplify(gcT, tolerance, preserveCollapsed) + if err != nil { + return nil, err + } + if appendT != nil { + if err := ret.Push(appendT); err != nil { + return nil, err + } + } + } + return ret, nil + default: + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "unknown shape type: %T", t) + } +} + +type simplifiableShape interface { + geom.T + + Coord(i int) geom.Coord + NumCoords() int +} + +// simplifySimplifiableShape wraps simplifySimplifiableShapeInternal, performing extra +// processing for preserveCollapsed. +func simplifySimplifiableShape( + t simplifiableShape, tolerance float64, preserveCollapsed bool, +) (geom.T, error) { + minPoints := 0 + if preserveCollapsed { + switch t.(type) { + case *geom.LinearRing: + minPoints = 4 + case *geom.LineString: + minPoints = 2 + } + } + + ret, err := simplifySimplifiableShapeInternal(t, tolerance, minPoints) + if err != nil { + return nil, err + } + switch ret := ret.(type) { + case *geom.LineString: + // If we have a LineString with the same first and last point, we should + // collapse the geometry entirely. + if !preserveCollapsed && ret.NumCoords() == 2 && coordEqual(ret.Coord(0), ret.Coord(1)) { + return nil, nil + } + } + return ret, nil +} + +// simplifySimplifiableShapeInternal applies the Douglas-Peucker algorithm to the given +// shape, returning the shape which is simplified. +// Algorithm at https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm#Algorithm. +func simplifySimplifiableShapeInternal( + t simplifiableShape, tolerance float64, minPoints int, +) (geom.T, error) { + numCoords := t.NumCoords() + // If we already have the minimum number of points, return early. + if numCoords <= 2 || numCoords <= minPoints { + return t, nil + } + + // Keep a track of points to keep. Always keep the first and last point. + keepPoints := make([]bool, numCoords) + keepPoints[0] = true + keepPoints[numCoords-1] = true + numKeepPoints := 2 + + if tolerance == 0 { + // We can special case tolerance 0 to be O(n) by only through + // the list of coordinates once and determining whether a given + // intermediate point lies on the edge of the previous and next + // point. + prevCoord := t.Coord(0) + for i := 1; i < t.NumCoords()-1; i++ { + currCoord := t.Coord(i) + nextCoord := t.Coord(i + 1) + + // Let AB by the line from the prev coord to the current coord. + // Let AC be the line from the prev coord to the coord one ahead. + ab := coordSub(currCoord, prevCoord) + ac := coordSub(nextCoord, prevCoord) + + // If AB and AC have a cross product of 0, they are parallel. + // If these are parallel, then we need to determine whether B + // is on the line segment AC. + if coordCross(ab, ac) == 0 { + // Project AB onto line AC. If the projection lies on the edge bounded by + // AC, we should skip the current point. + // See `findSplitIndexForSimplifiableShape` for description of the logic. + r := coordDot(ab, ac) / coordNorm2(ac) + if r >= 0 && r <= 1 { + continue + } + } + + // Since B does not lie on line AC, we should keep the point. + prevCoord = currCoord + keepPoints[i] = true + numKeepPoints++ + } + } else { + // Apply the algorithm, noting points to keep in the keepPoints array. + var recurse func(startIdx, endIdx int) + recurse = func(startIdx, endIdx int) { + // Find the index to split at. + splitIdx := findSplitIndexForSimplifiableShape(t, startIdx, endIdx, tolerance, minPoints > numKeepPoints) + // Return if there are no points to split at. + if splitIdx == startIdx { + return + } + keepPoints[splitIdx] = true + numKeepPoints++ + recurse(startIdx, splitIdx) + recurse(splitIdx, endIdx) + } + recurse(0, numCoords-1) + } + + // If we keep every point, return the existing shape. + if numKeepPoints == numCoords { + return t, nil + } + + // Copy the flat coordinates over in the order they came in. + newFlatCoords := make([]float64, 0, numKeepPoints*t.Layout().Stride()) + for i, keep := range keepPoints { + shouldAdd := keep + // We need to add enough points to satisfy minPoints. If we have not marked + // enough points to keep, add the first points we initially marked as not to + // keep until we hit minPoints. This is safe as we will still create a shape + // resembling the original shape. + // This is required for cases where polygons may simplify into a 2 or 3 point + // linestring, in which simplification may result in an invalid outer ring. + if !shouldAdd && numKeepPoints < minPoints { + shouldAdd = true + numKeepPoints++ + } + if shouldAdd { + newFlatCoords = append(newFlatCoords, []float64(t.Coord(i))...) + } + } + + switch t.(type) { + case *geom.LineString: + return geom.NewLineStringFlat(t.Layout(), newFlatCoords).SetSRID(t.SRID()), nil + case *geom.LinearRing: + return geom.NewLinearRingFlat(t.Layout(), newFlatCoords), nil + } + return nil, errors.AssertionFailedf("unknown shape: %T", t) +} + +// findSplitIndexForSimplifiableShape finds the perpendicular distance for all +// points between the coordinates of the start and end index exclusive to the edge +// formed by coordinates of the start and end index. +// Returns the startIdx if there is no further points to split. +func findSplitIndexForSimplifiableShape( + t simplifiableShape, startIdx int, endIdx int, tolerance float64, forceSplit bool, +) int { + // If there are no points between, then we have no more points to split. + if endIdx-startIdx < 2 { + return startIdx + } + + edgeV0 := t.Coord(startIdx) + edgeV1 := t.Coord(endIdx) + + edgeIsSamePoint := coordEqual(edgeV0, edgeV1) + splitIdx := startIdx + var maxDistance float64 + + for i := startIdx + 1; i < endIdx; i++ { + curr := t.Coord(i) + var dist float64 + // If we are not equal any of the edges, we have to find the closest + // perpendicular distance from the point to the edge. + if !coordEqual(curr, edgeV0) && !coordEqual(curr, edgeV1) { + // Project the point onto the edge. If the point lies on the edge, + // use that projection. Otherwise, the distance is the minimum distance + // to one vertex of the edge. We use the math below. + // + // From http://www.faqs.org/faqs/graphics/algorithms-faq/, section 1.02 + // + // Let the point be C (Cx,Cy) and the line be AB (Ax,Ay) to (Bx,By). + // Let P be the point of perpendicular projection of C on AB. The parameter + // r, which indicates P's position along AB, is computed by the dot product + // of AC and AB divided by the square of the length of AB: + // + // (1) AC dot AB + // r = --------- + // ||AB||^2 + // + // r has the following meaning: + // + // r=0 P = A + // r=1 P = B + // r<0 P is on the backward extension of AB + // r>1 P is on the forward extension of AB + // 0= 1 { + // Closest to V1. + closestPointToEdge = edgeV1 + } else if r > 0 { + // Use the projection as the closest point. + closestPointToEdge = coordAdd(edgeV0, coordMul(ab, r)) + } + } + + // Now find the distance. + dist = coordNorm(coordSub(closestPointToEdge, curr)) + } + if (dist > tolerance && dist > maxDistance) || forceSplit { + forceSplit = false + splitIdx = i + maxDistance = dist + } + } + return splitIdx +} diff --git a/pkg/geo/geomfn/snap.go b/pkg/geo/geomfn/snap.go new file mode 100644 index 0000000..75c3ba8 --- /dev/null +++ b/pkg/geo/geomfn/snap.go @@ -0,0 +1,22 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geos" +) + +// Snap returns the input geometry with the vertices snapped to the target +// geometry. Tolerance is used to control where snapping is performed. +// If no snapping occurs then the input geometry is returned unchanged. +func Snap(input, target geo.Geometry, tolerance float64) (geo.Geometry, error) { + snappedEWKB, err := geos.Snap(input.EWKB(), target.EWKB(), tolerance) + if err != nil { + return geo.Geometry{}, err + } + return geo.ParseGeometryFromEWKB(snappedEWKB) +} diff --git a/pkg/geo/geomfn/snap_to_grid.go b/pkg/geo/geomfn/snap_to_grid.go new file mode 100644 index 0000000..ad5a5eb --- /dev/null +++ b/pkg/geo/geomfn/snap_to_grid.go @@ -0,0 +1,78 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "math" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/twpayne/go-geom" +) + +// SnapToGrid snaps all coordinates in the Geometry to the given grid size, +// offset by the given origin. It will remove duplicate points from the results. +// If the resulting geometry is invalid, it will be converted to it's EMPTY form. +func SnapToGrid(g geo.Geometry, origin geom.Coord, gridSize geom.Coord) (geo.Geometry, error) { + if len(origin) != 4 { + return geo.Geometry{}, pgerror.Newf(pgcode.InvalidParameterValue, "origin must be 4D") + } + if len(gridSize) != 4 { + return geo.Geometry{}, pgerror.Newf(pgcode.InvalidParameterValue, "gridSize must be 4D") + } + if g.Empty() { + return g, nil + } + geomT, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + retGeomT, err := snapToGrid(geomT, origin, gridSize) + if err != nil { + return geo.Geometry{}, err + } + return geo.MakeGeometryFromGeomT(retGeomT) +} + +func snapCoordinateToGrid( + l geom.Layout, dst []float64, src []float64, origin geom.Coord, gridSize geom.Coord, +) { + dst[0] = snapOrdinateToGrid(src[0], origin[0], gridSize[0]) + dst[1] = snapOrdinateToGrid(src[1], origin[1], gridSize[1]) + if l.ZIndex() != -1 { + dst[l.ZIndex()] = snapOrdinateToGrid(src[l.ZIndex()], origin[l.ZIndex()], gridSize[l.ZIndex()]) + } + if l.MIndex() != -1 { + dst[l.MIndex()] = snapOrdinateToGrid(src[l.MIndex()], origin[l.MIndex()], gridSize[l.MIndex()]) + } +} + +func snapOrdinateToGrid( + ordinate float64, originOrdinate float64, gridSizeOrdinate float64, +) float64 { + // A zero grid size ordinate indicates a dimension should not be snapped. + // For PostGIS compatibility, a negative grid size ordinate also results + // in a dimension not being snapped. + if gridSizeOrdinate <= 0 { + return ordinate + } + return math.RoundToEven((ordinate-originOrdinate)/gridSizeOrdinate)*gridSizeOrdinate + originOrdinate +} + +func snapToGrid(t geom.T, origin geom.Coord, gridSize geom.Coord) (geom.T, error) { + if t.Empty() { + return t, nil + } + t, err := applyOnCoordsForGeomT(t, func(l geom.Layout, dst []float64, src []float64) error { + snapCoordinateToGrid(l, dst, src, origin, gridSize) + return nil + }) + if err != nil { + return nil, err + } + return removeConsecutivePointsFromGeomT(t) +} diff --git a/pkg/geo/geomfn/subdivide.go b/pkg/geo/geomfn/subdivide.go new file mode 100644 index 0000000..406a1d7 --- /dev/null +++ b/pkg/geo/geomfn/subdivide.go @@ -0,0 +1,299 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "math" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/errors" + geom "github.com/twpayne/go-geom" +) + +// Subdivide returns a geometry divided into parts, where each part contains no more than the number of vertices provided. +func Subdivide(g geo.Geometry, maxVertices int) ([]geo.Geometry, error) { + if g.Empty() { + return []geo.Geometry{g}, nil + } + const minMaxVertices = 5 + if maxVertices < minMaxVertices { + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "max_vertices number cannot be less than %v", minMaxVertices) + } + + gt, err := g.AsGeomT() + if err != nil { + return nil, errors.Wrap(err, "could not transform input geometry into geom.T") + } + dim, err := dimensionFromGeomT(gt) + if err != nil { + return nil, errors.Wrap(err, "could not calculate geometry dimension") + } + // maxDepth 50 => 2^50 ~= 10^15 subdivisions. + const maxDepth = 50 + const startDepth = 0 + geomTs, err := subdivideRecursive(gt, maxVertices, startDepth, dim, maxDepth) + if err != nil { + return nil, err + } + + output := []geo.Geometry{} + for _, cg := range geomTs { + geo.AdjustGeomTSRID(cg, g.SRID()) + g, err := geo.MakeGeometryFromGeomT(cg) + if err != nil { + return []geo.Geometry{}, errors.Wrap(err, "could not transform output geom.T into geometry") + } + output = append(output, g) + } + return output, nil +} + +// subdivideRecursive performs the recursive subdivision on the gt provided. +// Subdivided geom.T's that have less vertices than maxVertices are added to the results. +// depth is compared with maxDepth, so that maximum number of subdivisions is never exceeded. +// If gt passed is a GeometryCollection, some of the types may be shallower than the others. +// We need dim to make sure that those geometries are ignored. +func subdivideRecursive( + gt geom.T, maxVertices int, depth int, dim int, maxDepth int, +) ([]geom.T, error) { + var results []geom.T + clip := geo.BoundingBoxFromGeomTGeometryType(gt) + width := clip.HiX - clip.LoX + height := clip.HiY - clip.LoY + if width == 0 && height == 0 { + // Point is a special case, because actual dimension is checked later in the code. + if gt, ok := gt.(*geom.Point); ok && dim == 0 { + results = append(results, gt) + } + return results, nil + } + // Boundaries needs to be widen slightly for intersection to work properly. + const tolerance = 1e-12 + if width == 0 { + clip.HiX += tolerance + clip.LoX -= tolerance + width = 2 * tolerance + } + if height == 0 { + clip.HiY += tolerance + clip.LoY -= tolerance + height = 2 * tolerance + } + // Recurse into collections other than MultiPoint. + switch gt.(type) { + case *geom.MultiLineString, *geom.MultiPolygon, *geom.GeometryCollection: + err := forEachGeomTInCollectionForSubdivide(gt, func(gi geom.T) error { + // It's not a subdivision yet, so depth stays the same. + subdivisions, err := subdivideRecursive(gi, maxVertices, depth, dim, maxDepth) + if err != nil { + return err + } + results = append(results, subdivisions...) + return nil + }) + if err != nil { + return nil, err + } + return results, nil + } + currDim, err := dimensionFromGeomT(gt) + if err != nil { + return nil, errors.Wrap(err, "error checking geom.T dimension") + } + if currDim < dim { + // Ignore lower dimension object produced by clipping at a shallower recursion level. + return results, nil + } + if depth > maxDepth { + results = append(results, gt) + return results, nil + } + nVertices := CountVertices(gt) + if nVertices == 0 { + return results, nil + } + if nVertices <= maxVertices { + results = append(results, gt) + return results, nil + } + + splitHorizontally := width > height + splitPoint, err := calculateSplitPointCoordForSubdivide(gt, nVertices, splitHorizontally, *clip) + if err != nil { + return nil, err + } + subBox1 := *clip + subBox2 := *clip + if splitHorizontally { + subBox1.HiX = splitPoint + subBox2.LoX = splitPoint + } else { + subBox1.HiY = splitPoint + subBox2.LoY = splitPoint + } + + clipped1, err := clipGeomTByBoundingBoxForSubdivide(gt, &subBox1) + if err != nil { + return nil, errors.Wrap(err, "error clipping geom.T") + } + clipped2, err := clipGeomTByBoundingBoxForSubdivide(gt, &subBox2) + if err != nil { + return nil, errors.Wrap(err, "error clipping geom.T") + } + depth++ + subdivisions1, err := subdivideRecursive(clipped1, maxVertices, depth, dim, maxDepth) + if err != nil { + return nil, err + } + subdivisions2, err := subdivideRecursive(clipped2, maxVertices, depth, dim, maxDepth) + if err != nil { + return nil, err + } + results = append(results, subdivisions1...) + results = append(results, subdivisions2...) + return results, nil +} + +// forEachGeomTInCollectionForSubdivide calls the provided function for each geometry in the passed in collection. +// Ignores MultiPoint input. +func forEachGeomTInCollectionForSubdivide(gt geom.T, fn func(geom.T) error) error { + switch gt := gt.(type) { + case *geom.GeometryCollection: + for i := 0; i < gt.NumGeoms(); i++ { + err := fn(gt.Geom(i)) + if err != nil { + return err + } + } + return nil + case *geom.MultiPolygon: + for i := 0; i < gt.NumPolygons(); i++ { + err := fn(gt.Polygon(i)) + if err != nil { + return err + } + } + case *geom.MultiLineString: + for i := 0; i < gt.NumLineStrings(); i++ { + err := fn(gt.LineString(i)) + if err != nil { + return err + } + } + } + return nil +} + +// calculateSplitPointCoordForSubdivide calculates the coords that can be used for splitting +// the geom.T provided and returns their X or Y value, depending on the splitHorizontally argument. +// These coords will be coords of the closest point to the center in case of a Polygon +// and simply the center coords in case of any other geometry type or +// closest point to the center being on the boundaries. +// Specialized to be used by the Subdivide function. +func calculateSplitPointCoordForSubdivide( + gt geom.T, nVertices int, splitHorizontally bool, clip geo.CartesianBoundingBox, +) (float64, error) { + var pivot float64 + switch gt := gt.(type) { + case *geom.Polygon: + var err error + pivot, err = findMostCentralPointValueForPolygon(gt, nVertices, splitHorizontally, &clip) + if err != nil { + return 0, errors.Wrap(err, "error finding most central point for polygon") + } + if splitHorizontally { + // Ignore point on the boundaries + if clip.LoX == pivot || clip.HiX == pivot { + pivot = (clip.LoX + clip.HiX) / 2 + } + } else { + // Ignore point on the boundaries + if clip.LoY == pivot || clip.HiY == pivot { + pivot = (clip.LoY + clip.HiY) / 2 + } + } + default: + if splitHorizontally { + pivot = (clip.LoX + clip.HiX) / 2 + } else { + pivot = (clip.LoY + clip.HiY) / 2 + } + } + return pivot, nil +} + +// findMostCentralPointValueForPolygon finds the value of the most central point for a geom.Polygon. +// Can be X or Y value, depending on the splitHorizontally argument. +func findMostCentralPointValueForPolygon( + poly *geom.Polygon, nVertices int, splitHorizontally bool, clip *geo.CartesianBoundingBox, +) (float64, error) { + pivot := math.MaxFloat64 + ringToTrimIndex := 0 + ringArea := float64(0) + pivotEps := math.MaxFloat64 + var ptEps float64 + nVerticesOuterRing := CountVertices(poly.LinearRing(0)) + if nVertices >= 2*nVerticesOuterRing { + // When there are more points in holes than in outer ring, trim holes starting from biggest. + for i := 1; i < poly.NumLinearRings(); i++ { + currentRingArea := poly.LinearRing(i).Area() + if currentRingArea >= ringArea { + ringArea = currentRingArea + ringToTrimIndex = i + } + } + } + pa := poly.LinearRing(ringToTrimIndex) + // Find the most central point in the chosen ring. + for j := 0; j < pa.NumCoords(); j++ { + var pt float64 + if splitHorizontally { + pt = pa.Coord(j).X() + xHalf := (clip.LoX + clip.HiX) / 2 + ptEps = math.Abs(pt - xHalf) + } else { + pt = pa.Coord(j).Y() + yHalf := (clip.LoY + clip.HiY) / 2 + ptEps = math.Abs(pt - yHalf) + } + if ptEps < pivotEps { + pivot = pt + pivotEps = ptEps + } + } + return pivot, nil +} + +// clipGeomTByBoundingBoxForSubdivide returns a result of intersection and simplification of the geom.T and bounding box provided. +// The result is also a geom.T. +// Specialized to be used in the Subdivide function. +func clipGeomTByBoundingBoxForSubdivide(gt geom.T, clip *geo.CartesianBoundingBox) (geom.T, error) { + g, err := geo.MakeGeometryFromGeomT(gt) + if err != nil { + return nil, errors.Wrap(err, "error transforming geom.T to geometry") + } + clipgt := clip.ToGeomT(g.SRID()) + clipg, err := geo.MakeGeometryFromGeomT(clipgt) + if err != nil { + return nil, errors.Wrap(err, "error transforming geom.T to geometry") + } + out, err := Intersection(g, clipg) + if err != nil { + return nil, errors.Wrap(err, "error applying intersection") + } + // Simplify is required to remove the unnecessary points. Otherwise vertices count is altered and too many subdivision may be returned. + out, err = SimplifyGEOS(out, 0) + if err != nil { + return nil, errors.Wrap(err, "simplifying error") + } + gt, err = out.AsGeomT() + if err != nil { + return nil, errors.Wrap(err, "error transforming geometry to geom.T") + } + return gt, nil +} diff --git a/pkg/geo/geomfn/swap_ordinates.go b/pkg/geo/geomfn/swap_ordinates.go new file mode 100644 index 0000000..f215238 --- /dev/null +++ b/pkg/geo/geomfn/swap_ordinates.go @@ -0,0 +1,79 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "strings" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/twpayne/go-geom" +) + +// SwapOrdinates returns a version of the given geometry with given ordinates swapped. +// The ords parameter is a 2-characters string naming the ordinates to swap. Valid names are: x,y,z and m. +func SwapOrdinates(geometry geo.Geometry, ords string) (geo.Geometry, error) { + if geometry.Empty() { + return geometry, nil + } + + t, err := geometry.AsGeomT() + if err != nil { + return geometry, err + } + + newT, err := applyOnCoordsForGeomT(t, func(l geom.Layout, dst, src []float64) error { + if len(ords) != 2 { + return pgerror.Newf(pgcode.InvalidParameterValue, "invalid ordinate specification. need two letters from the set (x, y, z and m)") + } + ordsIndices, err := getOrdsIndices(l, ords) + if err != nil { + return err + } + + dst[ordsIndices[0]], dst[ordsIndices[1]] = src[ordsIndices[1]], src[ordsIndices[0]] + return nil + }) + if err != nil { + return geometry, err + } + + return geo.MakeGeometryFromGeomT(newT) +} + +// getOrdsIndices get the indices position of ordinates string +func getOrdsIndices(l geom.Layout, ords string) ([2]int, error) { + ords = strings.ToUpper(ords) + var ordIndices [2]int + for i := 0; i < len(ords); i++ { + oi := findOrdIndex(ords[i], l) + if oi == -2 { + return ordIndices, pgerror.Newf(pgcode.InvalidParameterValue, "invalid ordinate specification. need two letters from the set (x, y, z and m)") + } + if oi == -1 { + return ordIndices, pgerror.Newf(pgcode.InvalidParameterValue, "geometry does not have a %s ordinate", string(ords[i])) + } + ordIndices[i] = oi + } + + return ordIndices, nil +} + +func findOrdIndex(ordString uint8, l geom.Layout) int { + switch ordString { + case 'X': + return 0 + case 'Y': + return 1 + case 'Z': + return l.ZIndex() + case 'M': + return l.MIndex() + default: + return -2 + } +} diff --git a/pkg/geo/geomfn/tile_envelope.go b/pkg/geo/geomfn/tile_envelope.go new file mode 100644 index 0000000..e0f61ee --- /dev/null +++ b/pkg/geo/geomfn/tile_envelope.go @@ -0,0 +1,112 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" +) + +// TileEnvelope returns a geometry as a rectangular Polygon giving the +// extent of a tile in the XYZ tile system +func TileEnvelope( + tileZoom int, tileX int, tileY int, bounds geo.Geometry, margin float64, +) (geo.Geometry, error) { + bbox := bounds.BoundingBoxRef() + if bbox == nil { + return geo.Geometry{}, pgerror.Newf( + pgcode.InvalidParameterValue, + "Unable to compute bbox") + } + srid := bounds.SRID() + + if tileZoom < 0 || tileZoom >= 32 { + return geo.Geometry{}, pgerror.Newf( + pgcode.InvalidParameterValue, + "Invalid tile zoom value, %d", + tileZoom, + ) + } + + if bbox.HiX-bbox.LoX <= 0 || bbox.HiY-bbox.LoY <= 0 { + return geo.Geometry{}, pgerror.New( + pgcode.InvalidParameterValue, + "Geometric bounds are too small", + ) + } + + if margin < -0.5 { + return geo.Geometry{}, pgerror.Newf( + pgcode.InvalidParameterValue, + "Margin value must not be less than -50%%, %d", + margin, + ) + } + + tileZoomu := uint32(tileZoom) + if tileZoomu > 31 { + tileZoomu = 31 + } + + worldTileSize := uint32(0x01) << tileZoomu + + if tileX < 0 || uint32(tileX) >= worldTileSize { + return geo.Geometry{}, pgerror.Newf( + pgcode.InvalidParameterValue, + "Invalid tile x value, %d", + tileX, + ) + } + + if tileY < 0 || uint32(tileY) >= worldTileSize { + return geo.Geometry{}, pgerror.Newf( + pgcode.InvalidParameterValue, + "Invalid tile y value, %d", + tileY, + ) + } + + tileGeoSizeX := (bbox.HiX - bbox.LoX) / float64(worldTileSize) + tileGeoSizeY := (bbox.HiY - bbox.LoY) / float64(worldTileSize) + + var x1 float64 + var x2 float64 + + // 1 margin (100%) is the same as a single tile width + // if the size of the tile with margins span more than the total number of tiles, + // reset x1/x2 to the bounds + if 1+margin*2 > float64(worldTileSize) { + x1 = bbox.LoX + x2 = bbox.HiX + } else { + x1 = bbox.LoX + tileGeoSizeX*(float64(tileX)-margin) + x2 = bbox.LoX + tileGeoSizeX*(float64(tileX)+1+margin) + } + + y1 := bbox.HiY - tileGeoSizeY*(float64(tileY)+1+margin) + y2 := bbox.HiY - tileGeoSizeY*(float64(tileY)-margin) + + // Clip y-axis to the given bounds + if y1 < bbox.LoY { + y1 = bbox.LoY + } + if y2 > bbox.HiY { + y2 = bbox.HiY + } + + envelope := &geo.CartesianBoundingBox{ + BoundingBox: geopb.BoundingBox{ + LoX: x1, + LoY: y1, + HiX: x2, + HiY: y2, + }, + } + + return geo.MakeGeometryFromGeomT(envelope.ToGeomT(srid)) +} diff --git a/pkg/geo/geomfn/topology_operations.go b/pkg/geo/geomfn/topology_operations.go new file mode 100644 index 0000000..a8b69e2 --- /dev/null +++ b/pkg/geo/geomfn/topology_operations.go @@ -0,0 +1,244 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "math" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geos" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" +) + +// Boundary returns the boundary of a given Geometry. +func Boundary(g geo.Geometry) (geo.Geometry, error) { + // follow PostGIS behavior + if g.Empty() { + return g, nil + } + boundaryEWKB, err := geos.Boundary(g.EWKB()) + if err != nil { + return geo.Geometry{}, err + } + return geo.ParseGeometryFromEWKB(boundaryEWKB) +} + +// Centroid returns the Centroid of a given Geometry. +func Centroid(g geo.Geometry) (geo.Geometry, error) { + centroidEWKB, err := geos.Centroid(g.EWKB()) + if err != nil { + return geo.Geometry{}, err + } + return geo.ParseGeometryFromEWKB(centroidEWKB) +} + +// MinimumBoundingCircle returns minimum bounding circle of an EWKB +func MinimumBoundingCircle(g geo.Geometry) (geo.Geometry, geo.Geometry, float64, error) { + if BoundingBoxHasInfiniteCoordinates(g) || BoundingBoxHasNaNCoordinates(g) { + return geo.Geometry{}, geo.Geometry{}, 0, pgerror.Newf(pgcode.InvalidParameterValue, "value out of range: overflow") + } + + polygonEWKB, centroidEWKB, radius, err := geos.MinimumBoundingCircle(g.EWKB()) + if err != nil { + return geo.Geometry{}, geo.Geometry{}, 0, err + } + + polygon, err := geo.ParseGeometryFromEWKB(polygonEWKB) + if err != nil { + return geo.Geometry{}, geo.Geometry{}, 0, err + } + + centroid, err := geo.ParseGeometryFromEWKB(centroidEWKB) + if err != nil { + return geo.Geometry{}, geo.Geometry{}, 0, err + } + + return polygon, centroid, radius, nil +} + +// ClipByRect clips a given Geometry by the given BoundingBox. +func ClipByRect(g geo.Geometry, b geo.CartesianBoundingBox) (geo.Geometry, error) { + if g.Empty() { + return g, nil + } + clipByRectEWKB, err := geos.ClipByRect(g.EWKB(), b.LoX, b.LoY, b.HiX, b.HiY) + if err != nil { + return geo.Geometry{}, err + } + return geo.ParseGeometryFromEWKB(clipByRectEWKB) +} + +// ConvexHull returns the convex hull of a given Geometry. +func ConvexHull(g geo.Geometry) (geo.Geometry, error) { + convexHullEWKB, err := geos.ConvexHull(g.EWKB()) + if err != nil { + return geo.Geometry{}, err + } + return geo.ParseGeometryFromEWKB(convexHullEWKB) +} + +// Difference returns the difference between two given Geometries. +func Difference(a, b geo.Geometry) (geo.Geometry, error) { + // follow PostGIS behavior + if a.Empty() || b.Empty() { + return a, nil + } + if a.SRID() != b.SRID() { + return geo.Geometry{}, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + diffEWKB, err := geos.Difference(a.EWKB(), b.EWKB()) + if err != nil { + return geo.Geometry{}, err + } + return geo.ParseGeometryFromEWKB(diffEWKB) +} + +// SimplifyGEOS returns a simplified Geometry with GEOS. +func SimplifyGEOS(g geo.Geometry, tolerance float64) (geo.Geometry, error) { + if math.IsNaN(tolerance) || g.ShapeType2D() == geopb.ShapeType_Point || g.ShapeType2D() == geopb.ShapeType_MultiPoint { + return g, nil + } + simplifiedEWKB, err := geos.Simplify(g.EWKB(), tolerance) + if err != nil { + return geo.Geometry{}, err + } + return geo.ParseGeometryFromEWKB(simplifiedEWKB) +} + +// SimplifyPreserveTopology returns a simplified Geometry with topology preserved. +func SimplifyPreserveTopology(g geo.Geometry, tolerance float64) (geo.Geometry, error) { + simplifiedEWKB, err := geos.TopologyPreserveSimplify(g.EWKB(), tolerance) + if err != nil { + return geo.Geometry{}, err + } + return geo.ParseGeometryFromEWKB(simplifiedEWKB) +} + +// PointOnSurface returns the PointOnSurface of a given Geometry. +func PointOnSurface(g geo.Geometry) (geo.Geometry, error) { + pointOnSurfaceEWKB, err := geos.PointOnSurface(g.EWKB()) + if err != nil { + return geo.Geometry{}, err + } + return geo.ParseGeometryFromEWKB(pointOnSurfaceEWKB) +} + +// Intersection returns the geometries of intersection between A and B. +func Intersection(a geo.Geometry, b geo.Geometry) (geo.Geometry, error) { + if a.SRID() != b.SRID() { + return geo.Geometry{}, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + // Match PostGIS. + if a.Empty() { + return a, nil + } + if b.Empty() { + return b, nil + } + retEWKB, err := geos.Intersection(a.EWKB(), b.EWKB()) + if err != nil { + return geo.Geometry{}, err + } + return geo.ParseGeometryFromEWKB(retEWKB) +} + +// UnaryUnion returns the geometry of union between input geometry components +func UnaryUnion(g geo.Geometry) (geo.Geometry, error) { + retEWKB, err := geos.UnaryUnion(g.EWKB()) + if err != nil { + return geo.Geometry{}, err + } + return geo.ParseGeometryFromEWKB(retEWKB) +} + +// Union returns the geometries of union between A and B. +func Union(a geo.Geometry, b geo.Geometry) (geo.Geometry, error) { + if a.SRID() != b.SRID() { + return geo.Geometry{}, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + retEWKB, err := geos.Union(a.EWKB(), b.EWKB()) + if err != nil { + return geo.Geometry{}, err + } + return geo.ParseGeometryFromEWKB(retEWKB) +} + +// SymDifference returns the geometries of symmetric difference between A and B. +func SymDifference(a geo.Geometry, b geo.Geometry) (geo.Geometry, error) { + if a.SRID() != b.SRID() { + return geo.Geometry{}, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + retEWKB, err := geos.SymDifference(a.EWKB(), b.EWKB()) + if err != nil { + return geo.Geometry{}, err + } + return geo.ParseGeometryFromEWKB(retEWKB) +} + +// SharedPaths Returns a geometry collection containing paths shared by the two input geometries. +func SharedPaths(a geo.Geometry, b geo.Geometry) (geo.Geometry, error) { + if a.SRID() != b.SRID() { + return geo.Geometry{}, geo.NewMismatchingSRIDsError(a.SpatialObject(), b.SpatialObject()) + } + paths, err := geos.SharedPaths(a.EWKB(), b.EWKB()) + if err != nil { + return geo.Geometry{}, err + } + gm, err := geo.ParseGeometryFromEWKB(paths) + if err != nil { + return geo.Geometry{}, err + } + return gm, nil +} + +// MinimumRotatedRectangle Returns a minimum rotated rectangle enclosing a geometry +func MinimumRotatedRectangle(g geo.Geometry) (geo.Geometry, error) { + paths, err := geos.MinimumRotatedRectangle(g.EWKB()) + if err != nil { + return geo.Geometry{}, err + } + gm, err := geo.ParseGeometryFromEWKB(paths) + if err != nil { + return geo.Geometry{}, err + } + return gm, nil +} + +// BoundingBoxHasInfiniteCoordinates checks if the bounding box of a Geometry +// has an infinite coordinate. +func BoundingBoxHasInfiniteCoordinates(g geo.Geometry) bool { + boundingBox := g.BoundingBoxRef() + if boundingBox == nil { + return false + } + // Don't use `:= range []float64{...}` to avoid memory allocation. + isInf := func(ord float64) bool { + return math.IsInf(ord, 0) + } + if isInf(boundingBox.LoX) || isInf(boundingBox.LoY) || isInf(boundingBox.HiX) || isInf(boundingBox.HiY) { + return true + } + return false +} + +// BoundingBoxHasNaNCoordinates checks if the bounding box of a Geometry +// has a NaN coordinate. +func BoundingBoxHasNaNCoordinates(g geo.Geometry) bool { + boundingBox := g.BoundingBoxRef() + if boundingBox == nil { + return false + } + // Don't use `:= range []float64{...}` to avoid memory allocation. + isNaN := func(ord float64) bool { + return math.IsNaN(ord) + } + if isNaN(boundingBox.LoX) || isNaN(boundingBox.LoY) || isNaN(boundingBox.HiX) || isNaN(boundingBox.HiY) { + return true + } + return false +} diff --git a/pkg/geo/geomfn/unary_operators.go b/pkg/geo/geomfn/unary_operators.go new file mode 100644 index 0000000..571d5d2 --- /dev/null +++ b/pkg/geo/geomfn/unary_operators.go @@ -0,0 +1,244 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geos" + "github.com/cockroachdb/errors" + "github.com/twpayne/go-geom" + "github.com/twpayne/go-geom/encoding/ewkb" +) + +// Length returns the length of a given Geometry. +// Note only (MULTI)LINESTRING objects have a length. +// (MULTI)POLYGON objects should use Perimeter. +func Length(g geo.Geometry) (float64, error) { + geomRepr, err := g.AsGeomT() + if err != nil { + return 0, err + } + // Fast path. + switch geomRepr.(type) { + case *geom.LineString, *geom.MultiLineString: + return geos.Length(g.EWKB()) + } + return lengthFromGeomT(geomRepr) +} + +// lengthFromGeomT returns the length from a geom.T, recursing down +// GeometryCollections if required. +func lengthFromGeomT(geomRepr geom.T) (float64, error) { + // Length in GEOS will also include polygon "perimeters". + // As such, gate based on on shape underneath. + switch geomRepr := geomRepr.(type) { + case *geom.Point, *geom.MultiPoint, *geom.Polygon, *geom.MultiPolygon: + return 0, nil + case *geom.LineString, *geom.MultiLineString: + ewkb, err := ewkb.Marshal(geomRepr, geo.DefaultEWKBEncodingFormat) + if err != nil { + return 0, err + } + return geos.Length(ewkb) + case *geom.GeometryCollection: + total := float64(0) + for _, subG := range geomRepr.Geoms() { + subLength, err := lengthFromGeomT(subG) + if err != nil { + return 0, err + } + total += subLength + } + return total, nil + default: + return 0, errors.AssertionFailedf("unknown geometry type: %T", geomRepr) + } +} + +// Perimeter returns the perimeter of a given Geometry. +// Note only (MULTI)POLYGON objects have a perimeter. +// (MULTI)LineString objects should use Length. +func Perimeter(g geo.Geometry) (float64, error) { + geomRepr, err := g.AsGeomT() + if err != nil { + return 0, err + } + // Fast path. + switch geomRepr.(type) { + case *geom.Polygon, *geom.MultiPolygon: + return geos.Length(g.EWKB()) + } + return perimeterFromGeomT(geomRepr) +} + +// perimeterFromGeomT returns the perimeter from a geom.T, recursing down +// GeometryCollections if required. +func perimeterFromGeomT(geomRepr geom.T) (float64, error) { + // Length in GEOS will also include polygon "perimeters". + // As such, gate based on on shape underneath. + switch geomRepr := geomRepr.(type) { + case *geom.Point, *geom.MultiPoint, *geom.LineString, *geom.MultiLineString: + return 0, nil + case *geom.Polygon, *geom.MultiPolygon: + ewkb, err := ewkb.Marshal(geomRepr, geo.DefaultEWKBEncodingFormat) + if err != nil { + return 0, err + } + return geos.Length(ewkb) + case *geom.GeometryCollection: + total := float64(0) + for _, subG := range geomRepr.Geoms() { + subLength, err := perimeterFromGeomT(subG) + if err != nil { + return 0, err + } + total += subLength + } + return total, nil + default: + return 0, errors.AssertionFailedf("unknown geometry type: %T", geomRepr) + } +} + +// Area returns the area of a given Geometry. +func Area(g geo.Geometry) (float64, error) { + return geos.Area(g.EWKB()) +} + +// Dimension returns the topological dimension of a given Geometry. +func Dimension(g geo.Geometry) (int, error) { + t, err := g.AsGeomT() + if err != nil { + return 0, err + } + return dimensionFromGeomT(t) +} + +// dimensionFromGeomT returns the dimension from a geom.T, recursing down +// GeometryCollections if required. +func dimensionFromGeomT(geomRepr geom.T) (int, error) { + switch geomRepr := geomRepr.(type) { + case *geom.Point, *geom.MultiPoint: + return 0, nil + case *geom.LineString, *geom.MultiLineString: + return 1, nil + case *geom.Polygon, *geom.MultiPolygon: + return 2, nil + case *geom.GeometryCollection: + maxDim := 0 + for _, g := range geomRepr.Geoms() { + dim, err := dimensionFromGeomT(g) + if err != nil { + return 0, err + } + if dim > maxDim { + maxDim = dim + } + } + return maxDim, nil + default: + return 0, errors.AssertionFailedf("unknown geometry type: %T", geomRepr) + } +} + +// Points returns the points of all coordinates in a geometry as a multipoint. +func Points(g geo.Geometry) (geo.Geometry, error) { + t, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + layout := t.Layout() + if gc, ok := t.(*geom.GeometryCollection); ok && gc.Empty() { + layout = geom.XY + } + points := geom.NewMultiPoint(layout).SetSRID(t.SRID()) + iter := geo.NewGeomTIterator(t, geo.EmptyBehaviorOmit) + for { + geomRepr, hasNext, err := iter.Next() + if err != nil { + return geo.Geometry{}, err + } else if !hasNext { + break + } else if geomRepr.Empty() { + continue + } + switch geomRepr := geomRepr.(type) { + case *geom.Point: + if err = pushCoord(points, geomRepr.Coords()); err != nil { + return geo.Geometry{}, err + } + case *geom.LineString: + for i := 0; i < geomRepr.NumCoords(); i++ { + if err = pushCoord(points, geomRepr.Coord(i)); err != nil { + return geo.Geometry{}, err + } + } + case *geom.Polygon: + for i := 0; i < geomRepr.NumLinearRings(); i++ { + linearRing := geomRepr.LinearRing(i) + for j := 0; j < linearRing.NumCoords(); j++ { + if err = pushCoord(points, linearRing.Coord(j)); err != nil { + return geo.Geometry{}, err + } + } + } + default: + return geo.Geometry{}, errors.AssertionFailedf("unexpected type: %T", geomRepr) + } + } + return geo.MakeGeometryFromGeomT(points) +} + +// pushCoord is a helper function for PointsFromGeomT that appends +// a coordinate to a multipoint as a point. +func pushCoord(points *geom.MultiPoint, coord geom.Coord) error { + point, err := geom.NewPoint(points.Layout()).SetCoords(coord) + if err != nil { + return err + } + return points.Push(point) +} + +// Normalize returns the geometry in its normalized form. +func Normalize(g geo.Geometry) (geo.Geometry, error) { + retEWKB, err := geos.Normalize(g.EWKB()) + if err != nil { + return geo.Geometry{}, err + } + return geo.ParseGeometryFromEWKB(retEWKB) +} + +// MinimumClearance returns the minimum distance a vertex can move to produce +// an invalid geometry, or infinity if no clearance was found. +func MinimumClearance(g geo.Geometry) (float64, error) { + return geos.MinimumClearance(g.EWKB()) +} + +// MinimumClearanceLine returns the line spanning the minimum distance a vertex +// can move to produce an invalid geometry, or an empty line if no clearance was +// found. +func MinimumClearanceLine(g geo.Geometry) (geo.Geometry, error) { + retEWKB, err := geos.MinimumClearanceLine(g.EWKB()) + if err != nil { + return geo.Geometry{}, err + } + return geo.ParseGeometryFromEWKB(retEWKB) +} + +// CountVertices returns a number of vertices (points) for the geom.T provided. +func CountVertices(t geom.T) int { + switch t := t.(type) { + case *geom.GeometryCollection: + // FlatCoords() does not work on GeometryCollection. + numPoints := 0 + for _, g := range t.Geoms() { + numPoints += CountVertices(g) + } + return numPoints + default: + return len(t.FlatCoords()) / t.Stride() + } +} diff --git a/pkg/geo/geomfn/unary_predicates.go b/pkg/geo/geomfn/unary_predicates.go new file mode 100644 index 0000000..5dfa2a2 --- /dev/null +++ b/pkg/geo/geomfn/unary_predicates.go @@ -0,0 +1,118 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geos" + "github.com/cockroachdb/errors" + "github.com/twpayne/go-geom" +) + +// IsClosed returns whether the given geometry has equal start and end points. +// Collections and multi-types must contain all-closed geometries. Empty +// geometries are not closed. +func IsClosed(g geo.Geometry) (bool, error) { + t, err := g.AsGeomT() + if err != nil { + return false, err + } + return isClosedFromGeomT(t) +} + +// isClosedFromGeomT returns whether the given geom.T is closed, recursing +// into collections. +func isClosedFromGeomT(t geom.T) (bool, error) { + if t.Empty() { + return false, nil + } + switch t := t.(type) { + case *geom.Point, *geom.MultiPoint: + return true, nil + case *geom.LinearRing: + return t.Coord(0).Equal(t.Layout(), t.Coord(t.NumCoords()-1)), nil + case *geom.LineString: + return t.Coord(0).Equal(t.Layout(), t.Coord(t.NumCoords()-1)), nil + case *geom.MultiLineString: + for i := 0; i < t.NumLineStrings(); i++ { + if closed, err := isClosedFromGeomT(t.LineString(i)); err != nil || !closed { + return false, err + } + } + return true, nil + case *geom.Polygon: + for i := 0; i < t.NumLinearRings(); i++ { + if closed, err := isClosedFromGeomT(t.LinearRing(i)); err != nil || !closed { + return false, err + } + } + return true, nil + case *geom.MultiPolygon: + for i := 0; i < t.NumPolygons(); i++ { + if closed, err := isClosedFromGeomT(t.Polygon(i)); err != nil || !closed { + return false, err + } + } + return true, nil + case *geom.GeometryCollection: + for _, g := range t.Geoms() { + if closed, err := isClosedFromGeomT(g); err != nil || !closed { + return false, err + } + } + return true, nil + default: + return false, errors.AssertionFailedf("unknown geometry type: %T", t) + } +} + +// IsCollection returns whether the given geometry is of a collection type. +func IsCollection(g geo.Geometry) (bool, error) { + switch g.ShapeType() { + case geopb.ShapeType_MultiPoint, geopb.ShapeType_MultiLineString, geopb.ShapeType_MultiPolygon, + geopb.ShapeType_GeometryCollection: + return true, nil + default: + return false, nil + } +} + +// IsEmpty returns whether the given geometry is empty. +func IsEmpty(g geo.Geometry) (bool, error) { + return g.Empty(), nil +} + +// IsRing returns whether the given geometry is a ring, i.e. that it is a +// simple and closed line. +func IsRing(g geo.Geometry) (bool, error) { + // We explicitly check for empty geometries before checking the type, + // to follow PostGIS behavior where all empty geometries return false. + if g.Empty() { + return false, nil + } + if g.ShapeType() != geopb.ShapeType_LineString { + t, err := g.AsGeomT() + if err != nil { + return false, err + } + e := geom.ErrUnsupportedType{Value: t} + return false, errors.Wrap(e, "should only be called on a linear feature") + } + if closed, err := IsClosed(g); err != nil || !closed { + return false, err + } + if simple, err := IsSimple(g); err != nil || !simple { + return false, err + } + return true, nil +} + +// IsSimple returns whether the given geometry is simple, i.e. that it does not +// intersect or lie tangent to itself. +func IsSimple(g geo.Geometry) (bool, error) { + return geos.IsSimple(g.EWKB()) +} diff --git a/pkg/geo/geomfn/validity_check.go b/pkg/geo/geomfn/validity_check.go new file mode 100644 index 0000000..9d08d65 --- /dev/null +++ b/pkg/geo/geomfn/validity_check.go @@ -0,0 +1,105 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geos" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/twpayne/go-geom" +) + +// ValidDetail contains information about the validity of a geometry. +type ValidDetail struct { + IsValid bool + // Reason is only populated if IsValid = false. + Reason string + // InvalidLocation is only populated if IsValid = false. + InvalidLocation geo.Geometry +} + +// IsValid returns whether the given Geometry is valid. +func IsValid(g geo.Geometry) (bool, error) { + isValid, err := geos.IsValid(g.EWKB()) + if err != nil { + return false, err + } + return isValid, nil +} + +// IsValidReason returns the reasoning for whether the Geometry is valid or invalid. +func IsValidReason(g geo.Geometry) (string, error) { + reason, err := geos.IsValidReason(g.EWKB()) + if err != nil { + return "", err + } + return reason, nil +} + +// IsValidDetail returns information about the validity of a Geometry. +// It takes in a flag parameter which behaves the same as the GEOS module, where 1 +// means that self-intersecting rings forming holes are considered valid. +func IsValidDetail(g geo.Geometry, flags int) (ValidDetail, error) { + isValid, reason, locEWKB, err := geos.IsValidDetail(g.EWKB(), flags) + if err != nil { + return ValidDetail{}, err + } + var loc geo.Geometry + if len(locEWKB) > 0 { + loc, err = geo.ParseGeometryFromEWKB(locEWKB) + if err != nil { + return ValidDetail{}, err + } + } + return ValidDetail{ + IsValid: isValid, + Reason: reason, + InvalidLocation: loc, + }, nil +} + +// IsValidTrajectory returns whether a geometry encodes a valid trajectory +func IsValidTrajectory(line geo.Geometry) (bool, error) { + t, err := line.AsGeomT() + if err != nil { + return false, err + } + lineString, ok := t.(*geom.LineString) + if !ok { + return false, pgerror.Newf(pgcode.InvalidParameterValue, "expected LineString, got %s", line.ShapeType().String()) + } + mIndex := t.Layout().MIndex() + if mIndex < 0 { + return false, pgerror.Newf(pgcode.InvalidParameterValue, "LineString does not have M coordinates") + } + + coords := lineString.Coords() + for i := 1; i < len(coords); i++ { + if coords[i][mIndex] <= coords[i-1][mIndex] { + return false, nil + } + } + return true, nil +} + +// MakeValid returns a valid form of the given Geometry. +func MakeValid(g geo.Geometry) (geo.Geometry, error) { + validEWKB, err := geos.MakeValid(g.EWKB()) + if err != nil { + return geo.Geometry{}, err + } + return geo.ParseGeometryFromEWKB(validEWKB) +} + +// EqualsExact validates if two geometry objects are exact equal under some epsilon +func EqualsExact(lhs, rhs geo.Geometry, epsilon float64) bool { + equalsExact, err := geos.EqualsExact(lhs.EWKB(), rhs.EWKB(), epsilon) + if err != nil { + return false + } + return equalsExact +} diff --git a/pkg/geo/geomfn/voronoi.go b/pkg/geo/geomfn/voronoi.go new file mode 100644 index 0000000..e6892bb --- /dev/null +++ b/pkg/geo/geomfn/voronoi.go @@ -0,0 +1,34 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geomfn + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geos" +) + +// VoronoiDiagram Computes the Voronoi Diagram from the vertices of the supplied geometry. +func VoronoiDiagram( + g geo.Geometry, env *geo.Geometry, tolerance float64, onlyEdges bool, +) (geo.Geometry, error) { + var envEWKB geopb.EWKB + if !(env == nil) { + if g.SRID() != env.SRID() { + return geo.Geometry{}, geo.NewMismatchingSRIDsError(g.SpatialObject(), env.SpatialObject()) + } + envEWKB = env.EWKB() + } + paths, err := geos.VoronoiDiagram(g.EWKB(), envEWKB, tolerance, onlyEdges) + if err != nil { + return geo.Geometry{}, err + } + gm, err := geo.ParseGeometryFromEWKB(paths) + if err != nil { + return geo.Geometry{}, err + } + return gm, nil +} diff --git a/pkg/geo/geoproj/.clang-format b/pkg/geo/geoproj/.clang-format new file mode 100644 index 0000000..ad6a09e --- /dev/null +++ b/pkg/geo/geoproj/.clang-format @@ -0,0 +1,109 @@ +--- +Language: Cpp +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Right +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: false +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 100 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + - Regex: '^(<|"(gtest|isl|json)/)' + Priority: 3 + - Regex: '^".*"$' + Priority: 4 + - Regex: '.*' + Priority: 5 +IncludeIsMainRegex: '(Test)?$' +IndentCaseLabels: false +IndentWidth: 2 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 8 +UseTab: Never +... + diff --git a/pkg/geo/geoproj/geoproj.go b/pkg/geo/geoproj/geoproj.go new file mode 100644 index 0000000..bfd3f7f --- /dev/null +++ b/pkg/geo/geoproj/geoproj.go @@ -0,0 +1,94 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +// Package geoproj contains functions that interface with the PROJ library. +package geoproj + +// #cgo CXXFLAGS: -std=c++14 +// #cgo CPPFLAGS: -I../../../c-deps/proj/src +// #cgo !windows LDFLAGS: -lproj +// #cgo linux LDFLAGS: -lrt -lm -lpthread +// #cgo windows LDFLAGS: -lproj_4_9 -lshlwapi -lrpcrt4 +// +// #include "proj.h" +import "C" +import ( + "math" + "unsafe" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geographiclib" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geoprojbase" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" +) + +// maxArrayLen is the maximum safe length for this architecture. +const maxArrayLen = 1<<31 - 1 + +func cStatusToUnsafeGoBytes(s C.CR_PROJ_Status) []byte { + if s.data == nil { + return nil + } + // Interpret the C pointer as a pointer to a Go array, then slice. + return (*[maxArrayLen]byte)(unsafe.Pointer(s.data))[:s.len:s.len] +} + +// GetProjMetadata returns metadata about the given projection. +// The return arguments are a bool representing whether it is a latlng, a spheroid +// object and an error if anything was erroneous was found. +func GetProjMetadata(b geoprojbase.Proj4Text) (bool, *geographiclib.Spheroid, error) { + var majorAxis, eccentricitySquared C.double + var isLatLng C.int + if err := cStatusToUnsafeGoBytes( + C.CR_PROJ_GetProjMetadata( + (*C.char)(unsafe.Pointer(&b.Bytes()[0])), + (*C.int)(unsafe.Pointer(&isLatLng)), + (*C.double)(unsafe.Pointer(&majorAxis)), + (*C.double)(unsafe.Pointer(&eccentricitySquared)), + ), + ); err != nil { + return false, nil, pgerror.Newf(pgcode.InvalidParameterValue, "error from PROJ: %s", string(err)) + } + // flattening = e^2 / 1 + sqrt(1-e^2). + // See: https://en.wikipedia.org/wiki/Eccentricity_(mathematics), derived from + // e = sqrt(f(2-f)) + flattening := float64(eccentricitySquared) / (1 + math.Sqrt(1-float64(eccentricitySquared))) + return isLatLng != 0, geographiclib.NewSpheroid(float64(majorAxis), flattening), nil +} + +// Project projects the given xCoords, yCoords and zCoords from one +// coordinate system to another using proj4text. +// Array elements are edited in place. +func Project( + from geoprojbase.Proj4Text, + to geoprojbase.Proj4Text, + xCoords []float64, + yCoords []float64, + zCoords []float64, +) error { + if len(xCoords) != len(yCoords) || len(xCoords) != len(zCoords) { + return pgerror.Newf( + pgcode.InvalidParameterValue, + "len(xCoords) != len(yCoords) != len(zCoords): %d != %d != %d", + len(xCoords), + len(yCoords), + len(zCoords), + ) + } + if len(xCoords) == 0 { + return nil + } + if err := cStatusToUnsafeGoBytes(C.CR_PROJ_Transform( + (*C.char)(unsafe.Pointer(&from.Bytes()[0])), + (*C.char)(unsafe.Pointer(&to.Bytes()[0])), + C.long(len(xCoords)), + (*C.double)(unsafe.Pointer(&xCoords[0])), + (*C.double)(unsafe.Pointer(&yCoords[0])), + (*C.double)(unsafe.Pointer(&zCoords[0])), + )); err != nil { + return pgerror.Newf(pgcode.InvalidParameterValue, "error from PROJ: %s", string(err)) + } + return nil +} diff --git a/pkg/geo/geoproj/proj.cc b/pkg/geo/geoproj/proj.cc new file mode 100644 index 0000000..6ad73ef --- /dev/null +++ b/pkg/geo/geoproj/proj.cc @@ -0,0 +1,89 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +#include "proj.h" +#include +#include +#include +#include + +const char* DEFAULT_ERROR_MSG = "PROJ could not parse proj4text"; + +namespace { +CR_PROJ_Status CR_PROJ_ErrorFromErrorCode(int code) { + char* err = pj_strerrno(code); + if (err == nullptr) { + err = (char*)DEFAULT_ERROR_MSG; + } + return {.data = err, .len = strlen(err)}; +} + +} // namespace + +CR_PROJ_Status CR_PROJ_Transform(char* fromSpec, char* toSpec, long point_count, double* x, + double* y, double* z) { + CR_PROJ_Status err = {.data = NULL, .len = 0}; + auto ctx = pj_ctx_alloc(); + auto fromPJ = pj_init_plus_ctx(ctx, fromSpec); + if (fromPJ == nullptr) { + err = CR_PROJ_ErrorFromErrorCode(pj_ctx_get_errno(ctx)); + pj_ctx_free(ctx); + return err; + } + auto toPJ = pj_init_plus_ctx(ctx, toSpec); + if (toPJ == nullptr) { + err = CR_PROJ_ErrorFromErrorCode(pj_ctx_get_errno(ctx)); + pj_free(fromPJ); + pj_ctx_free(ctx); + return err; + } + // If we have a latlng from, transform to radians. + if (pj_is_latlong(fromPJ)) { + for (auto i = 0; i < point_count; i++) { + x[i] = x[i] * DEG_TO_RAD; + y[i] = y[i] * DEG_TO_RAD; + } + } + pj_transform(fromPJ, toPJ, point_count, 0, x, y, z); + int errCode = pj_ctx_get_errno(ctx); + if (errCode != 0) { + err = CR_PROJ_ErrorFromErrorCode(errCode); + pj_free(toPJ); + pj_free(fromPJ); + pj_ctx_free(ctx); + return err; + } + + // If we have a latlng to, transform to degrees. + if (pj_is_latlong(toPJ)) { + for (auto i = 0; i < point_count; i++) { + x[i] = x[i] * RAD_TO_DEG; + y[i] = y[i] * RAD_TO_DEG; + } + } + pj_free(toPJ); + pj_free(fromPJ); + pj_ctx_free(ctx); + return err; +} + +CR_PROJ_Status CR_PROJ_GetProjMetadata(char* spec, int* retIsLatLng, double* retMajorAxis, + double* retEccentricitySquared) { + CR_PROJ_Status err = {.data = NULL, .len = 0}; + auto ctx = pj_ctx_alloc(); + auto pj = pj_init_plus_ctx(ctx, spec); + if (pj == nullptr) { + err = CR_PROJ_ErrorFromErrorCode(pj_ctx_get_errno(ctx)); + pj_ctx_free(ctx); + return err; + } + + *retIsLatLng = pj_is_latlong(pj); + pj_get_spheroid_defn(pj, retMajorAxis, retEccentricitySquared); + + pj_free(pj); + pj_ctx_free(ctx); + return err; +} diff --git a/pkg/geo/geoproj/proj.h b/pkg/geo/geoproj/proj.h new file mode 100644 index 0000000..5ad14cb --- /dev/null +++ b/pkg/geo/geoproj/proj.h @@ -0,0 +1,33 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// CR_PROJ_Slice contains data that does not need to be freed. +// It can be either a Go or C pointer (which indicates who allocated the +// memory). +typedef struct { + char* data; + size_t len; +} CR_PROJ_Slice; + +typedef CR_PROJ_Slice CR_PROJ_Status; + +// CR_PROJ_Transform converts the given x/y/z coordinates to a new project specification. +// Note points (x[i], y[i], z[i]) are in the range 0 <= i < point_coint. +CR_PROJ_Status CR_PROJ_Transform(char* fromSpec, char* toSpec, long point_count, double* x, + double* y, double* z); + +// CR_PROJ_GetProjMetadata gets the metadata for a given spec in relation to +// the spheroid it represents. +CR_PROJ_Status CR_PROJ_GetProjMetadata(char* spec, int* retIsLatLng, double* retMajorAxis, + double* retEccentricitySquared); +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/pkg/geo/geoproj/proj_api.h b/pkg/geo/geoproj/proj_api.h new file mode 100644 index 0000000..24a6f05 --- /dev/null +++ b/pkg/geo/geoproj/proj_api.h @@ -0,0 +1,184 @@ +/****************************************************************************** + * Project: PROJ.4 + * Purpose: Public (application) include file for PROJ.4 API, and constants. + * Author: Frank Warmerdam, + * + ****************************************************************************** + * Copyright (c) 2001, Frank Warmerdam + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +/* General projections header file */ +#ifndef PROJ_API_H +#define PROJ_API_H + +/* standard inclusions */ +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * This version number should be updated with every release! The format of + * PJ_VERSION is + * + * * Before version 4.10.0: PJ_VERSION=MNP where M, N, and P are the major, + * minor, and patch numbers; e.g., PJ_VERSION=493 for version 4.9.3. + * + * * Version 4.10.0 and later: PJ_VERSION=MMMNNNPP later where MMM, NNN, PP + * are the major, minor, and patch numbers (the minor and patch numbers + * are padded with leading zeros if necessary); e.g., PJ_VERSION=401000 + * for version 4.10.0. + */ +#define PJ_VERSION 493 + +/* pj_init() and similar functions can be used with a non-C locale */ +/* Can be detected too at runtime if the symbol pj_atof exists */ +#define PJ_LOCALE_SAFE 1 + +extern char const pj_release[]; /* global release id string */ + +#define RAD_TO_DEG 57.295779513082321 +#define DEG_TO_RAD .017453292519943296 + + +extern int pj_errno; /* global error return code */ + +#if !defined(PROJECTS_H) + typedef struct { double u, v; } projUV; + typedef struct { double u, v, w; } projUVW; + typedef void *projPJ; + #define projXY projUV + #define projLP projUV + #define projXYZ projUVW + #define projLPZ projUVW + typedef void *projCtx; +#else + typedef PJ *projPJ; + typedef projCtx_t *projCtx; +# define projXY XY +# define projLP LP +# define projXYZ XYZ +# define projLPZ LPZ +#endif + +/* file reading api, like stdio */ +typedef int *PAFile; +typedef struct projFileAPI_t { + PAFile (*FOpen)(projCtx ctx, const char *filename, const char *access); + size_t (*FRead)(void *buffer, size_t size, size_t nmemb, PAFile file); + int (*FSeek)(PAFile file, long offset, int whence); + long (*FTell)(PAFile file); + void (*FClose)(PAFile); +} projFileAPI; + +/* procedure prototypes */ + +projXY pj_fwd(projLP, projPJ); +projLP pj_inv(projXY, projPJ); + +projXYZ pj_fwd3d(projLPZ, projPJ); +projLPZ pj_inv3d(projXYZ, projPJ); + +int pj_transform( projPJ src, projPJ dst, long point_count, int point_offset, + double *x, double *y, double *z ); +int pj_datum_transform( projPJ src, projPJ dst, long point_count, int point_offset, + double *x, double *y, double *z ); +int pj_geocentric_to_geodetic( double a, double es, + long point_count, int point_offset, + double *x, double *y, double *z ); +int pj_geodetic_to_geocentric( double a, double es, + long point_count, int point_offset, + double *x, double *y, double *z ); +int pj_compare_datums( projPJ srcdefn, projPJ dstdefn ); +int pj_apply_gridshift( projCtx, const char *, int, + long point_count, int point_offset, + double *x, double *y, double *z ); +void pj_deallocate_grids(void); +void pj_clear_initcache(void); +int pj_is_latlong(projPJ); +int pj_is_geocent(projPJ); +void pj_get_spheroid_defn(projPJ defn, double *major_axis, double *eccentricity_squared); +void pj_pr_list(projPJ); +void pj_free(projPJ); +void pj_set_finder( const char *(*)(const char *) ); +void pj_set_searchpath ( int count, const char **path ); +projPJ pj_init(int, char **); +projPJ pj_init_plus(const char *); +projPJ pj_init_ctx( projCtx, int, char ** ); +projPJ pj_init_plus_ctx( projCtx, const char * ); +char *pj_get_def(projPJ, int); +projPJ pj_latlong_from_proj( projPJ ); +void *pj_malloc(size_t); +void pj_dalloc(void *); +void *pj_calloc (size_t n, size_t size); +void *pj_dealloc (void *ptr); +char *pj_strerrno(int); +int *pj_get_errno_ref(void); +const char *pj_get_release(void); +void pj_acquire_lock(void); +void pj_release_lock(void); +void pj_cleanup_lock(void); + +projCtx pj_get_default_ctx(void); +projCtx pj_get_ctx( projPJ ); +void pj_set_ctx( projPJ, projCtx ); +projCtx pj_ctx_alloc(void); +void pj_ctx_free( projCtx ); +int pj_ctx_get_errno( projCtx ); +void pj_ctx_set_errno( projCtx, int ); +void pj_ctx_set_debug( projCtx, int ); +void pj_ctx_set_logger( projCtx, void (*)(void *, int, const char *) ); +void pj_ctx_set_app_data( projCtx, void * ); +void *pj_ctx_get_app_data( projCtx ); +void pj_ctx_set_fileapi( projCtx, projFileAPI *); +projFileAPI *pj_ctx_get_fileapi( projCtx ); + +void pj_log( projCtx ctx, int level, const char *fmt, ... ); +void pj_stderr_logger( void *, int, const char * ); + +/* file api */ +projFileAPI *pj_get_default_fileapi(); + +PAFile pj_ctx_fopen(projCtx ctx, const char *filename, const char *access); +size_t pj_ctx_fread(projCtx ctx, void *buffer, size_t size, size_t nmemb, PAFile file); +int pj_ctx_fseek(projCtx ctx, PAFile file, long offset, int whence); +long pj_ctx_ftell(projCtx ctx, PAFile file); +void pj_ctx_fclose(projCtx ctx, PAFile file); +char *pj_ctx_fgets(projCtx ctx, char *line, int size, PAFile file); + +PAFile pj_open_lib(projCtx, const char *, const char *); + +int pj_run_selftests (int verbosity); + + +#define PJ_LOG_NONE 0 +#define PJ_LOG_ERROR 1 +#define PJ_LOG_DEBUG_MAJOR 2 +#define PJ_LOG_DEBUG_MINOR 3 + +#ifdef __cplusplus +} +#endif + +#endif /* ndef PROJ_API_H */ + diff --git a/pkg/geo/geoproj/zcgo_flags.go b/pkg/geo/geoproj/zcgo_flags.go new file mode 100644 index 0000000..5e06e73 --- /dev/null +++ b/pkg/geo/geoproj/zcgo_flags.go @@ -0,0 +1,7 @@ +// GENERATED FILE DO NOT EDIT + +package geoproj + +// #cgo CPPFLAGS: -I/Users/jeremyyang/go/src/github.com/cockroachdb/cockroachdb-parser/bin/c-deps/archived_cdep_libjemalloc_macosarm/include +// #cgo LDFLAGS: -L/Users/jeremyyang/go/src/github.com/cockroachdb/cockroachdb-parser/bin/c-deps/archived_cdep_libjemalloc_macosarm/lib -L/Users/jeremyyang/go/src/github.com/cockroachdb/cockroachdb-parser/bin/c-deps/archived_cdep_libproj_macosarm/lib +import "C" diff --git a/pkg/geo/geoprojbase/embeddedproj/embeddedproj/embedded_proj.go b/pkg/geo/geoprojbase/embeddedproj/embeddedproj/embedded_proj.go new file mode 100644 index 0000000..77a3d19 --- /dev/null +++ b/pkg/geo/geoprojbase/embeddedproj/embeddedproj/embedded_proj.go @@ -0,0 +1,81 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +// Package embeddedproj defines the format used to embed static projection data +// in the Cockroach binary. Is is used to load the data as well as to generate +// the data file (from cmd/generate-spatial-ref-sys). +package embeddedproj + +import ( + "compress/gzip" + "encoding/json" + "io" +) + +// Spheroid stores the metadata for a spheroid. Each spheroid is referenced by +// its unique hash. +type Spheroid struct { + Hash int64 + Radius float64 + Flattening float64 +} + +// Projection stores the metadata for a projection; it mirrors the fields in +// geoprojbase.ProjInfo but with modifications that allow serialization and +// deserialization. +type Projection struct { + SRID int + AuthName string + AuthSRID int + SRText string + Proj4Text string + Bounds Bounds + IsLatLng bool + // The hash of the spheroid represented by the SRID. + Spheroid int64 +} + +// Bounds stores the bounds of a projection. +type Bounds struct { + MinX float64 + MaxX float64 + MinY float64 + MaxY float64 +} + +// Data stores all the spheroid and projection data. +type Data struct { + Spheroids []Spheroid + Projections []Projection +} + +// Encode writes serializes Data as a gzip-compressed json. +func Encode(d Data, w io.Writer) error { + data, err := json.MarshalIndent(d, "", " ") + if err != nil { + return err + } + zw, err := gzip.NewWriterLevel(w, gzip.BestCompression) + if err != nil { + return err + } + if _, err := zw.Write(data); err != nil { + return err + } + return zw.Close() +} + +// Decode deserializes Data from a gzip-compressed json generated by Encode(). +func Decode(r io.Reader) (Data, error) { + zr, err := gzip.NewReader(r) + if err != nil { + return Data{}, err + } + var result Data + if err := json.NewDecoder(zr).Decode(&result); err != nil { + return Data{}, err + } + return result, nil +} diff --git a/pkg/geo/geos/.clang-format b/pkg/geo/geos/.clang-format new file mode 100644 index 0000000..ad6a09e --- /dev/null +++ b/pkg/geo/geos/.clang-format @@ -0,0 +1,109 @@ +--- +Language: Cpp +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Right +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: false +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 100 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + - Regex: '^(<|"(gtest|isl|json)/)' + Priority: 3 + - Regex: '^".*"$' + Priority: 4 + - Regex: '.*' + Priority: 5 +IncludeIsMainRegex: '(Test)?$' +IndentCaseLabels: false +IndentWidth: 2 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 8 +UseTab: Never +... + diff --git a/pkg/geo/geos/geos.cc b/pkg/geo/geos/geos.cc new file mode 100644 index 0000000..628151a --- /dev/null +++ b/pkg/geo/geos/geos.cc @@ -0,0 +1,1558 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +#include +#include +#if _WIN32 +#include +#else +#include +#endif // #if _WIN32 +#include +#include +#include +#include +#include + +#include "geos.h" + +#if _WIN32 +#define dlopen(x, y) LoadLibrary(x) +#define dlsym GetProcAddress +#define dlclose FreeLibrary +#define dlerror() ((char*)"failed to execute dlsym") +typedef HMODULE dlhandle; +#else +typedef void* dlhandle; +#endif // #if _WIN32 + +#define CR_GEOS_NO_ERROR_DEFINED_MESSAGE "geos: returned invalid result but error not populated" + +namespace { + +// Data Types adapted from `capi/geos_c.h.in` in GEOS. +typedef void* CR_GEOS_Handle; +typedef void (*CR_GEOS_MessageHandler)(const char*, void*); +typedef void* CR_GEOS_WKTReader; +typedef void* CR_GEOS_WKBReader; +typedef void* CR_GEOS_WKBWriter; +typedef void* CR_GEOS_BufferParams; + +// Function declarations from `capi/geos_c.h.in` in GEOS. +typedef CR_GEOS_Handle (*CR_GEOS_init_r)(); +typedef void (*CR_GEOS_finish_r)(CR_GEOS_Handle); +typedef CR_GEOS_MessageHandler (*CR_GEOS_Context_setErrorMessageHandler_r)(CR_GEOS_Handle, + CR_GEOS_MessageHandler, + void*); +typedef void (*CR_GEOS_Free_r)(CR_GEOS_Handle, void* buffer); +typedef void (*CR_GEOS_SetSRID_r)(CR_GEOS_Handle, CR_GEOS_Geometry, int); +typedef int (*CR_GEOS_GetSRID_r)(CR_GEOS_Handle, CR_GEOS_Geometry); +typedef void (*CR_GEOS_GeomDestroy_r)(CR_GEOS_Handle, CR_GEOS_Geometry); + +typedef char (*CR_GEOS_HasZ_r)(CR_GEOS_Handle, CR_GEOS_Geometry); +typedef char (*CR_GEOS_IsEmpty_r)(CR_GEOS_Handle, CR_GEOS_Geometry); +typedef char (*CR_GEOS_IsSimple_r)(CR_GEOS_Handle, CR_GEOS_Geometry); +typedef int (*CR_GEOS_GeomTypeId_r)(CR_GEOS_Handle, CR_GEOS_Geometry); + +typedef CR_GEOS_WKTReader (*CR_GEOS_WKTReader_create_r)(CR_GEOS_Handle); +typedef CR_GEOS_Geometry (*CR_GEOS_WKTReader_read_r)(CR_GEOS_Handle, CR_GEOS_WKTReader, + const char*); +typedef void (*CR_GEOS_WKTReader_destroy_r)(CR_GEOS_Handle, CR_GEOS_WKTReader); + +typedef CR_GEOS_WKBReader (*CR_GEOS_WKBReader_create_r)(CR_GEOS_Handle); +typedef CR_GEOS_Geometry (*CR_GEOS_WKBReader_read_r)(CR_GEOS_Handle, CR_GEOS_WKBReader, const char*, + size_t); +typedef void (*CR_GEOS_WKBReader_destroy_r)(CR_GEOS_Handle, CR_GEOS_WKBReader); + +typedef CR_GEOS_BufferParams (*CR_GEOS_BufferParams_create_r)(CR_GEOS_Handle); +typedef void (*CR_GEOS_GEOSBufferParams_destroy_r)(CR_GEOS_Handle, CR_GEOS_BufferParams); +typedef int (*CR_GEOS_BufferParams_setEndCapStyle_r)(CR_GEOS_Handle, CR_GEOS_BufferParams, + int endCapStyle); +typedef int (*CR_GEOS_BufferParams_setJoinStyle_r)(CR_GEOS_Handle, CR_GEOS_BufferParams, + int joinStyle); +typedef int (*CR_GEOS_BufferParams_setMitreLimit_r)(CR_GEOS_Handle, CR_GEOS_BufferParams, + double mitreLimit); +typedef int (*CR_GEOS_BufferParams_setQuadrantSegments_r)(CR_GEOS_Handle, CR_GEOS_BufferParams, + int quadrantSegments); +typedef int (*CR_GEOS_BufferParams_setSingleSided_r)(CR_GEOS_Handle, CR_GEOS_BufferParams, + int singleSided); +typedef CR_GEOS_Geometry (*CR_GEOS_BufferWithParams_r)(CR_GEOS_Handle, CR_GEOS_Geometry, + CR_GEOS_BufferParams, double width); + +typedef char (*CR_GEOS_isValid_r)(CR_GEOS_Handle, CR_GEOS_Geometry); +typedef char* (*CR_GEOS_isValidReason_r)(CR_GEOS_Handle, CR_GEOS_Geometry); +typedef char (*CR_GEOS_isValidDetail_r)(CR_GEOS_Handle, CR_GEOS_Geometry, int flags, char** reason, + CR_GEOS_Geometry* loc); +typedef CR_GEOS_Geometry (*CR_GEOS_MakeValid_r)(CR_GEOS_Handle, CR_GEOS_Geometry); + +typedef int (*CR_GEOS_Area_r)(CR_GEOS_Handle, CR_GEOS_Geometry, double*); +typedef int (*CR_GEOS_Length_r)(CR_GEOS_Handle, CR_GEOS_Geometry, double*); +typedef int (*CR_GEOS_MinimumClearance_r)(CR_GEOS_Handle, CR_GEOS_Geometry, double*); +typedef CR_GEOS_Geometry (*CR_GEOS_MinimumClearanceLine_r)(CR_GEOS_Handle, CR_GEOS_Geometry); +typedef int (*CR_GEOS_Normalize_r)(CR_GEOS_Handle, CR_GEOS_Geometry); +typedef CR_GEOS_Geometry (*CR_GEOS_LineMerge_r)(CR_GEOS_Handle, CR_GEOS_Geometry); + +typedef CR_GEOS_Geometry (*CR_GEOS_Boundary_r)(CR_GEOS_Handle, CR_GEOS_Geometry); +typedef CR_GEOS_Geometry (*CR_GEOS_Centroid_r)(CR_GEOS_Handle, CR_GEOS_Geometry); +typedef CR_GEOS_Geometry (*CR_GEOS_ConvexHull_r)(CR_GEOS_Handle, CR_GEOS_Geometry); +typedef CR_GEOS_Geometry (*CR_GEOS_Difference_r)(CR_GEOS_Handle, CR_GEOS_Geometry, + CR_GEOS_Geometry); +typedef CR_GEOS_Geometry (*CR_GEOS_Simplify_r)(CR_GEOS_Handle, CR_GEOS_Geometry, double); +typedef CR_GEOS_Geometry (*CR_GEOS_TopologyPreserveSimplify_r)(CR_GEOS_Handle, CR_GEOS_Geometry, + double); +typedef CR_GEOS_Geometry (*CR_GEOS_UnaryUnion_r)(CR_GEOS_Handle, CR_GEOS_Geometry); +typedef CR_GEOS_Geometry (*CR_GEOS_Union_r)(CR_GEOS_Handle, CR_GEOS_Geometry, CR_GEOS_Geometry); +typedef CR_GEOS_Geometry (*CR_GEOS_Intersection_r)(CR_GEOS_Handle, CR_GEOS_Geometry, + CR_GEOS_Geometry); +typedef CR_GEOS_Geometry (*CR_GEOS_SymDifference_r)(CR_GEOS_Handle, CR_GEOS_Geometry, + CR_GEOS_Geometry); +typedef CR_GEOS_Geometry (*CR_GEOS_PointOnSurface_r)(CR_GEOS_Handle, CR_GEOS_Geometry); + +typedef CR_GEOS_Geometry (*CR_GEOS_Interpolate_r)(CR_GEOS_Handle, CR_GEOS_Geometry, double); + +typedef CR_GEOS_Geometry (*CR_GEOS_MinimumBoundingCircle_r)(CR_GEOS_Handle, CR_GEOS_Geometry, double*, CR_GEOS_Geometry**); + +typedef int (*CR_GEOS_Distance_r)(CR_GEOS_Handle, CR_GEOS_Geometry, CR_GEOS_Geometry, double*); +typedef int (*CR_GEOS_FrechetDistance_r)(CR_GEOS_Handle, CR_GEOS_Geometry, CR_GEOS_Geometry, + double*); +typedef int (*CR_GEOS_FrechetDistanceDensify_r)(CR_GEOS_Handle, CR_GEOS_Geometry, CR_GEOS_Geometry, + double, double*); +typedef int (*CR_GEOS_HausdorffDistance_r)(CR_GEOS_Handle, CR_GEOS_Geometry, CR_GEOS_Geometry, + double*); +typedef int (*CR_GEOS_HausdorffDistanceDensify_r)(CR_GEOS_Handle, CR_GEOS_Geometry, + CR_GEOS_Geometry, double, double*); + +typedef CR_GEOS_PreparedInternalGeometry (*CR_GEOS_Prepare_r)(CR_GEOS_Handle, CR_GEOS_Geometry); +typedef void (*CR_GEOS_PreparedGeom_destroy_r)(CR_GEOS_Handle, CR_GEOS_PreparedInternalGeometry); + +typedef char (*CR_GEOS_PreparedIntersects_r)(CR_GEOS_Handle, CR_GEOS_PreparedInternalGeometry, CR_GEOS_Geometry); + +typedef char (*CR_GEOS_Covers_r)(CR_GEOS_Handle, CR_GEOS_Geometry, CR_GEOS_Geometry); +typedef char (*CR_GEOS_CoveredBy_r)(CR_GEOS_Handle, CR_GEOS_Geometry, CR_GEOS_Geometry); +typedef char (*CR_GEOS_Contains_r)(CR_GEOS_Handle, CR_GEOS_Geometry, CR_GEOS_Geometry); +typedef char (*CR_GEOS_Crosses_r)(CR_GEOS_Handle, CR_GEOS_Geometry, CR_GEOS_Geometry); +typedef char (*CR_GEOS_Disjoint_r)(CR_GEOS_Handle, CR_GEOS_Geometry, CR_GEOS_Geometry); +typedef char (*CR_GEOS_Equals_r)(CR_GEOS_Handle, CR_GEOS_Geometry, CR_GEOS_Geometry); +typedef char (*CR_GEOS_Intersects_r)(CR_GEOS_Handle, CR_GEOS_Geometry, CR_GEOS_Geometry); +typedef char (*CR_GEOS_Overlaps_r)(CR_GEOS_Handle, CR_GEOS_Geometry, CR_GEOS_Geometry); +typedef char (*CR_GEOS_Touches_r)(CR_GEOS_Handle, CR_GEOS_Geometry, CR_GEOS_Geometry); +typedef char (*CR_GEOS_Within_r)(CR_GEOS_Handle, CR_GEOS_Geometry, CR_GEOS_Geometry); + +typedef char* (*CR_GEOS_Relate_r)(CR_GEOS_Handle, CR_GEOS_Geometry, CR_GEOS_Geometry); +typedef char* (*CR_GEOS_RelateBoundaryNodeRule_r)(CR_GEOS_Handle, CR_GEOS_Geometry, + CR_GEOS_Geometry, int); +typedef char (*CR_GEOS_RelatePattern_r)(CR_GEOS_Handle, CR_GEOS_Geometry, CR_GEOS_Geometry, + const char*); + +typedef CR_GEOS_WKBWriter (*CR_GEOS_WKBWriter_create_r)(CR_GEOS_Handle); +typedef char* (*CR_GEOS_WKBWriter_write_r)(CR_GEOS_Handle, CR_GEOS_WKBWriter, CR_GEOS_Geometry, + size_t*); +typedef void (*CR_GEOS_WKBWriter_setByteOrder_r)(CR_GEOS_Handle, CR_GEOS_WKBWriter, int); +typedef void (*CR_GEOS_WKBWriter_setOutputDimension_r)(CR_GEOS_Handle, CR_GEOS_WKBWriter, int); +typedef void (*CR_GEOS_WKBWriter_destroy_r)(CR_GEOS_Handle, CR_GEOS_WKBWriter); +typedef void (*CR_GEOS_WKBWriter_setIncludeSRID_r)(CR_GEOS_Handle, CR_GEOS_WKBWriter, const char); +typedef CR_GEOS_Geometry (*CR_GEOS_ClipByRect_r)(CR_GEOS_Handle, CR_GEOS_Geometry, double, double, + double, double); + +typedef CR_GEOS_Geometry (*CR_GEOS_SharedPaths_r)(CR_GEOS_Handle, CR_GEOS_Geometry, + CR_GEOS_Geometry); + +typedef CR_GEOS_Geometry (*CR_GEOS_Node_r)(CR_GEOS_Handle, CR_GEOS_Geometry); + +typedef CR_GEOS_Geometry (*CR_GEOS_VoronoiDiagram_r)(CR_GEOS_Handle, CR_GEOS_Geometry, + CR_GEOS_Geometry, double, int); + +typedef char (*CR_GEOS_EqualsExact_r)(CR_GEOS_Handle, CR_GEOS_Geometry, + CR_GEOS_Geometry, double); + +typedef CR_GEOS_Geometry (*CR_GEOS_MinimumRotatedRectangle_r)(CR_GEOS_Handle, CR_GEOS_Geometry); + +typedef CR_GEOS_Geometry (*CR_GEOS_Snap_r)(CR_GEOS_Handle, CR_GEOS_Geometry, CR_GEOS_Geometry, double); +typedef const char* (*CR_GEOS_Version_r)(); + +std::string ToString(CR_GEOS_Slice slice) { return std::string(slice.data, slice.len); } + +} // namespace + +struct CR_GEOS { + dlhandle geoscHandle; + dlhandle geosHandle; + + CR_GEOS_init_r GEOS_init_r; + CR_GEOS_finish_r GEOS_finish_r; + CR_GEOS_Context_setErrorMessageHandler_r GEOSContext_setErrorMessageHandler_r; + CR_GEOS_Free_r GEOSFree_r; + + CR_GEOS_HasZ_r GEOSHasZ_r; + CR_GEOS_IsEmpty_r GEOSisEmpty_r; + CR_GEOS_IsSimple_r GEOSisSimple_r; + CR_GEOS_GeomTypeId_r GEOSGeomTypeId_r; + + CR_GEOS_BufferParams_create_r GEOSBufferParams_create_r; + CR_GEOS_GEOSBufferParams_destroy_r GEOSBufferParams_destroy_r; + CR_GEOS_BufferParams_setEndCapStyle_r GEOSBufferParams_setEndCapStyle_r; + CR_GEOS_BufferParams_setJoinStyle_r GEOSBufferParams_setJoinStyle_r; + CR_GEOS_BufferParams_setMitreLimit_r GEOSBufferParams_setMitreLimit_r; + CR_GEOS_BufferParams_setQuadrantSegments_r GEOSBufferParams_setQuadrantSegments_r; + CR_GEOS_BufferParams_setSingleSided_r GEOSBufferParams_setSingleSided_r; + CR_GEOS_BufferWithParams_r GEOSBufferWithParams_r; + + CR_GEOS_SetSRID_r GEOSSetSRID_r; + CR_GEOS_GetSRID_r GEOSGetSRID_r; + CR_GEOS_GeomDestroy_r GEOSGeom_destroy_r; + + CR_GEOS_WKTReader_create_r GEOSWKTReader_create_r; + CR_GEOS_WKTReader_destroy_r GEOSWKTReader_destroy_r; + CR_GEOS_WKTReader_read_r GEOSWKTReader_read_r; + + CR_GEOS_WKBReader_create_r GEOSWKBReader_create_r; + CR_GEOS_WKBReader_destroy_r GEOSWKBReader_destroy_r; + CR_GEOS_WKBReader_read_r GEOSWKBReader_read_r; + + CR_GEOS_isValid_r GEOSisValid_r; + CR_GEOS_isValidReason_r GEOSisValidReason_r; + CR_GEOS_isValidDetail_r GEOSisValidDetail_r; + CR_GEOS_MakeValid_r GEOSMakeValid_r; + + CR_GEOS_Area_r GEOSArea_r; + CR_GEOS_Length_r GEOSLength_r; + CR_GEOS_MinimumClearance_r GEOSMinimumClearance_r; + CR_GEOS_MinimumClearanceLine_r GEOSMinimumClearanceLine_r; + CR_GEOS_Normalize_r GEOSNormalize_r; + CR_GEOS_LineMerge_r GEOSLineMerge_r; + + CR_GEOS_Boundary_r GEOSBoundary_r; + CR_GEOS_Centroid_r GEOSGetCentroid_r; + CR_GEOS_ConvexHull_r GEOSConvexHull_r; + CR_GEOS_Difference_r GEOSDifference_r; + CR_GEOS_Simplify_r GEOSSimplify_r; + CR_GEOS_TopologyPreserveSimplify_r GEOSTopologyPreserveSimplify_r; + CR_GEOS_UnaryUnion_r GEOSUnaryUnion_r; + CR_GEOS_Union_r GEOSUnion_r; + CR_GEOS_PointOnSurface_r GEOSPointOnSurface_r; + CR_GEOS_Intersection_r GEOSIntersection_r; + CR_GEOS_SymDifference_r GEOSSymDifference_r; + + CR_GEOS_MinimumBoundingCircle_r GEOSMinimumBoundingCircle_r; + + CR_GEOS_Interpolate_r GEOSInterpolate_r; + + CR_GEOS_Distance_r GEOSDistance_r; + CR_GEOS_FrechetDistance_r GEOSFrechetDistance_r; + CR_GEOS_FrechetDistanceDensify_r GEOSFrechetDistanceDensify_r; + CR_GEOS_HausdorffDistance_r GEOSHausdorffDistance_r; + CR_GEOS_HausdorffDistanceDensify_r GEOSHausdorffDistanceDensify_r; + + CR_GEOS_Prepare_r GEOSPrepare_r; + CR_GEOS_PreparedGeom_destroy_r GEOSPreparedGeom_destroy_r; + + CR_GEOS_PreparedIntersects_r GEOSPreparedIntersects_r; + + CR_GEOS_Covers_r GEOSCovers_r; + CR_GEOS_CoveredBy_r GEOSCoveredBy_r; + CR_GEOS_Contains_r GEOSContains_r; + CR_GEOS_Crosses_r GEOSCrosses_r; + CR_GEOS_Disjoint_r GEOSDisjoint_r; + CR_GEOS_Equals_r GEOSEquals_r; + CR_GEOS_Intersects_r GEOSIntersects_r; + CR_GEOS_Overlaps_r GEOSOverlaps_r; + CR_GEOS_Touches_r GEOSTouches_r; + CR_GEOS_Within_r GEOSWithin_r; + + CR_GEOS_Relate_r GEOSRelate_r; + CR_GEOS_RelateBoundaryNodeRule_r GEOSRelateBoundaryNodeRule_r; + CR_GEOS_RelatePattern_r GEOSRelatePattern_r; + + CR_GEOS_WKBWriter_create_r GEOSWKBWriter_create_r; + CR_GEOS_WKBWriter_destroy_r GEOSWKBWriter_destroy_r; + CR_GEOS_WKBWriter_setByteOrder_r GEOSWKBWriter_setByteOrder_r; + CR_GEOS_WKBWriter_setOutputDimension_r GEOSWKBWriter_setOutputDimension_r; + CR_GEOS_WKBWriter_setIncludeSRID_r GEOSWKBWriter_setIncludeSRID_r; + CR_GEOS_WKBWriter_write_r GEOSWKBWriter_write_r; + + CR_GEOS_ClipByRect_r GEOSClipByRect_r; + + CR_GEOS_SharedPaths_r GEOSSharedPaths_r; + CR_GEOS_VoronoiDiagram_r GEOSVoronoiDiagram_r; + CR_GEOS_EqualsExact_r GEOSEqualsExact_r; + CR_GEOS_MinimumRotatedRectangle_r GEOSMinimumRotatedRectangle_r; + + CR_GEOS_Node_r GEOSNode_r; + + CR_GEOS_Snap_r GEOSSnap_r; + + CR_GEOS_Version_r GEOSversion; + + CR_GEOS(dlhandle geoscHandle, dlhandle geosHandle) + : geoscHandle(geoscHandle), geosHandle(geosHandle) {} + + ~CR_GEOS() { + if (geoscHandle != NULL) { + dlclose(geoscHandle); + } + if (geosHandle != NULL) { + dlclose(geosHandle); + } + } + + char* Init() { +#define INIT(x) \ + do { \ + auto error = InitSym(&x, #x); \ + if (error != nullptr) { \ + return error; \ + } \ + } while (0) + + INIT(GEOS_init_r); + INIT(GEOS_finish_r); + INIT(GEOSFree_r); + INIT(GEOSContext_setErrorMessageHandler_r); + INIT(GEOSGeom_destroy_r); + INIT(GEOSBufferParams_create_r); + INIT(GEOSBufferParams_destroy_r); + INIT(GEOSBufferParams_setEndCapStyle_r); + INIT(GEOSBufferParams_setJoinStyle_r); + INIT(GEOSBufferParams_setMitreLimit_r); + INIT(GEOSBufferParams_setQuadrantSegments_r); + INIT(GEOSBufferParams_setSingleSided_r); + INIT(GEOSBufferWithParams_r); + INIT(GEOSHasZ_r); + INIT(GEOSisEmpty_r); + INIT(GEOSGeomTypeId_r); + INIT(GEOSSetSRID_r); + INIT(GEOSGetSRID_r); + INIT(GEOSisValid_r); + INIT(GEOSisValidReason_r); + INIT(GEOSisValidDetail_r); + INIT(GEOSMakeValid_r); + INIT(GEOSArea_r); + INIT(GEOSLength_r); + INIT(GEOSMinimumClearance_r); + INIT(GEOSMinimumClearanceLine_r); + INIT(GEOSNormalize_r); + INIT(GEOSLineMerge_r); + INIT(GEOSisSimple_r); + INIT(GEOSBoundary_r); + INIT(GEOSDifference_r); + INIT(GEOSGetCentroid_r); + INIT(GEOSMinimumBoundingCircle_r); + INIT(GEOSConvexHull_r); + INIT(GEOSSimplify_r); + INIT(GEOSTopologyPreserveSimplify_r); + INIT(GEOSUnaryUnion_r); + INIT(GEOSUnion_r); + INIT(GEOSPointOnSurface_r); + INIT(GEOSIntersection_r); + INIT(GEOSSymDifference_r); + INIT(GEOSInterpolate_r); + INIT(GEOSDistance_r); + INIT(GEOSFrechetDistance_r); + INIT(GEOSFrechetDistanceDensify_r); + INIT(GEOSHausdorffDistance_r); + INIT(GEOSHausdorffDistanceDensify_r); + INIT(GEOSPrepare_r); + INIT(GEOSPreparedGeom_destroy_r); + INIT(GEOSPreparedIntersects_r); + INIT(GEOSCovers_r); + INIT(GEOSCoveredBy_r); + INIT(GEOSContains_r); + INIT(GEOSCrosses_r); + INIT(GEOSDisjoint_r); + INIT(GEOSEquals_r); + INIT(GEOSIntersects_r); + INIT(GEOSOverlaps_r); + INIT(GEOSTouches_r); + INIT(GEOSWithin_r); + INIT(GEOSRelate_r); + INIT(GEOSVoronoiDiagram_r); + INIT(GEOSEqualsExact_r); + INIT(GEOSMinimumRotatedRectangle_r); + INIT(GEOSRelateBoundaryNodeRule_r); + INIT(GEOSRelatePattern_r); + INIT(GEOSSharedPaths_r); + INIT(GEOSWKTReader_create_r); + INIT(GEOSWKTReader_destroy_r); + INIT(GEOSWKTReader_read_r); + INIT(GEOSWKBReader_create_r); + INIT(GEOSWKBReader_destroy_r); + INIT(GEOSWKBReader_read_r); + INIT(GEOSWKBWriter_create_r); + INIT(GEOSWKBWriter_destroy_r); + INIT(GEOSWKBWriter_setByteOrder_r); + INIT(GEOSWKBWriter_setOutputDimension_r); + INIT(GEOSWKBWriter_setIncludeSRID_r); + INIT(GEOSWKBWriter_write_r); + INIT(GEOSClipByRect_r); + INIT(GEOSNode_r); + INIT(GEOSSnap_r); + INIT(GEOSversion); + return nullptr; + +#undef INIT + } + + template char* InitSym(T* ptr, const char* symbol) { + *ptr = reinterpret_cast(dlsym(geoscHandle, symbol)); + if (ptr == nullptr) { + return dlerror(); + } + return nullptr; + } +}; + +CR_GEOS_Slice cStringToSlice(char* cstr) { + CR_GEOS_Slice slice = {.data = cstr, .len = strlen(cstr)}; + return slice; +} + +// Given data that will be deallocated on return, with length +// equal to len, returns a CR_GEOS_String to return to Go. +CR_GEOS_String toGEOSString(const char* data, size_t len) { + CR_GEOS_String result = {.data = nullptr, .len = len}; + if (len == 0) { + return result; + } + result.data = static_cast(malloc(len)); + memcpy(result.data, data, len); + return result; +} + +void errorHandler(const char* msg, void* buffer) { + std::string* str = static_cast(buffer); + *str = std::string("geos error: ") + msg; +} + +CR_GEOS_Status CR_GEOS_Init(CR_GEOS_Slice geoscLoc, CR_GEOS_Slice geosLoc, CR_GEOS** lib) { + // Open the libgeos.$(EXT) first, so that libgeos_c.$(EXT) can read it. + std::string error; + auto geosLocStr = ToString(geosLoc); + dlhandle geosHandle = dlopen(geosLocStr.c_str(), RTLD_LAZY); + if (!geosHandle) { + errorHandler(dlerror(), &error); + return toGEOSString(error.data(), error.length()); + } + + auto geoscLocStr = ToString(geoscLoc); + dlhandle geoscHandle = dlopen(geoscLocStr.c_str(), RTLD_LAZY); + if (!geoscHandle) { + errorHandler(dlerror(), &error); + dlclose(geosHandle); + return toGEOSString(error.data(), error.length()); + } + + std::unique_ptr ret(new CR_GEOS(geoscHandle, geosHandle)); + auto initError = ret->Init(); + if (initError != nullptr) { + errorHandler(initError, &error); + dlclose(geosHandle); + dlclose(geoscHandle); + return toGEOSString(error.data(), error.length()); + } + + *lib = ret.release(); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Handle initHandleWithErrorBuffer(CR_GEOS* lib, std::string* buffer) { + auto handle = lib->GEOS_init_r(); + lib->GEOSContext_setErrorMessageHandler_r(handle, errorHandler, buffer); + return handle; +} + +CR_GEOS_Geometry CR_GEOS_GeometryFromSlice(CR_GEOS* lib, CR_GEOS_Handle handle, + CR_GEOS_Slice slice) { + auto wkbReader = lib->GEOSWKBReader_create_r(handle); + auto geom = lib->GEOSWKBReader_read_r(handle, wkbReader, slice.data, slice.len); + lib->GEOSWKBReader_destroy_r(handle, wkbReader); + return geom; +} + +void CR_GEOS_Version(CR_GEOS* lib, CR_GEOS_String* ret) { + auto version = lib->GEOSversion(); + *ret = toGEOSString(version, strlen(version)); +} + +void CR_GEOS_writeGeomToEWKB(CR_GEOS* lib, CR_GEOS_Handle handle, CR_GEOS_Geometry geom, + CR_GEOS_String* ewkb, int srid) { + auto hasZ = lib->GEOSHasZ_r(handle, geom); + auto wkbWriter = lib->GEOSWKBWriter_create_r(handle); + lib->GEOSWKBWriter_setByteOrder_r(handle, wkbWriter, 1); + if (hasZ) { + lib->GEOSWKBWriter_setOutputDimension_r(handle, wkbWriter, 3); + } + if (srid != 0) { + lib->GEOSSetSRID_r(handle, geom, srid); + } + lib->GEOSWKBWriter_setIncludeSRID_r(handle, wkbWriter, true); + ewkb->data = lib->GEOSWKBWriter_write_r(handle, wkbWriter, geom, &ewkb->len); + lib->GEOSWKBWriter_destroy_r(handle, wkbWriter); +} + +CR_GEOS_Status CR_GEOS_WKTToEWKB(CR_GEOS* lib, CR_GEOS_Slice wkt, int srid, CR_GEOS_String* ewkb) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + + auto wktReader = lib->GEOSWKTReader_create_r(handle); + auto wktStr = ToString(wkt); + auto geom = lib->GEOSWKTReader_read_r(handle, wktReader, wktStr.c_str()); + lib->GEOSWKTReader_destroy_r(handle, wktReader); + + if (geom != NULL) { + CR_GEOS_writeGeomToEWKB(lib, handle, geom, ewkb, srid); + lib->GEOSGeom_destroy_r(handle, geom); + } + + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_ClipByRect(CR_GEOS* lib, CR_GEOS_Slice ewkb, double xmin, double ymin, + double xmax, double ymax, CR_GEOS_String* clippedEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + *clippedEWKB = {.data = NULL, .len = 0}; + + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, ewkb); + if (geom != nullptr) { + auto clippedGeom = lib->GEOSClipByRect_r(handle, geom, xmin, ymin, xmax, ymax); + if (clippedGeom != nullptr) { + auto srid = lib->GEOSGetSRID_r(handle, geom); + CR_GEOS_writeGeomToEWKB(lib, handle, clippedGeom, clippedEWKB, srid); + lib->GEOSGeom_destroy_r(handle, clippedGeom); + } + lib->GEOSGeom_destroy_r(handle, geom); + } + + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_Buffer(CR_GEOS* lib, CR_GEOS_Slice ewkb, CR_GEOS_BufferParamsInput params, + double distance, CR_GEOS_String* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + *ret = {.data = NULL, .len = 0}; + + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, ewkb); + if (geom != nullptr) { + auto gParams = lib->GEOSBufferParams_create_r(handle); + if (gParams != nullptr) { + if (lib->GEOSBufferParams_setEndCapStyle_r(handle, gParams, params.endCapStyle) && + lib->GEOSBufferParams_setJoinStyle_r(handle, gParams, params.joinStyle) && + lib->GEOSBufferParams_setMitreLimit_r(handle, gParams, params.mitreLimit) && + lib->GEOSBufferParams_setQuadrantSegments_r(handle, gParams, params.quadrantSegments) && + lib->GEOSBufferParams_setSingleSided_r(handle, gParams, params.singleSided)) { + auto bufferedGeom = lib->GEOSBufferWithParams_r(handle, geom, gParams, distance); + if (bufferedGeom != nullptr) { + auto srid = lib->GEOSGetSRID_r(handle, geom); + CR_GEOS_writeGeomToEWKB(lib, handle, bufferedGeom, ret, srid); + lib->GEOSGeom_destroy_r(handle, bufferedGeom); + } + } + lib->GEOSBufferParams_destroy_r(handle, gParams); + } + lib->GEOSGeom_destroy_r(handle, geom); + } + + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +// +// Unary operators +// + +template +CR_GEOS_Status CR_GEOS_UnaryOperator(CR_GEOS* lib, T fn, CR_GEOS_Slice a, R* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, a); + if (geom != nullptr) { + auto r = fn(handle, geom, ret); + // ret == 0 indicates an exception. + if (r == 0) { + if (error.length() == 0) { + error.assign(CR_GEOS_NO_ERROR_DEFINED_MESSAGE); + } + } + lib->GEOSGeom_destroy_r(handle, geom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +template +CR_GEOS_Status CR_GEOS_BinaryOperator(CR_GEOS* lib, T fn, CR_GEOS_Slice a, CR_GEOS_Slice b, + R* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto wkbReader = lib->GEOSWKBReader_create_r(handle); + auto geomA = lib->GEOSWKBReader_read_r(handle, wkbReader, a.data, a.len); + auto geomB = lib->GEOSWKBReader_read_r(handle, wkbReader, b.data, b.len); + lib->GEOSWKBReader_destroy_r(handle, wkbReader); + if (geomA != nullptr && geomB != nullptr) { + auto r = fn(handle, geomA, geomB, ret); + // ret == 0 indicates an exception. + if (r == 0) { + if (error.length() == 0) { + error.assign(CR_GEOS_NO_ERROR_DEFINED_MESSAGE); + } + } + } + if (geomA != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomA); + } + if (geomB != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomB); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_Area(CR_GEOS* lib, CR_GEOS_Slice a, double* ret) { + return CR_GEOS_UnaryOperator(lib, lib->GEOSArea_r, a, ret); +} + +CR_GEOS_Status CR_GEOS_Length(CR_GEOS* lib, CR_GEOS_Slice a, double* ret) { + return CR_GEOS_UnaryOperator(lib, lib->GEOSLength_r, a, ret); +} + +CR_GEOS_Status CR_GEOS_MinimumClearance(CR_GEOS* lib, CR_GEOS_Slice g, double* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, g); + *ret = 0; + if (geom != nullptr) { + auto r = lib->GEOSMinimumClearance_r(handle, geom, ret); + if (r == 2) { + if (error.length() == 0) { + error.assign(CR_GEOS_NO_ERROR_DEFINED_MESSAGE); + } + } + lib->GEOSGeom_destroy_r(handle, geom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_MinimumClearanceLine(CR_GEOS* lib, CR_GEOS_Slice g, + CR_GEOS_String* clearanceEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + *clearanceEWKB = {.data = NULL, .len = 0}; + + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, g); + if (geom != nullptr) { + auto clearance = lib->GEOSMinimumClearanceLine_r(handle, geom); + auto srid = lib->GEOSGetSRID_r(handle, clearance); + CR_GEOS_writeGeomToEWKB(lib, handle, clearance, clearanceEWKB, srid); + lib->GEOSGeom_destroy_r(handle, geom); + } + + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_Normalize(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_String* normalizedEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + *normalizedEWKB = {.data = NULL, .len = 0}; + + auto geomA = CR_GEOS_GeometryFromSlice(lib, handle, a); + if (geomA != nullptr) { + auto exceptionStatus = lib->GEOSNormalize_r(handle, geomA); + if (exceptionStatus != -1) { + auto srid = lib->GEOSGetSRID_r(handle, geomA); + CR_GEOS_writeGeomToEWKB(lib, handle, geomA, normalizedEWKB, srid); + } else { + error.assign(CR_GEOS_NO_ERROR_DEFINED_MESSAGE); + } + lib->GEOSGeom_destroy_r(handle, geomA); + } + + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_LineMerge(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_String* ewkb) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + *ewkb = {.data = NULL, .len = 0}; + + auto geomA = CR_GEOS_GeometryFromSlice(lib, handle, a); + if (geomA != nullptr) { + auto merged = lib->GEOSLineMerge_r(handle, geomA); + auto srid = lib->GEOSGetSRID_r(handle, merged); + CR_GEOS_writeGeomToEWKB(lib, handle, merged, ewkb, srid); + lib->GEOSGeom_destroy_r(handle, geomA); + } + + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +// +// Unary predicates. +// + +template +CR_GEOS_Status CR_GEOS_UnaryPredicate(CR_GEOS* lib, T fn, CR_GEOS_Slice a, char* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, a); + if (geom != nullptr) { + auto r = fn(handle, geom); + // r == 2 indicates an exception. + if (r == 2) { + if (error.length() == 0) { + error.assign(CR_GEOS_NO_ERROR_DEFINED_MESSAGE); + } + } else { + *ret = r; + } + lib->GEOSGeom_destroy_r(handle, geom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_IsSimple(CR_GEOS* lib, CR_GEOS_Slice a, char* ret) { + return CR_GEOS_UnaryPredicate(lib, lib->GEOSisSimple_r, a, ret); +} + +// +// Validity checking. +// + +CR_GEOS_Status CR_GEOS_IsValid(CR_GEOS* lib, CR_GEOS_Slice g, char* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, g); + *ret = 0; + if (geom != nullptr) { + auto r = lib->GEOSisValid_r(handle, geom); + if (r == 2) { + if (error.length() == 0) { + error.assign(CR_GEOS_NO_ERROR_DEFINED_MESSAGE); + } + } else { + *ret = r; + } + lib->GEOSGeom_destroy_r(handle, geom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_IsValidReason(CR_GEOS* lib, CR_GEOS_Slice g, CR_GEOS_String* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, g); + *ret = {.data = NULL, .len = 0}; + if (geom != nullptr) { + auto r = lib->GEOSisValidReason_r(handle, geom); + if (r != NULL) { + *ret = toGEOSString(r, strlen(r)); + lib->GEOSFree_r(handle, r); + } + lib->GEOSGeom_destroy_r(handle, geom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_IsValidDetail(CR_GEOS* lib, CR_GEOS_Slice g, int flags, char* retIsValid, + CR_GEOS_String* retReason, CR_GEOS_String* retLocationEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, g); + *retReason = {.data = NULL, .len = 0}; + *retLocationEWKB = {.data = NULL, .len = 0}; + if (geom != nullptr) { + char* reason = NULL; + CR_GEOS_Geometry loc = NULL; + auto r = lib->GEOSisValidDetail_r(handle, geom, flags, &reason, &loc); + + if (r == 2) { + if (error.length() == 0) { + error.assign(CR_GEOS_NO_ERROR_DEFINED_MESSAGE); + } + } else { + *retIsValid = r; + } + + if (reason != NULL) { + *retReason = toGEOSString(reason, strlen(reason)); + lib->GEOSFree_r(handle, reason); + } + + if (loc != NULL) { + auto srid = lib->GEOSGetSRID_r(handle, geom); + CR_GEOS_writeGeomToEWKB(lib, handle, loc, retLocationEWKB, srid); + lib->GEOSGeom_destroy_r(handle, loc); + } + + lib->GEOSGeom_destroy_r(handle, geom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_MakeValid(CR_GEOS* lib, CR_GEOS_Slice g, CR_GEOS_String* validEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, g); + *validEWKB = {.data = NULL, .len = 0}; + if (geom != nullptr) { + auto validGeom = lib->GEOSMakeValid_r(handle, geom); + if (validGeom != nullptr) { + auto srid = lib->GEOSGetSRID_r(handle, geom); + CR_GEOS_writeGeomToEWKB(lib, handle, validGeom, validEWKB, srid); + lib->GEOSGeom_destroy_r(handle, validGeom); + } + lib->GEOSGeom_destroy_r(handle, geom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +// +// Topology operators. +// + +CR_GEOS_Status CR_GEOS_Boundary(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_String* boundaryEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, a); + *boundaryEWKB = {.data = NULL, .len = 0}; + if (geom != nullptr) { + auto boundaryGeom = lib->GEOSBoundary_r(handle, geom); + if (boundaryGeom != nullptr) { + auto srid = lib->GEOSGetSRID_r(handle, geom); + CR_GEOS_writeGeomToEWKB(lib, handle, boundaryGeom, boundaryEWKB, srid); + lib->GEOSGeom_destroy_r(handle, boundaryGeom); + } + lib->GEOSGeom_destroy_r(handle, geom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_Centroid(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_String* centroidEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, a); + *centroidEWKB = {.data = NULL, .len = 0}; + if (geom != nullptr) { + auto centroidGeom = lib->GEOSGetCentroid_r(handle, geom); + if (centroidGeom != nullptr) { + auto srid = lib->GEOSGetSRID_r(handle, geom); + CR_GEOS_writeGeomToEWKB(lib, handle, centroidGeom, centroidEWKB, srid); + lib->GEOSGeom_destroy_r(handle, centroidGeom); + } + lib->GEOSGeom_destroy_r(handle, geom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_MinimumBoundingCircle(CR_GEOS* lib, CR_GEOS_Slice a, double* radius, + CR_GEOS_String* centerEWKB, CR_GEOS_String* polygonEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, a); + CR_GEOS_Geometry* centerGeom; + *polygonEWKB = {.data = NULL, .len = 0}; + if (geom != nullptr) { + auto circlePolygonGeom = lib->GEOSMinimumBoundingCircle_r(handle, geom, radius, ¢erGeom); + if (circlePolygonGeom != nullptr) { + auto srid = lib->GEOSGetSRID_r(handle, geom); + CR_GEOS_writeGeomToEWKB(lib, handle, circlePolygonGeom, polygonEWKB, srid); + lib->GEOSGeom_destroy_r(handle, circlePolygonGeom); + } + if (centerGeom != nullptr) { + auto srid = lib->GEOSGetSRID_r(handle, geom); + CR_GEOS_writeGeomToEWKB(lib, handle, centerGeom, centerEWKB, srid); + lib->GEOSGeom_destroy_r(handle, centerGeom); + } + lib->GEOSGeom_destroy_r(handle, geom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_ConvexHull(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_String* convexHullEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, a); + *convexHullEWKB = {.data = NULL, .len = 0}; + if (geom != nullptr) { + auto convexHullGeom = lib->GEOSConvexHull_r(handle, geom); + if (convexHullGeom != nullptr) { + auto srid = lib->GEOSGetSRID_r(handle, geom); + CR_GEOS_writeGeomToEWKB(lib, handle, convexHullGeom, convexHullEWKB, srid); + lib->GEOSGeom_destroy_r(handle, convexHullGeom); + } + lib->GEOSGeom_destroy_r(handle, geom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_Difference(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + CR_GEOS_String* diffEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geomA = CR_GEOS_GeometryFromSlice(lib, handle, a); + auto geomB = CR_GEOS_GeometryFromSlice(lib, handle, b); + *diffEWKB = {.data = NULL, .len = 0}; + if (geomA != nullptr && geomB != nullptr) { + auto diffGeom = lib->GEOSDifference_r(handle, geomA, geomB); + if (diffGeom != nullptr) { + auto srid = lib->GEOSGetSRID_r(handle, geomA); + CR_GEOS_writeGeomToEWKB(lib, handle, diffGeom, diffEWKB, srid); + lib->GEOSGeom_destroy_r(handle, diffGeom); + } + } + if (geomA != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomA); + } + if (geomB != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomB); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_Simplify(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_String* simplifyEWKB, + double tolerance) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, a); + *simplifyEWKB = {.data = NULL, .len = 0}; + if (geom != nullptr) { + auto simplifyGeom = lib->GEOSSimplify_r(handle, geom, tolerance); + if (simplifyGeom != nullptr) { + auto srid = lib->GEOSGetSRID_r(handle, geom); + CR_GEOS_writeGeomToEWKB(lib, handle, simplifyGeom, simplifyEWKB, srid); + lib->GEOSGeom_destroy_r(handle, simplifyGeom); + } + lib->GEOSGeom_destroy_r(handle, geom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_TopologyPreserveSimplify(CR_GEOS* lib, CR_GEOS_Slice a, + CR_GEOS_String* simplifyEWKB, double tolerance) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, a); + *simplifyEWKB = {.data = NULL, .len = 0}; + if (geom != nullptr) { + auto simplifyGeom = lib->GEOSTopologyPreserveSimplify_r(handle, geom, tolerance); + if (simplifyGeom != nullptr) { + auto srid = lib->GEOSGetSRID_r(handle, geom); + CR_GEOS_writeGeomToEWKB(lib, handle, simplifyGeom, simplifyEWKB, srid); + lib->GEOSGeom_destroy_r(handle, simplifyGeom); + } + lib->GEOSGeom_destroy_r(handle, geom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_UnaryUnion(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_String* unionEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + + auto geomA = CR_GEOS_GeometryFromSlice(lib, handle, a); + *unionEWKB = {.data = NULL, .len = 0}; + if (geomA != nullptr) { + auto r = lib->GEOSUnaryUnion_r(handle, geomA); + if (r != NULL) { + auto srid = lib->GEOSGetSRID_r(handle, r); + CR_GEOS_writeGeomToEWKB(lib, handle, r, unionEWKB, srid); + lib->GEOSGeom_destroy_r(handle, r); + } + lib->GEOSGeom_destroy_r(handle, geomA); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_Union(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + CR_GEOS_String* unionEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + *unionEWKB = {.data = NULL, .len = 0}; + + auto geomA = CR_GEOS_GeometryFromSlice(lib, handle, a); + auto geomB = CR_GEOS_GeometryFromSlice(lib, handle, b); + if (geomA != nullptr && geomB != nullptr) { + auto unionGeom = lib->GEOSUnion_r(handle, geomA, geomB); + if (unionGeom != nullptr) { + auto srid = lib->GEOSGetSRID_r(handle, geomA); + CR_GEOS_writeGeomToEWKB(lib, handle, unionGeom, unionEWKB, srid); + lib->GEOSGeom_destroy_r(handle, unionGeom); + } + } + if (geomA != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomA); + } + if (geomB != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomB); + } + + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_PointOnSurface(CR_GEOS* lib, CR_GEOS_Slice a, + CR_GEOS_String* pointOnSurfaceEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, a); + *pointOnSurfaceEWKB = {.data = NULL, .len = 0}; + if (geom != nullptr) { + auto pointOnSurfaceGeom = lib->GEOSPointOnSurface_r(handle, geom); + if (pointOnSurfaceGeom != nullptr) { + auto srid = lib->GEOSGetSRID_r(handle, geom); + CR_GEOS_writeGeomToEWKB(lib, handle, pointOnSurfaceGeom, pointOnSurfaceEWKB, srid); + lib->GEOSGeom_destroy_r(handle, pointOnSurfaceGeom); + } + lib->GEOSGeom_destroy_r(handle, geom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_Intersection(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + CR_GEOS_String* intersectionEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + *intersectionEWKB = {.data = NULL, .len = 0}; + + auto geomA = CR_GEOS_GeometryFromSlice(lib, handle, a); + auto geomB = CR_GEOS_GeometryFromSlice(lib, handle, b); + if (geomA != nullptr && geomB != nullptr) { + auto intersectionGeom = lib->GEOSIntersection_r(handle, geomA, geomB); + if (intersectionGeom != nullptr) { + auto srid = lib->GEOSGetSRID_r(handle, geomA); + CR_GEOS_writeGeomToEWKB(lib, handle, intersectionGeom, intersectionEWKB, srid); + lib->GEOSGeom_destroy_r(handle, intersectionGeom); + } + } + if (geomA != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomA); + } + if (geomB != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomB); + } + + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_SymDifference(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + CR_GEOS_String* symdifferenceEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + *symdifferenceEWKB = {.data = NULL, .len = 0}; + + auto geomA = CR_GEOS_GeometryFromSlice(lib, handle, a); + auto geomB = CR_GEOS_GeometryFromSlice(lib, handle, b); + if (geomA != nullptr && geomB != nullptr) { + auto symdifferenceGeom = lib->GEOSSymDifference_r(handle, geomA, geomB); + if (symdifferenceGeom != nullptr) { + auto srid = lib->GEOSGetSRID_r(handle, geomA); + CR_GEOS_writeGeomToEWKB(lib, handle, symdifferenceGeom, symdifferenceEWKB, srid); + lib->GEOSGeom_destroy_r(handle, symdifferenceGeom); + } + } + if (geomA != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomA); + } + if (geomB != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomB); + } + + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +// +// Linear Reference +// + +CR_GEOS_Status CR_GEOS_Interpolate(CR_GEOS* lib, CR_GEOS_Slice a, double distance, + CR_GEOS_String* interpolatedPointEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, a); + *interpolatedPointEWKB = {.data = NULL, .len = 0}; + if (geom != nullptr) { + auto interpolatedPoint = lib->GEOSInterpolate_r(handle, geom, distance); + if (interpolatedPoint != nullptr) { + auto srid = lib->GEOSGetSRID_r(handle, geom); + CR_GEOS_writeGeomToEWKB(lib, handle, interpolatedPoint, interpolatedPointEWKB, srid); + lib->GEOSGeom_destroy_r(handle, interpolatedPoint); + } + lib->GEOSGeom_destroy_r(handle, geom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +// +// Binary operators +// + +CR_GEOS_Status CR_GEOS_Distance(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, double* ret) { + return CR_GEOS_BinaryOperator(lib, lib->GEOSDistance_r, a, b, ret); +} +CR_GEOS_Status CR_GEOS_FrechetDistance(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + double* ret) { + return CR_GEOS_BinaryOperator(lib, lib->GEOSFrechetDistance_r, a, b, ret); +} + +CR_GEOS_Status CR_GEOS_FrechetDistanceDensify(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + double densifyFrac, double* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + + auto wkbReader = lib->GEOSWKBReader_create_r(handle); + auto geomA = lib->GEOSWKBReader_read_r(handle, wkbReader, a.data, a.len); + auto geomB = lib->GEOSWKBReader_read_r(handle, wkbReader, b.data, b.len); + lib->GEOSWKBReader_destroy_r(handle, wkbReader); + + if (geomA != nullptr && geomB != nullptr) { + auto r = lib->GEOSFrechetDistanceDensify_r(handle, geomA, geomB, densifyFrac, ret); + // ret == 0 indicates an exception. + if (r == 0) { + if (error.length() == 0) { + error.assign(CR_GEOS_NO_ERROR_DEFINED_MESSAGE); + } + } + } + if (geomA != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomA); + } + if (geomB != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomB); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_HausdorffDistance(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + double* ret) { + return CR_GEOS_BinaryOperator(lib, lib->GEOSHausdorffDistance_r, a, b, ret); +} + +CR_GEOS_Status CR_GEOS_HausdorffDistanceDensify(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + double densifyFrac, double* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + + auto wkbReader = lib->GEOSWKBReader_create_r(handle); + auto geomA = lib->GEOSWKBReader_read_r(handle, wkbReader, a.data, a.len); + auto geomB = lib->GEOSWKBReader_read_r(handle, wkbReader, b.data, b.len); + lib->GEOSWKBReader_destroy_r(handle, wkbReader); + + if (geomA != nullptr && geomB != nullptr) { + auto r = lib->GEOSHausdorffDistanceDensify_r(handle, geomA, geomB, densifyFrac, ret); + // ret == 0 indicates an exception. + if (r == 0) { + if (error.length() == 0) { + error.assign(CR_GEOS_NO_ERROR_DEFINED_MESSAGE); + } + } + } + if (geomA != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomA); + } + if (geomB != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomB); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +// +// PreparedGeometry +// + +CR_GEOS_Status CR_GEOS_Prepare(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_PreparedGeometry** ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + + auto wkbReader = lib->GEOSWKBReader_create_r(handle); + auto geom = lib->GEOSWKBReader_read_r(handle, wkbReader, a.data, a.len); + lib->GEOSWKBReader_destroy_r(handle, wkbReader); + if (geom != nullptr) { + auto preparedGeom = lib->GEOSPrepare_r(handle, geom); + auto tmp = new CR_GEOS_PreparedGeometry(); + tmp->g = geom; + tmp->p = preparedGeom; + *ret = tmp; + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_PreparedGeometryDestroy(CR_GEOS* lib, CR_GEOS_PreparedGeometry* a) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + lib->GEOSPreparedGeom_destroy_r(handle, a->p); + lib->GEOSGeom_destroy_r(handle, a->g); + lib->GEOS_finish_r(handle); + delete a; + return toGEOSString(error.data(), error.length()); +} + +template +CR_GEOS_Status CR_GEOS_PreparedBinaryPredicate(CR_GEOS* lib, T fn, CR_GEOS_PreparedInternalGeometry a, CR_GEOS_Slice b, + char* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + + auto wkbReader = lib->GEOSWKBReader_create_r(handle); + auto geomB = lib->GEOSWKBReader_read_r(handle, wkbReader, b.data, b.len); + lib->GEOSWKBReader_destroy_r(handle, wkbReader); + + if (geomB != nullptr) { + auto r = fn(handle, a, geomB); + // ret == 2 indicates an exception. + if (r == 2) { + if (error.length() == 0) { + error.assign(CR_GEOS_NO_ERROR_DEFINED_MESSAGE); + } + } else { + *ret = r; + } + } + if (geomB != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomB); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_PreparedIntersects(CR_GEOS* lib, CR_GEOS_PreparedGeometry* a, CR_GEOS_Slice b, char* ret) { + return CR_GEOS_PreparedBinaryPredicate(lib, lib->GEOSPreparedIntersects_r, a->p, b, ret); +} + +// +// Binary predicates +// + +template +CR_GEOS_Status CR_GEOS_BinaryPredicate(CR_GEOS* lib, T fn, CR_GEOS_Slice a, CR_GEOS_Slice b, + char* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + + auto wkbReader = lib->GEOSWKBReader_create_r(handle); + auto geomA = lib->GEOSWKBReader_read_r(handle, wkbReader, a.data, a.len); + auto geomB = lib->GEOSWKBReader_read_r(handle, wkbReader, b.data, b.len); + lib->GEOSWKBReader_destroy_r(handle, wkbReader); + + if (geomA != nullptr && geomB != nullptr) { + auto r = fn(handle, geomA, geomB); + // ret == 2 indicates an exception. + if (r == 2) { + if (error.length() == 0) { + error.assign(CR_GEOS_NO_ERROR_DEFINED_MESSAGE); + } + } else { + *ret = r; + } + } + if (geomA != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomA); + } + if (geomB != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomB); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_Covers(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret) { + return CR_GEOS_BinaryPredicate(lib, lib->GEOSCovers_r, a, b, ret); +} + +CR_GEOS_Status CR_GEOS_CoveredBy(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret) { + return CR_GEOS_BinaryPredicate(lib, lib->GEOSCoveredBy_r, a, b, ret); +} + +CR_GEOS_Status CR_GEOS_Contains(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret) { + return CR_GEOS_BinaryPredicate(lib, lib->GEOSContains_r, a, b, ret); +} + +CR_GEOS_Status CR_GEOS_Crosses(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret) { + return CR_GEOS_BinaryPredicate(lib, lib->GEOSCrosses_r, a, b, ret); +} + +CR_GEOS_Status CR_GEOS_Disjoint(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret) { + return CR_GEOS_BinaryPredicate(lib, lib->GEOSDisjoint_r, a, b, ret); +} + +CR_GEOS_Status CR_GEOS_Equals(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret) { + return CR_GEOS_BinaryPredicate(lib, lib->GEOSEquals_r, a, b, ret); +} + +CR_GEOS_Status CR_GEOS_Intersects(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret) { + return CR_GEOS_BinaryPredicate(lib, lib->GEOSIntersects_r, a, b, ret); +} + +CR_GEOS_Status CR_GEOS_Overlaps(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret) { + return CR_GEOS_BinaryPredicate(lib, lib->GEOSOverlaps_r, a, b, ret); +} + +CR_GEOS_Status CR_GEOS_PreparedIntersects(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret) { + return CR_GEOS_BinaryPredicate(lib, lib->GEOSPreparedIntersects_r, a, b, ret); +} + +CR_GEOS_Status CR_GEOS_Touches(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret) { + return CR_GEOS_BinaryPredicate(lib, lib->GEOSTouches_r, a, b, ret); +} + +CR_GEOS_Status CR_GEOS_Within(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret) { + return CR_GEOS_BinaryPredicate(lib, lib->GEOSWithin_r, a, b, ret); +} + +// +// DE-9IM related +// See: https://en.wikipedia.org/wiki/DE-9IM. +// + +CR_GEOS_Status CR_GEOS_Relate(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, CR_GEOS_String* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + + auto wkbReader = lib->GEOSWKBReader_create_r(handle); + auto geomA = lib->GEOSWKBReader_read_r(handle, wkbReader, a.data, a.len); + auto geomB = lib->GEOSWKBReader_read_r(handle, wkbReader, b.data, b.len); + lib->GEOSWKBReader_destroy_r(handle, wkbReader); + + if (geomA != nullptr && geomB != nullptr) { + auto r = lib->GEOSRelate_r(handle, geomA, geomB); + if (r != NULL) { + *ret = toGEOSString(r, strlen(r)); + lib->GEOSFree_r(handle, r); + } + } + if (geomA != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomA); + } + if (geomB != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomB); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_RelateBoundaryNodeRule(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + int bnr, CR_GEOS_String* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + + auto wkbReader = lib->GEOSWKBReader_create_r(handle); + auto geomA = lib->GEOSWKBReader_read_r(handle, wkbReader, a.data, a.len); + auto geomB = lib->GEOSWKBReader_read_r(handle, wkbReader, b.data, b.len); + lib->GEOSWKBReader_destroy_r(handle, wkbReader); + + if (geomA != nullptr && geomB != nullptr) { + auto r = lib->GEOSRelateBoundaryNodeRule_r(handle, geomA, geomB, bnr); + if (r != NULL) { + *ret = toGEOSString(r, strlen(r)); + lib->GEOSFree_r(handle, r); + } + } + if (geomA != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomA); + } + if (geomB != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomB); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_RelatePattern(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + CR_GEOS_Slice pattern, char* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + + auto wkbReader = lib->GEOSWKBReader_create_r(handle); + auto geomA = lib->GEOSWKBReader_read_r(handle, wkbReader, a.data, a.len); + auto geomB = lib->GEOSWKBReader_read_r(handle, wkbReader, b.data, b.len); + auto p = std::string(pattern.data, pattern.len); + lib->GEOSWKBReader_destroy_r(handle, wkbReader); + + if (geomA != nullptr && geomB != nullptr) { + auto r = lib->GEOSRelatePattern_r(handle, geomA, geomB, p.c_str()); + // ret == 2 indicates an exception. + if (r == 2) { + if (error.length() == 0) { + error.assign(CR_GEOS_NO_ERROR_DEFINED_MESSAGE); + } + } else { + *ret = r; + } + } + if (geomA != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomA); + } + if (geomB != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomB); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_SharedPaths(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + CR_GEOS_String* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + + auto wkbReader = lib->GEOSWKBReader_create_r(handle); + auto geomA = lib->GEOSWKBReader_read_r(handle, wkbReader, a.data, a.len); + auto geomB = lib->GEOSWKBReader_read_r(handle, wkbReader, b.data, b.len); + lib->GEOSWKBReader_destroy_r(handle, wkbReader); + *ret = {.data = NULL, .len = 0}; + if (geomA != nullptr && geomB != nullptr) { + auto r = lib->GEOSSharedPaths_r(handle, geomA, geomB); + if (r != NULL) { + auto srid = lib->GEOSGetSRID_r(handle, r); + CR_GEOS_writeGeomToEWKB(lib, handle, r, ret, srid); + lib->GEOSGeom_destroy_r(handle, r); + } + } + if (geomA != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomA); + } + if (geomB != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomB); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_Node(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_String* nodeEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + *nodeEWKB = {.data = NULL, .len = 0}; + + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, a); + if (geom != nullptr) { + auto r = lib->GEOSNode_r(handle, geom); + if (r != NULL) { + auto srid = lib->GEOSGetSRID_r(handle, r); + CR_GEOS_writeGeomToEWKB(lib,handle,r,nodeEWKB, srid); + lib->GEOSGeom_destroy_r(handle, r); + } + lib->GEOSGeom_destroy_r(handle, geom); + } + + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_VoronoiDiagram(CR_GEOS* lib, CR_GEOS_Slice g, CR_GEOS_Slice env, + double tolerance, int onlyEdges, CR_GEOS_String* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + + auto geomG = CR_GEOS_GeometryFromSlice(lib, handle, g); + CR_GEOS_Geometry geomEnv = nullptr; + if (env.data != nullptr) { + geomEnv = CR_GEOS_GeometryFromSlice(lib, handle, env); + } + *ret = {.data = NULL, .len = 0}; + + if (geomG != nullptr) { + auto r = lib->GEOSVoronoiDiagram_r(handle, geomG, geomEnv, tolerance, onlyEdges); + if (r != NULL) { + auto srid = lib->GEOSGetSRID_r(handle, r); + CR_GEOS_writeGeomToEWKB(lib, handle, r, ret, srid); + lib->GEOSGeom_destroy_r(handle, r); + } + lib->GEOSGeom_destroy_r(handle, geomG); + } + if (geomEnv != nullptr) { + lib->GEOSGeom_destroy_r(handle, geomEnv); + } + + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_EqualsExact(CR_GEOS* lib, CR_GEOS_Slice lhs, CR_GEOS_Slice rhs, + double tolerance, char* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto lhsGeom = CR_GEOS_GeometryFromSlice(lib, handle, lhs); + auto rhsGeom = CR_GEOS_GeometryFromSlice(lib, handle, rhs); + *ret = 0; + if (lhsGeom != nullptr && rhsGeom != nullptr) { + auto r = lib->GEOSEqualsExact_r(handle, lhsGeom, rhsGeom, tolerance); + *ret = r; + } + if (lhsGeom != nullptr) { + lib->GEOSGeom_destroy_r(handle, lhsGeom); + } + if (rhsGeom != nullptr) { + lib->GEOSGeom_destroy_r(handle, rhsGeom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_MinimumRotatedRectangle(CR_GEOS* lib, CR_GEOS_Slice g, CR_GEOS_String* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto gGeom = CR_GEOS_GeometryFromSlice(lib, handle, g); + *ret = {.data = NULL, .len = 0}; + if (gGeom != nullptr) { + auto r = lib->GEOSMinimumRotatedRectangle_r(handle, gGeom); + if (r != NULL) { + auto srid = lib->GEOSGetSRID_r(handle, r); + CR_GEOS_writeGeomToEWKB(lib, handle, r, ret, srid); + lib->GEOSGeom_destroy_r(handle, r); + } + lib->GEOSGeom_destroy_r(handle, gGeom); + } + + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_Snap(CR_GEOS* lib, CR_GEOS_Slice input, CR_GEOS_Slice target, double tolerance, CR_GEOS_String* snappedEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto gGeomInput = CR_GEOS_GeometryFromSlice(lib, handle, input); + auto gGeomTarget = CR_GEOS_GeometryFromSlice(lib, handle, target); + *snappedEWKB = {.data = NULL, .len = 0}; + if (gGeomInput != nullptr && gGeomTarget != nullptr) { + auto r = lib->GEOSSnap_r(handle, gGeomInput, gGeomTarget, tolerance); + if (r != NULL) { + auto srid = lib->GEOSGetSRID_r(handle, r); + CR_GEOS_writeGeomToEWKB(lib, handle, r, snappedEWKB, srid); + lib->GEOSGeom_destroy_r(handle, r); + } + } + if (gGeomInput != nullptr) { + lib->GEOSGeom_destroy_r(handle, gGeomInput); + } + if (gGeomTarget != nullptr) { + lib->GEOSGeom_destroy_r(handle, gGeomTarget); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} diff --git a/pkg/geo/geos/geos.go b/pkg/geo/geos/geos.go new file mode 100644 index 0000000..d1b3076 --- /dev/null +++ b/pkg/geo/geos/geos.go @@ -0,0 +1,1125 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +// Package geos is a wrapper around the spatial data types between the geo +// package and the GEOS C library. The GEOS library is dynamically loaded +// at init time. +// Operations will error if the GEOS library was not found. +package geos + +import ( + "fmt" + "os" + "path" + "path/filepath" + "runtime" + "sync" + "unsafe" + + "github.com/cockroachdb/cockroachdb-parser/pkg/build/bazel" + "github.com/cockroachdb/cockroachdb-parser/pkg/docs" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/errors" +) + +// #cgo CXXFLAGS: -std=c++14 +// #cgo !windows LDFLAGS: -ldl -lm +// +// #include "geos.h" +import "C" + +// EnsureInitErrorDisplay is used to control the error message displayed by +// EnsureInit. +type EnsureInitErrorDisplay int + +const ( + // EnsureInitErrorDisplayPrivate displays the full error message, including + // path info. It is intended for log messages. + EnsureInitErrorDisplayPrivate EnsureInitErrorDisplay = iota + // EnsureInitErrorDisplayPublic displays a redacted error message, excluding + // path info. It is intended for errors to display for the client. + EnsureInitErrorDisplayPublic +) + +// maxArrayLen is the maximum safe length for this architecture. +const maxArrayLen = 1<<31 - 1 + +// geosOnce contains the global instance of CR_GEOS, to be initialized +// during at a maximum of once. +// If it has failed to open, the error will be populated in "err". +// This should only be touched by "fetchGEOSOrError". +var geosOnce struct { + geos *C.CR_GEOS + loc string + err error + once sync.Once +} + +// PreparedGeometry is an instance of a GEOS PreparedGeometry. +type PreparedGeometry *C.CR_GEOS_PreparedGeometry + +// EnsureInit attempts to start GEOS if it has not been opened already +// and returns the location if found, and an error if the CR_GEOS is not valid. +func EnsureInit( + errDisplay EnsureInitErrorDisplay, flagLibraryDirectoryValue string, +) (string, error) { + crdbBinaryLoc := "" + if len(os.Args) > 0 { + crdbBinaryLoc = os.Args[0] + } + _, err := ensureInit(errDisplay, flagLibraryDirectoryValue, crdbBinaryLoc) + return geosOnce.loc, err +} + +// ensureInitInternal ensures initialization has been done, always displaying +// errors privately and not assuming a flag has been set if initialized +// for the first time. +func ensureInitInternal() (*C.CR_GEOS, error) { + return ensureInit(EnsureInitErrorDisplayPrivate, "", "") +} + +// ensureInits behaves as described in EnsureInit, but also returns the GEOS +// C object which should be hidden from the public eye. +func ensureInit( + errDisplay EnsureInitErrorDisplay, flagLibraryDirectoryValue string, crdbBinaryLoc string, +) (*C.CR_GEOS, error) { + geosOnce.once.Do(func() { + geosOnce.geos, geosOnce.loc, geosOnce.err = initGEOS( + findLibraryDirectories(flagLibraryDirectoryValue, crdbBinaryLoc), + ) + }) + if geosOnce.err != nil && errDisplay == EnsureInitErrorDisplayPublic { + return nil, pgerror.Newf(pgcode.System, "geos: this operation is not available") + } + return geosOnce.geos, geosOnce.err +} + +// appendLibraryExt appends the extension expected for the running OS. +func getLibraryExt(base string) string { + switch runtime.GOOS { + case "darwin": + return base + ".dylib" + case "windows": + return base + ".dll" + default: + return base + ".so" + } +} + +const ( + libgeosFileName = "libgeos" + libgeoscFileName = "libgeos_c" +) + +// findLibraryDirectories returns the default locations where GEOS is installed. +func findLibraryDirectories(flagLibraryDirectoryValue string, crdbBinaryLoc string) []string { + // Try path by trying to find all parenting paths and appending + // `lib/libgeos_c.` to the current working directory, as well + // as the directory in which the cockroach binary is initialized. + locs := []string{} + if flagLibraryDirectoryValue != "" { + locs = append(locs, flagLibraryDirectoryValue) + } + // Account for the libraries to be in a bazel runfile path. + if bazel.BuiltWithBazel() { + pathsToCheck := []string{ + path.Join("c-deps", "libgeos_foreign", "lib"), + path.Join("external", "archived_cdep_libgeos_linux", "lib"), + path.Join("external", "archived_cdep_libgeos_linuxarm", "lib"), + path.Join("external", "archived_cdep_libgeos_macos", "lib"), + path.Join("external", "archived_cdep_libgeos_macosarm", "lib"), + path.Join("external", "archived_cdep_libgeos_windows", "bin"), + } + for _, path := range pathsToCheck { + if p, err := bazel.Runfile(path); err == nil { + locs = append(locs, p) + } + } + } + locs = append( + locs, + findLibraryDirectoriesInParentingDirectories(crdbBinaryLoc)..., + ) + cwd, err := os.Getwd() + if err != nil { + fmt.Fprintf(os.Stderr, "warning: cannot retrieve cwd: %v", err) + } else { + locs = append( + locs, + findLibraryDirectoriesInParentingDirectories(cwd)..., + ) + } + return locs +} + +// findLibraryDirectoriesInParentingDirectories attempts to find GEOS by looking at +// parenting folders and looking inside `lib/libgeos_c.*`. +// This is basically only useful for CI runs. +func findLibraryDirectoriesInParentingDirectories(dir string) []string { + locs := []string{} + + for { + checkDir := filepath.Join(dir, "lib") + found := true + for _, file := range []string{ + filepath.Join(checkDir, getLibraryExt(libgeoscFileName)), + filepath.Join(checkDir, getLibraryExt(libgeosFileName)), + } { + if _, err := os.Stat(file); err != nil { + found = false + break + } + } + if found { + locs = append(locs, checkDir) + } + parentDir := filepath.Dir(dir) + if parentDir == dir { + break + } + dir = parentDir + } + return locs +} + +// initGEOS initializes the CR_GEOS by attempting to dlopen all +// the paths as parsed in by locs. +func initGEOS(dirs []string) (*C.CR_GEOS, string, error) { + var err error + for _, dir := range dirs { + var ret *C.CR_GEOS + newErr := statusToError( + C.CR_GEOS_Init( + goToCSlice([]byte(filepath.Join(dir, getLibraryExt(libgeoscFileName)))), + goToCSlice([]byte(filepath.Join(dir, getLibraryExt(libgeosFileName)))), + &ret, + ), + ) + if newErr == nil { + return ret, dir, nil + } + err = errors.CombineErrors( + err, + errors.Wrapf( + newErr, + "geos: cannot load GEOS from dir %q", + dir, + ), + ) + } + if err != nil { + return nil, "", wrapGEOSInitError(errors.Wrap(err, "geos: error during GEOS init")) + } + return nil, "", wrapGEOSInitError(errors.Newf("geos: no locations to init GEOS")) +} + +func wrapGEOSInitError(err error) error { + page := "linux" + switch runtime.GOOS { + case "darwin": + page = "mac" + case "windows": + page = "windows" + } + return pgerror.WithCandidateCode( + errors.WithHintf( + err, + "Ensure you have the spatial libraries installed as per the instructions in %s", + docs.URL("install-cockroachdb-"+page), + ), + pgcode.ConfigFile, + ) +} + +// goToCSlice returns a CR_GEOS_Slice from a given Go byte slice. +func goToCSlice(b []byte) C.CR_GEOS_Slice { + if len(b) == 0 { + return C.CR_GEOS_Slice{data: nil, len: 0} + } + return C.CR_GEOS_Slice{ + data: (*C.char)(unsafe.Pointer(&b[0])), + len: C.size_t(len(b)), + } +} + +// cStringToUnsafeGoBytes convert a CR_GEOS_String to a Go +// byte slice that refer to the underlying C memory. +func cStringToUnsafeGoBytes(s C.CR_GEOS_String) []byte { + return cToUnsafeGoBytes(s.data, s.len) +} + +func cToUnsafeGoBytes(data *C.char, len C.size_t) []byte { + if data == nil { + return nil + } + // Interpret the C pointer as a pointer to a Go array, then slice. + return (*[maxArrayLen]byte)(unsafe.Pointer(data))[:len:len] +} + +// cStringToSafeGoBytes converts a CR_GEOS_String to a Go byte slice. +// Additionally, it frees the C memory. +func cStringToSafeGoBytes(s C.CR_GEOS_String) []byte { + unsafeBytes := cStringToUnsafeGoBytes(s) + b := make([]byte, len(unsafeBytes)) + copy(b, unsafeBytes) + C.free(unsafe.Pointer(s.data)) + return b +} + +// A Error wraps an error returned from a GEOS operation. +type Error struct { + msg string +} + +// Error implements the error interface. +func (err *Error) Error() string { + return err.msg +} + +func statusToError(s C.CR_GEOS_Status) error { + if s.data == nil { + return nil + } + return &Error{msg: string(cStringToSafeGoBytes(s))} +} + +// Version returns the GEOS version used. +func Version() (string, error) { + g, err := ensureInitInternal() + if err != nil { + return "", err + } + var version C.CR_GEOS_String + C.CR_GEOS_Version(g, &version) + // Returns a `const char*`, so we don't have to free anything. + return string(cStringToUnsafeGoBytes(version)), nil +} + +// BufferParamsJoinStyle maps to the GEOSBufJoinStyles enum in geos_c.h.in. +type BufferParamsJoinStyle int + +// These should be kept in sync with the geos_c.h.in corresponding enum definition. +const ( + BufferParamsJoinStyleRound = 1 + BufferParamsJoinStyleMitre = 2 + BufferParamsJoinStyleBevel = 3 +) + +// BufferParamsEndCapStyle maps to the GEOSBufCapStyles enum in geos_c.h.in. +type BufferParamsEndCapStyle int + +// These should be kept in sync with the geos_c.h.in corresponding enum definition. +const ( + BufferParamsEndCapStyleRound = 1 + BufferParamsEndCapStyleFlat = 2 + BufferParamsEndCapStyleSquare = 3 +) + +// BufferParams are parameters to provide into the GEOS buffer function. +type BufferParams struct { + JoinStyle BufferParamsJoinStyle + EndCapStyle BufferParamsEndCapStyle + SingleSided bool + QuadrantSegments int + MitreLimit float64 +} + +// Buffer buffers the given geometry by the given distance and params. +func Buffer(ewkb geopb.EWKB, params BufferParams, distance float64) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + singleSided := 0 + if params.SingleSided { + singleSided = 1 + } + cParams := C.CR_GEOS_BufferParamsInput{ + endCapStyle: C.int(params.EndCapStyle), + joinStyle: C.int(params.JoinStyle), + singleSided: C.int(singleSided), + quadrantSegments: C.int(params.QuadrantSegments), + mitreLimit: C.double(params.MitreLimit), + } + var cEWKB C.CR_GEOS_String + if err := statusToError(C.CR_GEOS_Buffer(g, goToCSlice(ewkb), cParams, C.double(distance), &cEWKB)); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} + +// Area returns the area of an EWKB. +func Area(ewkb geopb.EWKB) (float64, error) { + g, err := ensureInitInternal() + if err != nil { + return 0, err + } + var area C.double + if err := statusToError(C.CR_GEOS_Area(g, goToCSlice(ewkb), &area)); err != nil { + return 0, err + } + return float64(area), nil +} + +// Boundary returns the boundary of an EWKB. +func Boundary(ewkb geopb.EWKB) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + if err := statusToError(C.CR_GEOS_Boundary(g, goToCSlice(ewkb), &cEWKB)); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} + +// Difference returns the difference between two EWKB. +func Difference(ewkb1 geopb.EWKB, ewkb2 geopb.EWKB) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var diffEWKB C.CR_GEOS_String + if err := statusToError(C.CR_GEOS_Difference(g, goToCSlice(ewkb1), goToCSlice(ewkb2), &diffEWKB)); err != nil { + return nil, err + } + return cStringToSafeGoBytes(diffEWKB), nil +} + +// Length returns the length of an EWKB. +func Length(ewkb geopb.EWKB) (float64, error) { + g, err := ensureInitInternal() + if err != nil { + return 0, err + } + var length C.double + if err := statusToError(C.CR_GEOS_Length(g, goToCSlice(ewkb), &length)); err != nil { + return 0, err + } + return float64(length), nil +} + +// Normalize returns the geometry in its normalized form. +func Normalize(a geopb.EWKB) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + if err := statusToError(C.CR_GEOS_Normalize(g, goToCSlice(a), &cEWKB)); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} + +// LineMerge merges multilinestring constituents. +func LineMerge(a geopb.EWKB) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + if err := statusToError(C.CR_GEOS_LineMerge(g, goToCSlice(a), &cEWKB)); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} + +// IsSimple returns whether the EWKB is simple. +func IsSimple(ewkb geopb.EWKB) (bool, error) { + g, err := ensureInitInternal() + if err != nil { + return false, err + } + var ret C.char + if err := statusToError(C.CR_GEOS_IsSimple(g, goToCSlice(ewkb), &ret)); err != nil { + return false, err + } + return ret == 1, nil +} + +// Centroid returns the centroid of an EWKB. +func Centroid(ewkb geopb.EWKB) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + if err := statusToError(C.CR_GEOS_Centroid(g, goToCSlice(ewkb), &cEWKB)); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} + +// MinimumBoundingCircle returns minimum bounding circle of an EWKB +func MinimumBoundingCircle(ewkb geopb.EWKB) (geopb.EWKB, geopb.EWKB, float64, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, nil, 0, err + } + var centerEWKB C.CR_GEOS_String + var polygonEWKB C.CR_GEOS_String + var radius C.double + + if err := statusToError(C.CR_GEOS_MinimumBoundingCircle(g, goToCSlice(ewkb), &radius, ¢erEWKB, &polygonEWKB)); err != nil { + return nil, nil, 0, err + } + return cStringToSafeGoBytes(polygonEWKB), cStringToSafeGoBytes(centerEWKB), float64(radius), nil + +} + +// ConvexHull returns an EWKB which returns the convex hull of the given EWKB. +func ConvexHull(ewkb geopb.EWKB) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + if err := statusToError(C.CR_GEOS_ConvexHull(g, goToCSlice(ewkb), &cEWKB)); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} + +// Simplify returns an EWKB which returns the simplified EWKB. +func Simplify(ewkb geopb.EWKB, tolerance float64) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + if err := statusToError( + C.CR_GEOS_Simplify(g, goToCSlice(ewkb), &cEWKB, C.double(tolerance)), + ); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} + +// TopologyPreserveSimplify returns an EWKB which returns the simplified EWKB +// with the topology preserved. +func TopologyPreserveSimplify(ewkb geopb.EWKB, tolerance float64) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + if err := statusToError( + C.CR_GEOS_TopologyPreserveSimplify(g, goToCSlice(ewkb), &cEWKB, C.double(tolerance)), + ); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} + +// PointOnSurface returns an EWKB with a point that is on the surface of the given EWKB. +func PointOnSurface(ewkb geopb.EWKB) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + if err := statusToError(C.CR_GEOS_PointOnSurface(g, goToCSlice(ewkb), &cEWKB)); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} + +// Intersection returns an EWKB which contains the geometries of intersection between A and B. +func Intersection(a geopb.EWKB, b geopb.EWKB) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + if err := statusToError(C.CR_GEOS_Intersection(g, goToCSlice(a), goToCSlice(b), &cEWKB)); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} + +// UnaryUnion Returns an EWKB which is a union of input geometry components. +func UnaryUnion(a geopb.EWKB) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var unionEWKB C.CR_GEOS_String + if err := statusToError(C.CR_GEOS_UnaryUnion(g, goToCSlice(a), &unionEWKB)); err != nil { + return nil, err + } + return cStringToSafeGoBytes(unionEWKB), nil +} + +// Union returns an EWKB which is a union of shapes A and B. +func Union(a geopb.EWKB, b geopb.EWKB) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + if err := statusToError(C.CR_GEOS_Union(g, goToCSlice(a), goToCSlice(b), &cEWKB)); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} + +// SymDifference returns an EWKB which is the symmetric difference of shapes A and B. +func SymDifference(a geopb.EWKB, b geopb.EWKB) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + if err := statusToError(C.CR_GEOS_SymDifference(g, goToCSlice(a), goToCSlice(b), &cEWKB)); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} + +// InterpolateLine returns the point along the given LineString which is at +// a given distance from starting point. +// Note: For distance less than 0 it returns start point similarly for distance +// greater LineString's length. +// InterpolateLine also works with (Multi)LineString. However, the result is +// not appropriate as it combines all the LineString present in (MULTI)LineString, +// considering all the corner points of LineString overlaps each other. +func InterpolateLine(ewkb geopb.EWKB, distance float64) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + if err := statusToError(C.CR_GEOS_Interpolate(g, goToCSlice(ewkb), C.double(distance), &cEWKB)); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} + +// MinDistance returns the minimum distance between two EWKBs. +func MinDistance(a geopb.EWKB, b geopb.EWKB) (float64, error) { + g, err := ensureInitInternal() + if err != nil { + return 0, err + } + var distance C.double + if err := statusToError(C.CR_GEOS_Distance(g, goToCSlice(a), goToCSlice(b), &distance)); err != nil { + return 0, err + } + return float64(distance), nil +} + +// MinimumClearance returns the minimum distance a vertex can move to result in an +// invalid geometry. +func MinimumClearance(ewkb geopb.EWKB) (float64, error) { + g, err := ensureInitInternal() + if err != nil { + return 0, err + } + var distance C.double + if err := statusToError(C.CR_GEOS_MinimumClearance(g, goToCSlice(ewkb), &distance)); err != nil { + return 0, err + } + return float64(distance), nil +} + +// MinimumClearanceLine returns the line spanning the minimum clearance a vertex can +// move before producing an invalid geometry. +func MinimumClearanceLine(ewkb geopb.EWKB) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var clearanceEWKB C.CR_GEOS_String + if err := statusToError(C.CR_GEOS_MinimumClearanceLine(g, goToCSlice(ewkb), &clearanceEWKB)); err != nil { + return nil, err + } + return cStringToSafeGoBytes(clearanceEWKB), nil +} + +// ClipByRect clips a EWKB to the specified rectangle. +func ClipByRect( + ewkb geopb.EWKB, xMin float64, yMin float64, xMax float64, yMax float64, +) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + if err := statusToError(C.CR_GEOS_ClipByRect(g, goToCSlice(ewkb), C.double(xMin), + C.double(yMin), C.double(xMax), C.double(yMax), &cEWKB)); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} + +// +// PreparedGeometry +// + +// PrepareGeometry prepares a geometry in GEOS. +func PrepareGeometry(a geopb.EWKB) (PreparedGeometry, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var ret *C.CR_GEOS_PreparedGeometry + if err := statusToError(C.CR_GEOS_Prepare(g, goToCSlice(a), &ret)); err != nil { + return nil, err + } + return PreparedGeometry(ret), nil +} + +// PreparedGeomDestroy destroys a prepared geometry. +func PreparedGeomDestroy(a PreparedGeometry) { + g, err := ensureInitInternal() + if err != nil { + panic(errors.NewAssertionErrorWithWrappedErrf(err, "trying to destroy PreparedGeometry with no GEOS")) + } + ap := (*C.CR_GEOS_PreparedGeometry)(unsafe.Pointer(a)) + if err := statusToError(C.CR_GEOS_PreparedGeometryDestroy(g, ap)); err != nil { + panic(errors.NewAssertionErrorWithWrappedErrf(err, "PreparedGeometryDestroy returned an error")) + } +} + +// +// Binary predicates. +// + +// Covers returns whether the EWKB provided by A covers the EWKB provided by B. +func Covers(a geopb.EWKB, b geopb.EWKB) (bool, error) { + g, err := ensureInitInternal() + if err != nil { + return false, err + } + var ret C.char + if err := statusToError(C.CR_GEOS_Covers(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { + return false, err + } + return ret == 1, nil +} + +// CoveredBy returns whether the EWKB provided by A is covered by the EWKB provided by B. +func CoveredBy(a geopb.EWKB, b geopb.EWKB) (bool, error) { + g, err := ensureInitInternal() + if err != nil { + return false, err + } + var ret C.char + if err := statusToError(C.CR_GEOS_CoveredBy(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { + return false, err + } + return ret == 1, nil +} + +// Contains returns whether the EWKB provided by A contains the EWKB provided by B. +func Contains(a geopb.EWKB, b geopb.EWKB) (bool, error) { + g, err := ensureInitInternal() + if err != nil { + return false, err + } + var ret C.char + if err := statusToError(C.CR_GEOS_Contains(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { + return false, err + } + return ret == 1, nil +} + +// Crosses returns whether the EWKB provided by A crosses the EWKB provided by B. +func Crosses(a geopb.EWKB, b geopb.EWKB) (bool, error) { + g, err := ensureInitInternal() + if err != nil { + return false, err + } + var ret C.char + if err := statusToError(C.CR_GEOS_Crosses(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { + return false, err + } + return ret == 1, nil +} + +// Disjoint returns whether the EWKB provided by A is disjoint from the EWKB provided by B. +func Disjoint(a geopb.EWKB, b geopb.EWKB) (bool, error) { + g, err := ensureInitInternal() + if err != nil { + return false, err + } + var ret C.char + if err := statusToError(C.CR_GEOS_Disjoint(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { + return false, err + } + return ret == 1, nil +} + +// Equals returns whether the EWKB provided by A equals the EWKB provided by B. +func Equals(a geopb.EWKB, b geopb.EWKB) (bool, error) { + g, err := ensureInitInternal() + if err != nil { + return false, err + } + var ret C.char + if err := statusToError(C.CR_GEOS_Equals(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { + return false, err + } + return ret == 1, nil +} + +// PreparedIntersects returns whether the EWKB provided by A intersects the EWKB provided by B. +func PreparedIntersects(a PreparedGeometry, b geopb.EWKB) (bool, error) { + // Double check - since PreparedGeometry is actually a pointer to C type. + if a == nil { + return false, errors.New("provided PreparedGeometry is nil") + } + g, err := ensureInitInternal() + if err != nil { + return false, err + } + var ret C.char + ap := (*C.CR_GEOS_PreparedGeometry)(unsafe.Pointer(a)) + if err := statusToError(C.CR_GEOS_PreparedIntersects(g, ap, goToCSlice(b), &ret)); err != nil { + return false, err + } + return ret == 1, nil +} + +// Intersects returns whether the EWKB provided by A intersects the EWKB provided by B. +func Intersects(a geopb.EWKB, b geopb.EWKB) (bool, error) { + g, err := ensureInitInternal() + if err != nil { + return false, err + } + var ret C.char + if err := statusToError(C.CR_GEOS_Intersects(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { + return false, err + } + return ret == 1, nil +} + +// Overlaps returns whether the EWKB provided by A overlaps the EWKB provided by B. +func Overlaps(a geopb.EWKB, b geopb.EWKB) (bool, error) { + g, err := ensureInitInternal() + if err != nil { + return false, err + } + var ret C.char + if err := statusToError(C.CR_GEOS_Overlaps(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { + return false, err + } + return ret == 1, nil +} + +// Touches returns whether the EWKB provided by A touches the EWKB provided by B. +func Touches(a geopb.EWKB, b geopb.EWKB) (bool, error) { + g, err := ensureInitInternal() + if err != nil { + return false, err + } + var ret C.char + if err := statusToError(C.CR_GEOS_Touches(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { + return false, err + } + return ret == 1, nil +} + +// Within returns whether the EWKB provided by A is within the EWKB provided by B. +func Within(a geopb.EWKB, b geopb.EWKB) (bool, error) { + g, err := ensureInitInternal() + if err != nil { + return false, err + } + var ret C.char + if err := statusToError(C.CR_GEOS_Within(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { + return false, err + } + return ret == 1, nil +} + +// FrechetDistance returns the Frechet distance between the geometries. +func FrechetDistance(a, b geopb.EWKB) (float64, error) { + g, err := ensureInitInternal() + if err != nil { + return 0, err + } + var distance C.double + if err := statusToError( + C.CR_GEOS_FrechetDistance(g, goToCSlice(a), goToCSlice(b), &distance), + ); err != nil { + return 0, err + } + return float64(distance), nil +} + +// FrechetDistanceDensify returns the Frechet distance between the geometries. +func FrechetDistanceDensify(a, b geopb.EWKB, densifyFrac float64) (float64, error) { + g, err := ensureInitInternal() + if err != nil { + return 0, err + } + var distance C.double + if err := statusToError( + C.CR_GEOS_FrechetDistanceDensify(g, goToCSlice(a), goToCSlice(b), C.double(densifyFrac), &distance), + ); err != nil { + return 0, err + } + return float64(distance), nil +} + +// HausdorffDistance returns the Hausdorff distance between the geometries. +func HausdorffDistance(a, b geopb.EWKB) (float64, error) { + g, err := ensureInitInternal() + if err != nil { + return 0, err + } + var distance C.double + if err := statusToError( + C.CR_GEOS_HausdorffDistance(g, goToCSlice(a), goToCSlice(b), &distance), + ); err != nil { + return 0, err + } + return float64(distance), nil +} + +// HausdorffDistanceDensify returns the Hausdorff distance between the geometries. +func HausdorffDistanceDensify(a, b geopb.EWKB, densifyFrac float64) (float64, error) { + g, err := ensureInitInternal() + if err != nil { + return 0, err + } + var distance C.double + if err := statusToError( + C.CR_GEOS_HausdorffDistanceDensify(g, goToCSlice(a), goToCSlice(b), C.double(densifyFrac), &distance), + ); err != nil { + return 0, err + } + return float64(distance), nil +} + +// EqualsExact returns whether two geometry objects are equal with some epsilon +func EqualsExact(lhs, rhs geopb.EWKB, epsilon float64) (bool, error) { + g, err := ensureInitInternal() + if err != nil { + return false, err + } + var ret C.char + if err := statusToError( + C.CR_GEOS_EqualsExact(g, goToCSlice(lhs), goToCSlice(rhs), C.double(epsilon), &ret), + ); err != nil { + return false, err + } + return ret == 1, nil +} + +// +// DE-9IM related +// + +// Relate returns the DE-9IM relation between A and B. +func Relate(a geopb.EWKB, b geopb.EWKB) (string, error) { + g, err := ensureInitInternal() + if err != nil { + return "", err + } + var ret C.CR_GEOS_String + if err := statusToError(C.CR_GEOS_Relate(g, goToCSlice(a), goToCSlice(b), &ret)); err != nil { + return "", err + } + if ret.data == nil { + return "", errors.Newf("expected DE-9IM string but found nothing") + } + return string(cStringToSafeGoBytes(ret)), nil +} + +// RelateBoundaryNodeRule returns the DE-9IM relation between A and B given a boundary node rule. +func RelateBoundaryNodeRule(a geopb.EWKB, b geopb.EWKB, bnr int) (string, error) { + g, err := ensureInitInternal() + if err != nil { + return "", err + } + var ret C.CR_GEOS_String + if err := statusToError(C.CR_GEOS_RelateBoundaryNodeRule(g, goToCSlice(a), goToCSlice(b), C.int(bnr), &ret)); err != nil { + return "", err + } + if ret.data == nil { + return "", errors.Newf("expected DE-9IM string but found nothing") + } + return string(cStringToSafeGoBytes(ret)), nil +} + +// RelatePattern whether A and B have a DE-9IM relation matching the given pattern. +func RelatePattern(a geopb.EWKB, b geopb.EWKB, pattern string) (bool, error) { + g, err := ensureInitInternal() + if err != nil { + return false, err + } + var ret C.char + if err := statusToError( + C.CR_GEOS_RelatePattern(g, goToCSlice(a), goToCSlice(b), goToCSlice([]byte(pattern)), &ret), + ); err != nil { + return false, err + } + return ret == 1, nil +} + +// +// Validity checking. +// + +// IsValid returns whether the given geometry is valid. +func IsValid(ewkb geopb.EWKB) (bool, error) { + g, err := ensureInitInternal() + if err != nil { + return false, err + } + var ret C.char + if err := statusToError( + C.CR_GEOS_IsValid(g, goToCSlice(ewkb), &ret), + ); err != nil { + return false, err + } + return ret == 1, nil +} + +// IsValidReason the reasoning for whether the Geometry is valid or invalid. +func IsValidReason(ewkb geopb.EWKB) (string, error) { + g, err := ensureInitInternal() + if err != nil { + return "", err + } + var ret C.CR_GEOS_String + if err := statusToError( + C.CR_GEOS_IsValidReason(g, goToCSlice(ewkb), &ret), + ); err != nil { + return "", err + } + + return string(cStringToSafeGoBytes(ret)), nil +} + +// IsValidDetail returns information regarding whether a geometry is valid or invalid. +// It takes in a flag parameter which behaves the same as the GEOS module, where 1 +// means that self-intersecting rings forming holes are considered valid. +// It returns a bool representing whether it is valid, a string giving a reason for +// invalidity and an EWKB representing the location things are invalid at. +func IsValidDetail(ewkb geopb.EWKB, flags int) (bool, string, geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return false, "", nil, err + } + var retIsValid C.char + var retReason C.CR_GEOS_String + var retLocationEWKB C.CR_GEOS_String + if err := statusToError( + C.CR_GEOS_IsValidDetail( + g, + goToCSlice(ewkb), + C.int(flags), + &retIsValid, + &retReason, + &retLocationEWKB, + ), + ); err != nil { + return false, "", nil, err + } + return retIsValid == 1, + string(cStringToSafeGoBytes(retReason)), + cStringToSafeGoBytes(retLocationEWKB), + nil +} + +// MakeValid returns a valid form of the EWKB. +func MakeValid(ewkb geopb.EWKB) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + if err := statusToError(C.CR_GEOS_MakeValid(g, goToCSlice(ewkb), &cEWKB)); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} + +// SharedPaths Returns a EWKB containing paths shared by the two given EWKBs. +func SharedPaths(a geopb.EWKB, b geopb.EWKB) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + if err := statusToError( + C.CR_GEOS_SharedPaths(g, goToCSlice(a), goToCSlice(b), &cEWKB), + ); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} + +// Node returns a EWKB containing a set of linestrings using the least possible number of nodes while preserving all of the input ones. +func Node(a geopb.EWKB) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + err = statusToError(C.CR_GEOS_Node(g, goToCSlice(a), &cEWKB)) + if err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} + +// VoronoiDiagram Computes the Voronoi Diagram from the vertices of the supplied EWKBs. +func VoronoiDiagram(a, env geopb.EWKB, tolerance float64, onlyEdges bool) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + flag := 0 + if onlyEdges { + flag = 1 + } + if err := statusToError( + C.CR_GEOS_VoronoiDiagram(g, goToCSlice(a), goToCSlice(env), C.double(tolerance), C.int(flag), &cEWKB), + ); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} + +// MinimumRotatedRectangle Returns a minimum rotated rectangle enclosing a geometry +func MinimumRotatedRectangle(ewkb geopb.EWKB) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + if err := statusToError( + C.CR_GEOS_MinimumRotatedRectangle(g, goToCSlice(ewkb), &cEWKB), + ); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} + +// Snap returns the input EWKB with the vertices snapped to the target +// EWKB. Tolerance is used to control where snapping is performed. +// If no snapping occurs then the input geometry is returned unchanged. +func Snap(input, target geopb.EWKB, tolerance float64) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + if err := statusToError( + C.CR_GEOS_Snap(g, goToCSlice(input), goToCSlice(target), C.double(tolerance), &cEWKB), + ); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} diff --git a/pkg/geo/geos/geos.h b/pkg/geo/geos/geos.h new file mode 100644 index 0000000..8cf9a2e --- /dev/null +++ b/pkg/geo/geos/geos.h @@ -0,0 +1,195 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Data Types adapted from `capi/geos_c.h.in` in GEOS. +typedef void* CR_GEOS_Geometry; +typedef void* CR_GEOS_PreparedInternalGeometry; + +// NB: Both CR_GEOS_Slice and CR_GEOS_String can contain non-printable +// data, so neither is necessarily compatible with a NUL character +// terminated C string. Functions that need data that does not contain +// the NUL character, so that they can convert to a NUL terminated C +// string, must document that additional constraint in their interface. + +// CR_GEOS_Slice contains data that does not need to be freed. It +// can be either a Go or C pointer (which indicates who allocated the +// memory). +typedef struct { + char* data; + size_t len; +} CR_GEOS_Slice; + +// CR_GEOS_String contains a C pointer that needs to be freed. +typedef struct { + char* data; + size_t len; +} CR_GEOS_String; + +// CR_GEOS_PreparedGeometry is a wrapper containing GEOS PreparedGeometry and it's source Geometry. +// This allows us to free the memory for both at the same time. +typedef struct { + CR_GEOS_Geometry g; + CR_GEOS_PreparedInternalGeometry p; +} CR_GEOS_PreparedGeometry; + +// CR_GEOS_BufferParams are parameters that will be passed to buffer. +typedef struct { + int endCapStyle; + int joinStyle; + int singleSided; + int quadrantSegments; + double mitreLimit; +} CR_GEOS_BufferParamsInput; + +typedef CR_GEOS_String CR_GEOS_Status; + +// CR_GEOS contains all the functions loaded by GEOS. +typedef struct CR_GEOS CR_GEOS; + +// CR_GEOS_Init initializes the provided GEOSLib with GEOS using dlopen/dlsym. +// Returns a string containing an error if an error was found. The loc slice +// must be convertible to a NUL character terminated C string. +// The CR_GEOS object will be stored in lib. +CR_GEOS_Status CR_GEOS_Init(CR_GEOS_Slice geoscLoc, CR_GEOS_Slice geosLoc, CR_GEOS** lib); + +// CR_GEOS_WKTToWKB converts a given WKT into it's EWKB form. The wkt slice must be +// convertible to a NUL character terminated C string. +CR_GEOS_Status CR_GEOS_WKTToEWKB(CR_GEOS* lib, CR_GEOS_Slice wkt, int srid, CR_GEOS_String* ewkb); + +// CR_GEOS_ClipByRect clips a given EWKB by the given rectangle. +CR_GEOS_Status CR_GEOS_ClipByRect(CR_GEOS* lib, CR_GEOS_Slice ewkb, double xmin, double ymin, + double xmax, double ymax, CR_GEOS_String* clippedEWKB); +// CR_GEOS_Buffer buffers a given EWKB by the given distance and params. +CR_GEOS_Status CR_GEOS_Buffer(CR_GEOS* lib, CR_GEOS_Slice ewkb, CR_GEOS_BufferParamsInput params, + double distance, CR_GEOS_String* ret); +void CR_GEOS_Version(CR_GEOS* lib, CR_GEOS_String* ret); + +// +// Validity checking. +// + +CR_GEOS_Status CR_GEOS_IsValid(CR_GEOS* lib, CR_GEOS_Slice g, char* ret); +CR_GEOS_Status CR_GEOS_IsValidReason(CR_GEOS* lib, CR_GEOS_Slice g, CR_GEOS_String* ret); +CR_GEOS_Status CR_GEOS_IsValidDetail(CR_GEOS* lib, CR_GEOS_Slice g, int flags, char* retIsValid, + CR_GEOS_String* retReason, CR_GEOS_String* retLocationEWKB); +CR_GEOS_Status CR_GEOS_MakeValid(CR_GEOS* lib, CR_GEOS_Slice g, CR_GEOS_String* validEWKB); + +// +// Unary operators. +// + +CR_GEOS_Status CR_GEOS_Area(CR_GEOS* lib, CR_GEOS_Slice a, double* ret); +CR_GEOS_Status CR_GEOS_Length(CR_GEOS* lib, CR_GEOS_Slice a, double* ret); +CR_GEOS_Status CR_GEOS_Normalize(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_String* normalizedEWKB); +CR_GEOS_Status CR_GEOS_LineMerge(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_String* ewkb); +CR_GEOS_Status CR_GEOS_MinimumClearance(CR_GEOS* lib, CR_GEOS_Slice a, double* ret); +CR_GEOS_Status CR_GEOS_MinimumClearanceLine(CR_GEOS* lib, CR_GEOS_Slice a, + CR_GEOS_String* clearanceEWKB); + +// +// Unary predicates. +// + +CR_GEOS_Status CR_GEOS_IsSimple(CR_GEOS* lib, CR_GEOS_Slice a, char* ret); + +// +// Topology operators. +// + +CR_GEOS_Status CR_GEOS_Boundary(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_String* boundaryEWKB); +CR_GEOS_Status CR_GEOS_Centroid(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_String* centroidEWKB); +CR_GEOS_Status CR_GEOS_ConvexHull(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_String* convexHullEWKB); +CR_GEOS_Status CR_GEOS_Difference(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + CR_GEOS_String* diffEWKB); +CR_GEOS_Status CR_GEOS_Simplify(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_String* simplifyEWKB, + double tolerance); +CR_GEOS_Status CR_GEOS_TopologyPreserveSimplify(CR_GEOS* lib, CR_GEOS_Slice a, + CR_GEOS_String* simplifyEWKB, double tolerance); +CR_GEOS_Status CR_GEOS_UnaryUnion(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_String* unionEWKB); +CR_GEOS_Status CR_GEOS_Union(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + CR_GEOS_String* unionEWKB); +CR_GEOS_Status CR_GEOS_PointOnSurface(CR_GEOS* lib, CR_GEOS_Slice a, + CR_GEOS_String* pointOnSurfaceEWKB); +CR_GEOS_Status CR_GEOS_Intersection(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + CR_GEOS_String* intersectionEWKB); +CR_GEOS_Status CR_GEOS_SymDifference(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + CR_GEOS_String* symdifferenceEWKB); +CR_GEOS_Status CR_GEOS_SharedPaths(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + CR_GEOS_String* ret); +CR_GEOS_Status CR_GEOS_Node(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_String* ret); + +CR_GEOS_Status CR_GEOS_MinimumBoundingCircle(CR_GEOS* lib, CR_GEOS_Slice a, double* radius, + CR_GEOS_String* centerEWKB, CR_GEOS_String* polygonEWKB); + +CR_GEOS_Status CR_GEOS_MinimumRotatedRectangle(CR_GEOS* lib, CR_GEOS_Slice g, CR_GEOS_String* ret); +// +// Linear reference. +// +CR_GEOS_Status CR_GEOS_Interpolate(CR_GEOS* lib, CR_GEOS_Slice a, double distance, + CR_GEOS_String* interpolatedPoint); + +// +// Binary operators. +// + +CR_GEOS_Status CR_GEOS_Distance(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, double* ret); +CR_GEOS_Status CR_GEOS_FrechetDistance(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, double* ret); +CR_GEOS_Status CR_GEOS_FrechetDistanceDensify(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + double densifyFrac, double* ret); +CR_GEOS_Status CR_GEOS_HausdorffDistance(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + double* ret); +CR_GEOS_Status CR_GEOS_HausdorffDistanceDensify(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + double densifyFrac, double* ret); +CR_GEOS_Status CR_GEOS_EqualsExact(CR_GEOS* lib, CR_GEOS_Slice lhs, CR_GEOS_Slice rhs, + double tolerance, char* ret); + +// +// PreparedGeometry +// + +CR_GEOS_Status CR_GEOS_Prepare(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_PreparedGeometry** ret); +CR_GEOS_Status CR_GEOS_PreparedGeometryDestroy(CR_GEOS* lib, CR_GEOS_PreparedGeometry* g); + +CR_GEOS_Status CR_GEOS_PreparedIntersects(CR_GEOS* lib, CR_GEOS_PreparedGeometry* a, CR_GEOS_Slice b, char* ret); + +// +// Binary predicates. +// + +CR_GEOS_Status CR_GEOS_Covers(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret); +CR_GEOS_Status CR_GEOS_CoveredBy(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret); +CR_GEOS_Status CR_GEOS_Contains(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret); +CR_GEOS_Status CR_GEOS_Crosses(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret); +CR_GEOS_Status CR_GEOS_Disjoint(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret); +CR_GEOS_Status CR_GEOS_Equals(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret); +CR_GEOS_Status CR_GEOS_Intersects(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret); +CR_GEOS_Status CR_GEOS_Overlaps(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret); +CR_GEOS_Status CR_GEOS_Touches(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret); +CR_GEOS_Status CR_GEOS_Within(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, char* ret); + +// +// DE-9IM related +// + +CR_GEOS_Status CR_GEOS_Relate(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, CR_GEOS_String* ret); +CR_GEOS_Status CR_GEOS_RelateBoundaryNodeRule(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + int bnr, CR_GEOS_String* ret); +CR_GEOS_Status CR_GEOS_RelatePattern(CR_GEOS* lib, CR_GEOS_Slice a, CR_GEOS_Slice b, + CR_GEOS_Slice pattern, char* ret); + +CR_GEOS_Status CR_GEOS_VoronoiDiagram(CR_GEOS* lib, CR_GEOS_Slice g, CR_GEOS_Slice env, + double tolerance, int onlyEdges, CR_GEOS_String* ret); + +CR_GEOS_Status CR_GEOS_Snap(CR_GEOS* lib, CR_GEOS_Slice input, CR_GEOS_Slice target, double tolerance, CR_GEOS_String* ret); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/pkg/geo/geosegmentize/geosegmentize.go b/pkg/geo/geosegmentize/geosegmentize.go new file mode 100644 index 0000000..0ee0287 --- /dev/null +++ b/pkg/geo/geosegmentize/geosegmentize.go @@ -0,0 +1,148 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geosegmentize + +import ( + "math" + "strconv" + "strings" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/twpayne/go-geom" +) + +// Segmentize returns a modified geom.T having no segment longer +// than the given maximum segment length. +// segmentMaxAngleOrLength represents two different things depending +// on the object, which is about to segmentize as in case of geography +// it represents maximum segment angle whereas, in case of geometry it +// represents maximum segment distance. +// segmentizeCoords represents the function's definition which allows +// us to segmentize given two-points. We have to specify segmentizeCoords +// explicitly, as the algorithm for segmentation is significantly +// different for geometry and geography. +func Segmentize( + geometry geom.T, + segmentMaxAngleOrLength float64, + segmentizeCoords func(geom.Coord, geom.Coord, float64) ([]float64, error), +) (geom.T, error) { + if geometry.Empty() { + return geometry, nil + } + layout := geometry.Layout() + switch geometry := geometry.(type) { + case *geom.Point, *geom.MultiPoint: + return geometry, nil + case *geom.LineString: + var allFlatCoordinates []float64 + for pointIdx := 1; pointIdx < geometry.NumCoords(); pointIdx++ { + coords, err := segmentizeCoords(geometry.Coord(pointIdx-1), geometry.Coord(pointIdx), segmentMaxAngleOrLength) + if err != nil { + return nil, err + } + allFlatCoordinates = append( + allFlatCoordinates, + coords..., + ) + } + // Appending end point as it wasn't included in the iteration of coordinates. + allFlatCoordinates = append(allFlatCoordinates, geometry.Coord(geometry.NumCoords()-1)...) + return geom.NewLineStringFlat(layout, allFlatCoordinates).SetSRID(geometry.SRID()), nil + case *geom.MultiLineString: + segMultiLine := geom.NewMultiLineString(layout).SetSRID(geometry.SRID()) + for lineIdx := 0; lineIdx < geometry.NumLineStrings(); lineIdx++ { + l, err := Segmentize(geometry.LineString(lineIdx), segmentMaxAngleOrLength, segmentizeCoords) + if err != nil { + return nil, err + } + err = segMultiLine.Push(l.(*geom.LineString)) + if err != nil { + return nil, err + } + } + return segMultiLine, nil + case *geom.LinearRing: + var allFlatCoordinates []float64 + for pointIdx := 1; pointIdx < geometry.NumCoords(); pointIdx++ { + coords, err := segmentizeCoords(geometry.Coord(pointIdx-1), geometry.Coord(pointIdx), segmentMaxAngleOrLength) + if err != nil { + return nil, err + } + allFlatCoordinates = append( + allFlatCoordinates, + coords..., + ) + } + // Appending end point as it wasn't included in the iteration of coordinates. + allFlatCoordinates = append(allFlatCoordinates, geometry.Coord(geometry.NumCoords()-1)...) + return geom.NewLinearRingFlat(layout, allFlatCoordinates).SetSRID(geometry.SRID()), nil + case *geom.Polygon: + segPolygon := geom.NewPolygon(layout).SetSRID(geometry.SRID()) + for loopIdx := 0; loopIdx < geometry.NumLinearRings(); loopIdx++ { + l, err := Segmentize(geometry.LinearRing(loopIdx), segmentMaxAngleOrLength, segmentizeCoords) + if err != nil { + return nil, err + } + err = segPolygon.Push(l.(*geom.LinearRing)) + if err != nil { + return nil, err + } + } + return segPolygon, nil + case *geom.MultiPolygon: + segMultiPolygon := geom.NewMultiPolygon(layout).SetSRID(geometry.SRID()) + for polygonIdx := 0; polygonIdx < geometry.NumPolygons(); polygonIdx++ { + p, err := Segmentize(geometry.Polygon(polygonIdx), segmentMaxAngleOrLength, segmentizeCoords) + if err != nil { + return nil, err + } + err = segMultiPolygon.Push(p.(*geom.Polygon)) + if err != nil { + return nil, err + } + } + return segMultiPolygon, nil + case *geom.GeometryCollection: + segGeomCollection := geom.NewGeometryCollection().SetSRID(geometry.SRID()) + err := segGeomCollection.SetLayout(layout) + if err != nil { + return nil, err + } + for geoIdx := 0; geoIdx < geometry.NumGeoms(); geoIdx++ { + g, err := Segmentize(geometry.Geom(geoIdx), segmentMaxAngleOrLength, segmentizeCoords) + if err != nil { + return nil, err + } + err = segGeomCollection.Push(g) + if err != nil { + return nil, err + } + } + return segGeomCollection, nil + } + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "unknown type: %T", geometry) +} + +// CheckSegmentizeValidNumPoints checks whether segmentize would break down into +// too many points or NaN points. +func CheckSegmentizeValidNumPoints(numPoints float64, a geom.Coord, b geom.Coord) error { + if math.IsNaN(numPoints) { + return pgerror.Newf(pgcode.InvalidParameterValue, "cannot segmentize into %f points", numPoints) + } + if numPoints > float64(geo.MaxAllowedSplitPoints) { + return pgerror.Newf( + pgcode.InvalidParameterValue, + "attempting to segmentize into too many coordinates; need %s points between %v and %v, max %d", + strings.TrimRight(strconv.FormatFloat(numPoints, 'f', -1, 64), "."), + a, + b, + geo.MaxAllowedSplitPoints, + ) + } + return nil +} diff --git a/pkg/geo/geotest/geotest.go b/pkg/geo/geotest/geotest.go new file mode 100644 index 0000000..bddc9e9 --- /dev/null +++ b/pkg/geo/geotest/geotest.go @@ -0,0 +1,81 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geotest + +import ( + "math" + "testing" + + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/errors" + "github.com/stretchr/testify/require" + "github.com/twpayne/go-geom" +) + +// Epsilon is the default epsilon to use in spatial tests. +// This corresponds to ~1cm in lat/lng. +const Epsilon = 0.000001 + +// RequireGeographyInEpsilon and ensures the geometry shape and SRID are equal, +// and that each coordinate is within the provided epsilon. +func RequireGeographyInEpsilon(t *testing.T, expected, got geo.Geography, epsilon float64) { + expectedT, err := expected.AsGeomT() + require.NoError(t, err) + gotT, err := got.AsGeomT() + require.NoError(t, err) + RequireGeomTInEpsilon(t, expectedT, gotT, epsilon) +} + +// RequireGeometryInEpsilon and ensures the geometry shape and SRID are equal, +// and that each coordinate is within the provided epsilon. +func RequireGeometryInEpsilon(t *testing.T, expected, got geo.Geometry, epsilon float64) { + expectedT, err := expected.AsGeomT() + require.NoError(t, err) + gotT, err := got.AsGeomT() + require.NoError(t, err) + RequireGeomTInEpsilon(t, expectedT, gotT, epsilon) +} + +// FlatCoordsInEpsilon ensures the flat coords are within the expected epsilon. +func FlatCoordsInEpsilon(t *testing.T, expected []float64, actual []float64, epsilon float64) { + require.Equal(t, len(expected), len(actual), "expected %#v, got %#v", expected, actual) + for i := range expected { + require.True(t, math.Abs(expected[i]-actual[i]) < epsilon, "expected %#v, got %#v (mismatching at position %d)", expected, actual, i) + } +} + +// RequireGeomTInEpsilon ensures that the geom.T are equal, except for +// coords which should be within the given epsilon. +func RequireGeomTInEpsilon(t *testing.T, expectedT, gotT geom.T, epsilon float64) { + require.Equal(t, expectedT.SRID(), gotT.SRID()) + require.Equal(t, expectedT.Layout(), gotT.Layout()) + require.IsType(t, expectedT, gotT) + switch lhs := expectedT.(type) { + case *geom.Point, *geom.LineString: + FlatCoordsInEpsilon(t, expectedT.FlatCoords(), gotT.FlatCoords(), epsilon) + case *geom.MultiPoint, *geom.Polygon, *geom.MultiLineString: + require.Equal(t, expectedT.Ends(), gotT.Ends()) + FlatCoordsInEpsilon(t, expectedT.FlatCoords(), gotT.FlatCoords(), epsilon) + case *geom.MultiPolygon: + require.Equal(t, expectedT.Ends(), gotT.Ends()) + require.Equal(t, expectedT.Endss(), gotT.Endss()) + FlatCoordsInEpsilon(t, expectedT.FlatCoords(), gotT.FlatCoords(), epsilon) + case *geom.GeometryCollection: + rhs, ok := gotT.(*geom.GeometryCollection) + require.True(t, ok) + require.Len(t, rhs.Geoms(), len(lhs.Geoms())) + for i := range lhs.Geoms() { + RequireGeomTInEpsilon( + t, + lhs.Geom(i), + rhs.Geom(i), + epsilon, + ) + } + default: + panic(errors.AssertionFailedf("unknown geometry type: %T", expectedT)) + } +} diff --git a/pkg/geo/geotransform/geotransform.go b/pkg/geo/geotransform/geotransform.go new file mode 100644 index 0000000..3a7d819 --- /dev/null +++ b/pkg/geo/geotransform/geotransform.go @@ -0,0 +1,136 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +package geotransform + +import ( + "github.com/cockroachdb/cockroachdb-parser/pkg/geo" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geopb" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geoproj" + "github.com/cockroachdb/cockroachdb-parser/pkg/geo/geoprojbase" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/twpayne/go-geom" +) + +// Transform projects a given Geometry from a given Proj4Text to another Proj4Text. +func Transform( + g geo.Geometry, from geoprojbase.Proj4Text, to geoprojbase.Proj4Text, newSRID geopb.SRID, +) (geo.Geometry, error) { + if from.Equal(to) { + return g.CloneWithSRID(newSRID) + } + + t, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + + newT, err := transform(t, from, to, newSRID) + if err != nil { + return geo.Geometry{}, err + } + return geo.MakeGeometryFromGeomT(newT) +} + +// transform performs the projection operation on a geom.T object. +func transform( + t geom.T, from geoprojbase.Proj4Text, to geoprojbase.Proj4Text, newSRID geopb.SRID, +) (geom.T, error) { + switch t := t.(type) { + case *geom.Point: + newCoords, err := projectFlatCoords(t.FlatCoords(), t.Layout(), from, to) + if err != nil { + return nil, err + } + return geom.NewPointFlat(t.Layout(), newCoords).SetSRID(int(newSRID)), nil + case *geom.LineString: + newCoords, err := projectFlatCoords(t.FlatCoords(), t.Layout(), from, to) + if err != nil { + return nil, err + } + return geom.NewLineStringFlat(t.Layout(), newCoords).SetSRID(int(newSRID)), nil + case *geom.Polygon: + newCoords, err := projectFlatCoords(t.FlatCoords(), t.Layout(), from, to) + if err != nil { + return nil, err + } + return geom.NewPolygonFlat(t.Layout(), newCoords, t.Ends()).SetSRID(int(newSRID)), nil + case *geom.MultiPoint: + newCoords, err := projectFlatCoords(t.FlatCoords(), t.Layout(), from, to) + if err != nil { + return nil, err + } + return geom.NewMultiPointFlat(t.Layout(), newCoords).SetSRID(int(newSRID)), nil + case *geom.MultiLineString: + newCoords, err := projectFlatCoords(t.FlatCoords(), t.Layout(), from, to) + if err != nil { + return nil, err + } + return geom.NewMultiLineStringFlat(t.Layout(), newCoords, t.Ends()).SetSRID(int(newSRID)), nil + case *geom.MultiPolygon: + newCoords, err := projectFlatCoords(t.FlatCoords(), t.Layout(), from, to) + if err != nil { + return nil, err + } + return geom.NewMultiPolygonFlat(t.Layout(), newCoords, t.Endss()).SetSRID(int(newSRID)), nil + case *geom.GeometryCollection: + g := geom.NewGeometryCollection().SetSRID(int(newSRID)) + for _, subG := range t.Geoms() { + subGeom, err := transform(subG, from, to, 0) + if err != nil { + return nil, err + } + if err := g.Push(subGeom); err != nil { + return nil, err + } + } + return g, nil + default: + return nil, pgerror.Newf(pgcode.InvalidParameterValue, "unhandled type: %T", t) + } +} + +// projectFlatCoords projects a given flatCoords array and returns an array with the projected +// coordinates. +// Note M coordinates are not touched. +func projectFlatCoords( + flatCoords []float64, layout geom.Layout, from geoprojbase.Proj4Text, to geoprojbase.Proj4Text, +) ([]float64, error) { + numCoords := len(flatCoords) / layout.Stride() + // Allocate the map once and partition the arrays to store xCoords, yCoords and zCoords in order. + coords := make([]float64, numCoords*3) + xCoords := coords[numCoords*0 : numCoords*1] + yCoords := coords[numCoords*1 : numCoords*2] + zCoords := coords[numCoords*2 : numCoords*3] + for i := 0; i < numCoords; i++ { + base := i * layout.Stride() + xCoords[i] = flatCoords[base+0] + yCoords[i] = flatCoords[base+1] + // If ZIndex is != -1, it is 2 and forms part of our z coords. + if layout.ZIndex() != -1 { + zCoords[i] = flatCoords[base+layout.ZIndex()] + } + } + + if err := geoproj.Project(from, to, xCoords, yCoords, zCoords); err != nil { + return nil, err + } + newCoords := make([]float64, numCoords*layout.Stride()) + for i := 0; i < numCoords; i++ { + base := i * layout.Stride() + newCoords[base+0] = xCoords[i] + newCoords[base+1] = yCoords[i] + + if layout.ZIndex() != -1 { + newCoords[base+layout.ZIndex()] = zCoords[i] + } + if layout.MIndex() != -1 { + newCoords[base+layout.MIndex()] = flatCoords[i*layout.Stride()+layout.MIndex()] + } + } + + return newCoords, nil +} diff --git a/pkg/geo/twkb/twkb.go b/pkg/geo/twkb/twkb.go new file mode 100644 index 0000000..de2861a --- /dev/null +++ b/pkg/geo/twkb/twkb.go @@ -0,0 +1,359 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +// Package twkb implements the Tiny Well-known Binary (TWKB) encoding +// as described in https://github.com/TWKB/Specification/blob/master/twkb.md. +package twkb + +import ( + "bytes" + "math" + + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" + "github.com/twpayne/go-geom" +) + +// twkbType is represents a TWKB byte. +type twkbType uint8 + +const ( + twkbTypePoint twkbType = 1 + twkbTypeLineString twkbType = 2 + twkbTypePolygon twkbType = 3 + twkbTypeMultiPoint twkbType = 4 + twkbTypeMultiLineString twkbType = 5 + twkbTypeMultiPolygon twkbType = 6 + twkbTypeGeometryCollection twkbType = 7 +) + +type marshalOptions struct { + precisionXY int8 + precisionZ int8 + precisionM int8 +} + +type marshaller struct { + bytes.Buffer + o marshalOptions + // prevCoords keeps track of the previously written coordinates. + // This is needed as for Polygons, MultiLineString and MultiPolygons, the + // deltas written can be based off the previous ring/linestring/polygon. + prevCoords []int64 +} + +// MarshalOption is an option to pass into Marshal. +type MarshalOption func(o *marshalOptions) error + +// MarshalOptionPrecisionXY sets the XY precision for the TWKB. +func MarshalOptionPrecisionXY(p int64) MarshalOption { + return func(o *marshalOptions) error { + if p < -7 || p > 7 { + return pgerror.Newf( + pgcode.InvalidParameterValue, + "XY precision must be between -7 and 7 inclusive", + ) + } + o.precisionXY = int8(p) + return nil + } +} + +// MarshalOptionPrecisionZ sets the Z precision for the TWKB. +func MarshalOptionPrecisionZ(p int64) MarshalOption { + return func(o *marshalOptions) error { + if p < 0 || p > 7 { + return pgerror.Newf( + pgcode.InvalidParameterValue, + "Z precision must not be negative or greater than 7", + ) + } + + o.precisionZ = int8(p) + return nil + } +} + +// MarshalOptionPrecisionM sets the M precision for the TWKB. +func MarshalOptionPrecisionM(p int64) MarshalOption { + return func(o *marshalOptions) error { + if p < 0 || p > 7 { + return pgerror.Newf( + pgcode.InvalidParameterValue, + "M precision must not be negative or greater than 7", + ) + } + + o.precisionM = int8(p) + return nil + } +} + +// Marshal converts a geom.T into a TWKB encoded byte array. +func Marshal(t geom.T, opts ...MarshalOption) ([]byte, error) { + var o marshalOptions + for _, opt := range opts { + if err := opt(&o); err != nil { + return nil, err + } + } + // TODO(#48886): PostGIS increases the precision by 5 + // for all lat/lng SRIDs in certain cases. + m := marshaller{ + o: o, + prevCoords: make([]int64, t.Layout().Stride()), + } + if err := (&m).marshal(t); err != nil { + return nil, err + } + return m.Bytes(), nil +} + +func (m *marshaller) marshal(t geom.T) error { + // Write metadata byte. + typeAndPrecisionHeader, err := typeAndPrecisionHeaderByte(t, m.o) + if err != nil { + return err + } + if err := m.WriteByte(typeAndPrecisionHeader); err != nil { + return err + } + if err := m.WriteByte(metadataByte(t, m.o)); err != nil { + return err + } + if t.Layout().Stride() > 2 { + if err := m.WriteByte(extendedDimensionsByte(t, m.o)); err != nil { + return err + } + } + if !t.Empty() { + // TODO(#48886): write bounding box if we support writing bounding boxes. + switch t := t.(type) { + case *geom.Point: + if err := m.writeFlatCoords( + t.FlatCoords(), + t.Layout(), + false, /* writeLen */ + ); err != nil { + return err + } + case *geom.LineString: + if err := m.writeFlatCoords( + t.FlatCoords(), + t.Layout(), + true, /* writeLen */ + ); err != nil { + return err + } + case *geom.Polygon: + if err := m.writeGeomWithEnds( + t.FlatCoords(), + t.Ends(), + t.Layout(), + 0, /* startOffset */ + ); err != nil { + return err + } + case *geom.MultiPoint: + // TODO(#48886): write out the id list for MultiPoint. + if err := m.writeFlatCoords( + t.FlatCoords(), + t.Layout(), + true, /* writeLen */ + ); err != nil { + return err + } + case *geom.MultiLineString: + // TODO(#48886): write out the id list for MultiLineString. + if err := m.writeGeomWithEnds( + t.FlatCoords(), + t.Ends(), + t.Layout(), + 0, /* startOffset */ + ); err != nil { + return err + } + case *geom.MultiPolygon: + // TODO(#48886): write out the id list for MultiPolygon. + flatCoords := t.FlatCoords() + endss := t.Endss() + // Write the number of MultiPolygons. + if err := m.putUvarint(uint64(len(endss))); err != nil { + return err + } + startOffset := 0 + for _, ends := range endss { + // Write out each polygon. + if err := m.writeGeomWithEnds( + flatCoords, + ends, + t.Layout(), + startOffset, + ); err != nil { + return err + } + // Update the start offset to be the last end we encountered. + if len(ends) > 0 { + startOffset = ends[len(ends)-1] + } + } + case *geom.GeometryCollection: + // Write the number of geometries. + if err := m.putUvarint(uint64(t.NumGeoms())); err != nil { + return err + } + for _, gcT := range t.Geoms() { + // Reset prev coords each time. + for i := range m.prevCoords { + m.prevCoords[i] = 0 + } + if err := m.marshal(gcT); err != nil { + return err + } + } + default: + return pgerror.Newf(pgcode.InvalidParameterValue, "unknown TWKB type: %T", t) + } + } + return nil +} + +func (m *marshaller) writeGeomWithEnds( + flatCoords []float64, ends []int, layout geom.Layout, startOffset int, +) error { + // Write the number of elements. + if err := m.putUvarint(uint64(len(ends))); err != nil { + return err + } + start := startOffset + // Write each segment with the length of each ring at the beginning. + for _, end := range ends { + if err := m.writeFlatCoords( + flatCoords[start:end], + layout, + true, /* writeLen */ + ); err != nil { + return err + } + start = end + } + return nil +} + +// writeFlatCoords writes the flat coordinates into the buffer. +func (m *marshaller) writeFlatCoords( + flatCoords []float64, layout geom.Layout, writeLen bool, +) error { + stride := layout.Stride() + if writeLen { + if err := m.putUvarint(uint64(len(flatCoords) / stride)); err != nil { + return err + } + } + + zIndex := layout.ZIndex() + mIndex := layout.MIndex() + for i, coord := range flatCoords { + // Precision varies by coordinate. + precision := m.o.precisionXY + if i%stride == zIndex { + precision = m.o.precisionZ + } + if i%stride == mIndex { + precision = m.o.precisionM + } + curr := int64(math.Round(coord * math.Pow(10, float64(precision)))) + prev := m.prevCoords[i%stride] + + // The value written is the int version of the delta multiplied + // by 10^precision. + if err := m.putVarint(curr - prev); err != nil { + return err + } + m.prevCoords[i%stride] = curr + } + return nil +} + +// putVarint encodes an int64 into buf and returns the number of bytes written. +func (m *marshaller) putVarint(x int64) error { + ux := uint64(x) << 1 + if x < 0 { + ux = ^ux + } + return m.putUvarint(ux) +} + +// putUvarint encodes a uint64 into buf and returns the number of bytes written. +func (m *marshaller) putUvarint(x uint64) error { + for x >= 0x80 { + if err := m.WriteByte(byte(x) | 0x80); err != nil { + return err + } + x >>= 7 + } + return m.WriteByte(byte(x)) +} + +func typeAndPrecisionHeaderByte(t geom.T, o marshalOptions) (byte, error) { + var typ twkbType + switch t.(type) { + case *geom.Point: + typ = twkbTypePoint + case *geom.LineString: + typ = twkbTypeLineString + case *geom.Polygon: + typ = twkbTypePolygon + case *geom.MultiPoint: + typ = twkbTypeMultiPoint + case *geom.MultiLineString: + typ = twkbTypeMultiLineString + case *geom.MultiPolygon: + typ = twkbTypeMultiPolygon + case *geom.GeometryCollection: + typ = twkbTypeGeometryCollection + default: + return 0, pgerror.Newf(pgcode.InvalidParameterValue, "unknown TWKB type: %T", t) + } + + precisionZigZagged := zigzagInt8(o.precisionXY) + return (uint8(typ) & 0x0F) | ((precisionZigZagged & 0x0F) << 4), nil +} + +func metadataByte(t geom.T, o marshalOptions) byte { + var b byte + if t.Empty() { + b |= 0b10000 + } + if t.Layout().Stride() > 2 { + b |= 0b1000 + } + return b +} + +func extendedDimensionsByte(t geom.T, o marshalOptions) byte { + var extDimByte byte + // Follow PostGIS and do no bounds checking for precision under/overflow. + switch t.Layout() { + case geom.XYZ: + extDimByte |= 0b1 + extDimByte |= (byte(o.precisionZ) & 0b111) << 2 + case geom.XYM: + extDimByte |= 0b10 + extDimByte |= (byte(o.precisionM) & 0b111) << 5 + case geom.XYZM: + extDimByte |= 0b11 + extDimByte |= (byte(o.precisionZ) & 0b111) << 2 + extDimByte |= (byte(o.precisionM) & 0b111) << 5 + } + return extDimByte +} + +func zigzagInt8(x int8) byte { + if x < 0 { + return (uint8(-1-x) << 1) | 0x01 + } + return uint8(x) << 1 +} diff --git a/pkg/sql/lexbase/encode.go b/pkg/sql/lexbase/encode.go index 4f7bc03..5b31fd0 100644 --- a/pkg/sql/lexbase/encode.go +++ b/pkg/sql/lexbase/encode.go @@ -51,6 +51,13 @@ const ( // as Oracle is case insensitive if object name is not quoted. EncAlwaysQuoted + // EncSkipEscapeString indicates that the string should not be escaped, + // and non-ASCII characters are allowed. + // More specifically, it means that the tree.DString won't be wrapped + // with e'text' as the prefix and suffix, and single quotes within + // the string will not be escaped with a backslash. + EncSkipEscapeString + // EncFirstFreeFlagBit needs to remain unused; it is used as base // bit offset for tree.FmtFlags. EncFirstFreeFlagBit @@ -147,6 +154,7 @@ func EscapeSQLString(in string) string { func EncodeSQLStringWithFlags(buf *bytes.Buffer, in string, flags EncodeFlags) { // See http://www.postgresql.org/docs/9.4/static/sql-syntax-lexical.html start := 0 + skipEscape := flags.HasFlags(EncSkipEscapeString) escapedString := false bareStrings := flags.HasFlags(EncBareStrings) // Loop through each unicode code point. @@ -165,7 +173,13 @@ func EncodeSQLStringWithFlags(buf *bytes.Buffer, in string, flags EncodeFlags) { } } - if !escapedString { + // If non-ASCII characters are allowed, + // skip escaping and write the original UTF-8 character. + if r > maxPrintableChar && skipEscape { + continue + } + + if !skipEscape && !escapedString { buf.WriteString("e'") // begin e'xxx' string escapedString = true } @@ -177,17 +191,19 @@ func EncodeSQLStringWithFlags(buf *bytes.Buffer, in string, flags EncodeFlags) { } else { start = i + ln } - stringencoding.EncodeEscapedChar(buf, in, r, ch, i, '\'') + // If we skip escaping, we don't write the slash char before the + // quote char, so we will write the quote char directly. + stringencoding.EncodeEscapedChar(buf, in, r, ch, i, '\'', !skipEscape) } - quote := !escapedString && !bareStrings + quote := !escapedString && !bareStrings && !skipEscape if quote { buf.WriteByte('\'') // begin 'xxx' string if nothing was escaped } if start < len(in) { buf.WriteString(in[start:]) } - if escapedString || quote { + if !skipEscape && (escapedString || quote) { buf.WriteByte('\'') } } diff --git a/pkg/sql/parser/help_messages.go b/pkg/sql/parser/help_messages.go index d0d1436..f2c4944 100644 --- a/pkg/sql/parser/help_messages.go +++ b/pkg/sql/parser/help_messages.go @@ -1414,24 +1414,24 @@ ALTER VIRTUAL CLUSTER RESET DATA TO SYSTEM TIME