001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs.changeset;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trc;
006
007import java.awt.BorderLayout;
008import java.awt.FlowLayout;
009import java.awt.GridBagConstraints;
010import java.awt.GridBagLayout;
011import java.awt.Insets;
012import java.awt.event.ActionEvent;
013import java.awt.event.ComponentAdapter;
014import java.awt.event.ComponentEvent;
015import java.beans.PropertyChangeEvent;
016import java.beans.PropertyChangeListener;
017import java.text.DateFormat;
018import java.util.Collections;
019import java.util.Date;
020import java.util.HashSet;
021import java.util.Set;
022
023import javax.swing.AbstractAction;
024import javax.swing.BorderFactory;
025import javax.swing.JButton;
026import javax.swing.JLabel;
027import javax.swing.JOptionPane;
028import javax.swing.JPanel;
029import javax.swing.JToolBar;
030
031import org.openstreetmap.josm.Main;
032import org.openstreetmap.josm.actions.AutoScaleAction;
033import org.openstreetmap.josm.actions.downloadtasks.ChangesetHeaderDownloadTask;
034import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler;
035import org.openstreetmap.josm.data.osm.Changeset;
036import org.openstreetmap.josm.data.osm.ChangesetCache;
037import org.openstreetmap.josm.data.osm.DataSet;
038import org.openstreetmap.josm.data.osm.OsmPrimitive;
039import org.openstreetmap.josm.gui.HelpAwareOptionPane;
040import org.openstreetmap.josm.gui.MainApplication;
041import org.openstreetmap.josm.gui.help.HelpUtil;
042import org.openstreetmap.josm.gui.history.OpenChangesetPopupMenu;
043import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
044import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
045import org.openstreetmap.josm.gui.widgets.JosmTextArea;
046import org.openstreetmap.josm.gui.widgets.JosmTextField;
047import org.openstreetmap.josm.io.OnlineResource;
048import org.openstreetmap.josm.tools.ImageProvider;
049import org.openstreetmap.josm.tools.Utils;
050import org.openstreetmap.josm.tools.date.DateUtils;
051
052/**
053 * This panel displays the properties of the currently selected changeset in the
054 * {@link ChangesetCacheManager}.
055 *
056 */
057public class ChangesetDetailPanel extends JPanel implements PropertyChangeListener, ChangesetAware {
058
059    // CHECKSTYLE.OFF: SingleSpaceSeparator
060    private final JosmTextField tfID        = new JosmTextField(10);
061    private final JosmTextArea  taComment   = new JosmTextArea(5, 40);
062    private final JosmTextField tfOpen      = new JosmTextField(10);
063    private final JosmTextField tfUser      = new JosmTextField("");
064    private final JosmTextField tfCreatedOn = new JosmTextField(20);
065    private final JosmTextField tfClosedOn  = new JosmTextField(20);
066
067    private final OpenChangesetPopupMenuAction   actOpenChangesetPopupMenu   = new OpenChangesetPopupMenuAction();
068    private final DownloadChangesetContentAction actDownloadChangesetContent = new DownloadChangesetContentAction(this);
069    private final UpdateChangesetAction          actUpdateChangesets         = new UpdateChangesetAction();
070    private final RemoveFromCacheAction          actRemoveFromCache          = new RemoveFromCacheAction();
071    private final SelectInCurrentLayerAction     actSelectInCurrentLayer     = new SelectInCurrentLayerAction();
072    private final ZoomInCurrentLayerAction       actZoomInCurrentLayerAction = new ZoomInCurrentLayerAction();
073    // CHECKSTYLE.ON: SingleSpaceSeparator
074
075    private JButton btnOpenChangesetPopupMenu;
076
077    private transient Changeset currentChangeset;
078
079    protected JPanel buildActionButtonPanel() {
080        JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT));
081
082        JToolBar tb = new JToolBar(JToolBar.VERTICAL);
083        tb.setFloatable(false);
084
085        // -- display changeset
086        btnOpenChangesetPopupMenu = tb.add(actOpenChangesetPopupMenu);
087        actOpenChangesetPopupMenu.initProperties(currentChangeset);
088
089        // -- remove from cache action
090        tb.add(actRemoveFromCache);
091        actRemoveFromCache.initProperties(currentChangeset);
092
093        // -- changeset update
094        tb.add(actUpdateChangesets);
095        actUpdateChangesets.initProperties(currentChangeset);
096
097        // -- changeset content download
098        tb.add(actDownloadChangesetContent);
099        actDownloadChangesetContent.initProperties();
100
101        tb.add(actSelectInCurrentLayer);
102        MainApplication.getLayerManager().addActiveLayerChangeListener(actSelectInCurrentLayer);
103
104        tb.add(actZoomInCurrentLayerAction);
105        MainApplication.getLayerManager().addActiveLayerChangeListener(actZoomInCurrentLayerAction);
106
107        addComponentListener(
108                new ComponentAdapter() {
109                    @Override
110                    public void componentShown(ComponentEvent e) {
111                        MainApplication.getLayerManager().addAndFireActiveLayerChangeListener(actSelectInCurrentLayer);
112                        MainApplication.getLayerManager().addAndFireActiveLayerChangeListener(actZoomInCurrentLayerAction);
113                    }
114
115                    @Override
116                    public void componentHidden(ComponentEvent e) {
117                        // make sure the listener is unregistered when the panel becomes invisible
118                        MainApplication.getLayerManager().removeActiveLayerChangeListener(actSelectInCurrentLayer);
119                        MainApplication.getLayerManager().removeActiveLayerChangeListener(actZoomInCurrentLayerAction);
120                    }
121                }
122        );
123
124        pnl.add(tb);
125        return pnl;
126    }
127
128    protected JPanel buildDetailViewPanel() {
129        JPanel pnl = new JPanel(new GridBagLayout());
130
131        GridBagConstraints gc = new GridBagConstraints();
132        gc.anchor = GridBagConstraints.FIRST_LINE_START;
133        gc.insets = new Insets(0, 0, 2, 3);
134
135        //-- id
136        gc.fill = GridBagConstraints.HORIZONTAL;
137        gc.weightx = 0.0;
138        pnl.add(new JLabel(tr("ID:")), gc);
139
140        gc.fill = GridBagConstraints.HORIZONTAL;
141        gc.weightx = 0.0;
142        gc.gridx = 1;
143        pnl.add(tfID, gc);
144        tfID.setEditable(false);
145
146        //-- comment
147        gc.gridx = 0;
148        gc.gridy = 1;
149        gc.fill = GridBagConstraints.HORIZONTAL;
150        gc.weightx = 0.0;
151        pnl.add(new JLabel(tr("Comment:")), gc);
152
153        gc.fill = GridBagConstraints.BOTH;
154        gc.weightx = 1.0;
155        gc.weighty = 1.0;
156        gc.gridx = 1;
157        pnl.add(taComment, gc);
158        taComment.setEditable(false);
159
160        //-- Open/Closed
161        gc.gridx = 0;
162        gc.gridy = 2;
163        gc.fill = GridBagConstraints.HORIZONTAL;
164        gc.weightx = 0.0;
165        gc.weighty = 0.0;
166        pnl.add(new JLabel(tr("Open/Closed:")), gc);
167
168        gc.fill = GridBagConstraints.HORIZONTAL;
169        gc.gridx = 1;
170        pnl.add(tfOpen, gc);
171        tfOpen.setEditable(false);
172
173        //-- Created by:
174        gc.gridx = 0;
175        gc.gridy = 3;
176        gc.fill = GridBagConstraints.HORIZONTAL;
177        gc.weightx = 0.0;
178        pnl.add(new JLabel(tr("Created by:")), gc);
179
180        gc.fill = GridBagConstraints.HORIZONTAL;
181        gc.weightx = 1.0;
182        gc.gridx = 1;
183        pnl.add(tfUser, gc);
184        tfUser.setEditable(false);
185
186        //-- Created On:
187        gc.gridx = 0;
188        gc.gridy = 4;
189        gc.fill = GridBagConstraints.HORIZONTAL;
190        gc.weightx = 0.0;
191        pnl.add(new JLabel(tr("Created on:")), gc);
192
193        gc.fill = GridBagConstraints.HORIZONTAL;
194        gc.gridx = 1;
195        pnl.add(tfCreatedOn, gc);
196        tfCreatedOn.setEditable(false);
197
198        //-- Closed On:
199        gc.gridx = 0;
200        gc.gridy = 5;
201        gc.fill = GridBagConstraints.HORIZONTAL;
202        gc.weightx = 0.0;
203        pnl.add(new JLabel(tr("Closed on:")), gc);
204
205        gc.fill = GridBagConstraints.HORIZONTAL;
206        gc.gridx = 1;
207        pnl.add(tfClosedOn, gc);
208        tfClosedOn.setEditable(false);
209
210        return pnl;
211    }
212
213    protected final void build() {
214        setLayout(new BorderLayout());
215        setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
216        add(buildDetailViewPanel(), BorderLayout.CENTER);
217        add(buildActionButtonPanel(), BorderLayout.WEST);
218    }
219
220    protected void clearView() {
221        tfID.setText("");
222        taComment.setText("");
223        tfOpen.setText("");
224        tfUser.setText("");
225        tfCreatedOn.setText("");
226        tfClosedOn.setText("");
227    }
228
229    protected void updateView(Changeset cs) {
230        String msg;
231        if (cs == null) return;
232        tfID.setText(Integer.toString(cs.getId()));
233        taComment.setText(cs.getComment());
234
235        if (cs.isOpen()) {
236            msg = trc("changeset.state", "Open");
237        } else {
238            msg = trc("changeset.state", "Closed");
239        }
240        tfOpen.setText(msg);
241
242        if (cs.getUser() == null) {
243            msg = tr("anonymous");
244        } else {
245            msg = cs.getUser().getName();
246        }
247        tfUser.setText(msg);
248        DateFormat sdf = DateUtils.getDateTimeFormat(DateFormat.SHORT, DateFormat.SHORT);
249
250        Date createdDate = cs.getCreatedAt();
251        Date closedDate = cs.getClosedAt();
252        tfCreatedOn.setText(createdDate == null ? "" : sdf.format(createdDate));
253        tfClosedOn.setText(closedDate == null ? "" : sdf.format(closedDate));
254    }
255
256    /**
257     * Constructs a new {@code ChangesetDetailPanel}.
258     */
259    public ChangesetDetailPanel() {
260        build();
261    }
262
263    protected void setCurrentChangeset(Changeset cs) {
264        currentChangeset = cs;
265        if (cs == null) {
266            clearView();
267        } else {
268            updateView(cs);
269        }
270        actOpenChangesetPopupMenu.initProperties(currentChangeset);
271        actDownloadChangesetContent.initProperties();
272        actUpdateChangesets.initProperties(currentChangeset);
273        actRemoveFromCache.initProperties(currentChangeset);
274        actSelectInCurrentLayer.updateEnabledState();
275        actZoomInCurrentLayerAction.updateEnabledState();
276    }
277
278    /* ---------------------------------------------------------------------------- */
279    /* interface PropertyChangeListener                                             */
280    /* ---------------------------------------------------------------------------- */
281    @Override
282    public void propertyChange(PropertyChangeEvent evt) {
283        if (!evt.getPropertyName().equals(ChangesetCacheManagerModel.CHANGESET_IN_DETAIL_VIEW_PROP))
284            return;
285        setCurrentChangeset((Changeset) evt.getNewValue());
286    }
287
288    /**
289     * The action for removing the currently selected changeset from the changeset cache
290     */
291    class RemoveFromCacheAction extends AbstractAction {
292        RemoveFromCacheAction() {
293            putValue(NAME, tr("Remove from cache"));
294            new ImageProvider("dialogs", "delete").getResource().attachImageIcon(this);
295            putValue(SHORT_DESCRIPTION, tr("Remove the changeset in the detail view panel from the local cache"));
296        }
297
298        @Override
299        public void actionPerformed(ActionEvent evt) {
300            if (currentChangeset == null)
301                return;
302            ChangesetCache.getInstance().remove(currentChangeset);
303        }
304
305        public void initProperties(Changeset cs) {
306            setEnabled(cs != null);
307        }
308    }
309
310    /**
311     * Updates the current changeset from the OSM server
312     *
313     */
314    class UpdateChangesetAction extends AbstractAction {
315        UpdateChangesetAction() {
316            putValue(NAME, tr("Update changeset"));
317            new ImageProvider("dialogs/changeset", "updatechangesetcontent").getResource().attachImageIcon(this);
318            putValue(SHORT_DESCRIPTION, tr("Update the changeset from the OSM server"));
319        }
320
321        @Override
322        public void actionPerformed(ActionEvent evt) {
323            if (currentChangeset == null)
324                return;
325            ChangesetHeaderDownloadTask task = new ChangesetHeaderDownloadTask(
326                    ChangesetDetailPanel.this,
327                    Collections.singleton(currentChangeset.getId())
328            );
329            MainApplication.worker.submit(new PostDownloadHandler(task, task.download()));
330        }
331
332        public void initProperties(Changeset cs) {
333            setEnabled(cs != null && !Main.isOffline(OnlineResource.OSM_API));
334        }
335    }
336
337    /**
338     * The action for opening {@link OpenChangesetPopupMenu}
339     */
340    class OpenChangesetPopupMenuAction extends AbstractAction {
341        OpenChangesetPopupMenuAction() {
342            putValue(NAME, tr("View changeset"));
343            new ImageProvider("help/internet").getResource().attachImageIcon(this);
344        }
345
346        @Override
347        public void actionPerformed(ActionEvent evt) {
348            if (currentChangeset != null)
349                new OpenChangesetPopupMenu(currentChangeset.getId()).show(btnOpenChangesetPopupMenu);
350        }
351
352        void initProperties(Changeset cs) {
353            setEnabled(cs != null);
354        }
355    }
356
357    /**
358     * Selects the primitives in the content of this changeset in the current data layer.
359     *
360     */
361    class SelectInCurrentLayerAction extends AbstractAction implements ActiveLayerChangeListener {
362
363        SelectInCurrentLayerAction() {
364            putValue(NAME, tr("Select in layer"));
365            new ImageProvider("dialogs", "select").getResource().attachImageIcon(this);
366            putValue(SHORT_DESCRIPTION, tr("Select the primitives in the content of this changeset in the current data layer"));
367            updateEnabledState();
368        }
369
370        protected void alertNoPrimitivesToSelect() {
371            HelpAwareOptionPane.showOptionDialog(
372                    ChangesetDetailPanel.this,
373                    tr("<html>None of the objects in the content of changeset {0} is available in the current<br>"
374                            + "edit layer ''{1}''.</html>",
375                            currentChangeset.getId(),
376                            Utils.escapeReservedCharactersHTML(MainApplication.getLayerManager().getActiveDataSet().getName())
377                    ),
378                    tr("Nothing to select"),
379                    JOptionPane.WARNING_MESSAGE,
380                    HelpUtil.ht("/Dialog/ChangesetCacheManager#NothingToSelectInLayer")
381            );
382        }
383
384        @Override
385        public void actionPerformed(ActionEvent e) {
386            if (!isEnabled())
387                return;
388            DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
389            if (ds == null) {
390                return;
391            }
392            Set<OsmPrimitive> target = new HashSet<>();
393            for (OsmPrimitive p: ds.allPrimitives()) {
394                if (p.isUsable() && p.getChangesetId() == currentChangeset.getId()) {
395                    target.add(p);
396                }
397            }
398            if (target.isEmpty()) {
399                alertNoPrimitivesToSelect();
400                return;
401            }
402            ds.setSelected(target);
403        }
404
405        public void updateEnabledState() {
406            setEnabled(MainApplication.getLayerManager().getActiveDataSet() != null && currentChangeset != null);
407        }
408
409        @Override
410        public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
411            updateEnabledState();
412        }
413    }
414
415    /**
416     * Zooms to the primitives in the content of this changeset in the current
417     * data layer.
418     *
419     */
420    class ZoomInCurrentLayerAction extends AbstractAction implements ActiveLayerChangeListener {
421
422        ZoomInCurrentLayerAction() {
423            putValue(NAME, tr("Zoom to in layer"));
424            new ImageProvider("dialogs/autoscale", "selection").getResource().attachImageIcon(this);
425            putValue(SHORT_DESCRIPTION, tr("Zoom to the objects in the content of this changeset in the current data layer"));
426            updateEnabledState();
427        }
428
429        protected void alertNoPrimitivesToZoomTo() {
430            HelpAwareOptionPane.showOptionDialog(
431                    ChangesetDetailPanel.this,
432                    tr("<html>None of the objects in the content of changeset {0} is available in the current<br>"
433                            + "edit layer ''{1}''.</html>",
434                            currentChangeset.getId(),
435                            MainApplication.getLayerManager().getActiveDataSet().getName()
436                    ),
437                    tr("Nothing to zoom to"),
438                    JOptionPane.WARNING_MESSAGE,
439                    HelpUtil.ht("/Dialog/ChangesetCacheManager#NothingToZoomTo")
440            );
441        }
442
443        @Override
444        public void actionPerformed(ActionEvent e) {
445            if (!isEnabled())
446                return;
447            DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
448            if (ds == null) {
449                return;
450            }
451            Set<OsmPrimitive> target = new HashSet<>();
452            for (OsmPrimitive p: ds.allPrimitives()) {
453                if (p.isUsable() && p.getChangesetId() == currentChangeset.getId()) {
454                    target.add(p);
455                }
456            }
457            if (target.isEmpty()) {
458                alertNoPrimitivesToZoomTo();
459                return;
460            }
461            ds.setSelected(target);
462            AutoScaleAction.zoomToSelection();
463        }
464
465        public void updateEnabledState() {
466            setEnabled(MainApplication.getLayerManager().getActiveDataSet() != null && currentChangeset != null);
467        }
468
469        @Override
470        public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
471            updateEnabledState();
472        }
473    }
474
475    @Override
476    public Changeset getCurrentChangeset() {
477        return currentChangeset;
478    }
479}