OpenLayers使用

最近项目有用到ol来加载地图并进行一些常规的操作,ol的官网以api的格式来写的,而且全英文,对新手来说简直太不友好了(碎碎念)

官网指路:OpenLayers - Welcome

找到一个好网站:OpenLayers5示例

所以写个文章来记录一下,以备不时之需

前言:全篇使用vue3来进行开发,但是核心代码还是js居多,引用不管什么框架都是一样的,因vue自身的响响应式特性,本文章有些函数直接更改了源数据,如果用其他框架,要注意异步问题(比如react函数式开发),小改一下就可以啦~

一、生成地图

ol加载地图有多种方式

首先需要选定一个元素作为挂载对象,注意需要给元素设置高度才能够显示地图

  <div id="modelmap" class="mapBox"></div>

引入需要的类(文章举例的三种地图)

import {
  TileWMS,
TileImage,
  XYZ,
} from "ol/source";
import { fromLonLat, toLonLat, transform } from "ol/proj";
import TileGrid from 'ol/tilegrid/TileGrid.js'

1.TileWMS :来自WMS服务器的图层数据源加载。

const mapref = ref(); //地图实例

//初始化地图
const InitMap = () => {
  let _sourceTile = new Tile({
    source: new TileWMS({
      url: `${localStorage.getItem("mapUrl")}/geoserver/SeaMap/wms`,//地图在服务器上的地址
      params: {//配置请求参数
        LAYERS: "Groups002",//必填
        TILED: true,//是否平铺
        FORMAT: "image/png",//格式
      },
      serverType: "geoserver",//远程WMS服务器的类型:mapserver、geoserver、carmentasserver或qgis。只有当hidpi为真时才需要
      crossOrigin: "anonymous",//加载图像的crosorigin属性
    }),
  });

  mapref.value = new Map({
    target: document.getElementById("modelmap"),//获取到挂在dom
    layers: [_sourceTile],//图层挂载项
    view: new View({
      //视图
      center: transform([110.25, 21.75], "EPSG:4326", "EPSG:3857"),//中心点(此处需要是地图投影)
      zoom: 6,//地图初始缩放层级
    }),
    interactions: defaults({//地图默认事件
      // mouseWheelZoom: false,
      // doubleClickZoom: false,
      // shiftDragZoom: false,
      // dragPan: false,
    }),
    moveTolerance: 1, //光标必须移动的最小距离(以像素为单位)才能被检测为map move事件,而不是单击。
  });
};

onMounted(() => {
  InitMap();
});


2.:加载百度地图(借用TileImage类)

const InitMap = () => {
  let resolutions = [];
  let baiduX, baiduY;

//转换百度地图投影坐标
  for (let i = 0; i < 19; i++) {
    resolutions[i] = Math.pow(2, 18 - i);
  }
//网格加载
  let tilegrid = new TileGrid({
    origin: [0, 0],
    resolutions: resolutions,
  });

  let baidu_source = new TileImage({
    projection: "EPSG:3857",//投影类型
    tileGrid: tilegrid,
    tileUrlFunction: function (tileCoord) {
      if (!tileCoord) return "";
      let z = tileCoord[0];
      let x = tileCoord[1];
      let y = tileCoord[2];
      // 对编号xy处理
      baiduX = x < 0 ? x : "M" + -x;
      baiduY = -y;
      return (
        "http://online3.map.bdimg.com/onlinelabel/?qt=tile&x=" +
        baiduX +
        "&y=" +
        baiduY +
        "&z=" +
        z +
        "&styles=pl&udt=20151021&scaler=1&p=1"
      );
    },
  });

  let baidu_layer = new Tile({
    source: baidu_source,
  });

  mapref.value = new Map({
    target: "modelmap",
    layers: [baidu_layer],
    view: new View({
      projection: "EPSG:3857",
      center: [13531290.967373039, 4675318.865056401],
      zoom: 12,
    }),
  });
};

3.XYZ: 用于具有URL模板中定义的一组XYZ格式的URL的tile数据


const InitMap = () => {
  let _sourceTile = new Tile({
    source: new XYZ({
              url: '地图地址',
              visible: true,
            })
  });

  mapref.value = new Map({
    target: document.getElementById("modelmap"),//获取到挂在dom
    layers: [_sourceTile],//图层挂载项
    view: new View({
      //视图
      center: transform([110.25, 21.75], "EPSG:4326", "EPSG:3857"),//中心点(此处需要是地图投影)
      zoom: 6,//地图初始缩放层级
    }),
    moveTolerance: 1, //光标必须移动的最小距离(以像素为单位)才能被检测为map move事件,而不是单击。
  });
};

二、地图点位加载

引入需要的函数

import Feature from "ol/Feature";
import Point from "ol/geom/Point";
import { Tile, Vector as VectorLayer, Heatmap } from "ol/layer";
import { Circle, Icon, Fill, Stroke, Style, Text } from "ol/style";
import { fromLonLat, toLonLat, transform } from "ol/proj";



数据:

const deviceList=ref([
{lng:113.15036605511219,lat:19.315288421149006,data:'第一个点'}, 
{lng:111.07880399646379,lat:26.00387964670233,data:'第二个点'}, 
])

1.点位加载

1.1通过VectorLayer加载

少量数据时,VectorLayer是最优解


//生成点位
const renderOverlay = () => {
  let iconFeatureArr = deviceList.value.map((v) => {
    var iconFeature = new Feature({
      //feature-矢量集合图形
      geometry: new Point(
        transform([v.lng, v.lat], "EPSG:4326", "EPSG:3857")
      ),//生成集合体的形状是一个点
      data: v, //原数据--将要保存的点位数据存到集合体中,在点位交互的时候可以直拿到
      population: 40000,
      rainfall: 500,
    });
    let iconStyle = new Style({//点位样式
      image: new Icon({
        anchor: [0.5, 0.5],
        //anchorXUnits: "fraction",
        //anchorYUnits: "pixels",
        width: 40,
        height: 40,
        src: '点位图标地址',
      }),
    });
     iconFeature.setStyle(iconStyle);
    return iconFeature;
  });

  let vectorSource = new VectorSource({
    features: iconFeatureArr,
  });

  let vectorLayer = new VectorLayer({
    source: vectorSource,
  });
  //加入点位
  mapref.value.addLayer(vectorLayer);
};

1.2 海量数据点位加载 

如果使用VectorLayer加载,当叠加超过几千以上点位时就开始变慢,一万以上的要素点的时候,浏览器页面就开始卡顿或直接卡死,openlayers官方给出的优化意见是用webgl图层方式进行优化,使用webgl图层优点是渲染大量点很快,但是图标的样式不能根据要素(Feature)的风格样式(style)自定义设置,只能用图层(layer)的风格样式(style)

import WebGLPointsLayer from 'ol/layer/WebGLPoints.js';

//生成点位
const renderOverlay = () => {
  let iconFeatureArr = deviceList.value.map((v) => {
    var iconFeature = new Feature({
      //feature-矢量集合图形
      geometry: new Point(
        transform([v.lng, v.lat], "EPSG:4326", "EPSG:3857")
      ),//生成集合体的形状是一个点
      data: v, //原数据--将要保存的点位数据存到集合体中,在点位交互的时候可以直拿到
      population: 40000,
      rainfall: 500,
    });
    return iconFeature;
  });

 let source = new VectorSource({
    features: iconFeatureArr,
    wrapX: false,
  });
  let pointsLayer = new WebGLPointsLayer({
    source: source,
    style: newStyle,
  });
  mapref.value.addLayer(pointsLayer);
};

const newStyle = {
    symbol: {
      symbolType: 'circle', //图标形状可选值为:circle/triangle/square/image
      size: [
        //大小
        'interpolate',
        ['linear'],
        ['get', 'population'],
        40000,
        8,
        2000000,
        28,
      ],
      color: ['match', ['get', 'hover'], 1, '#ff3f3f', '#006688'], //设置hover值为1时的颜色为:#ff3f3f,默认颜色为:#006688
      rotateWithView: false, //是否随视图旋转
      offset: [0, 0], //偏移
      opacity: [
        //透明度
        'interpolate',
        ['linear'],
        ['get', 'population'],
        40000,
        0.6,
        2000000,
        0.92,
      ],
    },
  };

2.点位聚合

const renderOverlay = (pointList) => {
  let iconFeatureArr = pointList.map((v, index) => {
    let iconFeature = new Feature({
      geometry: new Point(
        transform([v.lng, v.lat], "EPSG:4326", "EPSG:3857")
      ),
      // geometry: new Point(fromLonLat(v.longitudeAndLatitudeDouble)),
      data: v, //原数据
      population: 40000,
      rainfall: 500,
    });
    let iconStyle = new Style({
      image: new Icon({
        anchor: [0.5, 0.5],
        //anchorXUnits: "fraction",
        //anchorYUnits: "pixels",
        width: 40,
        height: 40,
        src: '',
      }),
    });
    iconFeature.setStyle(iconStyle);
    return iconFeature;
  });

  // 矢量要素数据源
  let source = new VectorSource({
    features: iconFeatureArr,
  });

  // 聚合标注数据源
  let clusterSource = new Cluster({
    distance: 12,
    source: source,
  });

  // 加载聚合标注的矢量图层
  let styleCache = {}; //用于保存特定数量的聚合群的要素样式
  let vectorLayer = new VectorLayer({
    source: clusterSource,
    style: function (feature, resolution) {
      let size = feature.get("features").length; //获取该要素所在聚合群的要素数量
      let style = styleCache[size];
      if (size > 1) {
        style = [
          new Style({
            image: new Circle({
              radius: 10,
              stroke: new Stroke({
                color: "#fff",
              }),
              fill: new Fill({
                color: "#3399CC",
              }),
            }),
            text: new Text({
              text: size.toString(),
              fill: new Fill({
                color: "#fff",
              }),
            }),
          }),
        ];
      } else if (size == 1) {
         style = feature.get('features')[0].getStyle()
      }
      styleCache[size] = style;
      return style;
    },
  });
  //加入点位
  mapref.value.addLayer(vectorLayer);

};

聚合后的点位非常消耗性能,如果数据量大的话,亲测会造成卡顿甚至点位消失的情况,想到一个解决方案是,在0-8层级时点位聚和,超过8层级,点位不聚和,这样可以节省一部分计算性能,但是大量点位的话,还是建议做成前后端联动的聚合

加入视图层级判断即可

三、图形绘制 

1.动态图形绘制

Box:矩形;

LineString:折线

Polygon:多边形

Circle:圆形

Point:点

其中矩形需要借助圆形类来实现

先实例化一个矢量图层来作为绘画的涂鸦绘制层,再利用ol内置的Draw类来内进行绘制

import { createBox } from "ol/interaction/Draw";
import { Circle, Icon, Fill, Stroke, Style } from "ol/style";
import { Draw } from "ol/interaction";

const vector=ref()
const drawControls=ref()
//生成绘制层
const initDraw = () => {
  source.value = new VectorSource({ wrapX: false });
  vector.value = new VectorLayer({
    //最终展示的图形样式
    source: source.value,
    style: new Style({
      fill: new Fill({
        color: "rgba(255, 255, 255, 0.2)",
      }),
      stroke: new Stroke({
        color: "#ffcc33",
        width: 10,
      }),
      image: new Circle({
        radius: 7,
        fill: new Fill({
          color: "#ffcc33",
        }),
      }),
    }),
  });
  //将绘制层添加到地图容器中
  mapref.value.addLayer(vector.value);
};

//触发绘制函数
const drawLine = (type) => {
  if (!type) return;
  if (drawControls.value) mapref.value.removeInteraction(drawControls.value); //清除旧画笔
 //绘制矩形
  if (type === "Box") {
    //绘制矩形需将value值设为Circle
    drawControls.value = new Draw({
      source: source.value,
      type: "Circle",
      geometryFunction: createBox(),
    });
  } else {
    //勾绘类
    drawControls.value = new Draw({
      //source代表勾绘的要素属于的数据集
      source: source.value,
      type: type, //要画的geometry 类型
    });
  }

  //双击停止
  drawControls.value.on("drawend", function (e) {
    const geometry = e.feature.getGeometry();
    let pointArr = geometry.getCoordinates();
    coordinate.value.push(pointArr);
    console.log("投影坐标:", coordinate.value);
    mapref.value.removeInteraction(drawControls.value); //停止绘画操作
  });
  mapref.value.addInteraction(drawControls.value);
};

 2.测距

我们可以在地图上绘制多段直线来进行距离的测量,效果如图:

准备工作在第一小节-动态图形绘制中,绘制直线;双击停止后进行距离换算;

  //双击停止
  drawControls.value.on('drawend', function (e) {
    const geometry = e.feature.getGeometry();
    //给矢量图形添加数据
    let newFeature = new Feature({
      geometry: geometry,
      name: type,
    });
    let curentLong = 0;
    curentLong = geometry.getLength();//ol提供了获取距离的函数
    if (curentLong >= 1000) {
      curentLong = Math.round((curentLong / 1000) * 100) / 100 + ' ' + 'km';
    } else {
      curentLong = Math.round(curentLong) + ' ' + 'm';
    }
    let disstyle = new Style({
      stroke: new Stroke({
        color: '#ffcc33',
        width: 10,
      }),
      text: new Text({
        text: curentLong,
        font: '20px sans-serif',
        fill: new Fill({
          color: 'red',
        }),
      }),
    });
    newFeature.setStyle(disstyle);

    vector.value.getSource().addFeature(newFeature);

    mapref.value.removeInteraction(drawControls.value); //停止绘画操作
  });

3.面积测算

ol自带有面积测算函数

import { getArea } from 'ol/sphere'

给图形赋值的操作与测算距离一样。 

 let area = getArea(geometry, {
             projection: 'EPSG:3857',
        })
if (area > 10000) {
       fontText = Math.round((area / 1000000) * 100) / 100 + ' ' + 'km²'
 } else {
       fontText = Math.round(area * 100) / 100 + ' ' + 'm²'
}

4.绘制带方向的线段

其实还是绘制直线,只是在绘制完成后,在终点加入了带有方向的箭头点位罢了

 先在地图上添加一个展示最终结果的图层,当然,如果你想直接在绘制图层上加也是可以的,但是如果画的图形是不同样式的就不太方便了,看情况来吧

let  lineStringAimSoure =ref()
 lineStringAimSoure.value = new VectorLayer({
        source: new VectorSource({
            features: [],
        }),
    })
    mapref.value.addLayer(lineStringAimSoure.value)

 绘制直线的过程就不多说了,本章节第一小节就有,pointArr即绘制的折线点位数组,

拿到这个折线的图形数据,再给这条折线加上虚线等的样式,newFeature就是不带箭头的折线

重点来看方向箭头的加入

用折线最后两个点来计算旋转的角度,选择箭头朝向为右的图片,将点位添加到图层上即可

routearrow为箭头图片,或者你还可以画一个

 let rrow = renderrowStyle(pointArr[pointArr.length - 2], pointArr[pointArr.length - 1], routearrow)
        lineStringAimSoure.value.getSource().addFeatures([rrow, newFeature])
