Ticket #9081: bug9081_v2.patch

File bug9081_v2.patch, 16.6 KB (added by Balaitous, 6 years ago)

new version of the patch: little improuvment for prevent stange phenomena with self crossing way

  • src/org/openstreetmap/josm/actions/AlignInLineAction.java

     
    2828 * Aligns all selected nodes into a straight line (useful for
    2929 * roads that should be straight, but have side roads and
    3030 * therefore need multiple nodes)
     31 *
     32 * Case 1: Only ways selected, align each ways taking care of intersection.
     33 * Case 2: Single node selected, align this node relative to the surrounding nodes.
     34 * Case 3: Single node and ways selected, align this node relative to the surrounding nodes only parts of selected ways.
     35 * Case 4: Only nodes selected, align these nodes respect to the line passing through the most distant nodes.
    3136 *
    3237 * @author Matthew Newton
    3338 */
     
    4348    }
    4449
    4550    /**
     51     * InvalidSelection exception has to be raised when action can't be perform
     52     */
     53    private class InvalidSelection extends Exception {
     54
     55        /**
     56         * Create an InvalidSelection exception with default message
     57         */
     58        public InvalidSelection() {
     59            super(tr("Please select at least three nodes."));
     60        }
     61
     62        /**
     63         * Create an InvalidSelection exception with specific message
     64         * @param msg Message that will be display to the user
     65         */
     66        public InvalidSelection(String msg) {
     67            super(msg);
     68        }
     69    }
     70   
     71    /**
    4672     * Compute 2 anchor points to align a set of nodes.
    4773     * If all nodes are part of a same way anchor points are choose farthest relative to this way,
    4874     * else choose farthest nodes.
     
    99125        resultOut[1] = nodeb;
    100126    }
    101127
    102     private void showWarning() {
    103         showWarning(tr("Please select at least three nodes."));
    104     }
    105 
    106     private void showWarning(String msg) {
    107         new Notification(msg)
    108             .setIcon(JOptionPane.INFORMATION_MESSAGE)
    109             .show();
    110     }
    111 
    112     private static int indexWrap(int size, int i) {
    113         i = i % size; // -2 % 5 = -2, -7 % 5 = -2, -5 % 5 = 0
    114         if (i < 0) {
    115             i = size + i;
    116         }
    117         return i;
    118     }
    119     // get the node in w at index i relative to refI
    120     private static Node getNodeRelative(Way w, int refI, int i) {
    121         int absI = indexWrap(w.getNodesCount(), refI + i);
    122         if(w.isClosed() && refI + i < 0) {
    123             absI--;  // node duplicated in closed ways
    124         }
    125         return w.getNode(absI);
    126     }
    127 
    128128    /**
    129129     * The general algorithm here is to find the two selected nodes
    130130     * that are furthest apart, and then to align all other selected
     
    140140        if (!isEnabled())
    141141            return;
    142142
    143         Node[] anchors = new Node[2]; // oh, java I love you so much..
    144 
    145143        List<Node> selectedNodes = new ArrayList<Node>(getCurrentDataSet().getSelectedNodes());
    146         Collection<Way> selectedWays = getCurrentDataSet().getSelectedWays();
    147         List<Node> nodes = new ArrayList<Node>();
     144        List<Way> selectedWays = new ArrayList<Way>(getCurrentDataSet().getSelectedWays());
    148145
    149         //// Decide what to align based on selection:
     146        try {
     147            Command cmd = null;
     148            //// Decide what to align based on selection:
    150149
    151         /// Only ways selected -> For each way align their nodes taking care of intersection
    152         if(selectedNodes.isEmpty() && !selectedWays.isEmpty()) {
    153             alignMultiWay(selectedWays);
    154             return;
    155         }
    156         /// More than 3 nodes selected -> align those nodes
    157         else if(selectedNodes.size() >= 3) {
    158             nodes.addAll(selectedNodes);
    159             // use the nodes furthest apart as anchors
    160             nodePairFurthestApart(nodes, anchors);
    161         }
    162         /// One node selected -> align that node to the relevant neighbors
    163         else if (selectedNodes.size() == 1) {
    164             Node n = selectedNodes.iterator().next();
    165 
    166             Way w = null;
    167             if(selectedWays.size() == 1) {
    168                 w = selectedWays.iterator().next();
    169                 if (!w.containsNode(n))
    170                     // warning
    171                     return;
    172             } else {
    173                 List<Way> refWays = OsmPrimitive.getFilteredList(n.getReferrers(), Way.class);
    174                 if (refWays.size() == 1) { // node used in only one way
    175                     w = refWays.iterator().next();
    176                 }
     150            /// Only ways selected -> For each way align their nodes taking care of intersection
     151            if(selectedNodes.isEmpty() && !selectedWays.isEmpty()) {
     152                cmd = alignMultiWay(selectedWays);
    177153            }
    178             if (w == null || w.getNodesCount() < 3)
    179                 // warning, need at least 3 nodes
    180                 return;
    181 
    182             // Find anchors
    183             int nodeI = w.getNodes().indexOf(n);
    184             // End-node in non-circular way selected: align this node with the two neighbors.
    185             if ((nodeI == 0 || nodeI == w.getNodesCount()-1) && !w.isClosed()) {
    186                 int direction = nodeI == 0 ? 1 : -1;
    187                 anchors[0] = w.getNode(nodeI + direction);
    188                 anchors[1] = w.getNode(nodeI + direction*2);
    189             } else {
    190                 // o---O---o
    191                 anchors[0] = getNodeRelative(w, nodeI, 1);
    192                 anchors[1] = getNodeRelative(w, nodeI, -1);
     154            /// Only 1 node selected -> align this node relative to referers way
     155            else if(selectedNodes.size() == 1) {
     156                Node selectedNode = selectedNodes.get(0);
     157                List<Way> involvedWays = null;
     158                if(selectedWays.isEmpty())
     159                    /// No selected way, all way containing this node are used
     160                    involvedWays = OsmPrimitive.getFilteredList(selectedNode.getReferrers(), Way.class);
     161                else
     162                    /// Selected way, use only these ways
     163                    involvedWays = selectedWays;
     164                List<Line> lines = getInvolvedLines(selectedNode, involvedWays);
     165                if(lines.size() > 2 || lines.isEmpty())
     166                    throw new InvalidSelection();
     167                cmd = alignSingleNode(selectedNodes.get(0), lines);
    193168            }
    194             nodes.add(n);
    195         }
     169            /// More than 3 nodes selected -> align those nodes
     170            else if(selectedNodes.size() >= 3) {
     171                cmd = alignOnlyNodes(selectedNodes);
     172            }
     173            /// All others cases are invalid
     174            else {
     175                throw new InvalidSelection();
     176            }
    196177
    197         if (anchors[0] == null || anchors[1] == null) {
    198             showWarning();
    199             return;
    200         }
     178            // Do it!
     179            Main.main.undoRedo.add(cmd);
     180            Main.map.repaint();
    201181
    202 
    203         Collection<Command> cmds = new ArrayList<Command>(nodes.size());
    204 
    205         createAlignNodesCommands(anchors, nodes, cmds);
    206 
    207         // Do it!
    208         Main.main.undoRedo.add(new SequenceCommand(tr("Align Nodes in Line"), cmds));
    209         Main.map.repaint();
    210     }
    211 
    212     private void createAlignNodesCommands(Node[] anchors, Collection<Node> nodes, Collection<Command> cmds) {
    213         Node nodea = anchors[0];
    214         Node nodeb = anchors[1];
    215 
    216         // The anchors are aligned per definition
    217         nodes.remove(nodea);
    218         nodes.remove(nodeb);
    219 
    220         // Find out co-ords of A and B
    221         double ax = nodea.getEastNorth().east();
    222         double ay = nodea.getEastNorth().north();
    223         double bx = nodeb.getEastNorth().east();
    224         double by = nodeb.getEastNorth().north();
    225 
    226         // OK, for each node to move, work out where to move it!
    227         for (Node n : nodes) {
    228             // Get existing co-ords of node to move
    229             double nx = n.getEastNorth().east();
    230             double ny = n.getEastNorth().north();
    231 
    232             if (ax == bx) {
    233                 // Special case if AB is vertical...
    234                 nx = ax;
    235             } else if (ay == by) {
    236                 // ...or horizontal
    237                 ny = ay;
    238             } else {
    239                 // Otherwise calculate position by solving y=mx+c
    240                 double m1 = (by - ay) / (bx - ax);
    241                 double c1 = ay - (ax * m1);
    242                 double m2 = (-1) / m1;
    243                 double c2 = n.getEastNorth().north() - (n.getEastNorth().east() * m2);
    244 
    245                 nx = (c2 - c1) / (m1 - m2);
    246                 ny = (m1 * nx) + c1;
    247             }
    248             double newX = nx - n.getEastNorth().east();
    249             double newY = ny - n.getEastNorth().north();
    250             // Add the command to move the node to its new position.
    251             cmds.add(new MoveCommand(n, newX, newY));
     182        } catch (InvalidSelection except) {
     183            new Notification(except.getMessage())
     184                .setIcon(JOptionPane.INFORMATION_MESSAGE)
     185                .show();
    252186        }
    253187    }
    254188
    255189    /**
     190     * Align nodes in case that only nodes are selected
     191     * @param nodes Nodes to be aligned
     192     * @return Command that perform action
     193     * @throws InvalidSelection
     194     */
     195    private Command alignOnlyNodes(List<Node> nodes) throws InvalidSelection {
     196        Node[] anchors = new Node[2]; // oh, java I love you so much..
     197        // use the nodes furthest apart as anchors
     198        nodePairFurthestApart(nodes, anchors);
     199        Collection<Command> cmds = new ArrayList<Command>(nodes.size());
     200        Line line = new Line(anchors[0], anchors[1]);
     201        for(Node node: nodes)
     202            if(node != anchors[0] && node != anchors[1])
     203                cmds.add(line.projectionCommand(node));
     204        return new SequenceCommand(tr("Align Nodes in Line"), cmds);
     205    }
     206   
     207    /**
    256208     * Align way in case of multiple way #6819
    257209     * @param ways Collection of way to align
     210     * @return Command that perform action
     211     * @throws InvalidSelection
    258212     */
    259     private void alignMultiWay(Collection<Way> ways) {
     213    private Command alignMultiWay(Collection<Way> ways) throws InvalidSelection {
    260214        // Collect all nodes and compute line equation
    261215        HashSet<Node> nodes = new HashSet<Node>();
    262216        HashMap<Way, Line> lines = new HashMap<Way, Line>();
    263217        for(Way w: ways) {
    264             if(w.firstNode() == w.lastNode()) {
    265                 showWarning(tr("Can not align a polygon. Abort."));
    266                 return;
    267             }
     218            if(w.firstNode() == w.lastNode())
     219                throw new InvalidSelection(tr("Can not align a polygon. Abort."));
    268220            nodes.addAll(w.getNodes());
    269221            lines.put(w, new Line(w));
    270222        }
     
    282234            }
    283235            else if(referers.size() == 2) {
    284236                Command cmd = lines.get(referers.get(0)).intersectionCommand(n, lines.get(referers.get(1)));
    285                 if(cmd == null) {
    286                     showWarning(tr("Two parallels ways found. Abort."));
    287                     return;
    288                 }
    289237                cmds.add(cmd);
    290238            }
    291             else {
    292                 showWarning(tr("Intersection of three or more ways can not be solved. Abort."));
    293                 return;
    294             }
     239            else
     240                throw new InvalidSelection(tr("Intersection of three or more ways can not be solved. Abort."));
    295241        }
    296         Main.main.undoRedo.add(new SequenceCommand(tr("Align Nodes in Line"), cmds));
    297         Main.map.repaint();
     242        return new SequenceCommand(tr("Align Nodes in Line"), cmds);
    298243    }
    299244
    300245    /**
     246     * Get lines useful to do alignment of a single node
     247     * @param node Node to be aligned
     248     * @param refWays Ways where useful lines will be searched
     249     * @return List of useful lines
     250     * @throws InvalidSelection
     251     */
     252    private List<Line> getInvolvedLines(Node node, List<Way> refWays) throws InvalidSelection {
     253        ArrayList<Line> lines = new ArrayList<Line>();
     254        for(Way way: refWays) {
     255            List<Node> nodes = way.getNodes();
     256            for(int i = 1; i < nodes.size()-1; i++)
     257                if(nodes.get(i) == node)
     258                    lines.add(new Line(nodes.get(i-1), nodes.get(i+1)));
     259        }
     260        return lines;
     261    }
     262
     263    /**
     264     * Align a single node relative to a set of lines #9081
     265     * @param node Node to be aligned
     266     * @param lines Lines to align node on
     267     * @return Command that perform action
     268     * @throws InvalidSelection
     269     */
     270    private Command alignSingleNode(Node node, List<Line> lines) throws InvalidSelection {
     271        if(lines.size() == 1)
     272            return lines.get(0).projectionCommand(node);
     273        else if(lines.size() == 2)
     274            return lines.get(0).intersectionCommand(node,  lines.get(1));
     275        throw new InvalidSelection();
     276    }
     277
     278    /**
    301279     * Class that describe a line
    302280     */
    303281    private class Line {
     
    306284         * Line equation ax + by + c = 0
    307285         * Such as a^2 + b^2 = 1, ie (-b, a) is a unit vector of line
    308286         */
    309         private double a, b, c; // Line equation ax+by+c=0
     287        private double a, b, c;
    310288        /**
    311          * (xM, yM) are coordinate of a point of the line
     289         * (x1, y1) (x2, y2) are coordinates of extremity, considering line as segment
    312290         */
    313         private double xM, yM; // Coordinate of a point of the line
    314 
     291        private double x1, y1, x2, y2;
    315292        /**
    316          * Init a line equation from a way.
    317          * @param way
     293         * Length of segment used to build this line
    318294         */
    319         public Line(Way way) {
    320             xM = way.firstNode().getEastNorth().getX();
    321             yM = way.firstNode().getEastNorth().getY();
    322             double xB = way.lastNode().getEastNorth().getX();
    323             double yB = way.lastNode().getEastNorth().getY();
    324             a = yB - yM;
    325             b = xM - xB;
    326             double norm = Math.sqrt(a*a + b*b);
    327             if (norm == 0) {
    328                 norm = 1;
    329             }
     295        private double norm;
     296
     297        public Line(Node first, Node last) throws InvalidSelection {
     298            x1 = first.getEastNorth().getX();
     299            y1 = first.getEastNorth().getY();
     300            x2 = last.getEastNorth().getX();
     301            y2 = last.getEastNorth().getY();
     302            a = y2 - y1;
     303            b = x1 - x2;
     304            norm = Math.sqrt(a*a + b*b);
     305            if (norm == 0)
     306                // Nodes have same coordinates !
     307                throw new InvalidSelection();
    330308            a /= norm;
    331309            b /= norm;
    332             c = -(a*xM + b*yM);
     310            c = -(a*x1 + b*y1);
    333311        }
     312       
     313        /**
     314         * Init a line equation from a way using its extremities.
     315         * @param way
     316         * @throws InvalidSelection
     317         */
     318        public Line(Way way) throws InvalidSelection {
     319            this(way.firstNode(), way.lastNode());
     320        }
    334321
    335322        /**
    336323         * Orthogonal projection of a node N along this line.
     
    338325         * @return The command that do the projection of this node
    339326         */
    340327        public Command projectionCommand(Node n) {
    341             double s = (xM - n.getEastNorth().getX()) * a + (yM - n.getEastNorth().getY()) * b;
     328            double s = (x1 - n.getEastNorth().getX()) * a + (y1 - n.getEastNorth().getY()) * b;
    342329            return new MoveCommand(n, a*s, b*s);
    343330        }
    344331
     
    347334         * @param n Node to move to the intersection
    348335         * @param other Second line for intersection
    349336         * @return The command that move the node or null if line are parallels
     337         * @throws InvalidSelection
    350338         */
    351         public Command intersectionCommand(Node n, Line other) {
     339        public Command intersectionCommand(Node n, Line other) throws InvalidSelection {
    352340            double d = this.a * other.b - other.a * this.b;
    353             if(d == 0) return null;
     341            if(Math.abs(d) < 10e-6) throw new InvalidSelection(tr("Two parallels ways found. Abort."));
    354342            double x = (this.b * other.c - other.b * this.c) / d;
    355343            double y = (other.a * this.c - this.a * other.c) / d;
     344            if(!this.isValidPoint(x, y) || !other.isValidPoint(x, y))
     345                // To prevent strange phenomena with self crossing way
     346                throw new InvalidSelection();
    356347            return new MoveCommand(n, x - n.getEastNorth().getX(), y - n.getEastNorth().getY());
    357348        }
     349       
     350        /**
     351         * @return true if point (x, y) is "reasonably" close to the segment
     352         */
     353        private boolean isValidPoint(double x, double y) {
     354            double abs = (-b*(x-x1) + a*(y-y1)) / norm;
     355            return (abs > -1 && abs < 2);
     356        }
    358357    }
    359358
    360359    @Override