/* * Copyright (C) 2014 Gerd Petermann * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 or * version 2 as published by the Free Software Foundation. * * This program 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 * General Public License for more details. */ package uk.me.parabola.util; import java.awt.Shape; import java.awt.geom.Path2D; import java.awt.geom.PathIterator; import java.awt.geom.Rectangle2D; import java.util.Arrays; import uk.me.parabola.log.Logger; public class ShapeSplitter { private static final Logger log = Logger.getLogger(ShapeSplitter.class); private static final int LEFT = 0; private static final int TOP = 1; private static final int RIGHT = 2; private static final int BOTTOM= 3; /** * Clip a given shape with a given rectangle. * @param shape the subject shape to clip * @param clippingRect the clipping rectangle * @return the intersection of the shape and the rectangle * or null if they don't intersect. * The intersection may contain dangling edges. */ public static Path2D.Double clipShape (Shape shape, Rectangle2D clippingRect) { double minX = Double.POSITIVE_INFINITY,minY = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY,maxY = Double.NEGATIVE_INFINITY; PathIterator pit = shape.getPathIterator(null); double[] points = new double[512]; int num = 0; Path2D.Double result = null; double[] res = new double[6]; while (!pit.isDone()) { int type = pit.currentSegment(res); double x = res[0]; double y = res[1]; if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y; switch (type) { case PathIterator.SEG_LINETO: case PathIterator.SEG_MOVETO: if (num + 2 >= points.length) { points = Arrays.copyOf(points, points.length * 2); } points[num++] = x; points[num++] = y; break; case PathIterator.SEG_CLOSE: Path2D.Double segment = null; if (clippingRect.contains(minX, minY) == false || clippingRect.contains(maxX,maxY) == false){ Rectangle2D.Double bbox = new Rectangle2D.Double(minX,minY,maxX-minX,maxY-minY); segment = clipSinglePathWithSutherlandHodgman (points, num, clippingRect, bbox); } else segment = pointsToPath2D(points, num); if (segment != null){ if (result == null) result = segment; else result.append(segment, false); } num = 0; minX = minY = Double.POSITIVE_INFINITY; maxX = maxY = Double.NEGATIVE_INFINITY; break; default: log.error("Unsupported path iterator type " + type + ". This is an mkgmap error."); } pit.next(); } return result; } /** * Convert a list of longitude+latitude values to a Path2D.Double * @param points the pairs * @return the path or null if the path describes a point or line. */ public static Path2D.Double pointsToPath2D(double[] points, int num) { if (num < 2) return null; if (points[0] == points[num-2] && points[1] == points[num-1]) num -= 2; if (num < 6) return null; Path2D.Double path = new Path2D.Double(Path2D.WIND_NON_ZERO, num / 2 + 2); double lastX = points[0], lastY = points[1]; path.moveTo(lastX, lastY); int numOut = 1; for (int i = 2; i < num; ){ double x = points[i++], y = points[i++]; if (x != lastX || y != lastY){ path.lineTo(x,y); lastX = x; lastY = y; ++numOut; } } if (numOut < 3) return null; path.closePath(); return path; } // The Sutherland�Hodgman algorithm Pseudo-Code: // List outputList = subjectPolygon; // for (Edge clipEdge in clipPolygon) do // List inputList = outputList; // outputList.clear(); // Point S = inputList.last; // for (Point E in inputList) do // if (E inside clipEdge) then // if (S not inside clipEdge) then // outputList.add(ComputeIntersection(S,E,clipEdge)); // end if // outputList.add(E); // else if (S inside clipEdge) then // outputList.add(ComputeIntersection(S,E,clipEdge)); // end if // S = E; // done // done /** * Clip a single path with a given rectangle using the Sutherland-Hodgman algorithm. This is much faster compared to * the area.intersect method, but may create dangling edges. * @param points a list of longitude+latitude pairs * @param num the nnumber of valid values in points * @param clippingRect the clipping rectangle * @param bbox the bounding box of the path * @return the clipped path as a Path2D.Double or null if the result is empty */ public static Path2D.Double clipSinglePathWithSutherlandHodgman (double[] points, int num, Rectangle2D clippingRect, Rectangle2D.Double bbox) { if (num <= 2 || bbox.intersects(clippingRect) == false){ return null; } int countVals = num; if (points[0] == points[num-2] && points[1] == points[num-1]){ countVals -= 2; } double[] outputList = points; double[] input; double leftX = clippingRect.getMinX(); double rightX = clippingRect.getMaxX(); double lowerY = clippingRect.getMinY(); double upperY = clippingRect.getMaxY(); boolean eIsIn = false, sIsIn = false; for (int side = LEFT; side <= BOTTOM; side ++){ if (countVals < 6) return null; // ignore point or line boolean skipTestForThisSide; switch(side){ case LEFT: skipTestForThisSide = (bbox.getMinX() >= leftX); break; case TOP: skipTestForThisSide = (bbox.getMaxY() < upperY); break; case RIGHT: skipTestForThisSide = (bbox.getMaxX() < rightX); break; default: skipTestForThisSide = (bbox.getMinY() >= lowerY); } if (skipTestForThisSide) continue; input = outputList; outputList = new double[countVals + 16]; double sLon = 0,sLat = 0; double pLon = 0,pLat = 0; // intersection int posIn = countVals - 2; int posOut = 0; for (int i = 0; i < countVals+2; i+=2){ if (posIn >= countVals) posIn = 0; double eLon = input[posIn++]; double eLat = input[posIn++]; switch (side){ case LEFT: eIsIn = (eLon >= leftX); break; case TOP: eIsIn = (eLat < upperY); break; case RIGHT: eIsIn = (eLon < rightX); break; default: eIsIn = (eLat >= lowerY); } if (i > 0){ if (eIsIn != sIsIn){ // compute intersection double slope; if (eLon != sLon) slope = (eLat - sLat) / (eLon-sLon); else slope = 1; switch (side){ case LEFT: pLon = leftX; pLat = slope *(leftX-sLon) + sLat; break; case RIGHT: pLon = rightX; pLat = slope *(rightX-sLon) + sLat; break; case TOP: if (eLon != sLon) pLon = sLon + (upperY - sLat) / slope; else pLon = sLon; pLat = upperY; break; default: // BOTTOM if (eLon != sLon) pLon = sLon + (lowerY - sLat) / slope; else pLon = sLon; pLat = lowerY; break; } } int toAdd = 0; if (eIsIn){ if (!sIsIn){ toAdd += 2; } toAdd += 2; } else { if (sIsIn){ toAdd += 2; } } if (posOut + toAdd >= outputList.length) { // unlikely outputList = Arrays.copyOf(outputList, outputList.length * 2); } if (eIsIn){ if (!sIsIn){ outputList[posOut++] = pLon; outputList[posOut++] = pLat; } outputList[posOut++] = eLon; outputList[posOut++] = eLat; } else { if (sIsIn){ outputList[posOut++] = pLon; outputList[posOut++] = pLat; } } } // S = E sLon = eLon; sLat = eLat; sIsIn = eIsIn; } countVals = posOut; } return pointsToPath2D(outputList, countVals); } }