
简介基于HTML5的室内矢量地图项目面向Web与移动端开发者及室内导航应用设计者解决商场、机场、展馆等地图缩放模糊、交互单一、跨平台适配难的问题。包内共171个文件JS脚本处理地图逻辑与交互JSON/XML存放空间与商铺数据HTML/CSS搭建页面样式PNG/SVG等图标辅助展示另含Map与Python工具压缩包仅2.47MB轻量清晰。目前已有1354人学习使用。开发者可获得完整工程理解Canvas/SVG绘制原理掌握Leaflet或OpenLayers的图层管理、标记与高亮功能并通过拖拽缩放、商铺点击变色等交互体验矢量地图的实用性。组件可嵌入安卓/iOS的WebView为跨平台室内导航提供可复用基础适合WebGIS入门与二次开发参考。1. 室内矢量地图为什么我建议你先搞懂“矢量”这两个字最近总刷到“怎么导出高德地图的路况道路矢量”这类问题提问的人多半不是真想抓数据而是需要一份“能编辑、能计算、能渲染”的路网文件。这个需求放到室外是道路网络放到室内就是商场过道、机场通道、停车场车行线。今天想借这个由头把室内矢量地图从数据采集、格式设计到渲染和导航实现的整条链路聊透让你看完就能直接上手做一个小型项目。先说清楚一件事矢量地图不是截图不是卫片更不是一张CAD图纸转成PDF。矢量地图里的所有元素——墙、门、通道、房间、电梯、扶梯、POI图标——都以点、线、面几何对象存在每个对象还挂了一堆属性字段。比如一面墙是一个Polygon它的字段有“楼层F2”“材质”“耐火等级”一条通道是一条LineString字段有“宽度”“通行方向”“限高”。这就带来两个核心价值一是渲染不糊放大缩小都清晰二是能算能做最短路径、能算面积、能做空间分析。室内导航、应急疏散模拟、资产管理、店铺客流分析底层都依赖这套矢量数据。室内地图和室外地图的差别也不小主要在三处坐标系室外默认用经纬度WGS84或国测局坐标室内多采用独立局部坐标原点常常是图纸左下角单位是米。数据来源室外有卫星影像和公开路网室内是靠CAD图纸、BIM模型、现场测绘硬啃出来的。路网逻辑室外的一条路是二维线网室内需要三维分层楼层之间有楼梯、电梯、扶梯做垂直连接每一层还要做单独的通行网络。这就是室内矢量地图特殊的地方它不是把一张平面图矢量化就完事而是要建立一套“空间语义”让机器知道哪里能走、哪里不能走、楼层之间怎么切换。这篇文章适合三类人参考要做商场或园区室内导航的研发同学要替物业方搭建数字化底座的GIS工程师以及正在做毕设或个人项目、想搞一套室内地图demo的学生。整个链路我会按“数据怎么来—格式怎么定—渲染怎么选—导航怎么算—坑在哪”的顺序讲。2. 数据从哪来不要把地图生产想得太浪漫2.1 先分清室内矢量数据里到底有什么室内矢量地图不是一个孤零零的shp文件它一般由四层要素组成。面要素房间分区、店铺轮廓、中庭、卫生间、消防分区。这些是最底层的“地盘”用来做区域选择、热力统计和权限判断。线要素墙体中心线、通道中心线、导航路网、挡墙、栏杆。线要素是导航的核心很多项目做砸了就是因为路网画得随意走不通、穿墙、断头路一堆。点要素店铺入口点、电梯口、楼梯口、收银台、闸机口、洗手间门、服务台。点要素是POI的锚点导航的起终点就是它们。属性数据楼层号、空间名称、分类编码、开放状态、营业时间、门禁权限。属性才是灵魂没有属性的矢量图只是一堆几何轮廓。2.2 四种主流的数据生产路径第一种也是最常见的CAD图纸转矢量。商场、医院、写字楼运营方手里基本都有竣工图或平面布置图通常是DWG或DXF格式。把图纸拖进QGIS或者FME整理图层把墙体、门窗、房间名分门别类导出来。CAD图纸质量参差不齐经常有重复线、未闭合多边形、块引用需要先清理。我的习惯顺序是先用CAD软件把无关标注、尺寸线、图框全部冻结只保留建筑轮廓和房间分区再另存为DXF导入GIS。第二种BIM模型导出。新建项目或者改造项目如果有Revit模型直接从BIM里导出IFC再转成GeoJSON或者Shapefile。好处是几何干净属性全楼层信息、材质都有坏处是工作量大而且BIM里的“墙”和导航需要的“可通行区域”完全是两回事墙面是实体导航需要的是房间内的空区域中间还得自己抽路网。第三种现场测绘补点。很多老楼没有电子图纸只有纸质图。这种情况我会先用激光测距仪或手持GPS把关键点打下来再用QGIS的“Points to Path”和“Shape Digitize”手动描出来。别指望一次性测量完美后续还要用手机实测导航来校位。第四种从卫星影像或公开地图描图。室外区域可以用OSM数据做底室内区域由于建筑遮挡卫星影像基本没用只能现场看。室外道路数据想合法获取建议走OpenStreetMap下载或当地开放数据平台市区的路网、建筑物轮廓都能拿到虽说精度不一定能直接用于施工级分析但做室内外一体化导航demo足够。2.3 格式和坐标系多花半小时能省三天室内地图项目最不建议一上来就用经纬度。CAD图纸里坐标通常是毫米或米制的平面坐标你硬要转换成WGS84经纬度容易发生几十米的偏移尤其在高纬度地区更夸张。更稳的做法是项目内部统一使用局部平面直角坐标系原点设为建筑角点或图纸左下角单位用米。在做室内外切换时再通过一个“局部坐标→经纬度”的转换函数处理。数据格式方面小型项目直接用GeoJSON最方便浏览器原生解析调试也直观。要素一多、图层一复杂就建议走MBTiles或PMTiles矢量瓦片。属性表我喜欢额外存一份XLSX跟GeoJSON的id字段关联这样运营人员改营业时间、改店铺名不用碰几何数据。楼层字段建议单独设计比如“floor: F2”同时加一个“floor_order”: 2排序的时候不会把F10排到F2前面。注意CAD导出的DXF默认坐标系经常没有定义投影信息导入QGIS前先手动指定坐标系再另存为带投影的GeoPackage否则后面所有叠加分析都会开始漂。3. 技术栈选型室内地图不是随便套一个地图SDK就行3.1 渲染引擎怎么选直接决定你能不能做“矢量渲染”市面上的地图SDK很多但室内场景和室外场景的需求差别很大我按不同项目体量整理了选型对照方案适合场景坐标支持矢量渲染3D/楼层效果上手成本Leaflet L.CRS.Simple小demo、单层平面图局部坐标支持GeoJSON/SVG弱低MapLibre GL JS中大型室内Web应用可自定义变换强原生矢量瓦片支持3D建筑拉伸中Mapbox GL JS需要高质量底图经纬度为主强强中CesiumJSBIM模型、倾斜摄影、楼层级3D世界坐标支持极强高自绘Canvas/SVG极简原型、纯展示自由弱无低如果只是做商场室内导航原型我强烈建议先试Leaflet的自定义坐标系方案。Leaflet默认认为地图是经纬度但你可以在初始化时传crs: L.CRS.Simple把所有坐标当作平面像素/米来处理这样就不需要做任何投影转换直接用CAD导出的局部坐标。MapLibre则适合正式产品它能加载矢量瓦片动态设置建筑高度和楼层样式后期做点击查询、图层显隐都方便。3.2 数据服务和后台小项目别过度设计后台这块我见过太多一上来就搭PostGIS、GeoServer、瓦片服务器的方案结果数据量就几百个多边形服务器配置花了两天纯属自我感动。我的建议是分步走原型阶段把GeoJSON文件放在前端仓库里用Vite或Webpack直接静态加载。修改数据用QGIS导出GeoJSON刷新页面就看效果。数据量变大后换成PostGIS存储前端用GeoTIFF或矢量瓦片接口按需加载。后台可以自己用Node.js写一个简单的Tile接口或者直接用TileServer-GL加载MBTiles。必须上瓦片的时候用tippecanoe把GeoJSON切层成矢量瓦片切之前先做几何简化删除不可见属性一个商场的全量路网点位切成瓦片后往往只有几百KB。导航服务我建议独立出来不要跟地图渲染耦合。路径规划本质是在路网图上做图搜索和“怎么画图”无关。把所有楼层通道中心线合并成一个带楼层字段的图对象用Dijkstra或者A*算法算路径然后返回值是GeoJSON LineString前端拿到后叠加在地图上。这样做的好处是以后换渲染引擎导航逻辑完全不用动。4. 实操从GeoJSON到一张能用的室内导航地图4.1 准备一份最小可用的楼层数据我拿一个商场二层来做示例。假设二层平面是100米×80米的矩形中庭在中间四周是店铺两条主通道。GeoJSON结构大致是这样{ type: FeatureCollection, features: [ { type: Feature, geometry: { type: Polygon, coordinates: [[ [0, 0], [100, 0], [100, 80], [0, 80], [0, 0] ]] }, properties: { id: zone_2f_01, name: 二层全域, floor: F2, floor_order: 2, type: zone } }, { type: Feature, geometry: { type: LineString, coordinates: [[20, 10], [80, 10], [80, 70], [20, 70], [20, 10]] }, properties: { id: path_2f_main, name: 二层主通道, floor: F2, width: 6, direction: both } } ] }这里面的面是区域边界线是路网中心线。实际项目里路网不会只有一条线而是一张互相连接的图每个交叉口都要有节点。节点在GeoJSON里以MultiPoint或者独立的Feature保存properties里带上“node_id”线段的properties里带上“from”和“to”后面导航算法要用。4.2 用Leaflet快速渲染室内图层前端代码不长核心是初始化一个不基于经纬度的地图再直接把GeoJSON甩给它const map L.map(map, { crs: L.CRS.Simple, center: [40, 50], zoom: 1, minZoom: 0, maxZoom: 5 }); fetch(floor2.geojson) .then(res res.json()) .then(data { const layer L.geoJSON(data, { style: feature { if (feature.geometry.type Polygon) { return { color: #e0e0e0, weight: 1, fillOpacity: 0.3 }; } if (feature.geometry.type LineString) { return { color: #3a7afe, weight: 4, opacity: 0.8 }; } return {}; }, onEachFeature: (feature, layer) { layer.bindTooltip(${feature.properties.name}${feature.properties.floor}); } }).addTo(map); map.fitBounds(layer.getBounds(), { padding: [20, 20] }); });L.CRS.Simple的核心逻辑是把坐标直接映射到像素所以你给的数据单位是“米”它就直接当像素用。因为室内地图不涉及经纬度投影这是最不容易出错的渲染方式。如果底图是CAD导出的PNG需要先用L.imageOverlay加载底图再在onAdd里根据图片的实际尺寸设置bounds否则地图和图片是对不齐的。4.3 手写一个最短路径导航逻辑导航算法不复杂但我建议不要在渲染层写死。先把路网抽成一个邻接表const graph { nodeA: { nodeB: 12, nodeC: 8 }, nodeB: { nodeA: 12, nodeD: 20 }, nodeC: { nodeA: 8, nodeD: 15 }, nodeD: { nodeB: 20, nodeC: 15 } };然后跑一个经典Dijkstra。这里的关键是边权不只是几何距离还要考虑楼层切换代价。比如从F2的楼梯口A点走到F3的楼梯口B点垂直边的权重不是0应该按“下楼30秒、等电梯90秒”这种实际体感去设置。很多项目没有做这一步导致导航总是建议用户坐电梯但等电梯可能比走楼梯慢得多。Demojs部分给一个递归实现function dijkstra(graph, start, end) { const dist {}; const prev {}; const visited new Set(); Object.keys(graph).forEach(node { dist[node] Infinity; }); dist[start] 0; while (visited.size Object.keys(graph).length) { const current Object.keys(dist).filter(n !visited.has(n)) .sort((a, b) dist[a] - dist[b])[0]; visited.add(current); if (current end) break; (graph[current] || []).forEach(neighbor { const alt dist[current] neighbor.weight; if (alt dist[neighbor.id]) { dist[neighbor.id] alt; prev[neighbor.id] current; } }); } return { dist, prev }; }实际项目里别用这种O(n²)的写法数据点一多就要上优先队列。但原理是一样的。路径算完后把prev链表还原成坐标点数组生成一条LineString加到地图上顺便用箭头符号渲染方向这就是最基本可用的室内路径引导。4.4 部署时容易忽略的性能点室内地图看起来简单但真实场景容易卡顿。主要原因通常是数据没简化整个楼宇的全部要素一次性塞给前端渲染。对策有三个按楼层懒加载F1、F2、F3分开文件切楼层时再请求而不是一口气load全部。几何简化QGIS里用Simplify工具容差设0.1米把几千个点降到几百个肉眼基本无差别。不要频繁回源上线后用MBTiles或PMTiles做成按需瓦片前端只加载视野范围内的数据。上线部署的话纯静态方案用Nginx托管dist目录就行导航服务单独起一个Node或Go服务。移动端如果想做App用WebView包一层最快但要注意地图手势和原生地图手势的冲突最好把缩放按钮做在Web页面上避免双端协同的复杂度。5. 常见问题、雷区与合法获取数据这件事5.1 室内地图项目容易踩的坑我把实操中反复遇到的问题整理成一张速查表按优先级排的现象真正原因处理方式底图和矢量线错位CAD导出时的原点不一致统一设置数据集原点用同一参考点对楼层数据做平移缩放后线变模糊或变形使用raster底图而非矢量改用矢量瓦片或调整简化参数路径穿墙路网是手描的没有贴墙约束用面要素“可通行区域”做空间裁剪路网必须落在区域内切楼层后POI对不上每层CAD的坐标系偏移用每层唯一的控制点做仿射变换校准导航总是推荐绕路垂直边权重设置不当给楼梯/扶梯/电梯设置真实通行耗时打开页面加载很慢GeoJSON太大做简化、转瓦片、开启gzip手机定位飘得厉害室内没有有效GPS信号别裸用GPS加入蓝牙信标或WiFi指纹方案其中“路径穿墙”是最影响用户信任感的问题。室内导航不像室外导航墙体是硬约束用户看导航路线穿过一堵墙直接就会卸载App。我的经验是在数据生产阶段就建立“可通行区域面”然后强制路网必须从该面内部穿过任何一段线超出了面边界数据质检时全部标红宁缺毋滥。5.2 关于“导出高德路况矢量”的正确姿势这个热搜词背后反映的真实需求是很多人想拿一份现成的道路路况矢量数据来用。但路况数据不是静态文件它是不断变化的实时数据根本不存在“导出一次管终身”的完美文件。想合法获取这类数据目前靠谱的就三条路官方API高德、百度都有交通态势和路径规划API申请开发者Key后按接口文档拉数据。返回结果是路况等级、拥堵指数这类结构不是可以直接编辑的shp文件。想做可视化把返回的折线或坐标点自己落成GeoJSON即可。开放地图数据OpenStreetMap下载路网矢量道路等级、名称、形状都有做统计分析和底图完全够用。缺点是实时路况没有只有静态路网。数据授权合作如果做商业产品需要历史路况或高精度路网直接联系数据服务商签授权合同别自己去抓接口。顺便提醒一句抓接口不仅是服务条款问题而且很容易得到“半残”数据缺字段、缺几何、坐标不统一最后清洗成本反而比买数据还高。合规又省力的做法永远是API或开放数据。5.3 版权问题别等到上线前才想起来室内地图项目的版权风险往往被忽视。CAD图纸通常归建筑方或物业方所有拿来做项目前先确认有没有使用权BIM模型同理。在商场做室内地图商业化必需拿到业主或运营方的授权否则即使你地图做得好也会面临侵权风险。第三方地图平台的数据也不能想用就用商用前要了解是否需要单独申请权限。另一个容易被忽视的是“室内数据保密”。安全等级高的园区或医院室内精确平面图属于敏感资产哪怕是做导航项目也不应在非受控环境中存储和展示。处理这类数据时建议最小化权限、脱敏展示只开放导航所需信息。6. 几个从实战里攒下来的经验做了几个室内地图项目之后我最大的体会是这活儿70%的精力在处理数据而不是写代码。工具链再新如果CAD图纸脏、路网没拓扑、坐标系乱后面渲染和导航全是白折腾。所以我的习惯是第一个里程碑不是做出地图而是做出一张“经过质检的楼层路网图”哪怕UI丑一点都没关系。还有一个小技巧室内地图的POI不要只存“名称”尽量存“类别编码”。比如店铺、洗手间、电梯口、安全出口用编码关联后续做楼层切换筛选、路径规划推荐、无障碍导航都能省不少事。真实项目里只改一个字段就避免了一版需求返工。再分享一个调校坐标的土办法。CAD图纸转了若干次格式后局部坐标经常偏个零点几米肉眼很难发现。每次拿到新一版数据我会在QGIS里把“通道中心线”和“店铺面”叠在底图上用透明度检查贴合度确保路网中心线到店面边界距离不小于设计值。这个检查和洗手间是否标错楼层一样是最low但最有效的地图质量保障手段。做室内矢量地图这件事天花板很高从粗略的楼层平面到三维空间、从静态地图到实时定位、从单一导航到无障通道规划每一步都有扩展空间。但地基始终是那份可靠的数据点要准、线要通、面要闭合属性要全。希望你从这个项目里感受到的不只是“地图能画出来”而是“空间能被读懂”。本文还有配套的精品资源点击获取