leaflet小型场景地图开发

最近开发中常遇到一些小场景电子地图的开发,比如一个园区、楼层的平面图,需要展示并在上面标注某设备位置、查看设备信息等。

这类场景不需要gis底图(栅格/矢量瓦片图),不使用经纬度坐标,这似乎不适合用leaflet这类gis库。初时我也并未想到要用它,但其实除了这两点外,其它许多功能用这类gis库都比较合适,比如:信息框不跟随缩放、鼠标标绘、多边形编辑等等。而且其它类型的图形库很少同时包含这几项功能。

leaflet比较轻量,也可以和其它绘图库搭配使用,一般都能满足项目需求。所以使用leaflet.js多数时候更为合适。以下是一个简单的园区电子地图示例

一、主要功能清单

以下是这个示例的主要功能部分。我们先看看功能组成再做分析和实现。

  1. 展示底图:支持使用一张图片作为地图的"底图"。
  2. 显示比例尺:显示比例尺,并且需要可编辑。
  3. 放置设备:要求在地图上放置设备,并可进行编辑(移动、旋转、缩放)
  4. 标绘区域:用户用鼠标绘制区域。
  5. 编辑区域:区域的形状要支持编辑修改。
  6. 展示设备/区域信息:点击设备/区域时展示他们的详细信息。

二、实现

以上第4, 5两项可直接使用leaflet.pm插件实现。 第6点使用leaflet的Popup弹框功能展示即可(因为它们的状态信息不应该跟随缩放而变化大小)。

1, 2, 3点要稍做些处理,以下是按照实现的顺序列出的,这3点功能也包含在其中,详细如下。

1、坐标系选用

leaflet默认使用的是L.CRS.EPSG3857也就是使用经纬度作为坐标,而我们只是一个园区的地图,比较小,使用普通的平面直角坐标系,保存设备、区域的点位即可。设置如下:

js 复制代码
import L from 'leaflet';

const mapCase = L.map('容器元素id', {
  /* L.CRS.Simple是将经度和纬度直接映射为 x和 y。可用于平坦表面的地图*/
  crs: L.CRS.Simple, 
  /*使用L.CRS.Simple 后,将zoom在-2~2之间,默认0级为正常大小*/ 
  zoom: 0,
  minZoom: -2,
  maxZoom: 2,
  //leaflet默认每次缩放变更级别为1,
  //而我们园区地图较小,所以缩放级别的变化调小更合适
  zoomSnap: 0.25, // 滚轮每次缩放变更
  zoomDelta: 0.25, // 控件缩放每次变更
});

2、展示底图

将用户上传的图片 (一张)设置一个固定宽,并按其宽高比例设置为园区底图。设置了底图覆盖范围从[0,0]开始,让坐标系的原点与底图左下角重合

js 复制代码
function getImgSize(imgUrl) {
  const img = new Image();
  const IMG_WIDTH = 1600;

  return new Promise((resolve, reject) => {
    img.onload = () => {
      const ratio = img.width / img.height;
      const scopes = [IMG_WIDTH / ratio, IMG_WIDTH];

      resolve({ width: scopes[1], height: scopes[0],zoom:0 });
    };
    img.onerror = () => reject(null);
    img.src = imgUrl;
  });

}
// 设置底图
const imgSrc = '图片地址或数据';
getImgSize(imgSrc).then(imgObj=>{
    // 第2个参数为覆盖的范围。
    L.imageOverlay(imgSrc, [[0, 0], [imgObj.height, imgObj.width]]).addTo(mapCase);
    // 设置中心和缩放
    mapCase.setView([imgObj.height / 2, imgObj.width / 2], 0);
})

3、使用可编辑的比例尺

因为我们的底图是用户上传,规格不一,所以比例尺需要支持可以编辑。

leaflet自带的比例尺控件是不支持修改其值的,所以这里用扩展的方式实现一个可编辑(值的设置)的比例尺。实现如下:

  1. 单位使用px/m(像素/米);
  2. 固定比例尺控件长度为100px,方便用户编辑时用其在底图上做度量。
  3. 可用输入框形式输入m(米) 的部分。
  4. 暴露切换编辑、查看模式,值的设置、获取几个api。
