Leaflet【六】绘制交互图形、测量、经纬度展示

本文主要探讨了如何利用leaflet-draw插件在地图上绘制图形,以及通过leaflet-measure测量距离和面积,并将经纬度绘制到地图上。首先,我们使用leaflet-draw插件,该插件提供了一种简单而直观的方式来绘制各种形状(如点、线、多边形等)到地图上。然后,我们利用leaflet-measure插件,该插件可以测量地图上任意两点之间的距离,以及任意多边形的面积。最后,我们将经纬度数据绘制到地图上,以便于进行地理位置分析和可视化。这种方法为地理信息的收集、分析和可视化提供了一种有效的工具。

绘制图形

shell 复制代码
npm i leaflet-draw

简单使用

js 复制代码
import '@luomus/leaflet-draw/dist/leaflet.draw';
import '@luomus/leaflet-draw/dist/leaflet.draw.css';

const drawnItems = new L.FeatureGroup();
map.addLayer(drawnItems);
const drawControl = new L.Control.Draw({});
map.addControl(drawControl);

配置项说明

在L.Control.Draw当中可以添加一下配置项

配置项 说明
position 所处位置,取值:
draw 绘制时配置
edit 编辑时配置

其中在draw当中可以对点线面矩形圆灯进行对应的配置,部分配置如下:

其中对于这些配置在源码当中 L.Draw.Polyline 定义了对应的这些对象,所有的配置在option当中,下面只是稍微列举了一部分,源码位置在:node_modules/@luomus/leaflet-draw/dist/leaflet.draw-src.js

js 复制代码
const drawControl = new L.Control.Draw({
  // 位置
  position: 'topright',
  // 绘制时候的配置
  draw: {
    polyline: {
      shapeOptions: {
        stroke: true,
        color: '#3388ff',
        weight: 4,
        opacity: 0.5,
        fill: false,
        clickable: true
      }
    },
    polygon: {
      allowIntersection: false,
      drawError: {
        color: '#f40',
        message: '请点击别的位置的点'
      },
      shapeOptions: {
        stroke: true,
        color: '#3388ff',
        weight: 4,
        opacity: 0.5,
        fill: false,
        fillColor: null,
        fillOpacity: 0.2,
        clickable: true
      }
    },
    circle: {},
    rectangle: {
      shapeOptions: {
        clickable: false
      }
    },
    marker: {
      // icon: new MyCustomMarker()
    }
  },
  // 编辑
  edit: {
    featureGroup: drawnItems
  }
});

同样的在插件当中还提供了一个修改绘制配置的方法setDrawingOptions,传递的是一个对象,和draw当中配置的对象是一样的。

js 复制代码
drawControl.setDrawingOptions({
  rectangle: {
    shapeOptions: {
      color: '#0000FF'
    }
  }
});

中英文转换

在上面初始化了一个绘制对象之后,会发现展示的内容都是英文的,这对我们国内使用不是很方便,所以将其改为中文。这里直接把所有的都给替换掉了,将这段插入代码内即可。是因为在插件源码当中也是定义了一个L.drawLocal = {} 然后他里面都是英文,我们在外面重新定义的会直接取进行一个覆盖。

