博客 / 詳情

返回

常規博客看膩了,使用openlayers製作旅行地圖的個人博客👀

由於上半年經常跑出去玩,突然想做一個旅行地圖的博客,想起之前接觸過 openlayers 的項目,也懶得去調查別的庫了,直接用 openlayers 開幹。之前用github actions偶爾會構建失敗,然後vercel上部署的又需要科學訪問。最近域名備案完成,部署到服務器上了,可以正常訪問了。

  • 鏈接

    • 項目代碼github鏈接
    • 博客原文鏈接
    • 旅行地圖預覽鏈接
  • 效果預覽

ol_plane.gif

安裝

vue 的項目搭建就不説了,直接安裝 ol 就可以開寫了

npm i ol

創建地圖

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 數據。

const layer = new Vector({
  source: new SourceVector({
    url,
    format: new GeoJSON(),
  }),
});

layer.setStyle(CreateLayerStyle);

創建多個圖層添加到組內,比如亞洲圖層,中國圖層

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

實現放大現在省份圖層

由於中國圖層的 geojson 就只包含省份的邊界線,我想要在放大的時候加載出城市的邊界線,就得添加省份的 geojson 數據。

監聽地圖的 change 事件,判斷縮放發生大於某個數的時候,添加對應的省份圖層

  • LayerCacheMap 省份圖層
  • currentExtent 當前視圖範圍
  • isCityInView 判斷省份是否在當前視圖中
  • layer.setVisible 設置圖層顯示隱藏
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);
    }
  }
});
  • 效果

ol_zoom.gif

實現主題切換

監聽 isDark 的變化,遍歷所有圖層,使用 layer.setStyle 改變圖層的 style

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);
          }
        }
      }
    }
  }
});
  • 效果

ol_change_theme.gif

添加標點

  • 創建一個 marker layer 圖層來收集所有的點
  • 通過數據批量創建點要素,設置樣式
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);
});
  • 根據位置信息創建點要素
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);
  • 效果

ol_marker.png

為標點添加事件

  1. 移動到標點出顯示標點信息
  • 使用創建交互事件
  • layer 交互圖層為 marker container
  • condition 交互條件為鼠標懸停 pointerMove
import { pointerMove } from "ol/events/condition";

const interaction = new Select({
  layers: [layer], // 指定可以觸發交互的圖層
  condition: pointerMove, // 鼠標觸發條件
  style: null, // 禁用默認樣式
});
  1. 綁定交互事件觸發的回調函數
  • 獲取標點 event.selected[0]
  • 獲取標點信息 selectedFeature.get("info")
  • 在鼠標移入標點時觸發相應的事件,比如修改指針
  • 鼠標移出時觸發相應的事件
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. 添加點擊事件

觸發點擊事件跳轉到對應的鏈接

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);
  }
});
  • 效果

ol_marker_preview.gif

航行路線

其實標點做完已經完成了我的目標和想要的效果了,不過最近比較清閒,就想加點花哨的東西,添加一個飛機飛過的航行絡線。

  1. 創建飛機、路線圖層
const source = new SourceVector();
const layer = new Vector({ source });
map.addLayer(layer);
  1. 創建一架飛機
  • extent 目的地的座標
  • degrees 飛機初始的旋轉角度
  • countDegrees 通過起始座標和終點座標來計算 degrees
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 根據起始點和終點插值(我的效果是使用貝塞爾曲線創建的)
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 不停的改變飛機的座標位置

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();
  • 效果

ol_plane.gif

左上角是信息預覽和路線預覽的開關。

可以看到飛機的初始方向是對的,但飛行起來就不對了,因為我還沒有做哈哈哈哈,需要在動畫裏每一幀根據座標去計算飛機的角度,之後再更新吧。

飛機轉向(更新)

其實也很簡單,就是記錄一下上一次的位置信息,計算一下偏移角度調用 setRotation 在動畫的每一幀設置一下就可以了

if (lastCoords) {
  const degrees = toRadians(
    45 + 360 - countDegrees(lastCoords, coordsList[index])
  );
  (feature.getStyle() as Style)?.getImage()?.setRotation(degrees);
}
lastCoords = coordsList[index];
  • 效果

ol_plane_rotation.gif

總結

如果感興趣的話可以關注我的github。

對我的博客項目感興趣可以關注my blog github,我會不定期地持續地更新,歡迎大佬添加友鏈。

這裏是旅行地圖預覽地址,由於github actions經常會因為錯誤提交而構建失敗,之前vercel構建出來頁面刷新會404。最近域名弄好了,訪問正常了。

現在已經解決了vercel刷新404的問題,於是重新部署到vercel上m,my blog

所有的展示圖片來自錄屏再通過my tools轉換為 gif。

user avatar yangchen_5f61f615a6de4 頭像 jiasm 頭像 bbqzhiyoufanshu 頭像
3 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.