第06章 - 数据提供者与数据源

第06章:数据提供者与数据源

6.1 数据提供者概述

6.1.1 IProvider 接口

数据提供者(Provider)是 Mapsui 获取地理数据的核心组件:

public interface IProvider
{
    /// <summary>
    /// 坐标参考系统标识符
    /// </summary>
    string? CRS { get; set; }
    
    /// <summary>
    /// 数据范围
    /// </summary>
    MRect? Extent { get; }
    
    /// <summary>
    /// 获取指定范围内的要素
    /// </summary>
    IEnumerable<IFeature> GetFeatures(FetchInfo fetchInfo);
}

6.1.2 数据提供者类型

┌─────────────────────────────────────────────────────────────────┐
│                       IProvider 实现                             │
├─────────────────────────────────────────────────────────────────┤
│  本地数据          │  网络服务          │  内存数据              │
│  ─────────        │  ─────────        │  ─────────            │
│  ShapeFileProvider│  WmsProvider      │  MemoryProvider       │
│  GeoJsonProvider  │  WfsProvider      │  GeometryProvider     │
│  MbTilesProvider  │  ArcGisProvider   │  ProjectingProvider   │
└─────────────────────────────────────────────────────────────────┘

6.2 内置数据提供者

6.2.1 MemoryProvider - 内存数据提供者

// 创建要素集合
var features = new List<IFeature>
{
    CreatePointFeature(116.4, 39.9, "北京"),
    CreatePointFeature(121.5, 31.2, "上海"),
    CreatePointFeature(113.3, 23.1, "广州")
};

// 创建 MemoryProvider
var memoryProvider = new MemoryProvider(features)
{
    CRS = "EPSG:3857"
};

// 用于 Layer
var layer = new Layer
{
    Name = "Cities",
    DataSource = memoryProvider,
    Style = CreatePointStyle()
};

// 辅助方法
private PointFeature CreatePointFeature(double lon, double lat, string name)
{
    var point = SphericalMercator.FromLonLat(lon, lat).ToMPoint();
    var feature = new PointFeature(point);
    feature["name"] = name;
    return feature;
}

6.2.2 ProjectingProvider - 投影转换提供者

当数据源的坐标系与地图坐标系不同时,使用 ProjectingProvider 进行转换:

// 假设有一个 WGS84 坐标系的数据提供者
var wgs84Provider = new MyWgs84Provider();
wgs84Provider.CRS = "EPSG:4326";

// 包装为投影转换提供者
var projectingProvider = new ProjectingProvider(wgs84Provider)
{
    CRS = "EPSG:3857"  // 目标坐标系
};

// 使用投影提供者创建图层
var layer = new Layer
{
    Name = "ProjectedData",
    DataSource = projectingProvider,
    Style = CreateStyle()
};

// 设置地图坐标系
map.CRS = "EPSG:3857";

6.2.3 GeometryProvider - 几何提供者

// 使用 NTS 几何创建提供者
var geometryFactory = new GeometryFactory();

var geometries = new[]
{
    geometryFactory.CreatePoint(new Coordinate(116.4, 39.9)),
    geometryFactory.CreatePoint(new Coordinate(121.5, 31.2)),
    geometryFactory.CreateLineString(new[]
    {
        new Coordinate(116.4, 39.9),
        new Coordinate(121.5, 31.2)
    })
};

var geometryProvider = new GeometryProvider(geometries);

var layer = new Layer
{
    DataSource = geometryProvider,
    Style = new VectorStyle
    {
        Fill = new Brush(Color.Red),
        Line = new Pen(Color.Blue, 2)
    }
};

6.3 文件数据源

6.3.1 GeoJSON 数据源

使用 Mapsui.Extensions 包读取 GeoJSON:

using Mapsui.Extensions.GeoJson;

// 从文件读取 GeoJSON
var geoJsonPath = "data/cities.geojson";
var features = GeoJsonReader.ReadFeatures(File.ReadAllText(geoJsonPath));

var layer = new MemoryLayer
{
    Name = "GeoJsonData",
    Features = features,
    Style = CreateStyle()
};

// 从 URL 读取 GeoJSON
public async Task<MemoryLayer> LoadGeoJsonFromUrlAsync(string url)
{
    using var client = new HttpClient();
    var json = await client.GetStringAsync(url);
    var features = GeoJsonReader.ReadFeatures(json);
    
    return new MemoryLayer
    {
        Name = "RemoteGeoJson",
        Features = features
    };
}

6.3.2 Shapefile 数据源

使用 Mapsui.Nts 包读取 Shapefile:

using Mapsui.Providers.Shapefile;

// 读取 Shapefile
var shapefilePath = "data/countries.shp";
var shapeFileProvider = new ShapeFile(shapefilePath)
{
    CRS = "EPSG:4326"
};

// 如果地图使用 Web Mercator,需要投影转换
var projectingProvider = new ProjectingProvider(shapeFileProvider)
{
    CRS = "EPSG:3857"
};

var layer = new Layer
{
    Name = "Countries",
    DataSource = projectingProvider,
    Style = new VectorStyle
    {
        Fill = new Brush(new Color(200, 200, 200)),
        Outline = new Pen(Color.Gray, 1)
    }
};

6.4 网络服务数据源

6.4.1 WMS 服务

using Mapsui.Providers.Wms;

// 创建 WMS 提供者
var wmsUrl = "https://example.com/wms";
var wmsProvider = new WmsProvider(new Uri(wmsUrl))
{
    LayerNames = new[] { "roads", "buildings" },
    CRS = "EPSG:3857"
};

// 创建 WMS 图层
var wmsLayer = new ImageLayer
{
    Name = "WMS Layer",
    DataSource = wmsProvider
};

// 配置 WMS 参数
public WmsProvider CreateWmsProvider(string url, string[] layers)
{
    var provider = new WmsProvider(new Uri(url));
    provider.LayerNames = layers;
    provider.CRS = "EPSG:3857";
    provider.Version = "1.3.0";  // WMS 版本
    provider.ImageFormat = "image/png";  // 图像格式
    
    return provider;
}

6.4.2 WMTS 服务

using BruTile.Wmts;

// 创建 WMTS 瓦片源
var wmtsUrl = "https://example.com/wmts/1.0.0/WMTSCapabilities.xml";
var wmtsTileSource = new WmtsTileSource(
    new Uri(wmtsUrl),
    "layerIdentifier",
    new MemoryCache<byte[]>()
);

// 创建 WMTS 图层
var wmtsLayer = new TileLayer(wmtsTileSource)
{
    Name = "WMTS Layer"
};

6.4.3 ArcGIS 服务

使用 Mapsui.ArcGIS 包:

using Mapsui.ArcGIS;

// ArcGIS 动态地图服务
var arcGisUrl = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer";
var arcGisProvider = new ArcGISDynamicProvider(new Uri(arcGisUrl));

var arcGisLayer = new ImageLayer
{
    Name = "ArcGIS Imagery",
    DataSource = arcGisProvider
};

// ArcGIS 瓦片服务
var arcGisTileUrl = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer";
var arcGisTileLayer = new TileLayer(
    CreateArcGisTileSource(arcGisTileUrl)
);

6.5 自定义数据提供者

6.5.1 实现 IProvider

public class SqlServerProvider : IProvider
{
    private readonly string _connectionString;
    private readonly string _tableName;
    private readonly string _geometryColumn;
    private MRect? _extent;
    
    public string? CRS { get; set; } = "EPSG:4326";
    
    public MRect? Extent
    {
        get => _extent ??= CalculateExtent();
    }
    
    public SqlServerProvider(string connectionString, string tableName, 
                             string geometryColumn = "geom")
    {
        _connectionString = connectionString;
        _tableName = tableName;
        _geometryColumn = geometryColumn;
    }
    
    public IEnumerable<IFeature> GetFeatures(FetchInfo fetchInfo)
    {
        var features = new List<IFeature>();
        var extent = fetchInfo.Extent;
        
        using var connection = new SqlConnection(_connectionString);
        connection.Open();
        
        var sql = $@"
            SELECT *, {_geometryColumn}.STAsBinary() as WKB
            FROM {_tableName}
            WHERE {_geometryColumn}.STIntersects(
                geometry::STGeomFromText(
                    'POLYGON(({extent.MinX} {extent.MinY}, 
                             {extent.MaxX} {extent.MinY}, 
                             {extent.MaxX} {extent.MaxY}, 
                             {extent.MinX} {extent.MaxY}, 
                             {extent.MinX} {extent.MinY}))', 
                    4326)
            ) = 1";
        
        using var cmd = new SqlCommand(sql, connection);
        using var reader = cmd.ExecuteReader();
        
        var wkbReader = new WKBReader();
        
        while (reader.Read())
        {
            var wkb = (byte[])reader["WKB"];
            var geometry = wkbReader.Read(wkb);
            var feature = new GeometryFeature(geometry);
            
            // 复制所有属性
            for (int i = 0; i < reader.FieldCount; i++)
            {
                var columnName = reader.GetName(i);
                if (columnName != "WKB" && columnName != _geometryColumn)
                {
                    feature[columnName] = reader[i];
                }
            }
            
            features.Add(feature);
        }
        
        return features;
    }
    
    private MRect CalculateExtent()
    {
        using var connection = new SqlConnection(_connectionString);
        connection.Open();
        
        var sql = $@"
            SELECT 
                {_geometryColumn}.STEnvelope().STPointN(1).STX as MinX,
                {_geometryColumn}.STEnvelope().STPointN(1).STY as MinY,
                {_geometryColumn}.STEnvelope().STPointN(3).STX as MaxX,
                {_geometryColumn}.STEnvelope().STPointN(3).STY as MaxY
            FROM {_tableName}";
        
        using var cmd = new SqlCommand(sql, connection);
        using var reader = cmd.ExecuteReader();
        
        double minX = double.MaxValue, minY = double.MaxValue;
        double maxX = double.MinValue, maxY = double.MinValue;
        
        while (reader.Read())
        {
            minX = Math.Min(minX, (double)reader["MinX"]);
            minY = Math.Min(minY, (double)reader["MinY"]);
            maxX = Math.Max(maxX, (double)reader["MaxX"]);
            maxY = Math.Max(maxY, (double)reader["MaxY"]);
        }
        
        return new MRect(minX, minY, maxX, maxY);
    }
}

