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 edit layer 402 * 403 * @param ds the data set 404 */ 405 protected void mergeDataSet(DataSet ds) { 406 if (ds != null) { 407 final DataSetMerger visitor = new DataSetMerger(getLayer().getDataSet(), ds); 408 visitor.merge(); 409 if (!visitor.getConflicts().isEmpty()) { 410 getLayer().getConflicts().add(visitor.getConflicts()); 411 conflictsCount += visitor.getConflicts().size(); 412 } 413 } 414 } 415 416 @Override 417 protected void realRun() throws SAXException, IOException, OsmTransferException { 418 try { 419 while (!relationsToDownload.isEmpty() && !canceled) { 420 Relation r = relationsToDownload.pop(); 421 if (r.isNew()) { 422 continue; 423 } 424 rememberChildRelationsToDownload(r); 425 progressMonitor.setCustomText(tr("Downloading relation {0}", r.getDisplayName(DefaultNameFormatter.getInstance()))); 426 OsmServerObjectReader reader = new OsmServerObjectReader(r.getId(), OsmPrimitiveType.RELATION, 427 true); 428 DataSet dataSet = null; 429 try { 430 dataSet = reader.parseOsm(progressMonitor 431 .createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); 432 } catch (OsmApiException e) { 433 if (e.getResponseCode() == HttpURLConnection.HTTP_GONE) { 434 warnBecauseOfDeletedRelation(r); 435 continue; 436 } 437 throw e; 438 } 439 mergeDataSet(dataSet); 440 refreshView(r); 441 } 442 SwingUtilities.invokeLater(MainApplication.getMap()::repaint); 443 } catch (OsmTransferException e) { 444 if (canceled) { 445 Logging.warn(tr("Ignoring exception because task was canceled. Exception: {0}", e.toString())); 446 return; 447 } 448 lastException = e; 449 } 450 } 451 } 452 453 /** 454 * The asynchronous task for downloading a set of relations 455 */ 456 class DownloadRelationSetTask extends DownloadTask { 457 private final Set<Relation> relations; 458 459 DownloadRelationSetTask(Dialog parent, Set<Relation> relations) { 460 super(tr("Download relation members"), parent); 461 this.relations = relations; 462 } 463 464 protected void mergeDataSet(DataSet dataSet) { 465 if (dataSet != null) { 466 final DataSetMerger visitor = new DataSetMerger(getLayer().getDataSet(), dataSet); 467 visitor.merge(); 468 if (!visitor.getConflicts().isEmpty()) { 469 getLayer().getConflicts().add(visitor.getConflicts()); 470 conflictsCount += visitor.getConflicts().size(); 471 } 472 } 473 } 474 475 @Override 476 protected void realRun() throws SAXException, IOException, OsmTransferException { 477 try { 478 Iterator<Relation> it = relations.iterator(); 479 while (it.hasNext() && !canceled) { 480 Relation r = it.next(); 481 if (r.isNew()) { 482 continue; 483 } 484 progressMonitor.setCustomText(tr("Downloading relation {0}", r.getDisplayName(DefaultNameFormatter.getInstance()))); 485 OsmServerObjectReader reader = new OsmServerObjectReader(r.getId(), OsmPrimitiveType.RELATION, 486 true); 487 DataSet dataSet = reader.parseOsm(progressMonitor 488 .createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); 489 mergeDataSet(dataSet); 490 refreshView(r); 491 } 492 } catch (OsmTransferException e) { 493 if (canceled) { 494 Logging.warn(tr("Ignoring exception because task was canceled. Exception: {0}", e.toString())); 495 return; 496 } 497 lastException = e; 498 } 499 } 500 } 501}