第10章 - 投影与坐标系

第10章:投影与坐标系

10.1 坐标系基础

10.1.1 地理坐标系与投影坐标系

┌─────────────────────────────────────────────────────────────────┐
│                    坐标系统类型                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  地理坐标系 (Geographic CRS)        投影坐标系 (Projected CRS)  │
│  ────────────────────────          ────────────────────────    │
│  • 使用经度和纬度                   • 使用 X 和 Y 坐标          │
│  • 单位:度                         • 单位:米                   │
│  • 例如:WGS84 (EPSG:4326)          • 例如:Web Mercator       │
│                                      (EPSG:3857)               │
│                                                                 │
│      90°                            ┌─────────────┐             │
│       │                             │  Y          │             │
│   ────┼────  经度                   │  ↑          │             │
│       │     (X: -180 到 180)        │  │          │             │
│     -90°                            │  └──→ X     │             │
│      纬度                           └─────────────┘             │
│     (Y: -90 到 90)                   平面坐标                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

10.1.2 常用坐标系

EPSG 代码 名称 描述
EPSG:4326 WGS84 GPS 使用的地理坐标系
EPSG:3857 Web Mercator Google/OSM 使用的投影坐标系
EPSG:4490 CGCS2000 中国国家大地坐标系
EPSG:4214 Beijing 1954 北京54坐标系
EPSG:4610 Xian 1980 西安80坐标系

10.1.3 Mapsui 中的坐标系

// Mapsui 默认使用 Web Mercator (EPSG:3857)
var map = new Map
{
    CRS = "EPSG:3857"  // 默认值
};

// 数据提供者的坐标系
var provider = new MyProvider
{
    CRS = "EPSG:4326"  // 数据源是 WGS84
};

10.2 SphericalMercator 投影

10.2.1 经纬度与 Web Mercator 转换

using Mapsui.Projections;

// 经纬度 → Web Mercator
double lon = 116.4;  // 北京经度
double lat = 39.9;   // 北京纬度

var mercator = SphericalMercator.FromLonLat(lon, lat);
// mercator.X ≈ 12958173
// mercator.Y ≈ 4852832

// Web Mercator → 经纬度
var lonLat = SphericalMercator.ToLonLat(mercator.X, mercator.Y);
// lonLat.X ≈ 116.4
// lonLat.Y ≈ 39.9

10.2.2 坐标转换辅助类

public static class CoordinateConverter
{
    /// <summary>
    /// 经纬度转 Web Mercator
    /// </summary>
    public static MPoint LonLatToMercator(double lon, double lat)
    {
        var (x, y) = SphericalMercator.FromLonLat(lon, lat);
        return new MPoint(x, y);
    }
    
    /// <summary>
    /// Web Mercator 转经纬度
    /// </summary>
    public static (double Lon, double Lat) MercatorToLonLat(double x, double y)
    {
        var (lon, lat) = SphericalMercator.ToLonLat(x, y);
        return (lon, lat);
    }
    
    /// <summary>
    /// MPoint 转经纬度
    /// </summary>
    public static (double Lon, double Lat) MercatorToLonLat(MPoint point)
    {
        return MercatorToLonLat(point.X, point.Y);
    }
    
    /// <summary>
    /// 范围转换:经纬度 → Web Mercator
    /// </summary>
    public static MRect LonLatExtentToMercator(double minLon, double minLat, 
                                                double maxLon, double maxLat)
    {
        var min = SphericalMercator.FromLonLat(minLon, minLat);
        var max = SphericalMercator.FromLonLat(maxLon, maxLat);
        return new MRect(min.X, min.Y, max.X, max.Y);
    }
}

10.2.3 创建不同坐标的要素

// 使用经纬度坐标创建要素(自动转换为 Web Mercator)
public static PointFeature CreateFeatureFromLonLat(double lon, double lat, 
                                                    string name)
{
    var point = CoordinateConverter.LonLatToMercator(lon, lat);
    var feature = new PointFeature(point);
    feature["name"] = name;
    feature["longitude"] = lon;
    feature["latitude"] = lat;
    return feature;
}

