001 // License: GPL. For details, see LICENSE file.
002 package org.openstreetmap.josm.actions.downloadtasks;
003
004 import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005 import static org.openstreetmap.josm.tools.I18n.tr;
006 import static org.openstreetmap.josm.tools.I18n.trn;
007
008 import java.awt.EventQueue;
009 import java.awt.geom.Area;
010 import java.awt.geom.Rectangle2D;
011 import java.util.ArrayList;
012 import java.util.Collection;
013 import java.util.HashSet;
014 import java.util.LinkedHashSet;
015 import java.util.LinkedList;
016 import java.util.List;
017 import java.util.Set;
018 import java.util.concurrent.Future;
019
020 import javax.swing.JOptionPane;
021 import javax.swing.SwingUtilities;
022
023 import org.openstreetmap.josm.Main;
024 import org.openstreetmap.josm.actions.UpdateSelectionAction;
025 import org.openstreetmap.josm.data.Bounds;
026 import org.openstreetmap.josm.data.osm.DataSet;
027 import org.openstreetmap.josm.data.osm.OsmPrimitive;
028 import org.openstreetmap.josm.gui.HelpAwareOptionPane;
029 import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
030 import org.openstreetmap.josm.gui.layer.Layer;
031 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
032 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
033 import org.openstreetmap.josm.gui.progress.ProgressMonitor.CancelListener;
034 import org.openstreetmap.josm.tools.ExceptionUtil;
035 import org.openstreetmap.josm.tools.ImageProvider;
036
037 /**
038 * This class encapsulates the downloading of several bounding boxes that would otherwise be too
039 * large to download in one go. Error messages will be collected for all downloads and displayed as
040 * a list in the end.
041 * @author xeen
042 *
043 */
044 public class DownloadOsmTaskList {
045 private List<DownloadTask> osmTasks = new LinkedList<DownloadTask>();
046 private List<Future<?>> osmTaskFutures = new LinkedList<Future<?>>();
047 private ProgressMonitor progressMonitor;
048
049 /**
050 * Downloads a list of areas from the OSM Server
051 * @param newLayer Set to true if all areas should be put into a single new layer
052 * @param The List of Rectangle2D to download
053 */
054 public Future<?> download(boolean newLayer, List<Rectangle2D> rects, ProgressMonitor progressMonitor) {
055 this.progressMonitor = progressMonitor;
056 if (newLayer) {
057 Layer l = new OsmDataLayer(new DataSet(), OsmDataLayer.createNewName(), null);
058 Main.main.addLayer(l);
059 Main.map.mapView.setActiveLayer(l);
060 }
061
062 progressMonitor.beginTask(null, rects.size());
063 int i = 0;
064 for (Rectangle2D td : rects) {
065 i++;
066 DownloadTask dt = new DownloadOsmTask();
067 ProgressMonitor childProgress = progressMonitor.createSubTaskMonitor(1, false);
068 childProgress.setCustomText(tr("Download {0} of {1} ({2} left)", i, rects.size(), rects.size() - i));
069 Future<?> future = dt.download(false, new Bounds(td), childProgress);
070 osmTaskFutures.add(future);
071 osmTasks.add(dt);
072 }
073 progressMonitor.addCancelListener(new CancelListener() {
074 public void operationCanceled() {
075 for (DownloadTask dt : osmTasks) {
076 dt.cancel();
077 }
078 }
079 });
080 return Main.worker.submit(new PostDownloadProcessor());
081 }
082
083 /**
084 * Downloads a list of areas from the OSM Server
085 * @param newLayer Set to true if all areas should be put into a single new layer
086 * @param The Collection of Areas to download
087 */
088 public Future<?> download(boolean newLayer, Collection<Area> areas, ProgressMonitor progressMonitor) {
089 progressMonitor.beginTask(tr("Updating data"));
090 try {
091 List<Rectangle2D> rects = new ArrayList<Rectangle2D>(areas.size());
092 for (Area a : areas) {
093 rects.add(a.getBounds2D());
094 }
095
096 return download(newLayer, rects, progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
097 } finally {
098 progressMonitor.finishTask();
099 }
100 }
101
102 /**
103 * Replies the set of ids of all complete, non-new primitives (i.e. those with !
104 * primitive.incomplete)
105 *
106 * @return the set of ids of all complete, non-new primitives
107 */
108 protected Set<OsmPrimitive> getCompletePrimitives(DataSet ds) {
109 HashSet<OsmPrimitive> ret = new HashSet<OsmPrimitive>();
110 for (OsmPrimitive primitive : ds.allPrimitives()) {
111 if (!primitive.isIncomplete() && !primitive.isNew()) {
112 ret.add(primitive);
113 }
114 }
115 return ret;
116 }
117
118 /**
119 * Updates the local state of a set of primitives (given by a set of primitive ids) with the
120 * state currently held on the server.
121 *
122 * @param potentiallyDeleted a set of ids to check update from the server
123 */
124 protected void updatePotentiallyDeletedPrimitives(Set<OsmPrimitive> potentiallyDeleted) {
125 final ArrayList<OsmPrimitive> toSelect = new ArrayList<OsmPrimitive>();
126 for (OsmPrimitive primitive : potentiallyDeleted) {
127 if (primitive != null) {
128 toSelect.add(primitive);
129 }
130 }
131 EventQueue.invokeLater(new Runnable() {
132 public void run() {
133 new UpdateSelectionAction().updatePrimitives(toSelect);
134 }
135 });
136 }
137
138 /**
139 * Processes a set of primitives (given by a set of their ids) which might be deleted on the
140 * server. First prompts the user whether he wants to check the current state on the server. If
141 * yes, retrieves the current state on the server and checks whether the primitives are indeed
142 * deleted on the server.
143 *
144 * @param potentiallyDeleted a set of primitives (given by their ids)
145 */
146 protected void handlePotentiallyDeletedPrimitives(Set<OsmPrimitive> potentiallyDeleted) {
147 ButtonSpec[] options = new ButtonSpec[] {
148 new ButtonSpec(
149 tr("Check on the server"),
150 ImageProvider.get("ok"),
151 tr("Click to check whether objects in your local dataset are deleted on the server"),
152 null /* no specific help topic */
153 ),
154 new ButtonSpec(
155 tr("Ignore"),
156 ImageProvider.get("cancel"),
157 tr("Click to abort and to resume editing"),
158 null /* no specific help topic */
159 ),
160 };
161
162 String message = "<html>" + trn(
163 "There is {0} object in your local dataset which "
164 + "might be deleted on the server.<br>If you later try to delete or "
165 + "update this the server is likely to report a conflict.",
166 "There are {0} objects in your local dataset which "
167 + "might be deleted on the server.<br>If you later try to delete or "
168 + "update them the server is likely to report a conflict.",
169 potentiallyDeleted.size(), potentiallyDeleted.size())
170 + "<br>"
171 + trn("Click <strong>{0}</strong> to check the state of this object on the server.",
172 "Click <strong>{0}</strong> to check the state of these objects on the server.",
173 potentiallyDeleted.size(),
174 options[0].text) + "<br>"
175 + tr("Click <strong>{0}</strong> to ignore." + "</html>", options[1].text);
176
177 int ret = HelpAwareOptionPane.showOptionDialog(
178 Main.parent,
179 message,
180 tr("Deleted or moved objects"),
181 JOptionPane.WARNING_MESSAGE,
182 null,
183 options,
184 options[0],
185 ht("/Action/UpdateData#SyncPotentiallyDeletedObjects")
186 );
187 if (ret != 0 /* OK */)
188 return;
189
190 updatePotentiallyDeletedPrimitives(potentiallyDeleted);
191 }
192
193 /**
194 * Replies the set of primitive ids which have been downloaded by this task list
195 *
196 * @return the set of primitive ids which have been downloaded by this task list
197 */
198 public Set<OsmPrimitive> getDownloadedPrimitives() {
199 HashSet<OsmPrimitive> ret = new HashSet<OsmPrimitive>();
200 for (DownloadTask task : osmTasks) {
201 if (task instanceof DownloadOsmTask) {
202 DataSet ds = ((DownloadOsmTask) task).getDownloadedData();
203 if (ds != null) {
204 ret.addAll(ds.allPrimitives());
205 }
206 }
207 }
208 return ret;
209 }
210
211 class PostDownloadProcessor implements Runnable {
212 /**
213 * Grabs and displays the error messages after all download threads have finished.
214 */
215 public void run() {
216 progressMonitor.finishTask();
217
218 // wait for all download tasks to finish
219 //
220 for (Future<?> future : osmTaskFutures) {
221 try {
222 future.get();
223 } catch (Exception e) {
224 e.printStackTrace();
225 return;
226 }
227 }
228 LinkedHashSet<Object> errors = new LinkedHashSet<Object>();
229 for (DownloadTask dt : osmTasks) {
230 errors.addAll(dt.getErrorObjects());
231 }
232 if (!errors.isEmpty()) {
233 final StringBuilder sb = new StringBuilder();
234 for (Object error : errors) {
235 if (error instanceof String) {
236 sb.append("<li>").append(error).append("</li>").append("<br>");
237 } else if (error instanceof Exception) {
238 sb.append("<li>").append(ExceptionUtil.explainException((Exception) error)).append("</li>")
239 .append("<br>");
240 }
241 }
242 sb.insert(0, "<ul>");
243 sb.append("</ul>");
244
245 SwingUtilities.invokeLater(new Runnable() {
246 @Override
247 public void run() {
248 JOptionPane.showMessageDialog(Main.parent, "<html>"
249 + tr("The following errors occurred during mass download: {0}", sb.toString()) + "</html>",
250 tr("Errors during download"), JOptionPane.ERROR_MESSAGE);
251 }
252 });
253
254 return;
255 }
256
257 // FIXME: this is a hack. We assume that the user canceled the whole download if at
258 // least
259 // one task was canceled or if it failed
260 //
261 for (DownloadTask task : osmTasks) {
262 if (task instanceof DownloadOsmTask) {
263 DownloadOsmTask osmTask = (DownloadOsmTask) task;
264 if (osmTask.isCanceled() || osmTask.isFailed())
265 return;
266 }
267 }
268 final OsmDataLayer editLayer = Main.map.mapView.getEditLayer();
269 if (editLayer != null) {
270 Set<OsmPrimitive> myPrimitives = getCompletePrimitives(editLayer.data);
271 for (DownloadTask task : osmTasks) {
272 if (task instanceof DownloadOsmTask) {
273 DataSet ds = ((DownloadOsmTask) task).getDownloadedData();
274 if (ds != null) {
275 // myPrimitives.removeAll(ds.allPrimitives()) will do the same job but much slower
276 for (OsmPrimitive primitive: ds.allPrimitives()) {
277 myPrimitives.remove(primitive);
278 }
279 }
280 }
281 }
282 if (!myPrimitives.isEmpty()) {
283 handlePotentiallyDeletedPrimitives(myPrimitives);
284 }
285 }
286 }
287 }
288 }