Ticket #17177: 17177.patch

File 17177.patch, 37.9 KB (added by taylor.smock, 5 years ago)

WIP Patch. DO NOT APPLY. Parsing of Mapbox Vector Tiles partially works. Packed fields do not yet work (specifically, tags and geometry)

  • test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java

     
     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     */
     32    @Test
     33    void testSimpleMessage() {
     34        ProtoBufParser parser = new ProtoBufParser(new byte[] {(byte) 0x08, (byte) 0x96, (byte) 0x01});
     35        ProtoBufRecord record = new ProtoBufRecord(parser);
     36        assertEquals(WireType.VARINT, record.getType());
     37        assertEquals(150, record.asUnsignedVarInt().intValue());
     38    }
     39    /**
     40     * Test reading tile from Mapillary ( 14/3251/6258 )
     41     * @throws IOException if there is a problem reading the file
     42     */
     43    @Test
     44    void testRead_14_3251_6258() throws IOException {
     45        File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "6258.mvt").toFile();
     46        InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile);
     47        Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords();
     48        assertEquals(2, records.size());
     49        List<Layer> layers = new ArrayList<>();
     50        for (ProtoBufRecord record : records) {
     51            if (record.getField() == Layer.LAYER_FIELD) {
     52                layers.add(new Layer(record.getBytes()));
     53            } else {
     54                fail(MessageFormat.format("Invalid field {0}", record.getField()));
     55            }
     56        }
     57        Layer mapillarySequences = layers.get(0);
     58        Layer mapillaryPictures = layers.get(1);
     59        assertEquals("mapillary-sequences", mapillarySequences.getName());
     60        assertEquals("mapillary-images", mapillaryPictures.getName());
     61        assertEquals(8192, mapillarySequences.getExtent());
     62        assertEquals(8192, mapillaryPictures.getExtent());
     63
     64        assertEquals(1, mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 241083111).count());
     65        Feature testSequence = mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 241083111).findAny().orElse(null);
     66        assertEquals("jgxkXqVFM4jepMG3vP5Q9A", testSequence.getTags().get("key"));
     67        assertEquals("C15Ul6qVMfQFlzRcmQCLcA", testSequence.getTags().get("ikey"));
     68        assertEquals("x0hTY8cakpy0m3ui1GaG1A", testSequence.getTags().get("userkey"));
     69        assertEquals(Long.valueOf(1565196718638L), Long.valueOf(testSequence.getTags().get("captured_at")));
     70        assertEquals(0, Integer.parseInt(testSequence.getTags().get("pano")));
     71    }
     72}
  • src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java

     
     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 int size;
     16    private final Number[] numbers;
     17    /**
     18     * Create a new ProtoBufPacked object
     19     * @param bytes The packed bytes
     20     */
     21    public ProtoBufPacked(byte[] bytes) {
     22        this.bytes = bytes;
     23        this.size = ProtoBufParser.convertByteArray(this.nextVarInt()).intValue();
     24        List<Number> numbersT = new ArrayList<>();
     25        while (this.location < bytes.length) {
     26            numbersT.add(ProtoBufParser.convertByteArray(this.nextVarInt()));
     27        }
     28
     29        this.numbers = new Number[numbersT.size()];
     30        for (int i = 0; i < numbersT.size(); i++) {
     31            this.numbers[i] = numbersT.get(i);
     32        }
     33    }
     34
     35    /**
     36     * The number of expected values
     37     * @return The expected values
     38     */
     39    public int size() {
     40        return this.size;
     41    }
     42
     43    /**
     44     * Get the parsed number array
     45     * @return The number array
     46     */
     47    public Number[] getArray() {
     48        return this.numbers;
     49    }
     50
     51    /**
     52     * Check if this is actually packed
     53     * @return {@code true} if the size equals the array size
     54     */
     55    public boolean isPacked() {
     56        return this.numbers.length == this.size;
     57    }
     58
     59    private byte[] nextVarInt() {
     60        List<Byte> byteList = new ArrayList<>();
     61        while ((this.bytes[this.location] & ProtoBufParser.MOST_SIGNIFICANT_BYTE) == ProtoBufParser.MOST_SIGNIFICANT_BYTE) {
     62            // Get rid of the leading bit (shift left 1, then shift right 1 unsigned)
     63            byteList.add((byte) (this.bytes[this.location++] ^ ProtoBufParser.MOST_SIGNIFICANT_BYTE));
     64        }
     65        // The last byte doesn't drop the most significant bit
     66        byteList.add(this.bytes[this.location++]);
     67        byte[] byteArray = new byte[byteList.size()];
     68        for (int i = 0; i < byteList.size(); i++) {
     69            byteArray[i] = byteList.get(i);
     70        }
     71
     72        return byteArray;
     73    }
     74}
  • src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import java.io.IOException;
     5import java.io.InputStream;
     6import java.util.ArrayList;
     7import java.util.Collection;
     8import java.util.List;
     9
     10import org.apache.commons.io.IOUtils;
     11
     12/**
     13 * A basic Protobuf parser
     14 * @author Taylor Smock
     15 * @since xxx
     16 */
     17public class ProtoBufParser {
     18    /**
     19     * Used to get the most significant byte
     20     */
     21    static final byte MOST_SIGNIFICANT_BYTE = (byte) (1 << 7);
     22    // TODO switch to a better parser
     23    private final byte[] bytes;
     24    private int location;
     25    /**
     26     * Create a new parser
     27     * @param bytes The bytes to parse
     28     */
     29    public ProtoBufParser(byte[] bytes) {
     30        this.bytes = bytes;
     31        this.location = 0;
     32    }
     33
     34    /**
     35     * Create a new parser
     36     * @param inputStream The inputstream (will be fully read at this time)
     37     * @throws IOException If an exception occurs in {@link IOUtils#toByteArray(InputStream)}.
     38     */
     39    public ProtoBufParser(InputStream inputStream) throws IOException {
     40        this(IOUtils.toByteArray(inputStream));
     41    }
     42
     43    /**
     44     * Get the "next" wiretype
     45     * @return {@link WireType} expected
     46     */
     47    public WireType next() {
     48        // TODO is this right?
     49        return WireType.values()[this.bytes[this.location] << 3];
     50    }
     51
     52    /**
     53     * Get the next byte
     54     * @return The next byte
     55     */
     56    public byte nextByte() {
     57        return this.bytes[this.location++];
     58    }
     59
     60    /**
     61     * Check if there is more data to read
     62     * @return {@code true} if there is more data to read
     63     */
     64    public boolean hasNext() {
     65        return this.bytes.length > this.location;
     66    }
     67
     68    /**
     69     * Get the next var int ({@code WireType#VARINT})
     70     * @return The next var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum})
     71     */
     72    public byte[] nextVarInt() {
     73        List<Byte> byteList = new ArrayList<>();
     74        while ((this.bytes[this.location] & MOST_SIGNIFICANT_BYTE) == MOST_SIGNIFICANT_BYTE) {
     75            // Get rid of the leading bit (shift left 1, then shift right 1 unsigned)
     76            byteList.add((byte) (this.nextByte() ^ MOST_SIGNIFICANT_BYTE));
     77        }
     78        // The last byte doesn't drop the most significant bit
     79        byteList.add(this.nextByte());
     80        byte[] byteArray = new byte[byteList.size()];
     81        for (int i = 0; i < byteList.size(); i++) {
     82            byteArray[i] = byteList.get(i);
     83        }
     84
     85        return byteArray;
     86    }
     87
     88    /**
     89     * Get the next 32 bits ({@link WireType#THIRTY_TWO_BIT})
     90     * @return a byte array of the next 32 bits (4 bytes)
     91     */
     92    public byte[] nextFixed32() {
     93        // 4 bytes == 32 bits
     94        return readNextBytes(4);
     95    }
     96
     97    /**
     98     * Get the next 64 bits ({@link WireType#SIXTY_FOUR_BIT})
     99     * @return a byte array of the next 64 bits (8 bytes)
     100     */
     101    public byte[] nextFixed64() {
     102        // 8 bytes == 64 bits
     103        return readNextBytes(8);
     104    }
     105
     106    /**
     107     * Read an arbitrary number of bytes
     108     * @param size The number of bytes to read
     109     * @return a byte array of the specified size, filled with bytes read
     110     */
     111    private byte[] readNextBytes(int size) {
     112        byte[] bytesRead = new byte[size];
     113        for (int i = 0; i < bytesRead.length; i++) {
     114            bytesRead[i] = this.nextByte();
     115        }
     116        return bytesRead;
     117    }
     118
     119    /**
     120     * Get the next delimited message ({@link WireType#LENGTH_DELIMITED})
     121     * @return The next length delimited message
     122     */
     123    public byte[] nextLengthDelimited() {
     124        int length = convertByteArray(this.nextVarInt()).intValue();
     125        return readNextBytes(length);
     126    }
     127
     128    /**
     129     * Convert a byte array to a number (little endian)
     130     * @param bytes The bytes to convert
     131     * @return An appropriate {@link Number} class.
     132     */
     133    public static Number convertByteArray(byte[] bytes) {
     134        long number = 0;
     135        for (int i = 0; i < bytes.length; i++) {
     136            number += bytes[i] << 7 * i;
     137        }
     138        return convertLong(number);
     139    }
     140
     141    /**
     142     * Convert a long to an appropriate {@link Number} class
     143     * @param number The long to convert
     144     * @return A {@link Number}
     145     */
     146    public static Number convertLong(long number) {
     147        // TODO deal with booleans
     148        if (number <= Byte.MAX_VALUE && number >= Byte.MIN_VALUE) {
     149            return Byte.valueOf((byte) number);
     150        } else if (number <= Short.MAX_VALUE && number >= Short.MIN_VALUE) {
     151            return Short.valueOf((short) number);
     152        } else if (number <= Integer.MAX_VALUE && number >= Integer.MIN_VALUE) {
     153            return Integer.valueOf((int) number);
     154        } else if (number <= Long.MAX_VALUE && number >= Long.MIN_VALUE) {
     155            return Long.valueOf(number);
     156        }
     157        return number;
     158    }
     159
     160    /**
     161     * Read all records
     162     * @return A collection of all records
     163     */
     164    public Collection<ProtoBufRecord> allRecords() {
     165        Collection<ProtoBufRecord> records = new ArrayList<>();
     166        while (this.hasNext()) {
     167            records.add(new ProtoBufRecord(this));
     168        }
     169        return records;
     170    }
     171}
  • src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java

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

     
     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}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java

     
     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    MoveTo((byte) 1, (byte) 2),
     11    LineTo((byte) 2, (byte) 2),
     12    ClosePath((byte) 7, (byte) 0);
     13
     14    private final byte id;
     15    private final byte parameters;
     16
     17    Command(byte id, byte parameters) {
     18        this.id = id;
     19        this.parameters = parameters;
     20    }
     21
     22    /**
     23     * Get the command id
     24     * @return The id
     25     */
     26    public byte getId() {
     27        return this.id;
     28    }
     29
     30    /**
     31     * Get the number of parameters
     32     * @return The number of parameters
     33     */
     34    public byte getParameterNumber() {
     35        return this.parameters;
     36    }
     37}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import java.util.stream.Stream;
     5
     6/**
     7 * An indicator for a command to be executed
     8 * @author Taylor Smock
     9 * @since xxx
     10 */
     11public class CommandInteger {
     12    private final Command type;
     13    private final int operations;
     14
     15    /**
     16     * Create a new command
     17     * @param command the command (treated as an unsigned int)
     18     */
     19    public CommandInteger(final int command) {
     20        long unsigned = Integer.toUnsignedLong(command);
     21        this.type = Stream.of(Command.values()).filter(e -> e.getId() == (unsigned & 0x7)).findAny()
     22                .orElseThrow(InvalidMapboxVectorTileException::new);
     23        // This is safe, since we are shifting right 3 when we converted an int to a long (for unsigned).
     24        // So we <i>cannot</i> lose anything.
     25        this.operations = (int) (unsigned >> 3);
     26    }
     27}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import java.util.ArrayList;
     5import java.util.List;
     6
     7import org.openstreetmap.josm.data.osm.TagMap;
     8import org.openstreetmap.josm.data.protobuf.ProtoBufPacked;
     9import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
     10import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
     11import org.openstreetmap.josm.tools.Utils;
     12
     13/**
     14 * A Feature for a {@link Layer}
     15 * @author Taylor Smock
     16 * @since xxx
     17 */
     18public class Feature {
     19    private static final byte ID_FIELD = 1;
     20    private static final byte TAG_FIELD = 2;
     21    private static final byte GEOMETRY_TYPE_FIELD = 3;
     22    private static final byte GEOMETRY_FIELD = 4;
     23    /** The geometry of the feature. Required. */
     24    private final List<Object> geometry = new ArrayList<>();
     25
     26    /** The geometry type of the feature. Required. */
     27    private final GeometryTypes geometryType;
     28
     29    /** The tags of the feature. Optional. */
     30    private TagMap tags;
     31
     32    /** The id of the feature. Optional. */
     33    // Technically, uint64
     34    private final long id;
     35
     36    /**
     37     * Create a new Feature
     38     * @param layer The layer the feature is part of (required for tags)
     39     * @param record The record to create the feature from
     40     */
     41    public Feature(Layer layer, ProtoBufRecord record) {
     42        ProtoBufParser parser = new ProtoBufParser(record.getBytes());
     43        long tId = 0;
     44        GeometryTypes geometryTypeTemp = GeometryTypes.UNKNOWN;
     45        String key = null;
     46        while (parser.hasNext()) {
     47            ProtoBufRecord next = new ProtoBufRecord(parser);
     48            if (next.getField() == TAG_FIELD) {
     49                if (tags == null) {
     50                    tags = new TagMap();
     51                }
     52                // This is packed in v1 and v2
     53                ProtoBufPacked packed = new ProtoBufPacked(next.getBytes());
     54                if (packed.isPacked()) {
     55                    for (Number number : packed.getArray()) {
     56                        if (key == null) {
     57                            key = layer.getKey(number.intValue());
     58                        } else {
     59                            Object value = layer.getValue(number.intValue());
     60                            this.tags.put(key, Utils.intern(value.toString()));
     61                            key = null;
     62                        }
     63                   }
     64                } else {
     65                    if (key == null) {
     66                        key = layer.getKey(next.asSignedVarInt().intValue());
     67                    } else {
     68                        Object value = layer.getValue(next.asSignedVarInt().intValue());
     69                        this.tags.put(key, Utils.intern(value.toString()));
     70                        key = null;
     71                    }
     72                }
     73            } else if (next.getField() == GEOMETRY_FIELD) {
     74                // This is packed in v1 and v2
     75                ProtoBufPacked packed = new ProtoBufPacked(next.getBytes());
     76                // TODO fallback to non-packed
     77            } else if (next.getField() == GEOMETRY_TYPE_FIELD) {
     78                geometryTypeTemp = GeometryTypes.values()[next.asUnsignedVarInt().intValue()];
     79            } else if (next.getField() == ID_FIELD) {
     80                tId = next.asUnsignedVarInt().longValue();
     81            }
     82        }
     83        this.id = tId;
     84        this.geometryType = geometryTypeTemp;
     85    }
     86
     87    /**
     88     * Get the geometry instructions
     89     * @return The geometry
     90     */
     91    public List<Object> getGeometry() {
     92        return this.geometry;
     93    }
     94
     95    /**
     96     * Get the geometry type
     97     * @return The {@link GeometryTypes}
     98     */
     99    public GeometryTypes getGeometryType() {
     100        return this.geometryType;
     101    }
     102
     103    /**
     104     * Get the id of the object
     105     * @return The unique id in the layer, or 0.
     106     */
     107    public long getId() {
     108        return this.id;
     109    }
     110
     111    /**
     112     * Get the tags
     113     * @return A tag map
     114     */
     115    public TagMap getTags() {
     116        return this.tags;
     117    }
     118}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java

     
     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}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java

     
     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}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java

     
     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.util.ArrayList;
     6import java.util.Arrays;
     7import java.util.Collection;
     8import java.util.Collections;
     9import java.util.List;
     10import java.util.function.Function;
     11import java.util.stream.Collectors;
     12
     13import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
     14import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
     15import org.openstreetmap.josm.data.protobuf.WireType;
     16
     17/**
     18 * A Mapbox Vector Tile Layer
     19 * @author Taylor Smock
     20 * @since xxx
     21 */
     22public class Layer {
     23    private static final class ValueFields<T> {
     24        static final ValueFields<String> STRING = new ValueFields<>(1, ProtoBufRecord::asString);
     25        static final ValueFields<Float> FLOAT = new ValueFields<>(2, ProtoBufRecord::asFloat);
     26        static final ValueFields<Double> DOUBLE = new ValueFields<>(3, ProtoBufRecord::asDouble);
     27        static final ValueFields<Number> INT64 = new ValueFields<>(4, ProtoBufRecord::asUnsignedVarInt);
     28        // This may have issues if there are actual uint_values (i.e., more than {@link Long#MAX_VALUE})
     29        static final ValueFields<Number> UINT64 = new ValueFields<>(5, ProtoBufRecord::asUnsignedVarInt);
     30        static final ValueFields<Number> SINT64 = new ValueFields<>(6, ProtoBufRecord::asSignedVarInt);
     31        static final ValueFields<Boolean> BOOL = new ValueFields<>(7, r -> r.asUnsignedVarInt().longValue() != 0);
     32
     33        public static final Collection<ValueFields<?>> MAPPERS = Arrays.asList(STRING, FLOAT, DOUBLE, INT64, UINT64, SINT64, BOOL);
     34
     35        private final byte field;
     36        private final Function<ProtoBufRecord, T> conversion;
     37        private ValueFields(int field, Function<ProtoBufRecord, T> conversion) {
     38            this.field = (byte) field;
     39            this.conversion = conversion;
     40        }
     41
     42        /**
     43         * Get the field identifier for the value
     44         * @return The identifier
     45         */
     46        public byte getField() {
     47            return this.field;
     48        }
     49
     50        /**
     51         * Convert a protobuf record to a value
     52         * @param protobufRecord The record to convert
     53         * @return the converted value
     54         */
     55        public T convertValue(ProtoBufRecord protobufRecord) {
     56            return this.conversion.apply(protobufRecord);
     57        }
     58    }
     59
     60    /** The field value for a layer (in {@link ProtoBufRecord#getField}) */
     61    public static final byte LAYER_FIELD = 3;
     62    private static final byte VERSION_FIELD = 15;
     63    private static final byte NAME_FIELD = 1;
     64    private static final byte FEATURE_FIELD = 2;
     65    private static final byte KEY_FIELD = 3;
     66    private static final byte VALUE_FIELD = 4;
     67    private static final byte EXTENT_FIELD = 5;
     68    private static final int DEFAULT_EXTENT = 4096;
     69    private static final byte DEFAULT_VERSION = 1;
     70    /** This is <i>technically</i> an integer, but there are currently only two major versions (1, 2). Required. */
     71    private final byte version;
     72    /** A unique name for the layer. This <i>must</i> be unique on a per-tile basis. Required. */
     73    private final String name;
     74
     75    /** The extent of the tile, typically 4096. Required. */
     76    private final int extent;
     77
     78    /** A list of unique keys. Order is important. Optional. */
     79    private final List<String> keyList = new ArrayList<>();
     80    /** A list of unique values. Order is important. Optional. */
     81    private final List<Object> valueList = new ArrayList<>();
     82    /** The actual features of this layer in this tile */
     83    private final List<Feature> featureCollection;
     84
     85    /**
     86     * Create a layer from a collection of records
     87     * @param records The records to convert to a layer
     88     */
     89    public Layer(Collection<ProtoBufRecord> records) {
     90        // Do the unique required fields first
     91        this.version = records.stream().parallel()
     92                .filter(record -> record.getType() == WireType.VARINT && record.getField() == VERSION_FIELD)
     93                .map(ProtoBufRecord::asSignedVarInt).map(Number::byteValue)
     94                .findFirst().orElse(DEFAULT_VERSION);
     95        // Per spec, we cannot continue past this until we have checked the version number
     96        if (this.version != 1 && this.version != 2) {
     97            throw new IllegalArgumentException(tr("We do not understand version {0} of the vector tile specification", this.version));
     98        }
     99        this.name = records.stream().parallel()
     100                .filter(record -> record.getType() == WireType.LENGTH_DELIMITED && record.getField() == NAME_FIELD)
     101                .map(ProtoBufRecord::asString)
     102                .findFirst()
     103                .orElseThrow(() -> new IllegalArgumentException(tr("Vector tile layers must have a layer name")));
     104        this.extent = records.stream().parallel().filter(record -> record.getType() == WireType.VARINT && record.getField() == EXTENT_FIELD).map(ProtoBufRecord::asSignedVarInt).map(Number::intValue).findAny().orElse(DEFAULT_EXTENT);
     105
     106        List<ProtoBufRecord> features = new ArrayList<>();
     107        for (ProtoBufRecord record : records) {
     108            if (record.getField() == FEATURE_FIELD) {
     109                features.add(record);
     110            } else if (record.getField() == KEY_FIELD) {
     111                this.keyList.add(record.asString());
     112            } else if (record.getField() == VALUE_FIELD && record.getType() == WireType.LENGTH_DELIMITED) {
     113                ProtoBufParser parser = new ProtoBufParser(record.getBytes());
     114                ProtoBufRecord valueRecord = new ProtoBufRecord(parser);
     115                if (parser.hasNext()) {
     116                    throw new IllegalArgumentException(tr("Vector tile value fields are restricted to one value"));
     117                }
     118                this.valueList.add(ValueFields.MAPPERS.stream()
     119                        .filter(v -> v.getField() == valueRecord.getField())
     120                        .map(v -> v.convertValue(valueRecord)).findFirst()
     121                        .orElseThrow(() -> new IllegalArgumentException(tr("Unknown field in vector tile layer value ({0})", valueRecord.getField()))));
     122            } else if (record.getField() == VALUE_FIELD) {
     123                throw new IllegalArgumentException(tr("We do not under a value field in the vector tile"));
     124            }
     125        }
     126        this.featureCollection = features.parallelStream().map(feature -> new Feature(this, feature)).collect(Collectors.toList());
     127    }
     128
     129    /**
     130     * Create a new layer
     131     * @param bytes The bytes that the layer comes from
     132     */
     133    public Layer(byte[] bytes) {
     134        this(new ProtoBufParser(bytes).allRecords());
     135    }
     136
     137    /**
     138     * Get the extent of the tile
     139     * @return The layer extent
     140     */
     141    public int getExtent() {
     142        return this.extent;
     143    }
     144
     145    /**
     146     * Get the feature on this layer
     147     * @return the features
     148     */
     149    public Collection<Feature> getFeatures() {
     150        return Collections.unmodifiableCollection(this.featureCollection);
     151    }
     152
     153    /**
     154     * Get a specified key
     155     * @param index The index in the key list
     156     * @return The actual key
     157     */
     158    public String getKey(int index) {
     159        return this.keyList.get(index);
     160    }
     161
     162    /**
     163     * Get the name of the layer
     164     * @return The layer name
     165     */
     166    public String getName() {
     167        return this.name;
     168    }
     169
     170    /**
     171     * Get a specified value
     172     * @param index The index in the value list
     173     * @return The actual value. This can be a {@link String}, {@link Boolean}, {@link Integer}, or {@link Float} value.
     174     */
     175    public Object getValue(int index) {
     176        return this.valueList.get(index);
     177    }
     178}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java

     
     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}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/ParameterInteger.java

     
     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 final int value;
     11    /**
     12     * Create a new @link{ParameterInteger}
     13     * @param value The value to convert to a ParameterInteger (zigzag encoded).
     14     */
     15    public ParameterInteger(final int value) {
     16        this.value = ((value >> 1) ^ (-(value & 1)));
     17    }
     18
     19    /**
     20     * Get the value for this ParameterInteger
     21     * @return The decoded integer value
     22     */
     23    public int getValue() {
     24        return this.value;
     25    }
     26}