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

Last change on this file since 34562 was 34562, checked in by donvip, 7 years ago

update to JOSM 14153

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