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 * 055 */ 056public class ChildRelationBrowser extends JPanel { 057 /** the tree with relation children */ 058 private RelationTree childTree; 059 /** the tree model */ 060 private RelationTreeModel model; 061 062 /** the osm data layer this browser is related to */ 063 private 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(); 093 pnl.setLayout(new FlowLayout(FlowLayout.LEFT)); 094 095 // --- 096 DownloadAllChildRelationsAction downloadAction= new DownloadAllChildRelationsAction(); 097 pnl.add(new JButton(downloadAction)); 098 099 // --- 100 DownloadSelectedAction downloadSelectedAction= new DownloadSelectedAction(); 101 childTree.addTreeSelectionListener(downloadSelectedAction); 102 pnl.add(new JButton(downloadSelectedAction)); 103 104 // --- 105 EditAction editAction = new EditAction(); 106 childTree.addTreeSelectionListener(editAction); 107 pnl.add(new JButton(editAction)); 108 109 return pnl; 110 } 111 112 /** 113 * constructor 114 * 115 * @param layer the {@link OsmDataLayer} this browser is related to. Must not be null. 116 * @exception IllegalArgumentException thrown, if layer is null 117 */ 118 public ChildRelationBrowser(OsmDataLayer layer) throws IllegalArgumentException { 119 CheckParameterUtil.ensureParameterNotNull(layer, "layer"); 120 this.layer = layer; 121 model = new RelationTreeModel(); 122 build(); 123 } 124 125 /** 126 * constructor 127 * 128 * @param layer the {@link OsmDataLayer} this browser is related to. Must not be null. 129 * @param root the root relation 130 * @exception IllegalArgumentException thrown, if layer is null 131 */ 132 public ChildRelationBrowser(OsmDataLayer layer, Relation root) throws IllegalArgumentException { 133 this(layer); 134 populate(root); 135 } 136 137 /** 138 * populates the browser with a relation 139 * 140 * @param r the relation 141 */ 142 public void populate(Relation r) { 143 model.populate(r); 144 } 145 146 /** 147 * populates the browser with a list of relation members 148 * 149 * @param members the list of relation members 150 */ 151 152 public void populate(List<RelationMember> members) { 153 model.populate(members); 154 } 155 156 /** 157 * replies the parent dialog this browser is embedded in 158 * 159 * @return the parent dialog; null, if there is no {@link Dialog} as parent dialog 160 */ 161 protected Dialog getParentDialog() { 162 Component c = this; 163 while(c != null && ! (c instanceof Dialog)) { 164 c = c.getParent(); 165 } 166 return (Dialog)c; 167 } 168 169 /** 170 * Action for editing the currently selected relation 171 * 172 * 173 */ 174 class EditAction extends AbstractAction implements TreeSelectionListener { 175 public EditAction() { 176 putValue(SHORT_DESCRIPTION, tr("Edit the relation the currently selected relation member refers to.")); 177 putValue(SMALL_ICON, ImageProvider.get("dialogs", "edit")); 178 putValue(NAME, tr("Edit")); 179 refreshEnabled(); 180 } 181 182 protected void refreshEnabled() { 183 TreePath[] selection = childTree.getSelectionPaths(); 184 setEnabled(selection != null && selection.length > 0); 185 } 186 187 public void run() { 188 TreePath [] selection = childTree.getSelectionPaths(); 189 if (selection == null || selection.length == 0) return; 190 // do not launch more than 10 relation editors in parallel 191 // 192 for (int i=0; i < Math.min(selection.length,10);i++) { 193 Relation r = (Relation)selection[i].getLastPathComponent(); 194 if (r.isIncomplete()) { 195 continue; 196 } 197 RelationEditor editor = RelationEditor.getEditor(getLayer(), r, null); 198 editor.setVisible(true); 199 } 200 } 201 202 @Override 203 public void actionPerformed(ActionEvent e) { 204 if (!isEnabled()) 205 return; 206 run(); 207 } 208 209 @Override 210 public void valueChanged(TreeSelectionEvent e) { 211 refreshEnabled(); 212 } 213 } 214 215 /** 216 * Action for downloading all child relations for a given parent relation. 217 * Recursively. 218 */ 219 class DownloadAllChildRelationsAction extends AbstractAction{ 220 public DownloadAllChildRelationsAction() { 221 putValue(SHORT_DESCRIPTION, tr("Download all child relations (recursively)")); 222 putValue(SMALL_ICON, ImageProvider.get("download")); 223 putValue(NAME, tr("Download All Children")); 224 } 225 226 public void run() { 227 Main.worker.submit(new DownloadAllChildrenTask(getParentDialog(), (Relation)model.getRoot())); 228 } 229 230 @Override 231 public void actionPerformed(ActionEvent e) { 232 if (!isEnabled()) 233 return; 234 run(); 235 } 236 } 237 238 /** 239 * Action for downloading all selected relations 240 */ 241 class DownloadSelectedAction extends AbstractAction implements TreeSelectionListener { 242 public DownloadSelectedAction() { 243 putValue(SHORT_DESCRIPTION, tr("Download selected relations")); 244 // FIXME: replace with better icon 245 // 246 putValue(SMALL_ICON, ImageProvider.get("download")); 247 putValue(NAME, tr("Download Selected Children")); 248 updateEnabledState(); 249 } 250 251 protected void updateEnabledState() { 252 TreePath [] selection = childTree.getSelectionPaths(); 253 setEnabled(selection != null && selection.length > 0); 254 } 255 256 public void run() { 257 TreePath [] selection = childTree.getSelectionPaths(); 258 if (selection == null || selection.length == 0) 259 return; 260 HashSet<Relation> relations = new HashSet<Relation>(); 261 for (TreePath aSelection : selection) { 262 relations.add((Relation) aSelection.getLastPathComponent()); 263 } 264 Main.worker.submit(new DownloadRelationSetTask(getParentDialog(),relations)); 265 } 266 267 @Override 268 public void actionPerformed(ActionEvent e) { 269 if (!isEnabled()) 270 return; 271 run(); 272 } 273 274 @Override 275 public void valueChanged(TreeSelectionEvent e) { 276 updateEnabledState(); 277 } 278 } 279 280 /** 281 * The asynchronous task for downloading relation members. 282 * 283 * 284 */ 285 class DownloadAllChildrenTask extends PleaseWaitRunnable { 286 private boolean canceled; 287 private int conflictsCount; 288 private Exception lastException; 289 private Relation relation; 290 private Stack<Relation> relationsToDownload; 291 private Set<Long> downloadedRelationIds; 292 293 public DownloadAllChildrenTask(Dialog parent, Relation r) { 294 super(tr("Download relation members"), new PleaseWaitProgressMonitor(parent), false /* 295 * don't 296 * ignore 297 * exception 298 */); 299 this.relation = r; 300 relationsToDownload = new Stack<Relation>(); 301 downloadedRelationIds = new HashSet<Long>(); 302 relationsToDownload.push(this.relation); 303 } 304 305 @Override 306 protected void cancel() { 307 canceled = true; 308 OsmApi.getOsmApi().cancel(); 309 } 310 311 protected void refreshView(Relation relation){ 312 for (int i=0; i < childTree.getRowCount(); i++) { 313 Relation reference = (Relation)childTree.getPathForRow(i).getLastPathComponent(); 314 if (reference == relation) { 315 model.refreshNode(childTree.getPathForRow(i)); 316 } 317 } 318 } 319 320 @Override 321 protected void finish() { 322 if (canceled) 323 return; 324 if (lastException != null) { 325 ExceptionDialogUtil.explainException(lastException); 326 return; 327 } 328 329 if (conflictsCount > 0) { 330 JOptionPane.showMessageDialog( 331 Main.parent, 332 trn("There was {0} conflict during import.", 333 "There were {0} conflicts during import.", 334 conflictsCount, conflictsCount), 335 trn("Conflict in data", "Conflicts in data", conflictsCount), 336 JOptionPane.WARNING_MESSAGE 337 ); 338 } 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 PleaseWaitRunnable { 442 private boolean canceled; 443 private int conflictsCount; 444 private Exception lastException; 445 private Set<Relation> relations; 446 447 public DownloadRelationSetTask(Dialog parent, Set<Relation> relations) { 448 super(tr("Download relation members"), new PleaseWaitProgressMonitor(parent), false /* 449 * don't 450 * ignore 451 * exception 452 */); 453 this.relations = relations; 454 } 455 456 @Override 457 protected void cancel() { 458 canceled = true; 459 OsmApi.getOsmApi().cancel(); 460 } 461 462 protected void refreshView(Relation relation){ 463 for (int i=0; i < childTree.getRowCount(); i++) { 464 Relation reference = (Relation)childTree.getPathForRow(i).getLastPathComponent(); 465 if (reference == relation) { 466 model.refreshNode(childTree.getPathForRow(i)); 467 } 468 } 469 } 470 471 @Override 472 protected void finish() { 473 if (canceled) 474 return; 475 if (lastException != null) { 476 ExceptionDialogUtil.explainException(lastException); 477 return; 478 } 479 480 if (conflictsCount > 0) { 481 JOptionPane.showMessageDialog( 482 Main.parent, 483 trn("There was {0} conflict during import.", 484 "There were {0} conflicts during import.", 485 conflictsCount, conflictsCount), 486 trn("Conflict in data", "Conflicts in data", conflictsCount), 487 JOptionPane.WARNING_MESSAGE 488 ); 489 } 490 } 491 492 protected void mergeDataSet(DataSet dataSet) { 493 if (dataSet != null) { 494 final DataSetMerger visitor = new DataSetMerger(getLayer().data, dataSet); 495 visitor.merge(); 496 if (!visitor.getConflicts().isEmpty()) { 497 getLayer().getConflicts().add(visitor.getConflicts()); 498 conflictsCount += visitor.getConflicts().size(); 499 } 500 } 501 } 502 503 @Override 504 protected void realRun() throws SAXException, IOException, OsmTransferException { 505 try { 506 Iterator<Relation> it = relations.iterator(); 507 while(it.hasNext() && !canceled) { 508 Relation r = it.next(); 509 if (r.isNew()) { 510 continue; 511 } 512 progressMonitor.setCustomText(tr("Downloading relation {0}", r.getDisplayName(DefaultNameFormatter.getInstance()))); 513 OsmServerObjectReader reader = new OsmServerObjectReader(r.getId(), OsmPrimitiveType.RELATION, 514 true); 515 DataSet dataSet = reader.parseOsm(progressMonitor 516 .createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); 517 mergeDataSet(dataSet); 518 refreshView(r); 519 } 520 } catch (Exception e) { 521 if (canceled) { 522 Main.warn(tr("Ignoring exception because task was canceled. Exception: {0}", e.toString())); 523 return; 524 } 525 lastException = e; 526 } 527 } 528 } 529}