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