用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 均可直接运行,无需额外依赖。

相关推荐
苹果醋3几秒前
JAVA设计模式之策略模式
java·运维·spring boot·mysql·nginx
君莫愁。1 分钟前
【Unity】相机与UI的自适应
ui·unity·c#·游戏引擎
千寻技术帮4 分钟前
10370_基于Springboot的校园志愿者管理系统
java·spring boot·后端·毕业设计
Rinai_R4 分钟前
关于 Go 的内存管理这档事
java·开发语言·golang
咸鱼加辣5 分钟前
【python面试】你x的启动?
开发语言·python
聆风吟º6 分钟前
【Spring Boot 报错已解决】彻底解决 “Main method not found in class com.xxx.Application” 报错
java·spring boot·后端
木易 士心8 分钟前
数字身份的通行证:深入解析单点登录(SSO)的架构与艺术
java·大数据·架构
tyatyatya13 分钟前
MATLAB图形交互教程:鼠标拾取/坐标轴交互/动态绘图实战详解
开发语言·matlab·计算机外设
乐茵lin14 分钟前
golang中 Context的四大用法
开发语言·后端·学习·golang·编程·大学生·context
wasp52022 分钟前
AgentScope深入分析-设计模式与架构决策分分析
开发语言·python·agent·agentscope