Skip to content

Commit a42288e

Browse files
authored
Merge pull request #7 from NetTopologySuite/develop
Release to github packages.
2 parents ef05817 + 32f07a0 commit a42288e

File tree

4 files changed

+206
-28
lines changed

4 files changed

+206
-28
lines changed

NetTopologySuite.IO.VectorTiles.Common.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
<LangVersion>latest</LangVersion>
77
<Authors>NetTopologySuite - Team</Authors>
88
<Owners>NetTopologySuite - Team</Owners>
9-
<PackageVersion>0.0.15</PackageVersion>
9+
<PackageVersion>0.0.16</PackageVersion>
1010
<PackageLicenseUrl>https://raw.githubusercontent.com/NetTopologySuite/NetTopologySuite.IO.VectorTiles/main/LICENSE.md</PackageLicenseUrl>
1111
<RepositoryUrl>https://github.com/NetTopologySuite/NetTopologySuite.IO.VectorTiles</RepositoryUrl>
1212
<PackageIconUrl>https://github.com/NetTopologySuite/NetTopologySuite/raw/develop/icon.png</PackageIconUrl>
1313
</PropertyGroup>
14-
</Project>
14+
</Project>

README.md

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,160 @@
22

33
[![release](https://github.com/NetTopologySuite/NetTopologySuite.IO.VectorTiles/actions/workflows/release.yml/badge.svg)](https://github.com/NetTopologySuite/NetTopologySuite.IO.VectorTiles/actions/workflows/release.yml)
44

5-
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/OsmSharp/core/blob/develop/LICENSE.md)
5+
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/NetTopologySuite/NetTopologySuite.IO.VectorTiles/blob/develop/LICENSE.md)
66

7-
A package that can be used to generate vector tiles using NTS.
7+
A package that can be used to read or generate vector tiles using NTS.
88

