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