-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathMapboxTileReader.cs
More file actions
352 lines (292 loc) · 13.7 KB
/
MapboxTileReader.cs
File metadata and controls
352 lines (292 loc) · 13.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using NetTopologySuite.Features;
using NetTopologySuite.Geometries;
namespace NetTopologySuite.IO.VectorTiles.Mapbox
{
public class MapboxTileReader
{
private readonly GeometryFactory _factory;
public MapboxTileReader()
: this(new GeometryFactory(new PrecisionModel(), 4326))
{
}
public MapboxTileReader(GeometryFactory factory)
{
_factory = factory;
}
/// <summary>
/// Reads a Vector Tile stream.
/// </summary>
/// <param name="stream">Vector tile stream.</param>
/// <param name="tileDefinition">Tile information.</param>
/// <returns></returns>
public VectorTile Read(Stream stream, Tiles.Tile tileDefinition)
{
return Read(stream, tileDefinition, null);
}
/// <summary>
/// Reads a Vector Tile stream.
/// </summary>
/// <param name="stream">Vector tile stream.</param>
/// <param name="tileDefinition">Tile information.</param>
/// <param name="idAttributeName">Optional. Specifies the name of the attribute that the vector tile feature's ID should be stored in the NetTopologySuite Features AttributeTable.</param>
/// <returns></returns>
public VectorTile Read(Stream stream, Tiles.Tile tileDefinition, string idAttributeName)
{
// Deserialize the tile
var tile = ProtoBuf.Serializer.Deserialize<Mapbox.Tile>(stream);
var vectorTile = new VectorTile { TileId = tileDefinition.Id };
foreach (var mbTileLayer in tile.Layers)
{
Debug.Assert(mbTileLayer.Version == 2U);
var tgs = new TileGeometryTransform(tileDefinition, mbTileLayer.Extent);
var layer = new Layer {Name = mbTileLayer.Name};
foreach (var mbTileFeature in mbTileLayer.Features)
{
var feature = ReadFeature(tgs, mbTileLayer, mbTileFeature, idAttributeName);
layer.Features.Add(feature);
}
vectorTile.Layers.Add(layer);
}
return vectorTile;
}
private IFeature ReadFeature(TileGeometryTransform tgs, Tile.Layer mbTileLayer, Tile.Feature mbTileFeature, string idAttributeName)
{
var geometry = ReadGeometry(tgs, mbTileFeature.Type, mbTileFeature.Geometry);
var attributes = ReadAttributeTable(mbTileFeature, mbTileLayer.Keys, mbTileLayer.Values);
//Check to see if an id value is already captured in the attributes, if not, add it.
if (!string.IsNullOrEmpty(idAttributeName) && !mbTileLayer.Keys.Contains(idAttributeName))
{
ulong id = mbTileFeature.Id;
attributes.Add(idAttributeName, id);
}
return new Feature(geometry, attributes);
}
private Geometry ReadGeometry(TileGeometryTransform tgs, Tile.GeomType type, IList<uint> geometry)
{
switch (type)
{
case Tile.GeomType.Point:
return ReadPoint(tgs, geometry);
case Tile.GeomType.LineString:
return ReadLineString(tgs, geometry);
case Tile.GeomType.Polygon:
return ReadPolygon(tgs, geometry);
}
return null;
}
private Geometry ReadPoint(TileGeometryTransform tgs, IList<uint> geometry)
{
int currentIndex = 0; int currentX = 0; int currentY = 0;
var sequences = ReadCoordinateSequences(tgs, geometry, ref currentIndex, ref currentX, ref currentY, forPoint:true);
return CreatePuntal(sequences);
}
private Geometry ReadLineString(TileGeometryTransform tgs, IList<uint> geometry)
{
int currentIndex = 0; int currentX = 0; int currentY = 0;
var sequences = ReadCoordinateSequences(tgs, geometry, ref currentIndex, ref currentX, ref currentY);
return CreateLineal(sequences);
}
private Geometry ReadPolygon(TileGeometryTransform tgs, IList<uint> geometry)
{
int currentIndex = 0; int currentX = 0; int currentY = 0;
var sequences = ReadCoordinateSequences(tgs, geometry, ref currentIndex, ref currentX, ref currentY, 1);
return CreatePolygonal(sequences);
}
private Geometry CreatePuntal(CoordinateSequence[] sequences)
{
if (sequences == null || sequences.Length == 0)
return null;
var points = new Point[sequences.Length];
for (int i = 0; i < sequences.Length; i++)
points[i] = _factory.CreatePoint(sequences[i]);
if (points.Length == 1)
return points[0];
return _factory.CreateMultiPoint(points);
}
private Geometry CreateLineal(CoordinateSequence[] sequences)
{
if (sequences == null || sequences.Length == 0)
return null;
var lineStrings = new LineString[sequences.Length];
for (int i = 0; i < sequences.Length; i++)
lineStrings[i] = _factory.CreateLineString(sequences[i]);
if (lineStrings.Length == 1)
return lineStrings[0];
return _factory.CreateMultiLineString(lineStrings);
}
private Geometry CreatePolygonal(CoordinateSequence[] sequences)
{
var polygons = new List<Polygon>();
LinearRing shell = null;
var holes = new List<LinearRing>();
bool reverse = false;
for (int i = 0; i < sequences.Length; i++)
{
var ring = _factory.CreateLinearRing(sequences[i]);
// Shell rings should be CW (https://docs.mapbox.com/vector-tiles/specification/#winding-order)
if (!ring.IsCCW && !reverse)
{
if (shell != null)
{
polygons.Add(_factory.CreatePolygon(shell, holes.ToArray()));
holes.Clear();
}
shell = ring;
}
// Hole rings should be CCW https://docs.mapbox.com/vector-tiles/specification/#winding-order
else
{
if (shell == null)
{
if (sequences.Length >= 1)
{
// WARNING: this is not according to the spec but tiles exists like this in the wild
// that are rendered just fine by other tools, we can ignore them if we want to but
// should not throw an exception. The solution preferred here is to just read them
// but reverse them so the user gets what they expect according to the spec.
shell = ring.Reverse() as LinearRing;
reverse = true;
}
else
{
throw new InvalidOperationException("No shell defined.");
}
}
else
{
holes.Add(reverse ? ring.Reverse() as LinearRing : ring);
}
}
}
polygons.Add(_factory.CreatePolygon(shell, holes.ToArray()));
if (polygons.Count == 1)
return polygons[0];
return _factory.CreateMultiPolygon(polygons.ToArray());
}
private CoordinateSequence[] ReadCoordinateSequences(
TileGeometryTransform tgs, IList<uint> geometry,
ref int currentIndex, ref int currentX, ref int currentY, int buffer = 0, bool forPoint = false)
{
(var command, int count) = ParseCommandInteger(geometry[currentIndex]);
Debug.Assert(command == MapboxCommandType.MoveTo);
if (count > 1)
{
currentIndex++;
return ReadSinglePointSequences(tgs, geometry, count, ref currentIndex, ref currentX, ref currentY);
}
var sequences = new List<CoordinateSequence>();
var currentPosition = (currentX, currentY);
while (currentIndex < geometry.Count)
{
(command, count) = ParseCommandInteger(geometry[currentIndex++]);
Debug.Assert(command == MapboxCommandType.MoveTo);
Debug.Assert(count == 1);
// Read the current position
currentPosition = ParseOffset(currentPosition, geometry, ref currentIndex);
if (!forPoint)
{
// Read the next command (should be LineTo)
(command, count) = ParseCommandInteger(geometry[currentIndex++]);
if (command != MapboxCommandType.LineTo) count = 0;
}
else
{
count = 0;
}
// Create sequence, add starting point
var sequence = _factory.CoordinateSequenceFactory.Create(1 + count + buffer, 2);
int sequenceIndex = 0;
TransformOffsetAndAddToSequence(tgs, currentPosition, sequence, sequenceIndex++);
// Read and add offsets
for (int i = 1; i <= count; i++)
{
currentPosition = ParseOffset(currentPosition, geometry, ref currentIndex);
TransformOffsetAndAddToSequence(tgs, currentPosition, sequence, sequenceIndex++);
}
// Check for ClosePath command
if (currentIndex < geometry.Count)
{
(command, _) = ParseCommandInteger(geometry[currentIndex]);
if (command == MapboxCommandType.ClosePath)
{
Debug.Assert(buffer > 0);
sequence.SetOrdinate(sequenceIndex, Ordinate.X, sequence.GetOrdinate(0, Ordinate.X));
sequence.SetOrdinate(sequenceIndex, Ordinate.Y, sequence.GetOrdinate(0, Ordinate.Y));
currentIndex++;
sequenceIndex++;
}
}
Debug.Assert(sequenceIndex == sequence.Count);
sequences.Add(sequence);
}
// update current position values
currentX = currentPosition.currentX;
currentY = currentPosition.currentY;
return sequences.ToArray();
}
private CoordinateSequence[] ReadSinglePointSequences(TileGeometryTransform tgs, IList<uint> geometry,
int numSequences, ref int currentIndex, ref int currentX, ref int currentY)
{
var res = new CoordinateSequence[numSequences];
var currentPosition = (currentX, currentY);
for (int i = 0; i < numSequences; i++)
{
res[i] = _factory.CoordinateSequenceFactory.Create(1, 2);
currentPosition = ParseOffset(currentPosition, geometry, ref currentIndex);
TransformOffsetAndAddToSequence(tgs, currentPosition, res[i], 0);
}
currentX = currentPosition.currentX;
currentY = currentPosition.currentY;
return res;
}
private void TransformOffsetAndAddToSequence(TileGeometryTransform tgs, (int x, int y) localPosition, CoordinateSequence sequence, int index)
{
var (longitude, latitude) = tgs.TransformInverse(localPosition.x, localPosition.y);
sequence.SetOrdinate(index, Ordinate.X, longitude);
sequence.SetOrdinate(index, Ordinate.Y, latitude);
}
private (int, int) ParseOffset((int x, int y) currentPosition, IList<uint> parameterIntegers, ref int offset)
{
return (currentPosition.x + Decode(parameterIntegers[offset++]),
currentPosition.y + Decode(parameterIntegers[offset++]));
}
private static int Decode(uint parameterInteger)
{
return ((int) (parameterInteger >> 1) ^ ((int)-(parameterInteger & 1)));
}
private static (MapboxCommandType, int) ParseCommandInteger(uint commandInteger)
{
return unchecked(((MapboxCommandType) (commandInteger & 0x07U), (int)(commandInteger >> 3)));
}
private static IAttributesTable ReadAttributeTable(Tile.Feature mbTileFeature, List<string> keys, List<Tile.Value> values)
{
var att = new AttributesTable();
for (int i = 0; i < mbTileFeature.Tags.Count; i += 2)
{
string key = keys[(int)mbTileFeature.Tags[i]];
var value = values[(int)mbTileFeature.Tags[i + 1]];
if (value.HasBoolValue)
att.Add(key, value.BoolValue);
else if (value.HasDoubleValue)
att.Add(key, value.DoubleValue);
else if (value.HasFloatValue)
att.Add(key, value.FloatValue);
else if (value.HasIntValue)
att.Add(key, value.IntValue);
else if (value.HasSIntValue)
att.Add(key, value.SintValue);
else if (value.HasStringValue)
att.Add(key, value.StringValue);
else if (value.HasUIntValue)
att.Add(key, value.UintValue);
else
att.Add(key, null);
}
return att;
}
}
}