//方向箭头生成(起点,终点,箭头图片)
const renderrowStyle = (start, end, img) => {
    let dx = end[0] - start[0]
    let dy = end[1] - start[1]
    let rotation = Math.atan2(dy, dx) //旋转角度
    let arrow = new Feature({
        geometry: new Point(end),
    })
    arrow.setStyle(
        new Style({
            //点位样式
            image: new Icon({
                src: img,
                rotation: -rotation,
                opacity: 0.8,
                anchor: [0.5, 0.5],
                width: '26',
                height: '26',
                rotateWithView: false,
            }),
        })
    )
    return arrow
}

 或者,你想让这个线段上也显示方向,请移步第九章轨迹绘制-第二节-带方向箭头的轨迹绘制,两者结合,一个好看的带方向的折线就出现了~

四、图形回显

1.静态图形呈现

1.1 通过绘制的图形数据来加载

绘画能完成后一般会预先保存图形数据,放在数据库里保存,但是图形点位太复杂,根据坐标来呈现和复杂,我们可以直接保存图形的json数据,直接将图形加载在地图上

绘制完成时,我们可以通过writeGeometry函数拿到图形的json

需要加载的类:

import { fromLonLat, toLonLat, transform } from "ol/proj";
import GeoJSON from "ol/format/GeoJSON";
import Feature from "ol/Feature";
import { Circle, Icon, Fill, Stroke, Style, Text } from "ol/style";
import { Circle as CircleDraw } from "ol/geom.js";

 拿到json操作:

//双击停止
  drawControls.value.on("drawend", function (e) {
    const geometry = e.feature.getGeometry();
    let geoJSON = new GeoJSON().writeGeometry(geometry);
    let pointArr = geometry.getCoordinates(); //圆无点数据

//将投影坐标转换为经纬度
    let _arry =
      type == "Point"
        ? toLonLat(pointArr)
        : pointArr
        ? pointArr[0].map((item, index) => toLonLat(item))
        : [];

//用以保存图形的基本数据
    let currentData = {
      type: type,
      arry: _arry,
      geoJSON: geoJSON,
    };

//圆形需要获取圆心与半径,因为圆形拿不到几何json
    if (type == "Circle") {
      currentData.arry = [toLonLat(geometry.getCenter())];
      currentData.circleRadius = geometry.getRadius();
    }

    //给矢量图形添加数据
    let newFeature = new Feature({
      geometry: geometry,
      name: type,
      data: currentData,
    });


//将注入数据的图形加入到图层上
    vector.value.getSource().addFeature(newFeature);
    mapref.value.removeInteraction(drawControls.value); //停止绘画操作
  });

绘制:

const drawJSONdataSource=ref()

const jsonDATA=ref([
{
    "type": "LineString",
    "geoJson": "{\"type\":\"LineString\",\"coordinates\":[[127632.57350790268,-652399.8016907363],[-523610.90748179913,-782648.4978886766],[-403130.9232151327,-883591.2075838663],[101582.77455188613,-883591.2075838663],[130888.76105463691,-649143.6141440019]]}"
  },
  {
    "type": "Circle",
    "center": [13288834.00506184, 5451038.795126652],
    "circleRadius": 236259.58264730684,
    "geoJson": "{\"type\":\"GeometryCollection\",\"geometries\":[]}"
  },])

const renderDraw = (jsonDATA) => {
  let geoJSON = new GeoJSON();
  let FeatureArr = jsonDATA.map((item) => {
    let iFeature = "";
    if (item.type== "Circle") {
      //圆需要根据圆心与半径单独画出来
      iFeature = new Feature({
        geometry: new CircleDraw(
          transform(item.center, "EPSG:4326", "EPSG:3857"),
          parseInt(item.circleRadius)
        ),
        data: item,//将数据注入图形
      });
    } else {
      //点线面
      iFeature = new Feature({
        geometry: geoJSON.readGeometry(item.geoJson), //直接读取图形数据
        data: item,
      });
    }
    return iFeature;
  });

  let vectorSource = new VectorSource({
    features: FeatureArr,
  });

  drawJSONdataSource.value = new VectorLayer({
    source: vectorSource,
    name: "graphs",
  });

  mapref.value.addLayer(drawJSONdataSource.value);
};
1.2 通过经纬度点位信息来加载
1.2.1 圆形

圆形的加载借助Circle类

引入

import { Circle as CircleDraw } from 'ol/geom.js' 

 文档参数说明

 一般来讲,简单的画圆只需要圆心和半径就可,如果想画包裹样式的圆才需要layout

使用:

let circleArea = new Feature({
            geometry: new CircleDraw([110.64998386416187, 19.963120326685925], getRadius(2000)),
        })

其中 getRadius是米向投影半径转换的函数,如下

//半径计算(米->投影)
const getRadius = (radius) => {
    let metersPerUnit = mapref.value.getView().getProjection().getMetersPerUnit() //得到每单位投影的米数
    let circleRadius = radius / metersPerUnit
    return circleRadius
}
 1.2.2 折线

通过LineString类

引入:

import LineString from 'ol/geom/LineString'

使用 

coordinate 是一组折线的顶点点位

 let coordinate = [
        [110.64998386416187, 19.963120326685925],
        [111.6206369460752, 19.5444154716524],
        [111.49656850703367, 20.086461636543802],
    ]
    let LineGeometry = new LineString([])
    coordinate.forEach((item, index) => {
        let currentPoint = transform(item, 'EPSG:4326', 'EPSG:3857')
        LineGeometry.appendCoordinate(currentPoint)
    })
    let lineFeature = new Feature({ geometry: LineGeometry })
 1.2.3 矩形与其他几何体

矩形与其他几何体的生成都需要借助Polygon来实现,他们有个共同点:每个几何体点位数组的第一个与最后一个必须是同一个点位,首尾相连,才能生成一个闭合的几何体

引入:

import { Polygon } from 'ol/geom'

 let _Boxarry = [
            [110.70575977932371, 19.630515703687436],
            [111.49933780223941, 18.953036106215293],
            [109.94114445797426, 18.881848596521806],
            [110.27131925582971, 19.870232001209715],
            [109.39664566852842, 19.374059467054124],
            [110.70575977932371, 19.630515703687436],
        ].map((it) => transform(it, 'EPSG:4326', 'EPSG:3857'))
        let boxFeature = new Feature(new Polygon([_Boxarry]))

注意,Polygon可以同时生成多个几何体,他的参数为一个数组,每个数组里的数据又都是一个几何体数组

五、点位弹窗加载

弹窗加载有两种形式,一种是手动点击点位,弹窗呈现;一种为经纬度定位到响应的点位若有点位则城下弹窗

1.通过点击加载弹窗

通过全局监听地图的点击事件,同时需要在地图的选择事件上增加addCondition事件

加载的弹窗dom:

该弹窗在初始时隐藏,通过地图调用事件来动态加载,通过detailNodeVisible控制

    <div
      id="nodeModel"
      v-show="detailNodeVisible"
    >我是加载的弹窗</div>

js与引用

import Overlay from 'ol/Overlay';

const detailNodeVisible = ref(false); //地图-船舶弹窗

关于Overlay:

通过地图点击事件与forEachFeatureAtPixel函数结合,通过点击处的投影坐标判断点击处是否有点位

1.1 未聚合的点位

未聚合的点位

//地图点击事件
const addPointHandle = () => {
  // //添加事件
  mapref.value.on('click', (evt) => {
    //捕获到要素
    let feature = mapref.value.forEachFeatureAtPixel(evt.pixel, (feature) => {
      return feature;
    });
    //地图点位点击
    if (feature && feature.get('data')) {
      let popup = new Overlay({
        element: document.getElementById('nodeModel'),//挂载的点位弹窗dom
        autoPan: true,
        // positioning: "center-center",//Popup放置的位置
        stopEvent: true, //是否停止事件传播到地图窗口
        autoPanAnimation: {
          //当Popup超出地图边界时,为了Popup全部可见,地图移动的速度
          duration: 250,
        },
      });
      popup.setPosition(evt.coordinate);//此处为投影坐标
      mapref.value.addOverlay(popup);
      detailNodeVisible.value=true//弹窗展示
    }
  });
};

