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