// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.actions;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.AWTEvent;
import java.awt.Cursor;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.AWTEventListener;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Formatter;
import java.util.Locale;
import javax.swing.JLabel;
import javax.swing.JPanel;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.mapmode.MapMode;
import org.openstreetmap.josm.data.coor.EastNorth;
import org.openstreetmap.josm.data.imagery.OffsetBookmark;
import org.openstreetmap.josm.gui.ExtendedDialog;
import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings;
import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
import org.openstreetmap.josm.gui.widgets.JosmTextField;
import org.openstreetmap.josm.tools.GBC;
import org.openstreetmap.josm.tools.ImageProvider;
/**
* Adjust the position of an imagery layer.
* @since 3715
*/
public class ImageryAdjustAction extends MapMode implements AWTEventListener {
private static volatile ImageryOffsetDialog offsetDialog;
private static Cursor cursor = ImageProvider.getCursor("normal", "move");
private EastNorth old;
private EastNorth prevEastNorth;
private transient AbstractTileSourceLayer<?> layer;
private MapMode oldMapMode;
/**
* Constructs a new {@code ImageryAdjustAction} for the given layer.
* @param layer The imagery layer
*/
public ImageryAdjustAction(AbstractTileSourceLayer<?> layer) {
super(tr("New offset"), "adjustimg", tr("Adjust the position of this imagery layer"), cursor);
putValue("toolbar", Boolean.FALSE);
this.layer = layer;
}
@Override
public void enterMode() {
super.enterMode();
if (layer == null)
return;
if (!layer.isVisible()) {
layer.setVisible(true);
}
old = layer.getDisplaySettings().getDisplacement();
addListeners();
showOffsetDialog(new ImageryOffsetDialog());
}
private static void showOffsetDialog(ImageryOffsetDialog dlg) {
offsetDialog = dlg;
offsetDialog.setVisible(true);
}
private static void hideOffsetDialog() {
offsetDialog.setVisible(false);
offsetDialog = null;
}
protected void addListeners() {
Main.map.mapView.addMouseListener(this);
Main.map.mapView.addMouseMotionListener(this);
try {
Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
} catch (SecurityException ex) {
Main.error(ex);
}
}
@Override
public void exitMode() {
super.exitMode();
if (offsetDialog != null) {
if (layer != null) {
layer.getDisplaySettings().setDisplacement(old);
}
hideOffsetDialog();
// do not restore old mode here - this is called when the new mode is already known.
}
removeListeners();
}
protected void removeListeners() {
try {
Toolkit.getDefaultToolkit().removeAWTEventListener(this);
} catch (SecurityException ex) {
Main.error(ex);
}
if (Main.isDisplayingMapView()) {
Main.map.mapView.removeMouseMotionListener(this);
Main.map.mapView.removeMouseListener(this);
}
}
@Override
public void eventDispatched(AWTEvent event) {
if (!(event instanceof KeyEvent)
|| (event.getID() != KeyEvent.KEY_PRESSED)
|| (layer == null)
|| (offsetDialog != null && offsetDialog.areFieldsInFocus())) {
return;
}
KeyEvent kev = (KeyEvent) event;
int dx = 0;
int dy = 0;
switch (kev.getKeyCode()) {
case KeyEvent.VK_UP : dy = +1; break;
case KeyEvent.VK_DOWN : dy = -1; break;
case KeyEvent.VK_LEFT : dx = -1; break;
case KeyEvent.VK_RIGHT : dx = +1; break;
default: // Do nothing
}
if (dx != 0 || dy != 0) {
double ppd = layer.getPPD();
layer.getDisplaySettings().addDisplacement(new EastNorth(dx / ppd, dy / ppd));
if (offsetDialog != null) {
offsetDialog.updateOffset();
}
if (Main.isDebugEnabled()) {
Main.debug(getClass().getName()+" consuming event "+kev);
}
kev.consume();
}
}
@Override
public void mousePressed(MouseEvent e) {
if (e.getButton() != MouseEvent.BUTTON1)
return;
if (layer.isVisible()) {
requestFocusInMapView();
prevEastNorth = Main.map.mapView.getEastNorth(e.getX(), e.getY());
Main.map.mapView.setNewCursor(Cursor.MOVE_CURSOR, this);
}
}
@Override
public void mouseDragged(MouseEvent e) {
if (layer == null || prevEastNorth == null) return;
EastNorth eastNorth = Main.map.mapView.getEastNorth(e.getX(), e.getY());
EastNorth d = layer.getDisplaySettings().getDisplacement().add(eastNorth).subtract(prevEastNorth);
layer.getDisplaySettings().setDisplacement(d);
if (offsetDialog != null) {
offsetDialog.updateOffset();
}
prevEastNorth = eastNorth;
}
@Override
public void mouseReleased(MouseEvent e) {
Main.map.mapView.repaint();
Main.map.mapView.resetCursor(this);
prevEastNorth = null;
}
@Override
public void actionPerformed(ActionEvent e) {
if (offsetDialog != null || layer == null || Main.map == null)
return;
oldMapMode = Main.map.mapMode;
super.actionPerformed(e);
}
private class ImageryOffsetDialog extends ExtendedDialog implements FocusListener {
private final JosmTextField tOffset = new JosmTextField();
private final JosmTextField tBookmarkName = new JosmTextField();
private boolean ignoreListener;
/**
* Constructs a new {@code ImageryOffsetDialog}.
*/
ImageryOffsetDialog() {
super(Main.parent,
tr("Adjust imagery offset"),
new String[] {tr("OK"), tr("Cancel")},
false);
setButtonIcons(new String[] {"ok", "cancel"});
contentInsets = new Insets(10, 15, 5, 15);
JPanel pnl = new JPanel(new GridBagLayout());
pnl.add(new JMultilineLabel(tr("Use arrow keys or drag the imagery layer with mouse to adjust the imagery offset.\n" +
"You can also enter east and north offset in the {0} coordinates.\n" +
"If you want to save the offset as bookmark, enter the bookmark name below",
Main.getProjection().toString())), GBC.eop());
pnl.add(new JLabel(tr("Offset: ")), GBC.std());
pnl.add(tOffset, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 0, 0, 5));
pnl.add(new JLabel(tr("Bookmark name: ")), GBC.std());
pnl.add(tBookmarkName, GBC.eol().fill(GBC.HORIZONTAL));
tOffset.setColumns(16);
updateOffsetIntl();
tOffset.addFocusListener(this);
setContent(pnl);
setupDialog();
addWindowListener(new WindowEventHandler());
}
private boolean areFieldsInFocus() {
return tOffset.hasFocus();
}
@Override
public void focusGained(FocusEvent e) {
// Do nothing
}
@Override
public void focusLost(FocusEvent e) {
if (ignoreListener) return;
String ostr = tOffset.getText();
int semicolon = ostr.indexOf(';');
if (layer != null && semicolon >= 0 && semicolon + 1 < ostr.length()) {
try {
// here we assume that Double.parseDouble() needs '.' as a decimal separator
String easting = ostr.substring(0, semicolon).trim().replace(',', '.');
String northing = ostr.substring(semicolon + 1).trim().replace(',', '.');
double dx = Double.parseDouble(easting);
double dy = Double.parseDouble(northing);
layer.getDisplaySettings().setDisplacement(new EastNorth(dx, dy));
} catch (NumberFormatException nfe) {
// we repaint offset numbers in any case
Main.trace(nfe);
}
}
updateOffsetIntl();
if (Main.isDisplayingMapView()) {
Main.map.repaint();
}
}
private void updateOffset() {
ignoreListener = true;
updateOffsetIntl();
ignoreListener = false;
}
private void updateOffsetIntl() {
if (layer != null) {
// Support projections with very small numbers (e.g. 4326)
int precision = Main.getProjection().getDefaultZoomInPPD() >= 1.0 ? 2 : 7;
// US locale to force decimal separator to be '.'
try (Formatter us = new Formatter(Locale.US)) {
TileSourceDisplaySettings ds = layer.getDisplaySettings();
tOffset.setText(us.format(new StringBuilder()
.append("%1.").append(precision).append("f; %1.").append(precision).append('f').toString(),
ds.getDx(), ds.getDy()).toString());
}
}
}
private boolean confirmOverwriteBookmark() {
ExtendedDialog dialog = new ExtendedDialog(
Main.parent,
tr("Overwrite"),
new String[] {tr("Overwrite"), tr("Cancel")}
) { {
contentInsets = new Insets(10, 15, 10, 15);
} };
dialog.setContent(tr("Offset bookmark already exists. Overwrite?"));
dialog.setButtonIcons(new String[] {"ok.png", "cancel.png"});
dialog.setupDialog();
dialog.setVisible(true);
return dialog.getValue() == 1;
}
@Override
protected void buttonAction(int buttonIndex, ActionEvent evt) {
if (buttonIndex == 0 && tBookmarkName.getText() != null && !tBookmarkName.getText().isEmpty() &&
OffsetBookmark.getBookmarkByName(layer, tBookmarkName.getText()) != null &&
!confirmOverwriteBookmark()) {
return;
}
super.buttonAction(buttonIndex, evt);
restoreMapModeState();
}
@Override
public void setVisible(boolean visible) {
super.setVisible(visible);
if (visible)
return;
offsetDialog = null;
if (layer != null) {
if (getValue() != 1) {
layer.getDisplaySettings().setDisplacement(old);
} else if (tBookmarkName.getText() != null && !tBookmarkName.getText().isEmpty()) {
OffsetBookmark.bookmarkOffset(tBookmarkName.getText(), layer);
}
}
Main.main.menu.imageryMenu.refreshOffsetMenu();
}
private void restoreMapModeState() {
if (Main.map == null)
return;
if (oldMapMode != null) {
Main.map.selectMapMode(oldMapMode);
oldMapMode = null;
} else {
Main.map.selectSelectTool(false);
}
}
class WindowEventHandler extends WindowAdapter {
@Override
public void windowClosing(WindowEvent e) {
setVisible(false);
restoreMapModeState();
}
}
}
@Override
public void destroy() {
super.destroy();
removeListeners();
this.layer = null;
this.oldMapMode = null;
}
}