OsmSharp

repository·develop·Indexed 43 days ago

https://github.com/osmsharp/core

A .NET library for reading and writing OpenStreetMap (OSM) data in OSM-XML and OSM-PBF formats. It features a streamed architecture for minimal memory footprint, support for LINQ-based filtering, and geographic extraction via bounding boxes and polygons. The library includes the OsmSharp.Geo package for integration with NetTopologySuite (NTS) to convert OSM data into geometries and features.

Tokens
5.9K
Snippets
15
Records
20
Agent score
55%

What's inside OsmSharp

  1. How the memory-bounded batching pattern works

    develop

    The PBFCompleteWayReader avoids the massive memory overhead of a global node coordinate dictionary by processing the PBF in discrete batches. This is achieved through the following lifecycle:

    1. Way Collection: The reader iterates through ways using MoveNext(OsmGeoType.Way, lastId + 1). It buffers way metadata and tags and accumulates the required node IDs into a HashSet<long>.
    2. Budget Trigger: Once the number of unique node IDs in the current batch reaches the nodeBudget, the collection phase stops.
    3. Node Fetching: The reader calls source.Reset() and uses MoveNext(OsmGeoType.Node, sortedId) to perform a monotonic walk of the required node IDs. Because PBFOsmStreamSource maintains a block index, it can skip entire blobs that do not contain the requested IDs, making this second pass efficient.
    4. Materialization: Each fetched Node is stored in a dictionary. The reader then constructs CompleteWay objects by combining the buffered way data with the fetched node data.
    5. Cleanup: After the batch is yielded, the way buffer, node ID set, and node dictionary are discarded. The process repeats for the next range of ways.

    Note on Performance: The first batch may have a higher 'collect' time because it warms up the PBFOsmStreamSource block index by walking the node section. Subsequent batches benefit from a warm index and faster skips.

  2. Use persistent block-index sidecars with PBFOsmStreamSource

    develop

    The PBFOsmStreamSource supports a persistent block-index sidecar that allows for fast, targeted jumps to specific OSM primitive types (Nodes, Ways, or Relations) without decoding unnecessary blobs.

    When persistBlockIndex is set to true, the source builds an index as it walks the PBF. This index contains:

    • Tier-0: File offsets (start and end) for each blob on disk.
    • Tier-1: Presence of primitive types (HasNodes, HasWays, or HasRelations).
    • Tier-2: Min/max IDs for the types present.

    On Dispose(), the index is written to a file named {pbfPath}.blockindex. Subsequent runs with the same PBF will automatically load this sidecar, allowing the source to seek past irrelevant blobs without decompression.

    // Persist the accreted block index on Dispose() and periodically during the walk.
    using var src = new PBFOsmStreamSource(pbf, persistBlockIndex: true);
    foreach (var geo in src) { /* ... */ }
  3. Migrate from OsmSharp 5.x to 6.x

    develop

    When upgrading from version 5.x to 6.x, be aware of the following breaking changes:

    • Coordinate Precision: Latitude and longitude values on Nodes have been changed from float to double. Ensure your code accounts for this type change to maintain precision and avoid compilation errors.
    • Platform Support: PCL (Portable Class Library) profiles have been removed. Support for mobile/cross-platform scenarios is now handled via .NET Standard (netstandard1.3 and netstandard2.0).
  4. Convert an OSM stream to complete objects using ToComplete()

    develop

    When streaming from OSM-PBF or OSM-XML files, ways and relations only contain references to nodes or members by their IDs. OsmSharp provides the ToComplete() extension method to transform a stream of partial objects into a stream of complete objects, where the referenced nodes or members are actually included in the object.

    Important Memory Warning: Using ToComplete() requires keeping the referenced nodes/members in memory. To avoid OutOfMemoryException or excessive memory usage, you must apply all necessary filters (e.g., filtering by type or tags) to your stream before calling .ToComplete(). This ensures only the required nodes are held in memory.

    using (var fileStream = File.OpenRead("path/to/file.osm.pbf'))
    {
      // create source stream.
      var source = new PBFOsmStreamSource(fileStream);
      
      // filter all powerlines and keep all nodes.
      // IMPORTANT: Filter BEFORE calling ToComplete() to manage memory.
      var filtered = from osmGeo in source
       where osmGeo.Type == OsmSharp.OsmGeoType.Node || 
        (osmGeo.Type == OsmSharp.OsmGeoType.Way && osmGeo.Tags != null && osmGeo.Tags.Contains("power", "line"))
       select osmGeo;
    
      // convert to complete stream.
      var complete = filtered.ToComplete();
    }
  5. Convert OSM data to Shapefile using GeometryStream

    develop

    To extract geographical information from OSM data (like PBF files) and convert it to a Shapefile, you should follow a streaming pipeline:

    1. Create a source stream: Use PBFOsmStreamSource to read the OSM file.
    2. Filter OSM objects: Use LINQ to filter the OsmGeo objects (e.g., filtering by OsmGeoType or specific tags) before converting to a feature stream. This is critical for memory management; converting all objects to a feature stream will keep them in-memory.
    3. Convert to FeatureSource: Use the .ToFeatureSource() extension method on your filtered OSM stream.
    4. Filter Geometries: Filter the resulting features by their geometry type (e.g., where feature.Geometry is LineString).
    5. Build a FeatureCollection: Create a FeatureCollection and an AttributesTable. When adding features, ensure you use a constant number of attributes with the same names for all features to maintain Shapefile compatibility.
    6. Write to Shapefile: Use ShapefileDataWriter.GetHeader to generate a header based on the first feature and the total count, then use ShapefileDataWriter.Write to save the collection.
    using (var fileStream = File.OpenRead("path/to/file.osm.pbf"))
    {
      // 1. Create source stream
      var source = new PBFOsmStreamSource(fileStream);
      
      // 2. Filter OSM objects (CRITICAL: filter BEFORE ToFeatureSource to save memory)
      var filtered = from osmGeo in source
       where osmGeo.Type == OsmSharp.OsmGeoType.Node || 
        (osmGeo.Type == OsmSharp.OsmGeoType.Way && osmGeo.Tags != null && osmGeo.Tags.Contains("power", "line"))
       select osmGeo;
    
      // 3. Convert to feature stream
      var features = filtered.ToFeatureSource();
    
      // 4. Filter geometries
      var lineStrings = from feature in features
        where feature.Geometry is LineString
        select feature;
    
     // 5. Build feature collection
     var featureCollection = new FeatureCollection();
     var attributesTable = new AttributesTable();
     attributesTable.AddAttribute("type", "powerline");
     foreach (var feature in lineStrings)
     {
        // Ensure constant # of attributes with same names before writing
        featureCollection.Add(new Feature(feature.Geometry, attributesTable));
     }
    
     // 6. Convert to shape
     var header = ShapefileDataWriter.GetHeader(featureCollection.Features.First(), featureCollection.Features.Count);
     var shapeWriter = new ShapefileDataWriter("luxembourg.shp", new GeometryFactory())
     {
        Header = header
     };
     shapeWriter.Write(featureCollection.Features);
    }
  6. Install OsmSharp and OsmSharp.Geo

    develop

    You can install OsmSharp via NuGet Package Manager. For integration with NetTopologySuite (NTS) to convert OSM data into geometries/features, install the OsmSharp.Geo package.

    PM> Install-Package OsmSharp
    PM> Install-Package OsmSharp.Geo
  7. Run the CompleteBuildings sample project

    develop

    You can run the Sample.CompleteBuildings project using the dotnet CLI. You can control the memory usage by specifying a nodeBudget via a command-line argument. The budget represents the maximum number of unique node IDs held in memory at once.

    • Default behavior: Uses a 50,000 node budget.
    • Custom budget: Pass the budget as a trailing argument to the command.
    # Use the default 50k node budget
    dotnet run --project samples/Sample.CompleteBuildings
    
    # Use a custom 500k node budget
    dotnet run --project samples/Sample.CompleteBuildings -- 500000
    dotnet run --project samples/Sample.CompleteBuildings                # default 50k node budget
    dotnet run --project samples/Sample.CompleteBuildings -- 500000      # 500k node budget (bigger batch, fewer)
  8. Supported .NET Runtimes for OsmSharp 6.x

    develop

    OsmSharp 6.x provides expanded compatibility through .NET Standard support.

    • OsmSharp Core: Supports netstandard1.3 and netstandard2.0. (Note: PCL profiles have been removed as they are superseded by .NET Standard).
    • OsmSharp.Geo: Currently supports .NET Framework 4.5 and higher. Support for .NET Standard is pending stable releases from NTS (NetTopologySuite).
  9. Run the Sample.PBFBlockIndex sample

    develop

    To run the sample project, use the dotnet CLI. By default, it downloads a sample PBF (Luxembourg, ~52 MB) to the working directory. You can also provide a path to your own PBF file as a command-line argument.

    # Run with default sample data
    dotnet run -c Release
    
    # Run against your own PBF file
    dotnet run -c Release -- /path/to/some.osm.pbf
  10. Filter OSM data using a bounding box

    develop

    You can perform a geographic filter to cut out a specific area from an OSM data stream using a bounding box. This is achieved by calling FilterBox on an PBFOsmStreamSource. The method keeps ways and relations that use one or more nodes located within the specified coordinates.

    Note: In OSM data, only nodes contain direct geographical information, so the filter logic relies on the nodes contained within the bounding box to determine which ways and relations to retain.

    using (var fileStream = File.OpenRead("path/to/file.osm.pbf))
    {
      // create source stream.
      var source = new PBFOsmStreamSource(fileStream); 
      
      // filter by keeping everything inside the given bounding box.
      var filtered = source.FilterBox(6.238002777099609f, 49.72076145492323f, 
        6.272850036621093f, 49.69928180928878f);
      // ...
    }
  11. Filter OSM objects using LINQ

    develop

    When streaming data from OSM-PBF or OSM-XML files using OsmSharp, the source is provided as an IEnumerable<T>. This allows you to use standard LINQ queries to filter the stream and select only the specific objects you need based on their properties (e.g., UserName).

    To filter objects, wrap your file stream in a PBFOsmStreamSource (for PBF files) and apply a where clause to the resulting stream.

    using (var fileStream = File.OpenRead("path/to/file.osm.pbf"))
    {
      // create source stream.
      var source = new PBFOsmStreamSource(fileStream); 
      
      // let's use linq to leave only objects last touched by the given mapper.
      var filtered = from osmGeo in source where 
        osmGeo.UserName == "Javier Gutiérrez" 
        select osmGeo;  
        
      // 'filtered' is now an IEnumerable containing only matching objects
    }
  12. Filter OSM data by a bounding box

    develop

    You can extract a specific geographic region from a PBF source using the FilterBox method. This method takes four float parameters: left, top, right, and bottom. To save the filtered results, use a PBFOsmStreamTarget, register the filtered source using RegisterSource(), and then execute the extraction with Pull().

    var source = new PBFOsmStreamSource(
    	new FileInfo(@"/path/to/file.osm.pbf").OpenRead());
    
    var filtered = source.FilterBox(6.238002777099609f, 49.72076145492323f, 
    	6.272850036621093f, 49.69928180928878f); // left, top, right, bottom
    
    using (var stream = new FileInfo(@"/path/to/filterede.osm.pbf").Open(FileMode.Create, FileAccess.ReadWrite))
    {
       var target = new PBFOsmStreamTarget(stream);
       target.RegisterSource(filtered);
       target.Pull();
    }