6.5.2 带缓存的提供者

public class CachingProvider : IProvider
{
    private readonly IProvider _innerProvider;
    private readonly IMemoryCache _cache;
    private readonly TimeSpan _cacheExpiration;
    
    public string? CRS
    {
        get => _innerProvider.CRS;
        set => _innerProvider.CRS = value;
    }
    
    public MRect? Extent => _innerProvider.Extent;
    
    public CachingProvider(IProvider innerProvider, 
                           IMemoryCache cache,
                           TimeSpan? cacheExpiration = null)
    {
        _innerProvider = innerProvider;
        _cache = cache;
        _cacheExpiration = cacheExpiration ?? TimeSpan.FromMinutes(5);
    }
    
    public IEnumerable<IFeature> GetFeatures(FetchInfo fetchInfo)
    {
        var cacheKey = CreateCacheKey(fetchInfo);
        
        return _cache.GetOrCreate(cacheKey, entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = _cacheExpiration;
            return _innerProvider.GetFeatures(fetchInfo).ToList();
        })!;
    }
    
    private string CreateCacheKey(FetchInfo fetchInfo)
    {
        var extent = fetchInfo.Extent;
        return $"{_innerProvider.GetType().Name}_{extent.MinX}_{extent.MinY}_{extent.MaxX}_{extent.MaxY}_{fetchInfo.Resolution}";
    }
}

6.6 瓦片数据源

6.6.1 常用瓦片源

// OpenStreetMap
var osmTileSource = new HttpTileSource(
    new GlobalSphericalMercator(),
    "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
    name: "OpenStreetMap"
);

// Stamen 瓦片
var stamenTerrain = new HttpTileSource(
    new GlobalSphericalMercator(),
    "http://tile.stamen.com/terrain/{z}/{x}/{y}.png",
    name: "Stamen Terrain"
);

// 天地图
var tiandituVec = new HttpTileSource(
    new GlobalSphericalMercator(),
    "http://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=YOUR_TOKEN",
    name: "天地图矢量"
);

6.6.2 MBTiles 离线瓦片

using BruTile.MbTiles;

// 创建 MBTiles 瓦片源
var mbtilesPath = "maps/offline.mbtiles";
var mbTilesTileSource = new MbTilesTileSource(
    new SQLiteConnectionString(mbtilesPath, false)
);

// 创建离线瓦片图层
var offlineLayer = new TileLayer(mbTilesTileSource)
{
    Name = "Offline Map"
};

// 检查 MBTiles 文件信息
public void PrintMbTilesInfo(string path)
{
    using var connection = new SQLiteConnection(
        new SQLiteConnectionString(path, false)
    );
    connection.Open();
    
    var metadata = connection.Table<MbTilesMetadata>().ToList();
    foreach (var item in metadata)
    {
        Console.WriteLine($"{item.Name}: {item.Value}");
    }
}

6.7 数据源与坐标系

6.7.1 坐标系统匹配

public class CoordinateSystemHelper
{
    /// <summary>
    /// 确保数据提供者与地图坐标系统匹配
    /// </summary>
    public static IProvider EnsureMatchingCrs(IProvider provider, string mapCrs)
    {
        if (provider.CRS == mapCrs)
        {
            return provider;
        }
        
        // 如果坐标系不匹配,使用 ProjectingProvider 包装
        return new ProjectingProvider(provider)
        {
            CRS = mapCrs
        };
    }
    
    /// <summary>
    /// 创建带自动投影的图层
    /// </summary>
    public static Layer CreateLayerWithProjection(
        IProvider provider, 
        string providerCrs,
        string mapCrs,
        IStyle style)
    {
        provider.CRS = providerCrs;
        
        var effectiveProvider = EnsureMatchingCrs(provider, mapCrs);
        
        return new Layer
        {
            DataSource = effectiveProvider,
            Style = style
        };
    }
}

