Hi,

I have rewritten the MultiPoloygonRelation class regarding the algorithm described in http://wiki.openstreetmap.org/wiki/Relation:multipolygon/Algorithm.

This is a first (very early and not very well tested) implementation of this algorithm. I don't have time to go on with it within the next days. Multipolygons are a big discussion in the mailing list at the moment so maybe someone likes to review the code and gives some helpful comments on it. (Any comment is very welcome!!)

These are the open points:
* Tagged inner polygons are not considered yet! This a big open point which must be implemented next. (Some comments point to the place where to do this) * The code currently does not remove any inner or outer polygon segment. This must be changed in future. But at the moment I have no solution how to decide which segment should be removed and which segment must remain for map generation. (To be honest I did not think about it for long ;-) * The inner and outer tags in the polygon are not used at the moment. This must also be changed. * The code is completely unoptimized. It's just an early study if this can improve the multipolygon code. I am happy about any improvements! * The old insertPoints method does not consider the direction of the polygons. This can result in self intersecting polygons. I don't know if that matters but it can easily be improved. I am happy if someone can explain me if that matters than I can implement a fix (hopefully :-). * I don't know if my synthetic ids are generated well. I think there should be one synthetic generator for whole mkgmap?


Have a happy new year (with the multipolygons case fixed :-)))
WanMil
package uk.me.parabola.mkgmap.reader.osm;

import java.awt.Polygon;
import java.awt.geom.Line2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;

/**
 * Representation of an OSM Multipolygon Relation. This will combine the
 * different roles into one area.
 * 
 * @author Rene_A
 */
public class MultiPolygonRelation extends Relation {
        private static final Logger log = Logger
                        .getLogger(MultiPolygonRelation.class);

        // private final List<Way> outerSegments = new ArrayList<Way>();
        // private final List<Way> innerSegments = new ArrayList<Way>();
        private final Map<Long, Way> myWayMap;

        private final ArrayList<BitSet> containsMatrix = new 
ArrayList<BitSet>();

        // this list contains the polygons that should be used to create the 
garmin
        // map
        public final List<Way> polygonResults = new ArrayList<Way>();

        private static final List<String> relationTags = 
Arrays.asList("boundary",
                        "natural", "landuse", "building", "waterway");

        /**
         * Create an instance based on an existing relation. We need to do this
         * because the type of the relation is not known until after all its 
tags
         * are read in.
         * 
         * @param other
         *            The relation to base this one on.
         * @param wayMap
         *            Map of all ways.
         */
        public MultiPolygonRelation(Relation other, Map<Long, Way> wayMap) {
                myWayMap = wayMap;
                setId(other.getId());
                for (Map.Entry<Element, String> pairs : 
other.getRoles().entrySet()) {
                        addElement(pairs.getValue(), pairs.getKey());

                        // String value = pairs.getValue();
                        //
                        // if (value != null && pairs.getKey() instanceof Way) {
                        // Way way = (Way) pairs.getKey();
                        // if (value.equals("outer")) {
                        // outerSegments.add(way);
                        // } else if (value.equals("inner")) {
                        // innerSegments.add(way);
                        // }
                        // }
                }

                setName(other.getName());
                copyTags(other);
        }