js 复制代码
L.drawLocal = {
  draw: {
    toolbar: {
      // #TODO: this should be reorganized where actions are nested in actions
      // ex: actions.undo  or actions.cancel
      actions: {
        title: '取消绘图',//'Cancel drawing',
        text: '取消'//'Cancel'
      },
      finish: {
        title: '完成绘图',//'Finish drawing',
        text: '完成'
      },
      undo: {
        title: '删除最后绘制的点',//'Delete last point drawn',
        text: '撤销'//'Delete last point'
      },
      buttons: {
        polyline: '绘制一个多段线',//'Draw a polyline',
        polygon: '绘制一个多边形',//'Draw a polygon',
        rectangle: '绘制一个矩形',//'Draw a rectangle',
        circle: '绘制一个圆',//'Draw a circle',
        marker: '绘制一个标记',//'Draw a marker',
        circlemarker: '绘制一个圆形标记'//'Draw a circlemarker'
      }
    },
    handlers: {
      circle: {
        tooltip: {
          start: '单击并拖动以绘制圆'//'Click and drag to draw circle.'
        },
        radius: 'Radius'
      },
      circlemarker: {
        tooltip: {
          start: '单击"地图"以放置圆标记'//'Click map to place circle marker.'
        }
      },
      marker: {
        tooltip: {
          start: '单击"地图"以放置标记'//'Click map to place marker.'
        }
      },
      polygon: {
        tooltip: {
          start: '单击开始绘制形状',//'Click to start drawing shape.',
          cont: '单击继续绘制形状',//'Click to continue drawing shape.',
          end: '单击第一个点关闭此形状'//'Click first point to close this shape.'
        }
      },
      polyline: {
        error: '<strong>错误:</strong>形状边缘不能交叉!',//'<strong>Error:</strong> shape edges cannot cross!',
        tooltip: {
          start: '单击开始绘制线',//'Click to start drawing line.',
          cont: '单击以继续绘制线',//'Click to continue drawing line.',
          end: '单击"最后一点"以结束线'//'Click last point to finish line.'
        }
      },
      rectangle: {
        tooltip: {
          start: '单击并拖动以绘制矩形'//'Click and drag to draw rectangle.'
        }
      },
      simpleshape: {
        tooltip: {
          end: '释放鼠标完成绘图'//'Release mouse to finish drawing.'
        }
      }
    }
  },
  edit: {
    toolbar: {
      actions: {
        save: {
          title: '保存更改',//'Save changes',
          text: '保存'//'Save'
        },
        cancel: {
          title: '取消编辑,放弃所有更改',//'Cancel editing, discards all changes',
          text: '取消'//'Cancel'
        },
        clearAll: {
          title: '清除所有图层',//'Clear all layers',
          text: '清除所有'//'Clear All'
        }
      },
      buttons: {
        edit: '编辑图层',//'Edit layers',
        editDisabled: '无可编辑的图层',//'No layers to edit',
        remove: '删除图层',//'Delete layers',
        removeDisabled: '无可删除的图层'//'No layers to delete'
      }
    },
    handlers: {
      edit: {
        tooltip: {
          text: '拖动控制柄或标记以编辑要素',//'Drag handles or markers to edit features.',
          subtext: '单击"取消"撤消更改'//'Click cancel to undo changes.'
        }
      },
      remove: {
        tooltip: {
          text: '单击要删除的要素'//'Click on a feature to remove.'
        }
      }
    }
  }
};

事件

leaflet-draw插件的事件都是注册到map对象上的,到这里,可以了解一下Evented 事件

js 复制代码
// map.fire 也就是 触发指定类型的事件。您可以选择提供一个数据对象------侦听器函数的第一个参数将包含其属性,事件可以选择性地传播到事件父级。
this._map.fire(L.Draw.Event.CREATED, {layer: layer, layerType: this.type});

也就是相当于自定义事件了,在他插件源码中相当于定义了一个map的L.Draw.Event.CREATED事件,然后在引入插件之后就可以通过map去使用这个事件了。

  • 由于前面通过fire将事件传播过来,并且传递过来了一个对象,这里通过一个event去接收
  • 同时也就是为什么会有event.layerType和event.layer这两个值,对于绘制不同面可以去取不同的值,拿到值之后还可以将这个layer图层给添加到前面定义好的一个drawnItems,之后的修改也是修改这里面的layer。
js 复制代码
map.on(L.Draw.Event.CREATED, function (event) {
  console.log(' =====', event);
  // 作为永久存储 取这几个值给后端存着,后面再拿出来进行渲染
  console.log('类型 =====', event.layerType);
  console.log('坐标 =====', event.layer._latlngs || event.layer._latlng);
  console.log('配置 =====', event.layer.options); // 圆的半径在配置当中
  const layer = event.layer;
  drawnItems.addLayer(layer);
});

事件说明

