Index: trunk/src/org/openstreetmap/josm/data/osm/visitor/paint/ComputeStyleListWorker.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/visitor/paint/ComputeStyleListWorker.java	(revision 11914)
+++ trunk/src/org/openstreetmap/josm/data/osm/visitor/paint/ComputeStyleListWorker.java	(revision 11914)
@@ -0,0 +1,158 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.osm.visitor.paint;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.ForkJoinTask;
+import java.util.concurrent.RecursiveTask;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.osm.Changeset;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.visitor.Visitor;
+import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer.StyleRecord;
+import org.openstreetmap.josm.gui.NavigatableComponent;
+import org.openstreetmap.josm.gui.mappaint.ElemStyles;
+import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
+import org.openstreetmap.josm.gui.mappaint.StyleElementList;
+import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
+import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement;
+import org.openstreetmap.josm.gui.mappaint.styleelement.AreaIconElement;
+import org.openstreetmap.josm.gui.mappaint.styleelement.NodeElement;
+import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement;
+import org.openstreetmap.josm.gui.mappaint.styleelement.TextElement;
+import org.openstreetmap.josm.tools.JosmRuntimeException;
+import org.openstreetmap.josm.tools.bugreport.BugReport;
+
+/**
+ * Helper to cumpute style list.
+ * @since 11914 (extracted from StyledMapRenderer)
+ */
+public class ComputeStyleListWorker extends RecursiveTask<List<StyleRecord>> implements Visitor {
+    private final transient List<? extends OsmPrimitive> input;
+    private final transient List<StyleRecord> output;
+
+    private final transient ElemStyles styles = MapPaintStyles.getStyles();
+    private final int directExecutionTaskSize;
+    private final double circum;
+    private final NavigatableComponent nc;
+
+    private final boolean drawArea;
+    private final boolean drawMultipolygon;
+    private final boolean drawRestriction;
+
+    /**
+     * Constructs a new {@code ComputeStyleListWorker}.
+     * @param circum distance on the map in meters that 100 screen pixels represent
+     * @param nc navigatable component
+     * @param input the primitives to process
+     * @param output the list of styles to which styles will be added
+     * @param directExecutionTaskSize the threshold deciding whether to subdivide the tasks
+     */
+    ComputeStyleListWorker(double circum, NavigatableComponent nc,
+            final List<? extends OsmPrimitive> input, List<StyleRecord> output, int directExecutionTaskSize) {
+        this.circum = circum;
+        this.nc = nc;
+        this.input = input;
+        this.output = output;
+        this.directExecutionTaskSize = directExecutionTaskSize;
+        this.drawArea = circum <= Main.pref.getInteger("mappaint.fillareas", 10_000_000);
+        this.drawMultipolygon = drawArea && Main.pref.getBoolean("mappaint.multipolygon", true);
+        this.drawRestriction = Main.pref.getBoolean("mappaint.restriction", true);
+        this.styles.setDrawMultipolygon(drawMultipolygon);
+    }
+
+    @Override
+    protected List<StyleRecord> compute() {
+        if (input.size() <= directExecutionTaskSize) {
+            return computeDirectly();
+        } else {
+            final Collection<ForkJoinTask<List<StyleRecord>>> tasks = new ArrayList<>();
+            for (int fromIndex = 0; fromIndex < input.size(); fromIndex += directExecutionTaskSize) {
+                final int toIndex = Math.min(fromIndex + directExecutionTaskSize, input.size());
+                tasks.add(new ComputeStyleListWorker(circum, nc, input.subList(fromIndex, toIndex),
+                        new ArrayList<>(directExecutionTaskSize), directExecutionTaskSize).fork());
+            }
+            for (ForkJoinTask<List<StyleRecord>> task : tasks) {
+                output.addAll(task.join());
+            }
+            return output;
+        }
+    }
+
+    public List<StyleRecord> computeDirectly() {
+        MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
+        try {
+            for (final OsmPrimitive osm : input) {
+                acceptDrawable(osm);
+            }
+            return output;
+        } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
+            throw BugReport.intercept(e).put("input-size", input.size()).put("output-size", output.size());
+        } finally {
+            MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
+        }
+    }
+
+    private void acceptDrawable(final OsmPrimitive osm) {
+        try {
+            if (osm.isDrawable()) {
+                osm.accept(this);
+            }
+        } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
+            throw BugReport.intercept(e).put("osm", osm);
+        }
+    }
+
+    @Override
+    public void visit(Node n) {
+        add(n, StyledMapRenderer.computeFlags(n, false));
+    }
+
+    @Override
+    public void visit(Way w) {
+        add(w, StyledMapRenderer.computeFlags(w, true));
+    }
+
+    @Override
+    public void visit(Relation r) {
+        add(r, StyledMapRenderer.computeFlags(r, true));
+    }
+
+    @Override
+    public void visit(Changeset cs) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void add(Node osm, int flags) {
+        StyleElementList sl = styles.get(osm, circum, nc);
+        for (StyleElement s : sl) {
+            output.add(new StyleRecord(s, osm, flags));
+        }
+    }
+
+    public void add(Relation osm, int flags) {
+        StyleElementList sl = styles.get(osm, circum, nc);
+        for (StyleElement s : sl) {
+            if (drawMultipolygon && drawArea && (s instanceof AreaElement || s instanceof AreaIconElement)
+                    && (flags & StyledMapRenderer.FLAG_DISABLED) == 0) {
+                output.add(new StyleRecord(s, osm, flags));
+            } else if ((drawMultipolygon && drawArea && s instanceof TextElement) || (drawRestriction && s instanceof NodeElement)) {
+                output.add(new StyleRecord(s, osm, flags));
+            }
+        }
+    }
+
+    public void add(Way osm, int flags) {
+        StyleElementList sl = styles.get(osm, circum, nc);
+        for (StyleElement s : sl) {
+            if ((drawArea && (flags & StyledMapRenderer.FLAG_DISABLED) == 0) || !(s instanceof AreaElement)) {
+                output.add(new StyleRecord(s, osm, flags));
+            }
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java	(revision 11913)
+++ trunk/src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java	(revision 11914)
@@ -35,6 +35,4 @@
 import java.util.Optional;
 import java.util.concurrent.ForkJoinPool;
-import java.util.concurrent.ForkJoinTask;
-import java.util.concurrent.RecursiveTask;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
@@ -48,5 +46,4 @@
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.osm.BBox;
-import org.openstreetmap.josm.data.osm.Changeset;
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.Node;
@@ -57,5 +54,4 @@
 import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.osm.WaySegment;
-import org.openstreetmap.josm.data.osm.visitor.Visitor;
 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
@@ -69,10 +65,4 @@
 import org.openstreetmap.josm.gui.draw.MapViewPath;
 import org.openstreetmap.josm.gui.draw.MapViewPositionAndRotation;
-import org.openstreetmap.josm.gui.mappaint.ElemStyles;
-import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
-import org.openstreetmap.josm.gui.mappaint.StyleElementList;
-import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
-import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement;
-import org.openstreetmap.josm.gui.mappaint.styleelement.AreaIconElement;
 import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement;
 import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement.HorizontalTextAlignment;
@@ -83,5 +73,4 @@
 import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement;
 import org.openstreetmap.josm.gui.mappaint.styleelement.Symbol;
-import org.openstreetmap.josm.gui.mappaint.styleelement.TextElement;
 import org.openstreetmap.josm.gui.mappaint.styleelement.TextLabel;
 import org.openstreetmap.josm.gui.mappaint.styleelement.placement.PositionForAreaStrategy;
@@ -247,21 +236,21 @@
      * Not used in any public interfaces.
      */
-    private static final int FLAG_NORMAL = 0;
+    static final int FLAG_NORMAL = 0;
     /**
      * A primitive with {@link OsmPrimitive#isDisabled()}
      */
-    private static final int FLAG_DISABLED = 1;
+    static final int FLAG_DISABLED = 1;
     /**
      * A primitive with {@link OsmPrimitive#isMemberOfSelected()}
      */
-    private static final int FLAG_MEMBER_OF_SELECTED = 2;
+    static final int FLAG_MEMBER_OF_SELECTED = 2;
     /**
      * A primitive with {@link OsmPrimitive#isSelected()}
      */
-    private static final int FLAG_SELECTED = 4;
+    static final int FLAG_SELECTED = 4;
     /**
      * A primitive with {@link OsmPrimitive#isOuterMemberOfSelected()}
      */
-    private static final int FLAG_OUTERMEMBER_OF_SELECTED = 8;
+    static final int FLAG_OUTERMEMBER_OF_SELECTED = 8;
 
     private static final double PHI = Math.toRadians(20);
@@ -1495,130 +1484,4 @@
     }
 
-    private static class ComputeStyleListWorker extends RecursiveTask<List<StyleRecord>> implements Visitor {
-        private final transient List<? extends OsmPrimitive> input;
-        private final transient List<StyleRecord> output;
-
-        private final transient ElemStyles styles = MapPaintStyles.getStyles();
-        private final int directExecutionTaskSize;
-        private final double circum;
-        private final NavigatableComponent nc;
-
-        private final boolean drawArea;
-        private final boolean drawMultipolygon;
-        private final boolean drawRestriction;
-
-        /**
-         * Constructs a new {@code ComputeStyleListWorker}.
-         * @param circum distance on the map in meters that 100 screen pixels represent
-         * @param nc navigatable component
-         * @param input the primitives to process
-         * @param output the list of styles to which styles will be added
-         * @param directExecutionTaskSize the threshold deciding whether to subdivide the tasks
-         */
-        ComputeStyleListWorker(double circum, NavigatableComponent nc,
-                final List<? extends OsmPrimitive> input, List<StyleRecord> output, int directExecutionTaskSize) {
-            this.circum = circum;
-            this.nc = nc;
-            this.input = input;
-            this.output = output;
-            this.directExecutionTaskSize = directExecutionTaskSize;
-            this.drawArea = circum <= Main.pref.getInteger("mappaint.fillareas", 10_000_000);
-            this.drawMultipolygon = drawArea && Main.pref.getBoolean("mappaint.multipolygon", true);
-            this.drawRestriction = Main.pref.getBoolean("mappaint.restriction", true);
-            this.styles.setDrawMultipolygon(drawMultipolygon);
-        }
-
-        @Override
-        protected List<StyleRecord> compute() {
-            if (input.size() <= directExecutionTaskSize) {
-                return computeDirectly();
-            } else {
-                final Collection<ForkJoinTask<List<StyleRecord>>> tasks = new ArrayList<>();
-                for (int fromIndex = 0; fromIndex < input.size(); fromIndex += directExecutionTaskSize) {
-                    final int toIndex = Math.min(fromIndex + directExecutionTaskSize, input.size());
-                    final List<StyleRecord> output = new ArrayList<>(directExecutionTaskSize);
-                    tasks.add(new ComputeStyleListWorker(circum, nc, input.subList(fromIndex, toIndex), output, directExecutionTaskSize).fork());
-                }
-                for (ForkJoinTask<List<StyleRecord>> task : tasks) {
-                    output.addAll(task.join());
-                }
-                return output;
-            }
-        }
-
-        public List<StyleRecord> computeDirectly() {
-            MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
-            try {
-                for (final OsmPrimitive osm : input) {
-                    acceptDrawable(osm);
-                }
-                return output;
-            } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
-                throw BugReport.intercept(e).put("input-size", input.size()).put("output-size", output.size());
-            } finally {
-                MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
-            }
-        }
-
-        private void acceptDrawable(final OsmPrimitive osm) {
-            try {
-                if (osm.isDrawable()) {
-                    osm.accept(this);
-                }
-            } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
-                throw BugReport.intercept(e).put("osm", osm);
-            }
-        }
-
-        @Override
-        public void visit(Node n) {
-            add(n, computeFlags(n, false));
-        }
-
-        @Override
-        public void visit(Way w) {
-            add(w, computeFlags(w, true));
-        }
-
-        @Override
-        public void visit(Relation r) {
-            add(r, computeFlags(r, true));
-        }
-
-        @Override
-        public void visit(Changeset cs) {
-            throw new UnsupportedOperationException();
-        }
-
-        public void add(Node osm, int flags) {
-            StyleElementList sl = styles.get(osm, circum, nc);
-            for (StyleElement s : sl) {
-                output.add(new StyleRecord(s, osm, flags));
-            }
-        }
-
-        public void add(Relation osm, int flags) {
-            StyleElementList sl = styles.get(osm, circum, nc);
-            for (StyleElement s : sl) {
-                if (drawMultipolygon && drawArea && (s instanceof AreaElement || s instanceof AreaIconElement) && (flags & FLAG_DISABLED) == 0) {
-                    output.add(new StyleRecord(s, osm, flags));
-                } else if (drawMultipolygon && drawArea && s instanceof TextElement) {
-                    output.add(new StyleRecord(s, osm, flags));
-                } else if (drawRestriction && s instanceof NodeElement) {
-                    output.add(new StyleRecord(s, osm, flags));
-                }
-            }
-        }
-
-        public void add(Way osm, int flags) {
-            StyleElementList sl = styles.get(osm, circum, nc);
-            for (StyleElement s : sl) {
-                if ((drawArea && (flags & FLAG_DISABLED) == 0) || !(s instanceof AreaElement)) {
-                    output.add(new StyleRecord(s, osm, flags));
-                }
-            }
-        }
-    }
-
     /**
      * Sets the factory that creates the benchmark data receivers.
Index: trunk/src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java	(revision 11913)
+++ trunk/src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java	(revision 11914)
@@ -35,6 +35,4 @@
 import java.util.Hashtable;
 import java.util.List;
-import java.util.Locale;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.TimeZone;
@@ -1251,215 +1249,3 @@
         return endIndex;
     }
-
-    static final class Timezone {
-
-        static final Timezone ZERO = new Timezone(0.0);
-        private final double timezone;
-
-        Timezone(double hours) {
-            this.timezone = hours;
-        }
-
-        public double getHours() {
-            return timezone;
-        }
-
-        String formatTimezone() {
-            StringBuilder ret = new StringBuilder();
-
-            double timezone = this.timezone;
-            if (timezone < 0) {
-                ret.append('-');
-                timezone = -timezone;
-            } else {
-                ret.append('+');
-            }
-            ret.append((long) timezone).append(':');
-            int minutes = (int) ((timezone % 1) * 60);
-            if (minutes < 10) {
-                ret.append('0');
-            }
-            ret.append(minutes);
-
-            return ret.toString();
-        }
-
-        static Timezone parseTimezone(String timezone) throws ParseException {
-
-            if (timezone.isEmpty())
-                return ZERO;
-
-            String error = tr("Error while parsing timezone.\nExpected format: {0}", "+H:MM");
-
-            char sgnTimezone = '+';
-            StringBuilder hTimezone = new StringBuilder();
-            StringBuilder mTimezone = new StringBuilder();
-            int state = 1; // 1=start/sign, 2=hours, 3=minutes.
-            for (int i = 0; i < timezone.length(); i++) {
-                char c = timezone.charAt(i);
-                switch (c) {
-                    case ' ':
-                        if (state != 2 || hTimezone.length() != 0)
-                            throw new ParseException(error, i);
-                        break;
-                    case '+':
-                    case '-':
-                        if (state == 1) {
-                            sgnTimezone = c;
-                            state = 2;
-                        } else
-                            throw new ParseException(error, i);
-                        break;
-                    case ':':
-                    case '.':
-                        if (state == 2) {
-                            state = 3;
-                        } else
-                            throw new ParseException(error, i);
-                        break;
-                    case '0':
-                    case '1':
-                    case '2':
-                    case '3':
-                    case '4':
-                    case '5':
-                    case '6':
-                    case '7':
-                    case '8':
-                    case '9':
-                        switch (state) {
-                            case 1:
-                            case 2:
-                                state = 2;
-                                hTimezone.append(c);
-                                break;
-                            case 3:
-                                mTimezone.append(c);
-                                break;
-                            default:
-                                throw new ParseException(error, i);
-                        }
-                        break;
-                    default:
-                        throw new ParseException(error, i);
-                }
-            }
-
-            int h = 0;
-            int m = 0;
-            try {
-                h = Integer.parseInt(hTimezone.toString());
-                if (mTimezone.length() > 0) {
-                    m = Integer.parseInt(mTimezone.toString());
-                }
-            } catch (NumberFormatException nfe) {
-                // Invalid timezone
-                throw (ParseException) new ParseException(error, 0).initCause(nfe);
-            }
-
-            if (h > 12 || m > 59)
-                throw new ParseException(error, 0);
-            else
-                return new Timezone((h + m / 60.0) * (sgnTimezone == '-' ? -1 : 1));
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) return true;
-            if (!(o instanceof Timezone)) return false;
-            Timezone timezone1 = (Timezone) o;
-            return Double.compare(timezone1.timezone, timezone) == 0;
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(timezone);
-        }
-    }
-
-    static final class Offset {
-
-        static final Offset ZERO = new Offset(0);
-        private final long milliseconds;
-
-        private Offset(long milliseconds) {
-            this.milliseconds = milliseconds;
-        }
-
-        static Offset milliseconds(long milliseconds) {
-            return new Offset(milliseconds);
-        }
-
-        static Offset seconds(long seconds) {
-            return new Offset(1000 * seconds);
-        }
-
-        long getMilliseconds() {
-            return milliseconds;
-        }
-
-        long getSeconds() {
-            return milliseconds / 1000;
-        }
-
-        String formatOffset() {
-            if (milliseconds % 1000 == 0) {
-                return Long.toString(milliseconds / 1000);
-            } else if (milliseconds % 100 == 0) {
-                return String.format(Locale.ENGLISH, "%.1f", milliseconds / 1000.);
-            } else {
-                return String.format(Locale.ENGLISH, "%.3f", milliseconds / 1000.);
-            }
-        }
-
-        static Offset parseOffset(String offset) throws ParseException {
-            String error = tr("Error while parsing offset.\nExpected format: {0}", "number");
-
-            if (!offset.isEmpty()) {
-                try {
-                    if (offset.startsWith("+")) {
-                        offset = offset.substring(1);
-                    }
-                    return Offset.milliseconds(Math.round(Double.parseDouble(offset) * 1000));
-                } catch (NumberFormatException nfe) {
-                    throw (ParseException) new ParseException(error, 0).initCause(nfe);
-                }
-            } else {
-                return Offset.ZERO;
-            }
-        }
-
-        int getDayOffset() {
-            // Find day difference
-            return (int) Math.round(((double) getMilliseconds()) / TimeUnit.DAYS.toMillis(1));
-        }
-
-        Offset withoutDayOffset() {
-            return milliseconds(getMilliseconds() - TimeUnit.DAYS.toMillis(getDayOffset()));
-        }
-
-        Pair<Timezone, Offset> splitOutTimezone() {
-            // In hours
-            final double tz = ((double) withoutDayOffset().getSeconds()) / TimeUnit.HOURS.toSeconds(1);
-
-            // Due to imprecise clocks we might get a "+3:28" timezone, which should obviously be 3:30 with
-            // -2 minutes offset. This determines the real timezone and finds offset.
-            final double timezone = (double) Math.round(tz * 2) / 2; // hours, rounded to one decimal place
-            final long delta = Math.round(getMilliseconds() - timezone * TimeUnit.HOURS.toMillis(1));
-            return Pair.create(new Timezone(timezone), Offset.milliseconds(delta));
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) return true;
-            if (!(o instanceof Offset)) return false;
-            Offset offset = (Offset) o;
-            return milliseconds == offset.milliseconds;
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(milliseconds);
-        }
-    }
 }
Index: trunk/src/org/openstreetmap/josm/gui/layer/geoimage/Offset.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/geoimage/Offset.java	(revision 11914)
+++ trunk/src/org/openstreetmap/josm/gui/layer/geoimage/Offset.java	(revision 11914)
@@ -0,0 +1,97 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.layer.geoimage;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.text.ParseException;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+import org.openstreetmap.josm.tools.Pair;
+
+public final class Offset {
+
+    static final Offset ZERO = new Offset(0);
+    private final long milliseconds;
+
+    private Offset(long milliseconds) {
+        this.milliseconds = milliseconds;
+    }
+
+    static Offset milliseconds(long milliseconds) {
+        return new Offset(milliseconds);
+    }
+
+    static Offset seconds(long seconds) {
+        return new Offset(1000 * seconds);
+    }
+
+    long getMilliseconds() {
+        return milliseconds;
+    }
+
+    long getSeconds() {
+        return milliseconds / 1000;
+    }
+
+    String formatOffset() {
+        if (milliseconds % 1000 == 0) {
+            return Long.toString(milliseconds / 1000);
+        } else if (milliseconds % 100 == 0) {
+            return String.format(Locale.ENGLISH, "%.1f", milliseconds / 1000.);
+        } else {
+            return String.format(Locale.ENGLISH, "%.3f", milliseconds / 1000.);
+        }
+    }
+
+    static Offset parseOffset(String offset) throws ParseException {
+        String error = tr("Error while parsing offset.\nExpected format: {0}", "number");
+
+        if (!offset.isEmpty()) {
+            try {
+                if (offset.startsWith("+")) {
+                    offset = offset.substring(1);
+                }
+                return Offset.milliseconds(Math.round(Double.parseDouble(offset) * 1000));
+            } catch (NumberFormatException nfe) {
+                throw (ParseException) new ParseException(error, 0).initCause(nfe);
+            }
+        } else {
+            return Offset.ZERO;
+        }
+    }
+
+    int getDayOffset() {
+        // Find day difference
+        return (int) Math.round(((double) getMilliseconds()) / TimeUnit.DAYS.toMillis(1));
+    }
+
+    Offset withoutDayOffset() {
+        return milliseconds(getMilliseconds() - TimeUnit.DAYS.toMillis(getDayOffset()));
+    }
+
+    Pair<Timezone, Offset> splitOutTimezone() {
+        // In hours
+        final double tz = ((double) withoutDayOffset().getSeconds()) / TimeUnit.HOURS.toSeconds(1);
+
+        // Due to imprecise clocks we might get a "+3:28" timezone, which should obviously be 3:30 with
+        // -2 minutes offset. This determines the real timezone and finds offset.
+        final double timezone = (double) Math.round(tz * 2) / 2; // hours, rounded to one decimal place
+        final long delta = Math.round(getMilliseconds() - timezone * TimeUnit.HOURS.toMillis(1));
+        return Pair.create(new Timezone(timezone), Offset.milliseconds(delta));
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof Offset)) return false;
+        Offset offset = (Offset) o;
+        return milliseconds == offset.milliseconds;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(milliseconds);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/layer/geoimage/Timezone.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/geoimage/Timezone.java	(revision 11914)
+++ trunk/src/org/openstreetmap/josm/gui/layer/geoimage/Timezone.java	(revision 11914)
@@ -0,0 +1,133 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.layer.geoimage;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.text.ParseException;
+import java.util.Objects;
+
+public final class Timezone {
+
+    static final Timezone ZERO = new Timezone(0.0);
+    private final double timezone;
+
+    Timezone(double hours) {
+        this.timezone = hours;
+    }
+
+    public double getHours() {
+        return timezone;
+    }
+
+    String formatTimezone() {
+        StringBuilder ret = new StringBuilder();
+
+        double timezone = this.timezone;
+        if (timezone < 0) {
+            ret.append('-');
+            timezone = -timezone;
+        } else {
+            ret.append('+');
+        }
+        ret.append((long) timezone).append(':');
+        int minutes = (int) ((timezone % 1) * 60);
+        if (minutes < 10) {
+            ret.append('0');
+        }
+        ret.append(minutes);
+
+        return ret.toString();
+    }
+
+    static Timezone parseTimezone(String timezone) throws ParseException {
+
+        if (timezone.isEmpty())
+            return ZERO;
+
+        String error = tr("Error while parsing timezone.\nExpected format: {0}", "+H:MM");
+
+        char sgnTimezone = '+';
+        StringBuilder hTimezone = new StringBuilder();
+        StringBuilder mTimezone = new StringBuilder();
+        int state = 1; // 1=start/sign, 2=hours, 3=minutes.
+        for (int i = 0; i < timezone.length(); i++) {
+            char c = timezone.charAt(i);
+            switch (c) {
+                case ' ':
+                    if (state != 2 || hTimezone.length() != 0)
+                        throw new ParseException(error, i);
+                    break;
+                case '+':
+                case '-':
+                    if (state == 1) {
+                        sgnTimezone = c;
+                        state = 2;
+                    } else
+                        throw new ParseException(error, i);
+                    break;
+                case ':':
+                case '.':
+                    if (state == 2) {
+                        state = 3;
+                    } else
+                        throw new ParseException(error, i);
+                    break;
+                case '0':
+                case '1':
+                case '2':
+                case '3':
+                case '4':
+                case '5':
+                case '6':
+                case '7':
+                case '8':
+                case '9':
+                    switch (state) {
+                        case 1:
+                        case 2:
+                            state = 2;
+                            hTimezone.append(c);
+                            break;
+                        case 3:
+                            mTimezone.append(c);
+                            break;
+                        default:
+                            throw new ParseException(error, i);
+                    }
+                    break;
+                default:
+                    throw new ParseException(error, i);
+            }
+        }
+
+        int h = 0;
+        int m = 0;
+        try {
+            h = Integer.parseInt(hTimezone.toString());
+            if (mTimezone.length() > 0) {
+                m = Integer.parseInt(mTimezone.toString());
+            }
+        } catch (NumberFormatException nfe) {
+            // Invalid timezone
+            throw (ParseException) new ParseException(error, 0).initCause(nfe);
+        }
+
+        if (h > 12 || m > 59)
+            throw new ParseException(error, 0);
+        else
+            return new Timezone((h + m / 60.0) * (sgnTimezone == '-' ? -1 : 1));
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof Timezone)) return false;
+        Timezone timezone1 = (Timezone) o;
+        return Double.compare(timezone1.timezone, timezone) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(timezone);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/layer/imagery/ColorfulFilter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/imagery/ColorfulFilter.java	(revision 11914)
+++ trunk/src/org/openstreetmap/josm/gui/layer/imagery/ColorfulFilter.java	(revision 11914)
@@ -0,0 +1,136 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.layer.imagery;
+
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.BufferedImageOp;
+import java.awt.image.ColorModel;
+import java.awt.image.DataBuffer;
+import java.awt.image.DataBufferByte;
+import java.util.Optional;
+
+import org.openstreetmap.josm.Main;
+
+/**
+ * Colorful filter.
+ * @since 11914 (extracted from ColorfulImageProcessor)
+ */
+public class ColorfulFilter implements BufferedImageOp {
+    private final double colorfulness;
+
+    /**
+     * Create a new colorful filter.
+     * @param colorfulness The colorfulness as defined in the {@link ColorfulImageProcessor} class.
+     */
+    ColorfulFilter(double colorfulness) {
+        this.colorfulness = colorfulness;
+    }
+
+    @Override
+    public BufferedImage filter(BufferedImage src, BufferedImage dst) {
+        if (src.getWidth() == 0 || src.getHeight() == 0) {
+            return src;
+        }
+
+        BufferedImage dest = Optional.ofNullable(dst).orElseGet(() -> createCompatibleDestImage(src, null));
+        DataBuffer srcBuffer = src.getRaster().getDataBuffer();
+        DataBuffer destBuffer = dest.getRaster().getDataBuffer();
+        if (!(srcBuffer instanceof DataBufferByte) || !(destBuffer instanceof DataBufferByte)) {
+            Main.trace("Cannot apply color filter: Images do not use DataBufferByte.");
+            return src;
+        }
+
+        int type = src.getType();
+        if (type != dest.getType()) {
+            Main.trace("Cannot apply color filter: Src / Dest differ in type (" + type + '/' + dest.getType() + ')');
+            return src;
+        }
+        int redOffset;
+        int greenOffset;
+        int blueOffset;
+        int alphaOffset = 0;
+        switch (type) {
+        case BufferedImage.TYPE_3BYTE_BGR:
+            blueOffset = 0;
+            greenOffset = 1;
+            redOffset = 2;
+            break;
+        case BufferedImage.TYPE_4BYTE_ABGR:
+        case BufferedImage.TYPE_4BYTE_ABGR_PRE:
+            blueOffset = 1;
+            greenOffset = 2;
+            redOffset = 3;
+            break;
+        case BufferedImage.TYPE_INT_ARGB:
+        case BufferedImage.TYPE_INT_ARGB_PRE:
+            redOffset = 0;
+            greenOffset = 1;
+            blueOffset = 2;
+            alphaOffset = 3;
+            break;
+        default:
+            Main.trace("Cannot apply color filter: Source image is of wrong type (" + type + ").");
+            return src;
+        }
+        doFilter((DataBufferByte) srcBuffer, (DataBufferByte) destBuffer, redOffset, greenOffset, blueOffset,
+                alphaOffset, src.getAlphaRaster() != null);
+        return dest;
+    }
+
+    private void doFilter(DataBufferByte src, DataBufferByte dest, int redOffset, int greenOffset, int blueOffset,
+            int alphaOffset, boolean hasAlpha) {
+        byte[] srcPixels = src.getData();
+        byte[] destPixels = dest.getData();
+        if (srcPixels.length != destPixels.length) {
+            Main.trace("Cannot apply color filter: Source/Dest lengths differ.");
+            return;
+        }
+        int entries = hasAlpha ? 4 : 3;
+        for (int i = 0; i < srcPixels.length; i += entries) {
+            int r = srcPixels[i + redOffset] & 0xff;
+            int g = srcPixels[i + greenOffset] & 0xff;
+            int b = srcPixels[i + blueOffset] & 0xff;
+            double luminosity = r * .21d + g * .72d + b * .07d;
+            destPixels[i + redOffset] = mix(r, luminosity);
+            destPixels[i + greenOffset] = mix(g, luminosity);
+            destPixels[i + blueOffset] = mix(b, luminosity);
+            if (hasAlpha) {
+                destPixels[i + alphaOffset] = srcPixels[i + alphaOffset];
+            }
+        }
+    }
+
+    private byte mix(int color, double luminosity) {
+        int val = (int) (colorfulness * color + (1 - colorfulness) * luminosity);
+        if (val < 0) {
+            return 0;
+        } else if (val > 0xff) {
+            return (byte) 0xff;
+        } else {
+            return (byte) val;
+        }
+    }
+
+    @Override
+    public Rectangle2D getBounds2D(BufferedImage src) {
+        return new Rectangle(src.getWidth(), src.getHeight());
+    }
+
+    @Override
+    public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) {
+        return new BufferedImage(src.getWidth(), src.getHeight(), src.getType());
+    }
+
+    @Override
+    public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) {
+        return (Point2D) srcPt.clone();
+    }
+
+    @Override
+    public RenderingHints getRenderingHints() {
+        return null;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/layer/imagery/ColorfulImageProcessor.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/imagery/ColorfulImageProcessor.java	(revision 11913)
+++ trunk/src/org/openstreetmap/josm/gui/layer/imagery/ColorfulImageProcessor.java	(revision 11914)
@@ -2,16 +2,6 @@
 package org.openstreetmap.josm.gui.layer.imagery;
 
-import java.awt.Rectangle;
-import java.awt.RenderingHints;
-import java.awt.geom.Point2D;
-import java.awt.geom.Rectangle2D;
 import java.awt.image.BufferedImage;
-import java.awt.image.BufferedImageOp;
-import java.awt.image.ColorModel;
-import java.awt.image.DataBuffer;
-import java.awt.image.DataBufferByte;
-import java.util.Optional;
 
-import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.gui.layer.ImageProcessor;
 
@@ -65,120 +55,3 @@
         return "ColorfulImageProcessor [colorfulness=" + colorfulness + ']';
     }
