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

Last change on this file since 24713 was 24713, checked in by bastik, 14 years ago

'applied #j5729 (patch by robome) - make terracing with existing address nodes easier'

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