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

Last change on this file since 33579 was 33579, checked in by donvip, 8 years ago

update to JOSM 12678

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