9+
## Getting started
10+
11+
This package is still experimental and does not yet have a NuGet package. To load this package into your app, download the source code, copy all three projects into your solution folder. Then within Visual Studio use the *"Add Existing Project"* option to add these to your solution. From there, you can use the *"Add Project References"* function in Visual Studio to bring this functionality into your app.
12+
13+
### Create a vector tile
14+
15+
The following shows how to create an individual vector tile, pass in a Feature from NTS, and write the tile as a file.
16+
17+
```csharp
18+
//Define which tile you want to create.
19+
var tileDefinition = new NetTopologySuite.IO.VectorTiles.Tiles.Tile(x, y, zoom);
20+
21+
//Create a vector tile instance and pass om the tile ID from the tile definition above.
22+
var vt = new VectorTile { TileId = tileDefinition.id };
23+
24+
//Create one or more layers. Ideally one layer per dataset.
25+
var lyr = new Layer { Name = "layer1" };
26+
27+
//Add your NTS feature(s) to the layer. Loop through all your features and add them to the tile.
28+
lyr.Features.Add(myFeature);
29+
30+
//Add the layer to the vector tile.
31+
vt.Layers.Add(lyr);
32+
33+
//Output the tile to a stream.
34+
using (var fs = new FileStream(filePath, FileMode.Create))
35+
{
36+
//Write the tile to the stream.
37+
vt.Write(fs);
38+
}
39+
```
40+
41+
### Read a vector tile
42+
43+
The following shows how to read an individual vector tile and access the underlying NTS Features.
44+
45+
```csharp
46+
//Create a MapboxTileReader.
47+
var reader = new MapboxTileReader();
48+
49+
//Define which tile you want to read. You may be able to extract the x/y/zoom info from the file path of the tile.
50+
var tileDefinition = new NetTopologySuite.IO.VectorTiles.Tiles.Tile(x, y, zoom);
51+
52+
//Open a vector tile file as a stream.
53+
using (var fs = new FileStream(filePath, FileMode.Open))
54+
{
55+
//Read the vector tile.
56+
var vt = reader.Read(fs, tileDefinition);
57+
58+
//Loop through each layer.
59+
foreach(var l in vt.Layers)
60+
{
61+
//Access the features of the layer and do something with them.
62+
var features = l.Features;
63+
}
64+
}
65+
```
66+
67+
### Create tile ranges
68+
69+
More often than not you want to create a collection of tiles over a bounding box area and across multiple zoom levels. The `TileRange` class makes it easy to get the ID's of all tiles within an area for a specific zoom level. You can create a tile range for each zoom level.
70+
71+
```csharp
72+
//Create a tile range for a bounding box and specific zoom level.
73+
var tr = new NetTopologySuite.IO.VectorTiles.Tiles.TileRange(west, south, east, north, zoom);
74+
75+
//You can then enumerate over each tile within the range.
76+
77+
//Enumerate using a loop. Parallel for loops can be used too, just be sure where ever you are reading your source data from can handle multiple parallel requests.
78+
//Smaller datasets can be stored in memory, but for larger datasets that are stored in a database, you will want to throttle the number of parallel threads.
79+
foreach(var tile in tr)
80+
{
81+
//Read or write tiles.
82+
}
83+
84+
//Alternatively make use the built in `EnumerateInCenterFirst` if you want to access tiles in a spiral pattern move out from the center.
85+
//This is useful if you are creating a dynamic rendering of the tiles.
86+
var centerFirstRange = tr.EnumerateInCenterFirst();
87+
foreach(var tile in centerFirstRange)
88+
{
89+
//Read or write tiles.
90+
}
91+
```
92+
93+
### Feature IDs
94+
95+
GeoJSON features and features in vector tiles can have an ID property. This is for many things within an app, such as;
96+
97+
- Using the ID of an feature to query a database and retrieve addition details that shouldn't be stored in the tiles themselves. Generally you only want the data needed for visualization in the tiles.
98+
- Using the ID in combination with the feature state capability in some SDKs to join external data to tiles, or create hover over effects.
99+
100+
NTS features however don't have an ID property. To address this, you can pass an ID value into the attributes table of a feature. The `MapboxTileWriter` class in this library by default will automatically look at the attribute table for an attribute called `id` that is an `integer` or `ulong` number. Alternatively, when writing a tile you can specify an alternative attribute name to retrieve an ID value from. The following shows how to pass an ID from an NTS feature into it's equivalent vector tile feature when writing a vector tile.
101+
102+
```csharp
103+
//Create a feature.
104+
var myFeature = new Feature(new Point(0, 0), new AttributesTable(new Dictionary<string, object>()
105+
{
106+
{ "id", 12345 },
107+
{ "alternateId", 10101 },
108+
{ "someOtherValue", "Hello World" }
109+
}));
110+
111+
//Define which tile you want to create.
112+
var tileDefinition = new NetTopologySuite.IO.VectorTiles.Tiles.Tile(x, y, zoom);
113+
114+
//Create a vector tile.
115+
var vt = new VectorTile { TileId = tileDefinition.id };
116+
117+
//Create a layer.
118+
var lyr = new Layer { Name = "layer1" };
119+
120+
//Add your feature to the layer. Loop through all your features and add them to the tile.
121+
lyr.Features.Add(myFeature);
122+
123+
//Add the layer to the vector tile.
124+
vt.Layers.Add(lyr);
125+
126+
//Output the tile to a stream.
127+
using (var fs = new FileStream(filePath, FileMode.Create))
128+
{
129+
//Write the tile to the stream. This will automatically look for an "id" attribute that is a ulong or integer value as set the tile's feature ID to it.
130+
vt.Write(fs);
131+
132+
//Alternatively, pass in a different attribute name to have the vector tiles feature use that ID value.
133+
//vt.Write(fs, 4096, "alternateId");
134+
}
135+
```
136+
137+
Similarly, the `MapboxTileReader` class will allows you to specify an attribute name to capture the ID value in the NTS feature. If you don't specify an attribute name, the ID value won't be captured in the attribute table. This is done for backwards compatibility. The following shows how to capture a vector tile features ID and store it using a custom attribute in the attribute table.
138+
139+
```csharp
140+
//Create a MapboxTileReader.
141+
var reader = new MapboxTileReader();
142+
143+
//Define which tile you want to read. You may be able to extract the x/y/zoom info from the file path of the tile.
144+
var tileDefinition = new NetTopologySuite.IO.VectorTiles.Tiles.Tile(x, y, zoom);
145+
146+
//Open a vector tile file as a stream.
147+
using (var fs = new FileStream(filePath, FileMode.Open))
148+
{
149+
//Read the vector tile.
150+
//By default ID values won't be extracted from the vector tile feature, by passing in the name of an attribute to store the ID in, the ID will be added to the attribute table.
151+
//You can give this any name.
152+
var vt = reader.Read(fs, tileDefinition, "id");
153+
154+
//Loop through each layer.
155+
foreach(var l in vt.Layers)
156+
{
157+
//Access the features of the layer and do something with them.
158+
var features = l.Features;
159+
}
160+
}
161+
```

