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

Last change on this file since 11157 was 11157, checked in by bastiK, 8 years ago

see #13307 - validator/MultipolygonTest: add check for duplicate members (extracted from patch by Gerd Petermann, minor style and javadoc changes)

  • Property svn:eol-style set to native
File size: 23.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.validation.tests;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.geom.GeneralPath;
9import java.util.ArrayList;
10import java.util.Arrays;
11import java.util.Collection;
12import java.util.Collections;
13import java.util.HashMap;
14import java.util.HashSet;
15import java.util.List;
16import java.util.Map;
17import java.util.Map.Entry;
18import java.util.Set;
19
20import org.openstreetmap.josm.Main;
21import org.openstreetmap.josm.actions.CreateMultipolygonAction;
22import org.openstreetmap.josm.command.ChangeCommand;
23import org.openstreetmap.josm.command.Command;
24import org.openstreetmap.josm.data.osm.Node;
25import org.openstreetmap.josm.data.osm.OsmPrimitive;
26import org.openstreetmap.josm.data.osm.Relation;
27import org.openstreetmap.josm.data.osm.RelationMember;
28import org.openstreetmap.josm.data.osm.Way;
29import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
30import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
31import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;
32import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
33import org.openstreetmap.josm.data.validation.OsmValidator;
34import org.openstreetmap.josm.data.validation.Severity;
35import org.openstreetmap.josm.data.validation.Test;
36import org.openstreetmap.josm.data.validation.TestError;
37import org.openstreetmap.josm.gui.DefaultNameFormatter;
38import org.openstreetmap.josm.gui.mappaint.ElemStyles;
39import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
40import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement;
41import org.openstreetmap.josm.gui.progress.ProgressMonitor;
42import org.openstreetmap.josm.tools.Pair;
43
44/**
45 * Checks if multipolygons are valid
46 * @since 3669
47 */
48public class MultipolygonTest extends Test {
49
50 /** Non-Way in multipolygon */
51 public static final int WRONG_MEMBER_TYPE = 1601;
52 /** No useful role for multipolygon member */
53 public static final int WRONG_MEMBER_ROLE = 1602;
54 /** Multipolygon is not closed */
55 public static final int NON_CLOSED_WAY = 1603;
56 /** No outer way for multipolygon */
57 public static final int MISSING_OUTER_WAY = 1604;
58 /** Multipolygon inner way is outside */
59 public static final int INNER_WAY_OUTSIDE = 1605;
60 /** Intersection between multipolygon ways */
61 public static final int CROSSING_WAYS = 1606;
62 /** Style for outer way mismatches / With the currently used mappaint style(s) the style for outer way mismatches the area style */
63 public static final int OUTER_STYLE_MISMATCH = 1607;
64 /** With the currently used mappaint style the style for inner way equals the multipolygon style */
65 public static final int INNER_STYLE_MISMATCH = 1608;
66 /** Area style way is not closed */
67 public static final int NOT_CLOSED = 1609;
68 /** No area style for multipolygon */
69 public static final int NO_STYLE = 1610;
70 /** Multipolygon relation should be tagged with area tags and not the outer way(s) */
71 public static final int NO_STYLE_POLYGON = 1611;
72 /** Area style on outer way */
73 public static final int OUTER_STYLE = 1613;
74 /** Multipolygon member repeated (same primitive, same role */
75 public static final int REPEATED_MEMBER_SAME_ROLE = 1614;
76 /** Multipolygon member repeated (same primitive, different role) */
77 public static final int REPEATED_MEMBER_DIFF_ROLE = 1615;
78
79 private static volatile ElemStyles styles;
80
81 private final Set<String> keysCheckedByAnotherTest = new HashSet<>();
82
83 /**
84 * Constructs a new {@code MultipolygonTest}.
85 */
86 public MultipolygonTest() {
87 super(tr("Multipolygon"),
88 tr("This test checks if multipolygons are valid."));
89 }
90
91 @Override
92 public void initialize() {
93 styles = MapPaintStyles.getStyles();
94 }
95
96 @Override
97 public void startTest(ProgressMonitor progressMonitor) {
98 super.startTest(progressMonitor);
99 keysCheckedByAnotherTest.clear();
100 for (Test t : OsmValidator.getEnabledTests(false)) {
101 if (t instanceof UnclosedWays) {
102 keysCheckedByAnotherTest.addAll(((UnclosedWays) t).getCheckedKeys());
103 break;
104 }
105 }
106 }
107
108 @Override
109 public void endTest() {
110 keysCheckedByAnotherTest.clear();
111 super.endTest();
112 }
113
114 private static 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 static List<GeneralPath> createPolygons(List<Multipolygon.PolyData> joinedWays) {
125 List<GeneralPath> result = new ArrayList<>();
126 for (Multipolygon.PolyData way : joinedWays) {
127 result.add(createPath(way.getNodes()));
128 }
129 return result;
130 }
131
132 private static 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.hasOnlyAreaElemStyle(w)) {
151 List<Node> nodes = w.getNodes();
152 if (nodes.isEmpty()) return; // fix zero nodes bug
153 for (String key : keysCheckedByAnotherTest) {
154 if (w.hasKey(key)) {
155 return;
156 }
157 }
158 errors.add(TestError.builder(this, Severity.WARNING, NOT_CLOSED)
159 .message(tr("Area style way is not closed"))
160 .primitives(w)
161 .highlight(Arrays.asList(nodes.get(0), nodes.get(nodes.size() - 1)))
162 .build());
163 }
164 }
165
166 @Override
167 public void visit(Relation r) {
168 if (r.isMultipolygon()) {
169 checkMembersAndRoles(r);
170 checkOuterWay(r);
171 checkRepeatedWayMembers(r);
172
173 // Rest of checks is only for complete multipolygons
174 if (!r.hasIncompleteMembers()) {
175 Multipolygon polygon = MultipolygonCache.getInstance().get(Main.map.mapView, r);
176
177 // Create new multipolygon using the logics from CreateMultipolygonAction and see if roles match.
178 checkMemberRoleCorrectness(r);
179 checkStyleConsistency(r, polygon);
180 checkGeometry(r, polygon);
181 }
182 }
183 }
184
185 /**
186 * Checks that multipolygon has at least an outer way:<ul>
187 * <li>{@link #MISSING_OUTER_WAY}: No outer way for multipolygon</li>
188 * </ul>
189 * @param r relation
190 */
191 private void checkOuterWay(Relation r) {
192 boolean hasOuterWay = false;
193 for (RelationMember m : r.getMembers()) {
194 if ("outer".equals(m.getRole())) {
195 hasOuterWay = true;
196 break;
197 }
198 }
199 if (!hasOuterWay) {
200 errors.add(TestError.builder(this, Severity.WARNING, MISSING_OUTER_WAY)
201 .message(tr("No outer way for multipolygon"))
202 .primitives(r)
203 .build());
204 }
205 }
206
207 /**
208 * Create new multipolygon using the logics from CreateMultipolygonAction and see if roles match:<ul>
209 * <li>{@link #WRONG_MEMBER_ROLE}: Role for ''{0}'' should be ''{1}''</li>
210 * </ul>
211 * @param r relation
212 */
213 private void checkMemberRoleCorrectness(Relation r) {
214 final Pair<Relation, Relation> newMP = CreateMultipolygonAction.createMultipolygonRelation(r.getMemberPrimitives(Way.class), false);
215 if (newMP != null) {
216 for (RelationMember member : r.getMembers()) {
217 final Collection<RelationMember> memberInNewMP = newMP.b.getMembersFor(Collections.singleton(member.getMember()));
218 if (memberInNewMP != null && !memberInNewMP.isEmpty()) {
219 final String roleInNewMP = memberInNewMP.iterator().next().getRole();
220 if (!member.getRole().equals(roleInNewMP)) {
221 errors.add(TestError.builder(this, Severity.WARNING, WRONG_MEMBER_ROLE)
222 .message(RelationChecker.ROLE_VERIF_PROBLEM_MSG,
223 marktr("Role for ''{0}'' should be ''{1}''"),
224 member.getMember().getDisplayName(DefaultNameFormatter.getInstance()), roleInNewMP)
225 .primitives(addRelationIfNeeded(r, member.getMember()))
226 .highlight(member.getMember())
227 .build());
228 }
229 }
230 }
231 }
232 }
233
234 /**
235 * Various style-related checks:<ul>
236 * <li>{@link #NO_STYLE_POLYGON}: Multipolygon relation should be tagged with area tags and not the outer way</li>
237 * <li>{@link #INNER_STYLE_MISMATCH}: With the currently used mappaint style the style for inner way equals the multipolygon style</li>
238 * <li>{@link #OUTER_STYLE_MISMATCH}: Style for outer way mismatches</li>
239 * <li>{@link #OUTER_STYLE}: Area style on outer way</li>
240 * </ul>
241 * @param r relation
242 * @param polygon multipolygon
243 */
244 private void checkStyleConsistency(Relation r, Multipolygon polygon) {
245 if (styles != null && !"boundary".equals(r.get("type"))) {
246 AreaElement area = ElemStyles.getAreaElemStyle(r, false);
247 boolean areaStyle = area != null;
248 // If area style was not found for relation then use style of ways
249 if (area == null) {
250 for (Way w : polygon.getOuterWays()) {
251 area = ElemStyles.getAreaElemStyle(w, true);
252 if (area != null) {
253 break;
254 }
255 }
256 if (area == null) {
257 errors.add(TestError.builder(this, Severity.OTHER, NO_STYLE)
258 .message(tr("No area style for multipolygon"))
259 .primitives(r)
260 .build());
261 } else {
262 /* old style multipolygon - solve: copy tags from outer way to multipolygon */
263 errors.add(TestError.builder(this, Severity.WARNING, NO_STYLE_POLYGON)
264 .message(trn("Multipolygon relation should be tagged with area tags and not the outer way",
265 "Multipolygon relation should be tagged with area tags and not the outer ways",
266 polygon.getOuterWays().size()))
267 .primitives(r)
268 .build());
269 }
270 }
271
272 if (area != null) {
273 for (Way wInner : polygon.getInnerWays()) {
274 AreaElement areaInner = ElemStyles.getAreaElemStyle(wInner, false);
275
276 if (areaInner != null && area.equals(areaInner)) {
277 errors.add(TestError.builder(this, Severity.OTHER, INNER_STYLE_MISMATCH)
278 .message(tr("With the currently used mappaint style the style for inner way equals the multipolygon style"))
279 .primitives(addRelationIfNeeded(r, wInner))
280 .highlight(wInner)
281 .build());
282 }
283 }
284 for (Way wOuter : polygon.getOuterWays()) {
285 AreaElement areaOuter = ElemStyles.getAreaElemStyle(wOuter, false);
286 if (areaOuter != null) {
287 if (!area.equals(areaOuter)) {
288 String message = !areaStyle ? tr("Style for outer way mismatches")
289 : tr("With the currently used mappaint style(s) the style for outer way mismatches the area style");
290 errors.add(TestError.builder(this, Severity.OTHER, OUTER_STYLE_MISMATCH)
291 .message(message)
292 .primitives(addRelationIfNeeded(r, wOuter))
293 .highlight(wOuter)
294 .build());
295 } else if (areaStyle) { /* style on outer way of multipolygon, but equal to polygon */
296 errors.add(TestError.builder(this, Severity.WARNING, OUTER_STYLE)
297 .message(tr("Area style on outer way"))
298 .primitives(addRelationIfNeeded(r, wOuter))
299 .highlight(wOuter)
300 .build());
301 }
302 }
303 }
304 }
305 }
306 }
307
308 /**
309 * Various geometry-related checks:<ul>
310 * <li>{@link #NON_CLOSED_WAY}: Multipolygon is not closed</li>
311 * <li>{@link #INNER_WAY_OUTSIDE}: Multipolygon inner way is outside</li>
312 * <li>{@link #CROSSING_WAYS}: Intersection between multipolygon ways</li>
313 * </ul>
314 * @param r relation
315 * @param polygon multipolygon
316 */
317 private void checkGeometry(Relation r, Multipolygon polygon) {
318 List<Node> openNodes = polygon.getOpenEnds();
319 if (!openNodes.isEmpty()) {
320 errors.add(TestError.builder(this, Severity.WARNING, NON_CLOSED_WAY)
321 .message(tr("Multipolygon is not closed"))
322 .primitives(addRelationIfNeeded(r, openNodes))
323 .highlight(openNodes)
324 .build());
325 }
326
327 // For painting is used Polygon class which works with ints only. For validation we need more precision
328 List<PolyData> innerPolygons = polygon.getInnerPolygons();
329 List<PolyData> outerPolygons = polygon.getOuterPolygons();
330 List<GeneralPath> innerPolygonsPaths = innerPolygons.isEmpty() ? Collections.<GeneralPath>emptyList() : createPolygons(innerPolygons);
331 List<GeneralPath> outerPolygonsPaths = createPolygons(outerPolygons);
332 for (int i = 0; i < outerPolygons.size(); i++) {
333 PolyData pdOuter = outerPolygons.get(i);
334 // Check for intersection between outer members
335 for (int j = i+1; j < outerPolygons.size(); j++) {
336 checkCrossingWays(r, outerPolygons, outerPolygonsPaths, pdOuter, j);
337 }
338 }
339 for (int i = 0; i < innerPolygons.size(); i++) {
340 PolyData pdInner = innerPolygons.get(i);
341 // Check for intersection between inner members
342 for (int j = i+1; j < innerPolygons.size(); j++) {
343 checkCrossingWays(r, innerPolygons, innerPolygonsPaths, pdInner, j);
344 }
345 // Check for intersection between inner and outer members
346 boolean outside = true;
347 for (int o = 0; o < outerPolygons.size(); o++) {
348 outside &= checkCrossingWays(r, outerPolygons, outerPolygonsPaths, pdInner, o) == Intersection.OUTSIDE;
349 }
350 if (outside) {
351 errors.add(TestError.builder(this, Severity.WARNING, INNER_WAY_OUTSIDE)
352 .message(tr("Multipolygon inner way is outside"))
353 .primitives(r)
354 .highlightNodePairs(Collections.singletonList(pdInner.getNodes()))
355 .build());
356 }
357 }
358 }
359
360 private Intersection checkCrossingWays(Relation r, List<PolyData> polygons, List<GeneralPath> polygonsPaths, PolyData pd, int idx) {
361 Intersection intersection = getPolygonIntersection(polygonsPaths.get(idx), pd.getNodes());
362 if (intersection == Intersection.CROSSING) {
363 PolyData pdOther = polygons.get(idx);
364 if (pdOther != null) {
365 errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS)
366 .message(tr("Intersection between multipolygon ways"))
367 .primitives(r)
368 .highlightNodePairs(Arrays.asList(pd.getNodes(), pdOther.getNodes()))
369 .build());
370 }
371 }
372 return intersection;
373 }
374
375 /**
376 * Check for:<ul>
377 * <li>{@link #WRONG_MEMBER_ROLE}: No useful role for multipolygon member</li>
378 * <li>{@link #WRONG_MEMBER_TYPE}: Non-Way in multipolygon</li>
379 * </ul>
380 * @param r relation
381 */
382 private void checkMembersAndRoles(Relation r) {
383 for (RelationMember rm : r.getMembers()) {
384 if (rm.isWay()) {
385 if (!(rm.hasRole("inner", "outer") || !rm.hasRole())) {
386 errors.add(TestError.builder(this, Severity.WARNING, WRONG_MEMBER_ROLE)
387 .message(tr("No useful role for multipolygon member"))
388 .primitives(addRelationIfNeeded(r, rm.getMember()))
389 .build());
390 }
391 } else {
392 if (!rm.hasRole("admin_centre", "label", "subarea", "land_area")) {
393 errors.add(TestError.builder(this, Severity.WARNING, WRONG_MEMBER_TYPE)
394 .message(tr("Non-Way in multipolygon"))
395 .primitives(addRelationIfNeeded(r, rm.getMember()))
396 .build());
397 }
398 }
399 }
400 }
401
402 private static Collection<? extends OsmPrimitive> addRelationIfNeeded(Relation r, OsmPrimitive primitive) {
403 return addRelationIfNeeded(r, Collections.singleton(primitive));
404 }
405
406 private static Collection<? extends OsmPrimitive> addRelationIfNeeded(Relation r, Collection<? extends OsmPrimitive> primitives) {
407 // Fix #8212 : if the error references only incomplete primitives,
408 // add multipolygon in order to let user select something and fix the error
409 if (!primitives.contains(r)) {
410 for (OsmPrimitive p : primitives) {
411 if (!p.isIncomplete()) {
412 return primitives;
413 }
414 }
415 // Diamond operator does not work with Java 9 here
416 @SuppressWarnings("unused")
417 List<OsmPrimitive> newPrimitives = new ArrayList<OsmPrimitive>(primitives);
418 newPrimitives.add(0, r);
419 return newPrimitives;
420 } else {
421 return primitives;
422 }
423 }
424
425 /**
426 * Check for:<ul>
427 * <li>{@link #REPEATED_MEMBER_DIFF_ROLE}: Multipolygon member(s) repeated with different role</li>
428 * <li>{@link #REPEATED_MEMBER_SAME_ROLE}: Multipolygon member(s) repeated with same role</li>
429 * </ul>
430 * @param r relation
431 * @return true if repeated members have been detected, false otherwise
432 */
433 private boolean checkRepeatedWayMembers(Relation r) {
434 boolean hasDups = false;
435 Map<OsmPrimitive, List<RelationMember>> seenMemberPrimitives = new HashMap<>();
436 for (RelationMember rm : r.getMembers()) {
437 List<RelationMember> list = seenMemberPrimitives.get(rm.getMember());
438 if (list == null) {
439 list = new ArrayList<>(2);
440 seenMemberPrimitives.put(rm.getMember(), list);
441 } else {
442 hasDups = true;
443 }
444 list.add(rm);
445 }
446 if (hasDups) {
447 List<OsmPrimitive> repeatedSameRole = new ArrayList<>();
448 List<OsmPrimitive> repeatedDiffRole = new ArrayList<>();
449 for (Entry<OsmPrimitive, List<RelationMember>> e : seenMemberPrimitives.entrySet()) {
450 List<RelationMember> visited = e.getValue();
451 if (e.getValue().size() == 1)
452 continue;
453 // we found a duplicate member, check if the roles differ
454 boolean rolesDiffer = false;
455 RelationMember rm = visited.get(0);
456 List<OsmPrimitive> primitives = new ArrayList<>();
457 for (int i = 1; i < visited.size(); i++) {
458 RelationMember v = visited.get(i);
459 primitives.add(rm.getMember());
460 if (!v.getRole().equals(rm.getRole())) {
461 rolesDiffer = true;
462 }
463 }
464 if (rolesDiffer) {
465 repeatedDiffRole.addAll(primitives);
466 } else {
467 repeatedSameRole.addAll(primitives);
468 }
469 }
470 addRepeatedMemberError(r, repeatedDiffRole, REPEATED_MEMBER_DIFF_ROLE, tr("Multipolygon member(s) repeated with different role"));
471 addRepeatedMemberError(r, repeatedSameRole, REPEATED_MEMBER_SAME_ROLE, tr("Multipolygon member(s) repeated with same role"));
472 }
473 return hasDups;
474 }
475
476 private void addRepeatedMemberError(Relation r, List<OsmPrimitive> repeatedMembers, int errorCode, String msg) {
477 if (!repeatedMembers.isEmpty()) {
478 List<OsmPrimitive> prims = new ArrayList<>(1 + repeatedMembers.size());
479 prims.add(r);
480 prims.addAll(repeatedMembers);
481 errors.add(TestError.builder(this, Severity.WARNING, errorCode)
482 .message(msg)
483 .primitives(prims)
484 .highlight(repeatedMembers)
485 .build());
486 }
487 }
488
489 @Override
490 public Command fixError(TestError testError) {
491 if (testError.getCode() == REPEATED_MEMBER_SAME_ROLE) {
492 ArrayList<OsmPrimitive> primitives = new ArrayList<>(testError.getPrimitives());
493 if (primitives.size() >= 2) {
494 if (primitives.get(0) instanceof Relation) {
495 Relation oldRel = (Relation) primitives.get(0);
496 Relation newRel = new Relation(oldRel);
497 List<OsmPrimitive> repeatedPrims = primitives.subList(1, primitives.size());
498 List<RelationMember> oldMembers = oldRel.getMembers();
499
500 List<RelationMember> newMembers = new ArrayList<>();
501 HashSet<OsmPrimitive> toRemove = new HashSet<>(repeatedPrims);
502 HashSet<OsmPrimitive> found = new HashSet<>(repeatedPrims.size());
503 for (RelationMember rm : oldMembers) {
504 if (toRemove.contains(rm.getMember())) {
505 if (found.contains(rm.getMember()) == false) {
506 found.add(rm.getMember());
507 newMembers.add(rm);
508 }
509 } else {
510 newMembers.add(rm);
511 }
512 }
513 newRel.setMembers(newMembers);
514 return new ChangeCommand (oldRel, newRel);
515 }
516 }
517 }
518 return null;
519 }
520
521 @Override
522 public boolean isFixable(TestError testError) {
523 if (testError.getCode() == REPEATED_MEMBER_SAME_ROLE)
524 return true;
525 return false;
526 }
527}
Note: See TracBrowser for help on using the repository browser.