//被选择的图形/图标样式
let selectIconStyle = new Style({
  fill: new Fill({
    color: 'rgba(1, 210, 241, 0.2)',
  }),
  stroke: new Stroke({
    color: 'red',
    width: 4,
  }),
  image: new Icon({
    anchor: [0.5, 0.5],
    // anchorOrigin:'top-left',
    anchorXUnits: 'fraction',
    anchorYUnits: 'pixels',
    // color:'red',//选中图标着色
    width: 30,
    height: 30,
    src: chooseIcon,//chooseIcon为引入的图标地址
  }),
});

// 图形选择事件
const addToolSelect = () => {
  // 选择工具类
  let selectTool = new Select({
    multi: true,
    hitTolerance: 10, // 误差
    style: selectIconStyle, // 选中要素的样式
    // filter: (feature, layer) => {
    //   console.log(feature, layer);
    //   return layer === drawJSONdataSource.value;
    // },
    // layers: [drawJSONdataSource.value],
    addCondition: (evt) => {
      console.log(evt.target);
    },
  });
  //添加交互
  mapref.value.addInteraction(selectTool);
};

补充:

1.  为什么不在addToolSelect函数中的addCondition上写图形选中事件

  ---addCondition中的响应比直接监听地图的点击事件响应要满一些,呈现的效果有一些卡顿,本质上写在哪里都可以,只是效果不同

 2.  当图形与图标重叠时可以给图层增加zIndex属性,提高点位层级,这样就不会出现点位被盖住选不到的情况了,如下图:

1.2聚合后的点位

聚合后的点位在外面包裹了一层,需要用get('features')来获取点击位置的点位数组,然后通过数组中的feature拿到该点位的数据,如果点击的是聚点,那么拿到的数组长度大于1,进行放大图层操作;如果长度等于1,则说明点没有被聚合,可以加载弹窗

//地图点击事件
const addPointHandle = () => {
  // //添加事件
  mapref.value.on('click', (evt) => {
    // console.log('click,', evt);
    //捕获到要素
    let feature = mapref.value.forEachFeatureAtPixel(evt.pixel, (feature) => {
      return feature;
    });
    console.log(feature, feature.get('features'));
    //地图点位点击
    if (feature && feature.get('features')) {
      if (feature.get('features').length == 1) {
        //未聚合的点位-展开点位弹窗
        let data = feature.get('features')[0].get('data'); //这样就可以拿到排在第一的点位数据
        console.log(data);
        
        let popup = new Overlay({
          element: document.getElementById('nodeModel'), //挂载的点位弹窗dom
          autoPan: true,
          stopEvent: true, //是否停止事件传播到地图窗口
          autoPanAnimation: {
            duration: 250,
          },
        });
        popup.setPosition(evt.coordinate);
        mapref.value.addOverlay(popup);
        detailNodeVisible.value = true; //弹窗展示
      } else {
        //'点击了聚合点--放大图层
        mapref.value.getView().animate({
          center: feature.getGeometry().getCoordinates(),
          zoom: mapref.value.getView().getZoom() + 2,
        });
      }
    }
  });
};

2.通过经纬度定位到点位呈现弹窗

lng:经度

lat:纬度

//定位到某个点
const gotoPoint = (lng, lat, duration = 1000) => {
  if (!lng || !lat) return;
  let centers = transform([lng, lat], 'EPSG:4326', 'EPSG:3857');//转换为投影
  mapref.value.getView().animate({
    center: centers,
    duration: duration, //动画时间
    zoom: 10,
  });
  //经纬度锚点到船
  let coords = mapref.value.getPixelFromCoordinate(centers);//从投影转为像素坐标
  let feature = mapref.value.getFeaturesAtPixel(coords, {///通过页面的像素位置来定位到点位
    hitTolerance: 10,//偏移量
  });
  console.log(feature);
//...之后的操作与上一小节点击加载相同(有未聚合的点与聚合的点的区分)
};

六、地图底图切换

数据:

// 地图层数据
const mapList = ref([
      url: `/geoserver/wms`,
        key: 'map',
        type: 'WMS',
        name: '矢量图',
        img: haitu2,
        visible: false,//图层是否可见
        ol_uid: '',//锚点
        viewTile: null, //地图图层实例
    },
    {
        url: `/map/{z}/{x}/{y}.png`,
        key: 'sea',
        type: 'xyz',
        name: '卫星图',
        img: haitu1,
        visible: true,
        ol_uid: '',
        viewTile: null,
    },
]);

const currentMap = ref('sea'); //当前活动地图

html部分

<!-- 地图切换 -->
    <div
      :class="[
        'changeMap',
        { normal: mapType == 'out' },
        { active: mapType == 'on' },
      ]"
      @mouseenter="mapType = 'on'"
      @mouseleave="mapType = 'out'"
    >
      <div
        v-for="(item, index) in mapList"
        :key="index"
        @click="changeMap(item, index)"
        :class="['changeMapItem', { currenAfter: item.key == currentMap }]"
        :data-titlle="item.name"
      >
        <img :src="item.img" />
        <div class="cardBoxName">{{ item.name }}</div>
      </div>
    </div>

css:

  .changeMap {
    position: absolute;
    bottom: 60px;
    left: 26%;
    display: flex;
    transition: height 0.5s;
    padding: 0 0 10px 10px;

    .changeMapItem {
      border: 1px solid rgba(58, 119, 159, 0);
      height: 70px;
      .cardBoxName {
        position: absolute;
        background: rgba(59, 108, 140, 0.7);
        right: 1px;
        bottom: 10px;
        padding: 0 5px;
      }
      img {
        height: 68px;
      }
      &:hover {
        border: 1px solid #fff;
        .cardBoxName {
          background: rgb(19, 99, 174);
        }
      }
    }
    .currenAfter {
      border: 1px solid rgb(25, 122, 187);
      .cardBoxName {
        background: rgb(19, 99, 174);
      }
    }
  }
  .normal {
    .changeMapItem {
      transition: all 0.8s;
      margin-right: -130px;
    }
  }
  .active {
    background: rgba(58, 119, 159, 0.7);
    border-radius: 5px;
    padding: 10px;
    .changeMapItem {
      transition: all 0.8s;
      margin-right: 10px;
      position: relative;
      &:last-child {
        margin-right: 0px;
      }
    }
  }

地图加载

同时加载两种地图,根据生成地图图层时的ol_id进行辨别;通过切换地图图层的visible属性进行完成需求

import { Tile, Vector as VectorLayer} from 'ol/layer';
import { Vector as VectorSource, TileWMS, XYZ } from 'ol/source';
import 'ol/ol.css';
import Map from 'ol/Map';
import View from 'ol/View';

//初始化地图
const InitMap = () => {
  let wmsSource = mapList.value.map((item, index) => {
    let _sourceTile = new Tile({
      visible: item.visible,
      source:
        item.type == 'xyz'
          ? new XYZ({
              url: localStorage.getItem('mapUrl') + item.url,
            })
          : new TileWMS({
              url: localStorage.getItem('mapUrl') + item.url,
              params: {
                LAYERS: 'Groups002',
                TILED: true,
                FORMAT: 'image/png',
              },
              serverType: 'geoserver',
              crossOrigin: 'anonymous',
            }),
    });
    item.ol_uid = _sourceTile.ol_uid
    item.viewTile = _sourceTile
    return _sourceTile;
  });

  mapref.value = new Map({
    target: document.getElementById('map'),
    layers: wmsSource,
    view: new View({
      center: transform([110.64202144301072, 20.78474905684213], 'EPSG:4326', 'EPSG:3857'),
      projection: 'EPSG:3857',
      zoom: 10,
      maxZoom: 16,
    }),
    moveTolerance: 1, 
  });
};

