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