diff --git a/src/NetTopologySuite.IO.VectorTiles.Mapbox/MapboxTileWriter.cs b/src/NetTopologySuite.IO.VectorTiles.Mapbox/MapboxTileWriter.cs index 917c6b5..ba008aa 100644 --- a/src/NetTopologySuite.IO.VectorTiles.Mapbox/MapboxTileWriter.cs +++ b/src/NetTopologySuite.IO.VectorTiles.Mapbox/MapboxTileWriter.cs @@ -9,6 +9,12 @@ namespace NetTopologySuite.IO.VectorTiles.Mapbox // see: https://github.com/mapbox/vector-tile-spec/tree/master/2.1 public static class MapboxTileWriter { + private const uint DefaultMinLinealExtent = 1; + + private const uint DefaultMinPolygonalExtent = 2; + + private const string DefaultIdAttributeName = "id"; + /// /// Writes the tiles in a /z/x/y.mvt folder structure. /// @@ -16,7 +22,19 @@ public static class MapboxTileWriter /// The path. /// The extent. /// Replaces the files if they are already present. + [Obsolete("Use overload that can specify minLineal- and minPolygonalExtent")] public static void Write(this VectorTileTree tree, string path, uint extent = 4096) + => Write(tree, path, DefaultMinLinealExtent, DefaultMinPolygonalExtent, extent, DefaultIdAttributeName); + + /// + /// Writes the tiles in a /z/x/y.mvt folder structure. + /// + /// The tree. + /// The path. + /// The extent. + /// Replaces the files if they are already present. + public static void Write(this VectorTileTree tree, string path, uint minLinealExtent, uint minPolygonalExtent, + uint extent = 4096, string idAttributeName = DefaultIdAttributeName) { IEnumerable GetTiles() { @@ -26,7 +44,7 @@ IEnumerable GetTiles() } } - GetTiles().Write(path, extent); + GetTiles().Write(path, minLinealExtent, minPolygonalExtent, extent, idAttributeName); } /// @@ -36,7 +54,19 @@ IEnumerable GetTiles() /// The path. /// The extent. /// Replaces the files if they are already present. + [Obsolete("Use overload that can specify minLineal- and minPolygonalExtent")] public static void Write(this IEnumerable vectorTiles, string path, uint extent = 4096) + => Write(vectorTiles, path, DefaultMinLinealExtent, DefaultMinPolygonalExtent, extent, DefaultIdAttributeName); + + /// + /// Writes the tiles in a /z/x/y.mvt folder structure. + /// + /// The tiles. + /// The path. + /// The extent. + /// Replaces the files if they are already present. + public static void Write(this IEnumerable vectorTiles, string path, uint minLinealExtent, uint minPolygonalExtent, + uint extent = 4096, string idAttributeName = DefaultIdAttributeName) { foreach (var vectorTile in vectorTiles) { @@ -54,7 +84,7 @@ public static void Write(this IEnumerable vectorTiles, string path, string file = Path.Combine(xFolder, $"{tile.Y}.mvt"); using var stream = File.Open(file, FileMode.Create); - vectorTile.Write(stream, extent); + vectorTile.Write(stream, minLinealExtent, minPolygonalExtent, extent, idAttributeName); } } @@ -65,7 +95,21 @@ public static void Write(this IEnumerable vectorTiles, string path, /// The stream to write to. /// The extent. /// The name of an attribute property to use as the ID for the Feature. Vector tile feature ID's should be integer or ulong numbers. + [Obsolete("Use overload that can specify minLineal- and minPolygonalExtent")] public static void Write(this VectorTile vectorTile, Stream stream, uint extent = 4096, string idAttributeName = "id") + => Write(vectorTile, stream, DefaultMinLinealExtent, DefaultMinPolygonalExtent, extent, idAttributeName); + + /// + /// Writes the tile to the given stream. + /// + /// The vector tile. + /// The stream to write to. + /// The minimum length in pixel one of a lineal feature's extent edges has to have in order for the feature to be written. The default is 1. + /// The minimum area in pixel one of a polygonal feature's extent has to have in order for the feature to be written. The default is 2. + /// The extent. + /// The name of an attribute property to use as the ID for the Feature. Vector tile feature ID's should be integer or ulong numbers. + public static void Write(this VectorTile vectorTile, Stream stream, uint minLinealExtent, uint minPolygonalExtent, + uint extent = 4096, string idAttributeName = "id") { var tile = new Tiles.Tile(vectorTile.TileId); var tgt = new TileGeometryTransform(tile, extent); @@ -95,11 +139,11 @@ public static void Write(this VectorTile vectorTile, Stream stream, uint extent break; case ILineal lineal: feature.Type = Tile.GeomType.LineString; - EncodeTo(feature.Geometry, lineal, tgt); + EncodeTo(feature.Geometry, lineal, minLinealExtent, tgt); break; case IPolygonal polygonal: feature.Type = Tile.GeomType.Polygon; - EncodeTo(feature.Geometry, polygonal, tgt, tile.Zoom); + EncodeTo(feature.Geometry, polygonal, minPolygonalExtent, tgt); break; default: feature.Type = Tile.GeomType.Unknown; @@ -236,39 +280,46 @@ private static void EncodeTo(List destination, IPuntal puntal, TileGeometr destination[moveToIndex] = GenerateCommandInteger(MapboxCommandType.MoveTo, (destination.Count - moveToIndex) / 2); } - private static void EncodeTo(List destination, ILineal lineal, TileGeometryTransform tgt) + private static void EncodeTo(List destination, ILineal lineal, uint minLinealExtent, TileGeometryTransform tgt) { + bool HasValidLength((long x, long y) tpl) + => tpl.x >= minLinealExtent || tpl.y >= minLinealExtent; + var geometry = (Geometry)lineal; int currentX = 0, currentY = 0; for (int i = 0; i < geometry.NumGeometries; i++) { var lineString = (LineString)geometry.GetGeometryN(i); - if (tgt.IsGreaterThanOnePixelOfTile(lineString)) + if (HasValidLength(tgt.ExtentInPixel(lineString.EnvelopeInternal))) EncodeTo(destination, lineString.CoordinateSequence, tgt, ref currentX, ref currentY, false); } } - private static void EncodeTo(List destination, IPolygonal polygonal, TileGeometryTransform tgt, int zoom) + private static void EncodeTo(List destination, IPolygonal polygonal, uint minPolygonalExtent, TileGeometryTransform tgt) { + bool HasValidExtent((long x, long y) tpl) + => (tpl.x > minPolygonalExtent && tpl.y > 0) || + (tpl.x > 0 && tpl.y > minPolygonalExtent); + var geometry = (Geometry)polygonal; - //Test the whole polygon geometry is larger than a single pixel. - if (tgt.IsGreaterThanOnePixelOfTile(geometry)) + //Test the whole polygonal geometry has a valid extent. + if (HasValidExtent(tgt.ExtentInPixel(geometry.EnvelopeInternal))) { int currentX = 0, currentY = 0; for (int i = 0; i < geometry.NumGeometries; i++) { var polygon = (Polygon)geometry.GetGeometryN(i); - // Test that the exterior ring is larger than a single pixel. - if (!tgt.IsGreaterThanOnePixelOfTile(polygon.ExteriorRing)) + // Test that the exterior ring has a valid extent + if (!HasValidExtent(tgt.ExtentInPixel(polygon.ExteriorRing.EnvelopeInternal))) continue; EncodeTo(destination, polygon.ExteriorRing.CoordinateSequence, tgt, ref currentX, ref currentY, true, false); foreach (var interiorRing in polygon.InteriorRings) { - // Test that the interior ring is larger than a single pixel. - if (!tgt.IsGreaterThanOnePixelOfTile(interiorRing)) + // Test that the interior ring has a valid extent. + if (!HasValidExtent(tgt.ExtentInPixel(interiorRing.EnvelopeInternal))) continue; EncodeTo(destination, interiorRing.CoordinateSequence, tgt, ref currentX, ref currentY, true, true); diff --git a/src/NetTopologySuite.IO.VectorTiles.Mapbox/TileGeometryTransform.cs b/src/NetTopologySuite.IO.VectorTiles.Mapbox/TileGeometryTransform.cs index b729b32..f99b8f2 100644 --- a/src/NetTopologySuite.IO.VectorTiles.Mapbox/TileGeometryTransform.cs +++ b/src/NetTopologySuite.IO.VectorTiles.Mapbox/TileGeometryTransform.cs @@ -119,5 +119,13 @@ public bool IsGreaterThanOnePixelOfTile(Geometry geometry) // 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); } + + public (long x, long y) ExtentInPixel(Envelope env) + { + (long minX, long minY) = WebMercatorHandler.FromMetersToPixels(WebMercatorHandler.LatLonToMeters(env.MinY, env.MinX), ZoomResolution); + (long maxX, long maxY) = WebMercatorHandler.FromMetersToPixels(WebMercatorHandler.LatLonToMeters(env.MaxY, env.MaxX), ZoomResolution); + + return (maxX - minX, maxY - minY); + } } } diff --git a/test/NetTopologySuite.IO.VectorTiles.Samples/Startup.cs b/test/NetTopologySuite.IO.VectorTiles.Samples/Startup.cs index e9f9593..5dcf396 100644 --- a/test/NetTopologySuite.IO.VectorTiles.Samples/Startup.cs +++ b/test/NetTopologySuite.IO.VectorTiles.Samples/Startup.cs @@ -113,7 +113,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) tree.GetExtents(out Pages.IndexModel._BBox, out Pages.IndexModel._MinZoom, out Pages.IndexModel._MaxZoom); - tree.Write("wwwroot/tiles"); + tree.Write("wwwroot/tiles", 1, 2); } } } diff --git a/test/NetTopologySuite.IO.VectorTiles.Tests.Functional/Program.cs b/test/NetTopologySuite.IO.VectorTiles.Tests.Functional/Program.cs index 15c136c..31624a0 100644 --- a/test/NetTopologySuite.IO.VectorTiles.Tests.Functional/Program.cs +++ b/test/NetTopologySuite.IO.VectorTiles.Tests.Functional/Program.cs @@ -56,7 +56,7 @@ private static void RunTest(string fileName, int minZoom, int maxZoom, string st }; // write the tiles to disk as mvt. - Mapbox.MapboxTileWriter.Write(tree, "tiles"); + Mapbox.MapboxTileWriter.Write(tree, "tiles", 1, 2); stopwatch.Stop(); diff --git a/test/NetTopologySuite.IO.VectorTiles.Tests/Issues/Issue29.cs b/test/NetTopologySuite.IO.VectorTiles.Tests/Issues/Issue29.cs index fc32748..4478982 100644 --- a/test/NetTopologySuite.IO.VectorTiles.Tests/Issues/Issue29.cs +++ b/test/NetTopologySuite.IO.VectorTiles.Tests/Issues/Issue29.cs @@ -34,7 +34,7 @@ private static void WriteTile(int x, int y, int z) MemoryStream? ms; using (ms = new MemoryStream(1024 * 32)) { - vectorTile.Write(ms); + vectorTile.Write(ms, 1, 2); result = ms.ToArray(); } diff --git a/test/NetTopologySuite.IO.VectorTiles.Tests/Mapbox/FeatureIdTest.cs b/test/NetTopologySuite.IO.VectorTiles.Tests/Mapbox/FeatureIdTest.cs index 81815f7..adf0793 100644 --- a/test/NetTopologySuite.IO.VectorTiles.Tests/Mapbox/FeatureIdTest.cs +++ b/test/NetTopologySuite.IO.VectorTiles.Tests/Mapbox/FeatureIdTest.cs @@ -42,11 +42,11 @@ private void FeatureIdReadWriteTest(Features.Feature featureS, ulong expectedId, if (string.IsNullOrEmpty(idAttributeName)) { //No ID property specified when writing. - vtS.Write(ms); + vtS.Write(ms, 1, 2); } else { - vtS.Write(ms, 4096, idAttributeName); + vtS.Write(ms, 1, 2, 4096, idAttributeName); } ms.Position = 0; diff --git a/test/NetTopologySuite.IO.VectorTiles.Tests/RoundTripBase.cs b/test/NetTopologySuite.IO.VectorTiles.Tests/RoundTripBase.cs index 8815df1..d25d953 100644 --- a/test/NetTopologySuite.IO.VectorTiles.Tests/RoundTripBase.cs +++ b/test/NetTopologySuite.IO.VectorTiles.Tests/RoundTripBase.cs @@ -73,7 +73,7 @@ protected void AssertRoundTrip(Geometry inputGeometry, Geometry expectedGeometry VectorTile? vtD = null; using (var ms = new MemoryStream()) { - vtS.Write(ms, Extent); + vtS.Write(ms, 1, 2, Extent); ms.Position = 0; vtD = new MapboxTileReader(Factory).Read(ms, new VectorTiles.Tiles.Tile(0)); } @@ -153,7 +153,7 @@ protected void AssertRoundTripEmptyTile(string inputDefinition, string? name = n VectorTile? vtD = null; using (var ms = new MemoryStream()) { - vtS.Write(ms); + vtS.Write(ms, 1, 1); ms.Position = 0; vtD = new MapboxTileReader(Factory).Read(ms, new VectorTiles.Tiles.Tile(0)); } diff --git a/test/NetTopologySuite.IO.VectorTiles.Tests/Tilers/MapboxTileWriterTest.cs b/test/NetTopologySuite.IO.VectorTiles.Tests/Tilers/MapboxTileWriterTest.cs index c09c8a9..65792fc 100644 --- a/test/NetTopologySuite.IO.VectorTiles.Tests/Tilers/MapboxTileWriterTest.cs +++ b/test/NetTopologySuite.IO.VectorTiles.Tests/Tilers/MapboxTileWriterTest.cs @@ -83,7 +83,7 @@ private static void DoTestEmpty(Geometry geom, int numGeoms = 0) var f1 = new Feature(geom, att); l1.Features.Add(f1); using var ms = new MemoryStream(); - var e = Record.Exception(() => MapboxTileWriter.Write(vt1, ms)); + var e = Record.Exception(() => MapboxTileWriter.Write(vt1, ms, 1, 2)); Assert.Null(e); Assert.True(ms.Length > 0);