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

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 依赖 MapControlLayers + 自定义 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; 完成底图加载。

cs 复制代码
    private async Task InitializeMap()
    {
        // 获取MapControl实例
        var mapControl = this.FindControl<MapControl>("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 即可实时刷新。

cs 复制代码
    /// <summary>
    /// 圆扩散效果
    /// </summary>
    /// <returns></returns>
    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<MapControl>("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<Mapsui.Layers.PointFeature> { 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 均可直接运行,无需额外依赖。

相关推荐
Aevget42 分钟前
MyEclipse全新发布v2025.2——AI + Java 24 +更快的调试
java·ide·人工智能·eclipse·myeclipse
云中飞鸿1 小时前
C#类:将Get/Set方法放在一起
c#
Yue丶越1 小时前
【C语言】自定义类型:结构体
c语言·开发语言
合作小小程序员小小店1 小时前
桌面开发,点餐管理系统开发,基于C#,winform,sql server数据库
开发语言·数据库·sql·microsoft·c#
一 乐1 小时前
购物|明星周边商城|基于springboot的明星周边商城系统设计与实现(源码+数据库+文档)
java·数据库·spring boot·后端·spring
笃行客从不躺平1 小时前
线程池监控是什么
java·开发语言
星轨初途1 小时前
C++的输入输出(上)(算法竞赛类)
开发语言·c++·经验分享·笔记·算法
y1y1z1 小时前
Spring框架教程
java·后端·spring
曾经的三心草2 小时前
基于正倒排索引的Java文档搜索引擎3-实现Index类-实现搜索模块-实现DocSearcher类
java·python·搜索引擎