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