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 transient RelationTreeModel model;
061
062    /** the osm data layer this browser is related to */
063    private transient 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(new FlowLayout(FlowLayout.LEFT));
093
094        // ---
095        DownloadAllChildRelationsAction downloadAction = new DownloadAllChildRelationsAction();
096        pnl.add(new JButton(downloadAction));
097
098        // ---
099        DownloadSelectedAction downloadSelectedAction = new DownloadSelectedAction();
100        childTree.addTreeSelectionListener(downloadSelectedAction);
101        pnl.add(new JButton(downloadSelectedAction));
102
103        // ---
104        EditAction editAction = new EditAction();
105        childTree.addTreeSelectionListener(editAction);
106        pnl.add(new JButton(editAction));
107
108        return pnl;
109    }
110
111    /**
112     * constructor
113     *
114     * @param layer the {@link OsmDataLayer} this browser is related to. Must not be null.
115     * @throws IllegalArgumentException if layer is null
116     */
117    public ChildRelationBrowser(OsmDataLayer layer) {
118        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
119        this.layer = layer;
120        model = new RelationTreeModel();
121        build();
122    }
123
124    /**
125     * constructor
126     *
127     * @param layer the {@link OsmDataLayer} this browser is related to. Must not be null.
128     * @param root the root relation
129     * @throws IllegalArgumentException if layer is null
130     */
131    public ChildRelationBrowser(OsmDataLayer layer, Relation root) {
132        this(layer);
133        populate(root);
134    }
135
136    /**
137     * populates the browser with a relation
138     *
139     * @param r the relation
140     */
141    public void populate(Relation r) {
142        model.populate(r);
143    }
144
145    /**
146     * populates the browser with a list of relation members
147     *
148     * @param members the list of relation members
149     */
150
151    public void populate(List<RelationMember> members) {
152        model.populate(members);
153    }
154
155    /**
156     * replies the parent dialog this browser is embedded in
157     *
158     * @return the parent dialog; null, if there is no {@link Dialog} as parent dialog
159     */
160    protected Dialog getParentDialog() {
161        Component c = this;
162        while (c != null && !(c instanceof Dialog)) {
163            c = c.getParent();
164        }
165        return (Dialog) c;
166    }
167
168    /**
169     * Action for editing the currently selected relation
170     *
171     *
172     */
173    class EditAction extends AbstractAction implements TreeSelectionListener {
174        EditAction() {
175            putValue(SHORT_DESCRIPTION, tr("Edit the relation the currently selected relation member refers to."));
176            putValue(SMALL_ICON, ImageProvider.get("dialogs", "edit"));
177            putValue(NAME, tr("Edit"));
178            refreshEnabled();
179        }
180
181        protected void refreshEnabled() {
182            TreePath[] selection = childTree.getSelectionPaths();
183            setEnabled(selection != null && selection.length > 0);
184        }
185
186        public void run() {
187            TreePath[] selection = childTree.getSelectionPaths();
188            if (selection == null || selection.length == 0) return;
189            // do not launch more than 10 relation editors in parallel
190            //
191            for (int i = 0; i < Math.min(selection.length, 10); i++) {
192                Relation r = (Relation) selection[i].getLastPathComponent();
193                if (r.isIncomplete()) {
194                    continue;
195                }
196                RelationEditor editor = RelationEditor.getEditor(getLayer(), r, null);
197                editor.setVisible(true);
198            }
199        }
200
201        @Override
202        public void actionPerformed(ActionEvent e) {
203            if (!isEnabled())
204                return;
205            run();
206        }
207
208        @Override
209        public void valueChanged(TreeSelectionEvent e) {
210            refreshEnabled();
211        }
212    }
213
214    /**
215     * Action for downloading all child relations for a given parent relation.
216     * Recursively.
217     */
218    class DownloadAllChildRelationsAction extends AbstractAction {
219        DownloadAllChildRelationsAction() {
220            putValue(SHORT_DESCRIPTION, tr("Download all child relations (recursively)"));
221            putValue(SMALL_ICON, ImageProvider.get("download"));
222            putValue(NAME, tr("Download All Children"));
223        }
224
225        public void run() {
226            Main.worker.submit(new DownloadAllChildrenTask(getParentDialog(), (Relation) model.getRoot()));
227        }
228
229        @Override
230        public void actionPerformed(ActionEvent e) {
231            if (!isEnabled())
232                return;
233            run();
234        }
235    }
236
237    /**
238     * Action for downloading all selected relations
239     */
240    class DownloadSelectedAction extends AbstractAction implements TreeSelectionListener {
241        DownloadSelectedAction() {
242            putValue(SHORT_DESCRIPTION, tr("Download selected relations"));
243            // FIXME: replace with better icon
244            //
245            putValue(SMALL_ICON, ImageProvider.get("download"));
246            putValue(NAME, tr("Download Selected Children"));
247            updateEnabledState();
248        }
249
250        protected void updateEnabledState() {
251            TreePath[] selection = childTree.getSelectionPaths();
252            setEnabled(selection != null && selection.length > 0);
253        }
254
255        public void run() {
256            TreePath[] selection = childTree.getSelectionPaths();
257            if (selection == null || selection.length == 0)
258                return;
259            Set<Relation> relations = new HashSet<>();
260            for (TreePath aSelection : selection) {
261                relations.add((Relation) aSelection.getLastPathComponent());
262            }
263            Main.worker.submit(new DownloadRelationSetTask(getParentDialog(), relations));
264        }
265
266        @Override
267        public void actionPerformed(ActionEvent e) {
268            if (!isEnabled())
269                return;
270            run();
271        }
272
273        @Override
274        public void valueChanged(TreeSelectionEvent e) {
275            updateEnabledState();
276        }
277    }
278
279    abstract class DownloadTask extends PleaseWaitRunnable {
280        protected boolean canceled;
281        protected int conflictsCount;
282        protected Exception lastException;
283
284        DownloadTask(String title, Dialog parent) {
285            super(title, new PleaseWaitProgressMonitor(parent), false);
286        }
287
288        @Override
289        protected void cancel() {
290            canceled = true;
291            OsmApi.getOsmApi().cancel();
292        }
293
294        protected void refreshView(Relation relation) {
295            for (int i = 0; i < childTree.getRowCount(); i++) {
296                Relation reference = (Relation) childTree.getPathForRow(i).getLastPathComponent();
297                if (reference == relation) {
298                    model.refreshNode(childTree.getPathForRow(i));
299                }
300            }
301        }
302
303        @Override
304        protected void finish() {
305            if (canceled)
306                return;
307            if (lastException != null) {
308                ExceptionDialogUtil.explainException(lastException);
309                return;
310            }
311
312            if (conflictsCount > 0) {
313                JOptionPane.showMessageDialog(
314                        Main.parent,
315                        trn("There was {0} conflict during import.",
316                                "There were {0} conflicts during import.",
317                                conflictsCount, conflictsCount),
318                                trn("Conflict in data", "Conflicts in data", conflictsCount),
319                                JOptionPane.WARNING_MESSAGE
320                );
321            }
322        }
323    }
324
325    /**
326     * The asynchronous task for downloading relation members.
327     */
328    class DownloadAllChildrenTask extends DownloadTask {
329        private final Relation relation;
330        private final Stack<Relation> relationsToDownload;
331        private final Set<Long> downloadedRelationIds;
332
333        DownloadAllChildrenTask(Dialog parent, Relation r) {
334            super(tr("Download relation members"), parent);
335            this.relation = r;
336            relationsToDownload = new Stack<>();
337            downloadedRelationIds = new HashSet<>();
338            relationsToDownload.push(this.relation);
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 (OsmTransferException 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 DownloadTask {
442        private final Set<Relation> relations;
443
444        DownloadRelationSetTask(Dialog parent, Set<Relation> relations) {
445            super(tr("Download relation members"), parent);
446            this.relations = relations;
447        }
448
449        protected void mergeDataSet(DataSet dataSet) {
450            if (dataSet != null) {
451                final DataSetMerger visitor = new DataSetMerger(getLayer().data, dataSet);
452                visitor.merge();
453                if (!visitor.getConflicts().isEmpty()) {
454                    getLayer().getConflicts().add(visitor.getConflicts());
455                    conflictsCount += visitor.getConflicts().size();
456                }
457            }
458        }
459
460        @Override
461        protected void realRun() throws SAXException, IOException, OsmTransferException {
462            try {
463                Iterator<Relation> it = relations.iterator();
464                while (it.hasNext() && !canceled) {
465                    Relation r = it.next();
466                    if (r.isNew()) {
467                        continue;
468                    }
469                    progressMonitor.setCustomText(tr("Downloading relation {0}", r.getDisplayName(DefaultNameFormatter.getInstance())));
470                    OsmServerObjectReader reader = new OsmServerObjectReader(r.getId(), OsmPrimitiveType.RELATION,
471                            true);
472                    DataSet dataSet = reader.parseOsm(progressMonitor
473                            .createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
474                    mergeDataSet(dataSet);
475                    refreshView(r);
476                }
477            } catch (OsmTransferException e) {
478                if (canceled) {
479                    Main.warn(tr("Ignoring exception because task was canceled. Exception: {0}", e.toString()));
480                    return;
481                }
482                lastException = e;
483            }
484        }
485    }
486}