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