事件名 说明 反参
L.Draw.Event.CREATED 创建完成 layer、layerType
L.Draw.Event.EDITED 编辑完成 layers
L.Draw.Event.DELETED 删除 layers
L.Draw.Event.DRAWSTART 开始绘制 layerType
L.Draw.Event.DRAWSTOP 停止绘制 layerType
L.Draw.Event.DRAWVERTEX 绘制顶点 layers
L.Draw.Event.EDITSTART 开始编辑 handler
L.Draw.Event.EDITMOVE 编辑移动 layer
L.Draw.Event.EDITRESIZE 编辑缩放 layer
L.Draw.Event.EDITVERTEX 编辑顶点 layers、poly
L.Draw.Event.EDITSTOP 完成编辑 handler
L.Draw.Event.DELETESTART 开始删除 handler
L.Draw.Event.DELETESTOP 完成删除 handler
L.Draw.Event.TOOLBAROPENED 点击了图标,触发绘制
L.Draw.Event.TOOLBARCLOSED 取消绘制触发
L.Draw.Event.MARKERCONTEXT 右键单击标记 marker,layer,poly

测量

用来测量某两个或多个点之间的距离以及所围成的面积。

js 复制代码
// 先执行npm按照插件
npm i leaflet-measure

import 'leaflet-measure/dist/leaflet-measure';
import 'leaflet-measure/dist/leaflet-measure.css';

const measureControl = new L.Control.Measure({
  position: 'topright',
  primaryLengthUnit: 'feet',
  secondaryLengthUnit: 'miles',
  activeColor: '#F40',
  completedColor: '#F40',
  popupOptions: {className: 'leaflet-measure-resultpopup', autoPanPadding: [10, 10]}
});
measureControl.addTo(map);

插件的配置说明

属性 说明
position 插件按钮所处位置,左上、右下等等
primaryLengthUnit 主要单位
secondaryLengthUnit 次要单位
activeColor 主动执行测量时渲染的地图要素的基色
completedColor 测量完成之后渲染的颜色
popupOptions 弹出框配置,可以额外设置class类名进行自定义样式

经纬度标识

可以使用插件,插件可以在github,插件:leaflet.latlng-graticule 上进行下载获取,之后将插件引入,然后通过L.latlngGraticule进行实例化插件

js 复制代码
// 引入刚才下载的插件js
import './plugin/leaflet.latlng-graticule';

L.latlngGraticule({
  weight: '2.0',
  color: '#f40',
  fontColor: '#fff',
  opacity: 1,
  showLabel: true,
  dashArray: [5, 5],
  sides: ['北', '南', '东', '西'],
  zoomInterval: [
    {start: 2, end: 3, interval: 30},
    {start: 4, end: 4, interval: 10},
    {start: 5, end: 7, interval: 5},
    {start: 8, end: 18, interval: 1}
  ]
}).addTo(map);

插件的配置

属性 说明
weight number 表示
color 颜色值 表示经纬度线的颜色
fontColor 颜色值 表示经纬度文字的颜色
opacity number 表示透明度
showLabel boolean 是否显示经纬度文字
dashArray Array[number] 表示经纬度线的分隔程度
sides Array[string] 表示四个方向展示的内容
zoomInterval 展示层级 地图在第start到第end层级时,每interval度画一条经纬线

插件源码解析

首先可以参考一下Leaflet【三】图层组 & geoJson & 热力图 这篇文章,里面有对leaflet-heat插件的源码分析,这里同时简单看一下这个绘制经纬度的源码。

初始化对象,他是从layer基类当中继承而来。这里面有两个属性,一个是includes这个下面说一下,另外一个是options,也就是对这个插件的配置对象。

includes是Class基类当中的一个属性,是一个特殊的类属性,它将所有指定的对象合并到类中(这样的对象被称为mixins。)理解成vue2当中的混入就好了。那么这里就是通过继承L.Evented基类或L.Mixin.Events,该类获得了事件处理功能

js 复制代码
L.LatLngGraticule = L.Layer.extend({
  includes: (L.Evented.prototype || L.Mixin.Events),
  options:{}  // 配置,这里省略了
});

