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.awt.event.MouseAdapter;
013import java.awt.event.MouseEvent;
014import java.io.IOException;
015import java.net.HttpURLConnection;
016import java.util.HashSet;
017import java.util.Iterator;
018import java.util.List;
019import java.util.Set;
020import java.util.Stack;
021
022import javax.swing.AbstractAction;
023import javax.swing.JButton;
024import javax.swing.JOptionPane;
025import javax.swing.JPanel;
026import javax.swing.JScrollPane;
027import javax.swing.SwingUtilities;
028import javax.swing.event.TreeSelectionEvent;
029import javax.swing.event.TreeSelectionListener;
030import javax.swing.tree.TreePath;
031
032import org.openstreetmap.josm.Main;
033import org.openstreetmap.josm.data.osm.DataSet;
034import org.openstreetmap.josm.data.osm.DataSetMerger;
035import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
036import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
037import org.openstreetmap.josm.data.osm.Relation;
038import org.openstreetmap.josm.data.osm.RelationMember;
039import org.openstreetmap.josm.gui.ExceptionDialogUtil;
040import org.openstreetmap.josm.gui.MainApplication;
041import org.openstreetmap.josm.gui.PleaseWaitRunnable;
042import org.openstreetmap.josm.gui.layer.OsmDataLayer;
043import org.openstreetmap.josm.gui.progress.ProgressMonitor;
044import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor;
045import org.openstreetmap.josm.io.OsmApi;
046import org.openstreetmap.josm.io.OsmApiException;
047import org.openstreetmap.josm.io.OsmServerObjectReader;
048import org.openstreetmap.josm.io.OsmTransferException;
049import org.openstreetmap.josm.tools.CheckParameterUtil;
050import org.openstreetmap.josm.tools.ImageProvider;
051import org.openstreetmap.josm.tools.Logging;
052import org.openstreetmap.josm.tools.Utils;
053import org.xml.sax.SAXException;
054
055/**
056 * ChildRelationBrowser is a UI component which provides a tree-like view on the hierarchical
057 * structure of relations.
058 *
059 * @since 1828
060 */
061public class ChildRelationBrowser extends JPanel {
062    /** the tree with relation children */
063    private RelationTree childTree;
064    /**  the tree model */
065    private transient RelationTreeModel model;
066
067    /** the osm data layer this browser is related to */
068    private transient OsmDataLayer layer;
069
070    /** the editAction used in the bottom panel and for doubleClick */
071    private EditAction editAction;
072
073    /**
074     * Replies the {@link OsmDataLayer} this editor is related to
075     *
076     * @return the osm data layer
077     */
078    protected OsmDataLayer getLayer() {
079        return layer;
080    }
081
082    /**
083     * builds the UI
084     */
085    protected void build() {
086        setLayout(new BorderLayout());
087        childTree = new RelationTree(model);
088        JScrollPane pane = new JScrollPane(childTree);
089        add(pane, BorderLayout.CENTER);
090
091        add(buildButtonPanel(), BorderLayout.SOUTH);
092        childTree.setToggleClickCount(0);
093        childTree.addMouseListener(new MouseAdapter() {
094            @Override
095            public void mouseClicked(MouseEvent e) {
096                if (e.getClickCount() == 2
097                    && !e.isAltDown() && !e.isAltGraphDown() && !e.isControlDown() && !e.isMetaDown() && !e.isShiftDown()
098                    && childTree.getRowForLocation(e.getX(), e.getY()) == childTree.getMinSelectionRow()) {
099                    Relation r = (Relation) childTree.getLastSelectedPathComponent();
100                    if (r.isIncomplete()) {
101                        childTree.expandPath(childTree.getSelectionPath());
102                    } else {
103                        editAction.actionPerformed(new ActionEvent(e.getSource(), ActionEvent.ACTION_PERFORMED, null));
104                    }
105                }
106            }
107        });
108    }
109
110    /**
111     * builds the panel with the command buttons
112     *
113     * @return the button panel
114     */
115    protected JPanel buildButtonPanel() {
116        JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT));
117
118        // ---
119        DownloadAllChildRelationsAction downloadAction = new DownloadAllChildRelationsAction();
120        pnl.add(new JButton(downloadAction));
121
122        // ---
123        DownloadSelectedAction downloadSelectedAction = new DownloadSelectedAction();
124        childTree.addTreeSelectionListener(downloadSelectedAction);
125        pnl.add(new JButton(downloadSelectedAction));
126
127        // ---
128        editAction = new EditAction();
129        childTree.addTreeSelectionListener(editAction);
130        pnl.add(new JButton(editAction));
131
132        return pnl;
133    }
134
135    /**
136     * constructor
137     *
138     * @param layer the {@link OsmDataLayer} this browser is related to. Must not be null.
139     * @throws IllegalArgumentException if layer is null
140     */
141    public ChildRelationBrowser(OsmDataLayer layer) {
142        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
143        this.layer = layer;
144        model = new RelationTreeModel();
145        build();
146    }
147
148    /**
149     * constructor
150     *
151     * @param layer the {@link OsmDataLayer} this browser is related to. Must not be null.
152     * @param root the root relation
153     * @throws IllegalArgumentException if layer is null
154     */
155    public ChildRelationBrowser(OsmDataLayer layer, Relation root) {
156        this(layer);
157        populate(root);
158    }
159
160    /**
161     * populates the browser with a relation
162     *
163     * @param r the relation
164     */
165    public void populate(Relation r) {
166        model.populate(r);
167    }
168
169    /**
170     * populates the browser with a list of relation members
171     *
172     * @param members the list of relation members
173     */
174
175    public void populate(List<RelationMember> members) {
176        model.populate(members);
177    }
178
179    /**
180     * replies the parent dialog this browser is embedded in
181     *
182     * @return the parent dialog; null, if there is no {@link Dialog} as parent dialog
183     */
184    protected Dialog getParentDialog() {
185        Component c = this;
186        while (c != null && !(c instanceof Dialog)) {
187            c = c.getParent();
188        }
189        return (Dialog) c;
190    }
191
192    /**
193     * Action for editing the currently selected relation
194     *
195     *
196     */
197    class EditAction extends AbstractAction implements TreeSelectionListener {
198        EditAction() {
199            putValue(SHORT_DESCRIPTION, tr("Edit the relation the currently selected relation member refers to."));
200            new ImageProvider("dialogs", "edit").getResource().attachImageIcon(this, true);
201            putValue(NAME, tr("Edit"));
202            refreshEnabled();
203        }
204
205        protected void refreshEnabled() {
206            TreePath[] selection = childTree.getSelectionPaths();
207            setEnabled(selection != null && selection.length > 0);
208        }
209
210        public void run() {
211            TreePath[] selection = childTree.getSelectionPaths();
212            if (selection == null || selection.length == 0) return;
213            // do not launch more than 10 relation editors in parallel
214            //
215            for (int i = 0; i < Math.min(selection.length, 10); i++) {
216                Relation r = (Relation) selection[i].getLastPathComponent();
217                if (r.isIncomplete()) {
218                    continue;
219                }
220                RelationEditor editor = RelationEditor.getEditor(getLayer(), r, null);
221                editor.setVisible(true);
222            }
223        }
224
225        @Override
226        public void actionPerformed(ActionEvent e) {
227            if (!isEnabled())
228                return;
229            run();
230        }
231
232        @Override
233        public void valueChanged(TreeSelectionEvent e) {
234            refreshEnabled();
235        }
236    }
237
238    /**
239     * Action for downloading all child relations for a given parent relation.
240     * Recursively.
241     */
242    class DownloadAllChildRelationsAction extends AbstractAction {
243        DownloadAllChildRelationsAction() {
244            putValue(SHORT_DESCRIPTION, tr("Download all child relations (recursively)"));
245            new ImageProvider("download").getResource().attachImageIcon(this, true);
246            putValue(NAME, tr("Download All Children"));
247        }
248
249        public void run() {
250            MainApplication.worker.submit(new DownloadAllChildrenTask(getParentDialog(), (Relation) model.getRoot()));
251        }
252
253        @Override
254        public void actionPerformed(ActionEvent e) {
255            if (!isEnabled())
256                return;
257            run();
258        }
259    }
260
261    /**
262     * Action for downloading all selected relations
263     */
264    class DownloadSelectedAction extends AbstractAction implements TreeSelectionListener {
265        DownloadSelectedAction() {
266            putValue(SHORT_DESCRIPTION, tr("Download selected relations"));
267            // FIXME: replace with better icon
268            new ImageProvider("download").getResource().attachImageIcon(this, true);
269            putValue(NAME, tr("Download Selected Children"));
270            updateEnabledState();
271        }
272
273        protected void updateEnabledState() {
274            TreePath[] selection = childTree.getSelectionPaths();
275            setEnabled(selection != null && selection.length > 0);
276        }
277
278        public void run() {
279            TreePath[] selection = childTree.getSelectionPaths();
280            if (selection == null || selection.length == 0)
281                return;
282            Set<Relation> relations = new HashSet<>();
283            for (TreePath aSelection : selection) {
284                relations.add((Relation) aSelection.getLastPathComponent());
285            }
286            MainApplication.worker.submit(new DownloadRelationSetTask(getParentDialog(), relations));
287        }
288
289        @Override
290        public void actionPerformed(ActionEvent e) {
291            if (!isEnabled())
292                return;
293            run();
294        }
295
296        @Override
297        public void valueChanged(TreeSelectionEvent e) {
298            updateEnabledState();
299        }
300    }
301
302    abstract class DownloadTask extends PleaseWaitRunnable {
303        protected boolean canceled;
304        protected int conflictsCount;
305        protected Exception lastException;
306
307        DownloadTask(String title, Dialog parent) {
308            super(title, new PleaseWaitProgressMonitor(parent), false);
309        }
310
311        @Override
312        protected void cancel() {
313            canceled = true;
314            OsmApi.getOsmApi().cancel();
315        }
316
317        protected void refreshView(Relation relation) {
318            for (int i = 0; i < childTree.getRowCount(); i++) {
319                Relation reference = (Relation) childTree.getPathForRow(i).getLastPathComponent();
320                if (reference == relation) {
321                    model.refreshNode(childTree.getPathForRow(i));
322                }
323            }
324        }
325
326        @Override
327        protected void finish() {
328            if (canceled)
329                return;
330            if (lastException != null) {
331                ExceptionDialogUtil.explainException(lastException);
332                return;
333            }
334
335            if (conflictsCount > 0) {
336                JOptionPane.showMessageDialog(
337                        Main.parent,
338                        trn("There was {0} conflict during import.",
339                                "There were {0} conflicts during import.",
340                                conflictsCount, conflictsCount),
341                                trn("Conflict in data", "Conflicts in data", conflictsCount),
342                                JOptionPane.WARNING_MESSAGE
343                );
344            }
345        }
346    }
347
348    /**
349     * The asynchronous task for downloading relation members.
350     */
351    class DownloadAllChildrenTask extends DownloadTask {
352        private final Stack<Relation> relationsToDownload;
353        private final Set<Long> downloadedRelationIds;
354
355        DownloadAllChildrenTask(Dialog parent, Relation r) {
356            super(tr("Download relation members"), parent);
357            relationsToDownload = new Stack<>();
358            downloadedRelationIds = new HashSet<>();
359            relationsToDownload.push(r);
360        }
361
362        /**
363         * warns the user if a relation couldn't be loaded because it was deleted on
364         * the server (the server replied a HTTP code 410)
365         *
366         * @param r the relation
367         */
368        protected void warnBecauseOfDeletedRelation(Relation r) {
369            String message = tr("<html>The child relation<br>"
370                    + "{0}<br>"
371                    + "is deleted on the server. It cannot be loaded</html>",
372                    Utils.escapeReservedCharactersHTML(r.getDisplayName(DefaultNameFormatter.getInstance()))
373            );
374
375            JOptionPane.showMessageDialog(
376                    Main.parent,
377                    message,
378                    tr("Relation is deleted"),
379                    JOptionPane.WARNING_MESSAGE
380            );
381        }
382
383        /**
384         * Remembers the child relations to download
385         *
386         * @param parent the parent relation
387         */
388        protected void rememberChildRelationsToDownload(Relation parent) {
389            downloadedRelationIds.add(parent.getId());
390            for (RelationMember member: parent.getMembers()) {
391                if (member.isRelation()) {
392                    Relation child = member.getRelation();
393                    if (!downloadedRelationIds.contains(child.getId())) {
394                        relationsToDownload.push(child);
395                    }
396                }
397            }
398        }
399
400        /**
401         * Merges the primitives in <code>ds</code> to the dataset of the
402         * edit layer
403         *
404         * @param ds the data set
405         */
406        protected void mergeDataSet(DataSet ds) {
407            if (ds != null) {
408                final DataSetMerger visitor = new DataSetMerger(getLayer().data, ds);
409                visitor.merge();
410                if (!visitor.getConflicts().isEmpty()) {
411                    getLayer().getConflicts().add(visitor.getConflicts());
412                    conflictsCount += visitor.getConflicts().size();
413                }
414            }
415        }
416
417        @Override
418        protected void realRun() throws SAXException, IOException, OsmTransferException {
419            try {
420                while (!relationsToDownload.isEmpty() && !canceled) {
421                    Relation r = relationsToDownload.pop();
422                    if (r.isNew()) {
423                        continue;
424                    }
425                    rememberChildRelationsToDownload(r);
426                    progressMonitor.setCustomText(tr("Downloading relation {0}", r.getDisplayName(DefaultNameFormatter.getInstance())));
427                    OsmServerObjectReader reader = new OsmServerObjectReader(r.getId(), OsmPrimitiveType.RELATION,
428                            true);
429                    DataSet dataSet = null;
430                    try {
431                        dataSet = reader.parseOsm(progressMonitor
432                                .createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
433                    } catch (OsmApiException e) {
434                        if (e.getResponseCode() == HttpURLConnection.HTTP_GONE) {
435                            warnBecauseOfDeletedRelation(r);
436                            continue;
437                        }
438                        throw e;
439                    }
440                    mergeDataSet(dataSet);
441                    refreshView(r);
442                }
443                SwingUtilities.invokeLater(MainApplication.getMap()::repaint);
444            } catch (OsmTransferException e) {
445                if (canceled) {
446                    Logging.warn(tr("Ignoring exception because task was canceled. Exception: {0}", e.toString()));
447                    return;
448                }
449                lastException = e;
450            }
451        }
452    }
453
454    /**
455     * The asynchronous task for downloading a set of relations
456     */
457    class DownloadRelationSetTask extends DownloadTask {
458        private final Set<Relation> relations;
459
460        DownloadRelationSetTask(Dialog parent, Set<Relation> relations) {
461            super(tr("Download relation members"), parent);
462            this.relations = relations;
463        }
464
465        protected void mergeDataSet(DataSet dataSet) {
466            if (dataSet != null) {
467                final DataSetMerger visitor = new DataSetMerger(getLayer().data, dataSet);
468                visitor.merge();
469                if (!visitor.getConflicts().isEmpty()) {
470                    getLayer().getConflicts().add(visitor.getConflicts());
471                    conflictsCount += visitor.getConflicts().size();
472                }
473            }
474        }
475
476        @Override
477        protected void realRun() throws SAXException, IOException, OsmTransferException {
478            try {
479                Iterator<Relation> it = relations.iterator();
480                while (it.hasNext() && !canceled) {
481                    Relation r = it.next();
482                    if (r.isNew()) {
483                        continue;
484                    }
485                    progressMonitor.setCustomText(tr("Downloading relation {0}", r.getDisplayName(DefaultNameFormatter.getInstance())));
486                    OsmServerObjectReader reader = new OsmServerObjectReader(r.getId(), OsmPrimitiveType.RELATION,
487                            true);
488                    DataSet dataSet = reader.parseOsm(progressMonitor
489                            .createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
490                    mergeDataSet(dataSet);
491                    refreshView(r);
492                }
493            } catch (OsmTransferException e) {
494                if (canceled) {
495                    Logging.warn(tr("Ignoring exception because task was canceled. Exception: {0}", e.toString()));
496                    return;
497                }
498                lastException = e;
499            }
500        }
501    }
502}