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