/* * Copyright (C) 2006, 2011. * * 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.mkgmap.reader.osm.boundary; import java.awt.geom.Line2D; import java.util.ArrayList; import java.util.BitSet; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; import uk.me.parabola.imgfmt.app.Area; import uk.me.parabola.imgfmt.app.Coord; import uk.me.parabola.log.Logger; import uk.me.parabola.mkgmap.reader.osm.MultiPolygonRelation; import uk.me.parabola.mkgmap.reader.osm.Relation; import uk.me.parabola.mkgmap.reader.osm.Way; import uk.me.parabola.util.Java2DConverter; public class BoundaryRelation extends MultiPolygonRelation { private static final Logger log = Logger .getLogger(BoundaryRelation.class); private java.awt.geom.Area outerResultArea; /** keeps the result of the multipolygon processing */ private Boundary boundary; public BoundaryRelation(Relation other, Map<Long, Way> wayMap, Area bbox) { super(other, wayMap, bbox); } public Boundary getBoundary() { if (boundary == null) { if (outerResultArea == null) { return null; } boundary = new Boundary(outerResultArea, this.getTagEntryIterator(),"r"+this.getId()); outerResultArea = null; } return boundary; } /** * Process the ways in this relation. Joins way with the role "outer" Adds * ways with the role "inner" to the way with the role "outer" */ public void processElements() { log.info("Processing multipolygon", toBrowseURL()); List<Way> allWays = getSourceWays(); // // check if the multipolygon itself or the non inner member ways have a tag // // if not it does not make sense to process it and we could save the time // boolean shouldProcess = hasStyleRelevantTags(this); // if (shouldProcess == false) { // for (Way w : allWays) { // shouldProcess = hasStyleRelevantTags(w); // if (shouldProcess) { // break; // } // } // } // if (shouldProcess==false) { // log.info("Do not process multipolygon",getId(),"because it has no style relevant tags."); // return; // } // create an Area for the bbox to clip the polygons bboxArea = Java2DConverter.createBoundsArea(getBbox()); // join all single ways to polygons, try to close ways and remove non closed ways polygons = joinWays(allWays); outerWaysForLineTagging = new HashSet<Way>(); outerTags = new HashMap<String,String>(); removeOutOfBbox(polygons); boolean changed = true; while (changed) { changed = false; while (connectUnclosedWays(polygons)) { changed = true; } closeWays(polygons); } removeUnclosedWays(polygons); // now only closed ways are left => polygons only // check if we have at least one polygon left if (polygons.isEmpty()) { // do nothing log.info("Multipolygon " + toBrowseURL() + " does not contain a closed polygon."); tagOuterWays(); cleanup(); return; } removeWaysOutsideBbox(polygons); if (polygons.isEmpty()) { // do nothing log.info("Multipolygon " + toBrowseURL() + " is completely outside the bounding box. It is not processed."); tagOuterWays(); cleanup(); return; } // the intersectingPolygons marks all intersecting/overlapping polygons intersectingPolygons = new HashSet<JoinedWay>(); // check which polygons lie inside which other polygon createContainsMatrix(polygons); // unfinishedPolygons marks which polygons are not yet processed unfinishedPolygons = new BitSet(polygons.size()); unfinishedPolygons.set(0, polygons.size()); // create bitsets which polygons belong to the outer and to the inner role innerPolygons = new BitSet(); taggedInnerPolygons = new BitSet(); outerPolygons = new BitSet(); taggedOuterPolygons = new BitSet(); int wi = 0; for (Way w : polygons) { String role = getRole(w); if ("inner".equals(role)) { innerPolygons.set(wi); taggedInnerPolygons.set(wi); } else if ("outer".equals(role)) { outerPolygons.set(wi); taggedOuterPolygons.set(wi); } else { // unknown role => it could be both innerPolygons.set(wi); outerPolygons.set(wi); } wi++; } if (outerPolygons.isEmpty()) { log.warn("Multipolygon", toBrowseURL(), "does not contain any way tagged with role=outer or empty role."); cleanup(); return; } Queue<PolygonStatus> polygonWorkingQueue = new LinkedBlockingQueue<PolygonStatus>(); BitSet nestedOuterPolygons = new BitSet(); BitSet nestedInnerPolygons = new BitSet(); BitSet outmostPolygons ; BitSet outmostInnerPolygons = new BitSet(); boolean outmostInnerFound; do { outmostInnerFound = false; outmostPolygons = findOutmostPolygons(unfinishedPolygons); if (outmostPolygons.intersects(taggedInnerPolygons)) { outmostInnerPolygons.or(outmostPolygons); outmostInnerPolygons.and(taggedInnerPolygons); if (log.isDebugEnabled()) log.debug("wrong inner polygons: " + outmostInnerPolygons); // do not process polygons tagged with role=inner but which are // not contained by any other polygon unfinishedPolygons.andNot(outmostInnerPolygons); outmostPolygons.andNot(outmostInnerPolygons); outmostInnerFound = true; } } while (outmostInnerFound); if (!outmostPolygons.isEmpty()) { polygonWorkingQueue.addAll(getPolygonStatus(outmostPolygons, "outer")); } boolean outmostPolygonProcessing = true; outerResultArea = new java.awt.geom.Area(); while (!polygonWorkingQueue.isEmpty()) { // the polygon is not contained by any other unfinished polygon PolygonStatus currentPolygon = polygonWorkingQueue.poll(); // this polygon is now processed and should not be used by any // further step unfinishedPolygons.clear(currentPolygon.index); BitSet polygonContains = new BitSet(); polygonContains.or(containsMatrix.get(currentPolygon.index)); // use only polygon that are contained by the polygon polygonContains.and(unfinishedPolygons); // polygonContains is the intersection of the unfinished and // the contained polygons // get the holes // these are all polygons that are in the main polygon // and that are not contained by any other polygon boolean holesOk; BitSet holeIndexes; do { holeIndexes = findOutmostPolygons(polygonContains); holesOk = true; if (currentPolygon.outer) { // for role=outer only role=inner is allowed if (holeIndexes.intersects(taggedOuterPolygons)) { BitSet addOuterNestedPolygons = new BitSet(); addOuterNestedPolygons.or(holeIndexes); addOuterNestedPolygons.and(taggedOuterPolygons); nestedOuterPolygons.or(addOuterNestedPolygons); holeIndexes.andNot(addOuterNestedPolygons); // do not process them unfinishedPolygons.andNot(addOuterNestedPolygons); polygonContains.andNot(addOuterNestedPolygons); // recalculate the holes again to get all inner polygons // in the nested outer polygons holesOk = false; } } else { // for role=inner both role=inner and role=outer is supported // although inner in inner is not officially allowed if (holeIndexes.intersects(taggedInnerPolygons)) { // process inner in inner but issue a warning later BitSet addInnerNestedPolygons = new BitSet(); addInnerNestedPolygons.or(holeIndexes); addInnerNestedPolygons.and(taggedInnerPolygons); nestedInnerPolygons.or(addInnerNestedPolygons); } } } while (!holesOk); ArrayList<PolygonStatus> holes = getPolygonStatus(holeIndexes, (currentPolygon.outer ? "inner" : "outer")); // these polygons must all be checked for holes polygonWorkingQueue.addAll(holes); if (currentPolygon.outer) { // add the original ways to the list of ways that get the line tags of the mp // the joined ways may be changed by the auto closing algorithm outerWaysForLineTagging.addAll(currentPolygon.polygon.getOriginalWays()); } if (currentPolygon.outer) { java.awt.geom.Area toAdd = Java2DConverter.createArea(currentPolygon.polygon.getPoints()); if (outerResultArea.isEmpty()) outerResultArea = toAdd; else outerResultArea.add(toAdd); for (Way outerWay : currentPolygon.polygon.getOriginalWays()) { if (outmostPolygonProcessing) { for (Entry<String, String> tag : outerWay.getTagEntryIterator()) { outerTags.put(tag.getKey(), tag.getValue()); } outmostPolygonProcessing = false; } else { for (String tag : new ArrayList<String>(outerTags.keySet())) { if (outerTags.get(tag).equals(outerWay.getTag(tag)) == false) { outerTags.remove(tag); } } } } } else { outerResultArea.subtract(Java2DConverter .createArea(currentPolygon.polygon.getPoints())); } } // TODO tagging of the outer ways // if (log.isLoggable(Level.WARNING) && // (outmostInnerPolygons.cardinality()+unfinishedPolygons.cardinality()+nestedOuterPolygons.cardinality()+nestedInnerPolygons.cardinality() >= 1)) { // log.warn("Multipolygon", toBrowseURL(), "contains errors."); // // BitSet outerUnusedPolys = new BitSet(); // outerUnusedPolys.or(unfinishedPolygons); // outerUnusedPolys.or(outmostInnerPolygons); // outerUnusedPolys.or(nestedOuterPolygons); // outerUnusedPolys.or(nestedInnerPolygons); // outerUnusedPolys.or(unfinishedPolygons); // // use only the outer polygons // outerUnusedPolys.and(outerPolygons); // for (JoinedWay w : getWaysFromPolygonList(outerUnusedPolys)) { // outerWaysForLineTagging.addAll(w.getOriginalWays()); // } // // runIntersectionCheck(unfinishedPolygons); // runOutmostInnerPolygonCheck(outmostInnerPolygons); // runNestedOuterPolygonCheck(nestedOuterPolygons); // runNestedInnerPolygonCheck(nestedInnerPolygons); // runWrongInnerPolygonCheck(unfinishedPolygons, innerPolygons); // // // 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 // // => issue a warning // List<JoinedWay> lostWays = getWaysFromPolygonList(unfinishedPolygons); // for (JoinedWay w : lostWays) { // log.warn("Polygon", w, "is not processed due to an unknown reason."); // logWayURLs(Level.WARNING, "-", w); // } // } // if (hasStyleRelevantTags(this)) { outerTags.clear(); for (Entry<String,String> mpTags : getTagEntryIterator()) { if ("type".equals(mpTags.getKey())==false) { outerTags.put(mpTags.getKey(), mpTags.getValue()); } } } else { for (Entry<String,String> mpTags : outerTags.entrySet()) { addTag(mpTags.getKey(), mpTags.getValue()); } } // Go through all original outer ways, create a copy, tag them // with the mp tags and mark them only to be used for polyline processing // This enables the style file to decide if the polygon information or // the simple line information should be used. for (Way orgOuterWay : outerWaysForLineTagging) { // Way lineTagWay = new Way(FakeIdGenerator.makeFakeId(), orgOuterWay.getPoints()); // lineTagWay.setName(orgOuterWay.getName()); // lineTagWay.addTag(STYLE_FILTER_TAG, STYLE_FILTER_LINE); for (Entry<String,String> tag : outerTags.entrySet()) { // lineTagWay.addTag(tag.getKey(), tag.getValue()); // remove the tag from the original way if it has the same value if (tag.getValue().equals(orgOuterWay.getTag(tag.getKey()))) { removeTagsInOrgWays(orgOuterWay, tag.getKey()); } } // if (log.isDebugEnabled()) // log.debug("Add line way", lineTagWay.getId(), lineTagWay.toTagString()); // tileWayMap.put(lineTagWay.getId(), lineTagWay); } postProcessing(); cleanup(); } protected boolean connectUnclosedWays(List<JoinedWay> allWays) { List<JoinedWay> unclosed = new ArrayList<JoinedWay>(); for (JoinedWay w : allWays) { if (w.hasIdenticalEndPoints() == false) { unclosed.add(w); } } // try to connect ways lying outside or on the bbox if (unclosed.size() >= 2) { log.debug("Checking",unclosed.size(),"unclosed ways for connections outside the bbox"); Map<Coord, JoinedWay> outOfBboxPoints = new IdentityHashMap<Coord, JoinedWay>(); // check all ways for endpoints outside or on the bbox for (JoinedWay w : unclosed) { Coord c1 = w.getPoints().get(0); // if (bbox.insideBoundary(c1)==false) { // log.debug("Point",c1,"of way",w.getId(),"outside bbox"); outOfBboxPoints.put(c1, w); // } Coord c2 = w.getPoints().get(w.getPoints().size()-1); // if (bbox.insideBoundary(c2)==false) { // log.debug("Point",c2,"of way",w.getId(),"outside bbox"); outOfBboxPoints.put(c2, w); // } } if (outOfBboxPoints.size() < 2) { log.debug(outOfBboxPoints.size(),"point outside the bbox. No connection possible."); return false; } List<ConnectionData> coordPairs = new ArrayList<ConnectionData>(); ArrayList<Coord> coords = new ArrayList<Coord>(outOfBboxPoints.keySet()); for (int i = 0; i < coords.size(); i++) { for (int j = i + 1; j < coords.size(); j++) { ConnectionData cd = new ConnectionData(); cd.c1 = coords.get(i); cd.c2 = coords.get(j); cd.w1 = outOfBboxPoints.get(cd.c1); cd.w2 = outOfBboxPoints.get(cd.c2); // if (lineCutsBbox(cd.c1, cd.c2 )) { // // Check if the way can be closed with one additional point // // outside the bounding box. // // The additional point is combination of the coords of both endpoints. // // It works if the lines from the endpoints to the additional point does // // not cut the bounding box. // // This can be removed when the splitter guarantees to provide logical complete // // multi-polygons. // Coord edgePoint1 = new Coord(cd.c1.getLatitude(), cd.c2 // .getLongitude()); // Coord edgePoint2 = new Coord(cd.c2.getLatitude(), cd.c1 // .getLongitude()); // // if (lineCutsBbox(cd.c1, edgePoint1) == false // && lineCutsBbox(edgePoint1, cd.c2) == false) { // cd.imC = edgePoint1; // } else if (lineCutsBbox(cd.c1, edgePoint2) == false // && lineCutsBbox(edgePoint2, cd.c2) == false) { // cd.imC = edgePoint1; // } else { // // both endpoints are on opposite sides of the bounding box // // automatically closing such points would create wrong polygons in most cases // continue; // } // cd.distance = cd.c1.distance(cd.imC) + cd.imC.distance(cd.c2); // } else { cd.distance = cd.c1.distance(cd.c2); // } coordPairs.add(cd); } } if (coordPairs.isEmpty()) { log.debug("All potential connections cross the bbox. No connection possible."); return false; } else { // retrieve the connection with the minimum distance ConnectionData minCon = Collections.min(coordPairs, new Comparator<ConnectionData>() { public int compare(ConnectionData o1, ConnectionData o2) { return Double.compare(o1.distance, o2.distance); } }); if (minCon.distance < getMaxCloseDist()) { if (minCon.w1 == minCon.w2) { log.debug("Close a gap in way", minCon.w1); if (minCon.imC != null) minCon.w1.getPoints().add(minCon.imC); minCon.w1.closeWayArtificially(); } else { log.debug("Connect", minCon.w1, "with", minCon.w2); if (minCon.w1.getPoints().get(0) == minCon.c1) { Collections.reverse(minCon.w1.getPoints()); } if (minCon.w2.getPoints().get(0) != minCon.c2) { Collections.reverse(minCon.w2.getPoints()); } minCon.w1.getPoints().addAll(minCon.w2.getPoints()); minCon.w1.addWay(minCon.w2); allWays.remove(minCon.w2); } return true; } } } return false; } private double getMaxCloseDist() { double dist = 1000; String admString= getTag("admin_level"); if ("2".equals(admString)) { dist = 50000; } else if ("3".equals(admString)) { dist = 20000; }else if ("4".equals(admString)) { dist = 4000; } return dist; } protected void closeWays(ArrayList<JoinedWay> wayList) { for (JoinedWay way : wayList) { if (way.hasIdenticalEndPoints() || way.getPoints().size() < 3) { continue; } Coord p1 = way.getPoints().get(0); Coord p2 = way.getPoints().get(way.getPoints().size() - 1); if (getBbox().insideBoundary(p1) == false && getBbox().insideBoundary(p2) == false) { // both points lie outside the bbox or on the bbox // check if both points are on the same side of the bounding box if ((p1.getLatitude() <= getBbox().getMinLat() && p2.getLatitude() <= getBbox() .getMinLat()) || (p1.getLatitude() >= getBbox().getMaxLat() && p2 .getLatitude() >= getBbox().getMaxLat()) || (p1.getLongitude() <= getBbox().getMinLong() && p2 .getLongitude() <= getBbox().getMinLong()) || (p1.getLongitude() >= getBbox().getMaxLong() && p2 .getLongitude() >= getBbox().getMaxLong())) { // they are on the same side outside of the bbox // so just close them without worrying about if // they intersect itself because the intersection also // is outside the bbox way.closeWayArtificially(); log.info("Endpoints of way", way, "are both outside the bbox. Closing it directly."); continue; } } Line2D closingLine = new Line2D.Float(p1.getLongitude(), p1 .getLatitude(), p2.getLongitude(), p2.getLatitude()); boolean intersects = false; Coord lastPoint = null; // don't use the first and the last point // the closing line can intersect only in one point or complete. // Both isn't interesting for this check for (Coord thisPoint : way.getPoints().subList(1, way.getPoints().size() - 1)) { if (lastPoint != null) { if (closingLine.intersectsLine(lastPoint.getLongitude(), lastPoint.getLatitude(), thisPoint.getLongitude(), thisPoint.getLatitude())) { intersects = true; break; } } lastPoint = thisPoint; } if (!intersects) { // close the polygon // the new way segment does not intersect the rest of the // polygon // calc the distance to close double closeDist = way.getPoints().get(0).distance(way.getPoints().get(way.getPoints().size()-1)); if (closeDist <= getMaxCloseDist()) { log.info("Closing way", way); log.info("from", way.getPoints().get(0).toOSMURL()); log.info("to", way.getPoints().get(way.getPoints().size() - 1) .toOSMURL()); // mark this ways as artificially closed way.closeWayArtificially(); } } } } private void removeOutOfBbox(List<JoinedWay> polygons) { ListIterator<JoinedWay> pIter = polygons.listIterator(); while (pIter.hasNext()) { JoinedWay w = pIter.next(); Coord first = w.getPoints().get(0); Coord last = w.getPoints().get(w.getPoints().size() - 1); if (first != last) { // the way is not closed // check if one of start/endpoint is out of the bounding box // in this case it is too risky to close it if (getBbox().contains(first) == false || getBbox().contains(last) == false) { pIter.remove(); } } } } protected void cleanup() { super.cleanup(); this.getElements().clear(); ((ArrayList<?>)this.getElements()).trimToSize(); } }