001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.layer.geoimage; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trn; 006 007import java.awt.Component; 008import java.awt.Dimension; 009import java.awt.GridBagLayout; 010import java.util.Dictionary; 011import java.util.Hashtable; 012 013import javax.swing.JLabel; 014import javax.swing.JOptionPane; 015import javax.swing.JPanel; 016import javax.swing.JSlider; 017import javax.swing.event.ChangeListener; 018 019import org.openstreetmap.josm.data.gpx.GpxTimeOffset; 020import org.openstreetmap.josm.data.gpx.GpxTimezone; 021import org.openstreetmap.josm.gui.ExtendedDialog; 022import org.openstreetmap.josm.gui.MainApplication; 023import org.openstreetmap.josm.tools.GBC; 024import org.openstreetmap.josm.tools.JosmRuntimeException; 025import org.openstreetmap.josm.tools.Logging; 026 027/** 028 * Dialog used to manually adjust timezone and offset for GPX correlation. 029 * @since 18043 (extracted from {@link CorrelateGpxWithImages}) 030 */ 031public class AdjustTimezoneAndOffsetDialog extends ExtendedDialog { 032 033 private AdjustListener listener; 034 035 /** 036 * Constructs a new {@code AdjustTimezoneAndOffsetDialog} 037 * @param parent The parent element that will be used for position and maximum size 038 * @param tz initial timezone 039 * @param offset initial time offset 040 * @param dayOffset days offset 041 */ 042 public AdjustTimezoneAndOffsetDialog(Component parent, GpxTimezone tz, GpxTimeOffset offset, int dayOffset) { 043 super(parent, tr("Adjust timezone and offset"), tr("Close")); 044 setContent(buildContent(tz, offset, dayOffset)); 045 setButtonIcons("ok"); 046 } 047 048 private Component buildContent(GpxTimezone a, GpxTimeOffset b, int dayOffset) { 049 // Info Labels 050 final JLabel lblMatches = new JLabel(); 051 052 // Timezone Slider 053 // The slider allows to switch timezone from -12:00 to 12:00 in 30 minutes steps. Therefore the range is -24 to 24. 054 final JLabel lblTimezone = new JLabel(); 055 final JSlider sldTimezone = new JSlider(-24, 24, 0); 056 sldTimezone.setPaintLabels(true); 057 Dictionary<Integer, JLabel> labelTable = new Hashtable<>(); 058 // CHECKSTYLE.OFF: ParenPad 059 for (int i = -12; i <= 12; i += 6) { 060 labelTable.put(i * 2, new JLabel(new GpxTimezone(i).formatTimezone())); 061 } 062 // CHECKSTYLE.ON: ParenPad 063 sldTimezone.setLabelTable(labelTable); 064 065 // Minutes Slider 066 final JLabel lblMinutes = new JLabel(); 067 final JSlider sldMinutes = new JSlider(-15, 15, 0); 068 sldMinutes.setPaintLabels(true); 069 sldMinutes.setMajorTickSpacing(5); 070 071 // Seconds slider 072 final JLabel lblSeconds = new JLabel(); 073 final JSlider sldSeconds = new JSlider(-600, 600, 0); 074 sldSeconds.setPaintLabels(true); 075 labelTable = new Hashtable<>(); 076 // CHECKSTYLE.OFF: ParenPad 077 for (int i = -60; i <= 60; i += 30) { 078 labelTable.put(i * 10, new JLabel(GpxTimeOffset.seconds(i).formatOffset())); 079 } 080 // CHECKSTYLE.ON: ParenPad 081 sldSeconds.setLabelTable(labelTable); 082 sldSeconds.setMajorTickSpacing(300); 083 084 // Put everything together 085 JPanel p = new JPanel(new GridBagLayout()); 086 p.setPreferredSize(new Dimension(400, 230)); 087 p.add(lblMatches, GBC.eol().fill()); 088 p.add(lblTimezone, GBC.eol().fill()); 089 p.add(sldTimezone, GBC.eol().fill().insets(0, 0, 0, 10)); 090 p.add(lblMinutes, GBC.eol().fill()); 091 p.add(sldMinutes, GBC.eol().fill().insets(0, 0, 0, 10)); 092 p.add(lblSeconds, GBC.eol().fill()); 093 p.add(sldSeconds, GBC.eol().fill()); 094 095 // If there's an error in the calculation the found values 096 // will be off range for the sliders. Catch this error 097 // and inform the user about it. 098 try { 099 sldTimezone.setValue((int) (a.getHours() * 2)); 100 sldMinutes.setValue((int) (b.getSeconds() / 60)); 101 final long deciSeconds = b.getMilliseconds() / 100; 102 sldSeconds.setValue((int) (deciSeconds % 600)); 103 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException ex) { 104 Logging.warn(ex); 105 JOptionPane.showMessageDialog(MainApplication.getMainFrame(), 106 tr("An error occurred while trying to match the photos to the GPX track." 107 +" You can adjust the sliders to manually match the photos."), 108 tr("Matching photos to track failed"), 109 JOptionPane.WARNING_MESSAGE); 110 } 111 112 // This is called whenever one of the sliders is moved. 113 // It updates the labels 114 ChangeListener sliderListener = x -> { 115 final GpxTimezone timezone = new GpxTimezone(sldTimezone.getValue() / 2.); 116 final int minutes = sldMinutes.getValue(); 117 final int seconds = sldSeconds.getValue(); 118 119 lblTimezone.setText(tr("Timezone: {0}", timezone.formatTimezone())); 120 lblMinutes.setText(tr("Minutes: {0}", minutes)); 121 lblSeconds.setText(tr("Seconds: {0}", GpxTimeOffset.milliseconds(100L * seconds).formatOffset())); 122 123 StringBuilder sb = new StringBuilder("<html>"); 124 if (listener != null) { 125 sb.append(listener.valuesChanged(timezone, minutes, seconds)).append("<br>"); 126 } 127 128 lblMatches.setText(sb.append(trn("(Time difference of {0} day)", "Time difference of {0} days", 129 Math.abs(dayOffset), Math.abs(dayOffset))).append("</html>").toString()); 130 }; 131 132 // Call the sliderListener once manually so labels get adjusted 133 sliderListener.stateChanged(null); 134 135 // Listeners added here, otherwise it tries to match three times 136 // (when setting the default values) 137 sldTimezone.addChangeListener(sliderListener); 138 sldMinutes.addChangeListener(sliderListener); 139 sldSeconds.addChangeListener(sliderListener); 140 141 return p; 142 } 143 144 /** 145 * Listener called when the sliders are moved. 146 */ 147 public interface AdjustListener { 148 /** 149 * Provides a textual description matching the new state after the change of values. 150 * @param timezone new timezone 151 * @param minutes new minutes offset 152 * @param seconds new seconds offset 153 * @return an HTML textual description matching the new state after the change of values 154 */ 155 String valuesChanged(GpxTimezone timezone, int minutes, int seconds); 156 } 157 158 /** 159 * Sets the {@link AdjustListener}. 160 * @param listener adjust listener, can be null 161 * @return {@code this} 162 */ 163 public final AdjustTimezoneAndOffsetDialog adjustListener(AdjustListener listener) { 164 this.listener = listener; 165 return this; 166 } 167}