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