Ticket #67: parallel-way-action.0.3.patch

File parallel-way-action.0.3.patch, 34.9 KB (added by olejorgenb, 10 years ago)

Initial cursor support, error message on invalid selection, code cleanup, more customization preferences

  • .settings/org.eclipse.jdt.core.prefs

    diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
    index 75908f5..015e084 100644
    a b  
    1 #Sun May 23 17:35:27 CEST 2010
     1#Fri May 27 00:19:47 CEST 2011
    22eclipse.preferences.version=1
    33org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
    44org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
    org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning 
    7474org.eclipse.jdt.core.compiler.source=1.6
    7575org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
    7676org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
     77org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0
    7778org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
    7879org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
    7980org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
    org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 
    8485org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80
    8586org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0
    8687org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
     88org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0
    8789org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
    8890org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
    8991org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
    org.eclipse.jdt.core.formatter.comment.indent_root_tags=false 
    130132org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=do not insert
    131133org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert
    132134org.eclipse.jdt.core.formatter.comment.line_length=100
     135org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true
     136org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true
    133137org.eclipse.jdt.core.formatter.compact_else_if=true
    134138org.eclipse.jdt.core.formatter.continuation_indentation=2
    135139org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
     140org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off
     141org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
    136142org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
     143org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true
    137144org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
    138145org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
    139146org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
    org.eclipse.jdt.core.formatter.insert_new_line_after_annotation=do not insert 
    149156org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
    150157org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member=insert
    151158org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
     159org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert
    152160org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
    153161org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert
    154162org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
    org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constan 
    318326org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
    319327org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
    320328org.eclipse.jdt.core.formatter.join_lines_in_comments=true
    321 org.eclipse.jdt.core.formatter.join_wrapped_lines=true
     329org.eclipse.jdt.core.formatter.join_wrapped_lines=false
    322330org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
    323331org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
    324332org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false
    org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 
    331339org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true
    332340org.eclipse.jdt.core.formatter.tabulation.char=space
    333341org.eclipse.jdt.core.formatter.tabulation.size=4
     342org.eclipse.jdt.core.formatter.use_on_off_tags=true
    334343org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
    335344org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
     345org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true
  • new file src/org/openstreetmap/josm/actions/mapmode/ParallelWayAction.java

    diff --git a/images/cursor/modifier/selection_add_element.png b/images/cursor/modifier/selection_add_element.png
    new file mode 100644
    index 0000000..0ca9b67
    Binary files /dev/null and b/images/cursor/modifier/selection_add_element.png differ
    diff --git a/images/cursor/modifier/selection_toggle_element.png b/images/cursor/modifier/selection_toggle_element.png
    new file mode 100644
    index 0000000..43fd97b
    Binary files /dev/null and b/images/cursor/modifier/selection_toggle_element.png differ
    diff --git a/images/mapmode/parallel.png b/images/mapmode/parallel.png
    new file mode 100644
    index 0000000..a02c6fc
    Binary files /dev/null and b/images/mapmode/parallel.png differ
    diff --git a/src/org/openstreetmap/josm/actions/mapmode/ParallelWayAction.java b/src/org/openstreetmap/josm/actions/mapmode/ParallelWayAction.java
    new file mode 100644
    index 0000000..6a8d486
    - +  
     1// License: GPL. Copyright 2007 by Immanuel Scholz and others
     2
     3package org.openstreetmap.josm.actions.mapmode;
     4
     5import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
     6import static org.openstreetmap.josm.tools.I18n.tr;
     7
     8import java.awt.AWTEvent;
     9import java.awt.Cursor;
     10import java.awt.Point;
     11import java.awt.Toolkit;
     12import java.awt.event.AWTEventListener;
     13import java.awt.event.ActionEvent;
     14import java.awt.event.InputEvent;
     15import java.awt.event.KeyEvent;
     16import java.awt.event.MouseEvent;
     17import java.util.ArrayList;
     18import java.util.Collections;
     19import java.util.HashMap;
     20import java.util.List;
     21
     22import javax.swing.JOptionPane;
     23
     24import org.openstreetmap.josm.Main;
     25import org.openstreetmap.josm.actions.CombineWayAction;
     26import org.openstreetmap.josm.command.AddCommand;
     27import org.openstreetmap.josm.command.Command;
     28import org.openstreetmap.josm.command.SequenceCommand;
     29import org.openstreetmap.josm.data.coor.EastNorth;
     30import org.openstreetmap.josm.data.osm.DataSet;
     31import org.openstreetmap.josm.data.osm.Node;
     32import org.openstreetmap.josm.data.osm.OsmPrimitive;
     33import org.openstreetmap.josm.data.osm.Way;
     34import org.openstreetmap.josm.data.osm.WaySegment;
     35import org.openstreetmap.josm.gui.MapFrame;
     36import org.openstreetmap.josm.gui.MapView;
     37import org.openstreetmap.josm.gui.layer.Layer;
     38import org.openstreetmap.josm.gui.layer.OsmDataLayer;
     39import org.openstreetmap.josm.tools.Geometry;
     40import org.openstreetmap.josm.tools.ImageProvider;
     41import org.openstreetmap.josm.tools.Shortcut;
     42
     43//// TODO: (list below)
     44/* == Functionality ==
     45 *
     46 * 1. Use selected nodes as split points for the selected ways.
     47 *
     48 * The ways containing the selected nodes will be split and only the "inner"
     49 * parts will be copied
     50 *
     51 * 2. Enter exact offset
     52 *
     53 * 3. Improve snapping
     54 *
     55 * Need at least a setting for step length
     56 *
     57 * 4. Visual cues? Highlight source path, draw offset line, etc?
     58 *
     59 * 5. Cursors (Half-done)
     60 *
     61 * 6. (long term) Parallelize and adjust offsets of existing ways
     62 *
     63 * == Code quality ==
     64 *
     65 * a) The mode, flags, and modifiers might be updated more than necessary.
     66 *
     67 * Not a performance problem, but better if they where more centralized
     68 *
     69 * b) Extract generic MapMode services into a super class and/or utility class
     70 */
     71
     72/**
     73 * MapMode for making parallel ways.
     74 *
     75 * All calculations are done in projected coordinates.
     76 *
     77 * @author Ole Jørgen Brønner (olejorgenb)
     78 */
     79public class ParallelWayAction extends MapMode implements AWTEventListener {
     80
     81    private enum Mode {
     82        dragging, normal
     83    }
     84
     85    //// Preferences and flags
     86    // See updateModeLocalPreferences for defaults
     87    private Mode mode;
     88    private boolean copyTags;
     89    private boolean copyTagsDefault;
     90
     91    private boolean snap;
     92    private boolean snapDefault;
     93
     94    private double snapThreshold;
     95
     96    private ModifiersSpec snapModiferCombo;
     97    private ModifiersSpec copyTagsModiferCombo;
     98    private ModifiersSpec addToSelectionModifierCombo;
     99    private ModifiersSpec toggleSelectedModifierCombo;
     100    private ModifiersSpec setSelectedModifierCombo;
     101
     102    private int initialMoveDelay;
     103
     104    private final MapView mv;
     105
     106    private boolean ctrl;
     107    private boolean alt;
     108    private boolean shift;
     109
     110    // Mouse tracking state
     111    private Point mousePressedPos;
     112    private boolean mouseIsDown;
     113    private long mousePressedTime;
     114    private boolean mouseHasBeenDragged;
     115
     116    private WaySegment referenceSegment;
     117    private ParallelWays pWays;
     118
     119    public ParallelWayAction(MapFrame mapFrame) {
     120        super(tr("Parallel"), "parallel", tr("Makes a paralell copy of the selected way(s)"), Shortcut
     121                .registerShortcut("mapmode:parallel", tr("Mode: {0}", tr("Parallel")), KeyEvent.VK_P,
     122                        Shortcut.GROUP_EDIT, Shortcut.SHIFT_DEFAULT), mapFrame, ImageProvider.getCursor("normal",
     123                        "selection"));
     124        putValue("help", ht("/Action/Parallel"));
     125        mv = mapFrame.mapView;
     126        updateModeLocalPreferences();
     127    }
     128
     129    @Override
     130    public void enterMode() {
     131        // super.enterMode() updates the status line and cursor so we need our state to be set correctly
     132        setMode(Mode.normal);
     133        pWays = null;
     134        updateAllPreferences(); // All default values should've been set now
     135
     136        super.enterMode();
     137
     138        mv.addMouseListener(this);
     139        mv.addMouseMotionListener(this);
     140
     141        //// Needed to update the mouse cursor if modifiers are changed when the mouse is motionless
     142        try {
     143            Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
     144        } catch (SecurityException ex) {
     145        }
     146    }
     147
     148    @Override
     149    public void exitMode() {
     150        super.exitMode();
     151        mv.removeMouseListener(this);
     152        mv.removeMouseMotionListener(this);
     153        Main.map.statusLine.setDist(-1);
     154        Main.map.statusLine.repaint();
     155        try {
     156            Toolkit.getDefaultToolkit().removeAWTEventListener(this);
     157        } catch (SecurityException ex) {
     158        }
     159        pWays = null;
     160    }
     161
     162    @Override
     163    public String getModeHelpText() {
     164        // TODO: add more detailed feedback based on modifier state.
     165        // TODO: dynamic messages based on preferences. (Could be problematic translation wise)
     166        switch (mode) {
     167        case normal:
     168            return tr("Select ways as in Select mode. Drag selected ways or a single way to create a parallel copy (Alt toggles tag preservation)");
     169        case dragging:
     170            return tr("Hold Ctrl to toggle snapping");
     171        }
     172        return ""; // impossible ..
     173    }
     174
     175    // Separated due to "race condition" between default values
     176    private void updateAllPreferences() {
     177        updateModeLocalPreferences();
     178        // @formatter:off
     179        initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay", -1 /* default set in owner */);
     180        // @formatter:on
     181    }
     182
     183    private void updateModeLocalPreferences() {
     184        // @formatter:off
     185        snapThreshold   = Main.pref.getDouble (prefKey("snap-threshold"), 0.35);
     186        snapDefault     = Main.pref.getBoolean(prefKey("snap-default"),      true);
     187        copyTagsDefault = Main.pref.getBoolean(prefKey("copy-tags-default"), true);
     188
     189        snapModiferCombo            = new ModifiersSpec(getStringPref("snap-modifer-combo",              "?sC"));
     190        copyTagsModiferCombo        = new ModifiersSpec(getStringPref("copy-tags-modifier-combo",        "As?"));
     191        addToSelectionModifierCombo = new ModifiersSpec(getStringPref("add-to-selection-modifier-combo", "aSc"));
     192        toggleSelectedModifierCombo = new ModifiersSpec(getStringPref("toggle-selection-modifier-combo", "asC"));
     193        setSelectedModifierCombo    = new ModifiersSpec(getStringPref("set-selection-modifier-combo",    "asc"));
     194        // @formatter:on
     195    }
     196
     197    @Override
     198    public boolean layerIsSupported(Layer layer) {
     199        return layer instanceof OsmDataLayer;
     200    }
     201
     202    @Override
     203    public void eventDispatched(AWTEvent e) {
     204        if (Main.map == null || mv == null || !mv.isActiveLayerDrawable())
     205            return;
     206
     207        // Should only get InputEvents due to the mask in enterMode
     208        if (updateModifiersState((InputEvent) e)) {
     209            updateStatusLine();
     210            updateCursor();
     211        }
     212    }
     213
     214    private boolean updateModifiersState(InputEvent e) {
     215        boolean oldAlt = alt, oldShift = shift, oldCtrl = ctrl;
     216        alt = (e.getModifiers() & (ActionEvent.ALT_MASK | InputEvent.ALT_GRAPH_MASK)) != 0;
     217        ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
     218        shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
     219        boolean changed = (oldAlt != alt || oldShift != shift || oldCtrl != ctrl);
     220        return changed;
     221    }
     222
     223    private void updateCursor() {
     224        Cursor newCursor = null;
     225        switch (mode) {
     226        case normal:
     227            if (setSelectedModifierCombo.matchWithKnown(alt, shift, ctrl)) {
     228                newCursor = ImageProvider.getCursor("normal", "selection");
     229            } else if (addToSelectionModifierCombo.matchWithKnown(alt, shift, ctrl)) {
     230                newCursor = ImageProvider.getCursor("normal", "selection_add_element");
     231            } else if (toggleSelectedModifierCombo.matchWithKnown(alt, shift, ctrl)) {
     232                newCursor = ImageProvider.getCursor("normal", "selection_toggle_element");
     233            } else {
     234                // TODO: set to a cursor indicating an error
     235            }
     236            break;
     237        case dragging:
     238            if (snap) {
     239                // TODO: snapping cursor?
     240                newCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
     241            } else {
     242                newCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
     243            }
     244        }
     245        if (newCursor != null) {
     246            mv.setNewCursor(newCursor, this);
     247        }
     248    }
     249
     250    private void setMode(Mode mode) {
     251        this.mode = mode;
     252        updateCursor();
     253        updateStatusLine();
     254    }
     255
     256    private boolean isValidModiferCombination() {
     257        // TODO: implement to give feedback on invalid modifier combination
     258        return true;
     259    }
     260
     261    @Override
     262    public void mousePressed(MouseEvent e) {
     263        updateModifiersState(e);
     264        // Other buttons are off limit, but we still get events.
     265        if (e.getButton() != MouseEvent.BUTTON1)
     266            return;
     267
     268        if (!mv.isActiveLayerVisible())
     269            return;
     270        if (!mv.isActiveLayerDrawable())
     271            return;
     272        if (!(Boolean) this.getValue("active"))
     273            return;
     274
     275        updateFlagsOnlyChangableOnPress();
     276        updateFlagsChangableAlways();
     277
     278        mouseIsDown = true;
     279        mousePressedPos = e.getPoint();
     280        mousePressedTime = System.currentTimeMillis();
     281
     282    }
     283
     284    @Override
     285    public void mouseClicked(MouseEvent e) {
     286    }
     287
     288    @Override
     289    public void mouseReleased(MouseEvent e) {
     290        updateModifiersState(e);
     291        // Other buttons are off limit, but we still get events.
     292        if (e.getButton() != MouseEvent.BUTTON1)
     293            return;
     294
     295        pWays = null;
     296        setMode(Mode.normal);
     297
     298        if (!mouseHasBeenDragged) {
     299            assert (pWays == null);
     300            // use point from press or click event? (or are these always the same)
     301            Way nearestWay = mv.getNearestWay(e.getPoint(), OsmPrimitive.isSelectablePredicate);
     302            if (nearestWay == null) {
     303                if (setSelectedModifierCombo.matchWithKnown(alt, shift, ctrl)) {
     304                    getCurrentDataSet().clearSelection();
     305                }
     306                resetMouseTrackingState();
     307                return;
     308            }
     309            boolean isSelected = nearestWay.isSelected();
     310            if (addToSelectionModifierCombo.matchWithKnown(alt, shift, ctrl)) {
     311                if (!isSelected) {
     312                    getCurrentDataSet().addSelected(nearestWay);
     313                }
     314            } else if (toggleSelectedModifierCombo.matchWithKnown(alt, shift, ctrl)) {
     315                if (isSelected) {
     316                    getCurrentDataSet().clearSelection(nearestWay);
     317                } else {
     318                    getCurrentDataSet().addSelected(nearestWay);
     319                }
     320            } else if (setSelectedModifierCombo.matchWithKnown(alt, shift, ctrl)) {
     321                getCurrentDataSet().setSelected(nearestWay);
     322            } // else -> invalid modifier combination
     323        }
     324        resetMouseTrackingState();
     325    }
     326
     327    @Override
     328    public void mouseDragged(MouseEvent e) {
     329        // WTF.. the event passed here doesn't have button info?
     330        // Since we get this event from other buttons too, we must check that
     331        // _BUTTON1_ is down.
     332        if (!mouseIsDown)
     333            return;
     334
     335        boolean modifersChanged = updateModifiersState(e);
     336        updateFlagsChangableAlways();
     337
     338        if (modifersChanged) {
     339            // Since this could be remotely slow, do it conditionally
     340            updateStatusLine();
     341            updateCursor();
     342        }
     343
     344        if ((System.currentTimeMillis() - mousePressedTime) < initialMoveDelay)
     345            return;
     346        // Assuming this event only is emitted when the mouse has moved
     347        // Setting this after the check above means we tolerate clicks with some movement
     348        mouseHasBeenDragged = true;
     349
     350        Point p = e.getPoint();
     351        if (pWays == null) {
     352            // Should we ensure that the copyTags modifiers are still valid?
     353
     354            // Important to use mouse position from the press, since the drag
     355            // event can come quite late
     356            if (!isModifiersValidForDragMode())
     357                return;
     358            if (!initParallelWays(mousePressedPos, copyTags)) {
     359                // TODO: Not ideal feedback. Maybe changing the cursor could be a good mechanism?
     360                JOptionPane.showMessageDialog(
     361                        Main.parent,
     362                        tr("ParallelWayAction\n" +
     363                                "The ways selected must form a simple branchless path"),
     364                        tr("Make parallel way error"),
     365                        JOptionPane.INFORMATION_MESSAGE);
     366                return;
     367            }
     368            setMode(Mode.dragging);
     369        }
     370
     371        //// Calculate distance to the reference line
     372        EastNorth enp = mv.getEastNorth((int) p.getX(), (int) p.getY());
     373        EastNorth nearestPointOnRefLine = Geometry.closestPointToLine(referenceSegment.getFirstNode().getEastNorth(),
     374                referenceSegment.getSecondNode().getEastNorth(), enp);
     375        double d = enp.distance(nearestPointOnRefLine);
     376        // TODO: abuse of isToTheRightSideOfLine function.
     377        boolean toTheRight = Geometry.isToTheRightSideOfLine(referenceSegment.getFirstNode(),
     378                referenceSegment.getFirstNode(), referenceSegment.getSecondNode(), new Node(enp));
     379
     380        if (snap) {
     381            // TODO: Very simple snapping
     382            // - Snap steps and/or threshold relative to the distance?
     383            long closestWholeUnit = Math.round(d);
     384            if (Math.abs(closestWholeUnit - d) < snapThreshold) {
     385                d = closestWholeUnit;
     386            } else {
     387                d = closestWholeUnit + Math.signum(closestWholeUnit - d) * -0.5;
     388            }
     389        }
     390        if (toTheRight) {
     391            d = -d;
     392        }
     393        pWays.changeOffset(d);
     394
     395        Main.map.statusLine.setDist(Math.abs(d));
     396        Main.map.statusLine.repaint();
     397        mv.repaint();
     398    }
     399
     400    private boolean isModifiersValidForDragMode() {
     401        return (!alt && !shift && !ctrl) || snapModiferCombo.matchWithKnown(alt, shift, ctrl)
     402                || snapModiferCombo.matchWithKnown(alt, shift, ctrl);
     403    }
     404
     405    private void updateFlagsOnlyChangableOnPress() {
     406        copyTags = copyTagsDefault != copyTagsModiferCombo.matchWithKnown(alt, shift, ctrl);
     407    }
     408
     409    private void updateFlagsChangableAlways() {
     410        snap = snapDefault != snapModiferCombo.matchWithKnown(alt, shift, ctrl);
     411    }
     412
     413    private void resetMouseTrackingState() {
     414        mouseIsDown = false;
     415        mousePressedPos = null;
     416        mouseHasBeenDragged = false;
     417    }
     418
     419    // TODO: rename
     420    private boolean initParallelWays(Point p, boolean copyTags) {
     421        referenceSegment = mv.getNearestWaySegment(p, Way.isUsablePredicate, true);
     422        if (referenceSegment == null)
     423            return false;
     424
     425        // The collection returned is very inefficient so we collect it in an ArrayList
     426        // Not sure if the list is iterated multiple times any more...
     427        List<Way> selectedWays = new ArrayList<Way>(getCurrentDataSet().getSelectedWays());
     428        if (!selectedWays.contains(referenceSegment.way)) {
     429            getCurrentDataSet().setSelected(referenceSegment.way);
     430            selectedWays.clear();
     431            selectedWays.add(referenceSegment.way);
     432        }
     433
     434        try {
     435            pWays = new ParallelWays(selectedWays, copyTags, selectedWays.indexOf(referenceSegment.way));
     436            pWays.commit(null);
     437            getCurrentDataSet().setSelected(pWays.ways);
     438            return true;
     439        } catch (IllegalArgumentException e) {
     440//            System.err.println(e);
     441            pWays = null;
     442            return false;
     443        }
     444    }
     445
     446    // TODO: 'ParallelPath' better name?
     447    static final class ParallelWays {
     448        private List<Way> ways;
     449        private List<Node> sortedNodes;
     450
     451        private int nodeCount;
     452
     453        private EastNorth[] pts;
     454        private EastNorth[] normals;
     455
     456        public ParallelWays(List<Way> sourceWays, boolean copyTags, int refWayIndex) {
     457            // Possible/sensible to use PrimetiveDeepCopy here?
     458
     459            //// Make a deep copy of the ways, keeping the copied ways connected
     460            HashMap<Node, Node> splitNodeMap = new HashMap<Node, Node>(sourceWays.size());
     461            for (Way w : sourceWays) {
     462                if (!splitNodeMap.containsKey(w.firstNode())) {
     463                    splitNodeMap.put(w.firstNode(), copyNode(w.firstNode(), copyTags));
     464                }
     465                if (!splitNodeMap.containsKey(w.lastNode())) {
     466                    splitNodeMap.put(w.lastNode(), copyNode(w.lastNode(), copyTags));
     467                }
     468            }
     469            ways = new ArrayList<Way>(sourceWays.size());
     470            for (Way w : sourceWays) {
     471                Way wCopy = new Way();
     472                wCopy.addNode(splitNodeMap.get(w.firstNode()));
     473                for (int i = 1; i < w.getNodesCount() - 1; i++) {
     474                    wCopy.addNode(copyNode(w.getNode(i), copyTags));
     475                }
     476                wCopy.addNode(splitNodeMap.get(w.lastNode()));
     477                if (copyTags) {
     478                    wCopy.setKeys(w.getKeys());
     479                }
     480                ways.add(wCopy);
     481            }
     482            sourceWays = null; // Ensure that we only use the copies from now
     483
     484            //// Find a linear ordering of the nodes. Fail if there isn't one.
     485            CombineWayAction.NodeGraph nodeGraph = CombineWayAction.NodeGraph.createUndirectedGraphFromNodeWays(ways);
     486            sortedNodes = nodeGraph.buildSpanningPath();
     487            if (sortedNodes == null)
     488                throw new IllegalArgumentException("Ways must have spanning path"); // Create a dedicated exception?
     489
     490            //// Ugly method of ensuring that the offset isn't inverted. I'm sure there is a better and more elegant way, but I'm starting to get sleepy, so I do this for now.
     491            {
     492                Way refWay = ways.get(refWayIndex);
     493                boolean refWayReversed = false;
     494                if (isClosedPath()) { // Nodes occur more than once in the list
     495                    if (refWay.firstNode() == sortedNodes.get(0) && refWay.lastNode() == sortedNodes.get(0)) {
     496                        refWayReversed = sortedNodes.get(1) != refWay.getNode(1);
     497                    } else if (refWay.lastNode() == sortedNodes.get(0)) {
     498                        refWayReversed =
     499                                sortedNodes.get(sortedNodes.size() - 1) != refWay.getNode(refWay.getNodesCount() - 1);
     500                    } else if (refWay.firstNode() == sortedNodes.get(0)) {
     501                        refWayReversed = sortedNodes.get(1) != refWay.getNode(1);
     502                    } else {
     503                        refWayReversed =
     504                                sortedNodes.indexOf(refWay.firstNode()) > sortedNodes.indexOf(refWay.lastNode());
     505                    }
     506
     507                } else {
     508                    refWayReversed = sortedNodes.indexOf(refWay.firstNode()) > sortedNodes.indexOf(refWay.lastNode());
     509                }
     510                if (refWayReversed) {
     511                    Collections.reverse(sortedNodes); // need to keep the orientation of the reference way.
     512                    System.err.println("reversed!");
     513                }
     514            }
     515
     516            //// Initialize the required parameters. (segment normals, etc.)
     517            nodeCount = sortedNodes.size();
     518            pts = new EastNorth[nodeCount];
     519            normals = new EastNorth[nodeCount - 1];
     520            int i = 0;
     521            for (Node n : sortedNodes) {
     522                EastNorth t = n.getEastNorth();
     523                pts[i] = t;
     524                i++;
     525            }
     526            for (i = 0; i < nodeCount - 1; i++) {
     527                double dx = pts[i + 1].getX() - pts[i].getX();
     528                double dy = pts[i + 1].getY() - pts[i].getY();
     529                double len = Math.sqrt(dx * dx + dy * dy);
     530                normals[i] = new EastNorth(-dy / len, dx / len);
     531            }
     532        }
     533
     534        public boolean isClosedPath() {
     535            return sortedNodes.get(0) == sortedNodes.get(sortedNodes.size() - 1);
     536        }
     537
     538        public void changeOffset(double d) {
     539            //// This is the core algorithm:
     540            /* 1. Calculate a parallel line, offset by 'd', to each segment in
     541             *    the path
     542             * 2. Find the intersection of lines belonging to neighboring
     543             *    segments. These become the new node positions
     544             * 3. Do some special casing for closed paths
     545             *
     546             * Simple and probably not even close to optimal performance wise
     547             */
     548
     549            EastNorth[] ppts = new EastNorth[nodeCount];
     550
     551            EastNorth prevA = add(pts[0], mul(normals[0], d));
     552            EastNorth prevB = add(pts[1], mul(normals[0], d));
     553            for (int i = 1; i < nodeCount - 1; i++) {
     554                EastNorth A = add(pts[i], mul(normals[i], d));
     555                EastNorth B = add(pts[i + 1], mul(normals[i], d));
     556                if (Geometry.segmentsParallel(A, B, prevA, prevB)) {
     557                    ppts[i] = A;
     558                } else {
     559                    ppts[i] = Geometry.getLineLineIntersection(A, B, prevA, prevB);
     560                }
     561                prevA = A;
     562                prevB = B;
     563            }
     564            if (isClosedPath()) {
     565                EastNorth A = add(pts[0], mul(normals[0], d));
     566                EastNorth B = add(pts[1], mul(normals[0], d));
     567                if (Geometry.segmentsParallel(A, B, prevA, prevB)) {
     568                    ppts[0] = A;
     569                } else {
     570                    ppts[0] = Geometry.getLineLineIntersection(A, B, prevA, prevB);
     571                }
     572                ppts[nodeCount - 1] = ppts[0];
     573            } else {
     574                ppts[0] = add(pts[0], mul(normals[0], d));
     575                ppts[nodeCount - 1] = add(pts[nodeCount - 1], mul(normals[nodeCount - 2], d));
     576            }
     577
     578            for (int i = 0; i < nodeCount; i++) {
     579                sortedNodes.get(i).setEastNorth(ppts[i]);
     580            }
     581        }
     582
     583        // Draw helper lines instead like DrawAction ExtrudeAction?
     584        public void commit(DataSet ds) {
     585            SequenceCommand undoCommand = new SequenceCommand("Make parallel way(s)", makeAddWayAndNodesCommandList());
     586            Main.main.undoRedo.add(undoCommand);
     587        }
     588
     589        private List<Command> makeAddWayAndNodesCommandList() {
     590            ArrayList<Command> commands = new ArrayList<Command>(sortedNodes.size() + ways.size());
     591            for (int i = 0; i < sortedNodes.size() - 1; i++) {
     592                commands.add(new AddCommand(sortedNodes.get(i)));
     593            }
     594            if (!isClosedPath()) {
     595                commands.add(new AddCommand(sortedNodes.get(sortedNodes.size() - 1)));
     596            }
     597            for (Way w : ways) {
     598                commands.add(new AddCommand(w));
     599            }
     600            return commands;
     601        }
     602
     603        static private Node copyNode(Node source, boolean copyTags) {
     604            if (copyTags)
     605                return new Node(source, true);
     606            else {
     607                Node n = new Node();
     608                n.setCoor(source.getCoor());
     609                return n;
     610            }
     611        }
     612
     613        // We need either a dedicated vector type, or operations such as these
     614        // added to EastNorth...
     615        static private EastNorth mul(EastNorth en, double f) {
     616            return new EastNorth(en.getX() * f, en.getY() * f);
     617        }
     618
     619        static private EastNorth add(EastNorth a, EastNorth b) {
     620            return new EastNorth(a.east() + b.east(), a.north() + b.north());
     621        }
     622    }
     623
     624    static final public class ModifiersSpec {
     625        static public final char ON = 1, OFF = 0, UNKNOWN = 0;
     626        public int alt = UNKNOWN;
     627        public int shift = UNKNOWN;
     628        public int ctrl = UNKNOWN;
     629
     630        /**
     631         *  'A' = Alt, 'S' = Shift, 'C' = Ctrl
     632         *  Lowercase signifies off and '?' means unknown/optional.
     633         *  Order is Alt, Shift, Ctrl
     634         * @param str
     635         */
     636        public ModifiersSpec(String str) {
     637            assert (str.length() == 3);
     638            char a = str.charAt(0);
     639            char s = str.charAt(1);
     640            char c = str.charAt(2);
     641            // @formatter:off
     642            alt   = (a == '?' ? UNKNOWN : (a == 'A' ? ON : OFF));
     643            shift = (s == '?' ? UNKNOWN : (s == 'S' ? ON : OFF));
     644            ctrl  = (c == '?' ? UNKNOWN : (c == 'C' ? ON : OFF));
     645            // @formatter:on
     646        }
     647
     648        public ModifiersSpec(final int alt, final int shift, final int ctrl) {
     649            this.alt = alt;
     650            this.shift = shift;
     651            this.ctrl = ctrl;
     652        }
     653
     654        public boolean matchWithKnown(final int knownAlt, final int knownShift, final int knownCtrl) {
     655            return match(alt, knownAlt) && match(shift, knownShift) && match(ctrl, knownCtrl);
     656        }
     657
     658        public boolean matchWithKnown(final boolean knownAlt, final boolean knownShift, final boolean knownCtrl) {
     659            return match(alt, knownAlt) && match(shift, knownShift) && match(ctrl, knownCtrl);
     660        }
     661
     662        private boolean match(final int a, final int knownValue) {
     663            assert (knownValue == ON | knownValue == OFF);
     664            return a == knownValue || a == UNKNOWN;
     665        }
     666
     667        private boolean match(final int a, final boolean knownValue) {
     668            return a == (knownValue ? ON : OFF);
     669        }
     670        // does java have built in 3-state support?
     671    }
     672
     673    private String prefKey(String subKey) {
     674        return "edit.make-parallel-way-action." + subKey;
     675    }
     676
     677    private String getStringPref(String subKey, String def) {
     678        return Main.pref.get(prefKey(subKey), def);
     679    }
     680
     681    private String getStringPref(String subKey) {
     682        return getStringPref(subKey, null);
     683    }
     684}
  • src/org/openstreetmap/josm/gui/MapFrame.java

    diff --git a/src/org/openstreetmap/josm/gui/MapFrame.java b/src/org/openstreetmap/josm/gui/MapFrame.java
    index 96bddae..830c567 100644
    a b import org.openstreetmap.josm.actions.mapmode.DeleteAction; 
    4040import org.openstreetmap.josm.actions.mapmode.DrawAction;
    4141import org.openstreetmap.josm.actions.mapmode.ExtrudeAction;
    4242import org.openstreetmap.josm.actions.mapmode.MapMode;
     43import org.openstreetmap.josm.actions.mapmode.ParallelWayAction;
    4344import org.openstreetmap.josm.actions.mapmode.SelectAction;
    4445import org.openstreetmap.josm.actions.mapmode.ZoomAction;
    4546import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
    public class MapFrame extends JPanel implements Destroyable, LayerChangeListener 
    129130        addMapMode(new IconToggleButton(new SelectAction(this)));
    130131        addMapMode(new IconToggleButton(new DrawAction(this)));
    131132        addMapMode(new IconToggleButton(new ExtrudeAction(this)));
     133        addMapMode(new IconToggleButton(new ParallelWayAction(this)));
    132134        addMapMode(new IconToggleButton(new ZoomAction(this)));
    133135        addMapMode(new IconToggleButton(new DeleteAction(this)));
    134136
  • src/org/openstreetmap/josm/tools/Geometry.java

    diff --git a/src/org/openstreetmap/josm/tools/Geometry.java b/src/org/openstreetmap/josm/tools/Geometry.java
    index ade8359..9930fbb 100644
    a b public class Geometry { 
    332332        else
    333333            return new EastNorth(segmentP1.getX() + ldx * offset, segmentP1.getY() + ldy * offset);
    334334    }
     335    public static EastNorth closestPointToLine(EastNorth lineP1, EastNorth lineP2, EastNorth point) {
     336        double ldx = lineP2.getX() - lineP1.getX();
     337        double ldy = lineP2.getY() - lineP1.getY();
     338
     339        if (ldx == 0 && ldy == 0) //segment zero length
     340            return lineP1;
     341
     342        double pdx = point.getX() - lineP1.getX();
     343        double pdy = point.getY() - lineP1.getY();
     344
     345        double offset = (pdx * ldx + pdy * ldy) / (ldx * ldx + ldy * ldy);
     346        return new EastNorth(lineP1.getX() + ldx * offset, lineP1.getY() + ldy * offset);
     347    }
    335348
    336349    /**
    337350     * This method tests if secondNode is clockwise to first node.
    public class Geometry { 
    457470
    458471        return inside;
    459472    }
    460    
     473
    461474    /**
    462475     * returns area of a closed way in square meters
    463476     * (approximate(?), but should be OK for small areas)
    public class Geometry { 
    477490        }
    478491        return Math.abs(area/2);
    479492    }
    480    
     493
    481494    protected static double calcX(Node p1){
    482495        double lat1, lon1, lat2, lon2;
    483496        double dlon, dlat;
    public class Geometry { 
    494507        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
    495508        return 6367000 * c;
    496509    }
    497    
     510
    498511    protected static double calcY(Node p1){
    499512        double lat1, lon1, lat2, lon2;
    500513        double dlon, dlat;