001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.plugins.streetside.gui;
003
004import java.awt.BorderLayout;
005import java.awt.Color;
006import java.awt.Component;
007import java.awt.event.ActionEvent;
008import java.awt.image.BufferedImage;
009import java.io.ByteArrayInputStream;
010import java.io.IOException;
011import java.text.MessageFormat;
012import java.util.Arrays;
013import java.util.List;
014
015import javax.imageio.ImageIO;
016import javax.swing.AbstractAction;
017import javax.swing.Action;
018import javax.swing.JComponent;
019import javax.swing.KeyStroke;
020import javax.swing.SwingUtilities;
021
022import org.apache.log4j.Logger;
023import org.openstreetmap.josm.data.cache.CacheEntry;
024import org.openstreetmap.josm.data.cache.CacheEntryAttributes;
025import org.openstreetmap.josm.data.cache.ICachedLoaderListener;
026import org.openstreetmap.josm.gui.SideButton;
027import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
028import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
029import org.openstreetmap.josm.plugins.streetside.StreetsideDataListener;
030import org.openstreetmap.josm.plugins.streetside.StreetsideImage;
031import org.openstreetmap.josm.plugins.streetside.StreetsideLayer;
032import org.openstreetmap.josm.plugins.streetside.StreetsidePlugin;
033import org.openstreetmap.josm.plugins.streetside.actions.WalkListener;
034import org.openstreetmap.josm.plugins.streetside.actions.WalkThread;
035import org.openstreetmap.josm.plugins.streetside.cache.StreetsideCache;
036import org.openstreetmap.josm.plugins.streetside.gui.imageinfo.ImageInfoHelpPopup;
037import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
038import org.openstreetmap.josm.tools.I18n;
039import org.openstreetmap.josm.tools.ImageProvider;
040
041/**
042 * Toggle dialog that shows an image and some buttons.
043 *
044 * @author nokutu
045 * @author renerr18
046 */
047public final class StreetsideMainDialog extends ToggleDialog implements
048                ICachedLoaderListener, StreetsideDataListener {
049
050        private static final long serialVersionUID = 2645654786827812861L;
051
052  final static Logger logger = Logger.getLogger(StreetsideMainDialog.class);
053
054  public static final String BASE_TITLE = I18n.marktr("Microsoft Streetside image");
055
056        private static final String MESSAGE_SEPARATOR = " — ";
057
058        private static StreetsideMainDialog instance;
059
060        private volatile StreetsideAbstractImage image;
061
062        private final SideButton nextButton = new SideButton(new NextPictureAction());
063  private final SideButton previousButton = new SideButton(new PreviousPictureAction());
064  /**
065   * Button used to jump to the image following the red line
066   */
067  public final SideButton redButton = new SideButton(new RedAction());
068  /**
069   * Button used to jump to the image following the blue line
070   */
071  public final SideButton blueButton = new SideButton(new BlueAction());
072
073  private final SideButton playButton = new SideButton(new PlayAction());
074  private final SideButton pauseButton = new SideButton(new PauseAction());
075  private final SideButton stopButton = new SideButton(new StopAction());
076
077  private ImageInfoHelpPopup imageInfoHelp;
078
079        /**
080         * Buttons mode.
081         *
082         * @author nokutu
083         */
084        public enum MODE {
085                /**
086                 * Standard mode to view pictures.
087                 */
088                NORMAL,
089                /**
090                 * Mode when in walk.
091                 */
092                WALK
093        }
094
095        /**
096         * Object containing the shown image and that handles zoom and drag
097         */
098        public StreetsideImageDisplay streetsideImageDisplay;
099
100        private StreetsideCache imageCache;
101        public StreetsideCache thumbnailCache;
102
103        private StreetsideMainDialog() {
104                super(I18n.tr(StreetsideMainDialog.BASE_TITLE), "streetside-main", I18n.tr("Open Streetside window"), null, 200,
105                                true, StreetsidePreferenceSetting.class);
106                addShortcuts();
107
108                streetsideImageDisplay = new StreetsideImageDisplay();
109
110                blueButton.setForeground(Color.BLUE);
111    redButton.setForeground(Color.RED);
112
113                setMode(MODE.NORMAL);
114        }
115
116        /**
117         * Adds the shortcuts to the buttons.
118         */
119  private void addShortcuts() {
120    nextButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("PAGE_DOWN"), "next");
121    nextButton.getActionMap().put("next", new NextPictureAction());
122    previousButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("PAGE_UP"), "previous");
123    previousButton.getActionMap().put("previous", new PreviousPictureAction());
124    blueButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control PAGE_UP"), "blue");
125    blueButton.getActionMap().put("blue", new BlueAction());
126    redButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control PAGE_DOWN"), "red");
127    redButton.getActionMap().put("red", new RedAction());
128  }
129
130        /**
131         * Returns the unique instance of the class.
132         *
133         * @return The unique instance of the class.
134         */
135        public static synchronized StreetsideMainDialog getInstance() {
136                if (StreetsideMainDialog.instance == null) {
137                        StreetsideMainDialog.instance = new StreetsideMainDialog();
138                }
139                return StreetsideMainDialog.instance;
140        }
141
142        /**
143         * @return true, iff the singleton instance is present
144         */
145        public static boolean hasInstance() {
146                return StreetsideMainDialog.instance != null;
147        }
148
149        public synchronized void setImageInfoHelp(ImageInfoHelpPopup popup) {
150                imageInfoHelp = popup;
151        }
152
153        /**
154         * Sets a new mode for the dialog.
155         *
156         * @param mode The mode to be set. Must not be {@code null}.
157         */
158        public void setMode(MODE mode) {
159                switch (mode) {
160                case WALK:
161      createLayout(
162        streetsideImageDisplay,
163        Arrays.asList(playButton, pauseButton, stopButton)
164      );
165      break;
166    case NORMAL:
167    default:
168      createLayout(
169        streetsideImageDisplay,
170        Arrays.asList(blueButton, previousButton, nextButton, redButton)
171      );
172      break;
173                }
174                disableAllButtons();
175    if (MODE.NORMAL.equals(mode)) {
176      updateImage();
177    }
178    revalidate();
179    repaint();
180        }
181
182        /**
183         * Destroys the unique instance of the class.
184         */
185        public static synchronized void destroyInstance() {
186                StreetsideMainDialog.instance = null;
187        }
188
189        /**
190         * Downloads the full quality picture of the selected StreetsideImage and sets
191         * in the StreetsideImageDisplay object.
192         */
193        public synchronized void updateImage() {
194                updateImage(true);
195        }
196
197        /**
198         * Downloads the picture of the selected StreetsideImage and sets in the
199         * StreetsideImageDisplay object.
200         *
201         * @param fullQuality If the full quality picture must be downloaded or just the
202         *                    thumbnail.
203         */
204        public synchronized void updateImage(boolean fullQuality) {
205                if (!SwingUtilities.isEventDispatchThread()) {
206                        SwingUtilities.invokeLater(this::updateImage);
207                } else {
208                        if (!StreetsideLayer.hasInstance()) {
209                                return;
210                        }
211                        if (image == null) {
212                                streetsideImageDisplay.setImage(null, null);
213                                setTitle(I18n.tr(StreetsideMainDialog.BASE_TITLE));
214                                return;
215                        }
216
217                        if (imageInfoHelp != null && StreetsideProperties.IMAGEINFO_HELP_COUNTDOWN.get() > 0 && imageInfoHelp.showPopup()) {
218                                // Count down the number of times the popup will be displayed
219                                StreetsideProperties.IMAGEINFO_HELP_COUNTDOWN.put(StreetsideProperties.IMAGEINFO_HELP_COUNTDOWN.get() - 1);
220                        }
221
222                        if (image instanceof StreetsideImage) {
223                                final StreetsideImage streetsideImage = (StreetsideImage) image;
224                                // Downloads the thumbnail.
225                                streetsideImageDisplay.setImage(null, null);
226                                if (thumbnailCache != null) {
227                                        thumbnailCache.cancelOutstandingTasks();
228                                }
229                                thumbnailCache = new StreetsideCache(streetsideImage.getId(),
230                                                StreetsideCache.Type.THUMBNAIL);
231                                try {
232                                        thumbnailCache.submit(this, false);
233                                } catch (final IOException e) {
234                                        logger.error(e);
235                                }
236
237                                // Downloads the full resolution image.
238                                if (fullQuality || new StreetsideCache(streetsideImage.getId(),
239                                                StreetsideCache.Type.FULL_IMAGE).get() != null) {
240                                        if (imageCache != null) {
241                                                imageCache.cancelOutstandingTasks();
242                                        }
243                                        imageCache = new StreetsideCache(streetsideImage.getId(),
244                                                        StreetsideCache.Type.FULL_IMAGE);
245                                        try {
246                                                imageCache.submit(this, false);
247                                        } catch (final IOException e) {
248                                                logger.error(e);
249                                        }
250                                }
251                        }
252                        updateTitle();
253                }
254        }
255
256        /**
257   * Disables all the buttons in the dialog
258   */
259  private void disableAllButtons() {
260    nextButton.setEnabled(false);
261    previousButton.setEnabled(false);
262    blueButton.setEnabled(false);
263    redButton.setEnabled(false);
264  }
265
266        /**
267         * Sets a new StreetsideImage to be shown.
268         *
269         * @param image The image to be shown.
270         */
271        public synchronized void setImage(StreetsideAbstractImage image) {
272                this.image = image;
273        }
274
275        /**
276         * Updates the title of the dialog.
277         */
278        public synchronized void updateTitle() {
279                if (!SwingUtilities.isEventDispatchThread()) {
280                        SwingUtilities.invokeLater(this::updateTitle);
281                } else if (image != null) {
282                        final StringBuilder title = new StringBuilder(I18n.tr(StreetsideMainDialog.BASE_TITLE));
283                        if (image instanceof StreetsideImage) {
284                                title.append(StreetsideMainDialog.MESSAGE_SEPARATOR).append(MessageFormat.format("(heading {0}°)",Double.toString(image.getHe())));
285        setTitle(title.toString());
286                        }
287                }
288        }
289
290        /**
291         * Returns the {@link StreetsideAbstractImage} object which is being shown.
292         *
293         * @return The {@link StreetsideAbstractImage} object which is being shown.
294         */
295        public synchronized StreetsideAbstractImage getImage() {
296                return image;
297        }
298
299        /**
300         * Action class form the next image button.
301         *
302         * @author nokutu
303         */
304        private static class NextPictureAction extends AbstractAction {
305
306                private static final long serialVersionUID = 6333692154558730392L;
307
308                /**
309                 * Constructs a normal NextPictureAction
310                 */
311                NextPictureAction() {
312                        super(I18n.tr("Next picture"));
313                        putValue(Action.SHORT_DESCRIPTION, I18n.tr("Shows the next picture in the sequence"));
314                        new ImageProvider("help", "next").getResource().attachImageIcon(this, true);
315                }
316
317                @Override
318                public void actionPerformed(ActionEvent e) {
319                        StreetsideLayer.getInstance().getData().selectNext();
320                }
321        }
322
323        /**
324         * Action class for the previous image button.
325         *
326         * @author nokutu
327         */
328        private static class PreviousPictureAction extends AbstractAction {
329
330                private static final long serialVersionUID = 4390593660514657107L;
331
332                /**
333                 * Constructs a normal PreviousPictureAction
334                 */
335                PreviousPictureAction() {
336                        super(I18n.tr("Previous picture"));
337                        putValue(Action.SHORT_DESCRIPTION, I18n.tr("Shows the previous picture in the sequence"));
338                        new ImageProvider("help", "previous").getResource().attachImageIcon(this, true);
339                }
340
341                @Override
342                public void actionPerformed(ActionEvent e) {
343                        StreetsideLayer.getInstance().getData().selectPrevious();
344                }
345        }
346
347        /**
348         * Action class to jump to the image following the red line.
349         *
350         * @author nokutu
351         */
352        private static class RedAction extends AbstractAction {
353
354                private static final long serialVersionUID = -1244456062285831231L;
355
356                /**
357                 * Constructs a normal RedAction
358                 */
359                RedAction() {
360                        putValue(Action.NAME, I18n.tr("Jump to red"));
361                        putValue(Action.SHORT_DESCRIPTION,
362                                        I18n.tr("Jumps to the picture at the other side of the red line"));
363                        new ImageProvider("dialogs", "red").getResource().attachImageIcon(this, true);
364                }
365
366                @Override
367                public void actionPerformed(ActionEvent e) {
368                        if (StreetsideMainDialog.getInstance().getImage() != null) {
369                                StreetsideLayer.getInstance().getData()
370                                .setSelectedImage(StreetsideLayer.getInstance().getNNearestImage(1), true);
371                        }
372                }
373        }
374
375        /**
376         * Action class to jump to the image following the blue line.
377         *
378         * @author nokutu
379         */
380        private static class BlueAction extends AbstractAction {
381
382                private static final long serialVersionUID = 5951233534212838780L;
383
384                /**
385                 * Constructs a normal BlueAction
386                 */
387                BlueAction() {
388                        putValue(Action.NAME, I18n.tr("Jump to blue"));
389                        putValue(Action.SHORT_DESCRIPTION,
390                                        I18n.tr("Jumps to the picture at the other side of the blue line"));
391                        new ImageProvider("dialogs", "blue").getResource().attachImageIcon(this, true);
392                }
393
394                @Override
395                public void actionPerformed(ActionEvent e) {
396                        if (StreetsideMainDialog.getInstance().getImage() != null) {
397                                StreetsideLayer.getInstance().getData()
398                                .setSelectedImage(StreetsideLayer.getInstance().getNNearestImage(2), true);
399                        }
400                }
401        }
402
403        private static class StopAction extends AbstractAction implements WalkListener {
404
405                private static final long serialVersionUID = 8789972456611625341L;
406
407                private WalkThread thread;
408
409                /**
410                 * Constructs a normal StopAction
411                 */
412                StopAction() {
413                        putValue(Action.NAME, I18n.tr("Stop"));
414                        putValue(Action.SHORT_DESCRIPTION, I18n.tr("Stops the walk."));
415                        new ImageProvider("dialogs/streetsideStop.png").getResource().attachImageIcon(this, true);
416                        StreetsidePlugin.getStreetsideWalkAction().addListener(this);
417                }
418
419                @Override
420                public void actionPerformed(ActionEvent e) {
421                        if (thread != null) {
422                                thread.stopWalk();
423                        }
424                }
425
426                @Override
427                public void walkStarted(WalkThread thread) {
428                        this.thread = thread;
429                }
430        }
431
432        private static class PlayAction extends AbstractAction implements WalkListener {
433
434                private static final long serialVersionUID = -1572747020946842769L;
435
436                private transient WalkThread thread;
437
438                /**
439                 * Constructs a normal PlayAction
440                 */
441                PlayAction() {
442                        putValue(Action.NAME, I18n.tr("Play"));
443                        putValue(Action.SHORT_DESCRIPTION, I18n.tr("Continues with the paused walk."));
444                        new ImageProvider("dialogs/streetsidePlay.png").getResource().attachImageIcon(this, true);
445                        StreetsidePlugin.getStreetsideWalkAction().addListener(this);
446                }
447
448                @Override
449                public void actionPerformed(ActionEvent e) {
450                        if (thread != null) {
451                                thread.play();
452                        }
453                }
454
455                @Override
456                public void walkStarted(WalkThread thread) {
457                        if (thread != null) {
458                                this.thread = thread;
459                        }
460                }
461        }
462
463        private static class PauseAction extends AbstractAction implements WalkListener {
464
465                /**
466                 *
467                 */
468                private static final long serialVersionUID = -8758326399460817222L;
469                private WalkThread thread;
470
471                /**
472                 * Constructs a normal PauseAction
473                 */
474                PauseAction() {
475                        putValue(Action.NAME, I18n.tr("Pause"));
476                        putValue(Action.SHORT_DESCRIPTION, I18n.tr("Pauses the walk."));
477                        new ImageProvider("dialogs/streetsidePause.png").getResource().attachImageIcon(this, true);
478                        StreetsidePlugin.getStreetsideWalkAction().addListener(this);
479                }
480
481                @Override
482                public void actionPerformed(ActionEvent e) {
483                        thread.pause();
484                }
485
486                @Override
487                public void walkStarted(WalkThread thread) {
488                        this.thread = thread;
489                }
490        }
491
492        /**
493         * When the pictures are returned from the cache, they are set in the
494         * {@link StreetsideImageDisplay} object.
495         */
496        @Override
497        public void loadingFinished(final CacheEntry data, final CacheEntryAttributes attributes, final LoadResult result) {
498                if (!SwingUtilities.isEventDispatchThread()) {
499                        SwingUtilities.invokeLater(() -> loadingFinished(data, attributes, result));
500
501                } else if (data != null && result == LoadResult.SUCCESS) {
502                        try {
503                                final BufferedImage img = ImageIO.read(new ByteArrayInputStream(data.getContent()));
504                                if (img == null) {
505                                        return;
506                                }
507                                if (
508                                                streetsideImageDisplay.getImage() == null
509                                                || img.getHeight() > streetsideImageDisplay.getImage().getHeight()
510                                                ) {
511                                        streetsideImageDisplay.setImage(
512                                                        img,
513                                                        null);
514                                }
515                        } catch (final IOException e) {
516                                logger.error(e);
517                        }
518                }
519        }
520
521
522        /**
523         * Creates the layout of the dialog.
524         *
525         * @param data    The content of the dialog
526         * @param buttons The buttons where you can click
527         */
528        public void createLayout(Component data, List<SideButton> buttons) {
529                removeAll();
530                createLayout(data, true, buttons);
531                add(titleBar, BorderLayout.NORTH);
532        }
533
534        @Override
535        public void selectedImageChanged(StreetsideAbstractImage oldImage, StreetsideAbstractImage newImage) {
536                setImage(newImage);
537                if (newImage != null && newImage.getSequence() != null) {
538                if (newImage.next() != null) {
539                    nextButton.setEnabled(true);
540                }
541                if (newImage.previous() != null) {
542                    previousButton.setEnabled(true);
543                }
544        }
545                updateImage();
546        }
547
548        @Override
549        public void imagesAdded() {
550                // This method is enforced by StreetsideDataListener, but only selectedImageChanged() is needed
551        }
552
553        /**
554         * @return the streetsideImageDisplay
555         */
556        public StreetsideImageDisplay getStreetsideImageDisplay() {
557                return streetsideImageDisplay;
558        }
559
560        /**
561         * @param streetsideImageDisplay the streetsideImageDisplay to set
562         */
563        public void setStreetsideImageDisplay(StreetsideImageDisplay streetsideImageDisplay) {
564                this.streetsideImageDisplay = streetsideImageDisplay;
565        }
566}