在 vue3 中使用 openlayers 制作旅行地图

在 vue3 中使用 openlayers 制作旅行地图

由于上半年经常跑出去,突然想做一个旅行地图的博客,想起之前接触过 openlayers 的项目,也懒得去调查别的库了,直接用 openlayers 开干。由于博客是挂在github上的,可能会有点卡。

安装

vue 的项目搭建就不说了,直接安装 ol 就可以开写了

bash 复制代码
npm i ol

创建地图

ts 复制代码
const { center, zoom, minZoom, maxZoom, extent } = MAP_DEFAULT_OPTIONS;
const map = new Map({
  target: "map",
  layers: [],
  controls: [],
});
map.setView(
  new View({
    center: fromLonLat(center),
    zoom,
    minZoom,
    maxZoom,
    constrainResolution: true,
    extent: transformExtent(extent, EPSG4326, map.getView().getProjection()),
  })
);

添加图层

创建图层,我这里用的是 geojson 的数据创建的,可以在网上找到你想要创建地图的 geojson 数据。

ts 复制代码
const layer = new Vector({
  source: new SourceVector({
    url,
    format: new GeoJSON(),
  }),
});

layer.setStyle(CreateLayerStyle);

创建多个图层添加到组内,比如亚洲图层,中国图层

ts 复制代码
const layerGroup = new Group({
  layers: [asiaLayer, chinaLayer],
});
map.addLayer(layerGroup);

实现放大现在省份图层

由于中国图层的 geojson 就只包含省份的边界线,我想要在放大的时候加载出城市的边界线,就得添加省份的 geojson 数据。

监听地图的 change 事件,判断缩放发生大于某个数的时候,添加对应的省份图层

  • LayerCacheMap 省份图层
  • currentExtent 当前视图范围
  • isCityInView 判断省份是否在当前视图中
  • layer.setVisible 设置吐图层显示隐藏
ts 复制代码
map.getView().on("change", function (event) {
  const mapView = event.target;
  // 获取新的缩放级别
  const zoom = event.target.getZoom();
  // 当前视图范围
  const currentExtent = mapView.calculateExtent(map.getSize());

  const transformedExtent = transformExtent(
    currentExtent,
    mapView.getProjection(),
    EPSG4326
  );

  if (zoom > index) {
    // 显示2级涂层
    for (const key in ALL_EXTENT) {
      const extent = ALL_EXTENT[key];

      // 判断省份是否在当前视图中
      const isCityInView = intersects(extent, transformedExtent);
      const layer = LayerCacheMap[key];
      if (!layer) continue;

      if (isCityInView) {
        layer.setVisible(true);
      } else {
        layer.setVisible(false);
      }
    }
  } else {
    // 移除2级涂层
    for (const key in ALL_EXTENT) {
      const layer = LayerCacheMap[key];
      if (layer) layer.setVisible(false);
    }
  }
});
  • 效果

实现主题切换

监听 isDark 的变化,遍历所有图层,使用 layer.setStyle 改变图层的 style

ts 复制代码
const isDark = useDark();

watch(isDark, () => {
  for (const key in LayerCacheMap) {
    if (Object.prototype.hasOwnProperty.call(LayerCacheMap, key)) {
      const map = LayerCacheMap[(key as any) as LayerIndex];
      for (const key in map) {
        if (Object.prototype.hasOwnProperty.call(map, key)) {
          const layerMap = map[key];
          if (layerMap.layer) {
            // 设置主题
            layerMap.layer.setStyle(CreateLayerStyle);
          }
        }
      }
    }
  }
});
  • 效果

添加标点

  • 创建一个 marker layer 图层来收集所有的点
  • 通过数据批量创建点要素,设置样式
ts 复制代码
const container = new Vector({
  source: new SourceVector(),
});

// 获取标点的数据
const markerList = CreateMapMarkerData();

markerList.forEach((item) => {
  // 创建点要素,添加到container layer中
  const pointFeature = CreatePointFeature(item);
  if (pointFeature) container.getSource()?.addFeature(pointFeature);
});
  • 根据位置信息创建点要素
ts 复制代码
const pointFeature = new Feature({
  geometry: new Point(fromLonLat(item.coords)), // 设置点的坐标
  info: item,
});

// 创建一个图标样式
const iconStyle = new Style({
  image: new Icon({
    src: "/images/icons/marker.svg",
    color: "red",
    scale: 1,
    anchor: [0.15, 0.9], // 图标的锚点位置
  }),
});
pointFeature.setStyle(iconStyle);
  • 效果

为标点添加事件

  1. 移动到标点出显示标点信息
  • 使用创建交互事件
  • layer 交互图层为 marker container
  • condition 交互条件为鼠标悬停 pointerMove
ts 复制代码
import { pointerMove } from "ol/events/condition";

const interaction = new Select({
  layers: [layer], // 指定可以触发交互的图层
  condition: pointerMove, // 鼠标触发条件
  style: null, // 禁用默认样式
});
  1. 绑定交互事件触发的回调函数
  • 获取标点 event.selected[0]
  • 获取标点信息 selectedFeature.get("info")
  • 在鼠标移入标点时触发相应的事件,比如修改指针
  • 鼠标移出时触发相应的事件
