source: josm/trunk/src/org/openstreetmap/josm/data/osm/visitor/paint/relations/Multipolygon.java@ 8512

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

checkstyle: redundant modifiers

  • Property svn:eol-style set to native
File size: 22.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.osm.visitor.paint.relations;
3
4import java.awt.geom.Path2D;
5import java.awt.geom.Path2D.Double;
6import java.awt.geom.PathIterator;
7import java.awt.geom.Rectangle2D;
8import java.util.ArrayList;
9import java.util.Collection;
10import java.util.Collections;
11import java.util.HashSet;
12import java.util.Iterator;
13import java.util.List;
14import java.util.Set;
15
16import org.openstreetmap.josm.Main;
17import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
18import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
19import org.openstreetmap.josm.data.coor.EastNorth;
20import org.openstreetmap.josm.data.osm.DataSet;
21import org.openstreetmap.josm.data.osm.Node;
22import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
23import org.openstreetmap.josm.data.osm.Relation;
24import org.openstreetmap.josm.data.osm.RelationMember;
25import org.openstreetmap.josm.data.osm.Way;
26import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
27import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
28import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;
29
30/**
31 * Multipolygon data used to represent complex areas, see <a href="https://wiki.openstreetmap.org/wiki/Relation:multipolygon">wiki</a>.
32 * @since 2788
33 */
34public class Multipolygon {
35
36 /** preference key for a collection of roles which indicate that the respective member belongs to an
37 * <em>outer</em> polygon. Default is <tt>outer</tt>.
38 */
39 public static final String PREF_KEY_OUTER_ROLES = "mappaint.multipolygon.outer.roles";
40
41 /** preference key for collection of role prefixes which indicate that the respective
42 * member belongs to an <em>outer</em> polygon. Default is empty.
43 */
44 public static final String PREF_KEY_OUTER_ROLE_PREFIXES = "mappaint.multipolygon.outer.role-prefixes";
45
46 /** preference key for a collection of roles which indicate that the respective member belongs to an
47 * <em>inner</em> polygon. Default is <tt>inner</tt>.
48 */
49 public static final String PREF_KEY_INNER_ROLES = "mappaint.multipolygon.inner.roles";
50
51 /** preference key for collection of role prefixes which indicate that the respective
52 * member belongs to an <em>inner</em> polygon. Default is empty.
53 */
54 public static final String PREF_KEY_INNER_ROLE_PREFIXES = "mappaint.multipolygon.inner.role-prefixes";
55
56 /**
57 * <p>Kind of strategy object which is responsible for deciding whether a given
58 * member role indicates that the member belongs to an <em>outer</em> or an
59 * <em>inner</em> polygon.</p>
60 *
61 * <p>The decision is taken based on preference settings, see the four preference keys
62 * above.</p>
63 */
64 private static class MultipolygonRoleMatcher implements PreferenceChangedListener {
65 private final List<String> outerExactRoles = new ArrayList<>();
66 private final List<String> outerRolePrefixes = new ArrayList<>();
67 private final List<String> innerExactRoles = new ArrayList<>();
68 private final List<String> innerRolePrefixes = new ArrayList<>();
69
70 private void initDefaults() {
71 outerExactRoles.clear();
72 outerRolePrefixes.clear();
73 innerExactRoles.clear();
74 innerRolePrefixes.clear();
75 outerExactRoles.add("outer");
76 innerExactRoles.add("inner");
77 }
78
79 private void setNormalized(Collection<String> literals, List<String> target) {
80 target.clear();
81 for (String l: literals) {
82 if (l == null) {
83 continue;
84 }
85 l = l.trim();
86 if (!target.contains(l)) {
87 target.add(l);
88 }
89 }
90 }
91
92 private void initFromPreferences() {
93 initDefaults();
94 if (Main.pref == null) return;
95 Collection<String> literals;
96 literals = Main.pref.getCollection(PREF_KEY_OUTER_ROLES);
97 if (literals != null && !literals.isEmpty()) {
98 setNormalized(literals, outerExactRoles);
99 }
100 literals = Main.pref.getCollection(PREF_KEY_OUTER_ROLE_PREFIXES);
101 if (literals != null && !literals.isEmpty()) {
102 setNormalized(literals, outerRolePrefixes);
103 }
104 literals = Main.pref.getCollection(PREF_KEY_INNER_ROLES);
105 if (literals != null && !literals.isEmpty()) {
106 setNormalized(literals, innerExactRoles);
107 }
108 literals = Main.pref.getCollection(PREF_KEY_INNER_ROLE_PREFIXES);
109 if (literals != null && !literals.isEmpty()) {
110 setNormalized(literals, innerRolePrefixes);
111 }
112 }
113
114 @Override
115 public void preferenceChanged(PreferenceChangeEvent evt) {
116 if (PREF_KEY_INNER_ROLE_PREFIXES.equals(evt.getKey()) ||
117 PREF_KEY_INNER_ROLES.equals(evt.getKey()) ||
118 PREF_KEY_OUTER_ROLE_PREFIXES.equals(evt.getKey()) ||
119 PREF_KEY_OUTER_ROLES.equals(evt.getKey())) {
120 initFromPreferences();
121 }
122 }
123
124 public boolean isOuterRole(String role) {
125 if (role == null) return false;
126 for (String candidate: outerExactRoles) {
127 if (role.equals(candidate)) return true;
128 }
129 for (String candidate: outerRolePrefixes) {
130 if (role.startsWith(candidate)) return true;
131 }
132 return false;
133 }
134
135 public boolean isInnerRole(String role) {
136 if (role == null) return false;
137 for (String candidate: innerExactRoles) {
138 if (role.equals(candidate)) return true;
139 }
140 for (String candidate: innerRolePrefixes) {
141 if (role.startsWith(candidate)) return true;
142 }
143 return false;
144 }
145 }
146
147 /*
148 * Init a private global matcher object which will listen to preference changes.
149 */
150 private static MultipolygonRoleMatcher roleMatcher;
151
152 private static synchronized MultipolygonRoleMatcher getMultipolygonRoleMatcher() {
153 if (roleMatcher == null) {
154 roleMatcher = new MultipolygonRoleMatcher();
155 if (Main.pref != null) {
156 roleMatcher.initFromPreferences();
157 Main.pref.addPreferenceChangeListener(roleMatcher);
158 }
159 }
160 return roleMatcher;
161 }
162
163 public static class JoinedWay {
164 private final List<Node> nodes;
165 private final Collection<Long> wayIds;
166 private final boolean selected;
167
168 public JoinedWay(List<Node> nodes, Collection<Long> wayIds, boolean selected) {
169 this.nodes = nodes;
170 this.wayIds = wayIds;
171 this.selected = selected;
172 }
173
174 public List<Node> getNodes() {
175 return nodes;
176 }
177
178 public Collection<Long> getWayIds() {
179 return wayIds;
180 }
181
182 public boolean isSelected() {
183 return selected;
184 }
185
186 public boolean isClosed() {
187 return nodes.isEmpty() || nodes.get(nodes.size() - 1).equals(nodes.get(0));
188 }
189 }
190
191 public static class PolyData {
192 public enum Intersection {INSIDE, OUTSIDE, CROSSING}
193
194 private final Path2D.Double poly;
195 public boolean selected;
196 private Rectangle2D bounds;
197 private final Collection<Long> wayIds;
198 private final List<Node> nodes;
199 private final List<PolyData> inners;
200
201 public PolyData(Way closedWay) {
202 this(closedWay.getNodes(), closedWay.isSelected(), Collections.singleton(closedWay.getUniqueId()));
203 }
204
205 public PolyData(JoinedWay joinedWay) {
206 this(joinedWay.getNodes(), joinedWay.isSelected(), joinedWay.getWayIds());
207 }
208
209 private PolyData(List<Node> nodes, boolean selected, Collection<Long> wayIds) {
210 this.wayIds = Collections.unmodifiableCollection(wayIds);
211 this.nodes = new ArrayList<>(nodes);
212 this.selected = selected;
213 this.inners = new ArrayList<>();
214 this.poly = new Path2D.Double();
215 this.poly.setWindingRule(Path2D.WIND_EVEN_ODD);
216 buildPoly();
217 }
218
219 private void buildPoly() {
220 boolean initial = true;
221 for (Node n : nodes) {
222 EastNorth p = n.getEastNorth();
223 if (p != null) {
224 if (initial) {
225 poly.moveTo(p.getX(), p.getY());
226 initial = false;
227 } else {
228 poly.lineTo(p.getX(), p.getY());
229 }
230 }
231 }
232 if (!initial) { // fix #7593
233 poly.closePath();
234 }
235 for (PolyData inner : inners) {
236 appendInner(inner.poly);
237 }
238 }
239
240 public PolyData(PolyData copy) {
241 this.selected = copy.selected;
242 this.poly = (Double) copy.poly.clone();
243 this.wayIds = Collections.unmodifiableCollection(copy.wayIds);
244 this.nodes = new ArrayList<>(copy.nodes);
245 this.inners = new ArrayList<>(copy.inners);
246 }
247
248 public Intersection contains(Path2D.Double p) {
249 int contains = 0;
250 int total = 0;
251 double[] coords = new double[6];
252 for (PathIterator it = p.getPathIterator(null); !it.isDone(); it.next()) {
253 switch (it.currentSegment(coords)) {
254 case PathIterator.SEG_MOVETO:
255 case PathIterator.SEG_LINETO:
256 if (poly.contains(coords[0], coords[1])) {
257 contains++;
258 }
259 total++;
260 }
261 }
262 if (contains == total) return Intersection.INSIDE;
263 if (contains == 0) return Intersection.OUTSIDE;
264 return Intersection.CROSSING;
265 }
266
267 public void addInner(PolyData inner) {
268 inners.add(inner);
269 appendInner(inner.poly);
270 }
271
272 private void appendInner(Path2D.Double inner) {
273 poly.append(inner.getPathIterator(null), false);
274 }
275
276 public Path2D.Double get() {
277 return poly;
278 }
279
280 public Rectangle2D getBounds() {
281 if (bounds == null) {
282 bounds = poly.getBounds2D();
283 }
284 return bounds;
285 }
286
287 public Collection<Long> getWayIds() {
288 return wayIds;
289 }
290
291 public List<Node> getNodes() {
292 return nodes;
293 }
294
295 private void resetNodes(DataSet dataSet) {
296 if (!nodes.isEmpty()) {
297 DataSet ds = dataSet;
298 // Find DataSet (can be null for several nodes when undoing nodes creation, see #7162)
299 for (Iterator<Node> it = nodes.iterator(); it.hasNext() && ds == null;) {
300 ds = it.next().getDataSet();
301 }
302 nodes.clear();
303 if (ds == null) {
304 // DataSet still not found. This should not happen, but a warning does no harm
305 Main.warn("DataSet not found while resetting nodes in Multipolygon. " +
306 "This should not happen, you may report it to JOSM developers.");
307 } else if (wayIds.size() == 1) {
308 Way w = (Way) ds.getPrimitiveById(wayIds.iterator().next(), OsmPrimitiveType.WAY);
309 nodes.addAll(w.getNodes());
310 } else if (!wayIds.isEmpty()) {
311 List<Way> waysToJoin = new ArrayList<>();
312 for (Long wayId : wayIds) {
313 Way w = (Way) ds.getPrimitiveById(wayId, OsmPrimitiveType.WAY);
314 if (w != null && w.getNodesCount() > 0) { // fix #7173 (empty ways on purge)
315 waysToJoin.add(w);
316 }
317 }
318 if (!waysToJoin.isEmpty()) {
319 nodes.addAll(joinWays(waysToJoin).iterator().next().getNodes());
320 }
321 }
322 resetPoly();
323 }
324 }
325
326 private void resetPoly() {
327 poly.reset();
328 buildPoly();
329 bounds = null;
330 }
331
332 public void nodeMoved(NodeMovedEvent event) {
333 final Node n = event.getNode();
334 boolean innerChanged = false;
335 for (PolyData inner : inners) {
336 if (inner.nodes.contains(n)) {
337 inner.resetPoly();
338 innerChanged = true;
339 }
340 }
341 if (nodes.contains(n) || innerChanged) {
342 resetPoly();
343 }
344 }
345
346 public void wayNodesChanged(WayNodesChangedEvent event) {
347 final Long wayId = event.getChangedWay().getUniqueId();
348 boolean innerChanged = false;
349 for (PolyData inner : inners) {
350 if (inner.wayIds.contains(wayId)) {
351 inner.resetNodes(event.getDataset());
352 innerChanged = true;
353 }
354 }
355 if (wayIds.contains(wayId) || innerChanged) {
356 resetNodes(event.getDataset());
357 }
358 }
359 }
360
361 private final List<Way> innerWays = new ArrayList<>();
362 private final List<Way> outerWays = new ArrayList<>();
363 private final List<PolyData> combinedPolygons = new ArrayList<>();
364 private final List<Node> openEnds = new ArrayList<>();
365
366 private boolean incomplete;
367
368 public Multipolygon(Relation r) {
369 load(r);
370 }
371
372 private void load(Relation r) {
373 MultipolygonRoleMatcher matcher = getMultipolygonRoleMatcher();
374
375 // Fill inner and outer list with valid ways
376 for (RelationMember m : r.getMembers()) {
377 if (m.getMember().isIncomplete()) {
378 this.incomplete = true;
379 } else if (m.getMember().isDrawable()) {
380 if (m.isWay()) {
381 Way w = m.getWay();
382
383 if (w.getNodesCount() < 2) {
384 continue;
385 }
386
387 if (matcher.isInnerRole(m.getRole())) {
388 innerWays.add(w);
389 } else if (matcher.isOuterRole(m.getRole())) {
390 outerWays.add(w);
391 } else if (!m.hasRole()) {
392 outerWays.add(w);
393 } // Remaining roles ignored
394 } // Non ways ignored
395 }
396 }
397
398 final List<PolyData> innerPolygons = new ArrayList<>();
399 final List<PolyData> outerPolygons = new ArrayList<>();
400 createPolygons(innerWays, innerPolygons);
401 createPolygons(outerWays, outerPolygons);
402 if (!outerPolygons.isEmpty()) {
403 addInnerToOuters(innerPolygons, outerPolygons);
404 }
405 }
406
407 public final boolean isIncomplete() {
408 return incomplete;
409 }
410
411 private void createPolygons(List<Way> ways, List<PolyData> result) {
412 List<Way> waysToJoin = new ArrayList<>();
413 for (Way way: ways) {
414 if (way.isClosed()) {
415 result.add(new PolyData(way));
416 } else {
417 waysToJoin.add(way);
418 }
419 }
420
421 for (JoinedWay jw: joinWays(waysToJoin)) {
422 result.add(new PolyData(jw));
423 if (!jw.isClosed()) {
424 openEnds.add(jw.getNodes().get(0));
425 openEnds.add(jw.getNodes().get(jw.getNodes().size() - 1));
426 }
427 }
428 }
429
430 public static Collection<JoinedWay> joinWays(Collection<Way> waysToJoin) {
431 final Collection<JoinedWay> result = new ArrayList<>();
432 final Way[] joinArray = waysToJoin.toArray(new Way[waysToJoin.size()]);
433 int left = waysToJoin.size();
434 while (left > 0) {
435 Way w = null;
436 boolean selected = false;
437 List<Node> nodes = null;
438 Set<Long> wayIds = new HashSet<>();
439 boolean joined = true;
440 while (joined && left > 0) {
441 joined = false;
442 for (int i = 0; i < joinArray.length && left != 0; ++i) {
443 if (joinArray[i] != null) {
444 Way c = joinArray[i];
445 if (c.getNodesCount() == 0) {
446 continue;
447 }
448 if (w == null) {
449 w = c;
450 selected = w.isSelected();
451 joinArray[i] = null;
452 --left;
453 } else {
454 int mode = 0;
455 int cl = c.getNodesCount()-1;
456 int nl;
457 if (nodes == null) {
458 nl = w.getNodesCount()-1;
459 if (w.getNode(nl) == c.getNode(0)) {
460 mode = 21;
461 } else if (w.getNode(nl) == c.getNode(cl)) {
462 mode = 22;
463 } else if (w.getNode(0) == c.getNode(0)) {
464 mode = 11;
465 } else if (w.getNode(0) == c.getNode(cl)) {
466 mode = 12;
467 }
468 } else {
469 nl = nodes.size()-1;
470 if (nodes.get(nl) == c.getNode(0)) {
471 mode = 21;
472 } else if (nodes.get(0) == c.getNode(cl)) {
473 mode = 12;
474 } else if (nodes.get(0) == c.getNode(0)) {
475 mode = 11;
476 } else if (nodes.get(nl) == c.getNode(cl)) {
477 mode = 22;
478 }
479 }
480 if (mode != 0) {
481 joinArray[i] = null;
482 joined = true;
483 if (c.isSelected()) {
484 selected = true;
485 }
486 --left;
487 if (nodes == null) {
488 nodes = w.getNodes();
489 wayIds.add(w.getUniqueId());
490 }
491 nodes.remove((mode == 21 || mode == 22) ? nl : 0);
492 if (mode == 21) {
493 nodes.addAll(c.getNodes());
494 } else if (mode == 12) {
495 nodes.addAll(0, c.getNodes());
496 } else if (mode == 22) {
497 for (Node node : c.getNodes()) {
498 nodes.add(nl, node);
499 }
500 } else /* mode == 11 */ {
501 for (Node node : c.getNodes()) {
502 nodes.add(0, node);
503 }
504 }
505 wayIds.add(c.getUniqueId());
506 }
507 }
508 }
509 }
510 }
511
512 if (nodes == null && w != null) {
513 nodes = w.getNodes();
514 wayIds.add(w.getUniqueId());
515 }
516
517 result.add(new JoinedWay(nodes, wayIds, selected));
518 }
519
520 return result;
521 }
522
523 public PolyData findOuterPolygon(PolyData inner, List<PolyData> outerPolygons) {
524
525 // First try to test only bbox, use precise testing only if we don't get unique result
526 Rectangle2D innerBox = inner.getBounds();
527 PolyData insidePolygon = null;
528 PolyData intersectingPolygon = null;
529 int insideCount = 0;
530 int intersectingCount = 0;
531
532 for (PolyData outer: outerPolygons) {
533 if (outer.getBounds().contains(innerBox)) {
534 insidePolygon = outer;
535 insideCount++;
536 } else if (outer.getBounds().intersects(innerBox)) {
537 intersectingPolygon = outer;
538 intersectingCount++;
539 }
540 }
541
542 if (insideCount == 1)
543 return insidePolygon;
544 else if (intersectingCount == 1)
545 return intersectingPolygon;
546
547 PolyData result = null;
548 for (PolyData combined : outerPolygons) {
549 if (combined.contains(inner.poly) != Intersection.OUTSIDE) {
550 if (result == null || result.contains(combined.poly) == Intersection.INSIDE) {
551 result = combined;
552 }
553 }
554 }
555 return result;
556 }
557
558 private void addInnerToOuters(List<PolyData> innerPolygons, List<PolyData> outerPolygons) {
559
560 if (innerPolygons.isEmpty()) {
561 combinedPolygons.addAll(outerPolygons);
562 } else if (outerPolygons.size() == 1) {
563 PolyData combinedOuter = new PolyData(outerPolygons.get(0));
564 for (PolyData inner: innerPolygons) {
565 combinedOuter.addInner(inner);
566 }
567 combinedPolygons.add(combinedOuter);
568 } else {
569 for (PolyData outer: outerPolygons) {
570 combinedPolygons.add(new PolyData(outer));
571 }
572
573 for (PolyData pdInner: innerPolygons) {
574 PolyData o = findOuterPolygon(pdInner, combinedPolygons);
575 if (o == null) {
576 o = outerPolygons.get(0);
577 }
578 o.addInner(pdInner);
579 }
580 }
581 }
582
583 /**
584 * Replies the list of outer ways.
585 * @return the list of outer ways
586 */
587 public List<Way> getOuterWays() {
588 return outerWays;
589 }
590
591 /**
592 * Replies the list of inner ways.
593 * @return the list of inner ways
594 */
595 public List<Way> getInnerWays() {
596 return innerWays;
597 }
598
599 public List<PolyData> getCombinedPolygons() {
600 return combinedPolygons;
601 }
602
603 public List<PolyData> getInnerPolygons() {
604 final List<PolyData> innerPolygons = new ArrayList<>();
605 createPolygons(innerWays, innerPolygons);
606 return innerPolygons;
607 }
608
609 public List<PolyData> getOuterPolygons() {
610 final List<PolyData> outerPolygons = new ArrayList<>();
611 createPolygons(outerWays, outerPolygons);
612 return outerPolygons;
613 }
614
615 /**
616 * Returns the start and end node of non-closed rings.
617 * @return the start and end node of non-closed rings.
618 */
619 public List<Node> getOpenEnds() {
620 return openEnds;
621 }
622}
Note: See TracBrowser for help on using the repository browser.