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