Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release v1.0.2 #26

Merged
merged 12 commits into from
Nov 14, 2023
2 changes: 1 addition & 1 deletion NetTopologySuite.IO.VectorTiles.Common.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<LangVersion>latest</LangVersion>
<Authors>NetTopologySuite - Team</Authors>
<Owners>NetTopologySuite - Team</Owners>
<PackageVersion>1.0.0</PackageVersion>
<PackageVersion>1.0.3</PackageVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/NetTopologySuite/NetTopologySuite.IO.VectorTiles</RepositoryUrl>
<PackageIcon>icon.png</PackageIcon>
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A package that can be used to read or generate vector tiles using NTS.

## Getting started

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.
This package is somewhat experimental. A NuGet-package is hosted on [Github Packages](https://github.com/orgs/NetTopologySuite/packages?repo_name=NetTopologySuite.IO.VectorTiles)

### Create a vector tile

Expand Down
88 changes: 49 additions & 39 deletions src/NetTopologySuite.IO.VectorTiles.Mapbox/MapboxTileWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.IO;
using NetTopologySuite.Features;
using NetTopologySuite.Geometries;
using NetTopologySuite.IO.VectorTiles.Tiles.WebMercator;

namespace NetTopologySuite.IO.VectorTiles.Mapbox
{
Expand All @@ -21,7 +20,7 @@ public static void Write(this VectorTileTree tree, string path, uint extent = 40
{
IEnumerable<VectorTile> GetTiles()
{
foreach (var tile in tree)
foreach (ulong tile in tree)
{
yield return tree[tile];
}
Expand All @@ -42,11 +41,17 @@ public static void Write(this IEnumerable<VectorTile> vectorTiles, string path,
foreach (var vectorTile in vectorTiles)
{
var tile = new Tiles.Tile(vectorTile.TileId);
var zFolder = Path.Combine(path, tile.Zoom.ToString());
if (!Directory.Exists(zFolder)) Directory.CreateDirectory(zFolder);
var xFolder = Path.Combine(zFolder, tile.X.ToString());
if (!Directory.Exists(xFolder)) Directory.CreateDirectory(xFolder);
var file = Path.Combine(xFolder, $"{tile.Y}.mvt");
string zFolder = Path.Combine(path, tile.Zoom.ToString());

if (!Directory.Exists(zFolder))
Directory.CreateDirectory(zFolder);

string xFolder = Path.Combine(zFolder, tile.X.ToString());

if (!Directory.Exists(xFolder))
Directory.CreateDirectory(xFolder);

string file = Path.Combine(xFolder, $"{tile.Y}.mvt");

using var stream = File.Open(file, FileMode.Create);
vectorTile.Write(stream, extent);
Expand Down Expand Up @@ -77,6 +82,10 @@ public static void Write(this VectorTile vectorTile, Stream stream, uint extent
{
var feature = new Mapbox.Tile.Feature();

// Features with empty geometries cannot be encoded
if (localLayerFeature.Geometry.IsEmpty)
continue;

// Encode geometry
switch (localLayerFeature.Geometry)
{
Expand Down Expand Up @@ -105,7 +114,7 @@ public static void Write(this VectorTile vectorTile, Stream stream, uint extent
AddAttributes(feature.Tags, keys, values, localLayerFeature.Attributes);

//Try and retrieve an ID from the attributes.
var id = localLayerFeature.Attributes.GetOptionalValue(idAttributeName);
object id = localLayerFeature.Attributes.GetOptionalValue(idAttributeName);

//Converting ID to string, then trying to parse. This will handle situations will ignore situations where the ID value is not actually an integer or ulong number.
if (id != null && ulong.TryParse(id.ToString(), out ulong idVal))
Expand All @@ -132,12 +141,12 @@ private static void AddAttributes(List<uint> tags, Dictionary<string, uint> keys
if (attributes == null || attributes.Count == 0)
return;

var aKeys = attributes.GetNames();
var aValues = attributes.GetValues();
string[] aKeys = attributes.GetNames();
object[] aValues = attributes.GetValues();

for (var a = 0; a < aKeys.Length; a++)
for (int a = 0; a < aKeys.Length; a++)
{
var key = aKeys[a];
string key = aKeys[a];
if (string.IsNullOrEmpty(key)) continue;

var tileValue = ToTileValue(aValues[a]);
Expand Down Expand Up @@ -180,6 +189,8 @@ private static Tile.Value ToTileValue(object value)

case string stringValue:
return new Tile.Value { StringValue = stringValue };
default:
break;
}

return null;
Expand All @@ -196,19 +207,30 @@ private static IEnumerable<uint> Encode(IPuntal puntal, TileGeometryTransform tg
for (int i = 0; i < geometry.NumGeometries; i++)
{
var point = (Point)geometry.GetGeometryN(i);
// if the point is empty, there is nothing we can do with it
if (point.IsEmpty) continue;

int previousX = currentX, previousY = currentY;
(int x, int y) = tgt.Transform(point.CoordinateSequence, CoordinateIndex, ref currentX, ref currentY);
if (i == 0 || x > 0 || y > 0)

if (i == 0 || tgt.IsPointInExtent(currentX, currentY))
{
parameters.Add(GenerateParameterInteger(x));
parameters.Add(GenerateParameterInteger(y));
}
else
{
// discard point if it lies outside tile extent and rollback to previous point
// only for the case of multipoint
currentX = previousX;
currentY = previousY;
}
}

// Return result
yield return GenerateCommandInteger(MapboxCommandType.MoveTo, parameters.Count / 2);
foreach (uint parameter in parameters)
yield return parameter;

}

private static IEnumerable<uint> Encode(ILineal lineal, TileGeometryTransform tgt)
Expand All @@ -228,15 +250,15 @@ private static IEnumerable<uint> Encode(IPolygonal polygonal, TileGeometryTransf
var geometry = (Geometry)polygonal;

//Test the whole polygon geometry is larger than a single pixel.
if (IsGreaterThanOnePixelOfTile(geometry, zoom))
if (tgt.IsGreaterThanOnePixelOfTile(geometry))
{
int currentX = 0, currentY = 0;
for (int i = 0; i < geometry.NumGeometries; i++)
{
var polygon = (Polygon)geometry.GetGeometryN(i);

//Test that individual polygons are larger than a single pixel.
if (!IsGreaterThanOnePixelOfTile(polygon, zoom))
if (!tgt.IsGreaterThanOnePixelOfTile(polygon))
continue;

foreach (uint encoded in Encode(polygon.Shell.CoordinateSequence, tgt, ref currentX, ref currentY, true, false))
Expand All @@ -255,7 +277,12 @@ private static IEnumerable<uint> Encode(CoordinateSequence sequence, TileGeometr
bool ring = false, bool ccw = false)
{
// how many parameters for LineTo command
int count = sequence.Count;
// skipping the last point for rings since ClosePath is used instead
int count = ring ? sequence.Count - 1 : sequence.Count;

// If the sequence is empty there is nothing we can do with it.
if (count == 0)
return Array.Empty<uint>();

// if we have a ring we need to check orientation
if (ring)
Expand All @@ -266,10 +293,11 @@ private static IEnumerable<uint> Encode(CoordinateSequence sequence, TileGeometr
CoordinateSequences.Reverse(sequence);
}
}
var encoded = new List<uint>();

// Start point
encoded.Add(GenerateCommandInteger(MapboxCommandType.MoveTo, 1));
var encoded = new List<uint>
{
// Start point
GenerateCommandInteger(MapboxCommandType.MoveTo, 1)
};
var position = tgt.Transform(sequence, 0, ref currentX, ref currentY);
encoded.Add(GenerateParameterInteger(position.x));
encoded.Add(GenerateParameterInteger(position.y));
Expand Down Expand Up @@ -349,23 +377,5 @@ private static uint GenerateParameterInteger(int value)
{ // ParameterInteger = (value << 1) ^ (value >> 31)
return (uint)((value << 1) ^ (value >> 31));
}

/// <summary>
/// Checks to see if a geometries envelope is greater than 1 square pixel in size for a specified zoom leve.
/// </summary>
/// <param name="polygon">Polygon to test.</param>
/// <param name="zoom">Zoom level </param>
/// <returns></returns>
private static bool IsGreaterThanOnePixelOfTile(Geometry polygon, int zoom)
{
(double x1, double y1) = WebMercatorHandler.MetersToPixels(WebMercatorHandler.LatLonToMeters(polygon.EnvelopeInternal.MinY, polygon.EnvelopeInternal.MinX), zoom, 512);
(double x2, double y2) = WebMercatorHandler.MetersToPixels(WebMercatorHandler.LatLonToMeters(polygon.EnvelopeInternal.MaxY, polygon.EnvelopeInternal.MaxX), zoom, 512);

var dx = Math.Abs(x2 - x1);
var dy = Math.Abs(y2 - y1);

//Both must be greater than 0, and atleast one of them needs to be larger than 1.
return dx > 0 && dy > 0 && (dx > 1 || dy > 1);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ private int ComputeHashCode()
res ^= _uintValue.GetHashCode();
else if (HasSIntValue)
res ^= _sintValue.GetHashCode();
else if (HasSIntValue)
else if (HasStringValue)
res ^= _stringValue?.GetHashCode() ?? 0;

return res;
Expand Down Expand Up @@ -87,6 +87,14 @@ public override string ToString()

return sb.ToString();
}

private bool ShouldSerializeBoolValue() => HasBoolValue;
private bool ShouldSerializeIntValue() => HasIntValue;
private bool ShouldSerializeSintValue() => HasSIntValue;
private bool ShouldSerializeUintValue() => HasUIntValue;
private bool ShouldSerializeFloatValue() => HasFloatValue;
private bool ShouldSerializeDoubleValue() => HasDoubleValue;
private bool ShouldSerializeStringValue() => HasStringValue;
}
}
}
2 changes: 0 additions & 2 deletions src/NetTopologySuite.IO.VectorTiles.Mapbox/Tile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,6 @@ public bool BoolValue
}
}

private bool ShouldSerializeBoolValue() { return HasBoolValue; }

ProtoBuf.IExtension _extensionObject;

ProtoBuf.IExtension ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
Expand Down
82 changes: 66 additions & 16 deletions src/NetTopologySuite.IO.VectorTiles.Mapbox/TileGeometryTransform.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

using System;
using System.Runtime.CompilerServices;
using NetTopologySuite.Geometries;
using NetTopologySuite.IO.VectorTiles.Tiles.WebMercator;
Expand All @@ -11,11 +11,11 @@ namespace NetTopologySuite.IO.VectorTiles.Mapbox
/// </summary>
internal struct TileGeometryTransform
{
private Tiles.Tile _tile;
private uint _extent;
private long _top;
private long _left;
private readonly Tiles.Tile _tile;
private readonly uint _extent;
private readonly long _top;
private readonly long _left;

/// <summary>
/// Initializes this transformation utility
/// </summary>
Expand All @@ -25,13 +25,19 @@ public TileGeometryTransform(Tiles.Tile tile, uint extent) : this()
{
_tile = tile;
_extent = extent;


// Precalculate the resolution of the tile for the specified zoom level.
ZoomResolution = WebMercatorHandler.Resolution(tile.Zoom, (int)extent);

var meters = WebMercatorHandler.LatLonToMeters(_tile.Top, _tile.Left);
var pixels = WebMercatorHandler.MetersToPixels(meters, tile.Zoom, (int) extent);
_top = (long)pixels.y;
_left = (long)pixels.x;
(_left, _top) = WebMercatorHandler.FromMetersToPixels(meters, ZoomResolution);
}

/// <summary>
/// The zoom level pixel resolution based on the extent.
/// </summary>
public double ZoomResolution { get; }

/// <summary>
/// Transforms the coordinate at <paramref name="index"/> of <paramref name="sequence"/> to the tile coordinate system.
/// The return value is the position relative to the local point at (<paramref name="currentX"/>, <paramref name="currentY"/>).
Expand All @@ -43,11 +49,18 @@ public TileGeometryTransform(Tiles.Tile tile, uint extent) : this()
/// <returns>The position relative to the local point at (<paramref name="currentX"/>, <paramref name="currentY"/>).</returns>
public (int x, int y) Transform(CoordinateSequence sequence, int index, ref int currentX, ref int currentY)
{
var lon = sequence.GetOrdinate(index, Ordinate.X);
var lat = sequence.GetOrdinate(index, Ordinate.Y);
// This should never happen.
if (sequence == null)
throw new ArgumentNullException(nameof(sequence));

if (sequence.Count == 0)
throw new ArgumentException("sequence is empty.", nameof(sequence));

double lon = sequence.GetOrdinate(index, Ordinate.X);
double lat = sequence.GetOrdinate(index, Ordinate.Y);

var meters = WebMercatorHandler.LatLonToMeters(lat, lon);
var pixels = WebMercatorHandler.MetersToPixels(meters, _tile.Zoom, (int) _extent);
var pixels = WebMercatorHandler.FromMetersToPixels(meters, ZoomResolution);

int localX = (int) (pixels.x - _left);
int localY = (int) (_top - pixels.y);
Expand All @@ -59,14 +72,51 @@ public TileGeometryTransform(Tiles.Tile tile, uint extent) : this()
return (dx, dy);
}

/// <summary>
/// Transforms the point in the local tile pixel coordinates into WGS84 coordinates.
/// The return value is longitude and latitude of the tile pixel point (<paramref name="x"/>, <paramref name="y"/>).
/// </summary>
/// <param name="x">The horizontal component of the point in the tile coordinate system</param>
/// <param name="y">The vertical component of the point in the tile coordinate system</param>
/// <returns>WGS84 coordinates of the point in tile "pixel" coordinates (<paramref name="x"/>, <paramref name="y"/>).</returns>
public (double longitude, double latitude) TransformInverse(int x, int y)
{
var globalX = _left + x;
var globalY = _top - y;
long globalX = _left + x;
long globalY = _top - y;

var meters = WebMercatorHandler.PixelsToMeters((globalX, globalY), _tile.Zoom, (int) _extent);
var meters = WebMercatorHandler.FromPixelsToMeters((globalX, globalY), ZoomResolution);
var coordinates = WebMercatorHandler.MetersToLatLon(meters);
return coordinates;
}

/// <summary>
/// Check if the point with tile coordinates (<paramref name="x"/>, <paramref name="y"/> lies inside tile extent
/// </summary>
/// <param name="x">Horizontal component of the point in the tile coordinate system</param>
/// <param name="y">Vertical component of the point in the tile coordinate system</param>
/// <returns>true if point lies inside tile extent</returns>
public bool IsPointInExtent(int x, int y)
{
return x >= 0 && y >= 0 && x < _extent && y < _extent;
}

/// <summary>
/// Checks to see if a geometries envelope is greater than 1 square pixel in size for a specified zoom level.
/// </summary>
/// <param name="polygon">Polygon to test.</param>
/// <returns>true if the <paramref name="polygon"/> is greater than 1 pixel in the tile pixel coordinates</returns>
public bool IsGreaterThanOnePixelOfTile(Geometry polygon)
{
if (polygon.IsEmpty) return false;

(double x1, double y1) = WebMercatorHandler.FromMetersToPixels(WebMercatorHandler.LatLonToMeters(polygon.EnvelopeInternal.MinY, polygon.EnvelopeInternal.MinX), ZoomResolution);
(double x2, double y2) = WebMercatorHandler.FromMetersToPixels(WebMercatorHandler.LatLonToMeters(polygon.EnvelopeInternal.MaxY, polygon.EnvelopeInternal.MaxX), ZoomResolution);

double dx = Math.Abs(x2 - x1);
double dy = Math.Abs(y2 - y1);

// Both must be greater than 0, and at least one of them needs to be larger than 1.
return dx > 0 && dy > 0 && (dx > 1 || dy > 1);
}
}
}
6 changes: 3 additions & 3 deletions src/NetTopologySuite.IO.VectorTiles/Tilers/LineStringTiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ public static IEnumerable<ulong> Tiles(this LineString lineString, int zoom)

// always two tiles or more, create hashset.
// make sure to return only unique tiles.
if (tiles == null) tiles = new HashSet<ulong> {previousTileId};
if (tiles == null)
tiles = new HashSet<ulong> {previousTileId};

// if the tiles are not neighbours then also return everything in between.
if (!Tile.IsDirectNeighbour(tileId, previousTileId))
{
// determine all tiles between the two.
var previousCoordinate = lineString.Coordinates[c - 1];
var previousTile = new Tile(previousTileId);
var previousTileCoordinates =
previousTile.SubCoordinates(previousCoordinate.Y, previousCoordinate.X);
var previousTileCoordinates = previousTile.SubCoordinates(previousCoordinate.Y, previousCoordinate.X);
var nextTile = new Tile(tileId);
var nextTileCoordinates = nextTile.SubCoordinates(coordinate.Y, coordinate.X);

Expand Down
Loading
Loading