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