-
-    static class ColorfulFilter implements BufferedImageOp {
-        private final double colorfulness;
-
-        /**
-         * Create a new colorful filter.
-         * @param colorfulness The colorfulness as defined in the {@link ColorfulImageProcessor} class.
-         */
-        ColorfulFilter(double colorfulness) {
-            this.colorfulness = colorfulness;
-        }
-
-        @Override
-        public BufferedImage filter(BufferedImage src, BufferedImage dst) {
-            if (src.getWidth() == 0 || src.getHeight() == 0) {
-                return src;
-            }
-
-            BufferedImage dest = Optional.ofNullable(dst).orElseGet(() -> createCompatibleDestImage(src, null));
-            DataBuffer srcBuffer = src.getRaster().getDataBuffer();
-            DataBuffer destBuffer = dest.getRaster().getDataBuffer();
-            if (!(srcBuffer instanceof DataBufferByte) || !(destBuffer instanceof DataBufferByte)) {
-                Main.trace("Cannot apply color filter: Images do not use DataBufferByte.");
-                return src;
-            }
-
-            int type = src.getType();
-            if (type != dest.getType()) {
-                Main.trace("Cannot apply color filter: Src / Dest differ in type (" + type + '/' + dest.getType() + ')');
-                return src;
-            }
-            int redOffset;
-            int greenOffset;
-            int blueOffset;
-            int alphaOffset = 0;
-            switch (type) {
-            case BufferedImage.TYPE_3BYTE_BGR:
-                blueOffset = 0;
-                greenOffset = 1;
-                redOffset = 2;
-                break;
-            case BufferedImage.TYPE_4BYTE_ABGR:
-            case BufferedImage.TYPE_4BYTE_ABGR_PRE:
-                blueOffset = 1;
-                greenOffset = 2;
-                redOffset = 3;
-                break;
-            case BufferedImage.TYPE_INT_ARGB:
-            case BufferedImage.TYPE_INT_ARGB_PRE:
-                redOffset = 0;
-                greenOffset = 1;
-                blueOffset = 2;
-                alphaOffset = 3;
-                break;
-            default:
-                Main.trace("Cannot apply color filter: Source image is of wrong type (" + type + ").");
-                return src;
-            }
-            doFilter((DataBufferByte) srcBuffer, (DataBufferByte) destBuffer, redOffset, greenOffset, blueOffset,
-                    alphaOffset, src.getAlphaRaster() != null);
-            return dest;
-        }
-
-        private void doFilter(DataBufferByte src, DataBufferByte dest, int redOffset, int greenOffset, int blueOffset,
-                int alphaOffset, boolean hasAlpha) {
-            byte[] srcPixels = src.getData();
-            byte[] destPixels = dest.getData();
-            if (srcPixels.length != destPixels.length) {
-                Main.trace("Cannot apply color filter: Source/Dest lengths differ.");
-                return;
-            }
-            int entries = hasAlpha ? 4 : 3;
-            for (int i = 0; i < srcPixels.length; i += entries) {
-                int r = srcPixels[i + redOffset] & 0xff;
-                int g = srcPixels[i + greenOffset] & 0xff;
-                int b = srcPixels[i + blueOffset] & 0xff;
-                double luminosity = r * .21d + g * .72d + b * .07d;
-                destPixels[i + redOffset] = mix(r, luminosity);
-                destPixels[i + greenOffset] = mix(g, luminosity);
-                destPixels[i + blueOffset] = mix(b, luminosity);
-                if (hasAlpha) {
-                    destPixels[i + alphaOffset] = srcPixels[i + alphaOffset];
-                }
-            }
-        }
-
-        private byte mix(int color, double luminosity) {
-            int val = (int) (colorfulness * color + (1 - colorfulness) * luminosity);
-            if (val < 0) {
-                return 0;
-            } else if (val > 0xff) {
-                return (byte) 0xff;
-            } else {
-                return (byte) val;
-            }
-        }
-
-        @Override
-        public Rectangle2D getBounds2D(BufferedImage src) {
-            return new Rectangle(src.getWidth(), src.getHeight());
-        }
-
-        @Override
-        public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) {
-            return new BufferedImage(src.getWidth(), src.getHeight(), src.getType());
-        }
-
-        @Override
-        public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) {
-            return (Point2D) srcPt.clone();
-        }
-
-        @Override
-        public RenderingHints getRenderingHints() {
-            return null;
-        }
-    }
 }
