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

Last change on this file since 9490 was 9490, checked in by Don-vip, 8 years ago

see #12377 - add multipolygon unit test

  • Property svn:eol-style set to native
File size: 14.9 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.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.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 polygon */
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 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(new TestError(this, Severity.WARNING, tr("Area style way is not closed"), NOT_CLOSED,
150 Collections.singletonList(w), Arrays.asList(nodes.get(0), nodes.get(nodes.size() - 1))));
151 }
152 }
153
154 @Override
155 public void visit(Relation r) {
156 if (r.isMultipolygon()) {
157 checkMembersAndRoles(r);
158
159 Multipolygon polygon = MultipolygonCache.getInstance().get(Main.map.mapView, r);
160
161 boolean hasOuterWay = false;
162 for (RelationMember m : r.getMembers()) {
163 if ("outer".equals(m.getRole())) {
164 hasOuterWay = true;
165 break;
166 }
167 }
168 if (!hasOuterWay) {
169 addError(r, new TestError(this, Severity.WARNING, tr("No outer way for multipolygon"), MISSING_OUTER_WAY, r));
170 }
171
172 if (r.hasIncompleteMembers()) {
173 return; // Rest of checks is only for complete multipolygons
174 }
175
176 // Create new multipolygon using the logics from CreateMultipolygonAction and see if roles match.
177 final Pair<Relation, Relation> newMP = CreateMultipolygonAction.createMultipolygonRelation(r.getMemberPrimitives(Way.class), false);
178 if (newMP != null) {
179 for (RelationMember member : r.getMembers()) {
180 final Collection<RelationMember> memberInNewMP = newMP.b.getMembersFor(Collections.singleton(member.getMember()));
181 if (memberInNewMP != null && !memberInNewMP.isEmpty()) {
182 final String roleInNewMP = memberInNewMP.iterator().next().getRole();
183 if (!member.getRole().equals(roleInNewMP)) {
184 addError(r, new TestError(this, Severity.WARNING, RelationChecker.ROLE_VERIF_PROBLEM_MSG,
185 tr("Role for ''{0}'' should be ''{1}''",
186 member.getMember().getDisplayName(DefaultNameFormatter.getInstance()), roleInNewMP),
187 MessageFormat.format("Role for ''{0}'' should be ''{1}''",
188 member.getMember().getDisplayName(DefaultNameFormatter.getInstance()), roleInNewMP),
189 WRONG_MEMBER_ROLE, Collections.singleton(r), Collections.singleton(member.getMember())));
190 }
191 }
192 }
193 }
194
195 if (styles != null && !"boundary".equals(r.get("type"))) {
196 AreaElement area = ElemStyles.getAreaElemStyle(r, false);
197 boolean areaStyle = area != null;
198 // If area style was not found for relation then use style of ways
199 if (area == null) {
200 for (Way w : polygon.getOuterWays()) {
201 area = ElemStyles.getAreaElemStyle(w, true);
202 if (area != null) {
203 break;
204 }
205 }
206 if (area == null) {
207 addError(r, new TestError(this, Severity.OTHER, tr("No area style for multipolygon"), NO_STYLE, r));
208 } else {
209 /* old style multipolygon - solve: copy tags from outer way to multipolygon */
210 addError(r, new TestError(this, Severity.WARNING,
211 trn("Multipolygon relation should be tagged with area tags and not the outer way",
212 "Multipolygon relation should be tagged with area tags and not the outer ways",
213 polygon.getOuterWays().size()),
214 NO_STYLE_POLYGON, r));
215 }
216 }
217
218 if (area != null) {
219 for (Way wInner : polygon.getInnerWays()) {
220 AreaElement areaInner = ElemStyles.getAreaElemStyle(wInner, false);
221
222 if (areaInner != null && area.equals(areaInner)) {
223 List<OsmPrimitive> l = new ArrayList<>();
224 l.add(r);
225 l.add(wInner);
226 addError(r, new TestError(this, Severity.OTHER,
227 tr("With the currently used mappaint style the style for inner way equals the multipolygon style"),
228 INNER_STYLE_MISMATCH, l, Collections.singletonList(wInner)));
229 }
230 }
231 for (Way wOuter : polygon.getOuterWays()) {
232 AreaElement areaOuter = ElemStyles.getAreaElemStyle(wOuter, false);
233 if (areaOuter != null) {
234 List<OsmPrimitive> l = new ArrayList<>();
235 l.add(r);
236 l.add(wOuter);
237 if (!area.equals(areaOuter)) {
238 addError(r, new TestError(this, Severity.WARNING, !areaStyle ? tr("Style for outer way mismatches")
239 : tr("With the currently used mappaint style(s) the style for outer way mismatches polygon"),
240 OUTER_STYLE_MISMATCH, l, Collections.singletonList(wOuter)));
241 } else if (areaStyle) { /* style on outer way of multipolygon, but equal to polygon */
242 addError(r, new TestError(this, Severity.WARNING, tr("Area style on outer way"), OUTER_STYLE,
243 l, Collections.singletonList(wOuter)));
244 }
245 }
246 }
247 }
248 }
249
250 List<Node> openNodes = polygon.getOpenEnds();
251 if (!openNodes.isEmpty()) {
252 List<OsmPrimitive> primitives = new LinkedList<>();
253 primitives.add(r);
254 primitives.addAll(openNodes);
255 Arrays.asList(openNodes, r);
256 addError(r, new TestError(this, Severity.WARNING, tr("Multipolygon is not closed"), NON_CLOSED_WAY,
257 primitives, openNodes));
258 }
259
260 // For painting is used Polygon class which works with ints only. For validation we need more precision
261 List<GeneralPath> outerPolygons = createPolygons(polygon.getOuterPolygons());
262 for (Multipolygon.PolyData pdInner : polygon.getInnerPolygons()) {
263 boolean outside = true;
264 boolean crossing = false;
265 Multipolygon.PolyData outerWay = null;
266 for (int i = 0; i < polygon.getOuterPolygons().size(); i++) {
267 GeneralPath outer = outerPolygons.get(i);
268 Intersection intersection = getPolygonIntersection(outer, pdInner.getNodes());
269 outside = outside & intersection == Intersection.OUTSIDE;
270 if (intersection == Intersection.CROSSING) {
271 crossing = true;
272 outerWay = polygon.getOuterPolygons().get(i);
273 }
274 }
275 if (outside || crossing) {
276 List<List<Node>> highlights = new ArrayList<>();
277 highlights.add(pdInner.getNodes());
278 if (outside) {
279 addError(r, new TestError(this, Severity.WARNING, tr("Multipolygon inner way is outside"),
280 INNER_WAY_OUTSIDE, Collections.singletonList(r), highlights));
281 } else if (outerWay != null) {
282 highlights.add(outerWay.getNodes());
283 addError(r, new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"),
284 CROSSING_WAYS, Collections.singletonList(r), highlights));
285 }
286 }
287 }
288 }
289 }
290
291 private void checkMembersAndRoles(Relation r) {
292 for (RelationMember rm : r.getMembers()) {
293 if (rm.isWay()) {
294 if (!(rm.hasRole("inner", "outer") || !rm.hasRole())) {
295 addError(r, new TestError(this, Severity.WARNING, tr("No useful role for multipolygon member"),
296 WRONG_MEMBER_ROLE, rm.getMember()));
297 }
298 } else {
299 if (!rm.hasRole("admin_centre", "label", "subarea", "land_area")) {
300 addError(r, new TestError(this, Severity.WARNING, tr("Non-Way in multipolygon"), WRONG_MEMBER_TYPE, rm.getMember()));
301 }
302 }
303 }
304 }
305
306 private static void addRelationIfNeeded(TestError error, Relation r) {
307 // Fix #8212 : if the error references only incomplete primitives,
308 // add multipolygon in order to let user select something and fix the error
309 Collection<? extends OsmPrimitive> primitives = error.getPrimitives();
310 if (!primitives.contains(r)) {
311 for (OsmPrimitive p : primitives) {
312 if (!p.isIncomplete()) {
313 return;
314 }
315 }
316 // Diamond operator does not work with Java 9 here
317 @SuppressWarnings("unused")
318 List<OsmPrimitive> newPrimitives = new ArrayList<OsmPrimitive>(primitives);
319 newPrimitives.add(0, r);
320 error.setPrimitives(newPrimitives);
321 }
322 }
323
324 private void addError(Relation r, TestError error) {
325 addRelationIfNeeded(error, r);
326 errors.add(error);
327 }
328}
Note: See TracBrowser for help on using the repository browser.