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 }