Ticket #6694: AngleSnappingv2.patch

File AngleSnappingv2.patch, 22.5 KB (added by akks, 14 years ago)
  • src/org/openstreetmap/josm/actions/mapmode/DrawAction.java

     
    7777    private Color selectedColor;
    7878
    7979    private Node currentBaseNode;
     80    private Node previousNode;
    8081    private EastNorth currentMouseEastNorth;
    8182
     83    private SnapHelper snapHelper = new SnapHelper();
     84
    8285    private Shortcut extraShortcut;
    8386    private Shortcut backspaceShortcut;
     87   
     88    boolean snapOn;
    8489           
    8590    public DrawAction(MapFrame mapFrame) {
    8691        super(tr("Draw"), "node/autonode", tr("Draw nodes"),
     
    116121        Main.map.mapView.repaint();
    117122    }
    118123
    119     /**
    120      * Takes the data from computeHelperLine to determine which ways/nodes should be highlighted
    121      * (if feature enabled). Also sets the target cursor if appropriate.
    122      */
    123     private void addHighlighting() {
    124         removeHighlighting();
    125         // if ctrl key is held ("no join"), don't highlight anything
    126         if (ctrl) {
    127             Main.map.mapView.setNewCursor(cursor, this);
    128             return;
    129         }
    130 
    131         // This happens when nothing is selected, but we still want to highlight the "target node"
    132         if (mouseOnExistingNode == null && getCurrentDataSet().getSelected().size() == 0
    133                 && mousePos != null) {
    134             mouseOnExistingNode = Main.map.mapView.getNearestNode(mousePos, OsmPrimitive.isSelectablePredicate);
    135         }
    136 
    137         if (mouseOnExistingNode != null) {
    138             Main.map.mapView.setNewCursor(cursorJoinNode, this);
    139             // We also need this list for the statusbar help text
    140             oldHighlights.add(mouseOnExistingNode);
    141             if(drawTargetHighlight) {
    142                 mouseOnExistingNode.setHighlighted(true);
    143             }
    144             return;
    145         }
    146 
    147         // Insert the node into all the nearby way segments
    148         if (mouseOnExistingWays.size() == 0) {
    149             Main.map.mapView.setNewCursor(cursor, this);
    150             return;
    151         }
    152 
    153         Main.map.mapView.setNewCursor(cursorJoinWay, this);
    154 
    155         // We also need this list for the statusbar help text
    156         oldHighlights.addAll(mouseOnExistingWays);
    157         if (!drawTargetHighlight) return;
    158         for (Way w : mouseOnExistingWays) {
    159             w.setHighlighted(true);
    160         }
    161     }
    162 
    163     /**
    164      * Removes target highlighting from primitives
    165      */
    166     private void removeHighlighting() {
    167         for(OsmPrimitive prim : oldHighlights) {
    168             prim.setHighlighted(false);
    169         }
    170         oldHighlights = new HashSet<OsmPrimitive>();
    171     }
    172 
    173124    @Override public void enterMode() {
    174125        if (!isEnabled())
    175126            return;
     
    178129        drawHelperLine = Main.pref.getBoolean("draw.helper-line", true);
    179130        drawTargetHighlight = Main.pref.getBoolean("draw.target-highlight", true);
    180131        wayIsFinished = false;
     132        snapHelper.init();
    181133       
    182134        backspaceShortcut = Shortcut.registerShortcut("mapmode:backspace", tr("Backspace in Add mode"), KeyEvent.VK_BACK_SPACE, Shortcut.GROUP_EDIT);
    183135        Main.registerActionShortcut(new BackSpaceAction(), backspaceShortcut);
     
    224176    public void eventDispatched(AWTEvent event) {
    225177        if(Main.map == null || Main.map.mapView == null || !Main.map.mapView.isActiveLayerDrawable())
    226178            return;
     179        if (event instanceof KeyEvent) {
     180                KeyEvent ke = (KeyEvent) event;
     181                if (ke.getKeyCode() == KeyEvent.VK_TAB &&
     182                    ke.getID()==KeyEvent.KEY_PRESSED) {
     183                    snapHelper.nextSnapMode();
     184                }
     185        } //  toggle angle snapping
    227186        updateKeyModifiers((InputEvent) event);
    228187        computeHelperLine();
    229188        addHighlighting();
     
    257216        lastUsedNode = null;
    258217        wayIsFinished = true;
    259218        Main.map.selectSelectTool(true);
    260 
     219        snapHelper.reset();
     220   
    261221        // Redraw to remove the helper line stub
    262222        computeHelperLine();
    263223        removeHighlighting();
     
    308268            n = Main.map.mapView.getNearestNode(mousePos, OsmPrimitive.isSelectablePredicate);
    309269        }
    310270
    311         if (n != null) {
     271        if (n != null && !snapHelper.isActive()) {
    312272            // user clicked on node
    313273            if (selection.isEmpty() || wayIsFinished) {
    314274                // select the clicked node and do nothing else
     
    327287                return;
    328288            }
    329289        } else {
    330             // no node found in clicked area
    331             n = new Node(Main.map.mapView.getLatLon(e.getX(), e.getY()));
     290            EastNorth newEN;
     291            if (n!=null) {
     292                // project found node to snapping line
     293                newEN = snapHelper.getSnapPoint(n.getEastNorth());
     294            } else { // n==null
     295                // no node found in clicked area
     296                EastNorth mouseEN = Main.map.mapView.getEastNorth(e.getX(), e.getY());
     297                newEN = snapOn ? snapHelper.getSnapPoint(mouseEN) : mouseEN;
     298            }
     299            snapHelper.resetDirection();
     300            n=new Node(newEN); //create node at clicked point
    332301            if (n.getCoor().isOutSideWorld()) {
    333302                JOptionPane.showMessageDialog(
    334303                        Main.parent,
     
    343312            cmds.add(new AddCommand(n));
    344313
    345314            if (!ctrl) {
    346                 // Insert the node into all the nearby way segments
    347                 List<WaySegment> wss = Main.map.mapView.getNearestWaySegments(e.getPoint(), OsmPrimitive.isSelectablePredicate);
    348                 Map<Way, List<Integer>> insertPoints = new HashMap<Way, List<Integer>>();
    349                 for (WaySegment ws : wss) {
    350                     List<Integer> is;
    351                     if (insertPoints.containsKey(ws.way)) {
    352                         is = insertPoints.get(ws.way);
    353                     } else {
    354                         is = new ArrayList<Integer>();
    355                         insertPoints.put(ws.way, is);
    356                     }
    357 
    358                     is.add(ws.lowerIndex);
    359                 }
    360 
    361                 Set<Pair<Node,Node>> segSet = new HashSet<Pair<Node,Node>>();
    362 
    363                 for (Map.Entry<Way, List<Integer>> insertPoint : insertPoints.entrySet()) {
    364                     Way w = insertPoint.getKey();
    365                     List<Integer> is = insertPoint.getValue();
    366 
    367                     Way wnew = new Way(w);
    368 
    369                     pruneSuccsAndReverse(is);
    370                     for (int i : is) {
    371                         segSet.add(
    372                                 Pair.sort(new Pair<Node,Node>(w.getNode(i), w.getNode(i+1))));
    373                     }
    374                     for (int i : is) {
    375                         wnew.addNode(i + 1, n);
    376                     }
    377 
    378                     // If ALT is pressed, a new way should be created and that new way should get
    379                     // selected. This works everytime unless the ways the nodes get inserted into
    380                     // are already selected. This is the case when creating a self-overlapping way
    381                     // but pressing ALT prevents this. Therefore we must de-select the way manually
    382                     // here so /only/ the new way will be selected after this method finishes.
    383                     if(alt) {
    384                         newSelection.add(insertPoint.getKey());
    385                     }
    386 
    387                     cmds.add(new ChangeCommand(insertPoint.getKey(), wnew));
    388                     replacedWays.add(insertPoint.getKey());
    389                     reuseWays.add(wnew);
    390                 }
    391 
    392                 adjustNode(segSet, n);
    393             }
     315                    // Insert the node into all the nearby way segments
     316                    List<WaySegment> wss = Main.map.mapView.getNearestWaySegments(
     317                            Main.map.mapView.getPoint(n), OsmPrimitive.isSelectablePredicate);
     318                    insertNodeIntoAllNearbySegments(wss, n, newSelection, cmds, replacedWays, reuseWays);
     319                    }   
    394320        }
    395 
     321        // now "n" is newly created or reused node that shoud be added to some way
     322       
    396323        // This part decides whether or not a "segment" (i.e. a connection) is made to an
    397324        // existing node.
    398325
     
    542469        removeHighlighting();
    543470        redrawIfRequired();
    544471    }
     472   
     473    private void insertNodeIntoAllNearbySegments(List<WaySegment> wss, Node n, Collection<OsmPrimitive> newSelection, Collection<Command> cmds, ArrayList<Way> replacedWays, ArrayList<Way> reuseWays) {
     474        Map<Way, List<Integer>> insertPoints = new HashMap<Way, List<Integer>>();
     475        for (WaySegment ws : wss) {
     476            List<Integer> is;
     477            if (insertPoints.containsKey(ws.way)) {
     478                is = insertPoints.get(ws.way);
     479            } else {
     480                is = new ArrayList<Integer>();
     481                insertPoints.put(ws.way, is);
     482            }
    545483
     484            is.add(ws.lowerIndex);
     485        }
     486
     487        Set<Pair<Node,Node>> segSet = new HashSet<Pair<Node,Node>>();
     488
     489        for (Map.Entry<Way, List<Integer>> insertPoint : insertPoints.entrySet()) {
     490            Way w = insertPoint.getKey();
     491            List<Integer> is = insertPoint.getValue();
     492
     493            Way wnew = new Way(w);
     494
     495            pruneSuccsAndReverse(is);
     496            for (int i : is) {
     497                segSet.add(
     498                        Pair.sort(new Pair<Node,Node>(w.getNode(i), w.getNode(i+1))));
     499            }
     500            for (int i : is) {
     501                wnew.addNode(i + 1, n);
     502            }
     503
     504            // If ALT is pressed, a new way should be created and that new way should get
     505            // selected. This works everytime unless the ways the nodes get inserted into
     506            // are already selected. This is the case when creating a self-overlapping way
     507            // but pressing ALT prevents this. Therefore we must de-select the way manually
     508            // here so /only/ the new way will be selected after this method finishes.
     509            if(alt) {
     510                newSelection.add(insertPoint.getKey());
     511            }
     512
     513            cmds.add(new ChangeCommand(insertPoint.getKey(), wnew));
     514            replacedWays.add(insertPoint.getKey());
     515            reuseWays.add(wnew);
     516        }
     517
     518        adjustNode(segSet, n);
     519    }
     520
     521
    546522    /**
    547523     * Prevent creation of ways that look like this: <---->
    548524     * This happens if users want to draw a no-exit-sideway from the main way like this:
     
    641617
    642618        Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
    643619
    644         Node selectedNode = null;
    645         Way selectedWay = null;
    646620        Node currentMouseNode = null;
    647621        mouseOnExistingNode = null;
    648622        mouseOnExistingWays = new HashSet<Way>();
     
    674648            currentMouseEastNorth = mv.getEastNorth(mousePos.x, mousePos.y);
    675649        }
    676650
     651        determineCurrentBaseNodeAndPreviousNode(selection);
     652        if (previousNode == null) snapHelper.reset();
     653       
     654        if (currentBaseNode == null || currentBaseNode == currentMouseNode)
     655            return; // Don't create zero length way segments.
     656
     657        // find out the distance, in metres, between the base point and the mouse cursor
     658        LatLon mouseLatLon = mv.getProjection().eastNorth2latlon(currentMouseEastNorth);
     659        distance = currentBaseNode.getCoor().greatCircleDistance(mouseLatLon);
     660
     661        double hdg = Math.toDegrees(currentBaseNode.getEastNorth()
     662                .heading(currentMouseEastNorth));
     663        if (previousNode != null) {
     664            angle = hdg - Math.toDegrees(previousNode.getEastNorth()
     665                    .heading(currentBaseNode.getEastNorth()));
     666            angle += angle < 0 ? 360 : 0;
     667        }
     668       
     669        if (snapOn) snapHelper.checkAngleSnapping(currentMouseEastNorth,angle);
     670       
     671        Main.map.statusLine.setAngle(angle);
     672        Main.map.statusLine.setHeading(hdg);
     673        Main.map.statusLine.setDist(distance);
     674        // Now done in redrawIfRequired()
     675        //updateStatusLine();
     676    }
     677   
     678   
     679    /**
     680     * Helper function that sets fields currentBaseNode and previousNode
     681     * @param selection
     682     * uses also lastUsedNode field
     683     */
     684    private void determineCurrentBaseNodeAndPreviousNode(Collection<OsmPrimitive>  selection) {
     685        Node selectedNode = null;
     686        Way selectedWay = null;
    677687        for (OsmPrimitive p : selection) {
    678688            if (p instanceof Node) {
    679689                if (selectedNode != null) return;
     
    683693                selectedWay = (Way) p;
    684694            }
    685695        }
     696        // we are here, if not more than 1 way or node is selected,
    686697
    687698        // the node from which we make a connection
    688699        currentBaseNode = null;
    689         Node previousNode = null;
     700        previousNode = null;
    690701
    691702        if (selectedNode == null) {
    692703            if (selectedWay == null)
     
    700711        } else if (selectedWay == null) {
    701712            currentBaseNode = selectedNode;
    702713        } else if (!selectedWay.isDeleted()) { // fix #7118
    703             if (selectedNode == selectedWay.getNode(0) || selectedNode == selectedWay.getNode(selectedWay.getNodesCount()-1)) {
     714            if (selectedNode == selectedWay.getNode(0)){
    704715                currentBaseNode = selectedNode;
     716                if (selectedWay.getNodesCount()>1) previousNode = selectedWay.getNode(1);
    705717            }
     718            if (selectedNode == selectedWay.lastNode()) {
     719                currentBaseNode = selectedNode;
     720                if (selectedWay.getNodesCount()>1)
     721                    previousNode = selectedWay.getNode(selectedWay.getNodesCount()-2);
     722            }
    706723        }
     724    }
    707725
    708         if (currentBaseNode == null || currentBaseNode == currentMouseNode)
    709             return; // Don't create zero length way segments.
    710726
    711         // find out the distance, in metres, between the base point and the mouse cursor
    712         LatLon mouseLatLon = mv.getProjection().eastNorth2latlon(currentMouseEastNorth);
    713         distance = currentBaseNode.getCoor().greatCircleDistance(mouseLatLon);
    714 
    715         double hdg = Math.toDegrees(currentBaseNode.getEastNorth()
    716                 .heading(currentMouseEastNorth));
    717         if (previousNode != null) {
    718             angle = hdg - Math.toDegrees(previousNode.getEastNorth()
    719                     .heading(currentBaseNode.getEastNorth()));
    720             angle += angle < 0 ? 360 : 0;
    721         }
    722 
    723         Main.map.statusLine.setAngle(angle);
    724         Main.map.statusLine.setHeading(hdg);
    725         Main.map.statusLine.setDist(distance);
    726         // Now done in redrawIfRequired()
    727         //updateStatusLine();
    728     }
    729 
    730727    /**
    731728     * Repaint on mouse exit so that the helper line goes away.
    732729     */
     
    734731        if(!Main.map.mapView.isActiveLayerDrawable())
    735732            return;
    736733        mousePos = e.getPoint();
     734        snapHelper.reset();
    737735        Main.map.mapView.repaint();
    738736    }
    739737
     
    741739     * @return If the node is the end of exactly one way, return this.
    742740     *  <code>null</code> otherwise.
    743741     */
    744     public Way getWayForNode(Node n) {
     742    public static Way getWayForNode(Node n) {
    745743        Way way = null;
    746744        for (Way w : Utils.filteredCollection(n.getReferrers(), Way.class)) {
    747745            if (!w.isUsable() || w.getNodesCount() < 1) {
     
    852850    static double det(double a, double b, double c, double d) {
    853851        return a * d - b * c;
    854852    }
     853/**
     854     * Takes the data from computeHelperLine to determine which ways/nodes should be highlighted
     855     * (if feature enabled). Also sets the target cursor if appropriate.
     856     */
     857    private void addHighlighting() {
     858        removeHighlighting();
     859        // if ctrl key is held ("no join"), don't highlight anything
     860        if (ctrl) {
     861            Main.map.mapView.setNewCursor(cursor, this);
     862            return;
     863        }
    855864
     865        // This happens when nothing is selected, but we still want to highlight the "target node"
     866        if (mouseOnExistingNode == null && getCurrentDataSet().getSelected().size() == 0
     867                && mousePos != null) {
     868            mouseOnExistingNode = Main.map.mapView.getNearestNode(mousePos, OsmPrimitive.isSelectablePredicate);
     869        }
     870
     871        if (mouseOnExistingNode != null) {
     872            Main.map.mapView.setNewCursor(cursorJoinNode, this);
     873            // We also need this list for the statusbar help text
     874            oldHighlights.add(mouseOnExistingNode);
     875            if(drawTargetHighlight) {
     876                mouseOnExistingNode.setHighlighted(true);
     877        }
     878            return;
     879        }
     880
     881        // Insert the node into all the nearby way segments
     882        if (mouseOnExistingWays.size() == 0) {
     883            Main.map.mapView.setNewCursor(cursor, this);
     884            return;
     885    }
     886
     887        Main.map.mapView.setNewCursor(cursorJoinWay, this);
     888
     889        // We also need this list for the statusbar help text
     890        oldHighlights.addAll(mouseOnExistingWays);
     891        if (!drawTargetHighlight) return;
     892        for (Way w : mouseOnExistingWays) {
     893            w.setHighlighted(true);
     894        }
     895    }
     896
     897    /**
     898     * Removes target highlighting from primitives
     899     */
     900    private void removeHighlighting() {
     901        for(OsmPrimitive prim : oldHighlights) {
     902            prim.setHighlighted(false);
     903        }
     904        oldHighlights = new HashSet<OsmPrimitive>();
     905    }
     906   
    856907    public void paint(Graphics2D g, MapView mv, Bounds box) {
    857         if (!drawHelperLine || wayIsFinished || shift) return;
    858 
    859908        // sanity checks
    860909        if (Main.map.mapView == null) return;
    861910        if (mousePos == null) return;
     
    865914
    866915        // don't draw line if mouse is outside window
    867916        if (!Main.map.mapView.getBounds().contains(mousePos)) return;
    868 
     917       
    869918        Graphics2D g2 = g;
     919        if (snapOn) snapHelper.draw(g2,mv);
     920        if (!drawHelperLine || wayIsFinished || shift) return;
     921       
    870922        g2.setColor(selectedColor);
    871923        g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
    872924        GeneralPath b = new GeneralPath();
     
    9751027        super.destroy();
    9761028        Main.unregisterActionShortcut(extraShortcut);
    9771029    }
    978    
     1030
    9791031    public static class BackSpaceAction extends AbstractAction {
    9801032
    9811033        @Override
     
    10011053    }
    10021054    }
    10031055
     1056    private class SnapHelper {
     1057        private boolean active;
     1058        private boolean fixed;
     1059        EastNorth dir2;
     1060        EastNorth projected;
     1061        String labelText;
     1062        double lastAngle;
     1063       
     1064        double snapAngleStep;
     1065        double snapAngleTolerance;
     1066       
     1067        double pe,pn; // (pe,pn) - direction of snapping line
     1068        double e0,n0; // (e0,n0) - origin of snapping line
     1069       
     1070        public SnapHelper() { }
     1071       
     1072        private  void init() {
     1073            reset(); fixed=false; snapOn=false;
     1074            snapAngleStep = Main.pref.getDouble("draw.anglesnap.step", 90.0);
     1075            snapAngleTolerance = Main.pref.getDouble("draw.anglesnap.tol", 20.0);
     1076        }
     1077       
     1078        private  void reset() {
     1079            active=false;
     1080            dir2=null; projected=null;
     1081            labelText=null;
     1082        }
     1083
     1084        private void draw(Graphics2D g2, MapView mv) {
     1085            if (!active) return;
     1086            g2.setColor(Color.ORANGE);
     1087            g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
     1088            GeneralPath b = new GeneralPath();
     1089            Point p1=mv.getPoint(currentBaseNode);
     1090            Point p2=mv.getPoint(dir2);
     1091            Point p3=mv.getPoint(projected);
     1092           
     1093            b.moveTo(p1.x,p1.y);
     1094            b.lineTo(p2.x,p2.y);
     1095            g2.draw(b);       
     1096            g2.drawString(labelText, p3.x-5, p3.y+20);
     1097            g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
     1098            g2.drawOval(p3.x-5, p3.y-5, 10, 10); // projected point
     1099        }
     1100
     1101        /* If mouse position is close to line at 15-30-45-... angle, remembers this direction
     1102         */
     1103        private void checkAngleSnapping(EastNorth currentEN, double angle) {
     1104            if (previousNode==null) return;
     1105            if (fixed) angle = lastAngle; // if direction is fixed
     1106            double nearestAngle = snapAngleStep*Math.round(angle/snapAngleStep);
     1107            // 95->90, 50->90, 40->0, 280->270, 340->360
     1108           
     1109            if (Math.abs(nearestAngle-180)>1e-3 && Math.abs(nearestAngle-angle)<snapAngleTolerance) {
     1110                if (Math.abs(nearestAngle-360)<1e-3) nearestAngle=0;
     1111                active=true;
     1112                if (fixed) labelText = String.format("%d FIX", (int) nearestAngle);
     1113                      else labelText = String.format("%d", (int) nearestAngle);
     1114                EastNorth prev = previousNode.getEastNorth();
     1115                EastNorth p0 = currentBaseNode.getEastNorth();
     1116               
     1117                double de,dn,l, phi;
     1118                e0=p0.east(); n0=p0.north();
     1119                de = e0-prev.east();
     1120                dn = n0-prev.north();
     1121                l=Math.hypot(de, dn);
     1122                de/=l; dn/=l;
     1123               
     1124                phi=nearestAngle*Math.PI/180;
     1125                // (pe,pn) - direction of snapping line
     1126                pe = de*Math.cos(phi) + dn*Math.sin(phi); 
     1127                pn = -de*Math.sin(phi) + dn*Math.cos(phi);
     1128                double scale = 20*Main.map.mapView.getDist100Pixel();
     1129                dir2 = new EastNorth( e0+scale*pe, n0+scale*pn);
     1130                lastAngle = nearestAngle;
     1131                getSnapPoint(currentEN);
     1132           } else {
     1133                reset();
     1134           }
     1135        }
     1136       
     1137        private EastNorth getSnapPoint(EastNorth p) {
     1138            if (!active) return p;
     1139            double de=p.east()-e0;
     1140            double dn=p.north()-n0;
     1141            double l = de*pe+dn*pn;
     1142            if (l<1e-5) {active=false; return p; } //  do not go backward!
     1143            return projected = new EastNorth(e0+l*pe, n0+l*pn);
     1144        }
     1145
     1146        private void fixDirection() {
     1147            if (active) {
     1148                fixed=true;
     1149            }
     1150        }
     1151
     1152        private void resetDirection() {
     1153            lastAngle=0;
     1154        }
     1155       
     1156        private void nextSnapMode() {
     1157            if (snapOn) {
     1158                if (fixed) { snapOn=false; fixed=false; }
     1159                else fixDirection();
     1160            } else {
     1161                snapOn=true;
     1162                fixed=false;
     1163            }
     1164        }
     1165
     1166        private boolean isActive() {
     1167            return active;
     1168        }
     1169    }
    10041170}