6.7.2 常用坐标系

public static class CommonCrs
{
    // WGS84 经纬度
    public const string Wgs84 = "EPSG:4326";
    
    // Web Mercator (Google, OSM 使用)
    public const string WebMercator = "EPSG:3857";
    
    // 中国2000坐标系
    public const string Cgcs2000 = "EPSG:4490";
    
    // 北京54坐标系
    public const string Beijing54 = "EPSG:4214";
    
    // 西安80坐标系
    public const string Xian80 = "EPSG:4610";
}

6.8 数据加载策略

6.8.1 同步加载

// 简单的同步加载
var layer = new MemoryLayer
{
    Features = LoadFeatures()  // 同步加载
};

private IEnumerable<IFeature> LoadFeatures()
{
    // 从数据库或文件加载
    return features;
}

6.8.2 异步加载

public class AsyncDataLoader
{
    private readonly Map _map;
    private readonly MemoryLayer _dataLayer;
    
    public AsyncDataLoader(Map map)
    {
        _map = map;
        _dataLayer = new MemoryLayer { Name = "AsyncData" };
        _map.Layers.Add(_dataLayer);
    }
    
    public async Task LoadDataAsync(string url)
    {
        try
        {
            // 显示加载指示
            ShowLoading();
            
            // 异步获取数据
            var features = await Task.Run(() => FetchFeaturesFromUrl(url));
            
            // 在 UI 线程更新
            _dataLayer.Features = features;
            _dataLayer.DataHasChanged();
        }
        catch (Exception ex)
        {
            ShowError(ex.Message);
        }
        finally
        {
            HideLoading();
        }
    }
    
    private IEnumerable<IFeature> FetchFeaturesFromUrl(string url)
    {
        using var client = new HttpClient();
        var json = client.GetStringAsync(url).Result;
        return GeoJsonReader.ReadFeatures(json);
    }
    
    private void ShowLoading() { /* 实现加载指示 */ }
    private void HideLoading() { /* 隐藏加载指示 */ }
    private void ShowError(string message) { /* 显示错误信息 */ }
}

6.8.3 分页加载

public class PagedDataProvider : IProvider
{
    private readonly Func<int, int, IEnumerable<IFeature>> _fetchPage;
    private readonly int _pageSize;
    
    public string? CRS { get; set; }
    public MRect? Extent { get; private set; }
    
    public PagedDataProvider(
        Func<int, int, IEnumerable<IFeature>> fetchPage,
        int pageSize = 100)
    {
        _fetchPage = fetchPage;
        _pageSize = pageSize;
    }
    
    public IEnumerable<IFeature> GetFeatures(FetchInfo fetchInfo)
    {
        var allFeatures = new List<IFeature>();
        int page = 0;
        
        while (true)
        {
            var pageFeatures = _fetchPage(page, _pageSize).ToList();
            
            if (pageFeatures.Count == 0)
                break;
            
            // 过滤在视口范围内的要素
            var filteredFeatures = pageFeatures
                .Where(f => IsInExtent(f, fetchInfo.Extent));
            
            allFeatures.AddRange(filteredFeatures);
            page++;
            
            if (pageFeatures.Count < _pageSize)
                break;
        }
        
        return allFeatures;
    }
    
    private bool IsInExtent(IFeature feature, MRect extent)
    {
        // 检查要素是否在范围内
        return feature.Extent?.Intersects(extent) == true;
    }
}

6.9 本章小结

本章详细介绍了 Mapsui 的数据提供者与数据源:

  1. IProvider 接口:数据提供者的核心抽象
  2. 内置数据提供者:MemoryProvider、ProjectingProvider、GeometryProvider
  3. 文件数据源:GeoJSON、Shapefile 文件读取
  4. 网络服务数据源:WMS、WMTS、ArcGIS 服务
  5. 自定义数据提供者:实现 IProvider 接口
  6. 瓦片数据源:在线瓦片和 MBTiles 离线瓦片
  7. 坐标系统处理:使用 ProjectingProvider 进行投影转换
  8. 数据加载策略:同步、异步和分页加载

在下一章中,我们将深入学习 Mapsui 的样式系统。

6.10 思考与练习

  1. 实现一个从 PostgreSQL/PostGIS 读取数据的自定义 Provider。
  2. 创建一个支持增量加载的数据提供者。
  3. 实现 WMS GetFeatureInfo 功能。
  4. 比较不同数据源的性能差异。
  5. 设计一个支持多数据源切换的图层。
posted @ 2026-01-08 14:09  我才是银古  阅读(15)  评论(0)    收藏  举报