Subversion Repositories mkgmap

Rev

Rev 3221 | View as "text/plain" | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * 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();
        }
}