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    }