OpenLayers 综合案例-轨迹回放

看过的知识不等于学会。唯有用心总结、系统记录,并通过温故知新反复实践,才能真正掌握一二

作为一名摸爬滚打三年的前端开发,开源社区给了我饭碗,我也将所学的知识体系回馈给大家,助你少走弯路!
OpenLayers、Leaflet 快速入门 ,每周保持更新 2 个案例
Cesium 快速入门,每周保持更新 4 个案例

OpenLayers 综合案例-轨迹回放

Vue 3 + OpenLayers 实现的 WebGIS 应用提供了完整的轨迹回放功能

主要功能

  1. 地图上显示一条预设轨迹线。
  2. 小车🚗沿轨迹逐点移动,拐弯时方向自动调整。
  3. 支持"开始""暂停""继续""重置"操作。
  4. 显示当前进度和状态。

MP4效果动画

vue 复制代码
<template>
  <div class="car-track-container">
    <div ref="mapContainer" class="map"></div>
    <div class="map-controls">
      <h3>小车轨迹回放 🚗</h3>
      <div class="control-group">
        <button class="control-btn" @click="startTrack" :disabled="playing">
          开始
        </button>
        <button class="control-btn" @click="pauseTrack" :disabled="!playing">
          暂停
        </button>
        <button
          class="control-btn"
          @click="resumeTrack"
          :disabled="playing || finished"
        >
          继续
        </button>
        <button class="control-btn" @click="resetTrack">重置</button>
      </div>
      <div class="stats-section">
        <div>
          进度:
          <span class="progress"
            >{{ progress + 1 }} / {{ trackPoints.length }}</span
          >
        </div>
        <div>
          状态: <span class="status">{{ statusText }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, computed } from "vue";
import Map from "ol/Map";
import View from "ol/View";
import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer";
import { XYZ, Vector as VectorSource } from "ol/source";
import { LineString, Point } from "ol/geom";
import Feature from "ol/Feature";
import { Style, Stroke, Icon } from "ol/style";
import { defaults as defaultControls, FullScreen, ScaleLine } from "ol/control";
import { fromLonLat } from "ol/proj";
import "ol/ol.css";

const map = ref(null);
const mapContainer = ref(null);
const vectorSource = ref(null);
const carFeature = ref(null);
const trackFeature = ref(null);
const playing = ref(false);
const finished = ref(false);
const progress = ref(0);
const timer = ref(null);

// 预设轨迹点(经纬度)
const trackPoints = ref([
  [116.39, 39.9],
  [116.392, 39.901],
  [116.395, 39.902],
  [116.398, 39.903],
  [116.4, 39.905],
  [116.402, 39.908],
  [116.404, 39.91],
  [116.406, 39.912],
  [116.408, 39.914],
  [116.41, 39.916],
]);

const statusText = computed(() => {
  if (finished.value) return "已完成";
  if (playing.value) return "播放中";
  if (progress.value > 0) return "已暂停";
  return "未开始";
});

// 计算两点间的角度(弧度,正北为0,顺时针)
function getAngle(p1, p2) {
  const [x1, y1] = p1;
  const [x2, y2] = p2;
  const dx = x2 - x1;
  const dy = y2 - y1;
  return Math.atan2(dy, dx);
}

function carStyle(angle = 0) {
  return new Style({
    image: new Icon({
      src: "/src/assets/car.png",
      anchor: [0.5, 0.5],
      rotateWithView: true,
      rotation: -angle, // 加上这一行实现方向旋转
    }),
  });
}

function trackLineStyle() {
  return new Style({
    stroke: new Stroke({ color: "#1976d2", width: 4, lineDash: [8, 8] }),
  });
}

// 绘制轨迹线
function drawTrack() {
  if (trackFeature.value) vectorSource.value.removeFeature(trackFeature.value);
  const coords = trackPoints.value.map((pt) => fromLonLat(pt));
  trackFeature.value = new Feature({ geometry: new LineString(coords) });
  trackFeature.value.setStyle(trackLineStyle());
  vectorSource.value.addFeature(trackFeature.value);
}

