Skip to content

绘制箭头线

这案例让我想到了实习的时候,考核就是绘制箭头线,一晃好几年过去了,仍历历在目...

介绍

  1. 使用Math.atan2(dy, dx)计算角度
  2. 需要准备一张箭头图片
  3. forEachSegment遍历线条的每个线段,rotateWithView: true让箭头随地图旋转

核心逻辑:

js
geometry.forEachSegment((start, end) => {
  const dx = end[0] - start[0];
  const dy = end[1] - start[1];
  const rotation = Math.atan2(dy, dx);

  styles.push(
    new Style({
      geometry: new Point(end),
      image: new Icon({
        src: "/arrow.png",
        anchor: [0.75, 0.5],
        rotateWithView: true,
        rotation: -rotation,
      }),
    })
  );
});
展开代码
vue
<template>
  <div class="map-container">
    <div ref="mapContainer" id="map"></div>
    <div class="controls">
      <div class="info">
        <p>点击地图开始绘制带箭头的线条</p>
        <p>双击结束绘制</p>
      </div>
      <button @click="clearLines">清除所有线条</button>
      <div class="control-group">
        <label>线条颜色:</label>
        <input type="color" v-model="lineColor" @input="updateStyle" />
      </div>
      <div class="control-group">
        <label>线条宽度:</label>
        <input
          type="range"
          min="1"
          max="10"
          v-model="lineWidth"
          @input="updateStyle"
        />
        <span>{{ lineWidth }}px</span>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import Map from "ol/Map";
import View from "ol/View";
import TileLayer from "ol/layer/Tile";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import XYZ from "ol/source/XYZ";
import Draw from "ol/interaction/Draw";
import Point from "ol/geom/Point";
import { Style, Stroke, Icon } from "ol/style";
import "ol/ol.css";

const mapContainer = ref(null);
let map = null;
let source = null;
let vector = null;
let draw = null;

// 响应式变量
const lineColor = ref("#ffcc33");
const lineWidth = ref(2);

// 样式函数
const styleFunction = (feature) => {
  const geometry = feature.getGeometry();
  const styles = [
    // 线条样式
    new Style({
      stroke: new Stroke({
        color: lineColor.value,
        width: parseInt(lineWidth.value),
      }),
    }),
  ];

  // 为每个线段添加箭头
  geometry.forEachSegment((start, end) => {
    const dx = end[0] - start[0];
    const dy = end[1] - start[1];
    const rotation = Math.atan2(dy, dx);

    // 在线段末端添加箭头
    styles.push(
      new Style({
        geometry: new Point(end),
        image: new Icon({
          src: "/src/openLayers/assets/arrow.png",
          anchor: [0.75, 0.5],
          rotateWithView: true,
          rotation: -rotation,
          scale: 1.2,
        }),
      })
    );
  });

  return styles;
};

// 更新样式
const updateStyle = () => {
  if (vector) {
    vector.changed();
  }
};

// 清除所有线条
const clearLines = () => {
  if (source) {
    source.clear();
  }
};

// 添加绘制交互
const addDrawInteraction = () => {
  draw = new Draw({
    source: source,
    type: "LineString",
  });

  draw.on("drawstart", () => {
    console.log("开始绘制线条");
  });

  draw.on("drawend", (event) => {
    console.log("绘制完成", event.feature);
  });

  map.addInteraction(draw);
};

onMounted(() => {
  // 创建矢量数据源
  source = new VectorSource();

  // 创建矢量图层
  vector = new VectorLayer({
    source: source,
    style: styleFunction,
  });

  // 创建瓦片图层
  const raster = new TileLayer({
    source: new XYZ({
      url: "https://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}",
    }),
  });

  // 创建视图
  const view = new View({
    center: [116.4074, 39.9042], // 北京市中心经纬度
    zoom: 10,
    projection: "EPSG:4326",
  });

  // 初始化地图
  map = new Map({
    target: mapContainer.value,
    layers: [raster, vector],
    view,
  });

  // 添加绘制交互
  addDrawInteraction();
});

onUnmounted(() => {
  if (map) {
    map.setTarget(undefined);
    map = null;
  }
});
</script>

<style scoped>
.map-container {
  width: 100vw;
  height: 100vh;
  position: relative;
  font-family: sans-serif;
}

#map {
  width: 100%;
  height: 100%;
}

.controls {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 1000;
  background-color: rgba(255, 255, 255, 0.9);
  padding: 15px;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-width: 220px;
}

.info p {
  margin: 5px 0;
  font-size: 14px;
  color: #666;
}

.control-group {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 14px;
}

.control-group label {
  min-width: 80px;
  font-weight: 500;
}

.control-group input[type="color"] {
  width: 40px;
  height: 30px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.control-group input[type="range"] {
  flex: 1;
  min-width: 100px;
}

.control-group span {
  min-width: 40px;
  text-align: right;
  font-weight: bold;
  color: #007bff;
}

.controls button {
  padding: 8px 16px;
  background-color: #dc3545;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  font-size: 14px;
  transition: background-color 0.3s ease;
}

.controls button:hover {
  background-color: #c82333;
}
</style>