Index: src/uk/me/parabola/mkgmap/reader/osm/MultiPolygonRelation.java =================================================================== --- src/uk/me/parabola/mkgmap/reader/osm/MultiPolygonRelation.java (revision 1596) +++ src/uk/me/parabola/mkgmap/reader/osm/MultiPolygonRelation.java (working copy) @@ -7,15 +7,19 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; +import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Queue; import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.LinkedBlockingQueue; import java.util.logging.Level; @@ -41,7 +45,8 @@ private Set intersectingPolygons; private final uk.me.parabola.imgfmt.app.Area bbox; - + private Area bboxArea; + /** * A point that has a lower or equal squared distance from * a line is treated as if it lies one the line.
@@ -423,10 +428,7 @@ if (remove) { // check if the polygon contains the complete bounding box - Rectangle bboxRect = new Rectangle(bbox.getMinLat(), bbox - .getMinLong(), bbox.getMaxLat() - bbox.getMinLat(), - bbox.getMaxLong() - bbox.getMinLong()); - if (w.getBounds().contains(bboxRect)) { + if (w.getBounds().contains(bboxArea.getBounds())) { remove = false; } } @@ -527,6 +529,11 @@ } } + // create an Area for the bbox to clip the polygons + bboxArea = new Area(new Rectangle(bbox.getMinLong(), bbox + .getMinLat(), bbox.getMaxLong() - bbox.getMinLong(), + bbox.getMaxLat() - bbox.getMinLat())); + polygons = joinWays(allWays); closeWays(polygons); removeUnclosedWays(polygons); @@ -639,15 +646,20 @@ || hasPolygonTags(currentPolygon.polygon); if (processPolygon) { + List singularOuterPolygons; + if (holes.isEmpty()) { + singularOuterPolygons = Collections + .singletonList((Way) new JoinedWay(currentPolygon.polygon)); + } else { + List innerWays = new ArrayList(holes.size()); + for (PolygonStatus polygonHoleStatus : holes) { + innerWays.add(polygonHoleStatus.polygon); + } - List innerWays = new ArrayList(holes.size()); - for (PolygonStatus polygonHoleStatus : holes) { - innerWays.add(polygonHoleStatus.polygon); + singularOuterPolygons = cutOutInnerPolygons(currentPolygon.polygon, + innerWays); } - - List singularOuterPolygons = cutOutInnerPolygons( - currentPolygon.polygon, innerWays); - + if (currentPolygon.polygon.getOriginalWays().size() == 1) { // the original way was a closed polygon which // has been replaced by the new cutted polygon @@ -689,7 +701,7 @@ runIntersectionCheck(unfinishedPolygons); runWrongInnerPolygonCheck(unfinishedPolygons, innerPolygons); - // we have at least one ring that could not be processed + // we have at least one polygon that could not be processed // Probably we have intersecting or overlapping polygons // one possible reason is if the relation overlaps the tile // bounds @@ -704,8 +716,7 @@ cleanup(); } - - private void runIntersectionCheck(BitSet unfinishedRings) { + private void runIntersectionCheck(BitSet unfinishedPolys) { if (intersectingPolygons.isEmpty()) { // nothing to do return; @@ -716,7 +727,7 @@ boolean oneOufOfBbox = false; for (JoinedWay polygon : intersectingPolygons) { int pi = polygons.indexOf(polygon); - unfinishedRings.clear(pi); + unfinishedPolys.clear(pi); boolean outOfBbox = false; for (Coord c : polygon.getPoints()) { @@ -736,7 +747,7 @@ private void runWrongInnerPolygonCheck(BitSet unfinishedPolygons, BitSet innerPolygons) { - // find all unfinished inner rings that are not contained by any + // find all unfinished inner polygons that are not contained by any BitSet wrongInnerPolygons = findOutmostPolygons(unfinishedPolygons, innerPolygons); if (log.isDebugEnabled()) { log.debug("unfinished", unfinishedPolygons); @@ -780,6 +791,49 @@ roleMap.clear(); containsMatrix = null; polygons = null; + bboxArea = null; + intersectingPolygons = null; + } + + private CutPoint calcNextCutPoint(AreaCutData areaData) { + if (areaData.innerAreas == null || areaData.innerAreas.isEmpty()) { + return null; + } + + if (areaData.innerAreas.size() == 1) { + // make it short if there is only one inner area + Rectangle outerBounds = areaData.outerArea.getBounds(); + CoordinateAxis axis = (outerBounds.width < outerBounds.height ? CoordinateAxis.LONGITUDE : CoordinateAxis.LATITUDE); + CutPoint oneCutPoint = new CutPoint(axis); + oneCutPoint.addArea(areaData.innerAreas.get(0)); + return oneCutPoint; + } + + ArrayList innerStart = new ArrayList( + areaData.innerAreas); + + ArrayList bestCutPoints = new ArrayList(CoordinateAxis.values().length); + + for (CoordinateAxis axis : CoordinateAxis.values()) { + CutPoint bestCutPoint = new CutPoint(axis); + CutPoint currentCutPoint = new CutPoint(axis); + + Collections.sort(innerStart, (axis == CoordinateAxis.LONGITUDE ? COMP_LONG_START: COMP_LAT_START)); + + Iterator startIter = innerStart.iterator(); + while (startIter.hasNext()) { + Area nextStart = startIter.next(); + currentCutPoint.addArea(nextStart); + + if (currentCutPoint.compareTo(bestCutPoint) > 0) { + bestCutPoint = currentCutPoint.duplicate(); + } + } + bestCutPoints.add(bestCutPoint); + } + + return Collections.max(bestCutPoints); + } /** @@ -794,112 +848,166 @@ * polygons */ private List cutOutInnerPolygons(Way outerPolygon, List innerPolygons) { - // we use the java.awt.geom.Area class because it's a quick - // implementation of what we need + if (innerPolygons.isEmpty()) { + Way outerWay = new JoinedWay(outerPolygon); + if (log.isDebugEnabled()) { + log.debug("Way", outerPolygon.getId(), "splitted to way", outerWay.getId()); + } + return Collections.singletonList(outerWay); + } + + // use the java.awt.geom.Area class because it's a quick + // implementation of what's needed // this list contains all non overlapping and singular areas // of the outerPolygon - List outerAreas = new ArrayList(); - - // 1st create an Area object of the outerPolygon and put it to the list - List oa = createAreas(outerPolygon); - - Area bboxArea = new Area(new Rectangle(bbox.getMinLong(), bbox - .getMinLat(), bbox.getMaxLong() - bbox.getMinLong(), - bbox.getMaxLat() - bbox.getMinLat())); + Queue areasToCut = new LinkedList(); + Collection finishedAreas = new ArrayList(innerPolygons.size()); - // clip the bounding box - for (Area outerArea : oa) { - if (bboxArea.contains(outerArea.getBounds())) { - // no clipping necessary - outerAreas.add(outerArea); - } else { - // the area might intersect the bounding box - // => clip it to the bounding box - outerArea.intersect(bboxArea); - outerAreas.addAll(areaToSingularAreas(outerArea)); - } - } - - List innerAreas = new ArrayList(); + // create a list of Area objects from the outerPolygon (clipped to the bounding box) + List outerAreas = createAreas(outerPolygon, true); + + // create the inner areas + List innerAreas = new ArrayList(innerPolygons.size()+2); for (Way innerPolygon : innerPolygons) { - innerAreas.addAll(createAreas(innerPolygon)); + // don't need to clip to the bounding box because + // these polygons are just used to cut out holes + innerAreas.addAll(createAreas(innerPolygon, false)); } - // go through all innerPolygons (holes) and cut them from the outerPolygon - for (Area innerArea : innerAreas) { - - List outerAfterThisStep = new ArrayList(); + // initialize the cut data queue + if (innerAreas.isEmpty()) { + // this is a multipolygon without any inner areas + // nothing to cut + finishedAreas.addAll(outerAreas); + } else if (outerAreas.size() == 1) { + // there is one outer area only + // it is checked before that all inner areas are inside this outer area + AreaCutData initialCutData = new AreaCutData(); + initialCutData.outerArea = outerAreas.get(0); + initialCutData.innerAreas = innerAreas; + areasToCut.add(initialCutData); + } else { + // multiple outer areas for (Area outerArea : outerAreas) { - // check if this outerArea is probably intersected by the inner - // area to save computation time in case it is not - if (outerArea.getBounds().intersects(innerArea.getBounds()) == false) { - outerAfterThisStep.add(outerArea); - continue; + AreaCutData initialCutData = new AreaCutData(); + initialCutData.outerArea = outerArea; + initialCutData.innerAreas = new ArrayList(innerAreas + .size()); + for (Area innerArea : innerAreas) { + if (outerArea.getBounds().intersects( + innerArea.getBounds())) { + initialCutData.innerAreas.add(innerArea); + } } - - // cut the hole - outerArea.subtract(innerArea); - if (outerArea.isEmpty()) { - // this outer area space can be abandoned - } else if (outerArea.isSingular()) { - // the area is singular - // => no further splits necessary - outerAfterThisStep.add(outerArea); + + if (initialCutData.innerAreas.isEmpty()) { + // this is either an error + // or the outer area has been cut into pieces on the tile bounds + finishedAreas.add(outerArea); } else { - // 1st cut in two halfs in the middle of the inner area + areasToCut.add(initialCutData); + } + } + } - // Cut the bounding box into two rectangles - Rectangle r1; - Rectangle r2; + while (areasToCut.isEmpty() == false) { + AreaCutData areaCutData = areasToCut.poll(); + CutPoint cutPoint = calcNextCutPoint(areaCutData); + + if (cutPoint == null) { + finishedAreas.add(areaCutData.outerArea); + continue; + } + + assert cutPoint.getNumberOfAreas() > 0 : "Number of cut areas == 0 in mp "+getId(); + + // cut out the holes + for (Area cutArea : cutPoint.getAreas()) { + areaCutData.outerArea.subtract(cutArea); + } + + if (areaCutData.outerArea.isEmpty()) { + // this outer area space can be abandoned + continue; + } + + // the inner areas of the cut point have been processed + // they are no longer needed + areaCutData.innerAreas.removeAll(cutPoint.getAreas()); - // Get the bounds of this polygon - Rectangle innerBounds = innerArea.getBounds(); - Rectangle outerBounds = outerArea.getBounds(); - if (outerBounds.width > outerBounds.height) { - int cutWidth = (innerBounds.x - outerBounds.x) - + innerBounds.width / 2; - r1 = new Rectangle(outerBounds.x, outerBounds.y, - cutWidth, outerBounds.height); - r2 = new Rectangle(outerBounds.x + cutWidth, - outerBounds.y, outerBounds.width - cutWidth, - outerBounds.height); - } else { - int cutHeight = (innerBounds.y - outerBounds.y) - + innerBounds.height / 2; - r1 = new Rectangle(outerBounds.x, outerBounds.y, - outerBounds.width, cutHeight); - r2 = new Rectangle(outerBounds.x, outerBounds.y - + cutHeight, outerBounds.width, - outerBounds.height - cutHeight); - } + if (areaCutData.outerArea.isSingular()) { + // the area is singular + // => no further splits necessary + if (areaCutData.innerAreas.isEmpty()) { + // this area is finished and needs no further cutting + finishedAreas.add(areaCutData.outerArea); + } else { + // readd this area to further processing + areasToCut.add(areaCutData); + } + } else { + // we need to cut the area into two halfs to get singular areas + Rectangle r1 = cutPoint.getCutRectangleForArea(areaCutData.outerArea, true); + Rectangle r2 = cutPoint.getCutRectangleForArea(areaCutData.outerArea, false); - // Now find the intersection of these two boxes with the - // original polygon. This will make two new areas, and each - // area will be one (or more) polygons. - Area a1 = outerArea; - Area a2 = (Area) a1.clone(); - a1.intersect(new Area(r1)); - a2.intersect(new Area(r2)); + // Now find the intersection of these two boxes with the + // original polygon. This will make two new areas, and each + // area will be one (or more) polygons. + Area a1 = areaCutData.outerArea; + Area a2 = (Area) a1.clone(); + a1.intersect(new Area(r1)); + a2.intersect(new Area(r2)); - outerAfterThisStep.addAll(areaToSingularAreas(a1)); - outerAfterThisStep.addAll(areaToSingularAreas(a2)); + if (areaCutData.innerAreas.isEmpty()) { + finishedAreas.addAll(areaToSingularAreas(a1)); + finishedAreas.addAll(areaToSingularAreas(a2)); + } else { + ArrayList cuttedAreas = new ArrayList(); + cuttedAreas.addAll(areaToSingularAreas(a1)); + cuttedAreas.addAll(areaToSingularAreas(a2)); + + for (Area nextOuterArea : cuttedAreas) { + ArrayList nextInnerAreas = null; + // go through all remaining inner areas and check if they + // must be further processed with the nextOuterArea + for (Area nonProcessedInner : areaCutData.innerAreas) { + if (nextOuterArea.intersects(nonProcessedInner.getBounds2D())) { + if (nextInnerAreas == null) { + nextInnerAreas = new ArrayList(); + } + nextInnerAreas.add(nonProcessedInner); + } + } + + if (nextInnerAreas == null || nextInnerAreas.isEmpty()) { + finishedAreas.add(nextOuterArea); + } else { + AreaCutData outCutData = new AreaCutData(); + outCutData.outerArea = nextOuterArea; + outCutData.innerAreas= nextInnerAreas; + areasToCut.add(outCutData); + } + } } } - outerAreas = outerAfterThisStep; + } - + // convert the java.awt.geom.Area back to the mkgmap way - List cutOuterPolygon = new ArrayList(outerAreas.size()); - for (Area area : outerAreas) { + List cuttedOuterPolygon = new ArrayList(finishedAreas.size()); + for (Area area : finishedAreas) { Way w = singularAreaToWay(area, FakeIdGenerator.makeFakeId()); if (w != null) { w.copyTags(outerPolygon); - cutOuterPolygon.add(w); + cuttedOuterPolygon.add(w); + if (log.isDebugEnabled()) { + log.debug("Way", outerPolygon.getId(), "splitted to way", w.getId()); + } } } - return cutOuterPolygon; + return cuttedOuterPolygon; } /** @@ -986,22 +1094,18 @@ * erroneous cases properly the method might return a list of areas. * * @param w a closed way + * @param clipBbox true if the areas should be clipped to the bounding box; false else * @return a list of enclosed ares */ - private List createAreas(Way w) { + private List createAreas(Way w, boolean clipBbox) { Area area = new Area(createPolygon(w.getPoints())); + if (clipBbox && bboxArea.contains(area.getBounds())==false) { + // the area intersects the bounding box => clip it + area.intersect(bboxArea); + } List areaList = areaToSingularAreas(area); - if (areaList.size() > 1) { - if (bbox.allInsideBoundary(w.getPoints())) { - log.warn("Polygon", w.getId(), "intersects itself. It is splitted into", areaList.size(), "polygons."); - log.warn("The polygon is composed of"); - logWayURLs(Level.WARNING, "-", w); - } else { - log.info("Polygon", w.getId(), - "intersects itself and the tile bounds. Maybe the polygon is not completely contained in the tile."); - log.info("The polygon is composed of"); - logWayURLs(Level.INFO, "-", w); - } + if (log.isDebugEnabled()) { + log.debug("Bbox clipped way",w.getId()+"=>",areaList.size(),"distinct area(s)."); } return areaList; } @@ -1182,7 +1286,7 @@ } /** - * Checks if the polygon with polygonIndex1 contains the ring with polygonIndex2. + * Checks if the polygon with polygonIndex1 contains the polygon with polygonIndex2. * * @return true if polygon(polygonIndex1) contains polygon(polygonIndex2) */ @@ -1644,7 +1748,7 @@ } } } - + public List getOriginalIds() { ArrayList idList = new ArrayList(getOriginalWays().size()); for (Way w : getOriginalWays()) { @@ -1688,4 +1792,167 @@ this.polygon = polygon; } } + + private static class AreaCutData { + Area outerArea; + List innerAreas; + } + + private static class CutPoint implements Comparable{ + int startPoint = Integer.MAX_VALUE; + int stopPoint = Integer.MIN_VALUE; + TreeSet areas; + private final CoordinateAxis axis; + + public CutPoint(CoordinateAxis axis) { + this.axis = axis; + this.areas = new TreeSet( + (axis == CoordinateAxis.LONGITUDE ? COMP_LONG_STOP : COMP_LAT_STOP)); + } + + public CutPoint duplicate() { + CutPoint newCutPoint = new CutPoint(this.axis); + newCutPoint.areas.addAll(areas); + newCutPoint.startPoint = startPoint; + newCutPoint.stopPoint = stopPoint; + return newCutPoint; + } + + public int getCutPoint() { + return startPoint + (stopPoint - startPoint) / 2; + } + + public Rectangle getCutRectangleForArea(Area toCut, boolean firstRect) { + Rectangle areaRect = toCut.getBounds(); + if (axis == CoordinateAxis.LONGITUDE) { + int newWidth = getCutPoint()-areaRect.x; + if (firstRect) { + return new Rectangle(areaRect.x, areaRect.y, newWidth, areaRect.height); + } else { + return new Rectangle(areaRect.x+newWidth, areaRect.y, areaRect.width-newWidth, areaRect.height); + } + } else { + int newHeight = getCutPoint()-areaRect.y; + if (firstRect) { + return new Rectangle(areaRect.x, areaRect.y, areaRect.width, newHeight); + } else { + return new Rectangle(areaRect.x, areaRect.y+newHeight, areaRect.width, areaRect.height-newHeight); + } + } + } + + public Collection getAreas() { + return areas; + } + + public void addArea(Area area) { + // remove all areas that do not overlap with the new area + while (areas.isEmpty() == false + && axis.getStop(areas.first()) < axis + .getStart(area)) { + // remove the first area + areas.pollFirst(); + } + + areas.add(area); + startPoint = axis.getStart(Collections.max(areas, + (axis == CoordinateAxis.LONGITUDE ? COMP_LONG_START + : COMP_LAT_START))); + stopPoint = axis.getStop(areas.first()); + } + + public int getNumberOfAreas() { + return this.areas.size(); + } + + @Override + public int compareTo(CutPoint o) { + if (this == o) { + return 0; + } + int ndiff = getNumberOfAreas()-o.getNumberOfAreas(); + if (ndiff != 0) { + return ndiff; + } + // prefer the larger area that is splitted + return (stopPoint-startPoint)-(o.stopPoint-o.startPoint); + } + + @Override + public String toString() { + return axis +" "+getNumberOfAreas()+" "+startPoint+" "+stopPoint+" "+getCutPoint(); + } + + } + + private static enum CoordinateAxis { + LATITUDE(false), LONGITUDE(true); + + private CoordinateAxis(boolean useX) { + this.useX = useX; + } + + private final boolean useX; + + public int getStart(Area area) { + return getStart(area.getBounds()); + } + + public int getStart(Rectangle rect) { + return (useX ? rect.x : rect.y); + } + + public int getStop(Area area) { + return getStop(area.getBounds()); + } + + public int getStop(Rectangle rect) { + return (useX ? rect.x + rect.width : rect.y + rect.height); + } + + } + + private static final AreaComparator COMP_LONG_START = new AreaComparator( + true, CoordinateAxis.LONGITUDE); + private static final AreaComparator COMP_LONG_STOP = new AreaComparator( + false, CoordinateAxis.LONGITUDE); + private static final AreaComparator COMP_LAT_START = new AreaComparator( + true, CoordinateAxis.LATITUDE); + private static final AreaComparator COMP_LAT_STOP = new AreaComparator( + false, CoordinateAxis.LATITUDE); + + private static class AreaComparator implements Comparator { + + private final CoordinateAxis axis; + private final boolean startPoint; + + public AreaComparator(boolean startPoint, CoordinateAxis axis) { + this.startPoint = startPoint; + this.axis = axis; + } + + @Override + public int compare(Area o1, Area o2) { + if (o1 == o2) { + return 0; + } + + if (startPoint) { + int cmp = axis.getStart(o1) - axis.getStart(o2); + if (cmp == 0) { + return axis.getStop(o1) - axis.getStop(o2); + } else { + return cmp; + } + } else { + int cmp = axis.getStop(o1) - axis.getStop(o2); + if (cmp == 0) { + return axis.getStart(o1) - axis.getStart(o2); + } else { + return cmp; + } + } + } + + } }