Ignore:
Timestamp:
2009-03-06T19:55:38+01:00 (16 years ago)
Author:
zere
Message:

New version of Terracer plugin. Supports non-quadrilateral terraces and also fills out Karlsruhe schema tags automatically.

Location:
applications/editors/josm/plugins/terracer/src/terracer
Files:
1 added
2 edited

Legend:

Unmodified
Added
Removed
  • applications/editors/josm/plugins/terracer/src/terracer/TerracerAction.java

    r13561 r13990  
    99
    1010import static org.openstreetmap.josm.tools.I18n.tr;
    11 
     11import static org.openstreetmap.josm.tools.I18n.trn;
     12
     13import java.awt.BorderLayout;
     14import java.awt.Choice;
     15import java.awt.Component;
     16import java.awt.GridBagLayout;
    1217import java.awt.event.ActionEvent;
     18import java.awt.event.ItemEvent;
     19import java.awt.event.ItemListener;
    1320import java.awt.event.KeyEvent;
     21import java.util.ArrayList;
     22import java.util.Arrays;
    1423import java.util.Collection;
     24import java.util.Collections;
     25import java.util.HashMap;
    1526import java.util.LinkedList;
    16 
     27import java.util.List;
     28import java.util.Map;
     29import java.util.TreeMap;
     30import java.util.TreeSet;
     31
     32import javax.swing.JComponent;
     33import javax.swing.JFormattedTextField;
     34import javax.swing.JLabel;
    1735import javax.swing.JOptionPane;
     36import javax.swing.JPanel;
     37import javax.swing.JSpinner;
     38import javax.swing.JTextField;
     39import javax.swing.SpinnerNumberModel;
     40import javax.swing.SpringLayout;
     41import javax.swing.event.ChangeEvent;
     42import javax.swing.event.ChangeListener;
    1843
    1944import org.openstreetmap.josm.Main;
     
    2550import org.openstreetmap.josm.data.osm.Node;
    2651import org.openstreetmap.josm.data.osm.OsmPrimitive;
     52import org.openstreetmap.josm.data.osm.Relation;
     53import org.openstreetmap.josm.data.osm.RelationMember;
    2754import org.openstreetmap.josm.data.osm.Way;
     55import org.openstreetmap.josm.gui.tagging.TaggingPreset.Item;
     56import org.openstreetmap.josm.tools.AutoCompleteComboBox;
     57import org.openstreetmap.josm.tools.GBC;
     58import org.openstreetmap.josm.tools.Pair;
    2859import org.openstreetmap.josm.tools.Shortcut;
    2960
     
    4071public final class TerracerAction extends JosmAction {
    4172
     73        // smsms1 asked for the last value to be remembered to make it easier to do
     74        // repeated terraces. this is the easiest, but not necessarily nicest, way.
     75        private static String lastSelectedValue = "";
     76
    4277        public TerracerAction() {
    4378                super(tr("Terrace a building"),
     
    65100                                Way way = (Way)prim;
    66101
    67                                 if ((way.nodes.size() == 5) &&
     102                                if ((way.nodes.size() >= 5) &&
    68103                                                way.isClosed()) {
    69104                                        // first ask the user how many buildings to terrace into
    70                                         String answer =
    71                                                 JOptionPane.showInputDialog(
    72                                                                 tr("How many buildings are in the terrace?"));
    73                                        
    74                                         // if the answer was null then the user clicked "Cancel"
    75                                         if (answer != null) {
    76                                                 int nb = Integer.parseInt(answer);
    77                                                 terraceBuilding(way, nb);
     105                                        HouseNumberDialog dialog = new HouseNumberDialog();
     106                                        final JOptionPane optionPane = new JOptionPane(dialog, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION);
     107
     108                                        String title = trn("Change {0} object", "Change {0} objects", sel.size(), sel.size());
     109                                        if(sel.size() == 0)
     110                                                title = tr("Nothing selected!");
     111
     112                                        optionPane.createDialog(Main.parent, title).setVisible(true);
     113                                        Object answerObj = optionPane.getValue();
     114                                        if (answerObj != null &&
     115                                                        answerObj != JOptionPane.UNINITIALIZED_VALUE &&
     116                                                        (answerObj instanceof Integer &&
     117                                                                        (Integer)answerObj == JOptionPane.OK_OPTION)) {
     118
     119                                                // call out to the method which does the actual
     120                                                // terracing.
     121                                                terraceBuilding(way,
     122                                                                dialog.numberFrom(),
     123                                                                dialog.numberTo(),
     124                                                                dialog.stepSize(),
     125                                                                dialog.streetName());
     126
    78127                                        }
    79128                                } else {
     
    89138                if (badSelect) {
    90139                        JOptionPane.showMessageDialog(Main.parent,
    91                                         tr("Select a single, closed way of four nodes."));
     140                                        tr("Select a single, closed way of at least four nodes."));
    92141                }
    93142        }
     
    102151         *
    103152         * @param w The closed, quadrilateral way to terrace.
    104          * @param nb The number of buildings to terrace into.
    105          */
    106         private void terraceBuilding(Way w, int nb) {
     153         */
     154        private void terraceBuilding(Way w, int from, int to, int step, String streetName) {
     155                final int nb = 1 + (to - from) / step;
    107156
    108157                // now find which is the longest side connecting the first node
    109                 Node[] nodes = w.nodes.toArray(new Node[] {});
    110                 double side1 = nodes[0].coor.greatCircleDistance(nodes[1].coor);
    111                 double side2 = nodes[0].coor.greatCircleDistance(nodes[3].coor);
    112                
     158                Pair<Way,Way> interp = findFrontAndBack(w);
     159
     160                final double frontLength = wayLength(interp.a);
     161                final double backLength = wayLength(interp.b);
     162
    113163                // new nodes array to hold all intermediate nodes
    114164                Node[][] new_nodes = new Node[2][nb + 1];
    115                
    116                 if (side1 > side2) {
    117                         new_nodes[0][0] = nodes[0];
    118                         new_nodes[0][nb] = nodes[1];
    119                         new_nodes[1][0] = nodes[3];
    120                         new_nodes[1][nb] = nodes[2];
    121                 } else {
    122                         new_nodes[0][0] = nodes[0];
    123                         new_nodes[0][nb] = nodes[3];
    124                         new_nodes[1][0] = nodes[1];
    125                         new_nodes[1][nb] = nodes[2];
    126                 }
    127                        
    128         Collection<Command> commands = new LinkedList<Command>();
    129         Collection<Way> ways = new LinkedList<Way>();
    130 
    131         // create intermediate nodes by interpolating.
    132         for (int i = 1; i < nb; ++i) {
    133                         new_nodes[0][i] = interpolateNode(new_nodes[0][0], new_nodes[0][nb],
    134                                         (double)(i)/(double)(nb));
    135                         new_nodes[1][i] = interpolateNode(new_nodes[1][0], new_nodes[1][nb],
    136                                         (double)(i)/(double)(nb));
     165
     166                Collection<Command> commands = new LinkedList<Command>();
     167                Collection<Way> ways = new LinkedList<Way>();
     168
     169                // create intermediate nodes by interpolating.
     170                for (int i = 0; i <= nb; ++i) {
     171                        new_nodes[0][i] = interpolateAlong(interp.a, frontLength * (double)(i) / (double)(nb));
     172                        new_nodes[1][i] = interpolateAlong(interp.b, backLength * (double)(i) / (double)(nb));
    137173                        commands.add(new AddCommand(new_nodes[0][i]));
    138174                        commands.add(new AddCommand(new_nodes[1][i]));
    139175                }
    140176
    141         // assemble new quadrilateral, closed ways
    142         for (int i = 0; i < nb; ++i) {
    143                 Way terr = new Way();
    144                 // Using Way.nodes.add rather than Way.addNode because the latter doesn't
    145                 // exist in older versions of JOSM.
    146                 terr.nodes.add(new_nodes[0][i]);
    147                 terr.nodes.add(new_nodes[0][i+1]);
    148                 terr.nodes.add(new_nodes[1][i+1]);
    149                 terr.nodes.add(new_nodes[1][i]);
    150                 terr.nodes.add(new_nodes[0][i]);
    151                 ways.add(terr);
    152                 commands.add(new AddCommand(terr));
    153         }
    154        
    155         Main.main.undoRedo.add(new SequenceCommand(tr("Terrace"), commands));
    156         Main.ds.setSelected(ways);
     177                // create a new relation for addressing
     178                Relation relatedStreet = new Relation();
     179                relatedStreet.put("type", "relatedStreet");
     180                if (streetName != null) {
     181                        relatedStreet.put("name", streetName);
     182                }
     183                // note that we don't actually add the street member to the relation, as
     184                // the name isn't unambiguous and it could cause confusion if the editor were
     185                // to automatically select one which wasn't the one the user intended.
     186
     187                // assemble new quadrilateral, closed ways
     188                for (int i = 0; i < nb; ++i) {
     189                        Way terr = new Way();
     190                        // Using Way.nodes.add rather than Way.addNode because the latter doesn't
     191                        // exist in older versions of JOSM.
     192                        terr.nodes.add(new_nodes[0][i]);
     193                        terr.nodes.add(new_nodes[0][i+1]);
     194                        terr.nodes.add(new_nodes[1][i+1]);
     195                        terr.nodes.add(new_nodes[1][i]);
     196                        terr.nodes.add(new_nodes[0][i]);
     197                        terr.put("addr:housenumber", "" + (from + i * step));
     198                        terr.put("building", "yes");
     199                        if (streetName != null) {
     200                                terr.put("addr:street", streetName);
     201                        }
     202                        relatedStreet.members.add(new RelationMember("house", terr));
     203                        ways.add(terr);
     204                        commands.add(new AddCommand(terr));
     205                }
     206
     207                commands.add(new AddCommand(relatedStreet));
     208
     209                Main.main.undoRedo.add(new SequenceCommand(tr("Terrace"), commands));
     210                Main.ds.setSelected(ways);
     211        }
     212
     213        /**
     214         * Creates a node at a certain distance along a way, as calculated by the
     215         * great circle distance.
     216         *
     217         * Note that this really isn't an efficient way to do this and leads to
     218         * O(N^2) running time for the main algorithm, but its simple and easy
     219         * to understand, and probably won't matter for reasonable-sized ways.
     220         *
     221         * @param w The way to interpolate.
     222         * @param l The length at which to place the node.
     223         * @return A node at a distance l along w from the first point.
     224         */
     225        private Node interpolateAlong(Way w, double l) {
     226                Node n = null;
     227                for (Pair<Node,Node> p : w.getNodePairs(false)) {
     228                        final double seg_length = p.a.coor.greatCircleDistance(p.b.coor);
     229                        if (l <= seg_length) {
     230                                n = interpolateNode(p.a, p.b, l / seg_length);
     231                                break;
     232                        } else {
     233                                l -= seg_length;
     234                        }
     235                }
     236                if (n == null) {
     237                        // sometimes there is a small overshoot due to numerical roundoff, so we just
     238                        // set these cases to be equal to the last node. its not pretty, but it works ;-)
     239                        n = w.nodes.get(w.nodes.size() - 1);
     240                }
     241                return n;
     242        }
     243
     244        /**
     245         * Calculates the great circle length of a way by summing the great circle
     246         * distance of each pair of nodes.
     247         *
     248         * @param w The way to calculate length of.
     249         * @return The length of the way.
     250         */
     251        private double wayLength(Way w) {
     252                double length = 0.0;
     253                for (Pair<Node,Node> p : w.getNodePairs(false)) {
     254                        length += p.a.coor.greatCircleDistance(p.b.coor);
     255                }
     256                return length;
     257        }
     258
     259        /**
     260         * Given a way, try and find a definite front and back by looking at the
     261         * segments to find the "sides". Sides are assumed to be single segments
     262         * which cannot be contiguous.
     263         *
     264         * @param w The way to analyse.
     265         * @return A pair of ways (front, back) pointing in the same directions.
     266         */
     267        private Pair<Way, Way> findFrontAndBack(Way w) {
     268                // calculate the "side-ness" score for each segment of the way
     269                double[] sideness = calculateSideness(w);
     270
     271                // find the largest two sidenesses which are not contiguous
     272                int[] indexes = sortedIndexes(sideness);
     273                int side1 = indexes[0];
     274                int side2 = indexes[1];
     275                // if side2 is contiguous with side1 then look further down the
     276                // list. we know there are at least 4 sides, as anything smaller
     277                // than a quadrilateral would have been rejected at an earlier
     278                // stage.
     279                if (Math.abs(side1 - side2) < 2) {
     280                        side2 = indexes[2];
     281                }
     282                if (Math.abs(side1 - side2) < 2) {
     283                        side2 = indexes[3];
     284                }
     285
     286                // swap side1 and side2 into sorted order.
     287                if (side1 > side2) {
     288                        // i can't believe i have to write swap() myself - surely java standard
     289                        // library has this somewhere??!!?ONE!
     290                        int tmp = side2;
     291                        side2 = side1;
     292                        side1 = tmp;
     293                }
     294
     295                Way front = new Way();
     296                Way back = new Way();
     297                for (int i = side2 + 1; i < w.nodes.size() - 1; ++i) {
     298                        front.nodes.add(w.nodes.get(i));
     299                }
     300                for (int i = 0; i <= side1; ++i) {
     301                        front.nodes.add(w.nodes.get(i));
     302                }
     303                // add the back in reverse order so that the front and back ways point
     304                // in the same direction.
     305                for (int i = side2; i > side1; --i) {
     306                        back.nodes.add(w.nodes.get(i));
     307                }
     308
     309                return new Pair<Way, Way>(front, back);
     310        }
     311
     312        /**
     313         * Given an array of doubles (but this could made generic very easily) sort
     314         * into order and return the array of indexes such that, for a returned array
     315         * x, a[x[i]] is sorted for ascending index i.
     316         *
     317         * This isn't efficient at all, but should be fine for the small arrays we're
     318         * expecting. If this gets slow - replace it with some more efficient algorithm.
     319         *
     320         * @param a The array to sort.
     321         * @return An array of indexes, the same size as the input, such that a[x[i]]
     322         * is in sorted order.
     323         */
     324        private int[] sortedIndexes(final double[] a) {
     325                class SortWithIndex implements Comparable<SortWithIndex> {
     326                        public double x;
     327                        public int i;
     328                        public SortWithIndex(double a, int b) {
     329                                x = a; i = b;
     330                        }
     331                        public int compareTo(SortWithIndex o) {
     332                                return Double.compare(x, o.x);
     333                        };
     334                }
     335
     336                final int length = a.length;
     337                ArrayList<SortWithIndex> sortable = new ArrayList<SortWithIndex>(length);
     338                for (int i = 0; i < length; ++i) {
     339                        sortable.add(new SortWithIndex(a[i], i));
     340                }
     341                Collections.sort(sortable);
     342
     343                int[] indexes = new int[length];
     344                for (int i = 0; i < length; ++i) {
     345                        indexes[i] = sortable.get(i).i;
     346                }
     347
     348                return indexes;
     349        }
     350
     351        /**
     352         * Calculate "sideness" metric for each segment in a way.
     353         */
     354        private double[] calculateSideness(Way w) {
     355                final int length = w.nodes.size() - 1;
     356                double[] sideness = new double[length];
     357
     358                sideness[0] = calculateSideness(
     359                                w.nodes.get(length - 1), w.nodes.get(0),
     360                                w.nodes.get(1), w.nodes.get(2));
     361                for (int i = 1; i < length - 1; ++i) {
     362                        sideness[i] = calculateSideness(
     363                                        w.nodes.get(i-1), w.nodes.get(i),
     364                                        w.nodes.get(i+1), w.nodes.get(i+2));
     365                }
     366                sideness[length-1] = calculateSideness(
     367                                w.nodes.get(length - 2), w.nodes.get(length - 1),
     368                                w.nodes.get(length), w.nodes.get(1));
     369
     370                return sideness;
     371        }
     372
     373        /**
     374         * Calculate sideness of a single segment given the nodes which make up that
     375         * segment and its previous and next segments in order. Sideness is calculated
     376         * for the segment b-c.
     377         */
     378        private double calculateSideness(Node a, Node b, Node c, Node d) {
     379                final double ndx = b.coor.getX() - a.coor.getX();
     380                final double pdx = d.coor.getX() - c.coor.getX();
     381                final double ndy = b.coor.getY() - a.coor.getY();
     382                final double pdy = d.coor.getY() - c.coor.getY();
     383
     384                return (ndx * pdx + ndy * pdy) /
     385                Math.sqrt((ndx * ndx + ndy * ndy) * (pdx * pdx + pdy * pdy));
     386        }
     387
     388        /**
     389         * Dialog box to allow users to input housenumbers in a nice way.
     390         */
     391        class HouseNumberDialog extends JPanel {
     392                private SpinnerNumberModel lo, hi;
     393                private JSpinner clo, chi;
     394                private Choice step;
     395                private AutoCompleteComboBox street;
     396
     397                public HouseNumberDialog() {
     398                        super(new GridBagLayout());
     399                        lo = new SpinnerNumberModel(1, 1, 1, 1);
     400                        hi = new SpinnerNumberModel(1, 1, null, 1);
     401                        step = new Choice();
     402                        step.add(tr("All"));
     403                        step.add(tr("Even"));
     404                        step.add(tr("Odd"));
     405                        clo = new JSpinner(lo);
     406                        chi = new JSpinner(hi);
     407
     408                        lo.addChangeListener(new ChangeListener() {
     409                                public void stateChanged(ChangeEvent e) {
     410                                        hi.setMinimum((Integer)lo.getNumber());
     411                                }
     412                        });
     413                        hi.addChangeListener(new ChangeListener() {
     414                                public void stateChanged(ChangeEvent e) {
     415                                        lo.setMaximum((Integer)hi.getNumber());
     416                                }
     417                        });
     418                        step.addItemListener(new ItemListener() {
     419                                public void itemStateChanged(ItemEvent e) {
     420                                        if (step.getSelectedItem() == tr("All")) {
     421                                                hi.setStepSize(1);
     422                                                lo.setStepSize(1);
     423                                        } else {
     424                                                int odd_or_even = 0;
     425                                                int min = 0;
     426
     427                                                if (step.getSelectedItem() == tr("Even")) {
     428                                                        odd_or_even = 0;
     429                                                        min = 2;
     430                                                } else {
     431                                                        odd_or_even = 1;
     432                                                        min = 1;
     433                                                }
     434
     435                                                if ((lo.getNumber().intValue() & 1) != odd_or_even) {
     436                                                        int nextval = lo.getNumber().intValue() - 1;
     437                                                        lo.setValue((nextval > min) ? nextval : min);
     438                                                }
     439                                                if ((hi.getNumber().intValue() & 1) != odd_or_even) {
     440                                                        int nextval = hi.getNumber().intValue() - 1;
     441                                                        hi.setValue((nextval > min) ? nextval : min);
     442                                                }
     443                                                lo.setMinimum(min);
     444                                                hi.setStepSize(2);
     445                                                lo.setStepSize(2);
     446                                        }
     447                                }
     448                        });
     449
     450                        final TreeSet<String> names = createAutoCompletionInfo();
     451
     452                        street = new AutoCompleteComboBox();
     453                        street.setPossibleItems(names);
     454                        street.setEditable(true);
     455                        street.setSelectedItem(null);
     456
     457                        JFormattedTextField x;
     458                        x = ((JSpinner.DefaultEditor)clo.getEditor()).getTextField();
     459                        x.setColumns(5);
     460                        x = ((JSpinner.DefaultEditor)chi.getEditor()).getTextField();
     461                        x.setColumns(5);
     462                        addLabelled("From number: ", clo);
     463                        addLabelled("To number: ",   chi);
     464                        addLabelled("Interpolation: ", step);
     465                        addLabelled("Street name (Optional): ", street);
     466                }
     467
     468                private void addLabelled(String str, Component c) {
     469                        JLabel label = new JLabel(str);
     470                        add(label, GBC.std());
     471                        label.setLabelFor(c);
     472                        add(c, GBC.eol());
     473                }
     474
     475                public int numberFrom() {
     476                        return lo.getNumber().intValue();
     477                }
     478
     479                public int numberTo() {
     480                        return hi.getNumber().intValue();
     481                }
     482
     483                public int stepSize() {
     484                        return (step.getSelectedItem() == tr("All")) ? 1 : 2;
     485                }
     486
     487                public String streetName() {
     488                        Object selected = street.getSelectedItem();
     489                        if (selected == null) {
     490                                return null;
     491                        } else {
     492                                String name = selected.toString();
     493                                if (name.isEmpty()) {
     494                                        return null;
     495                                } else {
     496                                        return name;
     497                                }
     498                        }
     499                }
     500        }
     501
     502        /**
     503         * Generates a list of all visible names of highways in order to do
     504         * autocompletion on the road name.
     505         */
     506        private TreeSet<String> createAutoCompletionInfo() {
     507                final TreeSet<String> names = new TreeSet<String>();
     508                for (OsmPrimitive osm : Main.ds.allNonDeletedPrimitives()) {
     509                        if (osm.keys != null &&
     510                                        osm.keys.containsKey("highway") &&
     511                                        osm.keys.containsKey("name")) {
     512                                names.add(osm.keys.get("name"));
     513                        }
     514                }
     515                return names;
    157516        }
    158517
     
    167526         */
    168527        private Node interpolateNode(Node a, Node b, double f) {
    169         // this isn't quite right - we should probably be interpolating
     528                Node n = new Node(interpolateLatLon(a, b, f));
     529                return n;
     530        }
     531
     532        /**
     533         * Calculates the interpolated position between the argument nodes. Interpolates
     534         * linearly in Lat/Lon coordinates.
     535         *
     536         * @param a First node, at which f=0.
     537         * @param b Last node, at which f=1.
     538         * @param f Fractional position between first and last nodes.
     539         * @return The interpolated position.
     540         */
     541        private LatLon interpolateLatLon(Node a, Node b, double f) {
     542                // this isn't quite right - we should probably be interpolating
    170543                // screen coordinates rather than lat/lon, but it doesn't seem to
    171544                // make a great deal of difference at the scale of most terraces.
    172                 Node n = new Node(new LatLon(
    173                                 a.coor.lat() * (1.0 - f) + b.coor.lat() * f,
    174                                 a.coor.lon() * (1.0 - f) + b.coor.lon() * f
    175                                 ));
    176                 return n;
     545                return new LatLon(a.coor.lat() * (1.0 - f) + b.coor.lat() * f,
     546                                a.coor.lon() * (1.0 - f) + b.coor.lon() * f);
    177547        }
    178548}
  • applications/editors/josm/plugins/terracer/src/terracer/TerracerPlugin.java

    r13553 r13990  
    2020        public TerracerPlugin() {
    2121                MainMenu.add(Main.main.menu.toolsMenu, new TerracerAction());
     22                MainMenu.add(Main.main.menu.toolsMenu, new ReverseTerraceAction());
    2223        }
    2324}
Note: See TracChangeset for help on using the changeset viewer.