// License: WTFPL. For details, see LICENSE file. package iodb; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Component; import java.awt.FlowLayout; import java.awt.Graphics2D; import java.awt.GridLayout; import java.awt.Point; import java.awt.RenderingHints; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Date; import java.util.List; import javax.swing.AbstractAction; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.KeyStroke; import javax.swing.border.CompoundBorder; import javax.swing.border.EmptyBorder; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.gui.JosmUserIdentityManager; import org.openstreetmap.josm.gui.MapView; import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener; import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer; import org.openstreetmap.josm.gui.layer.MapViewPaintable; import org.openstreetmap.josm.tools.HttpClient; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.LanguageInfo; import org.openstreetmap.josm.tools.OpenBrowser; /** * The dialog which presents a choice between imagery align options. * * @author Zverik * @license WTFPL */ public class OffsetDialog extends JDialog implements ActionListener, ZoomChangeListener, MapViewPaintable { protected static final String PREF_CALIBRATION = "iodb.show.calibration"; protected static final String PREF_DEPRECATED = "iodb.show.deprecated"; private static final int MAX_OFFSETS = Main.pref.getInteger("iodb.max.offsets", 4); /** * Whether to create a modal frame. It turns out, modal dialogs * block swing worker thread, so offset deprecation, for example, takes * place only after the dialog is closed. Very inconvenient. */ private static final boolean MODAL = false; private List<ImageryOffsetBase> offsets; private ImageryOffsetBase selectedOffset; private JPanel buttonPanel; /** * Initialize the dialog and install listeners. * @param offsets The list of offset to choose from. */ public OffsetDialog(List<ImageryOffsetBase> offsets) { super(JOptionPane.getFrameForComponent(Main.parent), ImageryOffsetTools.DIALOG_TITLE, MODAL ? ModalityType.DOCUMENT_MODAL : ModalityType.MODELESS); setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); setResizable(false); this.offsets = offsets; // make this dialog close on "escape" getRootPane().registerKeyboardAction(this, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); } /** * Creates the GUI. */ private void prepareDialog() { updateButtonPanel(); final JCheckBox calibrationBox = new JCheckBox(tr("Calibration geometries")); calibrationBox.setSelected(Main.pref.getBoolean(PREF_CALIBRATION, true)); calibrationBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Main.pref.put(PREF_CALIBRATION, calibrationBox.isSelected()); updateButtonPanel(); } }); final JCheckBox deprecatedBox = new JCheckBox(tr("Deprecated offsets")); deprecatedBox.setSelected(Main.pref.getBoolean(PREF_DEPRECATED, false)); deprecatedBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Main.pref.put(PREF_DEPRECATED, deprecatedBox.isSelected()); updateButtonPanel(); } }); Box checkBoxPanel = new Box(BoxLayout.X_AXIS); checkBoxPanel.add(calibrationBox); checkBoxPanel.add(deprecatedBox); JButton cancelButton = new JButton(tr("Cancel"), ImageProvider.get("cancel")); cancelButton.addActionListener(this); JButton helpButton = new JButton(new HelpAction()); JPanel cancelPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); cancelPanel.add(cancelButton); cancelPanel.add(helpButton); Box dialog = new Box(BoxLayout.Y_AXIS); dialog.add(buttonPanel); dialog.add(checkBoxPanel); dialog.add(cancelPanel); dialog.setBorder(new CompoundBorder(dialog.getBorder(), new EmptyBorder(5, 5, 5, 5))); setContentPane(dialog); pack(); setLocationRelativeTo(Main.parent); } /** * As the name states, this method updates the button panel. It is called * when a user clicks filtering checkboxes or deprecates an offset. */ private void updateButtonPanel() { List<ImageryOffsetBase> filteredOffsets = filterOffsets(); if (buttonPanel == null) buttonPanel = new JPanel(); buttonPanel.removeAll(); buttonPanel.setLayout(new GridLayout(filteredOffsets.size(), 1, 0, 5)); for (ImageryOffsetBase offset : filteredOffsets) { OffsetDialogButton button = new OffsetDialogButton(offset); button.addActionListener(this); JPopupMenu popupMenu = new JPopupMenu(); popupMenu.add(new OffsetInfoAction(offset)); if (!offset.isDeprecated()) { DeprecateOffsetAction action = new DeprecateOffsetAction(offset); action.setListener(new DeprecateOffsetListener(offset)); popupMenu.add(action); } button.setComponentPopupMenu(popupMenu); buttonPanel.add(button); } pack(); Main.map.mapView.repaint(); } /** * Make a filtered offset list out of the full one. Takes into * account both checkboxes. */ private List<ImageryOffsetBase> filterOffsets() { boolean showCalibration = Main.pref.getBoolean(PREF_CALIBRATION, true); boolean showDeprecated = Main.pref.getBoolean(PREF_DEPRECATED, false); List<ImageryOffsetBase> filteredOffsets = new ArrayList<>(); for (ImageryOffsetBase offset : offsets) { if (offset.isDeprecated() && !showDeprecated) continue; if (offset instanceof CalibrationObject && !showCalibration) continue; filteredOffsets.add(offset); if (filteredOffsets.size() >= MAX_OFFSETS) break; } return filteredOffsets; } /** * This listener method is called when a user pans or zooms the map. * It does nothing, only passes the event to all displayed offset buttons. */ @Override public void zoomChanged() { for (Component c : buttonPanel.getComponents()) { if (c instanceof OffsetDialogButton) { ((OffsetDialogButton) c).updateLocation(); } } } /** * Draw dots on the map where offsets are located. I doubt it has practical * value, but looks nice. */ @Override public void paint(Graphics2D g, MapView mv, Bounds bbox) { if (offsets == null) return; Graphics2D g2 = (Graphics2D) g.create(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setStroke(new BasicStroke(2)); for (ImageryOffsetBase offset : filterOffsets()) { Point p = mv.getPoint(offset.getPosition()); g2.setColor(Color.BLACK); g2.fillOval(p.x - 2, p.y - 2, 5, 5); g2.setColor(Color.WHITE); g2.drawOval(p.x - 3, p.y - 3, 7, 7); } } /** * Display the dialog and get the return value is case of a modal frame. * Creates GUI, install a temporary map layer (see {@link #paint} and * shows the window. * @return Null for a non-modal dialog, the selected offset * (or, again, a null value) otherwise. */ public ImageryOffsetBase showDialog() { selectedOffset = null; prepareDialog(); MapView.addZoomChangeListener(this); if (!MODAL) { Main.map.mapView.addTemporaryLayer(this); Main.map.mapView.repaint(); } setVisible(true); return selectedOffset; } /** * This is a listener method for all buttons (except "Help"). * It assigns a selected offset value and closes the dialog. * If the dialog wasn't modal, it applies the offset immediately. * Should it apply the offset either way? Probably. * @see #applyOffset() */ @Override public void actionPerformed(ActionEvent e) { if (e.getSource() instanceof OffsetDialogButton) { selectedOffset = ((OffsetDialogButton) e.getSource()).getOffset(); } else selectedOffset = null; boolean closeDialog = MODAL || selectedOffset == null || selectedOffset instanceof CalibrationObject || Main.pref.getBoolean("iodb.close.on.select", true); if (closeDialog) { MapView.removeZoomChangeListener(this); setVisible(false); } if (!MODAL) { if (closeDialog) { Main.map.mapView.removeTemporaryLayer(this); Main.map.mapView.repaint(); } if (selectedOffset != null) { applyOffset(); if (!closeDialog) updateButtonPanel(); } } } /** * Either applies imagery offset or adds a calibration geometry layer. * If the offset for each type was chosen for the first time ever, * it displays an informational message. */ public void applyOffset() { if (selectedOffset instanceof ImageryOffset) { AbstractTileSourceLayer layer = ImageryOffsetTools.getTopImageryLayer(); ImageryOffsetTools.applyLayerOffset(layer, (ImageryOffset) selectedOffset); ImageryOffsetWatcher.getInstance().markGood(); Main.map.repaint(); if (!Main.pref.getBoolean("iodb.offset.message", false)) { JOptionPane.showMessageDialog(Main.parent, tr("The topmost imagery layer has been shifted to presumably match\n" + "OSM data in the area. Please check that the offset is still valid\n" + "by downloading GPS tracks and comparing them and OSM data to the imagery."), ImageryOffsetTools.DIALOG_TITLE, JOptionPane.INFORMATION_MESSAGE); Main.pref.put("iodb.offset.message", true); } } else if (selectedOffset instanceof CalibrationObject) { CalibrationLayer clayer = new CalibrationLayer((CalibrationObject) selectedOffset); Main.getLayerManager().addLayer(clayer); clayer.panToCenter(); if (!Main.pref.getBoolean("iodb.calibration.message", false)) { JOptionPane.showMessageDialog(Main.parent, tr("A layer has been added with a calibration geometry. Hide data layers,\n" + "find the corresponding feature on the imagery layer and move it accordingly."), ImageryOffsetTools.DIALOG_TITLE, JOptionPane.INFORMATION_MESSAGE); Main.pref.put("iodb.calibration.message", true); } } } /** * A lisntener for successful deprecations. */ private class DeprecateOffsetListener implements QuerySuccessListener { ImageryOffsetBase offset; /** * Initialize the listener with an offset. */ DeprecateOffsetListener(ImageryOffsetBase offset) { this.offset = offset; } /** * Remove the deprecated offset from the offsets list. Then rebuild the button panel. */ @Override public void queryPassed() { offset.setDeprecated(new Date(), JosmUserIdentityManager.getInstance().getUserName(), ""); updateButtonPanel(); } } /** * Opens a web browser with the wiki page in user's language. */ static class HelpAction extends AbstractAction { HelpAction() { super(tr("Help")); putValue(SMALL_ICON, ImageProvider.get("help")); } @Override public void actionPerformed(ActionEvent e) { String base = Main.pref.get("url.openstreetmap-wiki", "http://wiki.openstreetmap.org/wiki/"); String lang = LanguageInfo.getWikiLanguagePrefix(); String page = "Imagery_Offset_Database"; try { // this logic was snatched from {@link org.openstreetmap.josm.gui.dialogs.properties.PropertiesDialog.HelpAction} HttpClient.Response conn = HttpClient.create(new URL(base + lang + page), "HEAD").connect(); if (conn.getResponseCode() != 200) { conn.disconnect(); lang = ""; } } catch (IOException ex) { lang = ""; } OpenBrowser.displayUrl(base + lang + page); } } }