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.isArea() && 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 }