001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.dialogs.relation; 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.Component; 009import java.awt.Dialog; 010import java.awt.FlowLayout; 011import java.awt.Point; 012import java.awt.event.ActionEvent; 013import java.awt.event.MouseEvent; 014import java.io.IOException; 015import java.util.Arrays; 016import java.util.List; 017import java.util.Set; 018import java.util.stream.Collectors; 019 020import javax.swing.AbstractAction; 021import javax.swing.JButton; 022import javax.swing.JOptionPane; 023import javax.swing.JPanel; 024import javax.swing.JPopupMenu; 025import javax.swing.JScrollPane; 026import javax.swing.JTree; 027import javax.swing.SwingUtilities; 028import javax.swing.event.TreeSelectionEvent; 029import javax.swing.event.TreeSelectionListener; 030import javax.swing.tree.TreePath; 031 032import org.openstreetmap.josm.data.osm.DataSet; 033import org.openstreetmap.josm.data.osm.DataSetMerger; 034import org.openstreetmap.josm.data.osm.DefaultNameFormatter; 035import org.openstreetmap.josm.data.osm.OsmPrimitive; 036import org.openstreetmap.josm.data.osm.Relation; 037import org.openstreetmap.josm.data.osm.RelationMember; 038import org.openstreetmap.josm.gui.ExceptionDialogUtil; 039import org.openstreetmap.josm.gui.MainApplication; 040import org.openstreetmap.josm.gui.PleaseWaitRunnable; 041import org.openstreetmap.josm.gui.PopupMenuHandler; 042import org.openstreetmap.josm.gui.dialogs.relation.actions.DuplicateRelationAction; 043import org.openstreetmap.josm.gui.layer.OsmDataLayer; 044import org.openstreetmap.josm.gui.progress.ProgressMonitor; 045import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor; 046import org.openstreetmap.josm.gui.util.GuiHelper; 047import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 048import org.openstreetmap.josm.io.MultiFetchServerObjectReader; 049import org.openstreetmap.josm.io.OsmTransferException; 050import org.openstreetmap.josm.tools.CheckParameterUtil; 051import org.openstreetmap.josm.tools.ImageProvider; 052import org.openstreetmap.josm.tools.Logging; 053import org.openstreetmap.josm.tools.Utils; 054import org.xml.sax.SAXException; 055 056/** 057 * ChildRelationBrowser is a UI component which provides a tree-like view on the hierarchical 058 * structure of relations. 059 * 060 * @since 1828 061 */ 062public class ChildRelationBrowser extends JPanel { 063 /** the tree with relation children */ 064 private RelationTree childTree; 065 /** the tree model */ 066 private final transient RelationTreeModel model; 067 068 /** the osm data layer this browser is related to */ 069 private final transient OsmDataLayer layer; 070 071 /** the editAction used in the bottom panel and for doubleClick */ 072 private EditAction editAction; 073 074 /** 075 * Replies the {@link OsmDataLayer} this editor is related to 076 * 077 * @return the osm data layer 078 */ 079 protected OsmDataLayer getLayer() { 080 return layer; 081 } 082 083 /** 084 * builds the UI 085 */ 086 protected void build() { 087 setLayout(new BorderLayout()); 088 childTree = new RelationTree(model); 089 JScrollPane pane = new JScrollPane(childTree); 090 add(pane, BorderLayout.CENTER); 091 092 final JPopupMenu popupMenu = new JPopupMenu(); 093 final PopupMenuHandler popupMenuHandler = new PopupMenuHandler(popupMenu); 094 RelationPopupMenus.setupHandler(popupMenuHandler, DuplicateRelationAction.class); 095 096 add(buildButtonPanel(), BorderLayout.SOUTH); 097 childTree.setToggleClickCount(0); 098 childTree.addMouseListener(new PopupMenuLauncher(popupMenu) { 099 @Override 100 public void mouseClicked(MouseEvent e) { 101 if (e.getClickCount() == 2 102 && !e.isAltDown() && !e.isAltGraphDown() && !e.isControlDown() && !e.isMetaDown() && !e.isShiftDown() 103 && childTree.getRowForLocation(e.getX(), e.getY()) == childTree.getMinSelectionRow()) { 104 Relation r = (Relation) childTree.getLastSelectedPathComponent(); 105 if (r != null && r.isIncomplete()) { 106 childTree.expandPath(childTree.getSelectionPath()); 107 } else { 108 editAction.actionPerformed(new ActionEvent(e.getSource(), ActionEvent.ACTION_PERFORMED, null)); 109 } 110 } 111 } 112 113 @Override 114 protected TreePath checkTreeSelection(JTree tree, Point p) { 115 final TreePath treeSelection = super.checkTreeSelection(tree, p); 116 final TreePath[] selectionPaths = tree.getSelectionPaths(); 117 if (selectionPaths == null) { 118 return treeSelection; 119 } 120 final List<OsmPrimitive> relations = Arrays.stream(selectionPaths) 121 .map(TreePath::getLastPathComponent) 122 .map(OsmPrimitive.class::cast) 123 .collect(Collectors.toList()); 124 popupMenuHandler.setPrimitives(relations); 125 return treeSelection; 126 } 127 }); 128 } 129 130 /** 131 * builds the panel with the command buttons 132 * 133 * @return the button panel 134 */ 135 protected JPanel buildButtonPanel() { 136 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT)); 137 138 // --- 139 DownloadAllChildRelationsAction downloadAction = new DownloadAllChildRelationsAction(); 140 pnl.add(new JButton(downloadAction)); 141 142 // --- 143 DownloadSelectedAction downloadSelectedAction = new DownloadSelectedAction(); 144 childTree.addTreeSelectionListener(downloadSelectedAction); 145 pnl.add(new JButton(downloadSelectedAction)); 146 147 // --- 148 editAction = new EditAction(); 149 childTree.addTreeSelectionListener(editAction); 150 pnl.add(new JButton(editAction)); 151 152 return pnl; 153 } 154 155 /** 156 * constructor 157 * 158 * @param layer the {@link OsmDataLayer} this browser is related to. Must not be null. 159 * @throws IllegalArgumentException if layer is null 160 */ 161 public ChildRelationBrowser(OsmDataLayer layer) { 162 CheckParameterUtil.ensureParameterNotNull(layer, "layer"); 163 this.layer = layer; 164 model = new RelationTreeModel(); 165 build(); 166 } 167 168 /** 169 * constructor 170 * 171 * @param layer the {@link OsmDataLayer} this browser is related to. Must not be null. 172 * @param root the root relation 173 * @throws IllegalArgumentException if layer is null 174 */ 175 public ChildRelationBrowser(OsmDataLayer layer, Relation root) { 176 this(layer); 177 populate(root); 178 } 179 180 /** 181 * populates the browser with a relation 182 * 183 * @param r the relation 184 */ 185 public void populate(Relation r) { 186 model.populate(r); 187 } 188 189 /** 190 * populates the browser with a list of relation members 191 * 192 * @param members the list of relation members 193 */ 194 195 public void populate(List<RelationMember> members) { 196 model.populate(members); 197 } 198 199 /** 200 * replies the parent dialog this browser is embedded in 201 * 202 * @return the parent dialog; null, if there is no {@link Dialog} as parent dialog 203 */ 204 protected Dialog getParentDialog() { 205 Component c = this; 206 while (c != null && !(c instanceof Dialog)) { 207 c = c.getParent(); 208 } 209 return (Dialog) c; 210 } 211 212 /** 213 * Action for editing the currently selected relation 214 */ 215 class EditAction extends AbstractAction implements TreeSelectionListener { 216 EditAction() { 217 putValue(SHORT_DESCRIPTION, tr("Edit the relation the currently selected relation member refers to")); 218 new ImageProvider("dialogs", "edit").getResource().attachImageIcon(this, true); 219 putValue(NAME, tr("Edit")); 220 refreshEnabled(); 221 } 222 223 protected void refreshEnabled() { 224 TreePath[] selection = childTree.getSelectionPaths(); 225 setEnabled(selection != null && selection.length > 0); 226 } 227 228 public void run() { 229 TreePath[] selection = childTree.getSelectionPaths(); 230 if (selection == null || selection.length == 0) return; 231 // do not launch more than 10 relation editors in parallel 232 // 233 for (int i = 0; i < Math.min(selection.length, 10); i++) { 234 Relation r = (Relation) selection[i].getLastPathComponent(); 235 if (!r.isUsable()) { 236 continue; 237 } 238 RelationEditor editor = RelationEditor.getEditor(getLayer(), r, null); 239 editor.setVisible(true); 240 } 241 } 242 243 @Override 244 public void actionPerformed(ActionEvent e) { 245 if (!isEnabled()) 246 return; 247 run(); 248 } 249 250 @Override 251 public void valueChanged(TreeSelectionEvent e) { 252 refreshEnabled(); 253 } 254 } 255 256 /** 257 * Action for downloading all child relations for a given parent relation. 258 * Recursively. 259 */ 260 class DownloadAllChildRelationsAction extends AbstractAction { 261 DownloadAllChildRelationsAction() { 262 putValue(SHORT_DESCRIPTION, tr("Download all child relations (recursively)")); 263 new ImageProvider("download").getResource().attachImageIcon(this, true); 264 putValue(NAME, tr("Download All Children")); 265 } 266 267 public void run() { 268 MainApplication.worker.submit(new DownloadAllChildrenTask(getParentDialog(), (Relation) model.getRoot())); 269 } 270 271 @Override 272 public void actionPerformed(ActionEvent e) { 273 if (!isEnabled()) 274 return; 275 run(); 276 } 277 } 278 279 /** 280 * Action for downloading all selected relations 281 */ 282 class DownloadSelectedAction extends AbstractAction implements TreeSelectionListener { 283 DownloadSelectedAction() { 284 putValue(SHORT_DESCRIPTION, tr("Download selected relations")); 285 // FIXME: replace with better icon 286 new ImageProvider("download").getResource().attachImageIcon(this, true); 287 putValue(NAME, tr("Download Selected Children")); 288 updateEnabledState(); 289 } 290 291 protected void updateEnabledState() { 292 TreePath[] selection = childTree.getSelectionPaths(); 293 setEnabled(selection != null && selection.length > 0); 294 } 295 296 public void run() { 297 TreePath[] selection = childTree.getSelectionPaths(); 298 if (selection == null || selection.length == 0) 299 return; 300 Set<Relation> relations = Arrays.stream(selection) 301 .map(s -> (Relation) s.getLastPathComponent()) 302 .collect(Collectors.toSet()); 303 MainApplication.worker.submit(new DownloadRelationSetTask(getParentDialog(), relations)); 304 } 305 306 @Override 307 public void actionPerformed(ActionEvent e) { 308 if (!isEnabled()) 309 return; 310 run(); 311 } 312 313 @Override 314 public void valueChanged(TreeSelectionEvent e) { 315 updateEnabledState(); 316 } 317 } 318 319 abstract class DownloadTask extends PleaseWaitRunnable { 320 protected boolean canceled; 321 protected int conflictsCount; 322 protected Exception lastException; 323 protected MultiFetchServerObjectReader reader; 324 325 DownloadTask(String title, Dialog parent) { 326 super(title, new PleaseWaitProgressMonitor(parent), false); 327 } 328 329 @Override 330 protected void cancel() { 331 canceled = true; 332 synchronized (this) { 333 if (reader != null) { 334 reader.cancel(); 335 } 336 } 337 } 338 339 protected MultiFetchServerObjectReader createReader() { 340 return MultiFetchServerObjectReader.create().setRecurseDownAppended(false).setRecurseDownRelations(true); 341 } 342 343 /** 344 * Merges the primitives in <code>ds</code> to the dataset of the edit layer 345 * 346 * @param ds the data set 347 */ 348 protected void mergeDataSet(DataSet ds) { 349 if (ds != null) { 350 final DataSetMerger visitor = new DataSetMerger(getLayer().getDataSet(), ds); 351 visitor.merge(); 352 if (!visitor.getConflicts().isEmpty()) { 353 getLayer().getConflicts().add(visitor.getConflicts()); 354 conflictsCount += visitor.getConflicts().size(); 355 } 356 } 357 } 358 359 protected void refreshView(Relation relation) { 360 GuiHelper.runInEDT(() -> { 361 for (int i = 0; i < childTree.getRowCount(); i++) { 362 Relation reference = (Relation) childTree.getPathForRow(i).getLastPathComponent(); 363 if (reference == relation) { 364 model.refreshNode(childTree.getPathForRow(i)); 365 } 366 } 367 }); 368 } 369 370 @Override 371 protected void finish() { 372 if (canceled) 373 return; 374 if (lastException != null) { 375 ExceptionDialogUtil.explainException(lastException); 376 return; 377 } 378 379 if (conflictsCount > 0) { 380 JOptionPane.showMessageDialog( 381 MainApplication.getMainFrame(), 382 trn("There was {0} conflict during import.", 383 "There were {0} conflicts during import.", 384 conflictsCount, conflictsCount), 385 trn("Conflict in data", "Conflicts in data", conflictsCount), 386 JOptionPane.WARNING_MESSAGE 387 ); 388 } 389 } 390 } 391 392 /** 393 * The asynchronous task for downloading relation members. 394 */ 395 class DownloadAllChildrenTask extends DownloadTask { 396 private final Relation relation; 397 398 DownloadAllChildrenTask(Dialog parent, Relation r) { 399 super(tr("Download relation members"), parent); 400 relation = r; 401 } 402 403 /** 404 * warns the user if a relation couldn't be loaded because it was deleted on 405 * the server (the server replied a HTTP code 410) 406 * 407 * @param r the relation 408 */ 409 protected void warnBecauseOfDeletedRelation(Relation r) { 410 String message = tr("<html>The child relation<br>" 411 + "{0}<br>" 412 + "is deleted on the server. It cannot be loaded</html>", 413 Utils.escapeReservedCharactersHTML(r.getDisplayName(DefaultNameFormatter.getInstance())) 414 ); 415 416 JOptionPane.showMessageDialog( 417 MainApplication.getMainFrame(), 418 message, 419 tr("Relation is deleted"), 420 JOptionPane.WARNING_MESSAGE 421 ); 422 } 423 424 @Override 425 protected void realRun() throws SAXException, IOException, OsmTransferException { 426 try { 427 reader = createReader(); 428 reader.append(relation.getMemberPrimitives(Relation.class)); 429 DataSet dataSet = reader.parseOsm(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); 430 mergeDataSet(dataSet); 431 Utils.filteredCollection(reader.getMissingPrimitives(), Relation.class).forEach(this::warnBecauseOfDeletedRelation); 432 for (Relation rel : dataSet.getRelations()) { 433 refreshView((Relation) getLayer().getDataSet().getPrimitiveById(rel)); 434 } 435 SwingUtilities.invokeLater(MainApplication.getMap()::repaint); 436 } catch (OsmTransferException e) { 437 if (canceled) { 438 Logging.warn(tr("Ignoring exception because task was canceled. Exception: {0}", e.toString())); 439 return; 440 } 441 lastException = e; 442 } 443 } 444 } 445 446 /** 447 * The asynchronous task for downloading a set of relations 448 */ 449 class DownloadRelationSetTask extends DownloadTask { 450 private final Set<Relation> relations; 451 452 DownloadRelationSetTask(Dialog parent, Set<Relation> relations) { 453 super(tr("Download relation members"), parent); 454 this.relations = relations; 455 } 456 457 @Override 458 protected void realRun() throws SAXException, IOException, OsmTransferException { 459 try { 460 reader = createReader(); 461 reader.append(relations); 462 DataSet dataSet = reader.parseOsm(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); 463 mergeDataSet(dataSet); 464 465 for (Relation rel : dataSet.getRelations()) { 466 refreshView((Relation) getLayer().getDataSet().getPrimitiveById(rel)); 467 } 468 469 } catch (OsmTransferException e) { 470 if (canceled) { 471 Logging.warn(tr("Ignoring exception because task was canceled. Exception: {0}", e.toString())); 472 return; 473 } 474 lastException = e; 475 } 476 } 477 } 478}