src/NetTopologySuite.IO.VectorTiles.Mapbox/MapboxTileWriter.cs

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.IO;
34
using NetTopologySuite.Features;
45
using NetTopologySuite.Geometries;
6+
using NetTopologySuite.IO.VectorTiles.Tiles.WebMercator;
57

68
namespace NetTopologySuite.IO.VectorTiles.Mapbox
79
{
@@ -24,10 +26,10 @@ IEnumerable<VectorTile> GetTiles()
2426
yield return tree[tile];
2527
}
2628
}
27-
29+
2830
GetTiles().Write(path, extent);
2931
}
30-
32+
3133
/// <summary>
3234
/// Writes the tiles in a /z/x/y.mvt folder structure.
3335
/// </summary>
@@ -50,7 +52,7 @@ public static void Write(this IEnumerable<VectorTile> vectorTiles, string path,
5052
vectorTile.Write(stream, extent);
5153
}
5254
}
53-
55+
5456
/// <summary>
5557
/// Writes the tile to the given stream.
5658
/// </summary>
@@ -66,7 +68,7 @@ public static void Write(this VectorTile vectorTile, Stream stream, uint extent
6668
var mapboxTile = new Mapbox.Tile();
6769
foreach (var localLayer in vectorTile.Layers)
6870
{
69-
var layer = new Mapbox.Tile.Layer {Version = 2, Name = localLayer.Name, Extent = extent};
71+
var layer = new Mapbox.Tile.Layer { Version = 2, Name = localLayer.Name, Extent = extent };
7072

7173
var keys = new Dictionary<string, uint>();
7274
var values = new Dictionary<Tile.Value, uint>();
@@ -88,7 +90,7 @@ public static void Write(this VectorTile vectorTile, Stream stream, uint extent
8890
break;
8991
case IPolygonal polygonal:
9092
feature.Type = Tile.GeomType.Polygon;
91-
feature.Geometry.AddRange(Encode(polygonal, tgt));
93+
feature.Geometry.AddRange(Encode(polygonal, tgt, tile.Zoom));
9294
break;
9395
default:
9496
feature.Type = Tile.GeomType.Unknown;
@@ -188,13 +190,13 @@ private static IEnumerable<uint> Encode(IPuntal puntal, TileGeometryTransform tg
188190
{
189191
const int CoordinateIndex = 0;
190192

191-
var geometry = (Geometry) puntal;
193+
var geometry = (Geometry)puntal;
192194
int currentX = 0, currentY = 0;
193195

194196
var parameters = new List<uint>();
195197
for (int i = 0; i < geometry.NumGeometries; i++)
196198
{
197-
var point = (Point) geometry.GetGeometryN(i);
199+
var point = (Point)geometry.GetGeometryN(i);
198200
var position = tgt.Transform(point.CoordinateSequence, CoordinateIndex, ref currentX, ref currentY);
199201
if (i == 0 || position.x > 0 || position.y > 0)
200202
{
@@ -222,23 +224,29 @@ private static IEnumerable<uint> Encode(ILineal lineal, TileGeometryTransform tg
222224
}
223225
}
224226

225-
private static IEnumerable<uint> Encode(IPolygonal polygonal, TileGeometryTransform tgt)
227+
private static IEnumerable<uint> Encode(IPolygonal polygonal, TileGeometryTransform tgt, int zoom)
226228
{
227229
var geometry = (Geometry)polygonal;
228-
int currentX = 0, currentY = 0;
229-
for (int i = 0; i < geometry.NumGeometries; i++)
230-
{
231-
var polygon = (Polygon)geometry.GetGeometryN(i);
232-
if (polygon.Area == 0d)
233-
continue;
234230

235-
//Shell rings should be CW, holes CCW as per spec: https://docs.mapbox.com/vector-tiles/specification/
236-
foreach (uint encoded in Encode(polygon.Shell.CoordinateSequence, tgt, ref currentX, ref currentY, true, false))
237-
yield return encoded;
238-
foreach (var hole in polygon.InteriorRings)
231+
//Test the whole polygon geometry is larger than a single pixel.
232+
if (IsGreaterThanOnePixelOfTile(geometry, zoom))
233+
{
234+
int currentX = 0, currentY = 0;
235+
for (int i = 0; i < geometry.NumGeometries; i++)
239236
{
240-
foreach (uint encoded in Encode(hole.CoordinateSequence, tgt, ref currentX, ref currentY, true, true))
237+
var polygon = (Polygon)geometry.GetGeometryN(i);
238+
239+
//Test that individual polygons are larger than a single pixel.
240+
if (!IsGreaterThanOnePixelOfTile(polygon, zoom))
241+
continue;
242+
243+
foreach (uint encoded in Encode(polygon.Shell.CoordinateSequence, tgt, ref currentX, ref currentY, true, false))
241244
yield return encoded;
245+
foreach (var hole in polygon.InteriorRings)
246+
{
247+
foreach (uint encoded in Encode(hole.CoordinateSequence, tgt, ref currentX, ref currentY, true, true))
248+
yield return encoded;
249+
}
242250
}
243251
}
244252
}
@@ -258,8 +266,6 @@ private static IEnumerable<uint> Encode(CoordinateSequence sequence, TileGeometr
258266
sequence = sequence.Copy();
259267
CoordinateSequences.Reverse(sequence);
260268
}
261-
262-
count--;
263269
}
264270
var encoded = new List<uint>();
265271

@@ -275,6 +281,7 @@ private static IEnumerable<uint> Encode(CoordinateSequence sequence, TileGeometr
275281
for (int i = 1; i < count; i++)
276282
{
277283
position = tgt.Transform(sequence, i, ref currentX, ref currentY);
284+
278285
if (position.x != 0 || position.y != 0)
279286
{
280287
encoded.Add(GenerateParameterInteger(position.x));
@@ -331,7 +338,7 @@ private static void GenerateClosePath(List<uint> geometry)
331338
/// </summary>
332339
private static uint GenerateCommandInteger(MapboxCommandType command, int count)
333340
{ // CommandInteger = (id & 0x7) | (count << 3)
334-
return (uint) (((int)command & 0x7) | (count << 3));
341+
return (uint)(((int)command & 0x7) | (count << 3));
335342
}
336343

337344
/// <summary>
@@ -341,7 +348,25 @@ private static uint GenerateCommandInteger(MapboxCommandType command, int count)
341348
/// <returns></returns>
342349
private static uint GenerateParameterInteger(int value)
343350
{ // ParameterInteger = (value << 1) ^ (value >> 31)
344-
return (uint) ((value<<1) ^ (value>> 31));
351+
return (uint)((value << 1) ^ (value >> 31));
352+
}
353+
354+
/// <summary>
355+
/// Checks to see if a geometries envelope is greater than 1 square pixel in size for a specified zoom leve.
356+
/// </summary>
357+
/// <param name="polygon">Polygon to test.</param>
358+
/// <param name="zoom">Zoom level </param>
359+
/// <returns></returns>
360+
private static bool IsGreaterThanOnePixelOfTile(Geometry polygon, int zoom)
361+
{
362+
var bottomLeft = WebMercatorHandler.MetersToPixels(WebMercatorHandler.LatLonToMeters(polygon.EnvelopeInternal.MinY, polygon.EnvelopeInternal.MinX), zoom, 512);
363+
var topRight = WebMercatorHandler.MetersToPixels(WebMercatorHandler.LatLonToMeters(polygon.EnvelopeInternal.MaxY, polygon.EnvelopeInternal.MaxX), zoom, 512);
364+
365+
var dx = Math.Abs(topRight.x - bottomLeft.x);
366+
var dy = Math.Abs(topRight.y - bottomLeft.y);
367+
368+
//Both must be greater than 0, and atleast one of them needs to be larger than 1.
369+
return dx > 0 && dy > 0 && (dx > 1 || dy > 1);
345370
}
346371
}
347372
}

src/NetTopologySuite.IO.VectorTiles.Mapbox/Tile.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ public System.Collections.Generic.List<Value> Values
198198
}
199199

200200
uint _extent = 4096;
201-
[ProtoBuf.ProtoMember(5, IsRequired = false, Name = @"extent", DataFormat = ProtoBuf.DataFormat.TwosComplement)]
201+
[ProtoBuf.ProtoMember(5, IsRequired = true, Name = @"extent", DataFormat = ProtoBuf.DataFormat.TwosComplement)]
202202
[System.ComponentModel.DefaultValue((uint)4096)]
203203
public uint Extent
204204
{

0 commit comments

Comments
 (0)