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 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<>(); 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 abstract class DownloadTask extends PleaseWaitRunnable { 281 protected boolean canceled; 282 protected int conflictsCount; 283 protected Exception lastException; 284 285 public DownloadTask(String title, Dialog parent) { 286 super(title, new PleaseWaitProgressMonitor(parent), false); 287 } 288 289 @Override 290 protected void cancel() { 291 canceled = true; 292 OsmApi.getOsmApi().cancel(); 293 } 294 295 protected void refreshView(Relation relation){ 296 for (int i=0; i < childTree.getRowCount(); i++) { 297 Relation reference = (Relation)childTree.getPathForRow(i).getLastPathComponent(); 298 if (reference == relation) { 299 model.refreshNode(childTree.getPathForRow(i)); 300 } 301 } 302 } 303 304 @Override 305 protected void finish() { 306 if (canceled) 307 return; 308 if (lastException != null) { 309 ExceptionDialogUtil.explainException(lastException); 310 return; 311 } 312 313 if (conflictsCount > 0) { 314 JOptionPane.showMessageDialog( 315 Main.parent, 316 trn("There was {0} conflict during import.", 317 "There were {0} conflicts during import.", 318 conflictsCount, conflictsCount), 319 trn("Conflict in data", "Conflicts in data", conflictsCount), 320 JOptionPane.WARNING_MESSAGE 321 ); 322 } 323 } 324 } 325 326 /** 327 * The asynchronous task for downloading relation members. 328 */ 329 class DownloadAllChildrenTask extends DownloadTask { 330 private final Relation relation; 331 private final Stack<Relation> relationsToDownload; 332 private final Set<Long> downloadedRelationIds; 333 334 public DownloadAllChildrenTask(Dialog parent, Relation r) { 335 super(tr("Download relation members"), parent); 336 this.relation = r; 337 relationsToDownload = new Stack<>(); 338 downloadedRelationIds = new HashSet<>(); 339 relationsToDownload.push(this.relation); 340 } 341 342 /** 343 * warns the user if a relation couldn't be loaded because it was deleted on 344 * the server (the server replied a HTTP code 410) 345 * 346 * @param r the relation 347 */ 348 protected void warnBecauseOfDeletedRelation(Relation r) { 349 String message = tr("<html>The child relation<br>" 350 + "{0}<br>" 351 + "is deleted on the server. It cannot be loaded</html>", 352 r.getDisplayName(DefaultNameFormatter.getInstance()) 353 ); 354 355 JOptionPane.showMessageDialog( 356 Main.parent, 357 message, 358 tr("Relation is deleted"), 359 JOptionPane.WARNING_MESSAGE 360 ); 361 } 362 363 /** 364 * Remembers the child relations to download 365 * 366 * @param parent the parent relation 367 */ 368 protected void rememberChildRelationsToDownload(Relation parent) { 369 downloadedRelationIds.add(parent.getId()); 370 for (RelationMember member: parent.getMembers()) { 371 if (member.isRelation()) { 372 Relation child = member.getRelation(); 373 if (!downloadedRelationIds.contains(child.getId())) { 374 relationsToDownload.push(child); 375 } 376 } 377 } 378 } 379 380 /** 381 * Merges the primitives in <code>ds</code> to the dataset of the 382 * edit layer 383 * 384 * @param ds the data set 385 */ 386 protected void mergeDataSet(DataSet ds) { 387 if (ds != null) { 388 final DataSetMerger visitor = new DataSetMerger(getLayer().data, ds); 389 visitor.merge(); 390 if (!visitor.getConflicts().isEmpty()) { 391 getLayer().getConflicts().add(visitor.getConflicts()); 392 conflictsCount += visitor.getConflicts().size(); 393 } 394 } 395 } 396 397 @Override 398 protected void realRun() throws SAXException, IOException, OsmTransferException { 399 try { 400 while(! relationsToDownload.isEmpty() && !canceled) { 401 Relation r = relationsToDownload.pop(); 402 if (r.isNew()) { 403 continue; 404 } 405 rememberChildRelationsToDownload(r); 406 progressMonitor.setCustomText(tr("Downloading relation {0}", r.getDisplayName(DefaultNameFormatter.getInstance()))); 407 OsmServerObjectReader reader = new OsmServerObjectReader(r.getId(), OsmPrimitiveType.RELATION, 408 true); 409 DataSet dataSet = null; 410 try { 411 dataSet = reader.parseOsm(progressMonitor 412 .createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); 413 } catch(OsmApiException e) { 414 if (e.getResponseCode() == HttpURLConnection.HTTP_GONE) { 415 warnBecauseOfDeletedRelation(r); 416 continue; 417 } 418 throw e; 419 } 420 mergeDataSet(dataSet); 421 refreshView(r); 422 } 423 SwingUtilities.invokeLater(new Runnable() { 424 @Override 425 public void run() { 426 Main.map.repaint(); 427 } 428 }); 429 } catch (Exception e) { 430 if (canceled) { 431 Main.warn(tr("Ignoring exception because task was canceled. Exception: {0}", e.toString())); 432 return; 433 } 434 lastException = e; 435 } 436 } 437 } 438 439 /** 440 * The asynchronous task for downloading a set of relations 441 */ 442 class DownloadRelationSetTask extends DownloadTask { 443 private final Set<Relation> relations; 444 445 public DownloadRelationSetTask(Dialog parent, Set<Relation> relations) { 446 super(tr("Download relation members"), parent); 447 this.relations = relations; 448 } 449 450 protected void mergeDataSet(DataSet dataSet) { 451 if (dataSet != null) { 452 final DataSetMerger visitor = new DataSetMerger(getLayer().data, dataSet); 453 visitor.merge(); 454 if (!visitor.getConflicts().isEmpty()) { 455 getLayer().getConflicts().add(visitor.getConflicts()); 456 conflictsCount += visitor.getConflicts().size(); 457 } 458 } 459 } 460 461 @Override 462 protected void realRun() throws SAXException, IOException, OsmTransferException { 463 try { 464 Iterator<Relation> it = relations.iterator(); 465 while(it.hasNext() && !canceled) { 466 Relation r = it.next(); 467 if (r.isNew()) { 468 continue; 469 } 470 progressMonitor.setCustomText(tr("Downloading relation {0}", r.getDisplayName(DefaultNameFormatter.getInstance()))); 471 OsmServerObjectReader reader = new OsmServerObjectReader(r.getId(), OsmPrimitiveType.RELATION, 472 true); 473 DataSet dataSet = reader.parseOsm(progressMonitor 474 .createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); 475 mergeDataSet(dataSet); 476 refreshView(r); 477 } 478 } catch (Exception e) { 479 if (canceled) { 480 Main.warn(tr("Ignoring exception because task was canceled. Exception: {0}", e.toString())); 481 return; 482 } 483 lastException = e; 484 } 485 } 486 } 487}