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