// 绘制小车
function drawCar(idx = 0, angle = 0) {
  if (carFeature.value) vectorSource.value.removeFeature(carFeature.value);
  const coord = fromLonLat(trackPoints.value[idx]);
  carFeature.value = new Feature({ geometry: new Point(coord) });
  carFeature.value.setStyle(carStyle(angle));
  vectorSource.value.addFeature(carFeature.value);
}

// 开始
function startTrack() {
  if (playing.value || finished.value) return;
  playing.value = true;
  finished.value = false;
  if (progress.value === 0) {
    drawCar(0, getAngle(trackPoints.value[0], trackPoints.value[1]));
  }
  runTrack();
}

// 开始
function runTrack() {
  if (timer.value) clearInterval(timer.value);
  timer.value = setInterval(() => {
    if (!playing.value) return;
    if (progress.value >= trackPoints.value.length - 1) {
      playing.value = false;
      finished.value = true;
      clearInterval(timer.value);
      return;
    }
    const idx = progress.value;
    const nextIdx = idx + 1;
    const angle = getAngle(trackPoints.value[idx], trackPoints.value[nextIdx]);
    drawCar(nextIdx, angle);
    progress.value = nextIdx;
  }, 600);
}

// 暂停
function pauseTrack() {
  playing.value = false;
  if (timer.value) clearInterval(timer.value);
}

// 继续
function resumeTrack() {
  if (playing.value || finished.value) return;
  playing.value = true;
  runTrack();
}

// 重置
function resetTrack() {
  playing.value = false;
  finished.value = false;
  progress.value = 0;
  if (timer.value) clearInterval(timer.value);
  drawCar(0, getAngle(trackPoints.value[0], trackPoints.value[1]));
}

onMounted(() => {
  vectorSource.value = new VectorSource();
  const vectorLayer = new VectorLayer({
    source: vectorSource.value,
    zIndex: 10,
  });
  const baseLayer = new TileLayer({
    source: new XYZ({
      url: "https://webrd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}",
    }),
  });
  map.value = new Map({
    target: mapContainer.value,
    layers: [baseLayer, vectorLayer],
    view: new View({
      center: fromLonLat(trackPoints.value[0]),
      zoom: 14,
    }),
    controls: defaultControls().extend([new FullScreen(), new ScaleLine()]),
  });
  drawTrack();
  drawCar(0, getAngle(trackPoints.value[0], trackPoints.value[1]));
});

onUnmounted(() => {
  if (map.value) map.value.dispose();
  if (timer.value) clearInterval(timer.value);
});
</script>

<style scoped>
.car-track-container {
  position: relative;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  background: linear-gradient(135deg, #1a237e, #4a148c);
  font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
.map {
  width: 100%;
  height: 100%;
  background: #0d47a1;
}
.map-controls {
  position: absolute;
  top: 20px;
  right: 20px;
  background: rgba(255, 255, 255, 0.95);
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
  z-index: 2;
  width: 320px;
  backdrop-filter: blur(5px);
  border: 1px solid rgba(255, 255, 255, 0.3);
}
.map-controls h3 {
  margin-top: 0;
  margin-bottom: 15px;
  color: #1a237e;
  font-size: 1.4rem;
}
.control-group {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 15px;
}
.control-btn {
  flex: 1 1 45%;
  padding: 12px 15px;
  border: none;
  border-radius: 8px;
  background: #3f51b5;
  color: white;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.3s ease;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.control-btn:disabled {
  background: #b0bec5;
  cursor: not-allowed;
}
.control-btn:hover:not(:disabled) {
  background: #303f9f;
  transform: translateY(-2px);
  box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
}
.stats-section {
  margin-top: 10px;
  font-size: 1.1rem;
  color: #1976d2;
}
.progress {
  font-weight: bold;
  color: #ffc107;
}
.status {
  font-weight: bold;
  color: #388e3c;
}
</style>
相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax