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