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

Last change on this file since 21163 was 21163, checked in by lambertus, 14 years ago

Fix bug described in http://josm.openstreetmap.de/ticket/4985

File size: 20.4 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.event.ActionEvent;
14import java.awt.event.KeyEvent;
15import java.util.ArrayList;
16import java.util.Collection;
17import java.util.Collections;
18import java.util.Iterator;
19import java.util.LinkedList;
20import java.util.List;
21
22import javax.swing.JOptionPane;
23
24import org.openstreetmap.josm.Main;
25import org.openstreetmap.josm.actions.JosmAction;
26import org.openstreetmap.josm.command.AddCommand;
27import org.openstreetmap.josm.command.ChangePropertyCommand;
28import org.openstreetmap.josm.command.ChangeCommand;
29import org.openstreetmap.josm.command.Command;
30import org.openstreetmap.josm.command.DeleteCommand;
31import org.openstreetmap.josm.command.SequenceCommand;
32import org.openstreetmap.josm.data.coor.LatLon;
33import org.openstreetmap.josm.data.osm.Node;
34import org.openstreetmap.josm.data.osm.OsmPrimitive;
35import org.openstreetmap.josm.data.osm.Relation;
36import org.openstreetmap.josm.data.osm.RelationMember;
37import org.openstreetmap.josm.data.osm.TagCollection;
38import org.openstreetmap.josm.data.osm.Way;
39import org.openstreetmap.josm.tools.Pair;
40import org.openstreetmap.josm.tools.Shortcut;
41
42/**
43 * Terraces a quadrilateral, closed way into a series of quadrilateral,
44 * closed ways. If two ways are selected and one of them can be identified as
45 * a street (highway=*, name=*) then the given street will be added
46 * to the 'associatedStreet' relation.
47 *
48 *
49 * At present it only works on quadrilaterals, but there is no reason
50 * why it couldn't be extended to work with other shapes too. The
51 * algorithm employed is naive, but it works in the simple case.
52 *
53 * @author zere
54 */
55public final class TerracerAction extends JosmAction {
56
57 // smsms1 asked for the last value to be remembered to make it easier to do
58 // repeated terraces. this is the easiest, but not necessarily nicest, way.
59 // private static String lastSelectedValue = "";
60
61 Collection<Command> commands;
62
63 public TerracerAction() {
64 super(tr("Terrace a building"), "terrace",
65 tr("Creates individual buildings from a long building."),
66 Shortcut.registerShortcut("tools:Terracer", tr("Tool: {0}",
67 tr("Terrace a building")), KeyEvent.VK_T,
68 Shortcut.GROUP_EDIT, Shortcut.SHIFT_DEFAULT), true);
69 }
70
71 /**
72 * Checks that the selection is OK. If not, displays error message. If so
73 * calls to terraceBuilding(), which does all the real work.
74 */
75 public void actionPerformed(ActionEvent e) {
76 Collection<OsmPrimitive> sel = Main.main.getCurrentDataSet()
77 .getSelected();
78 Way outline = null;
79 Way street = null;
80
81 class InvalidUserInputException extends Exception {
82 InvalidUserInputException(String message) {
83 super(message);
84 }
85 InvalidUserInputException() {
86 super();
87 }
88 }
89
90 try {
91 if (sel.size() == 2) {
92 Iterator<OsmPrimitive> it = sel.iterator();
93 OsmPrimitive prim1 = it.next();
94 OsmPrimitive prim2 = it.next();
95 if (!(prim1 instanceof Way && prim2 instanceof Way))
96 throw new InvalidUserInputException();
97 Way way1 = (Way) prim1;
98 Way way2 = (Way) prim2;
99 if (way2.get("highway") != null) {
100 street = way2;
101 outline = way1;
102 } else if (way1.get("highway") != null) {
103 street = way1;
104 outline = way2;
105 } else
106 throw new InvalidUserInputException();
107 if (street.get("name") == null)
108 throw new InvalidUserInputException();
109
110 } else if (sel.size() == 1) {
111 OsmPrimitive prim = sel.iterator().next();
112
113 if (!(prim instanceof Way))
114 throw new InvalidUserInputException();
115
116 outline = (Way)prim;
117 } else
118 throw new InvalidUserInputException();
119
120 if (outline.getNodesCount() < 5)
121 throw new InvalidUserInputException();
122
123 if (!outline.isClosed())
124 throw new InvalidUserInputException();
125 } catch (InvalidUserInputException ex) {
126 JOptionPane.showMessageDialog(Main.parent,
127 tr("Select a single, closed way of at least four nodes."));
128 return;
129 }
130
131 // If we have a street, try to find a associatedStreet relation that could be reused.
132 Relation associatedStreet = null;
133 if (street != null) {
134 outer:for (OsmPrimitive osm : Main.main.getCurrentDataSet().allNonDeletedPrimitives()) {
135 if (!(osm instanceof Relation)) continue;
136 Relation rel = (Relation) osm;
137 if ("associatedStreet".equals(rel.get("type")) && street.get("name").equals(rel.get("name"))) {
138 List<RelationMember> members = rel.getMembers();
139 for (RelationMember m : members) {
140 if ("street".equals(m.getRole()) && m.isWay() && m.getMember().equals(street)) {
141 associatedStreet = rel;
142 break outer;
143 }
144 }
145 }
146 }
147 }
148
149 String title = trn("Change {0} object", "Change {0} objects", sel.size(), sel.size());
150 // show input dialog.
151 new HouseNumberInputHandler(this, outline, street, associatedStreet, title);
152 }
153
154 public Integer getNumber(String number) {
155 try {
156 return Integer.parseInt(number);
157 } catch (NumberFormatException ex) {
158 return null;
159 }
160 }
161
162 /**
163 * Terraces a single, closed, quadrilateral way.
164 *
165 * Any node must be adjacent to both a short and long edge, we naively
166 * choose the longest edge and its opposite and interpolate along them
167 * linearly to produce new nodes. Those nodes are then assembled into
168 * closed, quadrilateral ways and left in the selection.
169 *
170 * @param outline The closed, quadrilateral way to terrace.
171 * @param street The street, the buildings belong to (may be null)
172 * @param associatedStreet
173 * @param From
174 * @param To
175 * @param streetName the name of a street (may be null). Used if not null and street is null.
176 * @param handleRelations If the user likes to add a relation or extend an existing relation
177 * @param deleteOutline If the outline way should be deleted, when done
178 */
179 public void terraceBuilding(Way outline,
180 Way street,
181 Relation associatedStreet,
182 Integer segments,
183 String From,
184 String To,
185 int step,
186 String streetName,
187 boolean handleRelations,
188 boolean deleteOutline) {
189 final int nb;
190
191 Integer to, from;
192 to = getNumber(To);
193 from = getNumber(From);
194 if (to != null && from != null) {
195 nb = 1 + (to.intValue() - from.intValue()) / step;
196 } else if (segments != null) {
197 nb = segments.intValue();
198 } else {
199 // if we get here, there is is a bug in the input validation.
200 throw new TerracerRuntimeException(
201 "Could not determine segments from parameters, this is a bug. "
202 + "Parameters were: segments " + segments
203 + " from " + from + " to " + to + " step " + step);
204 }
205
206 // now find which is the longest side connecting the first node
207 Pair<Way, Way> interp = findFrontAndBack(outline);
208
209 final double frontLength = wayLength(interp.a);
210 final double backLength = wayLength(interp.b);
211
212 // new nodes array to hold all intermediate nodes
213 Node[][] new_nodes = new Node[2][nb + 1];
214
215 this.commands = new LinkedList<Command>();
216 Collection<Way> ways = new LinkedList<Way>();
217
218 if (nb > 1) {
219 // create intermediate nodes by interpolating.
220 for (int i = 0; i <= nb; ++i) {
221 new_nodes[0][i] = interpolateAlong(interp.a, frontLength * i / nb);
222 new_nodes[1][i] = interpolateAlong(interp.b, backLength * i / nb);
223 this.commands.add(new AddCommand(new_nodes[0][i]));
224 this.commands.add(new AddCommand(new_nodes[1][i]));
225 }
226
227 // assemble new quadrilateral, closed ways
228 for (int i = 0; i < nb; ++i) {
229 Way terr = new Way();
230 // Using Way.nodes.add rather than Way.addNode because the latter
231 // doesn't
232 // exist in older versions of JOSM.
233 terr.addNode(new_nodes[0][i]);
234 terr.addNode(new_nodes[0][i + 1]);
235 terr.addNode(new_nodes[1][i + 1]);
236 terr.addNode(new_nodes[1][i]);
237 terr.addNode(new_nodes[0][i]);
238
239 // add the tags of the outline to each building (e.g. source=*)
240 TagCollection.from(outline).applyTo(terr);
241
242 String number = null;
243 if (from != null) {
244 number = Integer.toString(from + i * step);
245 }
246 terr = addressBuilding(terr, street, streetName, number);
247
248 ways.add(terr);
249 this.commands.add(new AddCommand(terr));
250 }
251
252 if (deleteOutline) {
253 this.commands.add(DeleteCommand.delete(Main.main.getEditLayer(), Collections.singleton(outline), true, true));
254 }
255 } else {
256 // Single building, just add the address details
257 Way newOutline;
258 newOutline = addressBuilding(outline, street, streetName, From);
259 ways.add(newOutline);
260 this.commands.add(new ChangeCommand(outline, newOutline));
261 }
262
263 if (handleRelations) { // create a new relation or merge with existing
264 if (associatedStreet == null) { // create a new relation
265 associatedStreet = new Relation();
266 associatedStreet.put("type", "associatedStreet");
267 if (street != null) { // a street was part of the selection
268 associatedStreet.put("name", street.get("name"));
269 associatedStreet.addMember(new RelationMember("street", street));
270 } else {
271 associatedStreet.put("name", streetName);
272 }
273 for (Way w : ways) {
274 associatedStreet.addMember(new RelationMember("house", w));
275 }
276 this.commands.add(new AddCommand(associatedStreet));
277 }
278 else { // relation exists already - add new members
279 Relation newAssociatedStreet = new Relation(associatedStreet);
280 for (Way w : ways) {
281 newAssociatedStreet.addMember(new RelationMember("house", w));
282 }
283 this.commands.add(new ChangeCommand(associatedStreet, newAssociatedStreet));
284 }
285 }
286 Main.main.undoRedo.add(new SequenceCommand(tr("Terrace"), commands));
287 if (nb > 1) {
288 // Select the new building outlines (for quick reversing)
289 Main.main.getCurrentDataSet().setSelected(ways);
290 } else if (street != null) {
291 // Select the way (for quick selection of a new house (with the same way))
292 Main.main.getCurrentDataSet().setSelected(street);
293 }
294 }
295
296 /**
297 * Adds address details to a single building
298 *
299 * @param outline The closed, quadrilateral way to add the address to.
300 * @param street The street, the buildings belong to (may be null)
301 * @param streetName the name of a street (may be null). Used if not null and street is null.
302 * @param number The house number
303 * @return the way with added address details
304 */
305 private Way addressBuilding(Way outline, Way street, String streetName, String number) {
306 Way changedOutline = outline;
307 if (number != null) {
308 // only, if the user has specified house numbers
309 this.commands.add(new ChangePropertyCommand(changedOutline, "addr:housenumber", number));
310 }
311 changedOutline.put("building", "yes");
312 if (street != null) {
313 this.commands.add(new ChangePropertyCommand(changedOutline, "addr:street", street.get("name")));
314 } else if (streetName != null) {
315 this.commands.add(new ChangePropertyCommand(changedOutline, "addr:street", streetName));
316 }
317 return changedOutline;
318 }
319
320 /**
321 * Creates a node at a certain distance along a way, as calculated by the
322 * great circle distance.
323 *
324 * Note that this really isn't an efficient way to do this and leads to
325 * O(N^2) running time for the main algorithm, but its simple and easy
326 * to understand, and probably won't matter for reasonable-sized ways.
327 *
328 * @param w The way to interpolate.
329 * @param l The length at which to place the node.
330 * @return A node at a distance l along w from the first point.
331 */
332 private Node interpolateAlong(Way w, double l) {
333 List<Pair<Node,Node>> pairs = w.getNodePairs(false);
334 for (int i = 0; i < pairs.size(); ++i) {
335 Pair<Node,Node> p = pairs.get(i);
336 final double seg_length = p.a.getCoor().greatCircleDistance(p.b.getCoor());
337 if (l <= seg_length ||
338 i == pairs.size() - 1) { // be generous on the last segment (numerical roudoff can lead to a small overshoot)
339 return interpolateNode(p.a, p.b, l / seg_length);
340 } else {
341 l -= seg_length;
342 }
343 }
344 // we shouldn't get here
345 throw new IllegalStateException();
346 }
347
348 /**
349 * Calculates the great circle length of a way by summing the great circle
350 * distance of each pair of nodes.
351 *
352 * @param w The way to calculate length of.
353 * @return The length of the way.
354 */
355 private double wayLength(Way w) {
356 double length = 0.0;
357 for (Pair<Node, Node> p : w.getNodePairs(false)) {
358 length += p.a.getCoor().greatCircleDistance(p.b.getCoor());
359 }
360 return length;
361 }
362
363 /**
364 * Given a way, try and find a definite front and back by looking at the
365 * segments to find the "sides". Sides are assumed to be single segments
366 * which cannot be contiguous.
367 *
368 * @param w The way to analyse.
369 * @return A pair of ways (front, back) pointing in the same directions.
370 */
371 private Pair<Way, Way> findFrontAndBack(Way w) {
372 // calculate the "side-ness" score for each segment of the way
373 double[] sideness = calculateSideness(w);
374
375 // find the largest two sidenesses which are not contiguous
376 int[] indexes = sortedIndexes(sideness);
377 int side1 = indexes[0];
378 int side2 = indexes[1];
379 // if side2 is contiguous with side1 then look further down the
380 // list. we know there are at least 4 sides, as anything smaller
381 // than a quadrilateral would have been rejected at an earlier
382 // stage.
383 if (indexDistance(side1, side2, indexes.length) < 2) {
384 side2 = indexes[2];
385 }
386 if (indexDistance(side1, side2, indexes.length) < 2) {
387 side2 = indexes[3];
388 }
389
390 // if the second side has a shorter length and an approximately equal
391 // sideness then its better to choose the shorter, as with
392 // quadrilaterals
393 // created using the orthogonalise tool the sideness will be about the
394 // same for all sides.
395 if (sideLength(w, side1) > sideLength(w, side1 + 1)
396 && Math.abs(sideness[side1] - sideness[side1 + 1]) < 0.001) {
397 side1 = side1 + 1;
398 side2 = (side2 + 1) % (w.getNodesCount() - 1);
399 }
400
401 // swap side1 and side2 into sorted order.
402 if (side1 > side2) {
403 int tmp = side2;
404 side2 = side1;
405 side1 = tmp;
406 }
407
408 Way front = new Way();
409 Way back = new Way();
410 for (int i = side2 + 1; i < w.getNodesCount() - 1; ++i) {
411 front.addNode(w.getNode(i));
412 }
413 for (int i = 0; i <= side1; ++i) {
414 front.addNode(w.getNode(i));
415 }
416 // add the back in reverse order so that the front and back ways point
417 // in the same direction.
418 for (int i = side2; i > side1; --i) {
419 back.addNode(w.getNode(i));
420 }
421
422 return new Pair<Way, Way>(front, back);
423 }
424
425 /**
426 * returns the distance of two segments of a closed polygon
427 */
428 private int indexDistance(int i1, int i2, int n) {
429 return Math.min(positiveModulus(i1 - i2, n), positiveModulus(i2 - i1, n));
430 }
431
432 /**
433 * return the modulus in the range [0, n)
434 */
435 private int positiveModulus(int a, int n) {
436 if (n <=0)
437 throw new IllegalArgumentException();
438 int res = a % n;
439 if (res < 0) {
440 res += n;
441 }
442 return res;
443 }
444
445 /**
446 * Calculate the length of a side (from node i to i+1) in a way. This assumes that
447 * the way is closed, but I only ever call it for buildings.
448 */
449 private double sideLength(Way w, int i) {
450 Node a = w.getNode(i);
451 Node b = w.getNode((i + 1) % (w.getNodesCount() - 1));
452 return a.getCoor().greatCircleDistance(b.getCoor());
453 }
454
455 /**
456 * Given an array of doubles (but this could made generic very easily) sort
457 * into order and return the array of indexes such that, for a returned array
458 * x, a[x[i]] is sorted for ascending index i.
459 *
460 * This isn't efficient at all, but should be fine for the small arrays we're
461 * expecting. If this gets slow - replace it with some more efficient algorithm.
462 *
463 * @param a The array to sort.
464 * @return An array of indexes, the same size as the input, such that a[x[i]]
465 * is in sorted order.
466 */
467 private int[] sortedIndexes(final double[] a) {
468 class SortWithIndex implements Comparable<SortWithIndex> {
469 public double x;
470 public int i;
471
472 public SortWithIndex(double a, int b) {
473 x = a;
474 i = b;
475 }
476
477 public int compareTo(SortWithIndex o) {
478 return Double.compare(x, o.x);
479 };
480 }
481
482 final int length = a.length;
483 ArrayList<SortWithIndex> sortable = new ArrayList<SortWithIndex>(length);
484 for (int i = 0; i < length; ++i) {
485 sortable.add(new SortWithIndex(a[i], i));
486 }
487 Collections.sort(sortable);
488
489 int[] indexes = new int[length];
490 for (int i = 0; i < length; ++i) {
491 indexes[i] = sortable.get(i).i;
492 }
493
494 return indexes;
495 }
496
497 /**
498 * Calculate "sideness" metric for each segment in a way.
499 */
500 private double[] calculateSideness(Way w) {
501 final int length = w.getNodesCount() - 1;
502 double[] sideness = new double[length];
503
504 sideness[0] = calculateSideness(w.getNode(length - 1), w.getNode(0), w
505 .getNode(1), w.getNode(2));
506 for (int i = 1; i < length - 1; ++i) {
507 sideness[i] = calculateSideness(w.getNode(i - 1), w.getNode(i), w
508 .getNode(i + 1), w.getNode(i + 2));
509 }
510 sideness[length - 1] = calculateSideness(w.getNode(length - 2), w
511 .getNode(length - 1), w.getNode(length), w.getNode(1));
512
513 return sideness;
514 }
515
516 /**
517 * Calculate sideness of a single segment given the nodes which make up that
518 * segment and its previous and next segments in order. Sideness is calculated
519 * for the segment b-c.
520 */
521 private double calculateSideness(Node a, Node b, Node c, Node d) {
522 final double ndx = b.getCoor().getX() - a.getCoor().getX();
523 final double pdx = d.getCoor().getX() - c.getCoor().getX();
524 final double ndy = b.getCoor().getY() - a.getCoor().getY();
525 final double pdy = d.getCoor().getY() - c.getCoor().getY();
526
527 return (ndx * pdx + ndy * pdy)
528 / Math.sqrt((ndx * ndx + ndy * ndy) * (pdx * pdx + pdy * pdy));
529 }
530
531 /**
532 * Creates a new node at the interpolated position between the argument
533 * nodes. Interpolates linearly in projected coordinates.
534 *
535 * @param a First node, at which f=0.
536 * @param b Last node, at which f=1.
537 * @param f Fractional position between first and last nodes.
538 * @return A new node at the interpolated position.
539 */
540 private Node interpolateNode(Node a, Node b, double f) {
541 Node n = new Node(a.getEastNorth().interpolate(b.getEastNorth(), f));
542 return n;
543 }
544
545 @Override
546 protected void updateEnabledState() {
547 setEnabled(getCurrentDataSet() != null);
548 }
549}
Note: See TracBrowser for help on using the repository browser.