// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.plugins.Splinex; import static org.openstreetmap.josm.plugins.Splinex.SplinexPlugin.EPSILON; import static org.openstreetmap.josm.tools.I18n.marktr; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Color; import java.awt.Cursor; import java.awt.Graphics2D; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.util.Collection; import java.util.HashMap; import java.util.Map; import javax.swing.AbstractAction; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.mapmode.MapMode; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.command.MoveCommand; 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.visitor.paint.MapPaintSettings; import org.openstreetmap.josm.data.preferences.ColorProperty; 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.LayerManager.LayerAddEvent; import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent; import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener; import org.openstreetmap.josm.gui.layer.MapViewPaintable; import org.openstreetmap.josm.gui.layer.OsmDataLayer; import org.openstreetmap.josm.gui.util.KeyPressReleaseListener; import org.openstreetmap.josm.gui.util.ModifierListener; import org.openstreetmap.josm.plugins.Splinex.Spline.PointHandle; import org.openstreetmap.josm.plugins.Splinex.Spline.SplinePoint; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.Shortcut; @SuppressWarnings("serial") public class DrawSplineAction extends MapMode implements MapViewPaintable, KeyPressReleaseListener, ModifierListener, LayerChangeListener, ActiveLayerChangeListener { private final Cursor cursorJoinNode; private final Cursor cursorJoinWay; private Color rubberLineColor; private final Shortcut backspaceShortcut; private final BackSpaceAction backspaceAction; boolean drawHelperLine; public DrawSplineAction(MapFrame mapFrame) { super(tr("Spline drawing"), // name "spline2", // icon name tr("Draw a spline curve"), // tooltip mapFrame, getCursor()); backspaceShortcut = Shortcut.registerShortcut("mapmode:backspace", tr("Backspace in Add mode"), KeyEvent.VK_BACK_SPACE, Shortcut.DIRECT); backspaceAction = new BackSpaceAction(); cursorJoinNode = ImageProvider.getCursor("crosshair", "joinnode"); cursorJoinWay = ImageProvider.getCursor("crosshair", "joinway"); Main.getLayerManager().addLayerChangeListener(this); Main.getLayerManager().addActiveLayerChangeListener(this); readPreferences(); } private static Cursor getCursor() { try { return ImageProvider.getCursor("crosshair", "spline"); } catch (Exception e) { Main.error(e); } return Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR); } @Override public void enterMode() { if (!isEnabled()) return; super.enterMode(); Main.registerActionShortcut(backspaceAction, backspaceShortcut); Main.map.mapView.addMouseListener(this); Main.map.mapView.addMouseMotionListener(this); Main.map.mapView.addTemporaryLayer(this); Main.map.keyDetector.addModifierListener(this); Main.map.keyDetector.addKeyListener(this); } int initialMoveDelay, initialMoveThreshold; @Override protected void readPreferences() { rubberLineColor = new ColorProperty(marktr("helper line"), Color.RED).get(); initialMoveDelay = Main.pref.getInteger("edit.initial-move-", 200); initialMoveThreshold = Main.pref.getInteger("edit.initial-move-threshold", 5); initialMoveThreshold *= initialMoveThreshold; drawHelperLine = Main.pref.getBoolean("draw.helper-line", true); } @Override public void exitMode() { super.exitMode(); Main.map.mapView.removeMouseListener(this); Main.map.mapView.removeMouseMotionListener(this); Main.map.mapView.removeTemporaryLayer(this); Main.unregisterActionShortcut(backspaceAction, backspaceShortcut); Main.map.statusLine.activateAnglePanel(false); Main.map.keyDetector.removeModifierListener(this); Main.map.keyDetector.removeKeyListener(this); removeHighlighting(); Main.map.mapView.repaint(); } @Override public void modifiersChanged(int modifiers) { updateKeyModifiers(modifiers); } private Long mouseDownTime; private PointHandle ph; private Point helperEndpoint; private Point clickPos; public int index = 0; boolean lockCounterpart; private MoveCommand mc; private boolean dragControl; private boolean dragSpline; @Override public void mousePressed(MouseEvent e) { mouseDownTime = null; updateKeyModifiers(e); if (e.getButton() != MouseEvent.BUTTON1) { helperEndpoint = null; // Hide helper line when panning return; } if (!Main.map.mapView.isActiveLayerDrawable()) return; Spline spl = getSpline(); if (spl == null) return; helperEndpoint = null; dragControl = false; dragSpline = false; mouseDownTime = System.currentTimeMillis(); ph = spl.getNearestPoint(Main.map.mapView, e.getPoint()); if (e.getClickCount() == 2) { if (!spl.isClosed() && spl.nodeCount() > 1 && ph != null && ph.point == SplinePoint.ENDPOINT && ((ph.idx == 0 && direction == 1) || (ph.idx == spl.nodeCount() - 1 && direction == -1))) { Main.main.undoRedo.add(spl.new CloseSplineCommand()); return; } spl.finishSpline(); Main.map.repaint(); return; } clickPos = e.getPoint(); if (ph != null) { if (ctrl) { if (ph.point == SplinePoint.ENDPOINT) { ph = ph.otherPoint(SplinePoint.CONTROL_NEXT); lockCounterpart = true; } else lockCounterpart = false; } else { lockCounterpart = (ph.point != SplinePoint.ENDPOINT && Math.abs(ph.sn.cprev.east() + ph.sn.cnext.east()) < EPSILON && Math.abs(ph.sn.cprev.north() + ph.sn.cnext.north()) < EPSILON); } if (ph.point == SplinePoint.ENDPOINT && !Main.main.undoRedo.commands.isEmpty()) { Command cmd = Main.main.undoRedo.commands.getLast(); if (cmd instanceof MoveCommand) { mc = (MoveCommand) cmd; Collection<Node> pp = mc.getParticipatingPrimitives(); if (pp.size() != 1 || !pp.contains(ph.sn.node)) mc = null; else mc.changeStartPoint(ph.sn.node.getEastNorth()); } } if (ph.point != SplinePoint.ENDPOINT && !Main.main.undoRedo.commands.isEmpty()) { Command cmd = Main.main.undoRedo.commands.getLast(); if (!(cmd instanceof Spline.EditSplineCommand && ((Spline.EditSplineCommand) cmd).sn == ph.sn)) dragControl = true; } return; } if (!ctrl && spl.doesHit(e.getX(), e.getY(), Main.map.mapView)) { dragSpline = true; return; } if (spl.isClosed()) return; if (direction == 0) if (spl.nodeCount() < 2) direction = 1; else return; Node n = null; boolean existing = false; if (!ctrl) { n = Main.map.mapView.getNearestNode(e.getPoint(), OsmPrimitive::isUsable); existing = true; } if (n == null) { n = new Node(Main.map.mapView.getLatLon(e.getX(), e.getY())); existing = false; } int idx = direction == -1 ? 0 : spl.nodeCount(); Main.main.undoRedo.add(spl.new AddSplineNodeCommand(new Spline.SNode(n), existing, idx)); ph = spl.new PointHandle(idx, direction == -1 ? SplinePoint.CONTROL_PREV : SplinePoint.CONTROL_NEXT); lockCounterpart = true; Main.map.repaint(); } @Override public void mouseReleased(MouseEvent e) { mc = null; mouseDownTime = null; dragSpline = false; clickPos = null; mouseMoved(e); if (direction == 0 && ph != null) { if (ph.idx >= ph.getSpline().nodeCount() - 1) direction = 1; else if (ph.idx == 0) direction = -1; } } @Override public void mouseDragged(MouseEvent e) { updateKeyModifiers(e); if (mouseDownTime == null) return; if (!Main.map.mapView.isActiveLayerDrawable()) return; if (System.currentTimeMillis() - mouseDownTime < initialMoveDelay) return; Spline spl = getSpline(); if (spl == null) return; if (spl.isEmpty()) return; if (clickPos != null && clickPos.distanceSq(e.getPoint()) < initialMoveThreshold) return; EastNorth en = Main.map.mapView.getEastNorth(e.getX(), e.getY()); if (Main.getProjection().eastNorth2latlon(en).isOutSideWorld()) return; if (dragSpline) { if (mc == null) { mc = new MoveCommand(spl.getNodes(), Main.map.mapView.getEastNorth(clickPos.x, clickPos.y), en); Main.main.undoRedo.add(mc); clickPos = null; } else mc.applyVectorTo(en); Main.map.repaint(); return; } clickPos = null; if (ph == null) return; if (ph.point == SplinePoint.ENDPOINT) { if (mc == null) { mc = new MoveCommand(ph.sn.node, ph.sn.node.getEastNorth(), en); Main.main.undoRedo.add(mc); } else mc.applyVectorTo(en); } else { if (dragControl) { Main.main.undoRedo.add(new Spline.EditSplineCommand(ph.sn)); dragControl = false; } ph.movePoint(en); if (lockCounterpart) { if (ph.point == SplinePoint.CONTROL_NEXT) ph.sn.cprev = new EastNorth(0, 0).subtract(ph.sn.cnext); else if (ph.point == SplinePoint.CONTROL_PREV) ph.sn.cnext = new EastNorth(0, 0).subtract(ph.sn.cprev); } } Main.map.repaint(); } Node nodeHighlight; short direction; @Override public void mouseMoved(MouseEvent e) { updateKeyModifiers(e); if (!Main.map.mapView.isActiveLayerDrawable()) return; Spline spl = getSpline(); if (spl == null) return; Point oldHelperEndpoint = helperEndpoint; PointHandle oldph = ph; boolean redraw = false; ph = spl.getNearestPoint(Main.map.mapView, e.getPoint()); if (ph == null) if (!ctrl && spl.doesHit(e.getX(), e.getY(), Main.map.mapView)) { helperEndpoint = null; Main.map.mapView.setNewCursor(Cursor.MOVE_CURSOR, this); } else { Node n = null; if (!ctrl) n = Main.map.mapView.getNearestNode(e.getPoint(), OsmPrimitive::isUsable); if (n == null) { redraw = removeHighlighting(); helperEndpoint = e.getPoint(); Main.map.mapView.setNewCursor(cursor, this); } else { redraw = setHighlight(n); Main.map.mapView.setNewCursor(cursorJoinNode, this); helperEndpoint = Main.map.mapView.getPoint(n); } } else { helperEndpoint = null; Main.map.mapView.setNewCursor(cursorJoinWay, this); if (ph.point == SplinePoint.ENDPOINT) redraw = setHighlight(ph.sn.node); else redraw = removeHighlighting(); } if (!drawHelperLine || spl.isClosed() || direction == 0) helperEndpoint = null; if (redraw || oldHelperEndpoint != helperEndpoint || (oldph == null && ph != null) || (oldph != null && !oldph.equals(ph))) Main.map.repaint(); } /** * Repaint on mouse exit so that the helper line goes away. */ @Override public void mouseExited(MouseEvent e) { if (!Main.map.mapView.isActiveLayerDrawable()) return; removeHighlighting(); helperEndpoint = null; Main.map.mapView.repaint(); } private boolean setHighlight(Node n) { if (nodeHighlight == n) return false; removeHighlighting(); nodeHighlight = n; n.setHighlighted(true); return true; } /** * Removes target highlighting from primitives. Issues repaint if required. * Returns true if a repaint has been issued. */ private boolean removeHighlighting() { if (nodeHighlight != null) { nodeHighlight.setHighlighted(false); nodeHighlight = null; return true; } return false; } @Override public void paint(Graphics2D g, MapView mv, Bounds box) { Spline spl = getSpline(); if (spl == null) return; spl.paint(g, mv, rubberLineColor, Color.green, helperEndpoint, direction); if (ph != null && (ph.point != SplinePoint.ENDPOINT || (nodeHighlight != null && nodeHighlight.isDeleted()))) { g.setColor(MapPaintSettings.INSTANCE.getSelectedColor()); Point p = mv.getPoint(ph.getPoint()); g.fillRect(p.x - 1, p.y - 1, 3, 3); } } @Override public boolean layerIsSupported(Layer l) { return l instanceof OsmDataLayer; } @Override protected void updateEnabledState() { setEnabled(getLayerManager().getEditLayer() != null); } public static class BackSpaceAction extends AbstractAction { @Override public void actionPerformed(ActionEvent e) { Main.main.undoRedo.undo(); } } private Spline splCached; Spline getSpline() { if (splCached != null) return splCached; Layer l = getLayerManager().getEditLayer(); if (!(l instanceof OsmDataLayer)) return null; splCached = layerSplines.get(l); if (splCached == null) splCached = new Spline(); layerSplines.put(l, splCached); return splCached; } @Override public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { splCached = layerSplines.get(Main.getLayerManager().getActiveLayer()); } Map<Layer, Spline> layerSplines = new HashMap<>(); @Override public void layerOrderChanged(LayerOrderChangeEvent e) { // Do nothing } @Override public void layerAdded(LayerAddEvent e) { // Do nothing } @Override public void layerRemoving(LayerRemoveEvent e) { layerSplines.remove(e.getRemovedLayer()); splCached = null; } @Override public void doKeyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_DELETE && ph != null) { Spline spl = ph.getSpline(); if (spl.nodeCount() == 3 && spl.isClosed() && ph.idx == 1) return; // Don't allow to delete node when it results with two-node closed spline Main.main.undoRedo.add(spl.new DeleteSplineNodeCommand(ph.idx)); e.consume(); } if (e.getKeyCode() == KeyEvent.VK_ESCAPE && direction != 0) { direction = 0; Main.map.mapView.repaint(); e.consume(); } } @Override public void doKeyReleased(KeyEvent e) { // Do nothing } }