Leaflet 非地理地图实战:使用 SimpleCRS 构建游戏星图与平面坐标系
Leaflet 非地理地图实战使用 SimpleCRS 构建游戏星图与平面坐标系【免费下载链接】Leaflet JavaScript library for mobile-friendly interactive maps 项目地址: https://gitcode.com/gh_mirrors/le/Leaflet在地理信息领域地图通常意味着经纬度坐标系下的真实地表。但 Leaflet 的坐标参考系统CRS是可替换的通过内置的SimpleCRS你可以让 Leaflet 直接管理平面直角坐标系下的内容——游戏星图、建筑设计图、扫描件、自定义网格等。本文以 Leaflet 仓库官方教程《Non-geographical Maps》为主线逐步讲解如何用SimpleCRSImageOverlay构建一张完整的游戏星图并深入剖析SimpleCRS在源码层面的实现原理帮助你彻底避开像素与地图单位混淆坐标顺序颠倒这两大经典陷阱。什么是非地理地图并不是所有地图都描绘地球表面。很多时候地图指的是大幅扫描图片例如游戏地图。这类图片没有地理意义上的经度和纬度概念但依然需要一个坐标系来定位其中的元素。本教程选用的是经典游戏 Star Control II现已开源为 The Ur-Quan Masters 项目的星图。这张星图由读取开源数据文件的工具生成画面如下仔细观察可以发现图片四角带有游戏内置的方形网格坐标刻度这为我们建立自定义坐标系提供了依据游戏内置了一套规整的方形坐标系这正是SimpleCRS擅长处理的场景以简单方格为单位、无地理投影、y 轴可保持从下往上的常规数学方向。认识 CRS 与 SimpleCRSCRS是 Coordinate Reference System坐标参考系统的缩写地理学家用它来说明坐标向量中的数字到底代表什么。同样的[15, 60]在地球的经纬度体系中是印度洋上的一个点而在我们的星图中它可能是 Krueger-Z 星系。Leaflet 中的一张地图有且仅有一个 CRS并且只能在创建地图时指定。对游戏地图而言我们使用SimpleCRS——一个代表方形网格的坐标系统const map new LeafletMap(map, { crs: SimpleCRS });然后通过ImageOverlay把星图整张铺上去并给出它大约的边界范围const bounds [[0,0], [1000,1000]]; const image new ImageOverlay(uqm_map_full.png, bounds).addTo(map);最后用fitBounds让地图视角自动缩放到能容纳整个边界map.fitBounds(bounds);完整的可运行示例见 crs-simple-example1.md。但这个例子有一个明显问题执行fitBounds()之后我们看不到整张地图。这是使用SimpleCRS时遇到的第一个经典坑。陷阱一为什么看不到全图——负的 minZoom在 Leaflet 默认的EarthCRS中缩放级别 0 时经度 360 度被映射为 256 个水平像素约 170 度纬度被映射为 256 个垂直像素。也就是说整个世界在缩放级别 0 下只有 256x256 像素。而在SimpleCRS中1 个水平地图单位被映射为 1 个水平像素垂直方向同理。这意味着我们的星图大约是 1000x1000 像素大小——如果容器的minZoom不允许缩小到更低级别1000 像素宽的图就无法完整塞进通常只有几百像素宽的 HTML 容器。解决办法是允许minZoom取负值const map new LeafletMap(map, { crs: SimpleCRS, minZoom: -5 });从源码角度看缩放级别与比例尺的关系定义在 SimpleCRS.js 中static scale(zoom) { return 2 ** zoom; }即scale(zoom) 2^zoom。当zoom -5时比例尺约为 0.03125整张 1000x1000 单位的星图在屏幕上缩小到约 31x31 像素自然就能完整显示。作为对比CRS基类的默认scale是256 * 2^zoom见 CRS.js这就是地球类 CRS 在缩放级别 0 时世界只有 256 像素的由来。注意在 Map.js 中minZoom的默认值是undefined即继承自图层或回退为 0因此想让SimpleCRS地图缩得足够小必须显式设置负的minZoom。陷阱二像素不等于地图单位使用SimpleCRS时另一个常见误区是默认地图单位 图片像素。在本例中地图覆盖 1000x1000 个单位但星图原图实际是 2315x2315 像素。不同的项目可能要求 1 像素 1 地图单位、64 像素 1 地图单位或任意比例。请始终以地图单位为思考基准来建立网格坐标系再据此添加ImageOverlay、Marker等图层。事实上我们使用的图片覆盖的范围远不止 1000 个地图单位——图片四周还有相当宽裕的留白。通过测量 0 与 1000 坐标刻度之间相隔多少像素再向外推可以得到这张图更精确的边界const bounds [[-26.5,-25], [1021.5,1023]]; const image new ImageOverlay(uqm_map_full.png, bounds).addTo(map);顺带添加一个标记并设置初始视野const sol new LatLng([ 145, 175.2 ]); new Marker(sol).addTo(map); map.setView( [70, 120], 1);完整示例见 crs-simple-example2.md。陷阱三这不是你要找的 LatLng——[y, x] 顺序细心的读者会发现太阳Sol的坐标写成了[145, 175.2]而不是[175.2, 145]地图中心同理。在SimpleCRS中坐标对的形式是[y, x]而不是[x, y]这与 Leaflet 一贯使用[lat, lng]而非[lng, lat]的惯例一脉相承。用专业术语说Leaflet 偏好使用[northing, easting]北向分量在前、东向分量在后而不是[easting, northing]坐标对中的第一个值指向北即 y 方向第二个值指向东即 x 方向。关于[lng, lat]、[lat, lng]、[x, y]、[y, x]哪种更合理业界争论由来已久且没有统一共识。正是为了避免这种混乱Leaflet 引入了名为LatLng的类而不是含义更容易产生歧义的Coordinate。如果觉得用名为LatLng的东西去装[y, x]坐标难以理解你可以轻松地为它写一个包装函数const yx LatLng; const xy function(x, y) { if (Array.isArray(x)) { // When doing xy([x, y]); return new yx(x[1], x[0]); } return new yx(y, x); // When doing xy(x, y); };这样就能用顺手的[x, y]顺序添加几颗恒星甚至画一条航线const sol xy(175.2, 145.0); const mizar xy( 41.6, 130.1); const kruegerZ xy( 13.4, 56.5); const deneb xy(218.7, 8.3); new Marker( sol).addTo(map).bindPopup( Sol); new Marker( mizar).addTo(map).bindPopup( Mizar); new Marker(kruegerZ).addTo(map).bindPopup(Krueger-Z); new Marker( deneb).addTo(map).bindPopup( Deneb); const travel new Polyline([sol, deneb]).addTo(map);地图看起来与之前几乎一样但代码的可读性明显提升了。完整示例见 crs-simple-example3.md。源码层面的 SimpleCRS 实现解析理解了用法之后我们回到源码看看SimpleCRS究竟做了什么。定义位于 src/geo/crs/SimpleCRS.jsexport class SimpleCRS extends CRS { static projection LonLat; static transformation new Transformation(1, 0, -1, 0); static scale(zoom) { return 2 ** zoom; } static zoom(scale) { return Math.log(scale) / Math.LN2; } static distance(latlng1, latlng2) { const dx latlng2.lng - latlng1.lng, dy latlng2.lat - latlng1.lat; return Math.sqrt(dx * dx dy * dy); } static infinite true; }几个关键点投影LonLat来自 Projection.LonLat.js即等距圆柱投影Plate Carree。它把x直接映射为经度、y直接映射为纬度是平面世界如游戏地图的理想选择注释中明确写着Also suitable for flat worlds, e.g. game maps。其project返回new Point(latlng.lng, latlng.lat)——注意顺序这也正是坐标对是[y, x]这一陷阱的根源。仿射变换new Transformation(1, 0, -1, 0)来自 Transformation.js将点(x, y)变换为(a*x b, c*y d)。这里a1, b0, c-1, d0意味着 x 保持原样而y 被取反——对应注释中y 轴应当反转从下往上的要求即屏幕坐标的 y 向下增长而数学坐标的 y 向上增长二者通过-1系数互相转换。distance为欧几里得距离直接计算平面上的sqrt(dx² dy²)而不是地球 CRS 使用的 Haversine 大圆距离见 EarthCRS.js。这意味着在SimpleCRS中测量两点相距多少地图单位就是最简单的平面几何计算。infinite true坐标空间在两个轴向上都是无界的。在 CRS.js 中getProjectedBounds遇到infinite为真时会直接返回null地图因此可以无限平移、缩放到任意负缩放级别非常适合没有明确世界边界的游戏地图。整个坐标流转链路可以总结为latLngToPoint先经投影把经纬度变成平面坐标再用变换和缩放得到屏幕像素坐标pointToLatLng则执行完全逆向的过程见 CRS.js。理解了这条链路就能明白为什么SimpleCRS地图上 1 个地图单位在缩放级别 0 时恰好等于 1 个屏幕像素。小结通过SimpleCRSLeaflet 可以轻松驾驭任何平面坐标系内容。实践中的三条核心经验是务必为地图设置负的minZoom否则超出容器尺寸的平面图无法完整显示始终以地图单位而非图片像素来思考用精确测量的边界创建ImageOverlay牢记坐标顺序是[y, x]即[lat, lng]必要时用xy()包装函数恢复直觉的[x, y]写法。掌握以上要点你就能把游戏星图、楼层平面图、扫描文档等任意平面内容无缝接入 Leaflet 成熟的地图交互与图层体系。相关的可运行示例与源码均位于本仓库教程主体见 index.md三个分步示例见 crs-simple-example1.md、crs-simple-example2.md、crs-simple-example3.md核心实现见 SimpleCRS.js 与 CRS.js。【免费下载链接】Leaflet JavaScript library for mobile-friendly interactive maps 项目地址: https://gitcode.com/gh_mirrors/le/Leaflet创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →