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

File parallel-way-action.0.3.patch, 34.9 KB (added by olejorgenb, 12 months 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;