代码改变世界

深入解析:用C#采用Avalonia+Mapsui在离线地图上插入图片画信号扩散图

2025-12-27 09:47  tlnshuju  阅读(0)  评论(0)    收藏  举报

Mapsui 对 Avalonia 的完整支持,以及 MBTiles 离线瓦片、Raster/Vector 图层、内存位图图层等能力整理而成,全部步骤均可离线完成,无需联网请求在线瓦片。

在 Avalonia 中使用 Mapsui 4.X 和 5.0 绘制圆形时,核心差异集中在 API 设计、渲染逻辑、性能优化 三个层面,5.0 版本对图形绘制进行了重构,更贴合现代 UI 框架的设计理念,同时修复了 4.X 的部分限制。以下是具体差异对比及迁移指南:

一、核心差异总览

对比维度Mapsui 4.X(Avalonia)Mapsui 5.0(Avalonia)
绘制核心 API依赖 MapControl 的 Layers + 自定义 ILayer新增 ShapeLayer + 强类型 Circle 几何对象
坐标处理方式需手动转换屏幕坐标与地理坐标内置地理坐标直接支持,自动投影转换
样式配置分散在 SymbolStyle/LineStyle 中,配置繁琐统一的 ShapeStyle,支持填充、边框、透明度统一配置
性能表现大量自定义绘制易引发重绘卡顿优化了渲染管线,支持硬件加速,批量绘制更高效
灵活性需手动实现圆形算法(如多边形逼近)内置高精度圆形渲染,支持半径动态调整
兼容性兼容 Avalonia 0.10.x,不支持 .NET 6+ 新特性适配 Avalonia 11.x+,支持 .NET 6/7/8,API 更稳定

  • 二、开发环境准备

    • 安装 .NET 6+ SDK 与 Avalonia MVVM 模板

    • 新建 Avalonia 项目后,NuGet 引入 Mapsui.Avalonia(同时自动引入 SkiaSharp 渲染引擎)[10]

    • 若需要离线底图,再安装 Mapsui.Extensions(含 MBTiles、Raster 数据源支持)[6]


一、准备离线底图
可选方案(按常用程度排序):
a) MBTiles:提前用 TileMill、Maperitive 或 QGIS 把区域瓦片导出为 .mbtiles 文件(SQLite 单文件),拷贝到项目或用户目录即可。
b) GeoTIFF/GeoPackage:Mapsui 的 RasterSource 可直接读取本地影像,适合小范围高精度底图。
c) 自定义文件夹瓦片:将 XYZ 结构文件夹放在本地磁盘,通过 LocalFileTileProvider 加载。
路径只需应用有读取权限,部署时随行携带即可,实现真 · 离线。


