001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io; 003 004import static org.openstreetmap.josm.tools.I18n.marktr; 005import static org.openstreetmap.josm.tools.I18n.tr; 006import static org.openstreetmap.josm.tools.I18n.trn; 007 008import java.util.ArrayList; 009import java.util.Collection; 010import java.util.Iterator; 011import java.util.LinkedList; 012import java.util.List; 013 014import org.openstreetmap.josm.data.osm.Changeset; 015import org.openstreetmap.josm.data.osm.OsmPrimitive; 016import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 017import org.openstreetmap.josm.gui.JosmUserIdentityManager; 018import org.openstreetmap.josm.gui.io.UploadStrategySpecification; 019import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 020import org.openstreetmap.josm.gui.progress.ProgressMonitor; 021import org.openstreetmap.josm.tools.CheckParameterUtil; 022 023/** 024 * Class that uploads all changes to the osm server. 025 * 026 * This is done like this: - All objects with id = 0 are uploaded as new, except 027 * those in deleted, which are ignored - All objects in deleted list are 028 * deleted. - All remaining objects with modified flag set are updated. 029 */ 030public class OsmServerWriter { 031 /** 032 * This list contains all successfully processed objects. The caller of 033 * upload* has to check this after the call and update its dataset. 034 * 035 * If a server connection error occurs, this may contain fewer entries 036 * than where passed in the list to upload*. 037 */ 038 private Collection<OsmPrimitive> processed; 039 040 private static volatile List<OsmServerWritePostprocessor> postprocessors; 041 042 /** 043 * Registers a post-processor. 044 * @param pp post-processor to register 045 */ 046 public static void registerPostprocessor(OsmServerWritePostprocessor pp) { 047 if (postprocessors == null) { 048 postprocessors = new ArrayList<>(); 049 } 050 postprocessors.add(pp); 051 } 052 053 /** 054 * Unregisters a post-processor. 055 * @param pp post-processor to unregister 056 */ 057 public static void unregisterPostprocessor(OsmServerWritePostprocessor pp) { 058 if (postprocessors != null) { 059 postprocessors.remove(pp); 060 } 061 } 062 063 private final OsmApi api = OsmApi.getOsmApi(); 064 private boolean canceled; 065 066 private static final int MSECS_PER_SECOND = 1000; 067 private static final int SECONDS_PER_MINUTE = 60; 068 private static final int MSECS_PER_MINUTE = MSECS_PER_SECOND * SECONDS_PER_MINUTE; 069 070 private long uploadStartTime; 071 072 protected String timeLeft(int progress, int listSize) { 073 long now = System.currentTimeMillis(); 074 long elapsed = now - uploadStartTime; 075 if (elapsed == 0) { 076 elapsed = 1; 077 } 078 double uploadsPerMs = (double) progress / elapsed; 079 double uploadsLeft = (double) listSize - progress; 080 long msLeft = (long) (uploadsLeft / uploadsPerMs); 081 long minutesLeft = msLeft / MSECS_PER_MINUTE; 082 long secondsLeft = (msLeft / MSECS_PER_SECOND) % SECONDS_PER_MINUTE; 083 StringBuilder timeLeftStr = new StringBuilder().append(minutesLeft).append(':'); 084 if (secondsLeft < 10) { 085 timeLeftStr.append('0'); 086 } 087 return timeLeftStr.append(secondsLeft).toString(); 088 } 089 090 /** 091 * Uploads the changes individually. Invokes one API call per uploaded primitmive. 092 * 093 * @param primitives the collection of primitives to upload 094 * @param progressMonitor the progress monitor 095 * @throws OsmTransferException if an exception occurs 096 */ 097 protected void uploadChangesIndividually(Collection<? extends OsmPrimitive> primitives, ProgressMonitor progressMonitor) 098 throws OsmTransferException { 099 try { 100 progressMonitor.beginTask(tr("Starting to upload with one request per primitive ...")); 101 progressMonitor.setTicksCount(primitives.size()); 102 uploadStartTime = System.currentTimeMillis(); 103 for (OsmPrimitive osm : primitives) { 104 String msg; 105 switch(OsmPrimitiveType.from(osm)) { 106 case NODE: msg = marktr("{0}% ({1}/{2}), {3} left. Uploading node ''{4}'' (id: {5})"); break; 107 case WAY: msg = marktr("{0}% ({1}/{2}), {3} left. Uploading way ''{4}'' (id: {5})"); break; 108 case RELATION: msg = marktr("{0}% ({1}/{2}), {3} left. Uploading relation ''{4}'' (id: {5})"); break; 109 default: throw new AssertionError(); 110 } 111 int progress = progressMonitor.getTicks(); 112 progressMonitor.subTask( 113 tr(msg, 114 Math.round(100.0*progress/primitives.size()), 115 progress, 116 primitives.size(), 117 timeLeft(progress, primitives.size()), 118 osm.getName() == null ? osm.getId() : osm.getName(), osm.getId())); 119 makeApiRequest(osm, progressMonitor); 120 processed.add(osm); 121 progressMonitor.worked(1); 122 } 123 } catch (OsmTransferException e) { 124 throw e; 125 } catch (Exception e) { 126 throw new OsmTransferException(e); 127 } finally { 128 progressMonitor.finishTask(); 129 } 130 } 131 132 /** 133 * Upload all changes in one diff upload 134 * 135 * @param primitives the collection of primitives to upload 136 * @param progressMonitor the progress monitor 137 * @throws OsmTransferException if an exception occurs 138 */ 139 protected void uploadChangesAsDiffUpload(Collection<? extends OsmPrimitive> primitives, ProgressMonitor progressMonitor) 140 throws OsmTransferException { 141 try { 142 progressMonitor.beginTask(tr("Starting to upload in one request ...")); 143 processed.addAll(api.uploadDiff(primitives, progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false))); 144 } finally { 145 progressMonitor.finishTask(); 146 } 147 } 148 149 /** 150 * Upload all changes in one diff upload 151 * 152 * @param primitives the collection of primitives to upload 153 * @param progressMonitor the progress monitor 154 * @param chunkSize the size of the individual upload chunks. > 0 required. 155 * @throws IllegalArgumentException if chunkSize <= 0 156 * @throws OsmTransferException if an exception occurs 157 */ 158 protected void uploadChangesInChunks(Collection<? extends OsmPrimitive> primitives, ProgressMonitor progressMonitor, int chunkSize) 159 throws OsmTransferException { 160 if (chunkSize <= 0) 161 throw new IllegalArgumentException(tr("Value >0 expected for parameter ''{0}'', got {1}", "chunkSize", chunkSize)); 162 try { 163 progressMonitor.beginTask(tr("Starting to upload in chunks...")); 164 List<OsmPrimitive> chunk = new ArrayList<>(chunkSize); 165 Iterator<? extends OsmPrimitive> it = primitives.iterator(); 166 int numChunks = (int) Math.ceil((double) primitives.size() / (double) chunkSize); 167 int i = 0; 168 while (it.hasNext()) { 169 i++; 170 if (canceled) return; 171 int j = 0; 172 chunk.clear(); 173 while (it.hasNext() && j < chunkSize) { 174 if (canceled) return; 175 j++; 176 chunk.add(it.next()); 177 } 178 progressMonitor.setCustomText( 179 trn("({0}/{1}) Uploading {2} object...", 180 "({0}/{1}) Uploading {2} objects...", 181 chunk.size(), i, numChunks, chunk.size())); 182 processed.addAll(api.uploadDiff(chunk, progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false))); 183 } 184 } finally { 185 progressMonitor.finishTask(); 186 } 187 } 188 189 /** 190 * Send the dataset to the server. 191 * 192 * @param strategy the upload strategy. Must not be null. 193 * @param primitives list of objects to send 194 * @param changeset the changeset the data is uploaded to. Must not be null. 195 * @param monitor the progress monitor. If null, assumes {@link NullProgressMonitor#INSTANCE} 196 * @throws IllegalArgumentException if changeset is null 197 * @throws IllegalArgumentException if strategy is null 198 * @throws OsmTransferException if something goes wrong 199 */ 200 public void uploadOsm(UploadStrategySpecification strategy, Collection<? extends OsmPrimitive> primitives, 201 Changeset changeset, ProgressMonitor monitor) throws OsmTransferException { 202 CheckParameterUtil.ensureParameterNotNull(changeset, "changeset"); 203 processed = new LinkedList<>(); 204 monitor = monitor == null ? NullProgressMonitor.INSTANCE : monitor; 205 monitor.beginTask(tr("Uploading data ...")); 206 try { 207 api.initialize(monitor); 208 // check whether we can use diff upload 209 if (changeset.getId() == 0) { 210 api.openChangeset(changeset, monitor.createSubTaskMonitor(0, false)); 211 // update the user information 212 changeset.setUser(JosmUserIdentityManager.getInstance().asUser()); 213 } else { 214 api.updateChangeset(changeset, monitor.createSubTaskMonitor(0, false)); 215 } 216 api.setChangeset(changeset); 217 switch(strategy.getStrategy()) { 218 case SINGLE_REQUEST_STRATEGY: 219 uploadChangesAsDiffUpload(primitives, monitor.createSubTaskMonitor(0, false)); 220 break; 221 case INDIVIDUAL_OBJECTS_STRATEGY: 222 uploadChangesIndividually(primitives, monitor.createSubTaskMonitor(0, false)); 223 break; 224 case CHUNKED_DATASET_STRATEGY: 225 default: 226 uploadChangesInChunks(primitives, monitor.createSubTaskMonitor(0, false), strategy.getChunkSize()); 227 break; 228 } 229 } finally { 230 executePostprocessors(monitor); 231 monitor.finishTask(); 232 api.setChangeset(null); 233 } 234 } 235 236 void makeApiRequest(OsmPrimitive osm, ProgressMonitor progressMonitor) throws OsmTransferException { 237 if (osm.isDeleted()) { 238 api.deletePrimitive(osm, progressMonitor); 239 } else if (osm.isNew()) { 240 api.createPrimitive(osm, progressMonitor); 241 } else { 242 api.modifyPrimitive(osm, progressMonitor); 243 } 244 } 245 246 /** 247 * Cancel operation. 248 */ 249 public void cancel() { 250 this.canceled = true; 251 if (api != null) { 252 api.cancel(); 253 } 254 } 255 256 /** 257 * Replies the collection of successfully processed primitives 258 * 259 * @return the collection of successfully processed primitives 260 */ 261 public Collection<OsmPrimitive> getProcessedPrimitives() { 262 return processed; 263 } 264 265 /** 266 * Calls all registered upload postprocessors. 267 * @param pm progress monitor 268 */ 269 public void executePostprocessors(ProgressMonitor pm) { 270 if (postprocessors != null) { 271 for (OsmServerWritePostprocessor pp : postprocessors) { 272 pp.postprocessUploadedPrimitives(processed, pm); 273 } 274 } 275 } 276}