Index: applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/MapillaryLayer.java
===================================================================
--- applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/MapillaryLayer.java	(revision 31455)
+++ applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/MapillaryLayer.java	(revision 31456)
@@ -11,5 +11,5 @@
 import org.openstreetmap.josm.plugins.mapillary.mode.JoinMode;
 import org.openstreetmap.josm.plugins.mapillary.mode.SelectMode;
-import org.openstreetmap.josm.plugins.mapillary.utils.PluginState;
+import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryUtils;
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.gui.layer.Layer;
@@ -151,5 +151,5 @@
       Main.map.mapView.addMouseMotionListener(mode);
       NavigatableComponent.addZoomChangeListener(mode);
-      updateHelpText();
+      MapillaryUtils.updateHelpText();
     }
   }
@@ -583,5 +583,5 @@
   public void activeLayerChange(Layer oldLayer, Layer newLayer) {
     if (newLayer == this) {
-      updateHelpText();
+      MapillaryUtils.updateHelpText();
       MapillaryPlugin.setMenuEnabled(MapillaryPlugin.JOIN_MENU, true);
     } else
@@ -598,21 +598,3 @@
     // Nothing
   }
-
-  /**
-   * Updates the help text at the bottom of the window.
-   */
-  public void updateHelpText() {
-    String ret = "";
-    if (PluginState.isDownloading())
-      ret += tr("Downloading");
-    else if (this.data.size() > 0)
-      ret += tr("Total images: {0}", this.data.size());
-    else
-      ret += tr("No images found");
-    if (this.mode != null)
-      ret += " -- " + tr(this.mode.toString());
-    if (PluginState.isUploading())
-      ret += " -- " + PluginState.getUploadString();
-    Main.map.statusLine.setHelpText(ret);
-  }
 }
Index: applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryImportAction.java
===================================================================
--- applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryImportAction.java	(revision 31455)
+++ applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryImportAction.java	(revision 31456)
@@ -25,4 +25,5 @@
 import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
+import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryUtils;
 import org.openstreetmap.josm.tools.Shortcut;
 
@@ -153,8 +154,8 @@
       double caValue = 0;
       if (lat.getValue() instanceof RationalNumber[])
-        latValue = degMinSecToDouble((RationalNumber[]) lat.getValue(), lat_ref
+        latValue = MapillaryUtils.degMinSecToDouble((RationalNumber[]) lat.getValue(), lat_ref
             .getValue().toString());
       if (lon.getValue() instanceof RationalNumber[])
-        lonValue = degMinSecToDouble((RationalNumber[]) lon.getValue(), lon_ref
+        lonValue = MapillaryUtils.degMinSecToDouble((RationalNumber[]) lon.getValue(), lon_ref
             .getValue().toString());
       if (ca != null && ca.getValue() instanceof RationalNumber)
@@ -216,62 +217,3 @@
     return readNoTags(file);
   }
-
-  /**
-   * Calculates the decimal degree-value from a degree value given in
-   * degrees-minutes-seconds-format
-   *
-   * @param degMinSec
-   *          an array of length 3, the values in there are (in this order)
-   *          degrees, minutes and seconds
-   * @param ref
-   *          the latitude or longitude reference determining if the given value
-   *          is:
-   *          <ul>
-   *          <li>north (
-   *          {@link GpsTagConstants#GPS_TAG_GPS_LATITUDE_REF_VALUE_NORTH}) or
-   *          south (
-   *          {@link GpsTagConstants#GPS_TAG_GPS_LATITUDE_REF_VALUE_SOUTH}) of
-   *          the equator</li>
-   *          <li>east (
-   *          {@link GpsTagConstants#GPS_TAG_GPS_LONGITUDE_REF_VALUE_EAST}) or
-   *          west ({@link GpsTagConstants#GPS_TAG_GPS_LONGITUDE_REF_VALUE_WEST}
-   *          ) of the equator</li>
-   *          </ul>
-   * @return the decimal degree-value for the given input, negative when west of
-   *         0-meridian or south of equator, positive otherwise
-   * @throws IllegalArgumentException
-   *           if {@code degMinSec} doesn't have length 3 or if {@code ref} is
-   *           not one of the values mentioned above
-   */
-  // TODO: Maybe move into a separate utility class?
-  public static double degMinSecToDouble(RationalNumber[] degMinSec, String ref) {
-    if (degMinSec == null || degMinSec.length != 3) {
-      throw new IllegalArgumentException("Array's length must be 3.");
-    }
-    for (int i = 0; i < 3; i++)
-      if (degMinSec[i] == null)
-        throw new IllegalArgumentException("Null value in array.");
-
-    switch (ref) {
-      case GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF_VALUE_NORTH:
-      case GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF_VALUE_SOUTH:
-      case GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF_VALUE_EAST:
-      case GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF_VALUE_WEST:
-        break;
-      default:
-        throw new IllegalArgumentException("Invalid ref.");
-    }
-
-    double result = degMinSec[0].doubleValue(); // degrees
-    result += degMinSec[1].doubleValue() / 60; // minutes
-    result += degMinSec[2].doubleValue() / 3600; // seconds
-
-    if (GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF_VALUE_SOUTH.equals(ref)
-        || GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF_VALUE_WEST.equals(ref)) {
-      result *= -1;
-    }
-
-    result = 360 * ((result + 180) / 360 - Math.floor((result + 180) / 360)) - 180;
-    return result;
-  }
 }
