001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.AWTEvent; 007import java.awt.Cursor; 008import java.awt.GridBagLayout; 009import java.awt.Insets; 010import java.awt.Toolkit; 011import java.awt.event.AWTEventListener; 012import java.awt.event.ActionEvent; 013import java.awt.event.FocusEvent; 014import java.awt.event.FocusListener; 015import java.awt.event.KeyEvent; 016import java.awt.event.MouseEvent; 017import java.util.Locale; 018 019import javax.swing.JLabel; 020import javax.swing.JPanel; 021 022import org.openstreetmap.josm.actions.mapmode.MapMode; 023import org.openstreetmap.josm.data.coor.EastNorth; 024import org.openstreetmap.josm.data.coor.LatLon; 025import org.openstreetmap.josm.data.imagery.OffsetBookmark; 026import org.openstreetmap.josm.data.projection.ProjectionRegistry; 027import org.openstreetmap.josm.gui.ExtendedDialog; 028import org.openstreetmap.josm.gui.MainApplication; 029import org.openstreetmap.josm.gui.MapFrame; 030import org.openstreetmap.josm.gui.MapView; 031import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer; 032import org.openstreetmap.josm.gui.util.WindowGeometry; 033import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 034import org.openstreetmap.josm.gui.widgets.JosmTextField; 035import org.openstreetmap.josm.tools.GBC; 036import org.openstreetmap.josm.tools.ImageProvider; 037import org.openstreetmap.josm.tools.JosmDecimalFormatSymbolsProvider; 038import org.openstreetmap.josm.tools.Logging; 039import org.openstreetmap.josm.tools.Utils; 040 041/** 042 * Adjust the position of an imagery layer. 043 * @since 3715 044 */ 045public class ImageryAdjustAction extends MapMode implements AWTEventListener { 046 private static ImageryOffsetDialog offsetDialog; 047 048 private transient OffsetBookmark old; 049 private transient OffsetBookmark tempOffset; 050 private EastNorth prevEastNorth; 051 private transient AbstractTileSourceLayer<?> layer; 052 private MapMode oldMapMode; 053 private boolean exitingMode; 054 private boolean restoreOldMode; 055 056 /** 057 * Constructs a new {@code ImageryAdjustAction} for the given layer. 058 * @param layer The imagery layer 059 */ 060 public ImageryAdjustAction(AbstractTileSourceLayer<?> layer) { 061 super(tr("New offset"), "adjustimg", tr("Adjust the position of this imagery layer"), 062 ImageProvider.getCursor("normal", "move")); 063 putValue("toolbar", Boolean.FALSE); 064 this.layer = layer; 065 } 066 067 @Override 068 public void enterMode() { 069 super.enterMode(); 070 if (layer == null) 071 return; 072 if (!layer.isVisible()) { 073 layer.setVisible(true); 074 } 075 old = layer.getDisplaySettings().getOffsetBookmark(); 076 EastNorth curOff = old == null ? EastNorth.ZERO : old.getDisplacement(ProjectionRegistry.getProjection()); 077 LatLon center; 078 if (MainApplication.isDisplayingMapView()) { 079 center = ProjectionRegistry.getProjection().eastNorth2latlon(MainApplication.getMap().mapView.getCenter()); 080 } else { 081 center = LatLon.ZERO; 082 } 083 tempOffset = new OffsetBookmark( 084 ProjectionRegistry.getProjection().toCode(), 085 layer.getInfo().getId(), 086 layer.getInfo().getName(), 087 null, 088 curOff, center); 089 layer.getDisplaySettings().setOffsetBookmark(tempOffset); 090 addListeners(); 091 showOffsetDialog(new ImageryOffsetDialog()); 092 } 093 094 private static void showOffsetDialog(ImageryOffsetDialog dlg) { 095 offsetDialog = dlg; 096 offsetDialog.setVisible(true); 097 } 098 099 private static void hideOffsetDialog() { 100 offsetDialog.setVisible(false); 101 offsetDialog = null; 102 } 103 104 protected void addListeners() { 105 MapView mapView = MainApplication.getMap().mapView; 106 mapView.addMouseListener(this); 107 mapView.addMouseMotionListener(this); 108 try { 109 Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK); 110 } catch (SecurityException ex) { 111 Logging.error(ex); 112 } 113 } 114 115 @Override 116 public void exitMode() { 117 // do not restore old mode here - this is called when the new mode is already known. 118 restoreOldMode = false; 119 doExitMode(); 120 } 121 122 private void doExitMode() { 123 exitingMode = true; 124 try { 125 super.exitMode(); 126 } catch (IllegalArgumentException e) { 127 Logging.trace(e); 128 } 129 if (offsetDialog != null) { 130 if (layer != null) { 131 layer.getDisplaySettings().setOffsetBookmark(old); 132 } 133 hideOffsetDialog(); 134 } 135 removeListeners(); 136 exitingMode = false; 137 } 138 139 protected void removeListeners() { 140 try { 141 Toolkit.getDefaultToolkit().removeAWTEventListener(this); 142 } catch (SecurityException ex) { 143 Logging.error(ex); 144 } 145 if (MainApplication.isDisplayingMapView()) { 146 MapFrame map = MainApplication.getMap(); 147 map.mapView.removeMouseMotionListener(this); 148 map.mapView.removeMouseListener(this); 149 } 150 } 151 152 @Override 153 public void eventDispatched(AWTEvent event) { 154 if (!(event instanceof KeyEvent) 155 || (event.getID() != KeyEvent.KEY_PRESSED) 156 || (layer == null) 157 || (offsetDialog != null && offsetDialog.areFieldsInFocus())) { 158 return; 159 } 160 KeyEvent kev = (KeyEvent) event; 161 int dx = 0; 162 int dy = 0; 163 switch (kev.getKeyCode()) { 164 case KeyEvent.VK_UP : dy = +1; break; 165 case KeyEvent.VK_DOWN : dy = -1; break; 166 case KeyEvent.VK_LEFT : dx = -1; break; 167 case KeyEvent.VK_RIGHT : dx = +1; break; 168 case KeyEvent.VK_ESCAPE: 169 if (offsetDialog != null) { 170 restoreOldMode = true; 171 offsetDialog.setVisible(false); 172 return; 173 } 174 break; 175 default: // Do nothing 176 } 177 if (dx != 0 || dy != 0) { 178 double ppd = layer.getPPD(); 179 EastNorth d = tempOffset.getDisplacement().add(new EastNorth(dx / ppd, dy / ppd)); 180 tempOffset.setDisplacement(d); 181 layer.getDisplaySettings().setOffsetBookmark(tempOffset); 182 if (offsetDialog != null) { 183 offsetDialog.updateOffset(); 184 } 185 if (Logging.isDebugEnabled()) { 186 Logging.debug("{0} consuming event {1}", getClass().getName(), kev); 187 } 188 kev.consume(); 189 } 190 } 191 192 @Override 193 public void mousePressed(MouseEvent e) { 194 if (e.getButton() != MouseEvent.BUTTON1) 195 return; 196 197 if (layer.isVisible()) { 198 requestFocusInMapView(); 199 MapView mapView = MainApplication.getMap().mapView; 200 prevEastNorth = mapView.getEastNorth(e.getX(), e.getY()); 201 mapView.setNewCursor(Cursor.MOVE_CURSOR, this); 202 } 203 } 204 205 @Override 206 public void mouseDragged(MouseEvent e) { 207 if (layer == null || prevEastNorth == null) return; 208 EastNorth eastNorth = MainApplication.getMap().mapView.getEastNorth(e.getX(), e.getY()); 209 EastNorth d = tempOffset.getDisplacement().add(eastNorth).subtract(prevEastNorth); 210 tempOffset.setDisplacement(d); 211 layer.getDisplaySettings().setOffsetBookmark(tempOffset); 212 if (offsetDialog != null) { 213 offsetDialog.updateOffset(); 214 } 215 prevEastNorth = eastNorth; 216 } 217 218 @Override 219 public void mouseReleased(MouseEvent e) { 220 MapView mapView = MainApplication.getMap().mapView; 221 mapView.repaint(); 222 mapView.resetCursor(this); 223 prevEastNorth = null; 224 } 225 226 @Override 227 public void actionPerformed(ActionEvent e) { 228 MapFrame map = MainApplication.getMap(); 229 if (offsetDialog != null || layer == null || map == null) 230 return; 231 oldMapMode = map.mapMode; 232 super.actionPerformed(e); 233 } 234 235 private static final class ConfirmOverwriteBookmarkDialog extends ExtendedDialog { 236 ConfirmOverwriteBookmarkDialog() { 237 super(MainApplication.getMainFrame(), tr("Overwrite"), tr("Overwrite"), tr("Cancel")); 238 contentInsets = new Insets(10, 15, 10, 15); 239 setContent(tr("Offset bookmark already exists. Overwrite?")); 240 setButtonIcons("ok", "cancel"); 241 } 242 } 243 244 private class ImageryOffsetDialog extends ExtendedDialog implements FocusListener { 245 private final JosmTextField tOffset = new JosmTextField(); 246 private final JosmTextField tBookmarkName = new JosmTextField(); 247 private boolean ignoreListener; 248 249 /** 250 * Constructs a new {@code ImageryOffsetDialog}. 251 */ 252 ImageryOffsetDialog() { 253 super(MainApplication.getMainFrame(), 254 tr("Adjust imagery offset"), 255 new String[] {tr("OK"), tr("Cancel")}, 256 false, false); // Do not dispose on close, so HIDE_ON_CLOSE remains the default behaviour and setVisible is called 257 setButtonIcons("ok", "cancel"); 258 configureContextsensitiveHelp("Action/ImageryAdjust", true); 259 contentInsets = new Insets(10, 15, 5, 15); 260 JPanel pnl = new JPanel(new GridBagLayout()); 261 pnl.add(new JMultilineLabel(tr("Use arrow keys or drag the imagery layer with mouse to adjust the imagery offset.\n" + 262 "You can also enter east and north offset in the {0} coordinates.\n" + 263 "If you want to save the offset as bookmark, enter the bookmark name below", 264 ProjectionRegistry.getProjection().toString())), GBC.eop()); 265 pnl.add(new JLabel(tr("Offset:")), GBC.std()); 266 pnl.add(tOffset, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 0, 0, 5)); 267 pnl.add(new JLabel(tr("Bookmark name: ")), GBC.std()); 268 pnl.add(tBookmarkName, GBC.eol().fill(GBC.HORIZONTAL)); 269 tOffset.setColumns(16); 270 updateOffsetIntl(); 271 tOffset.addFocusListener(this); 272 setContent(pnl); 273 setupDialog(); 274 setRememberWindowGeometry(getClass().getName() + ".geometry", 275 WindowGeometry.centerInWindow(MainApplication.getMainFrame(), getSize())); 276 } 277 278 private boolean areFieldsInFocus() { 279 return tOffset.hasFocus(); 280 } 281 282 @Override 283 public void focusGained(FocusEvent e) { 284 // Do nothing 285 } 286 287 @Override 288 public void focusLost(FocusEvent e) { 289 if (ignoreListener) return; 290 String ostr = tOffset.getText(); 291 int semicolon = ostr.indexOf(';'); 292 if (layer != null && semicolon >= 0 && semicolon + 1 < ostr.length()) { 293 try { 294 String easting = ostr.substring(0, semicolon).trim(); 295 String northing = ostr.substring(semicolon + 1).trim(); 296 double dx = JosmDecimalFormatSymbolsProvider.parseDouble(easting); 297 double dy = JosmDecimalFormatSymbolsProvider.parseDouble(northing); 298 tempOffset.setDisplacement(new EastNorth(dx, dy)); 299 layer.getDisplaySettings().setOffsetBookmark(tempOffset); 300 } catch (NumberFormatException nfe) { 301 // we repaint offset numbers in any case 302 Logging.trace(nfe); 303 } 304 } 305 updateOffsetIntl(); 306 if (layer != null) { 307 layer.invalidate(); 308 } 309 } 310 311 private void updateOffset() { 312 ignoreListener = true; 313 updateOffsetIntl(); 314 ignoreListener = false; 315 } 316 317 private void updateOffsetIntl() { 318 if (layer != null) { 319 // ROOT locale to force decimal separator to be '.' 320 tOffset.setText(layer.getDisplaySettings().getDisplacementString(Locale.ROOT)); 321 } 322 } 323 324 private boolean confirmOverwriteBookmark() { 325 return new ConfirmOverwriteBookmarkDialog().showDialog().getValue() == 1; 326 } 327 328 @Override 329 protected void buttonAction(int buttonIndex, ActionEvent evt) { 330 restoreOldMode = true; 331 if (buttonIndex == 0 && !Utils.isEmpty(tBookmarkName.getText()) && 332 OffsetBookmark.getBookmarkByName(layer, tBookmarkName.getText()) != null && 333 !confirmOverwriteBookmark()) { 334 return; 335 } 336 super.buttonAction(buttonIndex, evt); 337 } 338 339 @Override 340 public void setVisible(boolean visible) { 341 super.setVisible(visible); 342 if (visible) 343 return; 344 ignoreListener = true; 345 offsetDialog = null; 346 if (layer != null) { 347 if (getValue() != 1) { 348 layer.getDisplaySettings().setOffsetBookmark(old); 349 } else if (!Utils.isEmpty(tBookmarkName.getText())) { 350 OffsetBookmark.bookmarkOffset(tBookmarkName.getText(), layer); 351 } 352 } 353 MainApplication.getMenu().imageryMenu.refreshOffsetMenu(); 354 restoreMapModeState(); 355 } 356 357 private void restoreMapModeState() { 358 MapFrame map = MainApplication.getMap(); 359 if (map == null) 360 return; 361 if (oldMapMode != null) { 362 if (restoreOldMode || (!exitingMode && getValue() == ExtendedDialog.DialogClosedOtherwise)) { 363 map.selectMapMode(oldMapMode); 364 } 365 oldMapMode = null; 366 } else if (!exitingMode && !map.selectSelectTool(false)) { 367 exitModeAndRestoreOldMode(); 368 map.mapMode = null; 369 } 370 } 371 372 private void exitModeAndRestoreOldMode() { 373 restoreOldMode = true; 374 doExitMode(); 375 restoreOldMode = false; 376 } 377 } 378 379 @Override 380 public void destroy() { 381 super.destroy(); 382 removeListeners(); 383 this.layer = null; 384 this.oldMapMode = null; 385 } 386}