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

Last change on this file since 9061 was 9061, checked in by bastiK, 9 years ago

mappaint partial fill: render unclosed areas differently (margin on both sites) (see #12104)

  • Property svn:eol-style set to native
File size: 23.1 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 static 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 {
193 INSIDE,
194 OUTSIDE,
195 CROSSING
196 }
197
198 private final Path2D.Double poly;
199 public boolean selected;
200 private Rectangle2D bounds;
201 private final Collection<Long> wayIds;
202 private final List<Node> nodes;
203 private final List<PolyData> inners;
204
205 public PolyData(Way closedWay) {
206 this(closedWay.getNodes(), closedWay.isSelected(), Collections.singleton(closedWay.getUniqueId()));
207 }
208
209 public PolyData(JoinedWay joinedWay) {
210 this(joinedWay.getNodes(), joinedWay.isSelected(), joinedWay.getWayIds());
211 }
212
213 private PolyData(List<Node> nodes, boolean selected, Collection<Long> wayIds) {
214 this.wayIds = Collections.unmodifiableCollection(wayIds);
215 this.nodes = new ArrayList<>(nodes);
216 this.selected = selected;
217 this.inners = new ArrayList<>();
218 this.poly = new Path2D.Double();
219 this.poly.setWindingRule(Path2D.WIND_EVEN_ODD);
220 buildPoly();
221 }
222
223 private void buildPoly() {
224 boolean initial = true;
225 for (Node n : nodes) {
226 EastNorth p = n.getEastNorth();
227 if (p != null) {
228 if (initial) {
229 poly.moveTo(p.getX(), p.getY());
230 initial = false;
231 } else {
232 poly.lineTo(p.getX(), p.getY());
233 }
234 }
235 }
236 if (nodes.size() >= 3 && nodes.get(0) == nodes.get(nodes.size() - 1)) {
237 poly.closePath();
238 }
239 for (PolyData inner : inners) {
240 appendInner(inner.poly);
241 }
242 }
243
244 public PolyData(PolyData copy) {
245 this.selected = copy.selected;
246 this.poly = (Double) copy.poly.clone();
247 this.wayIds = Collections.unmodifiableCollection(copy.wayIds);
248 this.nodes = new ArrayList<>(copy.nodes);
249 this.inners = new ArrayList<>(copy.inners);
250 }
251
252 public Intersection contains(Path2D.Double p) {
253 int contains = 0;
254 int total = 0;
255 double[] coords = new double[6];
256 for (PathIterator it = p.getPathIterator(null); !it.isDone(); it.next()) {
257 switch (it.currentSegment(coords)) {
258 case PathIterator.SEG_MOVETO:
259 case PathIterator.SEG_LINETO:
260 if (poly.contains(coords[0], coords[1])) {
261 contains++;
262 }
263 total++;
264 }
265 }
266 if (contains == total) return Intersection.INSIDE;
267 if (contains == 0) return Intersection.OUTSIDE;
268 return Intersection.CROSSING;
269 }
270
271 public void addInner(PolyData inner) {
272 inners.add(inner);
273 appendInner(inner.poly);
274 }
275
276 private void appendInner(Path2D.Double inner) {
277 poly.append(inner.getPathIterator(null), false);
278 }
279
280 public Path2D.Double get() {
281 return poly;
282 }
283
284 public Rectangle2D getBounds() {
285 if (bounds == null) {
286 bounds = poly.getBounds2D();
287 }
288 return bounds;
289 }
290
291 public Collection<Long> getWayIds() {
292 return wayIds;
293 }
294
295 public List<Node> getNodes() {
296 return nodes;
297 }
298
299 private void resetNodes(DataSet dataSet) {
300 if (!nodes.isEmpty()) {
301 DataSet ds = dataSet;
302 // Find DataSet (can be null for several nodes when undoing nodes creation, see #7162)
303 for (Iterator<Node> it = nodes.iterator(); it.hasNext() && ds == null;) {
304 ds = it.next().getDataSet();
305 }
306 nodes.clear();
307 if (ds == null) {
308 // DataSet still not found. This should not happen, but a warning does no harm
309 Main.warn("DataSet not found while resetting nodes in Multipolygon. " +
310 "This should not happen, you may report it to JOSM developers.");
311 } else if (wayIds.size() == 1) {
312 Way w = (Way) ds.getPrimitiveById(wayIds.iterator().next(), OsmPrimitiveType.WAY);
313 nodes.addAll(w.getNodes());
314 } else if (!wayIds.isEmpty()) {
315 List<Way> waysToJoin = new ArrayList<>();
316 for (Long wayId : wayIds) {
317 Way w = (Way) ds.getPrimitiveById(wayId, OsmPrimitiveType.WAY);
318 if (w != null && w.getNodesCount() > 0) { // fix #7173 (empty ways on purge)
319 waysToJoin.add(w);
320 }
321 }
322 if (!waysToJoin.isEmpty()) {
323 nodes.addAll(joinWays(waysToJoin).iterator().next().getNodes());
324 }
325 }
326 resetPoly();
327 }
328 }
329
330 private void resetPoly() {
331 poly.reset();
332 buildPoly();
333 bounds = null;
334 }
335
336 public void nodeMoved(NodeMovedEvent event) {
337 final Node n = event.getNode();
338 boolean innerChanged = false;
339 for (PolyData inner : inners) {
340 if (inner.nodes.contains(n)) {
341 inner.resetPoly();
342 innerChanged = true;
343 }
344 }
345 if (nodes.contains(n) || innerChanged) {
346 resetPoly();
347 }
348 }
349
350 public void wayNodesChanged(WayNodesChangedEvent event) {
351 final Long wayId = event.getChangedWay().getUniqueId();
352 boolean innerChanged = false;
353 for (PolyData inner : inners) {
354 if (inner.wayIds.contains(wayId)) {
355 inner.resetNodes(event.getDataset());
356 innerChanged = true;
357 }
358 }
359 if (wayIds.contains(wayId) || innerChanged) {
360 resetNodes(event.getDataset());
361 }
362 }
363
364 public boolean isClosed() {
365 if (nodes.size() < 3 || nodes.get(0) != nodes.get(nodes.size() - 1)) return false;
366 for (PolyData inner : inners) {
367 if (!inner.isClosed()) return false;
368 }
369 return true;
370 }
371 }
372
373 private final List<Way> innerWays = new ArrayList<>();
374 private final List<Way> outerWays = new ArrayList<>();
375 private final List<PolyData> combinedPolygons = new ArrayList<>();
376 private final List<Node> openEnds = new ArrayList<>();
377
378 private boolean incomplete;
379
380 public Multipolygon(Relation r) {
381 load(r);
382 }
383
384 private void load(Relation r) {
385 MultipolygonRoleMatcher matcher = getMultipolygonRoleMatcher();
386
387 // Fill inner and outer list with valid ways
388 for (RelationMember m : r.getMembers()) {
389 if (m.getMember().isIncomplete()) {
390 this.incomplete = true;
391 } else if (m.getMember().isDrawable()) {
392 if (m.isWay()) {
393 Way w = m.getWay();
394
395 if (w.getNodesCount() < 2) {
396 continue;
397 }
398
399 if (matcher.isInnerRole(m.getRole())) {
400 innerWays.add(w);
401 } else if (matcher.isOuterRole(m.getRole())) {
402 outerWays.add(w);
403 } else if (!m.hasRole()) {
404 outerWays.add(w);
405 } // Remaining roles ignored
406 } // Non ways ignored
407 }
408 }
409
410 final List<PolyData> innerPolygons = new ArrayList<>();
411 final List<PolyData> outerPolygons = new ArrayList<>();
412 createPolygons(innerWays, innerPolygons);
413 createPolygons(outerWays, outerPolygons);
414 if (!outerPolygons.isEmpty()) {
415 addInnerToOuters(innerPolygons, outerPolygons);
416 }
417 }
418
419 public final boolean isIncomplete() {
420 return incomplete;
421 }
422
423 private void createPolygons(List<Way> ways, List<PolyData> result) {
424 List<Way> waysToJoin = new ArrayList<>();
425 for (Way way: ways) {
426 if (way.isClosed()) {
427 result.add(new PolyData(way));
428 } else {
429 waysToJoin.add(way);
430 }
431 }
432
433 for (JoinedWay jw: joinWays(waysToJoin)) {
434 result.add(new PolyData(jw));
435 if (!jw.isClosed()) {
436 openEnds.add(jw.getNodes().get(0));
437 openEnds.add(jw.getNodes().get(jw.getNodes().size() - 1));
438 }
439 }
440 }
441
442 public static Collection<JoinedWay> joinWays(Collection<Way> waysToJoin) {
443 final Collection<JoinedWay> result = new ArrayList<>();
444 final Way[] joinArray = waysToJoin.toArray(new Way[waysToJoin.size()]);
445 int left = waysToJoin.size();
446 while (left > 0) {
447 Way w = null;
448 boolean selected = false;
449 List<Node> nodes = null;
450 Set<Long> wayIds = new HashSet<>();
451 boolean joined = true;
452 while (joined && left > 0) {
453 joined = false;
454 for (int i = 0; i < joinArray.length && left != 0; ++i) {
455 if (joinArray[i] != null) {
456 Way c = joinArray[i];
457 if (c.getNodesCount() == 0) {
458 continue;
459 }
460 if (w == null) {
461 w = c;
462 selected = w.isSelected();
463 joinArray[i] = null;
464 --left;
465 } else {
466 int mode = 0;
467 int cl = c.getNodesCount()-1;
468 int nl;
469 if (nodes == null) {
470 nl = w.getNodesCount()-1;
471 if (w.getNode(nl) == c.getNode(0)) {
472 mode = 21;
473 } else if (w.getNode(nl) == c.getNode(cl)) {
474 mode = 22;
475 } else if (w.getNode(0) == c.getNode(0)) {
476 mode = 11;
477 } else if (w.getNode(0) == c.getNode(cl)) {
478 mode = 12;
479 }
480 } else {
481 nl = nodes.size()-1;
482 if (nodes.get(nl) == c.getNode(0)) {
483 mode = 21;
484 } else if (nodes.get(0) == c.getNode(cl)) {
485 mode = 12;
486 } else if (nodes.get(0) == c.getNode(0)) {
487 mode = 11;
488 } else if (nodes.get(nl) == c.getNode(cl)) {
489 mode = 22;
490 }
491 }
492 if (mode != 0) {
493 joinArray[i] = null;
494 joined = true;
495 if (c.isSelected()) {
496 selected = true;
497 }
498 --left;
499 if (nodes == null) {
500 nodes = w.getNodes();
501 wayIds.add(w.getUniqueId());
502 }
503 nodes.remove((mode == 21 || mode == 22) ? nl : 0);
504 if (mode == 21) {
505 nodes.addAll(c.getNodes());
506 } else if (mode == 12) {
507 nodes.addAll(0, c.getNodes());
508 } else if (mode == 22) {
509 for (Node node : c.getNodes()) {
510 nodes.add(nl, node);
511 }
512 } else /* mode == 11 */ {
513 for (Node node : c.getNodes()) {
514 nodes.add(0, node);
515 }
516 }
517 wayIds.add(c.getUniqueId());
518 }
519 }
520 }
521 }
522 }
523
524 if (nodes == null && w != null) {
525 nodes = w.getNodes();
526 wayIds.add(w.getUniqueId());
527 }
528
529 result.add(new JoinedWay(nodes, wayIds, selected));
530 }
531
532 return result;
533 }
534
535 public PolyData findOuterPolygon(PolyData inner, List<PolyData> outerPolygons) {
536
537 // First try to test only bbox, use precise testing only if we don't get unique result
538 Rectangle2D innerBox = inner.getBounds();
539 PolyData insidePolygon = null;
540 PolyData intersectingPolygon = null;
541 int insideCount = 0;
542 int intersectingCount = 0;
543
544 for (PolyData outer: outerPolygons) {
545 if (outer.getBounds().contains(innerBox)) {
546 insidePolygon = outer;
547 insideCount++;
548 } else if (outer.getBounds().intersects(innerBox)) {
549 intersectingPolygon = outer;
550 intersectingCount++;
551 }
552 }
553
554 if (insideCount == 1)
555 return insidePolygon;
556 else if (intersectingCount == 1)
557 return intersectingPolygon;
558
559 PolyData result = null;
560 for (PolyData combined : outerPolygons) {
561 if (combined.contains(inner.poly) != Intersection.OUTSIDE) {
562 if (result == null || result.contains(combined.poly) == Intersection.INSIDE) {
563 result = combined;
564 }
565 }
566 }
567 return result;
568 }
569
570 private void addInnerToOuters(List<PolyData> innerPolygons, List<PolyData> outerPolygons) {
571
572 if (innerPolygons.isEmpty()) {
573 combinedPolygons.addAll(outerPolygons);
574 } else if (outerPolygons.size() == 1) {
575 PolyData combinedOuter = new PolyData(outerPolygons.get(0));
576 for (PolyData inner: innerPolygons) {
577 combinedOuter.addInner(inner);
578 }
579 combinedPolygons.add(combinedOuter);
580 } else {
581 for (PolyData outer: outerPolygons) {
582 combinedPolygons.add(new PolyData(outer));
583 }
584
585 for (PolyData pdInner: innerPolygons) {
586 PolyData o = findOuterPolygon(pdInner, combinedPolygons);
587 if (o == null) {
588 o = outerPolygons.get(0);
589 }
590 o.addInner(pdInner);
591 }
592 }
593 }
594
595 /**
596 * Replies the list of outer ways.
597 * @return the list of outer ways
598 */
599 public List<Way> getOuterWays() {
600 return outerWays;
601 }
602
603 /**
604 * Replies the list of inner ways.
605 * @return the list of inner ways
606 */
607 public List<Way> getInnerWays() {
608 return innerWays;
609 }
610
611 public List<PolyData> getCombinedPolygons() {
612 return combinedPolygons;
613 }
614
615 public List<PolyData> getInnerPolygons() {
616 final List<PolyData> innerPolygons = new ArrayList<>();
617 createPolygons(innerWays, innerPolygons);
618 return innerPolygons;
619 }
620
621 public List<PolyData> getOuterPolygons() {
622 final List<PolyData> outerPolygons = new ArrayList<>();
623 createPolygons(outerWays, outerPolygons);
624 return outerPolygons;
625 }
626
627 /**
628 * Returns the start and end node of non-closed rings.
629 * @return the start and end node of non-closed rings.
630 */
631 public List<Node> getOpenEnds() {
632 return openEnds;
633 }
634}
Note: See TracBrowser for help on using the repository browser.