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