[8378] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
[358] | 2 | package org.openstreetmap.josm.actions.mapmode;
|
---|
| 3 |
|
---|
[5098] | 4 | import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
|
---|
| 5 | import static org.openstreetmap.josm.tools.I18n.marktr;
|
---|
[358] | 6 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
[1459] | 7 | import static org.openstreetmap.josm.tools.I18n.trn;
|
---|
[358] | 8 |
|
---|
[5459] | 9 | import java.awt.BasicStroke;
|
---|
| 10 | import java.awt.Color;
|
---|
| 11 | import java.awt.Cursor;
|
---|
| 12 | import java.awt.Graphics2D;
|
---|
| 13 | import java.awt.Point;
|
---|
[4611] | 14 | import java.awt.event.ActionEvent;
|
---|
[358] | 15 | import java.awt.event.KeyEvent;
|
---|
| 16 | import java.awt.event.MouseEvent;
|
---|
| 17 | import java.util.ArrayList;
|
---|
| 18 | import java.util.Collection;
|
---|
| 19 | import java.util.Collections;
|
---|
| 20 | import java.util.HashMap;
|
---|
[359] | 21 | import java.util.HashSet;
|
---|
[389] | 22 | import java.util.Iterator;
|
---|
[608] | 23 | import java.util.LinkedList;
|
---|
| 24 | import java.util.List;
|
---|
[359] | 25 | import java.util.Map;
|
---|
[388] | 26 | import java.util.Set;
|
---|
[5098] | 27 |
|
---|
[5459] | 28 | import javax.swing.AbstractAction;
|
---|
| 29 | import javax.swing.JCheckBoxMenuItem;
|
---|
| 30 | import javax.swing.JMenuItem;
|
---|
| 31 | import javax.swing.JOptionPane;
|
---|
[13225] | 32 | import javax.swing.SwingUtilities;
|
---|
[358] | 33 |
|
---|
[4782] | 34 | import org.openstreetmap.josm.actions.JosmAction;
|
---|
[358] | 35 | import org.openstreetmap.josm.command.AddCommand;
|
---|
| 36 | import org.openstreetmap.josm.command.ChangeCommand;
|
---|
| 37 | import org.openstreetmap.josm.command.Command;
|
---|
| 38 | import org.openstreetmap.josm.command.SequenceCommand;
|
---|
[2450] | 39 | import org.openstreetmap.josm.data.Bounds;
|
---|
[14134] | 40 | import org.openstreetmap.josm.data.UndoRedoHandler;
|
---|
[358] | 41 | import org.openstreetmap.josm.data.coor.EastNorth;
|
---|
[12330] | 42 | import org.openstreetmap.josm.data.osm.DataSelectionListener;
|
---|
[608] | 43 | import org.openstreetmap.josm.data.osm.DataSet;
|
---|
[358] | 44 | import org.openstreetmap.josm.data.osm.Node;
|
---|
| 45 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
---|
| 46 | import org.openstreetmap.josm.data.osm.Way;
|
---|
| 47 | import org.openstreetmap.josm.data.osm.WaySegment;
|
---|
[12330] | 48 | import org.openstreetmap.josm.data.osm.event.SelectionEventManager;
|
---|
[10827] | 49 | import org.openstreetmap.josm.data.osm.visitor.paint.ArrowPaintHelper;
|
---|
[2666] | 50 | import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
|
---|
[12987] | 51 | import org.openstreetmap.josm.data.preferences.AbstractProperty;
|
---|
[10874] | 52 | import org.openstreetmap.josm.data.preferences.BooleanProperty;
|
---|
| 53 | import org.openstreetmap.josm.data.preferences.CachingProperty;
|
---|
| 54 | import org.openstreetmap.josm.data.preferences.DoubleProperty;
|
---|
[12987] | 55 | import org.openstreetmap.josm.data.preferences.NamedColorProperty;
|
---|
[10874] | 56 | import org.openstreetmap.josm.data.preferences.StrokeProperty;
|
---|
[12630] | 57 | import org.openstreetmap.josm.gui.MainApplication;
|
---|
[4782] | 58 | import org.openstreetmap.josm.gui.MainMenu;
|
---|
[12630] | 59 | import org.openstreetmap.josm.gui.MapFrame;
|
---|
[608] | 60 | import org.openstreetmap.josm.gui.MapView;
|
---|
[10827] | 61 | import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
|
---|
[5922] | 62 | import org.openstreetmap.josm.gui.NavigatableComponent;
|
---|
[10875] | 63 | import org.openstreetmap.josm.gui.draw.MapPath2D;
|
---|
[1405] | 64 | import org.openstreetmap.josm.gui.layer.Layer;
|
---|
[608] | 65 | import org.openstreetmap.josm.gui.layer.MapViewPaintable;
|
---|
[1379] | 66 | import org.openstreetmap.josm.gui.layer.OsmDataLayer;
|
---|
[7217] | 67 | import org.openstreetmap.josm.gui.util.KeyPressReleaseListener;
|
---|
[12517] | 68 | import org.openstreetmap.josm.gui.util.ModifierExListener;
|
---|
[5098] | 69 | import org.openstreetmap.josm.tools.Geometry;
|
---|
[358] | 70 | import org.openstreetmap.josm.tools.ImageProvider;
|
---|
[608] | 71 | import org.openstreetmap.josm.tools.Pair;
|
---|
[1084] | 72 | import org.openstreetmap.josm.tools.Shortcut;
|
---|
[4539] | 73 | import org.openstreetmap.josm.tools.Utils;
|
---|
[358] | 74 |
|
---|
| 75 | /**
|
---|
[4539] | 76 | * Mapmode to add nodes, create and extend ways.
|
---|
[1023] | 77 | */
|
---|
[12517] | 78 | public class DrawAction extends MapMode implements MapViewPaintable, DataSelectionListener, KeyPressReleaseListener, ModifierExListener {
|
---|
[8346] | 79 |
|
---|
[12182] | 80 | /**
|
---|
| 81 | * If this property is set, the draw action moves the viewport when adding new points.
|
---|
| 82 | * @since 12182
|
---|
| 83 | */
|
---|
| 84 | public static final CachingProperty<Boolean> VIEWPORT_FOLLOWING = new BooleanProperty("draw.viewport.following", false).cached();
|
---|
| 85 |
|
---|
[8510] | 86 | private static final Color ORANGE_TRANSPARENT = new Color(Color.ORANGE.getRed(), Color.ORANGE.getGreen(), Color.ORANGE.getBlue(), 128);
|
---|
[8346] | 87 |
|
---|
[12131] | 88 | private static final ArrowPaintHelper START_WAY_INDICATOR = new ArrowPaintHelper(Utils.toRadians(90), 8);
|
---|
[10827] | 89 |
|
---|
[11495] | 90 | static final CachingProperty<Boolean> USE_REPEATED_SHORTCUT
|
---|
[10874] | 91 | = new BooleanProperty("draw.anglesnap.toggleOnRepeatedA", true).cached();
|
---|
[11495] | 92 | static final CachingProperty<BasicStroke> RUBBER_LINE_STROKE
|
---|
[10874] | 93 | = new StrokeProperty("draw.stroke.helper-line", "3").cached();
|
---|
| 94 |
|
---|
[11495] | 95 | static final CachingProperty<BasicStroke> HIGHLIGHT_STROKE
|
---|
[10874] | 96 | = new StrokeProperty("draw.anglesnap.stroke.highlight", "10").cached();
|
---|
[11495] | 97 | static final CachingProperty<BasicStroke> HELPER_STROKE
|
---|
[10874] | 98 | = new StrokeProperty("draw.anglesnap.stroke.helper", "1 4").cached();
|
---|
| 99 |
|
---|
[11495] | 100 | static final CachingProperty<Double> SNAP_ANGLE_TOLERANCE
|
---|
[10874] | 101 | = new DoubleProperty("draw.anglesnap.tolerance", 5.0).cached();
|
---|
[11495] | 102 | static final CachingProperty<Boolean> DRAW_CONSTRUCTION_GEOMETRY
|
---|
[10874] | 103 | = new BooleanProperty("draw.anglesnap.drawConstructionGeometry", true).cached();
|
---|
[11495] | 104 | static final CachingProperty<Boolean> SHOW_PROJECTED_POINT
|
---|
[10874] | 105 | = new BooleanProperty("draw.anglesnap.drawProjectedPoint", true).cached();
|
---|
[11495] | 106 | static final CachingProperty<Boolean> SNAP_TO_PROJECTIONS
|
---|
[10874] | 107 | = new BooleanProperty("draw.anglesnap.projectionsnap", true).cached();
|
---|
| 108 |
|
---|
[11495] | 109 | static final CachingProperty<Boolean> SHOW_ANGLE
|
---|
[10874] | 110 | = new BooleanProperty("draw.anglesnap.showAngle", true).cached();
|
---|
| 111 |
|
---|
[11495] | 112 | static final CachingProperty<Color> SNAP_HELPER_COLOR
|
---|
[12987] | 113 | = new NamedColorProperty(marktr("draw angle snap"), Color.ORANGE).cached();
|
---|
[10874] | 114 |
|
---|
[11495] | 115 | static final CachingProperty<Color> HIGHLIGHT_COLOR
|
---|
[12987] | 116 | = new NamedColorProperty(marktr("draw angle snap highlight"), ORANGE_TRANSPARENT).cached();
|
---|
[10874] | 117 |
|
---|
[12987] | 118 | static final AbstractProperty<Color> RUBBER_LINE_COLOR
|
---|
[10874] | 119 | = PaintColors.SELECTED.getProperty().getChildColor(marktr("helper line"));
|
---|
| 120 |
|
---|
[11495] | 121 | static final CachingProperty<Boolean> DRAW_HELPER_LINE
|
---|
[10874] | 122 | = new BooleanProperty("draw.helper-line", true).cached();
|
---|
[11495] | 123 | static final CachingProperty<Boolean> DRAW_TARGET_HIGHLIGHT
|
---|
[10874] | 124 | = new BooleanProperty("draw.target-highlight", true).cached();
|
---|
[11495] | 125 | static final CachingProperty<Double> SNAP_TO_INTERSECTION_THRESHOLD
|
---|
[10874] | 126 | = new DoubleProperty("edit.snap-intersection-threshold", 10).cached();
|
---|
| 127 |
|
---|
[6889] | 128 | private final Cursor cursorJoinNode;
|
---|
| 129 | private final Cursor cursorJoinWay;
|
---|
[1459] | 130 |
|
---|
[8840] | 131 | private transient Node lastUsedNode;
|
---|
[5922] | 132 | private double toleranceMultiplier;
|
---|
[358] | 133 |
|
---|
[8308] | 134 | private transient Node mouseOnExistingNode;
|
---|
| 135 | private transient Set<Way> mouseOnExistingWays = new HashSet<>();
|
---|
[5098] | 136 | // old highlights store which primitives are currently highlighted. This
|
---|
| 137 | // is true, even if target highlighting is disabled since the status bar
|
---|
| 138 | // derives its information from this list as well.
|
---|
[8308] | 139 | private transient Set<OsmPrimitive> oldHighlights = new HashSet<>();
|
---|
[5098] | 140 | // new highlights contains a list of primitives that should be highlighted
|
---|
[10873] | 141 | // but haven't been so far. The idea is to compare old and new and only
|
---|
[5098] | 142 | // repaint if there are changes.
|
---|
[8308] | 143 | private transient Set<OsmPrimitive> newHighlights = new HashSet<>();
|
---|
[8840] | 144 | private boolean wayIsFinished;
|
---|
[1169] | 145 | private Point mousePos;
|
---|
[1412] | 146 | private Point oldMousePos;
|
---|
[1023] | 147 |
|
---|
[8308] | 148 | private transient Node currentBaseNode;
|
---|
| 149 | private transient Node previousNode;
|
---|
[1169] | 150 | private EastNorth currentMouseEastNorth;
|
---|
[1023] | 151 |
|
---|
[11495] | 152 | private final transient DrawSnapHelper snapHelper = new DrawSnapHelper(this);
|
---|
[4768] | 153 |
|
---|
[8308] | 154 | private final transient Shortcut backspaceShortcut;
|
---|
[7217] | 155 | private final BackSpaceAction backspaceAction;
|
---|
[8308] | 156 | private final transient Shortcut snappingShortcut;
|
---|
[7217] | 157 | private boolean ignoreNextKeyRelease;
|
---|
[4956] | 158 |
|
---|
[5459] | 159 | private final SnapChangeAction snapChangeAction;
|
---|
| 160 | private final JCheckBoxMenuItem snapCheckboxMenuItem;
|
---|
[5739] | 161 | private static final BasicStroke BASIC_STROKE = new BasicStroke(1);
|
---|
[6069] | 162 |
|
---|
[10874] | 163 | private Point rightClickPressPos;
|
---|
[7227] | 164 |
|
---|
[8346] | 165 | /**
|
---|
| 166 | * Constructs a new {@code DrawAction}.
|
---|
[11713] | 167 | * @since 11713
|
---|
[8346] | 168 | */
|
---|
[11713] | 169 | public DrawAction() {
|
---|
[1169] | 170 | super(tr("Draw"), "node/autonode", tr("Draw nodes"),
|
---|
[5512] | 171 | Shortcut.registerShortcut("mapmode:draw", tr("Mode: {0}", tr("Draw")), KeyEvent.VK_A, Shortcut.DIRECT),
|
---|
[11713] | 172 | ImageProvider.getCursor("crosshair", null));
|
---|
[400] | 173 |
|
---|
[4956] | 174 | snappingShortcut = Shortcut.registerShortcut("mapmode:drawanglesnapping",
|
---|
[5979] | 175 | tr("Mode: Draw Angle snapping"), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE);
|
---|
[5459] | 176 | snapChangeAction = new SnapChangeAction();
|
---|
| 177 | snapCheckboxMenuItem = addMenuItem();
|
---|
[4782] | 178 | snapHelper.setMenuCheckBox(snapCheckboxMenuItem);
|
---|
[5512] | 179 | backspaceShortcut = Shortcut.registerShortcut("mapmode:backspace",
|
---|
| 180 | tr("Backspace in Add mode"), KeyEvent.VK_BACK_SPACE, Shortcut.DIRECT);
|
---|
| 181 | backspaceAction = new BackSpaceAction();
|
---|
[1444] | 182 | cursorJoinNode = ImageProvider.getCursor("crosshair", "joinnode");
|
---|
| 183 | cursorJoinWay = ImageProvider.getCursor("crosshair", "joinway");
|
---|
[7227] | 184 |
|
---|
| 185 | snapHelper.init();
|
---|
[1169] | 186 | }
|
---|
[358] | 187 |
|
---|
[5459] | 188 | private JCheckBoxMenuItem addMenuItem() {
|
---|
[12643] | 189 | int n = MainApplication.getMenu().editMenu.getItemCount();
|
---|
[8510] | 190 | for (int i = n-1; i > 0; i--) {
|
---|
[12643] | 191 | JMenuItem item = MainApplication.getMenu().editMenu.getItem(i);
|
---|
[8510] | 192 | if (item != null && item.getAction() != null && item.getAction() instanceof SnapChangeAction) {
|
---|
[12643] | 193 | MainApplication.getMenu().editMenu.remove(i);
|
---|
[4924] | 194 | }
|
---|
| 195 | }
|
---|
[12643] | 196 | return MainMenu.addWithCheckbox(MainApplication.getMenu().editMenu, snapChangeAction, MainMenu.WINDOW_MENU_GROUP.VOLATILE);
|
---|
[4924] | 197 | }
|
---|
| 198 |
|
---|
[1409] | 199 | /**
|
---|
[8931] | 200 | * Checks if a map redraw is required and does so if needed. Also updates the status bar.
|
---|
[13309] | 201 | * @param e event, can be null
|
---|
[8931] | 202 | * @return true if a repaint is needed
|
---|
[1458] | 203 | */
|
---|
[13309] | 204 | private boolean redrawIfRequired(Object e) {
|
---|
[1458] | 205 | updateStatusLine();
|
---|
[5098] | 206 | // repaint required if the helper line is active.
|
---|
[10874] | 207 | boolean needsRepaint = DRAW_HELPER_LINE.get() && !wayIsFinished;
|
---|
| 208 | if (DRAW_TARGET_HIGHLIGHT.get()) {
|
---|
[5104] | 209 | // move newHighlights to oldHighlights; only update changed primitives
|
---|
[8510] | 210 | for (OsmPrimitive x : newHighlights) {
|
---|
| 211 | if (oldHighlights.contains(x)) {
|
---|
[5104] | 212 | continue;
|
---|
| 213 | }
|
---|
| 214 | x.setHighlighted(true);
|
---|
| 215 | needsRepaint = true;
|
---|
[5098] | 216 | }
|
---|
[5104] | 217 | oldHighlights.removeAll(newHighlights);
|
---|
[8510] | 218 | for (OsmPrimitive x : oldHighlights) {
|
---|
[5104] | 219 | x.setHighlighted(false);
|
---|
| 220 | needsRepaint = true;
|
---|
| 221 | }
|
---|
[5098] | 222 | }
|
---|
[5104] | 223 | // required in order to print correct help text
|
---|
[5098] | 224 | oldHighlights = newHighlights;
|
---|
| 225 |
|
---|
[10874] | 226 | if (!needsRepaint && !DRAW_TARGET_HIGHLIGHT.get())
|
---|
[5100] | 227 | return false;
|
---|
[5098] | 228 |
|
---|
[4539] | 229 | // update selection to reflect which way being modified
|
---|
[10467] | 230 | OsmDataLayer editLayer = getLayerManager().getEditLayer();
|
---|
[12836] | 231 | Node baseNode = getCurrentBaseNode();
|
---|
| 232 | if (editLayer != null && baseNode != null && !editLayer.data.selectionEmpty()) {
|
---|
[13614] | 233 | DataSet currentDataSet = editLayer.getDataSet();
|
---|
[12836] | 234 | Way continueFrom = getWayForNode(baseNode);
|
---|
| 235 | if (alt && continueFrom != null && (!baseNode.isSelected() || continueFrom.isSelected())) {
|
---|
| 236 | addRemoveSelection(currentDataSet, baseNode, continueFrom);
|
---|
[5098] | 237 | needsRepaint = true;
|
---|
| 238 | } else if (!alt && continueFrom != null && !continueFrom.isSelected()) {
|
---|
[13309] | 239 | addSelection(currentDataSet, continueFrom);
|
---|
[5098] | 240 | needsRepaint = true;
|
---|
[4539] | 241 | }
|
---|
| 242 | }
|
---|
[5098] | 243 |
|
---|
[13309] | 244 | if (!needsRepaint && e instanceof SelectionChangeEvent) {
|
---|
| 245 | SelectionChangeEvent event = (SelectionChangeEvent) e;
|
---|
| 246 | needsRepaint = !event.getOldSelection().isEmpty() && event.getSelection().isEmpty();
|
---|
| 247 | }
|
---|
| 248 |
|
---|
[10467] | 249 | if (needsRepaint && editLayer != null) {
|
---|
| 250 | editLayer.invalidate();
|
---|
[5098] | 251 | }
|
---|
[5100] | 252 | return needsRepaint;
|
---|
[1409] | 253 | }
|
---|
| 254 |
|
---|
[6542] | 255 | private static void addRemoveSelection(DataSet ds, OsmPrimitive toAdd, OsmPrimitive toRemove) {
|
---|
| 256 | ds.beginUpdate(); // to prevent the selection listener to screw around with the state
|
---|
[12064] | 257 | try {
|
---|
[13309] | 258 | addSelection(ds, toAdd);
|
---|
| 259 | clearSelection(ds, toRemove);
|
---|
[12064] | 260 | } finally {
|
---|
| 261 | ds.endUpdate();
|
---|
| 262 | }
|
---|
[6542] | 263 | }
|
---|
| 264 |
|
---|
[13309] | 265 | private static void updatePreservedFlag(OsmPrimitive osm, boolean state) {
|
---|
| 266 | // Preserves selected primitives and selected way nodes
|
---|
| 267 | osm.setPreserved(state);
|
---|
| 268 | if (osm instanceof Way) {
|
---|
| 269 | for (Node n : ((Way) osm).getNodes()) {
|
---|
| 270 | n.setPreserved(state);
|
---|
| 271 | }
|
---|
| 272 | }
|
---|
| 273 | }
|
---|
| 274 |
|
---|
| 275 | private static void setSelection(DataSet ds, Collection<OsmPrimitive> toSet) {
|
---|
| 276 | toSet.stream().forEach(x -> updatePreservedFlag(x, true));
|
---|
| 277 | ds.setSelected(toSet);
|
---|
| 278 | }
|
---|
| 279 |
|
---|
| 280 | private static void setSelection(DataSet ds, OsmPrimitive toSet) {
|
---|
| 281 | updatePreservedFlag(toSet, true);
|
---|
| 282 | ds.setSelected(toSet);
|
---|
| 283 | }
|
---|
| 284 |
|
---|
| 285 | private static void addSelection(DataSet ds, OsmPrimitive toAdd) {
|
---|
| 286 | updatePreservedFlag(toAdd, true);
|
---|
| 287 | ds.addSelected(toAdd);
|
---|
| 288 | }
|
---|
| 289 |
|
---|
| 290 | private static void clearSelection(DataSet ds, OsmPrimitive toRemove) {
|
---|
| 291 | ds.clearSelection(toRemove);
|
---|
| 292 | updatePreservedFlag(toRemove, false);
|
---|
| 293 | }
|
---|
| 294 |
|
---|
[4956] | 295 | @Override
|
---|
| 296 | public void enterMode() {
|
---|
[1821] | 297 | if (!isEnabled())
|
---|
| 298 | return;
|
---|
[1169] | 299 | super.enterMode();
|
---|
[7227] | 300 | readPreferences();
|
---|
[6069] | 301 |
|
---|
[5106] | 302 | // determine if selection is suitable to continue drawing. If it
|
---|
| 303 | // isn't, set wayIsFinished to true to avoid superfluous repaints.
|
---|
[10382] | 304 | determineCurrentBaseNodeAndPreviousNode(getLayerManager().getEditDataSet().getSelected());
|
---|
[8064] | 305 | wayIsFinished = getCurrentBaseNode() == null;
|
---|
[5106] | 306 |
|
---|
[5922] | 307 | toleranceMultiplier = 0.01 * NavigatableComponent.PROP_SNAP_DISTANCE.get();
|
---|
| 308 |
|
---|
[4768] | 309 | snapHelper.init();
|
---|
[4782] | 310 | snapCheckboxMenuItem.getAction().setEnabled(true);
|
---|
[4956] | 311 |
|
---|
[12630] | 312 | MapFrame map = MainApplication.getMap();
|
---|
| 313 | map.statusLine.getAnglePanel().addMouseListener(snapHelper.anglePopupListener);
|
---|
[12639] | 314 | MainApplication.registerActionShortcut(backspaceAction, backspaceShortcut);
|
---|
[1409] | 315 |
|
---|
[12630] | 316 | map.mapView.addMouseListener(this);
|
---|
| 317 | map.mapView.addMouseMotionListener(this);
|
---|
| 318 | map.mapView.addTemporaryLayer(this);
|
---|
[12330] | 319 | SelectionEventManager.getInstance().addSelectionListenerForEdt(this);
|
---|
[1409] | 320 |
|
---|
[12630] | 321 | map.keyDetector.addKeyListener(this);
|
---|
| 322 | map.keyDetector.addModifierExListener(this);
|
---|
[7217] | 323 | ignoreNextKeyRelease = true;
|
---|
[1169] | 324 | }
|
---|
[1409] | 325 |
|
---|
[9572] | 326 | @Override
|
---|
[4956] | 327 | public void exitMode() {
|
---|
[1169] | 328 | super.exitMode();
|
---|
[12630] | 329 | MapFrame map = MainApplication.getMap();
|
---|
| 330 | map.mapView.removeMouseListener(this);
|
---|
| 331 | map.mapView.removeMouseMotionListener(this);
|
---|
| 332 | map.mapView.removeTemporaryLayer(this);
|
---|
[12330] | 333 | SelectionEventManager.getInstance().removeSelectionListener(this);
|
---|
[12639] | 334 | MainApplication.unregisterActionShortcut(backspaceAction, backspaceShortcut);
|
---|
[4776] | 335 | snapHelper.unsetFixedMode();
|
---|
[4782] | 336 | snapCheckboxMenuItem.getAction().setEnabled(false);
|
---|
[5512] | 337 |
|
---|
[12630] | 338 | map.statusLine.getAnglePanel().removeMouseListener(snapHelper.anglePopupListener);
|
---|
| 339 | map.statusLine.activateAnglePanel(false);
|
---|
[4956] | 340 |
|
---|
[13309] | 341 | DataSet ds = getLayerManager().getEditDataSet();
|
---|
| 342 | if (ds != null) {
|
---|
| 343 | ds.getSelected().stream().forEach(x -> updatePreservedFlag(x, false));
|
---|
| 344 | }
|
---|
| 345 |
|
---|
| 346 | removeHighlighting(null);
|
---|
[12630] | 347 | map.keyDetector.removeKeyListener(this);
|
---|
| 348 | map.keyDetector.removeModifierExListener(this);
|
---|
[1169] | 349 | }
|
---|
[1023] | 350 |
|
---|
[1169] | 351 | /**
|
---|
| 352 | * redraw to (possibly) get rid of helper line if selection changes.
|
---|
| 353 | */
|
---|
[5922] | 354 | @Override
|
---|
[12517] | 355 | public void modifiersExChanged(int modifiers) {
|
---|
[12630] | 356 | if (!MainApplication.isDisplayingMapView() || !MainApplication.getMap().mapView.isActiveLayerDrawable())
|
---|
[1169] | 357 | return;
|
---|
[12517] | 358 | updateKeyModifiersEx(modifiers);
|
---|
[1444] | 359 | computeHelperLine();
|
---|
[13309] | 360 | addHighlighting(null);
|
---|
[1169] | 361 | }
|
---|
[4956] | 362 |
|
---|
[7217] | 363 | @Override
|
---|
| 364 | public void doKeyPressed(KeyEvent e) {
|
---|
[10874] | 365 | if (!snappingShortcut.isEvent(e) && !(USE_REPEATED_SHORTCUT.get() && getShortcut().isEvent(e)))
|
---|
[4956] | 366 | return;
|
---|
[4782] | 367 | snapHelper.setFixedMode();
|
---|
[4956] | 368 | computeHelperLine();
|
---|
[13309] | 369 | redrawIfRequired(e);
|
---|
[4782] | 370 | }
|
---|
[7217] | 371 |
|
---|
| 372 | @Override
|
---|
| 373 | public void doKeyReleased(KeyEvent e) {
|
---|
[10874] | 374 | if (!snappingShortcut.isEvent(e) && !(USE_REPEATED_SHORTCUT.get() && getShortcut().isEvent(e)))
|
---|
[7217] | 375 | return;
|
---|
| 376 | if (ignoreNextKeyRelease) {
|
---|
| 377 | ignoreNextKeyRelease = false;
|
---|
| 378 | return;
|
---|
| 379 | }
|
---|
[4782] | 380 | snapHelper.unFixOrTurnOff();
|
---|
[4956] | 381 | computeHelperLine();
|
---|
[13309] | 382 | redrawIfRequired(e);
|
---|
[4782] | 383 | }
|
---|
| 384 |
|
---|
[1169] | 385 | /**
|
---|
| 386 | * redraw to (possibly) get rid of helper line if selection changes.
|
---|
| 387 | */
|
---|
[5922] | 388 | @Override
|
---|
[12330] | 389 | public void selectionChanged(SelectionChangeEvent event) {
|
---|
[12630] | 390 | if (!MainApplication.getMap().mapView.isActiveLayerDrawable())
|
---|
[1169] | 391 | return;
|
---|
[14510] | 392 | if (event.getSelection().isEmpty())
|
---|
| 393 | finishDrawing();
|
---|
[13225] | 394 | // Make sure helper line is computed later (causes deadlock in selection event chain otherwise)
|
---|
| 395 | SwingUtilities.invokeLater(() -> {
|
---|
[13309] | 396 | event.getOldSelection().stream().forEach(x -> updatePreservedFlag(x, false));
|
---|
| 397 | event.getSelection().stream().forEach(x -> updatePreservedFlag(x, true));
|
---|
[13334] | 398 | if (MainApplication.getMap() != null) {
|
---|
| 399 | computeHelperLine();
|
---|
| 400 | addHighlighting(event);
|
---|
| 401 | }
|
---|
[13225] | 402 | });
|
---|
[1169] | 403 | }
|
---|
[358] | 404 |
|
---|
[1400] | 405 | private void tryAgain(MouseEvent e) {
|
---|
[13309] | 406 | getLayerManager().getEditDataSet().clearSelection();
|
---|
[2692] | 407 | mouseReleased(e);
|
---|
[1400] | 408 | }
|
---|
[1409] | 409 |
|
---|
[1169] | 410 | /**
|
---|
[1438] | 411 | * This function should be called when the user wishes to finish his current draw action.
|
---|
| 412 | * If Potlatch Style is enabled, it will switch to select tool, otherwise simply disable
|
---|
| 413 | * the helper line until the user chooses to draw something else.
|
---|
| 414 | */
|
---|
| 415 | private void finishDrawing() {
|
---|
| 416 | lastUsedNode = null;
|
---|
| 417 | wayIsFinished = true;
|
---|
[12630] | 418 | MainApplication.getMap().selectSelectTool(true);
|
---|
[4768] | 419 | snapHelper.noSnapNow();
|
---|
[4956] | 420 |
|
---|
[1438] | 421 | // Redraw to remove the helper line stub
|
---|
[1444] | 422 | computeHelperLine();
|
---|
[13309] | 423 | removeHighlighting(null);
|
---|
[1438] | 424 | }
|
---|
| 425 |
|
---|
[4768] | 426 | @Override
|
---|
| 427 | public void mousePressed(MouseEvent e) {
|
---|
| 428 | if (e.getButton() == MouseEvent.BUTTON3) {
|
---|
| 429 | rightClickPressPos = e.getPoint();
|
---|
| 430 | }
|
---|
| 431 | }
|
---|
[4956] | 432 |
|
---|
[1438] | 433 | /**
|
---|
[1169] | 434 | * If user clicked with the left button, add a node at the current mouse
|
---|
| 435 | * position.
|
---|
| 436 | *
|
---|
| 437 | * If in nodeway mode, insert the node into the way.
|
---|
| 438 | */
|
---|
[7217] | 439 | @Override
|
---|
| 440 | public void mouseReleased(MouseEvent e) {
|
---|
[4768] | 441 | if (e.getButton() == MouseEvent.BUTTON3) {
|
---|
| 442 | Point curMousePos = e.getPoint();
|
---|
| 443 | if (curMousePos.equals(rightClickPressPos)) {
|
---|
[5555] | 444 | tryToSetBaseSegmentForAngleSnap();
|
---|
[4768] | 445 | }
|
---|
| 446 | return;
|
---|
| 447 | }
|
---|
[1400] | 448 | if (e.getButton() != MouseEvent.BUTTON1)
|
---|
[1169] | 449 | return;
|
---|
[12630] | 450 | MapView mapView = MainApplication.getMap().mapView;
|
---|
| 451 | if (!mapView.isActiveLayerDrawable())
|
---|
[1169] | 452 | return;
|
---|
[1935] | 453 | // request focus in order to enable the expected keyboard shortcuts
|
---|
| 454 | //
|
---|
[12630] | 455 | mapView.requestFocus();
|
---|
[4956] | 456 |
|
---|
[8510] | 457 | if (e.getClickCount() > 1 && mousePos != null && mousePos.equals(oldMousePos)) {
|
---|
[1400] | 458 | // A double click equals "user clicked last node again, finish way"
|
---|
[1412] | 459 | // Change draw tool only if mouse position is nearly the same, as
|
---|
| 460 | // otherwise fast clicks will count as a double click
|
---|
[1438] | 461 | finishDrawing();
|
---|
[1400] | 462 | return;
|
---|
| 463 | }
|
---|
[1412] | 464 | oldMousePos = mousePos;
|
---|
[4956] | 465 |
|
---|
[1169] | 466 | // we copy ctrl/alt/shift from the event just in case our global
|
---|
[7217] | 467 | // keyDetector didn't make it through the security manager. Unclear
|
---|
[1169] | 468 | // if that can ever happen but better be safe.
|
---|
[1438] | 469 | updateKeyModifiers(e);
|
---|
[1169] | 470 | mousePos = e.getPoint();
|
---|
[1023] | 471 |
|
---|
[10382] | 472 | DataSet ds = getLayerManager().getEditDataSet();
|
---|
[7005] | 473 | Collection<OsmPrimitive> selection = new ArrayList<>(ds.getSelected());
|
---|
[358] | 474 |
|
---|
[1169] | 475 | boolean newNode = false;
|
---|
[12630] | 476 | Node n = mapView.getNearestNode(mousePos, OsmPrimitive::isSelectable);
|
---|
[6027] | 477 | if (ctrl) {
|
---|
[10382] | 478 | Iterator<Way> it = ds.getSelectedWays().iterator();
|
---|
[6027] | 479 | if (it.hasNext()) {
|
---|
| 480 | // ctrl-click on node of selected way = reuse node despite of ctrl
|
---|
| 481 | if (!it.next().containsNode(n)) n = null;
|
---|
| 482 | } else {
|
---|
[8510] | 483 | n = null; // ctrl-click + no selected way = new node
|
---|
[6027] | 484 | }
|
---|
[1814] | 485 | }
|
---|
[1409] | 486 |
|
---|
[4768] | 487 | if (n != null && !snapHelper.isActive()) {
|
---|
[1169] | 488 | // user clicked on node
|
---|
[2009] | 489 | if (selection.isEmpty() || wayIsFinished) {
|
---|
[1169] | 490 | // select the clicked node and do nothing else
|
---|
| 491 | // (this is just a convenience option so that people don't
|
---|
| 492 | // have to switch modes)
|
---|
[4539] | 493 |
|
---|
[13309] | 494 | setSelection(ds, n);
|
---|
[4539] | 495 | // If we extend/continue an existing way, select it already now to make it obvious
|
---|
| 496 | Way continueFrom = getWayForNode(n);
|
---|
| 497 | if (continueFrom != null) {
|
---|
[13309] | 498 | addSelection(ds, continueFrom);
|
---|
[4539] | 499 | }
|
---|
| 500 |
|
---|
[1517] | 501 | // The user explicitly selected a node, so let him continue drawing
|
---|
| 502 | wayIsFinished = false;
|
---|
[1169] | 503 | return;
|
---|
| 504 | }
|
---|
| 505 | } else {
|
---|
[4768] | 506 | EastNorth newEN;
|
---|
[8510] | 507 | if (n != null) {
|
---|
[4768] | 508 | EastNorth foundPoint = n.getEastNorth();
|
---|
| 509 | // project found node to snapping line
|
---|
[4956] | 510 | newEN = snapHelper.getSnapPoint(foundPoint);
|
---|
[5922] | 511 | // do not add new node if there is some node within snapping distance
|
---|
[12630] | 512 | double tolerance = mapView.getDist100Pixel() * toleranceMultiplier;
|
---|
[5922] | 513 | if (foundPoint.distance(newEN) > tolerance) {
|
---|
[4768] | 514 | n = new Node(newEN); // point != projected, so we create new node
|
---|
| 515 | newNode = true;
|
---|
| 516 | }
|
---|
| 517 | } else { // n==null, no node found in clicked area
|
---|
[12630] | 518 | EastNorth mouseEN = mapView.getEastNorth(e.getX(), e.getY());
|
---|
[4782] | 519 | newEN = snapHelper.isSnapOn() ? snapHelper.getSnapPoint(mouseEN) : mouseEN;
|
---|
[4768] | 520 | n = new Node(newEN); //create node at clicked point
|
---|
| 521 | newNode = true;
|
---|
| 522 | }
|
---|
| 523 | snapHelper.unsetFixedMode();
|
---|
| 524 | }
|
---|
| 525 |
|
---|
[9062] | 526 | Collection<Command> cmds = new LinkedList<>();
|
---|
| 527 | Collection<OsmPrimitive> newSelection = new LinkedList<>(ds.getSelected());
|
---|
| 528 | List<Way> reuseWays = new ArrayList<>();
|
---|
| 529 | List<Way> replacedWays = new ArrayList<>();
|
---|
| 530 |
|
---|
[4768] | 531 | if (newNode) {
|
---|
[1640] | 532 | if (n.getCoor().isOutSideWorld()) {
|
---|
[2009] | 533 | JOptionPane.showMessageDialog(
|
---|
[14153] | 534 | MainApplication.getMainFrame(),
|
---|
[5512] | 535 | tr("Cannot add a node outside of the world."),
|
---|
| 536 | tr("Warning"),
|
---|
| 537 | JOptionPane.WARNING_MESSAGE
|
---|
| 538 | );
|
---|
[1169] | 539 | return;
|
---|
| 540 | }
|
---|
[12726] | 541 | cmds.add(new AddCommand(ds, n));
|
---|
[358] | 542 |
|
---|
[1169] | 543 | if (!ctrl) {
|
---|
[4956] | 544 | // Insert the node into all the nearby way segments
|
---|
[12630] | 545 | List<WaySegment> wss = mapView.getNearestWaySegments(
|
---|
| 546 | mapView.getPoint(n), OsmPrimitive::isSelectable);
|
---|
[4956] | 547 | if (snapHelper.isActive()) {
|
---|
[8510] | 548 | tryToMoveNodeOnIntersection(wss, n);
|
---|
[4956] | 549 | }
|
---|
| 550 | insertNodeIntoAllNearbySegments(wss, n, newSelection, cmds, replacedWays, reuseWays);
|
---|
| 551 | }
|
---|
[1169] | 552 | }
|
---|
[4768] | 553 | // now "n" is newly created or reused node that shoud be added to some way
|
---|
[4956] | 554 |
|
---|
[8510] | 555 | // This part decides whether or not a "segment" (i.e. a connection) is made to an existing node.
|
---|
[1023] | 556 |
|
---|
[1169] | 557 | // For a connection to be made, the user must either have a node selected (connection
|
---|
| 558 | // is made to that node), or he must have a way selected *and* one of the endpoints
|
---|
| 559 | // of that way must be the last used node (connection is made to last used node), or
|
---|
| 560 | // he must have a way and a node selected (connection is made to the selected node).
|
---|
[1023] | 561 |
|
---|
[1400] | 562 | // If the above does not apply, the selection is cleared and a new try is started
|
---|
[1677] | 563 |
|
---|
[1169] | 564 | boolean extendedWay = false;
|
---|
[1405] | 565 | boolean wayIsFinishedTemp = wayIsFinished;
|
---|
| 566 | wayIsFinished = false;
|
---|
[1677] | 567 |
|
---|
[1545] | 568 | // don't draw lines if shift is held
|
---|
[6093] | 569 | if (!selection.isEmpty() && !shift) {
|
---|
[1169] | 570 | Node selectedNode = null;
|
---|
| 571 | Way selectedWay = null;
|
---|
[1409] | 572 |
|
---|
[1169] | 573 | for (OsmPrimitive p : selection) {
|
---|
| 574 | if (p instanceof Node) {
|
---|
[1400] | 575 | if (selectedNode != null) {
|
---|
| 576 | // Too many nodes selected to do something useful
|
---|
| 577 | tryAgain(e);
|
---|
| 578 | return;
|
---|
| 579 | }
|
---|
[1169] | 580 | selectedNode = (Node) p;
|
---|
| 581 | } else if (p instanceof Way) {
|
---|
[1400] | 582 | if (selectedWay != null) {
|
---|
| 583 | // Too many ways selected to do something useful
|
---|
| 584 | tryAgain(e);
|
---|
| 585 | return;
|
---|
| 586 | }
|
---|
[1169] | 587 | selectedWay = (Way) p;
|
---|
| 588 | }
|
---|
| 589 | }
|
---|
[1409] | 590 |
|
---|
[1438] | 591 | // the node from which we make a connection
|
---|
| 592 | Node n0 = findNodeToContinueFrom(selectedNode, selectedWay);
|
---|
| 593 | // We have a selection but it isn't suitable. Try again.
|
---|
[8510] | 594 | if (n0 == null) {
|
---|
[1400] | 595 | tryAgain(e);
|
---|
| 596 | return;
|
---|
| 597 | }
|
---|
[8510] | 598 | if (!wayIsFinishedTemp) {
|
---|
| 599 | if (isSelfContainedWay(selectedWay, n0, n))
|
---|
[1503] | 600 | return;
|
---|
[1023] | 601 |
|
---|
[1503] | 602 | // User clicked last node again, finish way
|
---|
[10662] | 603 | if (n0 == n) {
|
---|
[1503] | 604 | finishDrawing();
|
---|
| 605 | return;
|
---|
| 606 | }
|
---|
[1023] | 607 |
|
---|
[1503] | 608 | // Ok we know now that we'll insert a line segment, but will it connect to an
|
---|
| 609 | // existing way or make a new way of its own? The "alt" modifier means that the
|
---|
| 610 | // user wants a new way.
|
---|
[9968] | 611 | Way way = alt ? null : (selectedWay != null ? selectedWay : getWayForNode(n0));
|
---|
[2339] | 612 | Way wayToSelect;
|
---|
[358] | 613 |
|
---|
[1503] | 614 | // Don't allow creation of self-overlapping ways
|
---|
[8510] | 615 | if (way != null) {
|
---|
| 616 | int nodeCount = 0;
|
---|
[8513] | 617 | for (Node p : way.getNodes()) {
|
---|
[8510] | 618 | if (p.equals(n0)) {
|
---|
[1814] | 619 | nodeCount++;
|
---|
| 620 | }
|
---|
[8513] | 621 | }
|
---|
[8510] | 622 | if (nodeCount > 1) {
|
---|
[1814] | 623 | way = null;
|
---|
| 624 | }
|
---|
[1503] | 625 | }
|
---|
[1023] | 626 |
|
---|
[1503] | 627 | if (way == null) {
|
---|
| 628 | way = new Way();
|
---|
| 629 | way.addNode(n0);
|
---|
[12726] | 630 | cmds.add(new AddCommand(ds, way));
|
---|
[2339] | 631 | wayToSelect = way;
|
---|
[1169] | 632 | } else {
|
---|
[1503] | 633 | int i;
|
---|
| 634 | if ((i = replacedWays.indexOf(way)) != -1) {
|
---|
| 635 | way = reuseWays.get(i);
|
---|
[2339] | 636 | wayToSelect = way;
|
---|
[1503] | 637 | } else {
|
---|
[2339] | 638 | wayToSelect = way;
|
---|
[1503] | 639 | Way wnew = new Way(way);
|
---|
| 640 | cmds.add(new ChangeCommand(way, wnew));
|
---|
| 641 | way = wnew;
|
---|
| 642 | }
|
---|
[1169] | 643 | }
|
---|
[358] | 644 |
|
---|
[1503] | 645 | // Connected to a node that's already in the way
|
---|
[8510] | 646 | if (way.containsNode(n)) {
|
---|
[1503] | 647 | wayIsFinished = true;
|
---|
| 648 | selection.clear();
|
---|
| 649 | }
|
---|
[1313] | 650 |
|
---|
[1503] | 651 | // Add new node to way
|
---|
[10662] | 652 | if (way.getNode(way.getNodesCount() - 1) == n0) {
|
---|
[1503] | 653 | way.addNode(n);
|
---|
[1814] | 654 | } else {
|
---|
[1503] | 655 | way.addNode(0, n);
|
---|
[1814] | 656 | }
|
---|
[358] | 657 |
|
---|
[1503] | 658 | extendedWay = true;
|
---|
[2351] | 659 | newSelection.clear();
|
---|
| 660 | newSelection.add(wayToSelect);
|
---|
[1503] | 661 | }
|
---|
[1169] | 662 | }
|
---|
[10874] | 663 | if (!extendedWay && !newNode) {
|
---|
| 664 | return; // We didn't do anything.
|
---|
| 665 | }
|
---|
[1409] | 666 |
|
---|
[10874] | 667 | String title = getTitle(newNode, n, newSelection, reuseWays, extendedWay);
|
---|
| 668 |
|
---|
| 669 | Command c = new SequenceCommand(title, cmds);
|
---|
| 670 |
|
---|
[14134] | 671 | UndoRedoHandler.getInstance().add(c);
|
---|
[10874] | 672 | if (!wayIsFinished) {
|
---|
| 673 | lastUsedNode = n;
|
---|
| 674 | }
|
---|
| 675 |
|
---|
[13309] | 676 | setSelection(ds, newSelection);
|
---|
[10874] | 677 |
|
---|
| 678 | // "viewport following" mode for tracing long features
|
---|
| 679 | // from aerial imagery or GPS tracks.
|
---|
[12182] | 680 | if (VIEWPORT_FOLLOWING.get()) {
|
---|
[12630] | 681 | mapView.smoothScrollTo(n.getEastNorth());
|
---|
[10874] | 682 | }
|
---|
| 683 | computeHelperLine();
|
---|
[13309] | 684 | removeHighlighting(e);
|
---|
[10874] | 685 | }
|
---|
| 686 |
|
---|
[10936] | 687 | private static String getTitle(boolean newNode, Node n, Collection<OsmPrimitive> newSelection, List<Way> reuseWays,
|
---|
[10874] | 688 | boolean extendedWay) {
|
---|
[1169] | 689 | String title;
|
---|
[1313] | 690 | if (!extendedWay) {
|
---|
[10874] | 691 | if (reuseWays.isEmpty()) {
|
---|
[1169] | 692 | title = tr("Add node");
|
---|
[1400] | 693 | } else {
|
---|
[1169] | 694 | title = tr("Add node into way");
|
---|
[1814] | 695 | for (Way w : reuseWays) {
|
---|
[2339] | 696 | newSelection.remove(w);
|
---|
[1814] | 697 | }
|
---|
[1400] | 698 | }
|
---|
[2558] | 699 | newSelection.clear();
|
---|
[2351] | 700 | newSelection.add(n);
|
---|
[1169] | 701 | } else if (!newNode) {
|
---|
| 702 | title = tr("Connect existing way to node");
|
---|
| 703 | } else if (reuseWays.isEmpty()) {
|
---|
| 704 | title = tr("Add a new node to an existing way");
|
---|
| 705 | } else {
|
---|
| 706 | title = tr("Add node into way and connect");
|
---|
| 707 | }
|
---|
[10874] | 708 | return title;
|
---|
[1169] | 709 | }
|
---|
[4956] | 710 |
|
---|
[8509] | 711 | private void insertNodeIntoAllNearbySegments(List<WaySegment> wss, Node n, Collection<OsmPrimitive> newSelection,
|
---|
| 712 | Collection<Command> cmds, List<Way> replacedWays, List<Way> reuseWays) {
|
---|
[7005] | 713 | Map<Way, List<Integer>> insertPoints = new HashMap<>();
|
---|
[4768] | 714 | for (WaySegment ws : wss) {
|
---|
| 715 | List<Integer> is;
|
---|
| 716 | if (insertPoints.containsKey(ws.way)) {
|
---|
| 717 | is = insertPoints.get(ws.way);
|
---|
| 718 | } else {
|
---|
[7005] | 719 | is = new ArrayList<>();
|
---|
[4768] | 720 | insertPoints.put(ws.way, is);
|
---|
| 721 | }
|
---|
[1023] | 722 |
|
---|
[4768] | 723 | is.add(ws.lowerIndex);
|
---|
| 724 | }
|
---|
| 725 |
|
---|
[8510] | 726 | Set<Pair<Node, Node>> segSet = new HashSet<>();
|
---|
[4768] | 727 |
|
---|
| 728 | for (Map.Entry<Way, List<Integer>> insertPoint : insertPoints.entrySet()) {
|
---|
| 729 | Way w = insertPoint.getKey();
|
---|
| 730 | List<Integer> is = insertPoint.getValue();
|
---|
| 731 |
|
---|
| 732 | Way wnew = new Way(w);
|
---|
| 733 |
|
---|
| 734 | pruneSuccsAndReverse(is);
|
---|
| 735 | for (int i : is) {
|
---|
[7005] | 736 | segSet.add(Pair.sort(new Pair<>(w.getNode(i), w.getNode(i+1))));
|
---|
[4768] | 737 | wnew.addNode(i + 1, n);
|
---|
| 738 | }
|
---|
| 739 |
|
---|
| 740 | // If ALT is pressed, a new way should be created and that new way should get
|
---|
[14273] | 741 | // selected. This works every time unless the ways the nodes get inserted into
|
---|
[4768] | 742 | // are already selected. This is the case when creating a self-overlapping way
|
---|
| 743 | // but pressing ALT prevents this. Therefore we must de-select the way manually
|
---|
| 744 | // here so /only/ the new way will be selected after this method finishes.
|
---|
[8510] | 745 | if (alt) {
|
---|
[4768] | 746 | newSelection.add(insertPoint.getKey());
|
---|
| 747 | }
|
---|
| 748 |
|
---|
| 749 | cmds.add(new ChangeCommand(insertPoint.getKey(), wnew));
|
---|
| 750 | replacedWays.add(insertPoint.getKey());
|
---|
| 751 | reuseWays.add(wnew);
|
---|
| 752 | }
|
---|
| 753 |
|
---|
| 754 | adjustNode(segSet, n);
|
---|
| 755 | }
|
---|
| 756 |
|
---|
[1438] | 757 | /**
|
---|
[5909] | 758 | * Prevent creation of ways that look like this: <---->
|
---|
[1438] | 759 | * This happens if users want to draw a no-exit-sideway from the main way like this:
|
---|
| 760 | * ^
|
---|
[5909] | 761 | * |<---->
|
---|
[1438] | 762 | * |
|
---|
| 763 | * The solution isn't ideal because the main way will end in the side way, which is bad for
|
---|
| 764 | * navigation software ("drive straight on") but at least easier to fix. Maybe users will fix
|
---|
| 765 | * it on their own, too. At least it's better than producing an error.
|
---|
| 766 | *
|
---|
[5909] | 767 | * @param selectedWay the way to check
|
---|
| 768 | * @param currentNode the current node (i.e. the one the connection will be made from)
|
---|
| 769 | * @param targetNode the target node (i.e. the one the connection will be made to)
|
---|
| 770 | * @return {@code true} if this would create a selfcontaining way, {@code false} otherwise.
|
---|
[1438] | 771 | */
|
---|
| 772 | private boolean isSelfContainedWay(Way selectedWay, Node currentNode, Node targetNode) {
|
---|
[8510] | 773 | if (selectedWay != null) {
|
---|
[1898] | 774 | int posn0 = selectedWay.getNodes().indexOf(currentNode);
|
---|
[10409] | 775 | // CHECKSTYLE.OFF: SingleSpaceSeparator
|
---|
[10678] | 776 | if ((posn0 != -1 && // n0 is part of way
|
---|
| 777 | (posn0 >= 1 && targetNode.equals(selectedWay.getNode(posn0-1)))) || // previous node
|
---|
| 778 | (posn0 < selectedWay.getNodesCount()-1 && targetNode.equals(selectedWay.getNode(posn0+1)))) { // next node
|
---|
[13309] | 779 | setSelection(getLayerManager().getEditDataSet(), targetNode);
|
---|
[1438] | 780 | lastUsedNode = targetNode;
|
---|
| 781 | return true;
|
---|
| 782 | }
|
---|
[10409] | 783 | // CHECKSTYLE.ON: SingleSpaceSeparator
|
---|
[1438] | 784 | }
|
---|
| 785 |
|
---|
| 786 | return false;
|
---|
| 787 | }
|
---|
| 788 |
|
---|
| 789 | /**
|
---|
| 790 | * Finds a node to continue drawing from. Decision is based upon given node and way.
|
---|
| 791 | * @param selectedNode Currently selected node, may be null
|
---|
| 792 | * @param selectedWay Currently selected way, may be null
|
---|
| 793 | * @return Node if a suitable node is found, null otherwise
|
---|
| 794 | */
|
---|
| 795 | private Node findNodeToContinueFrom(Node selectedNode, Way selectedWay) {
|
---|
| 796 | // No nodes or ways have been selected, this occurs when a relation
|
---|
| 797 | // has been selected or the selection is empty
|
---|
[8510] | 798 | if (selectedNode == null && selectedWay == null)
|
---|
[1438] | 799 | return null;
|
---|
| 800 |
|
---|
| 801 | if (selectedNode == null) {
|
---|
| 802 | if (selectedWay.isFirstLastNode(lastUsedNode))
|
---|
| 803 | return lastUsedNode;
|
---|
| 804 |
|
---|
| 805 | // We have a way selected, but no suitable node to continue from. Start anew.
|
---|
| 806 | return null;
|
---|
| 807 | }
|
---|
| 808 |
|
---|
| 809 | if (selectedWay == null)
|
---|
| 810 | return selectedNode;
|
---|
| 811 |
|
---|
| 812 | if (selectedWay.isFirstLastNode(selectedNode))
|
---|
| 813 | return selectedNode;
|
---|
| 814 |
|
---|
| 815 | // We have a way and node selected, but it's not at the start/end of the way. Start anew.
|
---|
| 816 | return null;
|
---|
| 817 | }
|
---|
| 818 |
|
---|
[4956] | 819 | @Override
|
---|
| 820 | public void mouseDragged(MouseEvent e) {
|
---|
[2692] | 821 | mouseMoved(e);
|
---|
| 822 | }
|
---|
| 823 |
|
---|
[4956] | 824 | @Override
|
---|
| 825 | public void mouseMoved(MouseEvent e) {
|
---|
[12630] | 826 | if (!MainApplication.getMap().mapView.isActiveLayerDrawable())
|
---|
[1169] | 827 | return;
|
---|
[608] | 828 |
|
---|
[1169] | 829 | // we copy ctrl/alt/shift from the event just in case our global
|
---|
[7217] | 830 | // keyDetector didn't make it through the security manager. Unclear
|
---|
[1169] | 831 | // if that can ever happen but better be safe.
|
---|
[1438] | 832 | updateKeyModifiers(e);
|
---|
| 833 | mousePos = e.getPoint();
|
---|
[6069] | 834 | if (snapHelper.isSnapOn() && ctrl)
|
---|
[5555] | 835 | tryToSetBaseSegmentForAngleSnap();
|
---|
[6069] | 836 |
|
---|
[1444] | 837 | computeHelperLine();
|
---|
[13309] | 838 | addHighlighting(e);
|
---|
[1438] | 839 | }
|
---|
[6069] | 840 |
|
---|
[5555] | 841 | /**
|
---|
| 842 | * This method is used to detect segment under mouse and use it as reference for angle snapping
|
---|
| 843 | */
|
---|
| 844 | private void tryToSetBaseSegmentForAngleSnap() {
|
---|
[12425] | 845 | if (mousePos != null) {
|
---|
[12630] | 846 | WaySegment seg = MainApplication.getMap().mapView.getNearestWaySegment(mousePos, OsmPrimitive::isSelectable);
|
---|
[12425] | 847 | if (seg != null) {
|
---|
| 848 | snapHelper.setBaseSegment(seg);
|
---|
| 849 | }
|
---|
[5555] | 850 | }
|
---|
| 851 | }
|
---|
[1438] | 852 |
|
---|
[1169] | 853 | /**
|
---|
| 854 | * This method prepares data required for painting the "helper line" from
|
---|
| 855 | * the last used position to the mouse cursor. It duplicates some code from
|
---|
[2692] | 856 | * mouseReleased() (FIXME).
|
---|
[1169] | 857 | */
|
---|
[14003] | 858 | private synchronized void computeHelperLine() {
|
---|
[1169] | 859 | if (mousePos == null) {
|
---|
| 860 | // Don't draw the line.
|
---|
| 861 | currentMouseEastNorth = null;
|
---|
| 862 | currentBaseNode = null;
|
---|
| 863 | return;
|
---|
| 864 | }
|
---|
[1438] | 865 |
|
---|
[13293] | 866 | DataSet ds = getLayerManager().getEditDataSet();
|
---|
| 867 | Collection<OsmPrimitive> selection = ds != null ? ds.getSelected() : Collections.emptyList();
|
---|
[608] | 868 |
|
---|
[12630] | 869 | MapView mv = MainApplication.getMap().mapView;
|
---|
[1169] | 870 | Node currentMouseNode = null;
|
---|
[1409] | 871 | mouseOnExistingNode = null;
|
---|
[7005] | 872 | mouseOnExistingWays = new HashSet<>();
|
---|
[608] | 873 |
|
---|
[1545] | 874 | if (!ctrl && mousePos != null) {
|
---|
[10716] | 875 | currentMouseNode = mv.getNearestNode(mousePos, OsmPrimitive::isSelectable);
|
---|
[1169] | 876 | }
|
---|
[1023] | 877 |
|
---|
[1409] | 878 | // We need this for highlighting and we'll only do so if we actually want to re-use
|
---|
| 879 | // *and* there is no node nearby (because nodes beat ways when re-using)
|
---|
[8510] | 880 | if (!ctrl && currentMouseNode == null) {
|
---|
[10716] | 881 | List<WaySegment> wss = mv.getNearestWaySegments(mousePos, OsmPrimitive::isSelectable);
|
---|
[8510] | 882 | for (WaySegment ws : wss) {
|
---|
[1409] | 883 | mouseOnExistingWays.add(ws.way);
|
---|
[1814] | 884 | }
|
---|
[1409] | 885 | }
|
---|
| 886 |
|
---|
[1169] | 887 | if (currentMouseNode != null) {
|
---|
| 888 | // user clicked on node
|
---|
| 889 | if (selection.isEmpty()) return;
|
---|
[1640] | 890 | currentMouseEastNorth = currentMouseNode.getEastNorth();
|
---|
[1409] | 891 | mouseOnExistingNode = currentMouseNode;
|
---|
[1169] | 892 | } else {
|
---|
| 893 | // no node found in clicked area
|
---|
[1823] | 894 | currentMouseEastNorth = mv.getEastNorth(mousePos.x, mousePos.y);
|
---|
[1169] | 895 | }
|
---|
[1023] | 896 |
|
---|
[4768] | 897 | determineCurrentBaseNodeAndPreviousNode(selection);
|
---|
[5512] | 898 | if (previousNode == null) {
|
---|
| 899 | snapHelper.noSnapNow();
|
---|
| 900 | }
|
---|
[4956] | 901 |
|
---|
[10662] | 902 | if (getCurrentBaseNode() == null || getCurrentBaseNode() == currentMouseNode)
|
---|
[4768] | 903 | return; // Don't create zero length way segments.
|
---|
| 904 |
|
---|
[13177] | 905 | showStatusInfo(-1, -1, -1, snapHelper.isSnapOn());
|
---|
[10662] | 906 |
|
---|
[12131] | 907 | double curHdg = Utils.toDegrees(getCurrentBaseNode().getEastNorth()
|
---|
[5512] | 908 | .heading(currentMouseEastNorth));
|
---|
[8510] | 909 | double baseHdg = -1;
|
---|
[4768] | 910 | if (previousNode != null) {
|
---|
[8645] | 911 | EastNorth en = previousNode.getEastNorth();
|
---|
| 912 | if (en != null) {
|
---|
[12131] | 913 | baseHdg = Utils.toDegrees(en.heading(getCurrentBaseNode().getEastNorth()));
|
---|
[8645] | 914 | }
|
---|
[4768] | 915 | }
|
---|
[4956] | 916 |
|
---|
[8510] | 917 | snapHelper.checkAngleSnapping(currentMouseEastNorth, baseHdg, curHdg);
|
---|
[4803] | 918 |
|
---|
[4954] | 919 | // status bar was filled by snapHelper
|
---|
[4803] | 920 | }
|
---|
| 921 |
|
---|
[11495] | 922 | static void showStatusInfo(double angle, double hdg, double distance, boolean activeFlag) {
|
---|
[12630] | 923 | MapFrame map = MainApplication.getMap();
|
---|
| 924 | map.statusLine.setAngle(angle);
|
---|
| 925 | map.statusLine.activateAnglePanel(activeFlag);
|
---|
| 926 | map.statusLine.setHeading(hdg);
|
---|
| 927 | map.statusLine.setDist(distance);
|
---|
[4768] | 928 | }
|
---|
[4803] | 929 |
|
---|
[4956] | 930 | /**
|
---|
| 931 | * Helper function that sets fields currentBaseNode and previousNode
|
---|
| 932 | * @param selection
|
---|
[4768] | 933 | * uses also lastUsedNode field
|
---|
| 934 | */
|
---|
[14003] | 935 | private synchronized void determineCurrentBaseNodeAndPreviousNode(Collection<OsmPrimitive> selection) {
|
---|
[4768] | 936 | Node selectedNode = null;
|
---|
| 937 | Way selectedWay = null;
|
---|
[1169] | 938 | for (OsmPrimitive p : selection) {
|
---|
| 939 | if (p instanceof Node) {
|
---|
[4956] | 940 | if (selectedNode != null)
|
---|
| 941 | return;
|
---|
[1169] | 942 | selectedNode = (Node) p;
|
---|
| 943 | } else if (p instanceof Way) {
|
---|
[4956] | 944 | if (selectedWay != null)
|
---|
| 945 | return;
|
---|
[1169] | 946 | selectedWay = (Way) p;
|
---|
| 947 | }
|
---|
| 948 | }
|
---|
[4768] | 949 | // we are here, if not more than 1 way or node is selected,
|
---|
[1023] | 950 |
|
---|
[1169] | 951 | // the node from which we make a connection
|
---|
| 952 | currentBaseNode = null;
|
---|
[4768] | 953 | previousNode = null;
|
---|
[6069] | 954 |
|
---|
[5820] | 955 | // Try to find an open way to measure angle from it. The way is not to be continued!
|
---|
| 956 | // warning: may result in changes of currentBaseNode and previousNode
|
---|
| 957 | // please remove if bugs arise
|
---|
| 958 | if (selectedWay == null && selectedNode != null) {
|
---|
| 959 | for (OsmPrimitive p: selectedNode.getReferrers()) {
|
---|
| 960 | if (p.isUsable() && p instanceof Way && ((Way) p).isFirstLastNode(selectedNode)) {
|
---|
[8510] | 961 | if (selectedWay != null) { // two uncontinued ways, nothing to take as reference
|
---|
| 962 | selectedWay = null;
|
---|
[5820] | 963 | break;
|
---|
| 964 | } else {
|
---|
| 965 | // set us ~continue this way (measure angle from it)
|
---|
| 966 | selectedWay = (Way) p;
|
---|
| 967 | }
|
---|
| 968 | }
|
---|
| 969 | }
|
---|
[6069] | 970 | }
|
---|
| 971 |
|
---|
[1169] | 972 | if (selectedNode == null) {
|
---|
[1458] | 973 | if (selectedWay == null)
|
---|
| 974 | return;
|
---|
[5820] | 975 | continueWayFromNode(selectedWay, lastUsedNode);
|
---|
[1169] | 976 | } else if (selectedWay == null) {
|
---|
| 977 | currentBaseNode = selectedNode;
|
---|
[4643] | 978 | } else if (!selectedWay.isDeleted()) { // fix #7118
|
---|
[5820] | 979 | continueWayFromNode(selectedWay, selectedNode);
|
---|
[1169] | 980 | }
|
---|
[4768] | 981 | }
|
---|
[6069] | 982 |
|
---|
[5820] | 983 | /**
|
---|
[10228] | 984 | * if one of the ends of {@code way} is given {@code node},
|
---|
| 985 | * then set currentBaseNode = node and previousNode = adjacent node of way
|
---|
[9230] | 986 | * @param way way to continue
|
---|
| 987 | * @param node starting node
|
---|
[5820] | 988 | */
|
---|
| 989 | private void continueWayFromNode(Way way, Node node) {
|
---|
| 990 | int n = way.getNodesCount();
|
---|
[10662] | 991 | if (node == way.firstNode()) {
|
---|
[5820] | 992 | currentBaseNode = node;
|
---|
[8510] | 993 | if (n > 1) previousNode = way.getNode(1);
|
---|
[10662] | 994 | } else if (node == way.lastNode()) {
|
---|
[5820] | 995 | currentBaseNode = node;
|
---|
[8510] | 996 | if (n > 1) previousNode = way.getNode(n-2);
|
---|
[5820] | 997 | }
|
---|
| 998 | }
|
---|
[1023] | 999 |
|
---|
[1169] | 1000 | /**
|
---|
| 1001 | * Repaint on mouse exit so that the helper line goes away.
|
---|
| 1002 | */
|
---|
[7217] | 1003 | @Override
|
---|
| 1004 | public void mouseExited(MouseEvent e) {
|
---|
[12636] | 1005 | OsmDataLayer editLayer = MainApplication.getLayerManager().getEditLayer();
|
---|
[10467] | 1006 | if (editLayer == null)
|
---|
[1169] | 1007 | return;
|
---|
| 1008 | mousePos = e.getPoint();
|
---|
[4768] | 1009 | snapHelper.noSnapNow();
|
---|
[13309] | 1010 | boolean repaintIssued = removeHighlighting(e);
|
---|
[5100] | 1011 | // force repaint in case snapHelper needs one. If removeHighlighting
|
---|
[10873] | 1012 | // caused one already, don't do it again.
|
---|
[8510] | 1013 | if (!repaintIssued) {
|
---|
[10467] | 1014 | editLayer.invalidate();
|
---|
[5100] | 1015 | }
|
---|
[1169] | 1016 | }
|
---|
[1023] | 1017 |
|
---|
[1169] | 1018 | /**
|
---|
[13146] | 1019 | * Replies the parent way of a node, if it is the end of exactly one usable way.
|
---|
[9230] | 1020 | * @param n node
|
---|
[1169] | 1021 | * @return If the node is the end of exactly one way, return this.
|
---|
| 1022 | * <code>null</code> otherwise.
|
---|
| 1023 | */
|
---|
[4768] | 1024 | public static Way getWayForNode(Node n) {
|
---|
[1169] | 1025 | Way way = null;
|
---|
[4539] | 1026 | for (Way w : Utils.filteredCollection(n.getReferrers(), Way.class)) {
|
---|
[2120] | 1027 | if (!w.isUsable() || w.getNodesCount() < 1) {
|
---|
[1814] | 1028 | continue;
|
---|
| 1029 | }
|
---|
[1898] | 1030 | Node firstNode = w.getNode(0);
|
---|
| 1031 | Node lastNode = w.getNode(w.getNodesCount() - 1);
|
---|
[10662] | 1032 | if ((firstNode == n || lastNode == n) && (firstNode != lastNode)) {
|
---|
[1169] | 1033 | if (way != null)
|
---|
| 1034 | return null;
|
---|
| 1035 | way = w;
|
---|
| 1036 | }
|
---|
| 1037 | }
|
---|
| 1038 | return way;
|
---|
| 1039 | }
|
---|
[359] | 1040 |
|
---|
[8064] | 1041 | /**
|
---|
| 1042 | * Replies the current base node, after having checked it is still usable (see #11105).
|
---|
| 1043 | * @return the current base node (can be null). If not-null, it's guaranteed the node is usable
|
---|
| 1044 | */
|
---|
[14003] | 1045 | public synchronized Node getCurrentBaseNode() {
|
---|
[8064] | 1046 | if (currentBaseNode != null && (currentBaseNode.getDataSet() == null || !currentBaseNode.isUsable())) {
|
---|
| 1047 | currentBaseNode = null;
|
---|
| 1048 | }
|
---|
[4086] | 1049 | return currentBaseNode;
|
---|
| 1050 | }
|
---|
| 1051 |
|
---|
[1169] | 1052 | private static void pruneSuccsAndReverse(List<Integer> is) {
|
---|
[8338] | 1053 | Set<Integer> is2 = new HashSet<>();
|
---|
[1169] | 1054 | for (int i : is) {
|
---|
| 1055 | if (!is2.contains(i - 1) && !is2.contains(i + 1)) {
|
---|
| 1056 | is2.add(i);
|
---|
| 1057 | }
|
---|
| 1058 | }
|
---|
| 1059 | is.clear();
|
---|
| 1060 | is.addAll(is2);
|
---|
| 1061 | Collections.sort(is);
|
---|
| 1062 | Collections.reverse(is);
|
---|
| 1063 | }
|
---|
[388] | 1064 |
|
---|
[1169] | 1065 | /**
|
---|
[11601] | 1066 | * Adjusts the position of a node to lie on a segment (or a segment intersection).
|
---|
[1169] | 1067 | *
|
---|
| 1068 | * If one or more than two segments are passed, the node is adjusted
|
---|
| 1069 | * to lie on the first segment that is passed.
|
---|
| 1070 | *
|
---|
[11601] | 1071 | * If two segments are passed, the node is adjusted to be at their intersection.
|
---|
[1169] | 1072 | *
|
---|
| 1073 | * No action is taken if no segments are passed.
|
---|
| 1074 | *
|
---|
| 1075 | * @param segs the segments to use as a reference when adjusting
|
---|
| 1076 | * @param n the node to adjust
|
---|
| 1077 | */
|
---|
[8510] | 1078 | private static void adjustNode(Collection<Pair<Node, Node>> segs, Node n) {
|
---|
[1169] | 1079 | switch (segs.size()) {
|
---|
[2339] | 1080 | case 0:
|
---|
| 1081 | return;
|
---|
| 1082 | case 2:
|
---|
[11601] | 1083 | adjustNodeTwoSegments(segs, n);
|
---|
| 1084 | break;
|
---|
| 1085 | default:
|
---|
| 1086 | adjustNodeDefault(segs, n);
|
---|
| 1087 | }
|
---|
| 1088 | }
|
---|
[389] | 1089 |
|
---|
[11601] | 1090 | private static void adjustNodeTwoSegments(Collection<Pair<Node, Node>> segs, Node n) {
|
---|
| 1091 | // This computes the intersection between the two segments and adjusts the node position.
|
---|
| 1092 | Iterator<Pair<Node, Node>> i = segs.iterator();
|
---|
| 1093 | Pair<Node, Node> seg = i.next();
|
---|
| 1094 | EastNorth pA = seg.a.getEastNorth();
|
---|
| 1095 | EastNorth pB = seg.b.getEastNorth();
|
---|
| 1096 | seg = i.next();
|
---|
| 1097 | EastNorth pC = seg.a.getEastNorth();
|
---|
| 1098 | EastNorth pD = seg.b.getEastNorth();
|
---|
[1023] | 1099 |
|
---|
[11601] | 1100 | double u = det(pB.east() - pA.east(), pB.north() - pA.north(), pC.east() - pD.east(), pC.north() - pD.north());
|
---|
[1029] | 1101 |
|
---|
[11601] | 1102 | // Check for parallel segments and do nothing if they are
|
---|
| 1103 | // In practice this will probably only happen when a way has been duplicated
|
---|
[1023] | 1104 |
|
---|
[11601] | 1105 | if (u == 0)
|
---|
| 1106 | return;
|
---|
[1169] | 1107 |
|
---|
[11601] | 1108 | // q is a number between 0 and 1
|
---|
| 1109 | // It is the point in the segment where the intersection occurs
|
---|
| 1110 | // if the segment is scaled to length 1
|
---|
[1169] | 1111 |
|
---|
[11601] | 1112 | double q = det(pB.north() - pC.north(), pB.east() - pC.east(), pD.north() - pC.north(), pD.east() - pC.east()) / u;
|
---|
| 1113 | EastNorth intersection = new EastNorth(
|
---|
| 1114 | pB.east() + q * (pA.east() - pB.east()),
|
---|
| 1115 | pB.north() + q * (pA.north() - pB.north()));
|
---|
[1169] | 1116 |
|
---|
[11601] | 1117 |
|
---|
| 1118 | // only adjust to intersection if within snapToIntersectionThreshold pixel of mouse click; otherwise
|
---|
| 1119 | // fall through to default action.
|
---|
| 1120 | // (for semi-parallel lines, intersection might be miles away!)
|
---|
[12630] | 1121 | MapFrame map = MainApplication.getMap();
|
---|
| 1122 | if (map.mapView.getPoint2D(n).distance(map.mapView.getPoint2D(intersection)) < SNAP_TO_INTERSECTION_THRESHOLD.get()) {
|
---|
[11601] | 1123 | n.setEastNorth(intersection);
|
---|
| 1124 | return;
|
---|
[1169] | 1125 | }
|
---|
[11601] | 1126 |
|
---|
| 1127 | adjustNodeDefault(segs, n);
|
---|
[1169] | 1128 | }
|
---|
[1023] | 1129 |
|
---|
[11601] | 1130 | private static void adjustNodeDefault(Collection<Pair<Node, Node>> segs, Node n) {
|
---|
| 1131 | EastNorth p = n.getEastNorth();
|
---|
| 1132 | Pair<Node, Node> seg = segs.iterator().next();
|
---|
| 1133 | EastNorth pA = seg.a.getEastNorth();
|
---|
| 1134 | EastNorth pB = seg.b.getEastNorth();
|
---|
| 1135 | double a = p.distanceSq(pB);
|
---|
| 1136 | double b = p.distanceSq(pA);
|
---|
| 1137 | double c = pA.distanceSq(pB);
|
---|
| 1138 | double q = (a - b + c) / (2*c);
|
---|
| 1139 | n.setEastNorth(new EastNorth(pB.east() + q * (pA.east() - pB.east()), pB.north() + q * (pA.north() - pB.north())));
|
---|
| 1140 | }
|
---|
| 1141 |
|
---|
[1169] | 1142 | // helper for adjustNode
|
---|
[1438] | 1143 | static double det(double a, double b, double c, double d) {
|
---|
[1169] | 1144 | return a * d - b * c;
|
---|
| 1145 | }
|
---|
[4917] | 1146 |
|
---|
| 1147 | private void tryToMoveNodeOnIntersection(List<WaySegment> wss, Node n) {
|
---|
[4956] | 1148 | if (wss.isEmpty())
|
---|
| 1149 | return;
|
---|
[4917] | 1150 | WaySegment ws = wss.get(0);
|
---|
[8510] | 1151 | EastNorth p1 = ws.getFirstNode().getEastNorth();
|
---|
| 1152 | EastNorth p2 = ws.getSecondNode().getEastNorth();
|
---|
[8064] | 1153 | if (snapHelper.dir2 != null && getCurrentBaseNode() != null) {
|
---|
| 1154 | EastNorth xPoint = Geometry.getSegmentSegmentIntersection(p1, p2, snapHelper.dir2,
|
---|
| 1155 | getCurrentBaseNode().getEastNorth());
|
---|
[8510] | 1156 | if (xPoint != null) {
|
---|
[5512] | 1157 | n.setEastNorth(xPoint);
|
---|
| 1158 | }
|
---|
[4917] | 1159 | }
|
---|
| 1160 | }
|
---|
[9059] | 1161 |
|
---|
[4956] | 1162 | /**
|
---|
[4768] | 1163 | * Takes the data from computeHelperLine to determine which ways/nodes should be highlighted
|
---|
[5098] | 1164 | * (if feature enabled). Also sets the target cursor if appropriate. It adds the to-be-
|
---|
| 1165 | * highlighted primitives to newHighlights but does not actually highlight them. This work is
|
---|
| 1166 | * done in redrawIfRequired. This means, calling addHighlighting() without redrawIfRequired()
|
---|
| 1167 | * will leave the data in an inconsistent state.
|
---|
[5512] | 1168 | *
|
---|
[5098] | 1169 | * The status bar derives its information from oldHighlights, so in order to update the status
|
---|
| 1170 | * bar both addHighlighting() and repaintIfRequired() are needed, since former fills newHighlights
|
---|
| 1171 | * and latter processes them into oldHighlights.
|
---|
[13309] | 1172 | * @param event event, can be null
|
---|
[4768] | 1173 | */
|
---|
[13309] | 1174 | private void addHighlighting(Object event) {
|
---|
[7005] | 1175 | newHighlights = new HashSet<>();
|
---|
[12630] | 1176 | MapView mapView = MainApplication.getMap().mapView;
|
---|
[5098] | 1177 |
|
---|
[4768] | 1178 | // if ctrl key is held ("no join"), don't highlight anything
|
---|
| 1179 | if (ctrl) {
|
---|
[12630] | 1180 | mapView.setNewCursor(cursor, this);
|
---|
[13309] | 1181 | redrawIfRequired(event);
|
---|
[4768] | 1182 | return;
|
---|
| 1183 | }
|
---|
[1023] | 1184 |
|
---|
[4768] | 1185 | // This happens when nothing is selected, but we still want to highlight the "target node"
|
---|
[13882] | 1186 | DataSet ds = getLayerManager().getEditDataSet();
|
---|
| 1187 | if (mouseOnExistingNode == null && mousePos != null && ds != null && ds.selectionEmpty()) {
|
---|
[12630] | 1188 | mouseOnExistingNode = mapView.getNearestNode(mousePos, OsmPrimitive::isSelectable);
|
---|
[4768] | 1189 | }
|
---|
| 1190 |
|
---|
| 1191 | if (mouseOnExistingNode != null) {
|
---|
[12630] | 1192 | mapView.setNewCursor(cursorJoinNode, this);
|
---|
[5098] | 1193 | newHighlights.add(mouseOnExistingNode);
|
---|
[13309] | 1194 | redrawIfRequired(event);
|
---|
[4768] | 1195 | return;
|
---|
| 1196 | }
|
---|
| 1197 |
|
---|
| 1198 | // Insert the node into all the nearby way segments
|
---|
[5922] | 1199 | if (mouseOnExistingWays.isEmpty()) {
|
---|
[12630] | 1200 | mapView.setNewCursor(cursor, this);
|
---|
[13309] | 1201 | redrawIfRequired(event);
|
---|
[4768] | 1202 | return;
|
---|
[4956] | 1203 | }
|
---|
[4768] | 1204 |
|
---|
[12630] | 1205 | mapView.setNewCursor(cursorJoinWay, this);
|
---|
[5098] | 1206 | newHighlights.addAll(mouseOnExistingWays);
|
---|
[13309] | 1207 | redrawIfRequired(event);
|
---|
[4768] | 1208 | }
|
---|
| 1209 |
|
---|
| 1210 | /**
|
---|
[5098] | 1211 | * Removes target highlighting from primitives. Issues repaint if required.
|
---|
[13309] | 1212 | * @param event event, can be null
|
---|
[8931] | 1213 | * @return true if a repaint has been issued.
|
---|
[4768] | 1214 | */
|
---|
[13309] | 1215 | private boolean removeHighlighting(Object event) {
|
---|
[7005] | 1216 | newHighlights = new HashSet<>();
|
---|
[13309] | 1217 | return redrawIfRequired(event);
|
---|
[4768] | 1218 | }
|
---|
[4956] | 1219 |
|
---|
[5922] | 1220 | @Override
|
---|
[14011] | 1221 | public synchronized void paint(Graphics2D g, MapView mv, Bounds box) {
|
---|
[1169] | 1222 | // sanity checks
|
---|
[12630] | 1223 | MapView mapView = MainApplication.getMap().mapView;
|
---|
| 1224 | if (mapView == null || mousePos == null
|
---|
[5512] | 1225 | // don't draw line if we don't know where from or where to
|
---|
[11452] | 1226 | || currentMouseEastNorth == null || getCurrentBaseNode() == null
|
---|
[5512] | 1227 | // don't draw line if mouse is outside window
|
---|
[12630] | 1228 | || !mapView.getState().getForView(mousePos.getX(), mousePos.getY()).isInView())
|
---|
[4956] | 1229 | return;
|
---|
[1023] | 1230 |
|
---|
[2528] | 1231 | Graphics2D g2 = g;
|
---|
[10874] | 1232 | snapHelper.drawIfNeeded(g2, mv.getState());
|
---|
| 1233 | if (!DRAW_HELPER_LINE.get() || wayIsFinished || shift)
|
---|
[5512] | 1234 | return;
|
---|
[4956] | 1235 |
|
---|
[10874] | 1236 | if (!snapHelper.isActive()) {
|
---|
| 1237 | g2.setColor(RUBBER_LINE_COLOR.get());
|
---|
| 1238 | g2.setStroke(RUBBER_LINE_STROKE.get());
|
---|
| 1239 | paintConstructionGeometry(mv, g2);
|
---|
| 1240 | } else if (DRAW_CONSTRUCTION_GEOMETRY.get()) {
|
---|
| 1241 | // else use color and stoke from snapHelper.draw
|
---|
| 1242 | paintConstructionGeometry(mv, g2);
|
---|
| 1243 | }
|
---|
| 1244 | }
|
---|
| 1245 |
|
---|
| 1246 | private void paintConstructionGeometry(MapView mv, Graphics2D g2) {
|
---|
[10827] | 1247 | MapPath2D b = new MapPath2D();
|
---|
| 1248 | MapViewPoint p1 = mv.getState().getPointFor(getCurrentBaseNode());
|
---|
| 1249 | MapViewPoint p2 = mv.getState().getPointFor(currentMouseEastNorth);
|
---|
[608] | 1250 |
|
---|
[10827] | 1251 | b.moveTo(p1);
|
---|
| 1252 | b.lineTo(p2);
|
---|
[1023] | 1253 |
|
---|
[1169] | 1254 | // if alt key is held ("start new way"), draw a little perpendicular line
|
---|
| 1255 | if (alt) {
|
---|
[10827] | 1256 | START_WAY_INDICATOR.paintArrowAt(b, p1, p2);
|
---|
[1169] | 1257 | }
|
---|
[1023] | 1258 |
|
---|
[1169] | 1259 | g2.draw(b);
|
---|
[5739] | 1260 | g2.setStroke(BASIC_STROKE);
|
---|
[1169] | 1261 | }
|
---|
[1023] | 1262 |
|
---|
[4956] | 1263 | @Override
|
---|
| 1264 | public String getModeHelpText() {
|
---|
[6289] | 1265 | StringBuilder rv;
|
---|
[1438] | 1266 | /*
|
---|
| 1267 | * No modifiers: all (Connect, Node Re-Use, Auto-Weld)
|
---|
| 1268 | * CTRL: disables node re-use, auto-weld
|
---|
[1545] | 1269 | * Shift: do not make connection
|
---|
| 1270 | * ALT: make connection but start new way in doing so
|
---|
[1438] | 1271 | */
|
---|
[1023] | 1272 |
|
---|
[1438] | 1273 | /*
|
---|
| 1274 | * Status line text generation is split into two parts to keep it maintainable.
|
---|
| 1275 | * First part looks at what will happen to the new node inserted on click and
|
---|
| 1276 | * the second part will look if a connection is made or not.
|
---|
| 1277 | *
|
---|
| 1278 | * Note that this help text is not absolutely accurate as it doesn't catch any special
|
---|
| 1279 | * cases (e.g. when preventing <---> ways). The only special that it catches is when
|
---|
| 1280 | * a way is about to be finished.
|
---|
| 1281 | *
|
---|
| 1282 | * First check what happens to the new node.
|
---|
| 1283 | */
|
---|
| 1284 |
|
---|
| 1285 | // oldHighlights stores the current highlights. If this
|
---|
| 1286 | // list is empty we can assume that we won't do any joins
|
---|
[1545] | 1287 | if (ctrl || oldHighlights.isEmpty()) {
|
---|
[6289] | 1288 | rv = new StringBuilder(tr("Create new node."));
|
---|
[1438] | 1289 | } else {
|
---|
| 1290 | // oldHighlights may store a node or way, check if it's a node
|
---|
[1458] | 1291 | OsmPrimitive x = oldHighlights.iterator().next();
|
---|
[1814] | 1292 | if (x instanceof Node) {
|
---|
[6289] | 1293 | rv = new StringBuilder(tr("Select node under cursor."));
|
---|
[1814] | 1294 | } else {
|
---|
[6289] | 1295 | rv = new StringBuilder(trn("Insert new node into way.", "Insert new node into {0} ways.",
|
---|
| 1296 | oldHighlights.size(), oldHighlights.size()));
|
---|
[1814] | 1297 | }
|
---|
[1169] | 1298 | }
|
---|
[1438] | 1299 |
|
---|
| 1300 | /*
|
---|
| 1301 | * Check whether a connection will be made
|
---|
| 1302 | */
|
---|
[11452] | 1303 | if (!wayIsFinished && getCurrentBaseNode() != null) {
|
---|
[1814] | 1304 | if (alt) {
|
---|
[8390] | 1305 | rv.append(' ').append(tr("Start new way from last node."));
|
---|
[1814] | 1306 | } else {
|
---|
[8390] | 1307 | rv.append(' ').append(tr("Continue way from last node."));
|
---|
[1814] | 1308 | }
|
---|
[4782] | 1309 | if (snapHelper.isSnapOn()) {
|
---|
[8390] | 1310 | rv.append(' ').append(tr("Angle snapping active."));
|
---|
[4768] | 1311 | }
|
---|
[1169] | 1312 | }
|
---|
[1459] | 1313 |
|
---|
[1447] | 1314 | Node n = mouseOnExistingNode;
|
---|
[10382] | 1315 | DataSet ds = getLayerManager().getEditDataSet();
|
---|
[1438] | 1316 | /*
|
---|
| 1317 | * Handle special case: Highlighted node == selected node => finish drawing
|
---|
| 1318 | */
|
---|
[10382] | 1319 | if (n != null && ds != null && ds.getSelectedNodes().contains(n)) {
|
---|
[1814] | 1320 | if (wayIsFinished) {
|
---|
[6289] | 1321 | rv = new StringBuilder(tr("Select node under cursor."));
|
---|
[1814] | 1322 | } else {
|
---|
[6289] | 1323 | rv = new StringBuilder(tr("Finish drawing."));
|
---|
[1814] | 1324 | }
|
---|
[1438] | 1325 | }
|
---|
| 1326 |
|
---|
| 1327 | /*
|
---|
| 1328 | * Handle special case: Self-Overlapping or closing way
|
---|
| 1329 | */
|
---|
[10382] | 1330 | if (ds != null && !ds.getSelectedWays().isEmpty() && !wayIsFinished && !alt) {
|
---|
| 1331 | Way w = ds.getSelectedWays().iterator().next();
|
---|
[1898] | 1332 | for (Node m : w.getNodes()) {
|
---|
[1545] | 1333 | if (m.equals(mouseOnExistingNode) || mouseOnExistingWays.contains(w)) {
|
---|
[8390] | 1334 | rv.append(' ').append(tr("Finish drawing."));
|
---|
[1438] | 1335 | break;
|
---|
| 1336 | }
|
---|
| 1337 | }
|
---|
| 1338 | }
|
---|
[6289] | 1339 | return rv.toString();
|
---|
[1169] | 1340 | }
|
---|
[1409] | 1341 |
|
---|
[5378] | 1342 | /**
|
---|
| 1343 | * Get selected primitives, while draw action is in progress.
|
---|
| 1344 | *
|
---|
| 1345 | * While drawing a way, technically the last node is selected.
|
---|
[6546] | 1346 | * This is inconvenient when the user tries to add/edit tags to the way.
|
---|
| 1347 | * For this case, this method returns the current way as selection,
|
---|
| 1348 | * to work around this issue.
|
---|
[5378] | 1349 | * Otherwise the normal selection of the current data layer is returned.
|
---|
[8931] | 1350 | * @return selected primitives, while draw action is in progress
|
---|
[5378] | 1351 | */
|
---|
| 1352 | public Collection<OsmPrimitive> getInProgressSelection() {
|
---|
[10382] | 1353 | DataSet ds = getLayerManager().getEditDataSet();
|
---|
[13941] | 1354 | if (ds == null) return Collections.emptyList();
|
---|
[10383] | 1355 | if (getCurrentBaseNode() != null && !ds.selectionEmpty()) {
|
---|
[8064] | 1356 | Way continueFrom = getWayForNode(getCurrentBaseNode());
|
---|
[6546] | 1357 | if (continueFrom != null)
|
---|
[5378] | 1358 | return Collections.<OsmPrimitive>singleton(continueFrom);
|
---|
| 1359 | }
|
---|
| 1360 | return ds.getSelected();
|
---|
| 1361 | }
|
---|
| 1362 |
|
---|
[4956] | 1363 | @Override
|
---|
| 1364 | public boolean layerIsSupported(Layer l) {
|
---|
[13434] | 1365 | return isEditableDataLayer(l);
|
---|
[1379] | 1366 | }
|
---|
[1821] | 1367 |
|
---|
| 1368 | @Override
|
---|
| 1369 | protected void updateEnabledState() {
|
---|
[10382] | 1370 | setEnabled(getLayerManager().getEditLayer() != null);
|
---|
[1821] | 1371 | }
|
---|
[3327] | 1372 |
|
---|
| 1373 | @Override
|
---|
| 1374 | public void destroy() {
|
---|
| 1375 | super.destroy();
|
---|
[14510] | 1376 | finishDrawing();
|
---|
[5459] | 1377 | snapChangeAction.destroy();
|
---|
[3327] | 1378 | }
|
---|
[4768] | 1379 |
|
---|
[12284] | 1380 | /**
|
---|
| 1381 | * Undo the last command. Binded by default to backspace key.
|
---|
| 1382 | */
|
---|
[4776] | 1383 | public class BackSpaceAction extends AbstractAction {
|
---|
[4611] | 1384 |
|
---|
| 1385 | @Override
|
---|
| 1386 | public void actionPerformed(ActionEvent e) {
|
---|
[14134] | 1387 | UndoRedoHandler.getInstance().undo();
|
---|
| 1388 | Command lastCmd = UndoRedoHandler.getInstance().getLastCommand();
|
---|
[8510] | 1389 | if (lastCmd == null) return;
|
---|
[9062] | 1390 | Node n = null;
|
---|
[4611] | 1391 | for (OsmPrimitive p: lastCmd.getParticipatingPrimitives()) {
|
---|
| 1392 | if (p instanceof Node) {
|
---|
[8510] | 1393 | if (n == null) {
|
---|
| 1394 | n = (Node) p; // found one node
|
---|
| 1395 | wayIsFinished = false;
|
---|
[10378] | 1396 | } else {
|
---|
[5098] | 1397 | // if more than 1 node were affected by previous command,
|
---|
| 1398 | // we have no way to continue, so we forget about found node
|
---|
[8510] | 1399 | n = null;
|
---|
[4956] | 1400 | break;
|
---|
[4611] | 1401 | }
|
---|
| 1402 | }
|
---|
| 1403 | }
|
---|
[4956] | 1404 | // select last added node - maybe we will continue drawing from it
|
---|
[8510] | 1405 | if (n != null) {
|
---|
[13309] | 1406 | addSelection(getLayerManager().getEditDataSet(), n);
|
---|
[5512] | 1407 | }
|
---|
[5098] | 1408 | }
|
---|
[4956] | 1409 | }
|
---|
[4611] | 1410 |
|
---|
[4782] | 1411 | private class SnapChangeAction extends JosmAction {
|
---|
[8509] | 1412 | /**
|
---|
| 1413 | * Constructs a new {@code SnapChangeAction}.
|
---|
| 1414 | */
|
---|
[8836] | 1415 | SnapChangeAction() {
|
---|
[7668] | 1416 | super(tr("Angle snapping"), /* ICON() */ "anglesnap",
|
---|
[5512] | 1417 | tr("Switch angle snapping mode while drawing"), null, false);
|
---|
[14397] | 1418 | setHelpId(ht("/Action/Draw/AngleSnap"));
|
---|
[4782] | 1419 | }
|
---|
[4956] | 1420 |
|
---|
| 1421 | @Override
|
---|
[4782] | 1422 | public void actionPerformed(ActionEvent e) {
|
---|
[8510] | 1423 | if (snapHelper != null) {
|
---|
[5512] | 1424 | snapHelper.toggleSnapping();
|
---|
| 1425 | }
|
---|
[4782] | 1426 | }
|
---|
[9409] | 1427 |
|
---|
| 1428 | @Override
|
---|
| 1429 | protected void updateEnabledState() {
|
---|
[12630] | 1430 | MapFrame map = MainApplication.getMap();
|
---|
| 1431 | setEnabled(map != null && map.mapMode instanceof DrawAction);
|
---|
[9409] | 1432 | }
|
---|
[4782] | 1433 | }
|
---|
[358] | 1434 | }
|
---|