001    // License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.actions.mapmode;
003    
004    import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005    import static org.openstreetmap.josm.tools.I18n.tr;
006    
007    import java.awt.AWTEvent;
008    import java.awt.BasicStroke;
009    import java.awt.Color;
010    import java.awt.Cursor;
011    import java.awt.Graphics2D;
012    import java.awt.Point;
013    import java.awt.Rectangle;
014    import java.awt.Toolkit;
015    import java.awt.event.AWTEventListener;
016    import java.awt.event.ActionEvent;
017    import java.awt.event.InputEvent;
018    import java.awt.event.KeyEvent;
019    import java.awt.event.MouseEvent;
020    import java.awt.geom.AffineTransform;
021    import java.awt.geom.GeneralPath;
022    import java.awt.geom.Line2D;
023    import java.awt.geom.NoninvertibleTransformException;
024    import java.awt.geom.Point2D;
025    import java.util.ArrayList;
026    import java.util.Collection;
027    import java.util.LinkedList;
028    import java.util.List;
029    
030    import org.openstreetmap.josm.Main;
031    import org.openstreetmap.josm.command.AddCommand;
032    import org.openstreetmap.josm.command.ChangeCommand;
033    import org.openstreetmap.josm.command.Command;
034    import org.openstreetmap.josm.command.MoveCommand;
035    import org.openstreetmap.josm.command.SequenceCommand;
036    import org.openstreetmap.josm.data.Bounds;
037    import org.openstreetmap.josm.data.coor.EastNorth;
038    import org.openstreetmap.josm.data.osm.Node;
039    import org.openstreetmap.josm.data.osm.OsmPrimitive;
040    import org.openstreetmap.josm.data.osm.Way;
041    import org.openstreetmap.josm.data.osm.WaySegment;
042    import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
043    import org.openstreetmap.josm.gui.MapFrame;
044    import org.openstreetmap.josm.gui.MapView;
045    import org.openstreetmap.josm.gui.layer.Layer;
046    import org.openstreetmap.josm.gui.layer.MapViewPaintable;
047    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
048    import org.openstreetmap.josm.tools.Geometry;
049    import org.openstreetmap.josm.tools.ImageProvider;
050    import org.openstreetmap.josm.tools.Shortcut;
051    
052    /**
053     * Makes a rectangle from a line, or modifies a rectangle.
054     */
055    public class ExtrudeAction extends MapMode implements MapViewPaintable {
056    
057        enum Mode { extrude, translate, select, create_new }
058    
059        private Mode mode = Mode.select;
060    
061        /**
062         * If true, when extruding create new node even if segments parallel.
063         */
064        private boolean alwaysCreateNodes = false;
065        private long mouseDownTime = 0;
066        private WaySegment selectedSegment = null;
067        private Color selectedColor;
068    
069        /**
070         * Possible directions to move to.
071         */
072        private List<EastNorth> possibleMoveDirections;
073    
074        /**
075         * The direction that is currently active.
076         */
077        private EastNorth activeMoveDirection;
078    
079        /**
080         * The position of the mouse cursor when the drag action was initiated.
081         */
082        private Point initialMousePos;
083        /**
084         * The time which needs to pass between click and release before something
085         * counts as a move, in milliseconds
086         */
087        private int initialMoveDelay = 200;
088        /**
089         * The initial EastNorths of node1 and node2
090         */
091        private EastNorth initialN1en;
092        private EastNorth initialN2en;
093        /**
094         * The new EastNorths of node1 and node2
095         */
096        private EastNorth newN1en;
097        private EastNorth newN2en;
098    
099        /**
100         * the command that performed last move.
101         */
102        private MoveCommand moveCommand;
103    
104        /** The cursor for the 'create_new' mode. */
105        private final Cursor cursorCreateNew;
106    
107        /**
108         * This listener is used to indicate the 'create_new' mode, if the Alt modifier is pressed.
109         */
110        private final AWTEventListener altKeyListener = new AWTEventListener() {
111            @Override
112            public void eventDispatched(AWTEvent e) {
113                if(Main.map == null || Main.map.mapView == null || !Main.map.mapView.isActiveLayerDrawable())
114                    return;
115                InputEvent ie = (InputEvent) e;
116                boolean alt = (ie.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
117                if(mode == Mode.select) {
118                    Main.map.mapView.setNewCursor(alt ? cursorCreateNew : cursor, this);
119                }
120            }
121        };
122    
123        /**
124         * Create a new SelectAction
125         * @param mapFrame The MapFrame this action belongs to.
126         */
127        public ExtrudeAction(MapFrame mapFrame) {
128            super(tr("Extrude"), "extrude/extrude", tr("Create areas"),
129                    Shortcut.registerShortcut("mapmode:extrude", tr("Mode: {0}", tr("Extrude")), KeyEvent.VK_X, Shortcut.DIRECT),
130                    mapFrame,
131                    ImageProvider.getCursor("normal", "rectangle"));
132            putValue("help", ht("/Action/Extrude"));
133            initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay",200);
134            selectedColor = PaintColors.SELECTED.get();
135            cursorCreateNew = ImageProvider.getCursor("normal", "rectangle_plus");
136        }
137    
138        @Override public String getModeHelpText() {
139            if (mode == Mode.translate)
140                return tr("Move a segment along its normal, then release the mouse button.");
141            else if (mode == Mode.extrude)
142                return tr("Draw a rectangle of the desired size, then release the mouse button.");
143            else if (mode == Mode.create_new)
144                return tr("Draw a rectangle of the desired size, then release the mouse button.");
145            else
146                return tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal, " +
147                "Alt-drag to create a new rectangle, double click to add a new node.");
148        }
149    
150        @Override public boolean layerIsSupported(Layer l) {
151            return l instanceof OsmDataLayer;
152        }
153    
154        @Override public void enterMode() {
155            super.enterMode();
156            Main.map.mapView.addMouseListener(this);
157            Main.map.mapView.addMouseMotionListener(this);
158            try {
159                Toolkit.getDefaultToolkit().addAWTEventListener(altKeyListener, AWTEvent.KEY_EVENT_MASK);
160            } catch (SecurityException ex) {
161            }
162        }
163    
164        @Override public void exitMode() {
165            Main.map.mapView.removeMouseListener(this);
166            Main.map.mapView.removeMouseMotionListener(this);
167            Main.map.mapView.removeTemporaryLayer(this);
168            try {
169                Toolkit.getDefaultToolkit().removeAWTEventListener(altKeyListener);
170            } catch (SecurityException ex) {
171            }
172            super.exitMode();
173        }
174    
175        /**
176         * If the left mouse button is pressed over a segment, switch
177         * to either extrude, translate or create_new mode depending on whether Ctrl or Alt is held.
178         */
179        @Override public void mousePressed(MouseEvent e) {
180            if(!Main.map.mapView.isActiveLayerVisible())
181                return;
182            if (!(Boolean)this.getValue("active"))
183                return;
184            if (e.getButton() != MouseEvent.BUTTON1)
185                return;
186    
187            updateKeyModifiers(e);
188    
189            selectedSegment = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate);
190    
191            if (selectedSegment == null) {
192                // If nothing gets caught, stay in select mode
193            } else {
194                // Otherwise switch to another mode
195    
196                if (ctrl) {
197                    mode = Mode.translate;
198                } else if (alt) {
199                    mode = Mode.create_new;
200                    // create a new segment and then select and extrude the new segment
201                    getCurrentDataSet().setSelected(selectedSegment.way);
202                    alwaysCreateNodes = true;
203                } else {
204                    mode = Mode.extrude;
205                    getCurrentDataSet().setSelected(selectedSegment.way);
206                    alwaysCreateNodes = shift;
207                }
208    
209                // remember initial positions for segment nodes.
210                initialN1en = selectedSegment.getFirstNode().getEastNorth();
211                initialN2en = selectedSegment.getSecondNode().getEastNorth();
212    
213                //gather possible move directions - perpendicular to the selected segment and parallel to neighbor segments
214                possibleMoveDirections = new ArrayList<EastNorth>();
215                possibleMoveDirections.add(new EastNorth(
216                        initialN1en.getY() - initialN2en.getY(),
217                        initialN2en.getX() - initialN1en.getX()));
218    
219                //add directions parallel to neighbor segments
220    
221                Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
222                if (prevNode != null) {
223                    EastNorth en = prevNode.getEastNorth();
224                    possibleMoveDirections.add(new EastNorth(
225                            initialN1en.getX() - en.getX(),
226                            initialN1en.getY() - en.getY()));
227                }
228    
229                Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
230                if (nextNode != null) {
231                    EastNorth en = nextNode.getEastNorth();
232                    possibleMoveDirections.add(new EastNorth(
233                            initialN2en.getX() - en.getX(),
234                            initialN2en.getY() - en.getY()));
235                }
236    
237                // Signifies that nothing has happened yet
238                newN1en = null;
239                newN2en = null;
240                moveCommand = null;
241    
242                Main.map.mapView.addTemporaryLayer(this);
243    
244                updateStatusLine();
245                Main.map.mapView.repaint();
246    
247                // Make note of time pressed
248                mouseDownTime = System.currentTimeMillis();
249    
250                // Make note of mouse position
251                initialMousePos = e.getPoint();
252            }
253        }
254    
255        /**
256         * Perform action depending on what mode we're in.
257         */
258        @Override public void mouseDragged(MouseEvent e) {
259            if(!Main.map.mapView.isActiveLayerVisible())
260                return;
261    
262            // do not count anything as a drag if it lasts less than 100 milliseconds.
263            if (System.currentTimeMillis() - mouseDownTime < initialMoveDelay)
264                return;
265    
266            if (mode == Mode.select) {
267                // Just sit tight and wait for mouse to be released.
268            } else {
269                //move, create new and extrude mode - move the selected segment
270    
271                EastNorth initialMouseEn = Main.map.mapView.getEastNorth(initialMousePos.x, initialMousePos.y);
272                EastNorth mouseEn = Main.map.mapView.getEastNorth(e.getPoint().x, e.getPoint().y);
273                EastNorth mouseMovement = new EastNorth(mouseEn.getX() - initialMouseEn.getX(), mouseEn.getY() - initialMouseEn.getY());
274    
275                double bestDistance = Double.POSITIVE_INFINITY;
276                EastNorth bestMovement = null;
277                activeMoveDirection = null;
278    
279                //find the best movement direction and vector
280                for (EastNorth direction: possibleMoveDirections) {
281                    EastNorth movement = calculateSegmentOffset(initialN1en, initialN2en, direction , mouseEn);
282                    if (movement == null) {
283                        //if direction parallel to segment.
284                        continue;
285                    }
286    
287                    double distanceFromMouseMovement = movement.distance(mouseMovement);
288                    if (bestDistance > distanceFromMouseMovement) {
289                        bestDistance = distanceFromMouseMovement;
290                        activeMoveDirection = direction;
291                        bestMovement = movement;
292                    }
293                }
294    
295                newN1en = new EastNorth(initialN1en.getX() + bestMovement.getX(), initialN1en.getY() + bestMovement.getY());
296                newN2en = new EastNorth(initialN2en.getX() + bestMovement.getX(), initialN2en.getY() + bestMovement.getY());
297    
298                // find out the movement distance, in metres
299                double distance = Main.getProjection().eastNorth2latlon(initialN1en).greatCircleDistance(Main.getProjection().eastNorth2latlon(newN1en));
300                Main.map.statusLine.setDist(distance);
301                updateStatusLine();
302    
303                Main.map.mapView.setNewCursor(Cursor.MOVE_CURSOR, this);
304    
305                if (mode == Mode.extrude || mode == Mode.create_new) {
306                    //nothing here
307                } else if (mode == Mode.translate) {
308                    //move nodes to new position
309                    if (moveCommand == null) {
310                        //make a new move command
311                        Collection<OsmPrimitive> nodelist = new LinkedList<OsmPrimitive>();
312                        nodelist.add(selectedSegment.getFirstNode());
313                        nodelist.add(selectedSegment.getSecondNode());
314                        moveCommand = new MoveCommand(nodelist, bestMovement.getX(), bestMovement.getY());
315                        Main.main.undoRedo.add(moveCommand);
316                    } else {
317                        //reuse existing move command
318                        moveCommand.moveAgainTo(bestMovement.getX(), bestMovement.getY());
319                    }
320                }
321    
322                Main.map.mapView.repaint();
323            }
324        }
325    
326        /**
327         * Do anything that needs to be done, then switch back to select mode
328         */
329        @Override public void mouseReleased(MouseEvent e) {
330    
331            if(!Main.map.mapView.isActiveLayerVisible())
332                return;
333    
334            if (mode == Mode.select) {
335                // Nothing to be done
336            } else {
337                if (mode == Mode.create_new) {
338                    if (e.getPoint().distance(initialMousePos) > 10 && newN1en != null) {
339                        // crete a new rectangle
340                        Collection<Command> cmds = new LinkedList<Command>();
341                        Node third = new Node(newN2en);
342                        Node fourth = new Node(newN1en);
343                        Way wnew = new Way();
344                        wnew.addNode(selectedSegment.getFirstNode());
345                        wnew.addNode(selectedSegment.getSecondNode());
346                        wnew.addNode(third);
347                        wnew.addNode(fourth);
348                        // ... and close the way
349                        wnew.addNode(selectedSegment.getFirstNode());
350                        // undo support
351                        cmds.add(new AddCommand(third));
352                        cmds.add(new AddCommand(fourth));
353                        cmds.add(new AddCommand(wnew));
354                        Command c = new SequenceCommand(tr("Extrude Way"), cmds);
355                        Main.main.undoRedo.add(c);
356                        getCurrentDataSet().setSelected(wnew);
357                    }
358                } else if (mode == Mode.extrude) {
359                    if( e.getClickCount() == 2 && e.getPoint().equals(initialMousePos) ) {
360                        // double click add a new node
361                        // Should maybe do the same as in DrawAction and fetch all nearby segments?
362                        WaySegment ws = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate);
363                        if (ws != null) {
364                            Node n = new Node(Main.map.mapView.getLatLon(e.getX(), e.getY()));
365                            EastNorth A = ws.getFirstNode().getEastNorth();
366                            EastNorth B = ws.getSecondNode().getEastNorth();
367                            n.setEastNorth(Geometry.closestPointToSegment(A, B, n.getEastNorth()));
368                            Way wnew = new Way(ws.way);
369                            wnew.addNode(ws.lowerIndex+1, n);
370                            SequenceCommand cmds = new SequenceCommand(tr("Add a new node to an existing way"),
371                                    new AddCommand(n), new ChangeCommand(ws.way, wnew));
372                            Main.main.undoRedo.add(cmds);
373                        }
374                    }
375                    else if (e.getPoint().distance(initialMousePos) > 10 && newN1en != null && selectedSegment != null) {
376                        // create extrusion
377    
378                        Collection<Command> cmds = new LinkedList<Command>();
379                        Way wnew = new Way(selectedSegment.way);
380                        int insertionPoint = selectedSegment.lowerIndex + 1;
381    
382                        //find if the new points overlap existing segments (in case of 90 degree angles)
383                        Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
384                        boolean nodeOverlapsSegment = prevNode != null && Geometry.segmentsParallel(initialN1en, prevNode.getEastNorth(), initialN1en, newN1en);
385                        boolean hasOtherWays = this.hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.way);
386    
387                        if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) {
388                            //move existing node
389                            Node n1Old = selectedSegment.getFirstNode();
390                            cmds.add(new MoveCommand(n1Old, Main.getProjection().eastNorth2latlon(newN1en)));
391                        } else {
392                            //introduce new node
393                            Node n1New = new Node(Main.getProjection().eastNorth2latlon(newN1en));
394                            wnew.addNode(insertionPoint, n1New);
395                            insertionPoint ++;
396                            cmds.add(new AddCommand(n1New));
397                        }
398    
399                        //find if the new points overlap existing segments (in case of 90 degree angles)
400                        Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
401                        nodeOverlapsSegment = nextNode != null && Geometry.segmentsParallel(initialN2en, nextNode.getEastNorth(), initialN2en, newN2en);
402                        hasOtherWays = hasNodeOtherWays(selectedSegment.getSecondNode(), selectedSegment.way);
403    
404                        if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) {
405                            //move existing node
406                            Node n2Old = selectedSegment.getSecondNode();
407                            cmds.add(new MoveCommand(n2Old, Main.getProjection().eastNorth2latlon(newN2en)));
408                        } else {
409                            //introduce new node
410                            Node n2New = new Node(Main.getProjection().eastNorth2latlon(newN2en));
411                            wnew.addNode(insertionPoint, n2New);
412                            insertionPoint ++;
413                            cmds.add(new AddCommand(n2New));
414                        }
415    
416                        //the way was a single segment, close the way
417                        if (wnew.getNodesCount() == 4) {
418                            wnew.addNode(selectedSegment.getFirstNode());
419                        }
420    
421                        cmds.add(new ChangeCommand(selectedSegment.way, wnew));
422                        Command c = new SequenceCommand(tr("Extrude Way"), cmds);
423                        Main.main.undoRedo.add(c);
424                    }
425                } else if (mode == Mode.translate) {
426                    //Commit translate
427                    //the move command is already committed in mouseDragged
428                }
429    
430                boolean alt = (e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
431                // Switch back into select mode
432                Main.map.mapView.setNewCursor(alt ? cursorCreateNew : cursor, this);
433                Main.map.mapView.removeTemporaryLayer(this);
434                selectedSegment = null;
435                moveCommand = null;
436                mode = Mode.select;
437    
438                updateStatusLine();
439                Main.map.mapView.repaint();
440            }
441        }
442    
443        /**
444         * This method tests if a node has other ways apart from the given one.
445         * @param node
446         * @param myWay
447         * @return true of node belongs only to myWay, false if there are more ways.
448         */
449        private boolean hasNodeOtherWays(Node node, Way myWay) {
450            for (OsmPrimitive p : node.getReferrers()) {
451                if (p instanceof Way && p.isUsable() && p != myWay)
452                    return true;
453            }
454            return false;
455        }
456    
457        /***
458         * This method calculates offset amount by witch to move the given segment perpendicularly for it to be in line with mouse position.
459         * @param segmentP1
460         * @param segmentP2
461         * @param targetPos
462         * @return offset amount of P1 and P2.
463         */
464        private static EastNorth calculateSegmentOffset(EastNorth segmentP1, EastNorth segmentP2, EastNorth moveDirection,
465                EastNorth targetPos) {
466            EastNorth intersectionPoint = Geometry.getLineLineIntersection(segmentP1, segmentP2, targetPos,
467                    new EastNorth(targetPos.getX() + moveDirection.getX(), targetPos.getY() + moveDirection.getY()));
468    
469            if (intersectionPoint == null)
470                return null;
471            else
472                //return distance form base to target position
473                return new EastNorth(targetPos.getX() - intersectionPoint.getX(),
474                        targetPos.getY() - intersectionPoint.getY());
475        }
476    
477    
478        /**
479         * Gets a node from selected way before given index.
480         * @param index  index of current node
481         * @return previous node or null if there are no nodes there.
482         */
483        private Node getPreviousNode(int index) {
484            if (index > 0)
485                return selectedSegment.way.getNode(index - 1);
486            else if (selectedSegment.way.isClosed())
487                return selectedSegment.way.getNode(selectedSegment.way.getNodesCount() - 2);
488            else
489                return null;
490        }
491    
492        /**
493         * Gets a node from selected way before given index.
494         * @param index index of current node
495         * @return next node or null if there are no nodes there.
496         */
497        private Node getNextNode(int index) {
498            int count = selectedSegment.way.getNodesCount();
499            if (index <  count - 1)
500                return selectedSegment.way.getNode(index + 1);
501            else if (selectedSegment.way.isClosed())
502                return selectedSegment.way.getNode(1);
503            else
504                return null;
505        }
506    
507        public void paint(Graphics2D g, MapView mv, Bounds box) {
508            if (mode == Mode.select) {
509                // Nothing to do
510            } else {
511                if (newN1en != null) {
512                    Graphics2D g2 = g;
513                    g2.setColor(selectedColor);
514                    g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
515    
516                    Point p1 = mv.getPoint(initialN1en);
517                    Point p2 = mv.getPoint(initialN2en);
518                    Point p3 = mv.getPoint(newN1en);
519                    Point p4 = mv.getPoint(newN2en);
520    
521                    if (mode == Mode.extrude || mode == Mode.create_new) {
522                        // Draw rectangle around new area.
523                        GeneralPath b = new GeneralPath();
524                        b.moveTo(p1.x, p1.y); b.lineTo(p3.x, p3.y);
525                        b.lineTo(p4.x, p4.y); b.lineTo(p2.x, p2.y);
526                        b.lineTo(p1.x, p1.y);
527                        g2.draw(b);
528                        g2.setStroke(new BasicStroke(1));
529                    } else if (mode == Mode.translate) {
530                        // Highlight the new and old segments.
531                        Line2D newline = new Line2D.Double(p3, p4);
532                        g2.draw(newline);
533                        g2.setStroke(new BasicStroke(1));
534                        Line2D oldline = new Line2D.Double(p1, p2);
535                        g2.draw(oldline);
536    
537                        if (activeMoveDirection != null) {
538    
539                            double fac = 1.0 / activeMoveDirection.distance(0,0);
540                            // mult by factor to get unit vector.
541                            EastNorth normalUnitVector = new EastNorth(activeMoveDirection.getX() * fac, activeMoveDirection.getY() * fac);
542    
543                            // Check to see if our new N1 is in a positive direction with respect to the normalUnitVector.
544                            // Even if the x component is zero, we should still be able to discern using +0.0 and -0.0
545                            if (newN1en != null && (newN1en.getX() > initialN1en.getX() != normalUnitVector.getX() > -0.0)) {
546                                // If not, use a sign-flipped version of the normalUnitVector.
547                                normalUnitVector = new EastNorth(-normalUnitVector.getX(), -normalUnitVector.getY());
548                            }
549    
550                            //HACK: swap Y, because the target pixels are top down, but EastNorth is bottom-up.
551                            //This is normally done by MapView.getPoint, but it does not work on vectors.
552                            normalUnitVector.setLocation(normalUnitVector.getX(), -normalUnitVector.getY());
553    
554                            // Draw a guideline along the normal.
555                            Line2D normline;
556                            Point2D centerpoint = new Point2D.Double((p1.getX()+p2.getX())*0.5, (p1.getY()+p2.getY())*0.5);
557                            normline = createSemiInfiniteLine(centerpoint, normalUnitVector, g2);
558                            g2.draw(normline);
559    
560                            // Draw right angle marker on initial position, only when moving at right angle
561                            if (activeMoveDirection == possibleMoveDirections.get(0)) {
562                                // EastNorth units per pixel
563                                double factor = 1.0/g2.getTransform().getScaleX();
564    
565                                double raoffsetx = 8.0*factor*normalUnitVector.getX();
566                                double raoffsety = 8.0*factor*normalUnitVector.getY();
567                                Point2D ra1 = new Point2D.Double(centerpoint.getX()+raoffsetx, centerpoint.getY()+raoffsety);
568                                Point2D ra3 = new Point2D.Double(centerpoint.getX()-raoffsety, centerpoint.getY()+raoffsetx);
569                                Point2D ra2 = new Point2D.Double(ra1.getX()-raoffsety, ra1.getY()+raoffsetx);
570                                GeneralPath ra = new GeneralPath();
571                                ra.moveTo((float)ra1.getX(), (float)ra1.getY());
572                                ra.lineTo((float)ra2.getX(), (float)ra2.getY());
573                                ra.lineTo((float)ra3.getX(), (float)ra3.getY());
574                                g2.draw(ra);
575                            }
576                        }
577                    }
578                }
579            }
580        }
581    
582        /**
583         * Create a new Line that extends off the edge of the viewport in one direction
584         * @param start The start point of the line
585         * @param unitvector A unit vector denoting the direction of the line
586         * @param g the Graphics2D object  it will be used on
587         */
588        static private Line2D createSemiInfiniteLine(Point2D start, Point2D unitvector, Graphics2D g) {
589            Rectangle bounds = g.getDeviceConfiguration().getBounds();
590            try {
591                AffineTransform invtrans = g.getTransform().createInverse();
592                Point2D widthpoint = invtrans.deltaTransform(new Point2D.Double(bounds.width,0), null);
593                Point2D heightpoint = invtrans.deltaTransform(new Point2D.Double(0,bounds.height), null);
594    
595                // Here we should end up with a gross overestimate of the maximum viewport diagonal in what
596                // Graphics2D calls 'user space'. Essentially a manhattan distance of manhattan distances.
597                // This can be used as a safe length of line to generate which will always go off-viewport.
598                double linelength = Math.abs(widthpoint.getX()) + Math.abs(widthpoint.getY()) + Math.abs(heightpoint.getX()) + Math.abs(heightpoint.getY());
599    
600                return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * linelength) , start.getY() + (unitvector.getY() * linelength)));
601            }
602            catch (NoninvertibleTransformException e) {
603                return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * 10) , start.getY() + (unitvector.getY() * 10)));
604            }
605        }
606    }