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

Last change on this file since 27852 was 27852, checked in by stoecker, 13 years ago

fix shortcut deprecation

File size: 29.3 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.Comparator;
19import java.util.Iterator;
20import java.util.LinkedList;
21import java.util.List;
22import java.util.Map.Entry;
23import java.util.Set;
24import java.util.regex.Matcher;
25import java.util.regex.Pattern;
26
27import javax.swing.JOptionPane;
28
29import org.openstreetmap.josm.Main;
30import org.openstreetmap.josm.actions.JosmAction;
31import org.openstreetmap.josm.command.AddCommand;
32import org.openstreetmap.josm.command.ChangeCommand;
33import org.openstreetmap.josm.command.ChangePropertyCommand;
34import org.openstreetmap.josm.command.Command;
35import org.openstreetmap.josm.command.DeleteCommand;
36import org.openstreetmap.josm.command.SequenceCommand;
37import org.openstreetmap.josm.data.osm.Node;
38import org.openstreetmap.josm.data.osm.OsmPrimitive;
39import org.openstreetmap.josm.data.osm.Relation;
40import org.openstreetmap.josm.data.osm.RelationMember;
41import org.openstreetmap.josm.data.osm.TagCollection;
42import org.openstreetmap.josm.data.osm.Way;
43import org.openstreetmap.josm.gui.ExtendedDialog;
44import org.openstreetmap.josm.tools.Pair;
45import org.openstreetmap.josm.tools.Shortcut;
46
47/**
48 * Terraces a quadrilateral, closed way into a series of quadrilateral,
49 * closed ways. If two ways are selected and one of them can be identified as
50 * a street (highway=*, name=*) then the given street will be added
51 * to the 'associatedStreet' relation.
52 *
53 *
54 * At present it only works on quadrilaterals, but there is no reason
55 * why it couldn't be extended to work with other shapes too. The
56 * algorithm employed is naive, but it works in the simple case.
57 *
58 * @author zere
59 */
60public final class TerracerAction extends JosmAction {
61
62 // smsms1 asked for the last value to be remembered to make it easier to do
63 // repeated terraces. this is the easiest, but not necessarily nicest, way.
64 // private static String lastSelectedValue = "";
65
66 Collection<Command> commands;
67
68 public TerracerAction() {
69 super(tr("Terrace a building"), "terrace",
70 tr("Creates individual buildings from a long building."),
71 Shortcut.registerShortcut("tools:Terracer", tr("Tool: {0}",
72 tr("Terrace a building")), KeyEvent.VK_T,
73 Shortcut.SHIFT), true);
74 }
75
76 /**
77 * Checks that the selection is OK. If not, displays error message. If so
78 * calls to terraceBuilding(), which does all the real work.
79 */
80 @Override
81 public void actionPerformed(ActionEvent e) {
82 Collection<OsmPrimitive> sel = getCurrentDataSet().getSelected();
83 Way outline = null;
84 Way street = null;
85 String streetname = null;
86 ArrayList<Node> housenumbers = new ArrayList<Node>();
87 Node init = null;
88
89 class InvalidUserInputException extends Exception {
90 InvalidUserInputException(String message) {
91 super(message);
92 }
93
94 InvalidUserInputException() {
95 super();
96 }
97 }
98
99 try {
100 if (sel.size() == 1) {
101 OsmPrimitive prim = sel.iterator().next();
102
103 if (!(prim instanceof Way))
104 throw new InvalidUserInputException();
105
106 outline = (Way) prim;
107 } else if (sel.size() > 1) {
108 List<Way> ways = OsmPrimitive.getFilteredList(sel, Way.class);
109 Iterator<Way> wit = ways.iterator();
110 while (wit.hasNext()) {
111 Way way = wit.next();
112 if (way.hasKey("building")) {
113 if (outline != null)
114 // already have a building
115 throw new InvalidUserInputException();
116 outline = way;
117 } else if (way.hasKey("highway")) {
118 if (street != null)
119 // already have a street
120 throw new InvalidUserInputException();
121 street = way;
122
123 if ((streetname = street.get("name")) == null)
124 throw new InvalidUserInputException();
125 } else
126 throw new InvalidUserInputException();
127 }
128
129 if (outline == null)
130 throw new InvalidUserInputException();
131
132 List<Node> nodes = OsmPrimitive.getFilteredList(sel, Node.class);
133 Iterator<Node> nit = nodes.iterator();
134 // Actually this should test if the selected address nodes lie
135 // within the selected outline. Any ideas how to do this?
136 while (nit.hasNext()) {
137 Node node = nit.next();
138 if (node.hasKey("addr:housenumber")) {
139 String nodesstreetname = node.get("addr:street");
140 // if a node has a street name if must be equal
141 // to the one of the other address nodes
142 if (nodesstreetname != null) {
143 if (streetname == null)
144 streetname = nodesstreetname;
145 else if (!nodesstreetname.equals(streetname))
146 throw new InvalidUserInputException();
147 }
148
149 housenumbers.add(node);
150 } else {
151 // A given node might not be an address node but then
152 // it has to be part of the building to help getting
153 // the number direction right.
154 if (!outline.containsNode(node) || init != null)
155 throw new InvalidUserInputException();
156 init = node;
157 }
158 }
159
160 Collections.sort(housenumbers, new HousenumberNodeComparator());
161 }
162
163 if (outline == null || !outline.isClosed() || outline.getNodesCount() < 5)
164 throw new InvalidUserInputException();
165 } catch (InvalidUserInputException ex) {
166 new ExtendedDialog(Main.parent, tr("Invalid selection"), new String[] {"OK"})
167 .setButtonIcons(new String[] {"ok"}).setIcon(JOptionPane.INFORMATION_MESSAGE)
168 .setContent(tr("Select a single, closed way of at least four nodes. " +
169 "(Optionally you can also select a street for the addr:street tag " +
170 "and a node to mark the start of numbering.)"))
171 .showDialog();
172 return;
173 }
174
175 // If we have a street, try to find an associatedStreet relation that could be reused.
176 Relation associatedStreet = null;
177 if (street != null) {
178 outer:for (OsmPrimitive osm : Main.main.getCurrentDataSet().allNonDeletedPrimitives()) {
179 if (!(osm instanceof Relation)) continue;
180 Relation rel = (Relation) osm;
181 if ("associatedStreet".equals(rel.get("type")) && street.get("name").equals(rel.get("name"))) {
182 List<RelationMember> members = rel.getMembers();
183 for (RelationMember m : members) {
184 if ("street".equals(m.getRole()) && m.isWay() && m.getMember().equals(street)) {
185 associatedStreet = rel;
186 break outer;
187 }
188 }
189 }
190 }
191 }
192
193 if (housenumbers.size() == 1) {
194 // Special case of one outline and one address node.
195 // Don't open the dialogue, just copy the node keys
196 // to the outline, set building just in case it isn't there
197 // and remove the node.
198 Collection<Command> commands = new LinkedList<Command>();
199 Way newOutline = new Way(outline);
200 for (Entry<String, String> entry : housenumbers.get(0).getKeys()
201 .entrySet()) {
202 newOutline.put(entry.getKey(), entry.getValue());
203 }
204 newOutline.put("building", "yes");
205 commands.add(new ChangeCommand(outline, newOutline));
206 commands.add(DeleteCommand.delete(Main.main.getEditLayer(),
207 housenumbers, true, true));
208 Main.main.undoRedo
209 .add(new SequenceCommand(tr("Terrace"), commands));
210 Main.main.getCurrentDataSet().setSelected(newOutline);
211 } else {
212 String title = trn("Change {0} object", "Change {0} objects", sel
213 .size(), sel.size());
214 // show input dialog.
215 new HouseNumberInputHandler(this, outline, init, street, streetname,
216 associatedStreet, housenumbers, title);
217 }
218 }
219
220 public Integer getNumber(String number) {
221 try {
222 return Integer.parseInt(number);
223 } catch (NumberFormatException ex) {
224 return null;
225 }
226 }
227
228 /**
229 * Sorts the house number nodes according their numbers only
230 *
231 * @param house
232 * number nodes
233 */
234 class HousenumberNodeComparator implements Comparator<Node> {
235 private final Pattern pat = Pattern.compile("^(\\d+)\\s*(.*)");
236
237 /*
238 * (non-Javadoc)
239 *
240 * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
241 */
242 @Override
243 public int compare(Node node1, Node node2) {
244 // It's necessary to strip off trailing non-numbers so we can
245 // compare the numbers itself numerically since string comparison
246 // doesn't work for numbers with different number of digits,
247 // e.g. 9 is higher than 11
248 String node1String = node1.get("addr:housenumber");
249 String node2String = node2.get("addr:housenumber");
250 Matcher mat = pat.matcher(node1String);
251 if (mat.find()) {
252 Integer node1Int = Integer.valueOf(mat.group(1));
253 String node1Rest = mat.group(2);
254 mat = pat.matcher(node2String);
255 if (mat.find()) {
256 Integer node2Int = Integer.valueOf(mat.group(1));
257 // If the numbers are the same, the rest has to make the decision,
258 // e.g. when comparing 23, 23a and 23b.
259 if (node1Int.equals(node2Int))
260 {
261 String node2Rest = mat.group(2);
262 return node1Rest.compareTo(node2Rest);
263 }
264
265 return node1Int.compareTo(node2Int);
266 }
267 }
268
269 return node1String.compareTo(node2String);
270 }
271 }
272
273 /**
274 * Terraces a single, closed, quadrilateral way.
275 *
276 * Any node must be adjacent to both a short and long edge, we naively
277 * choose the longest edge and its opposite and interpolate along them
278 * linearly to produce new nodes. Those nodes are then assembled into
279 * closed, quadrilateral ways and left in the selection.
280 *
281 * @param outline The closed, quadrilateral way to terrace.
282 * @param init The node that hints at which side to start the numbering
283 * @param street The street, the buildings belong to (may be null)
284 * @param associatedStreet
285 * @param segments The number of segments to generate
286 * @param From Starting housenumber
287 * @param To Ending housenumber
288 * @param step The step width to use
289 * @param housenumbers List of housenumbers to use. From and To are ignored
290 * if this is set.
291 * @param streetName the name of the street, derived from the street line
292 * or the house numbers (may be null)
293 * @param handleRelations If the user likes to add a relation or extend an
294 * existing relation
295 * @param deleteOutline If the outline way should be deleted when done
296 */
297 public void terraceBuilding(Way outline,
298 Node init,
299 Way street,
300 Relation associatedStreet,
301 Integer segments,
302 String From,
303 String To,
304 int step,
305 ArrayList<Node> housenumbers,
306 String streetName,
307 boolean handleRelations,
308 boolean deleteOutline) {
309 final int nb;
310 Integer to = null, from = null;
311 if (housenumbers.isEmpty()) {
312 to = getNumber(To);
313 from = getNumber(From);
314 if (to != null && from != null) {
315 nb = 1 + (to.intValue() - from.intValue()) / step;
316 } else if (segments != null) {
317 nb = segments.intValue();
318 } else {
319 // if we get here, there is is a bug in the input validation.
320 throw new TerracerRuntimeException(
321 "Could not determine segments from parameters, this is a bug. "
322 + "Parameters were: segments " + segments
323 + " from " + from + " to " + to + " step "
324 + step);
325 }
326 } else {
327 nb = housenumbers.size();
328 }
329
330 // now find which is the longest side connecting the first node
331 Pair<Way, Way> interp = findFrontAndBack(outline);
332
333 boolean swap = false;
334 if (init != null) {
335 if (interp.a.lastNode().equals(init) || interp.b.lastNode().equals(init)) {
336 swap = true;
337 }
338 }
339
340 final double frontLength = wayLength(interp.a);
341 final double backLength = wayLength(interp.b);
342
343 // new nodes array to hold all intermediate nodes
344 // This set will contain at least 4 existing nodes from the original outline (those, which coordinates match coordinates of outline nodes)
345 Node[][] new_nodes = new Node[2][nb + 1];
346 // This list will contain nodes of the outline that are used in new lines.
347 // These nodes will not be deleted with the outline (if deleting was prompted).
348 ArrayList<Node> reused_nodes = new ArrayList<Node>();
349
350 this.commands = new LinkedList<Command>();
351 Collection<Way> ways = new LinkedList<Way>();
352
353 if (nb > 1) {
354 for (int i = 0; i <= nb; ++i) {
355 int i_dir = swap ? nb - i : i;
356 new_nodes[0][i] = interpolateAlong(interp.a, frontLength * i_dir / nb);
357 new_nodes[1][i] = interpolateAlong(interp.b, backLength * i_dir / nb);
358 if (!outline.containsNode(new_nodes[0][i]))
359 this.commands.add(new AddCommand(new_nodes[0][i]));
360 else
361 reused_nodes.add(new_nodes[0][i]);
362 if (!outline.containsNode(new_nodes[1][i]))
363 this.commands.add(new AddCommand(new_nodes[1][i]));
364 else
365 reused_nodes.add(new_nodes[1][i]);
366 }
367
368 // assemble new quadrilateral, closed ways
369 for (int i = 0; i < nb; ++i) {
370 Way terr = new Way();
371 terr.addNode(new_nodes[0][i]);
372 terr.addNode(new_nodes[0][i + 1]);
373 terr.addNode(new_nodes[1][i + 1]);
374 terr.addNode(new_nodes[1][i]);
375 terr.addNode(new_nodes[0][i]);
376
377 // add the tags of the outline to each building (e.g. source=*)
378 TagCollection.from(outline).applyTo(terr);
379
380 String number = null;
381 Set<Entry<String, String>> additionalKeys = null;
382 if (housenumbers.isEmpty()) {
383 if (from != null) {
384 // only, if the user has specified house numbers
385 number = Integer.toString(from + i * step);
386 }
387 } else {
388 number = housenumbers.get(i).get("addr:housenumber");
389 additionalKeys = housenumbers.get(i).getKeys().entrySet();
390 }
391
392 terr = addressBuilding(terr, street, streetName, number,
393 additionalKeys);
394
395 ways.add(terr);
396 this.commands.add(new AddCommand(terr));
397 }
398
399 if (deleteOutline) {
400 // Delete outline nodes having no tags and referrers but the outline itself
401 List<Node> nodes = outline.getNodes();
402 ArrayList<Node> nodesToDelete = new ArrayList<Node>();
403 for (Node n : nodes)
404 if (!n.hasKeys() && n.getReferrers().size() == 1 && !reused_nodes.contains(n))
405 nodesToDelete.add(n);
406 if (nodesToDelete.size() > 0)
407 this.commands.add(DeleteCommand.delete(Main.main.getEditLayer(), nodesToDelete));
408 // Delete the way itself without nodes
409 this.commands.add(DeleteCommand.delete(Main.main.getEditLayer(), Collections.singleton(outline), false, true));
410
411 }
412 } else {
413 // Single building, just add the address details
414 Way newOutline;
415 newOutline = addressBuilding(outline, street, streetName, From, null);
416 ways.add(newOutline);
417 this.commands.add(new ChangeCommand(outline, newOutline));
418 }
419
420 if (handleRelations) { // create a new relation or merge with existing
421 if (associatedStreet == null) { // create a new relation
422 associatedStreet = new Relation();
423 associatedStreet.put("type", "associatedStreet");
424 if (street != null) { // a street was part of the selection
425 associatedStreet.put("name", street.get("name"));
426 associatedStreet.addMember(new RelationMember("street", street));
427 } else {
428 associatedStreet.put("name", streetName);
429 }
430 for (Way w : ways) {
431 associatedStreet.addMember(new RelationMember("house", w));
432 }
433 this.commands.add(new AddCommand(associatedStreet));
434 } else { // relation exists already - add new members
435 Relation newAssociatedStreet = new Relation(associatedStreet);
436 for (Way w : ways) {
437 newAssociatedStreet.addMember(new RelationMember("house", w));
438 }
439 this.commands.add(new ChangeCommand(associatedStreet, newAssociatedStreet));
440 }
441 }
442
443 // Remove the address node since their tags have been incorporated into
444 // the terraces.
445 // Or should removing them also be an option?
446 if (!housenumbers.isEmpty())
447 commands.add(DeleteCommand.delete(Main.main.getEditLayer(),
448 housenumbers, true, true));
449
450 Main.main.undoRedo.add(new SequenceCommand(tr("Terrace"), commands));
451 if (nb > 1) {
452 // Select the new building outlines (for quick reversing)
453 Main.main.getCurrentDataSet().setSelected(ways);
454 } else if (street != null) {
455 // Select the way (for quick selection of a new house (with the same way))
456 Main.main.getCurrentDataSet().setSelected(street);
457 }
458 }
459
460 /**
461 * Adds address details to a single building
462 *
463 * @param outline The closed, quadrilateral way to add the address to.
464 * @param street The street, the buildings belong to (may be null)
465 * @param streetName the name of a street (may be null). Used if not null and street is null.
466 * @param number The house number
467 * @param additionalKeys More keys to be copied onto the new outline
468 * @return the way with added address details
469 */
470 private Way addressBuilding(Way outline, Way street, String streetName,
471 String number, Set<Entry<String, String>> additionalKeys) {
472 Way changedOutline = outline;
473 if (number != null) {
474 // only, if the user has specified house numbers
475 this.commands.add(new ChangePropertyCommand(changedOutline, "addr:housenumber", number));
476 }
477 if (additionalKeys != null) {
478 for (Entry<String, String> entry : additionalKeys) {
479 this.commands.add(new ChangePropertyCommand(changedOutline,
480 entry.getKey(), entry.getValue()));
481 }
482 }
483 changedOutline.put("building", "yes");
484 if (street != null) {
485 this.commands.add(new ChangePropertyCommand(changedOutline, "addr:street", street.get("name")));
486 } else if (streetName != null) {
487 this.commands.add(new ChangePropertyCommand(changedOutline, "addr:street", streetName));
488 }
489 return changedOutline;
490 }
491
492 /**
493 * Creates a node at a certain distance along a way, as calculated by the
494 * great circle distance.
495 *
496 * Note that this really isn't an efficient way to do this and leads to
497 * O(N^2) running time for the main algorithm, but its simple and easy
498 * to understand, and probably won't matter for reasonable-sized ways.
499 *
500 * @param w The way to interpolate.
501 * @param l The length at which to place the node.
502 * @return A node at a distance l along w from the first point.
503 */
504 private Node interpolateAlong(Way w, double l) {
505 List<Pair<Node,Node>> pairs = w.getNodePairs(false);
506 for (int i = 0; i < pairs.size(); ++i) {
507 Pair<Node,Node> p = pairs.get(i);
508 final double seg_length = p.a.getCoor().greatCircleDistance(p.b.getCoor());
509 if (l <= seg_length || i == pairs.size() - 1) {
510 // be generous on the last segment (numerical roudoff can lead to a small overshoot)
511 return interpolateNode(p.a, p.b, l / seg_length);
512 } else {
513 l -= seg_length;
514 }
515 }
516 // we shouldn't get here
517 throw new IllegalStateException();
518 }
519
520 /**
521 * Calculates the great circle length of a way by summing the great circle
522 * distance of each pair of nodes.
523 *
524 * @param w The way to calculate length of.
525 * @return The length of the way.
526 */
527 private double wayLength(Way w) {
528 double length = 0.0;
529 for (Pair<Node, Node> p : w.getNodePairs(false)) {
530 length += p.a.getCoor().greatCircleDistance(p.b.getCoor());
531 }
532 return length;
533 }
534
535 /**
536 * Given a way, try and find a definite front and back by looking at the
537 * segments to find the "sides". Sides are assumed to be single segments
538 * which cannot be contiguous.
539 *
540 * @param w The way to analyse.
541 * @return A pair of ways (front, back) pointing in the same directions.
542 */
543 private Pair<Way, Way> findFrontAndBack(Way w) {
544 // calculate the "side-ness" score for each segment of the way
545 double[] sideness = calculateSideness(w);
546
547 // find the largest two sidenesses which are not contiguous
548 int[] indexes = sortedIndexes(sideness);
549 int side1 = indexes[0];
550 int side2 = indexes[1];
551 // if side2 is contiguous with side1 then look further down the
552 // list. we know there are at least 4 sides, as anything smaller
553 // than a quadrilateral would have been rejected at an earlier stage.
554 if (indexDistance(side1, side2, indexes.length) < 2) {
555 side2 = indexes[2];
556 }
557 if (indexDistance(side1, side2, indexes.length) < 2) {
558 side2 = indexes[3];
559 }
560
561 // if the second side has a shorter length and an approximately equal
562 // sideness then its better to choose the shorter, as with
563 // quadrilaterals
564 // created using the orthogonalise tool the sideness will be about the
565 // same for all sides.
566 if (sideLength(w, side1) > sideLength(w, side1 + 1)
567 && Math.abs(sideness[side1] - sideness[(side1 + 1) % (w.getNodesCount() - 1)]) < 0.001) {
568 side1 = (side1 + 1) % (w.getNodesCount() - 1);
569 side2 = (side2 + 1) % (w.getNodesCount() - 1);
570 }
571
572 // swap side1 and side2 into sorted order.
573 if (side1 > side2) {
574 int tmp = side2;
575 side2 = side1;
576 side1 = tmp;
577 }
578
579 Way front = new Way();
580 Way back = new Way();
581 for (int i = side2 + 1; i < w.getNodesCount() - 1; ++i) {
582 front.addNode(w.getNode(i));
583 }
584 for (int i = 0; i <= side1; ++i) {
585 front.addNode(w.getNode(i));
586 }
587 // add the back in reverse order so that the front and back ways point
588 // in the same direction.
589 for (int i = side2; i > side1; --i) {
590 back.addNode(w.getNode(i));
591 }
592
593 return new Pair<Way, Way>(front, back);
594 }
595
596 /**
597 * returns the distance of two segments of a closed polygon
598 */
599 private int indexDistance(int i1, int i2, int n) {
600 return Math.min(positiveModulus(i1 - i2, n), positiveModulus(i2 - i1, n));
601 }
602
603 /**
604 * return the modulus in the range [0, n)
605 */
606 private int positiveModulus(int a, int n) {
607 if (n <= 0)
608 throw new IllegalArgumentException();
609 int res = a % n;
610 if (res < 0) {
611 res += n;
612 }
613 return res;
614 }
615
616 /**
617 * Calculate the length of a side (from node i to i+1) in a way. This assumes that
618 * the way is closed, but I only ever call it for buildings.
619 */
620 private double sideLength(Way w, int i) {
621 Node a = w.getNode(i);
622 Node b = w.getNode((i + 1) % (w.getNodesCount() - 1));
623 return a.getCoor().greatCircleDistance(b.getCoor());
624 }
625
626 /**
627 * Given an array of doubles (but this could made generic very easily) sort
628 * into order and return the array of indexes such that, for a returned array
629 * x, a[x[i]] is sorted for ascending index i.
630 *
631 * This isn't efficient at all, but should be fine for the small arrays we're
632 * expecting. If this gets slow - replace it with some more efficient algorithm.
633 *
634 * @param a The array to sort.
635 * @return An array of indexes, the same size as the input, such that a[x[i]]
636 * is in sorted order.
637 */
638 private int[] sortedIndexes(final double[] a) {
639 class SortWithIndex implements Comparable<SortWithIndex> {
640 public double x;
641 public int i;
642
643 public SortWithIndex(double a, int b) {
644 x = a;
645 i = b;
646 }
647
648 @Override
649 public int compareTo(SortWithIndex o) {
650 return Double.compare(x, o.x);
651 };
652 }
653
654 final int length = a.length;
655 ArrayList<SortWithIndex> sortable = new ArrayList<SortWithIndex>(length);
656 for (int i = 0; i < length; ++i) {
657 sortable.add(new SortWithIndex(a[i], i));
658 }
659 Collections.sort(sortable);
660
661 int[] indexes = new int[length];
662 for (int i = 0; i < length; ++i) {
663 indexes[i] = sortable.get(i).i;
664 }
665
666 return indexes;
667 }
668
669 /**
670 * Calculate "sideness" metric for each segment in a way.
671 */
672 private double[] calculateSideness(Way w) {
673 final int length = w.getNodesCount() - 1;
674 double[] sideness = new double[length];
675
676 sideness[0] = calculateSideness(w.getNode(length - 1), w.getNode(0), w
677 .getNode(1), w.getNode(2));
678 for (int i = 1; i < length - 1; ++i) {
679 sideness[i] = calculateSideness(w.getNode(i - 1), w.getNode(i), w
680 .getNode(i + 1), w.getNode(i + 2));
681 }
682 sideness[length - 1] = calculateSideness(w.getNode(length - 2), w
683 .getNode(length - 1), w.getNode(length), w.getNode(1));
684
685 return sideness;
686 }
687
688 /**
689 * Calculate sideness of a single segment given the nodes which make up that
690 * segment and its previous and next segments in order. Sideness is calculated
691 * for the segment b-c.
692 */
693 private double calculateSideness(Node a, Node b, Node c, Node d) {
694 final double ndx = b.getCoor().getX() - a.getCoor().getX();
695 final double pdx = d.getCoor().getX() - c.getCoor().getX();
696 final double ndy = b.getCoor().getY() - a.getCoor().getY();
697 final double pdy = d.getCoor().getY() - c.getCoor().getY();
698
699 return (ndx * pdx + ndy * pdy)
700 / Math.sqrt((ndx * ndx + ndy * ndy) * (pdx * pdx + pdy * pdy));
701 }
702
703 /**
704 * Creates a new node at the interpolated position between the argument
705 * nodes. Interpolates linearly in projected coordinates.
706 *
707 * If new node coordinate matches a or b coordinates, a or b is returned.
708 *
709 * @param a First node, at which f=0.
710 * @param b Last node, at which f=1.
711 * @param f Fractional position between first and last nodes.
712 * @return A new node at the interpolated position (or a or b in case if f ≈ 0 or f ≈ 1).
713 */
714 private Node interpolateNode(Node a, Node b, double f) {
715 Node n = new Node(a.getEastNorth().interpolate(b.getEastNorth(), f));
716 if (n.getCoor().equalsEpsilon(a.getCoor()))
717 return a;
718 if (n.getCoor().equalsEpsilon(b.getCoor()))
719 return b;
720 return n;
721 }
722
723 @Override
724 protected void updateEnabledState() {
725 setEnabled(getCurrentDataSet() != null);
726 }
727}
Note: See TracBrowser for help on using the repository browser.