001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.osm.visitor.paint.relations; 003 004import java.awt.geom.Path2D; 005import java.awt.geom.PathIterator; 006import java.awt.geom.Rectangle2D; 007import java.util.ArrayList; 008import java.util.Collection; 009import java.util.Collections; 010import java.util.HashSet; 011import java.util.Iterator; 012import java.util.List; 013import java.util.Optional; 014import java.util.Set; 015 016import org.openstreetmap.josm.data.coor.EastNorth; 017import org.openstreetmap.josm.data.osm.DataSet; 018import org.openstreetmap.josm.data.osm.Node; 019import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 020import org.openstreetmap.josm.data.osm.Relation; 021import org.openstreetmap.josm.data.osm.RelationMember; 022import org.openstreetmap.josm.data.osm.Way; 023import org.openstreetmap.josm.data.osm.event.NodeMovedEvent; 024import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent; 025import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection; 026import org.openstreetmap.josm.data.projection.Projection; 027import org.openstreetmap.josm.data.projection.ProjectionRegistry; 028import org.openstreetmap.josm.spi.preferences.Config; 029import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent; 030import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener; 031import org.openstreetmap.josm.tools.Geometry; 032import org.openstreetmap.josm.tools.Geometry.AreaAndPerimeter; 033import org.openstreetmap.josm.tools.Logging; 034 035/** 036 * Multipolygon data used to represent complex areas, see <a href="https://wiki.openstreetmap.org/wiki/Relation:multipolygon">wiki</a>. 037 * @since 2788 038 */ 039public class Multipolygon { 040 041 /** preference key for a collection of roles which indicate that the respective member belongs to an 042 * <em>outer</em> polygon. Default is <code>outer</code>. 043 */ 044 public static final String PREF_KEY_OUTER_ROLES = "mappaint.multipolygon.outer.roles"; 045 046 /** preference key for collection of role prefixes which indicate that the respective 047 * member belongs to an <em>outer</em> polygon. Default is empty. 048 */ 049 public static final String PREF_KEY_OUTER_ROLE_PREFIXES = "mappaint.multipolygon.outer.role-prefixes"; 050 051 /** preference key for a collection of roles which indicate that the respective member belongs to an 052 * <em>inner</em> polygon. Default is <code>inner</code>. 053 */ 054 public static final String PREF_KEY_INNER_ROLES = "mappaint.multipolygon.inner.roles"; 055 056 /** preference key for collection of role prefixes which indicate that the respective 057 * member belongs to an <em>inner</em> polygon. Default is empty. 058 */ 059 public static final String PREF_KEY_INNER_ROLE_PREFIXES = "mappaint.multipolygon.inner.role-prefixes"; 060 061 /** 062 * <p>Kind of strategy object which is responsible for deciding whether a given 063 * member role indicates that the member belongs to an <em>outer</em> or an 064 * <em>inner</em> polygon.</p> 065 * 066 * <p>The decision is taken based on preference settings, see the four preference keys 067 * above.</p> 068 */ 069 private static class MultipolygonRoleMatcher implements PreferenceChangedListener { 070 private final List<String> outerExactRoles = new ArrayList<>(); 071 private final List<String> outerRolePrefixes = new ArrayList<>(); 072 private final List<String> innerExactRoles = new ArrayList<>(); 073 private final List<String> innerRolePrefixes = new ArrayList<>(); 074 075 private void initDefaults() { 076 outerExactRoles.clear(); 077 outerRolePrefixes.clear(); 078 innerExactRoles.clear(); 079 innerRolePrefixes.clear(); 080 outerExactRoles.add("outer"); 081 innerExactRoles.add("inner"); 082 } 083 084 private static void setNormalized(Collection<String> literals, List<String> target) { 085 target.clear(); 086 for (String l: literals) { 087 if (l == null) { 088 continue; 089 } 090 l = l.trim(); 091 if (!target.contains(l)) { 092 target.add(l); 093 } 094 } 095 } 096 097 private void initFromPreferences() { 098 initDefaults(); 099 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 this.wayIds = new ArrayList<>(wayIds); 188 this.selected = selected; 189 } 190 191 /** 192 * Replies the list of nodes. 193 * @return the list of nodes 194 */ 195 public List<Node> getNodes() { 196 return Collections.unmodifiableList(nodes); 197 } 198 199 /** 200 * Replies the list of way IDs. 201 * @return the list of way IDs 202 */ 203 public Collection<Long> getWayIds() { 204 return Collections.unmodifiableCollection(wayIds); 205 } 206 207 /** 208 * Determines if this is selected. 209 * @return {@code true} if this is selected 210 */ 211 public final boolean isSelected() { 212 return selected; 213 } 214 215 /** 216 * Sets whether this is selected 217 * @param selected {@code true} if this is selected 218 * @since 10312 219 */ 220 public final void setSelected(boolean selected) { 221 this.selected = selected; 222 } 223 224 /** 225 * Determines if this joined way is closed. 226 * @return {@code true} if this joined way is closed 227 */ 228 public boolean isClosed() { 229 return nodes.isEmpty() || getLastNode().equals(getFirstNode()); 230 } 231 232 /** 233 * Returns the first node. 234 * @return the first node 235 * @since 10312 236 */ 237 public Node getFirstNode() { 238 return nodes.get(0); 239 } 240 241 /** 242 * Returns the last node. 243 * @return the last node 244 * @since 10312 245 */ 246 public Node getLastNode() { 247 return nodes.get(nodes.size() - 1); 248 } 249 } 250 251 /** 252 * The polygon data for a multipolygon part. 253 * It contains the outline of this polygon in east/north space. 254 */ 255 public static class PolyData extends JoinedWay { 256 /** 257 * The intersection type used for {@link PolyData#contains(java.awt.geom.Path2D.Double)} 258 */ 259 public enum Intersection { 260 /** 261 * The polygon is completely inside this PolyData 262 */ 263 INSIDE, 264 /** 265 * The polygon is completely outside of this PolyData 266 */ 267 OUTSIDE, 268 /** 269 * The polygon is partially inside and outside of this PolyData 270 */ 271 CROSSING 272 } 273 274 private final Path2D.Double poly; 275 private Rectangle2D bounds; 276 private final List<PolyData> inners; 277 278 /** 279 * Constructs a new {@code PolyData} from a closed way. 280 * @param closedWay closed way 281 */ 282 public PolyData(Way closedWay) { 283 this(closedWay.getNodes(), closedWay.isSelected(), Collections.singleton(closedWay.getUniqueId())); 284 } 285 286 /** 287 * Constructs a new {@code PolyData} from a {@link JoinedWay}. 288 * @param joinedWay joined way 289 */ 290 public PolyData(JoinedWay joinedWay) { 291 this(joinedWay.nodes, joinedWay.selected, joinedWay.wayIds); 292 } 293 294 private PolyData(List<Node> nodes, boolean selected, Collection<Long> wayIds) { 295 super(nodes, wayIds, selected); 296 this.inners = new ArrayList<>(); 297 this.poly = new Path2D.Double(); 298 this.poly.setWindingRule(Path2D.WIND_EVEN_ODD); 299 buildPoly(); 300 } 301 302 /** 303 * Constructs a new {@code PolyData} from an existing {@code PolyData}. 304 * @param copy existing instance 305 */ 306 public PolyData(PolyData copy) { 307 super(copy.nodes, copy.wayIds, copy.selected); 308 this.poly = (Path2D.Double) copy.poly.clone(); 309 this.inners = new ArrayList<>(copy.inners); 310 } 311 312 private void buildPoly() { 313 boolean initial = true; 314 for (Node n : nodes) { 315 EastNorth p = n.getEastNorth(); 316 if (p != null) { 317 if (initial) { 318 poly.moveTo(p.getX(), p.getY()); 319 initial = false; 320 } else { 321 poly.lineTo(p.getX(), p.getY()); 322 } 323 } 324 } 325 if (nodes.size() >= 3 && nodes.get(0) == nodes.get(nodes.size() - 1)) { 326 poly.closePath(); 327 } 328 for (PolyData inner : inners) { 329 appendInner(inner.poly); 330 } 331 } 332 333 /** 334 * Checks if this multipolygon contains or crosses an other polygon. This is a quick+lazy test which assumes 335 * that a polygon is inside when all points are inside. It will fail when the polygon encloses a hole or crosses 336 * the edges of poly so that both end points are inside poly (think of a square overlapping a U-shape). 337 * @param p The path to check. Needs to be in east/north space. 338 * @return a {@link Intersection} constant 339 */ 340 public Intersection contains(Path2D.Double p) { 341 int contains = 0; 342 int total = 0; 343 double[] coords = new double[6]; 344 for (PathIterator it = p.getPathIterator(null); !it.isDone(); it.next()) { 345 switch (it.currentSegment(coords)) { 346 case PathIterator.SEG_MOVETO: 347 case PathIterator.SEG_LINETO: 348 if (poly.contains(coords[0], coords[1])) { 349 contains++; 350 } 351 total++; 352 break; 353 default: // Do nothing 354 } 355 } 356 if (contains == total) return Intersection.INSIDE; 357 if (contains == 0) return Intersection.OUTSIDE; 358 return Intersection.CROSSING; 359 } 360 361 /** 362 * Adds an inner polygon 363 * @param inner The polygon to add as inner polygon. 364 */ 365 public void addInner(PolyData inner) { 366 inners.add(inner); 367 appendInner(inner.poly); 368 } 369 370 private void appendInner(Path2D.Double inner) { 371 poly.append(inner.getPathIterator(null), false); 372 } 373 374 /** 375 * Gets the polygon outline and interior as java path 376 * @return The path in east/north space. 377 */ 378 public Path2D.Double get() { 379 return poly; 380 } 381 382 /** 383 * Gets the bounds as {@link Rectangle2D} in east/north space. 384 * @return The bounds 385 */ 386 public Rectangle2D getBounds() { 387 if (bounds == null) { 388 bounds = poly.getBounds2D(); 389 } 390 return bounds; 391 } 392 393 /** 394 * Gets a list of all inner polygons. 395 * @return The inner polygons. 396 */ 397 public List<PolyData> getInners() { 398 return Collections.unmodifiableList(inners); 399 } 400 401 private void resetNodes(DataSet dataSet) { 402 if (!nodes.isEmpty()) { 403 DataSet ds = dataSet; 404 // Find DataSet (can be null for several nodes when undoing nodes creation, see #7162) 405 for (Iterator<Node> it = nodes.iterator(); it.hasNext() && ds == null;) { 406 ds = it.next().getDataSet(); 407 } 408 nodes.clear(); 409 if (ds == null) { 410 // DataSet still not found. This should not happen, but a warning does no harm 411 Logging.warn("DataSet not found while resetting nodes in Multipolygon. " + 412 "This should not happen, you may report it to JOSM developers."); 413 } else if (wayIds.size() == 1) { 414 Way w = (Way) ds.getPrimitiveById(wayIds.iterator().next(), OsmPrimitiveType.WAY); 415 nodes.addAll(w.getNodes()); 416 } else if (!wayIds.isEmpty()) { 417 List<Way> waysToJoin = new ArrayList<>(); 418 for (Long wayId : wayIds) { 419 Way w = (Way) ds.getPrimitiveById(wayId, OsmPrimitiveType.WAY); 420 if (w != null && w.getNodesCount() > 0) { // fix #7173 (empty ways on purge) 421 waysToJoin.add(w); 422 } 423 } 424 if (!waysToJoin.isEmpty()) { 425 nodes.addAll(joinWays(waysToJoin).iterator().next().getNodes()); 426 } 427 } 428 resetPoly(); 429 } 430 } 431 432 private void resetPoly() { 433 poly.reset(); 434 buildPoly(); 435 bounds = null; 436 } 437 438 /** 439 * Check if this polygon was changed by a node move 440 * @param event The node move event 441 */ 442 public void nodeMoved(NodeMovedEvent event) { 443 final Node n = event.getNode(); 444 boolean innerChanged = false; 445 for (PolyData inner : inners) { 446 if (inner.nodes.contains(n)) { 447 inner.resetPoly(); 448 innerChanged = true; 449 } 450 } 451 if (nodes.contains(n) || innerChanged) { 452 resetPoly(); 453 } 454 } 455 456 /** 457 * Check if this polygon was affected by a way change 458 * @param event The way event 459 */ 460 public void wayNodesChanged(WayNodesChangedEvent event) { 461 final Long wayId = event.getChangedWay().getUniqueId(); 462 boolean innerChanged = false; 463 for (PolyData inner : inners) { 464 if (inner.wayIds.contains(wayId)) { 465 inner.resetNodes(event.getDataset()); 466 innerChanged = true; 467 } 468 } 469 if (wayIds.contains(wayId) || innerChanged) { 470 resetNodes(event.getDataset()); 471 } 472 } 473 474 @Override 475 public boolean isClosed() { 476 if (nodes.size() < 3 || !getFirstNode().equals(getLastNode())) 477 return false; 478 for (PolyData inner : inners) { 479 if (!inner.isClosed()) 480 return false; 481 } 482 return true; 483 } 484 485 /** 486 * Calculate area and perimeter length in the given projection. 487 * 488 * @param projection the projection to use for the calculation, {@code null} defaults to {@link ProjectionRegistry#getProjection()} 489 * @return area and perimeter 490 */ 491 public AreaAndPerimeter getAreaAndPerimeter(Projection projection) { 492 AreaAndPerimeter ap = Geometry.getAreaAndPerimeter(nodes, projection); 493 double area = ap.getArea(); 494 double perimeter = ap.getPerimeter(); 495 for (PolyData inner : inners) { 496 AreaAndPerimeter apInner = inner.getAreaAndPerimeter(projection); 497 area -= apInner.getArea(); 498 perimeter += apInner.getPerimeter(); 499 } 500 return new AreaAndPerimeter(area, perimeter); 501 } 502 } 503 504 private final List<Way> innerWays = new ArrayList<>(); 505 private final List<Way> outerWays = new ArrayList<>(); 506 private final List<PolyData> combinedPolygons = new ArrayList<>(); 507 private final List<Node> openEnds = new ArrayList<>(); 508 509 private boolean incomplete; 510 511 /** 512 * Constructs a new {@code Multipolygon} from a relation. 513 * @param r relation 514 */ 515 public Multipolygon(Relation r) { 516 load(r); 517 } 518 519 private void load(Relation r) { 520 MultipolygonRoleMatcher matcher = getMultipolygonRoleMatcher(); 521 522 // Fill inner and outer list with valid ways 523 for (RelationMember m : r.getMembers()) { 524 if (m.getMember().isIncomplete()) { 525 this.incomplete = true; 526 } else if (m.getMember().isDrawable() && m.isWay()) { 527 Way w = m.getWay(); 528 529 if (w.getNodesCount() < 2) { 530 continue; 531 } 532 533 if (matcher.isInnerRole(m.getRole())) { 534 innerWays.add(w); 535 } else if (!m.hasRole() || matcher.isOuterRole(m.getRole())) { 536 outerWays.add(w); 537 } // Remaining roles ignored 538 } // Non ways ignored 539 } 540 541 final List<PolyData> innerPolygons = new ArrayList<>(); 542 final List<PolyData> outerPolygons = new ArrayList<>(); 543 createPolygons(innerWays, innerPolygons); 544 createPolygons(outerWays, outerPolygons); 545 if (!outerPolygons.isEmpty()) { 546 addInnerToOuters(innerPolygons, outerPolygons); 547 } 548 } 549 550 /** 551 * Determines if this multipolygon is incomplete. 552 * @return {@code true} if this multipolygon is incomplete 553 */ 554 public final boolean isIncomplete() { 555 return incomplete; 556 } 557 558 private void createPolygons(List<Way> ways, List<PolyData> result) { 559 List<Way> waysToJoin = new ArrayList<>(); 560 for (Way way: ways) { 561 if (way.isClosed()) { 562 result.add(new PolyData(way)); 563 } else { 564 waysToJoin.add(way); 565 } 566 } 567 568 for (JoinedWay jw: joinWays(waysToJoin)) { 569 result.add(new PolyData(jw)); 570 if (!jw.isClosed()) { 571 openEnds.add(jw.getFirstNode()); 572 openEnds.add(jw.getLastNode()); 573 } 574 } 575 } 576 577 /** 578 * Attempt to combine the ways in the list if they share common end nodes 579 * @param waysToJoin The ways to join 580 * @return A collection of {@link JoinedWay} objects indicating the possible join of those ways 581 */ 582 public static Collection<JoinedWay> joinWays(Collection<Way> waysToJoin) { 583 final Collection<JoinedWay> result = new ArrayList<>(); 584 final Way[] joinArray = waysToJoin.toArray(new Way[0]); 585 int left = waysToJoin.size(); 586 while (left > 0) { 587 Way w = null; 588 boolean selected = false; 589 List<Node> nodes = null; 590 Set<Long> wayIds = new HashSet<>(); 591 boolean joined = true; 592 while (joined && left > 0) { 593 joined = false; 594 for (int i = 0; i < joinArray.length && left != 0; ++i) { 595 if (joinArray[i] != null) { 596 Way c = joinArray[i]; 597 if (c.getNodesCount() == 0) { 598 continue; 599 } 600 if (w == null) { 601 w = c; 602 selected = w.isSelected(); 603 joinArray[i] = null; 604 --left; 605 } else { 606 int mode = 0; 607 int cl = c.getNodesCount()-1; 608 int nl; 609 if (nodes == null) { 610 nl = w.getNodesCount()-1; 611 if (w.getNode(nl) == c.getNode(0)) { 612 mode = 21; 613 } else if (w.getNode(nl) == c.getNode(cl)) { 614 mode = 22; 615 } else if (w.getNode(0) == c.getNode(0)) { 616 mode = 11; 617 } else if (w.getNode(0) == c.getNode(cl)) { 618 mode = 12; 619 } 620 } else { 621 nl = nodes.size()-1; 622 if (nodes.get(nl) == c.getNode(0)) { 623 mode = 21; 624 } else if (nodes.get(0) == c.getNode(cl)) { 625 mode = 12; 626 } else if (nodes.get(0) == c.getNode(0)) { 627 mode = 11; 628 } else if (nodes.get(nl) == c.getNode(cl)) { 629 mode = 22; 630 } 631 } 632 if (mode != 0) { 633 joinArray[i] = null; 634 joined = true; 635 if (c.isSelected()) { 636 selected = true; 637 } 638 --left; 639 if (nodes == null) { 640 nodes = w.getNodes(); 641 wayIds.add(w.getUniqueId()); 642 } 643 nodes.remove((mode == 21 || mode == 22) ? nl : 0); 644 if (mode == 21) { 645 nodes.addAll(c.getNodes()); 646 } else if (mode == 12) { 647 nodes.addAll(0, c.getNodes()); 648 } else if (mode == 22) { 649 for (Node node : c.getNodes()) { 650 nodes.add(nl, node); 651 } 652 } else /* mode == 11 */ { 653 for (Node node : c.getNodes()) { 654 nodes.add(0, node); 655 } 656 } 657 wayIds.add(c.getUniqueId()); 658 } 659 } 660 } 661 } 662 } 663 664 if (nodes == null && w != null) { 665 nodes = w.getNodes(); 666 wayIds.add(w.getUniqueId()); 667 } 668 669 if (nodes != null) { 670 result.add(new JoinedWay(nodes, wayIds, selected)); 671 } 672 } 673 674 return result; 675 } 676 677 /** 678 * Find a matching outer polygon for the inner one 679 * @param inner The inner polygon to search the outer for 680 * @param outerPolygons The possible outer polygons 681 * @return The outer polygon that was found or <code>null</code> if none was found. 682 */ 683 public PolyData findOuterPolygon(PolyData inner, List<PolyData> outerPolygons) { 684 // First try to test only bbox, use precise testing only if we don't get unique result 685 Rectangle2D innerBox = inner.getBounds(); 686 PolyData insidePolygon = null; 687 PolyData intersectingPolygon = null; 688 int insideCount = 0; 689 int intersectingCount = 0; 690 691 for (PolyData outer: outerPolygons) { 692 if (outer.getBounds().contains(innerBox)) { 693 insidePolygon = outer; 694 insideCount++; 695 } else if (outer.getBounds().intersects(innerBox)) { 696 intersectingPolygon = outer; 697 intersectingCount++; 698 } 699 } 700 701 if (insideCount == 1) 702 return insidePolygon; 703 else if (intersectingCount == 1) 704 return intersectingPolygon; 705 706 PolyData result = null; 707 for (PolyData combined : outerPolygons) { 708 if (combined.contains(inner.poly) != Intersection.OUTSIDE 709 && (result == null || result.contains(combined.poly) == Intersection.INSIDE)) { 710 result = combined; 711 } 712 } 713 return result; 714 } 715 716 private void addInnerToOuters(List<PolyData> innerPolygons, List<PolyData> outerPolygons) { 717 if (innerPolygons.isEmpty()) { 718 combinedPolygons.addAll(outerPolygons); 719 } else if (outerPolygons.size() == 1) { 720 PolyData combinedOuter = new PolyData(outerPolygons.get(0)); 721 for (PolyData inner: innerPolygons) { 722 combinedOuter.addInner(inner); 723 } 724 combinedPolygons.add(combinedOuter); 725 } else { 726 for (PolyData outer: outerPolygons) { 727 combinedPolygons.add(new PolyData(outer)); 728 } 729 730 for (PolyData pdInner: innerPolygons) { 731 Optional.ofNullable(findOuterPolygon(pdInner, combinedPolygons)).orElseGet(() -> outerPolygons.get(0)) 732 .addInner(pdInner); 733 } 734 } 735 } 736 737 /** 738 * Replies the list of outer ways. 739 * @return the list of outer ways 740 */ 741 public List<Way> getOuterWays() { 742 return Collections.unmodifiableList(outerWays); 743 } 744 745 /** 746 * Replies the list of inner ways. 747 * @return the list of inner ways 748 */ 749 public List<Way> getInnerWays() { 750 return Collections.unmodifiableList(innerWays); 751 } 752 753 /** 754 * Replies the list of combined polygons. 755 * @return the list of combined polygons 756 */ 757 public List<PolyData> getCombinedPolygons() { 758 return Collections.unmodifiableList(combinedPolygons); 759 } 760 761 /** 762 * Replies the list of inner polygons. 763 * @return the list of inner polygons 764 */ 765 public List<PolyData> getInnerPolygons() { 766 final List<PolyData> innerPolygons = new ArrayList<>(); 767 createPolygons(innerWays, innerPolygons); 768 return innerPolygons; 769 } 770 771 /** 772 * Replies the list of outer polygons. 773 * @return the list of outer polygons 774 */ 775 public List<PolyData> getOuterPolygons() { 776 final List<PolyData> outerPolygons = new ArrayList<>(); 777 createPolygons(outerWays, outerPolygons); 778 return outerPolygons; 779 } 780 781 /** 782 * Returns the start and end node of non-closed rings. 783 * @return the start and end node of non-closed rings. 784 */ 785 public List<Node> getOpenEnds() { 786 return Collections.unmodifiableList(openEnds); 787 } 788}