点击地图切换

//地图切换
const changeMap = (node) => {
    mapList.value.forEach((item, index) => {
        let isVisible = item.ol_uid == node.ol_uid
        item.visible = isVisible
        item.viewTile.setVisible(isVisible)

        if (isVisible) {
            localStorage.setItem('currentMap', item.url)
            currentMap.value = item.key
        }
    })
}

 七、地图操作

1.定位

//定位到某个点(经度,纬度,动画时间)
const gotoPoint = (lng, lat, type, duration = 1000) => {
  if (!lng || !lat) return;
  let centers = transform([lng, lat], 'EPSG:4326', 'EPSG:3857');
  mapref.value.getView().animate({
    center: centers,
    duration: duration, //动画时间
    zoom: 10,
  });
};

2.地图缩放

//地图放大缩小(缩放大小)
const changeSize = (size) => {
  mapref.value.getView().animate({
    zoom: mapref.value.getView().getZoom() + size,
  });
};

3.比例尺加载

位于openlayer自带control类中,可进行dom挂载,方式有多种,文章举例为在地图初始加载时就加入比例尺

官网options

引入关键函数:

import { ScaleLine, defaults } from 'ol/control';

可将比例尺通过target挂载到想要放到dom的位置,className可以改变默认样式,通过default().extend()不仅仅可以挂载比例尺,相应的地图的放大缩小图标等也可通过这个函数进行一次性加载。

关键代码:

 mapref.value = new Map({
    target: document.getElementById('map'),
    layers: wmsSource,//上方生成的地图图层
    view: new View({
      center: transform(mapOpations.value.center, 'EPSG:4326', 'EPSG:3857'),
      projection: 'EPSG:3857',
      zoom:10,
      maxZoom: 16,
    }),

    //比例尺
    controls: new defaults().extend([
      new ScaleLine({
        target: document.getElementById('mapMeasuringScale'),
        className: 'mapMeasuringScale',
      }),
    ]),
    moveTolerance: 1, //光标必须移动的最小距离(以像素为单位)才能被检测为map move事件,而不是单击。
  });

八、热力图加载

import { Tile, Vector as VectorLayer, Heatmap } from 'ol/layer';


const thermalMapLayer=ref()//热力图图层
//加载热力图
const addThermalMap = async () => {
  const list = [
    {
      lonAbn: 123.123,
      latAbn: 123.123,
      cont:2
    },
    {
      lonAbn: 115.672337,
      latAbn: 13.248612,
      cont:3
    },
    {
      lonAbn: 123.528294,
      latAbn: 21.528294,
      cont:1
    },
    {
      lonAbn: 123.528294,
      latAbn: 21.528294,
      cont:5
    },
  ];
  thermalMapLayer.value = new Heatmap({
    source: new VectorSource(),
    opacity: 1, //透明度,默认1
    visible: true, //是否显示,默认true
    zIndex: 1, //图层渲染的Z索引,默认按图层加载顺序叠加
    gradient: ['#00f', '#0ff', '#0f0', '#ff0', '#f00'], //热图的颜色渐变
    blur: 15, //模糊大小(像素为单位)
    radius: 8, //半径大小(像素为单位)
    extent: [100, 30, 104, 40], //渲染范围,可选值,默认渲染全部
  });
  mapref.value.addLayer(thermalMapLayer.value);

  list.forEach((item, index) => {
    for (let i = 0; i < cont; i++) {
      let f = new Feature({
        geometry: new Point(
          transform([item.lonAbn, item.latAbn], 'EPSG:4326', 'EPSG:3857')
        ),
      });
      thermalMapLayer.value.getSource().addFeature(f);
    }
  });
};

九、船舶轨迹绘制

1、普通轨迹绘制:

一个最普通的轨迹就是向地图上添加了一条折线,这个折线由多个拐点组成,如下 

import LineString from 'ol/geom/LineString';
import Feature from 'ol/Feature';
import { Circle, Icon, Fill, Stroke, Style } from 'ol/style';


let shipTrajectory = [
  {
    latitude: 20.194063,
    longitude: 110.249173,
    time:'2023-12-12 12:00:00'
  },
  {
     latitude: 20.1911,
     longitude: 110.2322,
     time:'2023-12-12 14:00:00'
  },
  {
    latitude: 20.1886,
    longitude: 110.2163,
    time:'2023-12-12 15:00:00'
  },
  {
     latitude: 20.1844,
     longitude: 110.1988,
     time:'2023-12-12 16:00:00'
  },
];


//绘制轨迹
const createTrajectory = () => {
  let LineGeometry = new LineString([]);
  let coordinate = shipTrajectory;
  coordinate.forEach((item, index) => {
    LineGeometry.appendCoordinate(
      transform([item.longitude, item.latitude], 'EPSG:4326', 'EPSG:3857')
    );
  });
  let lineFeature = new Feature({ geometry: LineGeometry });
  lineFeature.setStyle(
    new Style({
      fill: new Fill({
        color: 'rgba(1, 210, 241, 0.2)',
      }),
      stroke: new Stroke({
        color: 'yellow',
        width: 2,
      }),
    })
  );

  let source = new VectorSource({
    features: [lineFeature],
  });
  traskvector.value = new VectorLayer({
    source: source,
  });
  mapref.value.addLayer(traskvector.value);
  mapref.value.getView().animate({
    center: transform(
      [coordinate[0].longitude, coordinate[0].latitude],
      'EPSG:4326',
      'EPSG:3857'
    ),
    duration: 1000,
    zoom: 8,
  });
};

2.带方向箭头的轨迹绘制

带方向箭头的轨迹核心是改变轨迹的样式,通过计算生成轨迹样式来绘制箭头

...
let lineFeature = new Feature({ geometry: LineGeometry });
  lineFeature.setStyle(renderStyle);
...

基础绘制如上图,renderStyle函数即生成样式函数,

通过计算两个拐点之间的夹角角度,算出旋转角度,旋转箭头实现

routearrow: 

//方向箭绘制
const renderStyle = (feature, resolution) => {
  // 箭头样式
  let styles = [];
  // 线条样式
  let backgroundLineStyle = new Style({
    stroke: new Stroke({
      color: 'green',
      width: 5,
    }),
  });
  styles.push(backgroundLineStyle);
  let geometry = feature.getGeometry();
  // 获取线段长度
  const length = geometry.getLength();
  // 箭头间隔距离(像素)
  const step = 50;
  // 将间隔像素距离转换成地图的真实距离
  const StepLength = step * resolution;
  // 得到一共需要绘制多少个 箭头
  const arrowNum = Math.floor(length / StepLength);
  const rotations = [];
  const distances = [0];
  geometry.forEachSegment(function (start, end) {
    let dx = end[0] - start[0];
    let dy = end[1] - start[1];
    let rotation = Math.atan2(dy, dx);
    distances.unshift(Math.sqrt(dx ** 2 + dy ** 2) + distances[0]);
    rotations.push(rotation);
  });
  // 利用之前计算得到的线段矢量信息,生成对应的点样式塞入默认样式中
  // 从而绘制内部箭头
  for (let i = 1; i < arrowNum; ++i) {
    const arrowCoord = geometry.getCoordinateAt(i / arrowNum);
    const d = i * StepLength;
    const grid = distances.findIndex((x) => x <= d);
    styles.push(
      new Style({
        geometry: new Point(arrowCoord),
        image: new Icon({
          src: routearrow,//引入的图标样式--即箭头(也可通过canvas画一个)
          opacity: 0.8,
          anchor: [0.5, 0.5],
          rotateWithView: false,
          // 读取 rotations 中计算存放的方向信息
          rotation: -rotations[distances.length - grid - 1],
          scale: 0.3,
        }),
      })
    );
  }
  return styles;
};

