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

Last change on this file since 11129 was 11129, checked in by simon04, 8 years ago

fix #13799 - Use builder pattern for TestError

  • Property svn:eol-style set to native
File size: 18.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.HashSet;
14import java.util.List;
15import java.util.Set;
16
17import org.openstreetmap.josm.Main;
18import org.openstreetmap.josm.actions.CreateMultipolygonAction;
19import org.openstreetmap.josm.data.osm.Node;
20import org.openstreetmap.josm.data.osm.OsmPrimitive;
21import org.openstreetmap.josm.data.osm.Relation;
22import org.openstreetmap.josm.data.osm.RelationMember;
23import org.openstreetmap.josm.data.osm.Way;
24import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
25import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
26import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;
27import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
28import org.openstreetmap.josm.data.validation.OsmValidator;
29import org.openstreetmap.josm.data.validation.Severity;
30import org.openstreetmap.josm.data.validation.Test;
31import org.openstreetmap.josm.data.validation.TestError;
32import org.openstreetmap.josm.gui.DefaultNameFormatter;
33import org.openstreetmap.josm.gui.mappaint.ElemStyles;
34import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
35import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement;
36import org.openstreetmap.josm.gui.progress.ProgressMonitor;
37import org.openstreetmap.josm.tools.Pair;
38
39/**
40 * Checks if multipolygons are valid
41 * @since 3669
42 */
43public class MultipolygonTest extends Test {
44
45 /** Non-Way in multipolygon */
46 public static final int WRONG_MEMBER_TYPE = 1601;
47 /** No useful role for multipolygon member */
48 public static final int WRONG_MEMBER_ROLE = 1602;
49 /** Multipolygon is not closed */
50 public static final int NON_CLOSED_WAY = 1603;
51 /** No outer way for multipolygon */
52 public static final int MISSING_OUTER_WAY = 1604;
53 /** Multipolygon inner way is outside */
54 public static final int INNER_WAY_OUTSIDE = 1605;
55 /** Intersection between multipolygon ways */
56 public static final int CROSSING_WAYS = 1606;
57 /** Style for outer way mismatches / With the currently used mappaint style(s) the style for outer way mismatches the area style */
58 public static final int OUTER_STYLE_MISMATCH = 1607;
59 /** With the currently used mappaint style the style for inner way equals the multipolygon style */
60 public static final int INNER_STYLE_MISMATCH = 1608;
61 /** Area style way is not closed */
62 public static final int NOT_CLOSED = 1609;
63 /** No area style for multipolygon */
64 public static final int NO_STYLE = 1610;
65 /** Multipolygon relation should be tagged with area tags and not the outer way(s) */
66 public static final int NO_STYLE_POLYGON = 1611;
67 /** Area style on outer way */
68 public static final int OUTER_STYLE = 1613;
69
70 private static volatile ElemStyles styles;
71
72 private final Set<String> keysCheckedByAnotherTest = new HashSet<>();
73
74 /**
75 * Constructs a new {@code MultipolygonTest}.
76 */
77 public MultipolygonTest() {
78 super(tr("Multipolygon"),
79 tr("This test checks if multipolygons are valid."));
80 }
81
82 @Override
83 public void initialize() {
84 styles = MapPaintStyles.getStyles();
85 }
86
87 @Override
88 public void startTest(ProgressMonitor progressMonitor) {
89 super.startTest(progressMonitor);
90 keysCheckedByAnotherTest.clear();
91 for (Test t : OsmValidator.getEnabledTests(false)) {
92 if (t instanceof UnclosedWays) {
93 keysCheckedByAnotherTest.addAll(((UnclosedWays) t).getCheckedKeys());
94 break;
95 }
96 }
97 }
98
99 @Override
100 public void endTest() {
101 keysCheckedByAnotherTest.clear();
102 super.endTest();
103 }
104
105 private static GeneralPath createPath(List<Node> nodes) {
106 GeneralPath result = new GeneralPath();
107 result.moveTo((float) nodes.get(0).getCoor().lat(), (float) nodes.get(0).getCoor().lon());
108 for (int i = 1; i < nodes.size(); i++) {
109 Node n = nodes.get(i);
110 result.lineTo((float) n.getCoor().lat(), (float) n.getCoor().lon());
111 }
112 return result;
113 }
114
115 private static List<GeneralPath> createPolygons(List<Multipolygon.PolyData> joinedWays) {
116 List<GeneralPath> result = new ArrayList<>();
117 for (Multipolygon.PolyData way : joinedWays) {
118 result.add(createPath(way.getNodes()));
119 }
120 return result;
121 }
122
123 private static Intersection getPolygonIntersection(GeneralPath outer, List<Node> inner) {
124 boolean inside = false;
125 boolean outside = false;
126
127 for (Node n : inner) {
128 boolean contains = outer.contains(n.getCoor().lat(), n.getCoor().lon());
129 inside = inside | contains;
130 outside = outside | !contains;
131 if (inside & outside) {
132 return Intersection.CROSSING;
133 }
134 }
135
136 return inside ? Intersection.INSIDE : Intersection.OUTSIDE;
137 }
138
139 @Override
140 public void visit(Way w) {
141 if (!w.isArea() && ElemStyles.hasOnlyAreaElemStyle(w)) {
142 List<Node> nodes = w.getNodes();
143 if (nodes.isEmpty()) return; // fix zero nodes bug
144 for (String key : keysCheckedByAnotherTest) {
145 if (w.hasKey(key)) {
146 return;
147 }
148 }
149 errors.add(TestError.builder(this, Severity.WARNING, NOT_CLOSED)
150 .message(tr("Area style way is not closed"))
151 .primitives(w)
152 .highlight(Arrays.asList(nodes.get(0), nodes.get(nodes.size() - 1)))
153 .build());
154 }
155 }
156
157 @Override
158 public void visit(Relation r) {
159 if (r.isMultipolygon()) {
160 checkMembersAndRoles(r);
161 checkOuterWay(r);
162
163 // Rest of checks is only for complete multipolygons
164 if (!r.hasIncompleteMembers()) {
165 Multipolygon polygon = MultipolygonCache.getInstance().get(Main.map.mapView, r);
166
167 // Create new multipolygon using the logics from CreateMultipolygonAction and see if roles match.
168 checkMemberRoleCorrectness(r);
169 checkStyleConsistency(r, polygon);
170 checkGeometry(r, polygon);
171 }
172 }
173 }
174
175 /**
176 * Checks that multipolygon has at least an outer way:<ul>
177 * <li>{@link #MISSING_OUTER_WAY}: No outer way for multipolygon</li>
178 * </ul>
179 * @param r relation
180 */
181 private void checkOuterWay(Relation r) {
182 boolean hasOuterWay = false;
183 for (RelationMember m : r.getMembers()) {
184 if ("outer".equals(m.getRole())) {
185 hasOuterWay = true;
186 break;
187 }
188 }
189 if (!hasOuterWay) {
190 errors.add(TestError.builder(this, Severity.WARNING, MISSING_OUTER_WAY)
191 .message(tr("No outer way for multipolygon"))
192 .primitives(r)
193 .build());
194 }
195 }
196
197 /**
198 * Create new multipolygon using the logics from CreateMultipolygonAction and see if roles match:<ul>
199 * <li>{@link #WRONG_MEMBER_ROLE}: Role for ''{0}'' should be ''{1}''</li>
200 * </ul>
201 * @param r relation
202 */
203 private void checkMemberRoleCorrectness(Relation r) {
204 final Pair<Relation, Relation> newMP = CreateMultipolygonAction.createMultipolygonRelation(r.getMemberPrimitives(Way.class), false);
205 if (newMP != null) {
206 for (RelationMember member : r.getMembers()) {
207 final Collection<RelationMember> memberInNewMP = newMP.b.getMembersFor(Collections.singleton(member.getMember()));
208 if (memberInNewMP != null && !memberInNewMP.isEmpty()) {
209 final String roleInNewMP = memberInNewMP.iterator().next().getRole();
210 if (!member.getRole().equals(roleInNewMP)) {
211 errors.add(TestError.builder(this, Severity.WARNING, WRONG_MEMBER_ROLE)
212 .message(RelationChecker.ROLE_VERIF_PROBLEM_MSG,
213 marktr("Role for ''{0}'' should be ''{1}''"),
214 member.getMember().getDisplayName(DefaultNameFormatter.getInstance()), roleInNewMP)
215 .primitives(addRelationIfNeeded(r, member.getMember()))
216 .highlight(member.getMember())
217 .build());
218 }
219 }
220 }
221 }
222 }
223
224 /**
225 * Various style-related checks:<ul>
226 * <li>{@link #NO_STYLE_POLYGON}: Multipolygon relation should be tagged with area tags and not the outer way</li>
227 * <li>{@link #INNER_STYLE_MISMATCH}: With the currently used mappaint style the style for inner way equals the multipolygon style</li>
228 * <li>{@link #OUTER_STYLE_MISMATCH}: Style for outer way mismatches</li>
229 * <li>{@link #OUTER_STYLE}: Area style on outer way</li>
230 * </ul>
231 * @param r relation
232 * @param polygon multipolygon
233 */
234 private void checkStyleConsistency(Relation r, Multipolygon polygon) {
235 if (styles != null && !"boundary".equals(r.get("type"))) {
236 AreaElement area = ElemStyles.getAreaElemStyle(r, false);
237 boolean areaStyle = area != null;
238 // If area style was not found for relation then use style of ways
239 if (area == null) {
240 for (Way w : polygon.getOuterWays()) {
241 area = ElemStyles.getAreaElemStyle(w, true);
242 if (area != null) {
243 break;
244 }
245 }
246 if (area == null) {
247 errors.add(TestError.builder(this, Severity.OTHER, NO_STYLE)
248 .message(tr("No area style for multipolygon"))
249 .primitives(r)
250 .build());
251 } else {
252 /* old style multipolygon - solve: copy tags from outer way to multipolygon */
253 errors.add(TestError.builder(this, Severity.WARNING, NO_STYLE_POLYGON)
254 .message(trn("Multipolygon relation should be tagged with area tags and not the outer way",
255 "Multipolygon relation should be tagged with area tags and not the outer ways",
256 polygon.getOuterWays().size()))
257 .primitives(r)
258 .build());
259 }
260 }
261
262 if (area != null) {
263 for (Way wInner : polygon.getInnerWays()) {
264 AreaElement areaInner = ElemStyles.getAreaElemStyle(wInner, false);
265
266 if (areaInner != null && area.equals(areaInner)) {
267 errors.add(TestError.builder(this, Severity.OTHER, INNER_STYLE_MISMATCH)
268 .message(tr("With the currently used mappaint style the style for inner way equals the multipolygon style"))
269 .primitives(addRelationIfNeeded(r, wInner))
270 .highlight(wInner)
271 .build());
272 }
273 }
274 for (Way wOuter : polygon.getOuterWays()) {
275 AreaElement areaOuter = ElemStyles.getAreaElemStyle(wOuter, false);
276 if (areaOuter != null) {
277 if (!area.equals(areaOuter)) {
278 String message = !areaStyle ? tr("Style for outer way mismatches")
279 : tr("With the currently used mappaint style(s) the style for outer way mismatches the area style");
280 errors.add(TestError.builder(this, Severity.OTHER, OUTER_STYLE_MISMATCH)
281 .message(message)
282 .primitives(addRelationIfNeeded(r, wOuter))
283 .highlight(wOuter)
284 .build());
285 } else if (areaStyle) { /* style on outer way of multipolygon, but equal to polygon */
286 errors.add(TestError.builder(this, Severity.WARNING, OUTER_STYLE)
287 .message(tr("Area style on outer way"))
288 .primitives(addRelationIfNeeded(r, wOuter))
289 .highlight(wOuter)
290 .build());
291 }
292 }
293 }
294 }
295 }
296 }
297
298 /**
299 * Various geometry-related checks:<ul>
300 * <li>{@link #NON_CLOSED_WAY}: Multipolygon is not closed</li>
301 * <li>{@link #INNER_WAY_OUTSIDE}: Multipolygon inner way is outside</li>
302 * <li>{@link #CROSSING_WAYS}: Intersection between multipolygon ways</li>
303 * </ul>
304 * @param r relation
305 * @param polygon multipolygon
306 */
307 private void checkGeometry(Relation r, Multipolygon polygon) {
308 List<Node> openNodes = polygon.getOpenEnds();
309 if (!openNodes.isEmpty()) {
310 errors.add(TestError.builder(this, Severity.WARNING, NON_CLOSED_WAY)
311 .message(tr("Multipolygon is not closed"))
312 .primitives(addRelationIfNeeded(r, openNodes))
313 .highlight(openNodes)
314 .build());
315 }
316
317 // For painting is used Polygon class which works with ints only. For validation we need more precision
318 List<PolyData> innerPolygons = polygon.getInnerPolygons();
319 List<PolyData> outerPolygons = polygon.getOuterPolygons();
320 List<GeneralPath> innerPolygonsPaths = innerPolygons.isEmpty() ? Collections.<GeneralPath>emptyList() : createPolygons(innerPolygons);
321 List<GeneralPath> outerPolygonsPaths = createPolygons(outerPolygons);
322 for (int i = 0; i < outerPolygons.size(); i++) {
323 PolyData pdOuter = outerPolygons.get(i);
324 // Check for intersection between outer members
325 for (int j = i+1; j < outerPolygons.size(); j++) {
326 checkCrossingWays(r, outerPolygons, outerPolygonsPaths, pdOuter, j);
327 }
328 }
329 for (int i = 0; i < innerPolygons.size(); i++) {
330 PolyData pdInner = innerPolygons.get(i);
331 // Check for intersection between inner members
332 for (int j = i+1; j < innerPolygons.size(); j++) {
333 checkCrossingWays(r, innerPolygons, innerPolygonsPaths, pdInner, j);
334 }
335 // Check for intersection between inner and outer members
336 boolean outside = true;
337 for (int o = 0; o < outerPolygons.size(); o++) {
338 outside &= checkCrossingWays(r, outerPolygons, outerPolygonsPaths, pdInner, o) == Intersection.OUTSIDE;
339 }
340 if (outside) {
341 errors.add(TestError.builder(this, Severity.WARNING, INNER_WAY_OUTSIDE)
342 .message(tr("Multipolygon inner way is outside"))
343 .primitives(r)
344 .highlightNodePairs(Collections.singletonList(pdInner.getNodes()))
345 .build());
346 }
347 }
348 }
349
350 private Intersection checkCrossingWays(Relation r, List<PolyData> polygons, List<GeneralPath> polygonsPaths, PolyData pd, int idx) {
351 Intersection intersection = getPolygonIntersection(polygonsPaths.get(idx), pd.getNodes());
352 if (intersection == Intersection.CROSSING) {
353 PolyData pdOther = polygons.get(idx);
354 if (pdOther != null) {
355 errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS)
356 .message(tr("Intersection between multipolygon ways"))
357 .primitives(r)
358 .highlightNodePairs(Arrays.asList(pd.getNodes(), pdOther.getNodes()))
359 .build());
360 }
361 }
362 return intersection;
363 }
364
365 /**
366 * Check for:<ul>
367 * <li>{@link #WRONG_MEMBER_ROLE}: No useful role for multipolygon member</li>
368 * <li>{@link #WRONG_MEMBER_TYPE}: Non-Way in multipolygon</li>
369 * </ul>
370 * @param r relation
371 */
372 private void checkMembersAndRoles(Relation r) {
373 for (RelationMember rm : r.getMembers()) {
374 if (rm.isWay()) {
375 if (!(rm.hasRole("inner", "outer") || !rm.hasRole())) {
376 errors.add(TestError.builder(this, Severity.WARNING, WRONG_MEMBER_ROLE)
377 .message(tr("No useful role for multipolygon member"))
378 .primitives(addRelationIfNeeded(r, rm.getMember()))
379 .build());
380 }
381 } else {
382 if (!rm.hasRole("admin_centre", "label", "subarea", "land_area")) {
383 errors.add(TestError.builder(this, Severity.WARNING, WRONG_MEMBER_TYPE)
384 .message(tr("Non-Way in multipolygon"))
385 .primitives(addRelationIfNeeded(r, rm.getMember()))
386 .build());
387 }
388 }
389 }
390 }
391
392 private static Collection<? extends OsmPrimitive> addRelationIfNeeded(Relation r, OsmPrimitive primitive) {
393 return addRelationIfNeeded(r, Collections.singleton(primitive));
394 }
395
396 private static Collection<? extends OsmPrimitive> addRelationIfNeeded(Relation r, Collection<? extends OsmPrimitive> primitives) {
397 // Fix #8212 : if the error references only incomplete primitives,
398 // add multipolygon in order to let user select something and fix the error
399 if (!primitives.contains(r)) {
400 for (OsmPrimitive p : primitives) {
401 if (!p.isIncomplete()) {
402 return null;
403 }
404 }
405 // Diamond operator does not work with Java 9 here
406 @SuppressWarnings("unused")
407 List<OsmPrimitive> newPrimitives = new ArrayList<OsmPrimitive>(primitives);
408 newPrimitives.add(0, r);
409 return newPrimitives;
410 } else {
411 return primitives;
412 }
413 }
414
415}
Note: See TracBrowser for help on using the repository browser.