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