3含有节点的轨迹

有时候,轨迹不仅仅要展示方向,可能还要展示一些其他的要素。如下

当前轨迹展示了七点终点以及相应轨迹结点的时间

该分为三部分:轨迹折线、轨迹节点、轨迹时间矩形框

相应的,我们在轨迹生成的过程中一起生成上述部分

结点就是生成了点位

时间戳即生成矩形,并将文字赋予在里面

矩形通过Polygon (Polygon在第四章1.2.3有讲解。)生成,生成矩形的数组为:

_Boxarry :[[左上顶点],[右上顶点],[右下顶点],[左下顶点],[左上顶点]]

注意几何体需要首尾相接所以需要五组数据 

import { LineString, Polygon } from 'ol/geom'

//绘制轨迹
const createTrajectory = () => {
    let LineGeometry = new LineString([])
    let coordinate = shipTrajectory
    coordinate.forEach((item, index) => {
        let currentPoint = transform([item.longitude, item.latitude], 'EPSG:4326', 'EPSG:3857')
        LineGeometry.appendCoordinate(currentPoint)
        let _Boxarry = [
            currentPoint,
            [currentPoint[0] + 1000, currentPoint[1]],
            [currentPoint[0] + 1000, currentPoint[1] - 120],
            [currentPoint[0], currentPoint[1] - 120],
            currentPoint,
        ]
        let boxFeature = new Feature(new Polygon([_Boxarry]))
        boxFeature.setStyle(
            new Style({
                fill: new Fill({
                    color: 'rgba(225,225,225, 0.5)',
                }),
                stroke: new Stroke({
                    color: 'rgb(19,208,191)',
                    width: 2,
                }),
                text: new Text({
                    text: item.time,
                    font: '13px sans-serif',
                    fill: new Fill({
                        color: '#0e0e1c',
                    }),
                }),
            })
        )
        let pointFe = new Feature(new Point(currentPoint))
        if (index == 0) {
            //起点
            pointFe.setStyle(
                new Style({
                    image: new Circle({
                        radius: 8,
                        fill: new Fill({ color: 'green' }),
                        stroke: new Stroke({
                            color: 'rgba(255, 255, 255, 0.5)',
                            width: 4,
                        }),
                    }),
                })
            )
        } else if (index == coordinate.length - 1) {
            // 终点
            pointFe.setStyle(
                new Style({
                    image: new Circle({
                        radius: 8,
                        fill: new Fill({ color: 'red' }),
                        stroke: new Stroke({
                            color: 'rgba(255, 255, 255, 0.5)',
                            width: 4,
                        }),
                    }),
                })
            )
        } else {
            pointFe.setStyle(
                new Style({
                    image: new Circle({
                        radius: 4,
                        fill: new Fill({ color: '#3399CC' }),
                        stroke: new Stroke({
                            color: 'rgba(255, 255, 255, 0.5)',
                            width: 2,
                        }),
                    }),
                })
            )
        }
        timePointBoxs.push(boxFeature)
        timePointBoxs.push(pointFe)
    })
    //轨迹加入
    let lineFeature = new Feature({ geometry: LineGeometry })
    lineFeature.setStyle(renderStyle)
    traskvector.value = new VectorLayer({
        source: new VectorSource({
            features: [lineFeature, ...timePointBoxs],
        }),
    })
    mapref.value.addLayer(traskvector.value)

    mapref.value.getView().animate({
    center: transform(
      [coordinate[0].longitude, coordinate[0].latitude],
      'EPSG:4326',
      'EPSG:3857'
    ),
    duration: 1000,
    zoom: 8,
  });
}

4.动点轨迹运动

4.1 利用监听图层渲染实现

当只有一条轨迹时,用这个比较方便

运动轨迹

轨迹数据:


    let result = [
        {
            latitude: 20.194063,
            longitude: 110.249173,
        },
        {
            latitude: 20.1911,
            longitude: 110.2322,
        },
        {
            latitude: 20.1886,
            longitude: 110.2163,
        },
        {
            latitude: 20.1844,
            longitude: 110.1988,
        },
    ]

首先需要绘制动点的运动轨迹

 变量定义

import LineString from 'ol/geom/LineString'
import { transform, toLonLat } from 'ol/proj'
import Point from 'ol/geom/Point'
import { Circle, Icon, Fill, Stroke, Style, Text } from 'ol/style'

const traskvector = ref() //轨迹图层
const passCoordinates = ref([]) //船舶轨迹数组
const passCoordinatesCopy = ref([]) //播放暂存的轨迹
const featureLayer = ref() //动点图层
const startTime = ref(0) //动点开始动画的时间
const LineGeometry = ref() //轨迹图形

绘制轨迹函数:

此步生成了轨迹的几何图形,动点运动的一些参数(步长、经纬度)就从该几何体上获取,如果不需要显示轨迹的话,也需要该步骤,只是轨迹不必添加到视图上即可。

//绘制轨迹与动画(轨迹数组)
const createTrajectory = (coordinate) => {
    traskvector.value && resetTrajectory()
    if (!coordinate?.length) {
        ElMessage.warning('当前时间段无历史轨迹!')
        return
    }
    LineGeometry.value = new LineString([])
    coordinate.forEach((item, index) => {
        LineGeometry.value.appendCoordinate(transform(item.longitudeAndLatitudeDouble, 'EPSG:4326', 'EPSG:3857'))
    })
    let lineFeature = new Feature({ geometry: LineGeometry.value })
    lineFeature.setStyle(
        new Style({
            fill: new Fill({
                color: 'rgba(1, 210, 241, 0.2)',
            }),
            stroke: new Stroke({
                color: 'yellow',
                width: 2,
            }),
        })
    )
    traskvector.value = new VectorLayer({
        source: new VectorSource({
            features: [lineFeature],
        }),
    })
    mapref.value.addLayer(traskvector.value) //轨迹加入
    mapref.value.getView().animate({
        center: transform(coordinate[0].longitudeAndLatitudeDouble, 'EPSG:4326', 'EPSG:3857'),
        duration: 500,
        zoom: mapref.value.getView().getZoom() + 4,
    })
}

//重置轨迹
const resetTrajectory = () => {
    mapref.value.un('postrender', pointMove)
    mapref.value.removeLayer(traskvector.value)
    mapref.value.removeLayer(featureLayer.value)
    startTime.value = 0
}

 实现动点运动运用了openlayers监听图层渲染事件

点击播放函数

//开始播放轨迹(点运动速度)
const startStrack = (speed=10) => {
    if (featureLayer.value) {//重复播放时需要清除已有的播放事件
        mapref.value.removeLayer(featureLayer.value)
        passCoordinates.value = []
        stopMove()
    }
    const particle = 20 // 轨迹分割的颗粒度,数值越小分的越细
    const trackLineLen = LineGeometry.value.getLength() // 轨迹在投影平面上的长度
    const resolution = mapref.value.getView().getResolution() // 当前平面的分辨率

    //点有可能是小数,到终点手动添加最后一个点
    const pointCount = trackLineLen / (resolution * particle)
    for (let i = 0; i <= pointCount; i++) {
        passCoordinates.value.push(LineGeometry.value.getCoordinateAt(i / pointCount))
    }
    const geoMarker = new Feature({
        type: 'geoMarker',
        geometry: new Point(passCoordinates.value[0]),
    })
    geoMarker.setId('shipPoint')

    featureLayer.value = new VectorLayer({
        source: new VectorSource({
            features: [geoMarker],
        }),
        style: new Style({
            image: new Circle({
                radius: 8,
                fill: new Fill({ color: '#333' }),
                stroke: new Stroke({
                    color: '#f00',
                    width: 2,
                }),
            }),
        }),
    })
    mapref.value.addLayer(featureLayer.value) //加入点位
    startTime.value = new Date().getTime()
    mapref.value.on('postrender', (evt) => pointMove(evt, speed))
    //第一次需要手动调用一遍,否则不执行
    mapref.value.render()
}

