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}