        /**
         * Combine a list of way segments to a list of maximally joined ways
         * 
         * @param segments
         *            a list of closed or unclosed ways
         * @return a list of closed ways
         */
        private ArrayList<JoinedWay> joinWays(List<Way> segments) {
                // this method implements RA-1 to RA-4
                // TODO check if the closed polygon is valid and implement a
                // backtracking algorithm to get other combinations

                ArrayList<JoinedWay> joinedWays = new ArrayList<JoinedWay>();
                if (segments == null || segments.size() == 0) {
                        return joinedWays;
                }

                // go through all segments and categorize them to closed and 
unclosed
                // list
                ArrayList<JoinedWay> unclosedWays = new ArrayList<JoinedWay>();
                for (Way orgSegment : segments) {
                        if (orgSegment.isClosed()) {
                                joinedWays.add(new JoinedWay(orgSegment));
                        } else {
                                unclosedWays.add(new JoinedWay(orgSegment));
                        }
                }

                while (unclosedWays.isEmpty() == false) {
                        JoinedWay joinWay = unclosedWays.remove(0);

                        // check if the current way is already closed or if it 
is the last
                        // way
                        if (joinWay.isClosed() || unclosedWays.isEmpty()) {
                                joinedWays.add(joinWay);
                                continue;
                        }

                        boolean joined = false;

                        // go through all ways and check if there is a way that 
can be
                        // joined with it
                        // in this case join the two ways
                        // => add all points of tempWay to joinWay, remove 
tempWay and put
                        // joinWay to the beginning of the list
                        // (not optimal but understandable - can be optimized 
later)
                        for (JoinedWay tempWay : unclosedWays) {
                                if (tempWay.isClosed()) {
                                        continue;
                                }

                                // use == or equals as comparator??
                                if (joinWay.getPoints().get(0) == 
tempWay.getPoints().get(0)) {
                                        for (Coord point : 
tempWay.getPoints().subList(1,
                                                        
tempWay.getPoints().size())) {
                                                joinWay.addPoint(0, point);
                                        }
                                        joined = true;
                                } else if (joinWay.getPoints().get(
                                                joinWay.getPoints().size() - 1) 
== tempWay.getPoints()
                                                .get(0)) {
                                        for (Coord point : 
tempWay.getPoints().subList(1,
                                                        
tempWay.getPoints().size() - 1)) {
                                                joinWay.addPoint(point);
                                        }
                                        joined = true;
                                } else if (joinWay.getPoints().get(0) == 
tempWay.getPoints()
                                                .get(tempWay.getPoints().size() 
- 1)) {
                                        int insertIndex = 0;
                                        for (Coord point : 
tempWay.getPoints().subList(0,
                                                        
tempWay.getPoints().size() - 1)) {
                                                joinWay.addPoint(insertIndex, 
point);
                                                insertIndex++;
                                        }
                                        joined = true;
                                } else if (joinWay.getPoints().get(
                                                joinWay.getPoints().size() - 1) 
== tempWay.getPoints()
                                                .get(tempWay.getPoints().size() 
- 1)) {
                                        int insertIndex = 
joinWay.getPoints().size();
                                        for (Coord point : 
tempWay.getPoints().subList(0,
                                                        
tempWay.getPoints().size() - 1)) {
                                                joinWay.addPoint(insertIndex, 
point);
                                        }
                                        joined = true;
                                }

                                if (joined) {
                                        unclosedWays.remove(tempWay);
                                        joinWay.addWay(tempWay);
                                        break;
                                }
                        }

                        if (joined) {
                                if (joinWay.isClosed()) {
                                        // it's closed => don't process it again
                                        joinedWays.add(joinWay);
                                } else if (unclosedWays.isEmpty()) {
                                        // no more ways to join with
                                        // it's not closed but we cannot join 
it more
                                        joinedWays.add(joinWay);
                                } else {
                                        // it is not yet closed => process it 
once again
                                        unclosedWays.add(0, joinWay);
                                }
                        } else {
                                // it's not closed but we cannot join it more
                                joinedWays.add(joinWay);
                        }
                }

