Skip to content

Commit f563f6f

Browse files
authored
Merge pull request #39 from bertt/copilot/fix-538812-38310808-3ac9b15f-ca6f-4813-b1cc-5c813b410298
Add tile encoder to complement existing decoder
2 parents 8b1a388 + f3b725f commit f563f6f

6 files changed

Lines changed: 514 additions & 1 deletion

File tree

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![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)
44

5-
.NET Standard 2.0 library for decoding a Mapbox vector tile.
5+
.NET Standard 2.0 library for encoding and decoding Mapbox vector tiles.
66

77
## Dependencies
88

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

1717
## Usage
1818

19+
### Decoding
20+
1921
```cs
2022
const string vtfile = "vectortile.pbf";
2123
var stream = File.OpenRead(vtfile);
2224
var layerInfos = VectorTileParser.Parse(stream);
2325
```
2426

27+
### Encoding
28+
29+
```cs
30+
// Create a layer
31+
var layer = new VectorTileLayer("my_layer", 2, 4096);
32+
33+
// Create a feature with attributes and geometry
34+
var attributes = new List<KeyValuePair<string, object>>
35+
{
36+
new KeyValuePair<string, object>("name", "Example"),
37+
new KeyValuePair<string, object>("value", 42)
38+
};
39+
40+
var coordinates = new[] { new Coordinate(100, 200) };
41+
var geometry = new List<ArraySegment<Coordinate>>
42+
{
43+
new ArraySegment<Coordinate>(coordinates)
44+
};
45+
46+
var feature = new VectorTileFeature("1", geometry, attributes, Tile.GeomType.Point, 4096);
47+
layer.VectorTileFeatures.Add(feature);
48+
49+
// Encode to stream
50+
var layers = new List<VectorTileLayer> { layer };
51+
var stream = VectorTileEncoder.Encode(layers, new MemoryStream());
52+
53+
// Save to file
54+
File.WriteAllBytes("output.pbf", ((MemoryStream)stream).ToArray());
55+
```
56+
2557
Tip: If you use this library with vector tiles loading from a webserver, you could run into the following exception:
2658
'ProtoBuf.ProtoException: Invalid wire-type; this usually means you have over-written a file without truncating or setting the length'
2759
Probably you need to check the GZip compression, see also TileParserTests.cs for an example.

src/AttributesEncoder.cs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace Mapbox.Vector.Tile;
5+
6+
public static class AttributesEncoder
7+
{
8+
public static List<uint> Encode(List<KeyValuePair<string, object>> attributes, List<string> keys, List<Tile.Value> values)
9+
{
10+
var tags = new List<uint>();
11+
12+
foreach (var attribute in attributes)
13+
{
14+
var key = attribute.Key;
15+
var value = attribute.Value;
16+
17+
// Get or add key index
18+
var keyIndex = keys.IndexOf(key);
19+
if (keyIndex == -1)
20+
{
21+
keyIndex = keys.Count;
22+
keys.Add(key);
23+
}
24+
25+
// Get or add value index
26+
var tileValue = CreateTileValue(value);
27+
var valueIndex = FindValueIndex(values, tileValue);
28+
if (valueIndex == -1)
29+
{
30+
valueIndex = values.Count;
31+
values.Add(tileValue);
32+
}
33+
34+
tags.Add((uint)keyIndex);
35+
tags.Add((uint)valueIndex);
36+
}
37+
38+
return tags;
39+
}
40+
41+
private static Tile.Value CreateTileValue(object value)
42+
{
43+
var tileValue = new Tile.Value();
44+
45+
switch (value)
46+
{
47+
case string stringValue:
48+
tileValue.StringValue = stringValue;
49+
break;
50+
case bool boolValue:
51+
tileValue.BoolValue = boolValue;
52+
break;
53+
case float floatValue:
54+
tileValue.FloatValue = floatValue;
55+
break;
56+
case double doubleValue:
57+
tileValue.DoubleValue = doubleValue;
58+
break;
59+
case int intValue:
60+
tileValue.IntValue = intValue;
61+
break;
62+
case long longValue:
63+
tileValue.IntValue = longValue;
64+
break;
65+
case uint uintValue:
66+
tileValue.UintValue = uintValue;
67+
break;
68+
case ulong ulongValue:
69+
tileValue.UintValue = ulongValue;
70+
break;
71+
default:
72+
throw new NotImplementedException($"Unsupported attribute type: {value.GetType()}");
73+
}
74+
75+
return tileValue;
76+
}
77+
78+
private static int FindValueIndex(List<Tile.Value> values, Tile.Value newValue)
79+
{
80+
for (int i = 0; i < values.Count; i++)
81+
{
82+
var existingValue = values[i];
83+
if (ValuesEqual(existingValue, newValue))
84+
{
85+
return i;
86+
}
87+
}
88+
return -1;
89+
}
90+
91+
private static bool ValuesEqual(Tile.Value a, Tile.Value b)
92+
{
93+
if (a.HasStringValue && b.HasStringValue)
94+
return a.StringValue == b.StringValue;
95+
if (a.HasBoolValue && b.HasBoolValue)
96+
return a.BoolValue == b.BoolValue;
97+
if (a.HasFloatValue && b.HasFloatValue)
98+
return Math.Abs(a.FloatValue - b.FloatValue) < 0.0001f;
99+
if (a.HasDoubleValue && b.HasDoubleValue)
100+
return Math.Abs(a.DoubleValue - b.DoubleValue) < 0.0001;
101+
if (a.HasIntValue && b.HasIntValue)
102+
return a.IntValue == b.IntValue;
103+
if (a.HasSIntValue && b.HasSIntValue)
104+
return a.SintValue == b.SintValue;
105+
if (a.HasUIntValue && b.HasUIntValue)
106+
return a.UintValue == b.UintValue;
107+
108+
return false;
109+
}
110+
}

