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.AreaElemStyle; 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 AreaElemStyle 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 AreaElemStyle 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 AreaElemStyle 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 { 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 List<OsmPrimitive> newPrimitives = new ArrayList<OsmPrimitive>(primitives); 305 newPrimitives.add(0, r); 306 error.setPrimitives(newPrimitives); 307 } 308 } 309 310 private void addError(Relation r, TestError error) { 311 addRelationIfNeeded(error, r); 312 errors.add(error); 313 } 314}