001 // License: GPL. See LICENSE file for details. 002 package org.openstreetmap.josm.data.validation.tests; 003 004 import static org.openstreetmap.josm.tools.I18n.tr; 005 006 import java.awt.geom.GeneralPath; 007 import java.util.ArrayList; 008 import java.util.Arrays; 009 import java.util.Collection; 010 import java.util.Collections; 011 import java.util.LinkedList; 012 import java.util.List; 013 014 import org.openstreetmap.josm.Main; 015 import org.openstreetmap.josm.data.osm.Node; 016 import org.openstreetmap.josm.data.osm.OsmPrimitive; 017 import org.openstreetmap.josm.data.osm.Relation; 018 import org.openstreetmap.josm.data.osm.RelationMember; 019 import org.openstreetmap.josm.data.osm.Way; 020 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon; 021 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.JoinedWay; 022 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection; 023 import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; 024 import org.openstreetmap.josm.data.validation.Severity; 025 import org.openstreetmap.josm.data.validation.Test; 026 import org.openstreetmap.josm.data.validation.TestError; 027 import org.openstreetmap.josm.gui.mappaint.AreaElemStyle; 028 import org.openstreetmap.josm.gui.mappaint.ElemStyles; 029 import org.openstreetmap.josm.gui.mappaint.MapPaintStyles; 030 031 public class MultipolygonTest extends Test { 032 033 protected static final int WRONG_MEMBER_TYPE = 1601; 034 protected static final int WRONG_MEMBER_ROLE = 1602; 035 protected static final int NON_CLOSED_WAY = 1603; 036 protected static final int MISSING_OUTER_WAY = 1604; 037 protected static final int INNER_WAY_OUTSIDE = 1605; 038 protected static final int CROSSING_WAYS = 1606; 039 protected static final int OUTER_STYLE_MISMATCH = 1607; 040 protected static final int INNER_STYLE_MISMATCH = 1608; 041 protected static final int NOT_CLOSED = 1609; 042 protected static final int NO_STYLE = 1610; 043 protected static final int NO_STYLE_POLYGON = 1611; 044 045 private static ElemStyles styles; 046 047 private final List<List<Node>> nonClosedWays = new ArrayList<List<Node>>(); 048 049 private final double SCALE = 1.0; // arbitrary scale - we could test every possible scale, but this should suffice 050 051 public MultipolygonTest() { 052 super(tr("Multipolygon"), 053 tr("This test checks if multipolygons are valid.")); 054 } 055 056 @Override 057 public void initialize() throws Exception { 058 styles = MapPaintStyles.getStyles(); 059 } 060 061 private List<List<Node>> joinWays(Collection<Way> ways) { 062 List<List<Node>> result = new ArrayList<List<Node>>(); 063 List<Way> waysToJoin = new ArrayList<Way>(); 064 for (Way way : ways) { 065 if (way.isClosed()) { 066 result.add(way.getNodes()); 067 } else { 068 waysToJoin.add(way); 069 } 070 } 071 072 for (JoinedWay jw : Multipolygon.joinWays(waysToJoin)) { 073 if (!jw.isClosed()) { 074 nonClosedWays.add(jw.getNodes()); 075 } else { 076 result.add(jw.getNodes()); 077 } 078 } 079 return result; 080 } 081 082 private GeneralPath createPath(List<Node> nodes) { 083 GeneralPath result = new GeneralPath(); 084 result.moveTo((float) nodes.get(0).getCoor().lat(), (float) nodes.get(0).getCoor().lon()); 085 for (int i=1; i<nodes.size(); i++) { 086 Node n = nodes.get(i); 087 result.lineTo((float) n.getCoor().lat(), (float) n.getCoor().lon()); 088 } 089 return result; 090 } 091 092 private List<GeneralPath> createPolygons(List<List<Node>> joinedWays) { 093 List<GeneralPath> result = new ArrayList<GeneralPath>(); 094 for (List<Node> way : joinedWays) { 095 result.add(createPath(way)); 096 } 097 return result; 098 } 099 100 private Intersection getPolygonIntersection(GeneralPath outer, List<Node> inner) { 101 boolean inside = false; 102 boolean outside = false; 103 104 for (Node n : inner) { 105 boolean contains = outer.contains(n.getCoor().lat(), n.getCoor().lon()); 106 inside = inside | contains; 107 outside = outside | !contains; 108 if (inside & outside) { 109 return Intersection.CROSSING; 110 } 111 } 112 113 return inside ? Intersection.INSIDE : Intersection.OUTSIDE; 114 } 115 116 @Override 117 public void visit(Way w) { 118 if (!w.isClosed() && ElemStyles.hasAreaElemStyle(w, false)) { 119 List<Node> nodes = w.getNodes(); 120 if (nodes.size()<1) return; // fix zero nodes bug 121 errors.add(new TestError(this, Severity.WARNING, tr("Area style way is not closed"), NOT_CLOSED, 122 Collections.singletonList(w), Arrays.asList(nodes.get(0), nodes.get(nodes.size() - 1)))); 123 } 124 } 125 126 @Override 127 public void visit(Relation r) { 128 nonClosedWays.clear(); 129 if (r.isMultipolygon()) { 130 checkMembersAndRoles(r); 131 132 Multipolygon polygon = MultipolygonCache.getInstance().get(Main.map.mapView, r); 133 134 boolean hasOuterWay = false; 135 for (RelationMember m : r.getMembers()) { 136 if ("outer".equals(m.getRole())) { 137 hasOuterWay = true; 138 break; 139 } 140 } 141 if (!hasOuterWay) { 142 errors.add(new TestError(this, Severity.WARNING, tr("No outer way for multipolygon"), MISSING_OUTER_WAY, r)); 143 } 144 145 for (RelationMember rm : r.getMembers()) { 146 if (!rm.getMember().isUsable()) 147 return; // Rest of checks is only for complete multipolygons 148 } 149 150 List<List<Node>> innerWays = joinWays(polygon.getInnerWays()); // Side effect - sets nonClosedWays 151 List<List<Node>> outerWays = joinWays(polygon.getOuterWays()); 152 if (styles != null) { 153 154 AreaElemStyle area = ElemStyles.getAreaElemStyle(r, false); 155 boolean areaStyle = area != null; 156 // If area style was not found for relation then use style of ways 157 if (area == null) { 158 for (Way w : polygon.getOuterWays()) { 159 area = ElemStyles.getAreaElemStyle(w, true); 160 if (area != null) { 161 break; 162 } 163 } 164 if(area == null) 165 errors.add(new TestError(this, Severity.OTHER, tr("No style for multipolygon"), NO_STYLE, r)); 166 else 167 errors.add( new TestError(this, Severity.OTHER, tr("No style in multipolygon relation"), 168 NO_STYLE_POLYGON, r)); 169 } 170 171 if (area != null) { 172 for (Way wInner : polygon.getInnerWays()) { 173 AreaElemStyle areaInner = ElemStyles.getAreaElemStyle(wInner, false); 174 175 if (areaInner != null && area.equals(areaInner)) { 176 List<OsmPrimitive> l = new ArrayList<OsmPrimitive>(); 177 l.add(r); 178 l.add(wInner); 179 errors.add( new TestError(this, Severity.WARNING, tr("Style for inner way equals multipolygon"), 180 INNER_STYLE_MISMATCH, l, Collections.singletonList(wInner))); 181 } 182 } 183 if(!areaStyle) { 184 for (Way wOuter : polygon.getOuterWays()) { 185 AreaElemStyle areaOuter = ElemStyles.getAreaElemStyle(wOuter, false); 186 if (areaOuter != null && !area.equals(areaOuter)) { 187 List<OsmPrimitive> l = new ArrayList<OsmPrimitive>(); 188 l.add(r); 189 l.add(wOuter); 190 errors.add(new TestError(this, Severity.WARNING, tr("Style for outer way mismatches"), 191 OUTER_STYLE_MISMATCH, l, Collections.singletonList(wOuter))); 192 } 193 } 194 } 195 } 196 } 197 198 List<Node> openNodes = new LinkedList<Node>(); 199 for (List<Node> w : nonClosedWays) { 200 if (w.size()<1) continue; 201 openNodes.add(w.get(0)); 202 openNodes.add(w.get(w.size() - 1)); 203 } 204 if (!openNodes.isEmpty()) { 205 List<OsmPrimitive> primitives = new LinkedList<OsmPrimitive>(); 206 primitives.add(r); 207 primitives.addAll(openNodes); 208 Arrays.asList(openNodes, r); 209 errors.add(new TestError(this, Severity.WARNING, tr("Multipolygon is not closed"), NON_CLOSED_WAY, 210 primitives, openNodes)); 211 } 212 213 // For painting is used Polygon class which works with ints only. For validation we need more precision 214 List<GeneralPath> outerPolygons = createPolygons(outerWays); 215 for (List<Node> pdInner : innerWays) { 216 boolean outside = true; 217 boolean crossing = false; 218 List<Node> outerWay = null; 219 for (int i=0; i<outerWays.size(); i++) { 220 GeneralPath outer = outerPolygons.get(i); 221 Intersection intersection = getPolygonIntersection(outer, pdInner); 222 outside = outside & intersection == Intersection.OUTSIDE; 223 if (intersection == Intersection.CROSSING) { 224 crossing = true; 225 outerWay = outerWays.get(i); 226 } 227 } 228 if (outside || crossing) { 229 List<List<Node>> highlights = new ArrayList<List<Node>>(); 230 highlights.add(pdInner); 231 if (outside) { 232 errors.add(new TestError(this, Severity.WARNING, tr("Multipolygon inner way is outside"), INNER_WAY_OUTSIDE, Collections.singletonList(r), highlights)); 233 } else if (crossing) { 234 highlights.add(outerWay); 235 errors.add(new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"), CROSSING_WAYS, Collections.singletonList(r), highlights)); 236 } 237 } 238 } 239 } 240 } 241 242 private void checkMembersAndRoles(Relation r) { 243 for (RelationMember rm : r.getMembers()) { 244 if (rm.isWay()) { 245 if (!("inner".equals(rm.getRole()) || "outer".equals(rm.getRole()) || !rm.hasRole())) { 246 errors.add(new TestError(this, Severity.WARNING, tr("No useful role for multipolygon member"), WRONG_MEMBER_ROLE, rm.getMember())); 247 } 248 } else { 249 if(!"admin_centre".equals(rm.getRole())) 250 errors.add(new TestError(this, Severity.WARNING, tr("Non-Way in multipolygon"), WRONG_MEMBER_TYPE, rm.getMember())); 251 } 252 } 253 } 254 }