src/GeometryEncoder.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace Mapbox.Vector.Tile;
6+
7+
public static class GeometryEncoder
8+
{
9+
private enum Command
10+
{
11+
MoveTo = 1,
12+
LineTo = 2,
13+
ClosePath = 7
14+
}
15+
16+
public static List<uint> EncodeGeometry(List<ArraySegment<Coordinate>> geometry, Tile.GeomType geomType)
17+
{
18+
var commands = new List<uint>();
19+
long lastX = 0;
20+
long lastY = 0;
21+
22+
foreach (var segment in geometry)
23+
{
24+
if (segment.Count == 0)
25+
continue;
26+
27+
// Access the underlying array and use offset
28+
var array = segment.Array!;
29+
var offset = segment.Offset;
30+
var count = segment.Count;
31+
32+
// MoveTo command for first point
33+
var firstPoint = array[offset];
34+
commands.Add(EncodeCommand(Command.MoveTo, 1));
35+
36+
var dx = firstPoint.X - lastX;
37+
var dy = firstPoint.Y - lastY;
38+
commands.Add((uint)ZigZag.Encode(dx));
39+
commands.Add((uint)ZigZag.Encode(dy));
40+
41+
lastX = firstPoint.X;
42+
lastY = firstPoint.Y;
43+
44+
// LineTo commands for remaining points (excluding last if it's a duplicate of first)
45+
var lineToCount = count - 1;
46+
47+
// For polygons, check if last point equals first point
48+
if (geomType == Tile.GeomType.Polygon && count > 1)
49+
{
50+
var lastPoint = array[offset + count - 1];
51+
if (lastPoint.X == firstPoint.X && lastPoint.Y == firstPoint.Y)
52+
{
53+
lineToCount--;
54+
}
55+
}
56+
57+
if (lineToCount > 0)
58+
{
59+
commands.Add(EncodeCommand(Command.LineTo, (uint)lineToCount));
60+
61+
for (int i = 1; i <= lineToCount; i++)
62+
{
63+
var point = array[offset + i];
64+
dx = point.X - lastX;
65+
dy = point.Y - lastY;
66+
commands.Add((uint)ZigZag.Encode(dx));
67+
commands.Add((uint)ZigZag.Encode(dy));
68+
69+
lastX = point.X;
70+
lastY = point.Y;
71+
}
72+
}
73+
74+
// ClosePath command for polygons only
75+
if (geomType == Tile.GeomType.Polygon)
76+
{
77+
commands.Add(EncodeCommand(Command.ClosePath, 1));
78+
}
79+
}
80+
81+
return commands;
82+
}
83+
84+
private static uint EncodeCommand(Command command, uint count)
85+
{
86+
return (count << 3) | (uint)command;
87+
}
88+
}