//撤销监听
const stopMove = () => {
    mapref.value.un('postrender', pointMove)
}

//轨迹动画(e,播放速度,是否停止)
const pointMove = (evt, speed = 10, isStop = false) => {
    if (isStop) {
        stopMove()
        return
    } else {
        // 轨迹动画的速度,数值越大位移越快
        // const speed = 10
        const frameState = evt.frameState
        // 执行动画已经过了多少时间(秒)
        const timeout = (frameState.time - startTime.value) / 1000
        let count = Math.round(speed * timeout)
        if (count >= passCoordinates.value.length - 1) {
            // 确保到达最后一个点位,并停止移动动画
            count = passCoordinates.value.length - 1
            stopMove()
        }
        const point = featureLayer.value.getSource().getFeatureById('shipPoint')
        point.getGeometry().setCoordinates(passCoordinates.value[count])
        // mapref.value.getView().animate({//视野跟随
        //     center: passCoordinates.value[count],
        //     duration: speed,
        // })
        mapref.value.render()
    }
}
4.2 多轨迹,多步骤动画

轨迹视频

利用setInternal来实现点位的移动,

轨迹数据

let data = [
        {
            id: 0,
            nodeDtoList: [
                {
                    id: 111,
                    lineList: [
                        [112.29555426624002, 20.636643779691937],
                        [112.4345752337581, 20.72870817723677],
                        [112.86322321693885, 20.907261520577052],
                        [113.36138168387862, 20.907261520577052],
                        [113.9116730136377, 20.907261520577052],
                        [114.57781514966186, 20.80448391495281],
                        [114.90798994751731, 20.47946420123712],
                    ],
                },
                {
                    id: 112,
                    lineList: [
                        [112.30134680655327, 20.582462198324052],
                        [112.34768712905931, 20.208086935291732],
                        [112.57938874158943, 19.952560060085062],
                        [113.15285023260152, 19.707518304028724],
                        [113.83636998956543, 19.778346935077593],
                        [114.61257039154138, 19.99608338173047],
                        [114.83847946375828, 20.27326090227801],
                        [114.90219740720404, 20.36011693261669],
                    ],
                },
                {
                    id: 113,
                    lineList: [
                        [112.35022384287798, 20.593300055732186],
                        [113.55346930295883, 20.468618276501886],
                        [114.87819627583632, 20.403526645487887],
                    ],
                },
            ],
        },
        {
            id: 1,
            nodeDtoList: [
                {
                    id: 121,
                    lineList: [
                        [114.86111776443708, 20.360221634423567],
                        [113.89705257066203, 20.10099260434633],
                        [112.78412436902464, 20.180801388842298],
                        [112.40133377737867, 20.552700251197123],
                    ],
                },
            ],
        },
    ]

变量定义:

const stepLineCoordinates = ref([]) //动画步骤-原始数据
const featureLayer = ref() //动点运动图层
const currentLines = ref([]) //动点投影轨迹数据
const currentIndex = ref(0) //当前动画进行的大步骤
const currentSpeed = ref(1) //轨迹播放速度
//动画速度数据
const speedOptions = ref([
    { value: 1, label: '×1' },
    { value: 2, label: '×2' },
    { value: 3, label: '×3' },
    { value: 4, label: '×4' },
])

 dom:

<template>
    <div class="mapBox">
        <div id="modelmap2" style="height: 100%"></div>
        <el-select class="speedSelect" @change="changeSteep" popper-class="link-popper" v-model="currentSpeed">
            <el-option v-for="item in speedOptions" :key="item.value" :label="item.label" :value="item.value" />
        </el-select>
    </div>
</template>
<style scoped lang="scss">
.mapBox {
    height: 400px;
    position: relative;
    .speedSelect {
        position: absolute;
        top: 0;
        left: 20px;
        z-index: 9999;
        width: 70px;
    }
}
</style>

用到的图片:

 routearrow2:

routearrowBlue:

 先画轨迹 initStepLine(步骤轨迹数组),通过addStepLine函数将轨迹的几何数据保存到源数据中,

调用播放函数runStart,开始播放轨迹动画

有点懒,回来再写解释吧。。。下方核心代码

initStepLine(data)

//轨迹初始化
function initStepLine(stepDataList) {
    if (!stepDataList.length) return
    stepLineCoordinates.value = JSON.parse(JSON.stringify(stepDataList))
    stepLineCoordinates.value.forEach((item, index) => {
        item.nodeDtoList.forEach((it) => {
            addStepLine(it.lineList, (data) => {
                it.lineListGeo = data
            })
        })
    })
}

//增加线条
function addStepLine(coordinate, fun) {
    let LineGeometry = new LineString([]) //轨迹
    coordinate.forEach((item, index) => {
        let currentPoint = transform(item, 'EPSG:4326', 'EPSG:3857')
        LineGeometry.appendCoordinate(currentPoint)
    })
    let lineFeature = new Feature({ geometry: LineGeometry })
    lineFeature.setStyle(renderStyle)
    let rrow = renderrowStyle(
        transform(coordinate[coordinate.length - 2], 'EPSG:4326', 'EPSG:3857'),
        transform(coordinate[coordinate.length - 1], 'EPSG:4326', 'EPSG:3857'),
        routearrowBlue
    )
    //加入轨迹与方向箭头
    finallySoure.value.getSource().addFeatures([rrow, lineFeature])
    if (fun) {
        //保存线条图形
        fun(LineGeometry)
    }
    vector.value.getSource().clear()
}

//轨迹箭头生成
const renderStyle = (feature, resolution) => {
    // 箭头样式
    let styles = []
    // 线条样式
    let backgroundLineStyle = new Style({
        stroke: new Stroke({
            color: 'rgb(19,208,191)',
            width: 2,
        }),
    })
    styles.push(backgroundLineStyle)
    let geometry = feature.getGeometry()
    // 获取线段长度
    const length = geometry.getLength()
    // 箭头间隔距离(像素)
    const step = 100
    // 将间隔像素距离转换成地图的真实距离
    const StepLength = step * resolution
    // 得到一共需要绘制多少个 箭头
    const arrowNum = Math.floor(length / StepLength)
    const rotations = []
    const distances = [0]
    geometry.forEachSegment(function (start, end) {
        let dx = end[0] - start[0]
        let dy = end[1] - start[1]
        let rotation = Math.atan2(dy, dx)
        distances.unshift(Math.sqrt(dx ** 2 + dy ** 2) + distances[0])
        rotations.push(rotation)
    })
    for (let i = 1; i < arrowNum; ++i) {
        const arrowCoord = geometry.getCoordinateAt(i / arrowNum)
        const d = i * StepLength
        const grid = distances.findIndex((x) => x <= d)
        styles.push(
            new Style({
                geometry: new Point(arrowCoord),
                image: new Icon({
                    src: routearrow2,
                    opacity: 0.8,
                    anchor: [0.5, 0.5],
                    rotateWithView: false,
                    // 读取 rotations 中计算存放的方向信息
                    rotation: -rotations[distances.length - grid - 1],
                    scale: 0.1,
                }),
            })
        )
    }
    return styles
}

