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

Last change on this file since 19658 was 19658, checked in by bastik, 15 years ago

josm terracer plugin - some updates and fixes

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