Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![NuGet Status](http://img.shields.io/nuget/v/mapbox-vector-tile.svg?style=flat)](https://www.nuget.org/packages/mapbox-vector-tile/) ![.NET 8](https://github.com/bertt/mapbox-vector-tile-cs/workflows/.NET%208/badge.svg)

.NET Standard 2.0 library for decoding a Mapbox vector tile.
.NET Standard 2.0 library for encoding and decoding Mapbox vector tiles.

## Dependencies

Expand All @@ -16,12 +16,44 @@ $ Install-Package mapbox-vector-tile

## Usage

### Decoding

```cs
const string vtfile = "vectortile.pbf";
var stream = File.OpenRead(vtfile);
var layerInfos = VectorTileParser.Parse(stream);
```

### Encoding

```cs
// Create a layer
var layer = new VectorTileLayer("my_layer", 2, 4096);

// Create a feature with attributes and geometry
var attributes = new List<KeyValuePair<string, object>>
{
new KeyValuePair<string, object>("name", "Example"),
new KeyValuePair<string, object>("value", 42)
};

var coordinates = new[] { new Coordinate(100, 200) };
var geometry = new List<ArraySegment<Coordinate>>
{
new ArraySegment<Coordinate>(coordinates)
};

var feature = new VectorTileFeature("1", geometry, attributes, Tile.GeomType.Point, 4096);
layer.VectorTileFeatures.Add(feature);

// Encode to stream
var layers = new List<VectorTileLayer> { layer };
var stream = VectorTileEncoder.Encode(layers, new MemoryStream());

// Save to file
File.WriteAllBytes("output.pbf", ((MemoryStream)stream).ToArray());
```

Tip: If you use this library with vector tiles loading from a webserver, you could run into the following exception:
'ProtoBuf.ProtoException: Invalid wire-type; this usually means you have over-written a file without truncating or setting the length'
Probably you need to check the GZip compression, see also TileParserTests.cs for an example.
Expand Down
110 changes: 110 additions & 0 deletions src/AttributesEncoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;

namespace Mapbox.Vector.Tile;

public static class AttributesEncoder
{
public static List<uint> Encode(List<KeyValuePair<string, object>> attributes, List<string> keys, List<Tile.Value> values)
{
var tags = new List<uint>();

foreach (var attribute in attributes)
{
var key = attribute.Key;
var value = attribute.Value;

// Get or add key index
var keyIndex = keys.IndexOf(key);
if (keyIndex == -1)
{
keyIndex = keys.Count;
keys.Add(key);
}

// Get or add value index
var tileValue = CreateTileValue(value);
var valueIndex = FindValueIndex(values, tileValue);
if (valueIndex == -1)
{
valueIndex = values.Count;
values.Add(tileValue);
}

tags.Add((uint)keyIndex);
tags.Add((uint)valueIndex);
}

return tags;
}

private static Tile.Value CreateTileValue(object value)
{
var tileValue = new Tile.Value();

switch (value)
{
case string stringValue:
tileValue.StringValue = stringValue;
break;
case bool boolValue:
tileValue.BoolValue = boolValue;
break;
case float floatValue:
tileValue.FloatValue = floatValue;
break;
case double doubleValue:
tileValue.DoubleValue = doubleValue;
break;
case int intValue:
tileValue.IntValue = intValue;
break;
case long longValue:
tileValue.IntValue = longValue;
break;
case uint uintValue:
tileValue.UintValue = uintValue;
break;
case ulong ulongValue:
tileValue.UintValue = ulongValue;
break;
default:
throw new NotImplementedException($"Unsupported attribute type: {value.GetType()}");
}

return tileValue;
}

private static int FindValueIndex(List<Tile.Value> values, Tile.Value newValue)
{
for (int i = 0; i < values.Count; i++)
{
var existingValue = values[i];
if (ValuesEqual(existingValue, newValue))
{
return i;
}
}
return -1;
}

private static bool ValuesEqual(Tile.Value a, Tile.Value b)
{
if (a.HasStringValue && b.HasStringValue)
return a.StringValue == b.StringValue;
if (a.HasBoolValue && b.HasBoolValue)
return a.BoolValue == b.BoolValue;
if (a.HasFloatValue && b.HasFloatValue)
return Math.Abs(a.FloatValue - b.FloatValue) < 0.0001f;
if (a.HasDoubleValue && b.HasDoubleValue)
return Math.Abs(a.DoubleValue - b.DoubleValue) < 0.0001;
if (a.HasIntValue && b.HasIntValue)
return a.IntValue == b.IntValue;
if (a.HasSIntValue && b.HasSIntValue)
return a.SintValue == b.SintValue;
if (a.HasUIntValue && b.HasUIntValue)
return a.UintValue == b.UintValue;

return false;
}
}
88 changes: 88 additions & 0 deletions src/GeometryEncoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Mapbox.Vector.Tile;

public static class GeometryEncoder
{
private enum Command
{
MoveTo = 1,
LineTo = 2,
ClosePath = 7
}

public static List<uint> EncodeGeometry(List<ArraySegment<Coordinate>> geometry, Tile.GeomType geomType)
{
var commands = new List<uint>();
long lastX = 0;
long lastY = 0;

foreach (var segment in geometry)
{
if (segment.Count == 0)
continue;

// Access the underlying array and use offset
var array = segment.Array!;
var offset = segment.Offset;
var count = segment.Count;

// MoveTo command for first point
var firstPoint = array[offset];
commands.Add(EncodeCommand(Command.MoveTo, 1));

var dx = firstPoint.X - lastX;
var dy = firstPoint.Y - lastY;
commands.Add((uint)ZigZag.Encode(dx));
commands.Add((uint)ZigZag.Encode(dy));

lastX = firstPoint.X;
lastY = firstPoint.Y;

// LineTo commands for remaining points (excluding last if it's a duplicate of first)
var lineToCount = count - 1;

// For polygons, check if last point equals first point
if (geomType == Tile.GeomType.Polygon && count > 1)
{
var lastPoint = array[offset + count - 1];
if (lastPoint.X == firstPoint.X && lastPoint.Y == firstPoint.Y)
{
lineToCount--;
}
}

if (lineToCount > 0)
{
commands.Add(EncodeCommand(Command.LineTo, (uint)lineToCount));

for (int i = 1; i <= lineToCount; i++)
{
var point = array[offset + i];
dx = point.X - lastX;
dy = point.Y - lastY;
commands.Add((uint)ZigZag.Encode(dx));
commands.Add((uint)ZigZag.Encode(dy));

lastX = point.X;
lastY = point.Y;
}
}

// ClosePath command for polygons only
if (geomType == Tile.GeomType.Polygon)
{
commands.Add(EncodeCommand(Command.ClosePath, 1));
}
}

return commands;
}

private static uint EncodeCommand(Command command, uint count)
{
return (count << 3) | (uint)command;
}
}
53 changes: 53 additions & 0 deletions src/VectorTileEncoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using ProtoBuf;
using System.Collections.Generic;
using System.IO;

namespace Mapbox.Vector.Tile;

public static class VectorTileEncoder
{
public static Stream Encode(List<VectorTileLayer> layers, Stream stream)
{
var tile = new Tile();

foreach (var vectorTileLayer in layers)
{
var layer = new Tile.Layer
{
Name = vectorTileLayer.Name,
Version = vectorTileLayer.Version,
Extent = vectorTileLayer.Extent
};

var keys = new List<string>();
var values = new List<Tile.Value>();

foreach (var vectorTileFeature in vectorTileLayer.VectorTileFeatures)
{
var feature = new Tile.Feature
{
Id = ulong.Parse(vectorTileFeature.Id),
Type = vectorTileFeature.GeometryType
};

// Encode attributes
var tags = AttributesEncoder.Encode(vectorTileFeature.Attributes, keys, values);
feature.Tags.AddRange(tags);

// Encode geometry
var geometry = GeometryEncoder.EncodeGeometry(vectorTileFeature.Geometry, vectorTileFeature.GeometryType);
feature.Geometry.AddRange(geometry);

layer.Features.Add(feature);
}

layer.Keys.AddRange(keys);
layer.Values.AddRange(values);

tile.Layers.Add(layer);
}

Serializer.Serialize(stream, tile);
return stream;
}
}
46 changes: 46 additions & 0 deletions tests/RealTileRoundTripTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Mapbox.Vector.Tile;
using NUnit.Framework;
using System.IO;

namespace mapbox.vector.tile.tests;

public class RealTileRoundTripTest
{
[Test]
public void TestRealTileRoundTrip()
{
// Test that we can decode a real tile and encode it back
var pbfFile = Path.Combine("testdata", "14-8801-5371.vector.pbf");

// Decode the original tile
var originalStream = File.OpenRead(pbfFile);
var decodedLayers = VectorTileParser.Parse(originalStream);
originalStream.Close();

Assert.That(decodedLayers.Count, Is.GreaterThan(0), "Should have decoded at least one layer");

// Encode it back
var encodedStream = new MemoryStream();
VectorTileEncoder.Encode(decodedLayers, encodedStream);
encodedStream.Seek(0, SeekOrigin.Begin);

Assert.That(encodedStream.Length, Is.GreaterThan(0), "Encoded stream should not be empty");

// Decode the encoded tile
var reDecodedLayers = VectorTileParser.Parse(encodedStream);

// Verify layer count matches
Assert.That(reDecodedLayers.Count, Is.EqualTo(decodedLayers.Count), "Layer count should match");

// Verify each layer
for (int i = 0; i < decodedLayers.Count; i++)
{
var original = decodedLayers[i];
var reDecoded = reDecodedLayers[i];

Assert.That(reDecoded.Name, Is.EqualTo(original.Name), $"Layer {i} name should match");
Assert.That(reDecoded.VectorTileFeatures.Count, Is.EqualTo(original.VectorTileFeatures.Count),
$"Layer '{original.Name}' feature count should match");
}
}
}
Loading
Loading