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.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 Changeset current = null;
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        public 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        public 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        public 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            if (cs == null) {
348                setEnabled(false);
349                return;
350            } else {
351                setEnabled(true);
352            }
353        }
354    }
355
356    /**
357     * Selects the primitives in the content of this changeset in the current
358     * data layer.
359     *
360     */
361    class SelectInCurrentLayerAction extends AbstractAction implements EditLayerChangeListener{
362
363        public SelectInCurrentLayerAction() {
364            putValue(NAME, tr("Select in layer"));
365            putValue(SMALL_ICON, ImageProvider.get("dialogs", "select"));
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(Collection<OsmPrimitive> primitives) {
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                            current.getId(),
376                            Main.main.getEditLayer().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 arg0) {
386            if (!isEnabled())
387                return;
388            if (Main.main == null || !Main.main.hasEditLayer()) return;
389            OsmDataLayer layer = Main.main.getEditLayer();
390            Set<OsmPrimitive> target = new HashSet<>();
391            for (OsmPrimitive p: layer.data.allPrimitives()) {
392                if (p.isUsable() && p.getChangesetId() == current.getId()) {
393                    target.add(p);
394                }
395            }
396            if (target.isEmpty()) {
397                alertNoPrimitivesToSelect(target);
398                return;
399            }
400            layer.data.setSelected(target);
401        }
402
403        public void updateEnabledState() {
404            if (Main.main == null || !Main.main.hasEditLayer()) {
405                setEnabled(false);
406                return;
407            }
408            setEnabled(current != null);
409        }
410
411        @Override
412        public void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) {
413            updateEnabledState();
414        }
415    }
416
417    /**
418     * Zooms to the primitives in the content of this changeset in the current
419     * data layer.
420     *
421     */
422    class ZoomInCurrentLayerAction extends AbstractAction implements EditLayerChangeListener{
423
424        public ZoomInCurrentLayerAction() {
425            putValue(NAME, tr("Zoom to in layer"));
426            putValue(SMALL_ICON, ImageProvider.get("dialogs/autoscale", "selection"));
427            putValue(SHORT_DESCRIPTION, tr("Zoom to the objects in the content of this changeset in the current data layer"));
428            updateEnabledState();
429        }
430
431        protected void alertNoPrimitivesToZoomTo() {
432            HelpAwareOptionPane.showOptionDialog(
433                    ChangesetDetailPanel.this,
434                    tr("<html>None of the objects in the content of changeset {0} is available in the current<br>"
435                            + "edit layer ''{1}''.</html>",
436                            current.getId(),
437                            Main.main.getEditLayer().getName()
438                    ),
439                    tr("Nothing to zoom to"),
440                    JOptionPane.WARNING_MESSAGE,
441                    HelpUtil.ht("/Dialog/ChangesetCacheManager#NothingToZoomTo")
442            );
443        }
444
445        @Override
446        public void actionPerformed(ActionEvent arg0) {
447            if (!isEnabled())
448                return;
449            if (Main.main == null || !Main.main.hasEditLayer()) return;
450            OsmDataLayer layer = Main.main.getEditLayer();
451            Set<OsmPrimitive> target = new HashSet<>();
452            for (OsmPrimitive p: layer.data.allPrimitives()) {
453                if (p.isUsable() && p.getChangesetId() == current.getId()) {
454                    target.add(p);
455                }
456            }
457            if (target.isEmpty()) {
458                alertNoPrimitivesToZoomTo();
459                return;
460            }
461            layer.data.setSelected(target);
462            AutoScaleAction.zoomToSelection();
463        }
464
465        public void updateEnabledState() {
466            if (Main.main == null || !Main.main.hasEditLayer()) {
467                setEnabled(false);
468                return;
469            }
470            setEnabled(current != null);
471        }
472
473        @Override
474        public void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) {
475            updateEnabledState();
476        }
477    }
478}