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