ts 复制代码
let markerInfo: MarkInfo = {
  info: {},
  coords: [],
};

interaction.on("select", (event) => {
  // 悬停事件触发
  if (event.selected.length > 0) {
    const selectedFeature = event.selected[0];
    // 保存标点信息
    markerInfo.info = selectedFeature.get("info");

    const geometry = selectedFeature.getGeometry();
    if (geometry instanceof Point) {
      // 保存标点位置
      markerInfo.coords = geometry.getCoordinates();
    }

    // 设置 preview 的显示内存
    const element = document.getElementById("map_marker_preview");

    element.textContent = markerInfo.info.title;
    // ...

    // 设置鼠标指针为 pointer
    map.getTargetElement().style.cursor = "pointer";
  } else {
    // 鼠标移出触发
    // ...

    map.getTargetElement().style.cursor = "default";
  }
});
  1. 添加点击事件

触发点击事件跳转到对应的链接

ts 复制代码
import { click } from "ol/events/condition";

const interaction = new Select({
  layers: [layer],
  condition: click,
  style: null,
});

interaction.on("select", (event) => {
  if (event.selected.length > 0) {
    const selectedFeature = event.selected[0];
    const info = selectedFeature.get("info");
    if (info?.route) router.push(info?.route);
  }
});
  • 效果

航行路线

其实标点做完已经完成了我的目标和想要的效果了,不过最近比较清闲,就想加点花哨的东西,添加一个飞机飞过的航行络线。

  1. 创建飞机、路线图层
ts 复制代码
const source = new SourceVector();
const layer = new Vector({ source });
map.addLayer(layer);
  1. 创建一架飞机
  • extent 目的地的坐标
  • degrees 飞机初始的旋转角度
  • countDegrees 通过起始坐标和终点坐标来计算 degrees
ts 复制代码
const extent = transform(event?.coords, EPSG3857, EPSG4326);
const degrees = countDegrees(START_POINT, extent);

const feature = new Feature({ geometry: new Point([]) });
const style = new Style({
  image: new Icon({
    src: "/images/icons/plane.svg",
    scale: 1,
    rotation: toRadians(45 + 360 - degrees),
  }),
});
feature.setStyle(style);
source.addFeature(feature);
  1. 飞行路线
  • 根据标点创建不同的路线
  • 使用 LineString 创建线段要素
  • interpolatePoints 根据起始点和终点插值(我的效果是使用贝塞尔曲线创建的)
ts 复制代码
const features: Record<string, Feature> = {};
const markers = CreateMapMarkerData();

markers
  .filter((m) => !!m.coords)
  .forEach((marker) => {
    // 插值
    const coords = interpolatePoints(START_POINT, marker.coords, 100);

    const feature = new Feature({
      geometry: new LineString(coords),
    });
    // 设置样式
    feature.setStyle(CreateLineStyle());
    features[marker.route] = feature;
  });
  1. 飞行动画

根据线路的坐标在设置的时间内 Duration 不停的改变飞机的坐标位置

ts 复制代码
const line = lineFeature?.getGeometry();
const coordsList = line.getCoordinates();
let startTime = new Date().getTime();

function animate() {
  const currentTime = new Date().getTime();
  const elapsedTime = currentTime - startTime;
  const fraction = elapsedTime / Duration;
  const index = Math.round(coordsList.length * fraction);

  if (index < coordsList.length) {
    const geometry = feature.getGeometry();
    if (geometry instanceof Point) {
      geometry?.setCoordinates(coordsList[index]);
    }
    // TODO 飞机转向

    requestAnimationFrame(animate);
  } else {
    callback();
  }
}

animate();
  • 效果

左上角是信息预览和路线预览的开关。

可以看到飞机的初始方向是对的,但飞行起来就不对了,因为我还没有做哈哈哈哈,需要在动画里每一帧根据坐标去计算飞机的角度,之后再更新吧。

总结

如果感兴趣的话可以关注我的github

对我的博客项目感兴趣可以关注my blog github,我会不定期地持续地更新。

这是旅行地图预览地址,gitHub pages没有科学的话可能会有点卡。

所有的展示图片来自录屏再通过my tools转换为 gif。

相关推荐
小小小小宇2 小时前
前端 Service Worker
前端
只喜欢赚钱的棉花没有糖3 小时前
http的缓存问题
前端·javascript·http
小小小小宇3 小时前
请求竞态问题统一封装
前端
loriloy3 小时前
前端资源帖
前端
源码超级联盟3 小时前
display的block和inline-block有什么区别
前端
GISer_Jing3 小时前
前端构建工具(Webpack\Vite\esbuild\Rspack)拆包能力深度解析
前端·webpack·node.js
让梦想疯狂3 小时前
开源、免费、美观的 Vue 后台管理系统模板
前端·javascript·vue.js
海云前端4 小时前
前端写简历有个很大的误区,就是夸张自己做过的东西。
前端
葡萄糖o_o4 小时前
ResizeObserver的错误
前端·javascript·html
AntBlack4 小时前
Python : AI 太牛了 ,撸了两个 Markdown 阅读器 ,谈谈使用感受
前端·人工智能·后端