// 使用示例
var beijing = CreateFeatureFromLonLat(116.4, 39.9, "北京");
var shanghai = CreateFeatureFromLonLat(121.5, 31.2, "上海");

10.3 ProjectingProvider

10.3.1 使用 ProjectingProvider

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

// 假设数据源使用 WGS84 (EPSG:4326)
var wgs84Provider = new MyWgs84Provider();
wgs84Provider.CRS = "EPSG:4326";

// 地图使用 Web Mercator (EPSG:3857)
var map = new Map { CRS = "EPSG:3857" };

// 使用 ProjectingProvider 包装
var projectingProvider = new ProjectingProvider(wgs84Provider)
{
    CRS = "EPSG:3857"  // 目标坐标系
};

var layer = new Layer
{
    Name = "ProjectedLayer",
    DataSource = projectingProvider
};

map.Layers.Add(layer);

10.3.2 自定义投影转换

Mapsui 默认只支持 WGS84 和 Web Mercator 之间的转换。对于其他坐标系,需要使用 ProjNet 库:

using ProjNet.CoordinateSystems;
using ProjNet.CoordinateSystems.Transformations;

public class CustomProjection : IProjection
{
    private readonly ICoordinateTransformation _transform;
    
    public CustomProjection(string fromCrs, string toCrs)
    {
        var csFactory = new CoordinateSystemFactory();
        var ctFactory = new CoordinateTransformationFactory();
        
        var fromCs = GetCoordinateSystem(fromCrs);
        var toCs = GetCoordinateSystem(toCrs);
        
        _transform = ctFactory.CreateFromCoordinateSystems(fromCs, toCs);
    }
    
    public MPoint Project(string fromCrs, string toCrs, MPoint point)
    {
        var result = _transform.MathTransform.Transform(
            new double[] { point.X, point.Y }
        );
        return new MPoint(result[0], result[1]);
    }
    
    private ICoordinateSystem GetCoordinateSystem(string crs)
    {
        // 根据 CRS 标识符返回坐标系统定义
        // 可以使用 WKT 字符串或 EPSG 代码
        return crs switch
        {
            "EPSG:4326" => GeographicCoordinateSystem.WGS84,
            "EPSG:3857" => ProjectedCoordinateSystem.WebMercator,
            // 添加更多坐标系...
            _ => throw new NotSupportedException($"Unsupported CRS: {crs}")
        };
    }
}

10.4 缩放级别与分辨率

10.4.1 分辨率概念

分辨率(Resolution)表示每个像素代表的地图单位(米):

// 常见缩放级别对应的分辨率(Web Mercator)
public static class ZoomLevelResolutions
{
    // 地球周长约 40075016.68 米
    private const double EarthCircumference = 40075016.68;
    
    // 标准瓦片大小
    private const int TileSize = 256;
    
    /// <summary>
    /// 获取指定缩放级别的分辨率
    /// </summary>
    public static double GetResolution(int zoomLevel)
    {
        return EarthCircumference / (TileSize * Math.Pow(2, zoomLevel));
    }
    
    /// <summary>
    /// 根据分辨率获取缩放级别
    /// </summary>
    public static int GetZoomLevel(double resolution)
    {
        return (int)Math.Round(
            Math.Log(EarthCircumference / (TileSize * resolution)) / Math.Log(2)
        );
    }
    
    // 预计算的分辨率表
    public static readonly double[] Resolutions = new double[]
    {
        156543.03392800014,  // 0 - 世界
        78271.51696400007,   // 1
        39135.75848200003,   // 2
        19567.87924100002,   // 3
        9783.93962050001,    // 4
        4891.96981025,       // 5
        2445.98490513,       // 6
        1222.99245256,       // 7
        611.496226281,       // 8
        305.748113141,       // 9
        152.87405657,        // 10
        76.4370282852,       // 11
        38.2185141426,       // 12
        19.1092570713,       // 13
        9.55462853565,       // 14
        4.77731426782,       // 15
        2.38865713391,       // 16
        1.19432856696,       // 17
        0.597164283478,      // 18 - 街道
        0.298582141739       // 19 - 建筑
    };
}

10.4.2 控制缩放级别

