package org.openstreetmap.josm.plugins.photoadjust;
//import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.Color;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.JosmAction;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.gui.ExtendedDialog;
import org.openstreetmap.josm.gui.MainMenu;
import org.openstreetmap.josm.gui.dialogs.LatLonDialog;
import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer;
import org.openstreetmap.josm.gui.layer.geoimage.ImageEntry;
import org.openstreetmap.josm.gui.layer.geoimage.ImageViewerDialog;
import org.openstreetmap.josm.gui.widgets.JosmTextField;
import org.openstreetmap.josm.tools.GBC;
import org.openstreetmap.josm.tools.ImageProvider;
/**
* Simple editor for photo GPS data.
*/
public class PhotoPropertyEditor {
public PhotoPropertyEditor() {
MainMenu.add(Main.main.menu.editMenu, new PropertyEditorAction());
}
/**
* Update the geo image layer and the image viewer.
*
* @param layer GeoImageLayer of the photo.
* @param photo The photo that is updated.
*/
private static void updateLayer(GeoImageLayer layer, ImageEntry photo) {
layer.updateBufferAndRepaint();
ImageViewerDialog.showImage(layer, photo);
}
/**
* Action if the menu entry is selected.
*/
private static class PropertyEditorAction extends JosmAction {
public PropertyEditorAction() {
super(tr("Edit photo GPS data"), // String name
(String)null, // String iconName
tr("Edit GPS data of selected photo."), // String tooltip
null, // Shortcut shortcut
true, // boolean registerInToolbar
"photoadjust/propertyeditor", // String toolbarId
true // boolean installAdapters
);
//putValue("help", ht("/Action/..."));
}
@Override
public void actionPerformed(ActionEvent evt) {
try {
final ImageEntry photo = ImageViewerDialog.getCurrentImage();
final GeoImageLayer layer = ImageViewerDialog.getCurrentLayer();
if (photo == null) {
throw new AssertionError("No image selected.");
}
StringBuilder title =
new StringBuilder(tr("Edit Photo GPS Data"));
if (photo.getFile() != null) {
title.append(" - ");
title.append(photo.getFile().getName());
}
PropertyEditorDialog dialog =
new PropertyEditorDialog(title.toString(), photo, layer);
if (dialog.getValue() == 1) {
dialog.updateImageTmp();
photo.applyTmp();
} else {
photo.discardTmp();
}
updateLayer(layer, photo);
} catch (AssertionError err) {
JOptionPane.showMessageDialog(Main.parent,
tr("Please select an image first."),
tr("No image selected"),
JOptionPane.INFORMATION_MESSAGE);
return;
}
}
/**
* Check if there is a selected image.
*
* @return {@code true} if the image viewer exists and there is an
* image shown, {@code false} otherwise.
*/
private boolean enabled() {
try {
//return ImageViewerDialog.getInstance().hasImage();
ImageViewerDialog.getInstance().hasImage();
return true;
} catch (AssertionError err) {
return false;
}
}
@Override
protected void updateEnabledState() {
setEnabled(enabled());
}
}
/**
* The actual photo property editor dialog.
*/
private static class PropertyEditorDialog extends ExtendedDialog {
private final JosmTextField coords = new JosmTextField(24);
private final JosmTextField altitude = new JosmTextField();
private final JosmTextField speed = new JosmTextField();
private final JosmTextField direction = new JosmTextField();
// Image that is to be updated.
private final ImageEntry image;
private final GeoImageLayer layer;
private static final Color BG_COLOR_ERROR = new Color(255, 224, 224);
public PropertyEditorDialog(String title, final ImageEntry image,
final GeoImageLayer layer) {
super(Main.parent, title, new String[] {tr("Ok"), tr("Cancel")});
this.image = image;
this.layer = layer;
setButtonIcons(new String[] {"ok", "cancel"});
final JPanel content = new JPanel(new GridBagLayout());
//content.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
if (image.hasThumbnail() || image.getFile() != null) {
final JLabel header = new JLabel(image.getFile() != null
? image.getFile().getName()
: "");
if (!image.hasThumbnail()) {
image.loadThumbnail();
}
if (image.hasThumbnail()) {
header.setIcon(new ImageIcon(image.getThumbnail()));
}
content.add(header, GBC.eol().fill().weight(1.0, 1.0));
}
content.add(new JSeparator(),
GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5));
content.add(new JLabel(tr("(Empty values delete the according fields.)")),
GBC.eol());
// Coordinates.
coords.setHint(tr("coordinates"));
//coords.setEditable(false);
LatLonInputVerifier coordsVerif = new LatLonInputVerifier();
coords.getDocument().addDocumentListener(coordsVerif);
coords.setToolTipText(tr("Latitude and longitude"));
content.add(new JLabel(tr("Coordinates:")),
GBC.std().insets(0, 0, 5, 0));
content.add(coords, GBC.std().fill(GBC.HORIZONTAL));
Action editCoordAction = new AbstractAction(tr("Edit")) {
@Override public void actionPerformed(ActionEvent evt) {
final LatLonDialog dialog
= new LatLonDialog(Main.parent,
tr("Edit Image Coordinates"), null);
dialog.setCoordinates(getLatLon());
dialog.showDialog();
if (dialog.getValue() == 1) {
LatLon coordinates = dialog.getCoordinates();
if (coordinates != null) {
coords.setText(coordinates.toStringCSV(" "));
}
}
}
};
final JButton editCoordBtn = new JButton(editCoordAction);
editCoordBtn
.setToolTipText(tr("Edit coordinates in separate editor"));
content.add(editCoordBtn, GBC.eol());
// Altitude/elevation.
altitude.setHint(tr("altitude"));
DoubleInputVerifier altVerif = new DoubleInputVerifier(altitude) {
@Override public void updateValue(Double value) {
image.getTmp().setElevation(value);
updateLayer(layer, image);
}
};
altitude.getDocument().addDocumentListener(altVerif);
content.add(new JLabel(tr("Altitude:")),
GBC.std().insets(0, 0, 5, 0));
content.add(altitude, GBC.std().fill(GBC.HORIZONTAL));
content.add(new JLabel(/* unit: meter */ tr("m")), GBC.eol());
// Speed.
speed.setHint(tr("speed"));
DoubleInputVerifier speedVerif = new DoubleInputVerifier(speed) {
@Override public void updateValue(Double value) {
image.getTmp().setSpeed(value);
updateLayer(layer, image);
}
};
speedVerif.setMinMax(0.0, null);
speed.getDocument().addDocumentListener(speedVerif);
speed.setToolTipText(tr("positive number or empty"));
content.add(new JLabel(tr("Speed:")),
GBC.std().insets(0, 0, 5, 0));
content.add(speed, GBC.std().fill(GBC.HORIZONTAL));
content.add(new JLabel(tr("km/h")), GBC.eol());
// Image direction.
direction.setHint(tr("direction"));
DoubleInputVerifier dirVerif = new DoubleInputVerifier(direction) {
@Override public void updateValue(Double value) {
image.getTmp().setExifImgDir(value);
updateLayer(layer, image);
}
};
dirVerif.setMinMax(-360.0, 360.0);
direction.getDocument().addDocumentListener(dirVerif);
direction.setToolTipText(tr("range -360.0 .. 360.0, or empty"));
content.add(new JLabel(tr("Direction:")),
GBC.std().insets(0, 0, 5, 0));
content.add(direction, GBC.std().fill(GBC.HORIZONTAL));
content.add(new JLabel(/* unit: degree (angle) */ tr("\u00b0")),
GBC.eol());
setInitialValues();
// Button row.
final JPanel buttonsPanel = new JPanel(new GridBagLayout());
// Undo.
Action undoAction = new AbstractAction(tr("Undo")) {
@Override public void actionPerformed(ActionEvent evt) {
setInitialValues();
}
};
final JButton undoButton = new JButton(undoAction);
undoButton.setToolTipText(tr("Undo changes made in this dialog"));
undoButton.setIcon(ImageProvider.get("undo"));
buttonsPanel.add(undoButton, GBC.std().insets(2, 2, 2, 2));
// Reload.
Action reloadAction = new AbstractAction(tr("Reload")) {
@Override public void actionPerformed(ActionEvent evt) {
final ImageEntry imgTmp = new ImageEntry(image.getFile());
imgTmp.extractExif();
setInitialValues(imgTmp);
}
};
final JButton reloadButton = new JButton(reloadAction);
reloadButton.setToolTipText(tr("Reload GPS data from image file"));
reloadButton.setIcon(ImageProvider.get("dialogs/refresh"));
buttonsPanel.add(reloadButton, GBC.std().insets(2, 2, 2, 2));
// // Apply.
// Action applyAction = new AbstractAction(tr("Apply")) {
// @Override public void actionPerformed(ActionEvent evt) {
// updateImageTmp();
// updateLayer(layer, image);
// }
// };
// final JButton applyButton = new JButton(applyAction);
// applyButton.setToolTipText(tr("Apply changes, keep dialog open"));
// applyButton.setIcon(ImageProvider.get("apply"));
// // fill(VERTICAL) to make the button the same height than the
// // other buttons. This is needed because the apply icon is
// // smaller.
// buttonsPanel.add(applyButton,
// GBC.std().insets(2, 2, 2, 2).fill(GBC.VERTICAL));
// Add buttons to form.
content.add(buttonsPanel, GBC.eol().insets(0, 5, 0, 5));
// The false in the next line makes it a dynamic width, but the
// scroll bars are gone...
setContent(content, false);
showDialog();
}
/**
* Initialize the dialog with image data. Uses the image the dialog
* was started with.
*/
private void setInitialValues() {
image.discardTmp();
setInitialValues(image);
}
/**
* Initialize the dialog with image data. The image can be specified.
*
* @param image Use the data of this image.
*/
private void setInitialValues(ImageEntry image) {
if (image.getPos() != null) {
//coords.setText(image.getPos().toDisplayString());
coords.setText(image.getPos().toStringCSV(" "));
} else {
coords.setText(null);
}
if (image.getElevation() != null) {
altitude.setText(image.getElevation().toString());
} else {
altitude.setText(null);
}
if (image.getSpeed() != null) {
speed.setText(image.getSpeed().toString());
} else {
speed.setText(null);
}
if (image.getExifImgDir() != null) {
direction.setText(image.getExifImgDir().toString());
} else {
direction.setText(null);
}
}
/**
* Check if the value of a dialog field is different from a value of
* type {@code Double}.
*
* @param txtFld Dialog text field.
* @param value Double value to compare with.
* @return {@code true} if the values differ, {@code false} otherwise.
*/
private boolean isDoubleFieldDifferent(JosmTextField txtFld,
Double value) {
final Double fieldValue = getDoubleValue(txtFld);
if (fieldValue == null) {
if (value != null) {
return true;
}
} else {
if (value == null ||
// Comparison of 'double' so -0.0 is equal to +0.0.
fieldValue.doubleValue() != value.doubleValue()) {
return true;
}
}
return false;
}
/**
* Convert dialog field value to {@code Double}.
*
* @param txtFld Dialog text field.
* @return Dialog field converted to {@code Double}. {@code null} if
* the field is empty or if the field value cannot be
* converted to double.
*/
private Double getDoubleValue(JosmTextField txtFld) {
final String text = txtFld.getText();
if (text == null || text.isEmpty()) {
return null;
}
try {
return Double.valueOf(text);
} catch(NumberFormatException nfe) {
return null;
}
}
/**
* Copy the values from the dialog to the temporary image copy.
*/
public void updateImageTmp() {
ImageEntry imgTmp = image.getTmp();
String text = coords.getText();
if (text == null || text.isEmpty()) {
if (imgTmp.getPos() != null) {
imgTmp.flagNewGpsData();
imgTmp.setPos(null);
}
} else {
if ( imgTmp.getPos() == null
|| !text.equals(imgTmp.getPos().toStringCSV(" "))) {
imgTmp.flagNewGpsData();
imgTmp.setPos(getLatLon());
}
}
if (isDoubleFieldDifferent(altitude, imgTmp.getElevation())) {
imgTmp.flagNewGpsData();
imgTmp.setElevation(getDoubleValue(altitude));
}
if (isDoubleFieldDifferent(speed, imgTmp.getSpeed())) {
imgTmp.flagNewGpsData();
imgTmp.setSpeed(getDoubleValue(speed));
}
if (isDoubleFieldDifferent(direction, imgTmp.getExifImgDir())) {
imgTmp.flagNewGpsData();
Double imgDir = getDoubleValue(direction);
if (imgDir != null) {
if (imgDir < 0.0) {
imgDir %= 360.0; // >-360.0...-0.0
imgDir += 360.0; // >0.0...360.0
}
if (imgDir >= 360.0) {
imgDir %= 360.0;
}
}
imgTmp.setExifImgDir(imgDir);
}
}
/**
* Parse coordinate text into LatLon.
*
* @return Coordinates, {@code null} if no coordinates set or if they
* are not valid.
*/
private LatLon getLatLon() {
LatLon latLon;
try {
latLon = LatLon.parse(coords.getText());
if (!latLon.isValid()) {
latLon = null;
}
} catch (IllegalArgumentException exn) {
latLon = null;
}
return latLon;
}
/**
* Parse the coordinate dialog field. Set the error marker if the
* field value is not valid.
*
* @return Coordinates converted to {@code LatLon}. {@code null} if
* the dialog field is empty or if the field value cannot be
* converted.
*/
protected LatLon parseLatLonUserInput() {
LatLon latLon;
final String coordsText = coords.getText();
try {
latLon = LatLon.parse(coordsText);
} catch (IllegalArgumentException exn) {
latLon = null;
}
if ( latLon == null && coordsText != null
&& !coordsText.isEmpty()) {
setErrorFeedback(coords);
setOkEnabled(false);
latLon = null;
} else {
clearErrorFeedback(coords);
setOkEnabled(true);
}
return latLon;
}
/**
* Parse a dialog field that displays a value of type {@code Double}.
* Set the error marker if the field value is not valid.
*
* @param txtFld Dialog text field.
* @param min Minimum value. Set to {@code null} if there is no
* minimum.
* @param max Maximum value. Set to {@code null} if there is no
* maximum.
* @return Parsed form value. {@code null} if
* the dialog field is empty, if the field value cannot be
* converted, or if the value is not within the limits.
*/
protected Double parseDoubleUserInput(JosmTextField txtFld,
Double min, Double max) {
boolean isError = false;
final String text = txtFld.getText();
Double value = null;
if (text == null || text.isEmpty()) {
isError = false;
} else {
try {
value = Double.parseDouble(text);
if (min != null && value < min) {
isError = true;
}
if (max != null && value > max) {
isError = true;
}
} catch(NumberFormatException nfe) {
isError = true;
}
}
if (isError) {
setErrorFeedback(txtFld);
setOkEnabled(false);
value = null;
} else {
clearErrorFeedback(txtFld);
setOkEnabled(true);
}
return value;
}
/**
* Mark a dialog field as erroneous.
*
* @param txtFld Dialog text field.
*/
protected void setErrorFeedback(JosmTextField txtFld) {
txtFld.setBorder(BorderFactory.createLineBorder(Color.RED, 1));
txtFld.setBackground(BG_COLOR_ERROR);
}
/**
* Clear the error marker from a dialog field.
*
* @param txtFld Dialog text field.
*/
protected void clearErrorFeedback(JosmTextField txtFld) {
txtFld.setBorder(UIManager.getBorder("TextField.border"));
txtFld.setBackground(UIManager.getColor("TextField.background"));
}
/**
* Enable or disable the OK button. It gets disabled if one of the
* dialog fields has an error.
*
* @param enabled {@code true} if the OK button is enabled,
* {@code false} if not.
*/
private void setOkEnabled(boolean enabled) {
if (buttons != null && !buttons.isEmpty()) {
buttons.get(0).setEnabled(enabled);
}
}
/**
* Interface for coordinate update.
*/
public interface UpdateLatLon {
/**
* Update of latitude and longitude.
*
* @param latLon New latitude/longitude value.
*/
public void updateLatLon(LatLon latLon);
}
/**
* Verify the coordinates. Parses the coordinate dialog field, sets
* or clears the error marker, updates the (temporary) image position,
* and updates the image layer to reflect the current coordinates.
*/
class LatLonInputVerifier implements DocumentListener, UpdateLatLon {
private void doUpdate() {
updateLatLon(parseLatLonUserInput());
}
@Override
public void updateLatLon(LatLon latLon) {
image.getTmp().setPos(latLon);
updateLayer(layer, image);
}
@Override
public void changedUpdate(DocumentEvent evt) {
doUpdate();
}
@Override
public void insertUpdate(DocumentEvent evt) {
doUpdate();
}
@Override
public void removeUpdate(DocumentEvent evt) {
doUpdate();
}
}
/**
* Interface for update of a {@code Double} field.
*/
public interface UpdateDoubleValue {
/**
* Update the associated value.
*
* @param value New double value.
*/
public void updateValue(Double value);
}
/**
* Verify the dialog field with double value. Parses the dialog
* field, sets or clears the error marker, calls the update method.
* The update method must be defined in the class instance.
*/
abstract class DoubleInputVerifier implements DocumentListener,
UpdateDoubleValue {
private final JosmTextField textField;
private Double minimum;
private Double maximum;
public DoubleInputVerifier(JosmTextField txtFld) {
textField = txtFld;
}
/**
* Set minimum and maximal value.
*
* @param min Minimum value. Set to {@code null} if there is no
* minimum.
* @param max Maximum value. Set to {@code null} if there is no
* maximum.
*/
public void setMinMax(Double min, Double max) {
minimum = min;
maximum = max;
}
private void doUpdate() {
updateValue(parseDoubleUserInput(textField, minimum, maximum));
}
@Override
public void changedUpdate(DocumentEvent evt) {
doUpdate();
}
@Override
public void insertUpdate(DocumentEvent evt) {
doUpdate();
}
@Override
public void removeUpdate(DocumentEvent evt) {
doUpdate();
}
}
}
}