                return joinedWays;
        }

        /**
         * Removes all ways non closed ways from the given list (
         * <code>{...@link Way#isClosed()} == false</code>)
         * 
         * @param wayList
         *            list of ways
         */
        private void removeUnclosedWays(ArrayList<JoinedWay> wayList) {
                Iterator<JoinedWay> it = wayList.iterator();
                boolean first = true;
                while (it.hasNext()) {
                        JoinedWay tempWay = it.next();
                        if (tempWay.isClosed() == false) {
                                if (first) {
                                        log.warn("Unclosed polygons in 
multipolygon relation "
                                                        + getId() + ":");
                                }
                                for (Way orgWay : tempWay.getOriginalWays()) {
                                        log.warn(" - way:", orgWay.getId(), 
"role:", getRoles()
                                                        .get(orgWay), "osm:", 
orgWay.toBrowseURL());
                                }

                                it.remove();
                                first = false;
                        }
                }
        }

        /**
         * 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() {
                // don't care about outer and inner declaration
                // because this is a first 
                ArrayList<Way> allWays = new ArrayList<Way>();
                for (Element element : getElements()) {
                        if (element instanceof Way) {
                                allWays.add((Way) element);
                        } else {
                                log.warn("Non way element", element.getId(), 
"in multipolygon",
                                                getId());
                        }
                }

                ArrayList<JoinedWay> rings = joinWays(allWays);
                removeUnclosedWays(rings);
                // now we have closed ways == rings only

                /* 
                 ===== start considering outer and inner ways ====
                 ArrayList<JoinedWay> joinedOuterWays = joinWays(outerWays);
                 ArrayList<JoinedWay> joinedInnerWays = joinWays(innerWays);
                
                 removeUnclosedWays(joinedOuterWays);
                 removeUnclosedWays(joinedInnerWays);

                // at this point we don't care about outer and inner
                // in the end we will compare if outer and inner tags are 
matching
                // what we detect here and issue some warnings and errors
                 ArrayList<JoinedWay> completeJoinedWays = new
                 ArrayList<JoinedWay>();
                 completeJoinedWays.addAll(joinedOuterWays);
                 completeJoinedWays.addAll(joinedInnerWays);
                 ===== end considering outer and inner ways ====
                 */

                // check if we have at least one ring left
                if (rings.isEmpty() == false) {

                        createContainsMatrix(rings);
                        

                        BitSet unfinishedRings = new BitSet(rings.size());
                        unfinishedRings.set(0, rings.size());

                        while (unfinishedRings.isEmpty() == false) {
                                // there are still unfinished rings

                                // find the next outer ring
                                int outerRingIndex = -1;
                                for (int checkOuterIndex = 
unfinishedRings.nextSetBit(0); checkOuterIndex >= 0; checkOuterIndex = 
unfinishedRings
                                                .nextSetBit(checkOuterIndex + 
1)) {
                                        // check if the checkOuterIndex ring is 
not contained by any
                                        // other unfinished ring
                                        boolean isNotContained = true;
                                        for (int possibleOuterIndex = 
unfinishedRings.nextSetBit(0); possibleOuterIndex >= 0; possibleOuterIndex = 
unfinishedRings
                                                        
.nextSetBit(possibleOuterIndex + 1)) {
                                                if 
(contains(possibleOuterIndex,checkOuterIndex)) {
                                                        isNotContained = false;
                                                        break;
                                                }
                                        }

                                        if (isNotContained) {
                                                // the checkOuterIndex ring is 
not contained by any other
                                                // unfinished ring
                                                outerRingIndex = 
checkOuterIndex;
                                                break;
                                        }
                                }

                                if (outerRingIndex < 0) {
                                        // we have an error in this multipolygon
                                        // => cannot continue
                                        log.error("Multipolygon " + 
toBrowseURL()
                                                        + " contains 
intersected ways");
                                        return;
                                }

                                // outerRingIndex is the ring that is not 
contained by any other
                                // ring
                                JoinedWay outerRing = rings.get(outerRingIndex);
                                // QA: check that all ways carry the role 
"outer" and issue
                                // warnings
                                checkRoles(outerRing.getOriginalWays(), 
"outer");

                                // this ring is now processed and should not be 
used by any
                                // further step
                                unfinishedRings.clear(outerRingIndex);

                                // create a list of inner rings
                                ArrayList<JoinedWay> innerRings = new 
ArrayList<JoinedWay>();
                                BitSet innerIndexes = new BitSet();

                                BitSet outerRingContains = 
containsMatrix.get(outerRingIndex);
                                // use only rings that are contained by the 
outer ring
                                outerRingContains.and(unfinishedRings);
                                // outerRingContains now contains all rings 
that are contained
                                // by the outer ring and that are not finished 
yet

                                // get the holes (inner rings)
                                // go through all inner rings of the outer ring
                                // and leave only the ones that are not 
contained by any other
                                // ring
                                for (int innerRingIndex = 
outerRingContains.nextSetBit(0); innerRingIndex >= 0; innerRingIndex = 
outerRingContains
                                                .nextSetBit(innerRingIndex + 
1)) {
                                        // check if the inner ring is not 
contained by any
                                        // other unfinished ring
                                        boolean realInnerRing = true;
                                        for (int unfinishedIndex = 
unfinishedRings.nextSetBit(0); unfinishedIndex >= 0; unfinishedIndex = 
unfinishedRings
                                                        
.nextSetBit(unfinishedIndex + 1)) {
                                                if (contains(unfinishedIndex, 
innerRingIndex)) {
                                                        realInnerRing = false;
                                                        break;
                                                }
                                        }

                                        if (realInnerRing) {
                                                // this is a real inner ring of 
the outer ring
                                                JoinedWay innerRing = 
rings.get(innerRingIndex);
                                                innerRings.add(innerRing);
                                                
innerIndexes.set(innerRingIndex);

                                                // QA: check that all ways 
carry the role "inner"
                                                // and issue warnings
                                                
checkRoles(innerRing.getOriginalWays(), "inner");

                                                // TODO in case the innerRing 
is tagged itself it
                                                // must also be treated
                                                // as special outer ring with 
its own inner polygons
                                        }
                                }

                                // all inner rings are used now, so they are 
finished
                                unfinishedRings.andNot(innerIndexes);

                                // now construct the outer polygon with its 
holes
                                for (Way innerRing : innerRings) {
                                        int[] insert = 
findCpa(outerRing.getPoints(), innerRing
                                                        .getPoints());
                                        if (insert[0] >= 0 && insert[1] >= 0) {
                                                insertPoints(outerRing, 
innerRing, insert[0], insert[1]);
                                        }
                                }

                                // set the tags of the outer ring
                                // does the multipolygon itself have any tags?
                                boolean validTagFound = false;
                                for (Map.Entry<String, String> tagEntry : 
getEntryIteratable()) {
                                        if 
(relationTags.contains(tagEntry.getKey())) {
                                                validTagFound = true;
                                                break;
                                        }
                                }

                                if (validTagFound) {
                                        // the multipolygon contains tags that 
overwhelm the tags
                                        // out the outer ring
                                        outerRing.copyTags(this);
                                } else {
                                        // the multipolygon does not contain 
any relevant tag
                                        // use the segments of the outer ring 
and merge the tags
                                        for (Way outerRingSegment : 
outerRing.getOriginalWays()) {
                                                // TODO uuh, this is bad => the 
last of the outer ring
                                                // segments win
                                                // => any better idea?
                                                for (Map.Entry<String, String> 
outSegmentTag : outerRingSegment
                                                                
.getEntryIteratable()) {
                                                        
outerRing.addTag(outSegmentTag.getKey(),
                                                                        
outSegmentTag.getValue());
                                                }
                                        }
                                }

                                polygonResults.add(outerRing);
                        }
                }

                // the polygonResults contain all polygons that should be used 
in the
                // map
                // TODO remove the input stuff? inner ways and outer segments?
                for (Way resultWay : polygonResults) {
                        myWayMap.put(resultWay.getId(), resultWay);
                }

        }

        /**
         * This is a QA method. All ways of the given wayList are checked if 
they
         * they carry the checkRole. If not a warning is logged.
         * 
         * @param wayList
         * @param checkRole
         */
        private void checkRoles(List<Way> wayList, String checkRole) {
                // QA: check that all ways carry the role "inner" and issue 
warnings
                for (Way tempWay : wayList) {
                        String realRole = getRoles().get(tempWay);
                        if (checkRole.equals(realRole) == false) {
                                log.warn("Way", tempWay.getId(), "carries 
role", realRole,
                                                "but should carry role", 
checkRole);
                        }
                }
        }

        private void createContainsMatrix(List<JoinedWay> wayList) {
                for (int i = 0; i < wayList.size(); i++) {
                        containsMatrix.add(new BitSet());
                }

                // mark which ring contains which ring
                for (int i = 0; i < wayList.size(); i++) {
                        Way tempRing = wayList.get(i);
                        BitSet bitSet = containsMatrix.get(i);

                        for (int j = 0; j < wayList.size(); j++) {
                                boolean contains = false;
                                if (i == j) {
                                        // this is special: a way does not 
contain itself for
                                        // our usage here
                                        contains = false;
                                } else {
                                        contains = contains(tempRing, 
wayList.get(j));
                                }

                                if (contains) {
                                        bitSet.set(j);
                                }
                        }
                }
        
        }
        
        /**
         * Checks if the ring with ringIndex1 contains the ring with ringIndex2.
         * @param ringIndex1
         * @param ringIndex2
         * @return true if ring(ringIndex1) contains ring(ringIndex2)
         */
        private boolean contains(int ringIndex1, int ringIndex2) {
                return containsMatrix.get(ringIndex1).get(ringIndex2);
        }

        /**
         * Checks if ring1 contains ring2.
         * 
         * @param ring1
         *            a closed way
         * @param ring2
         * @return true if ring1 contains ring2
         */
        private boolean contains(Way ring1, Way ring2) {
                // TODO this is a simple algorithm
                // might be improved 
                
                if (ring1.isClosed() == false) {
                        return false;
                }
                Polygon p = new Polygon();
                for (Coord c : ring1.getPoints()) {
                        p.addPoint(c.getLatitude(), c.getLongitude());
                }

                Coord p0 = ring2.getPoints().get(0);
                if (p.contains(p0.getLatitude(), p0.getLongitude()) == false) {
                        // we have one point that is not in way1 => way1 does 
not contain
                        // way2
                        return false;
                }

                // check all lines of way1 and way2 for intersections
                Iterator<Coord> it2 = ring2.getPoints().iterator();
                Coord p2_1 = it2.next();
                Coord p2_2 = null;
                while (it2.hasNext()) {
                        p2_2 = p2_1;
                        p2_1 = it2.next();

                        Iterator<Coord> it1 = ring1.getPoints().iterator();
                        Coord p1_1 = it1.next();
                        Coord p1_2 = null;
                        while (it1.hasNext()) {
                                p1_2 = p1_1;
                                p1_1 = it1.next();

                                boolean intersects = 
Line2D.linesIntersect(p1_1.getLatitude(),
                                                p1_1.getLongitude(), 
p1_2.getLatitude(), p1_2
                                                                
.getLongitude(), p2_1.getLatitude(), p2_1
                                                                
.getLongitude(), p2_2.getLatitude(), p2_2
                                                                
.getLongitude());

                                if (intersects) {
                                        return false;
                                }
                        }
                }

                // don't have any intersection
                // => ring1 contains ring2
                return true;
        }

        /**
         * Insert Coordinates into the outer way.
         * 
         * @param outer
         *            the outer way 
         * @param inner
         *            Way to be inserted
         * @param out
         *            Coordinates will be inserted after this point in the outer
         *            way.
         * @param in
         *            Points will be inserted starting at this index, then from
         *            element 0 to (including) this element;
         */
        private void insertPoints(Way outer, Way inner, int out, int in) {
                // TODO this algorithm may generate a self intersecting polygon
                // because it does not consider the direction of both ways
                // don't know if that's a problem
                
                List<Coord> outList = outer.getPoints();
                List<Coord> inList = inner.getPoints();
                int index = out + 1;
                for (int i = in; i < inList.size(); i++) {
                        outList.add(index++, inList.get(i));
                }
                for (int i = 0; i < in; i++) {
                        outList.add(index++, inList.get(i));
                }

                // Investigate and see if we can do the first alternative here 
by
                // changing the polygon splitter. If not then always do the 
alternative
                // and remove unused code.
                if (outer.getPoints().size() < 0 /* Always use alternative 
method for now */) {
                        outList.add(index++, inList.get(in));
                        outList.add(index, outList.get(out));
                } else {
                        // we shift the nodes to avoid duplicate nodes (large 
areas only)
                        int oLat = outList.get(out).getLatitude();
                        int oLon = outList.get(out).getLongitude();
                        int iLat = inList.get(in).getLatitude();
                        int iLon = inList.get(in).getLongitude();
                        if (Math.abs(oLat - iLat) > Math.abs(oLon - iLon)) {
                                int delta = (oLon > iLon) ? -1 : 1;
                                outList.add(index++, new Coord(iLat + delta, 
iLon));
                                outList.add(index, new Coord(oLat + delta, 
oLon));
                        } else {
                                int delta = (oLat > iLat) ? 1 : -1;
                                outList.add(index++, new Coord(iLat, iLon + 
delta));
                                outList.add(index, new Coord(oLat, oLon + 
delta));
                        }
                }
        }

        /**
         * find the Closest Point of Approach between two coordinate-lists This 
will
         * probably be moved to a Utils class
         * 
         * @param l1
         *            First list of points.
         * @param l2
         *            Second list of points.
         * @return The first element is the index in l1, the second in l2 which 
are
         *         the closest together.
         */
        private static int[] findCpa(List<Coord> l1, List<Coord> l2) {
                double oldDistance = Double.MAX_VALUE;
                Coord found1 = null;
                Coord found2 = null;

                for (Coord c1 : l1) {
                        for (Coord c2 : l2) {
                                double newDistance = 
c1.distanceInDegreesSquared(c2);
                                if (newDistance < oldDistance) {
                                        oldDistance = newDistance;
                                        found1 = c1;
                                        found2 = c2;
                                }
                        }
                }

                return new int[] { l1.indexOf(found1), l2.indexOf(found2) };
        }

        /**
         * This is a helper class that stores that gives access to the original 
segments
         * of a joined way.
         */
        private static class JoinedWay extends Way {
                private final List<Way> originalWays;

                public JoinedWay(Way originalWay) {
                        super(-originalWay.getId(), new 
ArrayList<Coord>(originalWay
                                        .getPoints()));
                        this.originalWays = new ArrayList<Way>();
                        this.originalWays.add(originalWay);
                }

                public void addPoint(int index, Coord point) {
                        getPoints().add(index, point);
                }

                public void addWay(Way way) {
                        if (way instanceof JoinedWay) {
                                this.originalWays
                                                .addAll(((JoinedWay) 
way).getOriginalWays());
                        } else {
                                this.originalWays.add(way);
                        }
                }

                public List<Way> getOriginalWays() {
                        return originalWays;
                }

        }

}
_______________________________________________
mkgmap-dev mailing list
[email protected]
http://www.mkgmap.org.uk/mailman/listinfo/mkgmap-dev

Reply via email to