// 设置缩放限制
map.Navigator.ZoomLimits = new MMinMax(
    ZoomLevelResolutions.GetResolution(18),  // 最大缩放(最小分辨率)
    ZoomLevelResolutions.GetResolution(3)    // 最小缩放(最大分辨率)
);

// 缩放到指定级别
public void ZoomToLevel(Map map, int zoomLevel)
{
    var resolution = ZoomLevelResolutions.GetResolution(zoomLevel);
    map.Navigator.ZoomTo(resolution);
}

// 获取当前缩放级别
public int GetCurrentZoomLevel(Map map)
{
    var resolution = map.Navigator.Viewport.Resolution;
    return ZoomLevelResolutions.GetZoomLevel(resolution);
}

10.5 范围与视口

10.5.1 MRect - 矩形范围

// 创建矩形范围
var extent = new MRect(minX, minY, maxX, maxY);

// 从经纬度创建范围
public static MRect CreateExtentFromLonLat(double minLon, double minLat,
                                            double maxLon, double maxLat)
{
    var min = SphericalMercator.FromLonLat(minLon, minLat);
    var max = SphericalMercator.FromLonLat(maxLon, maxLat);
    return new MRect(min.X, min.Y, max.X, max.Y);
}

// 中国范围
var chinaExtent = CreateExtentFromLonLat(73.5, 18.2, 135.0, 53.5);

10.5.2 视口定位

// 定位到指定范围
map.Navigator.ZoomToBox(extent);

// 定位到指定点和缩放级别
var center = CoordinateConverter.LonLatToMercator(116.4, 39.9);
var resolution = ZoomLevelResolutions.GetResolution(12);
map.Navigator.CenterOnAndZoomTo(center, resolution);

// 带动画的定位
map.Navigator.FlyTo(extent, duration: 1000);

// 获取当前视口范围
var currentExtent = map.Navigator.Viewport.Extent;

10.5.3 视口坐标转换

public class ViewportHelper
{
    private readonly Viewport _viewport;
    
    public ViewportHelper(Viewport viewport)
    {
        _viewport = viewport;
    }
    
    /// <summary>
    /// 屏幕坐标转地图坐标(Web Mercator)
    /// </summary>
    public MPoint ScreenToWorld(double screenX, double screenY)
    {
        return _viewport.ScreenToWorld(new MPoint(screenX, screenY));
    }
    
    /// <summary>
    /// 屏幕坐标转经纬度
    /// </summary>
    public (double Lon, double Lat) ScreenToLonLat(double screenX, double screenY)
    {
        var world = ScreenToWorld(screenX, screenY);
        return CoordinateConverter.MercatorToLonLat(world);
    }
    
    /// <summary>
    /// 地图坐标转屏幕坐标
    /// </summary>
    public MPoint WorldToScreen(MPoint worldPoint)
    {
        return _viewport.WorldToScreen(worldPoint);
    }
    
    /// <summary>
    /// 经纬度转屏幕坐标
    /// </summary>
    public MPoint LonLatToScreen(double lon, double lat)
    {
        var world = CoordinateConverter.LonLatToMercator(lon, lat);
        return WorldToScreen(world);
    }
}

10.6 地图比例尺

10.6.1 计算比例尺

public static class ScaleCalculator
{
    /// <summary>
    /// 计算当前地图比例尺
    /// </summary>
    /// <param name="resolution">分辨率(米/像素)</param>
    /// <param name="dpi">屏幕 DPI,默认 96</param>
    /// <returns>比例尺分母(1:X 中的 X)</returns>
    public static double CalculateScale(double resolution, double dpi = 96)
    {
        // 1 英寸 = 0.0254 米
        const double InchToMeter = 0.0254;
        
        // 每英寸的像素数 = DPI
        // 每米的像素数 = DPI / 0.0254
        // 比例尺 = 分辨率 * 每米的像素数
        
        return resolution * dpi / InchToMeter;
    }
    
    /// <summary>
    /// 格式化比例尺显示
    /// </summary>
    public static string FormatScale(double scaleDenominator)
    {
        if (scaleDenominator >= 1000000)
            return $"1:{scaleDenominator / 1000000:F0}M";
        if (scaleDenominator >= 1000)
            return $"1:{scaleDenominator / 1000:F0}K";
        return $"1:{scaleDenominator:F0}";
    }
}