initialize初始化,这里面没什么,就是将传递过来的options配置重新设置一下,以及将配置组装给到对应的样式。

js 复制代码
initialize: function (options) {
  L.setOptions(this, options);
  // 。。。。样式组装,省略
}

onAdd方法,添加图层到map上会执行,这个方法基本上都大同小异,先初始化canvas,将canvas加到html当中,然后把需要监听的事件都监听上,最后绘制。这个内容有点多,简单概括一下。

  • 首先拿到canvas的宽高以及地图的当前层级,然后根据zoomInterval当中的start和end和地图层级比较拿到对应的interval
  • 已当前地图层级时6为例,这个时候拿到的interval是5,那么绘制的时候会从canvas左上开始绘制,取对应的经纬度,然后往右往下进行绘制,每过了5度就绘制下一条破折线
  • 并且绘制的时候将对应的文本一起绘制上去,这样也就完成了绘制
  • 之后的地图移动事件会重新执行_reset方法,那么也就会重新__redraw。
js 复制代码
onAdd: function (map) {
  this._map = map;

  if (!this._canvas) {
    this._initCanvas();
  }

  map._panes.overlayPane.appendChild(this._canvas);

  map.on('viewreset', this._reset, this);
  map.on('move', this._reset, this);
  map.on('moveend', this._reset, this);

  if (map.options.zoomAnimation && L.Browser.any3d) {
    map.on('zoomanim', this._animateZoom, this);
  }

  this._reset();
},

直接看他绘制的方法,这里的initcanvas初始化一个canvas对象,然后_reset会将canvas大小设置一下,然后执行__draw绘制方法。

