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