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