diff --git a/common/pom.xml b/common/pom.xml index 15b4c1bacc..5a007b485f 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -34,8 +34,8 @@ - org.apache.logging.log4j - log4j-1.2-api + org.slf4j + slf4j-api org.geotools diff --git a/common/src/main/java/org/apache/sedona/common/Functions.java b/common/src/main/java/org/apache/sedona/common/Functions.java index f6e24e55c2..ccc6f7a084 100644 --- a/common/src/main/java/org/apache/sedona/common/Functions.java +++ b/common/src/main/java/org/apache/sedona/common/Functions.java @@ -16,6 +16,7 @@ import org.apache.sedona.common.geometryObjects.Circle; import org.apache.sedona.common.utils.GeomUtils; import org.apache.sedona.common.utils.GeometryGeoHashEncoder; +import org.apache.sedona.common.utils.GeometrySplitter; import org.geotools.geometry.jts.JTS; import org.geotools.referencing.CRS; import org.locationtech.jts.algorithm.MinimumBoundingCircle; @@ -128,7 +129,7 @@ public static double xMin(Geometry geometry) { } return min; } - + public static double xMax(Geometry geometry) { Coordinate[] points = geometry.getCoordinates(); double max = - Double.MAX_VALUE; @@ -146,7 +147,7 @@ public static double yMin(Geometry geometry) { } return min; } - + public static double yMax(Geometry geometry) { Coordinate[] points = geometry.getCoordinates(); double max = - Double.MAX_VALUE; @@ -214,7 +215,7 @@ public static Geometry flipCoordinates(Geometry geometry) { } public static String geohash(Geometry geometry, int precision) { - return GeometryGeoHashEncoder.calculate(geometry, precision); + return GeometryGeoHashEncoder.calculate(geometry, precision); } public static Geometry pointOnSurface(Geometry geometry) { @@ -431,7 +432,7 @@ public static Geometry lineFromMultiPoint(Geometry geometry) { } List coordinates = new ArrayList<>(); for(Coordinate c : geometry.getCoordinates()){ - coordinates.add(c); + coordinates.add(c); } return GEOMETRY_FACTORY.createLineString(coordinates.toArray(new Coordinate[0])); } @@ -537,4 +538,9 @@ public static Geometry difference(Geometry leftGeometry, Geometry rightGeometry) return leftGeometry.difference(rightGeometry); } } + + public static Geometry split(Geometry input, Geometry blade) { + // check input geometry + return new GeometrySplitter(GEOMETRY_FACTORY).split(input, blade); + } } diff --git a/common/src/main/java/org/apache/sedona/common/utils/GeomUtils.java b/common/src/main/java/org/apache/sedona/common/utils/GeomUtils.java index 736b5bcc21..1f60c53366 100644 --- a/common/src/main/java/org/apache/sedona/common/utils/GeomUtils.java +++ b/common/src/main/java/org/apache/sedona/common/utils/GeomUtils.java @@ -219,6 +219,76 @@ public static int getDimension(Geometry geometry) { return geometry.getCoordinate() != null && !java.lang.Double.isNaN(geometry.getCoordinate().getZ()) ? 3 : 2; } + /** + * Checks if the geometry only contains geometry of + * the same dimension. By dimension this refers to whether the + * geometries are all, for example, lines (1D). + * + * @param geometry geometry to check + * @return true iff geometry is homogeneous + */ + public static boolean geometryIsHomogeneous(Geometry geometry) { + int dimension = geometry.getDimension(); + + if (!geometry.isEmpty()) { + for (int i = 0; i < geometry.getNumGeometries(); i++) { + if (dimension != geometry.getGeometryN(i).getDimension()) { + return false; + } + } + } + + return true; + } + + /** + * Checks if either the geometry is, or contains, only point geometry. + * GeometryCollections that only contain points will return true. + * + * @param geometry geometry to check + * @return true iff geometry is puntal + */ + public static boolean geometryIsPuntal(Geometry geometry) { + if (geometry instanceof Puntal) { + return true; + } else if (geometryIsHomogeneous(geometry) && geometry.getDimension() == 0) { + return true; + } + return false; + } + + /** + * Checks if either the geometry is, or contains, only line geometry. + * GeometryCollections that only contain lines will return true. + * + * @param geometry geometry to check + * @return true iff geometry is lineal + */ + public static boolean geometryIsLineal(Geometry geometry) { + if (geometry instanceof Lineal) { + return true; + } else if (geometryIsHomogeneous(geometry) && geometry.getDimension() == 1) { + return true; + } + return false; + } + + /** + * Checks if either the geometry is, or contains, only polygon geometry. + * GeometryCollections that only contain polygons will return true. + * + * @param geometry geometry to check + * @return true iff geometry is polygonal + */ + public static boolean geometryIsPolygonal(Geometry geometry) { + if (geometry instanceof Polygonal) { + return true; + } else if (geometryIsHomogeneous(geometry) && geometry.getDimension() == 2) { + return true; + } + return false; + } + private static Map findFaceHoles(List faces) { Map parentMap = new HashMap<>(); faces.sort(Comparator.comparing((Polygon p) -> p.getEnvelope().getArea()).reversed()); @@ -250,4 +320,4 @@ private static int countParents(Map parentMap, Polygon face) { } return pCount; } -} \ No newline at end of file +} diff --git a/common/src/main/java/org/apache/sedona/common/utils/GeometrySplitter.java b/common/src/main/java/org/apache/sedona/common/utils/GeometrySplitter.java new file mode 100644 index 0000000000..b80986fb00 --- /dev/null +++ b/common/src/main/java/org/apache/sedona/common/utils/GeometrySplitter.java @@ -0,0 +1,333 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sedona.common.utils; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.MultiLineString; +import org.locationtech.jts.geom.MultiPoint; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.linearref.LinearGeometryBuilder; +import org.locationtech.jts.operation.polygonize.Polygonizer; + + +/** + * Class to split geometry by other geometry. + */ +public final class GeometrySplitter { + final static Logger logger = LoggerFactory.getLogger(GeometrySplitter.class); + private final GeometryFactory geometryFactory; + + public GeometrySplitter(GeometryFactory geometryFactory) { + this.geometryFactory = geometryFactory; + } + + /** + * Split input geometry by the blade geometry. + * Input geometry can be lineal (LineString or MultiLineString) + * or polygonal (Polygon or MultiPolygon). A GeometryCollection + * can also be used as an input but it must be homogeneous. + * For lineal geometry refer to the + * {@link splitLines(Geometry, Geometry) splitLines} method for + * restrictions on the blade. Refer to + * {@link splitPolygons(Geometry, Geometry) splitPolygons} for + * restrictions on the blade for polygonal input geometry. + *

+ * The result will be null if the input geometry and blade are either + * invalid in general or in relation to eachother. Otherwise, the result + * will always be a MultiLineString or MultiPolygon depending on the input, + * and even if the result is a single geometry. + * + * @param input input geometry + * @param blade geometry to use as a blade + * @return multi-geometry resulting from the split or null if invalid + */ + public GeometryCollection split(Geometry input, Geometry blade) { + GeometryCollection result = null; + + if (GeomUtils.geometryIsLineal(input)) { + result = splitLines(input, blade); + } else if (GeomUtils.geometryIsPolygonal(input)) { + result = splitPolygons(input, blade); + } + + return result; + } + + /** + * Split linear input geometry by the blade geometry. + * Input geometry is assumed to be either a LineString, + * MultiLineString, or a homogeneous collection of lines in a + * GeometryCollection. The blade geometry can be any indivdual + * puntal, lineal, or polygonal geometry or homogeneous collection + * of those geometries. Blades that are polygonal will use their + * boundary for the split. Will always return a MultiLineString. + * + * @param input input geometry to be split that must be lineal + * @param blade blade geometry to use for split + * + * @return input geometry split by blade + */ + public MultiLineString splitLines(Geometry input, Geometry blade) { + MultiLineString result = null; + + if (GeomUtils.geometryIsPolygonal(blade)) { + blade = blade.getBoundary(); + } + + if (GeomUtils.geometryIsPuntal(blade)) { + result = splitLinesByPoints(input, blade); + } else if (GeomUtils.geometryIsLineal(blade)) { + result = splitLinesByLines(input, blade); + } + + return result; + } + + /** + * Split polygonal input geometry by the blade geometry. + * Input geometry is assumed to be either a Polygon, MultiPolygon, + * or a GeometryCollection of only polygons. The blade geometry + * can be any individual lineal or polygonal geometry or homogeneous + * collection of those geometries. Blades that are polygonal + * will use their boundary for the split. Will always return a + * MultiPolygon. + * + * @param input input polygonal geometry to split + * @param blade geometry to split the input by + * @return input geometry split by the blade + */ + public MultiPolygon splitPolygons(Geometry input, Geometry blade) { + MultiPolygon result = null; + + if (GeomUtils.geometryIsPolygonal(blade)) { + blade = blade.getBoundary(); + } + + if (GeomUtils.geometryIsLineal(blade)) { + result = splitPolygonsByLines(input, blade); + } + + return result; + } + + private MultiLineString splitLinesByPoints(Geometry lines, Geometry points) { + // coords must be ordered for the algorithm in splitLineStringAtCoordinates + // do it here so the sorting is only done once per split operation + // use a deque for easy forward and reverse iteration + Deque pointCoords = getOrderedCoords(points); + + LinearGeometryBuilder lineBuilder = new LinearGeometryBuilder(geometryFactory); + lineBuilder.setIgnoreInvalidLines(true); + + for (int lineIndex = 0; lineIndex < lines.getNumGeometries(); lineIndex++) { + splitLineStringAtCoordinates((LineString)lines.getGeometryN(lineIndex), pointCoords, lineBuilder); + } + + MultiLineString result = (MultiLineString)ensureMultiGeometryOfDimensionN(lineBuilder.getGeometry(), 1); + + return result; + } + + private MultiLineString splitLinesByLines(Geometry inputLines, Geometry blade) { + // compute the intersection of inputLines and blade + // and pass back to splitLines to handle as points + Geometry intersectionWithBlade = inputLines.intersection(blade); + + if (intersectionWithBlade.isEmpty()) { + // blade and inputLines are disjoint so just return the input as a multilinestring + return (MultiLineString)ensureMultiGeometryOfDimensionN(inputLines, 1); + } else if (intersectionWithBlade.getDimension() != 0) { + logger.warn("Colinear sections detected between source and blade geometry. Returned null."); + return null; + } + + return splitLines(inputLines, intersectionWithBlade); + } + + private MultiPolygon splitPolygonsByLines(Geometry polygons, Geometry blade) { + ArrayList validResults = new ArrayList<>(); + + for (int polygonIndex = 0; polygonIndex < polygons.getNumGeometries(); polygonIndex++) { + Geometry originalPolygon = polygons.getGeometryN(polygonIndex); + Geometry candidatePolygons = generateCandidatePolygons(originalPolygon, blade); + addValidPolygonsToList(candidatePolygons, originalPolygon, validResults); + } + + return geometryFactory.createMultiPolygon(validResults.toArray(new Polygon[0])); + } + + private Deque getOrderedCoords(Geometry geometry) { + Coordinate[] pointCoords = geometry.getCoordinates(); + ArrayDeque coordsDeque = new ArrayDeque<>(pointCoords.length); + + // coords are ordered from left to right, bottom to top + Arrays.sort(pointCoords); + coordsDeque.addAll(Arrays.asList(pointCoords)); + + return coordsDeque; + } + + private void splitLineStringAtCoordinates(LineString line, Deque pointCoords, LinearGeometryBuilder lineBuilder) { + Coordinate[] lineCoords = line.getCoordinates(); + if (lineCoords.length > 1) { + lineBuilder.add(lineCoords[0]); + + for (int endCoordIndex = 1; endCoordIndex < lineCoords.length; endCoordIndex++) { + Coordinate endCoord = lineCoords[endCoordIndex]; + Iterator coordIterator = getIteratorForSegmentDirection(pointCoords, lineBuilder.getLastCoordinate(), endCoord); + + applyCoordsToLineSegment(lineBuilder, coordIterator, endCoord); + } + + lineBuilder.endLine(); + } + } + + private Iterator getIteratorForSegmentDirection(Deque coords, Coordinate startCoord, Coordinate endCoord) { + // line segments are assumed to be left to right, bottom to top + // and coords is also ordered that way + // however, if the line segment is backwards then coords will + // need to be iterated in reverse + Iterator coordIterator; + + if (endCoord.compareTo(startCoord) == -1) { + coordIterator = coords.descendingIterator(); + } else { + coordIterator = coords.iterator(); + } + + return coordIterator; + } + + private void applyCoordsToLineSegment(LinearGeometryBuilder lineBuilder, Iterator coordIterator, Coordinate endCoord) { + + while (coordIterator.hasNext()) { + Coordinate pointCoord = coordIterator.next(); + Coordinate lastCoord = lineBuilder.getLastCoordinate(); + + if (coordIsOnLineSegment(pointCoord, lastCoord, endCoord)) { + splitOnCoord(lineBuilder, pointCoord); + } + + } + + lineBuilder.add(endCoord, false); + } + + private boolean coordIsOnLineSegment(Coordinate coord, Coordinate startCoord, Coordinate endCoord) { + boolean result = false; + boolean segmentIsVertical = startCoord.x == endCoord.x; + boolean coordIsWithinVerticalRange = Math.min(startCoord.y, endCoord.y) <= coord.y && coord.y <= Math.max(startCoord.y, endCoord.y); + + if (coordIsWithinVerticalRange) { + if (segmentIsVertical) { + if (coord.x == startCoord.x) { + result = true; + } + } else { + double m = (startCoord.y - endCoord.y) / (startCoord.x - endCoord.x); + double yInt = startCoord.y - startCoord.x * m; + if (coord.y == coord.x * m + yInt) { + result = true; + } + } + } + + return result; + } + + private void splitOnCoord(LinearGeometryBuilder lineBuilder, Coordinate coord) { + lineBuilder.add(coord, false); + lineBuilder.endLine(); + lineBuilder.add(coord, true); + } + + private Geometry generateCandidatePolygons(Geometry polygons, Geometry blade) { + // restrict the blade to only be within the original polygon to + // avoid candidate polygons that are impossible + Geometry bladeWithinPolygons = blade.intersection(polygons); + + // a union will node all of the lines at intersections + // these nodes are required for Polygonizer to work correctly + Geometry totalLineWork = polygons.getBoundary().union(bladeWithinPolygons); + + Polygonizer polygonizer = new Polygonizer(); + polygonizer.add(totalLineWork); + + return polygonizer.getGeometry(); + } + + private void addValidPolygonsToList(Geometry polygons, Geometry original, List list) { + // polygons must be checked to make sure they are contained within the + // original geometry to ensure holes in the original geometry are excluded + for (int i = 0; i < polygons.getNumGeometries(); i++) { + Geometry candidateResult = polygons.getGeometryN(i); + if (candidateResult instanceof Polygon && original.contains(candidateResult)) { + list.add((Polygon)candidateResult); + } + } + } + + private GeometryCollection ensureMultiGeometryOfDimensionN(Geometry geometry, int dimension) { + GeometryCollection result = null; + + if (geometry != null) { + if (dimension == 0 && geometry instanceof MultiPoint) { + result = (MultiPoint) geometry; + } else if (dimension == 1 && geometry instanceof MultiLineString) { + result = (MultiLineString) geometry; + } else if (dimension == 2 && geometry instanceof MultiPolygon) { + result = (MultiPolygon) geometry; + } else { + ArrayList validGeometries = new ArrayList<>(); + + for (int n = 0; n < geometry.getNumGeometries(); n++) { + Geometry candidateGeometry = geometry.getGeometryN(n); + if (candidateGeometry.getDimension() == dimension) { + validGeometries.add(candidateGeometry); + } + } + + switch (dimension) { + case 0: + result = (GeometryCollection)geometryFactory.createMultiPoint(validGeometries.toArray(new Point[0])); + break; + case 1: + result = (GeometryCollection)geometryFactory.createMultiLineString(validGeometries.toArray(new LineString[0])); + break; + case 2: + result = (GeometryCollection)geometryFactory.createMultiPolygon(validGeometries.toArray(new Polygon[0])); + break; + } + } + } + + return result; + } +} diff --git a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java new file mode 100644 index 0000000000..abe522f9b2 --- /dev/null +++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java @@ -0,0 +1,241 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sedona.common; + +import org.junit.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.MultiLineString; +import org.locationtech.jts.geom.MultiPoint; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.io.ParseException; + +import static org.junit.Assert.*; + +public class FunctionsTest { + public static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory(); + + private Coordinate[] coordArray(double... coordValues) { + Coordinate[] coords = new Coordinate[(int)(coordValues.length / 2)]; + for (int i = 0; i < coordValues.length; i += 2) { + coords[(int)(i / 2)] = new Coordinate(coordValues[i], coordValues[i+1]); + } + return coords; + } + + @Test + public void splitLineStringByMultipoint() { + LineString lineString = GEOMETRY_FACTORY.createLineString(coordArray(0.0, 0.0, 1.5, 1.5, 2.0, 2.0)); + MultiPoint multiPoint = GEOMETRY_FACTORY.createMultiPointFromCoords(coordArray(0.5, 0.5, 1.0, 1.0)); + + String actualResult = Functions.split(lineString, multiPoint).norm().toText(); + String expectedResult = "MULTILINESTRING ((0 0, 0.5 0.5), (0.5 0.5, 1 1), (1 1, 1.5 1.5, 2 2))"; + + assertEquals(actualResult, expectedResult); + } + + @Test + public void splitMultiLineStringByMultiPoint() { + LineString[] lineStrings = new LineString[]{ + GEOMETRY_FACTORY.createLineString(coordArray(0.0, 0.0, 1.5, 1.5, 2.0, 2.0)), + GEOMETRY_FACTORY.createLineString(coordArray(3.0, 3.0, 4.5, 4.5, 5.0, 5.0)) + }; + MultiLineString multiLineString = GEOMETRY_FACTORY.createMultiLineString(lineStrings); + MultiPoint multiPoint = GEOMETRY_FACTORY.createMultiPointFromCoords(coordArray(0.5, 0.5, 1.0, 1.0, 3.5, 3.5, 4.0, 4.0)); + + String actualResult = Functions.split(multiLineString, multiPoint).norm().toText(); + String expectedResult = "MULTILINESTRING ((0 0, 0.5 0.5), (0.5 0.5, 1 1), (1 1, 1.5 1.5, 2 2), (3 3, 3.5 3.5), (3.5 3.5, 4 4), (4 4, 4.5 4.5, 5 5))"; + + assertEquals(actualResult, expectedResult); + } + + @Test + public void splitLineStringByMultiPointWithReverseOrder() { + LineString lineString = GEOMETRY_FACTORY.createLineString(coordArray(0.0, 0.0, 1.5, 1.5, 2.0, 2.0)); + MultiPoint multiPoint = GEOMETRY_FACTORY.createMultiPointFromCoords(coordArray(1.0, 1.0, 0.5, 0.5)); + + String actualResult = Functions.split(lineString, multiPoint).norm().toText(); + String expectedResult = "MULTILINESTRING ((0 0, 0.5 0.5), (0.5 0.5, 1 1), (1 1, 1.5 1.5, 2 2))"; + + assertEquals(actualResult, expectedResult); + } + + @Test + public void splitLineStringWithReverseOrderByMultiPoint() { + LineString lineString = GEOMETRY_FACTORY.createLineString(coordArray(2.0, 2.0, 1.5, 1.5, 0.0, 0.0)); + MultiPoint multiPoint = GEOMETRY_FACTORY.createMultiPointFromCoords(coordArray(0.5, 0.5, 1.0, 1.0)); + + String actualResult = Functions.split(lineString, multiPoint).norm().toText(); + String expectedResult = "MULTILINESTRING ((0 0, 0.5 0.5), (0.5 0.5, 1 1), (1 1, 1.5 1.5, 2 2))"; + + assertEquals(actualResult, expectedResult); + } + + @Test + public void splitLineStringWithVerticalSegmentByMultiPoint() { + LineString lineString = GEOMETRY_FACTORY.createLineString(coordArray(1.5, 0.0, 1.5, 1.5, 2.0, 2.0)); + MultiPoint multiPoint = GEOMETRY_FACTORY.createMultiPointFromCoords(coordArray(1.5, 0.5, 1.5, 1.0)); + + String actualResult = Functions.split(lineString, multiPoint).norm().toText(); + String expectedResult = "MULTILINESTRING ((1.5 0, 1.5 0.5), (1.5 0.5, 1.5 1), (1.5 1, 1.5 1.5, 2 2))"; + + assertEquals(actualResult, expectedResult); + } + + @Test + public void splitLineStringByLineString() { + LineString lineStringInput = GEOMETRY_FACTORY.createLineString(coordArray(0.0, 0.0, 2.0, 2.0)); + LineString lineStringBlade = GEOMETRY_FACTORY.createLineString(coordArray(1.0, 0.0, 1.0, 3.0)); + + String actualResult = Functions.split(lineStringInput, lineStringBlade).norm().toText(); + String expectedResult = "MULTILINESTRING ((0 0, 1 1), (1 1, 2 2))"; + + assertEquals(actualResult, expectedResult); + } + + @Test + public void splitLineStringByPolygon() { + LineString lineString = GEOMETRY_FACTORY.createLineString(coordArray(0.0, 0.0, 2.0, 2.0)); + Polygon polygon = GEOMETRY_FACTORY.createPolygon(coordArray(1.0, 0.0, 1.0, 3.0, 3.0, 3.0, 3.0, 0.0, 1.0, 0.0)); + + String actualResult = Functions.split(lineString, polygon).norm().toText(); + String expectedResult = "MULTILINESTRING ((0 0, 1 1), (1 1, 2 2))"; + + assertEquals(actualResult, expectedResult); + } + + @Test + public void splitPolygonByLineString() { + Polygon polygon = GEOMETRY_FACTORY.createPolygon(coordArray(1.0, 1.0, 5.0, 1.0, 5.0, 5.0, 1.0, 5.0, 1.0, 1.0)); + LineString lineString = GEOMETRY_FACTORY.createLineString(coordArray(3.0, 0.0, 3.0, 6.0)); + + String actualResult = Functions.split(polygon, lineString).norm().toText(); + String expectedResult = "MULTIPOLYGON (((1 1, 1 5, 3 5, 3 1, 1 1)), ((3 1, 3 5, 5 5, 5 1, 3 1)))"; + + assertEquals(actualResult, expectedResult); + } + + // // overlapping multipolygon by linestring + @Test + public void splitOverlappingMultiPolygonByLineString() { + Polygon[] polygons = new Polygon[]{ + GEOMETRY_FACTORY.createPolygon(coordArray(1.0, 1.0, 5.0, 1.0, 5.0, 5.0, 1.0, 5.0, 1.0, 1.0)), + GEOMETRY_FACTORY.createPolygon(coordArray(2.0, 1.0, 6.0, 1.0, 6.0, 5.0, 2.0, 5.0, 2.0, 1.0)) + }; + MultiPolygon multiPolygon = GEOMETRY_FACTORY.createMultiPolygon(polygons); + LineString lineString = GEOMETRY_FACTORY.createLineString(coordArray(3.0, 0.0, 3.0, 6.0)); + + String actualResult = Functions.split(multiPolygon, lineString).norm().toText(); + String expectedResult = "MULTIPOLYGON (((1 1, 1 5, 3 5, 3 1, 1 1)), ((2 1, 2 5, 3 5, 3 1, 2 1)), ((3 1, 3 5, 5 5, 5 1, 3 1)), ((3 1, 3 5, 6 5, 6 1, 3 1)))"; + + assertEquals(actualResult, expectedResult); + } + + @Test + public void splitPolygonByInsideRing() { + Polygon polygon = GEOMETRY_FACTORY.createPolygon(coordArray(1.0, 1.0, 5.0, 1.0, 5.0, 5.0, 1.0, 5.0, 1.0, 1.0)); + LineString lineString = GEOMETRY_FACTORY.createLineString(coordArray(2.0, 2.0, 3.0, 2.0, 3.0, 3.0, 2.0, 3.0, 2.0, 2.0)); + + String actualResult = Functions.split(polygon, lineString).norm().toText(); + String expectedResult = "MULTIPOLYGON (((1 1, 1 5, 5 5, 5 1, 1 1), (2 2, 3 2, 3 3, 2 3, 2 2)), ((2 2, 2 3, 3 3, 3 2, 2 2)))"; + + assertEquals(actualResult, expectedResult); + } + + @Test + public void splitPolygonByLineOutsideOfPolygon() { + Polygon polygon = GEOMETRY_FACTORY.createPolygon(coordArray(1.0, 1.0, 5.0, 1.0, 5.0, 5.0, 1.0, 5.0, 1.0, 1.0)); + LineString lineString = GEOMETRY_FACTORY.createLineString(coordArray(10.0, 10.0, 11.0, 11.0)); + + String actualResult = Functions.split(polygon, lineString).norm().toText(); + String expectedResult = "MULTIPOLYGON (((1 1, 1 5, 5 5, 5 1, 1 1)))"; + + assertEquals(actualResult, expectedResult); + } + + @Test + public void splitPolygonByPolygon() { + Polygon polygonInput = GEOMETRY_FACTORY.createPolygon(coordArray(0.0, 0.0, 4.0, 0.0, 4.0, 4.0, 0.0, 4.0, 0.0, 0.0)); + Polygon polygonBlade = GEOMETRY_FACTORY.createPolygon(coordArray(2.0, 0.0, 6.0, 0.0, 6.0, 4.0, 2.0, 4.0, 2.0, 0.0)); + + String actualResult = Functions.split(polygonInput, polygonBlade).norm().toText(); + String expectedResult = "MULTIPOLYGON (((0 0, 0 4, 2 4, 2 0, 0 0)), ((2 0, 2 4, 4 4, 4 0, 2 0)))"; + + assertEquals(actualResult, expectedResult); + } + + @Test + public void splitPolygonWithHoleByLineStringThroughHole() { + LinearRing shell = GEOMETRY_FACTORY.createLinearRing(coordArray(0.0, 0.0, 4.0, 0.0, 4.0, 4.0, 0.0, 4.0, 0.0, 0.0)); + LinearRing[] holes = new LinearRing[]{ + GEOMETRY_FACTORY.createLinearRing(coordArray(1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 2.0, 1.0, 1.0, 1.0)) + }; + Polygon polygon = GEOMETRY_FACTORY.createPolygon(shell, holes); + LineString lineString = GEOMETRY_FACTORY.createLineString(coordArray(1.5, -1.0, 1.5, 5.0)); + + String actualResult = Functions.split(polygon, lineString).norm().toText(); + String expectedResult = "MULTIPOLYGON (((0 0, 0 4, 1.5 4, 1.5 2, 1 2, 1 1, 1.5 1, 1.5 0, 0 0)), ((1.5 0, 1.5 1, 2 1, 2 2, 1.5 2, 1.5 4, 4 4, 4 0, 1.5 0)))"; + + assertEquals(actualResult, expectedResult); + } + + @Test + public void splitPolygonWithHoleByLineStringNotThroughHole() { + LinearRing shell = GEOMETRY_FACTORY.createLinearRing(coordArray(0.0, 0.0, 4.0, 0.0, 4.0, 4.0, 0.0, 4.0, 0.0, 0.0)); + LinearRing[] holes = new LinearRing[]{ + GEOMETRY_FACTORY.createLinearRing(coordArray(1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 2.0, 1.0, 1.0, 1.0)) + }; + Polygon polygon = GEOMETRY_FACTORY.createPolygon(shell, holes); + LineString lineString = GEOMETRY_FACTORY.createLineString(coordArray(3.0, -1.0, 3.0, 5.0)); + + String actualResult = Functions.split(polygon, lineString).norm().toText(); + String expectedResult = "MULTIPOLYGON (((0 0, 0 4, 3 4, 3 0, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1)), ((3 0, 3 4, 4 4, 4 0, 3 0)))"; + + assertEquals(actualResult, expectedResult); + } + + @Test + public void splitHomogeneousLinealGeometryCollectionByMultiPoint() { + LineString[] lineStrings = new LineString[]{ + GEOMETRY_FACTORY.createLineString(coordArray(0.0, 0.0, 1.5, 1.5, 2.0, 2.0)), + GEOMETRY_FACTORY.createLineString(coordArray(3.0, 3.0, 4.5, 4.5, 5.0, 5.0)) + }; + GeometryCollection geometryCollection = GEOMETRY_FACTORY.createGeometryCollection(lineStrings); + MultiPoint multiPoint = GEOMETRY_FACTORY.createMultiPointFromCoords(coordArray(0.5, 0.5, 1.0, 1.0, 3.5, 3.5, 4.0, 4.0)); + + String actualResult = Functions.split(geometryCollection, multiPoint).norm().toText(); + String expectedResult = "MULTILINESTRING ((0 0, 0.5 0.5), (0.5 0.5, 1 1), (1 1, 1.5 1.5, 2 2), (3 3, 3.5 3.5), (3.5 3.5, 4 4), (4 4, 4.5 4.5, 5 5))"; + + assertEquals(actualResult, expectedResult); + } + + @Test + public void splitHeterogeneousGeometryCollection() { + Geometry[] geometry = new Geometry[]{ + GEOMETRY_FACTORY.createLineString(coordArray(0.0, 0.0, 1.5, 1.5)), + GEOMETRY_FACTORY.createPolygon(coordArray(0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0)) + }; + GeometryCollection geometryCollection = GEOMETRY_FACTORY.createGeometryCollection(geometry); + LineString lineString = GEOMETRY_FACTORY.createLineString(coordArray(0.5, 0.0, 0.5, 5.0)); + + Geometry actualResult = Functions.split(geometryCollection, lineString); + + assertNull(actualResult); + } +} diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md index 82a104b91d..7e1de26a9d 100644 --- a/docs/api/sql/Function.md +++ b/docs/api/sql/Function.md @@ -317,7 +317,7 @@ WITH test_data as ( 'GEOMETRYCOLLECTION(POINT(40 10), POLYGON((0 0, 0 5, 5 5, 5 0, 0 0)))' ) as geom ) -SELECT ST_CollectionExtract(geom) as c1, ST_CollectionExtract(geom, 1) as c2 +SELECT ST_CollectionExtract(geom) as c1, ST_CollectionExtract(geom, 1) as c2 FROM test_data ``` @@ -422,7 +422,7 @@ Since: `v1.0.0` Spark SQL example: ```SQL -SELECT ST_DumpPoints(ST_GeomFromText('LINESTRING (0 0, 1 1, 1 0)')) +SELECT ST_DumpPoints(ST_GeomFromText('LINESTRING (0 0, 1 1, 1 0)')) ``` Output: `[POINT (0 0), POINT (0 1), POINT (1 1), POINT (1 0), POINT (0 0)]` @@ -1154,6 +1154,29 @@ SELECT ST_SimplifyPreserveTopology(polygondf.countyshape, 10.0) FROM polygondf ``` +## ST_Split + +Introduction: Split an input geometry by another geometry (called the blade). +Linear (LineString or MultiLineString) geometry can be split by a Point, MultiPoint, LineString, MultiLineString, Polygon, or MultiPolygon. +Polygonal (Polygon or MultiPolygon) geometry can be split by a LineString, MultiLineString, Polygon, or MultiPolygon. +In either case, when a polygonal blade is used then the boundary of the blade is what is actually split by. +ST_Split will always return either a MultiLineString or MultiPolygon even if they only contain a single geometry. +Homogeneous GeometryCollections are treated as a multi-geometry of the type it contains. +For example, if a GeometryCollection of only Point geometries is passed as a blade it is the same as passing a MultiPoint of the same geometries. + +Since: `v1.3.2` + +Format: `ST_Split (input: geometry, blade: geometry)` + +Spark SQL Example: +```SQL +SELECT ST_Split( + ST_GeomFromWKT('LINESTRING (0 0, 1.5 1.5, 2 2)'), + ST_GeomFromWKT('MULTIPOINT (0.5 0.5, 1 1)')) +``` + +Output: `MULTILINESTRING ((0 0, 0.5 0.5), (0.5 0.5, 1 1), (1 1, 1.5 1.5, 2 2))` + ## ST_SRID Introduction: Return the spatial refence system identifier (SRID) of the geometry. @@ -1274,12 +1297,12 @@ Table: +-------------------------------------------------------------+ |geometry | +-------------------------------------------------------------+ -|LINESTRING(0 0, 85 85, 100 100, 120 120, 21 21, 10 10, 5 5) | +|LINESTRING(0 0, 85 85, 100 100, 120 120, 21 21, 10 10, 5 5) | +-------------------------------------------------------------+ ``` Query -```SQL +```SQL select geom from geometries LATERAL VIEW ST_SubdivideExplode(geometry, 5) AS geom ``` @@ -1338,7 +1361,7 @@ Since: `v1.0.0` Spark SQL example (simple): ```SQL -SELECT ST_Transform(polygondf.countyshape, 'epsg:4326','epsg:3857') +SELECT ST_Transform(polygondf.countyshape, 'epsg:4326','epsg:3857') FROM polygondf ``` diff --git a/flink/src/main/java/org/apache/sedona/flink/Catalog.java b/flink/src/main/java/org/apache/sedona/flink/Catalog.java index 96f6b4eeb7..40e4c4dcae 100644 --- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java +++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java @@ -88,6 +88,7 @@ public static UserDefinedFunction[] getFuncs() { new Functions.ST_RemovePoint(), new Functions.ST_SetPoint(), new Functions.ST_LineFromMultiPoint(), + new Functions.ST_Split(), }; } diff --git a/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java b/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java index c104e9e0a1..198df1d346 100644 --- a/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java +++ b/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java @@ -478,4 +478,14 @@ public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.j return org.apache.sedona.common.Functions.lineFromMultiPoint(geom); } } + + public static class ST_Split extends ScalarFunction { + @DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class) + public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class) Object o1, + @DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class) Object o2) { + Geometry input = (Geometry) o1; + Geometry blade = (Geometry) o2; + return org.apache.sedona.common.Functions.split(input, blade); + } + } } diff --git a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java index cf04865d49..ab9c721b8c 100644 --- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java +++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java @@ -566,4 +566,10 @@ public void testLineFromMultiPoint() { Table pointTable = tableEnv.sqlQuery("SELECT ST_LineFromMultiPoint(ST_GeomFromWKT('MULTIPOINT((10 40), (40 30), (20 20), (30 10))'))"); assertEquals("LINESTRING (10 40, 40 30, 20 20, 30 10)", first(pointTable).getField(0).toString()); } + + @Test + public void testSplit() { + Table pointTable = tableEnv.sqlQuery("SELECT ST_Split(ST_GeomFromWKT('LINESTRING (0 0, 1.5 1.5, 2 2)'), ST_GeomFromWKT('MULTIPOINT (0.5 0.5, 1 1)'))"); + assertEquals("MULTILINESTRING ((0 0, 0.5 0.5), (0.5 0.5, 1 1), (1 1, 1.5 1.5, 2 2))", ((Geometry)first(pointTable).getField(0)).norm().toText()); + } } diff --git a/python/sedona/sql/st_functions.py b/python/sedona/sql/st_functions.py index a33b76bd76..43bb638c26 100644 --- a/python/sedona/sql/st_functions.py +++ b/python/sedona/sql/st_functions.py @@ -85,6 +85,7 @@ "ST_SetPoint", "ST_SetSRID", "ST_SRID", + "ST_Split", "ST_StartPoint", "ST_SubDivide", "ST_SubDivideExplode", @@ -105,7 +106,7 @@ _call_st_function = partial(call_sedona_function, "st_functions") - + @validate_argument_types def ST_3DDistance(a: ColumnOrName, b: ColumnOrName) -> Column: @@ -266,7 +267,7 @@ def ST_Boundary(geometry: ColumnOrName) -> Column: @validate_argument_types def ST_Buffer(geometry: ColumnOrName, buffer: ColumnOrNameOrNumber) -> Column: - """Calculate a geometry that represents all points whose distance from the + """Calculate a geometry that represents all points whose distance from the input geometry column is equal to or less than a given amount. :param geometry: Input geometry column to buffer. @@ -397,11 +398,11 @@ def ST_Distance(a: ColumnOrName, b: ColumnOrName) -> Column: def ST_Dump(geometry: ColumnOrName) -> Column: """Returns an array of geometries that are members of a multi-geometry or geometry collection column. If the input geometry is a regular geometry - then the geometry is returned inside of a single element array. + then the geometry is returned inside of a single element array. :param geometry: Geometry column to dump. :type geometry: ColumnOrName - :return: Array of geometries column comprised of the members of geometry. + :return: Array of geometries column comprised of the members of geometry. :rtype: Column """ return _call_st_function("ST_Dump", geometry) @@ -505,7 +506,7 @@ def ST_GeometryN(multi_geometry: ColumnOrName, n: Union[ColumnOrName, int]) -> C :type n: Union[ColumnOrName, int] :return: Geometry located at index n in multi_geometry as a geometry column. :rtype: Column - :raises ValueError: If + :raises ValueError: If """ if isinstance(n, int) and n < 0: raise ValueError(f"Index n for ST_GeometryN must by >= 0: {n} < 0") @@ -533,7 +534,7 @@ def ST_InteriorRingN(polygon: ColumnOrName, n: Union[ColumnOrName, int]) -> Colu :param n: Index of interior ring to return as either an integer or integer column, 0-th based. :type n: Union[ColumnOrName, int] :raises ValueError: If n is an integer and less than 0. - :return: Interior ring at index n as a linestring geometry column or null if n is greater than maximum index + :return: Interior ring at index n as a linestring geometry column or null if n is greater than maximum index :rtype: Column """ if isinstance(n, int) and n < 0: @@ -731,7 +732,7 @@ def ST_MinimumBoundingCircle(geometry: ColumnOrName, quadrant_segments: Optional def ST_MinimumBoundingRadius(geometry: ColumnOrName) -> Column: """Calculate the minimum bounding radius from the centroid of a geometry that will contain it. - :param geometry: Geometry column to generate minimum bounding radii for. + :param geometry: Geometry column to generate minimum bounding radii for. :type geometry: ColumnOrName :return: Struct column with a center field containing point geometry for the center of geometry and a radius field with a double value for the bounding radius. :rtype: Column @@ -973,6 +974,20 @@ def ST_SimplifyPreserveTopology(geometry: ColumnOrName, distance_tolerance: Colu return _call_st_function("ST_SimplifyPreserveTopology", (geometry, distance_tolerance)) +@validate_argument_types +def ST_Split(input: ColumnOrName, blade: ColumnOrName) -> Column: + """Split input geometry by the blade geometry. + + :param input: One geometry column to use. + :type input: ColumnOrName + :param blade: Other geometry column to use. + :type blase: ColumnOrName + :return: Multi-geometry representing the split of input by blade. + :rtype: Column + """ + return _call_st_function("ST_SymDifference", (input, blade)) + + @validate_argument_types def ST_SymDifference(a: ColumnOrName, b: ColumnOrName) -> Column: """Calculate the symmetric difference of two geometries (the regions that are only in one of them). @@ -999,7 +1014,7 @@ def ST_Transform(geometry: ColumnOrName, source_crs: ColumnOrName, target_crs: C :type target_crs: ColumnOrName :param disable_error: Whether to disable the error "Bursa wolf parameters required", defaults to None :type disable_error: Optional[Union[ColumnOrName, bool]], optional - :return: Geometry converted to the target coordinate system as an + :return: Geometry converted to the target coordinate system as an :rtype: Column """ args = (geometry, source_crs, target_crs) if disable_error is None else (geometry, source_crs, target_crs, disable_error) diff --git a/python/tests/sql/test_dataframe_api.py b/python/tests/sql/test_dataframe_api.py index bef6634b17..5094bbdde8 100644 --- a/python/tests/sql/test_dataframe_api.py +++ b/python/tests/sql/test_dataframe_api.py @@ -113,6 +113,7 @@ (stf.ST_SetPoint, ("line", 1, lambda: f.expr("ST_Point(1.0, 1.0)")), "linestring_geom", "", "LINESTRING (0 0, 1 1, 2 0, 3 0, 4 0, 5 0)"), (stf.ST_SetSRID, ("point", 3021), "point_geom", "ST_SRID(geom)", 3021), (stf.ST_SimplifyPreserveTopology, ("geom", 0.2), "0.9_poly", "", "POLYGON ((0 0, 1 0, 1 1, 0 0))"), + (stf.ST_Split, ("a", "b"), "overlapping_polys", "", "MULTIPOLYGON (((1 0, 0 0, 0 1, 1 1, 1 0)), ((2 0, 2 1, 3 1, 3 0, 2 0)))"), (stf.ST_SRID, ("point",), "point_geom", "", 0), (stf.ST_StartPoint, ("line",), "linestring_geom", "", "POINT (0 0)"), (stf.ST_SubDivide, ("line", 5), "linestring_geom", "", ["LINESTRING (0 0, 2.5 0)", "LINESTRING (2.5 0, 5 0)"]), @@ -318,13 +319,13 @@ def base_df(self, request): geojson = "{ \"type\": \"Feature\", \"properties\": { \"prop\": \"01\" }, \"geometry\": { \"type\": \"Point\", \"coordinates\": [ 0.0, 1.0 ] }}," gml_string = "-71.16,42.25 -71.17,42.25 -71.18,42.25" kml_string = "-71.16,42.26 -71.17,42.26" - + if request.param == "constructor": return TestDataFrameAPI.spark.sql("SELECT null").selectExpr( "0.0 AS x", "1.0 AS y", "'0.0,1.0' AS single_point", - "'0.0,0.0,1.0,0.0,1.0,1.0,0.0,0.0' AS multiple_point", + "'0.0,0.0,1.0,0.0,1.0,1.0,0.0,0.0' AS multiple_point", f"X'{wkb}' AS wkb", f"'{geojson}' AS geojson", "'s00twy01mt' AS geohash", diff --git a/sql/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala b/sql/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala index 3f467aec6d..6bc637b026 100644 --- a/sql/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala +++ b/sql/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala @@ -139,6 +139,7 @@ object Catalog { function[ST_LineFromMultiPoint](), function[ST_MPolyFromText](0), function[ST_MLineFromText](0), + function[ST_Split](), // Expression for rasters function[RS_NormalizedDifference](), function[RS_Mean](), diff --git a/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala b/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala index 5e5ad11b49..5fdcc58619 100644 --- a/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala +++ b/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala @@ -1042,3 +1042,16 @@ case class ST_LineFromMultiPoint(inputExpressions: Seq[Expression]) copy(inputExpressions = newChildren) } } + +/** + * Returns a multi-geometry that is the result of splitting the input geometry by the blade geometry + * + * @param inputExpressions + */ +case class ST_Split(inputExpressions: Seq[Expression]) + extends InferredBinaryExpression(Functions.split) with FoldableExpression { + + protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = { + copy(inputExpressions = newChildren) + } +} diff --git a/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala b/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala index 16073e9315..48c5a446cd 100644 --- a/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala +++ b/sql/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala @@ -86,7 +86,7 @@ object st_functions extends DataFrameAPI { def ST_ConvexHull(geometry: Column): Column = wrapExpression[ST_ConvexHull](geometry) def ST_ConvexHull(geometry: String): Column = wrapExpression[ST_ConvexHull](geometry) - + def ST_Difference(a: Column, b: Column): Column = wrapExpression[ST_Difference](a, b) def ST_Difference(a: String, b: String): Column = wrapExpression[ST_Difference](a, b) @@ -110,7 +110,7 @@ object st_functions extends DataFrameAPI { def ST_FlipCoordinates(geometry: Column): Column = wrapExpression[ST_FlipCoordinates](geometry) def ST_FlipCoordinates(geometry: String): Column = wrapExpression[ST_FlipCoordinates](geometry) - + def ST_Force_2D(geometry: Column): Column = wrapExpression[ST_Force_2D](geometry) def ST_Force_2D(geometry: String): Column = wrapExpression[ST_Force_2D](geometry) @@ -131,7 +131,7 @@ object st_functions extends DataFrameAPI { def ST_IsClosed(geometry: Column): Column = wrapExpression[ST_IsClosed](geometry) def ST_IsClosed(geometry: String): Column = wrapExpression[ST_IsClosed](geometry) - + def ST_IsEmpty(geometry: Column): Column = wrapExpression[ST_IsEmpty](geometry) def ST_IsEmpty(geometry: String): Column = wrapExpression[ST_IsEmpty](geometry) @@ -176,7 +176,7 @@ object st_functions extends DataFrameAPI { def ST_MinimumBoundingRadius(geometry: Column): Column = wrapExpression[ST_MinimumBoundingRadius](geometry) def ST_MinimumBoundingRadius(geometry: String): Column = wrapExpression[ST_MinimumBoundingRadius](geometry) - + def ST_Multi(geometry: Column): Column = wrapExpression[ST_Multi](geometry) def ST_Multi(geometry: String): Column = wrapExpression[ST_Multi](geometry) @@ -194,10 +194,10 @@ object st_functions extends DataFrameAPI { def ST_NumInteriorRings(geometry: Column): Column = wrapExpression[ST_NumInteriorRings](geometry) def ST_NumInteriorRings(geometry: String): Column = wrapExpression[ST_NumInteriorRings](geometry) - + def ST_PointN(geometry: Column, n: Column): Column = wrapExpression[ST_PointN](geometry, n) def ST_PointN(geometry: String, n: Int): Column = wrapExpression[ST_PointN](geometry, n) - + def ST_PointOnSurface(geometry: Column): Column = wrapExpression[ST_PointOnSurface](geometry) def ST_PointOnSurface(geometry: String): Column = wrapExpression[ST_PointOnSurface](geometry) @@ -206,7 +206,7 @@ object st_functions extends DataFrameAPI { def ST_RemovePoint(lineString: Column, index: Column): Column = wrapExpression[ST_RemovePoint](lineString, index) def ST_RemovePoint(lineString: String, index: Int): Column = wrapExpression[ST_RemovePoint](lineString, index) - + def ST_Reverse(geometry: Column): Column = wrapExpression[ST_Reverse](geometry) def ST_Reverse(geometry: String): Column = wrapExpression[ST_Reverse](geometry) @@ -230,7 +230,10 @@ object st_functions extends DataFrameAPI { def ST_SimplifyPreserveTopology(geometry: Column, distanceTolerance: Column): Column = wrapExpression[ST_SimplifyPreserveTopology](geometry, distanceTolerance) def ST_SimplifyPreserveTopology(geometry: String, distanceTolerance: Double): Column = wrapExpression[ST_SimplifyPreserveTopology](geometry, distanceTolerance) - + + def ST_Split(input: Column, blade: Column): Column = wrapExpression[ST_Split](input, blade) + def ST_Split(input: String, blade: String): Column = wrapExpression[ST_Split](input, blade) + def ST_SymDifference(a: Column, b: Column): Column = wrapExpression[ST_SymDifference](a, b) def ST_SymDifference(a: String, b: String): Column = wrapExpression[ST_SymDifference](a, b) @@ -238,16 +241,16 @@ object st_functions extends DataFrameAPI { def ST_Transform(geometry: String, sourceCRS: String, targetCRS: String): Column = wrapExpression[ST_Transform](geometry, sourceCRS, targetCRS, false) def ST_Transform(geometry: Column, sourceCRS: Column, targetCRS: Column, disableError: Column): Column = wrapExpression[ST_Transform](geometry, sourceCRS, targetCRS, disableError) def ST_Transform(geometry: String, sourceCRS: String, targetCRS: String, disableError: Boolean): Column = wrapExpression[ST_Transform](geometry, sourceCRS, targetCRS, disableError) - + def ST_Union(a: Column, b: Column): Column = wrapExpression[ST_Union](a, b) def ST_Union(a: String, b: String): Column = wrapExpression[ST_Union](a, b) - + def ST_X(point: Column): Column = wrapExpression[ST_X](point) def ST_X(point: String): Column = wrapExpression[ST_X](point) def ST_XMax(geometry: Column): Column = wrapExpression[ST_XMax](geometry) def ST_XMax(geometry: String): Column = wrapExpression[ST_XMax](geometry) - + def ST_XMin(geometry: Column): Column = wrapExpression[ST_XMin](geometry) def ST_XMin(geometry: String): Column = wrapExpression[ST_XMin](geometry) diff --git a/sql/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala b/sql/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala index 7d2deca7ab..2ae624919d 100644 --- a/sql/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala +++ b/sql/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala @@ -221,7 +221,7 @@ class dataFrameAPITestScala extends TestBaseScala { } it("Passed ST_Distance") { - val pointDf = sparkSession.sql("SELECT ST_Point(0.0, 0.0) AS a, ST_Point(1.0, 0.0) as b") + val pointDf = sparkSession.sql("SELECT ST_Point(0.0, 0.0) AS a, ST_Point(1.0, 0.0) as b") val df = pointDf.select(ST_Distance("a", "b")) val actualResult = df.take(1)(0).get(0).asInstanceOf[Double] val expectedResult = 1.0 @@ -367,7 +367,7 @@ class dataFrameAPITestScala extends TestBaseScala { val df = sridPointDf.select(ST_AsEWKB("geom")) val actualResult = Hex.encodeHexString(df.take(1)(0).get(0).asInstanceOf[Array[Byte]]) val expectedResult = "0101000020cd0b000000000000000000000000000000000000" - assert(actualResult == expectedResult) + assert(actualResult == expectedResult) } it("Passed ST_NPoints") { @@ -624,10 +624,10 @@ class dataFrameAPITestScala extends TestBaseScala { val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('LINESTRING (0 0, 1 0)') AS geom") val df = baseDf.select(ST_MinimumBoundingRadius("geom").as("c")).select("c.center", "c.radius") val rowResult = df.take(1)(0) - + val actualCenter = rowResult.get(0).asInstanceOf[Geometry].toText() val actualRadius = rowResult.getDouble(1) - + val expectedCenter = "POINT (0.5 0)" val expectedRadius = 0.5 @@ -770,6 +770,18 @@ class dataFrameAPITestScala extends TestBaseScala { assert(actualResult == expectedResult) } + it("Passed ST_Split") { + val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('LINESTRING (0 0, 1.5 1.5, 2 2)') AS input, ST_GeomFromWKT('MULTIPOINT (0.5 0.5, 1 1)') AS blade") + var df = baseDf.select(ST_Split("input", "blade")) + var actualResult = df.take(1)(0).get(0).asInstanceOf[Geometry].toText() + val expectedResult = "MULTILINESTRING ((0 0, 0.5 0.5), (0.5 0.5, 1 1), (1 1, 1.5 1.5, 2 2))" + assert(actualResult == expectedResult) + + df = baseDf.select(ST_Split($"input", $"blade")) + actualResult = df.take(1)(0).get(0).asInstanceOf[Geometry].toText() + assert(actualResult == expectedResult) + } + // predicates it("Passed ST_Contains") { val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('POLYGON ((0 0, 1 0, 1 1, 0 0))') AS a, ST_Point(0.5, 0.25) AS b") diff --git a/sql/src/test/scala/org/apache/sedona/sql/functionTestScala.scala b/sql/src/test/scala/org/apache/sedona/sql/functionTestScala.scala index 784a2200a5..2fadafd1b6 100644 --- a/sql/src/test/scala/org/apache/sedona/sql/functionTestScala.scala +++ b/sql/src/test/scala/org/apache/sedona/sql/functionTestScala.scala @@ -1791,4 +1791,21 @@ class functionTestScala extends TestBaseScala with Matchers with GeometrySample assert(result.head.get(0).asInstanceOf[String]==expectedGeom) } } + + it ("Should pass ST_Split") { + val geomTestCases = Map( + ("'LINESTRING (0 0, 1.5 1.5, 2 2)'", "'MULTIPOINT (0.5 0.5, 1 1)'") + -> "MULTILINESTRING ((0 0, 0.5 0.5), (0.5 0.5, 1 1), (1 1, 1.5 1.5, 2 2))", + ("null", "'MULTIPOINT (0.5 0.5, 1 1)'") + -> null, + ("'LINESTRING (0 0, 1.5 1.5, 2 2)'", "null") + -> null + ) + for(((target, blade), expected) <- geomTestCases) { + var df = sparkSession.sql(s"SELECT ST_Split(ST_GeomFromText($target), ST_GeomFromText($blade))") + var result = df.take(1)(0).get(0).asInstanceOf[Geometry] + var textResult = if (result == null) null else result.toText + assert(textResult==expected) + } + } }