source: osm/applications/editors/josm/plugins/terracer/src/org/openstreetmap/josm/plugins/terracer/TerracerAction.java

Last change on this file was 36181, checked in by taylor.smock, 6 months ago

Fix #6855: Reverse terracer considers buildings without an address when reversing a terrace

This also fixes several lint issues.

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