第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 的矢量切片功能:

  1. 基本概念:瓦片坐标系统、边界计算
  2. 生成切片:基本生成、批量生成
  3. 读取切片:从文件和 URL 读取
  4. 切片服务:ASP.NET Core 切片服务

12.7 下一步

下一章我们将学习 Feature 与属性管理。


相关资源

posted @ 2025-12-29 10:22  我才是银古  阅读(2)  评论(0)    收藏  举报