/* Spatial Operations & Editing Tools for uDig * * Axios Engineering under a funding contract with: * Wien Government * * http://wien.gov.at * http://www.axios.es * * (C) 2010, Vienna City - Municipal Department of Automated Data Processing, * Information and Communications Technologies. * Vienna City agrees to license under Lesser General Public License (LGPL). * * You can redistribute it and/or modify it under the terms of the * GNU Lesser General Public License as published by the Free Software * Foundation; version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package es.axios.lib.geometry.split; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import com.vividsolutions.jts.algorithm.CGAlgorithms; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.CoordinateArrays; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.LinearRing; import com.vividsolutions.jts.geom.MultiLineString; import com.vividsolutions.jts.geom.Polygon; import es.axios.lib.geometry.util.GeometryList; import es.axios.lib.geometry.util.GeometrySet; /** * <p> * Builds the useful split line. The useful split line is composed of split line fragments that intersect with the * polygon to split. Thus, the result of build will be presented as a {@link MultiLineString}, where each line intersect with * the polygon. * </p> * <p> * As subproduct, the build process provides the set of "Non Split Rings" and the {@link AdaptedPolygon}. * Non split rings are that rings which are not affected by the split line. The {@link AdaptedPolygon} * contains those additional vertexes resultant of the intersection of the polygon's boundaries with * the split line. * </p> * * @author Mauricio Pazos (www.axios.es) * @author Aritz Davila (www.axios.es) * @since 1.3.0 */ class UsefulSplitLineBuilder { /** Lines with less length than this, will be depreciated. */ public static final double DEPRECIATE_VALUE = 1.0E-4; /** The position of the initial point respect the polygon. */ private static final int OUTSIDE = 1; private static final int INSIDE = -1; private static final int NONE = 0; /** preserves the original split line */ private final LineString originalSplitLine; /** Store the lineStrings that fully intersect with the polygons and will form new features. */ private List<Geometry> usefulSplitLineSegments = new GeometryList<Geometry>(); /** The polygon with the start intersection coordinates added on it. */ private AdaptedPolygon adaptedPolygon = null; private final GeometryFactory geomFactory; /** * Will store the rings that won't be modified by the split operation. */ private Set<LinearRing> nonSplitRings = new GeometrySet<LinearRing>(); /** Maintin the final result, this is the useful split line. */ private Geometry resultSplitLine = null; /** * @return rings that won't be modified by the split operation. */ public Set<LinearRing> getNonSplitRings() { return this.nonSplitRings; } /** * @return the original split line */ public LineString getOriginalSplitLine() { return this.originalSplitLine; } /** * @return the useful split line */ public Geometry getResultSplitLine() { return resultSplitLine; } /** * Assure the first coordinate lies outside the polygon. * * @param modifiedCoords * The split line coordinates. * @param extBoundary * The exterior of the polygon. * @return An array of coordinates which first coordinate start inside the * ring. */ private Coordinate[] beginOutside(Coordinate[] modifiedCoords, Geometry extBoundary) { // get the first position inside the are of the ring. Geometry polygonArea = this.geomFactory.createPolygon((LinearRing) extBoundary, null); int outsidePos = -1; // find the first point that lie inside the area. for (int a = 0; a < modifiedCoords.length; a++) { Coordinate point = modifiedCoords[a]; if (!polygonArea.contains(this.geomFactory.createPoint(point))) { // thats the first point. // from that coordinate seek if there are 2 // intersections with the interior boundary. outsidePos = a; break; } } // get the first intersection. IntersectionStrategy interStrategy = new IntersectionStrategy(); interStrategy.findFirstIntersection(modifiedCoords, extBoundary); Coordinate firstIntersectionCoord = interStrategy.getIntersection(); int firstIntersectionPosition = interStrategy.getIntersectionPosition(); // if there isn't any coordinate outside, and there isn't any // intersection with the exterior boundary, return. if (outsidePos == -1 && ! interStrategy.foundIntersection()) { return modifiedCoords; } int posFirstCoord; // calculate which one go first, and start from that coordinate. if (outsidePos > firstIntersectionPosition || outsidePos == -1) { // this coordinate must be eliminated from the array. modifiedCoords[firstIntersectionPosition] = firstIntersectionCoord; posFirstCoord = firstIntersectionPosition; } else { posFirstCoord = outsidePos; } modifiedCoords = SplitUtil.createCoordinateArrayWithoutDuplicated(posFirstCoord, modifiedCoords.length - 1, modifiedCoords); return modifiedCoords; } /** * @see {{@link #newInstance()} */ private UsefulSplitLineBuilder(final LineString splitLine){ this.originalSplitLine = splitLine; this.geomFactory = splitLine.getFactory(); } /** * * @return New instance of {@link UsefulSplitLineBuilder} */ public static final UsefulSplitLineBuilder newInstance(final LineString splitLine){ UsefulSplitLineBuilder splitWrapper = new UsefulSplitLineBuilder(splitLine); return splitWrapper; } /** * This method will create a new lineString. This lineString will be the * useful part to be used when creating the graph. * * @param polygon * @return The useful part of the lineString, that means, the part of the * line that intersects with the feature and its interior. Null if there * is not intersection. */ public void build(final Polygon polygon) { this.adaptedPolygon = new AdaptedPolygon(polygon); Geometry usefulSplitLines = createUsefulSplitLine(this.originalSplitLine, this.adaptedPolygon); assert usefulSplitLines != null: "must be at least one split line."; //$NON-NLS-1$ this.resultSplitLine = null; if (usefulSplitLines instanceof LineString) { List<Geometry> ccwLines = new ArrayList<Geometry>(); for (int i = 0; i < usefulSplitLines.getNumGeometries(); i++) { // get the line and check if it is CCW oriented, if not, try to // orient it. If its colinear should be added without the isCCW // analysis. Geometry actual = usefulSplitLines.getGeometryN(i); Coordinate[] lineCoord = actual.getCoordinates(); if (lineCoord.length > 2 && !SplitUtil.isColinear(lineCoord)) { // close the line to obtain a ring and check the CCW. Coordinate[] closed = RingUtil.builRing(lineCoord); if (!CGAlgorithms.isCCW(closed)) { actual = reverseLineString(closed); assert CGAlgorithms.isCCW(actual.getCoordinates()) : "It should be CCW. Actual SplitLine: " + actual + ". Geometry to split: " + polygon; //$NON-NLS-1$ //$NON-NLS-2$ actual = reverseLineString(lineCoord); } } ccwLines.add(actual); } this.resultSplitLine = usefulSplitLines.getFactory().buildGeometry(ccwLines); } else { // merge the pieces of lines if its possible. this.resultSplitLine = SplitUtil.buildLineUnion(usefulSplitLines); } } /** * Reverse the given coordinates, and return the resultant lineString. * * @param intersectionCoordinates * Coordinates from a lineString. * @param geomFactory * Geometry factory. * @return The geometry built. */ private Geometry reverseLineString(Coordinate[] intersectionCoordinates) { intersectionCoordinates = CoordinateArrays.copyDeep(intersectionCoordinates); CoordinateArrays.reverse(intersectionCoordinates); return geomFactory.createLineString(intersectionCoordinates); } /** * <pre> * First step: * With the first step, the line is reduced, discarding the points * that won't do anything. * * -Seek the first coordinate outside the boundary. * -Seek the first intersection with the boundary. * -If the first intersection is before the first coordinate outside the * line, that coordinate will be the beginning of the line. * * Second step: * * Finding out where the first coordinate lies. * * -If it lies in the exterior of the polygon, go through the line finding * 2 intersections with the exterior boundary, get the lineString that is formed between those intersections * and reduce the line. Do this until it doesn't found 2 intersections. * After that, with the remaining line do the same but intersection with the interior. * * -If it lies inside a ring, first, sort the rings in the order the split line * is intersection them, then go through the line finding * 2 intersections with the interior boundary, get the lineString that is formed between those intersections * and reduce the line. Do this until it doesn't found 2 intersections. * After that, with the remaining line do the same but intersection with the exterior. * </pre> * * @param splitLine The lineString that would be checked. * @param adaptedPolygon (i/o) The polygon for checking. * * @return The collection of pieces of split line. Null if there is not an useful split line part */ public Geometry createUsefulSplitLine( final LineString splitLine, final AdaptedPolygon adaptedPolygon) { // does a defensive copy of coordinate final Coordinate[] clonedSplitLineCoord = cloneCoordinate(splitLine.getCoordinates()); // The line is reduced, discarding the points that won't split the polygon. Coordinate[] adaptedSplitLine = discardSplitLineSegmentAdaptingPolygon(clonedSplitLineCoord, splitLine , adaptedPolygon); UsefulSplitLine useful = new UsefulSplitLine(this.usefulSplitLineSegments, adaptedSplitLine); // finds where the first coordinate lies. It could be outside the // feature, or if it has rings, it could lie inside a ring. int location = whereIsFirstSplitLineCoord(adaptedSplitLine, adaptedPolygon); if (location == OUTSIDE) { // start from the outside of the polygon and go through the line. useful = makeUsefulSplitLineFromOutsidePolygon(adaptedPolygon, useful); // once it done, if still are coordinates for going through // them, check if there are some holes intersection. if (useful.getRemaininSplitLine().length > 0 && adaptedPolygon.asPolygon().getNumInteriorRing() > 0) { useful =makeUsefulSplitLineFromInsidePolygon(adaptedPolygon, useful); } this.usefulSplitLineSegments = useful.getUsefulSplitLineFragments(); this.nonSplitRings = extractNonSplitHolesFromPolygon(adaptedPolygon, this.usefulSplitLineSegments, this.nonSplitRings ); } else { // start from the inside of one of the rings. useful = makeUsefulSplitLineFromInsidePolygon(adaptedPolygon, useful); this.usefulSplitLineSegments = useful.getUsefulSplitLineFragments(); adaptedSplitLine = useful.getRemaininSplitLine(); if (adaptedSplitLine.length > 1) { useful = makeUsefulSplitLineFromOutsidePolygon(adaptedPolygon, useful); this.usefulSplitLineSegments = useful.getUsefulSplitLineFragments(); } } if( this.usefulSplitLineSegments.size() > 0 ){ return geomFactory.buildGeometry(this.usefulSplitLineSegments); } else { return null; } } /** * Will get a ring, and check if that ring is intersected with any of the * lines. If it doesn't intersect, this ring will be a non-split ring. * * @param adapted * The original polygon. * @param splitLineSegments * Geometry factory. * @param nonSplitRings */ private Set<LinearRing> extractNonSplitHolesFromPolygon( final AdaptedPolygon adapted, final List<Geometry> splitLineSegments, Set<LinearRing> nonSplitRings) { Polygon polygon = adapted.asPolygon(); // get the hole rings, and check if any ring is never intersected by those // lines. for (int j = 0; j < polygon.getNumInteriorRing(); j++) { LinearRing holeRing = (LinearRing) polygon.getInteriorRingN(j); // create a polygonRing with the purpose off testing touch between // polygon-line. Polygon polygonRing = this.geomFactory.createPolygon((LinearRing) holeRing, null); boolean intersects = false; // check this ring with all the lines finding if they intersect. for (int i = 0; i < splitLineSegments.size(); i++) { Geometry eachLine = splitLineSegments.get(i); // the intersection between the line and the boundary // must be a point or multiPoint, and not a LineString Geometry intersection = eachLine.intersection(holeRing); if (eachLine.intersects(holeRing) && !(intersection instanceof LineString || intersection instanceof MultiLineString) && !eachLine.touches(polygonRing)) { intersects = true; break; } } // if this ring isn't intersected by any line, add to the nonSplit // lines. if (!intersects) { nonSplitRings.add(holeRing); } } return nonSplitRings; } /** * <pre> * * Sort the interior rings. The first ring that intersects with the split * line and is closest to the beginning of the split line, that will be the * first ring. * * Go through the line, intersecting it with the interior rings of the * polygon. Start inside a ring, find 2 intersection, and the * coordinates between that 2 intersections will form a piece of line. Do * this until the line hasn't more coordinates, or there aren't 2 * intersections or more. * * The rings that aren't modified by the split line, will be added * to the non-split rings list. * * </pre> * * @param adapt * (i/o) The polygon feature. * @param usefulSplitLine * (i/o) useful Split line. * @return The list of useful split line. */ private UsefulSplitLine makeUsefulSplitLineFromInsidePolygon(AdaptedPolygon adapt, UsefulSplitLine usefulSplitLine) { // go through the line finding intersections with rings and sorting Polygon polygon = adapt.asPolygon(); Coordinate[] splitLineCoords = usefulSplitLine.getRemaininSplitLine(); Set<LinearRing> sortedRings = sortRing(splitLineCoords, polygon); // now that rings are ordered, start filtering the line. If a piece // of line with a ring will form a valid new geometry, separate that // piece of line. for (LinearRing hole : sortedRings) { if (splitLineCoords.length > 1) { // the coordinates will begin inside the ring. splitLineCoords = beginInsideRing(splitLineCoords, hole); // a valid hole must be intersected by the line twice. // find interior intersections. int numIntIntersection = SplitUtil.countIntersectionsFromPosition(0, splitLineCoords, hole); if (numIntIntersection >= 2) { usefulSplitLine = extractUsefulSplitLine(hole, usefulSplitLine, adapt); splitLineCoords = usefulSplitLine.getRemaininSplitLine(); } else { // not valid ring. Add to the list. this.nonSplitRings.add(hole); } } else { this.nonSplitRings.add(hole); } } // There are some rings that had been added to nonSplitRings // because the line was entirely covered, but those rings, intersect // with the line and the haven't got a chance of been tested. // Check if they intersect with any of the rings from the nonSplitRings. List<Geometry> usefulSplitLineSegments = usefulSplitLine.getUsefulSplitLineFragments(); List<LinearRing> correctSplitRings = new ArrayList<LinearRing>(); for (Geometry piece : usefulSplitLineSegments) { for (LinearRing hole : this.nonSplitRings) { // create a polygonRing with the purpose off testing touch // between polygon-line. Polygon polygonRing = geomFactory.createPolygon((LinearRing) hole, null); if (piece.intersects(hole) && !piece.touches(polygonRing)) { // remove that hole from the list, it is OK. correctSplitRings.add(hole); } } } this.nonSplitRings.removeAll((Collection< ? >) correctSplitRings); usefulSplitLine.setRemaininSplitLine(splitLineCoords); usefulSplitLine.setUsefulSplitLineFragments(usefulSplitLineSegments); return usefulSplitLine; } /** * <pre> * Sort the interior rings. * * The first ring will be the one that intersects first and is nearest * to the beginning of the split line, that in this case is the shortCoords[0]. * * </pre> * * @param shortCoords * @param polygon * @return The interior rings sorted. */ private Set<LinearRing> sortRing(Coordinate[] shortCoords, Polygon polygon) { Set<LinearRing> sortedRings = new LinkedHashSet<LinearRing>(); for (int i = 0; i < shortCoords.length - 1; i++) { Coordinate[] lineSegment = new Coordinate[2]; lineSegment[0] = shortCoords[i]; lineSegment[1] = shortCoords[i + 1]; LineString segment = this.geomFactory.createLineString(lineSegment); // intersects this segment with any of the rings, if intersects, // thats ring goes first. List<LinearRing> rings = SplitUtil.addRingThatIntersects(segment, polygon); // go through the rings, and for each one, get the intersection, and // calculate which one is the nearest respect the beginning of the // segment. Map<LinearRing, Double> ringsDistance = RingUtil.setRingWithClosestIntersectionDistance(rings, segment, lineSegment); sortedRings = addClosestRings(sortedRings, ringsDistance); } return sortedRings; } /** * <pre> * Recursive function. * Go through ringsDistance map getting each ring-distance and calculating * which is the lowest distance. At the end, add the closest ring to the sortedRing set, * remove this ring from the map, and call again the function, until the map is empty. * * * </pre> * * @param sortedRings * Set of ring sorted. * @param ringsDistance * Map with rings linked with closest distance. * @return Set of rings, ordered depending the given distance. */ private Set<LinearRing> addClosestRings(Set<LinearRing> sortedRings, Map<LinearRing, Double> ringsDistance) { if (ringsDistance.size() > 0) { // calculate which one has the closest distance Set<Entry<LinearRing, Double>> entrySet = ringsDistance.entrySet(); Iterator<Entry<LinearRing, Double>> iterator = entrySet.iterator(); double minDistance = Double.MAX_VALUE; LinearRing candidateRing = null; while (iterator.hasNext()) { Entry<LinearRing, Double> eachEntry = iterator.next(); Double distance = eachEntry.getValue(); if (Math.abs(distance) < minDistance) { minDistance = Math.abs(distance); candidateRing = eachEntry.getKey(); } } sortedRings.add( candidateRing); ringsDistance.remove(candidateRing); sortedRings = addClosestRings(sortedRings, ringsDistance); } return sortedRings; } /** * Assures the first coordinate lies inside any of the polygon rings. * * @param modifiedCoords * The split line coordinates. * @param gf * Geometry factory. * @param ring * An interior ring. * @return An array of coordinates which first coordinate start inside the * ring. */ private Coordinate[] beginInsideRing(Coordinate[] modifiedCoords, Geometry ring) { // get the first position inside the are of the ring. Polygon ringArea = this.geomFactory.createPolygon((LinearRing) ring, null); int outsidePos = -1; // find the first point that lie inside the area or on the boundary. for (int a = 0; a < modifiedCoords.length; a++) { Coordinate point = modifiedCoords[a]; if (ringArea.contains(this.geomFactory.createPoint(point)) || ringArea.getExteriorRing().contains(geomFactory.createPoint(point))) { // thats the first point. // from that coordinate seek if there are 2 // intersections with the interior boundary. outsidePos = a; break; } } // get the first intersection. IntersectionStrategy interStrategy = new IntersectionStrategy(); interStrategy.findFirstIntersection(modifiedCoords, ring); Coordinate firstIntersectionCoord = interStrategy.getIntersection(); int firstIntersectionPosition = interStrategy.getIntersectionPosition(); // if there isn't any coordinate inside, and there isn't any // intersection with this interior ring, return. if (outsidePos == -1 && !interStrategy.foundIntersection()) { return modifiedCoords; } int posFirstCoord; // calculate which one go first, and start from that coordinate. if (outsidePos > firstIntersectionPosition || outsidePos == -1) { // this coordinate must be eliminated from the array. modifiedCoords[firstIntersectionPosition] = firstIntersectionCoord; posFirstCoord = firstIntersectionPosition; } else { posFirstCoord = outsidePos; } modifiedCoords = SplitUtil.createCoordinateArrayWithoutDuplicated(posFirstCoord, modifiedCoords.length - 1, modifiedCoords); return modifiedCoords; } /** * <pre> * * Go through the line, intersecting it with the exterior of the polygon. * Start outside the polygon, find 2 intersection, and the coordinates * between that 2 intersections will form a piece of line. Do this until the * line hasn't more coordinates, or there aren't 2 intersections or more. * Finally, the remaining coordinates of the split line are returned. * * </pre> * * @param adapted * (i/o)The polygon feature. * @param remainingCoords * (i/o) The split line coordinates. * * @return The remaining coordinates of the split line. */ private UsefulSplitLine makeUsefulSplitLineFromOutsidePolygon( AdaptedPolygon adapted, UsefulSplitLine useful) { Coordinate[] remainingCoords = useful.getRemaininSplitLine(); Polygon polygon = adapted.asPolygon(); LineString extBoundary = polygon.getExteriorRing(); // start from the outside of the geometry. remainingCoords = beginOutside(remainingCoords, extBoundary); int numIntIntersection = SplitUtil.countIntersectionsFromPosition(0, remainingCoords, extBoundary); if (numIntIntersection >= 2) { useful.setRemaininSplitLine(remainingCoords); useful = extractUsefulSplitLine((LinearRing) extBoundary, useful, adapted); } return useful; } /** * Discards those segments of the line which won't affect the polygon. * * @param pieceOfBoundary Boundary of the polygon. It could be the exterior ring or internal ring * @param usefulSplitLine * @param adaptedPolygon (i/o) additional vertex are added as result of this method. * * * @return The list of useful split line. */ private UsefulSplitLine extractUsefulSplitLine( final LinearRing pieceOfBoundary, final UsefulSplitLine usefulSplitLine, AdaptedPolygon adaptedPolygon) { Coordinate[] remainingSplitLine = usefulSplitLine.getRemaininSplitLine(); List<Geometry> usefulSplitLineFragmentList = usefulSplitLine.getUsefulSplitLineFragments(); // does a defensive copy of adapted polygon. The adapted polygon will be modified only if // the piece of boundary intersect with a fragment of useful split line (that intersect with the polygon). AdaptedPolygon clonedPolygon = (AdaptedPolygon) adaptedPolygon.clone(); Polygon polygon = clonedPolygon.asPolygon(); // Search the pieces of split line that are between the first and second intersection. // If that piece of split line intersect with the polygon it is an useful line part. int secondSegmentPosition = 0; Coordinate secondIntersection= null; LineBoundaryIntersectionAssociation interSegmentList = new LineBoundaryIntersectionAssociation(remainingSplitLine.clone(), pieceOfBoundary); while( ((interSegmentList.countIntersections() - interSegmentList.getVisitedIntersection() ) >= 2) ){ // search the next two intersections. interSegmentList.moveNextIntersection(); int firstSegmentPosition = interSegmentList.getIntersectionSegmentPosition(); final Coordinate firstIntersection = interSegmentList.getIntersection(); final LineString firstRingSegment = interSegmentList.getRingSegment(); interSegmentList.moveNextIntersection(); secondSegmentPosition = interSegmentList.getIntersectionSegmentPosition(); secondIntersection = interSegmentList.getIntersection(); final LineString secondRingSegment = interSegmentList.getRingSegment(); // adapt the polygon inserting the intersection points clonedPolygon.insertVertex(pieceOfBoundary, firstRingSegment, firstIntersection ); clonedPolygon.insertVertex(pieceOfBoundary, secondRingSegment, secondIntersection ); polygon = clonedPolygon.asPolygon(); // build the candidate split line fragment LineString candidateSplitLineFragment = interSegmentList.buildLineBetweenIntersectionPoints(firstSegmentPosition, firstIntersection, secondSegmentPosition, secondIntersection); // if the pieceCoords intersects with the polygon, then add in the extractedSegmentLines list if( segmentsIntersectsPolygon(candidateSplitLineFragment, polygon) ){ usefulSplitLineFragmentList.add(candidateSplitLineFragment); // update the resultant adapted polygon with those intersection point that below to the useful segment adaptedPolygon.insertVertex(pieceOfBoundary, firstRingSegment, firstIntersection ); adaptedPolygon.insertVertex(pieceOfBoundary, secondRingSegment, secondIntersection ); } // move to previous intersection in order to analyze the next interSegmentList.moveBackIntersection(); } // end while // remove the processed split line fragment from remaining split line remainingSplitLine = removeProcessedSegment(interSegmentList, secondSegmentPosition, secondIntersection, remainingSplitLine); // set the resultant split line fragments and the rest of line usefulSplitLine.setUsefulSplitLineFragments(usefulSplitLineFragmentList); usefulSplitLine.setRemaininSplitLine(remainingSplitLine); return usefulSplitLine; } /** * Remove the part of split in the position indeed from the intersection point. * * @param interSegmentList * @param segmentPosition * @param intersection * @param remainingSplitLineCoords * @return the remaining split line */ private Coordinate[] removeProcessedSegment( final LineBoundaryIntersectionAssociation interSegmentList, final int segmentPosition, final Coordinate intersection, Coordinate[] remainingSplitLineCoords ) { // remove the headers coordinates until the current segment (included) from the remaining split line LineString splitLineFragment = interSegmentList.getSplitLineSegment( segmentPosition ); remainingSplitLineCoords = cutRemaininigSplitLine(splitLineFragment , remainingSplitLineCoords); if( remainingSplitLineCoords.length > 0 ){ // the segment is reduced using the second intersection point Coordinate[] reducedSplitLineFragment = SplitUtil.replaceCoordinate(0, intersection, splitLineFragment.getCoordinates()); reducedSplitLineFragment = SplitUtil.createCoordinateArrayWithoutDuplicated(0, reducedSplitLineFragment.length - 1 , reducedSplitLineFragment); remainingSplitLineCoords = SplitUtil.mergeCoordinate( reducedSplitLineFragment, remainingSplitLineCoords); } return remainingSplitLineCoords; } /** * Creates a new line, as a coordinate array, using the position of segment. * * @param splitLineFragment * @param remainingSplitLine * * @return a new coordinate array */ private Coordinate[] cutRemaininigSplitLine( final LineString splitLineFragment, final Coordinate[] remainingSplitLine ) { // search the coordinates of the segment in the position indeed int lastCoord = splitLineFragment.getNumPoints()-1; int lastCoordPosition = CoordinateArrays.indexOf(splitLineFragment.getCoordinateN(lastCoord), remainingSplitLine); // copy the coordinates which follow the last coordinate of split line fragment Coordinate[] newRemainingSplitLine; if(lastCoordPosition < (remainingSplitLine.length - 1) ){ // copy the rest of coordinates from the first coordinate of referenced segment. newRemainingSplitLine = SplitUtil.createCoordinateArrayWithoutDuplicated(lastCoordPosition, remainingSplitLine.length -1 , remainingSplitLine); } else { newRemainingSplitLine = new Coordinate[0]; } return newRemainingSplitLine; } private boolean segmentsIntersectsPolygon(final LineString usefulSegment, final Polygon polygon ) { Geometry intersection = polygon.intersection(usefulSegment); return SplitUtil.containsLineString(intersection); } /** * Checks where lies the first coordinate, it could be outside the polygon or * inside any of the ring. If the first doesn't lie in any of the described * places, check the next coordinate. * * TODO refactoring: improve the legibility structuring this method. * * @param shortCoords * @param adaptedPolygon * * @return Where it lies, it could be, outside, inside, of none of them. */ private int whereIsFirstSplitLineCoord(final Coordinate[] shortCoords, final AdaptedPolygon adaptedPolygon) { Polygon polygon = adaptedPolygon.asPolygon(); Geometry polygonArea = this.geomFactory.createPolygon((LinearRing) polygon.getExteriorRing(), null); for (int i = 0; i < shortCoords.length - 2; i++) { if (!polygonArea.contains(this.geomFactory.createPoint(shortCoords[i]))) { // It lies outside the polygon. // Start with the exterior of the polygon. return OUTSIDE; } // check it lies inside any holes. for (int j = 0; j < polygon.getNumInteriorRing(); j++) { Geometry ringArea = this.geomFactory.createPolygon((LinearRing) polygon.getInteriorRingN(j), null); if (ringArea.contains(this.geomFactory.createPoint(shortCoords[i]))) { // lies inside a ring. return INSIDE; } } // create a line segment between i and i + 1. Coordinate[] segCoords = new Coordinate[2]; segCoords[0] = shortCoords[i]; segCoords[1] = shortCoords[i + 1]; LineString segment = this.geomFactory.createLineString(segCoords); // calculate the distance with the exterior ring, if intersects. double minExtDistance = Double.MAX_VALUE; double distance; Geometry intersection = polygon.getExteriorRing().intersection(segment); // find the closest one to shortCoords[i] for (int j = 0; j < intersection.getNumGeometries(); j++) { Coordinate pointToTest = intersection.getGeometryN(j).getCoordinate(); distance = SplitUtil.calculateDistanceFromFirst(pointToTest, segCoords[0], segCoords[1]); if (Math.abs(distance) < minExtDistance) { minExtDistance = Math.abs(distance); } } double minRingDistance = Double.MAX_VALUE; for (int j = 0; j < polygon.getNumInteriorRing(); j++) { Geometry ring = polygon.getInteriorRingN(j); intersection = ring.intersection(segment); // find the closest one to shortCoords[i] for (int h = 0; h < intersection.getNumGeometries(); h++) { Coordinate pointToTest = intersection.getGeometryN(h).getCoordinate(); distance = SplitUtil.calculateDistanceFromFirst(pointToTest, segCoords[0], segCoords[1]); if (Math.abs(distance) < minRingDistance) { minRingDistance = Math.abs(distance); } } } if (minExtDistance < minRingDistance) { return OUTSIDE; } else if (minRingDistance < minExtDistance) { return INSIDE; } } // calculate the last coordinate where it lies. if (!polygonArea.contains(this.geomFactory.createPoint(shortCoords[shortCoords.length - 1]))) { // It lies outside the polygon. // Start with the exterior of the polygon. return OUTSIDE; } // check it lies inside any holes. for (int j = 0; j < polygon.getNumInteriorRing(); j++) { Geometry ringArea = this.geomFactory.createPolygon((LinearRing) polygon.getInteriorRingN(j), null); if (ringArea.contains(this.geomFactory.createPoint(shortCoords[shortCoords.length - 1]))) { // lies inside a ring. return INSIDE; } } return NONE; } /** * Make a copy of the coordinates. * * @param coords * @return A copy of the original coordinates. */ private Coordinate[] cloneCoordinate(Coordinate[] coords) { Coordinate[] cloneCoordinates = new Coordinate[coords.length]; for (int i = 0; i < coords.length; i++) { cloneCoordinates[i] = (Coordinate) coords[i].clone(); } return cloneCoordinates; } /** * <pre> * The useful part will be the next: * Seek the first coordinate outside the boundary. * Seek the first intersection with the boundary. * If the first intersection is before the first coordinate outside the * line, that coordinate will be the beginning of the line. * * Do the same with the last coordinate outside and the last intersection. * * </pre> * * @param lineCoords * @param splitLine * @param adaptedPolygon (i/o) * @return The piece of lineString that starts and ends outside the * boundary. */ private Coordinate[] discardSplitLineSegmentAdaptingPolygon(Coordinate[] lineCoords, LineString splitLine, AdaptedPolygon adaptedPolygon) { Polygon polygon = adaptedPolygon.asPolygon(); int posFirstCoord = 0; int posLastCoord = lineCoords.length - 1; Geometry boundary = polygon.getBoundary(); int firstOutsidePosition = SplitUtil.findPositionOfFirstCoordinateOutside(splitLine, polygon); // get the first intersection. IntersectionStrategy interStrategy = new IntersectionStrategy(); interStrategy.findFirstIntersection(lineCoords, boundary); final Coordinate firstIntersectionCoord = interStrategy.getIntersection(); final int firstIntersectionPosition = interStrategy.getIntersectionPosition(); // calculate which one go first, and start from that coordinate. if ((firstOutsidePosition > firstIntersectionPosition) || (firstOutsidePosition == -1)) { // this coordinate must be eliminated from the array. lineCoords[firstIntersectionPosition] = firstIntersectionCoord; posFirstCoord = firstIntersectionPosition; // insert the intersection coordinate into the polygon. adaptedPolygon = insertCoordinateIntoPolygon(firstIntersectionCoord, adaptedPolygon, splitLine); } else { posFirstCoord = firstOutsidePosition; } int lastOutsidePosition = SplitUtil.findLastCoordinateOutside(splitLine, polygon); // get the last intersection. interStrategy.findLastIntersection(lineCoords, boundary); final Coordinate lastCoord = interStrategy.getIntersection(); final int lastPosition = interStrategy.getIntersectionPosition(); if (lastOutsidePosition < lastPosition || lastOutsidePosition == -1) { // this coordinate must be eliminated from the array. lineCoords[lastPosition] = lastCoord; posLastCoord = lastPosition; // insert the intersection coordinate into the polygon. adaptedPolygon = insertCoordinateIntoPolygon(lastCoord, adaptedPolygon, splitLine); } else { posLastCoord = lastOutsidePosition; } // build a new lineString starting form the first intersection between // the line and the polygon to the last intersection between the line // and the polygon, with this, we assure all the useful part of the // line, the one that intersects with the polygon is being used. Coordinate[] newCoords = SplitUtil.createCoordinateArrayWithoutDuplicated(posFirstCoord, posLastCoord, lineCoords); return newCoords; } /** * A coordinate will be inserted in the adaptedPolygon. The coordinate is * the result of doing an intersection operation between a line and this * polygon, so this methods know that the coordinate belong to the polygon. * * Find the segment where the coordinate will lie. Find using the tangent * operation. * * TODO refactory: this method could be simplified using the AdaptedPolygon object * * @param coorToInsert * Coordinate to insert into polygon. * @param adaptedPolygon * Receiver polygon. * @return The adaptedPolygon, which will have the coordinate inserted on * it. */ private AdaptedPolygon insertCoordinateIntoPolygon(final Coordinate coorToInsert, final AdaptedPolygon adaptedPolygon1, LineString splitLine) { // get each coordinate, and calculate the tangent with coord(X) - // coorToInsert - coord(X+1). A coordiante will be added on the segment // which scores the lowest value. Polygon adaptedPolygon = adaptedPolygon1.asPolygon(); Coordinate[] boundaryCoord = adaptedPolygon.getExteriorRing().getCoordinates(); int positionToInsert = -1; int positionToInsertOnRing = -1; int nRing = -1; // check if the coordinate belongs to the exterior for (int i = 0; i < boundaryCoord.length - 1; i++) { Coordinate start = boundaryCoord[i]; Coordinate end = boundaryCoord[i + 1]; // calculate tangent between start - coorToInsert - end if (isSameTangent(start, coorToInsert, end) && isPointContainedInSegment(start, coorToInsert, end)) { assert positionToInsert == -1 : "position to insert modified twice."; //$NON-NLS-1$ positionToInsert = i; break; } } List<Coordinate> updatedShell = new ArrayList<Coordinate>(); Coordinate[] shellCoords; if (positionToInsert != -1) { for (int i = 0; i < boundaryCoord.length; i++) { updatedShell.add(boundaryCoord[i]); if (i == positionToInsert) { updatedShell.add(coorToInsert); } } shellCoords = updatedShell.toArray(new Coordinate[updatedShell.size()]); } else { shellCoords = boundaryCoord; } GeometryFactory gf = adaptedPolygon.getFactory(); LinearRing[] rings = null; // check if the coordinate belong to any of the interior rings in case // that there are rings. if (adaptedPolygon.getNumInteriorRing() != 0) { rings = new LinearRing[adaptedPolygon.getNumInteriorRing()]; for (int rIndex = 0; rIndex < adaptedPolygon.getNumInteriorRing(); rIndex++) { // add the rings without modify them. rings[rIndex] = (LinearRing) adaptedPolygon.getInteriorRingN(rIndex); // for each ring Coordinate[] ringCoords = rings[rIndex].getCoordinates(); for (int i = 0; i < ringCoords.length - 1 && positionToInsertOnRing == -1; i++) { Coordinate start = ringCoords[i]; Coordinate end = ringCoords[i + 1]; // calculate tangent between start - coorToInsert - end if (isSameTangent(start, coorToInsert, end) && isPointContainedInSegment(start, coorToInsert, end)) { assert positionToInsertOnRing == -1 : "position to insert modified twice."; //$NON-NLS-1$ positionToInsertOnRing = i; nRing = rIndex; break; } } } if (positionToInsertOnRing != -1) { // ring to be modified. assert nRing != -1 : "there should be a ring index."; //$NON-NLS-1$ // Get the ring which need to be modified, insert the // coordinate and at the end, build the polygon. Coordinate[] originalRing = rings[nRing].getCoordinates(); List<Coordinate> updatedHole = new ArrayList<Coordinate>(); for (int i = 0; i < originalRing.length; i++) { updatedHole.add(originalRing[i]); if (i == positionToInsertOnRing) { updatedHole.add(coorToInsert); } } Coordinate[] modifiedRingCoords = updatedHole.toArray(new Coordinate[updatedHole.size()]); LinearRing modifiedRing = gf.createLinearRing(modifiedRingCoords); rings[nRing] = modifiedRing; } } // create the polygon. assert shellCoords != null : "there should be at least shell coordinates to build a polygon."; //$NON-NLS-1$ LinearRing shellRing = gf.createLinearRing(shellCoords); Polygon newPolygon = gf.createPolygon(shellRing, rings); AdaptedPolygon rebuildedPolygon = new AdaptedPolygon(newPolygon); return rebuildedPolygon; } /** * Check if the provided point is contained in the range of that segment. * * @param start Coordinate where the segment starts. * @param point Provided point. * @param end Coordinate where the segment ends. * @return True if the point is between the start and end coordinate. */ private boolean isPointContainedInSegment( Coordinate start, Coordinate point, Coordinate end ) { // check if the points is contained in that line segment formed by the // start coordinate and the end coordinate. if (start.x > point.x && point.x > end.x) { // x are in order, now lets see if 'y' are in order too. if ((start.y > point.y && point.y > end.y) || (start.y < point.y && point.y < end.y)) { return true; } } else if (start.x < point.x && point.x < end.x) { if ((start.y > point.y && point.y > end.y) || (start.y < point.y && point.y < end.y)) { return true; } } return false; } /** * <p> * * <pre> * Check if coordToTest belongs to the segment formed by the start * coordinate and end coordinate. * * Calculate the tangent between the segment formed by start coordinate and * coordToTest. Then it does the same but this time for the segment formed * by coordToTest and end coordinate. If both tangent have the same value or * difference between them is near to zero, we could say that coordToTest * belongs to the segment formed by the start and end coordinates. * </pre> * * </p> * * @param start Coordinate where the segment starts. * @param coordToTest Coordinate test if it belongs to the line formed by the start and end * coordinates. * @param end Coordinate where the segment ends. * @return True if the have the same tangent. */ private boolean isSameTangent( Coordinate start, Coordinate coordToTest, Coordinate end ) { // check if the X or the Y have the same value. double firstX = Math.abs(start.x - coordToTest.x); double secondX = Math.abs(coordToTest.x - end.x); if (firstX < DEPRECIATE_VALUE && secondX < DEPRECIATE_VALUE) { return true; } double firstY = Math.abs(start.y - coordToTest.y); double secondY = Math.abs(coordToTest.y - end.y); if (firstY < DEPRECIATE_VALUE && secondY < DEPRECIATE_VALUE) { return true; } Coordinate[] newCoor; // calculate the tangent of the first segment. (i and i+1) newCoor = new Coordinate[2]; newCoor[0] = start; newCoor[1] = coordToTest; double dx1 = newCoor[1].x - newCoor[0].x; double dy1 = newCoor[1].y - newCoor[0].y; double tangent = dy1 / dx1; // calculate the tangent of the second segment (i+1 and i+2) newCoor = new Coordinate[2]; newCoor[0] = coordToTest; newCoor[1] = end; double dx2 = newCoor[1].x - newCoor[0].x; double dy2 = newCoor[1].y - newCoor[0].y; double tangentToCompare = dy2 / dx2; double diff = Math.abs(tangent - tangentToCompare); return (diff < DEPRECIATE_VALUE) ? true : false; } /** * The adapted polygon is the result of modifying the original polygon with the intersection vertex * with the split line. * * @return the polygon adapted to the split line */ public AdaptedPolygon getAdaptedPolygon() { return this.adaptedPolygon; } }