diff --git a/.classpath b/.classpath index 11208d5..b5edc13 100644 --- a/.classpath +++ b/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/.project b/.project index 4800fe3..5502783 100644 --- a/.project +++ b/.project @@ -1,23 +1,23 @@ - - - JOSM-improve-way - - - - - - org.eclipse.jdt.core.javabuilder - - - - - net.sf.eclipsecs.core.CheckstyleBuilder - - - - - - org.eclipse.jdt.core.javanature - net.sf.eclipsecs.core.CheckstyleNature - - + + + JOSM-improve-way + + + + + + org.eclipse.jdt.core.javabuilder + + + + + net.sf.eclipsecs.core.CheckstyleBuilder + + + + + + org.eclipse.jdt.core.javanature + net.sf.eclipsecs.core.CheckstyleNature + + diff --git a/build.xml b/build.xml index fd9e908..ad38f78 100644 --- a/build.xml +++ b/build.xml @@ -1,21 +1,21 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/src/org/openstreetmap/josm/core/patch/ImproveWayAccuracyAction.java b/src/org/openstreetmap/josm/core/patch/ImproveWayAccuracyAction.java new file mode 100644 index 0000000..dfe66b3 --- /dev/null +++ b/src/org/openstreetmap/josm/core/patch/ImproveWayAccuracyAction.java @@ -0,0 +1,747 @@ +/** + * This file is copy of same named class + * from package org.openstreetmap.josm.actions.mapmode + * -- + * Serves as base class for ImproveWay plugin + * until this file is merged in core. + */ + +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.core.patch; + +import static org.openstreetmap.josm.tools.I18n.marktr; +import static org.openstreetmap.josm.tools.I18n.tr; +import static org.openstreetmap.josm.tools.I18n.trn; + +import java.awt.BasicStroke; +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.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import javax.swing.JOptionPane; + +import org.openstreetmap.josm.actions.mapmode.MapMode; +import org.openstreetmap.josm.command.AddCommand; +import org.openstreetmap.josm.command.ChangeNodesCommand; +import org.openstreetmap.josm.command.Command; +import org.openstreetmap.josm.command.DeleteCommand; +import org.openstreetmap.josm.command.MoveCommand; +import org.openstreetmap.josm.command.SequenceCommand; +import org.openstreetmap.josm.data.Bounds; +import org.openstreetmap.josm.data.UndoRedoHandler; +import org.openstreetmap.josm.data.coor.EastNorth; +import org.openstreetmap.josm.data.osm.DataSelectionListener; +import org.openstreetmap.josm.data.osm.DataSet; +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.event.AbstractDatasetChangedEvent; +import org.openstreetmap.josm.data.osm.event.DataChangedEvent; +import org.openstreetmap.josm.data.osm.event.DataSetListener; +import org.openstreetmap.josm.data.osm.event.DatasetEventManager; +import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode; +import org.openstreetmap.josm.data.osm.event.NodeMovedEvent; +import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent; +import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent; +import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent; +import org.openstreetmap.josm.data.osm.event.SelectionEventManager; +import org.openstreetmap.josm.data.osm.event.TagsChangedEvent; +import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent; +import org.openstreetmap.josm.data.preferences.CachingProperty; +import org.openstreetmap.josm.data.preferences.IntegerProperty; +import org.openstreetmap.josm.data.preferences.NamedColorProperty; +import org.openstreetmap.josm.data.preferences.StrokeProperty; +import org.openstreetmap.josm.gui.MainApplication; +import org.openstreetmap.josm.gui.MapFrame; +import org.openstreetmap.josm.gui.MapView; +import org.openstreetmap.josm.gui.draw.MapViewPath; +import org.openstreetmap.josm.gui.draw.SymbolShape; +import org.openstreetmap.josm.gui.layer.AbstractMapViewPaintable; +import org.openstreetmap.josm.gui.layer.Layer; +import org.openstreetmap.josm.gui.util.ModifierExListener; +import org.openstreetmap.josm.tools.ImageProvider; +import org.openstreetmap.josm.tools.Logging; +import org.openstreetmap.josm.tools.Pair; +import org.openstreetmap.josm.tools.Shortcut; + +/** + * A special map mode that is optimized for improving way geometry. + * (by efficiently moving, adding and deleting way-nodes) + * + * @author Alexander Kachkaev <alexander@kachkaev.ru>, 2011 + */ +public class ImproveWayAccuracyAction extends MapMode implements DataSelectionListener, DataSetListener, ModifierExListener { + + protected static final String CROSSHAIR = /* ICON(cursor/)*/ "crosshair"; + + protected enum State { + SELECTING, IMPROVING + } + + protected State state; + + protected MapView mv; + + protected static final long serialVersionUID = 42L; + + protected transient Way targetWay; + protected transient Node candidateNode; + protected transient WaySegment candidateSegment; + + protected Point mousePos; + protected boolean dragging; + + protected Node endpoint1 = null; + protected Node endpoint2 = null; + + protected final Cursor cursorSelect = ImageProvider.getCursor(/* ICON(cursor/)*/ "normal", /* ICON(cursor/modifier/)*/ "mode"); + protected final Cursor cursorSelectHover = ImageProvider.getCursor(/* ICON(cursor/)*/ "hand", /* ICON(cursor/modifier/)*/ "mode"); + protected final Cursor cursorImprove = ImageProvider.getCursor(CROSSHAIR, null); + protected final Cursor cursorImproveAdd = ImageProvider.getCursor(CROSSHAIR, /* ICON(cursor/modifier/)*/ "addnode"); + protected final Cursor cursorImproveDelete = ImageProvider.getCursor(CROSSHAIR, /* ICON(cursor/modifier/)*/ "delete_node"); + protected final Cursor cursorImproveAddLock = ImageProvider.getCursor(CROSSHAIR, /* ICON(cursor/modifier/)*/ "add_node_lock"); + protected final Cursor cursorImproveLock = ImageProvider.getCursor(CROSSHAIR, /* ICON(cursor/modifier/)*/ "lock"); + + protected Color guideColor; + + protected static final CachingProperty SELECT_TARGET_WAY_STROKE + = new StrokeProperty("improvewayaccuracy.stroke.select-target", "2").cached(); + protected static final CachingProperty MOVE_NODE_STROKE + = new StrokeProperty("improvewayaccuracy.stroke.move-node", "1 6").cached(); + protected static final CachingProperty MOVE_NODE_INTERSECTING_STROKE + = new StrokeProperty("improvewayaccuracy.stroke.move-node-intersecting", "1 2 6").cached(); + protected static final CachingProperty ADD_NODE_STROKE + = new StrokeProperty("improvewayaccuracy.stroke.add-node", "1").cached(); + protected static final CachingProperty DELETE_NODE_STROKE + = new StrokeProperty("improvewayaccuracy.stroke.delete-node", "1").cached(); + protected static final CachingProperty DOT_SIZE + = new IntegerProperty("improvewayaccuracy.dot-size", 6).cached(); + + protected boolean selectionChangedBlocked; + + protected String oldModeHelpText; + + protected final transient AbstractMapViewPaintable temporaryLayer = new AbstractMapViewPaintable() { + @Override + public void paint(Graphics2D g, MapView mv, Bounds bbox) { + ImproveWayAccuracyAction.this.paint(g, mv, bbox); + } + }; + + /** + * Constructs a new {@code ImproveWayAccuracyAction}. + * @since 11713 + */ + public ImproveWayAccuracyAction() { + super(tr("Improve Way Accuracy"), "improvewayaccuracy", + tr("Improve Way Accuracy mode"), + Shortcut.registerShortcut("mapmode:ImproveWayAccuracy", + tr("Mode: {0}", tr("Improve Way Accuracy")), + KeyEvent.VK_W, Shortcut.DIRECT), Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + + readPreferences(); + } + + protected ImproveWayAccuracyAction(String name, String iconName, String tooltip, Shortcut shortcut, Cursor cursor) { + super(name, iconName, tooltip, shortcut, cursor); + } + + // ------------------------------------------------------------------------- + // Mode methods + // ------------------------------------------------------------------------- + @Override + public void enterMode() { + if (!isEnabled()) { + return; + } + super.enterMode(); + readPreferences(); + + MapFrame map = MainApplication.getMap(); + mv = map.mapView; + mousePos = null; + oldModeHelpText = ""; + + if (getLayerManager().getEditDataSet() == null) { + return; + } + + updateStateByCurrentSelection(); + + map.mapView.addMouseListener(this); + map.mapView.addMouseMotionListener(this); + map.mapView.addTemporaryLayer(temporaryLayer); + SelectionEventManager.getInstance().addSelectionListener(this); + DatasetEventManager.getInstance().addDatasetListener(this, FireMode.IMMEDIATELY); + + map.keyDetector.addModifierExListener(this); + } + + @Override + protected void readPreferences() { + guideColor = new NamedColorProperty(marktr("improve way accuracy helper line"), Color.RED).get(); + } + + @Override + public void exitMode() { + super.exitMode(); + + MapFrame map = MainApplication.getMap(); + map.mapView.removeMouseListener(this); + map.mapView.removeMouseMotionListener(this); + map.mapView.removeTemporaryLayer(temporaryLayer); + SelectionEventManager.getInstance().removeSelectionListener(this); + DatasetEventManager.getInstance().removeDatasetListener(this); + + map.keyDetector.removeModifierExListener(this); + temporaryLayer.invalidate(); + targetWay = null; + candidateNode = null; + candidateSegment = null; + } + + @Override + protected void updateStatusLine() { + String newModeHelpText = getModeHelpText(); + if (!newModeHelpText.equals(oldModeHelpText)) { + oldModeHelpText = newModeHelpText; + MapFrame map = MainApplication.getMap(); + map.statusLine.setHelpText(newModeHelpText); + map.statusLine.repaint(); + } + } + + @Override + public String getModeHelpText() { + if (state == State.SELECTING) { + if (targetWay != null) { + return tr("Click on the way to start improving its shape."); + } else { + return tr("Select a way that you want to make more accurate."); + } + } else { + if (ctrl) { + return tr("Click to add a new node. Release Ctrl to move existing nodes or hold Alt to delete."); + } else if (alt) { + return tr("Click to delete the highlighted node. Release Alt to move existing nodes or hold Ctrl to add new nodes."); + } else { + return tr("Click to move the highlighted node. Hold Ctrl to add new nodes, or Alt to delete."); + } + } + } + + @Override + public boolean layerIsSupported(Layer l) { + return isEditableDataLayer(l); + } + + @Override + protected void updateEnabledState() { + setEnabled(getLayerManager().getEditLayer() != null); + } + + // ------------------------------------------------------------------------- + // MapViewPaintable methods + // ------------------------------------------------------------------------- + /** + * Redraws temporary layer. Highlights targetWay in select mode. Draws + * preview lines in improve mode and highlights the candidateNode + * @param g The graphics + * @param mv The map view + * @param bbox The bounding box + */ + public void paint(Graphics2D g, MapView mv, Bounds bbox) { + if (mousePos == null || (candidateNode != null && candidateNode.getDataSet() == null)) { + return; + } + + g.setColor(guideColor); + + if (state == State.SELECTING && targetWay != null) { + // Highlighting the targetWay in Selecting state + // Non-native highlighting is used, because sometimes highlighted + // segments are covered with others, which is bad. + BasicStroke stroke = SELECT_TARGET_WAY_STROKE.get(); + g.setStroke(stroke); + + List nodes = targetWay.getNodes(); + + g.draw(new MapViewPath(mv).append(nodes, false).computeClippedLine(stroke)); + + } else if (state == State.IMPROVING) { + // Drawing preview lines and highlighting the node + // that is going to be moved. + // Non-native highlighting is used here as well. + MapViewPath b = new MapViewPath(mv); + findEndpoints(g); + drawPreviewLines(g, b); + highlightCandidateNode(g, mv, b); + } + } + + protected void findEndpoints(Graphics2D g) { + endpoint1 = null; + endpoint2 = null; + if (ctrl && candidateSegment != null) { + g.setStroke(ADD_NODE_STROKE.get()); + try { + endpoint1 = candidateSegment.getFirstNode(); + endpoint2 = candidateSegment.getSecondNode(); + } catch (ArrayIndexOutOfBoundsException e) { + Logging.error(e); + } + } else if (!alt && !ctrl && candidateNode != null) { + g.setStroke(MOVE_NODE_STROKE.get()); + List> wpps = targetWay.getNodePairs(false); + for (Pair wpp : wpps) { + if (wpp.a == candidateNode) { + endpoint1 = wpp.b; + } + if (wpp.b == candidateNode) { + endpoint2 = wpp.a; + } + if (endpoint1 != null && endpoint2 != null) { + break; + } + } + } else if (alt && !ctrl && candidateNode != null) { + g.setStroke(DELETE_NODE_STROKE.get()); + List nodes = targetWay.getNodes(); + int index = nodes.indexOf(candidateNode); + + // Only draw line if node is not first and/or last + if (index > 0 && index < (nodes.size() - 1)) { + endpoint1 = nodes.get(index - 1); + endpoint2 = nodes.get(index + 1); + } else if (targetWay.isClosed()) { + endpoint1 = targetWay.getNode(1); + endpoint2 = targetWay.getNode(nodes.size() - 2); + } + // TODO: indicate what part that will be deleted? (for end nodes) + } + } + + protected void drawPreviewLines(Graphics2D g, MapViewPath b) { + if (alt && !ctrl) { + // In delete mode + if (endpoint1 != null && endpoint2 != null) { + b.moveTo(endpoint1); + b.lineTo(endpoint2); + } + } else { + // In add or move mode + if (endpoint1 != null) { + b.moveTo(mousePos.x, mousePos.y); + b.lineTo(endpoint1); + } + if (endpoint2 != null) { + b.moveTo(mousePos.x, mousePos.y); + b.lineTo(endpoint2); + } + } + g.draw(b.computeClippedLine(g.getStroke())); + } + + protected void highlightCandidateNode(Graphics2D g, MapView mv, MapViewPath b) { + if (candidateNode != null) { + g.fill(new MapViewPath(mv).shapeAround(candidateNode, SymbolShape.SQUARE, DOT_SIZE.get())); + } + + if (!alt && !ctrl && candidateNode != null) { + b.reset(); + drawIntersectingWayHelperLines(b); + g.setStroke(MOVE_NODE_INTERSECTING_STROKE.get()); + g.draw(b.computeClippedLine(g.getStroke())); + } + } + + protected void drawIntersectingWayHelperLines(MapViewPath b) { + for (final OsmPrimitive referrer : candidateNode.getReferrers()) { + if (!(referrer instanceof Way) || targetWay.equals(referrer)) { + continue; + } + final List nodes = ((Way) referrer).getNodes(); + for (int i = 0; i < nodes.size(); i++) { + if (!candidateNode.equals(nodes.get(i))) { + continue; + } + if (i > 0) { + b.moveTo(mousePos.x, mousePos.y); + b.lineTo(nodes.get(i - 1)); + } + if (i < nodes.size() - 1) { + b.moveTo(mousePos.x, mousePos.y); + b.lineTo(nodes.get(i + 1)); + } + } + } + } + + // ------------------------------------------------------------------------- + // Event handlers + // ------------------------------------------------------------------------- + @Override + public void modifiersExChanged(int modifiers) { + if (!MainApplication.isDisplayingMapView() || !MainApplication.getMap().mapView.isActiveLayerDrawable()) { + return; + } + updateKeyModifiersEx(modifiers); + updateCursorDependentObjectsIfNeeded(); + updateCursor(); + updateStatusLine(); + temporaryLayer.invalidate(); + } + + @Override + public void selectionChanged(SelectionChangeEvent event) { + if (selectionChangedBlocked) { + return; + } + updateStateByCurrentSelection(); + } + + @Override + public void mouseDragged(MouseEvent e) { + dragging = true; + mouseMoved(e); + } + + @Override + public void mouseMoved(MouseEvent e) { + if (!isEnabled()) { + return; + } + + updateMousePosition(e); + updateKeyModifiers(e); + updateCursorDependentObjectsIfNeeded(); + updateCursor(); + updateStatusLine(); + temporaryLayer.invalidate(); + } + + @Override + public void mouseReleased(MouseEvent e) { + dragging = false; + if (!isEnabled() || e.getButton() != MouseEvent.BUTTON1) { + return; + } + + DataSet ds = getLayerManager().getEditDataSet(); + updateKeyModifiers(e); + updateMousePosition(e); + + if (state == State.SELECTING) { + if (targetWay != null) { + ds.setSelected(targetWay.getPrimitiveId()); + updateStateByCurrentSelection(); + } + } else if (state == State.IMPROVING) { + // Checking if the new coordinate is outside of the world + if (new Node(mv.getEastNorth(mousePos.x, mousePos.y)).isOutSideWorld()) { + JOptionPane.showMessageDialog(MainApplication.getMainFrame(), + tr("Cannot add a node outside of the world."), + tr("Warning"), JOptionPane.WARNING_MESSAGE); + return; + } + + if (ctrl && !alt && candidateSegment != null) { + // Add a new node to the highlighted segment. + Collection virtualSegments = new LinkedList<>(); + + // Check if other ways have the same segment. + // We have to make sure that we add the new node to all of them. + Set commonParentWays = new HashSet<>(candidateSegment.getFirstNode().getParentWays()); + commonParentWays.retainAll(candidateSegment.getSecondNode().getParentWays()); + for (Way w : commonParentWays) { + for (int i = 0; i < w.getNodesCount() - 1; i++) { + WaySegment testWS = new WaySegment(w, i); + if (testWS.isSimilar(candidateSegment)) { + virtualSegments.add(testWS); + } + } + } + + Collection virtualCmds = new LinkedList<>(); + // Create the new node + Node virtualNode = new Node(mv.getEastNorth(mousePos.x, mousePos.y)); + virtualCmds.add(new AddCommand(ds, virtualNode)); + + // Adding the node to all segments found + for (WaySegment virtualSegment : virtualSegments) { + Way w = virtualSegment.getWay(); + List modNodes = w.getNodes(); + modNodes.add(virtualSegment.getUpperIndex(), virtualNode); + virtualCmds.add(new ChangeNodesCommand(w, modNodes)); + } + + // Finishing the sequence command + String text = trn("Add a new node to way", + "Add a new node to {0} ways", + virtualSegments.size(), virtualSegments.size()); + + UndoRedoHandler.getInstance().add(new SequenceCommand(text, virtualCmds)); + + } else if (alt && !ctrl && candidateNode != null) { + // Deleting the highlighted node + + //check to see if node is in use by more than one object + long referrersCount = candidateNode.referrers(OsmPrimitive.class).count(); + long referrerWayCount = candidateNode.referrers(Way.class).count(); + if (referrersCount != 1 || referrerWayCount != 1) { + // detach node from way + final List nodes = targetWay.getNodes(); + nodes.remove(candidateNode); + if (nodes.size() < 2) { + final Command deleteCmd = DeleteCommand.delete(Collections.singleton(targetWay), true); + if (deleteCmd != null) { + UndoRedoHandler.getInstance().add(deleteCmd); + } + } else { + UndoRedoHandler.getInstance().add(new ChangeNodesCommand(targetWay, nodes)); + } + } else if (candidateNode.isTagged()) { + JOptionPane.showMessageDialog(MainApplication.getMainFrame(), + tr("Cannot delete node that has tags"), + tr("Error"), JOptionPane.ERROR_MESSAGE); + } else { + final Command deleteCmd = DeleteCommand.delete(Collections.singleton(candidateNode), true); + if (deleteCmd != null) { + UndoRedoHandler.getInstance().add(deleteCmd); + } + } + + } else if (candidateNode != null) { + // Moving the highlighted node + EastNorth nodeEN = candidateNode.getEastNorth(); + EastNorth cursorEN = mv.getEastNorth(mousePos.x, mousePos.y); + + UndoRedoHandler.getInstance().add( + new MoveCommand(candidateNode, cursorEN.east() - nodeEN.east(), cursorEN.north() - nodeEN.north())); + + // TODO the following line is commented out in this copy because checkCommandForLargeDistance method is package private + // SelectAction.checkCommandForLargeDistance(UndoRedoHandler.getInstance().getLastCommand()); + } + } + + mousePos = null; + updateCursor(); + updateStatusLine(); + temporaryLayer.invalidate(); + } + + @Override + public void mouseExited(MouseEvent e) { + if (!isEnabled()) { + return; + } + + if (!dragging) { + mousePos = null; + } + temporaryLayer.invalidate(); + } + + // ------------------------------------------------------------------------- + // Custom methods + // ------------------------------------------------------------------------- + + /** + * Sets mouse position based on mouse event; + * this method allows extending classes to override position + */ + protected void updateMousePosition(MouseEvent e) { + mousePos = e.getPoint(); + } + + /** + * Sets new cursor depending on state, mouse position + */ + protected void updateCursor() { + if (!isEnabled()) { + mv.setNewCursor(null, this); + return; + } + + if (state == State.SELECTING) { + mv.setNewCursor(targetWay == null ? cursorSelect + : cursorSelectHover, this); + } else if (state == State.IMPROVING) { + if (alt && !ctrl) { + mv.setNewCursor(cursorImproveDelete, this); + } else if (shift || dragging) { + if (ctrl) { + mv.setNewCursor(cursorImproveAddLock, this); + } else { + mv.setNewCursor(cursorImproveLock, this); + } + } else if (ctrl && !alt) { + mv.setNewCursor(cursorImproveAdd, this); + } else { + mv.setNewCursor(cursorImprove, this); + } + } + } + + /** + * Updates these objects under cursor: targetWay, candidateNode, + * candidateSegment + */ + public void updateCursorDependentObjectsIfNeeded() { + if (state == State.IMPROVING && (shift || dragging) + && !(candidateNode == null && candidateSegment == null)) { + return; + } + + if (mousePos == null) { + candidateNode = null; + candidateSegment = null; + return; + } + + if (state == State.SELECTING) { + targetWay = ImproveWayAccuracyHelper.findWay(mv, mousePos); + } else if (state == State.IMPROVING) { + if (ctrl && !alt) { + candidateSegment = ImproveWayAccuracyHelper.findCandidateSegment(mv, + targetWay, mousePos); + candidateNode = null; + } else { + candidateNode = ImproveWayAccuracyHelper.findCandidateNode(mv, + targetWay, mousePos); + candidateSegment = null; + } + } + } + + /** + * Switches to Selecting state + */ + public void startSelecting() { + state = State.SELECTING; + + targetWay = null; + + temporaryLayer.invalidate(); + updateStatusLine(); + } + + /** + * Switches to Improving state + * + * @param targetWay Way that is going to be improved + */ + public void startImproving(Way targetWay) { + state = State.IMPROVING; + + DataSet ds = getLayerManager().getEditDataSet(); + Collection currentSelection = ds.getSelected(); + if (currentSelection.size() != 1 + || !currentSelection.iterator().next().equals(targetWay)) { + selectionChangedBlocked = true; + ds.clearSelection(); + ds.setSelected(targetWay.getPrimitiveId()); + selectionChangedBlocked = false; + } + + this.targetWay = targetWay; + this.candidateNode = null; + this.candidateSegment = null; + + temporaryLayer.invalidate(); + updateStatusLine(); + } + + /** + * Updates the state according to the current selection. Goes to Improve + * state if a single way or node is selected. Extracts a way by a node in + * the second case. + */ + protected void updateStateByCurrentSelection() { + final List nodeList = new ArrayList<>(); + final List wayList = new ArrayList<>(); + final DataSet ds = getLayerManager().getEditDataSet(); + if (ds != null) { + final Collection sel = ds.getSelected(); + + // Collecting nodes and ways from the selection + for (OsmPrimitive p : sel) { + if (p instanceof Way) { + wayList.add((Way) p); + } + if (p instanceof Node) { + nodeList.add((Node) p); + } + } + + if (wayList.size() == 1) { + // Starting improving the single selected way + startImproving(wayList.get(0)); + return; + } else if (nodeList.size() == 1) { + // Starting improving the only way of the single selected node + List r = nodeList.get(0).getReferrers(); + if (r.size() == 1 && (r.get(0) instanceof Way)) { + startImproving((Way) r.get(0)); + return; + } + } + } + + // Starting selecting by default + startSelecting(); + } + + @Override + public void primitivesRemoved(PrimitivesRemovedEvent event) { + if (event.getPrimitives().contains(candidateNode) || event.getPrimitives().contains(targetWay)) { + updateCursorDependentObjectsIfNeeded(); + } + } + + @Override + public void primitivesAdded(PrimitivesAddedEvent event) { + // Do nothing + } + + @Override + public void tagsChanged(TagsChangedEvent event) { + // Do nothing + } + + @Override + public void nodeMoved(NodeMovedEvent event) { + // Do nothing + } + + @Override + public void wayNodesChanged(WayNodesChangedEvent event) { + // Do nothing + } + + @Override + public void relationMembersChanged(RelationMembersChangedEvent event) { + // Do nothing + } + + @Override + public void otherDatasetChange(AbstractDatasetChangedEvent event) { + // Do nothing + } + + @Override + public void dataChanged(DataChangedEvent event) { + // Do nothing + } +} diff --git a/src/org/openstreetmap/josm/plugins/improveway/ImproveWayAccuracyHelper.java b/src/org/openstreetmap/josm/core/patch/ImproveWayAccuracyHelper.java similarity index 79% rename from src/org/openstreetmap/josm/plugins/improveway/ImproveWayAccuracyHelper.java rename to src/org/openstreetmap/josm/core/patch/ImproveWayAccuracyHelper.java index 8e86a06..6db3c11 100644 --- a/src/org/openstreetmap/josm/plugins/improveway/ImproveWayAccuracyHelper.java +++ b/src/org/openstreetmap/josm/core/patch/ImproveWayAccuracyHelper.java @@ -1,177 +1,178 @@ -// License: GPL. For details, see LICENSE file. -package org.openstreetmap.josm.plugins.improveway; - -import java.awt.Point; -import java.util.Collection; -import java.util.List; - -import org.openstreetmap.josm.data.coor.EastNorth; -import org.openstreetmap.josm.data.osm.IWaySegment; -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.MainApplication; -import org.openstreetmap.josm.gui.MapView; -import org.openstreetmap.josm.tools.Geometry; -import org.openstreetmap.josm.tools.Pair; - -/** - * This static class contains functions used to find target way, node to move or - * segment to divide. - * - * @author Alexander Kachkaev <alexander@kachkaev.ru>, 2011 - */ -final class ImproveWayAccuracyHelper { - - private ImproveWayAccuracyHelper() { - // Hide default constructor for utils classes - } - - /** - * Finds the way to work on. If the mouse is on the node, extracts one of - * the ways containing it. If the mouse is on the way, simply returns it. - * - * @param mv the current map view - * @param p the cursor position - * @return {@code Way} or {@code null} in case there is nothing under the cursor. - */ - public static Way findWay(MapView mv, Point p) { - if (mv == null || p == null) { - return null; - } - - Node node = mv.getNearestNode(p, OsmPrimitive::isSelectable); - Way candidate = null; - - if (node != null) { - final Collection candidates = node.getReferrers(); - for (OsmPrimitive refferer : candidates) { - if (refferer instanceof Way) { - candidate = (Way) refferer; - break; - } - } - if (candidate != null) { - return candidate; - } - } - - return MainApplication.getMap().mapView.getNearestWay(p, OsmPrimitive::isSelectable); - } - - /** - * Returns the nearest node to cursor. All nodes that are behind segments - * are neglected. This is to avoid way self-intersection after moving the - * candidateNode to a new place. - * - * @param mv the current map view - * @param w the way to check - * @param p the cursor position - * @return nearest node to cursor - */ - public static Node findCandidateNode(MapView mv, Way w, Point p) { - if (mv == null || w == null || p == null) { - return null; - } - - EastNorth pEN = mv.getEastNorth(p.x, p.y); - - Double bestDistance = Double.MAX_VALUE; - Double currentDistance; - List> wpps = w.getNodePairs(false); - - Node result = null; - - mainLoop: - for (Node n : w.getNodes()) { - EastNorth nEN = n.getEastNorth(); - - if (nEN == null) { - // Might happen if lat/lon for that point are not known. - continue; - } - - currentDistance = pEN.distance(nEN); - - if (currentDistance < bestDistance) { - // Making sure this candidate is not behind any segment. - for (Pair wpp : wpps) { - if (!wpp.a.equals(n) - && !wpp.b.equals(n) - && Geometry.getSegmentSegmentIntersection( - wpp.a.getEastNorth(), wpp.b.getEastNorth(), - pEN, nEN) != null) { - continue mainLoop; - } - } - result = n; - bestDistance = currentDistance; - } - } - - return result; - } - - /** - * Returns the nearest way segment to cursor. The distance to segment ab is - * the length of altitude from p to ab (say, c) or the minimum distance from - * p to a or b if c is out of ab. - * - * The priority is given to segments where c is in ab. Otherwise, a segment - * with the largest angle apb is chosen. - * - * @param mv the current map view - * @param w the way to check - * @param p the cursor position - * @return nearest way segment to cursor - */ - public static IWaySegment findCandidateSegment(MapView mv, Way w, Point p) { - if (mv == null || w == null || p == null) { - return null; - } - - EastNorth pEN = mv.getEastNorth(p.x, p.y); - - Double currentDistance; - Double currentAngle; - Double bestDistance = Double.MAX_VALUE; - Double bestAngle = 0.0; - - int candidate = -1; - - List> wpps = w.getNodePairs(true); - - int i = -1; - for (Pair wpp : wpps) { - ++i; - - EastNorth a = wpp.a.getEastNorth(); - EastNorth b = wpp.b.getEastNorth(); - - // Finding intersection of the segment with its altitude from p - EastNorth altitudeIntersection = Geometry.closestPointToSegment(a, b, pEN); - currentDistance = pEN.distance(altitudeIntersection); - - if (!altitudeIntersection.equals(a) && !altitudeIntersection.equals(b)) { - // If the segment intersects with the altitude from p, - // make an angle too big to let this candidate win any others - // having the same distance. - currentAngle = Double.MAX_VALUE; - } else { - // Otherwise measure the angle - currentAngle = Math.abs(Geometry.getCornerAngle(a, pEN, b)); - } - - if (currentDistance < bestDistance - || (currentAngle > bestAngle && currentDistance < bestDistance * 1.0001 /* - * equality - */)) { - candidate = i; - bestAngle = currentAngle; - bestDistance = currentDistance; - } - - } - return candidate != -1 ? new IWaySegment<>(w, candidate) : null; - } -} +/** + * This file is copy of same named class + * from package org.openstreetmap.josm.actions.mapmode + * -- + * Serves as base class for ImproveWay plugin + * until this file is merged in core. + */ + +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.core.patch; + +import java.awt.Point; +import java.util.List; +import java.util.Optional; + +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.gui.MainApplication; +import org.openstreetmap.josm.gui.MapView; +import org.openstreetmap.josm.tools.Geometry; +import org.openstreetmap.josm.tools.Pair; + +/** + * This static class contains functions used to find target way, node to move or + * segment to divide. + * + * @author Alexander Kachkaev <alexander@kachkaev.ru>, 2011 + */ +class ImproveWayAccuracyHelper { + + protected ImproveWayAccuracyHelper() { + // Hide default constructor for utils classes + } + + /** + * Finds the way to work on. If the mouse is on the node, extracts one of + * the ways containing it. If the mouse is on the way, simply returns it. + * + * @param mv the current map view + * @param p the cursor position + * @return {@code Way} or {@code null} in case there is nothing under the cursor. + */ + public static Way findWay(MapView mv, Point p) { + if (mv == null || p == null) { + return null; + } + + Node node = mv.getNearestNode(p, OsmPrimitive::isSelectable); + + if (node != null) { + Optional candidate = node.referrers(Way.class).findFirst(); + if (candidate.isPresent()) { + return candidate.get(); + } + } + + return MainApplication.getMap().mapView.getNearestWay(p, OsmPrimitive::isSelectable); + } + + /** + * Returns the nearest node to cursor. All nodes that are “behind” segments + * are neglected. This is to avoid way self-intersection after moving the + * candidateNode to a new place. + * + * @param mv the current map view + * @param w the way to check + * @param p the cursor position + * @return nearest node to cursor + */ + public static Node findCandidateNode(MapView mv, Way w, Point p) { + if (mv == null || w == null || p == null) { + return null; + } + + EastNorth pEN = mv.getEastNorth(p.x, p.y); + + double bestDistance = Double.MAX_VALUE; + double currentDistance; + List> wpps = w.getNodePairs(false); + + Node result = null; + + mainLoop: + for (Node n : w.getNodes()) { + EastNorth nEN = n.getEastNorth(); + + if (nEN == null) { + // Might happen if lat/lon for that point are not known. + continue; + } + + currentDistance = pEN.distance(nEN); + + if (currentDistance < bestDistance) { + // Making sure this candidate is not behind any segment. + for (Pair wpp : wpps) { + if (!wpp.a.equals(n) + && !wpp.b.equals(n) + && Geometry.getSegmentSegmentIntersection( + wpp.a.getEastNorth(), wpp.b.getEastNorth(), + pEN, nEN) != null) { + continue mainLoop; + } + } + result = n; + bestDistance = currentDistance; + } + } + + return result; + } + + /** + * Returns the nearest way segment to cursor. The distance to segment ab is + * the length of altitude from p to ab (say, c) or the minimum distance from + * p to a or b if c is out of ab. + * + * The priority is given to segments where c is in ab. Otherwise, a segment + * with the largest angle apb is chosen. + * + * @param mv the current map view + * @param w the way to check + * @param p the cursor position + * @return nearest way segment to cursor + */ + public static WaySegment findCandidateSegment(MapView mv, Way w, Point p) { + if (mv == null || w == null || p == null) { + return null; + } + + EastNorth pEN = mv.getEastNorth(p.x, p.y); + + double currentDistance; + double currentAngle; + double bestDistance = Double.MAX_VALUE; + double bestAngle = 0.0; + + int candidate = -1; + + List> wpps = w.getNodePairs(true); + + int i = -1; + for (Pair wpp : wpps) { + ++i; + + EastNorth a = wpp.a.getEastNorth(); + EastNorth b = wpp.b.getEastNorth(); + + // Finding intersection of the segment with its altitude from p + EastNorth altitudeIntersection = Geometry.closestPointToSegment(a, b, pEN); + currentDistance = pEN.distance(altitudeIntersection); + + if (!altitudeIntersection.equals(a) && !altitudeIntersection.equals(b)) { + // If the segment intersects with the altitude from p, + // make an angle too big to let this candidate win any others + // having the same distance. + currentAngle = Double.MAX_VALUE; + } else { + // Otherwise measure the angle + currentAngle = Math.abs(Geometry.getCornerAngle(a, pEN, b)); + } + + if (currentDistance < bestDistance + || (currentAngle > bestAngle && currentDistance < bestDistance * 1.0001 /* + * equality + */)) { + candidate = i; + bestAngle = currentAngle; + bestDistance = currentDistance; + } + + } + return candidate != -1 ? new WaySegment(w, candidate) : null; + } +} diff --git a/src/org/openstreetmap/josm/plugins/improveway/ImproveWayAccuracyAction.java b/src/org/openstreetmap/josm/plugins/improveway/ImproveWayAccuracyAction.java deleted file mode 100644 index 2b1b063..0000000 --- a/src/org/openstreetmap/josm/plugins/improveway/ImproveWayAccuracyAction.java +++ /dev/null @@ -1,1032 +0,0 @@ -// License: GPL. For details, see LICENSE file. -package org.openstreetmap.josm.plugins.improveway; - -import static org.openstreetmap.josm.tools.I18n.marktr; -import static org.openstreetmap.josm.tools.I18n.tr; -import static org.openstreetmap.josm.tools.I18n.trn; - -import java.awt.Color; -import java.awt.Cursor; -import java.awt.FontMetrics; -import java.awt.Graphics2D; -import java.awt.Point; -import java.awt.RenderingHints; -import java.awt.Stroke; -import java.awt.event.KeyEvent; -import java.awt.event.MouseEvent; -import java.awt.geom.Arc2D; -import java.awt.geom.Ellipse2D; -import java.awt.geom.GeneralPath; -import java.awt.geom.Line2D; -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedList; -import java.util.List; -import java.util.Timer; -import java.util.TimerTask; - -import javax.swing.JOptionPane; - -import org.openstreetmap.josm.actions.ExpertToggleAction; -import org.openstreetmap.josm.actions.ExpertToggleAction.ExpertModeChangeListener; -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.MoveCommand; -import org.openstreetmap.josm.command.SequenceCommand; -import org.openstreetmap.josm.data.Bounds; -import org.openstreetmap.josm.data.UndoRedoHandler; -import org.openstreetmap.josm.data.coor.EastNorth; -import org.openstreetmap.josm.data.coor.LatLon; -import org.openstreetmap.josm.data.osm.DataSelectionListener; -import org.openstreetmap.josm.data.osm.DataSet; -import org.openstreetmap.josm.data.osm.IWaySegment; -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.event.SelectionEventManager; -import org.openstreetmap.josm.data.preferences.NamedColorProperty; -import org.openstreetmap.josm.data.projection.ProjectionRegistry; -import org.openstreetmap.josm.gui.MainApplication; -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.gui.util.GuiHelper; -import org.openstreetmap.josm.gui.util.KeyPressReleaseListener; -import org.openstreetmap.josm.gui.util.ModifierExListener; -import org.openstreetmap.josm.spi.preferences.Config; -import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent; -import org.openstreetmap.josm.tools.Geometry; -import org.openstreetmap.josm.tools.ImageProvider; -import org.openstreetmap.josm.tools.Logging; -import org.openstreetmap.josm.tools.Pair; -import org.openstreetmap.josm.tools.Shortcut; -import org.openstreetmap.josm.tools.Utils; - -/** - * @author Alexander Kachkaev <alexander@kachkaev.ru>, 2011 - */ -public class ImproveWayAccuracyAction extends MapMode implements MapViewPaintable, - DataSelectionListener, ModifierExListener, KeyPressReleaseListener, - ExpertModeChangeListener { - - enum State { - selecting, improving - } - - private State state; - - private MapView mv; - - private static final long serialVersionUID = 42L; - - private transient Way targetWay; - private transient Node candidateNode; - private transient IWaySegment candidateSegment; - - private Point mousePos; - private boolean dragging; - - private final Cursor cursorSelect; - private final Cursor cursorSelectHover; - private final Cursor cursorImprove; - private final Cursor cursorImproveAdd; - private final Cursor cursorImproveDelete; - private final Cursor cursorImproveAddLock; - private final Cursor cursorImproveLock; - - private Color guideColor; - private Color turnColor; - private Color distanceColor; - private Color arcFillColor; - private Color arcStrokeColor; - private Color perpendicularLineColor; - private Color equalAngleCircleColor; - - private transient Stroke selectTargetWayStroke; - private transient Stroke moveNodeStroke; - private transient Stroke moveNodeIntersectingStroke; - private transient Stroke addNodeStroke; - private transient Stroke deleteNodeStroke; - private transient Stroke arcStroke; - private transient Stroke perpendicularLineStroke; - private transient Stroke equalAngleCircleStroke; - private int dotSize; - - private boolean selectionChangedBlocked; - - protected String oldModeHelpText; - - private int arcRadiusPixels; - private int perpendicularLengthPixels; - private int turnTextDistance; - private int distanceTextDistance; - private int equalAngleCircleRadius; - private long longKeypressTime; - - private boolean helpersEnabled = false; - private boolean helpersUseOriginal = false; - private final transient Shortcut helpersShortcut; - private long keypressTime = 0; - private boolean helpersEnabledBeforeKeypressed = false; - private Timer longKeypressTimer; - private boolean isExpert = false; - - private boolean mod4 = false; // Windows/Super/Meta key - - /** - * Constructs a new {@code ImproveWayAccuracyAction}. - */ - public ImproveWayAccuracyAction() { - super(tr("Improve Way"), "improveway", - tr("Improve Way mode"), - Shortcut.registerShortcut("mapmode:ImproveWay", - tr("Mode: {0}", tr("Improve Way")), - KeyEvent.VK_W, Shortcut.DIRECT), Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); - - helpersShortcut = Shortcut.registerShortcut("mapmode:enablewayaccuracyhelpers", - tr("Mode: Enable way accuracy helpers"), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE); - - cursorSelect = ImageProvider.getCursor("normal", "mode"); - cursorSelectHover = ImageProvider.getCursor("hand", "mode"); - cursorImprove = ImageProvider.getCursor("crosshair", null); - cursorImproveAdd = ImageProvider.getCursor("crosshair", "addnode"); - cursorImproveDelete = ImageProvider.getCursor("crosshair", "delete_node"); - cursorImproveAddLock = ImageProvider.getCursor("crosshair", - "add_node_lock"); - cursorImproveLock = ImageProvider.getCursor("crosshair", "lock"); - ExpertToggleAction.addExpertModeChangeListener(this, true); - readPreferences(); - } - - // ------------------------------------------------------------------------- - // Mode methods - // ------------------------------------------------------------------------- - @Override - public void enterMode() { - if (!isEnabled()) { - return; - } - super.enterMode(); - - MapFrame map = MainApplication.getMap(); - mv = map.mapView; - mousePos = null; - oldModeHelpText = ""; - - if (getLayerManager().getEditDataSet() == null) { - return; - } - - updateStateByCurrentSelection(); - - map.keyDetector.addKeyListener(this); - map.mapView.addMouseListener(this); - map.mapView.addMouseMotionListener(this); - map.mapView.addTemporaryLayer(this); - SelectionEventManager.getInstance().addSelectionListener(this); - - map.keyDetector.addModifierExListener(this); - - if (!isExpert) return; - helpersEnabled = false; - keypressTime = 0; - resetTimer(); - longKeypressTimer.schedule(new TimerTask() { - @Override - public void run() { - helpersEnabled = true; - helpersUseOriginal = true; - MainApplication.getLayerManager().invalidateEditLayer(); - } - }, longKeypressTime); - } - - @Override - protected void readPreferences() { - guideColor = new NamedColorProperty(marktr("improve way accuracy helper line"), Color.RED).get(); - turnColor = new NamedColorProperty(marktr("improve way accuracy helper turn angle text"), new Color(240, 240, 240, 200)).get(); - distanceColor = new NamedColorProperty(marktr("improve way accuracy helper distance text"), new Color(240, 240, 240, 120)).get(); - arcFillColor = new NamedColorProperty(marktr("improve way accuracy helper arc fill"), new Color(200, 200, 200, 50)).get(); - arcStrokeColor = new NamedColorProperty(marktr("improve way accuracy helper arc stroke"), new Color(240, 240, 240, 150)).get(); - perpendicularLineColor = new NamedColorProperty(marktr("improve way accuracy helper perpendicular line"), - new Color(240, 240, 240, 150)).get(); - equalAngleCircleColor = new NamedColorProperty(marktr("improve way accuracy helper equal angle circle"), - new Color(240, 240, 240, 150)).get(); - - selectTargetWayStroke = GuiHelper.getCustomizedStroke(Config.getPref().get("improvewayaccuracy.stroke.select-target", "2")); - moveNodeStroke = GuiHelper.getCustomizedStroke(Config.getPref().get("improvewayaccuracy.stroke.move-node", "1 6")); - moveNodeIntersectingStroke = GuiHelper.getCustomizedStroke(Config.getPref().get("improvewayaccuracy.stroke.move-node-intersecting", "1 2 6")); - addNodeStroke = GuiHelper.getCustomizedStroke(Config.getPref().get("improvewayaccuracy.stroke.add-node", "1")); - deleteNodeStroke = GuiHelper.getCustomizedStroke(Config.getPref().get("improvewayaccuracy.stroke.delete-node", "1")); - arcStroke = GuiHelper.getCustomizedStroke(Config.getPref().get("improvewayaccuracy.stroke.helper-arc", "1")); - perpendicularLineStroke = GuiHelper.getCustomizedStroke(Config.getPref().get("improvewayaccuracy.stroke.helper-perpendicular-line", "1 6")); - equalAngleCircleStroke = GuiHelper.getCustomizedStroke(Config.getPref().get("improvewayaccuracy.stroke.helper-eual-angle-circle", "1")); - - dotSize = Config.getPref().getInt("improvewayaccuracy.dot-size", 6); - arcRadiusPixels = Config.getPref().getInt("improvewayaccuracy.helper-arc-radius", 200); - perpendicularLengthPixels = Config.getPref().getInt("improvewayaccuracy.helper-perpendicular-line-length", 100); - turnTextDistance = Config.getPref().getInt("improvewayaccuracy.helper-turn-text-distance", 15); - distanceTextDistance = Config.getPref().getInt("improvewayaccuracy.helper-distance-text-distance", 15); - equalAngleCircleRadius = Config.getPref().getInt("improvewayaccuracy.helper-equal-angle-circle-radius", 15); - longKeypressTime = Config.getPref().getInt("improvewayaccuracy.long-keypress-time", 250); - } - - @Override - public void exitMode() { - super.exitMode(); - - MainApplication.getMap().keyDetector.removeKeyListener(this); - MainApplication.getMap().mapView.removeMouseListener(this); - MainApplication.getMap().mapView.removeMouseMotionListener(this); - MainApplication.getMap().mapView.removeTemporaryLayer(this); - SelectionEventManager.getInstance().removeSelectionListener(this); - - MainApplication.getMap().keyDetector.removeModifierExListener(this); - MainApplication.getLayerManager().invalidateEditLayer(); - } - - @Override - protected void updateStatusLine() { - String newModeHelpText = getModeHelpText(); - if (!newModeHelpText.equals(oldModeHelpText)) { - oldModeHelpText = newModeHelpText; - MainApplication.getMap().statusLine.setHelpText(newModeHelpText); - MainApplication.getMap().statusLine.repaint(); - } - } - - @Override - public String getModeHelpText() { - if (state == State.selecting) { - if (targetWay != null) { - return tr("Click on the way to start improving its shape."); - } else { - return tr("Select a way that you want to make more accurate."); - } - } else { - if (ctrl) { - return tr("Click to add a new node. Release Ctrl to move existing nodes or hold Alt to delete."); - } else if (alt) { - return tr("Click to delete the highlighted node. Release Alt to move existing nodes or hold Ctrl to add new nodes."); - } else { - return tr("Click to move the highlighted node. Hold Ctrl to add new nodes, or Alt to delete."); - } - } - } - - @Override - public boolean layerIsSupported(Layer l) { - return l instanceof OsmDataLayer; - } - - @Override - protected void updateEnabledState() { - setEnabled(getLayerManager().getEditLayer() != null); - } - - // ------------------------------------------------------------------------- - // MapViewPaintable methods - // ------------------------------------------------------------------------- - /** - * Redraws temporary layer. Highlights targetWay in select mode. Draws - * preview lines in improve mode and highlights the candidateNode - */ - @Override - public void paint(Graphics2D g, MapView mv, Bounds bbox) { - - g.setColor(guideColor); - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - if (state == State.selecting && targetWay != null) { - // Highlighting the targetWay in Selecting state - // Non-native highlighting is used, because sometimes highlighted - // segments are covered with others, which is bad. - g.setStroke(selectTargetWayStroke); - - List nodes = targetWay.getNodes(); - - GeneralPath b = new GeneralPath(); - Point p0 = mv.getPoint(nodes.get(0)); - Point pn; - b.moveTo(p0.x, p0.y); - - for (Node n : nodes) { - pn = mv.getPoint(n); - b.lineTo(pn.x, pn.y); - } - if (targetWay.isClosed()) { - b.lineTo(p0.x, p0.y); - } - - g.draw(b); - - } else if (state == State.improving) { - // Drawing preview lines and highlighting the node - // that is going to be moved. - // Non-native highlighting is used here as well. - - // Finding endpoints - Point p1 = null, p2 = null; - if (ctrl && candidateSegment != null) { - g.setStroke(addNodeStroke); - p1 = mv.getPoint(candidateSegment.getFirstNode()); - p2 = mv.getPoint(candidateSegment.getSecondNode()); - } else if (!(alt ^ ctrl) && candidateNode != null) { - g.setStroke(moveNodeStroke); - List> wpps = targetWay.getNodePairs(false); - for (Pair wpp : wpps) { - if (wpp.a == candidateNode) { - p1 = mv.getPoint(wpp.b); - } - if (wpp.b == candidateNode) { - p2 = mv.getPoint(wpp.a); - } - if (p1 != null && p2 != null) { - break; - } - } - } else if (alt && !ctrl && candidateNode != null) { - g.setStroke(deleteNodeStroke); - List nodes = targetWay.getNodes(); - int index = nodes.indexOf(candidateNode); - - // Only draw line if node is not first and/or last - if (index > 0 && index < (nodes.size() - 1)) { - p1 = mv.getPoint(nodes.get(index - 1)); - p2 = mv.getPoint(nodes.get(index + 1)); - } - // TODO: indicate what part that will be deleted? (for end nodes) - } - - EastNorth newPointEN = getNewPointEN(); - Point newPoint = mv.getPoint(newPointEN); - - // Drawing preview lines - GeneralPath b = new GeneralPath(); - if (alt && !ctrl) { - // In delete mode - if (p1 != null && p2 != null) { - b.moveTo(p1.x, p1.y); - b.lineTo(p2.x, p2.y); - } - } else if (newPointEN != null && newPoint != null) { - // In add or move mode - if (p1 != null) { - b.moveTo(newPoint.x, newPoint.y); - b.lineTo(p1.x, p1.y); - } - if (p2 != null) { - b.moveTo(newPoint.x, newPoint.y); - b.lineTo(p2.x, p2.y); - } - } - g.draw(b); - - // Highlighting candidateNode - if (candidateNode != null) { - Point p = mv.getPoint(candidateNode); - g.setColor(guideColor); - g.fillRect(p.x - dotSize/2, p.y - dotSize/2, dotSize, dotSize); - } - - if (!alt && !ctrl && candidateNode != null) { - b.reset(); - drawIntersectingWayHelperLines(mv, b, newPoint); - g.setStroke(moveNodeIntersectingStroke); - g.draw(b); - } - - // Painting helpers visualizing turn angles and more - if (!helpersEnabled) return; - - // Perpendicular line at half distance - if (!(alt && !ctrl) && p1 != null && p2 != null) { - Point half = new Point( - (p1.x + p2.x)/2, - (p1.y + p2.y)/2 - ); - double heading = Math.atan2( - p2.y-p1.y, - p2.x-p1.x - ) + Math.PI/2; - g.setStroke(perpendicularLineStroke); - g.setColor(perpendicularLineColor); - g.draw(new Line2D.Double( - half.x + perpendicularLengthPixels * Math.cos(heading), - half.y + perpendicularLengthPixels * Math.sin(heading), - half.x - perpendicularLengthPixels * Math.cos(heading), - half.y - perpendicularLengthPixels * Math.sin(heading) - )); - } - - // Pie with turn angle - Node node; - LatLon coor, lastcoor = null; - Point point, lastpoint = null; - double distance; - double heading, lastheading = 0; - double turn; - Arc2D arc; - double arcRadius; - boolean candidateSegmentVisited = false; - int nodeCounter = 0; - int nodesCount = targetWay.getNodesCount(); - int endLoop = nodesCount; - if (targetWay.isClosed()) endLoop++; - for (int i = 0; i < endLoop; i++) { - // when way is closed we visit second node again - // to get turn for start/end node - node = targetWay.getNode(i == nodesCount ? 1 : i); - if (!helpersUseOriginal && newPointEN != null && - ctrl && - !candidateSegmentVisited && - candidateSegment != null && - candidateSegment.getSecondNode() == node - ) { - coor = ProjectionRegistry.getProjection().eastNorth2latlon(newPointEN); - point = newPoint; - candidateSegmentVisited = true; - i--; - } else if (!helpersUseOriginal && newPointEN != null && !alt && !ctrl && node == candidateNode) { - coor = ProjectionRegistry.getProjection().eastNorth2latlon(newPointEN); - point = newPoint; - } else if (!helpersUseOriginal && alt && !ctrl && node == candidateNode) { - continue; - } else { - coor = node.getCoor(); - point = mv.getPoint(coor); - } - if (nodeCounter >= 1) { - heading = fixHeading(-90+lastcoor.bearing(coor)*180/Math.PI); - distance = lastcoor.greatCircleDistance(coor); - if (nodeCounter >= 2) { - turn = Math.abs(fixHeading(heading-lastheading)); - double fixedHeading = fixHeading(heading - lastheading); - g.setColor(turnColor); - drawDisplacedlabel( - lastpoint.x, - lastpoint.y, - turnTextDistance, - (lastheading + fixedHeading/2 + (fixedHeading >= 0 ? 90 : -90))*Math.PI/180, - String.format("%1.0f °", turn), - g - ); - arcRadius = arcRadiusPixels; - arc = new Arc2D.Double( - lastpoint.x-arcRadius, - lastpoint.y-arcRadius, - arcRadius*2, - arcRadius*2, - -heading + (fixedHeading >= 0 ? 90 : -90), - fixedHeading, - Arc2D.PIE - ); - g.setStroke(arcStroke); - g.setColor(arcFillColor); - g.fill(arc); - g.setColor(arcStrokeColor); - g.draw(arc); - } - - // Display segment length - // avoid doubling first segment on closed ways - if (i != nodesCount) { - g.setColor(distanceColor); - drawDisplacedlabel( - (lastpoint.x+point.x)/2, - (lastpoint.y+point.y)/2, - distanceTextDistance, - (heading + 90)*Math.PI/180, - String.format("%1.0f m", distance), - g - ); - } - - lastheading = heading; - } - lastcoor = coor; - lastpoint = point; - nodeCounter++; - } - - // Find and display point where turn angle will be same with two neighbours - EastNorth equalAngleEN = findEqualAngleEN(); - if (equalAngleEN != null) { - Point equalAnglePoint = mv.getPoint(equalAngleEN); - Ellipse2D.Double equalAngleCircle = new Ellipse2D.Double( - equalAnglePoint.x-equalAngleCircleRadius/2, - equalAnglePoint.y-equalAngleCircleRadius/2, - equalAngleCircleRadius, - equalAngleCircleRadius); - g.setStroke(equalAngleCircleStroke); - g.setColor(equalAngleCircleColor); - g.draw(equalAngleCircle); - } - } - } - - // returns node index for closed ways using possibly under/overflowed index - // returns -1 if not closed and out of range - private int fixIndex(int count, boolean closed, int index) { - if (index >= 0 && index < count) return index; - if (!closed) return -1; - while (index < 0) index += count; - while (index >= count) index -= count; - return index; - } - - private double fixHeading(double heading) { - while (heading < -180) heading += 360; - while (heading > 180) heading -= 360; - return heading; - } - - public static void drawDisplacedlabel( - int x, - int y, - int distance, - double heading, - String labelText, - Graphics2D g - ) { - int labelWidth, labelHeight; - FontMetrics fontMetrics = g.getFontMetrics(); - labelWidth = fontMetrics.stringWidth(labelText); - labelHeight = fontMetrics.getHeight(); - g.drawString( - labelText, - (int) (x+(distance+(labelWidth-labelHeight)/2)*Math.cos(heading)-labelWidth/2), - (int) (y+distance*Math.sin(heading)+labelHeight/2) - ); - } - - public EastNorth getNewPointEN() { - if (mod4) { - return findEqualAngleEN(); - } else if (mousePos != null) { - return mv.getEastNorth(mousePos.x, mousePos.y); - } else { - return null; - } - } - - public EastNorth findEqualAngleEN() { - int index1 = -1; - int index2 = -1; - int realNodesCount = targetWay.getRealNodesCount(); - - for (int i = 0; i < realNodesCount; i++) { - Node node = targetWay.getNode(i); - if (node == candidateNode) { - index1 = i-1; - index2 = i+1; - } - if (candidateSegment != null) { - if (node == candidateSegment.getFirstNode()) index1 = i; - if (node == candidateSegment.getSecondNode()) index2 = i; - } - } - - int i11 = fixIndex(realNodesCount, targetWay.isClosed(), index1-1); - int i12 = fixIndex(realNodesCount, targetWay.isClosed(), index1); - int i21 = fixIndex(realNodesCount, targetWay.isClosed(), index2); - int i22 = fixIndex(realNodesCount, targetWay.isClosed(), index2+1); - if (i11 < 0 || i12 < 0 || i21 < 0 || i22 < 0) return null; - - EastNorth p11 = targetWay.getNode(i11).getEastNorth(); - EastNorth p12 = targetWay.getNode(i12).getEastNorth(); - EastNorth p21 = targetWay.getNode(i21).getEastNorth(); - EastNorth p22 = targetWay.getNode(i22).getEastNorth(); - - double a1 = Geometry.getSegmentAngle(p11, p12); - double a2 = Geometry.getSegmentAngle(p21, p22); - double a = fixHeading((a2-a1)*180/Math.PI)*Math.PI/180/3; - - EastNorth p1r = p11.rotate(p12, -a); - EastNorth p2r = p22.rotate(p21, a); - - return Geometry.getLineLineIntersection(p1r, p12, p21, p2r); - } - - protected void drawIntersectingWayHelperLines(MapView mv, GeneralPath b, Point newPoint) { - for (final OsmPrimitive referrer : candidateNode.getReferrers()) { - if (!(referrer instanceof Way) || targetWay.equals(referrer)) { - continue; - } - final List nodes = ((Way) referrer).getNodes(); - for (int i = 0; i < nodes.size(); i++) { - if (!candidateNode.equals(nodes.get(i))) { - continue; - } - if (i > 0) { - final Point p = mv.getPoint(nodes.get(i - 1)); - b.moveTo(newPoint.x, newPoint.y); - b.lineTo(p.x, p.y); - } - if (i < nodes.size() - 1) { - final Point p = mv.getPoint(nodes.get(i + 1)); - b.moveTo(newPoint.x, newPoint.y); - b.lineTo(p.x, p.y); - } - } - } - } - - // ------------------------------------------------------------------------- - // Event handlers - // ------------------------------------------------------------------------- - @Override - public void modifiersExChanged(int modifiers) { - if (!MainApplication.isDisplayingMapView() || !MainApplication.getMap().mapView.isActiveLayerDrawable()) { - return; - } - updateKeyModifiersEx(modifiers); - updateCursorDependentObjectsIfNeeded(); - updateCursor(); - updateStatusLine(); - MainApplication.getLayerManager().invalidateEditLayer(); - } - - @Override - public void selectionChanged(SelectionChangeEvent event) { - if (selectionChangedBlocked) { - return; - } - updateStateByCurrentSelection(); - } - - @Override - public void mouseDragged(MouseEvent e) { - dragging = true; - mouseMoved(e); - } - - @Override - public void mouseMoved(MouseEvent e) { - if (!isEnabled()) { - return; - } - - mousePos = e.getPoint(); - - updateKeyModifiers(e); - updateCursorDependentObjectsIfNeeded(); - updateCursor(); - updateStatusLine(); - MainApplication.getLayerManager().invalidateEditLayer(); - } - - @Override - public void mouseReleased(MouseEvent e) { - dragging = false; - if (!isEnabled() || e.getButton() != MouseEvent.BUTTON1) { - return; - } - - updateKeyModifiers(e); - mousePos = e.getPoint(); - EastNorth newPointEN = getNewPointEN(); - - if (state == State.selecting) { - if (targetWay != null) { - getLayerManager().getEditDataSet().setSelected(targetWay.getPrimitiveId()); - updateStateByCurrentSelection(); - } - } else if (state == State.improving && newPointEN != null) { - // Checking if the new coordinate is outside of the world - if (new Node(newPointEN).isOutSideWorld()) { - JOptionPane.showMessageDialog(MainApplication.getMainFrame(), - tr("Cannot add a node outside of the world."), - tr("Warning"), JOptionPane.WARNING_MESSAGE); - return; - } - - if (ctrl && !alt && candidateSegment != null) { - // Adding a new node to the highlighted segment - // Important: If there are other ways containing the same - // segment, a node must added to all of that ways. - Collection virtualCmds = new LinkedList<>(); - - // Creating a new node - Node virtualNode = new Node( - ProjectionRegistry.getProjection().eastNorth2latlon(newPointEN) - ); - virtualCmds.add(new AddCommand(getLayerManager().getEditDataSet(), virtualNode)); - - // Looking for candidateSegment copies in ways that are - // referenced - // by candidateSegment nodes - List firstNodeWays = new ArrayList<>(Utils.filteredCollection( - candidateSegment.getFirstNode().getReferrers(), - Way.class)); - List secondNodeWays = new ArrayList<>(Utils.filteredCollection( - candidateSegment.getFirstNode().getReferrers(), - Way.class)); - - Collection> virtualSegments = new LinkedList<>(); - for (Way w : firstNodeWays) { - List> wpps = w.getNodePairs(true); - for (Way w2 : secondNodeWays) { - if (!w.equals(w2)) { - continue; - } - // A way is referenced in both nodes. - // Checking if there is such segment - int i = -1; - for (Pair wpp : wpps) { - ++i; - boolean ab = wpp.a.equals(candidateSegment.getFirstNode()) - && wpp.b.equals(candidateSegment.getSecondNode()); - boolean ba = wpp.b.equals(candidateSegment.getFirstNode()) - && wpp.a.equals(candidateSegment.getSecondNode()); - if (ab || ba) { - virtualSegments.add(new IWaySegment<>(w, i)); - } - } - } - } - - // Adding the node to all segments found - for (IWaySegment virtualSegment : virtualSegments) { - Way w = virtualSegment.getWay(); - Way wnew = new Way(w); - wnew.addNode(virtualSegment.getUpperIndex(), virtualNode); - virtualCmds.add(new ChangeCommand(w, wnew)); - } - - // Finishing the sequence command - String text = trn("Add a new node to way", - "Add a new node to {0} ways", - virtualSegments.size(), virtualSegments.size()); - - UndoRedoHandler.getInstance().add(new SequenceCommand(text, virtualCmds)); - - } else if (alt && !ctrl && candidateNode != null) { - // Deleting the highlighted node - - //check to see if node is in use by more than one object - List referrers = candidateNode.getReferrers(); - Collection ways = Utils.filteredCollection(referrers, Way.class); - if (referrers.size() != 1 || ways.size() != 1) { - // detach node from way - final Way newWay = new Way(targetWay); - final List nodes = newWay.getNodes(); - nodes.remove(candidateNode); - newWay.setNodes(nodes); - UndoRedoHandler.getInstance().add(new ChangeCommand(targetWay, newWay)); - } else if (candidateNode.isTagged()) { - JOptionPane.showMessageDialog(MainApplication.getMainFrame(), - tr("Cannot delete node that has tags"), - tr("Error"), JOptionPane.ERROR_MESSAGE); - } else { - List nodeList = new ArrayList<>(); - nodeList.add(candidateNode); - Command deleteCmd = DeleteCommand.delete(nodeList, true); - if (deleteCmd != null) { - UndoRedoHandler.getInstance().add(deleteCmd); - } - } - - - } else if (candidateNode != null) { - // Moving the highlighted node - EastNorth nodeEN = candidateNode.getEastNorth(); - - Node saveCandidateNode = candidateNode; - UndoRedoHandler.getInstance().add(new MoveCommand(candidateNode, newPointEN.east() - nodeEN.east(), newPointEN.north() - - nodeEN.north())); - candidateNode = saveCandidateNode; - - } - } - - updateCursor(); - updateStatusLine(); - MainApplication.getLayerManager().invalidateEditLayer(); - } - - @Override - public void mouseExited(MouseEvent e) { - if (!isEnabled()) { - return; - } - - if (!dragging) { - mousePos = null; - } - MainApplication.getLayerManager().invalidateEditLayer(); - } - - // ------------------------------------------------------------------------- - // Custom methods - // ------------------------------------------------------------------------- - /** - * Sets new cursor depending on state, mouse position - */ - private void updateCursor() { - if (!isEnabled()) { - mv.setNewCursor(null, this); - return; - } - - if (state == State.selecting) { - mv.setNewCursor(targetWay == null ? cursorSelect - : cursorSelectHover, this); - } else if (state == State.improving) { - if (alt && !ctrl) { - mv.setNewCursor(cursorImproveDelete, this); - } else if (shift || dragging) { - if (ctrl) { - mv.setNewCursor(cursorImproveAddLock, this); - } else { - mv.setNewCursor(cursorImproveLock, this); - } - } else if (ctrl && !alt) { - mv.setNewCursor(cursorImproveAdd, this); - } else { - mv.setNewCursor(cursorImprove, this); - } - } - } - - /** - * Updates these objects under cursor: targetWay, candidateNode, - * candidateSegment - */ - public void updateCursorDependentObjectsIfNeeded() { - if (state == State.improving && (shift || dragging) - && !(candidateNode == null && candidateSegment == null)) { - return; - } - - if (mousePos == null) { - candidateNode = null; - candidateSegment = null; - return; - } - - if (state == State.selecting) { - targetWay = ImproveWayAccuracyHelper.findWay(mv, mousePos); - } else if (state == State.improving) { - if (ctrl && !alt) { - candidateSegment = ImproveWayAccuracyHelper.findCandidateSegment(mv, - targetWay, mousePos); - candidateNode = null; - } else { - candidateNode = ImproveWayAccuracyHelper.findCandidateNode(mv, - targetWay, mousePos); - candidateSegment = null; - } - } - } - - /** - * Switches to Selecting state - */ - public void startSelecting() { - state = State.selecting; - - targetWay = null; - - MainApplication.getLayerManager().invalidateEditLayer(); - updateStatusLine(); - } - - /** - * Switches to Improving state - * - * @param targetWay Way that is going to be improved - */ - public void startImproving(Way targetWay) { - state = State.improving; - - Collection currentSelection = getLayerManager().getEditDataSet().getSelected(); - if (currentSelection.size() != 1 - || !currentSelection.iterator().next().equals(targetWay)) { - selectionChangedBlocked = true; - getLayerManager().getEditDataSet().clearSelection(); - getLayerManager().getEditDataSet().setSelected(targetWay.getPrimitiveId()); - selectionChangedBlocked = false; - } - - this.targetWay = targetWay; - this.candidateNode = null; - this.candidateSegment = null; - - MainApplication.getLayerManager().invalidateEditLayer(); - updateStatusLine(); - } - - /** - * Updates the state according to the current selection. Goes to Improve - * state if a single way or node is selected. Extracts a way by a node in - * the second case. - * - */ - private void updateStateByCurrentSelection() { - final List nodeList = new ArrayList<>(); - final List wayList = new ArrayList<>(); - final DataSet editDataSet = getLayerManager().getEditDataSet(); - if (editDataSet != null) { - final Collection sel = editDataSet.getSelected(); - - // Collecting nodes and ways from the selection - for (OsmPrimitive p : sel) { - if (p instanceof Way) { - wayList.add((Way) p); - } - if (p instanceof Node) { - nodeList.add((Node) p); - } - } - } - - if (wayList.size() == 1) { - // Starting improving the single selected way - startImproving(wayList.get(0)); - return; - } else if (nodeList.size() == 1) { - // Starting improving the only way of the single selected node - List r = nodeList.get(0).getReferrers(); - if (r.size() == 1 && (r.get(0) instanceof Way)) { - startImproving((Way) r.get(0)); - return; - } - } - - // Starting selecting by default - startSelecting(); - } - - private void resetTimer() { - if (longKeypressTimer != null) { - try { - longKeypressTimer.cancel(); - longKeypressTimer.purge(); - } catch (IllegalStateException exception) { - Logging.debug(exception); - } - } - longKeypressTimer = new Timer(); - } - - @Override - public void doKeyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_WINDOWS) { - mod4 = true; - MainApplication.getLayerManager().invalidateEditLayer(); - return; - } - if (!helpersShortcut.isEvent(e) && !getShortcut().isEvent(e)) return; - if (!isExpert) return; - keypressTime = System.currentTimeMillis(); - helpersEnabledBeforeKeypressed = helpersEnabled; - if (!helpersEnabled) helpersEnabled = true; - helpersUseOriginal = true; - MainApplication.getLayerManager().invalidateEditLayer(); - } - - @Override - public void doKeyReleased(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_WINDOWS) { - mod4 = false; - MainApplication.getLayerManager().invalidateEditLayer(); - return; - } - if (!helpersShortcut.isEvent(e) && !getShortcut().isEvent(e)) return; - if (!isExpert) return; - resetTimer(); - long keyupTime = System.currentTimeMillis(); - if (keypressTime == 0) { // comes from enterMode - helpersEnabled = false; - } else if (keyupTime-keypressTime > longKeypressTime) { - helpersEnabled = helpersEnabledBeforeKeypressed; - } else { - helpersEnabled = !helpersEnabledBeforeKeypressed; - } - helpersUseOriginal = false; - MainApplication.getLayerManager().invalidateEditLayer(); - } - - @Override - public void expertChanged(boolean isExpert) { - this.isExpert = isExpert; - if (!isExpert && helpersEnabled) { - helpersEnabled = false; - MainApplication.getLayerManager().invalidateEditLayer(); - } - } - - @Override - public void preferenceChanged(PreferenceChangeEvent e) { - super.preferenceChanged(e); - if (isEnabled() && (e.getKey().startsWith("improvewayaccuracy") || e.getKey().startsWith("color.improve.way.accuracy"))) { - MainApplication.getLayerManager().invalidateEditLayer(); - } - } -} diff --git a/src/org/openstreetmap/josm/plugins/improveway/ImproveWayAction.java b/src/org/openstreetmap/josm/plugins/improveway/ImproveWayAction.java new file mode 100644 index 0000000..b60e791 --- /dev/null +++ b/src/org/openstreetmap/josm/plugins/improveway/ImproveWayAction.java @@ -0,0 +1,347 @@ +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.improveway; + +import org.openstreetmap.josm.core.patch.ImproveWayAccuracyAction; +import org.openstreetmap.josm.data.Bounds; +import org.openstreetmap.josm.data.coor.EastNorth; +import org.openstreetmap.josm.data.coor.LatLon; +import org.openstreetmap.josm.data.osm.Node; +import org.openstreetmap.josm.data.preferences.NamedColorProperty; +import org.openstreetmap.josm.data.projection.ProjectionRegistry; +import org.openstreetmap.josm.gui.MainApplication; +import org.openstreetmap.josm.gui.MapView; +import org.openstreetmap.josm.gui.util.GuiHelper; +import org.openstreetmap.josm.spi.preferences.Config; +import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent; +import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener; +import org.openstreetmap.josm.tools.Geometry; +import org.openstreetmap.josm.tools.Shortcut; + +import java.awt.Color; +import java.awt.Cursor; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.RenderingHints; +import java.awt.Stroke; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.awt.geom.Arc2D; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Line2D; + +import static org.openstreetmap.josm.tools.I18n.marktr; +import static org.openstreetmap.josm.tools.I18n.tr; + +/** + * @author András Kolesár kolesar@openstreetmap.hu 2016 + */ +public class ImproveWayAction extends ImproveWayAccuracyAction implements PreferenceChangedListener { + + protected Color turnColor; + protected Color distanceColor; + protected Color arcFillColor; + protected Color arcStrokeColor; + protected Color perpendicularLineColor; + protected Color equalAngleCircleColor; + + protected transient Stroke arcStroke; + protected transient Stroke perpendicularLineStroke; + protected transient Stroke equalAngleCircleStroke; + + protected int arcRadiusPixels; + protected int perpendicularLengthPixels; + protected int turnTextDistance; + protected int distanceTextDistance; + protected int equalAngleCircleRadius; + + protected ImproveWaySettings settings = new ImproveWaySettings(this, getShortcut()); + + public ImproveWayAction() { + super( + tr("Improve Way"), + "improveway", + tr("Improve Way mode"), + Shortcut.registerShortcut( + "mapmode:ImproveWay", + tr("Mode: {0}", tr("Improve Way")), + KeyEvent.VK_W, + Shortcut.DIRECT + ), + Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) + ); + } + + // ------------------------------------------------------------------------- + // Mode methods + // ------------------------------------------------------------------------- + @Override + public void enterMode() { + super.enterMode(); + settings.onEnterMode(); + } + + @Override + public void exitMode() { + super.exitMode(); + settings.onExitMode(); + } + + @Override + protected void readPreferences() { + super.readPreferences(); + turnColor = new NamedColorProperty(marktr("improve way accuracy helper turn angle text"), new Color(240, 240, 240, 200)).get(); + distanceColor = new NamedColorProperty(marktr("improve way accuracy helper distance text"), new Color(240, 240, 240, 120)).get(); + arcFillColor = new NamedColorProperty(marktr("improve way accuracy helper arc fill"), new Color(200, 200, 200, 50)).get(); + arcStrokeColor = new NamedColorProperty(marktr("improve way accuracy helper arc stroke"), new Color(240, 240, 240, 150)).get(); + perpendicularLineColor = new NamedColorProperty(marktr("improve way accuracy helper perpendicular line"), + new Color(240, 240, 240, 150)).get(); + equalAngleCircleColor = new NamedColorProperty(marktr("improve way accuracy helper equal angle circle"), + new Color(240, 240, 240, 150)).get(); + + arcStroke = GuiHelper.getCustomizedStroke(Config.getPref().get("improvewayaccuracy.stroke.helper-arc", "1")); + perpendicularLineStroke = GuiHelper.getCustomizedStroke(Config.getPref().get("improvewayaccuracy.stroke.helper-perpendicular-line", "1 6")); + equalAngleCircleStroke = GuiHelper.getCustomizedStroke(Config.getPref().get("improvewayaccuracy.stroke.helper-eual-angle-circle", "1")); + + arcRadiusPixels = Config.getPref().getInt("improvewayaccuracy.helper-arc-radius", 200); + perpendicularLengthPixels = Config.getPref().getInt("improvewayaccuracy.helper-perpendicular-line-length", 100); + turnTextDistance = Config.getPref().getInt("improvewayaccuracy.helper-turn-text-distance", 15); + distanceTextDistance = Config.getPref().getInt("improvewayaccuracy.helper-distance-text-distance", 15); + equalAngleCircleRadius = Config.getPref().getInt("improvewayaccuracy.helper-equal-angle-circle-radius", 15); + settings.longKeypressTime = Config.getPref().getInt("improvewayaccuracy.long-keypress-time", 250); + } + + @Override + public void paint(Graphics2D g, MapView mv, Bounds bbox) { + super.paint(g, mv, bbox); + + if (state == State.IMPROVING) { + // Painting helpers visualizing turn angles and more + if (!settings.helpersEnabled) return; + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + drawHalfDistanceLine(g, mv); + drawTurnAnglePie(g, mv); + drawEqualAnglePoint(g, mv); + } + } + + /** + * Draw a perpendicular line at half distance between two endpoints + */ + protected void drawHalfDistanceLine(Graphics2D g, MapView mv) { + if (!(alt && !ctrl) && endpoint1 != null && endpoint2 != null) { + Point p1 = mv.getPoint(endpoint1); + Point p2 = mv.getPoint(endpoint2); + Point half = new Point( + (p1.x + p2.x)/2, + (p1.y + p2.y)/2 + ); + double heading = Math.atan2( + p2.y-p1.y, + p2.x-p1.x + ) + Math.PI/2; + g.setStroke(perpendicularLineStroke); + g.setColor(perpendicularLineColor); + g.draw(new Line2D.Double( + half.x + perpendicularLengthPixels * Math.cos(heading), + half.y + perpendicularLengthPixels * Math.sin(heading), + half.x - perpendicularLengthPixels * Math.cos(heading), + half.y - perpendicularLengthPixels * Math.sin(heading) + )); + } + } + + /** + * Draw a pie (part of a circle) representing turn angle at each node + */ + protected void drawTurnAnglePie(Graphics2D g, MapView mv) { + LatLon lastcoor = null; + Point lastpoint = null; + double lastheading = 0d; + boolean candidateSegmentVisited = false; + int nodeCounter = 0; + + if (targetWay == null) return; + int nodesCount = targetWay.getNodesCount(); + int endLoop = nodesCount; + if (targetWay.isClosed()) endLoop++; + + LatLon newLatLon = getNewLatLon(); + Point newPoint = mv.getPoint(newLatLon); + + for (int i = 0; i < endLoop; i++) { + // when way is closed we visit second node again + // to get turn for start/end node + Node node = targetWay.getNode(i == nodesCount ? 1 : i); + LatLon coor; + Point point; + if (!settings.helpersUseOriginal && newLatLon != null && + ctrl && + !candidateSegmentVisited && + candidateSegment != null && + candidateSegment.getSecondNode() == node + ) { + coor = newLatLon; + point = newPoint; + candidateSegmentVisited = true; + i--; + } else if (!settings.helpersUseOriginal && newLatLon != null && !alt && !ctrl && node == candidateNode) { + coor = newLatLon; + point = newPoint; + } else if (!settings.helpersUseOriginal && alt && !ctrl && node == candidateNode) { + continue; + } else { + coor = node.getCoor(); + point = mv.getPoint(coor); + } + if (nodeCounter >= 1 && lastcoor != null && lastpoint != null) { + double heading = ImproveWayHelper.fixHeading(-90+lastcoor.bearing(coor)*180/Math.PI); + double distance = lastcoor.greatCircleDistance(coor); + if (nodeCounter >= 2) { + double turn = Math.abs(ImproveWayHelper.fixHeading(heading-lastheading)); + double fixedHeading = ImproveWayHelper.fixHeading(heading - lastheading); + g.setColor(turnColor); + ImproveWayHelper.drawDisplacedlabel( + lastpoint.x, + lastpoint.y, + turnTextDistance, + (lastheading + fixedHeading/2 + (fixedHeading >= 0 ? 90 : -90))*Math.PI/180, + String.format("%1.0f °", turn), + g + ); + double arcRadius = arcRadiusPixels; + Arc2D arc = new Arc2D.Double( + lastpoint.x-arcRadius, + lastpoint.y-arcRadius, + arcRadius*2, + arcRadius*2, + -heading + (fixedHeading >= 0 ? 90 : -90), + fixedHeading, + Arc2D.PIE + ); + g.setStroke(arcStroke); + g.setColor(arcFillColor); + g.fill(arc); + g.setColor(arcStrokeColor); + g.draw(arc); + } + + // Display segment length + // avoid doubling first segment on closed ways + if (i != nodesCount) { + g.setColor(distanceColor); + ImproveWayHelper.drawDisplacedlabel( + (lastpoint.x+point.x)/2, + (lastpoint.y+point.y)/2, + distanceTextDistance, + (heading + 90)*Math.PI/180, + String.format("%1.0f m", distance), + g + ); + } + + lastheading = heading; + } + lastcoor = coor; + lastpoint = point; + nodeCounter++; + } + } + + /** + * Draw a point where turn angle will be same with two neighbours + */ + protected void drawEqualAnglePoint(Graphics2D g, MapView mv) { + LatLon equalAngleLatLon = findEqualAngleLatLon(); + if (equalAngleLatLon != null) { + Point equalAnglePoint = mv.getPoint(equalAngleLatLon); + Ellipse2D.Double equalAngleCircle = new Ellipse2D.Double( + equalAnglePoint.x-equalAngleCircleRadius/2d, + equalAnglePoint.y-equalAngleCircleRadius/2d, + equalAngleCircleRadius, + equalAngleCircleRadius); + g.setStroke(equalAngleCircleStroke); + g.setColor(equalAngleCircleColor); + g.draw(equalAngleCircle); + } + } + + protected LatLon getNewLatLon() { + if (settings.meta) { + return findEqualAngleLatLon(); + } else if (mousePos != null) { + return mv.getLatLon(mousePos.x, mousePos.y); + } else { + return null; + } + } + + protected void setMouseToEqualAnglePoint() { + mousePos = mv.getPoint(findEqualAngleLatLon()); + } + + protected LatLon findEqualAngleLatLon() { + int index1 = -1; + int index2 = -1; + if (targetWay == null) return null; + int realNodesCount = targetWay.getRealNodesCount(); + + for (int i = 0; i < realNodesCount; i++) { + Node node = targetWay.getNode(i); + if (node == candidateNode) { + index1 = i-1; + index2 = i+1; + } + if (candidateSegment != null) { + if (node == candidateSegment.getFirstNode()) index1 = i; + if (node == candidateSegment.getSecondNode()) index2 = i; + } + } + + int i11 = ImproveWayHelper.fixIndex(realNodesCount, targetWay.isClosed(), index1-1); + int i12 = ImproveWayHelper.fixIndex(realNodesCount, targetWay.isClosed(), index1); + int i21 = ImproveWayHelper.fixIndex(realNodesCount, targetWay.isClosed(), index2); + int i22 = ImproveWayHelper.fixIndex(realNodesCount, targetWay.isClosed(), index2+1); + if (i11 < 0 || i12 < 0 || i21 < 0 || i22 < 0) return null; + + EastNorth p11 = targetWay.getNode(i11).getEastNorth(); + EastNorth p12 = targetWay.getNode(i12).getEastNorth(); + EastNorth p21 = targetWay.getNode(i21).getEastNorth(); + EastNorth p22 = targetWay.getNode(i22).getEastNorth(); + + double a1 = Geometry.getSegmentAngle(p11, p12); + double a2 = Geometry.getSegmentAngle(p21, p22); + double a = ImproveWayHelper.fixHeading((a2-a1)*180/Math.PI)*Math.PI/180/3; + + EastNorth p1r = p11.rotate(p12, -a); + EastNorth p2r = p22.rotate(p21, a); + + EastNorth intersection = Geometry.getLineLineIntersection(p1r, p12, p21, p2r); + return ProjectionRegistry.getProjection().eastNorth2latlon(intersection); + } + + @Override + protected void updateMousePosition(MouseEvent e) { + if (!settings.meta) { + super.updateMousePosition(e); + } else { + setMouseToEqualAnglePoint(); + } + } + + @Override + public void updateCursorDependentObjectsIfNeeded() { + if (!settings.meta) { + super.updateCursorDependentObjectsIfNeeded(); + } + } + + @Override + public void preferenceChanged(PreferenceChangeEvent e) { + super.preferenceChanged(e); + if (isEnabled() && (e.getKey().startsWith("improvewayaccuracy") || e.getKey().startsWith("color.improve.way.accuracy"))) { + MainApplication.getLayerManager().invalidateEditLayer(); + } + } + +} diff --git a/src/org/openstreetmap/josm/plugins/improveway/ImproveWayHelper.java b/src/org/openstreetmap/josm/plugins/improveway/ImproveWayHelper.java new file mode 100644 index 0000000..91e4a76 --- /dev/null +++ b/src/org/openstreetmap/josm/plugins/improveway/ImproveWayHelper.java @@ -0,0 +1,41 @@ +package org.openstreetmap.josm.plugins.improveway; + +import java.awt.FontMetrics; +import java.awt.Graphics2D; + +public class ImproveWayHelper { + public static void drawDisplacedlabel( + int x, + int y, + int distance, + double heading, + String labelText, + Graphics2D g + ) { + int labelWidth, labelHeight; + FontMetrics fontMetrics = g.getFontMetrics(); + labelWidth = fontMetrics.stringWidth(labelText); + labelHeight = fontMetrics.getHeight(); + g.drawString( + labelText, + (int) (x + (distance + (labelWidth - labelHeight) / 2) * Math.cos(heading) - labelWidth / 2), + (int) (y + distance * Math.sin(heading) + labelHeight / 2) + ); + } + + // returns node index for closed ways using possibly under/overflowed index + // returns -1 if not closed and out of range + protected static int fixIndex(int count, boolean closed, int index) { + if (index >= 0 && index < count) return index; + if (!closed) return -1; + while (index < 0) index += count; + while (index >= count) index -= count; + return index; + } + + protected static double fixHeading(double heading) { + while (heading < -180) heading += 360; + while (heading > 180) heading -= 360; + return heading; + } +} diff --git a/src/org/openstreetmap/josm/plugins/improveway/ImproveWayPlugin.java b/src/org/openstreetmap/josm/plugins/improveway/ImproveWayPlugin.java index f644544..b33f2bf 100644 --- a/src/org/openstreetmap/josm/plugins/improveway/ImproveWayPlugin.java +++ b/src/org/openstreetmap/josm/plugins/improveway/ImproveWayPlugin.java @@ -1,22 +1,22 @@ -// License: GPL. For details, see LICENSE file. -package org.openstreetmap.josm.plugins.improveway; - -import org.openstreetmap.josm.gui.IconToggleButton; -import org.openstreetmap.josm.gui.MainApplication; -import org.openstreetmap.josm.gui.MapFrame; -import org.openstreetmap.josm.plugins.Plugin; -import org.openstreetmap.josm.plugins.PluginInformation; - -public class ImproveWayPlugin extends Plugin { - - public ImproveWayPlugin(final PluginInformation info) { - super(info); - } - - @Override - public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) { - if (oldFrame == null && newFrame != null) { - MainApplication.getMap().addMapMode(new IconToggleButton(new ImproveWayAccuracyAction(), false)); - } - } -} +// License: GPL. For details, see LICENSE file. +package org.openstreetmap.josm.plugins.improveway; + +import org.openstreetmap.josm.gui.IconToggleButton; +import org.openstreetmap.josm.gui.MainApplication; +import org.openstreetmap.josm.gui.MapFrame; +import org.openstreetmap.josm.plugins.Plugin; +import org.openstreetmap.josm.plugins.PluginInformation; + +public class ImproveWayPlugin extends Plugin { + + public ImproveWayPlugin(final PluginInformation info) { + super(info); + } + + @Override + public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) { + if (oldFrame == null && newFrame != null) { + MainApplication.getMap().addMapMode(new IconToggleButton(new ImproveWayAction(), false)); + } + } +} diff --git a/src/org/openstreetmap/josm/plugins/improveway/ImproveWaySettings.java b/src/org/openstreetmap/josm/plugins/improveway/ImproveWaySettings.java new file mode 100644 index 0000000..4fed2f4 --- /dev/null +++ b/src/org/openstreetmap/josm/plugins/improveway/ImproveWaySettings.java @@ -0,0 +1,115 @@ +package org.openstreetmap.josm.plugins.improveway; + +import org.openstreetmap.josm.actions.ExpertToggleAction; +import org.openstreetmap.josm.actions.ExpertToggleAction.ExpertModeChangeListener; +import org.openstreetmap.josm.gui.MainApplication; +import org.openstreetmap.josm.gui.util.KeyPressReleaseListener; +import org.openstreetmap.josm.tools.Logging; +import org.openstreetmap.josm.tools.Shortcut; + +import java.awt.event.KeyEvent; +import java.util.Timer; +import java.util.TimerTask; + +public class ImproveWaySettings implements KeyPressReleaseListener, ExpertModeChangeListener { + + protected boolean helpersEnabled; + protected boolean helpersUseOriginal; + protected boolean helpersEnabledBeforeKeypressed; + protected long keypressTime; + protected long longKeypressTime; + protected Timer longKeypressTimer; + protected boolean isExpert; + protected boolean meta; // Windows/Super/Meta key + + protected final transient ImproveWayAction improveWayAction; + protected final transient Shortcut helpersShortcut; + + public ImproveWaySettings(ImproveWayAction improveWayAction, Shortcut helpersShortcut) { + this.improveWayAction = improveWayAction; + this.helpersShortcut = helpersShortcut; + ExpertToggleAction.addExpertModeChangeListener(this, true); + } + + void onEnterMode() { + MainApplication.getMap().keyDetector.addKeyListener(this); + if (!isExpert) return; + meta = false; + helpersEnabled = false; + keypressTime = 0; + resetTimer(); + longKeypressTimer.schedule(new TimerTask() { + @Override + public void run() { + helpersEnabled = true; + helpersUseOriginal = true; + MainApplication.getLayerManager().invalidateEditLayer(); + } + }, longKeypressTime); + } + + void onExitMode() { + MainApplication.getMap().keyDetector.removeKeyListener(this); + } + + @Override + public void doKeyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_WINDOWS) { + meta = true; + improveWayAction.setMouseToEqualAnglePoint(); + MainApplication.getLayerManager().invalidateEditLayer(); + return; + } + if (!helpersShortcut.isEvent(e)) return; + if (!isExpert) return; + keypressTime = System.currentTimeMillis(); + helpersEnabledBeforeKeypressed = helpersEnabled; + if (!helpersEnabled) helpersEnabled = true; + helpersUseOriginal = true; + MainApplication.getLayerManager().invalidateEditLayer(); + } + + @Override + public void doKeyReleased(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_WINDOWS) { + meta = false; + MainApplication.getLayerManager().invalidateEditLayer(); + return; + } + if (!helpersShortcut.isEvent(e)) return; + if (!isExpert) return; + resetTimer(); + long keyupTime = System.currentTimeMillis(); + if (keypressTime == 0) { // comes from enterMode + helpersEnabled = false; + } else if (keyupTime - keypressTime > longKeypressTime) { + helpersEnabled = helpersEnabledBeforeKeypressed; + } else { + helpersEnabled = !helpersEnabledBeforeKeypressed; + } + helpersUseOriginal = false; + MainApplication.getLayerManager().invalidateEditLayer(); + } + + protected void resetTimer() { + if (longKeypressTimer != null) { + try { + longKeypressTimer.cancel(); + longKeypressTimer.purge(); + } catch (IllegalStateException exception) { + Logging.debug(exception); + } + } + longKeypressTimer = new Timer(); + } + + @Override + public void expertChanged(boolean isExpert) { + this.isExpert = isExpert; + if (!isExpert && helpersEnabled) { + helpersEnabled = false; + MainApplication.getLayerManager().invalidateEditLayer(); + } + } + +}