Index: applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryImportIntoSequenceAction.java
===================================================================
--- applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryImportIntoSequenceAction.java	(revision 31455)
+++ applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryImportIntoSequenceAction.java	(revision 31456)
@@ -29,4 +29,5 @@
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
 import org.openstreetmap.josm.plugins.mapillary.MapillarySequence;
+import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryUtils;
 import org.openstreetmap.josm.tools.Shortcut;
 
@@ -151,8 +152,8 @@
       double caValue = 0;
       if (lat.getValue() instanceof RationalNumber[])
-        latValue = MapillaryImportAction.degMinSecToDouble(
+        latValue = MapillaryUtils.degMinSecToDouble(
             (RationalNumber[]) lat.getValue(), lat_ref.getValue().toString());
       if (lon.getValue() instanceof RationalNumber[])
-        lonValue = MapillaryImportAction.degMinSecToDouble(
+        lonValue = MapillaryUtils.degMinSecToDouble(
             (RationalNumber[]) lon.getValue(), lon_ref.getValue().toString());
       if (ca != null && ca.getValue() instanceof RationalNumber)
Index: applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryUploadAction.java
===================================================================
--- applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryUploadAction.java	(revision 31455)
+++ applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/actions/MapillaryUploadAction.java	(revision 31456)
@@ -17,5 +17,5 @@
 import org.openstreetmap.josm.plugins.mapillary.MapillaryPlugin;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryUploadDialog;
-import org.openstreetmap.josm.plugins.mapillary.oauth.OAuthUtils;
+import org.openstreetmap.josm.plugins.mapillary.oauth.UploadUtils;
 import org.openstreetmap.josm.tools.Shortcut;
 
@@ -52,5 +52,5 @@
         && (int) pane.getValue() == JOptionPane.OK_OPTION) {
       if (dialog.sequence.isSelected()) {
-        OAuthUtils.uploadSequence(MapillaryLayer.getInstance().getData()
+        UploadUtils.uploadSequence(MapillaryLayer.getInstance().getData()
             .getSelectedImage().getSequence());
       }
Index: applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/downloads/MapillarySquareDownloadManagerThread.java
===================================================================
--- applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/downloads/MapillarySquareDownloadManagerThread.java	(revision 31455)
+++ applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/downloads/MapillarySquareDownloadManagerThread.java	(revision 31456)
@@ -11,7 +11,7 @@
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.plugins.mapillary.MapillaryData;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryFilterDialog;
 import org.openstreetmap.josm.plugins.mapillary.gui.MapillaryMainDialog;
+import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryUtils;
 import org.openstreetmap.josm.plugins.mapillary.utils.PluginState;
 
@@ -69,5 +69,5 @@
     try {
       PluginState.startDownload();
-      MapillaryLayer.getInstance().updateHelpText();
+      MapillaryUtils.updateHelpText();
       downloadSequences();
       completeImages();
@@ -78,7 +78,7 @@
     } finally {
       PluginState.finishDownload();
-      MapillaryLayer.getInstance().updateHelpText();
+      MapillaryUtils.updateHelpText();
     }
-    MapillaryLayer.getInstance().updateHelpText();
+    MapillaryUtils.updateHelpText();
     MapillaryData.dataUpdated();
     MapillaryFilterDialog.getInstance().refresh();
Index: applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/oauth/OAuthUtils.java
===================================================================
--- applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/oauth/OAuthUtils.java	(revision 31455)
+++ applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/oauth/OAuthUtils.java	(revision 31456)
@@ -1,55 +1,14 @@
 package org.openstreetmap.josm.plugins.mapillary.oauth;
 
-import java.io.BufferedOutputStream;
 import java.io.BufferedReader;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
 import java.net.HttpURLConnection;
 import java.net.URL;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.HashMap;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
 
-import javax.imageio.ImageIO;
 import javax.json.Json;
 import javax.json.JsonObject;
 
-import org.apache.commons.imaging.ImageReadException;
-import org.apache.commons.imaging.ImageWriteException;
-import org.apache.commons.imaging.Imaging;
-import org.apache.commons.imaging.common.ImageMetadata;
-import org.apache.commons.imaging.common.RationalNumber;
-import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata;
-import org.apache.commons.imaging.formats.jpeg.exif.ExifRewriter;
-import org.apache.commons.imaging.formats.tiff.TiffImageMetadata;
-import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
-import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants;
-import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory;
-import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.entity.ContentType;
-import org.apache.http.entity.mime.MultipartEntityBuilder;
-import org.apache.http.entity.mime.content.FileBody;
-import org.apache.http.entity.mime.content.StringBody;
-import org.apache.http.impl.client.HttpClientBuilder;
 import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryImportedImage;
-import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
-import org.openstreetmap.josm.plugins.mapillary.MapillarySequence;
-import org.openstreetmap.josm.plugins.mapillary.utils.PluginState;
 
 /**
@@ -60,11 +19,4 @@
  */
 public class OAuthUtils {
-
-  private static final String[] keys = { "key", "AWSAccessKeyId", "acl",
-      "policy", "signature", "Content-Type" };
-  private static final String UPLOAD_URL = "https://s3-eu-west-1.amazonaws.com/mapillary.uploads.manual.images";
-
-  // Count to name temporal files.
-  private static int c = 0;
 
   /**
@@ -88,205 +40,3 @@
     return Json.createReader(in).readObject();
   }
-
-  /**
-   * Uploads the given MapillaryImportedImage object.
-   *
-   * @param image
-   *
-   * @throws NoSuchAlgorithmException
-   * @throws UnsupportedEncodingException
-   * @throws InvalidKeyException
-   *
-   */
-  public static void upload(MapillaryImportedImage image)
-      throws InvalidKeyException, UnsupportedEncodingException,
-      NoSuchAlgorithmException {
-    try {
-      upload(image, UUID.randomUUID());
-    } catch (IOException e) {
-      Main.error(e);
-    }
-  }
-
-  /**
-   * @param image
-   * @param uuid
-   *          The UUID used to create the sequence.
-   * @throws NoSuchAlgorithmException
-   * @throws InvalidKeyException
-   * @throws IOException
-   */
-  public static void upload(MapillaryImportedImage image, UUID uuid)
-      throws NoSuchAlgorithmException, InvalidKeyException, IOException {
-    String key = MapillaryUser.getUsername() + "/" + uuid.toString() + "/"
-        + image.getLatLon().lat() + "_" + image.getLatLon().lon() + "_"
-        + image.getCa() + "_" + image.datetimeOriginal + ".jpg";
-
-    String policy = null;
-    String signature = null;
-    policy = MapillaryUser.getSecrets().get("images_policy");
-    signature = MapillaryUser.getSecrets().get("images_hash");
-
-    HashMap<String, String> hash = new HashMap<>();
-    hash.put("key", key);
-    hash.put("AWSAccessKeyId", "AKIAI2X3BJAT2W75HILA");
-    hash.put("acl", "private");
-    hash.put("policy", policy);
-    hash.put("signature", signature);
-    hash.put("Content-Type", "image/jpeg");
-
-    try {
-      uploadFile(updateFile(image), hash);
-    } catch (ImageReadException | ImageWriteException e) {
-      Main.error(e);
-    }
-
-  }
-
-  /**
-   * @param file
-   * @param hash
-   * @throws IOException
-   */
-  public static void uploadFile(File file, HashMap<String, String> hash)
-      throws IOException {
-    HttpClientBuilder builder = HttpClientBuilder.create();
-    HttpClient httpClient = builder.build();
-    HttpPost httpPost = new HttpPost(UPLOAD_URL);
-
-    MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create();
-    for (String key : keys) {
-      entityBuilder.addPart(key, new StringBody(hash.get(key),
-          ContentType.TEXT_PLAIN));
-    }
-    entityBuilder.addPart("file", new FileBody(file));
-
-    HttpEntity entity = entityBuilder.build();
-    httpPost.setEntity(entity);
-
-    HttpResponse response = httpClient.execute(httpPost);
-    if (response.getStatusLine().toString().contains("204")) {
-      PluginState.imageUploaded();
-    }
-    file.delete();
-    MapillaryLayer.getInstance().updateHelpText();
-  }
-
-  /**
-   * Uploads the given {@link MapillarySequence}.
-   *
-   * @param sequence
-   *          The sequence to upload. It must contain only
-   *          {@link MapillaryImportedImage} objects.
-   */
-  public static void uploadSequence(MapillarySequence sequence) {
-    new SequenceUploadThread(sequence.getImages()).start();
-  }
-
-  private static class SequenceUploadThread extends Thread {
-    private List<MapillaryAbstractImage> images;
-    private UUID uuid;
-    ThreadPoolExecutor ex;
-
-    private SequenceUploadThread(List<MapillaryAbstractImage> images) {
-      this.images = images;
-      this.uuid = UUID.randomUUID();
-      this.ex = new ThreadPoolExecutor(3, 5, 25, TimeUnit.SECONDS,
-          new ArrayBlockingQueue<Runnable>(5));
-    }
-
-    @Override
-    public void run() {
-      PluginState.startUpload();
-      PluginState.imagesToUpload(this.images.size());
-      MapillaryLayer.getInstance().updateHelpText();
-      for (MapillaryAbstractImage img : this.images) {
-        if (!(img instanceof MapillaryImportedImage))
-          throw new IllegalArgumentException(
-              "The sequence contains downloaded images.");
-        this.ex.execute(new SingleUploadThread((MapillaryImportedImage) img,
-            this.uuid));
-      }
-      this.ex.shutdown();
-      PluginState.finishUpload();
-    }
-  }
-
-  private static class SingleUploadThread extends Thread {
-
-    private MapillaryImportedImage image;
-    private UUID uuid;
-
-    private SingleUploadThread(MapillaryImportedImage image, UUID uuid) {
-      this.image = image;
-      this.uuid = uuid;
-    }
-
-    @Override
-    public void run() {
-      try {
-        OAuthUtils.upload(this.image, this.uuid);
-      } catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
-        Main.error(e);
-      }
-    }
-  }
-
-  /**
-   * Returns a file containing the picture and an updated version of the EXIF
-   * tags.
-   *
-   * @param image
-   * @return A File object containing the picture and an updated version of the
-   *         EXIF tags.
-   * @throws ImageReadException
-   * @throws IOException
-   * @throws ImageWriteException
-   */
-  public static File updateFile(MapillaryImportedImage image)
-      throws ImageReadException, IOException, ImageWriteException {
-    TiffOutputSet outputSet = null;
-    TiffOutputDirectory exifDirectory = null;
-    TiffOutputDirectory gpsDirectory = null;
-    // If the image is imported, loads the rest of the EXIF data.
-    ImageMetadata metadata = Imaging.getMetadata(image.getFile());
-    final JpegImageMetadata jpegMetadata = (JpegImageMetadata) metadata;
-    if (null != jpegMetadata) {
-      final TiffImageMetadata exif = jpegMetadata.getExif();
-      if (null != exif) {
-        outputSet = exif.getOutputSet();
-      }
-    }
-    if (null == outputSet) {
-      outputSet = new TiffOutputSet();
-    }
-    gpsDirectory = outputSet.getOrCreateGPSDirectory();
-    exifDirectory = outputSet.getOrCreateExifDirectory();
-
-    gpsDirectory.removeField(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION_REF);
-    gpsDirectory.add(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION_REF,
-        GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION_REF_VALUE_TRUE_NORTH);
-
-    gpsDirectory.removeField(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION);
-    gpsDirectory.add(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION,
-        RationalNumber.valueOf(image.getCa()));
-
-    exifDirectory.removeField(ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL);
-    exifDirectory.add(ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL,
-        ((MapillaryImportedImage) image).getDate("yyyy/MM/dd hh:mm:ss"));
-
-    outputSet.setGPSInDegrees(image.getLatLon().lon(), image.getLatLon().lat());
-    File tempFile = new File(c + ".tmp");
-    c++;
-    OutputStream os = new BufferedOutputStream(new FileOutputStream(tempFile));
-
-    // Transforms the image into a byte array.
-    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-    ImageIO.write(image.getImage(), "jpg", outputStream);
-    byte[] imageBytes = outputStream.toByteArray();
-
-    new ExifRewriter().updateExifMetadataLossless(imageBytes, os, outputSet);
-
-    return tempFile;
-  }
 }
Index: applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/oauth/UploadUtils.java
===================================================================
--- applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/oauth/UploadUtils.java	(revision 31456)
+++ applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/oauth/UploadUtils.java	(revision 31456)
@@ -0,0 +1,264 @@
+package org.openstreetmap.josm.plugins.mapillary.oauth;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import javax.imageio.ImageIO;
+
+import org.apache.commons.imaging.ImageReadException;
+import org.apache.commons.imaging.ImageWriteException;
+import org.apache.commons.imaging.Imaging;
+import org.apache.commons.imaging.common.ImageMetadata;
+import org.apache.commons.imaging.common.RationalNumber;
+import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata;
+import org.apache.commons.imaging.formats.jpeg.exif.ExifRewriter;
+import org.apache.commons.imaging.formats.tiff.TiffImageMetadata;
+import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
+import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants;
+import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory;
+import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.entity.mime.content.FileBody;
+import org.apache.http.entity.mime.content.StringBody;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
+import org.openstreetmap.josm.plugins.mapillary.MapillaryImportedImage;
+import org.openstreetmap.josm.plugins.mapillary.MapillarySequence;
+import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryUtils;
+import org.openstreetmap.josm.plugins.mapillary.utils.PluginState;
+
+/**
+ * Upload utilities.
+ *
+ * @author nokutu
+ *
+ */
+public class UploadUtils {
+
+  private static final String[] keys = { "key", "AWSAccessKeyId", "acl",
+      "policy", "signature", "Content-Type" };
+  private static final String UPLOAD_URL = "https://s3-eu-west-1.amazonaws.com/mapillary.uploads.manual.images";
+  /** Count to name temporal files. */
+  private static int c = 0;
+
+  /**
+   * Uploads the given MapillaryImportedImage object.
+   *
+   * @param image
+   *
+   * @throws NoSuchAlgorithmException
+   * @throws UnsupportedEncodingException
+   * @throws InvalidKeyException
+   *
+   */
+  public static void upload(MapillaryImportedImage image)
+      throws InvalidKeyException, UnsupportedEncodingException,
+      NoSuchAlgorithmException {
+    try {
+      upload(image, UUID.randomUUID());
+    } catch (IOException e) {
+      Main.error(e);
+    }
+  }
+
+  /**
+   * @param image
+   * @param uuid
+   *          The UUID used to create the sequence.
+   * @throws NoSuchAlgorithmException
+   * @throws InvalidKeyException
+   * @throws IOException
+   */
+  public static void upload(MapillaryImportedImage image, UUID uuid)
+      throws NoSuchAlgorithmException, InvalidKeyException, IOException {
+    String key = MapillaryUser.getUsername() + "/" + uuid.toString() + "/"
+        + image.getLatLon().lat() + "_" + image.getLatLon().lon() + "_"
+        + image.getCa() + "_" + image.datetimeOriginal + ".jpg";
+
+    String policy = null;
+    String signature = null;
+    policy = MapillaryUser.getSecrets().get("images_policy");
+    signature = MapillaryUser.getSecrets().get("images_hash");
+
+    HashMap<String, String> hash = new HashMap<>();
+    hash.put("key", key);
+    hash.put("AWSAccessKeyId", "AKIAI2X3BJAT2W75HILA");
+    hash.put("acl", "private");
+    hash.put("policy", policy);
+    hash.put("signature", signature);
+    hash.put("Content-Type", "image/jpeg");
+
+    try {
+      uploadFile(updateFile(image), hash);
+    } catch (ImageReadException | ImageWriteException e) {
+      Main.error(e);
+    }
+
+  }
+
+  /**
+   * @param file
+   * @param hash
+   * @throws IOException
+   */
+  public static void uploadFile(File file, HashMap<String, String> hash)
+      throws IOException {
+    HttpClientBuilder builder = HttpClientBuilder.create();
+    HttpClient httpClient = builder.build();
+    HttpPost httpPost = new HttpPost(UPLOAD_URL);
+
+    MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create();
+    for (String key : keys) {
+      entityBuilder.addPart(key, new StringBody(hash.get(key),
+          ContentType.TEXT_PLAIN));
+    }
+    entityBuilder.addPart("file", new FileBody(file));
+
+    HttpEntity entity = entityBuilder.build();
+    httpPost.setEntity(entity);
+
+    HttpResponse response = httpClient.execute(httpPost);
+    if (response.getStatusLine().toString().contains("204")) {
+      PluginState.imageUploaded();
+    }
+    file.delete();
+    MapillaryUtils.updateHelpText();
+  }
+
+  /**
+   * Uploads the given {@link MapillarySequence}.
+   *
+   * @param sequence
+   *          The sequence to upload. It must contain only
+   *          {@link MapillaryImportedImage} objects.
+   */
+  public static void uploadSequence(MapillarySequence sequence) {
+    new SequenceUploadThread(sequence.getImages()).start();
+  }
+
+  private static class SequenceUploadThread extends Thread {
+    private List<MapillaryAbstractImage> images;
+    private UUID uuid;
+    ThreadPoolExecutor ex;
+
+    private SequenceUploadThread(List<MapillaryAbstractImage> images) {
+      this.images = images;
+      this.uuid = UUID.randomUUID();
+      this.ex = new ThreadPoolExecutor(3, 5, 25, TimeUnit.SECONDS,
+          new ArrayBlockingQueue<Runnable>(5));
+    }
+
+    @Override
+    public void run() {
+      PluginState.startUpload();
+      PluginState.imagesToUpload(this.images.size());
+      MapillaryUtils.updateHelpText();
+      for (MapillaryAbstractImage img : this.images) {
+        if (!(img instanceof MapillaryImportedImage))
+          throw new IllegalArgumentException(
+              "The sequence contains downloaded images.");
+        this.ex.execute(new SingleUploadThread((MapillaryImportedImage) img,
+            this.uuid));
+      }
+      this.ex.shutdown();
+      PluginState.finishUpload();
+    }
+  }
+
+  private static class SingleUploadThread extends Thread {
+
+    private MapillaryImportedImage image;
+    private UUID uuid;
+
+    private SingleUploadThread(MapillaryImportedImage image, UUID uuid) {
+      this.image = image;
+      this.uuid = uuid;
+    }
+
+    @Override
+    public void run() {
+      try {
+        upload(this.image, this.uuid);
+      } catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
+        Main.error(e);
+      }
+    }
+  }
+
+  /**
+   * Returns a file containing the picture and an updated version of the EXIF
+   * tags.
+   *
+   * @param image
+   * @return A File object containing the picture and an updated version of the
+   *         EXIF tags.
+   * @throws ImageReadException
+   * @throws IOException
+   * @throws ImageWriteException
+   */
+  public static File updateFile(MapillaryImportedImage image)
+      throws ImageReadException, IOException, ImageWriteException {
+    TiffOutputSet outputSet = null;
+    TiffOutputDirectory exifDirectory = null;
+    TiffOutputDirectory gpsDirectory = null;
+    // If the image is imported, loads the rest of the EXIF data.
+    ImageMetadata metadata = Imaging.getMetadata(image.getFile());
+    final JpegImageMetadata jpegMetadata = (JpegImageMetadata) metadata;
+    if (null != jpegMetadata) {
+      final TiffImageMetadata exif = jpegMetadata.getExif();
+      if (null != exif) {
+        outputSet = exif.getOutputSet();
+      }
+    }
+    if (null == outputSet) {
+      outputSet = new TiffOutputSet();
+    }
+    gpsDirectory = outputSet.getOrCreateGPSDirectory();
+    exifDirectory = outputSet.getOrCreateExifDirectory();
+
+    gpsDirectory.removeField(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION_REF);
+    gpsDirectory.add(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION_REF,
+        GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION_REF_VALUE_TRUE_NORTH);
+
+    gpsDirectory.removeField(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION);
+    gpsDirectory.add(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION,
+        RationalNumber.valueOf(image.getCa()));
+
+    exifDirectory.removeField(ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL);
+    exifDirectory.add(ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL,
+        ((MapillaryImportedImage) image).getDate("yyyy/MM/dd hh:mm:ss"));
+
+    outputSet.setGPSInDegrees(image.getLatLon().lon(), image.getLatLon().lat());
+    File tempFile = new File(c + ".tmp");
+    c++;
+    OutputStream os = new BufferedOutputStream(new FileOutputStream(tempFile));
+
+    // Transforms the image into a byte array.
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    ImageIO.write(image.getImage(), "jpg", outputStream);
+    byte[] imageBytes = outputStream.toByteArray();
+
+    new ExifRewriter().updateExifMetadataLossless(imageBytes, os, outputSet);
+
+    return tempFile;
+  }
+}
Index: applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryUtils.java
===================================================================
--- applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryUtils.java	(revision 31456)
+++ applications/editors/josm/plugins/mapillary/src/org/openstreetmap/josm/plugins/mapillary/utils/MapillaryUtils.java	(revision 31456)
@@ -0,0 +1,94 @@
+package org.openstreetmap.josm.plugins.mapillary.utils;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import org.apache.commons.imaging.common.RationalNumber;
+import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.plugins.mapillary.MapillaryLayer;
+
+/**
+ * Set of utilities.
+ *
+ * @author nokutu
+ *
+ */
+public class MapillaryUtils {
+  /**
+   * Updates the help text at the bottom of the window.
+   */
+  public static void updateHelpText() {
+    String ret = "";
+    if (PluginState.isDownloading())
+      ret += tr("Downloading");
+    else if (MapillaryLayer.getInstance().getData().size() > 0)
+      ret += tr("Total images: {0}", MapillaryLayer.getInstance().getData()
+          .size());
+    else
+      ret += tr("No images found");
+    if (MapillaryLayer.getInstance().mode != null)
+      ret += " -- " + tr(MapillaryLayer.getInstance().mode.toString());
+    if (PluginState.isUploading())
+      ret += " -- " + PluginState.getUploadString();
+    Main.map.statusLine.setHelpText(ret);
+  }
+
+  /**
+   * Calculates the decimal degree-value from a degree value given in
+   * degrees-minutes-seconds-format
+   *
+   * @param degMinSec
+   *          an array of length 3, the values in there are (in this order)
+   *          degrees, minutes and seconds
+   * @param ref
+   *          the latitude or longitude reference determining if the given value
+   *          is:
+   *          <ul>
+   *          <li>north (
+   *          {@link GpsTagConstants#GPS_TAG_GPS_LATITUDE_REF_VALUE_NORTH}) or
+   *          south (
+   *          {@link GpsTagConstants#GPS_TAG_GPS_LATITUDE_REF_VALUE_SOUTH}) of
+   *          the equator</li>
+   *          <li>east (
+   *          {@link GpsTagConstants#GPS_TAG_GPS_LONGITUDE_REF_VALUE_EAST}) or
+   *          west ({@link GpsTagConstants#GPS_TAG_GPS_LONGITUDE_REF_VALUE_WEST}
+   *          ) of the equator</li>
+   *          </ul>
+   * @return the decimal degree-value for the given input, negative when west of
+   *         0-meridian or south of equator, positive otherwise
+   * @throws IllegalArgumentException
+   *           if {@code degMinSec} doesn't have length 3 or if {@code ref} is
+   *           not one of the values mentioned above
+   */
+  // TODO: Maybe move into a separate utility class?
+  public static double degMinSecToDouble(RationalNumber[] degMinSec, String ref) {
+    if (degMinSec == null || degMinSec.length != 3) {
+      throw new IllegalArgumentException("Array's length must be 3.");
+    }
+    for (int i = 0; i < 3; i++)
+      if (degMinSec[i] == null)
+        throw new IllegalArgumentException("Null value in array.");
+
+    switch (ref) {
+      case GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF_VALUE_NORTH:
+      case GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF_VALUE_SOUTH:
+      case GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF_VALUE_EAST:
+      case GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF_VALUE_WEST:
+        break;
+      default:
+        throw new IllegalArgumentException("Invalid ref.");
+    }
+
+    double result = degMinSec[0].doubleValue(); // degrees
+    result += degMinSec[1].doubleValue() / 60; // minutes
+    result += degMinSec[2].doubleValue() / 3600; // seconds
+
+    if (GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF_VALUE_SOUTH.equals(ref)
+        || GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF_VALUE_WEST.equals(ref)) {
+      result *= -1;
+    }
+
+    result = 360 * ((result + 180) / 360 - Math.floor((result + 180) / 360)) - 180;
+    return result;
+  }
+}
