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