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

Last change on this file was 34972, checked in by donvip, 9 days ago

see #josm17580 #josm17581 #josm17582 #josm17583 #josm17584 #josm17585 #josm17586 #josm17587 #josm17588 - remove deprecated api (patches by taylor.smock)

File size: 32.9 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.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;
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 *
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
66    private Collection<Command> commands;
67    private Collection<OsmPrimitive> primitives;
68    private TagCollection tagsInConflict;
69
70    public TerracerAction() {
71        super(tr("Terrace a building"), "terrace",
72                tr("Creates individual buildings from a long building."),
73                Shortcut.registerShortcut("tools:Terracer", tr("Tool: {0}",
74                        tr("Terrace a building")), KeyEvent.VK_T,
75                        Shortcut.SHIFT), true);
76    }
77
78    protected static Set<Relation> findAssociatedStreets(Collection<OsmPrimitive> objects) {
79        Set<Relation> result = new HashSet<>();
80        if (objects != null) {
81            for (OsmPrimitive c : objects) {
82                if (c != null) {
83                    for (OsmPrimitive p : c.getReferrers()) {
84                        if (p instanceof Relation && "associatedStreet".equals(p.get("type"))) {
85                            result.add((Relation) p);
86                        }
87                    }
88                }
89            }
90        }
91        return result;
92    }
93
94    private static final class InvalidUserInputException extends Exception {
95        InvalidUserInputException(String message) {
96            super(message);
97        }
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 = getLayerManager().getEditDataSet().getSelected();
107        Way outline = null;
108        Way street = null;
109        String streetname = null;
110        ArrayList<Node> housenumbers = new ArrayList<>();
111        Node init = null;
112
113        try {
114            if (sel.size() == 1) {
115                OsmPrimitive prim = sel.iterator().next();
116
117                if (!(prim instanceof Way))
118                    throw new InvalidUserInputException(prim+" is not a way");
119
120                outline = (Way) prim;
121            } else if (sel.size() > 1) {
122                List<Way> ways = new ArrayList<>(Utils.filteredCollection(sel, Way.class));
123                Iterator<Way> wit = ways.iterator();
124                while (wit.hasNext()) {
125                    Way way = wit.next();
126                    if (way.hasKey("building")) {
127                        if (outline != null)
128                            // already have a building
129                            throw new InvalidUserInputException("already have a building");
130                        outline = way;
131                    } else if (way.hasKey("highway")) {
132                        if (street != null)
133                            // already have a street
134                            throw new InvalidUserInputException("already have a street");
135                        street = way;
136                        streetname = street.get("name");
137                        if (streetname == null)
138                            throw new InvalidUserInputException("street does not have any name");
139                    } else
140                        throw new InvalidUserInputException(way+" is neither a building nor a highway");
141                }
142
143                if (outline == null)
144                    throw new InvalidUserInputException("no outline way found");
145
146                List<Node> nodes = new ArrayList<>(Utils.filteredCollection(sel, Node.class));
147                Iterator<Node> nit = nodes.iterator();
148                // Actually this should test if the selected address nodes lie
149                // within the selected outline. Any ideas how to do this?
150                while (nit.hasNext()) {
151                    Node node = nit.next();
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                Collections.sort(housenumbers, 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"), new String[] {"OK"})
183                .setButtonIcons(new String[] {"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            try {
218                terraceBuilding(outline, init, street, associatedStreet, 0, null, null, 0, 
219                        housenumbers, streetname, associatedStreet != null, false, "yes");
220            } catch (UserCancelException ex) {
221                Logging.trace(ex);
222            } finally {
223                this.commands.clear();
224                this.commands = null;
225            }
226        } else {
227            String title = trn("Change {0} object", "Change {0} objects", sel.size(), sel.size());
228            // show input dialog.
229            new HouseNumberInputHandler(this, outline, init, street, streetname, outline.get("building"),
230                    associatedStreet, housenumbers, title).dialog.showDialog();
231        }
232    }
233
234    public 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     *
245     * @param house
246     *            number nodes
247     */
248    static class HousenumberNodeComparator implements Comparator<Node> {
249        private final Pattern pat = Pattern.compile("^(\\d+)\\s*(.*)");
250
251        @Override
252        public int compare(Node node1, Node node2) {
253            // It's necessary to strip off trailing non-numbers so we can
254            // compare the numbers itself numerically since string comparison
255            // doesn't work for numbers with different number of digits,
256            // e.g. 9 is higher than 11
257            String node1String = node1.get("addr:housenumber");
258            String node2String = node2.get("addr:housenumber");
259            Matcher mat = pat.matcher(node1String);
260            if (mat.find()) {
261                Integer node1Int = Integer.valueOf(mat.group(1));
262                String node1Rest = mat.group(2);
263                mat = pat.matcher(node2String);
264                if (mat.find()) {
265                    Integer node2Int = Integer.valueOf(mat.group(1));
266                    // If the numbers are the same, the rest has to make the decision,
267                    // e.g. when comparing 23, 23a and 23b.
268                    if (node1Int.equals(node2Int)) {
269                      String node2Rest = mat.group(2);
270                      return node1Rest.compareTo(node2Rest);
271                    }
272
273                    return node1Int.compareTo(node2Int);
274                }
275            }
276
277            return node1String.compareTo(node2String);
278        }
279    }
280
281    /**
282     * Terraces a single, closed, quadrilateral way.
283     *
284     * Any node must be adjacent to both a short and long edge, we naively
285     * choose the longest edge and its opposite and interpolate along them
286     * linearly to produce new nodes. Those nodes are then assembled into
287     * closed, quadrilateral ways and left in the selection.
288     *
289     * @param outline The closed, quadrilateral way to terrace.
290     * @param init The node that hints at which side to start the numbering
291     * @param street The street, the buildings belong to (may be null)
292     * @param associatedStreet associated street relation
293     * @param segments The number of segments to generate
294     * @param start Starting housenumber
295     * @param end Ending housenumber
296     * @param step The step width to use
297     * @param housenumbers List of housenumbers to use. From and To are ignored
298     *        if this is set.
299     * @param streetName the name of the street, derived from the street line
300     *        or the house numbers (may be null)
301     * @param handleRelations If the user likes to add a relation or extend an
302     *        existing relation
303     * @param keepOutline If the outline way should be kept
304     * @param buildingValue The value for {@code building} key to add
305     * @throws UserCancelException if user cancels the operation
306     */
307    public void terraceBuilding(final Way outline, Node init, Way street, Relation associatedStreet, Integer segments,
308                String start, String end, int step, List<Node> housenumbers, String streetName, boolean handleRelations,
309                boolean keepOutline, String buildingValue) throws UserCancelException {
310        final int nb;
311        Integer to = null, from = null;
312        if (housenumbers == null || housenumbers.isEmpty()) {
313            to = getNumber(end);
314            from = getNumber(start);
315            if (to != null && from != null) {
316                nb = 1 + (to.intValue() - from.intValue()) / step;
317            } else if (segments != null) {
318                nb = segments.intValue();
319            } else {
320                // if we get here, there is is a bug in the input validation.
321                throw new TerracerRuntimeException(
322                        "Could not determine segments from parameters, this is a bug. "
323                                + "Parameters were: segments " + segments
324                                + " from " + from + " to " + to + " step " + step);
325            }
326        } else {
327            nb = housenumbers.size();
328        }
329
330        // now find which is the longest side connecting the first node
331        Pair<Way, Way> interp = findFrontAndBack(outline);
332
333        final boolean swap = init != null && (interp.a.lastNode().equals(init) || interp.b.lastNode().equals(init));
334
335        final double frontLength = wayLength(interp.a);
336        final double backLength = wayLength(interp.b);
337
338        // new nodes array to hold all intermediate nodes
339        // This set will contain at least 4 existing nodes from the original outline
340        // (those, which coordinates match coordinates of outline nodes)
341        Node[][] newNodes = new Node[2][nb + 1];
342        // This list will contain nodes of the outline that are used in new lines.
343        // These nodes will not be deleted with the outline (if deleting was prompted).
344        List<Node> reusedNodes = new ArrayList<>();
345
346        this.commands = new LinkedList<>();
347        Collection<Way> ways = new LinkedList<>();
348        DataSet ds = getLayerManager().getEditDataSet();
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(ds, newNodes[0][i]));
358                else
359                    reusedNodes.add(newNodes[0][i]);
360                if (!outline.containsNode(newNodes[1][i]))
361                    this.commands.add(new AddCommand(ds, 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(ds, 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(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(housenumbers, true, true));
418        }
419
420        if (handleRelations) { // create a new relation or merge with existing
421            if (associatedStreet == null) {  // create a new relation
422                addNewAssociatedStreetRelation(street, streetName, ways);
423            } else { // relation exists already - add new members
424                updateAssociatedStreetRelation(associatedStreet, housenumbers, ways);
425            }
426        }
427
428        UndoRedoHandler.getInstance().add(createTerracingCommand(outline));
429        if (nb <= 1 && street != null) {
430            // Select the way (for quick selection of a new house (with the same way))
431            MainApplication.getLayerManager().getEditDataSet().setSelected(street);
432        } else {
433            // Select the new building outlines (for quick reversing)
434            MainApplication.getLayerManager().getEditDataSet().setSelected(ways);
435        }
436    }
437
438    private void updateAssociatedStreetRelation(Relation associatedStreet, List<Node> housenumbers, Collection<Way> ways) {
439        Relation newAssociatedStreet = new Relation(associatedStreet);
440        // remove housenumbers as they have been deleted
441        newAssociatedStreet.removeMembersFor(housenumbers);
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     * @return {@code outline}
508     * @throws UserCancelException if user cancels the operation
509     */
510    private void addressBuilding(Way outline, Way street, String streetName, Relation associatedStreet,
511            List<Node> housenumbers, int i, String defaultNumber, String buildingValue) throws UserCancelException {
512        Node houseNum = (housenumbers != null && i >= 0 && i < housenumbers.size()) ? housenumbers.get(i) : null;
513        boolean buildingAdded = false;
514        boolean numberAdded = false;
515        Map<String, String> tags = new HashMap<>();
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                tags.put(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            tags.put("building", buildingValue);
532        }
533        if (defaultNumber != null && !numberAdded) {
534            tags.put("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                tags.put("addr:street", street.get("name"));
540            } else if (streetName != null && !streetName.trim().isEmpty()) {
541                tags.put("addr:street", streetName.trim());
542            }
543        }
544        if (!tags.isEmpty()) {
545            commands.add(new ChangePropertyCommand(getLayerManager().getEditDataSet(), Collections.singleton(outline), tags));
546        }
547    }
548
549    /**
550     * Creates a node at a certain distance along a way, as calculated by the
551     * great circle distance.
552     *
553     * Note that this really isn't an efficient way to do this and leads to
554     * O(N^2) running time for the main algorithm, but its simple and easy
555     * to understand, and probably won't matter for reasonable-sized ways.
556     *
557     * @param w The way to interpolate.
558     * @param l The length at which to place the node.
559     * @return A node at a distance l along w from the first point.
560     */
561    private Node interpolateAlong(Way w, double l) {
562        List<Pair<Node, Node>> pairs = w.getNodePairs(false);
563        for (int i = 0; i < pairs.size(); ++i) {
564            Pair<Node, Node> p = pairs.get(i);
565            final double seg_length = p.a.getCoor().greatCircleDistance(p.b.getCoor());
566            if (l <= seg_length || i == pairs.size() - 1) {
567                // be generous on the last segment (numerical roudoff can lead to a small overshoot)
568                return interpolateNode(p.a, p.b, l / seg_length);
569            } else {
570                l -= seg_length;
571            }
572        }
573        // we shouldn't get here
574        throw new IllegalStateException();
575    }
576
577    /**
578     * Calculates the great circle length of a way by summing the great circle
579     * distance of each pair of nodes.
580     *
581     * @param w The way to calculate length of.
582     * @return The length of the way.
583     */
584    private double wayLength(Way w) {
585        double length = 0.0;
586        for (Pair<Node, Node> p : w.getNodePairs(false)) {
587            length += p.a.getCoor().greatCircleDistance(p.b.getCoor());
588        }
589        return length;
590    }
591
592    /**
593     * Given a way, try and find a definite front and back by looking at the
594     * segments to find the "sides". Sides are assumed to be single segments
595     * which cannot be contiguous.
596     *
597     * @param w The way to analyse.
598     * @return A pair of ways (front, back) pointing in the same directions.
599     */
600    private Pair<Way, Way> findFrontAndBack(Way w) {
601        // calculate the "side-ness" score for each segment of the way
602        double[] sideness = calculateSideness(w);
603
604        // find the largest two sidenesses which are not contiguous
605        int[] indexes = sortedIndexes(sideness);
606        int side1 = indexes[0];
607        int side2 = indexes[1];
608        // if side2 is contiguous with side1 then look further down the
609        // list. we know there are at least 4 sides, as anything smaller
610        // than a quadrilateral would have been rejected at an earlier stage.
611        if (indexDistance(side1, side2, indexes.length) < 2) {
612            side2 = indexes[2];
613        }
614        if (indexDistance(side1, side2, indexes.length) < 2) {
615            side2 = indexes[3];
616        }
617
618        // if the second side has a shorter length and an approximately equal
619        // sideness then its better to choose the shorter, as with
620        // quadrilaterals
621        // created using the orthogonalise tool the sideness will be about the
622        // same for all sides.
623        if (sideLength(w, side1) > sideLength(w, side1 + 1)
624                && Math.abs(sideness[side1] - sideness[(side1 + 1) % (w.getNodesCount() - 1)]) < 0.001) {
625            side1 = (side1 + 1) % (w.getNodesCount() - 1);
626            side2 = (side2 + 1) % (w.getNodesCount() - 1);
627        }
628
629        // swap side1 and side2 into sorted order.
630        if (side1 > side2) {
631            int tmp = side2;
632            side2 = side1;
633            side1 = tmp;
634        }
635
636        Way front = new Way();
637        Way back = new Way();
638        for (int i = side2 + 1; i < w.getNodesCount() - 1; ++i) {
639            front.addNode(w.getNode(i));
640        }
641        for (int i = 0; i <= side1; ++i) {
642            front.addNode(w.getNode(i));
643        }
644        // add the back in reverse order so that the front and back ways point
645        // in the same direction.
646        for (int i = side2; i > side1; --i) {
647            back.addNode(w.getNode(i));
648        }
649
650        return new Pair<>(front, back);
651    }
652
653    /**
654     * returns the distance of two segments of a closed polygon
655     */
656    private int indexDistance(int i1, int i2, int n) {
657        return Math.min(positiveModulus(i1 - i2, n), positiveModulus(i2 - i1, n));
658    }
659
660    /**
661     * return the modulus in the range [0, n)
662     */
663    private int positiveModulus(int a, int n) {
664        if (n <= 0)
665            throw new IllegalArgumentException();
666        int res = a % n;
667        if (res < 0) {
668            res += n;
669        }
670        return res;
671    }
672
673    /**
674     * Calculate the length of a side (from node i to i+1) in a way. This assumes that
675     * the way is closed, but I only ever call it for buildings.
676     */
677    private double sideLength(Way w, int i) {
678        Node a = w.getNode(i);
679        Node b = w.getNode((i + 1) % (w.getNodesCount() - 1));
680        return a.getCoor().greatCircleDistance(b.getCoor());
681    }
682
683    /**
684     * Given an array of doubles (but this could made generic very easily) sort
685     * into order and return the array of indexes such that, for a returned array
686     * x, a[x[i]] is sorted for ascending index i.
687     *
688     * This isn't efficient at all, but should be fine for the small arrays we're
689     * expecting. If this gets slow - replace it with some more efficient algorithm.
690     *
691     * @param a The array to sort.
692     * @return An array of indexes, the same size as the input, such that a[x[i]]
693     * is in sorted order.
694     */
695    private int[] sortedIndexes(final double[] a) {
696        class SortWithIndex implements Comparable<SortWithIndex> {
697            public double x;
698            public int i;
699
700            SortWithIndex(double a, int b) {
701                x = a;
702                i = b;
703            }
704
705            @Override
706            public int compareTo(SortWithIndex o) {
707                return Double.compare(x, o.x);
708            }
709        }
710
711        final int length = a.length;
712        ArrayList<SortWithIndex> sortable = new ArrayList<>(length);
713        for (int i = 0; i < length; ++i) {
714            sortable.add(new SortWithIndex(a[i], i));
715        }
716        Collections.sort(sortable);
717
718        int[] indexes = new int[length];
719        for (int i = 0; i < length; ++i) {
720            indexes[i] = sortable.get(i).i;
721        }
722
723        return indexes;
724    }
725
726    /**
727     * Calculate "sideness" metric for each segment in a way.
728     */
729    private double[] calculateSideness(Way w) {
730        final int length = w.getNodesCount() - 1;
731        double[] sideness = new double[length];
732
733        sideness[0] = calculateSideness(w.getNode(length - 1), w.getNode(0), w
734                .getNode(1), w.getNode(2));
735        for (int i = 1; i < length - 1; ++i) {
736            sideness[i] = calculateSideness(w.getNode(i - 1), w.getNode(i), w
737                    .getNode(i + 1), w.getNode(i + 2));
738        }
739        sideness[length - 1] = calculateSideness(w.getNode(length - 2), w
740                .getNode(length - 1), w.getNode(length), w.getNode(1));
741
742        return sideness;
743    }
744
745    /**
746     * Calculate sideness of a single segment given the nodes which make up that
747     * segment and its previous and next segments in order. Sideness is calculated
748     * for the segment b-c.
749     */
750    private double calculateSideness(Node a, Node b, Node c, Node d) {
751        final double ndx = b.getCoor().getX() - a.getCoor().getX();
752        final double pdx = d.getCoor().getX() - c.getCoor().getX();
753        final double ndy = b.getCoor().getY() - a.getCoor().getY();
754        final double pdy = d.getCoor().getY() - c.getCoor().getY();
755
756        return (ndx * pdx + ndy * pdy)
757                / Math.sqrt((ndx * ndx + ndy * ndy) * (pdx * pdx + pdy * pdy));
758    }
759
760    /**
761     * Creates a new node at the interpolated position between the argument
762     * nodes. Interpolates linearly in projected coordinates.
763     *
764     * If new node coordinate matches a or b coordinates, a or b is returned.
765     *
766     * @param a First node, at which f=0.
767     * @param b Last node, at which f=1.
768     * @param f Fractional position between first and last nodes.
769     * @return A new node at the interpolated position (or a or b in case if f ≈ 0 or f ≈ 1).
770     */
771    private Node interpolateNode(Node a, Node b, double f) {
772        Node n = new Node(a.getEastNorth().interpolate(b.getEastNorth(), f));
773        if (n.getCoor().equalsEpsilon(a.getCoor()))
774            return a;
775        if (n.getCoor().equalsEpsilon(b.getCoor()))
776            return b;
777        return n;
778    }
779
780    @Override
781    protected void updateEnabledState() {
782        setEnabled(getLayerManager().getEditDataSet() != null);
783    }
784}
Note: See TracBrowser for help on using the repository browser.