三、初始化 Mapsui 控件与离线层

  • 在窗口或用户控件中置入 <mapsui:MapControl Name="mapCtl"/>

  • 代码端新建 Map 实例,先添加离线底图层:
    – MBTiles 层:new TileLayer(new MBTilesSource("Data/mymap.mbtiles", 0, 14))
    – 影像层:new RasterLayer(new RasterSource("Data/area.tif"))

  • 设置 mapCtl.Map = map; 完成底图加载。

    private async Task InitializeMap()
    {
        // 获取MapControl实例
        var mapControl = this.FindControl("map");
        if (mapControl != null)
        {
                // 创建地图
                try
                {
                    await Task.Run(() =>
                    {
                        var connectionString = new SQLite.SQLiteConnectionString("E:\\MySolution\\UAV\\create_mbtiles\\roadmap.mbtiles", false); // 替换为MBTiles文件路径
                        var tileSource = new BruTile.MbTiles.MbTilesTileSource(connectionString);
                        // 创建图层并添加到地图
                        var layer = new TileLayer(tileSource)
                        {
                            Name = "Offline Map",
                            Enabled = true, // 确保图层启用,
                        };
                        var map = new Map();
                        //var map = new Map
                        //{
                        //    CRS = "EPSG:3857", // 设置地图的坐标系统为 Web Mercator
                        //};
                        map.Layers.Add(layer);
                        mapControl.Map = map;
                    });
                //await Task.Delay(1000);
                await Dispatcher.UIThread.InvokeAsync(() =>
                    {
                        zoom = 13;
                        // 转换经纬度到地图的坐标系统
                        var point = new MPoint(87.62444, 43.830763); // 经度在前,纬度在后
                        var transformedPoint = SphericalMercator.FromLonLat(point.X, point.Y);
                        mapControl.Map.Navigator.CenterOn(transformedPoint.x, transformedPoint.y);
                        mapControl.Map.Navigator.ZoomToLevel(zoom);
                        //mapControl.Map.Navigator.ZoomTo(zoom); // 设置合适的缩放级别
                        TextTitle1.Text = string.Format("{0}-{1}", mapControl.Map.Navigator.Viewport.Resolution, mapControl.Map.Navigator.Viewport.Rotation);
                        TextTitle.Text = zoom.ToString();
                        //    //SetMapViewToUrumqi(mapControl);
                        //    //AddMarkersToMap(mapControl);
                        //    //SetupMapInteractions(mapControl);
                        mapControl.Refresh();
                    });
                }
                catch (System.Exception ex)
                {
                    Console.WriteLine($"设置地图视图失败: {ex.Message}");
                }
        }
    }

  1. 生成“信号扩散”位图
    目标是把计算后的场强栅格变成一张带透明通道的 PNG,再叠加到地图。

    • 在内存建 WriteableBitmap(Avalonia 位图)或 SKBitmap(Skia 原生),分辨率可同可视区域,也可预生成 512/1024 等二次方尺寸。

    • 按经纬度→像素坐标转换(用 Mapsui 的 SphericalMercator 类即可)后,对每个像素采样距离、衰减模型,算出 dBm→颜色梯度,写入 RGBA。

    • 完成后得到一张透明底 PNG,中心强、边缘透明,扩散效果完成。


  1. 把位图作为图层叠加
    Mapsui 允许将任意 SKBitmap 包装成 MemoryLayerImageLayer

    • 创建 RasterLayer(new RasterSource(bitmap, 覆盖范围Envelope))

    • 将该层插入到 map.Layers.Insert(1, signalLayer),处于底图之上、注记之下。

    • 若后续需要移动/更新,只需重新生成位图并替换该层,调用 RefreshGraphics 即可实时刷新。

    /// 
    /// 圆扩散效果
    /// 
    /// 
    private async Task AddCircle()
    {
        // 扩散效果参数
        var minScale = 1.0; // 最小缩放值
        var maxScale = 20; // 最大缩放值(扩散范围)
        var scaleStep = 0.5; // 每次递增的缩放步长
        var alphaStep = 5; // 透明度递减步长
        var currentScale = minScale;
        var currentAlpha = 100; // 初始透明度(0-255)
        // 创建用于绘制图形的内存图层
        var graphicsLayer = new MemoryLayer("GraphicsLayer");
        var mapControl = this.FindControl("map")?.Map;
        mapControl?.Layers.Add(graphicsLayer); // 添加到地图
        var point = new MPoint(87.62444, 43.830763); // 经度在前,纬度在后
        var transformedPoint = SphericalMercator.FromLonLat(point.X, point.Y);
        // 添加圆形
        var circleFeature = new PointFeature(transformedPoint.x, transformedPoint.y); // 圆形中心坐标(x,y)
        var pulseStyle = new SymbolStyle
        {
            SymbolType = SymbolType.Ellipse,
            Outline = new Pen(Color.Red, 2), // 红色边框
            Fill = new Brush(Color.FromArgb(currentAlpha, 255, 0, 0)), // 红色填充
            SymbolScale = 10 // 初始大小
        };
        circleFeature.Styles.Add(pulseStyle);
        //这个值一定不能放在前面,否则会出错
        SymbolStyle.DefaultWidth = 20;   // 覆盖默认宽度
        SymbolStyle.DefaultHeight = 20;  // 覆盖默认高度
        // 2. 上层:图片(通过缩放和偏移与圆形对齐)
        var imageStyle = new ImageStyle
        {
            Image = new Mapsui.Styles.Image
            {
                Source = "file://E:/MySolution/UAV/UAVSolution/UAVMonitor/Assets/images/disturb.png"
            },
            SymbolScale = 1.0, // 图片缩放(确保适配圆形大小)
            //RelativeOffset = new RelativeOffset(1, 1) // 图片中心与圆形中心对齐
        };
        circleFeature.Styles.Add(imageStyle);
        // 创建要素并添加样式(先添加圆形,再添加图片,确保图片在上方)
        //var combinedFeature = new PointFeature(transformedPoint.x, transformedPoint.y);
        //combinedFeature.Styles.Add(pulseStyle); // 先添加圆形(底层)
        //combinedFeature.Styles.Add(imageStyle);  // 后添加图片(上层)
        // 将图形添加到图层
        graphicsLayer.Features = new List { circleFeature };
        graphicsLayer.FeaturesWereModified(); // 通知图层数据已更新
        // 创建定时器(每50ms更新一次)
        var timer = new Timer(50);
        timer.Elapsed += (sender, e) =>
        {
            // 跨线程更新UI(如果是WPF/WinForms需要Dispatcher)
            Dispatcher.UIThread.InvokeAsync(() =>
            {
                // 更新缩放值
                currentScale += scaleStep;
                // 更新透明度(随扩散逐渐变淡)
                currentAlpha -= alphaStep;
                // 超出范围后重置
                if (currentScale > maxScale || currentAlpha <= 0)
                {
                    currentScale = minScale;
                    currentAlpha = 100;
                }
                // 更新样式的SymbolScale和透明度
                pulseStyle.SymbolScale = currentScale;
                pulseStyle.Fill.Color = Color.FromArgb(currentAlpha, 255, 0, 0);
                pulseStyle.Outline.Color = Color.FromArgb(currentAlpha, 255, 0, 0);
                // 通知图层数据变更并刷新地图
                graphicsLayer.FeaturesWereModified();
                mapControl?.Refresh();
            });
        };
        // 启动定时器
        timer.Start();
    }

  1. 坐标与范围同步

    • 扩散图需要知道“世界范围”——在生成位图时记录左下、右上经纬度,构造 Envelope 赋给 RasterSource,Mapsui 会自动缩放/平移到正确位置。

    • 当用户平移缩放地图时,无需重绘位图,只要范围不变就不必重新生成;若信号源或强度变化,则后台重新渲染并替换图层。


  1. 性能与体验优化

    • 大区域高分辨率栅格可先裁切到可视区大小,减少像素运算。

    • 多个信号源可合并到同一张位图,减少图层数量。

    • 若需要动画“雷达扩散”,可定时(200 ms)生成递增半径的位图并替换,利用 Skia 的 GPU 加速渲染依旧流畅。

    • 对底图做层级缓存(Mapsui 默认已做),缩放时先显示低清瓦片,停止后再清析高清瓦片,避免卡顿。


  1. 部署与打包

    • MBTiles/影像/程序集一并发布,路径可用 AppContext.BaseDirectory 拼接,确保桌面、移动端都能定位。

    • Mapsui 基于 SkiaSharp,在 Windows、Linux、macOS、Android、iOS 均可直接运行,无需额外依赖。