@Override public void visit(Way w) { if (!w.isUsable() || !w.isClosed()) return; String natural = w.get("natural"); if (natural == null) return; else if ("coastline".equals(natural) && Geometry.isClockwise(w)) { reportError(w, tr("Reversed coastline: land not on left side"), WRONGLY_ORDERED_COAST); } else if ("land".equals(natural) && Geometry.isClockwise(w)) { reportError(w, tr("Reversed land: land not on left side"), WRONGLY_ORDERED_LAND); } else return; }
/** * Search by closed status. * * @throws ParseError if an error has been encountered while compiling */ @Test public void testClosed() throws ParseError { SearchContext sc = new SearchContext("closed"); // Closed sc.w1.addNode(sc.n1); for (Way w : new Way[] {sc.w1}) { assertTrue(w.toString(), w.isClosed()); sc.match(w, true); } // Unclosed for (OsmPrimitive p : new OsmPrimitive[] {sc.n1, sc.n2, sc.w2, sc.r1, sc.r2}) { sc.match(p, false); } }
/** * Determines whether a way is oriented clockwise. * * <p>Internals: Assuming a closed non-looping way, compute twice the area of the polygon using * the formula {@code 2 * area = sum (X[n] * Y[n+1] - X[n+1] * Y[n])}. If the area is negative the * way is ordered in a clockwise direction. * * <p>See http://paulbourke.net/geometry/polyarea/ * * @param w the way to be checked. * @return true if and only if way is oriented clockwise. * @throws IllegalArgumentException if way is not closed (see {@link Way#isClosed}). */ public static boolean isClockwise(Way w) { if (!w.isClosed()) { throw new IllegalArgumentException("Way must be closed to check orientation."); } double area2 = 0.; int nodesCount = w.getNodesCount(); for (int node = 1; node <= /*sic! consider last-first as well*/ nodesCount; node++) { LatLon coorPrev = w.getNode(node - 1).getCoor(); LatLon coorCurr = w.getNode(node % nodesCount).getCoor(); area2 += coorPrev.lon() * coorCurr.lat(); area2 -= coorCurr.lon() * coorPrev.lat(); } return area2 < 0; }
/** * Determines if ways can be joined into a polygon. * * @param ways The ways collection to check * @return true if all ways can be joined into a polygon */ protected static boolean checkWaysArePolygon(Collection<Way> ways) { // For each way, nodes strictly between first and last should't be reference by an other way for (Way way : ways) { for (Node node : way.getNodes()) { if (way.isFirstLastNode(node)) continue; for (Way wayOther : ways) { if (way == wayOther) continue; if (node.getReferrers().contains(wayOther)) return false; } } } // Test if ways can be joined Way currentWay = null; Node startNode = null, endNode = null; int used = 0; while (true) { Way nextWay = null; for (Way w : ways) { if (w.isClosed()) return ways.size() == 1; if (w == currentWay) continue; if (currentWay == null) { nextWay = w; startNode = w.firstNode(); endNode = w.lastNode(); break; } if (w.firstNode() == endNode) { nextWay = w; endNode = w.lastNode(); break; } if (w.lastNode() == endNode) { nextWay = w; endNode = w.firstNode(); break; } } if (nextWay == null) return false; used += 1; currentWay = nextWay; if (endNode == startNode) return used == ways.size(); } }
/** * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway. * * @param ways The list of outer ways that belong to that multigon. * @return The newly created outer way */ private Way joinWays(List<WayInPolygon> ways) throws UserCancelException { // leave original orientation, if all paths are reverse. boolean allReverse = true; for (WayInPolygon way : ways) { allReverse &= !way.insideToTheRight; } if (allReverse) { for (WayInPolygon way : ways) { way.insideToTheRight = !way.insideToTheRight; } } Way joinedWay = joinOrientedWays(ways); // should not happen if (joinedWay == null || !joinedWay.isClosed()) throw new RuntimeException("Join areas internal error."); return joinedWay; }
/** * Gets called whenever the shortcut is pressed or the menu entry is selected Checks whether the * selected objects are suitable to join and joins them if so */ @Override public void actionPerformed(ActionEvent e) { LinkedList<Way> ways = new LinkedList<Way>(Main.main.getCurrentDataSet().getSelectedWays()); if (ways.isEmpty()) { new Notification(tr("Please select at least one closed way that should be joined.")) .setIcon(JOptionPane.INFORMATION_MESSAGE) .show(); return; } List<Node> allNodes = new ArrayList<Node>(); for (Way way : ways) { if (!way.isClosed()) { new Notification( tr("One of the selected ways is not closed and therefore cannot be joined.")) .setIcon(JOptionPane.INFORMATION_MESSAGE) .show(); return; } allNodes.addAll(way.getNodes()); } // TODO: Only display this warning when nodes outside dataSourceArea are deleted boolean ok = Command.checkAndConfirmOutlyingOperation( "joinarea", tr("Join area confirmation"), trn( "The selected way has nodes outside of the downloaded data region.", "The selected ways have nodes outside of the downloaded data region.", ways.size()) + "<br/>" + tr("This can lead to nodes being deleted accidentally.") + "<br/>" + tr("Are you really sure to continue?") + tr("Please abort if you are not sure"), tr("The selected area is incomplete. Continue?"), allNodes, null); if (!ok) return; // analyze multipolygon relations and collect all areas List<Multipolygon> areas = collectMultipolygons(ways); if (areas == null) // too complex multipolygon relations found return; if (!testJoin(areas)) { new Notification(tr("No intersection found. Nothing was changed.")) .setIcon(JOptionPane.INFORMATION_MESSAGE) .show(); return; } if (!resolveTagConflicts(areas)) return; // user canceled, do nothing. try { JoinAreasResult result = joinAreas(areas); if (result.hasChanges) { List<Way> allWays = new ArrayList<Way>(); for (Multipolygon pol : result.polygons) { allWays.add(pol.outerWay); allWays.addAll(pol.innerWays); } DataSet ds = Main.main.getCurrentDataSet(); ds.setSelected(allWays); Main.map.mapView.repaint(); } else { new Notification(tr("No intersection found. Nothing was changed.")) .setIcon(JOptionPane.INFORMATION_MESSAGE) .show(); } } catch (UserCancelException exception) { // revert changes // FIXME: this is dirty hack makeCommitsOneAction(tr("Reverting changes")); Main.main.undoRedo.undo(); Main.main.undoRedo.redoCommands.clear(); } }