js 复制代码
__draw: function (label) {
  function _parse_px_to_int(txt) {
    if (txt.length > 2) {
      if (txt.charAt(txt.length - 2) == 'p') {
        txt = txt.substr(0, txt.length - 2);
      }
    }
    try {
      return parseInt(txt, 10);
    } catch (e) {
    }
    return 0;
  };

  var self = this,
    canvas = this._canvas,
    map = this._map,
    curvedLon = this.options.lngLineCurved,
    curvedLat = this.options.latLineCurved;

  if (L.Browser.canvas && map) {

    var latInterval = this._currLatInterval,
      lngInterval = this._currLngInterval;


    // 获取canvas对象,设置对应的宽高样式等
    var ctx = canvas.getContext('2d');
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.lineWidth = this.options.weight;
    ctx.strokeStyle = this.options.color;
    ctx.fillStyle = this.options.fontColor;
    ctx.setLineDash(this.options.dashArray);

    if (this.options.font) {
      ctx.font = this.options.font;
    }
    var txtWidth = ctx.measureText('0').width;
    var txtHeight = 12;
    try {
      var _font_size = ctx.font.trim().split(' ')[0];
      txtHeight = _parse_px_to_int(_font_size);
    } catch (e) {
    }

    var ww = canvas.width,
      hh = canvas.height;

    var lt = map.containerPointToLatLng(L.point(0, 0));
    var rt = map.containerPointToLatLng(L.point(ww, 0));
    var rb = map.containerPointToLatLng(L.point(ww, hh));

    var _lat_b = rb.lat,
      _lat_t = lt.lat;
    var _lon_l = lt.lng,
      _lon_r = rt.lng;

    var _point_per_lat = (_lat_t - _lat_b) / (hh * 0.2);

    _point_per_lat = _point_per_lat < 1 ? 1 : _point_per_lat;

    _lat_b = _lat_b < -90 ? -90 : parseInt(_lat_b - _point_per_lat, 10);

    _lat_t = _lat_t > 90 ? 90 : parseInt(_lat_t - _point_per_lat, 10);

    var _point_per_lon = (_lon_r - _lon_l) / (ww * 0.2);

    _point_per_lon = _point_per_lon < 1 ? 1 : _point_per_lon;

    if (_lon_l > 0 && _lon_r < 0) {
      _lon_r += 360;
    }
    _lon_r = parseInt(_lon_r + _point_per_lon, 10);
    _lon_l = parseInt(_lon_l - _point_per_lon, 10);

    var ll, latstr, lngstr, _lon_delta = 0.5;

    function __draw_lat_line(self, lat_tick) {
      ll = self._latLngToCanvasPoint(L.latLng(lat_tick, _lon_l));
      latstr = self.__format_lat(lat_tick);
      txtWidth = ctx.measureText(latstr).width;
      var spacer = self.options.showLabel && label ? txtWidth + 10 : 0;

      if (curvedLat) {
        if (typeof (curvedLat) == 'number') {
          _lon_delta = curvedLat;
        }

        var __lon_left = _lon_l, __lon_right = _lon_r;
        if (ll.x > 0) {
          var __lon_left = map.containerPointToLatLng(L.point(0, ll.y));
          __lon_left = __lon_left.lng - _point_per_lon;
          ll.x = 0;
        }
        var rr = self._latLngToCanvasPoint(L.latLng(lat_tick, __lon_right));
        if (rr.x < ww) {
          __lon_right = map.containerPointToLatLng(L.point(ww, rr.y));
          __lon_right = __lon_right.lng + _point_per_lon;
          if (__lon_left > 0 && __lon_right < 0) {
            __lon_right += 360;
          }
        }

        ctx.beginPath();
        ctx.moveTo(ll.x + spacer, ll.y);
        var _prev_p = null;
        for (var j = __lon_left; j <= __lon_right; j += _lon_delta) {
          rr = self._latLngToCanvasPoint(L.latLng(lat_tick, j));
          ctx.lineTo(rr.x - spacer, rr.y);

          if (self.options.showLabel && label && _prev_p != null) {
            if (_prev_p.x < 0 && rr.x >= 0) {
              var _s = (rr.x - 0) / (rr.x - _prev_p.x);
              var _y = rr.y - ((rr.y - _prev_p.y) * _s);
              ctx.fillText(latstr, 0, _y + (txtHeight / 2));
            } else if (_prev_p.x <= (ww - txtWidth) && rr.x > (ww - txtWidth)) {
              var _s = (rr.x - ww) / (rr.x - _prev_p.x);
              var _y = rr.y - ((rr.y - _prev_p.y) * _s);
              ctx.fillText(latstr, ww - txtWidth, _y + (txtHeight / 2) - 2);
            }
          }

          _prev_p = {x: rr.x, y: rr.y, lon: j, lat: i};
        }
        ctx.stroke();
      } else {
        var __lon_right = _lon_r;
        var rr = self._latLngToCanvasPoint(L.latLng(lat_tick, __lon_right));
        if (curvedLon) {
          __lon_right = map.containerPointToLatLng(L.point(0, rr.y));
          __lon_right = __lon_right.lng;
          rr = self._latLngToCanvasPoint(L.latLng(lat_tick, __lon_right));

          var __lon_left = map.containerPointToLatLng(L.point(ww, rr.y));
          __lon_left = __lon_left.lng;
          ll = self._latLngToCanvasPoint(L.latLng(lat_tick, __lon_left));
        }

        ctx.beginPath();
        ctx.moveTo(1 + spacer, ll.y);
        ctx.lineTo(rr.x - 1 - spacer, rr.y);
        ctx.stroke();
        if (self.options.showLabel && label) {
          var _yy = ll.y + (txtHeight / 2) - 2;
          ctx.fillText(latstr, 0, _yy);
          ctx.fillText(latstr, ww - txtWidth, _yy);
        }
      }
    };

    if (latInterval > 0) {
      for (var i = latInterval; i <= _lat_t; i += latInterval) {
        if (i >= _lat_b) {
          __draw_lat_line(this, i);
        }
      }
      for (var i = 0; i >= _lat_b; i -= latInterval) {
        if (i <= _lat_t) {
          __draw_lat_line(this, i);
        }
      }
    }

    function __draw_lon_line(self, lon_tick) {
      lngstr = self.__format_lng(lon_tick);
      txtWidth = ctx.measureText(lngstr).width;
      var bb = self._latLngToCanvasPoint(L.latLng(_lat_b, lon_tick));
      var spacer = self.options.showLabel && label ? txtHeight + 5 : 0;

      if (curvedLon) {
        if (typeof (curvedLon) == 'number') {
          _lat_delta = curvedLon;
        }

        ctx.beginPath();
        ctx.moveTo(bb.x, 5 + spacer);
        var _prev_p = null;
        for (var j = _lat_b; j < _lat_t; j += _lat_delta) {
          var tt = self._latLngToCanvasPoint(L.latLng(j, lon_tick));
          ctx.lineTo(tt.x, tt.y - spacer);

          if (self.options.showLabel && label && _prev_p != null) {
            if (_prev_p.y > 8 && tt.y <= 8) {
              ctx.fillText(lngstr, tt.x - (txtWidth / 2), txtHeight + 5);
            } else if (_prev_p.y >= hh && tt.y < hh) {
              ctx.fillText(lngstr, tt.x - (txtWidth / 2), hh - 2);
            }
          }

          _prev_p = {x: tt.x, y: tt.y, lon: lon_tick, lat: j};
        }
        ctx.stroke();
      } else {
        var __lat_top = _lat_t;
        var tt = self._latLngToCanvasPoint(L.latLng(__lat_top, lon_tick));
        if (curvedLat) {
          __lat_top = map.containerPointToLatLng(L.point(tt.x, 0));
          __lat_top = __lat_top.lat;
          if (__lat_top > 90) {
            __lat_top = 90;
          }
          tt = self._latLngToCanvasPoint(L.latLng(__lat_top, lon_tick));

          var __lat_bottom = map.containerPointToLatLng(L.point(bb.x, hh));
          __lat_bottom = __lat_bottom.lat;
          if (__lat_bottom < -90) {
            __lat_bottom = -90;
          }
          bb = self._latLngToCanvasPoint(L.latLng(__lat_bottom, lon_tick));
        }

        ctx.beginPath();
        ctx.moveTo(tt.x, 5 + spacer);
        ctx.lineTo(bb.x, hh - 1 - spacer);
        ctx.stroke();

        if (self.options.showLabel && label) {
          ctx.fillText(lngstr, tt.x - (txtWidth / 2), txtHeight + 5);
          ctx.fillText(lngstr, bb.x - (txtWidth / 2), hh - 3);
        }
      }
    };

    if (lngInterval > 0) {
      for (var i = lngInterval; i <= _lon_r; i += lngInterval) {
        if (i >= _lon_l) {
          __draw_lon_line(this, i);
        }
      }
      for (var i = 0; i >= _lon_l; i -= lngInterval) {
        if (i <= _lon_r) {
          __draw_lon_line(this, i);
        }
      }
    }
  }
},
相关推荐
GIS思维6 小时前
ArcGIS定义投影与投影的区别(数据和底图不套合的原因和解决办法)
arcgis·gis·地理信息·arcgis坐标系·动态投影
西凉河的葛三叔8 小时前
vue3+elementui-plus el-dialog全局配置点击空白处不关闭弹窗
前端·vue3·elementui-plus
xiaohuatu8 小时前
CSRF保护--laravel进阶篇
vue3·laravel·csrf
duansamve17 小时前
WebGIS地图框架有哪些?
javascript·gis·openlayers·cesium·mapbox·leaflet·webgis
moonless02221 天前
【GISer精英计划_01】GIS的生态位与基石
gis
WineMonk2 天前
ArcGIS Pro ADGeoProcessing DAML
arcgis·gis·arcgis pro sdk·daml
兔子小姐_3 天前
实验十三 生态安全评价
arcgis·gis
半度℃温热5 天前
ERROR TypeError: AutoImport is not a function
webpack·vue3·element-plus·插件版本·autoimport
朝阳396 天前
vue3 中直接使用 JSX ( lang=“tsx“ 的用法)
vue3·tsx·jsx
xcLeigh7 天前
VUE3实现简洁的特色美食网站源码
前端·源码·vue3