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.Main; 017import org.openstreetmap.josm.data.coor.EastNorth; 018import org.openstreetmap.josm.data.osm.DataSet; 019import org.openstreetmap.josm.data.osm.Node; 020import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 021import org.openstreetmap.josm.data.osm.Relation; 022import org.openstreetmap.josm.data.osm.RelationMember; 023import org.openstreetmap.josm.data.osm.Way; 024import org.openstreetmap.josm.data.osm.event.NodeMovedEvent; 025import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent; 026import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection; 027import org.openstreetmap.josm.data.projection.Projection; 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 335 * @param p The path to check. Needs to be in east/north space. 336 * @return a {@link Intersection} constant 337 */ 338 public Intersection contains(Path2D.Double p) { 339 int contains = 0; 340 int total = 0; 341 double[] coords = new double[6]; 342 for (PathIterator it = p.getPathIterator(null); !it.isDone(); it.next()) { 343 switch (it.currentSegment(coords)) { 344 case PathIterator.SEG_MOVETO: 345 case PathIterator.SEG_LINETO: 346 if (poly.contains(coords[0], coords[1])) { 347 contains++; 348 } 349 total++; 350 break; 351 default: // Do nothing 352 } 353 } 354 if (contains == total) return Intersection.INSIDE; 355 if (contains == 0) return Intersection.OUTSIDE; 356 return Intersection.CROSSING; 357 } 358 359 /** 360 * Adds an inner polygon 361 * @param inner The polygon to add as inner polygon. 362 */ 363 public void addInner(PolyData inner) { 364 inners.add(inner); 365 appendInner(inner.poly); 366 } 367 368 private void appendInner(Path2D.Double inner) { 369 poly.append(inner.getPathIterator(null), false); 370 } 371 372 /** 373 * Gets the polygon outline and interior as java path 374 * @return The path in east/north space. 375 */ 376 public Path2D.Double get() { 377 return poly; 378 } 379 380 /** 381 * Gets the bounds as {@link Rectangle2D} in east/north space. 382 * @return The bounds 383 */ 384 public Rectangle2D getBounds() { 385 if (bounds == null) { 386 bounds = poly.getBounds2D(); 387 } 388 return bounds; 389 } 390 391 /** 392 * Gets a list of all inner polygons. 393 * @return The inner polygons. 394 */ 395 public List<PolyData> getInners() { 396 return Collections.unmodifiableList(inners); 397 } 398 399 private void resetNodes(DataSet dataSet) { 400 if (!nodes.isEmpty()) { 401 DataSet ds = dataSet; 402 // Find DataSet (can be null for several nodes when undoing nodes creation, see #7162) 403 for (Iterator<Node> it = nodes.iterator(); it.hasNext() && ds == null;) { 404 ds = it.next().getDataSet(); 405 } 406 nodes.clear(); 407 if (ds == null) { 408 // DataSet still not found. This should not happen, but a warning does no harm 409 Logging.warn("DataSet not found while resetting nodes in Multipolygon. " + 410 "This should not happen, you may report it to JOSM developers."); 411 } else if (wayIds.size() == 1) { 412 Way w = (Way) ds.getPrimitiveById(wayIds.iterator().next(), OsmPrimitiveType.WAY); 413 nodes.addAll(w.getNodes()); 414 } else if (!wayIds.isEmpty()) { 415 List<Way> waysToJoin = new ArrayList<>(); 416 for (Long wayId : wayIds) { 417 Way w = (Way) ds.getPrimitiveById(wayId, OsmPrimitiveType.WAY); 418 if (w != null && w.getNodesCount() > 0) { // fix #7173 (empty ways on purge) 419 waysToJoin.add(w); 420 } 421 } 422 if (!waysToJoin.isEmpty()) { 423 nodes.addAll(joinWays(waysToJoin).iterator().next().getNodes()); 424 } 425 } 426 resetPoly(); 427 } 428 } 429 430 private void resetPoly() { 431 poly.reset(); 432 buildPoly(); 433 bounds = null; 434 } 435 436 /** 437 * Check if this polygon was changed by a node move 438 * @param event The node move event 439 */ 440 public void nodeMoved(NodeMovedEvent event) { 441 final Node n = event.getNode(); 442 boolean innerChanged = false; 443 for (PolyData inner : inners) { 444 if (inner.nodes.contains(n)) { 445 inner.resetPoly(); 446 innerChanged = true; 447 } 448 } 449 if (nodes.contains(n) || innerChanged) { 450 resetPoly(); 451 } 452 } 453 454 /** 455 * Check if this polygon was affected by a way change 456 * @param event The way event 457 */ 458 public void wayNodesChanged(WayNodesChangedEvent event) { 459 final Long wayId = event.getChangedWay().getUniqueId(); 460 boolean innerChanged = false; 461 for (PolyData inner : inners) { 462 if (inner.wayIds.contains(wayId)) { 463 inner.resetNodes(event.getDataset()); 464 innerChanged = true; 465 } 466 } 467 if (wayIds.contains(wayId) || innerChanged) { 468 resetNodes(event.getDataset()); 469 } 470 } 471 472 @Override 473 public boolean isClosed() { 474 if (nodes.size() < 3 || !getFirstNode().equals(getLastNode())) 475 return false; 476 for (PolyData inner : inners) { 477 if (!inner.isClosed()) 478 return false; 479 } 480 return true; 481 } 482 483 /** 484 * Calculate area and perimeter length in the given projection. 485 * 486 * @param projection the projection to use for the calculation, {@code null} defaults to {@link Main#getProjection()} 487 * @return area and perimeter 488 */ 489 public AreaAndPerimeter getAreaAndPerimeter(Projection projection) { 490 AreaAndPerimeter ap = Geometry.getAreaAndPerimeter(nodes, projection); 491 double area = ap.getArea(); 492 double perimeter = ap.getPerimeter(); 493 for (PolyData inner : inners) { 494 AreaAndPerimeter apInner = inner.getAreaAndPerimeter(projection); 495 area -= apInner.getArea(); 496 perimeter += apInner.getPerimeter(); 497 } 498 return new AreaAndPerimeter(area, perimeter); 499 } 500 } 501 502 private final List<Way> innerWays = new ArrayList<>(); 503 private final List<Way> outerWays = new ArrayList<>(); 504 private final List<PolyData> combinedPolygons = new ArrayList<>(); 505 private final List<Node> openEnds = new ArrayList<>(); 506 507 private boolean incomplete; 508 509 /** 510 * Constructs a new {@code Multipolygon} from a relation. 511 * @param r relation 512 */ 513 public Multipolygon(Relation r) { 514 load(r); 515 } 516 517 private void load(Relation r) { 518 MultipolygonRoleMatcher matcher = getMultipolygonRoleMatcher(); 519 520 // Fill inner and outer list with valid ways 521 for (RelationMember m : r.getMembers()) { 522 if (m.getMember().isIncomplete()) { 523 this.incomplete = true; 524 } else if (m.getMember().isDrawable() && m.isWay()) { 525 Way w = m.getWay(); 526 527 if (w.getNodesCount() < 2) { 528 continue; 529 } 530 531 if (matcher.isInnerRole(m.getRole())) { 532 innerWays.add(w); 533 } else if (!m.hasRole() || matcher.isOuterRole(m.getRole())) { 534 outerWays.add(w); 535 } // Remaining roles ignored 536 } // Non ways ignored 537 } 538 539 final List<PolyData> innerPolygons = new ArrayList<>(); 540 final List<PolyData> outerPolygons = new ArrayList<>(); 541 createPolygons(innerWays, innerPolygons); 542 createPolygons(outerWays, outerPolygons); 543 if (!outerPolygons.isEmpty()) { 544 addInnerToOuters(innerPolygons, outerPolygons); 545 } 546 } 547 548 /** 549 * Determines if this multipolygon is incomplete. 550 * @return {@code true} if this multipolygon is incomplete 551 */ 552 public final boolean isIncomplete() { 553 return incomplete; 554 } 555 556 private void createPolygons(List<Way> ways, List<PolyData> result) { 557 List<Way> waysToJoin = new ArrayList<>(); 558 for (Way way: ways) { 559 if (way.isClosed()) { 560 result.add(new PolyData(way)); 561 } else { 562 waysToJoin.add(way); 563 } 564 } 565 566 for (JoinedWay jw: joinWays(waysToJoin)) { 567 result.add(new PolyData(jw)); 568 if (!jw.isClosed()) { 569 openEnds.add(jw.getFirstNode()); 570 openEnds.add(jw.getLastNode()); 571 } 572 } 573 } 574 575 /** 576 * Attempt to combine the ways in the list if they share common end nodes 577 * @param waysToJoin The ways to join 578 * @return A collection of {@link JoinedWay} objects indicating the possible join of those ways 579 */ 580 public static Collection<JoinedWay> joinWays(Collection<Way> waysToJoin) { 581 final Collection<JoinedWay> result = new ArrayList<>(); 582 final Way[] joinArray = waysToJoin.toArray(new Way[0]); 583 int left = waysToJoin.size(); 584 while (left > 0) { 585 Way w = null; 586 boolean selected = false; 587 List<Node> nodes = null; 588 Set<Long> wayIds = new HashSet<>(); 589 boolean joined = true; 590 while (joined && left > 0) { 591 joined = false; 592 for (int i = 0; i < joinArray.length && left != 0; ++i) { 593 if (joinArray[i] != null) { 594 Way c = joinArray[i]; 595 if (c.getNodesCount() == 0) { 596 continue; 597 } 598 if (w == null) { 599 w = c; 600 selected = w.isSelected(); 601 joinArray[i] = null; 602 --left; 603 } else { 604 int mode = 0; 605 int cl = c.getNodesCount()-1; 606 int nl; 607 if (nodes == null) { 608 nl = w.getNodesCount()-1; 609 if (w.getNode(nl) == c.getNode(0)) { 610 mode = 21; 611 } else if (w.getNode(nl) == c.getNode(cl)) { 612 mode = 22; 613 } else if (w.getNode(0) == c.getNode(0)) { 614 mode = 11; 615 } else if (w.getNode(0) == c.getNode(cl)) { 616 mode = 12; 617 } 618 } else { 619 nl = nodes.size()-1; 620 if (nodes.get(nl) == c.getNode(0)) { 621 mode = 21; 622 } else if (nodes.get(0) == c.getNode(cl)) { 623 mode = 12; 624 } else if (nodes.get(0) == c.getNode(0)) { 625 mode = 11; 626 } else if (nodes.get(nl) == c.getNode(cl)) { 627 mode = 22; 628 } 629 } 630 if (mode != 0) { 631 joinArray[i] = null; 632 joined = true; 633 if (c.isSelected()) { 634 selected = true; 635 } 636 --left; 637 if (nodes == null) { 638 nodes = w.getNodes(); 639 wayIds.add(w.getUniqueId()); 640 } 641 nodes.remove((mode == 21 || mode == 22) ? nl : 0); 642 if (mode == 21) { 643 nodes.addAll(c.getNodes()); 644 } else if (mode == 12) { 645 nodes.addAll(0, c.getNodes()); 646 } else if (mode == 22) { 647 for (Node node : c.getNodes()) { 648 nodes.add(nl, node); 649 } 650 } else /* mode == 11 */ { 651 for (Node node : c.getNodes()) { 652 nodes.add(0, node); 653 } 654 } 655 wayIds.add(c.getUniqueId()); 656 } 657 } 658 } 659 } 660 } 661 662 if (nodes == null && w != null) { 663 nodes = w.getNodes(); 664 wayIds.add(w.getUniqueId()); 665 } 666 667 if (nodes != null) { 668 result.add(new JoinedWay(nodes, wayIds, selected)); 669 } 670 } 671 672 return result; 673 } 674 675 /** 676 * Find a matching outer polygon for the inner one 677 * @param inner The inner polygon to search the outer for 678 * @param outerPolygons The possible outer polygons 679 * @return The outer polygon that was found or <code>null</code> if none was found. 680 */ 681 public PolyData findOuterPolygon(PolyData inner, List<PolyData> outerPolygons) { 682 // First try to test only bbox, use precise testing only if we don't get unique result 683 Rectangle2D innerBox = inner.getBounds(); 684 PolyData insidePolygon = null; 685 PolyData intersectingPolygon = null; 686 int insideCount = 0; 687 int intersectingCount = 0; 688 689 for (PolyData outer: outerPolygons) { 690 if (outer.getBounds().contains(innerBox)) { 691 insidePolygon = outer; 692 insideCount++; 693 } else if (outer.getBounds().intersects(innerBox)) { 694 intersectingPolygon = outer; 695 intersectingCount++; 696 } 697 } 698 699 if (insideCount == 1) 700 return insidePolygon; 701 else if (intersectingCount == 1) 702 return intersectingPolygon; 703 704 PolyData result = null; 705 for (PolyData combined : outerPolygons) { 706 if (combined.contains(inner.poly) != Intersection.OUTSIDE 707 && (result == null || result.contains(combined.poly) == Intersection.INSIDE)) { 708 result = combined; 709 } 710 } 711 return result; 712 } 713 714 private void addInnerToOuters(List<PolyData> innerPolygons, List<PolyData> outerPolygons) { 715 if (innerPolygons.isEmpty()) { 716 combinedPolygons.addAll(outerPolygons); 717 } else if (outerPolygons.size() == 1) { 718 PolyData combinedOuter = new PolyData(outerPolygons.get(0)); 719 for (PolyData inner: innerPolygons) { 720 combinedOuter.addInner(inner); 721 } 722 combinedPolygons.add(combinedOuter); 723 } else { 724 for (PolyData outer: outerPolygons) { 725 combinedPolygons.add(new PolyData(outer)); 726 } 727 728 for (PolyData pdInner: innerPolygons) { 729 Optional.ofNullable(findOuterPolygon(pdInner, combinedPolygons)).orElseGet(() -> outerPolygons.get(0)) 730 .addInner(pdInner); 731 } 732 } 733 } 734 735 /** 736 * Replies the list of outer ways. 737 * @return the list of outer ways 738 */ 739 public List<Way> getOuterWays() { 740 return Collections.unmodifiableList(outerWays); 741 } 742 743 /** 744 * Replies the list of inner ways. 745 * @return the list of inner ways 746 */ 747 public List<Way> getInnerWays() { 748 return Collections.unmodifiableList(innerWays); 749 } 750 751 /** 752 * Replies the list of combined polygons. 753 * @return the list of combined polygons 754 */ 755 public List<PolyData> getCombinedPolygons() { 756 return Collections.unmodifiableList(combinedPolygons); 757 } 758 759 /** 760 * Replies the list of inner polygons. 761 * @return the list of inner polygons 762 */ 763 public List<PolyData> getInnerPolygons() { 764 final List<PolyData> innerPolygons = new ArrayList<>(); 765 createPolygons(innerWays, innerPolygons); 766 return innerPolygons; 767 } 768 769 /** 770 * Replies the list of outer polygons. 771 * @return the list of outer polygons 772 */ 773 public List<PolyData> getOuterPolygons() { 774 final List<PolyData> outerPolygons = new ArrayList<>(); 775 createPolygons(outerWays, outerPolygons); 776 return outerPolygons; 777 } 778 779 /** 780 * Returns the start and end node of non-closed rings. 781 * @return the start and end node of non-closed rings. 782 */ 783 public List<Node> getOpenEnds() { 784 return Collections.unmodifiableList(openEnds); 785 } 786}