1 | // License: GPL. See LICENSE file for details.
|
---|
2 | //
|
---|
3 | package org.openstreetmap.josm.actions;
|
---|
4 |
|
---|
5 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
6 |
|
---|
7 | import java.awt.event.ActionEvent;
|
---|
8 | import java.awt.event.KeyEvent;
|
---|
9 | import java.util.Collection;
|
---|
10 | import java.util.LinkedList;
|
---|
11 |
|
---|
12 | import javax.swing.JOptionPane;
|
---|
13 |
|
---|
14 | import org.openstreetmap.josm.Main;
|
---|
15 | import org.openstreetmap.josm.command.Command;
|
---|
16 | import org.openstreetmap.josm.command.MoveCommand;
|
---|
17 | import org.openstreetmap.josm.command.SequenceCommand;
|
---|
18 | import org.openstreetmap.josm.data.coor.EastNorth;
|
---|
19 | import org.openstreetmap.josm.data.osm.Node;
|
---|
20 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
---|
21 | import org.openstreetmap.josm.data.osm.Way;
|
---|
22 | import org.openstreetmap.josm.tools.ShortCut;
|
---|
23 |
|
---|
24 | /**
|
---|
25 | * Aligns all selected nodes within a rectangle.
|
---|
26 | *
|
---|
27 | * There are many ways this could be done, for example:
|
---|
28 | * - find smallest rectangle to contain all points (rectangular hull) OR
|
---|
29 | * - find largest rectangle to fit inside OR
|
---|
30 | * - find both and compute the average
|
---|
31 | *
|
---|
32 | * Also, it would be possible to let the user specify more input, e.g.
|
---|
33 | * two nodes that should remain where they are.
|
---|
34 | *
|
---|
35 | * This method uses the following algorithm:
|
---|
36 | * 1. compute "heading" of all four edges
|
---|
37 | * 2. select the edge that is oriented closest to the average of all headings
|
---|
38 | * @author Frederik Ramm <frederik@remote.org>
|
---|
39 | */
|
---|
40 | public final class AlignInRectangleAction extends JosmAction {
|
---|
41 |
|
---|
42 | public AlignInRectangleAction() {
|
---|
43 | super(tr("Align Nodes in Rectangle"), "alignrect", tr("Move the selected nodes into a rectangle."),
|
---|
44 | ShortCut.registerShortCut("tools:alignrect", tr("Tool: Align in rectangle"), KeyEvent.VK_Q, ShortCut.GROUP_EDIT), true);
|
---|
45 | }
|
---|
46 |
|
---|
47 | public void actionPerformed(ActionEvent e) {
|
---|
48 | Collection<OsmPrimitive> sel = Main.ds.getSelected();
|
---|
49 | Way myWay = null;
|
---|
50 | if (sel.size() == 1)
|
---|
51 | for (OsmPrimitive osm : sel)
|
---|
52 | if (osm instanceof Way)
|
---|
53 | myWay = (Way) osm;
|
---|
54 |
|
---|
55 | if ((myWay == null) || (myWay.nodes.size() != 5) || (!myWay.nodes.get(0).equals(myWay.nodes.get(4)))) {
|
---|
56 | JOptionPane.showMessageDialog(Main.parent, tr("Please select one circular way of exactly four nodes."));
|
---|
57 | return;
|
---|
58 | }
|
---|
59 |
|
---|
60 | // find orientation of all four edges, compute average
|
---|
61 | double avg_angle = 0;
|
---|
62 | double angle[] = new double[4];
|
---|
63 | EastNorth en[] = new EastNorth[5];
|
---|
64 | for (int i=0; i<5; i++) en[i] = new EastNorth(myWay.nodes.get(i).eastNorth.east(), myWay.nodes.get(i).eastNorth.north());
|
---|
65 | for (int i=0; i<4; i++) {
|
---|
66 | angle[i] = Math.asin((en[i+1].north()-en[i].north())/en[i+1].distance(en[i])) + 2 * Math.PI;
|
---|
67 | while(angle[i] > Math.PI/4) angle[i] -= Math.PI/2;
|
---|
68 | avg_angle += angle[i];
|
---|
69 | }
|
---|
70 | avg_angle /= 4;
|
---|
71 |
|
---|
72 | // select edge that is closest to average, and use it as the base for the following
|
---|
73 | double best_dist = 0;
|
---|
74 | int base = 0;
|
---|
75 | for (int i=0; i<4; i++)
|
---|
76 | {
|
---|
77 | if ((i==0)||(Math.abs(angle[i]-avg_angle))<best_dist)
|
---|
78 | {
|
---|
79 | best_dist = Math.abs(angle[i]-avg_angle);
|
---|
80 | base = i;
|
---|
81 | }
|
---|
82 | }
|
---|
83 |
|
---|
84 |
|
---|
85 | // nice symbolic names for the nodes we're working with
|
---|
86 | EastNorth begin = en[base]; // first node of base segment
|
---|
87 | EastNorth end = en[(base+1)%4]; // second node of base segment
|
---|
88 | EastNorth next = en[(base+2)%4]; // node following the second node of the base seg
|
---|
89 | EastNorth prev= en[(base+3)%4]; // node before the first node of the base seg
|
---|
90 |
|
---|
91 | // find a parallel to the base segment
|
---|
92 | double base_slope = (end.north() - begin.north()) / (end.east() - begin.east());
|
---|
93 | // base intercept of parallels that go through "next" and "prev" points
|
---|
94 | double b1 = next.north() - base_slope * next.east();
|
---|
95 | double b2 = prev.north() - base_slope * prev.east();
|
---|
96 | // average of both
|
---|
97 | double opposite_b = (b1+b2)/2;
|
---|
98 |
|
---|
99 | // find the point on the base segment from which distance to "next" is shortest
|
---|
100 | double u = ((next.east()-begin.east())*(end.east()-begin.east()) + (next.north()-begin.north())*(end.north()-begin.north()))/end.distanceSq(begin);
|
---|
101 | EastNorth end2 = new EastNorth(begin.east()+u*(end.east()-begin.east()), begin.north()+u*(end.north()-begin.north()));
|
---|
102 |
|
---|
103 | // same for "prev"
|
---|
104 | u = ((prev.east()-begin.east())*(end.east()-begin.east()) + (prev.north()-begin.north())*(end.north()-begin.north()))/end.distanceSq(begin);
|
---|
105 | EastNorth begin2 = new EastNorth(begin.east()+u*(end.east()-begin.east()), begin.north()+u*(end.north()-begin.north()));
|
---|
106 |
|
---|
107 | // new "begin" and "end" points are halfway between their old position and
|
---|
108 | // the base points found above
|
---|
109 | end = new EastNorth((end2.east()+end.east())/2, (end2.north()+end.north())/2);
|
---|
110 | begin = new EastNorth((begin2.east()+begin.east())/2, (begin2.north()+begin.north())/2);
|
---|
111 |
|
---|
112 | double other_slope = -1 / base_slope;
|
---|
113 | double next_b = end.north() - other_slope * end.east();
|
---|
114 | double prev_b = begin.north() - other_slope * begin.east();
|
---|
115 |
|
---|
116 | double x = (opposite_b-next_b)/(other_slope-base_slope);
|
---|
117 | double y = opposite_b + base_slope * x;
|
---|
118 | next = new EastNorth(x, y);
|
---|
119 |
|
---|
120 | x = (opposite_b-prev_b)/(other_slope-base_slope);
|
---|
121 | y = opposite_b + base_slope * x;
|
---|
122 | prev = new EastNorth(x, y);
|
---|
123 |
|
---|
124 | Collection<Command> cmds = new LinkedList<Command>();
|
---|
125 | for (int i=0; i<4; i++) {
|
---|
126 | Node n = myWay.nodes.get(i);
|
---|
127 | EastNorth ref = (i == base) ? begin : (i == (base+1)%4) ? end : (i==(base+2)%4) ? next : prev;
|
---|
128 | double dx = ref.east()-n.eastNorth.east();
|
---|
129 | double dy = ref.north()-n.eastNorth.north();
|
---|
130 | cmds.add(new MoveCommand(n, dx, dy));
|
---|
131 | }
|
---|
132 |
|
---|
133 | Main.main.undoRedo.add(new SequenceCommand(tr("Align Nodes in Rectangle"), cmds));
|
---|
134 | Main.map.repaint();
|
---|
135 | }
|
---|
136 | }
|
---|