001// License: GPL. See LICENSE file for details.
002package org.openstreetmap.josm.data.validation.tests;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.awt.geom.GeneralPath;
008import java.text.MessageFormat;
009import java.util.ArrayList;
010import java.util.Arrays;
011import java.util.Collection;
012import java.util.Collections;
013import java.util.HashSet;
014import java.util.LinkedList;
015import java.util.List;
016import java.util.Set;
017
018import org.openstreetmap.josm.Main;
019import org.openstreetmap.josm.actions.CreateMultipolygonAction;
020import org.openstreetmap.josm.data.osm.Node;
021import org.openstreetmap.josm.data.osm.OsmPrimitive;
022import org.openstreetmap.josm.data.osm.Relation;
023import org.openstreetmap.josm.data.osm.RelationMember;
024import org.openstreetmap.josm.data.osm.Way;
025import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
026import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.JoinedWay;
027import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;
028import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
029import org.openstreetmap.josm.data.validation.OsmValidator;
030import org.openstreetmap.josm.data.validation.Severity;
031import org.openstreetmap.josm.data.validation.Test;
032import org.openstreetmap.josm.data.validation.TestError;
033import org.openstreetmap.josm.gui.DefaultNameFormatter;
034import org.openstreetmap.josm.gui.mappaint.AreaElemStyle;
035import org.openstreetmap.josm.gui.mappaint.ElemStyles;
036import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
037import org.openstreetmap.josm.gui.progress.ProgressMonitor;
038import org.openstreetmap.josm.tools.Pair;
039
040/**
041 * Checks if multipolygons are valid
042 * @since 3669
043 */
044public class MultipolygonTest extends Test {
045
046    protected static final int WRONG_MEMBER_TYPE = 1601;
047    protected static final int WRONG_MEMBER_ROLE = 1602;
048    protected static final int NON_CLOSED_WAY = 1603;
049    protected static final int MISSING_OUTER_WAY = 1604;
050    protected static final int INNER_WAY_OUTSIDE = 1605;
051    protected static final int CROSSING_WAYS = 1606;
052    protected static final int OUTER_STYLE_MISMATCH = 1607;
053    protected static final int INNER_STYLE_MISMATCH = 1608;
054    protected static final int NOT_CLOSED = 1609;
055    protected static final int NO_STYLE = 1610;
056    protected static final int NO_STYLE_POLYGON = 1611;
057    protected static final int OUTER_STYLE = 1613;
058
059    private static ElemStyles styles;
060
061    private final List<List<Node>> nonClosedWays = new ArrayList<>();
062    private final Set<String> keysCheckedByAnotherTest = new HashSet<>();
063
064    /**
065     * Constructs a new {@code MultipolygonTest}.
066     */
067    public MultipolygonTest() {
068        super(tr("Multipolygon"),
069                tr("This test checks if multipolygons are valid."));
070    }
071
072    @Override
073    public void initialize() {
074        styles = MapPaintStyles.getStyles();
075    }
076
077    @Override
078    public void startTest(ProgressMonitor progressMonitor) {
079        super.startTest(progressMonitor);
080        keysCheckedByAnotherTest.clear();
081        for (Test t : OsmValidator.getEnabledTests(false)) {
082            if (t instanceof UnclosedWays) {
083                keysCheckedByAnotherTest.addAll(((UnclosedWays)t).getCheckedKeys());
084                break;
085            }
086        }
087    }
088
089    @Override
090    public void endTest() {
091        keysCheckedByAnotherTest.clear();
092        super.endTest();
093    }
094
095    private List<List<Node>> joinWays(Collection<Way> ways) {
096        List<List<Node>> result = new ArrayList<>();
097        List<Way> waysToJoin = new ArrayList<>();
098        for (Way way : ways) {
099            if (way.isClosed()) {
100                result.add(way.getNodes());
101            } else {
102                waysToJoin.add(way);
103            }
104        }
105
106        for (JoinedWay jw : Multipolygon.joinWays(waysToJoin)) {
107            if (!jw.isClosed()) {
108                nonClosedWays.add(jw.getNodes());
109            } else {
110                result.add(jw.getNodes());
111            }
112        }
113        return result;
114    }
115
116    private GeneralPath createPath(List<Node> nodes) {
117        GeneralPath result = new GeneralPath();
118        result.moveTo((float) nodes.get(0).getCoor().lat(), (float) nodes.get(0).getCoor().lon());
119        for (int i=1; i<nodes.size(); i++) {
120            Node n = nodes.get(i);
121            result.lineTo((float) n.getCoor().lat(), (float) n.getCoor().lon());
122        }
123        return result;
124    }
125
126    private List<GeneralPath> createPolygons(List<List<Node>> joinedWays) {
127        List<GeneralPath> result = new ArrayList<>();
128        for (List<Node> way : joinedWays) {
129            result.add(createPath(way));
130        }
131        return result;
132    }
133
134    private Intersection getPolygonIntersection(GeneralPath outer, List<Node> inner) {
135        boolean inside = false;
136        boolean outside = false;
137
138        for (Node n : inner) {
139            boolean contains = outer.contains(n.getCoor().lat(), n.getCoor().lon());
140            inside = inside | contains;
141            outside = outside | !contains;
142            if (inside & outside) {
143                return Intersection.CROSSING;
144            }
145        }
146
147        return inside ? Intersection.INSIDE : Intersection.OUTSIDE;
148    }
149
150    @Override
151    public void visit(Way w) {
152        if (!w.isArea() && ElemStyles.hasOnlyAreaElemStyle(w)) {
153            List<Node> nodes = w.getNodes();
154            if (nodes.size()<1) return; // fix zero nodes bug
155            for (String key : keysCheckedByAnotherTest) {
156                if (w.hasKey(key)) {
157                    return;
158                }
159            }
160            errors.add(new TestError(this, Severity.WARNING, tr("Area style way is not closed"), NOT_CLOSED,
161                    Collections.singletonList(w), Arrays.asList(nodes.get(0), nodes.get(nodes.size() - 1))));
162        }
163    }
164
165    @Override
166    public void visit(Relation r) {
167        nonClosedWays.clear();
168        if (r.isMultipolygon()) {
169            checkMembersAndRoles(r);
170
171            Multipolygon polygon = MultipolygonCache.getInstance().get(Main.map.mapView, r);
172
173            boolean hasOuterWay = false;
174            for (RelationMember m : r.getMembers()) {
175                if ("outer".equals(m.getRole())) {
176                    hasOuterWay = true;
177                    break;
178                }
179            }
180            if (!hasOuterWay) {
181                addError(r, new TestError(this, Severity.WARNING, tr("No outer way for multipolygon"), MISSING_OUTER_WAY, r));
182            }
183
184            if (r.hasIncompleteMembers()) {
185                return; // Rest of checks is only for complete multipolygons
186            }
187
188            // Create new multipolygon using the logics from CreateMultipolygonAction and see if roles match.
189            final Pair<Relation, Relation> newMP = CreateMultipolygonAction.createMultipolygonRelation(r.getMemberPrimitives(Way.class), false);
190            if (newMP != null) {
191                for (RelationMember member : r.getMembers()) {
192                    final Collection<RelationMember> memberInNewMP = newMP.b.getMembersFor(Collections.singleton(member.getMember()));
193                    if (memberInNewMP != null && !memberInNewMP.isEmpty()) {
194                        final String roleInNewMP = memberInNewMP.iterator().next().getRole();
195                        if (!member.getRole().equals(roleInNewMP)) {
196                            addError(r, new TestError(this, Severity.WARNING, RelationChecker.ROLE_VERIF_PROBLEM_MSG,
197                                    tr("Role for ''{0}'' should be ''{1}''",
198                                            member.getMember().getDisplayName(DefaultNameFormatter.getInstance()), roleInNewMP),
199                                    MessageFormat.format("Role for ''{0}'' should be ''{1}''",
200                                            member.getMember().getDisplayName(DefaultNameFormatter.getInstance()), roleInNewMP),
201                                    WRONG_MEMBER_ROLE, Collections.singleton(r), Collections.singleton(member.getMember())));
202                        }
203                    }
204                }
205            }
206
207            List<List<Node>> innerWays = joinWays(polygon.getInnerWays()); // Side effect - sets nonClosedWays
208            List<List<Node>> outerWays = joinWays(polygon.getOuterWays());
209            if (styles != null && !"boundary".equals(r.get("type"))) {
210                AreaElemStyle area = ElemStyles.getAreaElemStyle(r, false);
211                boolean areaStyle = area != null;
212                // If area style was not found for relation then use style of ways
213                if (area == null) {
214                    for (Way w : polygon.getOuterWays()) {
215                        area = ElemStyles.getAreaElemStyle(w, true);
216                        if (area != null) {
217                            break;
218                        }
219                    }
220                    if (area == null) {
221                        addError(r, new TestError(this, Severity.OTHER, tr("No area style for multipolygon"), NO_STYLE, r));
222                    } else {
223                        /* old style multipolygon - solve: copy tags from outer way to multipolygon */
224                        addError(r, new TestError(this, Severity.WARNING, 
225                                trn("Multipolygon relation should be tagged with area tags and not the outer way",
226                                        "Multipolygon relation should be tagged with area tags and not the outer ways", polygon.getOuterWays().size()),
227                           NO_STYLE_POLYGON, r));
228                    }
229                }
230
231                if (area != null) {
232                    for (Way wInner : polygon.getInnerWays()) {
233                        AreaElemStyle areaInner = ElemStyles.getAreaElemStyle(wInner, false);
234
235                        if (areaInner != null && area.equals(areaInner)) {
236                            List<OsmPrimitive> l = new ArrayList<>();
237                            l.add(r);
238                            l.add(wInner);
239                            addError(r, new TestError(this, Severity.WARNING, tr("Style for inner way equals multipolygon"),
240                                    INNER_STYLE_MISMATCH, l, Collections.singletonList(wInner)));
241                        }
242                    }
243                    for (Way wOuter : polygon.getOuterWays()) {
244                        AreaElemStyle areaOuter = ElemStyles.getAreaElemStyle(wOuter, false);
245                        if (areaOuter != null) {
246                            List<OsmPrimitive> l = new ArrayList<>();
247                            l.add(r);
248                            l.add(wOuter);
249                            if (!area.equals(areaOuter)) {
250                                addError(r, new TestError(this, Severity.WARNING, !areaStyle ? tr("Style for outer way mismatches")
251                                : tr("Style for outer way mismatches polygon"),
252                                OUTER_STYLE_MISMATCH, l, Collections.singletonList(wOuter)));
253                            } else if (areaStyle) { /* style on outer way of multipolygon, but equal to polygon */
254                                addError(r, new TestError(this, Severity.WARNING, tr("Area style on outer way"), OUTER_STYLE,
255                                l, Collections.singletonList(wOuter)));
256                            }
257                        }
258                    }
259                }
260            }
261
262            List<Node> openNodes = new LinkedList<>();
263            for (List<Node> w : nonClosedWays) {
264                if (w.size()<1) continue;
265                openNodes.add(w.get(0));
266                openNodes.add(w.get(w.size() - 1));
267            }
268            if (!openNodes.isEmpty()) {
269                List<OsmPrimitive> primitives = new LinkedList<>();
270                primitives.add(r);
271                primitives.addAll(openNodes);
272                Arrays.asList(openNodes, r);
273                addError(r, new TestError(this, Severity.WARNING, tr("Multipolygon is not closed"), NON_CLOSED_WAY,
274                        primitives, openNodes));
275            }
276
277            // For painting is used Polygon class which works with ints only. For validation we need more precision
278            List<GeneralPath> outerPolygons = createPolygons(outerWays);
279            for (List<Node> pdInner : innerWays) {
280                boolean outside = true;
281                boolean crossing = false;
282                List<Node> outerWay = null;
283                for (int i=0; i<outerWays.size(); i++) {
284                    GeneralPath outer = outerPolygons.get(i);
285                    Intersection intersection = getPolygonIntersection(outer, pdInner);
286                    outside = outside & intersection == Intersection.OUTSIDE;
287                    if (intersection == Intersection.CROSSING) {
288                        crossing = true;
289                        outerWay = outerWays.get(i);
290                    }
291                }
292                if (outside || crossing) {
293                    List<List<Node>> highlights = new ArrayList<>();
294                    highlights.add(pdInner);
295                    if (outside) {
296                        addError(r, new TestError(this, Severity.WARNING, tr("Multipolygon inner way is outside"), INNER_WAY_OUTSIDE, Collections.singletonList(r), highlights));
297                    } else if (crossing) {
298                        highlights.add(outerWay);
299                        addError(r, new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"), CROSSING_WAYS, Collections.singletonList(r), highlights));
300                    }
301                }
302            }
303        }
304    }
305
306    private void checkMembersAndRoles(Relation r) {
307        for (RelationMember rm : r.getMembers()) {
308            if (rm.isWay()) {
309                if (!(rm.hasRole("inner", "outer") || !rm.hasRole())) {
310                    addError(r, new TestError(this, Severity.WARNING, tr("No useful role for multipolygon member"), WRONG_MEMBER_ROLE, rm.getMember()));
311                }
312            } else {
313                if (!rm.hasRole("admin_centre", "label", "subarea", "land_area")) {
314                    addError(r, new TestError(this, Severity.WARNING, tr("Non-Way in multipolygon"), WRONG_MEMBER_TYPE, rm.getMember()));
315                }
316            }
317        }
318    }
319
320    private void addRelationIfNeeded(TestError error, Relation r) {
321        // Fix #8212 : if the error references only incomplete primitives,
322        // add multipolygon in order to let user select something and fix the error
323        Collection<? extends OsmPrimitive> primitives = error.getPrimitives();
324        if (!primitives.contains(r)) {
325            for (OsmPrimitive p : primitives) {
326                if (!p.isIncomplete()) {
327                    return;
328                }
329            }
330            List<OsmPrimitive> newPrimitives = new ArrayList<>(primitives);
331            newPrimitives.add(0, r);
332            error.setPrimitives(newPrimitives);
333        }
334    }
335
336    private void addError(Relation r, TestError error) {
337        addRelationIfNeeded(error, r);
338        errors.add(error);
339    }
340}