由於上半年經常跑出去玩,突然想做一個旅行地圖的部落格,想起之前接觸過 openlayers 的專案,也懶得去調查別的庫了,直接用 openlayers 開幹。由於github經常構建失敗,我部署到vercel上去了,現在能正常訪問了,而且感覺速度也稍微比github快一些。
安裝
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);
}
}
});
- 效果
實現主題切換
監聽 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);
}
}
}
}
}
});
- 效果
新增標點
- 建立一個 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);
- 效果
為標點新增事件
- 移動到標點出顯示標點資訊
- 使用建立互動事件
- layer 互動圖層為 marker container
- condition 互動條件為滑鼠懸停 pointerMove
import { pointerMove } from "ol/events/condition";
const interaction = new Select({
layers: [layer], // 指定可以觸發互動的圖層
condition: pointerMove, // 滑鼠觸發條件
style: null, // 禁用預設樣式
});
- 繫結互動事件觸發的回撥函式
- 獲取標點 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";
}
});
- 新增點選事件
觸發點選事件跳轉到對應的連結
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);
}
});
- 效果
航行路線
其實標點做完已經完成了我的目標和想要的效果了,不過最近比較清閒,就想加點花哨的東西,新增一個飛機飛過的航行絡線。
- 建立飛機、路線圖層
const source = new SourceVector();
const layer = new Vector({ source });
map.addLayer(layer);
- 建立一架飛機
- 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);
- 飛行路線
- 根據標點建立不同的路線
- 使用 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;
});
- 飛行動畫
根據線路的座標在設定的時間內 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();
- 效果
左上角是資訊預覽和路線預覽的開關。
可以看到飛機的初始方向是對的,但飛行起來就不對了,因為我還沒有做哈哈哈哈,需要在動畫裡每一幀根據座標去計算飛機的角度,之後再更新吧。
飛機轉向(更新)
其實也很簡單,就是記錄一下上一次的位置資訊,計算一下偏移角度呼叫 setRotation 在動畫的每一幀設定一下就可以了
if (lastCoords) {
const degrees = toRadians(
45 + 360 - countDegrees(lastCoords, coordsList[index])
);
(feature.getStyle() as Style)?.getImage()?.setRotation(degrees);
}
lastCoords = coordsList[index];
- 效果
總結
如果感興趣的話可以關注我的github。
對我的部落格專案感興趣可以關注my blog github,我會不定期地持續地更新,歡迎大佬新增友鏈。
這裡是旅行地圖預覽地址,由於github actions經常會因為錯誤提交而構建失敗,之前vercel構建出來頁面重新整理會404。
現在已經解決了vercel重新整理404的問題,於是重新部署到vercel上m,my blog
所有的展示圖片來自錄屏再透過my tools轉換為 gif。