// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.plugins.fastdraw; 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.KeyEvent; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.swing.JOptionPane; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.mapmode.MapMode; import org.openstreetmap.josm.command.AddCommand; import org.openstreetmap.josm.command.ChangeCommand; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.command.DeleteCommand; import org.openstreetmap.josm.command.SequenceCommand; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.coor.LatLon; 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.gui.ConditionalOptionPaneUtil; import org.openstreetmap.josm.gui.MapFrame; import org.openstreetmap.josm.gui.MapView; import org.openstreetmap.josm.gui.datatransfer.OsmTransferHandler; 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.gui.util.KeyPressReleaseListener; import org.openstreetmap.josm.gui.util.ModifierListener; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.Shortcut; import org.openstreetmap.josm.tools.TextTagParser; class FastDrawingMode extends MapMode implements MapViewPaintable, KeyPressReleaseListener, ModifierListener { // CHECKSTYLE.OFF: LineLength private static final String SIMPLIFYMODE_MESSAGE = tr("Q=Options, Enter=save, Ctrl-Enter=save with tags, Up/Down=tune"); private static final String DRAWINGMODE_MESSAGE = tr("Click or Click&drag to continue, Ctrl-Click to add fixed node, Shift-Click to delete, Enter to simplify or save, Ctrl-Shift-Click to start new line"); // CHECKSTYLE.ON: LineLength private FDSettings settings; private final DrawnPolyLine line; private MapView mv; private String statusText; private boolean drawing; private double eps; private final Cursor cursorDraw; private final Cursor cursorCtrl; private final Cursor cursorShift; private final Cursor cursorReady; //private final Cursor cursorNode; private final Cursor cursorDrawing; private boolean nearSomeNode; private LatLon highlightedFragmentStart; private int nearestPointIndex; private int dragNode = -1; private List<Node> oldNodes; private boolean lineWasSaved; private boolean deltaChanged; private Way oldWay; FastDrawingMode(MapFrame mapFrame) { super(tr("FastDrawing"), "turbopen.png", tr("Fast drawing mode"), Shortcut.registerShortcut("mapmode:fastdraw", tr("Mode: {0}", tr("Fast drawing mode")), KeyEvent.VK_F, Shortcut.SHIFT), mapFrame, Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); line = new DrawnPolyLine(); cursorDraw = ImageProvider.getCursor("crosshair", null); cursorCtrl = ImageProvider.getCursor("crosshair", "fixed"); cursorShift = ImageProvider.getCursor("crosshair", "new"); cursorReady = ImageProvider.getCursor("crosshair", "ready"); //cursorNode = ImageProvider.getCursor("crosshair", "joinnode"); cursorDrawing = ImageProvider.getCursor("crosshair", "mode"); //loadPrefs(); } // <editor-fold defaultstate="collapsed" desc="Event listeners"> @Override public void enterMode() { super.enterMode(); lineWasSaved = false; settings = new FDSettings(); settings.loadPrefs(); settings.savePrefs(); eps = settings.startingEps; mv = Main.map.mapView; line.setMv(mv); if (getLayerManager().getEditDataSet() == null) return; Main.map.mapView.addMouseListener(this); Main.map.mapView.addMouseMotionListener(this); Main.map.mapView.addTemporaryLayer(this); Main.map.keyDetector.addKeyListener(this); Main.map.keyDetector.addModifierListener(this); } @Override public void exitMode() { super.exitMode(); if (line.wasSimplified() && !lineWasSaved) saveAsWay(false); Main.map.mapView.removeMouseListener(this); Main.map.mapView.removeMouseMotionListener(this); Main.map.mapView.removeTemporaryLayer(this); Main.map.keyDetector.removeKeyListener(this); Main.map.keyDetector.removeModifierListener(this); settings.savePrefs(); Main.map.mapView.setCursor(cursorDraw); repaint(); } @Override public boolean layerIsSupported(Layer l) { return l instanceof OsmDataLayer; } @Override protected void updateEnabledState() { setEnabled(getLayerManager().getEditLayer() != null); } private final ArrayList<Point> fixedPoints = new ArrayList<>(3000); // temporary storate for paint ////////// Event listener methods @Override public void paint(Graphics2D g, MapView mv, Bounds bbox) { LinkedList<LatLon> pts = line.getPoints(); if (pts.isEmpty()) return; if (line.wasSimplified()) { // we are drawing simplified version, that exists g.setStroke(settings.simplifiedStroke); } else { g.setStroke(settings.normalStroke); } int bigDotSize = settings.bigDotSize; Point p1, p2; LatLon pp1, pp2; p1 = line.getPoint(pts.get(0)); g.setColor(settings.COLOR_FIXED.get()); g.fillOval(p1.x - bigDotSize/2, p1.y - bigDotSize/2, bigDotSize, bigDotSize); Color lineColor, initLineColor; initLineColor = line.wasSimplified() ? settings.COLOR_SIMPLIFIED.get() : settings.COLOR_NORMAL.get(); lineColor = initLineColor; int rp, dp; dp = line.wasSimplified() ? settings.bigDotSize : settings.dotSize; rp = dp/2; if (pts.size() > 1) { Iterator<LatLon> it1, it2; it1 = pts.listIterator(0); it2 = pts.listIterator(1); fixedPoints.clear(); for (int i = 0; i < pts.size() - 1; i++) { pp1 = it1.next(); p1 = line.getPoint(pp1); pp2 = it2.next(); p2 = line.getPoint(pp2); if (shift && highlightedFragmentStart == pp1 && nearestPointIndex < 0) { lineColor = settings.COLOR_SELECTEDFRAGMENT.get(); } if (!shift && line.isLastPoint(i)) { lineColor = settings.COLOR_EDITEDFRAGMENT.get(); } g.setColor(lineColor); g.drawLine(p1.x, p1.y, p2.x, p2.y); if (line.isFixed(pp2)) { lineColor = initLineColor; fixedPoints.add(p2); } else { g.fillRect(p2.x - rp, p2.y - rp, dp, dp); } if (!drawing) { if (!line.wasSimplified() && nearestPointIndex == i+1) { if (shift) { // highlight node to delete g.setStroke(settings.deleteStroke); g.setColor(settings.COLOR_DELETE.get()); g.drawLine(p2.x - 5, p2.y - 5, p2.x + 5, p2.y + 5); g.drawLine(p2.x - 5, p2.y + 5, p2.x + 5, p2.y - 5); g.setStroke(settings.normalStroke); } else if (ctrl) { // highlight node to toggle fixation g.setStroke(settings.deleteStroke); g.setColor(line.isFixed(pp2) ? settings.COLOR_NORMAL.get() : settings.COLOR_FIXED.get()); g.fillOval(p2.x - bigDotSize/2-2, p2.y - bigDotSize/2-2, bigDotSize+4, bigDotSize+4); g.setStroke(settings.normalStroke); } } } } g.setColor(settings.COLOR_FIXED.get()); for (Point p: fixedPoints) { g.fillOval(p.x - bigDotSize/2, p.y - bigDotSize/2, bigDotSize, bigDotSize); } } if (settings.drawLastSegment && !drawing && dragNode < 0 && !shift && nearestPointIndex <= 0 && !line.wasSimplified()) { // draw line to current point g.setColor(lineColor); Point lp = line.getLastPoint(); Point mp = mv.getMousePosition(); if (lp != null && mp != null) g.drawLine(lp.x, lp.y, mp.x, mp.y); } if (deltaChanged) { g.setColor(lineColor); Point lp = line.getLastPoint(); int r = (int) settings.minPixelsBetweenPoints; if (lp != null) g.drawOval(lp.x-r, lp.y-r, 2*r, 2*r); } } @Override public void mousePressed(MouseEvent e) { if (!isEnabled()) return; if (e.getButton() != MouseEvent.BUTTON1) return; updateKeyModifiers(e); requestFocusInMapView(); int idx = line.findClosestPoint(e.getPoint(), settings.maxDist); if (idx == 0 && !line.isClosed()) { line.closeLine(); // the way should become closed drawing = false; dragNode = 0; updateCursor(); return; } autoCloseIfNeeded(); if (ctrl && shift) { newDrawing(); repaint(); return; } if (!ctrl && shift) { if (idx >= 0) { line.deleteNode(idx); nearestPointIndex = -1; } else line.tryToDeleteSegment(e.getPoint()); return; } if (idx >= 0) { if (ctrl) { // toggle fixed point line.toggleFixed(idx); } // node dragging dragNode = idx; return; } startDrawing(e.getPoint(), settings.fixedClick); } private void startDrawing(Point point, boolean fixFlag) { //if (line.isClosed()) { setStatusLine(tr(SIMPLIFYMODE_MESSAGE));return; } drawing = true; if (line.wasSimplified()) { // new line started after simplification // we need to save old line saveAsWay(false); newDrawing(); //line.clearSimplifiedVersion(); } LatLon p = mv.getLatLon(point.x, point.y); if (settings.snapNodes) { // find existing node near point and use it Node nd1 = getNearestNode(point, settings.maxDist); if (nd1 != null) { // found node, make it fixed point of the line //System.out.println("node "+nd1); p = nd1.getCoor(); line.fixPoint(p); } } line.addLast(p); if (ctrl || fixFlag) line.fixPoint(p); setStatusLine(tr("Please move the mouse to draw new way")); repaint(); } @Override public void mouseReleased(MouseEvent e) { if (e.getButton() != MouseEvent.BUTTON1) return; stopDrawing(); } private void stopDrawing() { if (!isEnabled()) return; dragNode = -1; drawing = false; highlightedFragmentStart = null; if (!line.isClosed()) setStatusLine(DRAWINGMODE_MESSAGE); updateCursor(); repaint(); } @Override public void mouseDragged(MouseEvent e) { mouseMoved(e); } @Override public void mouseMoved(MouseEvent e) { if (!isEnabled()) return; updateKeyModifiers(e); deltaChanged = false; Node nd1 = getNearestNode(e.getPoint(), settings.maxDist); boolean nearSomeNode2 = nd1 != null; boolean needRepaint = false; if (nearSomeNode != nearSomeNode2) { nearSomeNode = nearSomeNode2; updateCursor(); needRepaint = true; } int nearestIdx2 = line.findClosestPoint(e.getPoint(), settings.maxDist); if (nearestPointIndex != nearestIdx2) { nearestPointIndex = nearestIdx2; updateCursor(); needRepaint = true; } if (settings.drawLastSegment) { needRepaint = true; } if (!drawing) { if (dragNode >= 0) { line.moveNode(dragNode, getLatLon(e)); repaint(); return; } if (shift && nearestPointIndex == -1) { // find line fragment to highlight LatLon h2 = line.findBigSegment(e.getPoint()); if (highlightedFragmentStart != h2) { highlightedFragmentStart = h2; needRepaint = true; } } if (needRepaint) { repaint(); } return; } if (line.isClosed()) setStatusLine(SIMPLIFYMODE_MESSAGE); // do not draw points close to existing points - we do not want self-intersections if (nearestPointIndex >= 0) { return; } Point lastP = line.getLastPoint(); // last point of line fragment being edited // free mouse-drawing if (nearSomeNode) { if (settings.snapNodes && Math.hypot(e.getX() - lastP.x, e.getY() - lastP.y) > 1e-2) { line.addFixed(nd1.getCoor()); // snap to node coords repaint(); return; } } else { if (Math.hypot(e.getX() - lastP.x, e.getY() - lastP.y) > settings.minPixelsBetweenPoints) { line.addLast(getLatLon(e)); // add new point repaint(); return; } } autoCloseIfNeeded(); } @Override public void doKeyPressed(KeyEvent e) { if (getShortcut().isEvent(e)) { // repeated press tryToLoadWay(); return; } switch(e.getKeyCode()) { case KeyEvent.VK_BACK_SPACE: if (line.wasSimplified()) { // return to line editing line.clearSimplifiedVersion(); repaint(); eps = settings.startingEps; } back(); break; case KeyEvent.VK_ENTER: e.consume(); // first Enter = simplify, second = save the way if (!line.wasSimplified()) { //line.simplify(eps); switch(settings.simplifyMode) { case 0: //case 1: eps = line.autoSimplify(settings.startingEps, settings.epsilonMult, settings.pkmBlockSize, settings.maxPointsPerKm); break; case 1: //case 2: case 3: line.simplify(eps); break; } if (settings.simplifyMode == 2) { // autosave saveAsWay(true); } else { repaint(); showSimplifyHint(); } } else { saveAsWay(true); } break; case KeyEvent.VK_DOWN: if (ctrl || shift || alt) return; // more details e.consume(); if (line.wasSimplified()) changeEpsilon(settings.epsilonMult); else changeDelta(1/1.1); break; case KeyEvent.VK_UP: if (ctrl || shift || alt) return; // less details e.consume(); if (line.wasSimplified()) changeEpsilon(1/settings.epsilonMult); else changeDelta(1.1); break; case KeyEvent.VK_ESCAPE: e.consume(); Point lastPoint = line.getLastPoint(); if (!line.isClosed()) line.moveToTheEnd(); if (lastPoint == null || lastPoint.equals(line.getLastPoint())) { if (line.getPoints().size() > 5) { boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog( "delete_drawn_line", Main.parent, tr("Are you sure you do not want to save the line containing {0} points?", line.getPoints().size()), tr("Delete confirmation"), JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_OPTION); if (!answer) break; } newDrawing(); // stop drawing Main.map.selectSelectTool(false); } break; case KeyEvent.VK_I: JOptionPane.showMessageDialog(Main.parent, tr("{0} m - length of the line\n{1} nodes\n{2} points per km (maximum)\n{3} points per km (average)", line.getLength(), line.getPoints().size(), line.getNodesPerKm(settings.pkmBlockSize), line.getNodesPerKm(1000000)), tr("Line information"), JOptionPane.INFORMATION_MESSAGE); break; case KeyEvent.VK_Q: // less details e.consume(); new FastDrawConfigDialog(settings).showDialog(); if (line.wasSimplified()) { eps = line.autoSimplify(settings.startingEps, settings.epsilonMult, settings.pkmBlockSize, settings.maxPointsPerKm); showSimplifyHint(); } repaint(); break; case KeyEvent.VK_SPACE: e.consume(); if (!drawing) { Point p = Main.map.mapView.getMousePosition(); if (p != null) startDrawing(p, settings.fixedSpacebar); } break; } } @Override public void doKeyReleased(KeyEvent keyEvent) { //System.out.println("released "+keyEvent); if (keyEvent.getKeyCode() == KeyEvent.VK_SPACE) stopDrawing(); updateCursor(); } @Override public void modifiersChanged(int modifiers) { updateKeyModifiers(modifiers); updateCursor(); } @Override protected void updateStatusLine() { Main.map.statusLine.setHelpText(statusText); Main.map.statusLine.repaint(); } // </editor-fold> // <editor-fold defaultstate="collapsed" desc="Different action helper methods"> public void newDrawing() { oldWay = null; oldNodes = null; eps = settings.startingEps; line.clear(); } private void saveAsWay(boolean autoExit) { List<LatLon> pts = line.getPoints(); int n = pts.size(); if (n < 2) return; //do not save oversimplified lines if (line.isClosed() && n == 2) return; if (line.isClosed() && n == 3) pts.remove(2); // two-point way can not be closed Collection<Command> cmds = new LinkedList<>(); int i = 0; Way w; if (oldWay == null) { w = new Way(); } else { w = new Way(oldWay); w.setNodes(new ArrayList<Node>()); // nodes will be created frosm scratch } LatLon first = pts.get(0); Node firstNode = null; for (LatLon p : pts) { Node nd = Main.map.mapView.getNearestNode(line.getPoint(p), OsmPrimitive::isSelectable); // there may be a node with the same coords! if (nd != null && p.greatCircleDistance(nd.getCoor()) > 0.01) nd = null; if (nd == null) { if (i > 0 && p.equals(first)) { nd = firstNode; } else { nd = new Node(p); cmds.add(new AddCommand(nd)); } } if (nd.getCoor().isOutSideWorld()) { JOptionPane.showMessageDialog(Main.parent, tr("Cannot place node outside of the world.")); return; } if (i == 0) { firstNode = nd; } w.addNode(nd); i++; } if (ctrl) { // paste tags - from ctrl-shift-v new OsmTransferHandler().pasteTags(Collections.singleton(w)); } if (!settings.autoTags.isEmpty()) { Map<String, String> tags = TextTagParser.readTagsFromText(settings.autoTags); for (String k: tags.keySet()) { w.put(k, tags.get(k)); } } if (oldWay != null) { List<Node> nodes = w.getNodes(); cmds.add(new ChangeCommand(oldWay, w)); for (Node nd: oldNodes) { // node from old way but not in new way if (!nodes.contains(nd)) { List<OsmPrimitive> refs = nd.getReferrers(); // does someone need this node? if no-delete it. if (refs.size() == 1 && !nd.isDeleted() && nd.isUsable() && !nd.isTagged()) cmds.add(new DeleteCommand(nd)); } } oldWay = null; // that is all with this command } else cmds.add(new AddCommand(w)); Command c = new SequenceCommand(tr("Draw the way by mouse"), cmds); if (getLayerManager().getEditLayer() == null) return; Main.main.undoRedo.add(c); lineWasSaved = true; newDrawing(); // stop drawing if (autoExit) { // Select this way and switch drawing mode off getLayerManager().getEditDataSet().setSelected(w); Main.map.selectSelectTool(false); } } public void back() { line.undo(); repaint(); } void changeEpsilon(double k) { //System.out.println(tr("Eps={0}", eps)); eps *= k; line.simplify(eps); /* I18N: Eps = Epsilon, the tolerance parameter */ showSimplifyHint(); repaint(); } void changeDelta(double k) { settings.minPixelsBetweenPoints *= k; deltaChanged = true; setStatusLine(tr("min distance={0} px ({1} m)", (int) settings.minPixelsBetweenPoints, mv.getDist100Pixel()/100*settings.minPixelsBetweenPoints)); repaint(); } /*private Node findClosestNode(LatLon p, double d) { Node nn=null; double dist,minD=1e10,x,y; Point pscreen=getPoint(p); x=pscreen.x; y=pscreen.y; BBox b=new BBox(new LatLon(p.lat()-deltaLatLon,p.lon()-deltaLatLon), new LatLon(p.lat()+deltaLatLon,p.lon()+deltaLatLon)); List<Node> nodes = getCurrentDataSet().searchNodes(b); for (Node n: nodes) { dist = Math.sqrt(getPoint(n.getCoor()).distanceSq(x,y)); if (dist<d && dist<minD) { nn=n; minD=dist; }; } return nn; }*/ private void loadFromWay(Way w) { Object[] nodes = w.getNodes().toArray(); int n = nodes.length; if (w.isClosed()) n--; for (int i = 0; i < n; i++) { Node nd = (Node) nodes[i]; List<OsmPrimitive> refs = nd.getReferrers(); if (refs.size() > 1 || nd.isTagged()) { line.addFixed(nd.getCoor()); } else { line.addLast(nd.getCoor()); } } if (w.isClosed()) line.closeLine(); oldNodes = w.getNodes(); oldWay = w; } private void setStatusLine(String tr) { statusText = tr; updateStatusLine(); } private void showSimplifyHint() { setStatusLine(tr("Eps={0}, {1} points, {2} p/km", eps, line.getSimplePointsCount(), line.getNodesPerKm(settings.pkmBlockSize))+" " +SIMPLIFYMODE_MESSAGE); } private void updateCursor() { if (shift) Main.map.mapView.setCursor(cursorShift); else if (line.isClosed() || (nearestPointIndex == 0)) Main.map.mapView.setCursor(cursorReady); else if (ctrl) Main.map.mapView.setCursor(cursorCtrl); else if (nearSomeNode && settings.snapNodes) Main.map.mapView.setCursor(cursorCtrl); else if (drawing) Main.map.mapView.setCursor(cursorDrawing); else Main.map.mapView.setCursor(cursorDraw); } private void repaint() { Main.map.mapView.repaint(); } private void tryToLoadWay() { updateCursor(); Collection<Way> selectedWays = Main.getLayerManager().getEditDataSet().getSelectedWays(); if (selectedWays != null // if there is a selection && selectedWays.size() == 1 // and one way is selected && line.getPoints().size() == 0) /* and ther is no already drawn line */ { // we can start drawing new way starting from old one Way w = selectedWays.iterator().next(); if (w.isNew() || settings.allowEditExistingWays) loadFromWay(w); } } private void autoCloseIfNeeded() { if (settings.drawClosed && line.getPointCount() > 1 && !line.isClosed()) { line.closeLine(); } } // </editor-fold> // <editor-fold defaultstate="collapsed" desc="Helper functions"> private Node getNearestNode(Point point, double maxDist) { Node nd = Main.map.mapView.getNearestNode(point, OsmPrimitive::isSelectable); if (nd != null && line.getPoint(nd.getCoor()).distance(point) <= maxDist) return nd; else return null; } LatLon getLatLon(MouseEvent e) { return mv.getLatLon(e.getX(), e.getY()); } // </editor-fold> }