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

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

New version of Terracer plugin. Supports non-quadrilateral terraces and also fills out Karlsruhe schema tags automatically.

File size: 16.7 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 // swap side1 and side2 into sorted order.
287 if (side1 > side2) {
288 // i can't believe i have to write swap() myself - surely java standard
289 // library has this somewhere??!!?ONE!
290 int tmp = side2;
291 side2 = side1;
292 side1 = tmp;
293 }
294
295 Way front = new Way();
296 Way back = new Way();
297 for (int i = side2 + 1; i < w.nodes.size() - 1; ++i) {
298 front.nodes.add(w.nodes.get(i));
299 }
300 for (int i = 0; i <= side1; ++i) {
301 front.nodes.add(w.nodes.get(i));
302 }
303 // add the back in reverse order so that the front and back ways point
304 // in the same direction.
305 for (int i = side2; i > side1; --i) {
306 back.nodes.add(w.nodes.get(i));
307 }
308
309 return new Pair<Way, Way>(front, back);
310 }
311
312 /**
313 * Given an array of doubles (but this could made generic very easily) sort
314 * into order and return the array of indexes such that, for a returned array
315 * x, a[x[i]] is sorted for ascending index i.
316 *
317 * This isn't efficient at all, but should be fine for the small arrays we're
318 * expecting. If this gets slow - replace it with some more efficient algorithm.
319 *
320 * @param a The array to sort.
321 * @return An array of indexes, the same size as the input, such that a[x[i]]
322 * is in sorted order.
323 */
324 private int[] sortedIndexes(final double[] a) {
325 class SortWithIndex implements Comparable<SortWithIndex> {
326 public double x;
327 public int i;
328 public SortWithIndex(double a, int b) {
329 x = a; i = b;
330 }
331 public int compareTo(SortWithIndex o) {
332 return Double.compare(x, o.x);
333 };
334 }
335
336 final int length = a.length;
337 ArrayList<SortWithIndex> sortable = new ArrayList<SortWithIndex>(length);
338 for (int i = 0; i < length; ++i) {
339 sortable.add(new SortWithIndex(a[i], i));
340 }
341 Collections.sort(sortable);
342
343 int[] indexes = new int[length];
344 for (int i = 0; i < length; ++i) {
345 indexes[i] = sortable.get(i).i;
346 }
347
348 return indexes;
349 }
350
351 /**
352 * Calculate "sideness" metric for each segment in a way.
353 */
354 private double[] calculateSideness(Way w) {
355 final int length = w.nodes.size() - 1;
356 double[] sideness = new double[length];
357
358 sideness[0] = calculateSideness(
359 w.nodes.get(length - 1), w.nodes.get(0),
360 w.nodes.get(1), w.nodes.get(2));
361 for (int i = 1; i < length - 1; ++i) {
362 sideness[i] = calculateSideness(
363 w.nodes.get(i-1), w.nodes.get(i),
364 w.nodes.get(i+1), w.nodes.get(i+2));
365 }
366 sideness[length-1] = calculateSideness(
367 w.nodes.get(length - 2), w.nodes.get(length - 1),
368 w.nodes.get(length), w.nodes.get(1));
369
370 return sideness;
371 }
372
373 /**
374 * Calculate sideness of a single segment given the nodes which make up that
375 * segment and its previous and next segments in order. Sideness is calculated
376 * for the segment b-c.
377 */
378 private double calculateSideness(Node a, Node b, Node c, Node d) {
379 final double ndx = b.coor.getX() - a.coor.getX();
380 final double pdx = d.coor.getX() - c.coor.getX();
381 final double ndy = b.coor.getY() - a.coor.getY();
382 final double pdy = d.coor.getY() - c.coor.getY();
383
384 return (ndx * pdx + ndy * pdy) /
385 Math.sqrt((ndx * ndx + ndy * ndy) * (pdx * pdx + pdy * pdy));
386 }
387
388 /**
389 * Dialog box to allow users to input housenumbers in a nice way.
390 */
391 class HouseNumberDialog extends JPanel {
392 private SpinnerNumberModel lo, hi;
393 private JSpinner clo, chi;
394 private Choice step;
395 private AutoCompleteComboBox street;
396
397 public HouseNumberDialog() {
398 super(new GridBagLayout());
399 lo = new SpinnerNumberModel(1, 1, 1, 1);
400 hi = new SpinnerNumberModel(1, 1, null, 1);
401 step = new Choice();
402 step.add(tr("All"));
403 step.add(tr("Even"));
404 step.add(tr("Odd"));
405 clo = new JSpinner(lo);
406 chi = new JSpinner(hi);
407
408 lo.addChangeListener(new ChangeListener() {
409 public void stateChanged(ChangeEvent e) {
410 hi.setMinimum((Integer)lo.getNumber());
411 }
412 });
413 hi.addChangeListener(new ChangeListener() {
414 public void stateChanged(ChangeEvent e) {
415 lo.setMaximum((Integer)hi.getNumber());
416 }
417 });
418 step.addItemListener(new ItemListener() {
419 public void itemStateChanged(ItemEvent e) {
420 if (step.getSelectedItem() == tr("All")) {
421 hi.setStepSize(1);
422 lo.setStepSize(1);
423 } else {
424 int odd_or_even = 0;
425 int min = 0;
426
427 if (step.getSelectedItem() == tr("Even")) {
428 odd_or_even = 0;
429 min = 2;
430 } else {
431 odd_or_even = 1;
432 min = 1;
433 }
434
435 if ((lo.getNumber().intValue() & 1) != odd_or_even) {
436 int nextval = lo.getNumber().intValue() - 1;
437 lo.setValue((nextval > min) ? nextval : min);
438 }
439 if ((hi.getNumber().intValue() & 1) != odd_or_even) {
440 int nextval = hi.getNumber().intValue() - 1;
441 hi.setValue((nextval > min) ? nextval : min);
442 }
443 lo.setMinimum(min);
444 hi.setStepSize(2);
445 lo.setStepSize(2);
446 }
447 }
448 });
449
450 final TreeSet<String> names = createAutoCompletionInfo();
451
452 street = new AutoCompleteComboBox();
453 street.setPossibleItems(names);
454 street.setEditable(true);
455 street.setSelectedItem(null);
456
457 JFormattedTextField x;
458 x = ((JSpinner.DefaultEditor)clo.getEditor()).getTextField();
459 x.setColumns(5);
460 x = ((JSpinner.DefaultEditor)chi.getEditor()).getTextField();
461 x.setColumns(5);
462 addLabelled("From number: ", clo);
463 addLabelled("To number: ", chi);
464 addLabelled("Interpolation: ", step);
465 addLabelled("Street name (Optional): ", street);
466 }
467
468 private void addLabelled(String str, Component c) {
469 JLabel label = new JLabel(str);
470 add(label, GBC.std());
471 label.setLabelFor(c);
472 add(c, GBC.eol());
473 }
474
475 public int numberFrom() {
476 return lo.getNumber().intValue();
477 }
478
479 public int numberTo() {
480 return hi.getNumber().intValue();
481 }
482
483 public int stepSize() {
484 return (step.getSelectedItem() == tr("All")) ? 1 : 2;
485 }
486
487 public String streetName() {
488 Object selected = street.getSelectedItem();
489 if (selected == null) {
490 return null;
491 } else {
492 String name = selected.toString();
493 if (name.isEmpty()) {
494 return null;
495 } else {
496 return name;
497 }
498 }
499 }
500 }
501
502 /**
503 * Generates a list of all visible names of highways in order to do
504 * autocompletion on the road name.
505 */
506 private TreeSet<String> createAutoCompletionInfo() {
507 final TreeSet<String> names = new TreeSet<String>();
508 for (OsmPrimitive osm : Main.ds.allNonDeletedPrimitives()) {
509 if (osm.keys != null &&
510 osm.keys.containsKey("highway") &&
511 osm.keys.containsKey("name")) {
512 names.add(osm.keys.get("name"));
513 }
514 }
515 return names;
516 }
517
518 /**
519 * Creates a new node at the interpolated position between the argument
520 * nodes. Interpolates linearly in Lat/Lon coordinates.
521 *
522 * @param a First node, at which f=0.
523 * @param b Last node, at which f=1.
524 * @param f Fractional position between first and last nodes.
525 * @return A new node at the interpolated position.
526 */
527 private Node interpolateNode(Node a, Node b, double f) {
528 Node n = new Node(interpolateLatLon(a, b, f));
529 return n;
530 }
531
532 /**
533 * Calculates the interpolated position between the argument nodes. Interpolates
534 * linearly in Lat/Lon coordinates.
535 *
536 * @param a First node, at which f=0.
537 * @param b Last node, at which f=1.
538 * @param f Fractional position between first and last nodes.
539 * @return The interpolated position.
540 */
541 private LatLon interpolateLatLon(Node a, Node b, double f) {
542 // this isn't quite right - we should probably be interpolating
543 // screen coordinates rather than lat/lon, but it doesn't seem to
544 // make a great deal of difference at the scale of most terraces.
545 return new LatLon(a.coor.lat() * (1.0 - f) + b.coor.lat() * f,
546 a.coor.lon() * (1.0 - f) + b.coor.lon() * f);
547 }
548}
Note: See TracBrowser for help on using the repository browser.