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.awt.event.WindowAdapter; 018import java.awt.event.WindowEvent; 019import java.util.Formatter; 020import java.util.Locale; 021 022import javax.swing.JLabel; 023import javax.swing.JPanel; 024 025import org.openstreetmap.josm.Main; 026import org.openstreetmap.josm.actions.mapmode.MapMode; 027import org.openstreetmap.josm.data.coor.EastNorth; 028import org.openstreetmap.josm.data.imagery.OffsetBookmark; 029import org.openstreetmap.josm.gui.ExtendedDialog; 030import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer; 031import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings; 032import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 033import org.openstreetmap.josm.gui.widgets.JosmTextField; 034import org.openstreetmap.josm.tools.GBC; 035import org.openstreetmap.josm.tools.ImageProvider; 036 037/** 038 * Adjust the position of an imagery layer. 039 * @since 3715 040 */ 041public class ImageryAdjustAction extends MapMode implements AWTEventListener { 042 private static volatile ImageryOffsetDialog offsetDialog; 043 private static Cursor cursor = ImageProvider.getCursor("normal", "move"); 044 045 private EastNorth old; 046 private EastNorth prevEastNorth; 047 private transient AbstractTileSourceLayer<?> layer; 048 private MapMode oldMapMode; 049 050 /** 051 * Constructs a new {@code ImageryAdjustAction} for the given layer. 052 * @param layer The imagery layer 053 */ 054 public ImageryAdjustAction(AbstractTileSourceLayer<?> layer) { 055 super(tr("New offset"), "adjustimg", 056 tr("Adjust the position of this imagery layer"), Main.map, 057 cursor); 058 putValue("toolbar", Boolean.FALSE); 059 this.layer = layer; 060 } 061 062 @Override 063 public void enterMode() { 064 super.enterMode(); 065 if (layer == null) 066 return; 067 if (!layer.isVisible()) { 068 layer.setVisible(true); 069 } 070 old = layer.getDisplaySettings().getDisplacement(); 071 addListeners(); 072 offsetDialog = new ImageryOffsetDialog(); 073 offsetDialog.setVisible(true); 074 } 075 076 protected void addListeners() { 077 Main.map.mapView.addMouseListener(this); 078 Main.map.mapView.addMouseMotionListener(this); 079 try { 080 Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK); 081 } catch (SecurityException ex) { 082 Main.error(ex); 083 } 084 } 085 086 @Override 087 public void exitMode() { 088 super.exitMode(); 089 if (offsetDialog != null) { 090 if (layer != null) { 091 layer.getDisplaySettings().setDisplacement(old); 092 } 093 offsetDialog.setVisible(false); 094 // do not restore old mode here - this is called when the new mode is already known. 095 offsetDialog = null; 096 } 097 removeListeners(); 098 } 099 100 protected void removeListeners() { 101 try { 102 Toolkit.getDefaultToolkit().removeAWTEventListener(this); 103 } catch (SecurityException ex) { 104 Main.error(ex); 105 } 106 if (Main.isDisplayingMapView()) { 107 Main.map.mapView.removeMouseMotionListener(this); 108 Main.map.mapView.removeMouseListener(this); 109 } 110 } 111 112 @Override 113 public void eventDispatched(AWTEvent event) { 114 if (!(event instanceof KeyEvent) 115 || (event.getID() != KeyEvent.KEY_PRESSED) 116 || (layer == null) 117 || (offsetDialog != null && offsetDialog.areFieldsInFocus())) { 118 return; 119 } 120 KeyEvent kev = (KeyEvent) event; 121 int dx = 0; 122 int dy = 0; 123 switch (kev.getKeyCode()) { 124 case KeyEvent.VK_UP : dy = +1; break; 125 case KeyEvent.VK_DOWN : dy = -1; break; 126 case KeyEvent.VK_LEFT : dx = -1; break; 127 case KeyEvent.VK_RIGHT : dx = +1; break; 128 default: // Do nothing 129 } 130 if (dx != 0 || dy != 0) { 131 double ppd = layer.getPPD(); 132 layer.getDisplaySettings().addDisplacement(new EastNorth(dx / ppd, dy / ppd)); 133 if (offsetDialog != null) { 134 offsetDialog.updateOffset(); 135 } 136 if (Main.isDebugEnabled()) { 137 Main.debug(getClass().getName()+" consuming event "+kev); 138 } 139 kev.consume(); 140 } 141 } 142 143 @Override 144 public void mousePressed(MouseEvent e) { 145 if (e.getButton() != MouseEvent.BUTTON1) 146 return; 147 148 if (layer.isVisible()) { 149 requestFocusInMapView(); 150 prevEastNorth = Main.map.mapView.getEastNorth(e.getX(), e.getY()); 151 Main.map.mapView.setNewCursor(Cursor.MOVE_CURSOR, this); 152 } 153 } 154 155 @Override 156 public void mouseDragged(MouseEvent e) { 157 if (layer == null || prevEastNorth == null) return; 158 EastNorth eastNorth = Main.map.mapView.getEastNorth(e.getX(), e.getY()); 159 EastNorth d = layer.getDisplaySettings().getDisplacement().add(eastNorth).subtract(prevEastNorth); 160 layer.getDisplaySettings().setDisplacement(d); 161 if (offsetDialog != null) { 162 offsetDialog.updateOffset(); 163 } 164 prevEastNorth = eastNorth; 165 } 166 167 @Override 168 public void mouseReleased(MouseEvent e) { 169 Main.map.mapView.repaint(); 170 Main.map.mapView.resetCursor(this); 171 prevEastNorth = null; 172 } 173 174 @Override 175 public void actionPerformed(ActionEvent e) { 176 if (offsetDialog != null || layer == null || Main.map == null) 177 return; 178 oldMapMode = Main.map.mapMode; 179 super.actionPerformed(e); 180 } 181 182 private class ImageryOffsetDialog extends ExtendedDialog implements FocusListener { 183 private final JosmTextField tOffset = new JosmTextField(); 184 private final JosmTextField tBookmarkName = new JosmTextField(); 185 private boolean ignoreListener; 186 187 /** 188 * Constructs a new {@code ImageryOffsetDialog}. 189 */ 190 ImageryOffsetDialog() { 191 super(Main.parent, 192 tr("Adjust imagery offset"), 193 new String[] {tr("OK"), tr("Cancel")}, 194 false); 195 setButtonIcons(new String[] {"ok", "cancel"}); 196 contentInsets = new Insets(10, 15, 5, 15); 197 JPanel pnl = new JPanel(new GridBagLayout()); 198 pnl.add(new JMultilineLabel(tr("Use arrow keys or drag the imagery layer with mouse to adjust the imagery offset.\n" + 199 "You can also enter east and north offset in the {0} coordinates.\n" + 200 "If you want to save the offset as bookmark, enter the bookmark name below", 201 Main.getProjection().toString())), GBC.eop()); 202 pnl.add(new JLabel(tr("Offset: ")), GBC.std()); 203 pnl.add(tOffset, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 0, 0, 5)); 204 pnl.add(new JLabel(tr("Bookmark name: ")), GBC.std()); 205 pnl.add(tBookmarkName, GBC.eol().fill(GBC.HORIZONTAL)); 206 tOffset.setColumns(16); 207 updateOffsetIntl(); 208 tOffset.addFocusListener(this); 209 setContent(pnl); 210 setupDialog(); 211 addWindowListener(new WindowEventHandler()); 212 } 213 214 private boolean areFieldsInFocus() { 215 return tOffset.hasFocus(); 216 } 217 218 @Override 219 public void focusGained(FocusEvent e) { 220 // Do nothing 221 } 222 223 @Override 224 public void focusLost(FocusEvent e) { 225 if (ignoreListener) return; 226 String ostr = tOffset.getText(); 227 int semicolon = ostr.indexOf(';'); 228 if (layer != null && semicolon >= 0 && semicolon + 1 < ostr.length()) { 229 try { 230 // here we assume that Double.parseDouble() needs '.' as a decimal separator 231 String easting = ostr.substring(0, semicolon).trim().replace(',', '.'); 232 String northing = ostr.substring(semicolon + 1).trim().replace(',', '.'); 233 double dx = Double.parseDouble(easting); 234 double dy = Double.parseDouble(northing); 235 layer.getDisplaySettings().setDisplacement(new EastNorth(dx, dy)); 236 } catch (NumberFormatException nfe) { 237 // we repaint offset numbers in any case 238 Main.trace(nfe); 239 } 240 } 241 updateOffsetIntl(); 242 if (Main.isDisplayingMapView()) { 243 Main.map.repaint(); 244 } 245 } 246 247 private void updateOffset() { 248 ignoreListener = true; 249 updateOffsetIntl(); 250 ignoreListener = false; 251 } 252 253 private void updateOffsetIntl() { 254 if (layer != null) { 255 // Support projections with very small numbers (e.g. 4326) 256 int precision = Main.getProjection().getDefaultZoomInPPD() >= 1.0 ? 2 : 7; 257 // US locale to force decimal separator to be '.' 258 try (Formatter us = new Formatter(Locale.US)) { 259 TileSourceDisplaySettings ds = layer.getDisplaySettings(); 260 tOffset.setText(us.format(new StringBuilder() 261 .append("%1.").append(precision).append("f; %1.").append(precision).append('f').toString(), 262 ds.getDx(), ds.getDy()).toString()); 263 } 264 } 265 } 266 267 private boolean confirmOverwriteBookmark() { 268 ExtendedDialog dialog = new ExtendedDialog( 269 Main.parent, 270 tr("Overwrite"), 271 new String[] {tr("Overwrite"), tr("Cancel")} 272 ) { { 273 contentInsets = new Insets(10, 15, 10, 15); 274 } }; 275 dialog.setContent(tr("Offset bookmark already exists. Overwrite?")); 276 dialog.setButtonIcons(new String[] {"ok.png", "cancel.png"}); 277 dialog.setupDialog(); 278 dialog.setVisible(true); 279 return dialog.getValue() == 1; 280 } 281 282 @Override 283 protected void buttonAction(int buttonIndex, ActionEvent evt) { 284 if (buttonIndex == 0 && tBookmarkName.getText() != null && !tBookmarkName.getText().isEmpty() && 285 OffsetBookmark.getBookmarkByName(layer, tBookmarkName.getText()) != null && 286 !confirmOverwriteBookmark()) { 287 return; 288 } 289 super.buttonAction(buttonIndex, evt); 290 restoreMapModeState(); 291 } 292 293 @Override 294 public void setVisible(boolean visible) { 295 super.setVisible(visible); 296 if (visible) 297 return; 298 offsetDialog = null; 299 if (layer != null) { 300 if (getValue() != 1) { 301 layer.getDisplaySettings().setDisplacement(old); 302 } else if (tBookmarkName.getText() != null && !tBookmarkName.getText().isEmpty()) { 303 OffsetBookmark.bookmarkOffset(tBookmarkName.getText(), layer); 304 } 305 } 306 Main.main.menu.imageryMenu.refreshOffsetMenu(); 307 } 308 309 private void restoreMapModeState() { 310 if (Main.map == null) 311 return; 312 if (oldMapMode != null) { 313 Main.map.selectMapMode(oldMapMode); 314 oldMapMode = null; 315 } else { 316 Main.map.selectSelectTool(false); 317 } 318 } 319 320 class WindowEventHandler extends WindowAdapter { 321 @Override 322 public void windowClosing(WindowEvent e) { 323 setVisible(false); 324 restoreMapModeState(); 325 } 326 } 327 } 328 329 @Override 330 public void destroy() { 331 super.destroy(); 332 removeListeners(); 333 this.layer = null; 334 this.oldMapMode = null; 335 } 336}