001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.awt.Font;
008import java.awt.GridBagLayout;
009import java.io.IOException;
010import java.text.MessageFormat;
011import java.util.ArrayList;
012import java.util.Collections;
013import java.util.HashSet;
014import java.util.LinkedHashSet;
015import java.util.List;
016import java.util.Set;
017import java.util.stream.Collectors;
018
019import javax.swing.JLabel;
020import javax.swing.JOptionPane;
021import javax.swing.JPanel;
022import javax.swing.JScrollPane;
023
024import org.openstreetmap.josm.actions.downloadtasks.DownloadReferrersTask;
025import org.openstreetmap.josm.data.osm.DataSet;
026import org.openstreetmap.josm.data.osm.OsmPrimitive;
027import org.openstreetmap.josm.data.osm.PrimitiveId;
028import org.openstreetmap.josm.gui.ExtendedDialog;
029import org.openstreetmap.josm.gui.MainApplication;
030import org.openstreetmap.josm.gui.PleaseWaitRunnable;
031import org.openstreetmap.josm.gui.layer.OsmDataLayer;
032import org.openstreetmap.josm.gui.progress.ProgressMonitor;
033import org.openstreetmap.josm.gui.util.GuiHelper;
034import org.openstreetmap.josm.gui.widgets.HtmlPanel;
035import org.openstreetmap.josm.gui.widgets.JosmTextArea;
036import org.openstreetmap.josm.io.MultiFetchOverpassObjectReader;
037import org.openstreetmap.josm.io.OsmTransferException;
038import org.openstreetmap.josm.io.OverpassDownloadReader;
039import org.openstreetmap.josm.tools.GBC;
040import org.xml.sax.SAXException;
041
042/**
043 * Task for downloading a set of primitives with all referrers.
044 */
045public class DownloadPrimitivesWithReferrersTask extends PleaseWaitRunnable {
046    /** If true download into a new layer */
047    private final boolean newLayer;
048    /** List of primitives id to download */
049    private final List<PrimitiveId> ids;
050    /** If true, download members for relation */
051    private final boolean full;
052    /** If true, download also referrers */
053    private final boolean downloadReferrers;
054
055    /** Temporary layer where downloaded primitives are put */
056    private final OsmDataLayer tmpLayer;
057    /** Flag indicated that user ask for cancel this task */
058    private boolean canceled;
059    /** Reference to the task currently running */
060    private PleaseWaitRunnable currentTask;
061
062    /** set of missing ids, with overpass API these are also deleted objects */
063    private Set<PrimitiveId> missingPrimitives;
064
065    /**
066     * Constructor
067     *
068     * @param newLayer if the data should be downloaded into a new layer
069     * @param ids List of primitive id to download
070     * @param downloadReferrers if the referrers of the object should be downloaded as well,
071     *     i.e., parent relations, and for nodes, additionally, parent ways
072     * @param full if the members of a relation should be downloaded as well
073     * @param newLayerName the name to use for the new layer, can be null.
074     * @param monitor ProgressMonitor to use, or null to create a new one
075     */
076    public DownloadPrimitivesWithReferrersTask(boolean newLayer, List<PrimitiveId> ids, boolean downloadReferrers,
077            boolean full, String newLayerName, ProgressMonitor monitor) {
078        super(tr("Download objects"), monitor, false);
079        this.ids = ids;
080        this.downloadReferrers = downloadReferrers;
081        this.full = full;
082        this.newLayer = newLayer;
083        // Check we don't try to download new primitives
084        for (PrimitiveId primitiveId : ids) {
085            if (primitiveId.isNew()) {
086                throw new IllegalArgumentException(MessageFormat.format(
087                        "Cannot download new primitives (ID {0})", primitiveId.getUniqueId()));
088            }
089        }
090        // All downloaded primitives are put in a tmpLayer
091        tmpLayer = new OsmDataLayer(new DataSet(), newLayerName != null ? newLayerName : OsmDataLayer.createNewName(), null);
092    }
093
094    /**
095     * Cancel recursively the task. Do not call directly
096     * @see DownloadPrimitivesWithReferrersTask#operationCanceled()
097     */
098    @Override
099    protected void cancel() {
100        synchronized (this) {
101            canceled = true;
102            if (currentTask != null)
103                currentTask.operationCanceled();
104        }
105    }
106
107    @Override
108    protected void realRun() throws SAXException, IOException, OsmTransferException {
109        if (Boolean.TRUE.equals(OverpassDownloadReader.FOR_MULTI_FETCH.get())) {
110            useOverpassApi();
111        } else {
112            useOSMApi();
113        }
114    }
115
116    private void useOverpassApi() {
117        String request = MultiFetchOverpassObjectReader.genOverpassQuery(ids, true, downloadReferrers, full);
118        currentTask = new DownloadFromOverpassTask(request, tmpLayer.data, getProgressMonitor().createSubTaskMonitor(1, false));
119        currentTask.run();
120        missingPrimitives = ids.stream()
121                .filter(id -> tmpLayer.data.getPrimitiveById(id) == null)
122                .collect(Collectors.toSet());
123    }
124
125    private void useOSMApi() {
126        getProgressMonitor().setTicksCount(ids.size()+1);
127        // First, download primitives
128        DownloadPrimitivesTask mainTask = new DownloadPrimitivesTask(tmpLayer, ids, full,
129                getProgressMonitor().createSubTaskMonitor(1, false));
130        synchronized (this) {
131            currentTask = mainTask;
132            if (canceled) {
133                currentTask = null;
134                return;
135            }
136        }
137        currentTask.run();
138
139        missingPrimitives = mainTask.getMissingPrimitives();
140
141        // Then, download referrers for each primitive
142        if (downloadReferrers && tmpLayer.data != null) {
143            // see #18895: don't try to download parents for invisible objects
144            List<PrimitiveId> visible = ids.stream().map(tmpLayer.data::getPrimitiveById)
145                    .filter(p -> p != null && p.isVisible()).collect(Collectors.toList());
146            if (!visible.isEmpty()) {
147                currentTask = new DownloadReferrersTask(tmpLayer, visible);
148                currentTask.run();
149                synchronized (this) {
150                    if (currentTask.getProgressMonitor().isCanceled())
151                        cancel();
152                }
153            }
154        }
155        currentTask = null;
156    }
157
158    @Override
159    protected void finish() {
160        synchronized (this) {
161            if (canceled)
162                return;
163        }
164
165        // Append downloaded data to JOSM
166        OsmDataLayer layer = MainApplication.getLayerManager().getEditLayer();
167        if (layer == null || this.newLayer || !layer.isDownloadable())
168            MainApplication.getLayerManager().addLayer(tmpLayer);
169        else
170            layer.mergeFrom(tmpLayer);
171
172        // Collect known deleted primitives
173        final Set<PrimitiveId> del = new HashSet<>();
174        DataSet ds = MainApplication.getLayerManager().getEditDataSet();
175        for (PrimitiveId id : ids) {
176            OsmPrimitive osm = ds.getPrimitiveById(id);
177            if (osm != null && osm.isDeleted()) {
178                del.add(id);
179            }
180        }
181        final Set<PrimitiveId> errs;
182        if (missingPrimitives != null) {
183            errs = missingPrimitives.stream().filter(id -> !del.contains(id)).collect(Collectors.toCollection(LinkedHashSet::new));
184        } else {
185            errs = Collections.emptySet();
186        }
187
188        // Warm about missing primitives
189        if (!errs.isEmpty()) {
190            final String assumedApiRC;
191            if (Boolean.TRUE.equals(OverpassDownloadReader.FOR_MULTI_FETCH.get())) {
192                assumedApiRC = trn("The server did not return data for the requested object, it was either deleted or does not exist.",
193                        "The server did not return data for the requested objects, they were either deleted or do not exist.",
194                        errs.size());
195
196            } else {
197                assumedApiRC = tr("The server replied with response code 404.<br>"
198                        + "This usually means, the server does not know an object with the requested id.");
199            }
200            GuiHelper.runInEDTAndWait(() -> reportProblemDialog(errs,
201                    trn("Object could not be downloaded", "Some objects could not be downloaded", errs.size()),
202                    trn("One object could not be downloaded.<br>",
203                            "{0} objects could not be downloaded.<br>",
204                            errs.size(),
205                            errs.size())
206                            + assumedApiRC,
207                    tr("missing objects:"),
208                    JOptionPane.ERROR_MESSAGE
209                    ).showDialog());
210        }
211
212        // Warm about deleted primitives
213        if (!del.isEmpty())
214            GuiHelper.runInEDTAndWait(() -> reportProblemDialog(del,
215                    trn("Object deleted", "Objects deleted", del.size()),
216                    trn(
217                        "One downloaded object is deleted.",
218                        "{0} downloaded objects are deleted.",
219                        del.size(),
220                        del.size()),
221                    null,
222                    JOptionPane.WARNING_MESSAGE
223            ).showDialog());
224    }
225
226    /**
227     * Return ids of really downloaded primitives.
228     * @return List of primitives id or null if no primitives were downloaded
229     */
230    public List<PrimitiveId> getDownloadedId() {
231        synchronized (this) {
232            if (canceled)
233                return null;
234        }
235        List<PrimitiveId> downloaded = new ArrayList<>(ids);
236        if (missingPrimitives != null) {
237            downloaded.removeAll(missingPrimitives);
238        }
239        return downloaded;
240    }
241
242    /**
243     * Dialog for report a problem during download.
244     * @param errs Primitives involved
245     * @param title Title of dialog
246     * @param text Detail message
247     * @param listLabel List of primitives description
248     * @param msgType Type of message, see {@link JOptionPane}
249     * @return The Dialog object
250     */
251    public static ExtendedDialog reportProblemDialog(Set<PrimitiveId> errs,
252            String title, String text, String listLabel, int msgType) {
253        JPanel p = new JPanel(new GridBagLayout());
254        p.add(new HtmlPanel(text), GBC.eop());
255        JosmTextArea txt = new JosmTextArea();
256        if (listLabel != null) {
257            JLabel missing = new JLabel(listLabel);
258            missing.setFont(missing.getFont().deriveFont(Font.PLAIN));
259            missing.setLabelFor(txt);
260            p.add(missing, GBC.eol());
261        }
262        txt.setFont(GuiHelper.getMonospacedFont(txt));
263        txt.setEditable(false);
264        txt.setBackground(p.getBackground());
265        txt.setColumns(40);
266        txt.setRows(1);
267        txt.setText(errs.stream().map(pid -> pid.getType().getAPIName().substring(0, 1) + pid.getUniqueId())
268                .collect(Collectors.joining(", ")));
269        JScrollPane scroll = new JScrollPane(txt);
270        p.add(scroll, GBC.eop().weight(1.0, 0.0).fill(GBC.HORIZONTAL));
271
272        return new ExtendedDialog(
273                MainApplication.getMainFrame(),
274                title,
275                tr("Ok"))
276        .setButtonIcons("ok")
277        .setIcon(msgType)
278        .setContent(p, false);
279    }
280}