Skip to content

Commit

Permalink
Merge pull request #26 from NetTopologySuite/develop
Browse files Browse the repository at this point in the history
Release v1.0.2
  • Loading branch information
xivk authored Nov 14, 2023
2 parents 49d008d + 1fbb921 commit 86808a6
Show file tree
Hide file tree
Showing 39 changed files with 163,827 additions and 362 deletions.
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

0 comments on commit 86808a6

Please sign in to comment.