10.6.2 缩放到指定比例尺

public void ZoomToScale(Map map, double scaleDenominator, double dpi = 96)
{
    const double InchToMeter = 0.0254;
    var resolution = scaleDenominator * InchToMeter / dpi;
    map.Navigator.ZoomTo(resolution);
}

// 缩放到 1:10000
ZoomToScale(map, 10000);

10.7 距离计算

10.7.1 Haversine 公式

public static class DistanceCalculator
{
    private const double EarthRadius = 6371000;  // 地球半径(米)
    
    /// <summary>
    /// 使用 Haversine 公式计算两点间的球面距离
    /// </summary>
    public static double HaversineDistance(
        double lat1, double lon1, 
        double lat2, double lon2)
    {
        var dLat = ToRadians(lat2 - lat1);
        var dLon = ToRadians(lon2 - lon1);
        
        var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
                Math.Cos(ToRadians(lat1)) * Math.Cos(ToRadians(lat2)) *
                Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
        
        var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
        
        return EarthRadius * c;
    }
    
    /// <summary>
    /// 计算 Web Mercator 坐标点间的距离
    /// </summary>
    public static double DistanceBetweenMercatorPoints(MPoint p1, MPoint p2)
    {
        var (lon1, lat1) = SphericalMercator.ToLonLat(p1.X, p1.Y);
        var (lon2, lat2) = SphericalMercator.ToLonLat(p2.X, p2.Y);
        
        return HaversineDistance(lat1, lon1, lat2, lon2);
    }
    
    private static double ToRadians(double degrees)
    {
        return degrees * Math.PI / 180;
    }
}

10.7.2 面积计算

public static class AreaCalculator
{
    /// <summary>
    /// 计算多边形面积(使用球面计算)
    /// </summary>
    public static double CalculateSphericalArea(IEnumerable<MPoint> mercatorPoints)
    {
        // 转换为经纬度
        var lonLatPoints = mercatorPoints
            .Select(p => SphericalMercator.ToLonLat(p.X, p.Y))
            .ToList();
        
        return CalculateSphericalAreaFromLonLat(lonLatPoints);
    }
    
    /// <summary>
    /// 使用球面多边形面积公式
    /// </summary>
    private static double CalculateSphericalAreaFromLonLat(
        List<(double X, double Y)> points)
    {
        const double EarthRadius = 6371000;
        
        double area = 0;
        int n = points.Count;
        
        for (int i = 0; i < n; i++)
        {
            int j = (i + 1) % n;
            
            var lon1 = ToRadians(points[i].X);
            var lat1 = ToRadians(points[i].Y);
            var lon2 = ToRadians(points[j].X);
            var lat2 = ToRadians(points[j].Y);
            
            area += (lon2 - lon1) * (2 + Math.Sin(lat1) + Math.Sin(lat2));
        }
        
        return Math.Abs(area * EarthRadius * EarthRadius / 2);
    }
    
    private static double ToRadians(double degrees)
    {
        return degrees * Math.PI / 180;
    }
}

10.8 本章小结

本章详细介绍了 Mapsui 中的投影与坐标系:

  1. 坐标系基础:地理坐标系与投影坐标系的区别
  2. SphericalMercator:经纬度与 Web Mercator 之间的转换
  3. ProjectingProvider:自动坐标系转换
  4. 缩放级别与分辨率:分辨率与缩放级别的关系
  5. 范围与视口:MRect 和视口坐标转换
  6. 地图比例尺:计算和显示比例尺
  7. 距离与面积计算:使用球面公式计算距离和面积

在下一章中,我们将学习瓦片图层与地图服务的使用。

10.9 思考与练习

  1. 实现一个支持 CGCS2000 到 Web Mercator 投影转换的 Provider。
  2. 创建一个坐标拾取工具,显示点击位置的经纬度和 Web Mercator 坐标。
  3. 实现一个动态比例尺显示控件。
  4. 创建一个多边形面积测量工具,显示球面面积。
  5. 实现根据当前视口范围计算显示范围内所有要素的功能。
posted @ 2026-01-08 14:40  我才是银古  阅读(9)  评论(0)    收藏  举报