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