=== added file 'images/alignparallel.png' Binary files images/alignparallel.png 1970-01-01 00:00:00 +0000 and images/alignparallel.png 2008-08-30 23:01:27 +0000 differ === added file 'src/org/openstreetmap/josm/actions/AlignNodesStraightAndParallel.java' --- src/org/openstreetmap/josm/actions/AlignNodesStraightAndParallel.java 1970-01-01 00:00:00 +0000 +++ src/org/openstreetmap/josm/actions/AlignNodesStraightAndParallel.java 2008-08-30 23:46:11 +0000 @@ -0,0 +1,193 @@ +// AlignWaysStraightAndParallel.java + +// Copyright (C) 2008 Aled Morris +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package org.openstreetmap.josm.actions; + +import static org.openstreetmap.josm.tools.I18n.tr; + +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; + +import javax.swing.JOptionPane; + +import org.openstreetmap.josm.Main; +import org.openstreetmap.josm.command.Command; +import org.openstreetmap.josm.command.MoveCommand; +import org.openstreetmap.josm.command.SequenceCommand; +import org.openstreetmap.josm.data.Bearing; +import org.openstreetmap.josm.data.osm.Node; +import org.openstreetmap.josm.data.osm.OsmPrimitive; +import org.openstreetmap.josm.data.osm.Way; + +public final class AlignNodesStraightAndParallel extends JosmAction { + + public AlignNodesStraightAndParallel() { + super(tr("Align Nodes in Parallel Lines"), "alignparallel", tr("Move the selected nodes to make them straight and parallel"), KeyEvent.VK_EQUALS, 0, true); + } + + /** + * User must select two or more ways. The algorithm moves all the nodes in the ways so they are straight and parallel. + * The user may also optionally select a subset of nodes within the way(s); only the selected nodes will be moved. + */ + public void actionPerformed(ActionEvent e) { + Collection sel = Main.ds.getSelected(); + HashSet selectedNodes = new HashSet(); + Collection selectedWays = new LinkedList(); + + for (OsmPrimitive osm : sel) + { + if (osm instanceof Node) { + selectedNodes.add((Node)osm); + } + else if (osm instanceof Way) { + selectedWays.add(((Way)osm)); + } + } + + if (selectedWays.size() < 2) { + JOptionPane.showMessageDialog(Main.parent, tr("Please select at least two ways (and optionally, a subset of the way's nodes).")); + return; + } + + ArrayList> nodeChains = new ArrayList>(); + + // Go through each way that was selected, and create a node chain containing either: a) all the nodes in the way + // or b) just the nodes that were selected in the way. + for (Way way : selectedWays) { + ArrayList nodeChain = new ArrayList(); + + // see if any of the nodes in the way were selected; if so, add them to the chain. + for (Node node : way.nodes) { + if (selectedNodes.contains(node)) { + nodeChain.add(node); + selectedNodes.remove(node); // Ensure no node is added twice (can happen in circular ways) + } + } + + // if the chain is empty, add all the nodes from the way. + if (nodeChain.size() == 0) { + nodeChain.addAll(way.nodes); + } + + if (nodeChain.size() > 1) { + nodeChains.add(nodeChain); + } + } + + double[] chainLengths = new double[nodeChains.size()]; + double totalChainLength = 0; + for (int i = 0; i < nodeChains.size(); i++) { + ArrayList nodeChain = nodeChains.get(i); + for (int j = 0; j < nodeChain.size() - 1; j++) { + double distance = nodeChain.get(j).eastNorth.distance(nodeChain.get(j+1).eastNorth); + chainLengths[i] += distance; + totalChainLength += distance; + } + } + + // Normalize the chain lengths so that they add up to 1 in total. This allows them to be used + // as weights for the average bearing calculation. + double[] chainWeights = new double[nodeChains.size()]; + for (int i = 0; i < chainWeights.length; i++) { + chainWeights[i] = chainLengths[i] / totalChainLength; + } + + // Align the node chains so they go in the same direction: first, find the bearing of each chain, and compare it with the first. + // Reverse the chain if necessary, or bail out if the bearings are too different. + Bearing[] chainBearings = new Bearing[nodeChains.size()]; + for (int i = 0; i < nodeChains.size(); i++) { + ArrayList chain = nodeChains.get(i); + Node startNode = chain.get(0); + Node endNode = chain.get(chain.size() - 1); + chainBearings[i] = new Bearing(); + chainBearings[i].setCoords(startNode.eastNorth.east(), startNode.eastNorth.north(), endNode.eastNorth.east(), endNode.eastNorth.north()); + + if (i > 0) { + double absDiff = chainBearings[0].absDifference(chainBearings[i]).getDegrees(); + + // if bearing difference is > 45 degrees, there is probably something wrong. + if (absDiff > 45 && absDiff < 135) { + JOptionPane.showMessageDialog(Main.parent, tr("Please select ways within 45 degrees of each other")); + return; + } else if (absDiff > 135) { + for (int j = 0; j < chain.size() / 2; j++) { + Node temp = chain.get(j); + int swapIndex = chain.size() - 1 - j; + chain.set(j, chain.get(swapIndex)); + chain.set(swapIndex, temp); + } + startNode = chain.get(0); + endNode = chain.get(chain.size() - 1); + chainBearings[i].setCoords(startNode.eastNorth.east(), startNode.eastNorth.north(), endNode.eastNorth.east(), endNode.eastNorth.north()); + } + } + } + + // Work out the average bearing. + // Weight each node chain in the calculation according to its length. + Bearing averageBearing = Bearing.weightedAverage(chainBearings, chainWeights) ; + + Collection cmds = new LinkedList(); + + for (ArrayList nodeChain : nodeChains) { + // Move each node in the chain so that it is in a straight line of the correct bearing. + // All nodes in the chain must be moved so that y = mx + c + // where m is the average gradient of all the node chains, and take x and y as the + // midpoint of the chain in order to calculate c. + double m = averageBearing.getGradient(); + double x_mid = (nodeChain.get(0).eastNorth.east() + nodeChain.get(nodeChain.size() - 1).eastNorth.east()) / 2; + double y_mid = (nodeChain.get(0).eastNorth.north() + nodeChain.get(nodeChain.size() - 1).eastNorth.north()) / 2; + double c = y_mid - (m * x_mid); + + for (Node n : nodeChain) { + double x_node = n.eastNorth.east(); + double y_node = n.eastNorth.north(); + + double x_new = 0; + double y_new = 0; + + // special cases where line is horizontal or vertical. + if (averageBearing.isHorizontal()) { + x_new = x_node; + y_new = y_mid; + } + else if (averageBearing.isVertical()) { + x_new = x_mid; + y_new = y_node; + } + else { + // Create a new line definition (i.e. m2 and c2) for the line that runs through the node, + // and is orthogonal to the average bearing. + double m_orth = averageBearing.add(Bearing.NINETY_DEGREES).getGradient(); + double c_orth = y_node - (m_orth * x_node); + + // now solve the simultaneous equation for x and y + x_new = (c_orth - c) / (m - m_orth); + y_new = ((c * m_orth) - (c_orth * m)) / (m_orth - m); + } + cmds.add(new MoveCommand(n, x_new - n.eastNorth.east(), y_new - n.eastNorth.north())); + } + } + + Main.main.undoRedo.add(new SequenceCommand(tr("Align Nodes Straight and Parallel"), cmds)); + Main.map.repaint(); + } +} === added file 'src/org/openstreetmap/josm/data/Bearing.java' --- src/org/openstreetmap/josm/data/Bearing.java 1970-01-01 00:00:00 +0000 +++ src/org/openstreetmap/josm/data/Bearing.java 2008-08-30 23:33:47 +0000 @@ -0,0 +1,122 @@ +// Bearing.java + +// Copyright (C) 2008 Aled Morris +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package org.openstreetmap.josm.data; + +public class Bearing { + // Some interesting bearings + public static Bearing NINETY_DEGREES = new Bearing(90); + + // Bearing is stored internally as degrees + public double degrees; + + private final static double RADIANS_PER_DEGREE = Math.PI / 180d; + private final static double DEGREES_PER_RADIAN = 180d / Math.PI; + + public Bearing() { + setDegrees(0); + } + + public Bearing(double degrees) { + setDegrees(degrees); + } + + public Bearing(double x_from, double y_from, double x_to, double y_to) { + setCoords(x_from, y_from, x_to, y_to); + } + + public double getDegrees() { + return degrees; + } + + public void setDegrees(double value) { + degrees = value; + while (degrees < 0) { + degrees += 360; + } + degrees %= 360; + } + + public double getRadians() { + return degrees * RADIANS_PER_DEGREE; + } + + public void setRadians(double radians) { + setDegrees(radians * DEGREES_PER_RADIAN); + } + + public void setCoords(double x_from, double y_from, double x_to, double y_to) { + double dx = x_to - x_from; + double dy = y_to - y_from; + + if (dx == 0) { + setDegrees(dy < 0 ? 180 : 0); + } + else if (dy == 0) { + setDegrees(dx > 0 ? 90 : 270); + } + else { + setRadians(Math.atan(dx / dy)); + if (dy < 0) { + setDegrees(degrees + 180); + } + } + } + + // smallest angle between two bearings. Will always be between 0 and 180 degrees + public Bearing absDifference(Bearing other) { + double diff = Math.abs(degrees - other.degrees); + + if (diff > 180) { + diff = 360 - diff; + } + + return new Bearing(diff); + } + + public double getGradient() { + return Math.tan((Math.PI / 2) - getRadians()); + } + + public Bearing add(Bearing other) { + return new Bearing(degrees + other.degrees); + } + + public boolean isHorizontal() { + return degrees == 90 || degrees == 270; + } + + public boolean isVertical() { + return degrees == 0 || degrees == 180; + } + + public static Bearing average(Bearing[] bearings) { + double total = 0; + for (Bearing b : bearings) { + total += b.degrees; + } + return new Bearing(total / bearings.length); + } + + public static Bearing weightedAverage(Bearing[] bearings, double[] weights) { + double total = 0; + for (int i = 0; i < bearings.length; i++) { + total += (bearings[i].degrees * weights[i]); + } + return new Bearing(total); + } +} === modified file 'src/org/openstreetmap/josm/gui/MainMenu.java' --- src/org/openstreetmap/josm/gui/MainMenu.java 2008-08-24 19:08:26 +0000 +++ src/org/openstreetmap/josm/gui/MainMenu.java 2008-08-25 20:15:42 +0000 @@ -18,6 +18,7 @@ import org.openstreetmap.josm.actions.AlignInCircleAction; import org.openstreetmap.josm.actions.AlignInLineAction; import org.openstreetmap.josm.actions.AlignInRectangleAction; +import org.openstreetmap.josm.actions.AlignNodesStraightAndParallel; import org.openstreetmap.josm.actions.AutoScaleAction; import org.openstreetmap.josm.actions.CombineWayAction; import org.openstreetmap.josm.actions.CopyAction; @@ -100,6 +101,7 @@ public final JosmAction alignInCircle = new AlignInCircleAction(); public final JosmAction alignInLine = new AlignInLineAction(); public final JosmAction alignInRect = new AlignInRectangleAction(); + public final JosmAction AlignNodesStraightAndParallel = new AlignNodesStraightAndParallel(); public final JosmAction mergeNodes = new MergeNodesAction(); public final JosmAction joinNodeWay = new JoinNodeWayAction(); public final JosmAction unglueNodes = new UnGlueAction(); @@ -223,6 +225,8 @@ current.setAccelerator(alignInCircle.shortCut); current = toolsMenu.add(alignInLine); current.setAccelerator(alignInLine.shortCut); + current = toolsMenu.add(AlignNodesStraightAndParallel); + current.setAccelerator(AlignNodesStraightAndParallel.shortCut); current = toolsMenu.add(alignInRect); current.setAccelerator(alignInRect.shortCut); toolsMenu.addSeparator();