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

Last change on this file since 23191 was 23191, checked in by stoecker, 14 years ago

remove tabs

File size: 21.1 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 // new nodes array to hold all intermediate nodes
207 Node[][] new_nodes = new Node[2][nb + 1];
208
209 this.commands = new LinkedList<Command>();
210 Collection<Way> ways = new LinkedList<Way>();
211
212 // Should this building be terraced (i.e. is there more then one section?)
213 if (nb > 1) {
214 // create intermediate nodes by interpolating.
215
216 // now find which is the longest side connecting the first node
217 Pair<Way, Way> interp = findFrontAndBack(outline);
218
219 final double frontLength = wayLength(interp.a);
220 final double backLength = wayLength(interp.b);
221
222 for (int i = 0; i <= nb; ++i) {
223 new_nodes[0][i] = interpolateAlong(interp.a, frontLength * i / nb);
224 new_nodes[1][i] = interpolateAlong(interp.b, backLength * i / nb);
225 this.commands.add(new AddCommand(new_nodes[0][i]));
226 this.commands.add(new AddCommand(new_nodes[1][i]));
227 }
228
229 // assemble new quadrilateral, closed ways
230 for (int i = 0; i < nb; ++i) {
231 Way terr = new Way();
232 // Using Way.nodes.add rather than Way.addNode because the latter
233 // doesn't
234 // exist in older versions of JOSM.
235 terr.addNode(new_nodes[0][i]);
236 terr.addNode(new_nodes[0][i + 1]);
237 terr.addNode(new_nodes[1][i + 1]);
238 terr.addNode(new_nodes[1][i]);
239 terr.addNode(new_nodes[0][i]);
240
241 // add the tags of the outline to each building (e.g. source=*)
242 TagCollection.from(outline).applyTo(terr);
243
244 String number = null;
245 if (from != null) {
246 number = Integer.toString(from + i * step);
247 }
248 terr = addressBuilding(terr, street, streetName, number);
249
250 ways.add(terr);
251 this.commands.add(new AddCommand(terr));
252 }
253
254 if (deleteOutline) {
255 this.commands.add(DeleteCommand.delete(Main.main.getEditLayer(), Collections.singleton(outline), true, true));
256 }
257 } else {
258 // Single section, just add the address details
259 Way newOutline;
260 newOutline = addressBuilding(outline, street, streetName, From);
261 ways.add(newOutline);
262 this.commands.add(new ChangeCommand(outline, newOutline));
263 }
264
265 if (handleRelations) { // create a new relation or merge with existing
266 if (associatedStreet == null) { // create a new relation
267 associatedStreet = new Relation();
268 associatedStreet.put("type", "associatedStreet");
269 if (street != null) { // a street was part of the selection
270 associatedStreet.put("name", street.get("name"));
271 associatedStreet.addMember(new RelationMember("street", street));
272 } else {
273 associatedStreet.put("name", streetName);
274 }
275 for (Way w : ways) {
276 associatedStreet.addMember(new RelationMember("house", w));
277 }
278 this.commands.add(new AddCommand(associatedStreet));
279 }
280 else { // relation exists already - add new members
281 Relation newAssociatedStreet = new Relation(associatedStreet);
282 for (Way w : ways) {
283 newAssociatedStreet.addMember(new RelationMember("house", w));
284 }
285 this.commands.add(new ChangeCommand(associatedStreet, newAssociatedStreet));
286 }
287 }
288 Main.main.undoRedo.add(new SequenceCommand(tr("Terrace"), commands));
289 if (nb > 1) {
290 // Select the new building outlines (for quick reversing)
291 Main.main.getCurrentDataSet().setSelected(ways);
292 } else if (street != null) {
293 // Select the way (for quick selection of a new house (with the same way))
294 Main.main.getCurrentDataSet().setSelected(street);
295 }
296 }
297
298 /**
299 * Adds address details to a single building
300 *
301 * @param outline The closed, quadrilateral way to add the address to.
302 * @param street The street, the buildings belong to (may be null)
303 * @param streetName the name of a street (may be null). Used if not null and street is null.
304 * @param number The house number
305 * @return the way with added address details
306 */
307 private Way addressBuilding(Way outline, Way street, String streetName, String number) {
308 Way changedOutline = outline;
309 if (number != null) {
310 // only, if the user has specified house numbers
311 this.commands.add(new ChangePropertyCommand(changedOutline, "addr:housenumber", number));
312 }
313 changedOutline.put("building", "yes");
314 if (street != null) {
315 this.commands.add(new ChangePropertyCommand(changedOutline, "addr:street", street.get("name")));
316 } else if (streetName != null) {
317 this.commands.add(new ChangePropertyCommand(changedOutline, "addr:street", streetName));
318 }
319 return changedOutline;
320 }
321
322 /**
323 * Creates a node at a certain distance along a way, as calculated by the
324 * great circle distance.
325 *
326 * Note that this really isn't an efficient way to do this and leads to
327 * O(N^2) running time for the main algorithm, but its simple and easy
328 * to understand, and probably won't matter for reasonable-sized ways.
329 *
330 * @param w The way to interpolate.
331 * @param l The length at which to place the node.
332 * @return A node at a distance l along w from the first point.
333 */
334 private Node interpolateAlong(Way w, double l) {
335 List<Pair<Node,Node>> pairs = w.getNodePairs(false);
336 for (int i = 0; i < pairs.size(); ++i) {
337 Pair<Node,Node> p = pairs.get(i);
338 final double seg_length = p.a.getCoor().greatCircleDistance(p.b.getCoor());
339 if (l <= seg_length ||
340 i == pairs.size() - 1) { // be generous on the last segment (numerical roudoff can lead to a small overshoot)
341 return interpolateNode(p.a, p.b, l / seg_length);
342 } else {
343 l -= seg_length;
344 }
345 }
346 // we shouldn't get here
347 throw new IllegalStateException();
348 }
349
350 /**
351 * Calculates the great circle length of a way by summing the great circle
352 * distance of each pair of nodes.
353 *
354 * @param w The way to calculate length of.
355 * @return The length of the way.
356 */
357 private double wayLength(Way w) {
358 double length = 0.0;
359 for (Pair<Node, Node> p : w.getNodePairs(false)) {
360 length += p.a.getCoor().greatCircleDistance(p.b.getCoor());
361 }
362 return length;
363 }
364
365 /**
366 * Given a way, try and find a definite front and back by looking at the
367 * segments to find the "sides". Sides are assumed to be single segments
368 * which cannot be contiguous.
369 *
370 * @param w The way to analyse.
371 * @return A pair of ways (front, back) pointing in the same directions.
372 */
373 private Pair<Way, Way> findFrontAndBack(Way w) {
374 // calculate the "side-ness" score for each segment of the way
375 double[] sideness = calculateSideness(w);
376
377 // find the largest two sidenesses which are not contiguous
378 int[] indexes = sortedIndexes(sideness);
379 int side1 = indexes[0];
380 int side2 = indexes[1];
381 // if side2 is contiguous with side1 then look further down the
382 // list. we know there are at least 4 sides, as anything smaller
383 // than a quadrilateral would have been rejected at an earlier
384 // stage.
385 if (indexDistance(side1, side2, indexes.length) < 2) {
386 side2 = indexes[2];
387 }
388 if (indexDistance(side1, side2, indexes.length) < 2) {
389 side2 = indexes[3];
390 }
391
392 // if the second side has a shorter length and an approximately equal
393 // sideness then its better to choose the shorter, as with
394 // quadrilaterals
395 // created using the orthogonalise tool the sideness will be about the
396 // same for all sides.
397 if (sideLength(w, side1) > sideLength(w, side1 + 1)
398 && Math.abs(sideness[side1] - sideness[side1 + 1]) < 0.001) {
399 side1 = side1 + 1;
400 side2 = (side2 + 1) % (w.getNodesCount() - 1);
401 }
402
403 // swap side1 and side2 into sorted order.
404 if (side1 > side2) {
405 int tmp = side2;
406 side2 = side1;
407 side1 = tmp;
408 }
409
410 Way front = new Way();
411 Way back = new Way();
412 for (int i = side2 + 1; i < w.getNodesCount() - 1; ++i) {
413 front.addNode(w.getNode(i));
414 }
415 for (int i = 0; i <= side1; ++i) {
416 front.addNode(w.getNode(i));
417 }
418 // add the back in reverse order so that the front and back ways point
419 // in the same direction.
420 for (int i = side2; i > side1; --i) {
421 back.addNode(w.getNode(i));
422 }
423
424 return new Pair<Way, Way>(front, back);
425 }
426
427 /**
428 * returns the distance of two segments of a closed polygon
429 */
430 private int indexDistance(int i1, int i2, int n) {
431 return Math.min(positiveModulus(i1 - i2, n), positiveModulus(i2 - i1, n));
432 }
433
434 /**
435 * return the modulus in the range [0, n)
436 */
437 private int positiveModulus(int a, int n) {
438 if (n <=0)
439 throw new IllegalArgumentException();
440 int res = a % n;
441 if (res < 0) {
442 res += n;
443 }
444 return res;
445 }
446
447 /**
448 * Calculate the length of a side (from node i to i+1) in a way. This assumes that
449 * the way is closed, but I only ever call it for buildings.
450 */
451 private double sideLength(Way w, int i) {
452 Node a = w.getNode(i);
453 Node b = w.getNode((i + 1) % (w.getNodesCount() - 1));
454 return a.getCoor().greatCircleDistance(b.getCoor());
455 }
456
457 /**
458 * Given an array of doubles (but this could made generic very easily) sort
459 * into order and return the array of indexes such that, for a returned array
460 * x, a[x[i]] is sorted for ascending index i.
461 *
462 * This isn't efficient at all, but should be fine for the small arrays we're
463 * expecting. If this gets slow - replace it with some more efficient algorithm.
464 *
465 * @param a The array to sort.
466 * @return An array of indexes, the same size as the input, such that a[x[i]]
467 * is in sorted order.
468 */
469 private int[] sortedIndexes(final double[] a) {
470 class SortWithIndex implements Comparable<SortWithIndex> {
471 public double x;
472 public int i;
473
474 public SortWithIndex(double a, int b) {
475 x = a;
476 i = b;
477 }
478
479 public int compareTo(SortWithIndex o) {
480 return Double.compare(x, o.x);
481 };
482 }
483
484 final int length = a.length;
485 ArrayList<SortWithIndex> sortable = new ArrayList<SortWithIndex>(length);
486 for (int i = 0; i < length; ++i) {
487 sortable.add(new SortWithIndex(a[i], i));
488 }
489 Collections.sort(sortable);
490
491 int[] indexes = new int[length];
492 for (int i = 0; i < length; ++i) {
493 indexes[i] = sortable.get(i).i;
494 }
495
496 return indexes;
497 }
498
499 /**
500 * Calculate "sideness" metric for each segment in a way.
501 */
502 private double[] calculateSideness(Way w) {
503 final int length = w.getNodesCount() - 1;
504 double[] sideness = new double[length];
505
506 sideness[0] = calculateSideness(w.getNode(length - 1), w.getNode(0), w
507 .getNode(1), w.getNode(2));
508 for (int i = 1; i < length - 1; ++i) {
509 sideness[i] = calculateSideness(w.getNode(i - 1), w.getNode(i), w
510 .getNode(i + 1), w.getNode(i + 2));
511 }
512 sideness[length - 1] = calculateSideness(w.getNode(length - 2), w
513 .getNode(length - 1), w.getNode(length), w.getNode(1));
514
515 return sideness;
516 }
517
518 /**
519 * Calculate sideness of a single segment given the nodes which make up that
520 * segment and its previous and next segments in order. Sideness is calculated
521 * for the segment b-c.
522 */
523 private double calculateSideness(Node a, Node b, Node c, Node d) {
524 final double ndx = b.getCoor().getX() - a.getCoor().getX();
525 final double pdx = d.getCoor().getX() - c.getCoor().getX();
526 final double ndy = b.getCoor().getY() - a.getCoor().getY();
527 final double pdy = d.getCoor().getY() - c.getCoor().getY();
528
529 return (ndx * pdx + ndy * pdy)
530 / Math.sqrt((ndx * ndx + ndy * ndy) * (pdx * pdx + pdy * pdy));
531 }
532
533 /**
534 * Creates a new node at the interpolated position between the argument
535 * nodes. Interpolates linearly in projected coordinates.
536 *
537 * @param a First node, at which f=0.
538 * @param b Last node, at which f=1.
539 * @param f Fractional position between first and last nodes.
540 * @return A new node at the interpolated position.
541 */
542 private Node interpolateNode(Node a, Node b, double f) {
543 Node n = new Node(a.getEastNorth().interpolate(b.getEastNorth(), f));
544 return n;
545 }
546
547 @Override
548 protected void updateEnabledState() {
549 setEnabled(getCurrentDataSet() != null);
550 }
551}
Note: See TracBrowser for help on using the repository browser.