001    // License: GPL. See LICENSE file for details.
002    package org.openstreetmap.josm.data.validation.tests;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.awt.geom.GeneralPath;
007    import java.util.ArrayList;
008    import java.util.Arrays;
009    import java.util.Collection;
010    import java.util.Collections;
011    import java.util.LinkedList;
012    import java.util.List;
013    
014    import org.openstreetmap.josm.Main;
015    import org.openstreetmap.josm.data.osm.Node;
016    import org.openstreetmap.josm.data.osm.OsmPrimitive;
017    import org.openstreetmap.josm.data.osm.Relation;
018    import org.openstreetmap.josm.data.osm.RelationMember;
019    import org.openstreetmap.josm.data.osm.Way;
020    import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
021    import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.JoinedWay;
022    import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;
023    import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
024    import org.openstreetmap.josm.data.validation.Severity;
025    import org.openstreetmap.josm.data.validation.Test;
026    import org.openstreetmap.josm.data.validation.TestError;
027    import org.openstreetmap.josm.gui.mappaint.AreaElemStyle;
028    import org.openstreetmap.josm.gui.mappaint.ElemStyles;
029    import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
030    
031    public class MultipolygonTest extends Test {
032    
033        protected static final int WRONG_MEMBER_TYPE = 1601;
034        protected static final int WRONG_MEMBER_ROLE = 1602;
035        protected static final int NON_CLOSED_WAY = 1603;
036        protected static final int MISSING_OUTER_WAY = 1604;
037        protected static final int INNER_WAY_OUTSIDE = 1605;
038        protected static final int CROSSING_WAYS = 1606;
039        protected static final int OUTER_STYLE_MISMATCH = 1607;
040        protected static final int INNER_STYLE_MISMATCH = 1608;
041        protected static final int NOT_CLOSED = 1609;
042        protected static final int NO_STYLE = 1610;
043        protected static final int NO_STYLE_POLYGON = 1611;
044    
045        private static ElemStyles styles;
046    
047        private final List<List<Node>> nonClosedWays = new ArrayList<List<Node>>();
048    
049        private final double SCALE = 1.0; // arbitrary scale - we could test every possible scale, but this should suffice
050    
051        public MultipolygonTest() {
052            super(tr("Multipolygon"),
053                    tr("This test checks if multipolygons are valid."));
054        }
055    
056        @Override
057        public void initialize() throws Exception {
058            styles = MapPaintStyles.getStyles();
059        }
060    
061        private List<List<Node>> joinWays(Collection<Way> ways) {
062            List<List<Node>> result = new ArrayList<List<Node>>();
063            List<Way> waysToJoin = new ArrayList<Way>();
064            for (Way way : ways) {
065                if (way.isClosed()) {
066                    result.add(way.getNodes());
067                } else {
068                    waysToJoin.add(way);
069                }
070            }
071    
072            for (JoinedWay jw : Multipolygon.joinWays(waysToJoin)) {
073                if (!jw.isClosed()) {
074                    nonClosedWays.add(jw.getNodes());
075                } else {
076                    result.add(jw.getNodes());
077                }
078            }
079            return result;
080        }
081    
082        private GeneralPath createPath(List<Node> nodes) {
083            GeneralPath result = new GeneralPath();
084            result.moveTo((float) nodes.get(0).getCoor().lat(), (float) nodes.get(0).getCoor().lon());
085            for (int i=1; i<nodes.size(); i++) {
086                Node n = nodes.get(i);
087                result.lineTo((float) n.getCoor().lat(), (float) n.getCoor().lon());
088            }
089            return result;
090        }
091    
092        private List<GeneralPath> createPolygons(List<List<Node>> joinedWays) {
093            List<GeneralPath> result = new ArrayList<GeneralPath>();
094            for (List<Node> way : joinedWays) {
095                result.add(createPath(way));
096            }
097            return result;
098        }
099    
100        private Intersection getPolygonIntersection(GeneralPath outer, List<Node> inner) {
101            boolean inside = false;
102            boolean outside = false;
103    
104            for (Node n : inner) {
105                boolean contains = outer.contains(n.getCoor().lat(), n.getCoor().lon());
106                inside = inside | contains;
107                outside = outside | !contains;
108                if (inside & outside) {
109                    return Intersection.CROSSING;
110                }
111            }
112    
113            return inside ? Intersection.INSIDE : Intersection.OUTSIDE;
114        }
115    
116        @Override
117        public void visit(Way w) {
118            if (!w.isClosed() && ElemStyles.hasAreaElemStyle(w, false)) {
119                List<Node> nodes = w.getNodes();
120                if (nodes.size()<1) return; // fix zero nodes bug
121                errors.add(new TestError(this, Severity.WARNING, tr("Area style way is not closed"), NOT_CLOSED,
122                        Collections.singletonList(w), Arrays.asList(nodes.get(0), nodes.get(nodes.size() - 1))));
123            }
124        }
125    
126        @Override
127        public void visit(Relation r) {
128            nonClosedWays.clear();
129            if (r.isMultipolygon()) {
130                checkMembersAndRoles(r);
131    
132                Multipolygon polygon = MultipolygonCache.getInstance().get(Main.map.mapView, r);
133    
134                boolean hasOuterWay = false;
135                for (RelationMember m : r.getMembers()) {
136                    if ("outer".equals(m.getRole())) {
137                        hasOuterWay = true;
138                        break;
139                    }
140                }
141                if (!hasOuterWay) {
142                    errors.add(new TestError(this, Severity.WARNING, tr("No outer way for multipolygon"), MISSING_OUTER_WAY, r));
143                }
144    
145                for (RelationMember rm : r.getMembers()) {
146                    if (!rm.getMember().isUsable())
147                        return; // Rest of checks is only for complete multipolygons
148                }
149    
150                List<List<Node>> innerWays = joinWays(polygon.getInnerWays()); // Side effect - sets nonClosedWays
151                List<List<Node>> outerWays = joinWays(polygon.getOuterWays());
152                if (styles != null) {
153    
154                    AreaElemStyle area = ElemStyles.getAreaElemStyle(r, false);
155                    boolean areaStyle = area != null;
156                    // If area style was not found for relation then use style of ways
157                    if (area == null) {
158                        for (Way w : polygon.getOuterWays()) {
159                            area = ElemStyles.getAreaElemStyle(w, true);
160                            if (area != null) {
161                                break;
162                            }
163                        }
164                        if(area == null)
165                            errors.add(new TestError(this, Severity.OTHER, tr("No style for multipolygon"), NO_STYLE, r));
166                        else
167                            errors.add( new TestError(this, Severity.OTHER, tr("No style in multipolygon relation"),
168                                NO_STYLE_POLYGON, r));
169                    }
170    
171                    if (area != null) {
172                        for (Way wInner : polygon.getInnerWays()) {
173                            AreaElemStyle areaInner = ElemStyles.getAreaElemStyle(wInner, false);
174    
175                            if (areaInner != null && area.equals(areaInner)) {
176                                List<OsmPrimitive> l = new ArrayList<OsmPrimitive>();
177                                l.add(r);
178                                l.add(wInner);
179                                errors.add( new TestError(this, Severity.WARNING, tr("Style for inner way equals multipolygon"),
180                                        INNER_STYLE_MISMATCH, l, Collections.singletonList(wInner)));
181                            }
182                        }
183                        if(!areaStyle) {
184                            for (Way wOuter : polygon.getOuterWays()) {
185                                AreaElemStyle areaOuter = ElemStyles.getAreaElemStyle(wOuter, false);
186                                if (areaOuter != null && !area.equals(areaOuter)) {
187                                    List<OsmPrimitive> l = new ArrayList<OsmPrimitive>();
188                                    l.add(r);
189                                    l.add(wOuter);
190                                    errors.add(new TestError(this, Severity.WARNING, tr("Style for outer way mismatches"),
191                                    OUTER_STYLE_MISMATCH, l, Collections.singletonList(wOuter)));
192                                }
193                            }
194                        }
195                    }
196                }
197    
198                List<Node> openNodes = new LinkedList<Node>();
199                for (List<Node> w : nonClosedWays) {
200                    if (w.size()<1) continue;
201                    openNodes.add(w.get(0));
202                    openNodes.add(w.get(w.size() - 1));
203                }
204                if (!openNodes.isEmpty()) {
205                    List<OsmPrimitive> primitives = new LinkedList<OsmPrimitive>();
206                    primitives.add(r);
207                    primitives.addAll(openNodes);
208                    Arrays.asList(openNodes, r);
209                    errors.add(new TestError(this, Severity.WARNING, tr("Multipolygon is not closed"), NON_CLOSED_WAY,
210                            primitives, openNodes));
211                }
212    
213                // For painting is used Polygon class which works with ints only. For validation we need more precision
214                List<GeneralPath> outerPolygons = createPolygons(outerWays);
215                for (List<Node> pdInner : innerWays) {
216                    boolean outside = true;
217                    boolean crossing = false;
218                    List<Node> outerWay = null;
219                    for (int i=0; i<outerWays.size(); i++) {
220                        GeneralPath outer = outerPolygons.get(i);
221                        Intersection intersection = getPolygonIntersection(outer, pdInner);
222                        outside = outside & intersection == Intersection.OUTSIDE;
223                        if (intersection == Intersection.CROSSING) {
224                            crossing = true;
225                            outerWay = outerWays.get(i);
226                        }
227                    }
228                    if (outside || crossing) {
229                        List<List<Node>> highlights = new ArrayList<List<Node>>();
230                        highlights.add(pdInner);
231                        if (outside) {
232                            errors.add(new TestError(this, Severity.WARNING, tr("Multipolygon inner way is outside"), INNER_WAY_OUTSIDE, Collections.singletonList(r), highlights));
233                        } else if (crossing) {
234                            highlights.add(outerWay);
235                            errors.add(new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"), CROSSING_WAYS, Collections.singletonList(r), highlights));
236                        }
237                    }
238                }
239            }
240        }
241    
242        private void checkMembersAndRoles(Relation r) {
243            for (RelationMember rm : r.getMembers()) {
244                if (rm.isWay()) {
245                    if (!("inner".equals(rm.getRole()) || "outer".equals(rm.getRole()) || !rm.hasRole())) {
246                        errors.add(new TestError(this, Severity.WARNING, tr("No useful role for multipolygon member"), WRONG_MEMBER_ROLE, rm.getMember()));
247                    }
248                } else {
249                    if(!"admin_centre".equals(rm.getRole()))
250                        errors.add(new TestError(this, Severity.WARNING, tr("Non-Way in multipolygon"), WRONG_MEMBER_TYPE, rm.getMember()));
251                }
252            }
253        }
254    }