Ticket #6694: AngleSnapping.patch

File AngleSnapping.patch, 18.7 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"),
     
    95100        cursorJoinWay = ImageProvider.getCursor("crosshair", "joinway");
    96101    }
    97102
     103    private void insertNodeIntoAllNearbySegments(List<WaySegment> wss, Node n, Collection<OsmPrimitive> newSelection, Collection<Command> cmds, ArrayList<Way> replacedWays, ArrayList<Way> reuseWays) {
     104        Map<Way, List<Integer>> insertPoints = new HashMap<Way, List<Integer>>();
     105        for (WaySegment ws : wss) {
     106            List<Integer> is;
     107            if (insertPoints.containsKey(ws.way)) {
     108                is = insertPoints.get(ws.way);
     109            } else {
     110                is = new ArrayList<Integer>();
     111                insertPoints.put(ws.way, is);
     112            }
     113
     114            is.add(ws.lowerIndex);
     115        }
     116
     117        Set<Pair<Node,Node>> segSet = new HashSet<Pair<Node,Node>>();
     118
     119        for (Map.Entry<Way, List<Integer>> insertPoint : insertPoints.entrySet()) {
     120            Way w = insertPoint.getKey();
     121            List<Integer> is = insertPoint.getValue();
     122
     123            Way wnew = new Way(w);
     124
     125            pruneSuccsAndReverse(is);
     126            for (int i : is) {
     127                segSet.add(
     128                        Pair.sort(new Pair<Node,Node>(w.getNode(i), w.getNode(i+1))));
     129            }
     130            for (int i : is) {
     131                wnew.addNode(i + 1, n);
     132            }
     133
     134            // If ALT is pressed, a new way should be created and that new way should get
     135            // selected. This works everytime unless the ways the nodes get inserted into
     136            // are already selected. This is the case when creating a self-overlapping way
     137            // but pressing ALT prevents this. Therefore we must de-select the way manually
     138            // here so /only/ the new way will be selected after this method finishes.
     139            if(alt) {
     140                newSelection.add(insertPoint.getKey());
     141            }
     142
     143            cmds.add(new ChangeCommand(insertPoint.getKey(), wnew));
     144            replacedWays.add(insertPoint.getKey());
     145            reuseWays.add(wnew);
     146        }
     147
     148        adjustNode(segSet, n);
     149    }
     150
    98151    /**
    99152     * Checks if a map redraw is required and does so if needed. Also updates the status bar
    100153     */
     
    116169        Main.map.mapView.repaint();
    117170    }
    118171
    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 
    173172    @Override public void enterMode() {
    174173        if (!isEnabled())
    175174            return;
     
    224223    public void eventDispatched(AWTEvent event) {
    225224        if(Main.map == null || Main.map.mapView == null || !Main.map.mapView.isActiveLayerDrawable())
    226225            return;
     226        if (event instanceof KeyEvent) {
     227                KeyEvent ke = (KeyEvent) event;
     228                if (ke.getKeyCode() == KeyEvent.VK_TAB &&
     229                    ke.getID()==KeyEvent.KEY_PRESSED) snapOn=!snapOn;
     230        } //  toggle angle snapping
    227231        updateKeyModifiers((InputEvent) event);
    228232        computeHelperLine();
    229233        addHighlighting();
     
    257261        lastUsedNode = null;
    258262        wayIsFinished = true;
    259263        Main.map.selectSelectTool(true);
    260 
     264        snapHelper.reset();
     265   
    261266        // Redraw to remove the helper line stub
    262267        computeHelperLine();
    263268        removeHighlighting();
     
    293298        // if that can ever happen but better be safe.
    294299        updateKeyModifiers(e);
    295300        mousePos = e.getPoint();
     301        EastNorth mouseEN = Main.map.mapView.getEastNorth(e.getX(), e.getY());;
     302        if (snapOn) {
     303            mouseEN = snapHelper.getSnapPoint(mouseEN);
     304        }
    296305
    297306        DataSet ds = getCurrentDataSet();
    298307        Collection<OsmPrimitive> selection = new ArrayList<OsmPrimitive>(ds.getSelected());
     
    328337            }
    329338        } else {
    330339            // no node found in clicked area
    331             n = new Node(Main.map.mapView.getLatLon(e.getX(), e.getY()));
     340            n=new Node(mouseEN);
    332341            if (n.getCoor().isOutSideWorld()) {
    333342                JOptionPane.showMessageDialog(
    334343                        Main.parent,
     
    345354            if (!ctrl) {
    346355                // Insert the node into all the nearby way segments
    347356                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);
     357                insertNodeIntoAllNearbySegments(wss, n, newSelection, cmds, replacedWays, reuseWays);
    356358                    }
    357 
    358                     is.add(ws.lowerIndex);
    359359                }
    360360
    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             }
    394         }
    395 
    396361        // This part decides whether or not a "segment" (i.e. a connection) is made to an
    397362        // existing node.
    398363
     
    641606
    642607        Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
    643608
    644         Node selectedNode = null;
    645         Way selectedWay = null;
    646609        Node currentMouseNode = null;
    647610        mouseOnExistingNode = null;
    648611        mouseOnExistingWays = new HashSet<Way>();
     
    674637            currentMouseEastNorth = mv.getEastNorth(mousePos.x, mousePos.y);
    675638        }
    676639
     640        determineCurrentBaseNodeAndPreviousNode(selection);
     641        if (previousNode == null) snapHelper.reset();
     642       
     643        if (currentBaseNode == null || currentBaseNode == currentMouseNode)
     644            return; // Don't create zero length way segments.
     645
     646        // find out the distance, in metres, between the base point and the mouse cursor
     647        LatLon mouseLatLon = mv.getProjection().eastNorth2latlon(currentMouseEastNorth);
     648        distance = currentBaseNode.getCoor().greatCircleDistance(mouseLatLon);
     649
     650        double hdg = Math.toDegrees(currentBaseNode.getEastNorth()
     651                .heading(currentMouseEastNorth));
     652        if (previousNode != null) {
     653            angle = hdg - Math.toDegrees(previousNode.getEastNorth()
     654                    .heading(currentBaseNode.getEastNorth()));
     655            angle += angle < 0 ? 360 : 0;
     656        }
     657       
     658        if (snapOn) snapHelper.checkAngleSnapping(currentMouseEastNorth,angle);
     659       
     660        Main.map.statusLine.setAngle(angle);
     661        Main.map.statusLine.setHeading(hdg);
     662        Main.map.statusLine.setDist(distance);
     663        // Now done in redrawIfRequired()
     664        //updateStatusLine();
     665    }
     666    private void determineCurrentBaseNodeAndPreviousNode(Collection<OsmPrimitive>  selection) {
     667        Node selectedNode = null;
     668        Way selectedWay = null;
    677669        for (OsmPrimitive p : selection) {
    678670            if (p instanceof Node) {
    679671                if (selectedNode != null) return;
     
    683675                selectedWay = (Way) p;
    684676            }
    685677        }
     678        // we are here, if not more than 1 way or node is selected,
    686679
    687680        // the node from which we make a connection
    688681        currentBaseNode = null;
    689         Node previousNode = null;
     682        previousNode = null;
    690683
    691684        if (selectedNode == null) {
    692685            if (selectedWay == null)
     
    704697                currentBaseNode = selectedNode;
    705698            }
    706699        }
     700    }
    707701
    708         if (currentBaseNode == null || currentBaseNode == currentMouseNode)
    709             return; // Don't create zero length way segments.
     702/**
     703     * Takes the data from computeHelperLine to determine which ways/nodes should be highlighted
     704     * (if feature enabled). Also sets the target cursor if appropriate.
     705     */
     706    private void addHighlighting() {
     707        removeHighlighting();
     708        // if ctrl key is held ("no join"), don't highlight anything
     709        if (ctrl) {
     710            Main.map.mapView.setNewCursor(cursor, this);
     711            return;
     712        }
    710713
    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        // This happens when nothing is selected, but we still want to highlight the "target node"
     715        if (mouseOnExistingNode == null && getCurrentDataSet().getSelected().size() == 0
     716                && mousePos != null) {
     717            mouseOnExistingNode = Main.map.mapView.getNearestNode(mousePos, OsmPrimitive.isSelectablePredicate);
     718        }
    714719
    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;
     720        if (mouseOnExistingNode != null) {
     721            Main.map.mapView.setNewCursor(cursorJoinNode, this);
     722            // We also need this list for the statusbar help text
     723            oldHighlights.add(mouseOnExistingNode);
     724            if(drawTargetHighlight) {
     725                mouseOnExistingNode.setHighlighted(true);
    721726        }
     727            return;
     728        }
    722729
    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();
     730        // Insert the node into all the nearby way segments
     731        if (mouseOnExistingWays.size() == 0) {
     732            Main.map.mapView.setNewCursor(cursor, this);
     733            return;
    728734    }
    729735
     736        Main.map.mapView.setNewCursor(cursorJoinWay, this);
     737
     738        // We also need this list for the statusbar help text
     739        oldHighlights.addAll(mouseOnExistingWays);
     740        if (!drawTargetHighlight) return;
     741        for (Way w : mouseOnExistingWays) {
     742            w.setHighlighted(true);
     743        }
     744    }
     745
    730746    /**
     747     * Removes target highlighting from primitives
     748     */
     749    private void removeHighlighting() {
     750        for(OsmPrimitive prim : oldHighlights) {
     751            prim.setHighlighted(false);
     752        }
     753        oldHighlights = new HashSet<OsmPrimitive>();
     754    }
     755   
     756    /**
    731757     * Repaint on mouse exit so that the helper line goes away.
    732758     */
    733759    @Override public void mouseExited(MouseEvent e) {
    734760        if(!Main.map.mapView.isActiveLayerDrawable())
    735761            return;
    736762        mousePos = e.getPoint();
     763        snapHelper.reset();
    737764        Main.map.mapView.repaint();
    738765    }
    739766
     
    741768     * @return If the node is the end of exactly one way, return this.
    742769     *  <code>null</code> otherwise.
    743770     */
    744     public Way getWayForNode(Node n) {
     771    public static Way getWayForNode(Node n) {
    745772        Way way = null;
    746773        for (Way w : Utils.filteredCollection(n.getReferrers(), Way.class)) {
    747774            if (!w.isUsable() || w.getNodesCount() < 1) {
     
    854881    }
    855882
    856883    public void paint(Graphics2D g, MapView mv, Bounds box) {
    857         if (!drawHelperLine || wayIsFinished || shift) return;
    858 
     884       
    859885        // sanity checks
    860886        if (Main.map.mapView == null) return;
    861887        if (mousePos == null) return;
     
    865891
    866892        // don't draw line if mouse is outside window
    867893        if (!Main.map.mapView.getBounds().contains(mousePos)) return;
     894       
     895        Graphics2D g2 = g;
     896        if (snapOn) snapHelper.draw(g2,mv);
     897        if (!drawHelperLine || wayIsFinished || shift) return;
    868898
    869         Graphics2D g2 = g;
    870899        g2.setColor(selectedColor);
    871900        g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
    872901        GeneralPath b = new GeneralPath();
     
    10011030    }
    10021031    }
    10031032
     1033    private class SnapHelper {
     1034        private boolean active;
     1035        EastNorth dir2;
     1036        EastNorth projected;
     1037        String labelText;
     1038       
     1039       
     1040        double pe,pn; // (pe,pn) - direction of snapping line
     1041        double e0,n0; // (e0,n0) - origin of snapping line
     1042       
     1043       
     1044         
     1045        public SnapHelper() { }
     1046       
     1047        private  void reset() {
     1048            active=false;
     1049            dir2=null; projected=null;
     1050            labelText=null;
     1051        }
     1052
     1053        private void draw(Graphics2D g2, MapView mv) {
     1054            if (!active) return;
     1055            g2.setColor(Color.ORANGE);
     1056            g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
     1057            GeneralPath b = new GeneralPath();
     1058            Point p1=mv.getPoint(currentBaseNode);
     1059            Point p2=mv.getPoint(dir2);
     1060            Point p3=mv.getPoint(projected);
     1061           
     1062            b.moveTo(p1.x,p1.y);
     1063            b.lineTo(p2.x,p2.y);
     1064            g2.draw(b);       
     1065            g2.drawString(labelText, p3.x-5, p3.y+20);
     1066            g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
     1067            g2.drawOval(p3.x-5, p3.y-5, 10, 10); // projected point
     1068        }
     1069
     1070        /* If mouse position is close to line at 15-30-45-... angle, remembers this direction
     1071         */
     1072        private void checkAngleSnapping(EastNorth currrentEN, double angle) {
     1073            if (previousNode==null) return;
     1074           
     1075            double nearestAngle = 15*Math.round(angle/15);
     1076            // 95->90, 50->90, 40->0, 280->270, 340->360
     1077            if (nearestAngle!=180 && Math.abs(nearestAngle-angle)<7) {
     1078                active=true;
     1079                labelText = String.format("%d", (int) nearestAngle);
     1080                EastNorth prev = previousNode.getEastNorth();
     1081                EastNorth p0 = currentBaseNode.getEastNorth();
     1082               
     1083                double de,dn,l, phi;
     1084                e0=p0.east(); n0=p0.north();
     1085                de = e0-prev.east();
     1086                dn = n0-prev.north();
     1087                l=Math.hypot(de, dn);
     1088                de/=l; dn/=l;
     1089               
     1090                phi=nearestAngle*Math.PI/180;
     1091                // (pe,pn) - direction of snapping line
     1092                pe = de*Math.cos(phi) + dn*Math.sin(phi); 
     1093                pn = -de*Math.sin(phi) + dn*Math.cos(phi);
     1094                double scale = 20*Main.map.mapView.getDist100Pixel();
     1095                dir2 = new EastNorth( e0+scale*pe, n0+scale*pn);
     1096                getSnapPoint(currrentEN);
     1097           } else {
     1098                reset();
     1099            }
     1100        }
     1101       
     1102        private EastNorth getSnapPoint(EastNorth p) {
     1103            if (!active) return p;
     1104            double de=p.east()-e0;
     1105            double dn=p.north()-n0;
     1106            double l = de*pe+dn*pn;
     1107            return projected = new EastNorth(e0+l*pe, n0+l*pn);
     1108        }
     1109    }
     1110
    10041111}