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