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.trn; 006 007import java.awt.BorderLayout; 008import java.awt.FlowLayout; 009import java.awt.event.ActionEvent; 010import java.awt.event.ComponentAdapter; 011import java.awt.event.ComponentEvent; 012import java.beans.PropertyChangeEvent; 013import java.beans.PropertyChangeListener; 014import java.util.ArrayList; 015import java.util.Collection; 016import java.util.HashSet; 017import java.util.List; 018import java.util.Set; 019import java.util.stream.Collectors; 020 021import javax.swing.AbstractAction; 022import javax.swing.BorderFactory; 023import javax.swing.DefaultListSelectionModel; 024import javax.swing.JButton; 025import javax.swing.JOptionPane; 026import javax.swing.JPanel; 027import javax.swing.JPopupMenu; 028import javax.swing.JScrollPane; 029import javax.swing.JSeparator; 030import javax.swing.JTable; 031import javax.swing.JToolBar; 032import javax.swing.event.ListSelectionEvent; 033import javax.swing.event.ListSelectionListener; 034 035import org.openstreetmap.josm.Main; 036import org.openstreetmap.josm.actions.AutoScaleAction; 037import org.openstreetmap.josm.actions.downloadtasks.ChangesetContentDownloadTask; 038import org.openstreetmap.josm.data.osm.Changeset; 039import org.openstreetmap.josm.data.osm.OsmPrimitive; 040import org.openstreetmap.josm.data.osm.PrimitiveId; 041import org.openstreetmap.josm.data.osm.history.History; 042import org.openstreetmap.josm.data.osm.history.HistoryDataSet; 043import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive; 044import org.openstreetmap.josm.gui.HelpAwareOptionPane; 045import org.openstreetmap.josm.gui.help.HelpUtil; 046import org.openstreetmap.josm.gui.history.HistoryBrowserDialogManager; 047import org.openstreetmap.josm.gui.history.HistoryLoadTask; 048import org.openstreetmap.josm.gui.io.DownloadPrimitivesWithReferrersTask; 049import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent; 050import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener; 051import org.openstreetmap.josm.gui.layer.OsmDataLayer; 052import org.openstreetmap.josm.gui.util.GuiHelper; 053import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 054import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 055import org.openstreetmap.josm.tools.ImageProvider; 056import org.openstreetmap.josm.tools.bugreport.BugReportExceptionHandler; 057 058/** 059 * The panel which displays the content of a changeset in a scrollable table. 060 * 061 * It listens to property change events for {@link ChangesetCacheManagerModel#CHANGESET_IN_DETAIL_VIEW_PROP} 062 * and updates its view accordingly. 063 * 064 */ 065public class ChangesetContentPanel extends JPanel implements PropertyChangeListener, ChangesetAware { 066 067 private ChangesetContentTableModel model; 068 private transient Changeset currentChangeset; 069 070 private DownloadChangesetContentAction actDownloadContentAction; 071 private ShowHistoryAction actShowHistory; 072 private SelectInCurrentLayerAction actSelectInCurrentLayerAction; 073 private ZoomInCurrentLayerAction actZoomInCurrentLayerAction; 074 075 private final HeaderPanel pnlHeader = new HeaderPanel(); 076 public DownloadObjectAction actDownloadObjectAction; 077 078 protected void buildModels() { 079 DefaultListSelectionModel selectionModel = new DefaultListSelectionModel(); 080 model = new ChangesetContentTableModel(selectionModel); 081 actDownloadContentAction = new DownloadChangesetContentAction(this); 082 actDownloadContentAction.initProperties(); 083 084 actDownloadObjectAction = new DownloadObjectAction(); 085 model.getSelectionModel().addListSelectionListener(actDownloadObjectAction); 086 087 actShowHistory = new ShowHistoryAction(); 088 model.getSelectionModel().addListSelectionListener(actShowHistory); 089 090 actSelectInCurrentLayerAction = new SelectInCurrentLayerAction(); 091 model.getSelectionModel().addListSelectionListener(actSelectInCurrentLayerAction); 092 Main.getLayerManager().addActiveLayerChangeListener(actSelectInCurrentLayerAction); 093 094 actZoomInCurrentLayerAction = new ZoomInCurrentLayerAction(); 095 model.getSelectionModel().addListSelectionListener(actZoomInCurrentLayerAction); 096 Main.getLayerManager().addActiveLayerChangeListener(actZoomInCurrentLayerAction); 097 098 addComponentListener( 099 new ComponentAdapter() { 100 @Override 101 public void componentShown(ComponentEvent e) { 102 Main.getLayerManager().addAndFireActiveLayerChangeListener(actSelectInCurrentLayerAction); 103 Main.getLayerManager().addAndFireActiveLayerChangeListener(actZoomInCurrentLayerAction); 104 } 105 106 @Override 107 public void componentHidden(ComponentEvent e) { 108 // make sure the listener is unregistered when the panel becomes invisible 109 Main.getLayerManager().removeActiveLayerChangeListener(actSelectInCurrentLayerAction); 110 Main.getLayerManager().removeActiveLayerChangeListener(actZoomInCurrentLayerAction); 111 } 112 } 113 ); 114 } 115 116 protected JPanel buildContentPanel() { 117 JPanel pnl = new JPanel(new BorderLayout()); 118 JTable tblContent = new JTable( 119 model, 120 new ChangesetContentTableColumnModel(), 121 model.getSelectionModel() 122 ); 123 tblContent.addMouseListener(new PopupMenuLauncher(new ChangesetContentTablePopupMenu())); 124 pnl.add(new JScrollPane(tblContent), BorderLayout.CENTER); 125 return pnl; 126 } 127 128 protected JPanel buildActionButtonPanel() { 129 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT)); 130 JToolBar tb = new JToolBar(JToolBar.VERTICAL); 131 tb.setFloatable(false); 132 133 tb.add(actDownloadContentAction); 134 tb.addSeparator(); 135 tb.add(actDownloadObjectAction); 136 tb.add(actShowHistory); 137 tb.addSeparator(); 138 tb.add(actSelectInCurrentLayerAction); 139 tb.add(actZoomInCurrentLayerAction); 140 141 pnl.add(tb); 142 return pnl; 143 } 144 145 protected final void build() { 146 setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 147 setLayout(new BorderLayout()); 148 buildModels(); 149 150 add(pnlHeader, BorderLayout.NORTH); 151 add(buildActionButtonPanel(), BorderLayout.WEST); 152 add(buildContentPanel(), BorderLayout.CENTER); 153 } 154 155 /** 156 * Constructs a new {@code ChangesetContentPanel}. 157 */ 158 public ChangesetContentPanel() { 159 build(); 160 } 161 162 /** 163 * Replies the changeset content model 164 * @return The model 165 */ 166 public ChangesetContentTableModel getModel() { 167 return model; 168 } 169 170 protected void setCurrentChangeset(Changeset cs) { 171 currentChangeset = cs; 172 if (cs == null) { 173 model.populate(null); 174 } else { 175 model.populate(cs.getContent()); 176 } 177 actDownloadContentAction.initProperties(); 178 pnlHeader.setChangeset(cs); 179 } 180 181 /* ---------------------------------------------------------------------------- */ 182 /* interface PropertyChangeListener */ 183 /* ---------------------------------------------------------------------------- */ 184 @Override 185 public void propertyChange(PropertyChangeEvent evt) { 186 if (!evt.getPropertyName().equals(ChangesetCacheManagerModel.CHANGESET_IN_DETAIL_VIEW_PROP)) 187 return; 188 Changeset cs = (Changeset) evt.getNewValue(); 189 setCurrentChangeset(cs); 190 } 191 192 private void alertNoPrimitivesTo(Collection<HistoryOsmPrimitive> primitives, String title, String helpTopic) { 193 HelpAwareOptionPane.showOptionDialog( 194 this, 195 trn("<html>The selected object is not available in the current<br>" 196 + "edit layer ''{0}''.</html>", 197 "<html>None of the selected objects is available in the current<br>" 198 + "edit layer ''{0}''.</html>", 199 primitives.size(), 200 Main.getLayerManager().getEditLayer().getName() 201 ), 202 title, JOptionPane.WARNING_MESSAGE, helpTopic 203 ); 204 } 205 206 class ChangesetContentTablePopupMenu extends JPopupMenu { 207 ChangesetContentTablePopupMenu() { 208 add(actDownloadContentAction); 209 add(new JSeparator()); 210 add(actDownloadObjectAction); 211 add(actShowHistory); 212 add(new JSeparator()); 213 add(actSelectInCurrentLayerAction); 214 add(actZoomInCurrentLayerAction); 215 } 216 } 217 218 class ShowHistoryAction extends AbstractAction implements ListSelectionListener { 219 220 private final class ShowHistoryTask implements Runnable { 221 private final Collection<HistoryOsmPrimitive> primitives; 222 223 private ShowHistoryTask(Collection<HistoryOsmPrimitive> primitives) { 224 this.primitives = primitives; 225 } 226 227 @Override 228 public void run() { 229 try { 230 for (HistoryOsmPrimitive p : primitives) { 231 final History h = HistoryDataSet.getInstance().getHistory(p.getPrimitiveId()); 232 if (h == null) { 233 continue; 234 } 235 GuiHelper.runInEDT(() -> HistoryBrowserDialogManager.getInstance().show(h)); 236 } 237 } catch (final RuntimeException e) { 238 GuiHelper.runInEDT(() -> BugReportExceptionHandler.handleException(e)); 239 } 240 } 241 } 242 243 ShowHistoryAction() { 244 putValue(NAME, tr("Show history")); 245 new ImageProvider("dialogs", "history").getResource().attachImageIcon(this); 246 putValue(SHORT_DESCRIPTION, tr("Download and show the history of the selected objects")); 247 updateEnabledState(); 248 } 249 250 protected List<HistoryOsmPrimitive> filterPrimitivesWithUnloadedHistory(Collection<HistoryOsmPrimitive> primitives) { 251 List<HistoryOsmPrimitive> ret = new ArrayList<>(primitives.size()); 252 for (HistoryOsmPrimitive p: primitives) { 253 if (HistoryDataSet.getInstance().getHistory(p.getPrimitiveId()) == null) { 254 ret.add(p); 255 } 256 } 257 return ret; 258 } 259 260 public void showHistory(final Collection<HistoryOsmPrimitive> primitives) { 261 262 List<HistoryOsmPrimitive> toLoad = filterPrimitivesWithUnloadedHistory(primitives); 263 if (!toLoad.isEmpty()) { 264 HistoryLoadTask task = new HistoryLoadTask(ChangesetContentPanel.this); 265 for (HistoryOsmPrimitive p: toLoad) { 266 task.add(p); 267 } 268 Main.worker.submit(task); 269 } 270 271 Main.worker.submit(new ShowHistoryTask(primitives)); 272 } 273 274 protected final void updateEnabledState() { 275 setEnabled(model.hasSelectedPrimitives()); 276 } 277 278 @Override 279 public void actionPerformed(ActionEvent arg0) { 280 Set<HistoryOsmPrimitive> selected = model.getSelectedPrimitives(); 281 if (selected.isEmpty()) return; 282 showHistory(selected); 283 } 284 285 @Override 286 public void valueChanged(ListSelectionEvent e) { 287 updateEnabledState(); 288 } 289 } 290 291 class DownloadObjectAction extends AbstractAction implements ListSelectionListener { 292 293 DownloadObjectAction() { 294 putValue(NAME, tr("Download objects")); 295 putValue(SMALL_ICON, ImageProvider.get("downloadprimitive")); 296 putValue(SHORT_DESCRIPTION, tr("Download the current version of the selected objects")); 297 updateEnabledState(); 298 } 299 300 @Override 301 public void actionPerformed(ActionEvent arg0) { 302 final List<PrimitiveId> primitiveIds = model.getSelectedPrimitives().stream().map(HistoryOsmPrimitive::getPrimitiveId) 303 .collect(Collectors.toList()); 304 Main.worker.submit(new DownloadPrimitivesWithReferrersTask(false, primitiveIds, true, true, null, null)); 305 } 306 307 protected final void updateEnabledState() { 308 setEnabled(model.hasSelectedPrimitives()); 309 } 310 311 @Override 312 public void valueChanged(ListSelectionEvent e) { 313 updateEnabledState(); 314 } 315 } 316 317 abstract class SelectionBasedAction extends AbstractAction implements ListSelectionListener, ActiveLayerChangeListener { 318 319 protected Set<OsmPrimitive> getTarget() { 320 if (!isEnabled()) { 321 return null; 322 } 323 OsmDataLayer layer = Main.getLayerManager().getEditLayer(); 324 if (layer == null) { 325 return null; 326 } 327 Set<OsmPrimitive> target = new HashSet<>(); 328 for (HistoryOsmPrimitive p : model.getSelectedPrimitives()) { 329 OsmPrimitive op = layer.data.getPrimitiveById(p.getPrimitiveId()); 330 if (op != null) { 331 target.add(op); 332 } 333 } 334 return target; 335 } 336 337 public final void updateEnabledState() { 338 setEnabled(Main.getLayerManager().getEditLayer() != null && model.hasSelectedPrimitives()); 339 } 340 341 @Override 342 public void valueChanged(ListSelectionEvent e) { 343 updateEnabledState(); 344 } 345 346 @Override 347 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { 348 updateEnabledState(); 349 } 350 351 } 352 353 class SelectInCurrentLayerAction extends SelectionBasedAction { 354 355 SelectInCurrentLayerAction() { 356 putValue(NAME, tr("Select in layer")); 357 new ImageProvider("dialogs", "select").getResource().attachImageIcon(this); 358 putValue(SHORT_DESCRIPTION, tr("Select the corresponding primitives in the current data layer")); 359 updateEnabledState(); 360 } 361 362 @Override 363 public void actionPerformed(ActionEvent arg0) { 364 final Set<OsmPrimitive> target = getTarget(); 365 if (target == null) { 366 return; 367 } else if (target.isEmpty()) { 368 alertNoPrimitivesTo(model.getSelectedPrimitives(), tr("Nothing to select"), 369 HelpUtil.ht("/Dialog/ChangesetCacheManager#NothingToSelectInLayer")); 370 return; 371 } 372 Main.getLayerManager().getEditLayer().data.setSelected(target); 373 } 374 } 375 376 class ZoomInCurrentLayerAction extends SelectionBasedAction { 377 378 ZoomInCurrentLayerAction() { 379 putValue(NAME, tr("Zoom to in layer")); 380 new ImageProvider("dialogs/autoscale", "selection").getResource().attachImageIcon(this); 381 putValue(SHORT_DESCRIPTION, tr("Zoom to the corresponding objects in the current data layer")); 382 updateEnabledState(); 383 } 384 385 @Override 386 public void actionPerformed(ActionEvent arg0) { 387 final Set<OsmPrimitive> target = getTarget(); 388 if (target == null) { 389 return; 390 } else if (target.isEmpty()) { 391 alertNoPrimitivesTo(model.getSelectedPrimitives(), tr("Nothing to zoom to"), 392 HelpUtil.ht("/Dialog/ChangesetCacheManager#NothingToZoomTo")); 393 return; 394 } 395 Main.getLayerManager().getEditLayer().data.setSelected(target); 396 AutoScaleAction.zoomToSelection(); 397 } 398 } 399 400 private static class HeaderPanel extends JPanel { 401 402 private transient Changeset current; 403 404 HeaderPanel() { 405 build(); 406 } 407 408 protected final void build() { 409 setLayout(new FlowLayout(FlowLayout.LEFT)); 410 add(new JMultilineLabel(tr("The content of this changeset is not downloaded yet."))); 411 add(new JButton(new DownloadAction())); 412 413 } 414 415 public void setChangeset(Changeset cs) { 416 setVisible(cs != null && cs.getContent() == null); 417 this.current = cs; 418 } 419 420 private class DownloadAction extends AbstractAction { 421 DownloadAction() { 422 putValue(NAME, tr("Download now")); 423 putValue(SHORT_DESCRIPTION, tr("Download the changeset content")); 424 new ImageProvider("dialogs/changeset", "downloadchangesetcontent").getResource().attachImageIcon(this); 425 } 426 427 @Override 428 public void actionPerformed(ActionEvent evt) { 429 if (current == null) return; 430 ChangesetContentDownloadTask task = new ChangesetContentDownloadTask(HeaderPanel.this, current.getId()); 431 ChangesetCacheManager.getInstance().runDownloadTask(task); 432 } 433 } 434 } 435 436 @Override 437 public Changeset getCurrentChangeset() { 438 return currentChangeset; 439 } 440}