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