尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C#上位机集成百度地图:WebBrowser控件实现坐标点标注与双向通信

C#上位机集成百度地图:WebBrowser控件实现坐标点标注与双向通信 坐标点操作是很多C#桌面应用里绕不开的需求尤其是上位机、GIS类工具、设备定位系统。但百度地图官方SDK主要面向Web端和移动端桌面端C#项目调用起来总有那么一层别扭。我自己在多个项目里试过不同方案踩了不少坑之后沉淀出一套比较顺手的做法用WebBrowser控件承载百度地图JavaScript API再通过C#与Js互相调用实现坐标点的设置、读取、标注、回传。这套方式不挑界面框架WinForm和WPF通用而且改动成本低、热更新容易。这篇文章就把完整实现流程拆开来讲包括项目思路、百度地图Key申请、坐标转换、C#与JS双向通信、常见坑点排查全程带上可直接套用的代码。无论你现在写的是设备点位管理系统、巡检工具还是车辆轨迹回放程序这套逻辑都能直接搬过去用。1. 方案选型为什么是WebBrowser加JS API1.1 先想清楚要解决什么问题做地图功能之前先别急着写代码把需求拆清楚能省很多返工时间。坐标点相关功能看起来简单无非就是“地图上放个点、拿回经纬度”但实际落地时会遇到几个关键问题第一个是地图底图从哪里来。自建离线地图成本极高瓦片数据、坐标系转换、路网更新全是坑个人项目和小团队没必要碰。第二个是需要什么交互能力比如点地图取坐标、拖拽标记、缩放浏览、逆地理编码查地址。第三个是这套功能要嵌入到什么架构里是纯WinForm工具、WPF程序还是带网络接口的服务端程序。不同的架构选择的嵌入方式完全不同。我自己的经验是先画一个最简单的用例图或流程清单把“设置坐标点”和“读取坐标点”各自涉及的操作列出来。设置坐标点通常包含新增标记、点击地图取点、输入经纬度定位读取坐标点则包括获取当前标记坐标、批量导出、逆地址解析、坐标转换后存储。把这些操作明确了再选技术方案就清晰得多。1.2 不同嵌入方案的实际对比C#桌面端集成百度地图目前主流有三种做法第一种是调用百度地图Web服务API也就是HTTP接口。在C#里用HttpClient请求地理编码、逆地理编码、路线规划等接口返回JSON后自行处理。这种方式适合不需要可视化地图界面的场景比如后台批量把地址转成坐标、把坐标转成省市区。优点是逻辑简单不依赖浏览器控件调试方便缺点是无法提供交互式地图界面用户不能直观地看到地图上点位分布。第二种是使用百度地图Windows SDK。百度官方一度提供过Windows端SDK但维护更新节奏不稳定而且很多时候SDK还停留在较老的版本功能覆盖不如Web端完整。对于商用项目依赖这种半官方半停滞的SDK风险不小。第三种就是我在实际项目中用得最多的方案在WinForm或WPF窗口里嵌入WebBrowser控件WPF里是WebBrowser或WebView2加载一个本地的HTML页面页面引入百度地图JavaScript API再由C#通过COM互操作调用页面里的JS函数或者让页面里的JS回调C#方法。三种方案的区别我用一个表格给自己做过对比方案界面展示交互能力开发成本维护成本适用场景Web服务API无地图界面仅数据请求低低后台批量处理Windows SDK原生地图较好中高原生重度集成WebBrowserJS API完整地图强中中可视化交互工具实际选型时我的原则是只要有用户在地图上“看”和“点”的需求就选第三种方案。因为我做过的项目中几乎没有一个仅仅满足于“存个坐标”大家都会说“能不能在地图上标出来”而标出来这个动作只有网页地图能很方便地实现。1.3 技术架构和数据流设计我最终确定的架构模型是这样的C#主程序负责业务逻辑和数据管理维护一个坐标点集合嵌入的WebBrowser控件负责地图渲染和用户交互承载的是一个只有几十行代码的本地HTML文件C#与HTML之间建立双向通信通道C#可以调用JS函数给地图下发指令JS也可以在地图事件触发时把结果回传给C#。通信链路的核心是WebBrowser控件的ObjectForScripting属性。C#方面设置一个带有ComVisible特性的类实例JS端通过window.external调用C#公开的方法反向通信用WebBrowser.Document.InvokeScriptC#这边直接像调普通方法一样执行JS函数。数据流上设置坐标点的路径是用户在地图上点击JS监听click事件拿到经纬度调用window.external.OnMapPointSelected回调C#C#把坐标写入一个全局集合同时可以调InvokeScript让地图添加标注。读取坐标点的路径则是C#把已有点位数据通过InvokeScript传给页面页面循环调BMap.Marker添加到地图。整个数据流形成了闭环地图只是表现层真正的数据和业务逻辑都在C#里。这套架构最大的好处是解耦前端只是渲染后端只管数据和业务将来要换地图服务商比如换成高德或天地图只需要改HTML页面里的JS初始化代码C#绝大部分代码都不用动。2. 环境准备从Key申请到测试页面跑通2.1 百度地图开放平台Key申请动手写代码之前需要先去百度地图开放平台lbsyun.baidu.com注册开发者账号并创建应用拿到访问应用AK。这里有几个容易踩的坑点我每次演示给同事看都得强调一遍。创建应用时“应用类型”务必选择“浏览器端”因为我们的Key是给JavaScript API用的。如果选成服务端类型JS调用时会报校验失败。Referer白名单建议先用通配符比如*开发调试阶段最省事等正式上线再收紧到具体域名。另外要注意每个Key有每日免费配额个人开发调试完全够用但如果要做多个项目共用同一个Key建议还是分开申请避免将来一个项目流量异常影响其他项目的正常使用。Key申请成功之后页面会给你一串AK字符串这串字符串就好比门禁卡。别把它硬编码到HTML文件里这个文件很可能被分发到客户机器上一旦泄露别人可以盗刷你的配额。我一般做法是做成可配置项在C#里加载到HTML模板中或者干脆从配置文件读取。2.2 第一个测试页面有了AK之后创建一个简单的HTML测试页面。这个页面先完成地图初始化、显示一个默认坐标点的功能确保Key有效、地图能正常加载然后再和C#做集成。初始化百度地图JavaScript API的方式是引入一个script标签script typetext/javascript srchttps://api.map.baidu.com/api?v3.0ak你的AK/script注意这里有个版本坑v3.0是当前最常用的稳定版本如果项目历史遗留用了v2.0的老接口不要去动它。百度地图JS API的版本升级偶尔会调整参数或回调方式贸然升级可能把原本正常的页面弄坏。地图初始化的最小实现如下!DOCTYPE html html head meta charsetutf-8 / title地图坐标管理/title script typetext/javascript srchttps://api.map.baidu.com/api?v3.0ak你的AK/script style typetext/css html, body, #map { width: 100%; height: 100%; margin: 0; padding: 0; } /style /head body div idmap/div script typetext/javascript var map new BMap.Map(map); var point new BMap.Point(116.404, 39.915); map.centerAndZoom(point, 15); map.enableScrollWheelZoom(true); var marker new BMap.Marker(point); map.addOverlay(marker); /script /body /html这段代码跑通之后说明Key和网络环境都没问题。这里有个很多人忽略的点开发机的浏览器如果开了代理或者系统防火墙设置了限制api.map.baidu.com的域名能通但瓦片图数据一般走maponline相关的子域名可能会被拦截导致底图出不来。遇到这种情况先拿浏览器直接打开这个HTML测试地图能正常出现再继续。2.3 坐标体系基础知识WGS84、GCJ02和BD09做坐标点功能绕不开坐标体系这个概念。国内地图坐标系因为法规和国内测绘政策的原因存在“偏移”的情况这个知识点必须在项目一开始就讲清楚。国际上通用的坐标体系是WGS84GPS设备直接输出的经纬度就是WGS84坐标系。国测局规定国内所有的电子地图必须使用加密后的坐标体系GCJ02也叫火星坐标系这是国测局加密后的结果。百度地图又在此基础上做了一次二次偏移形成BD09坐标系。所以同样一个物理位置在GPS设备上读出来一个经纬度在高德地图上又会显示成另一个经纬度在百度地图上又不一样。直接用百度地图JavaScript API的BMap.Point创建的坐标点是BD09坐标。而我们的设备或上位机读取到的GPS坐标通常是WGS84。如果拿着WGS84的坐标直接在地图上显示会出现几十米到几百米不等的偏移在城市楼宇密集区尤其明显。设置坐标点前必须先做坐标转换这个我在第5章会详细讲转换算法和C#实现。HTML测试页面运行正常后项目的第一步就算完成了。接下来进入C#集成阶段。3. C#与百度地图核心交互双向通信打通3.1 WebBrowser控件初始化WinForm和WPF添加WebBrowser控件的方法不同但初始化逻辑相差不大。我是以WinForm为例演示WPF项目用WebBrowser控件时把Init方法绑定到Loaded事件即可。WebBrowser控件默认IE内核版本可能比较低这会导致百度地图API某些特性无法支撑。IE11以上的环境才比较稳妥。设置方法是通过注册表修改FEATURE_BROWSER_EMULATION值将当前进程的IE内核版本强制指定为11或更高。网上有各种改注册表的方式但最简单稳定的是在程序启动时写注册表public static void SetWebBrowserEmulation() { string exeName Process.GetCurrentProcess().ProcessName; string keyPath Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION; using (RegistryKey key Registry.CurrentUser.CreateSubKey(keyPath)) { key.SetValue(exeName .exe, 11001, RegistryValueKind.DWord); } }数值11001代表IE11 Edge模式也可用11000。很多开发新手在这里踩坑地图加载出来白屏或报“对象不支持该属性或方法”多半是内核版本太低。WebBrowser控件初始化还涉及一个silent模式设置可以用反射把ActiveX插件的脚本错误提示关掉否则页面JS有小错误时总是弹出一个难看的错误框打断用户体验。这个设置网上有现成代码反射设置到webBrowser.ActiveXInstance的Silent属性即可。接着是双向通信的基础设置。C#类需要加[ComVisible(true)]特性然后把实例赋给WebBrowser的ObjectForScripting属性[ComVisible(true)] public class JsBridge { // JS调用C#的方法写在这里 } this.webBrowser.ObjectForScripting bridgeInstance; this.webBrowser.ScriptErrorsSuppressed true; this.webBrowser.AllowWebBrowserDrop false; this.webBrowser.IsWebBrowserContextMenuEnabled false;这里需要强调ScriptErrorsSuppressed这个属性。它设为true之后JS报错不会弹框但也会让真正的错误被隐藏调试变得困难。我个人做法是开发阶段设为false发布时再改成true。项目后期如果发现地图操作没反应先把这行改回false看有没有JS报错这步排查能解决80%的交互问题。3.2 C#调用JS方法设置坐标点C#到JS的通路用InvokeScript。这个方法的签名基本是InvokeScript(string scriptName, params object[] args)第一个参数是JS里的函数名后面的数组是传给JS的参数。在地图场景中最常用的一个C#到JS的调用是“设置地图中心点并添加标记”。HTML页面里先定义一个初始化标记的函数function setMapPoint(lng, lat, markerName) { var point new BMap.Point(lng, lat); map.centerAndZoom(point, 16); map.clearOverlays(); var marker new BMap.Marker(point); marker.setTitle(markerName); map.addOverlay(marker); }C#端调用public void SetMapPoint(double lng, double lat, string showName) { string jsCode string.Format(setMapPoint({0}, {1}, {2}), lng.ToString(CultureInfo.InvariantCulture), lat.ToString(CultureInfo.InvariantCulture), showName.Replace(, \\)); webBrowser.Document.InvokeScript(eval, new object[] { jsCode }); }这里有个细节很多新手会踩InvokeScript第一个参数传函数名后参数数组里的类型必须是字符串或数值如果传double类型在某些系统区域设置下会出问题因为double.ToString()默认会带上当前区域的小数点分隔符。有些区域设置逗号作为小数点拼出来的JS字符串就是setMapPoint(116,404, 39,915)直接语法错误。所以拼接参数时用CultureInfo.InvariantCulture保证英文小数点。这是我做国际化项目时踩过的坑印象极其深刻。InvokeScript有两个变体一个是传入函数名和参数对象数组如上面eval那样另一个是传入函数名加参数后执行指定的方法。使用eval拼接整个JS语句是最灵活的方式但要注意JS注入问题——参数里不能携带可疑代码。对于坐标点名称这种用户输入字段替换掉单引号和反斜杠基本就安全了。3.3 JS调用C#方法读取坐标回传JS到C#的通路走的是window.external。凡是在[ComVisible(true)]类上公开的方法JS都能直接调用。我在处理“地图点击获取坐标”这个功能时定义的回调方法签名是这样的[ComVisible(true)] public class JsBridge { public void OnMapClick(string lng, string lat) { double.TryParse(lng, NumberStyles.Float, CultureInfo.InvariantCulture, out double resultLng); double.TryParse(lat, NumberStyles.Float, CultureInfo.InvariantCulture, out double resultLat); if (OnPointSelected ! null) { OnPointSelected(resultLng, resultLat); } } public void OnMarkerDragEnd(string lng, string lat) { // 标记拖动结束回调 } }对应的事件和订阅逻辑写在主窗体public event Actiondouble, double OnPointSelected;JS端处理地图点击事件的写法map.addEventListener(click, function(e) { var lng e.point.lng; var lat e.point.lat; window.external.OnMapClick(lng.toString(), lat.toString()); });注意此时C#里的OnMapClick方法会像一个普通方法一样被调用但JS传递的是字符串类型。C#端需要解析字符串为double而且建议统一用InvariantCulture解析因为JS的(lng).toString()始终是用英文小数点输出和区域设置无关。反过来C#传JS时用InvariantCulture格式化两边就不会因为区域设置出现鸡同鸭讲的情况。这里有一个值得注意的问题window.external调用C#方法是同步阻塞的。如果C#方法内部执行时间过长比如去数据库查询、写日志、调用远程接口页面JS线程会卡住。我见过同事在回调方法里直接写了个同步的网络请求结果页面点击地图后整个界面卡了十几秒用户还以为是程序崩溃了。最佳实践是回调里只做轻量级操作比如暂存坐标、更新UI耗时操作另开线程处理。3.4 WPF项目中的差异说明WPF项目的WebBrowser控件在底层也是IE内核所以前面说的注册表设置同样适用。最大的区别是WPF的WebBrowser不是WPF原生的依赖属性体系因此不能直接绑定数据。通常的做法和WinForm基本一样仍然使用ObjectForScripting和InvokeScript。另外WPF的WebBrowser有一个特性它本身是独立于WPF布局系统之外的窗口句柄控件所以在缩放、动画、透明方面会受限。如果项目对UI体验要求比较高建议考虑升级到WebView2控件它是基于Chromium内核的现代方案性能更好C#和JS之间的互操作也更流畅。但WebView2的API和WebBrowser的ObjectForScripting机制不同需要额外学习成本。我建议先把WebBrowser这套跑通确认业务逻辑没问题再决定要不要迁移到WebView2迁移时业务逻辑层代码可以复用很大一部分。4. 坐标点管理的完整实现设置、读取、展示与导出4.1 坐标点实体的数据建模代码框架搭好之后接下来做实际的业务功能。做任何功能之前先想数据模型这里我定义坐标点实体类的时候考虑了三个维度坐标本身的经纬度、坐标对应的地址描述、业务上需要的附加信息比如编号、名称、类型。一个简化但够用的坐标点实体public class MapPointModel { public string Id { get; set; } public string Name { get; set; } public double Longitude { get; set; } public double Latitude { get; set; } public string Address { get; set; } public DateTime CreateTime { get; set; } public string LngLatText { get { return string.Format({0:F6}, {1:F6}, Longitude, Latitude); } } }经纬度字段建议用double类型存数据库时保留6位小数对应的精度大约是0.11米完全满足定位需求再多的小数位也没有实际意义。坐标点名称和ID字段是业务联动的关键比如一个点位对应一个设备设备ID就是这个坐标点的Id这样才能实现从点到设备、从设备到点的双向查询。4.2 批量添加坐标点到地图实际项目里往往不是只加一个点而是加载一个点集合。比如巡检系统启动时要把今天所有需要巡检的设备点位都标注到地图上。HTML页面里定义批量加载函数function loadPoints(pointList) { map.clearOverlays(); var points JSON.parse(pointList); for (var i 0; i points.length; i) { var item points[i]; var point new BMap.Point(item.lng, item.lat); var marker new BMap.Marker(point); marker.setTitle(item.name); map.addOverlay(marker); // 给标记绑定单击事件 (function(marker, item) { marker.addEventListener(click, function() { window.external.OnMarkerClick(item.id, item.name, item.lng, item.lat, item.address); }); })(marker, item); } }C#端传数据最优雅的方式是把List序列化成JSON字符串然后传给JS的loadPoints函数。这里需要注意JSON序列化时日期字段的格式以及中文是否被转义的问题。用Newtonsoft.Json的默认配置就行JS解析JSON字符串没有任何障碍。private void ShowPointsOnMap(ListMapPointModel points) { string json JsonConvert.SerializeObject(points.Select(p new { id p.Id, name p.Name, lng p.Longitude, lat p.Latitude, address p.Address })); webBrowser.Document.InvokeScript(eval, new object[] { loadPoints( json.Replace(\\, \\\\).Replace(, \\) ) }); }这段代码里有一个必须注意的点把JSON作为字符串嵌入JS代码时JSON里的引号和反斜杠会破坏JS语法轻则模板解析出错重则整个脚本不执行。所以需要对JSON内容做一次转义处理。更稳妥的方案是像上面那样让loadPoints函数接收字符串参数函数内部再用JSON.parse解析这样传递过程中就不需要多层转义只需要处理好最外层单引号的转义即可。4.3 在地图上通过点击设置坐标点设置坐标点最常见的交互是用户在地图上点击某个位置程序在地图上打点同时C#记录这个坐标。这一步的实现涉及第3章讲的双向通信。需要把JS的点击事件和C#的回调串起来并且让事件触发时在地图上显示一个临时标记让用户确认自己的操作生效了。JS端实现var tempMarker null; map.addEventListener(click, function(e) { var point e.point; // 移除之前的临时标记 if (tempMarker) { map.removeOverlay(tempMarker); } // 放置新标记 tempMarker new BMap.Marker(point); map.addOverlay(tempMarker); // 回传坐标给C# window.external.OnMapClick(point.lng.toString(), point.lat.toString()); });C#端在回调事件里做三件事更新界面上显示经纬度的TextBox把坐标数据写入当前编辑对象如果开启了逆地理编码功能调用百度地图Web服务API获取地址名称。拿逆地理编码举个例子C#端可以用HttpClient请求百度Geocoding APIstring url string.Format(https://api.map.baidu.com/reverse_geocoding/v3/?ak{0}location{1},{2}outputjsoncoordtypebd09ll, _ak, lat, lng); using (HttpClient client new HttpClient()) { string result await client.GetStringAsync(url); dynamic json JsonConvert.DeserializeObject(result); string address json.result.formatted_address; // 更新界面 }这个Web服务接口免费版每日配额有限频繁点击地图会很快消耗配额所以我会加一个短时间内的去重机制比如同一个坐标1秒内不重复请求或者把坐标取整后缓存地址减少重复调用。反过来如果需求是精确设置某一个坐标点也可以让用户手动输入经纬度C#调用JS的setMapPoint函数把地图镜头拉过去。4.4 坐标点的读取、编辑与异常处理“读取坐标点”在项目中的含义不仅仅是拿当前经纬度还包括批量导入和导出以及与业务数据的联动。界面上我一般放一个DataGridView或ListView用于展示当前坐标点集合每行包含名称、经纬度、地址、操作按钮。点击某一行C#调用JS把地图定位到对应坐标并高亮这个标记。这个联动逻辑很直白private void dataGridView1_SelectionChanged(object sender, EventArgs e) { if (dataGridView1.CurrentRow ! null) { double lng Convert.ToDouble(dataGridView1.CurrentRow.Cells[Longitude].Value); double lat Convert.ToDouble(dataGridView1.CurrentRow.Cells[Latitude].Value); SetMapPoint(lng, lat, dataGridView1.CurrentRow.Cells[Name].Value.ToString()); } }编辑坐标点有两种方式一是直接在地图上拖动标记拖完后读取新坐标并更新到数据网格和数据库。二是双击表格里的坐标单元格手动输入新的经纬度然后地图标记同步更新。两种方式都需要在坐标保存前做一次合法性校验。合法性校验我总结了三个边界条件经度范围必须在-180到180之间纬度范围必须在-90到90之间非数字和空字符串直接拒绝。有些项目还需要判断坐标点是否落在行政区域内这个需要调用百度地图的行政区域查询接口或预置边界数据看项目需求而定但前两个边界条件是所有项目的基本底线。地图上拖动标记的实现JS端只需要在加载标记时加上enableDragging并监听dragend事件marker.enableDragging(); marker.addEventListener(dragend, function(e) { var point e.point; window.external.OnMarkerDragEnd(point.lng.toString(), point.lat.toString()); });C#端的OnMarkerDragEnd回调也很简单找到当前行更新经纬度列把数据修改状态标记为true。要注意的是如果数据绑定到表格是逐行操作拖完后刷新DataGridView时别把用户当前选中行顶掉否则用户体验很差。4.5 坐标点数据的导出与持久化坐标点数据能不能导出直接决定了工具的可复用性。我在项目里支持三种导出方式复制到剪贴板、导出CSV文件、导出JSON文件。复制功能平时最常用用户在地图上选一个点直接按CtrlC就能把坐标复制出来粘贴到Excel或微信里都很方便。CSV导出用C#自带的StreamWriter即可注意写入BOM头否则用Excel打开UTF-8编码的CSV时中文会乱码using (StreamWriter sw new StreamWriter(filePath, false, new UTF8Encoding(true))) { sw.WriteLine(名称,经度,纬度,地址); foreach (var p in pointList) { sw.WriteLine({0},{1},{2},{3}, p.Name, p.Longitude, p.Latitude, p.Address); } }数据库持久化方面最简单的是SQLite或Access按项目环境决定。如果只是工具型程序也可以直接存本地JSON文件每次启动时加载退出时保存。这种轻量方案对个人工具和小团队内部工具足够用布置起来零成本。5. 坐标转换算法彻底解决百度和GPS坐标偏移5.1 转换的必要性和测试方法拿一台GPS模块输出的坐标WGS84直接放到百度地图上显示可能会发现点位偏了几十米甚至几百米。这是国内坐标系加密造成的系统性问题不是地图API出错了。怎么判断当前的坐标是不是BD09有一个非常简单的测试方法在百度地图上手动搜索一个你熟悉的地标比如公司楼下然后用鼠标点击地标中心把点击获取的经纬度记下来。再拿GPS或手机自带定位多数手机默认输出GCJ02在同一地点读取经纬度两个坐标数值对比如果数值差异在0.001度以上说明坐标系不同需要转换。我建议项目中把坐标转换做成交互式工具一个窗口左边输入WGS84坐标右边显示转换后的BD09坐标再在百度地图上验证。这样大家测试起来非常直观。5.2 坐标转换的C#实现百度地图JavaScript API本身提供了坐标转换接口可以用BMap.Convertor.translate转换单个或批量GPS坐标但它是一个异步调用要在网页里接回调C#这边拿结果还要再走一遍桥接通信链路长、调试费劲。如果C#端需要批量转换WGS84到BD09直接写转换算法更加高效。我整理了已经在多个生产项目中验证过的C#实现可以直接粘贴使用public static class CoordinateConverter { private const double PI 3.1415926535897932384626; private const double A 6378245.0; private const double EE 0.00669342162296594323; private static bool OutOfChina(double lat, double lng) { if (lng 72.004 || lng 137.8347) return true; if (lat 0.8293 || lat 55.8271) return true; return false; } private static double TransformLat(double x, double y) { double ret -100.0 2.0 * x 3.0 * y 0.2 * y * y 0.1 * x * y 0.2 * Math.Sqrt(Math.Abs(x)); ret (20.0 * Math.Sin(6.0 * x * PI) 20.0 * Math.Sin(2.0 * x * PI)) * 2.0 / 3.0; ret (20.0 * Math.Sin(y * PI) 40.0 * Math.Sin(y / 3.0 * PI)) * 2.0 / 3.0; ret (160.0 * Math.Sin(y / 12.0 * PI) 320 * Math.Sin(y * PI / 30.0)) * 2.0 / 3.0; return ret; } private static double TransformLng(double x, double y) { double ret 300.0 x 2.0 * y 0.1 * x * x 0.1 * x * y 0.1 * Math.Sqrt(Math.Abs(x)); ret (20.0 * Math.Sin(6.0 * x * PI) 20.0 * Math.Sin(2.0 * x * PI)) * 2.0 / 3.0; ret (20.0 * Math.Sin(x * PI) 40.0 * Math.Sin(x / 3.0 * PI)) * 2.0 / 3.0; ret (150.0 * Math.Sin(x / 12.0 * PI) 300.0 * Math.Sin(x / 30.0 * PI)) * 2.0 / 3.0; return ret; } private static double[] Wgs84ToGcj02(double lat, double lng) { if (OutOfChina(lat, lng)) { return new double[] { lat, lng }; } double dLat TransformLat(lng - 105.0, lat - 35.0); double dLng TransformLng(lng - 105.0, lat - 35.0); double radLat lat / 180.0 * PI; double magic Math.Sin(radLat); magic 1 - EE * magic * magic; double sqrtMagic Math.Sqrt(magic); dLat (dLat * 180.0) / ((A * (1 - EE)) / (magic * sqrtMagic) * PI); dLng (dLng * 180.0) / (A / sqrtMagic * Math.Cos(radLat) * PI); return new double[] { lat dLat, lng dLng }; } private static double[] Gcj02ToBd09(double lat, double lng) { double z Math.Sqrt(lng * lng lat * lat) 0.00002 * Math.Sin(lat * PI); double theta Math.Atan2(lat, lng) 0.000003 * Math.Cos(lng * PI); double bdLng z * Math.Cos(theta) 0.0065; double bdLat z * Math.Sin(theta) 0.006; return new double[] { bdLat, bdLng }; } public static double[] Wgs84ToBd09(double lat, double lng) { double[] gcj02 Wgs84ToGcj02(lat, lng); return Gcj02ToBd09(gcj02[0], gcj02[1]); } }这个算法本质是从GCJ02和BD09的转换模型反推出来的网上流传很多版本实测精度在1米以内对地图标记来说完全够用。反向转换BD09到WGS84也一样的思路先BD09转GCJ02再GCJ02转WGS84把上面公式里的加减号反过来处理即可。5.3 批量转换与性能考虑批量转换时这个算法每秒可以处理几万组坐标性能完全不是问题。我在上位机项目里曾经一次性导入2万个GPS轨迹点转换加保存几百毫秒就完成了。但需要注意精度问题上面这个转换算法是针对标准WGS84坐标的近似模型。如果是千寻或RTK高精度定位设备输出的差分GPS坐标精度在厘米级别经过这套转换后误差可能扩大。这种情况下建议直接用百度地图坐标转换服务保存一份转换前后的映射表避免反复调用接口产生费用。6. 常见问题与排查技巧实录6.1 地图白屏或加载不出来这是出现频率最高的问题。现象是WebBrowser一片空白打开开发者工具也看不到错误信息因为被ScriptErrorsSuppressed吞掉了。排查步骤按下面顺序走第一步单独用浏览器打开HTML文件看地图是否能正常加载。如果不能检查网络是否能访问api.map.baidu.com以及AK是否有效。第二步确认WebBrowser的IE内核版本用前面说的注册表把FEATURE_BROWSER_EMULATION设置为11001重启程序。第三步检查网页在WebBrowser里的加载路径。如果是file://协议加载本地HTML跨域问题较少如果是加载iframe或远程URL需要检查页面里有没有被安全策略拦截的资源。第四步也是最隐蔽的坑百度地图JS API脚本加载是异步的在页面里可能已经出现了但还没完全初始化。如果在BMap对象还没定义时就调用new BMap.Map()JS会直接抛异常。解决方法是把地图初始化代码放到window.onload事件里或者使用百度提供的initialize回调机制。另外最近几年百度地图JS API有遇到新开的域名和IP无法加载的情况程序发布到不熟悉的客户环境时建议测试第一件事就是看地图底图瓦片能不能加载出来排除DNS或防火墙拦截。6.2 Key验证失败与配额耗尽页面加载时若提示“APP Referer校验失败”多半是Key的Referer白名单配置问题。开发阶段直接用*通配符别在这时候浪费时间配具体域名。如果提示“APP权限被封禁”或“你的AK已失效”检查是否开启了某些只能用于服务端API的权限或者Key过期了。百度地图开放平台的Key默认有效期比较长但商用项目建议每年检查一次。配额耗尽的表现是地图底图能加载但Geocoding、Route等Web服务接口返回APP_QUOTA_LIMIT错误。处理办法是拆分Key地图展示用一个浏览器端KeyWeb服务调用用另一个服务端Key这样地图的访问不会消耗Web服务的配额两者互不干扰。另一个办法是增加缓存把逆地理编码的结果存起来重复请求直接走缓存。6.3 坐标偏移问题坐标偏移的排查通常发现于这样一个场景用GPS设备定位一个点在百度地图上打的标记与实际位置偏差很大看起来像是坐标漂移。首先用第5章的测试方法确认当前坐标体系。如果GPS输出是WGS84地图显示用BD09就必须先做转换。另外要注意一个特殊场景如果GPS输出的坐标已经是被移动设备处理过的GCJ02坐标很多手机的定位SDK会输出火星坐标你再用WGS84转BD09去处理它就会二次偏移。判断方法很简单拿到坐标后先放到在线坐标转换工具里看转换结果如果转换后地图上的位置反而不对了说明原始坐标本身就是GCJ02不需要再走WGS84转换逻辑。这个坑我在对接第三方定位设备时踩过因为是国产GPS模块固件默认输出GCJ02坐标而我按照WGS84处理结果点位总是偏。6.4 双向通信失效或回调不触发表现是C#调InvokeScript没反应或者JS回调window.external没反应。第一种情况是ObjectForScripting没设置或者设置的类没有加ComVisible特性。第二种情况是JS里调用的方法名和C#方法名不一致比如大小写写错。第三种是方法签名不一致C#方法有多个重载或者参数类型不兼容JS动态语言调用时传字符串或数值在COM层转换失败静默失败后没有任何异常提示。排查方法是把ScriptErrorsSuppressed设为false打开看具体错误信息或者在JS里加try-catch把错误信息拼到console再从C#监听ConsoleMessage事件。这需要一点耐心但能快速定位问题。还有一个容易踩的坑页面如果在调试工具里手动刷新过JS重新加载后window.external的绑定关系会重新建立但C#端如果缓存了旧的HTMLDocument对象可能导致直接操作旧DOM时行为异常。一般我写自动化测试脚本时会用InvokeScript(eval, ...)来统一执行JS避免持有多个不同文档对象。6.5 控件复用与多窗口场景如果项目有多个窗口都需要显示地图不要在每个窗口里都放一个WebBrowser再加载一次HTML。这会白白浪费内存而且多个页面同时请求瓦片图会出现底图串流卡顿的问题。我的做法是设计一个单例的地图窗口或地图控件主程序只维护一个WebBrowser实例其他窗口通过消息机制或事件把坐标操作请求发给地图窗口地图窗口统一处理。这样通信链路统一代码维护也更方便。7. 实操心得这个项目做下来我最大的体会是地图功能本身并不复杂复杂的是把地图组件嵌入到原有业务系统时通信和坐标体系带来的各种摩擦。C#和JS的双向通信如果一开始没设计好后面加功能会很痛苦。如果项目规模较大建议把桥接类单独抽象出来定义一套统一的事件模型不要在主窗体里写一堆裸的InvokeScript调用。还有一点想提醒大家先把坐标体系的概念弄清楚再动手这件事能帮你避免后期大部分返工。我以前接过一个同事的半成品项目他看着百度地图示例代码能跑就以为完事了实际GPS坐标直接怼上去偏移了上百米测试人员在现场找点位找了半天后来才发现是坐标系没处理。这就是典型的“最不起眼的地方翻最大的车”。最后一个小技巧这整套方案其实不只是百度地图能用。把HTML页面里的初始化代码换成高德的JS API改用GCJ02坐标系其他所有通信机制、坐标点管理逻辑、导出导入功能全都可以复用。项目后续如果需要在高德和百度之间切换成本非常低只需要替换一个HTML模板文件和坐标转换算法C#主程序几乎零改动。
返回列表