// License: GPL. Copyright 2007 by Immanuel Scholz and others package org.openstreetmap.josm.actions.mapmode; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Cursor; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.geom.GeneralPath; import java.awt.geom.Line2D; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.util.Collection; import java.util.LinkedList; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.command.AddCommand; import org.openstreetmap.josm.command.ChangeCommand; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.command.MoveCommand; import org.openstreetmap.josm.command.SequenceCommand; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.coor.EastNorth; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.data.osm.WaySegment; import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors; import org.openstreetmap.josm.gui.MapFrame; import org.openstreetmap.josm.gui.MapView; import org.openstreetmap.josm.gui.layer.Layer; import org.openstreetmap.josm.gui.layer.MapViewPaintable; import org.openstreetmap.josm.gui.layer.OsmDataLayer; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.Shortcut; /** * Makes a rectangle from a line, or modifies a rectangle. */ public class ExtrudeAction extends MapMode implements MapViewPaintable { enum Mode { extrude, translate, select } private Mode mode = Mode.select; private long mouseDownTime = 0; private WaySegment selectedSegment = null; private Color selectedColor; /** * The old cursor before the user pressed the mouse button. */ private Cursor oldCursor; /** * The position of the mouse cursor when the drag action was initiated. */ private Point initialMousePos; /** * The time which needs to pass between click and release before something * counts as a move, in milliseconds */ private int initialMoveDelay = 200; /** * The initial EastNorths of node1 and node2 */ private EastNorth initialN1en; private EastNorth initialN2en; /** * The new EastNorths of node1 and node2 */ private EastNorth newN1en; private EastNorth newN2en; /** * This is to work around some deficiencies in MoveCommand when translating */ private EastNorth lastTranslatedN1en; /** * Create a new SelectAction * @param mapFrame The MapFrame this action belongs to. */ public ExtrudeAction(MapFrame mapFrame) { super(tr("Extrude"), "extrude/extrude", tr("Create areas"), Shortcut.registerShortcut("mapmode:extrude", tr("Mode: {0}", tr("Extrude")), KeyEvent.VK_X, Shortcut.GROUP_EDIT), mapFrame, getCursor("normal", "rectangle", Cursor.DEFAULT_CURSOR)); putValue("help", "Action/Extrude/Extrude"); initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay",200); selectedColor = PaintColors.SELECTED.get(); } private static Cursor getCursor(String name, String mod, int def) { try { return ImageProvider.getCursor(name, mod); } catch (Exception e) { } return Cursor.getPredefinedCursor(def); } private void setCursor(Cursor c) { if (oldCursor == null) { oldCursor = Main.map.mapView.getCursor(); Main.map.mapView.setCursor(c); } } private void restoreCursor() { if (oldCursor != null) { Main.map.mapView.setCursor(oldCursor); oldCursor = null; } } @Override public void enterMode() { super.enterMode(); Main.map.mapView.addMouseListener(this); Main.map.mapView.addMouseMotionListener(this); } @Override public void exitMode() { super.exitMode(); Main.map.mapView.removeMouseListener(this); Main.map.mapView.removeMouseMotionListener(this); Main.map.mapView.removeTemporaryLayer(this); } /** * Perform action depending on what mode we're in. */ @Override public void mouseDragged(MouseEvent e) { if(!Main.map.mapView.isActiveLayerVisible()) return; // do not count anything as a drag if it lasts less than 100 milliseconds. if (System.currentTimeMillis() - mouseDownTime < initialMoveDelay) return; if (mode == Mode.select) { // Just sit tight and wait for mouse to be released. } else { Node nd1 = selectedSegment.way.getNode(selectedSegment.lowerIndex); Node nd2 = selectedSegment.way.getNode(selectedSegment.lowerIndex + 1); EastNorth en1 = nd1.getEastNorth(); EastNorth en2 = nd2.getEastNorth(); EastNorth en3 = Main.map.mapView.getEastNorth(e.getPoint().x, e.getPoint().y); double u = ((en3.east() - en1.east()) * (en2.east() - en1.east()) + (en3.north() - en1.north()) * (en2.north() - en1.north())) / en2.distanceSq(en1); // the point on the segment from which the distance to mouse pos is shortest EastNorth base = new EastNorth(en1.east() + u * (en2.east() - en1.east()), en1.north() + u * (en2.north() - en1.north())); // find out the distance, in metres, between the base point and the mouse cursor double distance = Main.proj.eastNorth2latlon(base).greatCircleDistance(Main.proj.eastNorth2latlon(en3)); Main.map.statusLine.setDist(distance); updateStatusLine(); // compute vertical and horizontal components. double xoff = en3.east() - base.east(); double yoff = en3.north() - base.north(); newN1en = new EastNorth(en1.getX() + xoff, en1.getY() + yoff); newN2en = new EastNorth(en2.getX() + xoff, en2.getY() + yoff); // find out the distance, in metres, between the initial position of N1 and the new one. Main.map.statusLine.setDist(Main.proj.eastNorth2latlon(initialN1en).greatCircleDistance(Main.proj.eastNorth2latlon(newN1en))); updateStatusLine(); setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); if (mode == Mode.extrude) { } else if (mode == Mode.translate) { Command c = !Main.main.undoRedo.commands.isEmpty() ? Main.main.undoRedo.commands.getLast() : null; if (c instanceof SequenceCommand) { c = ((SequenceCommand)c).getLastCommand(); } Node n1 = selectedSegment.way.getNode(selectedSegment.lowerIndex); Node n2 = selectedSegment.way.getNode(selectedSegment.lowerIndex+1); EastNorth difference = new EastNorth(newN1en.getX()-lastTranslatedN1en.getX(), newN1en.getY()-lastTranslatedN1en.getY()); // Better way of testing list equality non-order-sensitively? if (c instanceof MoveCommand && ((MoveCommand)c).getMovedNodes().contains(n1) && ((MoveCommand)c).getMovedNodes().contains(n2) && ((MoveCommand)c).getMovedNodes().size() == 2) { // MoveCommand doesn't let us know how much it has already moved the selection // so we have to do some ugly record-keeping. ((MoveCommand)c).moveAgain(difference.getX(), difference.getY()); lastTranslatedN1en = newN1en; } else { Collection<OsmPrimitive> nodelist = new LinkedList<OsmPrimitive>(); nodelist.add(n1); nodelist.add(n2); Main.main.undoRedo.add(c = new MoveCommand(nodelist, difference.getX(), difference.getY())); lastTranslatedN1en = newN1en; } } Main.map.mapView.repaint(); } } /** * Create a new Line that extends off the edge of the viewport in one direction * @param start The start point of the line * @param unitvector A unit vector denoting the direction of the line * @param g the Graphics2D object it will be used on */ static private Line2D createSemiInfiniteLine(Point2D start, Point2D unitvector, Graphics2D g) { Rectangle bounds = g.getDeviceConfiguration().getBounds(); try { AffineTransform invtrans = g.getTransform().createInverse(); Point2D widthpoint = invtrans.deltaTransform(new Point2D.Double(bounds.width,0), null); Point2D heightpoint = invtrans.deltaTransform(new Point2D.Double(0,bounds.height), null); // Here we should end up with a gross overestimate of the maximum viewport diagonal in what // Graphics2D calls 'user space'. Essentially a manhattan distance of manhattan distances. // This can be used as a safe length of line to generate which will always go off-viewport. double linelength = Math.abs(widthpoint.getX()) + Math.abs(widthpoint.getY()) + Math.abs(heightpoint.getX()) + Math.abs(heightpoint.getY()); return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * linelength) , start.getY() + (unitvector.getY() * linelength))); } catch (NoninvertibleTransformException e) { return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * 10) , start.getY() + (unitvector.getY() * 10))); } } public void paint(Graphics2D g, MapView mv, Bounds box) { if (mode == Mode.select) { // Nothing to do } else { if (newN1en != null) { Graphics2D g2 = g; g2.setColor(selectedColor); g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); Point p1 = mv.getPoint(initialN1en); Point p2 = mv.getPoint(initialN2en); Point p3 = mv.getPoint(newN1en); Point p4 = mv.getPoint(newN2en); if (mode == Mode.extrude) { // Draw rectangle around new area. GeneralPath b = new GeneralPath(); b.moveTo(p1.x, p1.y); b.lineTo(p3.x, p3.y); b.lineTo(p4.x, p4.y); b.lineTo(p2.x, p2.y); b.lineTo(p1.x, p1.y); g2.draw(b); g2.setStroke(new BasicStroke(1)); } else if (mode == Mode.translate) { // Highlight the new and old segments. Line2D newline = new Line2D.Double(p3, p4); g2.draw(newline); g2.setStroke(new BasicStroke(1)); Line2D oldline = new Line2D.Double(p1, p2); g2.draw(oldline); EastNorth segmentVector = new EastNorth(initialN2en.getX()-initialN1en.getX(), initialN2en.getY()-initialN1en.getY()); double fac = 1.0 / Math.hypot(segmentVector.getX(), segmentVector.getY()); // swap coords to get normal, mult by factor to get unit vector. EastNorth normalUnitVector = new EastNorth(segmentVector.getY() * fac, segmentVector.getX() * fac); // Draw a guideline along the normal. Line2D normline; Point2D centerpoint = new Point2D.Double((p1.getX()+p2.getX())*0.5, (p1.getY()+p2.getY())*0.5); EastNorth drawnorm; // Check to see if our new N1 is in a positive direction with respect to the normalUnitVector. // Even if the x component is zero, we should still be able to discern using +0.0 and -0.0 if (newN1en == null || (newN1en.getX() > initialN1en.getX() == normalUnitVector.getX() > -0.0)) { drawnorm = normalUnitVector; } else { // If not, use a sign-flipped version of the normalUnitVector. drawnorm = new EastNorth(-normalUnitVector.getX(), -normalUnitVector.getY()); } normline = createSemiInfiniteLine(centerpoint, drawnorm, g2); g2.draw(normline); // EastNorth units per pixel double factor = 1.0/g2.getTransform().getScaleX(); // Draw right angle marker on initial position. double raoffsetx = 8.0*factor*drawnorm.getX(); double raoffsety = 8.0*factor*drawnorm.getY(); Point2D ra1 = new Point2D.Double(centerpoint.getX()+raoffsetx, centerpoint.getY()+raoffsety); Point2D ra3 = new Point2D.Double(centerpoint.getX()-raoffsety, centerpoint.getY()+raoffsetx); Point2D ra2 = new Point2D.Double(ra1.getX()-raoffsety, ra1.getY()+raoffsetx); GeneralPath ra = new GeneralPath(); ra.moveTo((float)ra1.getX(), (float)ra1.getY()); ra.lineTo((float)ra2.getX(), (float)ra2.getY()); ra.lineTo((float)ra3.getX(), (float)ra3.getY()); g2.draw(ra); } } } } /** * If the left mouse button is pressed over a segment, switch * to either extrude or translate mode depending on whether ctrl is held. */ @Override public void mousePressed(MouseEvent e) { if(!Main.map.mapView.isActiveLayerVisible()) return; if (!(Boolean)this.getValue("active")) return; if (e.getButton() != MouseEvent.BUTTON1) return; // boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0; // boolean alt = (e.getModifiers() & ActionEvent.ALT_MASK) != 0; // boolean shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0; selectedSegment = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate); if (selectedSegment == null) { // If nothing gets caught, stay in select mode } else { // Otherwise switch to another mode // For extrusion, these positions are actually never changed, // but keeping note of this anyway allows us to not continually // look it up and also allows us to unify code with the translate mode initialN1en = selectedSegment.way.getNode(selectedSegment.lowerIndex).getEastNorth(); initialN2en = selectedSegment.way.getNode(selectedSegment.lowerIndex + 1).getEastNorth(); // Signifies that nothing has happened yet newN1en = null; newN2en = null; Main.map.mapView.addTemporaryLayer(this); updateStatusLine(); Main.map.mapView.repaint(); // Make note of time pressed mouseDownTime = System.currentTimeMillis(); // Make note of mouse position initialMousePos = e.getPoint(); // Switch mode. if ( (e.getModifiers() & ActionEvent.CTRL_MASK) != 0 ) { mode = Mode.translate; lastTranslatedN1en = initialN1en; } else { mode = Mode.extrude; getCurrentDataSet().setSelected(selectedSegment.way); } } } /** * Do anything that needs to be done, then switch back to select mode */ @Override public void mouseReleased(MouseEvent e) { if(!Main.map.mapView.isActiveLayerVisible()) return; if (mode == Mode.select) { // Nothing to be done } else { if (mode == Mode.extrude) { if (e.getPoint().distance(initialMousePos) > 10 && newN1en != null) { // Commit extrusion Node n1 = selectedSegment.way.getNode(selectedSegment.lowerIndex); //Node n2 = selectedSegment.way.getNode(selectedSegment.lowerIndex+1); Node n3 = new Node(Main.proj.eastNorth2latlon(newN2en)); Node n4 = new Node(Main.proj.eastNorth2latlon(newN1en)); Way wnew = new Way(selectedSegment.way); wnew.addNode(selectedSegment.lowerIndex+1, n3); wnew.addNode(selectedSegment.lowerIndex+1, n4); if (wnew.getNodesCount() == 4) { wnew.addNode(n1); } Collection<Command> cmds = new LinkedList<Command>(); cmds.add(new AddCommand(n4)); cmds.add(new AddCommand(n3)); cmds.add(new ChangeCommand(selectedSegment.way, wnew)); Command c = new SequenceCommand(tr("Extrude Way"), cmds); Main.main.undoRedo.add(c); } } else if (mode == Mode.translate) { // I don't think there's anything to do } // Switch back into select mode restoreCursor(); Main.map.mapView.removeTemporaryLayer(this); selectedSegment = null; mode = Mode.select; updateStatusLine(); Main.map.mapView.repaint(); } } @Override public String getModeHelpText() { if (mode == Mode.translate) return tr("Move a segment along its normal, then release the mouse button."); else if (mode == Mode.extrude) return tr("Draw a rectangle of the desired size, then release the mouse button."); else return tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal."); } @Override public boolean layerIsSupported(Layer l) { return l instanceof OsmDataLayer; } }