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