Ticket #17177: 17177.1.patch

File 17177.1.patch, 77.9 KB (added by taylor.smock, 4 years ago)

Initial rendering work (currently only works on zoom level 14)

  • src/org/openstreetmap/josm/data/imagery/ImageryInfo.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java b/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java
    a b  
    6161        /** A WMS endpoint entry only stores the WMS server info, without layer, which are chosen later by the user. **/
    6262        WMS_ENDPOINT("wms_endpoint"),
    6363        /** WMTS stores GetCapabilities URL. Does not store any information about the layer **/
    64         WMTS("wmts");
     64        WMTS("wmts"),
     65        /** MapBox Vector Tiles entry*/
     66        MVT("mvt");
    6567
    6668        private final String typeString;
    6769
  • src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
    a b  
    3232import org.openstreetmap.josm.data.cache.CacheEntryAttributes;
    3333import org.openstreetmap.josm.data.cache.ICachedLoaderListener;
    3434import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob;
     35import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
    3536import org.openstreetmap.josm.data.preferences.LongProperty;
    3637import org.openstreetmap.josm.tools.HttpClient;
    3738import org.openstreetmap.josm.tools.Logging;
     
    295296            if (content.length > 0) {
    296297                try (ByteArrayInputStream in = new ByteArrayInputStream(content)) {
    297298                    tile.loadImage(in);
    298                     if (tile.getImage() == null) {
     299                    if (!(tile instanceof VectorTile) && tile.getImage() == null
     300                        || (tile instanceof VectorTile) && !tile.isLoaded()) {
    299301                        String s = new String(content, StandardCharsets.UTF_8);
    300302                        Matcher m = SERVICE_EXCEPTION_PATTERN.matcher(s);
    301303                        if (m.matches()) {
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4/**
     5 * Command integers for Mapbox Vector Tiles
     6 * @author Taylor Smock
     7 * @since xxx
     8 */
     9public enum Command {
     10    /**
     11     * For {@link GeometryTypes#POINT}, each {@link #MoveTo} is a new point.
     12     * For {@link GeometryTypes#LINESTRING} and {@link GeometryTypes#POLYGON}, each {@link #MoveTo} is a new geometry of the same type.
     13     */
     14    MoveTo((byte) 1, (byte) 2),
     15    /**
     16     * While not explicitly prohibited for {@link GeometryTypes#POINT}, it should be ignored.
     17     * For {@link GeometryTypes#LINESTRING} and {@link GeometryTypes#POLYGON}, each {@link #LineTo} extends that geometry.
     18     */
     19    LineTo((byte) 2, (byte) 2),
     20    /**
     21     * This is only explicitly valid for {@link GeometryTypes#POLYGON}. It closes the {@link GeometryTypes#POLYGON}.
     22     */
     23    ClosePath((byte) 7, (byte) 0);
     24
     25    private final byte id;
     26    private final byte parameters;
     27
     28    Command(byte id, byte parameters) {
     29        this.id = id;
     30        this.parameters = parameters;
     31    }
     32
     33    /**
     34     * Get the command id
     35     * @return The id
     36     */
     37    public byte getId() {
     38        return this.id;
     39    }
     40
     41    /**
     42     * Get the number of parameters
     43     * @return The number of parameters
     44     */
     45    public byte getParameterNumber() {
     46        return this.parameters;
     47    }
     48}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import java.util.Objects;
     5import java.util.stream.Stream;
     6
     7/**
     8 * An indicator for a command to be executed
     9 * @author Taylor Smock
     10 * @since xxx
     11 */
     12public class CommandInteger {
     13    private final Command type;
     14    private final short[] parameters;
     15    private int added;
     16
     17    /**
     18     * Create a new command
     19     * @param command the command (treated as an unsigned int)
     20     */
     21    public CommandInteger(final int command) {
     22        // Technically, the int is unsigned, but it is easier to work with the long
     23        final long unsigned = Integer.toUnsignedLong(command);
     24        this.type = Stream.of(Command.values()).filter(e -> e.getId() == (unsigned & 0x7)).findAny()
     25                .orElseThrow(InvalidMapboxVectorTileException::new);
     26        // This is safe, since we are shifting right 3 when we converted an int to a long (for unsigned).
     27        // So we <i>cannot</i> lose anything.
     28        final int operationsInt = (int) (unsigned >> 3);
     29        this.parameters = new short[operationsInt * this.type.getParameterNumber()];
     30    }
     31
     32    /**
     33     * Add a parameter
     34     * @param parameterInteger The parameter to add (converted to {@link short}).
     35     */
     36    public void addParameter(Number parameterInteger) {
     37        this.parameters[added++] = parameterInteger.shortValue();
     38    }
     39
     40    /**
     41     * Get the operations for the command
     42     * @return The operations
     43     */
     44    public short[] getOperations() {
     45        return this.parameters;
     46    }
     47
     48    /**
     49     * Get the command type
     50     * @return the command type
     51     */
     52    public Command getType() {
     53        return this.type;
     54    }
     55
     56    /**
     57     * Get the expected parameter length
     58     * @return The expected parameter size
     59     */
     60    public boolean hasAllExpectedParameters() {
     61            return this.added >= this.parameters.length;
     62    }
     63}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import java.io.IOException;
     5import java.text.NumberFormat;
     6import java.util.ArrayList;
     7import java.util.List;
     8
     9import org.openstreetmap.josm.data.osm.TagMap;
     10import org.openstreetmap.josm.data.protobuf.ProtoBufPacked;
     11import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
     12import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
     13import org.openstreetmap.josm.tools.Utils;
     14
     15import static org.openstreetmap.josm.tools.I18n.tr;
     16
     17/**
     18 * A Feature for a {@link Layer}
     19 * @author Taylor Smock
     20 * @since xxx
     21 */
     22public class Feature {
     23    private static final byte ID_FIELD = 1;
     24    private static final byte TAG_FIELD = 2;
     25    private static final byte GEOMETRY_TYPE_FIELD = 3;
     26    private static final byte GEOMETRY_FIELD = 4;
     27    /** The geometry of the feature. Required. */
     28    private final List<CommandInteger> geometry = new ArrayList<>();
     29
     30    /** The geometry type of the feature. Required. */
     31    private final GeometryTypes geometryType;
     32
     33    /** The tags of the feature. Optional. */
     34    private TagMap tags;
     35
     36    /** The id of the feature. Optional. */
     37    // Technically, uint64
     38    private final long id;
     39
     40    /**
     41     * Create a new Feature
     42     * @param layer The layer the feature is part of (required for tags)
     43     * @param record The record to create the feature from
     44     * @throws IOException - if an IO error occurs
     45     */
     46    public Feature(Layer layer, ProtoBufRecord record) throws IOException {
     47        long tId = 0;
     48        GeometryTypes geometryTypeTemp = GeometryTypes.UNKNOWN;
     49        String key = null;
     50        try (ProtoBufParser parser = new ProtoBufParser(record.getBytes())) {
     51            while (parser.hasNext()) {
     52                try (ProtoBufRecord next = new ProtoBufRecord(parser)) {
     53                    if (next.getField() == TAG_FIELD) {
     54                        if (tags == null) {
     55                            tags = new TagMap();
     56                        }
     57                        // This is packed in v1 and v2
     58                        ProtoBufPacked packed = new ProtoBufPacked(next.getBytes());
     59                        for (Number number : packed.getArray()) {
     60                            key = parseTagValue(key, layer, number);
     61                        }
     62                    } else if (next.getField() == GEOMETRY_FIELD) {
     63                        // This is packed in v1 and v2
     64                        ProtoBufPacked packed = new ProtoBufPacked(next.getBytes());
     65                        CommandInteger currentCommand = null;
     66                        for (Number number : packed.getArray()) {
     67                            if (currentCommand != null && currentCommand.hasAllExpectedParameters()) {
     68                                currentCommand = null;
     69                            }
     70                            if (currentCommand == null) {
     71                                currentCommand = new CommandInteger(number.intValue());
     72                                this.geometry.add(currentCommand);
     73                            } else {
     74                                currentCommand.addParameter(ParameterInteger.decode(number.intValue()));
     75                            }
     76                        }
     77                        // TODO fallback to non-packed
     78                    } else if (next.getField() == GEOMETRY_TYPE_FIELD) {
     79                        geometryTypeTemp = GeometryTypes.values()[next.asUnsignedVarInt().intValue()];
     80                    } else if (next.getField() == ID_FIELD) {
     81                        tId = next.asUnsignedVarInt().longValue();
     82                    }
     83                }
     84            }
     85        }
     86        this.id = tId;
     87        this.geometryType = geometryTypeTemp;
     88        record.close();
     89    }
     90
     91    /**
     92     * Parse a tag value
     93     * @param key The current key (or {@code null}, if {@code null}, the returned value will be the new key)
     94     * @param layer The layer with key/value information
     95     * @param number The number to get the value from
     96     * @return The new key (if {@code null}, then a value was parsed and added to tags)
     97     */
     98    private String parseTagValue(String key, Layer layer, Number number) {
     99        if (key == null) {
     100            key = layer.getKey(number.intValue());
     101        } else {
     102            Object value = layer.getValue(number.intValue());
     103            if (value instanceof Double || value instanceof Float) {
     104                // reset grouping if the instance is a singleton
     105                final NumberFormat numberFormat = NumberFormat.getNumberInstance();
     106                final boolean grouping = numberFormat.isGroupingUsed();
     107                try {
     108                    numberFormat.setGroupingUsed(false);
     109                    this.tags.put(key, numberFormat.format(value));
     110                } finally {
     111                    numberFormat.setGroupingUsed(grouping);
     112                }
     113            } else {
     114                this.tags.put(key, Utils.intern(value.toString()));
     115            }
     116            key = null;
     117        }
     118        return key;
     119    }
     120
     121    /**
     122     * Get the geometry instructions
     123     * @return The geometry
     124     */
     125    public List<CommandInteger> getGeometry() {
     126        return this.geometry;
     127    }
     128
     129    /**
     130     * Get the geometry type
     131     * @return The {@link GeometryTypes}
     132     */
     133    public GeometryTypes getGeometryType() {
     134        return this.geometryType;
     135    }
     136
     137    /**
     138     * Get the id of the object
     139     * @return The unique id in the layer, or 0.
     140     */
     141    public long getId() {
     142        return this.id;
     143    }
     144
     145    /**
     146     * Get the tags
     147     * @return A tag map
     148     */
     149    public TagMap getTags() {
     150        return this.tags;
     151    }
     152}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import java.awt.Shape;
     7import java.awt.geom.Ellipse2D;
     8import java.awt.geom.Path2D;
     9import java.util.Collection;
     10import java.util.Collections;
     11import java.util.HashSet;
     12import java.util.List;
     13
     14/**
     15 * A class to generate geometry for a vector tile
     16 * @author Taylor Smock
     17 * @since xxx
     18 */
     19public class Geometry {
     20    private static final byte CIRCLE_SIZE = 1;
     21    final Collection<Shape> shapes = new HashSet<>();
     22    private final Feature feature;
     23
     24    /**
     25     * Create a {@link Geometry} for a {@link Feature}
     26     * @param feature the {@link Feature} for the geometry
     27     */
     28    public Geometry(final Feature feature) {
     29        this.feature = feature;
     30        final GeometryTypes geometryType = this.feature.getGeometryType();
     31        final List<CommandInteger> commands = this.feature.getGeometry();
     32        final byte circleSize = CIRCLE_SIZE;
     33        if (geometryType == GeometryTypes.POINT) {
     34            for (CommandInteger command : commands) {
     35                final short[] operations = command.getOperations();
     36                // Each MoveTo command is a new point
     37                if (command.getType() == Command.MoveTo && operations.length % 2 == 0) {
     38                    for (int i = 0; i < operations.length / 2; i++) {
     39                        // move left/up by 1/2 circleSize, so that the circle is centered
     40                        shapes.add(new Ellipse2D.Float(operations[2 * i] - circleSize / 2f, operations[2 * i + 1] - circleSize / 2f, circleSize, circleSize));
     41                    }
     42                } else {
     43                    throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length));
     44                }
     45            }
     46        } else if (geometryType == GeometryTypes.LINESTRING || geometryType == GeometryTypes.POLYGON) {
     47            Path2D.Float line = null;
     48            for (CommandInteger command : commands) {
     49                final short[] operations = command.getOperations();
     50                // Technically, there is no reason why there can be multiple MoveTo operations in one command, but that is undefined behavior
     51                if (command.getType() == Command.MoveTo && operations.length == 2) {
     52                    final double x;
     53                    final double y;
     54                    if (line != null) {
     55                        x = line.getCurrentPoint().getX() + operations[0];
     56                        y = line.getCurrentPoint().getY() + operations[1];
     57                    } else {
     58                        x = operations[0];
     59                        y = operations[1];
     60                    }
     61                    line = new Path2D.Float();
     62                    line.moveTo(x, y);
     63                    shapes.add(line);
     64                } else if (command.getType() == Command.LineTo && operations.length % 2 == 0 && line != null) {
     65                    for (int i = 0; i < operations.length / 2; i++) {
     66                        final double x = line.getCurrentPoint().getX() + operations[2 * i];
     67                        final double y = line.getCurrentPoint().getY() + operations[2 * i + 1];
     68                        line.lineTo(x, y);
     69                    }
     70                }
     71                // ClosePath should only be used with Polygon geometry
     72                else if (geometryType == GeometryTypes.POLYGON && command.getType() == Command.ClosePath && line != null) {
     73                    line.closePath();
     74                } else {
     75                    throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length));
     76                }
     77            }
     78        }
     79    }
     80
     81    /**
     82     * Get the feature for this geometry
     83     * @return The feature
     84     */
     85    public Feature getFeature() {
     86        return this.feature;
     87    }
     88
     89    /**
     90     * Get the shapes to draw this geometry with
     91     * @return A collection of shapes
     92     */
     93    public Collection<Shape> getShapes() {
     94        return Collections.unmodifiableCollection(this.shapes);
     95    }
     96}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4/**
     5 * Geometry types used by Mapbox Vector Tiles
     6 * @author Taylor Smock
     7 * @since xxx
     8 */
     9public enum GeometryTypes {
     10    /** May be ignored */
     11    UNKNOWN((byte) 0),
     12    /** May be a point or a multipoint geometry. Uses <i>only</i> {@link Command#MoveTo}. Multiple {@link Command#MoveTo}
     13     * indicates that it is a multi-point object. */
     14    POINT((byte) 1),
     15    /** May be a line or a multiline geometry. Each line {@link Command#MoveTo} and one or more {@link Command#LineTo}. */
     16    LINESTRING((byte) 2),
     17    /** May be a polygon or a multipolygon. Each ring uses a {@link Command#MoveTo}, one or more {@link Command#LineTo},
     18     * and one {@link Command#ClosePath} command. See {@link Ring}s. */
     19    POLYGON((byte) 3);
     20
     21    private final byte id;
     22    GeometryTypes(byte id) {
     23        this.id = id;
     24    }
     25
     26    /**
     27     * Get the id for the geometry type
     28     * @return The id
     29     */
     30    public byte getId() {
     31        return this.id;
     32    }
     33
     34    /**
     35     * Rings used by {@link GeometryTypes#POLYGON}
     36     * @author Taylor Smock
     37     */
     38    public enum Ring {
     39        /** A ring that goes in the clockwise direction */
     40        ExteriorRing,
     41        /** A ring that goes in the anti-clockwise direction */
     42        InteriorRing
     43    }
     44}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4/**
     5 * Thrown when a mapbox vector tile does not match specifications.
     6 *
     7 * @author Taylor Smock
     8 * @since xxx
     9 */
     10public class InvalidMapboxVectorTileException extends RuntimeException {
     11    /**
     12     * Create a default {@link InvalidMapboxVectorTileException}.
     13     */
     14    public InvalidMapboxVectorTileException() {
     15        super();
     16    }
     17
     18    /**
     19     * Create a new {@link InvalidMapboxVectorTile} exception with a message
     20     * @param message The message
     21     */
     22    public InvalidMapboxVectorTileException(final String message) {
     23        super(message);
     24    }
     25}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3import static org.openstreetmap.josm.tools.I18n.tr;
     4
     5import java.io.IOException;
     6import java.util.ArrayList;
     7import java.util.Arrays;
     8import java.util.Collection;
     9import java.util.Collections;
     10import java.util.HashSet;
     11import java.util.List;
     12import java.util.Map;
     13import java.util.Objects;
     14import java.util.function.Function;
     15import java.util.stream.Collectors;
     16
     17import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
     18import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
     19import org.openstreetmap.josm.data.protobuf.WireType;
     20import org.openstreetmap.josm.tools.Logging;
     21
     22/**
     23 * A Mapbox Vector Tile Layer
     24 * @author Taylor Smock
     25 * @since xxx
     26 */
     27public class Layer {
     28    private static final class ValueFields<T> {
     29        static final ValueFields<String> STRING = new ValueFields<>(1, ProtoBufRecord::asString);
     30        static final ValueFields<Float> FLOAT = new ValueFields<>(2, ProtoBufRecord::asFloat);
     31        static final ValueFields<Double> DOUBLE = new ValueFields<>(3, ProtoBufRecord::asDouble);
     32        static final ValueFields<Number> INT64 = new ValueFields<>(4, ProtoBufRecord::asUnsignedVarInt);
     33        // This may have issues if there are actual uint_values (i.e., more than {@link Long#MAX_VALUE})
     34        static final ValueFields<Number> UINT64 = new ValueFields<>(5, ProtoBufRecord::asUnsignedVarInt);
     35        static final ValueFields<Number> SINT64 = new ValueFields<>(6, ProtoBufRecord::asSignedVarInt);
     36        static final ValueFields<Boolean> BOOL = new ValueFields<>(7, r -> r.asUnsignedVarInt().longValue() != 0);
     37
     38        public static final Collection<ValueFields<?>> MAPPERS = Arrays.asList(STRING, FLOAT, DOUBLE, INT64, UINT64, SINT64, BOOL);
     39
     40        private final byte field;
     41        private final Function<ProtoBufRecord, T> conversion;
     42        private ValueFields(int field, Function<ProtoBufRecord, T> conversion) {
     43            this.field = (byte) field;
     44            this.conversion = conversion;
     45        }
     46
     47        /**
     48         * Get the field identifier for the value
     49         * @return The identifier
     50         */
     51        public byte getField() {
     52            return this.field;
     53        }
     54
     55        /**
     56         * Convert a protobuf record to a value
     57         * @param protobufRecord The record to convert
     58         * @return the converted value
     59         */
     60        public T convertValue(ProtoBufRecord protobufRecord) {
     61            return this.conversion.apply(protobufRecord);
     62        }
     63    }
     64
     65    /** The field value for a layer (in {@link ProtoBufRecord#getField}) */
     66    public static final byte LAYER_FIELD = 3;
     67    private static final byte VERSION_FIELD = 15;
     68    private static final byte NAME_FIELD = 1;
     69    private static final byte FEATURE_FIELD = 2;
     70    private static final byte KEY_FIELD = 3;
     71    private static final byte VALUE_FIELD = 4;
     72    private static final byte EXTENT_FIELD = 5;
     73    /** The default extent for a vector tile */
     74    static final int DEFAULT_EXTENT = 4096;
     75    private static final byte DEFAULT_VERSION = 1;
     76    /** This is <i>technically</i> an integer, but there are currently only two major versions (1, 2). Required. */
     77    private final byte version;
     78    /** A unique name for the layer. This <i>must</i> be unique on a per-tile basis. Required. */
     79    private final String name;
     80
     81    /** The extent of the tile, typically 4096. Required. */
     82    private final int extent;
     83
     84    /** A list of unique keys. Order is important. Optional. */
     85    private final List<String> keyList = new ArrayList<>();
     86    /** A list of unique values. Order is important. Optional. */
     87    private final List<Object> valueList = new ArrayList<>();
     88    /** The actual features of this layer in this tile */
     89    private final List<Feature> featureCollection;
     90    /** The shapes to use to draw this layer */
     91    private final List<Geometry> geometryCollection;
     92
     93    /**
     94     * Create a layer from a collection of records
     95     * @param records The records to convert to a layer
     96     * @throws IOException - if an IO error occurs
     97     */
     98    public Layer(Collection<ProtoBufRecord> records) throws IOException {
     99        // Do the unique required fields first
     100        Map<Integer, List<ProtoBufRecord>> sorted = records.stream().collect(Collectors.groupingBy(ProtoBufRecord::getField));
     101        this.version = sorted.get((int) VERSION_FIELD).parallelStream().map(ProtoBufRecord::asSignedVarInt).map(Number::byteValue).findFirst().orElse(DEFAULT_VERSION);
     102        // Per spec, we cannot continue past this until we have checked the version number
     103        if (this.version != 1 && this.version != 2) {
     104            throw new IllegalArgumentException(tr("We do not understand version {0} of the vector tile specification", this.version));
     105        }
     106        this.name = sorted.getOrDefault((int) NAME_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString).findFirst()
     107                .orElseThrow(() -> new IllegalArgumentException(tr("Vector tile layers must have a layer name")));
     108        this.extent = sorted.getOrDefault((int) EXTENT_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asSignedVarInt).map(Number::intValue).findAny().orElse(DEFAULT_EXTENT);
     109
     110        sorted.getOrDefault((int) KEY_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString).forEachOrdered(this.keyList::add);
     111        sorted.getOrDefault((int) VALUE_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::getBytes).map(ProtoBufParser::new).map(parser1 -> {try {return new ProtoBufRecord(parser1);} catch (IOException e) {return null;}})
     112                .filter(Objects::nonNull)
     113                .map(value -> ValueFields.MAPPERS.parallelStream()
     114                        .filter(v -> v.getField() == value.getField())
     115                        .map(v -> v.convertValue(value)).findFirst()
     116                        .orElseThrow(() -> new IllegalArgumentException(tr("Unknown field in vector tile layer value ({0})", value.getField()))))
     117                .forEachOrdered(this.valueList::add);
     118        Collection<IOException> exceptions = new HashSet<>(0);
     119        this.featureCollection = sorted.getOrDefault((int) FEATURE_FIELD, Collections.emptyList()).parallelStream().map(feature -> {
     120            try {
     121                return new Feature(this, feature);
     122            } catch (IOException e) {
     123                exceptions.add(e);
     124            }
     125            return null;
     126        }).collect(Collectors.toList());
     127        this.geometryCollection = this.featureCollection.stream().map(Geometry::new).collect(Collectors.toList());
     128        if (!exceptions.isEmpty()) {
     129            throw exceptions.iterator().next();
     130        }
     131        // Cleanup bytes (for memory)
     132        for (ProtoBufRecord record : records) {
     133            try {
     134                record.close();
     135            } catch (Exception e) {
     136                Logging.error(e);
     137            }
     138        }
     139    }
     140
     141    /**
     142     * Create a new layer
     143     * @param bytes The bytes that the layer comes from
     144     * @throws IOException - if an IO error occurs
     145     */
     146    public Layer(byte[] bytes) throws IOException {
     147        this(new ProtoBufParser(bytes).allRecords());
     148    }
     149
     150    /**
     151     * Get the extent of the tile
     152     * @return The layer extent
     153     */
     154    public int getExtent() {
     155        return this.extent;
     156    }
     157
     158    /**
     159     * Get the feature on this layer
     160     * @return the features
     161     */
     162    public Collection<Feature> getFeatures() {
     163        return Collections.unmodifiableCollection(this.featureCollection);
     164    }
     165
     166    /**
     167     * Get the geometry for this layer
     168     * @return The geometry
     169     */
     170    public Collection<Geometry> getGeometry() {
     171        return Collections.unmodifiableCollection(this.geometryCollection);
     172    }
     173
     174    /**
     175     * Get a specified key
     176     * @param index The index in the key list
     177     * @return The actual key
     178     */
     179    public String getKey(int index) {
     180        return this.keyList.get(index);
     181    }
     182
     183    /**
     184     * Get the name of the layer
     185     * @return The layer name
     186     */
     187    public String getName() {
     188        return this.name;
     189    }
     190
     191    /**
     192     * Get a specified value
     193     * @param index The index in the value list
     194     * @return The actual value. This can be a {@link String}, {@link Boolean}, {@link Integer}, or {@link Float} value.
     195     */
     196    public Object getValue(int index) {
     197        return this.valueList.get(index);
     198    }
     199
     200    /**
     201     * Get the MapBox Vector Tile version specification for this layer
     202     * @return The version of the MapBox Vector Tile specification
     203     */
     204    public byte getVersion() {
     205        return this.version;
     206    }
     207}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import java.util.concurrent.ThreadPoolExecutor;
     5
     6import org.apache.commons.jcs3.access.behavior.ICacheAccess;
     7import org.openstreetmap.gui.jmapviewer.Tile;
     8import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
     9import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
     10import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
     11import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
     12import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     13import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
     14import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob;
     15import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
     16import org.openstreetmap.josm.data.imagery.TileJobOptions;
     17import org.openstreetmap.josm.data.preferences.IntegerProperty;
     18import org.openstreetmap.josm.tools.CheckParameterUtil;
     19
     20public class MapBoxVectorCachedTileLoader implements TileLoader, CachedTileLoader {
     21    protected final ICacheAccess<String, BufferedImageCacheEntry> cache;
     22    protected final TileLoaderListener listener;
     23    protected final TileJobOptions options;
     24    private static final IntegerProperty THREAD_LIMIT = new IntegerProperty("imagery.vector.mvtloader.maxjobs", TMSCachedTileLoader.THREAD_LIMIT.getDefaultValue());
     25    private static final ThreadPoolExecutor DEFAULT_DOWNLOAD_JOB_DISPATCHER = TMSCachedTileLoader.getNewThreadPoolExecutor("MVT-downloader-%d", THREAD_LIMIT.get());
     26
     27    /**
     28     * Constructor
     29     * @param listener          called when tile loading has finished
     30     * @param cache             of the cache
     31     * @param options           tile job options
     32     */
     33    public MapBoxVectorCachedTileLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache,
     34           TileJobOptions options) {
     35        CheckParameterUtil.ensureParameterNotNull(cache, "cache");
     36        this.cache = cache;
     37        this.options = options;
     38        this.listener = listener;
     39    }
     40
     41    @Override
     42    public void clearCache(TileSource source) {
     43        this.cache.remove(source.getName() + ':');
     44    }
     45
     46    @Override
     47    public TileJob createTileLoaderJob(Tile tile) {
     48        return new MapBoxVectorCachedTileLoaderJob(
     49                listener,
     50                tile,
     51                cache,
     52                options,
     53                getDownloadExecutor());
     54    }
     55
     56    @Override
     57    public void cancelOutstandingTasks() {
     58        final ThreadPoolExecutor executor = getDownloadExecutor();
     59        executor.getQueue().stream().filter(executor::remove).filter(MapBoxVectorCachedTileLoaderJob.class::isInstance).map(MapBoxVectorCachedTileLoaderJob.class::cast).forEach(JCSCachedTileLoaderJob::handleJobCancellation);
     60    }
     61
     62    @Override
     63    public boolean hasOutstandingTasks() {
     64        return getDownloadExecutor().getTaskCount() > getDownloadExecutor().getCompletedTaskCount();
     65    }
     66
     67    private ThreadPoolExecutor getDownloadExecutor() {
     68        return DEFAULT_DOWNLOAD_JOB_DISPATCHER;
     69    }
     70}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import java.util.concurrent.ThreadPoolExecutor;
     5
     6import org.apache.commons.jcs3.access.behavior.ICacheAccess;
     7import org.openstreetmap.gui.jmapviewer.Tile;
     8import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
     9import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
     10import org.openstreetmap.josm.data.imagery.TMSCachedTileLoaderJob;
     11import org.openstreetmap.josm.data.imagery.TileJobOptions;
     12
     13public class MapBoxVectorCachedTileLoaderJob extends TMSCachedTileLoaderJob {
     14
     15    public MapBoxVectorCachedTileLoaderJob(TileLoaderListener listener, Tile tile,
     16            ICacheAccess<String, BufferedImageCacheEntry> cache, TileJobOptions options,
     17            ThreadPoolExecutor downloadExecutor) {
     18        super(listener, tile, cache, options, downloadExecutor);
     19    }
     20}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import org.openstreetmap.josm.data.imagery.ImageryInfo;
     5import org.openstreetmap.josm.data.imagery.JosmTemplatedTMSTileSource;
     6import org.openstreetmap.josm.data.projection.Projection;
     7
     8/**
     9 * Tile Source handling for Mapbox Vector Tile sources
     10 * @author Taylor Smock
     11 * @since xxx
     12 */
     13public class MapboxVectorTileSource extends JosmTemplatedTMSTileSource {
     14    public MapboxVectorTileSource(ImageryInfo info, Projection tileProjection) {
     15        super(info);
     16        //super.initProjection();
     17    }
     18}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import java.util.Arrays;
     5import java.util.List;
     6
     7/**
     8 * Items that MAY be used to figure out if a file or server response MAY BE a Mapbox Vector Tile
     9 * @author Taylor Smock
     10 * @since xxx
     11 */
     12public final class MVTFile {
     13    /**
     14     * Extensions for Mapbox Vector Tiles.
     15     * This is a SHOULD, <i>not</i> a MUST.
     16     */
     17    public static final List<String> EXTENSION = Arrays.asList("mvt");
     18
     19    /**
     20     * mimetypes for Mapbox Vector Tiles
     21     * This is a SHOULD, <i>not</i> a MUST.
     22     */
     23    public static final List<String> MIMETYPE = Arrays.asList("application/vnd.mapbox-vector-tile");
     24
     25    /**
     26     * The default projection. This is Web Mercator, per specification.
     27     */
     28    public static final String DEFAULT_PROJECTION = "EPSG:3857";
     29
     30    private MVTFile() {
     31        // Hide the constructor
     32    }
     33}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import java.awt.BasicStroke;
     5import java.awt.Color;
     6import java.awt.Graphics;
     7import java.awt.Graphics2D;
     8import java.awt.Shape;
     9import java.awt.font.FontRenderContext;
     10import java.awt.geom.AffineTransform;
     11import java.awt.geom.Ellipse2D;
     12import java.awt.geom.Line2D;
     13import java.awt.geom.Path2D;
     14import java.awt.image.BufferedImage;
     15import java.awt.image.ImageObserver;
     16import java.io.IOException;
     17import java.io.InputStream;
     18import java.util.Collection;
     19import java.util.HashSet;
     20import java.util.stream.Collectors;
     21
     22import org.openstreetmap.gui.jmapviewer.Tile;
     23import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     24import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
     25import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
     26import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
     27
     28public class MVTTile extends Tile implements VectorTile {
     29    private Collection<Geometry> geometry;
     30    private int extent = Layer.DEFAULT_EXTENT;
     31
     32    public MVTTile(TileSource source, int xtile, int ytile, int zoom) {
     33        super(source, xtile, ytile, zoom);
     34    }
     35
     36    @Override
     37    public BufferedImage getImage() {
     38        return this.image;
     39    }
     40
     41    @Override
     42    public void paint(final Graphics g, final int x, final int y) {
     43        this.paint(g, x, y, 256, 256);
     44    }
     45
     46    @Override
     47    public void paint(Graphics g, int x, int y, int width, int height, ImageObserver observer) {
     48        if (!(g instanceof Graphics2D) || this.geometry == null) {
     49            if (getImage() != null) {
     50                g.drawImage(image, x, y, width, height, observer);
     51            }
     52            return;
     53        }
     54        final Graphics2D graphics = (Graphics2D) g;
     55        graphics.setColor(Color.GREEN);
     56        final AffineTransform original = graphics.getTransform();
     57        try {
     58            AffineTransform translation = (AffineTransform) original.clone();
     59            translation.translate(x, y);
     60            graphics.setTransform(translation);
     61            this.geometry.forEach(shapes -> {
     62                for (Shape shape : shapes.getShapes()) {
     63                    if (shape instanceof Ellipse2D) {
     64                        graphics.setColor(Color.GREEN);
     65                    } else if (shape instanceof Path2D) {
     66                        graphics.setColor(Color.RED);
     67                    }
     68                    graphics.draw(shape);
     69                }
     70            });
     71        } finally {
     72            graphics.setTransform(original);
     73        }
     74        FontRenderContext fontContext = graphics.getFontRenderContext();
     75        graphics.getFontMetrics().getHeight();
     76        graphics.setColor(Color.RED);
     77        graphics.drawString("0, 0", 1024, 1024);
     78    }
     79
     80    @Override
     81    public void setImage(final BufferedImage image) {
     82        this.image = image;
     83    }
     84
     85    @Override
     86    public void loadImage(final InputStream inputStream) throws IOException {
     87        if (this.image == null || this.image == Tile.LOADING_IMAGE || this.image == Tile.ERROR_IMAGE) {
     88            this.initLoading();
     89            ProtoBufParser parser = new ProtoBufParser(inputStream);
     90            Collection<ProtoBufRecord> protoBufRecords = parser.allRecords();
     91            Collection<Layer> layers = new HashSet<>();
     92            for (ProtoBufRecord record : protoBufRecords) {
     93                if (record.getField() == Layer.LAYER_FIELD) {
     94                    Layer mvtLayer = new Layer(new ProtoBufParser(record.getBytes()).allRecords());
     95                    layers.add(mvtLayer);
     96                    // Cleanup bytes
     97                    record.close();
     98                }
     99            }
     100            // TODO Store layers separately
     101            this.geometry = layers.stream().flatMap(layer -> layer.getGeometry().stream()).collect(Collectors.toList());
     102            this.extent = layers.stream().map(Layer::getExtent).max(Integer::compare).orElse(Layer.DEFAULT_EXTENT);
     103            BufferedImage bufferedImage = new BufferedImage(this.extent, this.extent, BufferedImage.TYPE_4BYTE_ABGR);
     104            Graphics2D graphics = bufferedImage.createGraphics();
     105            this.paint(graphics, 0, 0);
     106
     107            // TODO figure out a better way to free memory
     108            final int maxSize = 256;
     109            final BufferedImage resized = new BufferedImage(maxSize, maxSize, bufferedImage.getType());
     110            resized.getGraphics().drawImage(bufferedImage, 0, 0, resized.getWidth(), resized.getHeight(), null);
     111            this.image = resized;
     112            this.finishLoading();
     113        }
     114    }
     115
     116    @Override
     117    public int getExtent() {
     118        return this.extent;
     119    }
     120
     121    /**
     122     * Returns the X coordinate.
     123     * @return tile number on the x axis of this tile
     124     */
     125    public int getXtile() {
     126        return xtile;
     127    }
     128
     129    /**
     130     * Returns the Y coordinate.
     131     * @return tile number on the y axis of this tile
     132     */
     133    public int getYtile() {
     134        return ytile;
     135    }
     136
     137    public int getZoom() {
     138        return super.getZoom();
     139    }
     140}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/ParameterInteger.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/ParameterInteger.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/ParameterInteger.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4/**
     5 * The parameters that follow the {@link CommandInteger}.
     6 * @author Taylor Smock
     7 * @since xxx
     8 */
     9public class ParameterInteger {
     10    private ParameterInteger() {
     11        // Hide constructor
     12    }
     13
     14    /**
     15     * Get the value for this ParameterInteger
     16     * @param value The value to decode
     17     * @return The decoded integer value
     18     */
     19    public static int decode(int value) {
     20        return ((value >> 1) ^ (-(value & 1)));
     21    }
     22}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java
    new file mode 100644
    - +  
     1package org.openstreetmap.josm.data.imagery.vectortile;
     2
     3import java.awt.Graphics;
     4import java.awt.image.ImageObserver;
     5
     6public interface VectorTile {
     7    /**
     8     * Paints the vector tile on the {@link Graphics} <code>g</code> at the
     9     * position <code>x</code>/<code>y</code>.
     10     *
     11     * @param g the Graphics object
     12     * @param x x-coordinate in <code>g</code>
     13     * @param y y-coordinate in <code>g</code>
     14     */
     15    void paint(final Graphics g, final int x, final int y);
     16
     17    /**
     18     * Paints the vector tile on the {@link Graphics} <code>g</code> at the
     19     * position <code>x</code>/<code>y</code>.
     20     *  @param g the Graphics object
     21     * @param x x-coordinate in <code>g</code>
     22     * @param y y-coordinate in <code>g</code>
     23     * @param width width that tile should have
     24     * @param height height that tile should have
     25     * @param observer The paint observer. May be {@code null}.
     26     */
     27    void paint(Graphics g, int x, int y, int width, int height, ImageObserver observer);
     28
     29    /**
     30     * Get the extent of the tile (in pixels)
     31     * @return The tile extent (pixels)
     32     */
     33    int getExtent();
     34}
  • new file src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java b/src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import java.util.ArrayList;
     5import java.util.List;
     6
     7/**
     8 * Parse packed values (only numerical values)
     9 * @author Taylor Smock
     10 * @since xxx
     11 */
     12public class ProtoBufPacked {
     13    private final byte[] bytes;
     14    private int location;
     15    private final Number[] numbers;
     16    /**
     17     * Create a new ProtoBufPacked object
     18     * @param bytes The packed bytes
     19     */
     20    public ProtoBufPacked(byte[] bytes) {
     21        this.location = 0;
     22        this.bytes = bytes;
     23        List<Number> numbersT = new ArrayList<>();
     24        while (this.location < bytes.length) {
     25            numbersT.add(ProtoBufParser.convertByteArray(this.nextVarInt(), ProtoBufParser.VAR_INT_BYTE_SIZE));
     26        }
     27
     28        this.numbers = new Number[numbersT.size()];
     29        for (int i = 0; i < numbersT.size(); i++) {
     30            this.numbers[i] = numbersT.get(i);
     31        }
     32    }
     33
     34    /**
     35     * The number of expected values
     36     * @return The expected values
     37     */
     38    public int size() {
     39        return this.numbers.length;
     40    }
     41
     42    /**
     43     * Get the parsed number array
     44     * @return The number array
     45     */
     46    public Number[] getArray() {
     47        return this.numbers;
     48    }
     49
     50    private byte[] nextVarInt() {
     51        List<Byte> byteList = new ArrayList<>();
     52        while ((this.bytes[this.location] & ProtoBufParser.MOST_SIGNIFICANT_BYTE) == ProtoBufParser.MOST_SIGNIFICANT_BYTE) {
     53            // Get rid of the leading bit (shift left 1, then shift right 1 unsigned)
     54            byteList.add((byte) (this.bytes[this.location++] ^ ProtoBufParser.MOST_SIGNIFICANT_BYTE));
     55        }
     56        // The last byte doesn't drop the most significant bit
     57        byteList.add(this.bytes[this.location++]);
     58        byte[] byteArray = new byte[byteList.size()];
     59        for (int i = 0; i < byteList.size(); i++) {
     60            byteArray[i] = byteList.get(i);
     61        }
     62
     63        return byteArray;
     64    }
     65}
  • new file src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java b/src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import java.io.BufferedInputStream;
     5import java.io.ByteArrayInputStream;
     6import java.io.IOException;
     7import java.io.InputStream;
     8import java.util.ArrayList;
     9import java.util.Collection;
     10import java.util.List;
     11
     12import org.openstreetmap.josm.tools.Logging;
     13
     14
     15/**
     16 * A basic Protobuf parser
     17 * @author Taylor Smock
     18 * @since xxx
     19 */
     20public class ProtoBufParser implements AutoCloseable {
     21    /**
     22     * Used to get the most significant byte
     23     */
     24    static final byte MOST_SIGNIFICANT_BYTE = (byte) (1 << 7);
     25    /** The default byte size (see {@link #VAR_INT_BYTE_SIZE} for var ints) */
     26    public static final byte BYTE_SIZE = 8;
     27    /** The byte size for var ints (since the first byte is just an indicator for if the var int is done) */
     28    public static final byte VAR_INT_BYTE_SIZE = BYTE_SIZE - 1;
     29    // TODO switch to a better parser
     30    private final InputStream inputStream;
     31    /**
     32     * Create a new parser
     33     * @param bytes The bytes to parse
     34     */
     35    public ProtoBufParser(byte[] bytes) {
     36        this(new ByteArrayInputStream(bytes));
     37    }
     38
     39    /**
     40     * Create a new parser
     41     * @param inputStream The InputStream (will be fully read at this time)
     42     */
     43    public ProtoBufParser(InputStream inputStream) {
     44        if (inputStream.markSupported()) {
     45            this.inputStream = inputStream;
     46        } else {
     47            this.inputStream = new BufferedInputStream(inputStream);
     48        }
     49    }
     50
     51    @Override
     52    public void close() {
     53        try {
     54            this.inputStream.close();
     55        } catch (IOException e) {
     56            Logging.error(e);
     57        }
     58    }
     59
     60    /**
     61     * Get the "next" WireType
     62     * @return {@link WireType} expected
     63     * @throws IOException - if an IO error occurs
     64     */
     65    public WireType next() throws IOException {
     66        this.inputStream.mark(16);
     67        try {
     68            return WireType.values()[this.inputStream.read() << 3];
     69        } finally {
     70            this.inputStream.reset();
     71        }
     72    }
     73
     74    /**
     75     * Get the next byte
     76     * @return The next byte
     77     * @throws IOException - if an IO error occurs
     78     */
     79    public int nextByte() throws IOException {
     80        return this.inputStream.read();
     81    }
     82
     83    /**
     84     * Check if there is more data to read
     85     * @return {@code true} if there is more data to read
     86     * @throws IOException - if an IO error occurs
     87     */
     88    public boolean hasNext() throws IOException {
     89        return this.inputStream.available() > 0;
     90    }
     91
     92    /**
     93     * Get the next var int ({@code WireType#VARINT})
     94     * @return The next var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum})
     95     * @throws IOException - if an IO error occurs
     96     */
     97    public byte[] nextVarInt() throws IOException {
     98        List<Byte> byteList = new ArrayList<>();
     99        int currentByte = this.nextByte();
     100        while ((byte) (currentByte & MOST_SIGNIFICANT_BYTE) == MOST_SIGNIFICANT_BYTE) {
     101            // Get rid of the leading bit (shift left 1, then shift right 1 unsigned)
     102            byteList.add((byte) (currentByte ^ MOST_SIGNIFICANT_BYTE));
     103            currentByte = this.nextByte();
     104        }
     105        // The last byte doesn't drop the most significant bit
     106        byteList.add((byte) currentByte);
     107        byte[] byteArray = new byte[byteList.size()];
     108        for (int i = 0; i < byteList.size(); i++) {
     109            byteArray[i] = byteList.get(i);
     110        }
     111
     112        return byteArray;
     113    }
     114
     115    /**
     116     * Get the next 32 bits ({@link WireType#THIRTY_TWO_BIT})
     117     * @return a byte array of the next 32 bits (4 bytes)
     118     * @throws IOException - if an IO error occurs
     119     */
     120    public byte[] nextFixed32() throws IOException {
     121        // 4 bytes == 32 bits
     122        return readNextBytes(4);
     123    }
     124
     125    /**
     126     * Get the next 64 bits ({@link WireType#SIXTY_FOUR_BIT})
     127     * @return a byte array of the next 64 bits (8 bytes)
     128     * @throws IOException - if an IO error occurs
     129     */
     130    public byte[] nextFixed64() throws IOException {
     131        // 8 bytes == 64 bits
     132        return readNextBytes(8);
     133    }
     134
     135    /**
     136     * Read an arbitrary number of bytes
     137     * @param size The number of bytes to read
     138     * @return a byte array of the specified size, filled with bytes read (unsigned)
     139     * @throws IOException - if an IO error occurs
     140     */
     141    private byte[] readNextBytes(int size) throws IOException {
     142        byte[] bytesRead = new byte[size];
     143        for (int i = 0; i < bytesRead.length; i++) {
     144            bytesRead[i] = (byte) this.nextByte();
     145        }
     146        return bytesRead;
     147    }
     148
     149    /**
     150     * Get the next delimited message ({@link WireType#LENGTH_DELIMITED})
     151     * @return The next length delimited message
     152     * @throws IOException - if an IO error occurs
     153     */
     154    public byte[] nextLengthDelimited() throws IOException {
     155        int length = convertByteArray(this.nextVarInt(), VAR_INT_BYTE_SIZE).intValue();
     156        return readNextBytes(length);
     157    }
     158
     159    /**
     160     * Convert a byte array to a number (little endian)
     161     * @param bytes The bytes to convert
     162     * @param byteSize The size of the byte. For var ints, this is 7, for other ints, this is 8.
     163     * @return An appropriate {@link Number} class.
     164     */
     165    public static Number convertByteArray(byte[] bytes, byte byteSize) {
     166        long number = 0;
     167        for (int i = 0; i < bytes.length; i++) {
     168            // Need to convert to uint64 in order to avoid bit operation from filling in 1's and overflow issues
     169            number += Byte.toUnsignedLong(bytes[i]) << byteSize * i;
     170        }
     171        return convertLong(number);
     172    }
     173
     174    /**
     175     * Convert a long to an appropriate {@link Number} class
     176     * @param number The long to convert
     177     * @return A {@link Number}
     178     */
     179    public static Number convertLong(long number) {
     180        // TODO deal with booleans
     181        if (number <= Byte.MAX_VALUE && number >= Byte.MIN_VALUE) {
     182            return (byte) number;
     183        } else if (number <= Short.MAX_VALUE && number >= Short.MIN_VALUE) {
     184            return (short) number;
     185        } else if (number <= Integer.MAX_VALUE && number >= Integer.MIN_VALUE) {
     186            return (int) number;
     187        }
     188        return number;
     189    }
     190
     191    /**
     192     * Read all records
     193     * @return A collection of all records
     194     * @throws IOException - if an IO error occurs
     195     */
     196    public Collection<ProtoBufRecord> allRecords() throws IOException {
     197        Collection<ProtoBufRecord> records = new ArrayList<>();
     198        while (this.hasNext()) {
     199            records.add(new ProtoBufRecord(this));
     200        }
     201        return records;
     202    }
     203}
  • new file src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java b/src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3import static org.openstreetmap.josm.tools.I18n.tr;
     4
     5import java.io.IOException;
     6import java.nio.charset.StandardCharsets;
     7import java.util.stream.Stream;
     8
     9import org.openstreetmap.josm.tools.Utils;
     10
     11/**
     12 * A protobuf record, storing the {@link WireType}, the parsed field number, and the bytes for it.
     13 * @author Taylor Smock
     14 * @since xxx
     15 */
     16public class ProtoBufRecord implements AutoCloseable {
     17    private static final byte[] EMPTY_BYTES = {};
     18    private final WireType type;
     19    private final int field;
     20    private byte[] bytes;
     21
     22    /**
     23     * Create a new Protobuf record
     24     * @param parser The parser to use to create the record
     25     * @throws IOException - if an IO error occurs
     26     */
     27    public ProtoBufRecord(ProtoBufParser parser) throws IOException {
     28        Number number = ProtoBufParser.convertByteArray(parser.nextVarInt(), ProtoBufParser.VAR_INT_BYTE_SIZE);
     29        // I don't foresee having field numbers > {@code Integer#MAX_VALUE >> 3}
     30        this.field = (int) number.longValue() >> 3;
     31        // 7 is 111 (so last three bits)
     32        byte wireType = (byte) (number.longValue() & 7);
     33        this.type = Stream.of(WireType.values()).filter(wType -> wType.getTypeRepresentation() == wireType).findFirst().orElse(WireType.UNKNOWN);
     34
     35        if (this.type == WireType.VARINT) {
     36            this.bytes = parser.nextVarInt();
     37        } else if (this.type == WireType.SIXTY_FOUR_BIT) {
     38            this.bytes = parser.nextFixed64();
     39        } else if (this.type == WireType.THIRTY_TWO_BIT) {
     40            this.bytes = parser.nextFixed32();
     41        } else if (this.type == WireType.LENGTH_DELIMITED) {
     42            this.bytes = parser.nextLengthDelimited();
     43        // START_GROUP and END_GROUP are currently used by Mapbox Vector Tiles
     44        } else if (this.type == WireType.START_GROUP || this.type == WireType.END_GROUP) {
     45            this.bytes = EMPTY_BYTES;
     46        } else {
     47            throw new IllegalArgumentException(tr("Unknown type: {0} for field {1}", wireType, this.field));
     48        }
     49    }
     50
     51    /**
     52     * Get the field value
     53     * @return The field value
     54     */
     55    public int getField() {
     56        return this.field;
     57    }
     58
     59    /**
     60     * Get the WireType of the data
     61     * @return The {@link WireType} of the data
     62     */
     63    public WireType getType() {
     64        return this.type;
     65    }
     66
     67    /**
     68     * Get the raw bytes for this record
     69     * @return The bytes
     70     */
     71    public byte[] getBytes() {
     72        return this.bytes;
     73    }
     74
     75    /**
     76     * Get the var int ({@code WireType#VARINT})
     77     * @return The var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum})
     78     */
     79    public Number asUnsignedVarInt() {
     80        return ProtoBufParser.convertByteArray(this.bytes, ProtoBufParser.VAR_INT_BYTE_SIZE);
     81    }
     82
     83    /**
     84     * Get the signed var int ({@code WireType#VARINT}).
     85     * These are specially encoded so that they take up less space.
     86     *
     87     * @return The signed var int ({@code sint32} or {@code sint64})
     88     */
     89    public Number asSignedVarInt() {
     90        Number signed = this.asUnsignedVarInt();
     91        if (signed instanceof Long) {
     92            long value = ((Long) signed).longValue();
     93            return Long.valueOf((value << 1) ^ (value >> 63));
     94        }
     95        int value = signed.intValue();
     96        long number = (value << 1) ^ (value >> 31);
     97        return ProtoBufParser.convertLong(number);
     98    }
     99
     100    /**
     101     * Get as a double ({@link WireType#SIXTY_FOUR_BIT})
     102     * @return the double
     103     */
     104    public double asDouble() {
     105        long doubleNumber = ProtoBufParser.convertByteArray(asFixed64(), ProtoBufParser.BYTE_SIZE).longValue();
     106        return Double.longBitsToDouble(doubleNumber);
     107    }
     108
     109    /**
     110     * Get as a float ({@link WireType#THIRTY_TWO_BIT})
     111     * @return the float
     112     */
     113    public float asFloat() {
     114        int floatNumber = ProtoBufParser.convertByteArray(asFixed32(), ProtoBufParser.BYTE_SIZE).intValue();
     115        return Float.intBitsToFloat(floatNumber);
     116    }
     117
     118    /**
     119     * Get as a string ({@link WireType#LENGTH_DELIMITED})
     120     * @return The string (encoded as {@link StandardCharsets#UTF_8})
     121     */
     122    public String asString() {
     123        return Utils.intern(new String(this.bytes, StandardCharsets.UTF_8));
     124    }
     125
     126    /**
     127     * Get as 32 bits ({@link WireType#THIRTY_TWO_BIT})
     128     * @return a byte array of the 32 bits (4 bytes)
     129     */
     130    public byte[] asFixed32() {
     131        // TODO verify, or just assume?
     132        // 4 bytes == 32 bits
     133        return this.bytes;
     134    }
     135
     136    /**
     137     * Get as 64 bits ({@link WireType#SIXTY_FOUR_BIT})
     138     * @return a byte array of the 64 bits (8 bytes)
     139     */
     140    public byte[] asFixed64() {
     141        // TODO verify, or just assume?
     142        // 8 bytes == 64 bits
     143        return this.bytes;
     144    }
     145
     146    @Override
     147    public void close() {
     148        this.bytes = null;
     149    }
     150}
  • new file src/org/openstreetmap/josm/data/protobuf/WireType.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/protobuf/WireType.java b/src/org/openstreetmap/josm/data/protobuf/WireType.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4/**
     5 * The WireTypes
     6 * @author Taylor Smock
     7 * @since xxx
     8 */
     9public enum WireType {
     10    /** int32, int64, uint32, uint64, sing32, sint64, bool, enum */
     11    VARINT(0),
     12    /** fixed64, sfixed64, double */
     13    SIXTY_FOUR_BIT(1),
     14    /** string, bytes, embedded messages, packed repeated fields */
     15    LENGTH_DELIMITED(2),
     16    /**
     17     * start groups
     18     * @deprecated Unknown reason. Deprecated since at least 2012.
     19     */
     20    @Deprecated
     21    START_GROUP(3),
     22    /**
     23     * end groups
     24     * @deprecated Unknown reason. Deprecated since at least 2012.
     25     */
     26    @Deprecated
     27    END_GROUP(4),
     28    /** fixed32, sfixed32, float */
     29    THIRTY_TWO_BIT(5),
     30
     31    /** For unknown WireTypes */
     32    UNKNOWN(Byte.MAX_VALUE);
     33
     34    private final byte type;
     35    WireType(int value) {
     36        this.type = (byte) value;
     37    }
     38
     39    /**
     40     * Get the type representation (byte form)
     41     * @return The wire type byte representation
     42     */
     43    public byte getTypeRepresentation() {
     44        return this.type;
     45    }
     46}
  • new file src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java b/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.imagery;
     3
     4import java.util.Collection;
     5import java.util.Collections;
     6
     7import org.openstreetmap.gui.jmapviewer.Tile;
     8import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
     9import org.openstreetmap.josm.data.imagery.ImageryInfo;
     10import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTFile;
     11import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
     12import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapBoxVectorCachedTileLoader;
     13import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSource;
     14import org.openstreetmap.josm.data.projection.Projections;
     15import org.openstreetmap.josm.gui.layer.AbstractCachedTileSourceLayer;
     16
     17public class MVTLayer extends AbstractCachedTileSourceLayer<MapboxVectorTileSource> {
     18    private static final String CACHE_REGION_NAME = "MVT";
     19
     20    public MVTLayer(ImageryInfo info) {
     21        super(info);
     22    }
     23
     24    @Override
     25    protected Class<? extends TileLoader> getTileLoaderClass() {
     26        return MapBoxVectorCachedTileLoader.class;
     27    }
     28
     29    @Override
     30    protected String getCacheName() {
     31        return CACHE_REGION_NAME;
     32    }
     33
     34    @Override
     35    public Collection<String> getNativeProjections() {
     36        // MapBox Vector Tiles <i>specifically</i> only support EPSG:3857 ("it is exclusively geared towards square pixel tiles in {link to EPSG:3857}").
     37        return Collections.singleton(MVTFile.DEFAULT_PROJECTION);
     38    }
     39
     40    @Override
     41    protected MapboxVectorTileSource getTileSource() {
     42        MapboxVectorTileSource source = new MapboxVectorTileSource(this.info, Projections.getProjectionByCode(MVTFile.DEFAULT_PROJECTION));
     43        this.info.setAttribution(source);
     44        return source;
     45    }
     46
     47    @Override
     48    public Tile createTile(MapboxVectorTileSource source, int x, int y, int zoom) {
     49        return new MVTTile(source, x, y, zoom);
     50    }
     51
     52}
  • src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java b/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
    a b  
    8787import org.openstreetmap.josm.data.imagery.OffsetBookmark;
    8888import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
    8989import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
     90import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
    9091import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
    9192import org.openstreetmap.josm.data.preferences.BooleanProperty;
    9293import org.openstreetmap.josm.data.preferences.IntegerProperty;
     
    868869            if (coordinateConverter.requiresReprojection()) {
    869870                tile = new ReprojectionTile(tileSource, x, y, zoom);
    870871            } else {
    871                 tile = new Tile(tileSource, x, y, zoom);
     872                tile = createTile(tileSource, x, y, zoom);
    872873            }
    873874            tileCache.addTile(tile);
    874875        }
     
    10061007            g.setClip(oldClip);
    10071008        }
    10081009    }
     1010
     1011    /**
     1012     * Draw a vector tile on screen.
     1013     * @param g the Graphics2D
     1014     * @param tile the vector tile
     1015     * @param anchorImage tile anchor in image coordinates
     1016     * @param anchorScreen tile anchor in screen coordinates
     1017     * @param clip clipping region in screen coordinates (can be null)
     1018     */
     1019    private void drawVectorTileInside(Graphics2D g, VectorTile tile, TileAnchor anchorImage, TileAnchor anchorScreen, Shape clip) {
     1020        AffineTransform imageToScreen = anchorImage.convert(anchorScreen);
     1021        Point2D screen0 = imageToScreen.transform(new Point2D.Double(0, 0), null);
     1022        Point2D screen1 = imageToScreen.transform(new Point2D.Double(
     1023                tile.getExtent(), tile.getExtent()), null);
     1024
     1025        Shape oldClip = null;
     1026        if (clip != null) {
     1027            oldClip = g.getClip();
     1028            g.clip(clip);
     1029        }
     1030        tile.paint(g, (int) Math.round(screen0.getX()), (int) Math.round(screen0.getY()),
     1031                (int) Math.round(screen1.getX()) - (int) Math.round(screen0.getX()),
     1032                (int) Math.round(screen1.getY()) - (int) Math.round(screen0.getY()), this);
     1033        if (clip != null) {
     1034            g.setClip(oldClip);
     1035        }
     1036    }
    10091037
    10101038    private List<Tile> paintTileImages(Graphics2D g, TileSet ts) {
    10111039        Object paintMutex = new Object();
     
    10211049                    img = getLoadedTileImage(tile);
    10221050                    anchorImage = getAnchor(tile, img);
    10231051                }
    1024                 if (img == null || anchorImage == null) {
     1052                if (img == null || anchorImage == null || (tile instanceof VectorTile && !tile.isLoaded())) {
    10251053                    miss = true;
    10261054                }
    10271055            }
     
    10301058                return;
    10311059            }
    10321060
    1033             img = applyImageProcessors(img);
     1061            if (img != null) {
     1062                img = applyImageProcessors(img);
     1063            }
    10341064
    10351065            TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile);
    10361066            synchronized (paintMutex) {
    10371067                //cannot paint in parallel
    1038                 drawImageInside(g, img, anchorImage, anchorScreen, null);
     1068                if (tile instanceof VectorTile) {
     1069                    drawVectorTileInside(g, (VectorTile) tile, anchorImage, anchorScreen, null);
     1070                } else {
     1071                    drawImageInside(g, img, anchorImage, anchorScreen, null);
     1072                }
    10391073            }
    10401074            MapView mapView = MainApplication.getMap().mapView;
    10411075            if (tile instanceof ReprojectionTile && ((ReprojectionTile) tile).needsUpdate(mapView.getScale())) {
     
    18301864
    18311865                for (int x = minX; x <= maxX; x++) {
    18321866                    for (int y = minY; y <= maxY; y++) {
    1833                         requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel));
     1867                        requestedTiles.add(createTile(tileSource, x, y, currentZoomLevel));
    18341868                    }
    18351869                }
    18361870            }
     
    19291963        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
    19301964    }
    19311965
     1966    /**
     1967     * Create a new tile. Added to allow use of custom {@link Tile} objects.
     1968     *
     1969     * @param source Tile source
     1970     * @param x X coordinate
     1971     * @param y Y coordinate
     1972     * @param zoom Zoom level
     1973     * @return The new {@link Tile}
     1974     * @since xxx
     1975     */
     1976    public Tile createTile(T source, int x, int y, int zoom) {
     1977        return new Tile(source, x, y, zoom);
     1978    }
     1979
    19321980    @Override
    19331981    public synchronized void destroy() {
    19341982        super.destroy();
  • src/org/openstreetmap/josm/gui/layer/ImageryLayer.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java b/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java
    a b  
    3737import org.openstreetmap.josm.gui.MapView;
    3838import org.openstreetmap.josm.gui.MenuScroller;
    3939import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
     40import org.openstreetmap.josm.gui.layer.imagery.MVTLayer;
    4041import org.openstreetmap.josm.gui.widgets.UrlLabel;
    4142import org.openstreetmap.josm.tools.GBC;
    4243import org.openstreetmap.josm.tools.ImageProcessor;
     
    168169        case BING:
    169170        case SCANEX:
    170171            return new TMSLayer(info);
     172        case MVT:
     173            return new MVTLayer(info);
    171174        default:
    172175            throw new AssertionError(tr("Unsupported imagery type: {0}", info.getImageryType()));
    173176        }
  • src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java b/src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java
    a b  
    312312        activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.WMS));
    313313        activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.TMS));
    314314        activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.WMTS));
     315        activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.MVT));
    315316        activeToolbar.add(remove);
    316317        activePanel.add(activeToolbar, BorderLayout.EAST);
    317318        add(activePanel, GBC.eol().fill(GridBagConstraints.BOTH).weight(2.0, 0.4).insets(5, 0, 0, 5));
     
    439440                break;
    440441            case WMTS:
    441442                icon = /* ICON(dialogs/) */ "add_wmts";
     443                break;
     444            case MVT:
     445                icon = /* ICON(dialogs/) */ "add_wmts"; // TODO replace
    442446                break;
    443447            default:
    444448                break;
     
    459463                break;
    460464            case WMTS:
    461465                p = new AddWMTSLayerPanel();
     466                break;
     467            case MVT:
     468                p = new AddMVTLayerPanel();
    462469                break;
    463470            default:
    464471                throw new IllegalStateException("Type " + type + " not supported");
  • new file test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java b/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5import static org.junit.jupiter.api.Assertions.fail;
     6
     7import java.io.File;
     8import java.io.IOException;
     9import java.io.InputStream;
     10import java.nio.file.Paths;
     11import java.text.MessageFormat;
     12import java.util.ArrayList;
     13import java.util.Collection;
     14import java.util.List;
     15
     16import org.junit.jupiter.api.Test;
     17import org.openstreetmap.josm.TestUtils;
     18import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Feature;
     19import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
     20import org.openstreetmap.josm.io.Compression;
     21
     22/**
     23 * Test class for {@link ProtoBufParser} and {@link ProtoBufRecord}
     24 * @author Taylor Smock
     25 * @since xxx
     26 */
     27class ProtoBufTest {
     28    /**
     29     * Test simple message.
     30     * Check that a simple message is readable
     31     * @throws IOException - if an IO error occurs
     32     */
     33    @Test
     34    void testSimpleMessage() throws IOException {
     35        ProtoBufParser parser = new ProtoBufParser(new byte[] {(byte) 0x08, (byte) 0x96, (byte) 0x01});
     36        ProtoBufRecord record = new ProtoBufRecord(parser);
     37        assertEquals(WireType.VARINT, record.getType());
     38        assertEquals(150, record.asUnsignedVarInt().intValue());
     39    }
     40    /**
     41     * Test reading tile from Mapillary ( 14/3251/6258 )
     42     * @throws IOException if there is a problem reading the file
     43     */
     44    @Test
     45    void testRead_14_3251_6258() throws IOException {
     46        File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "6258.mvt").toFile();
     47        InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile);
     48        Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords();
     49        assertEquals(2, records.size());
     50        List<Layer> layers = new ArrayList<>();
     51        for (ProtoBufRecord record : records) {
     52            if (record.getField() == Layer.LAYER_FIELD) {
     53                layers.add(new Layer(record.getBytes()));
     54            } else {
     55                fail(MessageFormat.format("Invalid field {0}", record.getField()));
     56            }
     57        }
     58        Layer mapillarySequences = layers.get(0);
     59        Layer mapillaryPictures = layers.get(1);
     60        assertEquals("mapillary-sequences", mapillarySequences.getName());
     61        assertEquals("mapillary-images", mapillaryPictures.getName());
     62        assertEquals(8192, mapillarySequences.getExtent());
     63        assertEquals(8192, mapillaryPictures.getExtent());
     64
     65        assertEquals(1, mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 241083111).count());
     66        Feature testSequence = mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 241083111).findAny().orElse(null);
     67        assertEquals("jgxkXqVFM4jepMG3vP5Q9A", testSequence.getTags().get("key"));
     68        assertEquals("C15Ul6qVMfQFlzRcmQCLcA", testSequence.getTags().get("ikey"));
     69        assertEquals("x0hTY8cakpy0m3ui1GaG1A", testSequence.getTags().get("userkey"));
     70        assertEquals(Long.valueOf(1565196718638L), Long.valueOf(testSequence.getTags().get("captured_at")));
     71        assertEquals(0, Integer.parseInt(testSequence.getTags().get("pano")));
     72    }
     73}