第12章-矢量切片生成
第12章:矢量切片生成
12.1 矢量切片概述
矢量切片(Vector Tiles)是一种将矢量数据分割成瓦片的技术,广泛应用于 Web 地图。NetTopologySuite.IO.VectorTiles 提供了 Mapbox 矢量切片格式的读写支持。
12.1.1 矢量切片优势
- 按需加载:只加载当前视图范围内的数据
- 样式灵活:客户端可动态修改样式
- 文件小:使用 Protocol Buffers 编码
- 交互性强:支持点击查询等交互
12.1.2 安装 NuGet 包
dotnet add package NetTopologySuite.IO.VectorTiles
dotnet add package NetTopologySuite.IO.VectorTiles.Mapbox
12.2 基本概念
12.2.1 瓦片坐标系统
// 瓦片坐标 (z, x, y)
// z: 缩放级别 (0-22)
// x: 列号
// y: 行号
// 缩放级别 0: 整个世界是一个瓦片
// 缩放级别 1: 2x2 = 4 个瓦片
// 缩放级别 n: 2^n x 2^n 个瓦片
12.2.2 瓦片边界计算
using NetTopologySuite.IO.VectorTiles.Tiles;
public static class TileHelper
{
/// <summary>
/// 计算瓦片的地理边界
/// </summary>
public static (double MinLon, double MinLat, double MaxLon, double MaxLat)
GetTileBounds(int z, int x, int y)
{
var n = Math.Pow(2, z);
var minLon = x / n * 360.0 - 180.0;
var maxLon = (x + 1) / n * 360.0 - 180.0;
var minLatRad = Math.Atan(Math.Sinh(Math.PI * (1 - 2 * (y + 1) / n)));
var maxLatRad = Math.Atan(Math.Sinh(Math.PI * (1 - 2 * y / n)));
var minLat = minLatRad * 180.0 / Math.PI;
var maxLat = maxLatRad * 180.0 / Math.PI;
return (minLon, minLat, maxLon, maxLat);
}
/// <summary>
/// 计算点所在的瓦片坐标
/// </summary>
public static (int X, int Y) GetTileIndex(double lon, double lat, int z)
{
var n = Math.Pow(2, z);
var x = (int)Math.Floor((lon + 180.0) / 360.0 * n);
var latRad = lat * Math.PI / 180.0;
var y = (int)Math.Floor((1.0 - Math.Log(Math.Tan(latRad) +
1.0 / Math.Cos(latRad)) / Math.PI) / 2.0 * n);
return (x, y);
}
}
// 使用示例
var (minLon, minLat, maxLon, maxLat) = TileHelper.GetTileBounds(10, 837, 421);
Console.WriteLine($"瓦片边界: ({minLon:F4}, {minLat:F4}) - ({maxLon:F4}, {maxLat:F4})");
var (tx, ty) = TileHelper.GetTileIndex(116.4074, 39.9042, 10);
Console.WriteLine($"北京所在瓦片: z=10, x={tx}, y={ty}");
12.3 生成矢量切片
12.3.1 基本生成
using NetTopologySuite.Features;
using NetTopologySuite.Geometries;
using NetTopologySuite.IO.VectorTiles;
using NetTopologySuite.IO.VectorTiles.Mapbox;
var factory = new GeometryFactory(new PrecisionModel(), 4326);
// 创建要素
var features = new List<IFeature>();
// 添加点
var point = factory.CreatePoint(new Coordinate(116.4074, 39.9042));
features.Add(new Feature(point, new AttributesTable
{
{ "name", "北京" },
{ "population", 21540000 }
}));
// 添加多边形
var polygon = factory.CreatePolygon(new Coordinate[]
{
new Coordinate(116.3, 39.8), new Coordinate(116.5, 39.8),
new Coordinate(116.5, 40.0), new Coordinate(116.3, 40.0),
new Coordinate(116.3, 39.8)
});
features.Add(new Feature(polygon, new AttributesTable
{
{ "name", "北京区域" },
{ "type", "city" }
}));
// 创建矢量切片
var tileIndex = new Tiles.Tile(10, 837, 421); // z, x, y
var vectorTile = new VectorTile { TileId = tileIndex.Id };
// 添加图层
var layer = new Layer { Name = "cities" };
foreach (var feature in features)
{
layer.Features.Add(feature);
}
vectorTile.Layers.Add(layer);
// 写入文件
using (var stream = File.Create("tile_10_837_421.mvt"))
{
vectorTile.Write(stream);
}
12.3.2 使用 MapboxTileWriter
using NetTopologySuite.IO.VectorTiles.Mapbox;
// 配置 Writer
var writerOptions = new MapboxTileWriter.Options
{
// 最小线段长度(extent 单位)
MinLinealExtent = 1,
// 最小多边形面积(extent 单位)
MinPolygonalExtent = 1
};
// 写入瓦片
using (var stream = File.Create("output.mvt"))
{
var writer = new MapboxTileWriter();
writer.Write(vectorTile, stream, writerOptions);
}
12.3.3 批量生成切片
public class VectorTileGenerator
{
private readonly GeometryFactory _factory;
private readonly int _minZoom;
private readonly int _maxZoom;
public VectorTileGenerator(int minZoom = 0, int maxZoom = 14)
{
_factory = new GeometryFactory(new PrecisionModel(), 4326);
_minZoom = minZoom;
_maxZoom = maxZoom;
}
/// <summary>
/// 生成指定范围内的所有切片
/// </summary>
public void GenerateTiles(
IEnumerable<IFeature> features,
string outputDir,
string layerName,
Envelope? bounds = null)
{
var featureList = features.ToList();
// 计算边界
if (bounds == null)
{
bounds = new Envelope();
foreach (var feature in featureList)
{
bounds.ExpandToInclude(feature.Geometry.EnvelopeInternal);
}
}
for (int z = _minZoom; z <= _maxZoom; z++)
{
// 计算该级别需要生成的瓦片范围
var (minX, minY) = TileHelper.GetTileIndex(bounds.MinX, bounds.MaxY, z);
var (maxX, maxY) = TileHelper.GetTileIndex(bounds.MaxX, bounds.MinY, z);
Console.WriteLine($"生成级别 {z}: ({minX},{minY}) - ({maxX},{maxY})");
for (int x = minX; x <= maxX; x++)
{
for (int y = minY; y <= maxY; y++)
{
GenerateTile(featureList, outputDir, layerName, z, x, y);
}
}
}
}
private void GenerateTile(
List<IFeature> features,
string outputDir,
string layerName,
int z, int x, int y)
{
// 获取瓦片边界
var (minLon, minLat, maxLon, maxLat) = TileHelper.GetTileBounds(z, x, y);
var tileBounds = _factory.CreatePolygon(new Coordinate[]
{
new Coordinate(minLon, minLat),
new Coordinate(maxLon, minLat),
new Coordinate(maxLon, maxLat),
new Coordinate(minLon, maxLat),
new Coordinate(minLon, minLat)
});
// 筛选与瓦片相交的要素
var tileFeatures = features
.Where(f => f.Geometry.Intersects(tileBounds))
.Select(f =>
{
// 裁剪几何到瓦片范围
var clipped = f.Geometry.Intersection(tileBounds);
if (clipped.IsEmpty)
return null;
return new Feature(clipped, f.Attributes) as IFeature;
})
.Where(f => f != null)
.ToList();
if (tileFeatures.Count == 0)
return;
// 创建瓦片
var tileIndex = new Tiles.Tile(z, x, y);
var vectorTile = new VectorTile { TileId = tileIndex.Id };
var layer = new Layer { Name = layerName };
foreach (var feature in tileFeatures)
{
layer.Features.Add(feature!);
}
vectorTile.Layers.Add(layer);
// 创建目录并写入文件
var dir = Path.Combine(outputDir, z.ToString(), x.ToString());
Directory.CreateDirectory(dir);
var filePath = Path.Combine(dir, $"{y}.mvt");
using (var stream = File.Create(filePath))
{
vectorTile.Write(stream);
}
}
}
// 使用示例
var generator = new VectorTileGenerator(minZoom: 8, maxZoom: 14);
var features = new List<IFeature>
{
// 添加要素...
};
generator.GenerateTiles(features, "tiles", "cities");
12.4 读取矢量切片
12.4.1 读取单个切片
using NetTopologySuite.IO.VectorTiles.Mapbox;
// 读取切片
using (var stream = File.OpenRead("tile_10_837_421.mvt"))
{
var reader = new MapboxTileReader();
var tile = new Tiles.Tile(10, 837, 421);
var vectorTile = reader.Read(stream, tile);
Console.WriteLine($"图层数量: {vectorTile.Layers.Count}");
foreach (var layer in vectorTile.Layers)
{
Console.WriteLine($"\n图层: {layer.Name}");
Console.WriteLine($"要素数量: {layer.Features.Count}");
foreach (var feature in layer.Features)
{
Console.WriteLine($" 几何类型: {feature.Geometry.GeometryType}");
if (feature.Attributes != null)
{
foreach (var name in feature.Attributes.GetNames())
{
Console.WriteLine($" {name}: {feature.Attributes[name]}");
}
}
}
}
}
12.4.2 从 URL 读取
public async Task<VectorTile?> ReadTileFromUrl(string urlTemplate, int z, int x, int y)
{
var url = urlTemplate
.Replace("{z}", z.ToString())
.Replace("{x}", x.ToString())
.Replace("{y}", y.ToString());
using var httpClient = new HttpClient();
try
{
var data = await httpClient.GetByteArrayAsync(url);
using var stream = new MemoryStream(data);
var reader = new MapboxTileReader();
var tile = new Tiles.Tile(z, x, y);
return reader.Read(stream, tile);
}
catch (Exception ex)
{
Console.WriteLine($"读取切片失败: {ex.Message}");
return null;
}
}
// 使用示例
var tile = await ReadTileFromUrl(
"https://example.com/tiles/{z}/{x}/{y}.mvt",
10, 837, 421);
12.5 矢量切片服务
12.5.1 ASP.NET Core 切片服务
using Microsoft.AspNetCore.Mvc;
using NetTopologySuite.IO.VectorTiles;
using NetTopologySuite.IO.VectorTiles.Mapbox;
[ApiController]
[Route("tiles")]
public class VectorTileController : ControllerBase
{
private readonly IFeatureRepository _repository;
public VectorTileController(IFeatureRepository repository)
{
_repository = repository;
}
[HttpGet("{z}/{x}/{y}.mvt")]
public async Task<IActionResult> GetTile(int z, int x, int y)
{
// 验证参数
if (z < 0 || z > 22 || x < 0 || y < 0)
{
return BadRequest("Invalid tile coordinates");
}
// 计算瓦片边界
var (minLon, minLat, maxLon, maxLat) = TileHelper.GetTileBounds(z, x, y);
var bounds = new Envelope(minLon, maxLon, minLat, maxLat);
// 获取要素
var features = await _repository.GetFeaturesByExtentAsync(bounds);
if (features.Count == 0)
{
return NoContent();
}
// 生成切片
var tileIndex = new Tiles.Tile(z, x, y);
var vectorTile = new VectorTile { TileId = tileIndex.Id };
var layer = new Layer { Name = "features" };
foreach (var feature in features)
{
layer.Features.Add(feature);
}
vectorTile.Layers.Add(layer);
// 写入响应
using var memoryStream = new MemoryStream();
vectorTile.Write(memoryStream);
return File(
memoryStream.ToArray(),
"application/vnd.mapbox-vector-tile",
$"{y}.mvt");
}
}
12.5.2 带缓存的切片服务
public class CachedVectorTileService
{
private readonly IFeatureRepository _repository;
private readonly string _cacheDir;
public CachedVectorTileService(IFeatureRepository repository, string cacheDir)
{
_repository = repository;
_cacheDir = cacheDir;
}
public async Task<byte[]?> GetTileAsync(int z, int x, int y)
{
// 检查缓存
var cachePath = Path.Combine(_cacheDir, $"{z}/{x}/{y}.mvt");
if (File.Exists(cachePath))
{
return await File.ReadAllBytesAsync(cachePath);
}
// 生成切片
var (minLon, minLat, maxLon, maxLat) = TileHelper.GetTileBounds(z, x, y);
var bounds = new Envelope(minLon, maxLon, minLat, maxLat);
var features = await _repository.GetFeaturesByExtentAsync(bounds);
if (features.Count == 0)
{
return null;
}
var tileIndex = new Tiles.Tile(z, x, y);
var vectorTile = new VectorTile { TileId = tileIndex.Id };
var layer = new Layer { Name = "features" };
foreach (var feature in features)
{
layer.Features.Add(feature);
}
vectorTile.Layers.Add(layer);
using var memoryStream = new MemoryStream();
vectorTile.Write(memoryStream);
var data = memoryStream.ToArray();
// 保存到缓存
Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!);
await File.WriteAllBytesAsync(cachePath, data);
return data;
}
public void ClearCache()
{
if (Directory.Exists(_cacheDir))
{
Directory.Delete(_cacheDir, true);
}
}
}
12.6 本章小结
本章介绍了 NetTopologySuite 的矢量切片功能:
- 基本概念:瓦片坐标系统、边界计算
- 生成切片:基本生成、批量生成
- 读取切片:从文件和 URL 读取
- 切片服务:ASP.NET Core 切片服务
12.7 下一步
下一章我们将学习 Feature 与属性管理。
相关资源:

浙公网安备 33010602011771号