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.data.osm.Changeset; 032import org.openstreetmap.josm.data.osm.ChangesetCache; 033import org.openstreetmap.josm.data.osm.OsmPrimitive; 034import org.openstreetmap.josm.gui.HelpAwareOptionPane; 035import org.openstreetmap.josm.gui.MapView; 036import org.openstreetmap.josm.gui.MapView.EditLayerChangeListener; 037import org.openstreetmap.josm.gui.help.HelpUtil; 038import org.openstreetmap.josm.gui.layer.OsmDataLayer; 039import org.openstreetmap.josm.gui.widgets.JosmTextArea; 040import org.openstreetmap.josm.gui.widgets.JosmTextField; 041import org.openstreetmap.josm.io.OnlineResource; 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, ChangesetAware { 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(this); 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 transient Changeset currentChangeset; 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(currentChangeset); 076 077 // -- changeset update 078 tb.add(actUpdateChangesets); 079 actUpdateChangesets.initProperties(currentChangeset); 080 081 // -- changeset content download 082 tb.add(actDownloadChangesetContent); 083 actDownloadChangesetContent.initProperties(); 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 currentChangeset = cs; 243 if (cs == null) { 244 clearView(); 245 } else { 246 updateView(cs); 247 } 248 actDownloadChangesetContent.initProperties(); 249 actUpdateChangesets.initProperties(currentChangeset); 250 actRemoveFromCache.initProperties(currentChangeset); 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 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 (currentChangeset == null) 278 return; 279 ChangesetCache.getInstance().remove(currentChangeset); 280 } 281 282 public void initProperties(Changeset cs) { 283 setEnabled(cs != null); 284 } 285 } 286 287 /** 288 * Updates the current changeset from the OSM server 289 * 290 */ 291 class UpdateChangesetAction extends AbstractAction { 292 UpdateChangesetAction() { 293 putValue(NAME, tr("Update changeset")); 294 putValue(SMALL_ICON, ChangesetCacheManager.UPDATE_CONTENT_ICON); 295 putValue(SHORT_DESCRIPTION, tr("Update the changeset from the OSM server")); 296 } 297 298 @Override 299 public void actionPerformed(ActionEvent evt) { 300 if (currentChangeset == null) return; 301 Main.worker.submit( 302 new ChangesetHeaderDownloadTask( 303 ChangesetDetailPanel.this, 304 Collections.singleton(currentChangeset.getId()) 305 ) 306 ); 307 } 308 309 public void initProperties(Changeset cs) { 310 setEnabled(cs != null && !Main.isOffline(OnlineResource.OSM_API)); 311 } 312 } 313 314 /** 315 * Selects the primitives in the content of this changeset in the current 316 * data layer. 317 * 318 */ 319 class SelectInCurrentLayerAction extends AbstractAction implements EditLayerChangeListener { 320 321 SelectInCurrentLayerAction() { 322 putValue(NAME, tr("Select in layer")); 323 putValue(SMALL_ICON, ImageProvider.get("dialogs", "select")); 324 putValue(SHORT_DESCRIPTION, tr("Select the primitives in the content of this changeset in the current data layer")); 325 updateEnabledState(); 326 } 327 328 protected void alertNoPrimitivesToSelect() { 329 HelpAwareOptionPane.showOptionDialog( 330 ChangesetDetailPanel.this, 331 tr("<html>None of the objects in the content of changeset {0} is available in the current<br>" 332 + "edit layer ''{1}''.</html>", 333 currentChangeset.getId(), 334 Main.main.getEditLayer().getName() 335 ), 336 tr("Nothing to select"), 337 JOptionPane.WARNING_MESSAGE, 338 HelpUtil.ht("/Dialog/ChangesetCacheManager#NothingToSelectInLayer") 339 ); 340 } 341 342 @Override 343 public void actionPerformed(ActionEvent arg0) { 344 if (!isEnabled()) 345 return; 346 if (Main.main == null || !Main.main.hasEditLayer()) return; 347 OsmDataLayer layer = Main.main.getEditLayer(); 348 Set<OsmPrimitive> target = new HashSet<>(); 349 for (OsmPrimitive p: layer.data.allPrimitives()) { 350 if (p.isUsable() && p.getChangesetId() == currentChangeset.getId()) { 351 target.add(p); 352 } 353 } 354 if (target.isEmpty()) { 355 alertNoPrimitivesToSelect(); 356 return; 357 } 358 layer.data.setSelected(target); 359 } 360 361 public void updateEnabledState() { 362 if (Main.main == null || !Main.main.hasEditLayer()) { 363 setEnabled(false); 364 return; 365 } 366 setEnabled(currentChangeset != null); 367 } 368 369 @Override 370 public void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) { 371 updateEnabledState(); 372 } 373 } 374 375 /** 376 * Zooms to the primitives in the content of this changeset in the current 377 * data layer. 378 * 379 */ 380 class ZoomInCurrentLayerAction extends AbstractAction implements EditLayerChangeListener { 381 382 ZoomInCurrentLayerAction() { 383 putValue(NAME, tr("Zoom to in layer")); 384 putValue(SMALL_ICON, ImageProvider.get("dialogs/autoscale", "selection")); 385 putValue(SHORT_DESCRIPTION, tr("Zoom to the objects in the content of this changeset in the current data layer")); 386 updateEnabledState(); 387 } 388 389 protected void alertNoPrimitivesToZoomTo() { 390 HelpAwareOptionPane.showOptionDialog( 391 ChangesetDetailPanel.this, 392 tr("<html>None of the objects in the content of changeset {0} is available in the current<br>" 393 + "edit layer ''{1}''.</html>", 394 currentChangeset.getId(), 395 Main.main.getEditLayer().getName() 396 ), 397 tr("Nothing to zoom to"), 398 JOptionPane.WARNING_MESSAGE, 399 HelpUtil.ht("/Dialog/ChangesetCacheManager#NothingToZoomTo") 400 ); 401 } 402 403 @Override 404 public void actionPerformed(ActionEvent arg0) { 405 if (!isEnabled()) 406 return; 407 if (Main.main == null || !Main.main.hasEditLayer()) return; 408 OsmDataLayer layer = Main.main.getEditLayer(); 409 Set<OsmPrimitive> target = new HashSet<>(); 410 for (OsmPrimitive p: layer.data.allPrimitives()) { 411 if (p.isUsable() && p.getChangesetId() == currentChangeset.getId()) { 412 target.add(p); 413 } 414 } 415 if (target.isEmpty()) { 416 alertNoPrimitivesToZoomTo(); 417 return; 418 } 419 layer.data.setSelected(target); 420 AutoScaleAction.zoomToSelection(); 421 } 422 423 public void updateEnabledState() { 424 if (Main.main == null || !Main.main.hasEditLayer()) { 425 setEnabled(false); 426 return; 427 } 428 setEnabled(currentChangeset != null); 429 } 430 431 @Override 432 public void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) { 433 updateEnabledState(); 434 } 435 } 436 437 @Override 438 public Changeset getCurrentChangeset() { 439 return currentChangeset; 440 } 441}