source: osm/applications/editors/josm/plugins/terracer/src/terracer/TerracerAction.java@ 14045

Last change on this file since 14045 was 14045, checked in by zere, 16 years ago

Small fix for ambiguity when terracing exact quadrilaterals. Reordered text input boxes for better usability. Better i18n in dialog box.

File size: 17.5 KB
Line 
1/**
2 * Terracer: A JOSM Plugin for terraced houses.
3 *
4 * Copyright 2009 CloudMade Ltd.
5 *
6 * Released under the GPLv2, see LICENSE file for details.
7 */
8package terracer;
9
10import static org.openstreetmap.josm.tools.I18n.tr;
11import static org.openstreetmap.josm.tools.I18n.trn;
12
13import java.awt.BorderLayout;
14import java.awt.Choice;
15import java.awt.Component;
16import java.awt.GridBagLayout;
17import java.awt.event.ActionEvent;
18import java.awt.event.ItemEvent;
19import java.awt.event.ItemListener;
20import java.awt.event.KeyEvent;
21import java.util.ArrayList;
22import java.util.Arrays;
23import java.util.Collection;
24import java.util.Collections;
25import java.util.HashMap;
26import java.util.LinkedList;
27import java.util.List;
28import java.util.Map;
29import java.util.TreeMap;
30import java.util.TreeSet;
31
32import javax.swing.JComponent;
33import javax.swing.JFormattedTextField;
34import javax.swing.JLabel;
35import javax.swing.JOptionPane;
36import javax.swing.JPanel;
37import javax.swing.JSpinner;
38import javax.swing.JTextField;
39import javax.swing.SpinnerNumberModel;
40import javax.swing.SpringLayout;
41import javax.swing.event.ChangeEvent;
42import javax.swing.event.ChangeListener;
43
44import org.openstreetmap.josm.Main;
45import org.openstreetmap.josm.actions.JosmAction;
46import org.openstreetmap.josm.command.AddCommand;
47import org.openstreetmap.josm.command.Command;
48import org.openstreetmap.josm.command.SequenceCommand;
49import org.openstreetmap.josm.data.coor.LatLon;
50import org.openstreetmap.josm.data.osm.Node;
51import org.openstreetmap.josm.data.osm.OsmPrimitive;
52import org.openstreetmap.josm.data.osm.Relation;
53import org.openstreetmap.josm.data.osm.RelationMember;
54import org.openstreetmap.josm.data.osm.Way;
55import org.openstreetmap.josm.gui.tagging.TaggingPreset.Item;
56import org.openstreetmap.josm.tools.AutoCompleteComboBox;
57import org.openstreetmap.josm.tools.GBC;
58import org.openstreetmap.josm.tools.Pair;
59import org.openstreetmap.josm.tools.Shortcut;
60
61/**
62 * Terraces a quadrilateral, closed way into a series of quadrilateral,
63 * closed ways.
64 *
65 * At present it only works on quadrilaterals, but there is no reason
66 * why it couldn't be extended to work with other shapes too. The
67 * algorithm employed is naive, but it works in the simple case.
68 *
69 * @author zere
70 */
71public final class TerracerAction extends JosmAction {
72
73 // smsms1 asked for the last value to be remembered to make it easier to do
74 // repeated terraces. this is the easiest, but not necessarily nicest, way.
75 private static String lastSelectedValue = "";
76
77 public TerracerAction() {
78 super(tr("Terrace a building"),
79 "terrace",
80 tr("Creates individual buildings from a long building."),
81 Shortcut.registerShortcut("tools:Terracer",
82 tr("Tool: {0}", tr("Terrace a building")),
83 KeyEvent.VK_T, Shortcut.GROUP_EDIT,
84 Shortcut.SHIFT_DEFAULT),
85 true);
86 }
87
88 /**
89 * Checks that the selection is OK. If not, displays error message. If so
90 * calls to terraceBuilding(), which does all the real work.
91 */
92 public void actionPerformed(ActionEvent e) {
93 Collection<OsmPrimitive> sel = Main.ds.getSelected();
94 boolean badSelect = false;
95
96 if (sel.size() == 1) {
97 OsmPrimitive prim = sel.iterator().next();
98
99 if (prim instanceof Way) {
100 Way way = (Way)prim;
101
102 if ((way.nodes.size() >= 5) &&
103 way.isClosed()) {
104 // first ask the user how many buildings to terrace into
105 HouseNumberDialog dialog = new HouseNumberDialog();
106 final JOptionPane optionPane = new JOptionPane(dialog, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION);
107
108 String title = trn("Change {0} object", "Change {0} objects", sel.size(), sel.size());
109 if(sel.size() == 0)
110 title = tr("Nothing selected!");
111
112 optionPane.createDialog(Main.parent, title).setVisible(true);
113 Object answerObj = optionPane.getValue();
114 if (answerObj != null &&
115 answerObj != JOptionPane.UNINITIALIZED_VALUE &&
116 (answerObj instanceof Integer &&
117 (Integer)answerObj == JOptionPane.OK_OPTION)) {
118
119 // call out to the method which does the actual
120 // terracing.
121 terraceBuilding(way,
122 dialog.numberFrom(),
123 dialog.numberTo(),
124 dialog.stepSize(),
125 dialog.streetName());
126
127 }
128 } else {
129 badSelect = true;
130 }
131 } else {
132 badSelect = true;
133 }
134 } else {
135 badSelect = true;
136 }
137
138 if (badSelect) {
139 JOptionPane.showMessageDialog(Main.parent,
140 tr("Select a single, closed way of at least four nodes."));
141 }
142 }
143
144 /**
145 * Terraces a single, closed, quadrilateral way.
146 *
147 * Any node must be adjacent to both a short and long edge, we naively
148 * choose the longest edge and its opposite and interpolate along them
149 * linearly to produce new nodes. Those nodes are then assembled into
150 * closed, quadrilateral ways and left in the selection.
151 *
152 * @param w The closed, quadrilateral way to terrace.
153 */
154 private void terraceBuilding(Way w, int from, int to, int step, String streetName) {
155 final int nb = 1 + (to - from) / step;
156
157 // now find which is the longest side connecting the first node
158 Pair<Way,Way> interp = findFrontAndBack(w);
159
160 final double frontLength = wayLength(interp.a);
161 final double backLength = wayLength(interp.b);
162
163 // new nodes array to hold all intermediate nodes
164 Node[][] new_nodes = new Node[2][nb + 1];
165
166 Collection<Command> commands = new LinkedList<Command>();
167 Collection<Way> ways = new LinkedList<Way>();
168
169 // create intermediate nodes by interpolating.
170 for (int i = 0; i <= nb; ++i) {
171 new_nodes[0][i] = interpolateAlong(interp.a, frontLength * (double)(i) / (double)(nb));
172 new_nodes[1][i] = interpolateAlong(interp.b, backLength * (double)(i) / (double)(nb));
173 commands.add(new AddCommand(new_nodes[0][i]));
174 commands.add(new AddCommand(new_nodes[1][i]));
175 }
176
177 // create a new relation for addressing
178 Relation relatedStreet = new Relation();
179 relatedStreet.put("type", "relatedStreet");
180 if (streetName != null) {
181 relatedStreet.put("name", streetName);
182 }
183 // note that we don't actually add the street member to the relation, as
184 // the name isn't unambiguous and it could cause confusion if the editor were
185 // to automatically select one which wasn't the one the user intended.
186
187 // assemble new quadrilateral, closed ways
188 for (int i = 0; i < nb; ++i) {
189 Way terr = new Way();
190 // Using Way.nodes.add rather than Way.addNode because the latter doesn't
191 // exist in older versions of JOSM.
192 terr.nodes.add(new_nodes[0][i]);
193 terr.nodes.add(new_nodes[0][i+1]);
194 terr.nodes.add(new_nodes[1][i+1]);
195 terr.nodes.add(new_nodes[1][i]);
196 terr.nodes.add(new_nodes[0][i]);
197 terr.put("addr:housenumber", "" + (from + i * step));
198 terr.put("building", "yes");
199 if (streetName != null) {
200 terr.put("addr:street", streetName);
201 }
202 relatedStreet.members.add(new RelationMember("house", terr));
203 ways.add(terr);
204 commands.add(new AddCommand(terr));
205 }
206
207 commands.add(new AddCommand(relatedStreet));
208
209 Main.main.undoRedo.add(new SequenceCommand(tr("Terrace"), commands));
210 Main.ds.setSelected(ways);
211 }
212
213 /**
214 * Creates a node at a certain distance along a way, as calculated by the
215 * great circle distance.
216 *
217 * Note that this really isn't an efficient way to do this and leads to
218 * O(N^2) running time for the main algorithm, but its simple and easy
219 * to understand, and probably won't matter for reasonable-sized ways.
220 *
221 * @param w The way to interpolate.
222 * @param l The length at which to place the node.
223 * @return A node at a distance l along w from the first point.
224 */
225 private Node interpolateAlong(Way w, double l) {
226 Node n = null;
227 for (Pair<Node,Node> p : w.getNodePairs(false)) {
228 final double seg_length = p.a.coor.greatCircleDistance(p.b.coor);
229 if (l <= seg_length) {
230 n = interpolateNode(p.a, p.b, l / seg_length);
231 break;
232 } else {
233 l -= seg_length;
234 }
235 }
236 if (n == null) {
237 // sometimes there is a small overshoot due to numerical roundoff, so we just
238 // set these cases to be equal to the last node. its not pretty, but it works ;-)
239 n = w.nodes.get(w.nodes.size() - 1);
240 }
241 return n;
242 }
243
244 /**
245 * Calculates the great circle length of a way by summing the great circle
246 * distance of each pair of nodes.
247 *
248 * @param w The way to calculate length of.
249 * @return The length of the way.
250 */
251 private double wayLength(Way w) {
252 double length = 0.0;
253 for (Pair<Node,Node> p : w.getNodePairs(false)) {
254 length += p.a.coor.greatCircleDistance(p.b.coor);
255 }
256 return length;
257 }
258
259 /**
260 * Given a way, try and find a definite front and back by looking at the
261 * segments to find the "sides". Sides are assumed to be single segments
262 * which cannot be contiguous.
263 *
264 * @param w The way to analyse.
265 * @return A pair of ways (front, back) pointing in the same directions.
266 */
267 private Pair<Way, Way> findFrontAndBack(Way w) {
268 // calculate the "side-ness" score for each segment of the way
269 double[] sideness = calculateSideness(w);
270
271 // find the largest two sidenesses which are not contiguous
272 int[] indexes = sortedIndexes(sideness);
273 int side1 = indexes[0];
274 int side2 = indexes[1];
275 // if side2 is contiguous with side1 then look further down the
276 // list. we know there are at least 4 sides, as anything smaller
277 // than a quadrilateral would have been rejected at an earlier
278 // stage.
279 if (Math.abs(side1 - side2) < 2) {
280 side2 = indexes[2];
281 }
282 if (Math.abs(side1 - side2) < 2) {
283 side2 = indexes[3];
284 }
285
286 // if the second side has a shorter length and an approximately equal
287 // sideness then its better to choose the shorter, as with quadrilaterals
288 // created using the orthogonalise tool the sideness will be about the
289 // same for all sides.
290 if (sideLength(w, side1) > sideLength(w, side1 + 1) &&
291 Math.abs(sideness[side1] - sideness[side1 + 1]) < 0.001) {
292 side1 = side1 + 1;
293 side2 = (side2 + 1) % (w.nodes.size() - 1);
294 }
295
296 // swap side1 and side2 into sorted order.
297 if (side1 > side2) {
298 // i can't believe i have to write swap() myself - surely java standard
299 // library has this somewhere??!!?ONE!
300 int tmp = side2;
301 side2 = side1;
302 side1 = tmp;
303 }
304
305 Way front = new Way();
306 Way back = new Way();
307 for (int i = side2 + 1; i < w.nodes.size() - 1; ++i) {
308 front.nodes.add(w.nodes.get(i));
309 }
310 for (int i = 0; i <= side1; ++i) {
311 front.nodes.add(w.nodes.get(i));
312 }
313 // add the back in reverse order so that the front and back ways point
314 // in the same direction.
315 for (int i = side2; i > side1; --i) {
316 back.nodes.add(w.nodes.get(i));
317 }
318
319 return new Pair<Way, Way>(front, back);
320 }
321
322 /**
323 * Calculate the length of a side (from node i to i+1) in a way.
324 */
325 private double sideLength(Way w, int i) {
326 Node a = w.nodes.get(i);
327 Node b = w.nodes.get(i+1);
328 return a.coor.greatCircleDistance(b.coor);
329 }
330
331 /**
332 * Given an array of doubles (but this could made generic very easily) sort
333 * into order and return the array of indexes such that, for a returned array
334 * x, a[x[i]] is sorted for ascending index i.
335 *
336 * This isn't efficient at all, but should be fine for the small arrays we're
337 * expecting. If this gets slow - replace it with some more efficient algorithm.
338 *
339 * @param a The array to sort.
340 * @return An array of indexes, the same size as the input, such that a[x[i]]
341 * is in sorted order.
342 */
343 private int[] sortedIndexes(final double[] a) {
344 class SortWithIndex implements Comparable<SortWithIndex> {
345 public double x;
346 public int i;
347 public SortWithIndex(double a, int b) {
348 x = a; i = b;
349 }
350 public int compareTo(SortWithIndex o) {
351 return Double.compare(x, o.x);
352 };
353 }
354
355 final int length = a.length;
356 ArrayList<SortWithIndex> sortable = new ArrayList<SortWithIndex>(length);
357 for (int i = 0; i < length; ++i) {
358 sortable.add(new SortWithIndex(a[i], i));
359 }
360 Collections.sort(sortable);
361
362 int[] indexes = new int[length];
363 for (int i = 0; i < length; ++i) {
364 indexes[i] = sortable.get(i).i;
365 }
366
367 return indexes;
368 }
369
370 /**
371 * Calculate "sideness" metric for each segment in a way.
372 */
373 private double[] calculateSideness(Way w) {
374 final int length = w.nodes.size() - 1;
375 double[] sideness = new double[length];
376
377 sideness[0] = calculateSideness(
378 w.nodes.get(length - 1), w.nodes.get(0),
379 w.nodes.get(1), w.nodes.get(2));
380 for (int i = 1; i < length - 1; ++i) {
381 sideness[i] = calculateSideness(
382 w.nodes.get(i-1), w.nodes.get(i),
383 w.nodes.get(i+1), w.nodes.get(i+2));
384 }
385 sideness[length-1] = calculateSideness(
386 w.nodes.get(length - 2), w.nodes.get(length - 1),
387 w.nodes.get(length), w.nodes.get(1));
388
389 return sideness;
390 }
391
392 /**
393 * Calculate sideness of a single segment given the nodes which make up that
394 * segment and its previous and next segments in order. Sideness is calculated
395 * for the segment b-c.
396 */
397 private double calculateSideness(Node a, Node b, Node c, Node d) {
398 final double ndx = b.coor.getX() - a.coor.getX();
399 final double pdx = d.coor.getX() - c.coor.getX();
400 final double ndy = b.coor.getY() - a.coor.getY();
401 final double pdy = d.coor.getY() - c.coor.getY();
402
403 return (ndx * pdx + ndy * pdy) /
404 Math.sqrt((ndx * ndx + ndy * ndy) * (pdx * pdx + pdy * pdy));
405 }
406
407 /**
408 * Dialog box to allow users to input housenumbers in a nice way.
409 */
410 class HouseNumberDialog extends JPanel {
411 private SpinnerNumberModel lo, hi;
412 private JSpinner clo, chi;
413 private Choice step;
414 private AutoCompleteComboBox street;
415
416 public HouseNumberDialog() {
417 super(new GridBagLayout());
418 lo = new SpinnerNumberModel(1, 1, 1, 1);
419 hi = new SpinnerNumberModel(1, 1, null, 1);
420 step = new Choice();
421 step.add(tr("All"));
422 step.add(tr("Even"));
423 step.add(tr("Odd"));
424 clo = new JSpinner(lo);
425 chi = new JSpinner(hi);
426
427 lo.addChangeListener(new ChangeListener() {
428 public void stateChanged(ChangeEvent e) {
429 hi.setMinimum((Integer)lo.getNumber());
430 }
431 });
432 hi.addChangeListener(new ChangeListener() {
433 public void stateChanged(ChangeEvent e) {
434 lo.setMaximum((Integer)hi.getNumber());
435 }
436 });
437 step.addItemListener(new ItemListener() {
438 public void itemStateChanged(ItemEvent e) {
439 if (step.getSelectedItem() == tr("All")) {
440 hi.setStepSize(1);
441 lo.setStepSize(1);
442 } else {
443 int odd_or_even = 0;
444 int min = 0;
445
446 if (step.getSelectedItem() == tr("Even")) {
447 odd_or_even = 0;
448 min = 2;
449 } else {
450 odd_or_even = 1;
451 min = 1;
452 }
453
454 if ((lo.getNumber().intValue() & 1) != odd_or_even) {
455 int nextval = lo.getNumber().intValue() - 1;
456 lo.setValue((nextval > min) ? nextval : min);
457 }
458 if ((hi.getNumber().intValue() & 1) != odd_or_even) {
459 int nextval = hi.getNumber().intValue() - 1;
460 hi.setValue((nextval > min) ? nextval : min);
461 }
462 lo.setMinimum(min);
463 hi.setStepSize(2);
464 lo.setStepSize(2);
465 }
466 }
467 });
468
469 final TreeSet<String> names = createAutoCompletionInfo();
470
471 street = new AutoCompleteComboBox();
472 street.setPossibleItems(names);
473 street.setEditable(true);
474 street.setSelectedItem(null);
475
476 JFormattedTextField x;
477 x = ((JSpinner.DefaultEditor)clo.getEditor()).getTextField();
478 x.setColumns(5);
479 x = ((JSpinner.DefaultEditor)chi.getEditor()).getTextField();
480 x.setColumns(5);
481 addLabelled(tr("Highest number") + ": ", chi);
482 addLabelled(tr("Lowest number") + ": ", clo);
483 addLabelled(tr("Interpolation") + ": ", step);
484 addLabelled(tr("Street name") + " (" + tr("Optional") + "): ", street);
485 }
486
487 private void addLabelled(String str, Component c) {
488 JLabel label = new JLabel(str);
489 add(label, GBC.std());
490 label.setLabelFor(c);
491 add(c, GBC.eol());
492 }
493
494 public int numberFrom() {
495 return lo.getNumber().intValue();
496 }
497
498 public int numberTo() {
499 return hi.getNumber().intValue();
500 }
501
502 public int stepSize() {
503 return (step.getSelectedItem() == tr("All")) ? 1 : 2;
504 }
505
506 public String streetName() {
507 Object selected = street.getSelectedItem();
508 if (selected == null) {
509 return null;
510 } else {
511 String name = selected.toString();
512 if (name.length() == 0) {
513 return null;
514 } else {
515 return name;
516 }
517 }
518 }
519 }
520
521 /**
522 * Generates a list of all visible names of highways in order to do
523 * autocompletion on the road name.
524 */
525 private TreeSet<String> createAutoCompletionInfo() {
526 final TreeSet<String> names = new TreeSet<String>();
527 for (OsmPrimitive osm : Main.ds.allNonDeletedPrimitives()) {
528 if (osm.keys != null &&
529 osm.keys.containsKey("highway") &&
530 osm.keys.containsKey("name")) {
531 names.add(osm.keys.get("name"));
532 }
533 }
534 return names;
535 }
536
537 /**
538 * Creates a new node at the interpolated position between the argument
539 * nodes. Interpolates linearly in Lat/Lon coordinates.
540 *
541 * @param a First node, at which f=0.
542 * @param b Last node, at which f=1.
543 * @param f Fractional position between first and last nodes.
544 * @return A new node at the interpolated position.
545 */
546 private Node interpolateNode(Node a, Node b, double f) {
547 Node n = new Node(interpolateLatLon(a, b, f));
548 return n;
549 }
550
551 /**
552 * Calculates the interpolated position between the argument nodes. Interpolates
553 * linearly in Lat/Lon coordinates.
554 *
555 * @param a First node, at which f=0.
556 * @param b Last node, at which f=1.
557 * @param f Fractional position between first and last nodes.
558 * @return The interpolated position.
559 */
560 private LatLon interpolateLatLon(Node a, Node b, double f) {
561 // this isn't quite right - we should probably be interpolating
562 // screen coordinates rather than lat/lon, but it doesn't seem to
563 // make a great deal of difference at the scale of most terraces.
564 return new LatLon(a.coor.lat() * (1.0 - f) + b.coor.lat() * f,
565 a.coor.lon() * (1.0 - f) + b.coor.lon() * f);
566 }
567}
Note: See TracBrowser for help on using the repository browser.