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

Last change on this file since 30141 was 30141, checked in by donvip, 11 years ago

[josm_terracer] fix warning to increment version number

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