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.OTHER, tr("With the currently used mappaint style the style for inner way equals the multipolygon style"), 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("With the currently used mappaint style(s) the 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}