Ticket #22603: 22603.patch
File 22603.patch, 95.6 KB (added by , 3 years ago) |
---|
-
src/org/openstreetmap/josm/actions/ExtensionFileFilter.java
Subject: [PATCH] OSM PBF --- IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/actions/ExtensionFileFilter.java b/src/org/openstreetmap/josm/actions/ExtensionFileFilter.java
a b 27 27 import org.openstreetmap.josm.gui.io.importexport.NoteImporter; 28 28 import org.openstreetmap.josm.gui.io.importexport.OsmChangeImporter; 29 29 import org.openstreetmap.josm.gui.io.importexport.OsmImporter; 30 import org.openstreetmap.josm.gui.io.importexport.OsmPbfImporter; 30 31 import org.openstreetmap.josm.gui.io.importexport.OziWptImporter; 31 32 import org.openstreetmap.josm.gui.io.importexport.RtkLibImporter; 32 33 import org.openstreetmap.josm.gui.io.importexport.WMSLayerImporter; … … 65 66 final List<Class<? extends FileImporter>> importerNames = Arrays.asList( 66 67 OsmImporter.class, 67 68 OsmChangeImporter.class, 69 OsmPbfImporter.class, 68 70 GeoJSONImporter.class, 69 71 GpxImporter.class, 70 72 NMEAImporter.class, -
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
a b 49 49 this.parameters[added++] = parameterInteger.shortValue(); 50 50 } 51 51 52 /** 53 * Add a parameter 54 * @param parameterInteger The parameter to add (converted to {@code short}). 55 */ 56 public void addParameter(long parameterInteger) { 57 this.parameters[added++] = (short) parameterInteger; 58 } 59 52 60 /** 53 61 * Get the operations for the command 54 62 * @return The operations -
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
a b 27 27 private static final byte GEOMETRY_FIELD = 4; 28 28 /** 29 29 * The number format instance to use (using a static instance gets rid of quite o few allocations) 30 * Doing this reduced the allocations of {@link #parseTagValue(String, Layer, Number, List)} from 22.79% of parent to30 * Doing this reduced the allocations of {@link #parseTagValue(String, Layer, int, List)} from 22.79% of parent to 31 31 * 12.2% of parent. 32 32 */ 33 33 private static final NumberFormat NUMBER_FORMAT = NumberFormat.getNumberInstance(Locale.ROOT); … … 74 74 try (ProtobufRecord next = new ProtobufRecord(byteArrayOutputStream, parser)) { 75 75 if (next.getField() == TAG_FIELD) { 76 76 // This is packed in v1 and v2 77 ProtobufPacked packed = new ProtobufPacked( byteArrayOutputStream,next.getBytes());77 ProtobufPacked packed = new ProtobufPacked(next.getBytes()); 78 78 if (tagList == null) { 79 79 tagList = new ArrayList<>(packed.getArray().length); 80 80 } else { 81 81 tagList.ensureCapacity(tagList.size() + packed.getArray().length); 82 82 } 83 for ( Numbernumber : packed.getArray()) {84 key = parseTagValue(key, layer, number, tagList);83 for (long number : packed.getArray()) { 84 key = parseTagValue(key, layer, (int) number, tagList); 85 85 } 86 86 } else if (next.getField() == GEOMETRY_FIELD) { 87 87 // This is packed in v1 and v2 88 ProtobufPacked packed = new ProtobufPacked( byteArrayOutputStream,next.getBytes());88 ProtobufPacked packed = new ProtobufPacked(next.getBytes()); 89 89 CommandInteger currentCommand = null; 90 for ( Numbernumber : packed.getArray()) {90 for (long number : packed.getArray()) { 91 91 if (currentCommand != null && currentCommand.hasAllExpectedParameters()) { 92 92 currentCommand = null; 93 93 } 94 94 if (currentCommand == null) { 95 currentCommand = new CommandInteger( number.intValue());95 currentCommand = new CommandInteger(Math.toIntExact(number)); 96 96 this.geometry.add(currentCommand); 97 97 } else { 98 98 currentCommand.addParameter(ProtobufParser.decodeZigZag(number)); … … 127 127 * @param tagList The list to add the new value to 128 128 * @return The new key (if {@code null}, then a value was parsed and added to tags) 129 129 */ 130 private static String parseTagValue(String key, Layer layer, Numbernumber, List<String> tagList) {130 private static String parseTagValue(String key, Layer layer, int number, List<String> tagList) { 131 131 if (key == null) { 132 key = layer.getKey(number .intValue());132 key = layer.getKey(number); 133 133 } else { 134 134 tagList.add(key); 135 Object value = layer.getValue(number .intValue());135 Object value = layer.getValue(number); 136 136 if (value instanceof Double || value instanceof Float) { 137 137 // reset grouping if the instance is a singleton 138 138 -
new file src/org/openstreetmap/josm/data/osm/pbf/Blob.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/osm/pbf/Blob.java b/src/org/openstreetmap/josm/data/osm/pbf/Blob.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.osm.pbf; 3 4 import java.io.ByteArrayInputStream; 5 import java.io.IOException; 6 import java.io.InputStream; 7 import java.util.zip.InflaterInputStream; 8 9 import javax.annotation.Nonnull; 10 import javax.annotation.Nullable; 11 12 import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; 13 import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream; 14 import org.apache.commons.compress.compressors.zstandard.ZstdCompressorInputStream; 15 16 /** 17 * A "Blob" of data from an OSM PBF file. It, in turn, contains additional data in PBF format, which may be compressed. 18 */ 19 public final class Blob { 20 public enum CompressionType { 21 /** No compression */ 22 raw, 23 /** zlib compression */ 24 zlib, 25 /** lzma compression (optional) */ 26 lzma, 27 /** bzip2 compression (deprecated in 2010, so if we ever support saving PBF files, <i>don't use this compression type</i>) */ 28 bzip2, 29 /** lz4 compression (optional) */ 30 lz4, 31 /** zstd compression (optional) */ 32 zstd 33 } 34 35 private final Integer rawSize; 36 private final CompressionType compressionType; 37 private final byte[] bytes; 38 public Blob(@Nullable Integer rawSize, @Nonnull CompressionType compressionType, @Nonnull byte... bytes) { 39 this.rawSize = rawSize; 40 this.compressionType = compressionType; 41 this.bytes = bytes; 42 } 43 44 /** 45 * The raw size of the blob (after decompression) 46 * @return The raw size 47 */ 48 @Nullable 49 public Integer rawSize() { 50 return this.rawSize; 51 } 52 53 /** 54 * The compression type of the blob 55 * @return The compression type 56 */ 57 @Nonnull 58 public CompressionType compressionType() { 59 return this.compressionType; 60 } 61 62 /** 63 * The bytes that make up the blob data 64 * @return The bytes 65 */ 66 @Nonnull 67 public byte[] bytes() { 68 return this.bytes; 69 } 70 71 /** 72 * Get the decompressed inputstream for this blob 73 * @return The decompressed inputstream 74 * @throws IOException if we don't support the compression type <i>or</i> the decompressor has issues, see 75 * <ul> 76 * <li>{@link LZMACompressorInputStream}</li> 77 * <li>{@link ZstdCompressorInputStream}</li> 78 * <li>{@link BZip2CompressorInputStream}</li> 79 * </ul> 80 */ 81 @Nonnull 82 public InputStream inputStream() throws IOException { 83 final ByteArrayInputStream bais = new ByteArrayInputStream(this.bytes); 84 switch (this.compressionType) { 85 case raw: 86 return bais; 87 case lzma: 88 return new LZMACompressorInputStream(bais); 89 case zstd: 90 return new ZstdCompressorInputStream(bais); 91 case bzip2: 92 return new BZip2CompressorInputStream(bais); 93 case lz4: 94 throw new IOException("lz4 pbf is not currently supported"); 95 case zlib: 96 return new InflaterInputStream(bais); 97 default: 98 throw new IOException("unknown compression type is not currently supported: " + this.compressionType.name()); 99 } 100 } 101 } -
new file src/org/openstreetmap/josm/data/osm/pbf/BlobHeader.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/osm/pbf/BlobHeader.java b/src/org/openstreetmap/josm/data/osm/pbf/BlobHeader.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.osm.pbf; 3 4 import javax.annotation.Nonnull; 5 import javax.annotation.Nullable; 6 7 /** 8 * A "BlobHeader" which contains metadata for a {@link Blob}. 9 */ 10 public final class BlobHeader { 11 private final String type; 12 private final byte[] indexData; 13 private final int dataSize; 14 15 public BlobHeader(@Nonnull String type, @Nullable byte[] indexData, int dataSize) { 16 this.type = type; 17 this.indexData = indexData; 18 this.dataSize = dataSize; 19 } 20 21 public String type() { 22 return this.type; 23 } 24 25 public byte[] indexData() { 26 return this.indexData; 27 } 28 29 public int dataSize() { 30 return this.dataSize; 31 } 32 } -
new file src/org/openstreetmap/josm/data/osm/pbf/HeaderBlock.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/osm/pbf/HeaderBlock.java b/src/org/openstreetmap/josm/data/osm/pbf/HeaderBlock.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.osm.pbf; 3 4 import javax.annotation.Nonnull; 5 import javax.annotation.Nullable; 6 7 import org.openstreetmap.josm.data.osm.BBox; 8 9 /** 10 * The header block contains data on required features, optional features, the bbox of the data, the source, the osmosis replication timestamp, 11 * the osmosis replication sequence number, and the osmosis replication base url 12 */ 13 public final class HeaderBlock { 14 private final BBox bbox; 15 private final String[] requiredFeatures; 16 private final String[] optionalFeatures; 17 private final String writingProgram; 18 private final String source; 19 private final Long osmosisReplicationTimestamp; 20 private final Long osmosisReplicationSequenceNumber; 21 private final String osmosisReplicationBaseUrl; 22 23 /** 24 * Create a new {@link HeaderBlock} for an OSM PBF file 25 * @param bbox The bbox 26 * @param requiredFeatures The required features 27 * @param optionalFeatures The optional features 28 * @param writingProgram The program used to write the file 29 * @param source The source 30 * @param osmosisReplicationTimestamp The last time that osmosis updated the source (in seconds since epoch) 31 * @param osmosisReplicationSequenceNumber The replication sequence number 32 * @param osmosisReplicationBaseUrl The replication base url 33 */ 34 public HeaderBlock(@Nullable BBox bbox, @Nonnull String[] requiredFeatures, @Nonnull String[] optionalFeatures, 35 @Nullable String writingProgram, @Nullable String source, @Nullable Long osmosisReplicationTimestamp, 36 @Nullable Long osmosisReplicationSequenceNumber, @Nullable String osmosisReplicationBaseUrl) { 37 this.bbox = bbox; 38 this.requiredFeatures = requiredFeatures; 39 this.optionalFeatures = optionalFeatures; 40 this.writingProgram = writingProgram; 41 this.source = source; 42 this.osmosisReplicationTimestamp = osmosisReplicationTimestamp; 43 this.osmosisReplicationSequenceNumber = osmosisReplicationSequenceNumber; 44 this.osmosisReplicationBaseUrl = osmosisReplicationBaseUrl; 45 } 46 47 /** 48 * The required features to parse the PBF 49 * @return The required features 50 */ 51 @Nonnull 52 public String[] requiredFeatures() { 53 return this.requiredFeatures.clone(); 54 } 55 56 /** 57 * The optional features to parse the PBF 58 * @return The optional features 59 */ 60 @Nonnull 61 public String[] optionalFeatures() { 62 return this.optionalFeatures.clone(); 63 } 64 65 /** 66 * Get the program used to write the PBF 67 * @return The program that wrote the PBF 68 */ 69 @Nullable 70 public String writingProgram() { 71 return this.writingProgram; 72 } 73 74 /** 75 * The source 76 * @return The source (same as bbox field from OSM) 77 */ 78 @Nullable 79 public String source() { 80 return this.source; 81 } 82 83 /** 84 * The replication timestamp 85 * @return The time that the file was last updated 86 */ 87 @Nullable 88 public Long osmosisReplicationTimestamp() { 89 return this.osmosisReplicationTimestamp; 90 } 91 92 /** 93 * The replication sequence number 94 * @return The sequence number 95 */ 96 @Nullable 97 public Long osmosisReplicationSequenceNumber() { 98 return this.osmosisReplicationSequenceNumber; 99 } 100 101 /** 102 * The replication base URL 103 * @return the base url for replication, if we ever want/need to continue the replication 104 */ 105 @Nullable 106 public String osmosisReplicationBaseUrl() { 107 return this.osmosisReplicationBaseUrl; 108 } 109 110 /** 111 * The bbox 112 * @return The bbox 113 */ 114 @Nullable 115 public BBox bbox() { 116 return this.bbox; 117 } 118 } -
new file src/org/openstreetmap/josm/data/osm/pbf/Info.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/osm/pbf/Info.java b/src/org/openstreetmap/josm/data/osm/pbf/Info.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.osm.pbf; 3 4 import javax.annotation.Nullable; 5 6 /** 7 * Optional metadata for primitives 8 */ 9 public final class Info { 10 private final int version; 11 private final Long timestamp; 12 private final Long changeset; 13 private final Integer uid; 14 private final Integer userSid; 15 private final boolean visible; 16 17 public Info(int version, @Nullable Long timestamp, @Nullable Long changeset, @Nullable Integer uid, @Nullable Integer userSid, 18 boolean visible) { 19 this.version = version; 20 this.timestamp = timestamp; 21 this.changeset = changeset; 22 this.uid = uid; 23 this.userSid = userSid; 24 this.visible = visible; 25 } 26 27 public int version() { 28 return this.version; 29 } 30 31 public Long timestamp() { 32 return this.timestamp; 33 } 34 35 public Long changeset() { 36 return this.changeset; 37 } 38 39 public Integer uid() { 40 return this.uid; 41 } 42 43 public Integer userSid() { 44 return this.userSid; 45 } 46 47 public boolean isVisible() { 48 return this.visible; 49 } 50 } -
new file src/org/openstreetmap/josm/data/osm/pbf/OsmPbfFile.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/osm/pbf/OsmPbfFile.java b/src/org/openstreetmap/josm/data/osm/pbf/OsmPbfFile.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.osm.pbf; 3 4 import java.util.Arrays; 5 import java.util.Collections; 6 import java.util.List; 7 8 /** 9 * A class used to determine whether or not a file may be an OSM PBF file 10 */ 11 public final class OsmPbfFile { 12 /** 13 * Extensions for OSM PBF files. 14 * {@code "osm.pbf"} is a SHOULD, <i>not</i> a MUST. 15 */ 16 public static final List<String> EXTENSION = Collections.unmodifiableList(Arrays.asList("osm.pbf", "pbf")); 17 18 /** 19 * mimetypes for OSM PBF files 20 */ 21 public static final List<String> MIMETYPE = Collections.emptyList(); 22 23 private OsmPbfFile() { 24 // Hide the constructor 25 } 26 } -
new file src/org/openstreetmap/josm/data/osm/pbf/package-info.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/osm/pbf/package-info.java b/src/org/openstreetmap/josm/data/osm/pbf/package-info.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 /** 3 * A package for reading OSM PBF files 4 * See <a href="https://wiki.openstreetmap.org/wiki/PBF_Format">PBF format</a> for details. 5 * 6 * Note: {@link org.openstreetmap.josm.data.osm.pbf.BlobHeader} and {@link org.openstreetmap.josm.data.osm.pbf.Blob} are the "root" messages. 7 * The remaining messages are part of the {@link org.openstreetmap.josm.data.osm.pbf.Blob}. 8 */ 9 package org.openstreetmap.josm.data.osm.pbf; -
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
a b 2 2 package org.openstreetmap.josm.data.protobuf; 3 3 4 4 import java.io.ByteArrayOutputStream; 5 import java.util.ArrayList; 6 import java.util.List; 5 import java.util.Arrays; 7 6 8 7 /** 9 8 * Parse packed values (only numerical values) … … 12 11 * @since 17862 13 12 */ 14 13 public class ProtobufPacked { 15 private static final Number[] NO_NUMBERS = new Number[0];16 14 private final byte[] bytes; 17 private final Number[] numbers;15 private final long[] numbers; 18 16 private int location; 19 17 20 18 /** 21 19 * Create a new ProtobufPacked object 22 20 * 23 * @param byteArrayOutputStream A reusable ByteArrayOutputStream (helps to reduce memory allocations) 21 * @param ignored A reusable ByteArrayOutputStream (no longer used) 22 * @param bytes The packed bytes 23 * @deprecated since we aren't using the output stream anymore 24 */ 25 @Deprecated 26 public ProtobufPacked(ByteArrayOutputStream ignored, byte[] bytes) { 27 this(bytes); 28 } 29 30 /** 31 * Create a new ProtobufPacked object 32 * 24 33 * @param bytes The packed bytes 34 * @since xxx 25 35 */ 26 public ProtobufPacked( ByteArrayOutputStream byteArrayOutputStream,byte[] bytes) {36 public ProtobufPacked(byte[] bytes) { 27 37 this.location = 0; 28 38 this.bytes = bytes; 29 39 30 40 // By creating a list of size bytes.length, we avoid 36 MB of allocations from list growth. This initialization 31 41 // only adds 3.7 MB to the ArrayList#init calls. Note that the real-world test case (Mapillary vector tiles) 32 42 // primarily created Shorts. 33 List<Number> numbersT = new ArrayList<>(bytes.length); 43 long[] numbersT = new long[bytes.length]; 44 int index = 0; 34 45 // By reusing a ByteArrayOutputStream, we can reduce allocations in nextVarInt from 230 MB to 74 MB. 35 46 while (this.location < bytes.length) { 36 numbersT.add(ProtobufParser.convertByteArray(this.nextVarInt(byteArrayOutputStream), ProtobufParser.VAR_INT_BYTE_SIZE)); 47 int start = this.location; 48 numbersT[index] = ProtobufParser.convertByteArray(this.bytes, ProtobufParser.VAR_INT_BYTE_SIZE, 49 start, this.nextVarInt()); 50 index++; 37 51 } 38 52 39 this.numbers = numbersT.toArray(NO_NUMBERS); 53 if (numbersT.length == index) { 54 this.numbers = numbersT; 55 } else { 56 this.numbers = Arrays.copyOf(numbersT, index); 57 } 40 58 } 41 59 42 60 /** … … 44 62 * 45 63 * @return The number array 46 64 */ 47 public Number[] getArray() {65 public long[] getArray() { 48 66 return this.numbers; 49 67 } 50 68 51 private byte[] nextVarInt(final ByteArrayOutputStream byteArrayOutputStream) {52 // In a real world test, the largest List<Byte> seen had 3 elements. Use 4 to avoid most new array allocations.53 // Memory allocations went from 368 MB to 280 MB by using an initial array allocation. When using a54 // ByteArrayOutputStream, it went down to 230 MB. By further reusing the ByteArrayOutputStream between method55 // calls, it went down further to 73 MB.69 /** 70 * Gets the location where the next var int begins. Note: changes {@link ProtobufPacked#location}. 71 * @return The next varint location 72 */ 73 private int nextVarInt() { 56 74 while ((this.bytes[this.location] & ProtobufParser.MOST_SIGNIFICANT_BYTE) 57 75 == ProtobufParser.MOST_SIGNIFICANT_BYTE) { 58 76 // Get rid of the leading bit (shift left 1, then shift right 1 unsigned) 59 byteArrayOutputStream.write(this.bytes[this.location++] ^ ProtobufParser.MOST_SIGNIFICANT_BYTE); 77 this.bytes[this.location] = (byte) (this.bytes[this.location] ^ ProtobufParser.MOST_SIGNIFICANT_BYTE); 78 this.location++; 60 79 } 61 // The last byte doesn't drop the most significant bit 62 byteArrayOutputStream.write(this.bytes[this.location++]); 63 try { 64 return byteArrayOutputStream.toByteArray(); 65 } finally { 66 byteArrayOutputStream.reset(); 67 } 80 return ++this.location; 68 81 } 69 82 } -
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
a b 7 7 import java.io.IOException; 8 8 import java.io.InputStream; 9 9 import java.util.ArrayList; 10 import java.util.Arrays; 10 11 import java.util.Collection; 11 12 12 13 import org.openstreetmap.josm.tools.Logging; … … 30 31 * Used to get the most significant byte 31 32 */ 32 33 static final byte MOST_SIGNIFICANT_BYTE = (byte) (1 << 7); 34 private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; 35 33 36 /** 34 37 * Convert a byte array to a number (little endian) 35 38 * … … 38 41 * @return An appropriate {@link Number} class. 39 42 */ 40 43 public static Number convertByteArray(byte[] bytes, byte byteSize) { 44 return convertLong(convertByteArray(bytes, byteSize, 0, bytes.length)); 45 } 46 47 /** 48 * Convert a byte array to a number (little endian) 49 * 50 * @param bytes The bytes to convert 51 * @param byteSize The size of the byte. For var ints, this is 7, for other ints, this is 8. 52 * @param start The start position in the byte array 53 * @param end The end position in the byte array (exclusive - [start, end) ) 54 * @return t 55 * he number from the byte array. Depending upon length of time the number will be stored, narrowing may be helpful. 56 * @since xxx 57 */ 58 public static long convertByteArray(byte[] bytes, byte byteSize, int start, int end) { 41 59 long number = 0; 42 for (int i = 0; i < bytes.length; i++) {60 for (int i = start; i < end; i++) { 43 61 // Need to convert to uint64 in order to avoid bit operation from filling in 1's and overflow issues 44 number += Byte.toUnsignedLong(bytes[i]) << (byteSize * i);62 number += Byte.toUnsignedLong(bytes[i]) << (byteSize * (i - start)); 45 63 } 46 return convertLong(number);64 return number; 47 65 } 48 66 49 67 /** … … 71 89 * @return The decoded value 72 90 */ 73 91 public static Number decodeZigZag(Number signed) { 74 final long value = signed.longValue(); 75 return convertLong((value >> 1) ^ -(value & 1)); 92 return convertLong(decodeZigZag(signed.longValue())); 93 } 94 95 /** 96 * Decode a zig-zag encoded value 97 * 98 * @param signed The value to decode 99 * @return The decoded value 100 * @since xxx 101 */ 102 public static long decodeZigZag(long signed) { 103 return (signed >> 1) ^ -(signed & 1); 76 104 } 77 105 78 106 /** … … 203 231 * @throws IOException - if an IO error occurs 204 232 */ 205 233 public byte[] nextLengthDelimited(ByteArrayOutputStream byteArrayOutputStream) throws IOException { 206 int length = convertByteArray(this.nextVarInt(byteArrayOutputStream), VAR_INT_BYTE_SIZE).intValue(); 234 final byte[] nextVarInt = this.nextVarInt(byteArrayOutputStream); 235 int length = (int) convertByteArray(nextVarInt, VAR_INT_BYTE_SIZE, 0, nextVarInt.length); 207 236 return readNextBytes(length); 208 237 } 209 238 … … 236 265 * Read an arbitrary number of bytes 237 266 * 238 267 * @param size The number of bytes to read 239 * @return a byte array of the specified size,filled with bytes read (unsigned)268 * @return a byte array filled with bytes read (unsigned) 240 269 * @throws IOException - if an IO error occurs 241 270 */ 242 271 private byte[] readNextBytes(int size) throws IOException { 243 272 byte[] bytesRead = new byte[size]; 244 for (int i = 0; i < bytesRead.length; i++) { 245 bytesRead[i] = (byte) this.nextByte(); 273 int read = this.inputStream.read(bytesRead); 274 if (read == -1) { 275 return EMPTY_BYTE_ARRAY; 276 } else if (read != size) { 277 return Arrays.copyOf(bytesRead, read); 246 278 } 247 279 return bytesRead; 248 280 } -
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
a b 27 27 * @throws IOException - if an IO error occurs 28 28 */ 29 29 public ProtobufRecord(ByteArrayOutputStream byteArrayOutputStream, ProtobufParser parser) throws IOException { 30 Number number = ProtobufParser.convertByteArray(parser.nextVarInt(byteArrayOutputStream), ProtobufParser.VAR_INT_BYTE_SIZE); 30 final byte[] varInt = parser.nextVarInt(byteArrayOutputStream); 31 long number = ProtobufParser.convertByteArray(varInt, ProtobufParser.VAR_INT_BYTE_SIZE, 0, varInt.length); 31 32 // I don't foresee having field numbers > {@code Integer#MAX_VALUE >> 3} 32 this.field = (int) number .longValue()>> 3;33 this.field = (int) number >> 3; 33 34 // 7 is 111 (so last three bits) 34 byte wireType = (byte) (number .longValue()& 7);35 byte wireType = (byte) (number & 7); 35 36 // By not using a stream, we reduce the number of allocations (for getting the WireType) from 257 MB to 40 MB. 36 37 // (The remaining 40 MB is from WireType#values). By using the cached getAllValues(), we drop the 40 MB. 37 38 WireType tType = WireType.UNKNOWN; -
src/org/openstreetmap/josm/data/Bounds.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/Bounds.java b/src/org/openstreetmap/josm/data/Bounds.java
a b 6 6 import java.awt.geom.Rectangle2D; 7 7 import java.text.DecimalFormat; 8 8 import java.text.MessageFormat; 9 import java.util. Objects;9 import java.util.Arrays; 10 10 11 11 import org.openstreetmap.josm.data.coor.ILatLon; 12 12 import org.openstreetmap.josm.data.coor.LatLon; … … 592 592 593 593 @Override 594 594 public int hashCode() { 595 return Objects.hash(minLat, minLon, maxLat, maxLon);595 return Arrays.hashCode(new double[] {minLat, minLon, maxLat, maxLon}); 596 596 } 597 597 598 598 @Override -
new file src/org/openstreetmap/josm/gui/io/importexport/OsmPbfImporter.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/io/importexport/OsmPbfImporter.java b/src/org/openstreetmap/josm/gui/io/importexport/OsmPbfImporter.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.io.importexport; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import java.io.InputStream; 7 import java.util.Arrays; 8 9 import org.openstreetmap.josm.actions.ExtensionFileFilter; 10 import org.openstreetmap.josm.data.osm.DataSet; 11 import org.openstreetmap.josm.gui.progress.ProgressMonitor; 12 import org.openstreetmap.josm.io.IllegalDataException; 13 import org.openstreetmap.josm.io.OsmPbfReader; 14 15 /** 16 * File importer that reads *.osm.pbf data files. 17 */ 18 public class OsmPbfImporter extends OsmImporter { 19 /** 20 * The OSM file filter (*.osm.pbf files). 21 */ 22 public static final ExtensionFileFilter FILE_FILTER = ExtensionFileFilter.newFilterWithArchiveExtensions( 23 "osm.pbf", "osm.pbf", tr("OSM PBF Files") + " (*.osm.pbf, *.osm.pbf.gz, *.osm.pbf.bz2, *.osm.pbf.xz, *.osm.pbf.zip)", 24 ExtensionFileFilter.AddArchiveExtension.NONE, Arrays.asList("gz", "bz", "bz2", "xz", "zip")); 25 26 /** 27 * Constructs a new {@code OsmPbfImporter}. 28 */ 29 public OsmPbfImporter() { 30 super(FILE_FILTER); 31 } 32 33 /** 34 * Constructs a new {@code OsmPbfImporter} with the given extension file filter. 35 * @param filter The extension file filter 36 */ 37 public OsmPbfImporter(ExtensionFileFilter filter) { 38 super(filter); 39 } 40 41 @Override 42 protected DataSet parseDataSet(InputStream in, ProgressMonitor progressMonitor) throws IllegalDataException { 43 return OsmPbfReader.parseDataSet(in, progressMonitor); 44 } 45 } -
src/org/openstreetmap/josm/io/AbstractReader.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/io/AbstractReader.java b/src/org/openstreetmap/josm/io/AbstractReader.java
a b 273 273 274 274 protected abstract DataSet doParseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException; 275 275 276 @FunctionalInterface 277 protected interface BinaryParserWorker { 278 /** 279 * Effectively parses the file, depending on the binary format (PBF, etc.) 280 * @param ir input stream reader 281 * @throws IllegalDataException in case of invalid data 282 * @throws IOException in case of I/O error 283 */ 284 void accept(InputStream ir) throws IllegalDataException, IOException; 285 } 286 276 287 @FunctionalInterface 277 288 protected interface ParserWorker { 278 289 /** … … 284 295 void accept(InputStreamReader ir) throws IllegalDataException, IOException; 285 296 } 286 297 298 protected final DataSet doParseDataSet(InputStream source, ProgressMonitor progressMonitor, BinaryParserWorker parserWorker) 299 throws IllegalDataException { 300 return this.doParseDataSet(source, progressMonitor, (Object) parserWorker); 301 } 302 287 303 protected final DataSet doParseDataSet(InputStream source, ProgressMonitor progressMonitor, ParserWorker parserWorker) 288 304 throws IllegalDataException { 305 return this.doParseDataSet(source, progressMonitor, (Object) parserWorker); 306 } 307 308 private DataSet doParseDataSet(InputStream source, ProgressMonitor progressMonitor, Object parserWorker) 309 throws IllegalDataException { 289 310 if (progressMonitor == null) { 290 311 progressMonitor = NullProgressMonitor.INSTANCE; 291 312 } … … 296 317 progressMonitor.beginTask(tr("Prepare OSM data..."), 4); // read, prepare, post-process, render 297 318 progressMonitor.indeterminateSubTask(tr("Parsing OSM data...")); 298 319 299 try (InputStreamReader ir = UTFInputStreamReader.create(source)) { 300 parserWorker.accept(ir); 320 if (parserWorker instanceof ParserWorker) { 321 try (InputStreamReader ir = UTFInputStreamReader.create(source)) { 322 ((ParserWorker) parserWorker).accept(ir); 323 } 324 } else if (parserWorker instanceof BinaryParserWorker) { 325 ((BinaryParserWorker) parserWorker).accept(source); 326 } else { 327 throw new IllegalArgumentException("Unknown parser worker type: " + parserWorker.getClass()); 301 328 } 302 329 progressMonitor.worked(1); 303 330 -
src/org/openstreetmap/josm/io/OsmJsonReader.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/io/OsmJsonReader.java b/src/org/openstreetmap/josm/io/OsmJsonReader.java
a b 178 178 179 179 @Override 180 180 protected DataSet doParseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException { 181 return doParseDataSet(source, progressMonitor, ir -> {181 return doParseDataSet(source, progressMonitor, (ParserWorker) ir -> { 182 182 setParser(Json.createParser(ir)); 183 183 parse(); 184 184 }); -
new file src/org/openstreetmap/josm/io/OsmPbfReader.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/io/OsmPbfReader.java b/src/org/openstreetmap/josm/io/OsmPbfReader.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.io; 3 4 import java.io.BufferedInputStream; 5 import java.io.ByteArrayInputStream; 6 import java.io.ByteArrayOutputStream; 7 import java.io.IOException; 8 import java.io.InputStream; 9 import java.util.ArrayList; 10 import java.util.Arrays; 11 import java.util.HashMap; 12 import java.util.HashSet; 13 import java.util.List; 14 import java.util.Locale; 15 import java.util.Map; 16 import java.util.Set; 17 18 import javax.annotation.Nonnull; 19 import javax.annotation.Nullable; 20 21 import org.apache.commons.compress.utils.CountingInputStream; 22 import org.openstreetmap.josm.data.Bounds; 23 import org.openstreetmap.josm.data.DataSource; 24 import org.openstreetmap.josm.data.coor.LatLon; 25 import org.openstreetmap.josm.data.osm.BBox; 26 import org.openstreetmap.josm.data.osm.DataSet; 27 import org.openstreetmap.josm.data.osm.NodeData; 28 import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 29 import org.openstreetmap.josm.data.osm.PrimitiveData; 30 import org.openstreetmap.josm.data.osm.RelationData; 31 import org.openstreetmap.josm.data.osm.RelationMemberData; 32 import org.openstreetmap.josm.data.osm.Tagged; 33 import org.openstreetmap.josm.data.osm.User; 34 import org.openstreetmap.josm.data.osm.WayData; 35 import org.openstreetmap.josm.data.osm.pbf.Blob; 36 import org.openstreetmap.josm.data.osm.pbf.BlobHeader; 37 import org.openstreetmap.josm.data.osm.pbf.HeaderBlock; 38 import org.openstreetmap.josm.data.osm.pbf.Info; 39 import org.openstreetmap.josm.data.protobuf.ProtobufPacked; 40 import org.openstreetmap.josm.data.protobuf.ProtobufParser; 41 import org.openstreetmap.josm.data.protobuf.ProtobufRecord; 42 import org.openstreetmap.josm.data.protobuf.WireType; 43 import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 44 import org.openstreetmap.josm.gui.progress.ProgressMonitor; 45 import org.openstreetmap.josm.tools.Utils; 46 47 /** 48 * Read OSM data from an OSM PBF file 49 */ 50 public final class OsmPbfReader extends AbstractReader { 51 private static final long[] EMPTY_LONG = new long[0]; 52 /** 53 * Nano degrees 54 */ 55 private static final double NANO_DEGREES = 1e-9; 56 /** 57 * The maximimum BlobHeader size. BlobHeaders should (but not must) be less than half this 58 */ 59 private static final int MAX_BLOBHEADER_SIZE = 64 * 1024; 60 /** 61 * The maximim Blob size. Blobs should (but not must) be less than half this 62 */ 63 private static final int MAX_BLOB_SIZE = 32 * 1024 * 1024; 64 65 private OsmPbfReader() { 66 // Hide constructor 67 } 68 69 /** 70 * Parse the given input source and return the dataset. 71 * 72 * @param source the source input stream. Must not be null. 73 * @param progressMonitor the progress monitor. If null, {@link NullProgressMonitor#INSTANCE} is assumed 74 * @return the dataset with the parsed data 75 * @throws IllegalDataException if an error was found while parsing the data from the source 76 * @throws IllegalArgumentException if source is null 77 */ 78 public static DataSet parseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException { 79 return new OsmPbfReader().doParseDataSet(source, progressMonitor); 80 } 81 82 @Override 83 protected DataSet doParseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException { 84 return doParseDataSet(source, progressMonitor, this::parse); 85 } 86 87 private DataSet parse(InputStream source) throws IllegalDataException, IOException { 88 final CountingInputStream inputStream; 89 if (source.markSupported()) { 90 inputStream = new CountingInputStream(source); 91 } else { 92 inputStream = new CountingInputStream(new BufferedInputStream(source)); 93 } 94 try (ProtobufParser parser = new ProtobufParser(inputStream)) { 95 final ByteArrayOutputStream baos = new ByteArrayOutputStream(); 96 HeaderBlock headerBlock = null; 97 BlobHeader blobHeader = null; 98 while (parser.hasNext() && !this.cancel) { 99 if (blobHeader == null) { 100 blobHeader = parseBlobHeader(inputStream, baos, parser); 101 } else if ("OSMHeader".equals(blobHeader.type())) { 102 if (headerBlock != null) { 103 throw new IllegalDataException("Too many header blocks in protobuf"); 104 } 105 // OSM PBF is fun -- it has *nested* pbf data 106 Blob blob = parseBlob(blobHeader, inputStream, parser, baos); 107 headerBlock = parseHeaderBlock(blob, baos); 108 checkRequiredFeatures(headerBlock); 109 blobHeader = null; 110 } else if ("OSMData".equals(blobHeader.type())) { 111 if (headerBlock == null) { 112 throw new IllegalStateException("A header block must occur before the first data block"); 113 } 114 Blob blob = parseBlob(blobHeader, inputStream, parser, baos); 115 parseDataBlock(baos, headerBlock, blob); 116 blobHeader = null; 117 } // Other software *may* extend the FileBlocks (from just "OSMHeader" and "OSMData"), so don't throw an error. 118 } 119 } 120 return this.getDataSet(); 121 } 122 123 /** 124 * Parse a blob header 125 * 126 * @param cis A counting stream to ensure we don't read too much data 127 * @param baos A reusable stream 128 * @param parser The parser to read from 129 * @return The BlobHeader message 130 * @throws IOException if one of the streams has an issue 131 * @throws IllegalDataException If the OSM PBF is (probably) corrupted 132 */ 133 @Nonnull 134 private BlobHeader parseBlobHeader(CountingInputStream cis, ByteArrayOutputStream baos, ProtobufParser parser) 135 throws IOException, IllegalDataException { 136 String type = null; 137 byte[] indexData = null; 138 int datasize = Integer.MIN_VALUE; 139 int length = 0; 140 long start = cis.getBytesRead(); 141 while (parser.hasNext() && (length == 0 || cis.getBytesRead() - start < length)) { 142 final ProtobufRecord current = new ProtobufRecord(baos, parser); 143 switch (current.getField()) { 144 case 1: 145 type = current.asString(); 146 break; 147 case 2: 148 indexData = current.getBytes(); 149 break; 150 case 3: 151 datasize = current.asUnsignedVarInt().intValue(); 152 break; 153 default: 154 start = cis.getBytesRead(); 155 length += current.asUnsignedVarInt().intValue(); 156 if (length > MAX_BLOBHEADER_SIZE) { // There is a hard limit of 64 KiB for the BlobHeader. It *should* be less than 32 KiB. 157 throw new IllegalDataException("OSM PBF BlobHeader is too large. PBF is probably corrupted. (" + 158 Utils.getSizeString(MAX_BLOBHEADER_SIZE, Locale.ENGLISH) + " < " + Utils.getSizeString(length, Locale.ENGLISH)); 159 } 160 } 161 } 162 if (type == null || Integer.MIN_VALUE == datasize) { 163 throw new IllegalDataException("OSM PBF BlobHeader could not be read. PBF is probably corrupted."); 164 } else if (datasize > MAX_BLOB_SIZE) { // There is a hard limit of 32 MiB for the blob size. It *should* be less than 16 MiB. 165 throw new IllegalDataException("OSM PBF Blob size is too large. PBF is probably corrupted. (" 166 + Utils.getSizeString(MAX_BLOB_SIZE, Locale.ENGLISH) + " < " + Utils.getSizeString(datasize, Locale.ENGLISH)); 167 } 168 return new BlobHeader(type, indexData, datasize); 169 } 170 171 /** 172 * Parse a blob from the PBF file 173 * 174 * @param header The header with the blob information (most critically, the length of the blob) 175 * @param cis Used to ensure we don't read too much data 176 * @param parser The parser to read records from 177 * @param baos The reusable output stream 178 * @return The blob to use elsewhere 179 * @throws IOException If one of the streams has an issue 180 */ 181 @Nonnull 182 private Blob parseBlob(BlobHeader header, CountingInputStream cis, ProtobufParser parser, ByteArrayOutputStream baos) throws IOException { 183 long start = cis.getBytesRead(); 184 int size = Integer.MIN_VALUE; 185 Blob.CompressionType type = null; 186 ProtobufRecord current = null; 187 while (parser.hasNext() && cis.getBytesRead() - start < header.dataSize()) { 188 current = new ProtobufRecord(baos, parser); 189 switch (current.getField()) { 190 case 1: 191 type = Blob.CompressionType.raw; 192 break; 193 case 2: 194 size = current.asUnsignedVarInt().intValue(); 195 break; 196 case 3: 197 type = Blob.CompressionType.zlib; 198 break; 199 case 4: 200 type = Blob.CompressionType.lzma; 201 break; 202 case 5: 203 type = Blob.CompressionType.bzip2; 204 break; 205 case 6: 206 type = Blob.CompressionType.lz4; 207 break; 208 case 7: 209 type = Blob.CompressionType.zstd; 210 break; 211 default: 212 throw new IllegalStateException("Unknown compression type: " + current.getField()); 213 } 214 } 215 return new Blob(size, type, current.getBytes()); 216 } 217 218 /** 219 * Parse a header block. This assumes that the parser has hit a string with the text "OSMHeader". 220 * 221 * @param blob The blob with the header block data 222 * @param baos The reusable output stream to use 223 * @return The parsed HeaderBlock 224 * @throws IOException if one of the {@link InputStream}s has a problem 225 */ 226 @Nonnull 227 private HeaderBlock parseHeaderBlock(Blob blob, ByteArrayOutputStream baos) throws IOException { 228 try (InputStream blobInput = blob.inputStream(); 229 ProtobufParser parser = new ProtobufParser(blobInput)) { 230 BBox bbox = null; 231 List<String> required = new ArrayList<>(); 232 List<String> optional = new ArrayList<>(); 233 String program = null; 234 String source = null; 235 Long osmosisReplicationTimestamp = null; 236 Long osmosisReplicationSequenceNumber = null; 237 String osmosisReplicationBaseUrl = null; 238 while (parser.hasNext()) { 239 final ProtobufRecord current = new ProtobufRecord(baos, parser); 240 switch (current.getField()) { 241 case 1: // bbox 242 bbox = parseBBox(baos, current); 243 break; 244 case 4: // repeated required features 245 required.add(current.asString()); 246 break; 247 case 5: // repeated optional features 248 optional.add(current.asString()); 249 break; 250 case 16: // writing program 251 program = current.asString(); 252 break; 253 case 17: // source 254 source = current.asString(); 255 break; 256 case 32: // osmosis replication timestamp 257 osmosisReplicationTimestamp = current.asSignedVarInt().longValue(); 258 break; 259 case 33: // osmosis replication sequence number 260 osmosisReplicationSequenceNumber = current.asSignedVarInt().longValue(); 261 break; 262 case 34: // osmosis replication base url 263 osmosisReplicationBaseUrl = current.asString(); 264 break; 265 default: // fall through -- unknown header block field 266 } 267 } 268 return new HeaderBlock(bbox, required.toArray(new String[0]), optional.toArray(new String[0]), program, 269 source, osmosisReplicationTimestamp, osmosisReplicationSequenceNumber, osmosisReplicationBaseUrl); 270 } 271 } 272 273 /** 274 * Ensure that we support all the required features in the PBF 275 * 276 * @param headerBlock The HeaderBlock to check 277 * @throws IllegalDataException If there exists at least one feature that we do not support 278 */ 279 private static void checkRequiredFeatures(HeaderBlock headerBlock) throws IllegalDataException { 280 Set<String> supportedFeatures = new HashSet<>(Arrays.asList("OsmSchema-V0.6", "DenseNodes", "HistoricalInformation")); 281 for (String requiredFeature : headerBlock.requiredFeatures()) { 282 if (!supportedFeatures.contains(requiredFeature)) { 283 throw new IllegalDataException("PBF Parser: Unknown required feature " + requiredFeature); 284 } 285 } 286 } 287 288 /** 289 * Parse a data blob (should be "OSMData") 290 * 291 * @param baos The reusable stream 292 * @param headerBlock The header block with data source information 293 * @param blob The blob to read OSM data from 294 * @throws IOException if we don't support the compression type 295 * @throws IllegalDataException If an invalid OSM primitive was read 296 */ 297 private void parseDataBlock(ByteArrayOutputStream baos, HeaderBlock headerBlock, Blob blob) throws IOException, IllegalDataException { 298 String[] stringTable = null; // field 1, note that stringTable[0] is a delimiter, so it is always blank and unused 299 // field 2 -- we cannot parse these live just in case the following fields come later 300 List<ProtobufRecord> primitiveGroups = new ArrayList<>(); 301 int granularity = 100; // field 17 302 long latOffset = 0; // field 19 303 long lonOffset = 0; // field 20 304 int dateGranularity = 1000; // field 18, default is milliseconds since the 1970 epoch 305 try (InputStream inputStream = blob.inputStream(); 306 ProtobufParser parser = new ProtobufParser(inputStream)) { 307 while (parser.hasNext()) { 308 ProtobufRecord protobufRecord = new ProtobufRecord(baos, parser); 309 switch (protobufRecord.getField()) { 310 case 1: 311 stringTable = parseStringTable(baos, protobufRecord.getBytes()); 312 break; 313 case 2: 314 primitiveGroups.add(protobufRecord); 315 break; 316 case 17: 317 granularity = protobufRecord.asUnsignedVarInt().intValue(); 318 break; 319 case 18: 320 dateGranularity = protobufRecord.asUnsignedVarInt().intValue(); 321 break; 322 case 19: 323 latOffset = protobufRecord.asUnsignedVarInt().longValue(); 324 break; 325 case 20: 326 lonOffset = protobufRecord.asUnsignedVarInt().longValue(); 327 break; 328 default: // Pass, since someone might have extended the format 329 } 330 } 331 } 332 final PrimitiveBlockRecord primitiveBlockRecord = new PrimitiveBlockRecord(stringTable, granularity, latOffset, lonOffset, 333 dateGranularity); 334 final DataSet ds = getDataSet(); 335 if (!primitiveGroups.isEmpty()) { 336 try { 337 ds.beginUpdate(); 338 ds.addDataSource(new DataSource(new Bounds((LatLon) headerBlock.bbox().getMin(), (LatLon) headerBlock.bbox().getMax()), 339 headerBlock.source())); 340 } finally { 341 ds.endUpdate(); 342 } 343 } 344 for (ProtobufRecord primitiveGroup : primitiveGroups) { 345 try { 346 ds.beginUpdate(); 347 parsePrimitiveGroup(baos, primitiveGroup.getBytes(), primitiveBlockRecord); 348 } finally { 349 ds.endUpdate(); 350 } 351 } 352 } 353 354 /** 355 * This parses a bbox from a record (HeaderBBox message) 356 * 357 * @param baos The reusable {@link ByteArrayOutputStream} to avoid unnecessary allocations 358 * @param current The current record 359 * @return The <i>immutable</i> bbox, or {@code null} 360 * @throws IOException If something happens with the {@link InputStream}s (probably won't happen) 361 */ 362 @Nullable 363 private static BBox parseBBox(ByteArrayOutputStream baos, ProtobufRecord current) throws IOException { 364 try (ByteArrayInputStream bboxInputStream = new ByteArrayInputStream(current.getBytes()); 365 ProtobufParser bboxParser = new ProtobufParser(bboxInputStream)) { 366 double left = Double.NaN; 367 double right = Double.NaN; 368 double top = Double.NaN; 369 double bottom = Double.NaN; 370 while (bboxParser.hasNext()) { 371 ProtobufRecord protobufRecord = new ProtobufRecord(baos, bboxParser); 372 if (protobufRecord.getType() == WireType.VARINT) { 373 double value = protobufRecord.asSignedVarInt().longValue() * NANO_DEGREES; 374 switch (protobufRecord.getField()) { 375 case 1: 376 left = value; 377 break; 378 case 2: 379 right = value; 380 break; 381 case 3: 382 top = value; 383 break; 384 case 4: 385 bottom = value; 386 break; 387 default: // Fall through -- someone might have extended the format 388 } 389 } 390 } 391 if (!Double.isNaN(left) && !Double.isNaN(top) && !Double.isNaN(right) && !Double.isNaN(bottom)) { 392 return new BBox(left, top, right, bottom).toImmutable(); 393 } 394 } 395 return null; 396 } 397 398 /** 399 * Parse the string table 400 * 401 * @param baos The reusable stream 402 * @param bytes The message bytes 403 * @return The parsed table (reminder: index 0 is empty, note that all strings are already interned by {@link String#intern()}) 404 * @throws IOException if something happened while reading a {@link ByteArrayInputStream} 405 */ 406 @Nonnull 407 private String[] parseStringTable(ByteArrayOutputStream baos, byte[] bytes) throws IOException { 408 try (ByteArrayInputStream is = new ByteArrayInputStream(bytes); 409 ProtobufParser parser = new ProtobufParser(is)) { 410 List<String> list = new ArrayList<>(); 411 while (parser.hasNext()) { 412 ProtobufRecord protobufRecord = new ProtobufRecord(baos, parser); 413 if (protobufRecord.getField() == 1) { 414 list.add(protobufRecord.asString().intern()); // field is technically repeated bytes 415 } 416 } 417 return list.toArray(new String[0]); 418 } 419 } 420 421 /** 422 * Parse a PrimitiveGroup. Note: this parsing implementation doesn't check and make certain that all primitives in the group are the same 423 * type. 424 * 425 * @param baos The reusable stream 426 * @param bytes The bytes to decode 427 * @param primitiveBlockRecord The record to use for creating the primitives 428 * @throws IllegalDataException if one of the primitive records was invalid 429 * @throws IOException if something happened while reading a {@link ByteArrayInputStream} 430 */ 431 private void parsePrimitiveGroup(ByteArrayOutputStream baos, byte[] bytes, PrimitiveBlockRecord primitiveBlockRecord) 432 throws IllegalDataException, IOException { 433 try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); 434 ProtobufParser parser = new ProtobufParser(bais)) { 435 while (parser.hasNext()) { 436 ProtobufRecord protobufRecord = new ProtobufRecord(baos, parser); 437 switch (protobufRecord.getField()) { 438 case 1: // Nodes, repeated 439 parseNode(baos, protobufRecord.getBytes(), primitiveBlockRecord); 440 break; 441 case 2: // Dense nodes, not repeated 442 parseDenseNodes(baos, protobufRecord.getBytes(), primitiveBlockRecord); 443 break; 444 case 3: // Ways, repeated 445 parseWay(baos, protobufRecord.getBytes(), primitiveBlockRecord); 446 break; 447 case 4: // relations, repeated 448 parseRelation(baos, protobufRecord.getBytes(), primitiveBlockRecord); 449 break; 450 case 5: // Changesets, repeated 451 // Skip -- we don't have a good way to store changeset information in JOSM 452 default: // OSM PBF could be extended 453 } 454 } 455 } 456 } 457 458 /** 459 * Parse a singular node 460 * 461 * @param baos The reusable stream 462 * @param bytes The bytes to decode 463 * @param primitiveBlockRecord The record to use (mostly for tags and lat/lon calculations) 464 * @throws IllegalDataException if the PBF did not provide all the data necessary for node creation 465 * @throws IOException if something happened while reading a {@link ByteArrayInputStream} 466 */ 467 private void parseNode(ByteArrayOutputStream baos, byte[] bytes, PrimitiveBlockRecord primitiveBlockRecord) 468 throws IllegalDataException, IOException { 469 try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); 470 ProtobufParser parser = new ProtobufParser(bais)) { 471 long id = Long.MIN_VALUE; 472 List<String> keys = new ArrayList<>(); 473 List<String> values = new ArrayList<>(); 474 Info info = null; 475 long lat = Long.MIN_VALUE; 476 long lon = Long.MIN_VALUE; 477 while (parser.hasNext()) { 478 ProtobufRecord protobufRecord = new ProtobufRecord(baos, parser); 479 switch (protobufRecord.getField()) { 480 case 1: 481 id = protobufRecord.asSignedVarInt().intValue(); 482 break; 483 case 2: 484 for (long number : new ProtobufPacked(protobufRecord.getBytes()).getArray()) { 485 keys.add(primitiveBlockRecord.stringTable[(int) number]); 486 } 487 break; 488 case 3: 489 for (long number : new ProtobufPacked(protobufRecord.getBytes()).getArray()) { 490 values.add(primitiveBlockRecord.stringTable[(int) number]); 491 } 492 break; 493 case 4: 494 info = parseInfo(baos, protobufRecord.getBytes()); 495 break; 496 case 8: 497 lat = protobufRecord.asSignedVarInt().longValue(); 498 break; 499 case 9: 500 lon = protobufRecord.asSignedVarInt().longValue(); 501 break; 502 default: // Fall through -- PBF could be extended (unlikely) 503 } 504 } 505 if (id == Long.MIN_VALUE || lat == Long.MIN_VALUE || lon == Long.MIN_VALUE) { 506 throw new IllegalDataException("OSM PBF did not provide all the required node information"); 507 } 508 NodeData node = new NodeData(id); 509 node.setCoor(calculateLatLon(primitiveBlockRecord, lat, lon)); 510 addTags(node, keys, values); 511 if (info != null) { 512 setOsmPrimitiveData(primitiveBlockRecord, node, info); 513 } 514 buildPrimitive(node); 515 } 516 } 517 518 /** 519 * Parse dense nodes from a record 520 * 521 * @param baos The reusable output stream 522 * @param bytes The bytes for the dense node 523 * @param primitiveBlockRecord Used for data that is common between several different objects. 524 * @throws IllegalDataException if the nodes could not be parsed, or one of the nodes would be malformed 525 * @throws IOException if something happened while reading a {@link ByteArrayInputStream} 526 */ 527 private void parseDenseNodes(ByteArrayOutputStream baos, byte[] bytes, PrimitiveBlockRecord primitiveBlockRecord) 528 throws IllegalDataException, IOException { 529 long[] ids = EMPTY_LONG; 530 long[] lats = EMPTY_LONG; 531 long[] lons = EMPTY_LONG; 532 long[] keyVals = EMPTY_LONG; // technically can be int 533 Info[] denseInfo = null; 534 try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); 535 ProtobufParser parser = new ProtobufParser(bais)) { 536 while (parser.hasNext()) { 537 ProtobufRecord protobufRecord = new ProtobufRecord(baos, parser); 538 switch (protobufRecord.getField()) { 539 case 1: // packed node ids, DELTA encoded 540 long[] tids = decodePackedSInt64(new ProtobufPacked(protobufRecord.getBytes()).getArray()); 541 ids = joinArrays(ids, tids); 542 break; 543 case 5: // DenseInfo 544 denseInfo = parseDenseInfo(baos, protobufRecord.getBytes()); // not repeated or packed 545 break; 546 case 8: // packed lat, DELTA encoded 547 long[] tlats = decodePackedSInt64(new ProtobufPacked(protobufRecord.getBytes()).getArray()); 548 lats = joinArrays(lats, tlats); 549 break; 550 case 9: // packed lon, DELTA encoded 551 long[] tlons = decodePackedSInt64(new ProtobufPacked(protobufRecord.getBytes()).getArray()); 552 lons = joinArrays(lons, tlons); 553 break; 554 case 10: // key_val mappings, packed. '0' used as separator between nodes 555 long[] tkeyVal = new ProtobufPacked(protobufRecord.getBytes()).getArray(); 556 keyVals = joinArrays(keyVals, tkeyVal); 557 break; 558 default: // Someone might have extended the PBF format 559 } 560 } 561 } 562 int keyValIndex = 0; // This index must not reset between nodes, and must always increment 563 if (ids.length == lats.length && lats.length == lons.length && (denseInfo == null || denseInfo.length == lons.length)) { 564 long id = 0; 565 long lat = 0; 566 long lon = 0; 567 for (int i = 0; i < ids.length; i++) { 568 final NodeData node; 569 if (denseInfo != null) { 570 Info info = denseInfo[i]; 571 id += ids[i]; 572 node = new NodeData(id); 573 setOsmPrimitiveData(primitiveBlockRecord, node, info); 574 } else { 575 node = new NodeData(ids[i]); 576 } 577 lat += lats[i]; 578 lon += lons[i]; 579 // Not very efficient when Node doesn't store the LatLon. Hopefully not too much of an issue 580 node.setCoor(calculateLatLon(primitiveBlockRecord, lat, lon)); 581 String key = null; 582 while (keyValIndex < keyVals.length) { 583 int stringIndex = (int) keyVals[keyValIndex]; 584 // StringTable[0] is always an empty string, and acts as a separator between the tags of different nodes here 585 if (stringIndex != 0) { 586 if (key == null) { 587 key = primitiveBlockRecord.stringTable[stringIndex]; 588 } else { 589 node.put(key, primitiveBlockRecord.stringTable[stringIndex]); 590 key = null; 591 } 592 keyValIndex++; 593 } else { 594 keyValIndex++; 595 break; 596 } 597 } 598 // Just add the nodes as we make them -- avoid creating another list that expands every time we parse a node 599 buildPrimitive(node); 600 } 601 } else { 602 throw new IllegalDataException("OSM PBF has mismatched DenseNode lengths"); 603 } 604 } 605 606 /** 607 * Parse a way from the PBF 608 * 609 * @param baos The reusable stream 610 * @param bytes The bytes for the way 611 * @param primitiveBlockRecord Used for common information, like tags 612 * @throws IllegalDataException if an invalid way could have been created 613 * @throws IOException if something happened while reading a {@link ByteArrayInputStream} 614 */ 615 private void parseWay(ByteArrayOutputStream baos, byte[] bytes, PrimitiveBlockRecord primitiveBlockRecord) 616 throws IllegalDataException, IOException { 617 long id = Long.MIN_VALUE; 618 List<String> keys = new ArrayList<>(); 619 List<String> values = new ArrayList<>(); 620 Info info = null; 621 long[] refs = EMPTY_LONG; // DELTA encoded 622 // We don't do live drawing, so we don't care about lats and lons (we essentially throw them away with the current parser) 623 // This is for the optional feature "LocationsOnWays" 624 try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); 625 ProtobufParser parser = new ProtobufParser(bais)) { 626 while (parser.hasNext()) { 627 ProtobufRecord protobufRecord = new ProtobufRecord(baos, parser); 628 switch (protobufRecord.getField()) { 629 case 1: 630 id = protobufRecord.asUnsignedVarInt().intValue(); 631 break; 632 case 2: 633 for (long number : new ProtobufPacked(protobufRecord.getBytes()).getArray()) { 634 keys.add(primitiveBlockRecord.stringTable[(int) number]); 635 } 636 break; 637 case 3: 638 for (long number : new ProtobufPacked(protobufRecord.getBytes()).getArray()) { 639 values.add(primitiveBlockRecord.stringTable[(int) number]); 640 } 641 break; 642 case 4: 643 info = parseInfo(baos, protobufRecord.getBytes()); 644 break; 645 case 8: 646 long[] tRefs = decodePackedSInt64(new ProtobufPacked(protobufRecord.getBytes()).getArray()); 647 refs = joinArrays(refs, tRefs); 648 break; 649 // case 9 and 10 are for "LocationsOnWays" -- this is only usable if we can create the way geometry directly 650 // if this is ever supported, lats = joinArrays(lats, decodePackedSInt64(...)) 651 default: // PBF could be expanded by other people 652 } 653 } 654 } 655 if (refs.length == 0 || id == Long.MIN_VALUE) { 656 throw new IllegalDataException("A way with either no id or no nodes was found"); 657 } 658 WayData wayData = new WayData(id); 659 List<Long> nodeIds = new ArrayList<>(refs.length); 660 long ref = 0; 661 for (long tRef : refs) { 662 ref += tRef; 663 nodeIds.add(ref); 664 } 665 this.ways.put(wayData.getUniqueId(), nodeIds); 666 addTags(wayData, keys, values); 667 if (info != null) { 668 setOsmPrimitiveData(primitiveBlockRecord, wayData, info); 669 } 670 buildPrimitive(wayData); 671 } 672 673 /** 674 * Parse a relation from a PBF 675 * 676 * @param baos The reusable stream 677 * @param bytes The bytes to use 678 * @param primitiveBlockRecord Mostly used for tags 679 * @throws IllegalDataException if the PBF had a bad relation definition 680 * @throws IOException if something happened while reading a {@link ByteArrayInputStream} 681 */ 682 @Nonnull 683 private void parseRelation(ByteArrayOutputStream baos, byte[] bytes, PrimitiveBlockRecord primitiveBlockRecord) 684 throws IllegalDataException, IOException { 685 long id = Long.MIN_VALUE; 686 List<String> keys = new ArrayList<>(); 687 List<String> values = new ArrayList<>(); 688 Info info = null; 689 long[] rolesStringId = EMPTY_LONG; // Technically int 690 long[] memids = EMPTY_LONG; 691 long[] types = EMPTY_LONG; // Technically an enum 692 try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); 693 ProtobufParser parser = new ProtobufParser(bais)) { 694 while (parser.hasNext()) { 695 ProtobufRecord protobufRecord = new ProtobufRecord(baos, parser); 696 switch (protobufRecord.getField()) { 697 case 1: 698 id = protobufRecord.asUnsignedVarInt().intValue(); 699 break; 700 case 2: 701 for (long number : new ProtobufPacked(protobufRecord.getBytes()).getArray()) { 702 keys.add(primitiveBlockRecord.stringTable[(int) number]); 703 } 704 break; 705 case 3: 706 for (long number : new ProtobufPacked(protobufRecord.getBytes()).getArray()) { 707 values.add(primitiveBlockRecord.stringTable[(int) number]); 708 } 709 break; 710 case 4: 711 info = parseInfo(baos, protobufRecord.getBytes()); 712 break; 713 case 8: 714 long[] tRoles = new ProtobufPacked(protobufRecord.getBytes()).getArray(); 715 rolesStringId = joinArrays(rolesStringId, tRoles); 716 break; 717 case 9: 718 long[] tMemids = decodePackedSInt64(new ProtobufPacked(protobufRecord.getBytes()).getArray()); 719 memids = joinArrays(memids, tMemids); 720 break; 721 case 10: 722 long[] tTypes = new ProtobufPacked(protobufRecord.getBytes()).getArray(); 723 types = joinArrays(types, tTypes); 724 break; 725 default: // Fall through for PBF extensions 726 } 727 } 728 } 729 if (keys.size() != values.size() || rolesStringId.length != memids.length || memids.length != types.length || id == Long.MIN_VALUE) { 730 throw new IllegalDataException("OSM PBF contains a bad relation definition"); 731 } 732 RelationData data = new RelationData(id); 733 if (info != null) { 734 setOsmPrimitiveData(primitiveBlockRecord, data, info); 735 } 736 addTags(data, keys, values); 737 OsmPrimitiveType[] valueTypes = OsmPrimitiveType.values(); 738 List<RelationMemberData> members = new ArrayList<>(rolesStringId.length); 739 long memberId = 0; 740 for (int i = 0; i < rolesStringId.length; i++) { 741 String role = primitiveBlockRecord.stringTable[(int) rolesStringId[i]]; 742 memberId += memids[i]; 743 OsmPrimitiveType type = valueTypes[(int) types[i]]; 744 members.add(new RelationMemberData(role, type, memberId)); 745 } 746 this.relations.put(data.getUniqueId(), members); 747 buildPrimitive(data); 748 } 749 750 /** 751 * Parse info for an object 752 * 753 * @param baos The reusable stream to use 754 * @param bytes The bytes to decode 755 * @return The info for an object 756 * @throws IOException if something happened while reading a {@link ByteArrayInputStream} 757 */ 758 @Nonnull 759 private Info parseInfo(ByteArrayOutputStream baos, byte[] bytes) throws IOException { 760 try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); 761 ProtobufParser parser = new ProtobufParser(bais)) { 762 int version = -1; 763 Long timestamp = null; 764 Long changeset = null; 765 Integer uid = null; 766 Integer userSid = null; 767 boolean visible = true; 768 while (parser.hasNext()) { 769 ProtobufRecord record = new ProtobufRecord(baos, parser); 770 switch (record.getField()) { 771 case 1: 772 version = record.asUnsignedVarInt().intValue(); 773 break; 774 case 2: 775 timestamp = record.asUnsignedVarInt().longValue(); 776 break; 777 case 3: 778 changeset = record.asUnsignedVarInt().longValue(); 779 break; 780 case 4: 781 uid = record.asUnsignedVarInt().intValue(); 782 break; 783 case 5: 784 userSid = record.asUnsignedVarInt().intValue(); 785 break; 786 case 6: 787 visible = record.asUnsignedVarInt().byteValue() == 0; 788 break; 789 default: // Fall through, since the PBF format could be extended 790 } 791 } 792 return new Info(version, timestamp, changeset, uid, userSid, visible); 793 } 794 } 795 796 /** 797 * Calculate the actual lat lon 798 * 799 * @param primitiveBlockRecord The record with offset and granularity data 800 * @param lat The latitude from the PBF 801 * @param lon The longitude from the PBF 802 * @return The actual {@link LatLon}, accounting for PBF offset and granularity changes 803 */ 804 @Nonnull 805 private static LatLon calculateLatLon(PrimitiveBlockRecord primitiveBlockRecord, long lat, long lon) { 806 return new LatLon(NANO_DEGREES * (primitiveBlockRecord.latOffset + (primitiveBlockRecord.granularity * lat)), 807 NANO_DEGREES * (primitiveBlockRecord.lonOffset + (primitiveBlockRecord.granularity * lon))); 808 } 809 810 /** 811 * Add a set of tags to a primitive 812 * 813 * @param primitive The primitive to add tags to 814 * @param keys The keys (must match the size of the values) 815 * @param values The values (must match the size of the keys) 816 */ 817 private static void addTags(Tagged primitive, List<String> keys, List<String> values) { 818 if (keys.isEmpty()) { 819 return; 820 } 821 Map<String, String> tagMap = new HashMap<>(keys.size()); 822 for (int i = 0; i < keys.size(); i++) { 823 tagMap.put(keys.get(i), values.get(i)); 824 } 825 primitive.putAll(tagMap); 826 } 827 828 /** 829 * Set the primitive data for an object 830 * 831 * @param primitiveBlockRecord The record with data for the current primitive (currently uses {@link PrimitiveBlockRecord#stringTable} and 832 * {@link PrimitiveBlockRecord#dateGranularity}). 833 * @param primitive The primitive to add the information to 834 * @param info The specific info for the primitive 835 */ 836 private static void setOsmPrimitiveData(PrimitiveBlockRecord primitiveBlockRecord, PrimitiveData primitive, Info info) { 837 if (info.changeset() != null) { 838 primitive.setChangesetId(Math.toIntExact(info.changeset())); 839 } 840 primitive.setVisible(info.isVisible()); 841 if (info.timestamp() != null) { 842 primitive.setRawTimestamp(Math.toIntExact(info.timestamp() * primitiveBlockRecord.dateGranularity / 1000)); 843 } 844 if (info.uid() != null && info.userSid() != null) { 845 primitive.setUser(User.createOsmUser(info.uid(), primitiveBlockRecord.stringTable[info.userSid()])); 846 } else if (info.uid() != null) { 847 primitive.setUser(User.getById(info.uid())); 848 } 849 if (info.version() > 0) { 850 primitive.setVersion(info.version()); 851 } 852 } 853 854 /** 855 * Convert an array of numbers to an array of longs, decoded from uint (zig zag decoded) 856 * 857 * @param numbers The numbers to convert 858 * @return The long array (the same array that was passed in) 859 */ 860 @Nonnull 861 private static long[] decodePackedSInt64(long[] numbers) { 862 for (int i = 0; i < numbers.length; i++) { 863 numbers[i] = ProtobufParser.decodeZigZag(numbers[i]); 864 } 865 return numbers; 866 } 867 868 /** 869 * Join two different arrays 870 * 871 * @param array1 The first array 872 * @param array2 The second array 873 * @return The joined arrays -- may return one of the original arrays, if the other is empty 874 */ 875 @Nonnull 876 private static long[] joinArrays(long[] array1, long[] array2) { 877 if (array1.length == 0) { 878 return array2; 879 } 880 if (array2.length == 0) { 881 return array1; 882 } 883 long[] result = Arrays.copyOf(array1, array1.length + array2.length); 884 System.arraycopy(array2, 0, result, array1.length, array2.length); 885 return result; 886 } 887 888 /** 889 * Parse dense info 890 * 891 * @param baos The reusable stream 892 * @param bytes The bytes to decode 893 * @return The dense info array 894 * @throws IllegalDataException If the data has mismatched array lengths 895 * @throws IOException if something happened while reading a {@link ByteArrayInputStream} 896 */ 897 @Nonnull 898 private Info[] parseDenseInfo(ByteArrayOutputStream baos, byte[] bytes) throws IllegalDataException, IOException { 899 long[] version = EMPTY_LONG; // technically ints 900 long[] timestamp = EMPTY_LONG; 901 long[] changeset = EMPTY_LONG; 902 long[] uid = EMPTY_LONG; // technically int 903 long[] userSid = EMPTY_LONG; // technically int 904 long[] visible = EMPTY_LONG; // optional, true if not set, technically booleans 905 try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); 906 ProtobufParser parser = new ProtobufParser(bais)) { 907 while (parser.hasNext()) { 908 ProtobufRecord protobufRecord = new ProtobufRecord(baos, parser); 909 switch (protobufRecord.getField()) { 910 case 1: 911 long[] tVersion = new ProtobufPacked(protobufRecord.getBytes()).getArray(); 912 version = joinArrays(version, tVersion); 913 break; 914 case 2: 915 long[] tTimestamp = decodePackedSInt64(new ProtobufPacked(protobufRecord.getBytes()).getArray()); 916 timestamp = joinArrays(timestamp, tTimestamp); 917 break; 918 case 3: 919 long[] tChangeset = decodePackedSInt64(new ProtobufPacked(protobufRecord.getBytes()).getArray()); 920 changeset = joinArrays(changeset, tChangeset); 921 break; 922 case 4: 923 long[] tUid = decodePackedSInt64(new ProtobufPacked(protobufRecord.getBytes()).getArray()); 924 uid = joinArrays(uid, tUid); 925 break; 926 case 5: 927 long[] tUserSid = decodePackedSInt64(new ProtobufPacked(protobufRecord.getBytes()).getArray()); 928 userSid = joinArrays(userSid, tUserSid); 929 break; 930 case 6: 931 long[] tVisible = new ProtobufPacked(protobufRecord.getBytes()).getArray(); 932 visible = joinArrays(visible, tVisible); 933 break; 934 default: // Fall through 935 } 936 } 937 } 938 if (version.length == timestamp.length && timestamp.length == changeset.length && changeset.length == uid.length && 939 uid.length == userSid.length && (visible == EMPTY_LONG || visible.length == userSid.length)) { 940 Info[] infos = new Info[version.length]; 941 long lastTimestamp = 0; // delta encoded 942 long lastChangeset = 0; // delta encoded 943 long lastUid = 0; // delta encoded, 944 long lastUserSid = 0; // delta encoded, string id for username 945 for (int i = 0; i < version.length; i++) { 946 lastTimestamp += timestamp[i]; 947 lastChangeset += changeset[i]; 948 lastUid += uid[i]; 949 lastUserSid += userSid[i]; 950 infos[i] = new Info((int) version[i], lastTimestamp, lastChangeset, (int) lastUid, (int) lastUserSid, 951 visible == EMPTY_LONG || visible[i] == 1); 952 } 953 return infos; 954 } 955 throw new IllegalDataException("OSM PBF has mismatched DenseInfo lengths"); 956 } 957 958 /** 959 * A record class for passing PrimitiveBlock information to the PrimitiveGroup parser 960 */ 961 private static final class PrimitiveBlockRecord { 962 private final String[] stringTable; 963 private final int granularity; 964 private final long latOffset; 965 private final long lonOffset; 966 private final int dateGranularity; 967 968 /** 969 * Create a new record 970 * 971 * @param stringTable The string table (reminder: 0 index is empty, as it is used by DenseNode to separate node tags) 972 * @param granularity units of nanodegrees, used to store coordinates 973 * @param latOffset offset value between the output coordinates and the granularity grid in units of nanodegrees 974 * @param lonOffset offset value between the output coordinates and the granularity grid in units of nanodegrees 975 * @param dateGranularity Granularity of dates, normally represented in units of milliseconds since the 1970 epoch 976 */ 977 PrimitiveBlockRecord(String[] stringTable, int granularity, long latOffset, long lonOffset, 978 int dateGranularity) { 979 this.stringTable = stringTable; 980 this.granularity = granularity; 981 this.latOffset = latOffset; 982 this.lonOffset = lonOffset; 983 this.dateGranularity = dateGranularity; 984 } 985 986 } 987 } -
src/org/openstreetmap/josm/io/OsmReader.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/io/OsmReader.java b/src/org/openstreetmap/josm/io/OsmReader.java
a b 501 501 502 502 @Override 503 503 protected DataSet doParseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException { 504 return doParseDataSet(source, progressMonitor, ir -> {504 return doParseDataSet(source, progressMonitor, (ParserWorker) ir -> { 505 505 try { 506 506 setParser(XmlUtils.newSafeXMLInputFactory().createXMLStreamReader(ir)); 507 507 parse(); -
new file test/unit/org/openstreetmap/josm/data/protobuf/ProtobufPackedTest.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/test/unit/org/openstreetmap/josm/data/protobuf/ProtobufPackedTest.java b/test/unit/org/openstreetmap/josm/data/protobuf/ProtobufPackedTest.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.protobuf; 3 4 import static org.junit.jupiter.api.Assertions.assertArrayEquals; 5 6 import java.io.ByteArrayOutputStream; 7 8 import org.junit.jupiter.api.Test; 9 10 /** 11 * Test class for {@link ProtobufPacked} 12 */ 13 class ProtobufPackedTest { 14 @Test 15 void testSingleByteNumbers() { 16 long[] numbers = new ProtobufPacked(new ByteArrayOutputStream(), ProtobufTest.toByteArray(new int[]{0, 0, 1, 1, 2, 2, 3, 3, 4, 4})) 17 .getArray(); 18 assertArrayEquals(new long[] {0, 0, 1, 1, 2, 2, 3, 3, 4, 4}, numbers); 19 } 20 21 @Test 22 void testMultipleByteNumbers() { 23 byte[] bytes = ProtobufTest.toByteArray(new int[] {-128, 64, -18, 49, -70, 3}); 24 long[] numbers = new ProtobufPacked(new ByteArrayOutputStream(), bytes).getArray(); 25 assertArrayEquals(new long[] {8192, 6382, 442}, numbers); 26 } 27 } -
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
a b 42 42 * @author Taylor Smock 43 43 * @since 17862 44 44 */ 45 class ProtobufTest {45 public class ProtobufTest { 46 46 /** 47 47 * Convert an int array into a byte array 48 48 * @param intArray The int array to convert (NOTE: numbers must be below 255) 49 49 * @return A byte array that can be used 50 50 */ 51 static byte[] toByteArray(int[] intArray) {51 public static byte[] toByteArray(int[] intArray) { 52 52 byte[] byteArray = new byte[intArray.length]; 53 53 for (int i = 0; i < intArray.length; i++) { 54 54 if (intArray[i] > Byte.MAX_VALUE - Byte.MIN_VALUE) { … … 201 201 202 202 @Test 203 203 void testZigZag() { 204 assertEquals(0, ProtobufParser.decodeZigZag( 0).intValue());205 assertEquals(-1, ProtobufParser.decodeZigZag( 1).intValue());206 assertEquals(1, ProtobufParser.decodeZigZag( 2).intValue());207 assertEquals(-2, ProtobufParser.decodeZigZag( 3).intValue());204 assertEquals(0, ProtobufParser.decodeZigZag(Integer.valueOf(0)).intValue()); 205 assertEquals(-1, ProtobufParser.decodeZigZag(Integer.valueOf(1)).intValue()); 206 assertEquals(1, ProtobufParser.decodeZigZag(Long.valueOf(2)).intValue()); 207 assertEquals(-2, ProtobufParser.decodeZigZag(Long.valueOf(3)).intValue()); 208 208 } 209 209 } -
new file test/unit/org/openstreetmap/josm/gui/io/importexport/OsmPbfImporterTest.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/test/unit/org/openstreetmap/josm/gui/io/importexport/OsmPbfImporterTest.java b/test/unit/org/openstreetmap/josm/gui/io/importexport/OsmPbfImporterTest.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.io.importexport; 3 4 import static org.junit.jupiter.api.Assertions.assertAll; 5 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 6 import static org.junit.jupiter.api.Assertions.assertEquals; 7 import static org.junit.jupiter.api.Assertions.assertSame; 8 import static org.junit.jupiter.api.Assertions.assertThrows; 9 import static org.junit.jupiter.api.Assertions.assertTrue; 10 11 import java.io.ByteArrayInputStream; 12 import java.io.IOException; 13 import java.io.InputStream; 14 import java.nio.file.Files; 15 import java.nio.file.Paths; 16 import java.util.Arrays; 17 18 import org.junit.jupiter.api.BeforeAll; 19 import org.junit.jupiter.api.Test; 20 import org.openstreetmap.josm.TestUtils; 21 import org.openstreetmap.josm.data.coor.ILatLon; 22 import org.openstreetmap.josm.data.coor.LatLon; 23 import org.openstreetmap.josm.data.osm.AbstractPrimitive; 24 import org.openstreetmap.josm.data.osm.DataSet; 25 import org.openstreetmap.josm.data.osm.Relation; 26 import org.openstreetmap.josm.data.osm.Way; 27 import org.openstreetmap.josm.data.protobuf.ProtobufTest; 28 import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 29 import org.openstreetmap.josm.io.IllegalDataException; 30 import org.openstreetmap.josm.testutils.annotations.BasicPreferences; 31 32 /** 33 * Test class for {@link OsmPbfImporter} 34 */ 35 @BasicPreferences 36 class OsmPbfImporterTest { 37 /** 38 * BlobHeader, type=OSMHeader, datasize=146, blob compressed by zlib, compressed size 132 39 */ 40 private static final byte[] HEADER_DATA = ProtobufTest.toByteArray(new int[]{ 41 // BlobHeader, type=OSMHeader, datasize=146 42 0, 0, 0, 14, 10, 9, 79, 83, 77, 72, 101, 97, 100, 101, 114, 24, -110, 1, 43 // size=132, type=zlib 44 16, -124, 1, 26, -116, 1, 120, -100, -29, -110, -30, 56, -66, 125, -49, 3, 70, -127, -9, 55, -65, -35, 100, -108, 120, 113, -17, -50, 45 -115, 70, 102, -123, 31, -117, -97, 93, 107, 100, 86, -30, -13, 47, -50, 13, 78, -50, 72, -51, 77, -44, 13, 51, -48, 51, 83, -30, 46 114, 73, -51, 43, 78, -11, -53, 79, 73, 45, -42, 18, 12, -50, 47, 42, -47, 11, -87, 44, 72, -115, 47, -55, 72, -51, -117, -9, 116, 47 105, 98, -28, -49, 47, -50, 77, -50, -49, 43, 75, 45, 42, 81, 48, -48, 51, 119, -17, 98, 84, -55, 40, 41, 41, -80, -46, -41, 47, 47, 48 47, -41, -53, 47, 0, -22, 46, 41, 74, 77, 45, -55, 77, 44, -48, -53, 47, 74, -41, 79, 44, -56, -44, 7, -102, 11, 0, -14, -78, 50, 42 49 }); 50 51 private static OsmPbfImporter importer; 52 53 @BeforeAll 54 static void setup() { 55 importer = new OsmPbfImporter(); 56 } 57 58 @Test 59 void testGoodHeader() { 60 final ByteArrayInputStream goodHeader = new ByteArrayInputStream(HEADER_DATA); 61 // Test good data header 62 final DataSet ds = assertDoesNotThrow(() -> importer.parseDataSet(goodHeader, NullProgressMonitor.INSTANCE)); 63 assertTrue(ds.isEmpty()); 64 } 65 66 @Test 67 void testTooBigHeader() { 68 // Test a bad data header 69 byte[] badData = HEADER_DATA.clone(); 70 badData[1] = -128; 71 badData[2] = -128; 72 final ByteArrayInputStream badHeader = new ByteArrayInputStream(badData); 73 IllegalDataException ide = assertThrows(IllegalDataException.class, 74 () -> importer.parseDataSet(badHeader, NullProgressMonitor.INSTANCE)); 75 assertTrue(ide.getMessage().contains("OSM PBF BlobHeader is too large. PBF is probably corrupted")); 76 } 77 78 @Test 79 void testMissingRequiredFeature() { 80 // Test a bad data blob 81 byte[] badData = HEADER_DATA.clone(); 82 // OsmSchema-V0.6 -> OtmSchema-V0.6 83 badData[60] = -55; 84 // Correct zip information 85 badData[160] = -13; 86 badData[161] = 23; 87 badData[163] = 43; 88 final ByteArrayInputStream badBlob = new ByteArrayInputStream(badData); 89 final IllegalDataException ide = assertThrows(IllegalDataException.class, 90 () -> importer.parseDataSet(badBlob, NullProgressMonitor.INSTANCE)); 91 assertEquals("PBF Parser: Unknown required feature OtmSchema-V0.6", ide.getMessage()); 92 } 93 94 @Test 95 void testMultipleHeaders() { 96 byte[] badData = Arrays.copyOf(HEADER_DATA, HEADER_DATA.length * 2); 97 System.arraycopy(HEADER_DATA, 0, badData, HEADER_DATA.length, HEADER_DATA.length); 98 final ByteArrayInputStream badBlob = new ByteArrayInputStream(badData); 99 final IllegalDataException ide = assertThrows(IllegalDataException.class, 100 () -> importer.parseDataSet(badBlob, NullProgressMonitor.INSTANCE)); 101 assertEquals("Too many header blocks in protobuf", ide.getMessage()); 102 } 103 104 @Test 105 void testSimpleCase() throws IOException { 106 try (InputStream is = Files.newInputStream(Paths.get(TestUtils.getTestDataRoot(), "pbf", "osm", "simple.osm.pbf"))) { 107 DataSet ds = assertDoesNotThrow(() -> importer.parseDataSet(is, NullProgressMonitor.INSTANCE)); 108 assertEquals(1, ds.getRelations().size()); 109 assertAll(() -> assertEquals(4, ds.getNodes().size()), 110 () -> assertEquals(1, ds.getWays().size()), 111 () -> assertEquals(1, ds.getRelations().size()), 112 () -> assertTrue(ds.getNodes().stream() 113 .filter(node -> node.getCoor().equalsEpsilon((ILatLon) new LatLon(39.1998868, -108.6907137))) 114 .allMatch(node -> "house".equals(node.get("building")))), 115 () -> assertTrue(ds.getNodes().stream() 116 .filter(node -> !node.getCoor().equalsEpsilon((ILatLon) new LatLon(39.1998868, -108.6907137))) 117 .noneMatch(AbstractPrimitive::hasKeys)) 118 ); 119 Way way = ds.getWays().iterator().next(); 120 Relation rel = ds.getRelations().iterator().next(); 121 assertAll(() -> assertEquals(5, way.getNodes().size()), 122 () -> assertTrue(way.isClosed()), 123 () -> assertTrue(way.firstNode().equalsEpsilon(new LatLon(39.1998868, -108.6907137))), 124 () -> assertEquals("house", way.get("building")), 125 () -> assertEquals("house", rel.get("building")), 126 () -> assertEquals(1, rel.getMembersCount()), 127 () -> assertEquals("outer", rel.getRole(0)), 128 () -> assertSame(way, rel.getMember(0).getMember()) 129 ); 130 } 131 } 132 } -
new file test/data/pbf/osm/simple.osm.pbf
diff --git a/test/data/pbf/osm/simple.osm.pbf b/test/data/pbf/osm/simple.osm.pbf new file mode 100644 index 0000000000000000000000000000000000000000..efc489d88ddd294e8abc3412ad3064d81d61fb3d GIT binary patch literal 364 zc$@)j0h9g!000gO2~Sf^NM&JUWpWsS0T6W>eR!PXlHvIJ>fG~{Yyxj@K76>6P2%6` zhK?mn3Kv#BXkNmk#OGg}8=RbxnwzK_W}s)L#O0EjSDfmXpORYK#K_|1AMD%3Xz1+j znVVRkV4!EAXQ7~BXklhzX=<oYT#%Was%v1TmtO$XQ&N<gS^`w1mtT~w2>`L9Dk}g0 z01OHTPg6}qVRT^_$pH}90UE;rc%0)h;9>wmj-=AeoRrMGbS~D6{L<o7E|!wYf>bWn z{L+%tA}*fX(wvgag8ZDy^!z*_Um<TUw#T0Zn3$Nf<hWQEfItXLN`Xlw7Dgbc#li$6 zow(a(J^RY!+qQ^xDytXI-Jat;95WWOC*GXT9>fNeXJTd$Qjt>PXgu-rKNv6yF)~Rp zGAprhFbFV6FgUT_{Vc%9$jBz7p`^w!?+aWN6AP0R6B|MuBdZtl-}gL0jEo(ODH9l9 Kumb>H!#OZnIjMvI