source: josm/trunk/src/org/openstreetmap/josm/data/validation/tests/MultipolygonTest.java@ 8045

Last change on this file since 8045 was 8045, checked in by Klumbumbus, 9 years ago

fix #6363 - downgrade "Style for inner way equals multipolygon" validator warning to info level as this is no data error. Make the message more clear. Use own fill color for some landuses.

  • Property svn:eol-style set to native
File size: 14.9 KB
Line 
1// License: GPL. See LICENSE file for details.
2package org.openstreetmap.josm.data.validation.tests;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.geom.GeneralPath;
8import java.text.MessageFormat;
9import java.util.ArrayList;
10import java.util.Arrays;
11import java.util.Collection;
12import java.util.Collections;
13import java.util.HashSet;
14import java.util.LinkedList;
15import java.util.List;
16import java.util.Set;
17
18import org.openstreetmap.josm.Main;
19import org.openstreetmap.josm.actions.CreateMultipolygonAction;
20import org.openstreetmap.josm.data.osm.Node;
21import org.openstreetmap.josm.data.osm.OsmPrimitive;
22import org.openstreetmap.josm.data.osm.Relation;
23import org.openstreetmap.josm.data.osm.RelationMember;
24import org.openstreetmap.josm.data.osm.Way;
25import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
26import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.JoinedWay;
27import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;
28import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
29import org.openstreetmap.josm.data.validation.OsmValidator;
30import org.openstreetmap.josm.data.validation.Severity;
31import org.openstreetmap.josm.data.validation.Test;
32import org.openstreetmap.josm.data.validation.TestError;
33import org.openstreetmap.josm.gui.DefaultNameFormatter;
34import org.openstreetmap.josm.gui.mappaint.AreaElemStyle;
35import org.openstreetmap.josm.gui.mappaint.ElemStyles;
36import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
37import org.openstreetmap.josm.gui.progress.ProgressMonitor;
38import org.openstreetmap.josm.tools.Pair;
39
40/**
41 * Checks if multipolygons are valid
42 * @since 3669
43 */
44public class MultipolygonTest extends Test {
45
46 protected static final int WRONG_MEMBER_TYPE = 1601;
47 protected static final int WRONG_MEMBER_ROLE = 1602;
48 protected static final int NON_CLOSED_WAY = 1603;
49 protected static final int MISSING_OUTER_WAY = 1604;
50 protected static final int INNER_WAY_OUTSIDE = 1605;
51 protected static final int CROSSING_WAYS = 1606;
52 protected static final int OUTER_STYLE_MISMATCH = 1607;
53 protected static final int INNER_STYLE_MISMATCH = 1608;
54 protected static final int NOT_CLOSED = 1609;
55 protected static final int NO_STYLE = 1610;
56 protected static final int NO_STYLE_POLYGON = 1611;
57 protected static final int OUTER_STYLE = 1613;
58
59 private static ElemStyles styles;
60
61 private final List<List<Node>> nonClosedWays = new ArrayList<>();
62 private final Set<String> keysCheckedByAnotherTest = new HashSet<>();
63
64 /**
65 * Constructs a new {@code MultipolygonTest}.
66 */
67 public MultipolygonTest() {
68 super(tr("Multipolygon"),
69 tr("This test checks if multipolygons are valid."));
70 }
71
72 @Override
73 public void initialize() {
74 styles = MapPaintStyles.getStyles();
75 }
76
77 @Override
78 public void startTest(ProgressMonitor progressMonitor) {
79 super.startTest(progressMonitor);
80 keysCheckedByAnotherTest.clear();
81 for (Test t : OsmValidator.getEnabledTests(false)) {
82 if (t instanceof UnclosedWays) {
83 keysCheckedByAnotherTest.addAll(((UnclosedWays)t).getCheckedKeys());
84 break;
85 }
86 }
87 }
88
89 @Override
90 public void endTest() {
91 keysCheckedByAnotherTest.clear();
92 super.endTest();
93 }
94
95 private List<List<Node>> joinWays(Collection<Way> ways) {
96 List<List<Node>> result = new ArrayList<>();
97 List<Way> waysToJoin = new ArrayList<>();
98 for (Way way : ways) {
99 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("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}
Note: See TracBrowser for help on using the repository browser.