OsmPbfReader.java

package nl.tudelft.simulation.dsol.animation.gis.osm;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.InflaterInputStream;

import com.google.protobuf.ByteString;

import nl.tudelft.simulation.dsol.animation.gis.osm.pbf.OSMFileFormat;
import nl.tudelft.simulation.dsol.animation.gis.osm.pbf.OSMPBF;

/**
 * OsmPbfReader reads from an OSM PBF stream.
 * <p>
 * Copyright (c) 2025-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
 * for project information <a href="https://simulation.tudelft.nl/" target="_blank"> https://simulation.tudelft.nl</a>. The DSOL
 * project is distributed under a three-clause BSD-style license, which can be found at
 * <a href="https://https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">
 * https://https://simulation.tudelft.nl/dsol/docs/latest/license.html</a>.
 * </p>
 * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
 */
public class OsmPbfReader
{

    /**
     * Create a PBF reader.
     * @param inputFile the input file
     * @param processor the entity processor
     * @throws IOException on I/O error
     */
    public OsmPbfReader(final File inputFile, final OsmEntityProcessor processor) throws IOException
    {
        FileInputStream fis = new FileInputStream(inputFile.getAbsolutePath());
        DataInputStream dis = new DataInputStream(new BufferedInputStream(fis));

        while (dis.available() > 0)
        {
            int headerSize = dis.readInt();
            byte[] headerBytes = dis.readNBytes(headerSize);
            OSMFileFormat.BlobHeader header = OSMFileFormat.BlobHeader.parseFrom(headerBytes);

            byte[] blobBytes = dis.readNBytes(header.getDatasize());
            OSMFileFormat.Blob blob = OSMFileFormat.Blob.parseFrom(blobBytes);

            InputStream blobStream;
            if (blob.hasZlibData())
            {
                blobStream = new InflaterInputStream(new ByteArrayInputStream(blob.getZlibData().toByteArray()));
            }
            else if (blob.hasRaw())
            {
                blobStream = blob.getRaw().newInput();
            }
            else
            {
                continue;
            }

            if ("OSMData".equals(header.getType()))
            {
                OSMPBF.PrimitiveBlock block = OSMPBF.PrimitiveBlock.parseFrom(blobStream);
                parsePrimitiveBlock(block, processor);
            }
        }

        dis.close();
        fis.close();
    }

    /**
     * Parse a block of entities.
     * @param block the block
     * @param processor the processor to further process the entities
     */
    public static void parsePrimitiveBlock(final OSMPBF.PrimitiveBlock block, final OsmEntityProcessor processor)
    {
        long granularity = block.getGranularity();
        long latOffset = block.getLatOffset();
        long lonOffset = block.getLonOffset();
        List<ByteString> stringTable = block.getStringtable().getSList();

        for (OSMPBF.PrimitiveGroup group : block.getPrimitivegroupList())
        {
            if (group.hasDense())
            {
                long id = 0, lat = 0, lon = 0;
                for (int i = 0; i < group.getDense().getIdCount(); i++)
                {
                    id += group.getDense().getId(i);
                    lat += group.getDense().getLat(i);
                    lon += group.getDense().getLon(i);

                    double latDeg = 1e-9 * (latOffset + lat * granularity);
                    double lonDeg = 1e-9 * (lonOffset + lon * granularity);

                    Node node = new Node(id, latDeg, lonDeg);
                    processor.process(node);
                }
            }

            for (OSMPBF.Way way : group.getWaysList())
            {
                long wayId = way.getId();
                List<Long> refs = new ArrayList<>();
                long ref = 0;
                for (long delta : way.getRefsList())
                {
                    ref += delta;
                    refs.add(ref);
                }

                Map<String, String> tags = new HashMap<>();
                for (int i = 0; i < way.getKeysCount(); i++)
                {
                    String k = stringTable.get(way.getKeys(i)).toStringUtf8();
                    String v = stringTable.get(way.getVals(i)).toStringUtf8();
                    tags.put(k, v);
                }

                Way w = new Way(wayId, refs, tags);
                processor.process(w);
            }

            for (OSMPBF.Relation relation : group.getRelationsList())
            {
                long relationId = relation.getId();
                Relation rel = new Relation(relationId);
                long lastId = 0; // note that relation.getMemids(i) contains delta-encoded values, not absolute IDs.
                for (int i = 0; i < relation.getMemidsCount(); i++)
                {
                    long delta = relation.getMemids(i);
                    long ref = lastId + delta;
                    lastId = ref;
                    String role = stringTable.get(relation.getRolesSid(i)).toStringUtf8();
                    Relation.Type type = switch (relation.getTypes(i))
                    {
                        case NODE -> Relation.Type.NODE;
                        case WAY -> Relation.Type.WAY;
                        case RELATION -> Relation.Type.RELATION;
                    };
                    Relation.Member member = new Relation.Member(type, ref, role);
                    rel.addMember(member);
                }
                for (int i = 0; i < relation.getKeysCount(); i++)
                {
                    String k = stringTable.get(relation.getKeys(i)).toStringUtf8();
                    String v = stringTable.get(relation.getVals(i)).toStringUtf8();
                    rel.addTag(k, v);
                }
                processor.process(rel);
            }
        }
    }
}