js 复制代码
import L from 'leaflet';
/**可编辑的比例尺控件**/
function useNewScale(mapCase, option) {
  let baseScaleNum = option.value || 5;
  // 创建一些比例尺控件中的元素
  const inp = L.DomUtil.create('input');
  inp.setAttribute('type', 'number');
  inp.value = baseScaleNum;
  inp.style.cssText = 'width:40px;height:18px;box-sizing:border-box;margin:0 2px;';

  const container = L.DomUtil.create('div');
  container.style.cssText = 'box-sizing:border-box;width:100px;border:1px solid #000;border-top:none;background:#fff;padding:2px;';
  const span1 = L.DomUtil.create('span');
  span1.innerText = '100px/';
  const span2 = L.DomUtil.create('span');
  span2.innerText = baseScaleNum;
  const span3 = L.DomUtil.create('span');
  span3.innerText = 'm';
  // 初始化缩放级数
  const initZoom = () => mapCase.setZoom(0);
  // 更新比例尺视图中的数值
  const updateVal = function() {
    const zoom = mapCase.getZoom();
    const val = (baseScaleNum/Math.pow(2,zoom)).toFixed(2);

    span2.innerText = Number(val);
    inp.value = Number(val);
  };
  // 用户中途离开编辑改变了缩放的情况
  inp.addEventListener('focus',initZoom);

  L.Control.NewScale = L.Control.extend({
    onAdd: function(map) {
      container.appendChild(span1);
      container.appendChild(span2);
      container.appendChild(span3);
      // 监听缩放,修改比例尺数值
      map.on('zoomend', updateVal);

      return container;
    },
    onRemove: function() {
      // Nothing to do here
    },
  });

  L.control.NewScale = function(opts) {
    return new L.Control.NewScale(opts);
  };
  // 比例尺添加到地图
  L.control.NewScale({ position: 'bottomleft' }).addTo(mapCase);

  return {
    // 编辑/查看模式设置
    setMode:(mode) => {
      if (mode === 'edit') {
        initZoom();
        container.contains(span2) && container.replaceChild(inp, span2);
      } else {
        container.contains(inp) && container.replaceChild(span2, inp);
      }
    },
    // 设置新的比例尺数据
    setValue: (val) => {
      baseScaleNum = val;
      updateVal();
    },
    // 获取当前比例尺数据
    getValue:() => Number(inp.value),
  };
}

4、新建设备图层并结合第三方库

我们的设备使用图片来表示,但leaflet支持显示图片的只有svgOverLayer, imageOverlay, mark标记,前两者都不支持编辑,后者编辑只支持移动,缺少我们需要的"旋转、缩放"两项。

结合一个其它的图形库使用是个不错的办法(这里使用antv-x6做示例)。

用leaflet新建一个图层作为antv-x6的容器,使用这个库现成的功能做设备的编辑操作。为什么不用一个新的dom元素覆盖在leaflet容器上来实现呢?因为在进行地图缩放操作时,通过监听leaflet的缩放事件,来控制anvt-x6容器的缩放会比较生硬。

实现如下:

  • 缩放控制 :用leaflet创建一个svgOverLayer图层,图层的svg元素下再创建一个foreignObject元素放置普通的dom元素作为antv-x6实例的容器。利用svg缩放的特性,leaflet缩放时antv-x6容器就可以丝滑的跟随缩放。
  • 交互处理 :leaflet会在svgOverLayer上设置事件穿透 ,所以我们要在antv-x6新建的节点元素上启用事件(pointer-events:auto;),并设置阻止冒泡,这样将两个库的事件隔离开,不会导致交互冲突。
js 复制代码
import { Graph } from '@antv/x6';
// imgObj: 包含底图宽高。
function getDeviceContainer(imgObj) {
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  const foreign = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
  const container = document.createElement('div');
  const _css = `width:${imgObj.width}px;height:${imgObj.height}px;position:absolute;z-index:20;`;

  svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
  svg.setAttribute('viewBox', `0 0 ${imgObj.width} ${imgObj.height}`);
  svg.setAttribute('width', imgObj.width + 'px');
  svg.setAttribute('height', imgObj.height + 'px');

  foreign.setAttribute('width', imgObj.width + 'px');
  foreign.setAttribute('height', imgObj.height + 'px');

  foreign.appendChild(container);
  svg.appendChild(foreign);

  container.style.cssText = _css;
  // 隔绝antv与leaflet的事件影响
  container.onmousedown = function(e) {
    e.stopPropagation();
  };
  return { svg,container };
}
// 使用
const bgObj = {width,height};
const { svg,container:graphEl } = getDeviceContainer(bgObj);
const editControl = (node) => {
  if (!node) return false;
  else return editMap.device.includes(node.id);
};

const graph = new Graph({
  ...其它配置,
  container: graphEl,
  interacting: {
    nodeMovable: true,
  },
  width: bgObj.width,
  height: bgObj.height,
});
// 设置设备元素部分事件可用。
graph.view.viewport.setAttribute('style', 'pointer-events:auto;');

之后设备的添加/编辑功能使用antv-x6来完成即可。

三、总结

文章主要是想表明:即使是"园区、楼层"这种小型地图使用leaflet.js这类gis库也是比较合适的,如果开发中有遇到此类需求可以考虑leaflet.js+其它第三方图形库 开发的方式解决。第三方图形库最好选择svg类型的,这样在缩放时不用做其它处理。以上的一点经验希望对大家有所帮助。

相关推荐
qiyi.sky12 分钟前
JavaWeb——Vue组件库Element(3/6):常见组件:Dialog对话框、Form表单(介绍、使用、实际效果)
前端·javascript·vue.js
煸橙干儿~~16 分钟前
分析JS Crash(进程崩溃)
java·前端·javascript
安冬的码畜日常25 分钟前
【D3.js in Action 3 精译_027】3.4 让 D3 数据适应屏幕(下)—— D3 分段比例尺的用法
前端·javascript·信息可视化·数据可视化·d3.js·d3比例尺·分段比例尺
l1x1n01 小时前
No.3 笔记 | Web安全基础:Web1.0 - 3.0 发展史
前端·http·html
昨天;明天。今天。1 小时前
案例-任务清单
前端·javascript·css
zqx_72 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己3 小时前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称3 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色3 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2344 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js