src/VectorTileEncoder.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using ProtoBuf;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
5+
namespace Mapbox.Vector.Tile;
6+
7+
public static class VectorTileEncoder
8+
{
9+
public static Stream Encode(List<VectorTileLayer> layers, Stream stream)
10+
{
11+
var tile = new Tile();
12+
13+
foreach (var vectorTileLayer in layers)
14+
{
15+
var layer = new Tile.Layer
16+
{
17+
Name = vectorTileLayer.Name,
18+
Version = vectorTileLayer.Version,
19+
Extent = vectorTileLayer.Extent
20+
};
21+
22+
var keys = new List<string>();
23+
var values = new List<Tile.Value>();
24+
25+
foreach (var vectorTileFeature in vectorTileLayer.VectorTileFeatures)
26+
{
27+
var feature = new Tile.Feature
28+
{
29+
Id = ulong.Parse(vectorTileFeature.Id),
30+
Type = vectorTileFeature.GeometryType
31+
};
32+
33+
// Encode attributes
34+
var tags = AttributesEncoder.Encode(vectorTileFeature.Attributes, keys, values);
35+
feature.Tags.AddRange(tags);
36+
37+
// Encode geometry
38+
var geometry = GeometryEncoder.EncodeGeometry(vectorTileFeature.Geometry, vectorTileFeature.GeometryType);
39+
feature.Geometry.AddRange(geometry);
40+
41+
layer.Features.Add(feature);
42+
}
43+
44+
layer.Keys.AddRange(keys);
45+
layer.Values.AddRange(values);
46+
47+
tile.Layers.Add(layer);
48+
}
49+
50+
Serializer.Serialize(stream, tile);
51+
return stream;
52+
}
53+
}

tests/RealTileRoundTripTest.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using Mapbox.Vector.Tile;
2+
using NUnit.Framework;
3+
using System.IO;
4+
5+
namespace mapbox.vector.tile.tests;
6+
7+
public class RealTileRoundTripTest
8+
{
9+
[Test]
10+
public void TestRealTileRoundTrip()
11+
{
12+
// Test that we can decode a real tile and encode it back
13+
var pbfFile = Path.Combine("testdata", "14-8801-5371.vector.pbf");
14+
15+
// Decode the original tile
16+
var originalStream = File.OpenRead(pbfFile);
17+
var decodedLayers = VectorTileParser.Parse(originalStream);
18+
originalStream.Close();
19+
20+
Assert.That(decodedLayers.Count, Is.GreaterThan(0), "Should have decoded at least one layer");
21+
22+
// Encode it back
23+
var encodedStream = new MemoryStream();
24+
VectorTileEncoder.Encode(decodedLayers, encodedStream);
25+
encodedStream.Seek(0, SeekOrigin.Begin);
26+
27+
Assert.That(encodedStream.Length, Is.GreaterThan(0), "Encoded stream should not be empty");
28+
29+
// Decode the encoded tile
30+
var reDecodedLayers = VectorTileParser.Parse(encodedStream);
31+
32+
// Verify layer count matches
33+
Assert.That(reDecodedLayers.Count, Is.EqualTo(decodedLayers.Count), "Layer count should match");
34+
35+
// Verify each layer
36+
for (int i = 0; i < decodedLayers.Count; i++)
37+
{
38+
var original = decodedLayers[i];
39+
var reDecoded = reDecodedLayers[i];
40+
41+
Assert.That(reDecoded.Name, Is.EqualTo(original.Name), $"Layer {i} name should match");
42+
Assert.That(reDecoded.VectorTileFeatures.Count, Is.EqualTo(original.VectorTileFeatures.Count),
43+
$"Layer '{original.Name}' feature count should match");
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)