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

Last change on this file since 15178 was 15178, checked in by GerdP, 5 years ago

fix #17819

  • Create ArrayList instead of modifying a CopyList instance in joinWays()
  • Avoid to remove node, instead add sublist to avoid duplicate nodes, always use addAll().
  • use HashSet to store larger collections of way ids, Collections.singleton for single ids

Result: Multipolygon.load() performance is much better for very complex multipolygons (1.6 secs instead of 30), no changes for normal sized relations.

  • Property svn:eol-style set to native
File size: 28.9 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.PathIterator;
6import java.awt.geom.Rectangle2D;
7import java.util.ArrayList;
8import java.util.Collection;
9import java.util.Collections;
10import java.util.HashSet;
11import java.util.Iterator;
12import java.util.List;
13import java.util.Optional;
14import java.util.Set;
15
16import org.openstreetmap.josm.data.coor.EastNorth;
17import org.openstreetmap.josm.data.osm.DataSet;
18import org.openstreetmap.josm.data.osm.Node;
19import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
20import org.openstreetmap.josm.data.osm.Relation;
21import org.openstreetmap.josm.data.osm.RelationMember;
22import org.openstreetmap.josm.data.osm.Way;
23import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
24import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
25import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;
26import org.openstreetmap.josm.data.projection.Projection;
27import org.openstreetmap.josm.data.projection.ProjectionRegistry;
28import org.openstreetmap.josm.spi.preferences.Config;
29import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
30import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
31import org.openstreetmap.josm.tools.Geometry;
32import org.openstreetmap.josm.tools.Geometry.AreaAndPerimeter;
33import org.openstreetmap.josm.tools.Logging;
34
35/**
36 * Multipolygon data used to represent complex areas, see <a href="https://wiki.openstreetmap.org/wiki/Relation:multipolygon">wiki</a>.
37 * @since 2788
38 */
39public class Multipolygon {
40
41 /** preference key for a collection of roles which indicate that the respective member belongs to an
42 * <em>outer</em> polygon. Default is <code>outer</code>.
43 */
44 public static final String PREF_KEY_OUTER_ROLES = "mappaint.multipolygon.outer.roles";
45
46 /** preference key for collection of role prefixes which indicate that the respective
47 * member belongs to an <em>outer</em> polygon. Default is empty.
48 */
49 public static final String PREF_KEY_OUTER_ROLE_PREFIXES = "mappaint.multipolygon.outer.role-prefixes";
50
51 /** preference key for a collection of roles which indicate that the respective member belongs to an
52 * <em>inner</em> polygon. Default is <code>inner</code>.
53 */
54 public static final String PREF_KEY_INNER_ROLES = "mappaint.multipolygon.inner.roles";
55
56 /** preference key for collection of role prefixes which indicate that the respective
57 * member belongs to an <em>inner</em> polygon. Default is empty.
58 */
59 public static final String PREF_KEY_INNER_ROLE_PREFIXES = "mappaint.multipolygon.inner.role-prefixes";
60
61 /**
62 * <p>Kind of strategy object which is responsible for deciding whether a given
63 * member role indicates that the member belongs to an <em>outer</em> or an
64 * <em>inner</em> polygon.</p>
65 *
66 * <p>The decision is taken based on preference settings, see the four preference keys
67 * above.</p>
68 */
69 private static class MultipolygonRoleMatcher implements PreferenceChangedListener {
70 private final List<String> outerExactRoles = new ArrayList<>();
71 private final List<String> outerRolePrefixes = new ArrayList<>();
72 private final List<String> innerExactRoles = new ArrayList<>();
73 private final List<String> innerRolePrefixes = new ArrayList<>();
74
75 private void initDefaults() {
76 outerExactRoles.clear();
77 outerRolePrefixes.clear();
78 innerExactRoles.clear();
79 innerRolePrefixes.clear();
80 outerExactRoles.add("outer");
81 innerExactRoles.add("inner");
82 }
83
84 private static void setNormalized(Collection<String> literals, List<String> target) {
85 target.clear();
86 for (String l: literals) {
87 if (l == null) {
88 continue;
89 }
90 l = l.trim();
91 if (!target.contains(l)) {
92 target.add(l);
93 }
94 }
95 }
96
97 private void initFromPreferences() {
98 initDefaults();
99 if (Config.getPref() == null) return;
100 Collection<String> literals;
101 literals = Config.getPref().getList(PREF_KEY_OUTER_ROLES);
102 if (literals != null && !literals.isEmpty()) {
103 setNormalized(literals, outerExactRoles);
104 }
105 literals = Config.getPref().getList(PREF_KEY_OUTER_ROLE_PREFIXES);
106 if (literals != null && !literals.isEmpty()) {
107 setNormalized(literals, outerRolePrefixes);
108 }
109 literals = Config.getPref().getList(PREF_KEY_INNER_ROLES);
110 if (literals != null && !literals.isEmpty()) {
111 setNormalized(literals, innerExactRoles);
112 }
113 literals = Config.getPref().getList(PREF_KEY_INNER_ROLE_PREFIXES);
114 if (literals != null && !literals.isEmpty()) {
115 setNormalized(literals, innerRolePrefixes);
116 }
117 }
118
119 @Override
120 public void preferenceChanged(PreferenceChangeEvent evt) {
121 if (PREF_KEY_INNER_ROLE_PREFIXES.equals(evt.getKey()) ||
122 PREF_KEY_INNER_ROLES.equals(evt.getKey()) ||
123 PREF_KEY_OUTER_ROLE_PREFIXES.equals(evt.getKey()) ||
124 PREF_KEY_OUTER_ROLES.equals(evt.getKey())) {
125 initFromPreferences();
126 }
127 }
128
129 boolean isOuterRole(String role) {
130 if (role == null) return false;
131 for (String candidate: outerExactRoles) {
132 if (role.equals(candidate)) return true;
133 }
134 for (String candidate: outerRolePrefixes) {
135 if (role.startsWith(candidate)) return true;
136 }
137 return false;
138 }
139
140 boolean isInnerRole(String role) {
141 if (role == null) return false;
142 for (String candidate: innerExactRoles) {
143 if (role.equals(candidate)) return true;
144 }
145 for (String candidate: innerRolePrefixes) {
146 if (role.startsWith(candidate)) return true;
147 }
148 return false;
149 }
150 }
151
152 /*
153 * Init a private global matcher object which will listen to preference changes.
154 */
155 private static MultipolygonRoleMatcher roleMatcher;
156
157 private static synchronized MultipolygonRoleMatcher getMultipolygonRoleMatcher() {
158 if (roleMatcher == null) {
159 roleMatcher = new MultipolygonRoleMatcher();
160 if (Config.getPref() != null) {
161 roleMatcher.initFromPreferences();
162 Config.getPref().addPreferenceChangeListener(roleMatcher);
163 }
164 }
165 return roleMatcher;
166 }
167
168 /**
169 * Class representing a string of ways.
170 *
171 * The last node of one way is the first way of the next one.
172 * The string may or may not be closed.
173 */
174 public static class JoinedWay {
175 protected final List<Node> nodes;
176 protected final Collection<Long> wayIds;
177 protected boolean selected;
178
179 /**
180 * Constructs a new {@code JoinedWay}.
181 * @param nodes list of nodes - must not be null
182 * @param wayIds list of way IDs - must not be null
183 * @param selected whether joined way is selected or not
184 */
185 public JoinedWay(List<Node> nodes, Collection<Long> wayIds, boolean selected) {
186 this.nodes = new ArrayList<>(nodes);
187 // see #17819
188 if (wayIds.size() == 1) {
189 this.wayIds = Collections.singleton(wayIds.iterator().next());
190 } else {
191 this.wayIds = wayIds.size() <= 10 ? new ArrayList<>(wayIds) : new HashSet<>(wayIds);
192 }
193 this.selected = selected;
194 }
195
196 /**
197 * Replies the list of nodes.
198 * @return the list of nodes
199 */
200 public List<Node> getNodes() {
201 return Collections.unmodifiableList(nodes);
202 }
203
204 /**
205 * Replies the list of way IDs.
206 * @return the list of way IDs
207 */
208 public Collection<Long> getWayIds() {
209 return Collections.unmodifiableCollection(wayIds);
210 }
211
212 /**
213 * Determines if this is selected.
214 * @return {@code true} if this is selected
215 */
216 public final boolean isSelected() {
217 return selected;
218 }
219
220 /**
221 * Sets whether this is selected
222 * @param selected {@code true} if this is selected
223 * @since 10312
224 */
225 public final void setSelected(boolean selected) {
226 this.selected = selected;
227 }
228
229 /**
230 * Determines if this joined way is closed.
231 * @return {@code true} if this joined way is closed
232 */
233 public boolean isClosed() {
234 return nodes.isEmpty() || getLastNode().equals(getFirstNode());
235 }
236
237 /**
238 * Returns the first node.
239 * @return the first node
240 * @since 10312
241 */
242 public Node getFirstNode() {
243 return nodes.get(0);
244 }
245
246 /**
247 * Returns the last node.
248 * @return the last node
249 * @since 10312
250 */
251 public Node getLastNode() {
252 return nodes.get(nodes.size() - 1);
253 }
254 }
255
256 /**
257 * The polygon data for a multipolygon part.
258 * It contains the outline of this polygon in east/north space.
259 */
260 public static class PolyData extends JoinedWay {
261 /**
262 * The intersection type used for {@link PolyData#contains(java.awt.geom.Path2D.Double)}
263 */
264 public enum Intersection {
265 /**
266 * The polygon is completely inside this PolyData
267 */
268 INSIDE,
269 /**
270 * The polygon is completely outside of this PolyData
271 */
272 OUTSIDE,
273 /**
274 * The polygon is partially inside and outside of this PolyData
275 */
276 CROSSING
277 }
278
279 private final Path2D.Double poly;
280 private Rectangle2D bounds;
281 private final List<PolyData> inners;
282
283 /**
284 * Constructs a new {@code PolyData} from a closed way.
285 * @param closedWay closed way
286 */
287 public PolyData(Way closedWay) {
288 this(closedWay.getNodes(), closedWay.isSelected(), Collections.singleton(closedWay.getUniqueId()));
289 }
290
291 /**
292 * Constructs a new {@code PolyData} from a {@link JoinedWay}.
293 * @param joinedWay joined way
294 */
295 public PolyData(JoinedWay joinedWay) {
296 this(joinedWay.nodes, joinedWay.selected, joinedWay.wayIds);
297 }
298
299 private PolyData(List<Node> nodes, boolean selected, Collection<Long> wayIds) {
300 super(nodes, wayIds, selected);
301 this.inners = new ArrayList<>();
302 this.poly = new Path2D.Double();
303 this.poly.setWindingRule(Path2D.WIND_EVEN_ODD);
304 buildPoly();
305 }
306
307 /**
308 * Constructs a new {@code PolyData} from an existing {@code PolyData}.
309 * @param copy existing instance
310 */
311 public PolyData(PolyData copy) {
312 super(copy.nodes, copy.wayIds, copy.selected);
313 this.poly = (Path2D.Double) copy.poly.clone();
314 this.inners = new ArrayList<>(copy.inners);
315 }
316
317 private void buildPoly() {
318 boolean initial = true;
319 for (Node n : nodes) {
320 EastNorth p = n.getEastNorth();
321 if (p != null) {
322 if (initial) {
323 poly.moveTo(p.getX(), p.getY());
324 initial = false;
325 } else {
326 poly.lineTo(p.getX(), p.getY());
327 }
328 }
329 }
330 if (nodes.size() >= 3 && nodes.get(0) == nodes.get(nodes.size() - 1)) {
331 poly.closePath();
332 }
333 for (PolyData inner : inners) {
334 appendInner(inner.poly);
335 }
336 }
337
338 /**
339 * Checks if this multipolygon contains or crosses an other polygon. This is a quick+lazy test which assumes
340 * that a polygon is inside when all points are inside. It will fail when the polygon encloses a hole or crosses
341 * the edges of poly so that both end points are inside poly (think of a square overlapping a U-shape).
342 * @param p The path to check. Needs to be in east/north space.
343 * @return a {@link Intersection} constant
344 */
345 public Intersection contains(Path2D.Double p) {
346 int contains = 0;
347 int total = 0;
348 double[] coords = new double[6];
349 for (PathIterator it = p.getPathIterator(null); !it.isDone(); it.next()) {
350 switch (it.currentSegment(coords)) {
351 case PathIterator.SEG_MOVETO:
352 case PathIterator.SEG_LINETO:
353 if (poly.contains(coords[0], coords[1])) {
354 contains++;
355 }
356 total++;
357 break;
358 default: // Do nothing
359 }
360 }
361 if (contains == total) return Intersection.INSIDE;
362 if (contains == 0) return Intersection.OUTSIDE;
363 return Intersection.CROSSING;
364 }
365
366 /**
367 * Adds an inner polygon
368 * @param inner The polygon to add as inner polygon.
369 */
370 public void addInner(PolyData inner) {
371 inners.add(inner);
372 appendInner(inner.poly);
373 }
374
375 private void appendInner(Path2D.Double inner) {
376 poly.append(inner.getPathIterator(null), false);
377 }
378
379 /**
380 * Gets the polygon outline and interior as java path
381 * @return The path in east/north space.
382 */
383 public Path2D.Double get() {
384 return poly;
385 }
386
387 /**
388 * Gets the bounds as {@link Rectangle2D} in east/north space.
389 * @return The bounds
390 */
391 public Rectangle2D getBounds() {
392 if (bounds == null) {
393 bounds = poly.getBounds2D();
394 }
395 return bounds;
396 }
397
398 /**
399 * Gets a list of all inner polygons.
400 * @return The inner polygons.
401 */
402 public List<PolyData> getInners() {
403 return Collections.unmodifiableList(inners);
404 }
405
406 private void resetNodes(DataSet dataSet) {
407 if (!nodes.isEmpty()) {
408 DataSet ds = dataSet;
409 // Find DataSet (can be null for several nodes when undoing nodes creation, see #7162)
410 for (Iterator<Node> it = nodes.iterator(); it.hasNext() && ds == null;) {
411 ds = it.next().getDataSet();
412 }
413 nodes.clear();
414 if (ds == null) {
415 // DataSet still not found. This should not happen, but a warning does no harm
416 Logging.warn("DataSet not found while resetting nodes in Multipolygon. " +
417 "This should not happen, you may report it to JOSM developers.");
418 } else if (wayIds.size() == 1) {
419 Way w = (Way) ds.getPrimitiveById(wayIds.iterator().next(), OsmPrimitiveType.WAY);
420 nodes.addAll(w.getNodes());
421 } else if (!wayIds.isEmpty()) {
422 List<Way> waysToJoin = new ArrayList<>();
423 for (Long wayId : wayIds) {
424 Way w = (Way) ds.getPrimitiveById(wayId, OsmPrimitiveType.WAY);
425 if (w != null && w.getNodesCount() > 0) { // fix #7173 (empty ways on purge)
426 waysToJoin.add(w);
427 }
428 }
429 if (!waysToJoin.isEmpty()) {
430 nodes.addAll(joinWays(waysToJoin).iterator().next().getNodes());
431 }
432 }
433 resetPoly();
434 }
435 }
436
437 private void resetPoly() {
438 poly.reset();
439 buildPoly();
440 bounds = null;
441 }
442
443 /**
444 * Check if this polygon was changed by a node move
445 * @param event The node move event
446 */
447 public void nodeMoved(NodeMovedEvent event) {
448 final Node n = event.getNode();
449 boolean innerChanged = false;
450 for (PolyData inner : inners) {
451 if (inner.nodes.contains(n)) {
452 inner.resetPoly();
453 innerChanged = true;
454 }
455 }
456 if (nodes.contains(n) || innerChanged) {
457 resetPoly();
458 }
459 }
460
461 /**
462 * Check if this polygon was affected by a way change
463 * @param event The way event
464 */
465 public void wayNodesChanged(WayNodesChangedEvent event) {
466 final Long wayId = event.getChangedWay().getUniqueId();
467 boolean innerChanged = false;
468 for (PolyData inner : inners) {
469 if (inner.wayIds.contains(wayId)) {
470 inner.resetNodes(event.getDataset());
471 innerChanged = true;
472 }
473 }
474 if (wayIds.contains(wayId) || innerChanged) {
475 resetNodes(event.getDataset());
476 }
477 }
478
479 @Override
480 public boolean isClosed() {
481 if (nodes.size() < 3 || !getFirstNode().equals(getLastNode()))
482 return false;
483 for (PolyData inner : inners) {
484 if (!inner.isClosed())
485 return false;
486 }
487 return true;
488 }
489
490 /**
491 * Calculate area and perimeter length in the given projection.
492 *
493 * @param projection the projection to use for the calculation, {@code null} defaults to {@link ProjectionRegistry#getProjection()}
494 * @return area and perimeter
495 */
496 public AreaAndPerimeter getAreaAndPerimeter(Projection projection) {
497 AreaAndPerimeter ap = Geometry.getAreaAndPerimeter(nodes, projection);
498 double area = ap.getArea();
499 double perimeter = ap.getPerimeter();
500 for (PolyData inner : inners) {
501 AreaAndPerimeter apInner = inner.getAreaAndPerimeter(projection);
502 area -= apInner.getArea();
503 perimeter += apInner.getPerimeter();
504 }
505 return new AreaAndPerimeter(area, perimeter);
506 }
507 }
508
509 private final List<Way> innerWays = new ArrayList<>();
510 private final List<Way> outerWays = new ArrayList<>();
511 private final List<PolyData> combinedPolygons = new ArrayList<>();
512 private final List<Node> openEnds = new ArrayList<>();
513
514 private boolean incomplete;
515
516 /**
517 * Constructs a new {@code Multipolygon} from a relation.
518 * @param r relation
519 */
520 public Multipolygon(Relation r) {
521 load(r);
522 }
523
524 private void load(Relation r) {
525 MultipolygonRoleMatcher matcher = getMultipolygonRoleMatcher();
526
527 // Fill inner and outer list with valid ways
528 for (RelationMember m : r.getMembers()) {
529 if (m.getMember().isIncomplete()) {
530 this.incomplete = true;
531 } else if (m.getMember().isDrawable() && m.isWay()) {
532 Way w = m.getWay();
533
534 if (w.getNodesCount() < 2) {
535 continue;
536 }
537
538 if (matcher.isInnerRole(m.getRole())) {
539 innerWays.add(w);
540 } else if (!m.hasRole() || matcher.isOuterRole(m.getRole())) {
541 outerWays.add(w);
542 } // Remaining roles ignored
543 } // Non ways ignored
544 }
545
546 final List<PolyData> innerPolygons = new ArrayList<>();
547 final List<PolyData> outerPolygons = new ArrayList<>();
548 createPolygons(innerWays, innerPolygons);
549 createPolygons(outerWays, outerPolygons);
550 if (!outerPolygons.isEmpty()) {
551 addInnerToOuters(innerPolygons, outerPolygons);
552 }
553 }
554
555 /**
556 * Determines if this multipolygon is incomplete.
557 * @return {@code true} if this multipolygon is incomplete
558 */
559 public final boolean isIncomplete() {
560 return incomplete;
561 }
562
563 private void createPolygons(List<Way> ways, List<PolyData> result) {
564 List<Way> waysToJoin = new ArrayList<>();
565 for (Way way: ways) {
566 if (way.isClosed()) {
567 result.add(new PolyData(way));
568 } else {
569 waysToJoin.add(way);
570 }
571 }
572
573 for (JoinedWay jw: joinWays(waysToJoin)) {
574 result.add(new PolyData(jw));
575 if (!jw.isClosed()) {
576 openEnds.add(jw.getFirstNode());
577 openEnds.add(jw.getLastNode());
578 }
579 }
580 }
581
582 /**
583 * Attempt to combine the ways in the list if they share common end nodes
584 * @param waysToJoin The ways to join
585 * @return A collection of {@link JoinedWay} objects indicating the possible join of those ways
586 */
587 public static Collection<JoinedWay> joinWays(Collection<Way> waysToJoin) {
588 final Collection<JoinedWay> result = new ArrayList<>();
589 final Way[] joinArray = waysToJoin.toArray(new Way[0]);
590 int left = waysToJoin.size();
591 while (left > 0) {
592 Way w = null;
593 boolean selected = false;
594 List<Node> nodes = null;
595 Set<Long> wayIds = new HashSet<>();
596 boolean joined = true;
597 while (joined && left > 0) {
598 joined = false;
599 for (int i = 0; i < joinArray.length && left != 0; ++i) {
600 if (joinArray[i] != null) {
601 Way c = joinArray[i];
602 if (c.getNodesCount() == 0) {
603 continue;
604 }
605 if (w == null) {
606 w = c;
607 selected = w.isSelected();
608 joinArray[i] = null;
609 --left;
610 } else {
611 int mode = 0;
612 int cl = c.getNodesCount()-1;
613 int nl;
614 if (nodes == null) {
615 nl = w.getNodesCount()-1;
616 if (w.getNode(nl) == c.getNode(0)) {
617 mode = 21;
618 } else if (w.getNode(nl) == c.getNode(cl)) {
619 mode = 22;
620 } else if (w.getNode(0) == c.getNode(0)) {
621 mode = 11;
622 } else if (w.getNode(0) == c.getNode(cl)) {
623 mode = 12;
624 }
625 } else {
626 nl = nodes.size()-1;
627 if (nodes.get(nl) == c.getNode(0)) {
628 mode = 21;
629 } else if (nodes.get(0) == c.getNode(cl)) {
630 mode = 12;
631 } else if (nodes.get(0) == c.getNode(0)) {
632 mode = 11;
633 } else if (nodes.get(nl) == c.getNode(cl)) {
634 mode = 22;
635 }
636 }
637 if (mode != 0) {
638 joinArray[i] = null;
639 joined = true;
640 if (c.isSelected()) {
641 selected = true;
642 }
643 --left;
644 if (nodes == null) {
645 nodes = new ArrayList<>(w.getNodes());
646 wayIds.add(w.getUniqueId());
647 }
648 if (mode == 21) {
649 nodes.addAll(c.getNodes().subList(1, cl + 1));
650 } else if (mode == 12) {
651 nodes.addAll(0, c.getNodes().subList(0, cl));
652 } else {
653 ArrayList<Node> reversed = new ArrayList<>(c.getNodes());
654 Collections.reverse(reversed);
655 if (mode == 22) {
656 nodes.addAll(reversed.subList(1, cl + 1));
657 } else /* mode == 11 */ {
658 nodes.addAll(0, reversed.subList(0, cl));
659 }
660 }
661 wayIds.add(c.getUniqueId());
662 }
663 }
664 }
665 }
666 }
667
668 if (nodes == null && w != null) {
669 nodes = w.getNodes();
670 wayIds.add(w.getUniqueId());
671 }
672
673 if (nodes != null) {
674 result.add(new JoinedWay(nodes, wayIds, selected));
675 }
676 }
677
678 return result;
679 }
680
681 /**
682 * Find a matching outer polygon for the inner one
683 * @param inner The inner polygon to search the outer for
684 * @param outerPolygons The possible outer polygons
685 * @return The outer polygon that was found or <code>null</code> if none was found.
686 */
687 public PolyData findOuterPolygon(PolyData inner, List<PolyData> outerPolygons) {
688 // First try to test only bbox, use precise testing only if we don't get unique result
689 Rectangle2D innerBox = inner.getBounds();
690 PolyData insidePolygon = null;
691 PolyData intersectingPolygon = null;
692 int insideCount = 0;
693 int intersectingCount = 0;
694
695 for (PolyData outer: outerPolygons) {
696 if (outer.getBounds().contains(innerBox)) {
697 insidePolygon = outer;
698 insideCount++;
699 } else if (outer.getBounds().intersects(innerBox)) {
700 intersectingPolygon = outer;
701 intersectingCount++;
702 }
703 }
704
705 if (insideCount == 1)
706 return insidePolygon;
707 else if (intersectingCount == 1)
708 return intersectingPolygon;
709
710 PolyData result = null;
711 for (PolyData combined : outerPolygons) {
712 if (combined.contains(inner.poly) != Intersection.OUTSIDE
713 && (result == null || result.contains(combined.poly) == Intersection.INSIDE)) {
714 result = combined;
715 }
716 }
717 return result;
718 }
719
720 private void addInnerToOuters(List<PolyData> innerPolygons, List<PolyData> outerPolygons) {
721 if (innerPolygons.isEmpty()) {
722 combinedPolygons.addAll(outerPolygons);
723 } else if (outerPolygons.size() == 1) {
724 PolyData combinedOuter = new PolyData(outerPolygons.get(0));
725 for (PolyData inner: innerPolygons) {
726 combinedOuter.addInner(inner);
727 }
728 combinedPolygons.add(combinedOuter);
729 } else {
730 for (PolyData outer: outerPolygons) {
731 combinedPolygons.add(new PolyData(outer));
732 }
733
734 for (PolyData pdInner: innerPolygons) {
735 Optional.ofNullable(findOuterPolygon(pdInner, combinedPolygons)).orElseGet(() -> outerPolygons.get(0))
736 .addInner(pdInner);
737 }
738 }
739 }
740
741 /**
742 * Replies the list of outer ways.
743 * @return the list of outer ways
744 */
745 public List<Way> getOuterWays() {
746 return Collections.unmodifiableList(outerWays);
747 }
748
749 /**
750 * Replies the list of inner ways.
751 * @return the list of inner ways
752 */
753 public List<Way> getInnerWays() {
754 return Collections.unmodifiableList(innerWays);
755 }
756
757 /**
758 * Replies the list of combined polygons.
759 * @return the list of combined polygons
760 */
761 public List<PolyData> getCombinedPolygons() {
762 return Collections.unmodifiableList(combinedPolygons);
763 }
764
765 /**
766 * Replies the list of inner polygons.
767 * @return the list of inner polygons
768 */
769 public List<PolyData> getInnerPolygons() {
770 final List<PolyData> innerPolygons = new ArrayList<>();
771 createPolygons(innerWays, innerPolygons);
772 return innerPolygons;
773 }
774
775 /**
776 * Replies the list of outer polygons.
777 * @return the list of outer polygons
778 */
779 public List<PolyData> getOuterPolygons() {
780 final List<PolyData> outerPolygons = new ArrayList<>();
781 createPolygons(outerWays, outerPolygons);
782 return outerPolygons;
783 }
784
785 /**
786 * Returns the start and end node of non-closed rings.
787 * @return the start and end node of non-closed rings.
788 */
789 public List<Node> getOpenEnds() {
790 return Collections.unmodifiableList(openEnds);
791 }
792}
Note: See TracBrowser for help on using the repository browser.