//方向箭头生成(起点,终点,箭头图片)
const renderrowStyle = (start, end, img) => {
    let dx = end[0] - start[0]
    let dy = end[1] - start[1]
    let rotation = Math.atan2(dy, dx) //旋转角度
    let arrow = new Feature({
        geometry: new Point(end),
    })
    arrow.setStyle(
        new Style({
            //点位样式
            image: new Icon({
                src: img,
                rotation: -rotation,
                opacity: 0.8,
                anchor: [0.5, 0.5],
                width: '26',
                height: '26',
                rotateWithView: false,
            }),
        })
    )
    return arrow
}

//动画开始播放(速度)
function runStart() {
    currentIndex.value = 0 //步骤重置
    //重置正在进行的动画
    if (currentLines.value?.length) {
        cleanintervals()
    }
    const particle = 5 // 轨迹分割的颗粒度,数值越小分的越细
    const resolution = mapref.value.getView().getResolution() // 当前平面的分辨率
    let shipList = [] //步点的投影坐标数组
    stepLineCoordinates.value.forEach((item) => {
        let node = {
            id: item.id, //步骤id
            maxLong: 0, //当前步骤最长的轨迹长度
            coopList: [], //步骤内动画数组
        }
        item.nodeDtoList.forEach((it) => {
            let _list = []
            let trackLineLen = it.lineListGeo.getLength() // 轨迹在投影平面上的长度
            let pointCount = trackLineLen / (resolution * particle)
            for (let i = 0; i <= pointCount; i++) {
                _list.push(it.lineListGeo.getCoordinateAt(i / pointCount))
            }
            //将最后一个点加入,形成闭环
            _list.push(transform(it.lineList[it.lineList.length - 1], 'EPSG:4326', 'EPSG:3857'))
            //动点
            let geoMarker = new Feature({
                type: 'geoMarker',
                geometry: new Point(it.lineList[0]),
            })
            geoMarker.setId(it.id)
            node.maxLong = _list.length > node.maxLong ? _list.length : node.maxLong
            node.coopList.push({
                pointType: 'wuuw', //点类型
                array: _list, //步点的投影坐标
                geoMarker: geoMarker, //动点
                geoId: it.id,
            })
        })
        shipList.push(node)
    })
    // console.log(shipList)
    currentLines.value = shipList

    //生成点位运动图层
    featureLayer.value = new VectorLayer({
        source: new VectorSource({
            features: [],
        }),
        style: new Style({
            image: new Circle({
                radius: 8,
                fill: new Fill({ color: '#333' }),
                stroke: new Stroke({
                    color: '#f00',
                    width: 2,
                }),
            }),
        }),
    })
    mapref.value.addLayer(featureLayer.value) //加入点位图层
    //动画开始,从第一步骤开始运行
    renderStep(currentLines.value[0], currentSpeed.value)
}

//清除定时器
const cleanintervals = () => {
    featureLayer.value && mapref.value.removeLayer(featureLayer.value)
    currentLines.value.forEach((item) => {
        item.coopList.forEach((it) => {
            it.inter && clearInterval(it.inter)
        })
    })
}

//步骤播放(步骤数据,运动速度-->数据越大,运动越快)
const renderStep = (node, speed = 1) => {
    node.coopList.forEach((it) => {
        featureLayer.value.getSource().addFeature(it.geoMarker) //加入动点
        let point = featureLayer.value.getSource().getFeatureById(it.geoId) //获取动点源
        it.long = it.long ? it.long : 0 //记录轨迹播放步数
        renderInternal(it, point, speed, node.maxLong)
    })
}

//生成定时器动画(单个轨迹数据,动点源,运动速度,当前步骤最大长度)
const renderInternal = (it, point, speed, maxLong) => {
    it.inter = setInterval(() => {
        it.long += speed
        //防止步长太大超过数组长度
        if (it.long > it.array.length - 1) {
            it.long = it.array.length - 1
        }
        point.getGeometry().setCoordinates(it.array[it.long]) //动点运动
        //单轨迹结束
        if (it.long == it.array.length - 1) {
            clearInterval(it.inter)
            point && featureLayer.value.getSource().removeFeature(point) //清点
            //轨迹最长时,大步骤结束
            if (maxLong == it.array.length) {
                currentIndex.value++ //步骤加一
                if (currentIndex.value < currentLines.value.length) {
                    //大步骤结束
                    renderStep(currentLines.value[currentIndex.value], speed)
                } else {
                    //全部结束
                    currentIndex.value = 0
                }
            }
        }
    }, 100)
}

//改变播放速度
const changeSteep = (speed) => {
    console.log(speed)
    currentLines.value[currentIndex.value].coopList.forEach((item) => {
        clearInterval(item.inter)
        featureLayer.value.getSource().clear()
    })
    currentSpeed.value = speed
    renderStep(currentLines.value[currentIndex.value], speed)
}

十、一些常用函数

10.1 图层

10.1.1 向图层添加矢量addFeature、addFeatures

有两种方式可以向特定图层上添加数据-addFeature:只能添加一个;addFeatures:可以一次添加多个

图层实例.getSource().addFeature(矢量1)

图层实例.getSource().addFeatures([矢量1,矢量2]) 

10.1.2 清除图层中所有数据clear

图层实例.getSource().clear() 

10.1.3 获取图层中某个矢量数据getFeatureById

图层实例 .getSource().getFeatureById('矢量id')

10.1.4 清除图层中某个矢量removeFeature

图层实例.getSource().removeFeature(矢量) 

10.2 图形 

10.2.1 获取图形点位数据getCoordinates

矢量.getGeometry().getCoordinates()

10.2.2 修改图形点位数据setCoordinates

 矢量.getGeometry().setCoordinates([新的投影数据])

10.2.3 获取圆半径与圆心getCenter、getRadius

 矢量.getGeometry().getCenter()

 矢量.getGeometry().getRadius()

10.2.4 获取图形投影长度getLength

 矢量.getGeometry().getLength() 

10.3 地图 

10.3.1 添加图层addLayer

   地图实例.addLayer(图层实例)

 10.3.2获取视图、获取视图的缩放

地图实例.getView().getZoom()

  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在OpenLayers使用百度地图,需要进行以下步骤: 1. 引入OpenLayers的库文件,可以从官方网站https://openlayers.org/download/下载最新版本的OpenLayers库文件。 2. 引入百度地图的API文件,可以从百度地图开放平台https://lbsyun.baidu.com/下载最新版本的API文件。 3. 创建一个地图容器,这个容器用于显示地图。在HTML文件中添加一个div元素,用于包含地图容器。 4. 在JavaScript文件中,使用OpenLayers的API创建一个地图对象,并将其添加到地图容器中。在创建地图对象时,需要将百度地图的图层添加到地图中。 5. 在地图对象中添加一些控件,如缩放控件、鹰眼控件、比例尺控件等。 以下是一个基本的OpenLayers使用百度地图的示例代码: ```html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>OpenLayers使用百度地图</title> <link rel="stylesheet" href="https://openlayers.org/en/latest/css/ol.css" type="text/css"> <script src="https://openlayers.org/en/latest/build/ol.js"></script> <script src="http://api.map.baidu.com/api?v=2.0&ak=您的密钥"></script> <style> #map { width: 100%; height: 500px; } </style> </head> <body> <div id="map"></div> <script> var map = new ol.Map({ target: 'map', layers: [ new ol.layer.Tile({ source: new ol.source.BaiduMap({ key: '您的密钥', style: 'normal' }) }) ], view: new ol.View({ center: ol.proj.fromLonLat([116.397428, 39.90923]), zoom: 12 }) }); var zoomControl = new ol.control.Zoom(); map.addControl(zoomControl); </script> </body> </html> ``` 在上面的代码中,首先引入了OpenLayers和百度地图的API文件。然后,创建一个div元素用于包含地图容器。在JavaScript部分,使用OpenLayers的API创建一个地图对象,并将百度地图的图层添加到地图中。最后,向地图对象添加了一个缩放控件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值