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