Ticket #22603: 22603.patch

File 22603.patch, 95.6 KB (added by taylor.smock, 17 months 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  
    2727import org.openstreetmap.josm.gui.io.importexport.NoteImporter;
    2828import org.openstreetmap.josm.gui.io.importexport.OsmChangeImporter;
    2929import org.openstreetmap.josm.gui.io.importexport.OsmImporter;
     30import org.openstreetmap.josm.gui.io.importexport.OsmPbfImporter;
    3031import org.openstreetmap.josm.gui.io.importexport.OziWptImporter;
    3132import org.openstreetmap.josm.gui.io.importexport.RtkLibImporter;
    3233import org.openstreetmap.josm.gui.io.importexport.WMSLayerImporter;
     
    6566        final List<Class<? extends FileImporter>> importerNames = Arrays.asList(
    6667                OsmImporter.class,
    6768                OsmChangeImporter.class,
     69                OsmPbfImporter.class,
    6870                GeoJSONImporter.class,
    6971                GpxImporter.class,
    7072                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  
    4949        this.parameters[added++] = parameterInteger.shortValue();
    5050    }
    5151
     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
    5260    /**
    5361     * Get the operations for the command
    5462     * @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  
    2727    private static final byte GEOMETRY_FIELD = 4;
    2828    /**
    2929     * 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 to
     30     * Doing this reduced the allocations of {@link #parseTagValue(String, Layer, int, List)} from 22.79% of parent to
    3131     * 12.2% of parent.
    3232     */
    3333    private static final NumberFormat NUMBER_FORMAT = NumberFormat.getNumberInstance(Locale.ROOT);
     
    7474                try (ProtobufRecord next = new ProtobufRecord(byteArrayOutputStream, parser)) {
    7575                    if (next.getField() == TAG_FIELD) {
    7676                        // This is packed in v1 and v2
    77                         ProtobufPacked packed = new ProtobufPacked(byteArrayOutputStream, next.getBytes());
     77                        ProtobufPacked packed = new ProtobufPacked(next.getBytes());
    7878                        if (tagList == null) {
    7979                            tagList = new ArrayList<>(packed.getArray().length);
    8080                        } else {
    8181                            tagList.ensureCapacity(tagList.size() + packed.getArray().length);
    8282                        }
    83                         for (Number number : packed.getArray()) {
    84                             key = parseTagValue(key, layer, number, tagList);
     83                        for (long number : packed.getArray()) {
     84                            key = parseTagValue(key, layer, (int) number, tagList);
    8585                        }
    8686                    } else if (next.getField() == GEOMETRY_FIELD) {
    8787                        // This is packed in v1 and v2
    88                         ProtobufPacked packed = new ProtobufPacked(byteArrayOutputStream, next.getBytes());
     88                        ProtobufPacked packed = new ProtobufPacked(next.getBytes());
    8989                        CommandInteger currentCommand = null;
    90                         for (Number number : packed.getArray()) {
     90                        for (long number : packed.getArray()) {
    9191                            if (currentCommand != null && currentCommand.hasAllExpectedParameters()) {
    9292                                currentCommand = null;
    9393                            }
    9494                            if (currentCommand == null) {
    95                                 currentCommand = new CommandInteger(number.intValue());
     95                                currentCommand = new CommandInteger(Math.toIntExact(number));
    9696                                this.geometry.add(currentCommand);
    9797                            } else {
    9898                                currentCommand.addParameter(ProtobufParser.decodeZigZag(number));
     
    127127     * @param tagList The list to add the new value to
    128128     * @return The new key (if {@code null}, then a value was parsed and added to tags)
    129129     */
    130     private static String parseTagValue(String key, Layer layer, Number number, List<String> tagList) {
     130    private static String parseTagValue(String key, Layer layer, int number, List<String> tagList) {
    131131        if (key == null) {
    132             key = layer.getKey(number.intValue());
     132            key = layer.getKey(number);
    133133        } else {
    134134            tagList.add(key);
    135             Object value = layer.getValue(number.intValue());
     135            Object value = layer.getValue(number);
    136136            if (value instanceof Double || value instanceof Float) {
    137137                // reset grouping if the instance is a singleton
    138138
  • 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.
     2package org.openstreetmap.josm.data.osm.pbf;
     3
     4import java.io.ByteArrayInputStream;
     5import java.io.IOException;
     6import java.io.InputStream;
     7import java.util.zip.InflaterInputStream;
     8
     9import javax.annotation.Nonnull;
     10import javax.annotation.Nullable;
     11
     12import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
     13import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
     14import 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 */
     19public 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.
     2package org.openstreetmap.josm.data.osm.pbf;
     3
     4import javax.annotation.Nonnull;
     5import javax.annotation.Nullable;
     6
     7/**
     8 * A "BlobHeader" which contains metadata for a {@link Blob}.
     9 */
     10public 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.
     2package org.openstreetmap.josm.data.osm.pbf;
     3
     4import javax.annotation.Nonnull;
     5import javax.annotation.Nullable;
     6
     7import 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 */
     13public 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.
     2package org.openstreetmap.josm.data.osm.pbf;
     3
     4import javax.annotation.Nullable;
     5
     6/**
     7 * Optional metadata for primitives
     8 */
     9public 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.
     2package org.openstreetmap.josm.data.osm.pbf;
     3
     4import java.util.Arrays;
     5import java.util.Collections;
     6import java.util.List;
     7
     8/**
     9 * A class used to determine whether or not a file may be an OSM PBF file
     10 */
     11public 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 */
     9package 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  
    22package org.openstreetmap.josm.data.protobuf;
    33
    44import java.io.ByteArrayOutputStream;
    5 import java.util.ArrayList;
    6 import java.util.List;
     5import java.util.Arrays;
    76
    87/**
    98 * Parse packed values (only numerical values)
     
    1211 * @since 17862
    1312 */
    1413public class ProtobufPacked {
    15     private static final Number[] NO_NUMBERS = new Number[0];
    1614    private final byte[] bytes;
    17     private final Number[] numbers;
     15    private final long[] numbers;
    1816    private int location;
    1917
    2018    /**
    2119     * Create a new ProtobufPacked object
    2220     *
    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     *
    2433     * @param bytes The packed bytes
     34     * @since xxx
    2535     */
    26     public ProtobufPacked(ByteArrayOutputStream byteArrayOutputStream, byte[] bytes) {
     36    public ProtobufPacked(byte[] bytes) {
    2737        this.location = 0;
    2838        this.bytes = bytes;
    2939
    3040        // By creating a list of size bytes.length, we avoid 36 MB of allocations from list growth. This initialization
    3141        // only adds 3.7 MB to the ArrayList#init calls. Note that the real-world test case (Mapillary vector tiles)
    3242        // primarily created Shorts.
    33         List<Number> numbersT = new ArrayList<>(bytes.length);
     43        long[] numbersT = new long[bytes.length];
     44        int index = 0;
    3445        // By reusing a ByteArrayOutputStream, we can reduce allocations in nextVarInt from 230 MB to 74 MB.
    3546        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++;
    3751        }
    3852
    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        }
    4058    }
    4159
    4260    /**
     
    4462     *
    4563     * @return The number array
    4664     */
    47     public Number[] getArray() {
     65    public long[] getArray() {
    4866        return this.numbers;
    4967    }
    5068
    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 a
    54         // ByteArrayOutputStream, it went down to 230 MB. By further reusing the ByteArrayOutputStream between method
    55         // 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() {
    5674        while ((this.bytes[this.location] & ProtobufParser.MOST_SIGNIFICANT_BYTE)
    5775          == ProtobufParser.MOST_SIGNIFICANT_BYTE) {
    5876            // 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++;
    6079        }
    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;
    6881    }
    6982}
  • 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  
    77import java.io.IOException;
    88import java.io.InputStream;
    99import java.util.ArrayList;
     10import java.util.Arrays;
    1011import java.util.Collection;
    1112
    1213import org.openstreetmap.josm.tools.Logging;
     
    3031     * Used to get the most significant byte
    3132     */
    3233    static final byte MOST_SIGNIFICANT_BYTE = (byte) (1 << 7);
     34    private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
     35
    3336    /**
    3437     * Convert a byte array to a number (little endian)
    3538     *
     
    3841     * @return An appropriate {@link Number} class.
    3942     */
    4043    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) {
    4159        long number = 0;
    42         for (int i = 0; i < bytes.length; i++) {
     60        for (int i = start; i < end; i++) {
    4361            // 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));
    4563        }
    46         return convertLong(number);
     64        return number;
    4765    }
    4866
    4967    /**
     
    7189     * @return The decoded value
    7290     */
    7391    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);
    76104    }
    77105
    78106    /**
     
    203231     * @throws IOException - if an IO error occurs
    204232     */
    205233    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);
    207236        return readNextBytes(length);
    208237    }
    209238
     
    236265     * Read an arbitrary number of bytes
    237266     *
    238267     * @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)
    240269     * @throws IOException - if an IO error occurs
    241270     */
    242271    private byte[] readNextBytes(int size) throws IOException {
    243272        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);
    246278        }
    247279        return bytesRead;
    248280    }
  • 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  
    2727     * @throws IOException - if an IO error occurs
    2828     */
    2929    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);
    3132        // 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;
    3334        // 7 is 111 (so last three bits)
    34         byte wireType = (byte) (number.longValue() & 7);
     35        byte wireType = (byte) (number & 7);
    3536        // By not using a stream, we reduce the number of allocations (for getting the WireType) from 257 MB to 40 MB.
    3637        // (The remaining 40 MB is from WireType#values). By using the cached getAllValues(), we drop the 40 MB.
    3738        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  
    66import java.awt.geom.Rectangle2D;
    77import java.text.DecimalFormat;
    88import java.text.MessageFormat;
    9 import java.util.Objects;
     9import java.util.Arrays;
    1010
    1111import org.openstreetmap.josm.data.coor.ILatLon;
    1212import org.openstreetmap.josm.data.coor.LatLon;
     
    592592
    593593    @Override
    594594    public int hashCode() {
    595         return Objects.hash(minLat, minLon, maxLat, maxLon);
     595        return Arrays.hashCode(new double[] {minLat, minLon, maxLat, maxLon});
    596596    }
    597597
    598598    @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.
     2package org.openstreetmap.josm.gui.io.importexport;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import java.io.InputStream;
     7import java.util.Arrays;
     8
     9import org.openstreetmap.josm.actions.ExtensionFileFilter;
     10import org.openstreetmap.josm.data.osm.DataSet;
     11import org.openstreetmap.josm.gui.progress.ProgressMonitor;
     12import org.openstreetmap.josm.io.IllegalDataException;
     13import org.openstreetmap.josm.io.OsmPbfReader;
     14
     15/**
     16 * File importer that reads *.osm.pbf data files.
     17 */
     18public 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  
    273273
    274274    protected abstract DataSet doParseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException;
    275275
     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
    276287    @FunctionalInterface
    277288    protected interface ParserWorker {
    278289        /**
     
    284295        void accept(InputStreamReader ir) throws IllegalDataException, IOException;
    285296    }
    286297
     298    protected final DataSet doParseDataSet(InputStream source, ProgressMonitor progressMonitor, BinaryParserWorker parserWorker)
     299            throws IllegalDataException {
     300        return this.doParseDataSet(source, progressMonitor, (Object) parserWorker);
     301    }
     302
    287303    protected final DataSet doParseDataSet(InputStream source, ProgressMonitor progressMonitor, ParserWorker parserWorker)
    288304            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 {
    289310        if (progressMonitor == null) {
    290311            progressMonitor = NullProgressMonitor.INSTANCE;
    291312        }
     
    296317            progressMonitor.beginTask(tr("Prepare OSM data..."), 4); // read, prepare, post-process, render
    297318            progressMonitor.indeterminateSubTask(tr("Parsing OSM data..."));
    298319
    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());
    301328            }
    302329            progressMonitor.worked(1);
    303330
  • 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  
    178178
    179179    @Override
    180180    protected DataSet doParseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException {
    181         return doParseDataSet(source, progressMonitor, ir -> {
     181        return doParseDataSet(source, progressMonitor, (ParserWorker) ir -> {
    182182            setParser(Json.createParser(ir));
    183183            parse();
    184184        });
  • 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.
     2package org.openstreetmap.josm.io;
     3
     4import java.io.BufferedInputStream;
     5import java.io.ByteArrayInputStream;
     6import java.io.ByteArrayOutputStream;
     7import java.io.IOException;
     8import java.io.InputStream;
     9import java.util.ArrayList;
     10import java.util.Arrays;
     11import java.util.HashMap;
     12import java.util.HashSet;
     13import java.util.List;
     14import java.util.Locale;
     15import java.util.Map;
     16import java.util.Set;
     17
     18import javax.annotation.Nonnull;
     19import javax.annotation.Nullable;
     20
     21import org.apache.commons.compress.utils.CountingInputStream;
     22import org.openstreetmap.josm.data.Bounds;
     23import org.openstreetmap.josm.data.DataSource;
     24import org.openstreetmap.josm.data.coor.LatLon;
     25import org.openstreetmap.josm.data.osm.BBox;
     26import org.openstreetmap.josm.data.osm.DataSet;
     27import org.openstreetmap.josm.data.osm.NodeData;
     28import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
     29import org.openstreetmap.josm.data.osm.PrimitiveData;
     30import org.openstreetmap.josm.data.osm.RelationData;
     31import org.openstreetmap.josm.data.osm.RelationMemberData;
     32import org.openstreetmap.josm.data.osm.Tagged;
     33import org.openstreetmap.josm.data.osm.User;
     34import org.openstreetmap.josm.data.osm.WayData;
     35import org.openstreetmap.josm.data.osm.pbf.Blob;
     36import org.openstreetmap.josm.data.osm.pbf.BlobHeader;
     37import org.openstreetmap.josm.data.osm.pbf.HeaderBlock;
     38import org.openstreetmap.josm.data.osm.pbf.Info;
     39import org.openstreetmap.josm.data.protobuf.ProtobufPacked;
     40import org.openstreetmap.josm.data.protobuf.ProtobufParser;
     41import org.openstreetmap.josm.data.protobuf.ProtobufRecord;
     42import org.openstreetmap.josm.data.protobuf.WireType;
     43import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
     44import org.openstreetmap.josm.gui.progress.ProgressMonitor;
     45import org.openstreetmap.josm.tools.Utils;
     46
     47/**
     48 * Read OSM data from an OSM PBF file
     49 */
     50public 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  
    501501
    502502    @Override
    503503    protected DataSet doParseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException {
    504         return doParseDataSet(source, progressMonitor, ir -> {
     504        return doParseDataSet(source, progressMonitor, (ParserWorker) ir -> {
    505505            try {
    506506                setParser(XmlUtils.newSafeXMLInputFactory().createXMLStreamReader(ir));
    507507                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.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import static org.junit.jupiter.api.Assertions.assertArrayEquals;
     5
     6import java.io.ByteArrayOutputStream;
     7
     8import org.junit.jupiter.api.Test;
     9
     10/**
     11 * Test class for {@link ProtobufPacked}
     12 */
     13class 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  
    4242 * @author Taylor Smock
    4343 * @since 17862
    4444 */
    45 class ProtobufTest {
     45public class ProtobufTest {
    4646    /**
    4747     * Convert an int array into a byte array
    4848     * @param intArray The int array to convert (NOTE: numbers must be below 255)
    4949     * @return A byte array that can be used
    5050     */
    51     static byte[] toByteArray(int[] intArray) {
     51    public static byte[] toByteArray(int[] intArray) {
    5252        byte[] byteArray = new byte[intArray.length];
    5353        for (int i = 0; i < intArray.length; i++) {
    5454            if (intArray[i] > Byte.MAX_VALUE - Byte.MIN_VALUE) {
     
    201201
    202202    @Test
    203203    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());
    208208    }
    209209}
  • 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.
     2package org.openstreetmap.josm.gui.io.importexport;
     3
     4import static org.junit.jupiter.api.Assertions.assertAll;
     5import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
     6import static org.junit.jupiter.api.Assertions.assertEquals;
     7import static org.junit.jupiter.api.Assertions.assertSame;
     8import static org.junit.jupiter.api.Assertions.assertThrows;
     9import static org.junit.jupiter.api.Assertions.assertTrue;
     10
     11import java.io.ByteArrayInputStream;
     12import java.io.IOException;
     13import java.io.InputStream;
     14import java.nio.file.Files;
     15import java.nio.file.Paths;
     16import java.util.Arrays;
     17
     18import org.junit.jupiter.api.BeforeAll;
     19import org.junit.jupiter.api.Test;
     20import org.openstreetmap.josm.TestUtils;
     21import org.openstreetmap.josm.data.coor.ILatLon;
     22import org.openstreetmap.josm.data.coor.LatLon;
     23import org.openstreetmap.josm.data.osm.AbstractPrimitive;
     24import org.openstreetmap.josm.data.osm.DataSet;
     25import org.openstreetmap.josm.data.osm.Relation;
     26import org.openstreetmap.josm.data.osm.Way;
     27import org.openstreetmap.josm.data.protobuf.ProtobufTest;
     28import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
     29import org.openstreetmap.josm.io.IllegalDataException;
     30import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
     31
     32/**
     33 * Test class for {@link OsmPbfImporter}
     34 */
     35@BasicPreferences
     36class 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