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}