Ticket #17177: 17177.patch
| File 17177.patch, 37.9 KB (added by , 5 years ago) |
|---|
-
test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java
1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.protobuf; 3 4 import static org.junit.jupiter.api.Assertions.assertEquals; 5 import static org.junit.jupiter.api.Assertions.fail; 6 7 import java.io.File; 8 import java.io.IOException; 9 import java.io.InputStream; 10 import java.nio.file.Paths; 11 import java.text.MessageFormat; 12 import java.util.ArrayList; 13 import java.util.Collection; 14 import java.util.List; 15 16 import org.junit.jupiter.api.Test; 17 import org.openstreetmap.josm.TestUtils; 18 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Feature; 19 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer; 20 import org.openstreetmap.josm.io.Compression; 21 22 /** 23 * Test class for {@link ProtoBufParser} and {@link ProtoBufRecord} 24 * @author Taylor Smock 25 * @since xxx 26 */ 27 class 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. 2 package org.openstreetmap.josm.data.protobuf; 3 4 import java.util.ArrayList; 5 import java.util.List; 6 7 /** 8 * Parse packed values (only numerical values) 9 * @author Taylor Smock 10 * @since xxx 11 */ 12 public 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. 2 package org.openstreetmap.josm.data.protobuf; 3 4 import java.io.IOException; 5 import java.io.InputStream; 6 import java.util.ArrayList; 7 import java.util.Collection; 8 import java.util.List; 9 10 import org.apache.commons.io.IOUtils; 11 12 /** 13 * A basic Protobuf parser 14 * @author Taylor Smock 15 * @since xxx 16 */ 17 public 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. 2 package org.openstreetmap.josm.data.protobuf; 3 import static org.openstreetmap.josm.tools.I18n.tr; 4 5 import java.nio.charset.StandardCharsets; 6 import java.util.stream.Stream; 7 8 import 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 */ 15 public 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. 2 package org.openstreetmap.josm.data.protobuf; 3 4 /** 5 * The WireTypes 6 * @author Taylor Smock 7 * @since xxx 8 */ 9 public 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. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 /** 5 * Command integers for Mapbox Vector Tiles 6 * @author Taylor Smock 7 * @since xxx 8 */ 9 public 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. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import java.util.stream.Stream; 5 6 /** 7 * An indicator for a command to be executed 8 * @author Taylor Smock 9 * @since xxx 10 */ 11 public 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. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import java.util.ArrayList; 5 import java.util.List; 6 7 import org.openstreetmap.josm.data.osm.TagMap; 8 import org.openstreetmap.josm.data.protobuf.ProtoBufPacked; 9 import org.openstreetmap.josm.data.protobuf.ProtoBufParser; 10 import org.openstreetmap.josm.data.protobuf.ProtoBufRecord; 11 import org.openstreetmap.josm.tools.Utils; 12 13 /** 14 * A Feature for a {@link Layer} 15 * @author Taylor Smock 16 * @since xxx 17 */ 18 public 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. 2 package 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 */ 9 public 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. 2 package 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 */ 10 public 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. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 import static org.openstreetmap.josm.tools.I18n.tr; 4 5 import java.util.ArrayList; 6 import java.util.Arrays; 7 import java.util.Collection; 8 import java.util.Collections; 9 import java.util.List; 10 import java.util.function.Function; 11 import java.util.stream.Collectors; 12 13 import org.openstreetmap.josm.data.protobuf.ProtoBufParser; 14 import org.openstreetmap.josm.data.protobuf.ProtoBufRecord; 15 import org.openstreetmap.josm.data.protobuf.WireType; 16 17 /** 18 * A Mapbox Vector Tile Layer 19 * @author Taylor Smock 20 * @since xxx 21 */ 22 public 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. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import java.util.Arrays; 5 import 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 */ 12 public 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. 2 package 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 */ 9 public 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 }
