001 // License: GPL. For details, see LICENSE file. 002 package org.openstreetmap.josm.gui.dialogs.relation; 003 004 import static org.openstreetmap.josm.tools.I18n.tr; 005 import static org.openstreetmap.josm.tools.I18n.trn; 006 007 import java.awt.BorderLayout; 008 import java.awt.Component; 009 import java.awt.Dialog; 010 import java.awt.FlowLayout; 011 import java.awt.event.ActionEvent; 012 import java.io.IOException; 013 import java.net.HttpURLConnection; 014 import java.util.HashSet; 015 import java.util.Iterator; 016 import java.util.List; 017 import java.util.Set; 018 import java.util.Stack; 019 020 import javax.swing.AbstractAction; 021 import javax.swing.JButton; 022 import javax.swing.JOptionPane; 023 import javax.swing.JPanel; 024 import javax.swing.JScrollPane; 025 import javax.swing.SwingUtilities; 026 import javax.swing.event.TreeSelectionEvent; 027 import javax.swing.event.TreeSelectionListener; 028 import javax.swing.tree.TreePath; 029 030 import org.openstreetmap.josm.Main; 031 import org.openstreetmap.josm.data.osm.DataSet; 032 import org.openstreetmap.josm.data.osm.DataSetMerger; 033 import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 034 import org.openstreetmap.josm.data.osm.Relation; 035 import org.openstreetmap.josm.data.osm.RelationMember; 036 import org.openstreetmap.josm.gui.DefaultNameFormatter; 037 import org.openstreetmap.josm.gui.ExceptionDialogUtil; 038 import org.openstreetmap.josm.gui.PleaseWaitRunnable; 039 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 040 import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor; 041 import org.openstreetmap.josm.gui.progress.ProgressMonitor; 042 import org.openstreetmap.josm.io.OsmApi; 043 import org.openstreetmap.josm.io.OsmApiException; 044 import org.openstreetmap.josm.io.OsmServerObjectReader; 045 import org.openstreetmap.josm.io.OsmTransferException; 046 import org.openstreetmap.josm.tools.CheckParameterUtil; 047 import org.openstreetmap.josm.tools.ImageProvider; 048 import 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 */ 056 public 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 public void actionPerformed(ActionEvent e) { 203 if (!isEnabled()) 204 return; 205 run(); 206 } 207 208 public void valueChanged(TreeSelectionEvent e) { 209 refreshEnabled(); 210 } 211 } 212 213 /** 214 * Action for downloading all child relations for a given parent relation. 215 * Recursively. 216 */ 217 class DownloadAllChildRelationsAction extends AbstractAction{ 218 public DownloadAllChildRelationsAction() { 219 putValue(SHORT_DESCRIPTION, tr("Download all child relations (recursively)")); 220 putValue(SMALL_ICON, ImageProvider.get("download")); 221 putValue(NAME, tr("Download All Children")); 222 } 223 224 public void run() { 225 Main.worker.submit(new DownloadAllChildrenTask(getParentDialog(), (Relation)model.getRoot())); 226 } 227 228 public void actionPerformed(ActionEvent e) { 229 if (!isEnabled()) 230 return; 231 run(); 232 } 233 } 234 235 /** 236 * Action for downloading all selected relations 237 */ 238 class DownloadSelectedAction extends AbstractAction implements TreeSelectionListener { 239 public DownloadSelectedAction() { 240 putValue(SHORT_DESCRIPTION, tr("Download selected relations")); 241 // FIXME: replace with better icon 242 // 243 putValue(SMALL_ICON, ImageProvider.get("download")); 244 putValue(NAME, tr("Download Selected Children")); 245 updateEnabledState(); 246 } 247 248 protected void updateEnabledState() { 249 TreePath [] selection = childTree.getSelectionPaths(); 250 setEnabled(selection != null && selection.length > 0); 251 } 252 253 public void run() { 254 TreePath [] selection = childTree.getSelectionPaths(); 255 if (selection == null || selection.length == 0) 256 return; 257 HashSet<Relation> relations = new HashSet<Relation>(); 258 for (TreePath aSelection : selection) { 259 relations.add((Relation) aSelection.getLastPathComponent()); 260 } 261 Main.worker.submit(new DownloadRelationSetTask(getParentDialog(),relations)); 262 } 263 264 public void actionPerformed(ActionEvent e) { 265 if (!isEnabled()) 266 return; 267 run(); 268 } 269 270 public void valueChanged(TreeSelectionEvent e) { 271 updateEnabledState(); 272 } 273 } 274 275 /** 276 * The asynchronous task for downloading relation members. 277 * 278 * 279 */ 280 class DownloadAllChildrenTask extends PleaseWaitRunnable { 281 private boolean canceled; 282 private int conflictsCount; 283 private Exception lastException; 284 private Relation relation; 285 private Stack<Relation> relationsToDownload; 286 private Set<Long> downloadedRelationIds; 287 288 public DownloadAllChildrenTask(Dialog parent, Relation r) { 289 super(tr("Download relation members"), new PleaseWaitProgressMonitor(parent), false /* 290 * don't 291 * ignore 292 * exception 293 */); 294 this.relation = r; 295 relationsToDownload = new Stack<Relation>(); 296 downloadedRelationIds = new HashSet<Long>(); 297 relationsToDownload.push(this.relation); 298 } 299 300 @Override 301 protected void cancel() { 302 canceled = true; 303 OsmApi.getOsmApi().cancel(); 304 } 305 306 protected void refreshView(Relation relation){ 307 for (int i=0; i < childTree.getRowCount(); i++) { 308 Relation reference = (Relation)childTree.getPathForRow(i).getLastPathComponent(); 309 if (reference == relation) { 310 model.refreshNode(childTree.getPathForRow(i)); 311 } 312 } 313 } 314 315 @Override 316 protected void finish() { 317 if (canceled) 318 return; 319 if (lastException != null) { 320 ExceptionDialogUtil.explainException(lastException); 321 return; 322 } 323 324 if (conflictsCount > 0) { 325 JOptionPane.showMessageDialog( 326 Main.parent, 327 trn("There was {0} conflict during import.", 328 "There were {0} conflicts during import.", 329 conflictsCount, conflictsCount), 330 trn("Conflict in data", "Conflicts in data", conflictsCount), 331 JOptionPane.WARNING_MESSAGE 332 ); 333 } 334 } 335 336 /** 337 * warns the user if a relation couldn't be loaded because it was deleted on 338 * the server (the server replied a HTTP code 410) 339 * 340 * @param r the relation 341 */ 342 protected void warnBecauseOfDeletedRelation(Relation r) { 343 String message = tr("<html>The child relation<br>" 344 + "{0}<br>" 345 + "is deleted on the server. It cannot be loaded</html>", 346 r.getDisplayName(DefaultNameFormatter.getInstance()) 347 ); 348 349 JOptionPane.showMessageDialog( 350 Main.parent, 351 message, 352 tr("Relation is deleted"), 353 JOptionPane.WARNING_MESSAGE 354 ); 355 } 356 357 /** 358 * Remembers the child relations to download 359 * 360 * @param parent the parent relation 361 */ 362 protected void rememberChildRelationsToDownload(Relation parent) { 363 downloadedRelationIds.add(parent.getId()); 364 for (RelationMember member: parent.getMembers()) { 365 if (member.isRelation()) { 366 Relation child = member.getRelation(); 367 if (!downloadedRelationIds.contains(child.getId())) { 368 relationsToDownload.push(child); 369 } 370 } 371 } 372 } 373 374 /** 375 * Merges the primitives in <code>ds</code> to the dataset of the 376 * edit layer 377 * 378 * @param ds the data set 379 */ 380 protected void mergeDataSet(DataSet ds) { 381 if (ds != null) { 382 final DataSetMerger visitor = new DataSetMerger(getLayer().data, ds); 383 visitor.merge(); 384 if (!visitor.getConflicts().isEmpty()) { 385 getLayer().getConflicts().add(visitor.getConflicts()); 386 conflictsCount += visitor.getConflicts().size(); 387 } 388 } 389 } 390 391 @Override 392 protected void realRun() throws SAXException, IOException, OsmTransferException { 393 try { 394 while(! relationsToDownload.isEmpty() && !canceled) { 395 Relation r = relationsToDownload.pop(); 396 if (r.isNew()) { 397 continue; 398 } 399 rememberChildRelationsToDownload(r); 400 progressMonitor.setCustomText(tr("Downloading relation {0}", r.getDisplayName(DefaultNameFormatter.getInstance()))); 401 OsmServerObjectReader reader = new OsmServerObjectReader(r.getId(), OsmPrimitiveType.RELATION, 402 true); 403 DataSet dataSet = null; 404 try { 405 dataSet = reader.parseOsm(progressMonitor 406 .createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); 407 } catch(OsmApiException e) { 408 if (e.getResponseCode() == HttpURLConnection.HTTP_GONE) { 409 warnBecauseOfDeletedRelation(r); 410 continue; 411 } 412 throw e; 413 } 414 mergeDataSet(dataSet); 415 refreshView(r); 416 } 417 SwingUtilities.invokeLater(new Runnable() { 418 public void run() { 419 Main.map.repaint(); 420 } 421 }); 422 } catch (Exception e) { 423 if (canceled) { 424 System.out.println(tr("Warning: Ignoring exception because task was canceled. Exception: {0}", e 425 .toString())); 426 return; 427 } 428 lastException = e; 429 } 430 } 431 } 432 433 /** 434 * The asynchronous task for downloading a set of relations 435 */ 436 class DownloadRelationSetTask extends PleaseWaitRunnable { 437 private boolean canceled; 438 private int conflictsCount; 439 private Exception lastException; 440 private Set<Relation> relations; 441 442 public DownloadRelationSetTask(Dialog parent, Set<Relation> relations) { 443 super(tr("Download relation members"), new PleaseWaitProgressMonitor(parent), false /* 444 * don't 445 * ignore 446 * exception 447 */); 448 this.relations = relations; 449 } 450 451 @Override 452 protected void cancel() { 453 canceled = true; 454 OsmApi.getOsmApi().cancel(); 455 } 456 457 protected void refreshView(Relation relation){ 458 for (int i=0; i < childTree.getRowCount(); i++) { 459 Relation reference = (Relation)childTree.getPathForRow(i).getLastPathComponent(); 460 if (reference == relation) { 461 model.refreshNode(childTree.getPathForRow(i)); 462 } 463 } 464 } 465 466 @Override 467 protected void finish() { 468 if (canceled) 469 return; 470 if (lastException != null) { 471 ExceptionDialogUtil.explainException(lastException); 472 return; 473 } 474 475 if (conflictsCount > 0) { 476 JOptionPane.showMessageDialog( 477 Main.parent, 478 trn("There was {0} conflict during import.", 479 "There were {0} conflicts during import.", 480 conflictsCount, conflictsCount), 481 trn("Conflict in data", "Conflicts in data", conflictsCount), 482 JOptionPane.WARNING_MESSAGE 483 ); 484 } 485 } 486 487 protected void mergeDataSet(DataSet dataSet) { 488 if (dataSet != null) { 489 final DataSetMerger visitor = new DataSetMerger(getLayer().data, dataSet); 490 visitor.merge(); 491 if (!visitor.getConflicts().isEmpty()) { 492 getLayer().getConflicts().add(visitor.getConflicts()); 493 conflictsCount += visitor.getConflicts().size(); 494 } 495 } 496 } 497 498 @Override 499 protected void realRun() throws SAXException, IOException, OsmTransferException { 500 try { 501 Iterator<Relation> it = relations.iterator(); 502 while(it.hasNext() && !canceled) { 503 Relation r = it.next(); 504 if (r.isNew()) { 505 continue; 506 } 507 progressMonitor.setCustomText(tr("Downloading relation {0}", r.getDisplayName(DefaultNameFormatter.getInstance()))); 508 OsmServerObjectReader reader = new OsmServerObjectReader(r.getId(), OsmPrimitiveType.RELATION, 509 true); 510 DataSet dataSet = reader.parseOsm(progressMonitor 511 .createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); 512 mergeDataSet(dataSet); 513 refreshView(r); 514 } 515 } catch (Exception e) { 516 if (canceled) { 517 System.out.println(tr("Warning: Ignoring exception because task was canceled. Exception: {0}", e 518 .toString())); 519 return; 520 } 521 lastException = e; 522 } 523 } 524 } 525 }