Leaflet地图

Leaflet 是一个开源并且对移动端友好的交互式地图 JavaScript 库。 它大小仅仅只有 42 KB of JS, 并且拥有绝大部分开发者所需要的所有地图特性 。

Leaflet 简单、高效并且易用。 它可以高效的运行在桌面和移动平台, 拥有着大量的 扩展插件、 优秀的文档、简单易用的 API 和完善的案例, 以及可读性较好的 源码 。

2023 年 5 月 18 日 — Leaflet 1.9.4 正式发布!

添加高德离线地图:

      const map = L.map("map").setView([28.2008247, 112.9812698], 13);

      L.tileLayer("http://127.0.0.1:8000/{z}/{x}/{y}/tile.png", {
        minZoom: 7,
        maxZoom: 16,
        attribution: "© contributors",
      }).addTo(map);

以下为部署的高德地图,“ {z}/{x}/{y}/tile.png”为固定写法,其它地图类似:

http://127.0.0.1:8000/{z}/{x}/{y}/tile.png

给地图标记添加自定义事件:

      const map = L.map("map").setView([28.2008247, 112.9812698], 13);

      L.tileLayer("http://127.0.0.1:8000/{z}/{x}/{y}/tile.png", {
        minZoom: 7,
        maxZoom: 16,
        attribution: "© contributors",
      }).addTo(map);

      const marker = L.marker([28.2008247, 112.9812698]).addTo(map);

      marker.data = {
        lat: "",
        lon: "",
      };

      marker.on("click", function (e) {
        console.log(e.target.data);
        window.open("http://www.xxx.com", "_blank");
      });

Leaflet弹框Popup中添加图片及链接:

    const map = L.map("map").setView([28.2008247, 112.9812698], 13);

    L.tileLayer("http://127.0.0.1:8000/{z}/{x}/{y}/tile.png", {
      minZoom: 7,
      maxZoom: 16,
      attribution: "© contributors",
    }).addTo(map);

    const marker = L.marker([28.2008247, 112.9812698]).addTo(map);

    marker
      .bindPopup(
        "<b>Hello world!</b><div><a href='http://www.baidu.com' target='_blank'><img width='110' src='https://img0.baidu.com/it/u=3620159376,1528769435&fm=253&fmt=auto&app=138&f=JPEG?w=640&h=480'/></a></div>", { permanent: true, direction: "top", offset: [0, -20] }
      )
      .openPopup();

Popup特点:1、如果有多个标记都添加了Popup,则一次只会有一个生效;2、可以添加链接;

Leaflet中添加标注Tooltip:

    const map = L.map("map").setView([28.2008247, 112.9812698], 13);

    L.tileLayer("http://127.0.0.1:8000/{z}/{x}/{y}/tile.png", {
      minZoom: 7,
      maxZoom: 16,
      attribution: "&copy; contributors",
    }).addTo(map);

    marker
      .addTo(map)
      .bindTooltip(
        "<b>Hello world!</b><div><a href='http://www.xxx.com' target='_blank'><img width='50' src='https://img0.baidu.com/it/u=3620159376,1528769435&fm=253&fmt=auto&app=138&f=JPEG?w=640&h=480'/></a></div>",
        { permanent: true, direction: "top", offset: [0, -20] }
      );

Tooltip特点: 1、多个标注可以同时显示;2、如果添加了链接,链接会不生效;

React+TypeScript实例:

import React, { useEffect, useRef } from "react";
import * as L from "leaflet";
import "leaflet/dist/leaflet.css";

const App: React.FC = () => {
  const mapRef = useRef<HTMLDivElement>(null);

  const points: [number, number][]= [
    [28.222, 112.92],
    [28.224, 112.922],
    [28.226, 112.924],
    [28.228, 112.926],
    [28.23, 112.928],
    [28.23, 112.93],
    [28.2321, 112.932],
    [28.234, 112.9342],
    [28.2361, 112.9361],
    [28.238, 112.938],
    [28.24, 112.94],
    [28.242, 112.942],
    [28.244, 112.944],
    [28.246, 112.946],
    [28.248, 112.948],
    [28.25, 112.95],
    [28.252, 112.952],
    [28.255, 112.955]
  ];

  const draw = (points: [number, number][], width: number) => {
      const map: L.Map = L.map(mapRef.current as HTMLInputElement).setView([28.23, 112.93], 13);

      L.tileLayer(
        "http://wprd01.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scl=1&style=7",
        {
          minZoom: 11,
          maxZoom: 16,
          attribution:
            '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        }
      ).addTo(map);

      L.marker([28.23, 112.93]).addTo(map);

      points.forEach(point => {
        L.circle(point, {
          color: "red",
          fillColor: "red",
          fillOpacity: 1,
          radius: 30
        }).addTo(map);
      });

      const circle = L.circle(points[points.length - 1], {
        color: "red",
        fillColor: "#f03",
        fillOpacity: 0.5,
        weight: 2,
        radius: 1200 * width
      }).addTo(map);

      circle.bindPopup(`<b>半径为${width}公里</b>`);
  };

  useEffect(() => {
    draw(points, 0.25);
  }, []);

  return <div ref={mapRef}  style={{ height: "100vh" }}></div>;
};
export default App;

HTML实例:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Leaflet</title>
    <link
      rel="stylesheet"
      href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
      integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
      crossorigin=""
    />
    <script
      src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
      integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
      crossorigin=""
    ></script>
    <style>
      #map {
        height: 100vh;
      }
      .text-icon {
        color: red;
      }
    </style>
  </head>

  <body>
    <div id="map"></div>

    <script>
      const map = L.map("map").setView([28.23, 112.93], 13);

      L.tileLayer(
        "http://wprd01.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scl=1&style=7",
        {
          minZoom: 11,
          maxZoom: 16,
          attribution:
            '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        }
      ).addTo(map);

      const marker = L.marker([28.23, 112.93]).addTo(map);

      const points = [
        [28.222, 112.92],
        [28.224, 112.922],
        [28.226, 112.924],
        [28.228, 112.926],
        [28.23, 112.928],
        [28.23, 112.93],
        [28.2321, 112.932],
        [28.234, 112.9342],
        [28.2361, 112.9361],
        [28.238, 112.938],
        [28.24, 112.94],
        [28.242, 112.942],
        [28.244, 112.944],
        [28.246, 112.946],
        [28.248, 112.948],
        [28.25, 112.95],
        [28.252, 112.952],
        [28.255, 112.955]
      ];

    // const myIcon = L.divIcon({
    //   html: "0.5km",
    //   className: "text-icon",
    //   fillColor: "red",
    //   iconSize: 30
    // });

    // 然后,使用这个icon创建一个标记并添加到地图上
    // L.marker([51.5, -0.09], {
    //   icon: myIcon
    // }).addTo(map);

    let moveLocationLayerGroup = L.layerGroup().addTo(map); // 定义一个图层,用于加载移动路径

    // width:半径,单位为公里
    function draw(points, width) {
      if (points.length === 0) {
        return;
      }

      map.removeLayer(moveLocationLayerGroup);
      moveLocationLayerGroup = L.layerGroup().addTo(map);

      const marker = L.marker(points[0]).addTo(moveLocationLayerGroup);

      let antPolyline = L.polyline.antPath(points, {
        // 点的集合
        color: "#006600",
        opacity: 6,
        fillColor: "#006600",
        pulseColor: "#e5ffe5",
        delay: 10000,
        dashArray: [10, 30]
      }).addTo(moveLocationLayerGroup);

      // 首先,创建一个icon的实例
      // const myIcon = L.icon({
      //   iconUrl: "images/navigate.svg", // 图标图片的URL
      //   iconSize: [24, 24], // 图标的大小
      //   iconAnchor: [15, 15], // 图标的锚点,即图标的位置应该放置在地图上的位置
      //   popupAnchor: [-3, -76] // 弹出框的锚点,即当你点击图标时,弹出框应该出现在哪个位置
      // });

      // L.marker(points[points.length - 1], { icon: myIcon }).addTo(
      //   moveLocationLayerGroup
      // );

      // L.popup()
      //   .setLatLng(points[points.length - 1])
      //   .setContent("I am a standalone popup.")
      //   .openOn(map);

      const circle = L.circle(points[points.length - 1], {
        stroke: true,
        dashArray: "4",
        color: "red",
        fillColor: "#f03",
        fillOpacity: 0.2,
        weight: 1,
        radius: 1200 * width
      }).addTo(moveLocationLayerGroup);

      L.circle(points[points.length - 1], {
        color: "red",
        fillColor: "red",
        fillOpacity: 1,
        weight: 1,
        radius: 400 * width
      }).addTo(moveLocationLayerGroup);

      function onCircleOver(e) {
        // L.popup()
        //   .setLatLng(points[points.length - 1])
        //   .setContent("半径为0.5km.")
        //   .openOn(map);

        alert("半径为0.5km.");
      }

      // circle.bindLabel(`<b>半径为${width}公里</b>`);
      circle.on("mouseover", onCircleOver);
    }

    let width = 0.25;

    setInterval(function () {
      points.push([28.23 + Math.random() / 20, 112.93 + Math.random() / 20]);

      // width += 0.01;
      draw(points, width);
    }, 2000);

    </script>
  </body>
</html>
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  <title>Leaflet</title>
  <link rel="stylesheet" href="leaflet.css" />
  <script src="leaflet.js"></script>
  <script src="leaflet-ant-path/leaflet-ant-path.js"></script>
  <script src="leaflet-trackplayer/leaflet-trackplayer.js"></script>
  <style>
    html,
    body {
      margin: 0;
      padding: 0;
      height: 100%;
    }

    #map {
      height: 100vh;
    }
  </style>
</head>

<body>
  <div id="map"></div>

  <script>
    const map = L.map("map").setView([39.898457, 116.391844], 13);

    L.tileLayer(
      "http://{s}.tile.osm.org/{z}/{x}/{y}.png",
      {
        minZoom: 7,
        maxZoom: 16,
        attribution:
          '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
      }
    ).addTo(map);

    const points = [

    ];

    const newPoints = [
      [39.898457, 116.391844],
      [39.898595, 116.377947],
      [39.898341, 116.368001],
      [39.898063, 116.357144],
      [39.899095, 116.351934],
      [39.905871, 116.35067],
      [39.922329, 116.3498],
      [39.931017, 116.349671],
      [39.939104, 116.349225],
      [39.942233, 116.34991],
      [39.947263, 116.366892],
      [39.947568, 116.387537],
      [39.947764, 116.401988],
      [39.947929, 116.410824],
      [39.947558, 116.42674],
      [39.9397, 116.427338],
      [39.932404, 116.427919],
      [39.923109, 116.428377],
      [39.907094, 116.429583],
      [39.906858, 116.41404],
      [39.906622, 116.405321],
      [39.906324, 116.394954],
      [39.906308, 116.391264],
      [39.916611, 116.390748],
    ];

    // const myIcon = L.divIcon({
    //   html: "0.5km",
    //   className: "text-icon",
    //   fillColor: "red",
    //   iconSize: 30
    // });

    // 然后,使用这个icon创建一个标记并添加到地图上
    // L.marker([51.5, -0.09], {
    //   icon: myIcon
    // }).addTo(map);

    let moveLocationLayerGroup = L.layerGroup().addTo(map); // 定义一个图层,用于加载移动路径

    // width:半径,单位为公里
    function draw(points, width) {
      if (points.length === 0) {
        return;
      }

      map.removeLayer(moveLocationLayerGroup);
      moveLocationLayerGroup = L.layerGroup().addTo(map);

      const marker = L.marker(points[0]).addTo(moveLocationLayerGroup);

      let antPolyline = L.polyline
        .antPath(points, {
          // 点的集合
          color: "#006600",
          opacity: 6,
          fillColor: "#006600",
          pulseColor: "#e5ffe5",
          delay: 10000,
          dashArray: [10, 30],
        })
        .addTo(moveLocationLayerGroup);

      // 首先,创建一个icon的实例
      // const myIcon = L.icon({
      //   iconUrl: "images/navigate.svg", // 图标图片的URL
      //   iconSize: [24, 24], // 图标的大小
      //   iconAnchor: [15, 15], // 图标的锚点,即图标的位置应该放置在地图上的位置
      //   popupAnchor: [-3, -76] // 弹出框的锚点,即当你点击图标时,弹出框应该出现在哪个位置
      // });

      // L.marker(points[points.length - 1], { icon: myIcon }).addTo(
      //   moveLocationLayerGroup
      // );

      // L.popup()
      //   .setLatLng(points[points.length - 1])
      //   .setContent("I am a standalone popup.")
      //   .openOn(map);

      const circle = L.circle(points[points.length - 1], {
        stroke: true,
        dashArray: "4",
        color: "red",
        fillColor: "#f03",
        fillOpacity: 0.2,
        weight: 1,
        radius: 1200 * width,
      }).addTo(moveLocationLayerGroup);

      L.circle(points[points.length - 1], {
        color: "red",
        fillColor: "red",
        fillOpacity: 1,
        weight: 1,
        radius: 400 * width,
      }).addTo(moveLocationLayerGroup);

      function onCircleOver(e) {
        // L.popup()
        //   .setLatLng(points[points.length - 1])
        //   .setContent("半径为0.5km.")
        //   .openOn(map);

        alert("半径为0.5km.");
      }

      // circle.bindLabel(`<b>半径为${width}公里</b>`);
      circle.on("mouseover", onCircleOver);
    }

    let width = 0.25;

    let index = 0;
    const timer = setInterval(function () {
      if (index < newPoints.length) {
        points.push(newPoints[index]);
        draw(points, width);
        index++;
      } else {
        clearTimeout(timer);
      }

      // width += 0.01;
      
    }, 1000);
  </script>
</body>

</html>

实时展示多条动态轨迹图:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Leaflet</title>
    <link rel="stylesheet" href="leaflet.css" />
    <script src="leaflet.js"></script>
    <script src="plugins/leaflet-ant-path.js"></script>
    <script src="plugins/leaflet.rotatedMarker.js"></script>
    <style>
      html,
      body {
        margin: 0;
        padding: 0;
        height: 100%;
      }

      #map {
        height: 100vh;
      }
    </style>
  </head>

  <body>
    <div id="map"></div>

    <script>
      // 计算点位方向
      function calculateBearing(lat1, lon1, lat2, lon2) {
        const dLon = (lon2 - lon1) * (Math.PI / 180);
        const y = Math.sin(dLon) * Math.cos(lat2 * (Math.PI / 180));
        const x =
          Math.cos(lat1 * (Math.PI / 180)) * Math.sin(lat2 * (Math.PI / 180)) -
          Math.sin(lat1 * (Math.PI / 180)) *
            Math.cos(lat2 * (Math.PI / 180)) *
            Math.cos(dLon);
        let bearing = Math.atan2(y, x) * (180 / Math.PI);

        // 将方位角规范化为0到360度之间
        if (bearing < 0) {
          bearing = (bearing + 360) % 360;
        }

        return bearing;
      }

      const map = L.map("map").setView([28.23, 112.93], 13);

      L.tileLayer(
        "http://wprd01.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scl=1&style=7",
        {
          minZoom: 11,
          maxZoom: 16
          // attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        }
      ).addTo(map);

      const points = [
        [28.222, 112.92],
        [28.224, 112.922],
        [28.226, 112.924],
        [28.228, 112.926],
        [28.23, 112.928],
        [28.23, 112.93],
        [28.2321, 112.932],
        [28.234, 112.9342],
        [28.2361, 112.9361],
        [28.238, 112.938],
        [28.24, 112.94],
        [28.242, 112.942],
        [28.244, 112.944],
        [28.246, 112.946],
        [28.248, 112.948],
        [28.25, 112.95],
        [28.252, 112.952],
        [28.255, 112.955]
      ];

      const polylineStyleConfig = [
        {
          color: "#006600",
          fillColor: "#006600",
          pulseColor: "#e5ffe5"
        },
        {
          color: "#0000cc",
          fillColor: "#0000cc",
          pulseColor: "#ccccff"
        },
        {
          color: "#e62f00",
          fillColor: "#e62f00",
          pulseColor: "#ffc2b3"
        },
        {
          color: "#8600b3",
          fillColor: "#8600b3",
          pulseColor: "#ecb3ff"
        },
        {
          color: "#b35900",
          fillColor: "#b35900",
          pulseColor: "#ffd9b3"
        },
        {
          color: "#ff9900",
          fillColor: "#ff9900",
          pulseColor: "#ffe0b3"
        },
        {
          color: "#00b2b3",
          fillColor: "#00b2b3",
          pulseColor: "#b3ffff"
        },
        {
          color: "#000000",
          fillColor: "#000000",
          pulseColor: "#FFFFFF"
        },
        {
          color: "#006699",
          fillColor: "#006699",
          pulseColor: "#b3e6ff"
        },
        {
          color: "#cc0099",
          fillColor: "#cc0099",
          pulseColor: "#ffb3ec"
        }
      ];

      const iconConfig = [
        "./icon/car0.svg",
        "./icon/car1.svg",
        "./icon/car2.svg",
        "./icon/car3.svg",
        "./icon/car4.svg",
        "./icon/car5.svg",
        "./icon/car6.svg",
        "./icon/car7.svg",
        "./icon/car8.svg",
        "./icon/car9.svg"
      ];

      const layerGroups = {};

      // width:半径,单位为公里
      function drawPolyline(
        points,
        width,
        polylineStyleConfig,
        iconConfig,
        layerGroupName
      ) {
        if (points.length === 0) {
          return;
        }

        if (layerGroups[layerGroupName]) {
          map.removeLayer(layerGroups[layerGroupName]);
          layerGroups[layerGroupName] = L.layerGroup().addTo(map);
        } else {
          layerGroups[layerGroupName] = L.layerGroup().addTo(map);
        }

        // 起始点
        const marker = L.marker(points[0], {
          icon: L.icon({
            iconUrl: "./icon/nav.svg", // 图标图片的URL
            iconSize: [32, 32], // 图标的大小
            iconAnchor: [15, 15], // 图标的锚点,即图标的位置应该放置在地图上的位置
            popupAnchor: [-3, -76] // 弹出框的锚点,即当你点击图标时,弹出框应该出现在哪个位置
          })
        }).addTo(layerGroups[layerGroupName]);

        // 线
        L.polyline
          .antPath(points, {
            // 点的集合
            ...polylineStyleConfig,
            opacity: 6,
            delay: 10000,
            dashArray: [5, 30]
          })
          .addTo(layerGroups[layerGroupName]);

        // 最新的点
        L.marker(points[points.length - 1], {
          icon: L.icon({
            iconUrl: iconConfig,
            iconSize: [30, 30], // icon的大小
            iconAnchor: [16, 15], // icon的渲染的位置(相对与marker)
            shadowAnchor: [0, 0], // shadow的渲染的位置(相对于marker)
            popupAnchor: [0, 0] //若是绑定了popup的popup的打开位置(相对于icon)
          }),
          title: "test",
          draggable: true,
          rotationAngle:
            points.length > 1
              ? calculateBearing(
                  points[points.length - 2][0],
                  points[points.length - 2][1],
                  points[points.length - 1][0],
                  points[points.length - 1][1]
                )
              : 0
        }).addTo(layerGroups[layerGroupName]);

        const circle = L.circle(points[points.length - 1], {
          stroke: true,
          dashArray: "4",
          color: "red",
          fillColor: "#f03",
          fillOpacity: 0.2,
          weight: 2,
          radius: 1200 * width
        }).addTo(layerGroups[layerGroupName]);

        circle.on("mouseover", e => {
          alert("半径为0.5km.");
        });
      }

      let width = 0.25;

      setInterval(function () {
        points.push([28.23 + Math.random() / 20, 112.93 + Math.random() / 20]);
        drawPolyline(points, width, polylineStyleConfig[0], iconConfig[0], "line1");
      }, 2000);
    </script>
  </body>
</html>

转化成class:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  <title>Leaflet</title>
  <link rel="stylesheet" href="leaflet.css" />
  <script src="leaflet.js"></script>
  <!--叶片状蚂蚁路径-->
  <script src="plugins/leaflet-ant-path.js"></script>
  <!--方向插件-->
  <script src="plugins/leaflet.rotatedMarker.js"></script>
  <style>
    html,
    body {
      margin: 0;
      padding: 0;
      height: 100%;
    }

    #map {
      height: 100vh;
    }
  </style>
</head>

<body>
  <div id="map"></div>

  <script>
    class AntPathPolyline {
      static polylineStyleConfig = [
        {
          color: "#006600",
          fillColor: "#006600",
          pulseColor: "#e5ffe5"
        },
        {
          color: "#0000cc",
          fillColor: "#0000cc",
          pulseColor: "#ccccff"
        },
        {
          color: "#e62f00",
          fillColor: "#e62f00",
          pulseColor: "#ffc2b3"
        },
        {
          color: "#8600b3",
          fillColor: "#8600b3",
          pulseColor: "#ecb3ff"
        },
        {
          color: "#b35900",
          fillColor: "#b35900",
          pulseColor: "#ffd9b3"
        },
        {
          color: "#ff9900",
          fillColor: "#ff9900",
          pulseColor: "#ffe0b3"
        },
        {
          color: "#00b2b3",
          fillColor: "#00b2b3",
          pulseColor: "#b3ffff"
        },
        {
          color: "#000000",
          fillColor: "#000000",
          pulseColor: "#FFFFFF"
        },
        {
          color: "#006699",
          fillColor: "#006699",
          pulseColor: "#b3e6ff"
        },
        {
          color: "#cc0099",
          fillColor: "#cc0099",
          pulseColor: "#ffb3ec"
        }
      ];

      static carIconConfig = [
        "./icons/car0.png",
        "./icons/car1.png",
        "./icons/car2.png",
        "./icons/car3.png",
        "./icons/car4.png",
        "./icons/car5.png",
        "./icons/car6.png",
        "./icons/car7.png",
        "./icons/car8.png",
        "./icons/car9.png"
      ];

      static positionIconConfig = "./icons/nav.svg";

      static layerGroups = {};

      // 获取当前位置方向
      static calculateBearing(lat1, lon1, lat2, lon2) {
        const dLon = (lon2 - lon1) * (Math.PI / 180);
        const y = Math.sin(dLon) * Math.cos(lat2 * (Math.PI / 180));
        const x =
          Math.cos(lat1 * (Math.PI / 180)) *
          Math.sin(lat2 * (Math.PI / 180)) -
          Math.sin(lat1 * (Math.PI / 180)) *
          Math.cos(lat2 * (Math.PI / 180)) *
          Math.cos(dLon);
        let bearing = Math.atan2(y, x) * (180 / Math.PI);

        // 将方位角规范化为0到360度之间
        if (bearing < 0) {
          bearing = (bearing + 360) % 360;
        }

        return bearing;
      }

      // 生成随机字符串
      static generateUniqueChar() {
        // 获取当前时间戳,确保每次调用都是唯一的
        const timestamp = new Date().getTime().toString(36);
        // 生成一个随机数,并转换为36进制字符串
        const randomValue = Math.random()
          .toString(36)
          .substr(2, 9);
        // 返回时间戳和随机数的组合,确保唯一性
        return timestamp + randomValue;
      }

      drawPolyline(
        map,
        points,
        width,
        polylineStyle,
        carIcon,
        layerGroupName
      ) {
        if (points.length === 0) {
          return;
        }

        if (AntPathPolyline.layerGroups[layerGroupName]) {
          map.removeLayer(AntPathPolyline.layerGroups[layerGroupName]);
          AntPathPolyline.layerGroups[layerGroupName] = L.layerGroup().addTo(
            map
          );
        } else {
          AntPathPolyline.layerGroups[layerGroupName] = L.layerGroup().addTo(
            map
          );
        }

        // 起始点
        const marker = L.marker(points[0], {
          icon: L.icon({
            iconUrl: AntPathPolyline.positionIconConfig, // 图标图片的URL
            iconSize: [32, 32], // 图标的大小
            iconAnchor: [15, 15], // 图标的锚点,即图标的位置应该放置在地图上的位置
            popupAnchor: [-3, -76] // 弹出框的锚点,即当你点击图标时,弹出框应该出现在哪个位置
          })
        }).addTo(AntPathPolyline.layerGroups[layerGroupName]);

        // 线
        L.polyline
          .antPath(points, {
            ...polylineStyle,
            opacity: 6,
            delay: 10000,
            dashArray: [5, 30]
          })
          .addTo(AntPathPolyline.layerGroups[layerGroupName]);

        // 最新的点
        L.marker(points[points.length - 1], {
          icon: L.icon({
            iconUrl: carIcon,
            iconSize: [30, 30], // icon的大小
            iconAnchor: [16, 15], // icon的渲染的位置(相对与marker)
            shadowAnchor: [0, 0], // shadow的渲染的位置(相对于marker)
            popupAnchor: [0, 0] //若是绑定了popup的popup的打开位置(相对于icon)
          }),
          title: "test",
          draggable: true,
          rotationAngle:
            points.length > 1
              ? AntPathPolyline.calculateBearing(
                points[points.length - 2][0],
                points[points.length - 2][1],
                points[points.length - 1][0],
                points[points.length - 1][1]
              )
              : 0
        }).addTo(AntPathPolyline.layerGroups[layerGroupName]);

        const circle = L.circle(points[points.length - 1], {
          stroke: true,
          dashArray: "4",
          color: "red",
          fillColor: "#f03",
          fillOpacity: 0.2,
          weight: 2,
          radius: 1200 * width
        }).addTo(AntPathPolyline.layerGroups[layerGroupName]);

        circle.on("mouseover", e => {
          alert("半径为0.5km.");
        });
      }
    }

    const map = L.map("map").setView([39.898457, 116.391844], 13);

    L.tileLayer("http://{s}.tile.osm.org/{z}/{x}/{y}.png", {
      minZoom: 7,
      maxZoom: 16,
      attribution:
        '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    }).addTo(map);

    const points1 = [];
    const points2 = [];

    const newPoints1 = [
      [39.898457, 116.391844],
      [39.898595, 116.377947],
      [39.898341, 116.368001],
      [39.898063, 116.357144],
      [39.899095, 116.351934],
      [39.905871, 116.35067],
      [39.922329, 116.3498],
      [39.931017, 116.349671],
      [39.939104, 116.349225],
      [39.942233, 116.34991],
      [39.947263, 116.366892],
      [39.947568, 116.387537],
      [39.947764, 116.401988],
      [39.947929, 116.410824],
      [39.947558, 116.42674],
      [39.9397, 116.427338],
      [39.932404, 116.427919],
      [39.923109, 116.428377],
      [39.907094, 116.429583],
      [39.906858, 116.41404],
      [39.906622, 116.405321],
      [39.906324, 116.394954],
      [39.906308, 116.391264],
      [39.916611, 116.390748]
    ];

    const newPoints2 = JSON.parse(JSON.stringify(newPoints1)).reverse();

    const test1 = new AntPathPolyline();
    const testName1 = AntPathPolyline.generateUniqueChar();

    const test2 = new AntPathPolyline();
    const testName2 = AntPathPolyline.generateUniqueChar();

    let width = 0.25;

    let index = 0;

    const timer = setInterval(function () {
      if (index < newPoints1.length) {
        points1.push(newPoints1[index]);
        points2.push(newPoints2[index]);
        test1.drawPolyline(
          map,
          points1,
          width,
          AntPathPolyline.polylineStyleConfig[0],
          AntPathPolyline.carIconConfig[0],
          testName1
        );
        test2.drawPolyline(
          map,
          points2,
          width,
          AntPathPolyline.polylineStyleConfig[1],
          AntPathPolyline.carIconConfig[1],
          testName2
        );
        index++;
      } else {
        clearTimeout(timer);
      }
    }, 1000);

    console.log(AntPathPolyline.generateUniqueChar());

    // 叶片状蚂蚁路径效果:https://blog.csdn.net/gitblog_00009/article/details/137951724
    // 叶片状蚂蚁路径插件:https://gitcode.com/rubenspgcavalcante/leaflet-ant-path/overview?utm_source=artical_gitcode&isLogin=1
    // 使用Leaflet.rotatedMaker进行航班飞行航向模拟的实践:https://blog.csdn.net/yelangkingwuzuhu/article/details/137154712
    // 方向插件:https://gitee.com/mirrors_bbecquet/Leaflet.RotatedMarker
    // Leaflet 带箭头轨迹以及沿轨迹带方向的动态marker:https://segmentfault.com/a/1190000039319512?sort=votes
  </script>
</body>

</html>

在vue3中实现:

package.json:

{
  "name": "leaflet-vue3",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "leaflet": "^1.9.4",
    "leaflet-ant-path": "^1.3.0",
    "leaflet-rotatedmarker": "^0.2.0",
    "leaflet-trackplayer": "^2.0.2",
    "vue": "^3.4.29"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.5",
    "vite": "^5.3.1"
  }
}

 /src/tools/AntPathPolyline.js

import { antPath } from 'leaflet-ant-path';
import 'leaflet-rotatedmarker'

class AntPathPolyline {
    static polylineStyleConfig = [
      {
        color: "#006600",
        fillColor: "#006600",
        pulseColor: "#e5ffe5"
      },
      {
        color: "#0000cc",
        fillColor: "#0000cc",
        pulseColor: "#ccccff"
      },
      {
        color: "#e62f00",
        fillColor: "#e62f00",
        pulseColor: "#ffc2b3"
      },
      {
        color: "#8600b3",
        fillColor: "#8600b3",
        pulseColor: "#ecb3ff"
      },
      {
        color: "#b35900",
        fillColor: "#b35900",
        pulseColor: "#ffd9b3"
      },
      {
        color: "#ff9900",
        fillColor: "#ff9900",
        pulseColor: "#ffe0b3"
      },
      {
        color: "#00b2b3",
        fillColor: "#00b2b3",
        pulseColor: "#b3ffff"
      },
      {
        color: "#000000",
        fillColor: "#000000",
        pulseColor: "#FFFFFF"
      },
      {
        color: "#006699",
        fillColor: "#006699",
        pulseColor: "#b3e6ff"
      },
      {
        color: "#cc0099",
        fillColor: "#cc0099",
        pulseColor: "#ffb3ec"
      }
    ];

    static carIconConfig = [
      "src/assets/icons/car0.png",
      "src/assets/icons/car1.png",
      "src/assets/icons/car2.png",
      "src/assets/icons/car3.png",
      "src/assets/icons/car4.png",
      "src/assets/icons/car5.png",
      "src/assets/icons/car6.png",
      "src/assets/icons/car7.png",
      "src/assets/icons/car8.png",
      "src/assets/icons/car9.png"
    ];

    static positionIconConfig = "src/assets/icons/nav.svg";

    static layerGroups = {};

    // 获取当前位置方向
    static calculateBearing(lat1, lon1, lat2, lon2) {
      const dLon = (lon2 - lon1) * (Math.PI / 180);
      const y = Math.sin(dLon) * Math.cos(lat2 * (Math.PI / 180));
      const x =
        Math.cos(lat1 * (Math.PI / 180)) *
        Math.sin(lat2 * (Math.PI / 180)) -
        Math.sin(lat1 * (Math.PI / 180)) *
        Math.cos(lat2 * (Math.PI / 180)) *
        Math.cos(dLon);
      let bearing = Math.atan2(y, x) * (180 / Math.PI);

      // 将方位角规范化为0到360度之间
      if (bearing < 0) {
        bearing = (bearing + 360) % 360;
      }

      return bearing;
    }

    // 生成随机字符串
    static generateUniqueChar() {
      // 获取当前时间戳,确保每次调用都是唯一的
      const timestamp = new Date().getTime().toString(36);
      // 生成一个随机数,并转换为36进制字符串
      const randomValue = Math.random()
        .toString(36)
        .substr(2, 9);
      // 返回时间戳和随机数的组合,确保唯一性
      return timestamp + randomValue;
    }

    drawPolyline(
      map,
      points,
      width,
      polylineStyle,
      carIcon,
      layerGroupName
    ) {
      if (points.length === 0) {
        return;
      }

      if (AntPathPolyline.layerGroups[layerGroupName]) {
        map.removeLayer(AntPathPolyline.layerGroups[layerGroupName]);
        AntPathPolyline.layerGroups[layerGroupName] = L.layerGroup().addTo(
          map
        );
      } else {
        AntPathPolyline.layerGroups[layerGroupName] = L.layerGroup().addTo(
          map
        );
      }

      // 起始点
      const marker = L.marker(points[0], {
        icon: L.icon({
          iconUrl: AntPathPolyline.positionIconConfig, // 图标图片的URL
          iconSize: [32, 32], // 图标的大小
          iconAnchor: [15, 15], // 图标的锚点,即图标的位置应该放置在地图上的位置
          popupAnchor: [-3, -76] // 弹出框的锚点,即当你点击图标时,弹出框应该出现在哪个位置
        })
      }).addTo(AntPathPolyline.layerGroups[layerGroupName]);

      // 线
      L.polyline
        .antPath(points, {
          ...polylineStyle,
          opacity: 6,
          delay: 10000,
          dashArray: [5, 30]
        })
        .addTo(AntPathPolyline.layerGroups[layerGroupName]);

      // 最新的点
      L.marker(points[points.length - 1], {
        icon: L.icon({
          iconUrl: carIcon,
          iconSize: [30, 30], // icon的大小
          iconAnchor: [16, 15], // icon的渲染的位置(相对与marker)
          shadowAnchor: [0, 0], // shadow的渲染的位置(相对于marker)
          popupAnchor: [0, 0] //若是绑定了popup的popup的打开位置(相对于icon)
        }),
        title: "test",
        draggable: true,
        rotationAngle:
          points.length > 1
            ? AntPathPolyline.calculateBearing(
              points[points.length - 2][0],
              points[points.length - 2][1],
              points[points.length - 1][0],
              points[points.length - 1][1]
            )
            : 0
      }).addTo(AntPathPolyline.layerGroups[layerGroupName]);

      const circle = L.circle(points[points.length - 1], {
        stroke: true,
        dashArray: "4",
        color: "red",
        fillColor: "#f03",
        fillOpacity: 0.2,
        weight: 2,
        radius: 1200 * width
      }).addTo(AntPathPolyline.layerGroups[layerGroupName]);

      circle.on("mouseover", e => {
        alert("半径为0.5km.");
      });
    }
  }

  export default AntPathPolyline;

src/App.vue

<template>
  <div id="map"></div>
</template>

<script setup>
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import { onMounted } from "vue";
import AntPathPolyline from "./tools/AntPathPolyline";
onMounted(() => {
  const map = L.map("map").setView([39.898457, 116.391844], 13);

  L.tileLayer("http://{s}.tile.osm.org/{z}/{x}/{y}.png", {
    minZoom: 7,
    maxZoom: 16,
    attribution:
      '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
  }).addTo(map);

  const points1 = [];
  const points2 = [];

  const newPoints1 = [
    [39.898457, 116.391844],
    [39.898595, 116.377947],
    [39.898341, 116.368001],
    [39.898063, 116.357144],
    [39.899095, 116.351934],
    [39.905871, 116.35067],
    [39.922329, 116.3498],
    [39.931017, 116.349671],
    [39.939104, 116.349225],
    [39.942233, 116.34991],
    [39.947263, 116.366892],
    [39.947568, 116.387537],
    [39.947764, 116.401988],
    [39.947929, 116.410824],
    [39.947558, 116.42674],
    [39.9397, 116.427338],
    [39.932404, 116.427919],
    [39.923109, 116.428377],
    [39.907094, 116.429583],
    [39.906858, 116.41404],
    [39.906622, 116.405321],
    [39.906324, 116.394954],
    [39.906308, 116.391264],
    [39.916611, 116.390748],
  ];

  const newPoints2 = JSON.parse(JSON.stringify(newPoints1)).reverse();

  const test1 = new AntPathPolyline();
  const testName1 = AntPathPolyline.generateUniqueChar();

  const test2 = new AntPathPolyline();
  const testName2 = AntPathPolyline.generateUniqueChar();

  let width = 0.25;

  let index = 0;

  const timer = setInterval(function () {
    if (index < newPoints1.length) {
      points1.push(newPoints1[index]);
      points2.push(newPoints2[index]);
      test1.drawPolyline(
        map,
        points1,
        width,
        AntPathPolyline.polylineStyleConfig[0],
        AntPathPolyline.carIconConfig[0],
        testName1
      );
      test2.drawPolyline(
        map,
        points2,
        width,
        AntPathPolyline.polylineStyleConfig[1],
        AntPathPolyline.carIconConfig[1],
        testName2
      );
      index++;
    } else {
      clearTimeout(timer);
    }
  }, 1000);
});
</script>
<style scoped>
#map {
  height: 100% !important;
  width: 100% !important;
}
</style>

 

添加操作功能:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Leaflet</title>
    <link rel="stylesheet" href="leaflet.css" />
    <script src="leaflet.js"></script>
    <!--叶片状蚂蚁路径-->
    <script src="plugins/leaflet-ant-path.js"></script>
    <!--方向插件-->
    <script src="plugins/leaflet.rotatedMarker.js"></script>
    <style>
      html,
      body {
        margin: 0;
        padding: 0;
        height: 100%;
      }

      #search {
        height: 60px;
        width: 100%;
        position: fixed;
        top: 0;
        left: 0;
        background: #000000;
        z-index: 10000000;
      }

      #search-btn,
      #stop-btn {
        cursor: pointer;
      }

      #map {
        height: 100vh;
      }
    </style>
  </head>

  <body>
    <div id="search">
      <input id="search-input" />
      <button id="search-btn">搜索</button>
      <button id="stop-btn">停止</button>
    </div>
    <div id="map"></div>

    <script>
      class AntPathPolyline {
        static polylineStyleConfig = [
          {
            color: "#006600",
            fillColor: "#006600",
            pulseColor: "#e5ffe5"
          },
          {
            color: "#0000cc",
            fillColor: "#0000cc",
            pulseColor: "#ccccff"
          },
          {
            color: "#e62f00",
            fillColor: "#e62f00",
            pulseColor: "#ffc2b3"
          },
          {
            color: "#8600b3",
            fillColor: "#8600b3",
            pulseColor: "#ecb3ff"
          },
          {
            color: "#b35900",
            fillColor: "#b35900",
            pulseColor: "#ffd9b3"
          },
          {
            color: "#ff9900",
            fillColor: "#ff9900",
            pulseColor: "#ffe0b3"
          },
          {
            color: "#00b2b3",
            fillColor: "#00b2b3",
            pulseColor: "#b3ffff"
          },
          {
            color: "#000000",
            fillColor: "#000000",
            pulseColor: "#FFFFFF"
          },
          {
            color: "#006699",
            fillColor: "#006699",
            pulseColor: "#b3e6ff"
          },
          {
            color: "#cc0099",
            fillColor: "#cc0099",
            pulseColor: "#ffb3ec"
          }
        ];

        static carIconConfig = [
          "./icons/car0.png",
          "./icons/car1.png",
          "./icons/car2.png",
          "./icons/car3.png",
          "./icons/car4.png",
          "./icons/car5.png",
          "./icons/car6.png",
          "./icons/car7.png",
          "./icons/car8.png",
          "./icons/car9.png"
        ];

        static positionIconConfig = "./icons/nav.svg";

        static layerGroups = {};

        // 获取当前位置方向
        static calculateBearing(lat1, lon1, lat2, lon2) {
          const dLon = (lon2 - lon1) * (Math.PI / 180);
          const y = Math.sin(dLon) * Math.cos(lat2 * (Math.PI / 180));
          const x =
            Math.cos(lat1 * (Math.PI / 180)) *
              Math.sin(lat2 * (Math.PI / 180)) -
            Math.sin(lat1 * (Math.PI / 180)) *
              Math.cos(lat2 * (Math.PI / 180)) *
              Math.cos(dLon);
          let bearing = Math.atan2(y, x) * (180 / Math.PI);

          // 将方位角规范化为0到360度之间
          if (bearing < 0) {
            bearing = (bearing + 360) % 360;
          }

          return bearing;
        }

        // 生成随机字符串
        static generateUniqueChar() {
          // 获取当前时间戳,确保每次调用都是唯一的
          const timestamp = new Date().getTime().toString(36);
          // 生成一个随机数,并转换为36进制字符串
          const randomValue = Math.random()
            .toString(36)
            .substr(2, 9);
          // 返回时间戳和随机数的组合,确保唯一性
          return timestamp + randomValue;
        }

        drawPolyline(
          map,
          points,
          width,
          polylineStyle,
          carIcon,
          layerGroupName
        ) {
          if (points.length === 0) {
            return;
          }

          if (AntPathPolyline.layerGroups[layerGroupName]) {
            map.removeLayer(AntPathPolyline.layerGroups[layerGroupName]);
            AntPathPolyline.layerGroups[layerGroupName] = L.layerGroup().addTo(
              map
            );
          } else {
            AntPathPolyline.layerGroups[layerGroupName] = L.layerGroup().addTo(
              map
            );
          }

          // 起始点
          const marker = L.marker(points[0], {
            icon: L.icon({
              iconUrl: AntPathPolyline.positionIconConfig, // 图标图片的URL
              iconSize: [32, 32], // 图标的大小
              iconAnchor: [15, 15], // 图标的锚点,即图标的位置应该放置在地图上的位置
              popupAnchor: [-3, -76] // 弹出框的锚点,即当你点击图标时,弹出框应该出现在哪个位置
            })
          }).addTo(AntPathPolyline.layerGroups[layerGroupName]);

          // 线
          L.polyline
            .antPath(points, {
              ...polylineStyle,
              opacity: 6,
              delay: 10000,
              dashArray: [5, 30]
            })
            .addTo(AntPathPolyline.layerGroups[layerGroupName]);

          // 最新的点
          L.marker(points[points.length - 1], {
            icon: L.icon({
              iconUrl: carIcon,
              iconSize: [30, 30], // icon的大小
              iconAnchor: [16, 15], // icon的渲染的位置(相对与marker)
              shadowAnchor: [0, 0], // shadow的渲染的位置(相对于marker)
              popupAnchor: [0, 0] //若是绑定了popup的popup的打开位置(相对于icon)
            }),
            title: "test",
            draggable: true,
            rotationAngle:
              points.length > 1
                ? AntPathPolyline.calculateBearing(
                    points[points.length - 2][0],
                    points[points.length - 2][1],
                    points[points.length - 1][0],
                    points[points.length - 1][1]
                  )
                : 0
          }).addTo(AntPathPolyline.layerGroups[layerGroupName]);

          const circle = L.circle(points[points.length - 1], {
            stroke: true,
            dashArray: "4",
            color: "red",
            fillColor: "#f03",
            fillOpacity: 0.2,
            weight: 2,
            radius: 1200 * width
          }).addTo(AntPathPolyline.layerGroups[layerGroupName]);

          circle.on("mouseover", e => {
            alert("半径为0.5km.");
          });
        }
      }

      const map = L.map("map").setView([28.23, 112.93], 13);

      L.tileLayer(
        "http://wprd01.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scl=1&style=7",
        {
          minZoom: 11,
          maxZoom: 16
          // attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        }
      ).addTo(map);

      let width = 0.25;
      let timers = [];
      let points = {};

      document.getElementById("search-btn").onclick = function() {
        const allLayers = map._layers; // 注意:_layers是一个内部属性,可能在未来版本中改变

        // 遍历所有图层,并检查它们是否是L.layerGroup的实例
        for (const layerId in allLayers) {
          const layer = allLayers[layerId];

          // 检查图层是否是L.layerGroup的实例
          if (layer instanceof L.LayerGroup) {
            // 如果是,则从地图上移除它
            map.removeLayer(layer);
          }

          // 注意:对于嵌套的layerGroup(即layerGroup中包含其他layerGroup),
          // 你可能需要递归地处理它们,但上面的代码只处理了一级layerGroup。
        }

        if (timers.length > 0) {
          for (let i = 0; i < timers.length; i++) {
            clearInterval(timers[i]);
          }
        }

        timers = [];
        points = {};

        const polylineArr = ["a", "b", "c", "d", "e"];

        for (let i = 0; i < polylineArr.length; i++) {
          const antPathPolylineObj = new AntPathPolyline();
          const antPathPolylineName = AntPathPolyline.generateUniqueChar();
          points["data" + i] = [];

          const timer = setInterval(function() {
            points["data" + i].push([
              28.23 + ((1 + i) * Math.random()) / 20,
              112.93 + ((1 + i) * Math.random()) / 20
            ]);
            antPathPolylineObj.drawPolyline(
              map,
              points["data" + i],
              width,
              AntPathPolyline.polylineStyleConfig[i],
              AntPathPolyline.carIconConfig[i],
              antPathPolylineName
            );
          }, 1000);

          timers.push(timer);
        }
      };

      document.getElementById("stop-btn").onclick = function() {
        if (timers.length > 0) {
          for (let i = 0; i < timers.length; i++) {
            clearInterval(timers[i]);
          }
        }
      };
    </script>
  </body>
</html>

Leaflet 带箭头轨迹以及沿轨迹带方向的动态marker:

<!DOCTYPE html>
<html>
  <head>
    <title>Leaflet.RouteAnimate</title>
    <meta charset="utf-8" />
    <!-- 引入leafletapi -->
    <link rel="stylesheet" href="./leaflet.css" />
    <script src="./leaflet.js"></script>

    <!-- 引入插件 -->
    <script src="./plugins/leaflet.polylineDecorator.js"></script>
    <script src="./plugins/Leaflet.AnimatedMarker.js"></script>

    <style>
      body {
        margin: 0;
      }

      .map {
        position: absolute;
        height: 100%;
        right: 0;
        left: 0;
      }

      .menuBar {
        position: relative;
        text-align: center;
        top: 10px;
        margin: 0 50px;
        padding: 5px;
        border-radius: 3px;
        z-index: 999;
        color: #ffffff;
        background-color: rgba(0, 168, 0, 1);
      }
    </style>
  </head>

  <body>
    <div class="map" id="map"></div>
    <div class="menuBar">
      <input type="button" value="开始" onclick="startClick()" />
      <input type="button" value="暂停" onclick="pauseClick()" />
      <input type="button" value="加速" onclick="speetUp()" />
      <input type="button" value="减速" onclick="speetDown()" />
      <input type="button" value="停止" onclick="stopClick()" />
    </div>
  </body>

  <script>
    // 初始化地图
    var map = L.map("map", {
      center: [39.924317, 116.390619],
      zoom: 14,
      preferCanvas: true // 使用canvas模式渲染矢量图形
    });
    // 添加底图
    var tiles = L.tileLayer("http://{s}.tile.osm.org/{z}/{x}/{y}.png").addTo(
      map
    );

    var latlngs = [
      [39.898457, 116.391844],
      [39.898595, 116.377947],
      [39.898341, 116.368001],
      [39.898063, 116.357144],
      [39.899095, 116.351934],
      [39.905871, 116.35067],
      [39.922329, 116.3498],
      [39.931017, 116.349671],
      [39.939104, 116.349225],
      [39.942233, 116.34991],
      [39.947263, 116.366892],
      [39.947568, 116.387537],
      [39.947764, 116.401988],
      [39.947929, 116.410824],
      [39.947558, 116.42674],
      [39.9397, 116.427338],
      [39.932404, 116.427919],
      [39.923109, 116.428377],
      [39.907094, 116.429583],
      [39.906858, 116.41404],
      [39.906622, 116.405321],
      [39.906324, 116.394954],
      [39.906308, 116.391264],
      [39.916611, 116.390748]
    ];
    var speedList = [
      1,
      1,
      2,
      2,
      3,
      3,
      3,
      4,
      4,
      4,
      4,
      4,
      4,
      5,
      5,
      4,
      4,
      4,
      3,
      2,
      2,
      1,
      1,
      1
    ];
    // 轨迹线
    var routeLine = L.polyline(latlngs, {
      weight: 8
    }).addTo(map);
    // 实时轨迹线
    var realRouteLine = L.polyline([], {
      weight: 8,
      color: "#FF9900"
    }).addTo(map);
    // 轨迹方向箭头
    var decorator = L.polylineDecorator(routeLine, {
      patterns: [
        {
          repeat: 50,
          symbol: L.Symbol.arrowHead({
            pixelSize: 5,
            headAngle: 75,
            polygon: false,
            pathOptions: {
              stroke: true,
              weight: 2,
              color: "#FFFFFF"
            }
          })
        }
      ]
    }).addTo(map);

    var carIcon = L.icon({
      iconSize: [37, 26],
      iconAnchor: [19, 13],
      iconUrl: "./icons/car.png"
    });
    // 动态marker
    var animatedMarker = L.animatedMarker(routeLine.getLatLngs(), {
      speedList: speedList,
      interval: 200, // 默认为100mm
      icon: carIcon,
      playCall: updateRealLine
    }).addTo(map);
    var newLatlngs = [routeLine.getLatLngs()[0]];

    // 绘制已行走轨迹线(橙色那条)
    function updateRealLine(latlng) {
      newLatlngs.push(latlng);
      realRouteLine.setLatLngs(newLatlngs);
    }

    let speetX = 1; // 默认速度倍数
    // 加速
    function speetUp() {
      speetX = speetX * 2;
      animatedMarker.setSpeetX(speetX);
    }

    // 减速
    function speetDown() {
      speetX = speetX / 2;
      animatedMarker.setSpeetX(speetX);
    }

    // 开始
    function startClick() {
      animatedMarker.start();
    }

    // 暂停
    function pauseClick() {
      animatedMarker.pause();
    }

    // 停止
    function stopClick() {
      newLatlngs = [];
      animatedMarker.stop();
    }
  </script>
</html>

轨迹回放效果&控制台控制轨迹运动效果

App.vue

<template>
  <div id="map"></div>
</template>

<script setup lang="ts">
import { onMounted } from "vue";
import L from "leaflet";
import "leaflet-trackplayer";
import "leaflet/dist/leaflet.css";
import CAR from "@/assets/icons/car0.png";
import path from "@/tools/points";

onMounted(() => {
  let map = null;
  const sourceUrl =
    "https://server.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}.png";
  map = L.map("map").setView([23, 120], 15);
  const tileLayer = L.tileLayer(sourceUrl, {
    maxZoom: 18,
    minZoom: 2,
    attribution: "© modify",
  });
  tileLayer.addTo(map);

  let track = null;
  const initPath = () => {
    // 定义沿着轨迹移动的Icon
    let markerIcon = L.icon({
      iconSize: [28, 28],
      iconUrl: CAR, // 前面导入的img资源
      iconAnchor: [12, 12],
    });
    // 创建播放器对象并添加至地图
    track = new L.TrackPlayer(
      path,
      // 轨迹配置,都可以不要,保留markerIcon一个就可以了
      {
        markerIcon,
        speed: 500, // 播放速度
        weight: 6, // 轨迹线宽度
        passedLineColor: "#006600", // 已行驶轨迹部分的颜色
        notPassedLineColor: "red", // 未行驶轨迹部分的颜色
        panTo: true, // 地图跟随移动
        // 轨迹箭头样式
        polylineDecoratorOptions: {
          patterns: [
            /**
             * offset 第一个图案符号的偏移量,从线的起点开始。默认值:0
             * endOffset 最后一个图案符号的最小偏移量,从线的端点开始。默认值:0
             * repeat 重复间隔。定义每个连续符号的锚点之间的距离
             * symbol 图标样式
             * */
            {
              offset: 0,
              repeat: 40,
              symbol: L.Symbol.arrowHead({
                pixelSize: 5,
                pathOptions: { color: "#fbeee2", weight: 2, stroke: true },
              }),
            },
          ],
        },
        markerRotation: true, // 是否开启marker的旋转
      }
    ).addTo(map);
    track.start();
    // 停止播放
    // track.pause();
    // 清除轨迹
    // track.remove();
  };

  initPath();
});

// https://github.com/weijun-lab/Leaflet.TrackPlayer/blob/master/examples/index.html
// https://blog.csdn.net/qq_44973159/article/details/139859569
</script>

<style scoped>
#map {
  height: 100%;
}
</style>

package.json

{
  "name": "latest-vue3-ts",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build --mode production",
    "preview": "vite preview",
    "build-only": "vite build",
    "type-check": "vue-tsc --build --force"
  },
  "dependencies": {
    "leaflet": "^1.9.4",
    "leaflet-trackplayer": "^2.0.2",
    "vue": "^3.4.29",
  },
  "devDependencies": {
    "@tsconfig/node20": "^20.1.4",
    "@types/leaflet": "^1.9.12",
    "@types/node": "^20.14.5",
    "@vitejs/plugin-vue": "^5.0.5",
    "@vue/tsconfig": "^0.5.1",
    "npm-run-all2": "^6.2.0",
    "typescript": "~5.4.0",
    "unplugin-auto-import": "^0.17.6",
    "unplugin-vue-components": "^0.27.0",
    "vite": "^5.3.1",
    "vite-plugin-vue-setup-extend": "^0.4.0",
    "vue-tsc": "^2.0.21"
  }
}

/src/tools/points.ts

let path = [
    {
      lat: 34.25615548523744,
      lng: 108.91164044842363,
    },
    {
      lat: 34.256155386830855,
      lng: 108.91179023713374,
    },
    {
      lat: 34.256155386830855,
      lng: 108.91179023713374,
    },
    {
      lat: 34.25607942383744,
      lng: 108.91177925878043,
    },
    {
      lat: 34.255720609670156,
      lng: 108.91171038494707,
    },
    {
      lat: 34.255607664345405,
      lng: 108.91169441655762,
    },
    {
      lat: 34.25553269366626,
      lng: 108.91169442258713,
    },
    {
      lat: 34.25544769867856,
      lng: 108.91173736885014,
    },
    {
      lat: 34.25544769867856,
      lng: 108.91173736885014,
    },
    {
      lat: 34.25541482067108,
      lng: 108.91157060617357,
    },
    {
      lat: 34.255437230925885,
      lng: 108.91091151687152,
    },
    {
      lat: 34.2554647726071,
      lng: 108.90999074936826,
    },
    {
      lat: 34.255474922592086,
      lng: 108.90972209999609,
    },
    {
      lat: 34.255470035735925,
      lng: 108.90952435653506,
    },
    {
      lat: 34.25546585239153,
      lng: 108.90796530095042,
    },
    {
      lat: 34.255466079902156,
      lng: 108.90748786950532,
    },
    {
      lat: 34.255466139078585,
      lng: 108.90736001962813,
    },
    {
      lat: 34.25546047844199,
      lng: 108.90659889522819,
    },
    {
      lat: 34.25545553696015,
      lng: 108.90646504623344,
    },
    {
      lat: 34.255455684520776,
      lng: 108.90610644487133,
    },
    {
      lat: 34.25543990484673,
      lng: 108.90555904106137,
    },
    {
      lat: 34.255434929044085,
      lng: 108.90550010453336,
    },
    {
      lat: 34.25671044153241,
      lng: 108.90546803620235,
    },
    {
      lat: 34.256994331993134,
      lng: 108.9054630187248,
    },
    {
      lat: 34.2573861821876,
      lng: 108.90545199896282,
    },
    {
      lat: 34.2583997892619,
      lng: 108.90543593456538,
    },
    {
      lat: 34.25896857276571,
      lng: 108.90541491120209,
    },
    {
      lat: 34.2600241555513,
      lng: 108.90541482639716,
    },
    {
      lat: 34.26038901329847,
      lng: 108.9054088034598,
    },
    {
      lat: 34.260957801498556,
      lng: 108.9053717970368,
    },
    {
      lat: 34.261048767618306,
      lng: 108.90536579609017,
    },
    {
      lat: 34.26174549083055,
      lng: 108.90536574011179,
    },
    {
      lat: 34.262888033588865,
      lng: 108.9053716419483,
    },
    {
      lat: 34.263321862668384,
      lng: 108.90536561345179,
    },
    {
      lat: 34.26381066919356,
      lng: 108.90536057947523,
    },
    {
      lat: 34.264314469827035,
      lng: 108.90535454535133,
    },
    {
      lat: 34.264416428997436,
      lng: 108.90535453715839,
    },
    {
      lat: 34.264545377344014,
      lng: 108.90535452679667,
    },
    {
      lat: 34.26485025108296,
      lng: 108.90536549063917,
    },
    {
      lat: 34.26494221420379,
      lng: 108.90536548324928,
    },
    {
      lat: 34.265745895588346,
      lng: 108.9053544303257,
    },
    {
      lat: 34.26596581086138,
      lng: 108.90534442324677,
    },
    {
      lat: 34.2664006399377,
      lng: 108.90533339995,
    },
    {
      lat: 34.26711335674291,
      lng: 108.90532235431407,
    },
    {
      lat: 34.267682127119045,
      lng: 108.90532230860484,
    },
    {
      lat: 34.267977007932025,
      lng: 108.90532228490632,
    },
    {
      lat: 34.26842182796332,
      lng: 108.90532224915717,
    },
    {
      lat: 34.26893662309246,
      lng: 108.90531221835984,
    },
    {
      lat: 34.26961734908727,
      lng: 108.90530616999233,
    },
    {
      lat: 34.27079687296456,
      lng: 108.90529608575685,
    },
    {
      lat: 34.27079687296456,
      lng: 108.90529608575685,
    },
    {
      lat: 34.270796835711245,
      lng: 108.90539697877264,
    },
    {
      lat: 34.27080243135706,
      lng: 108.90641586657812,
    },
    {
      lat: 34.270802076591195,
      lng: 108.9072299373526,
    },
    {
      lat: 34.270812817234265,
      lng: 108.90777629238795,
    },
    {
      lat: 34.270822675023936,
      lng: 108.90806094950152,
    },
    {
      lat: 34.27082259586891,
      lng: 108.90822075550248,
    },
    {
      lat: 34.27082849640933,
      lng: 108.9084135191401,
    },
    {
      lat: 34.27083332877497,
      lng: 108.90873512064815,
    },
    {
      lat: 34.27083823372032,
      lng: 108.90891189899708,
    },
    {
      lat: 34.270843970260856,
      lng: 108.9093942860198,
    },
    {
      lat: 34.270843671165785,
      lng: 108.90992459787954,
    },
    {
      lat: 34.27084322644142,
      lng: 108.91067459821011,
    },
    {
      lat: 34.270842940218785,
      lng: 108.91113596785353,
    },
    {
      lat: 34.270842859216124,
      lng: 108.91126379113685,
    },
    {
      lat: 34.270847625398574,
      lng: 108.91162328889843,
    },
    {
      lat: 34.27084755144006,
      lng: 108.91173612991112,
    },
    {
      lat: 34.27085335722669,
      lng: 108.91202471962777,
    },
    {
      lat: 34.270852784122816,
      lng: 108.9128555142759,
    },
    {
      lat: 34.27085267748,
      lng: 108.91300529292631,
    },
    {
      lat: 34.27085254672574,
      lng: 108.91318702269936,
    },
    {
      lat: 34.27085205628723,
      lng: 108.91385101989933,
    },
    {
      lat: 34.27087119213721,
      lng: 108.91615435172467,
    },
    {
      lat: 34.27087566746897,
      lng: 108.91675434843464,
    },
    {
      lat: 34.2708814553737,
      lng: 108.91698994875553,
    },
    {
      lat: 34.27085429757733,
      lng: 108.9171776307563,
    },
    {
      lat: 34.27080602434836,
      lng: 108.91749908177066,
    },
    {
      lat: 34.27080602434836,
      lng: 108.91749908177066,
    },
    {
      lat: 34.270751948023054,
      lng: 108.91760590116054,
    },
    {
      lat: 34.27073590010758,
      lng: 108.9176648001958,
    },
    {
      lat: 34.270708807347326,
      lng: 108.91777660774166,
    },
    {
      lat: 34.27070375357,
      lng: 108.91783650359831,
    },
    {
      lat: 34.270708656282736,
      lng: 108.91793832501797,
    },
    {
      lat: 34.27073056759363,
      lng: 108.91802317433239,
    },
    {
      lat: 34.270778426427114,
      lng: 108.91815194371763,
    },
    {
      lat: 34.27082633013241,
      lng: 108.91823279701194,
    },
    {
      lat: 34.27090620143976,
      lng: 108.91833361195992,
    },
    {
      lat: 34.27096613594049,
      lng: 108.91837653091702,
    },
    {
      lat: 34.27103006872475,
      lng: 108.91841944945133,
    },
    {
      lat: 34.271094016769126,
      lng: 108.91844639632818,
    },
    {
      lat: 34.27115297737252,
      lng: 108.91846236313094,
    },
    {
      lat: 34.27124992726748,
      lng: 108.91847333575198,
    },
    {
      lat: 34.271362907088765,
      lng: 108.91844637465212,
    },
    {
      lat: 34.27141590080522,
      lng: 108.9184303988106,
    },
    {
      lat: 34.27146990941202,
      lng: 108.91839845127609,
    },
    {
      lat: 34.271506924906745,
      lng: 108.91836650505593,
    },
    {
      lat: 34.27161499324317,
      lng: 108.91824870518059,
    },
    {
      lat: 34.27165204453143,
      lng: 108.91817882572266,
    },
    {
      lat: 34.27169010556983,
      lng: 108.91809796526304,
    },
    {
      lat: 34.27170014671252,
      lng: 108.91805004879423,
    },
    {
      lat: 34.27170014671252,
      lng: 108.91805004879423,
    },
    {
      lat: 34.271813130666544,
      lng: 108.91801809584462,
    },
    {
      lat: 34.271926114560934,
      lng: 108.91798614283951,
    },
    {
      lat: 34.27202208565869,
      lng: 108.91797515437597,
    },
    {
      lat: 34.27211304847173,
      lng: 108.91797514704386,
    },
    {
      lat: 34.27364144760009,
      lng: 108.91794807112001,
    },
    {
      lat: 34.27430117651852,
      lng: 108.91794801793834,
    },
    {
      lat: 34.27430117651852,
      lng: 108.91794801793834,
    },
    {
      lat: 34.27452116643137,
      lng: 108.91786215053015,
    },
    {
      lat: 34.27504196130342,
      lng: 108.91785212600149,
    },
    {
      lat: 34.275856640147865,
      lng: 108.91783608824171,
    },
    {
      lat: 34.27635543402423,
      lng: 108.91783604803085,
    },
    {
      lat: 34.27635543402423,
      lng: 108.91783604803085,
    },
    {
      lat: 34.277267066837524,
      lng: 108.91782499372012,
    },
    {
      lat: 34.27870948266951,
      lng: 108.91780890532164,
    },
    {
      lat: 34.27895038797587,
      lng: 108.91780289635481,
    },
    {
      lat: 34.2793632206358,
      lng: 108.91779787178181,
    },
    {
      lat: 34.28040878884263,
      lng: 108.91779279619023,
    },
    {
      lat: 34.28146034867298,
      lng: 108.91779271140113,
    },
    {
      lat: 34.28146034867298,
      lng: 108.91779271140113,
    },
    {
      lat: 34.28263387644584,
      lng: 108.91777065508535,
    },
    {
      lat: 34.28417326218677,
      lng: 108.91773359532593,
    },
    {
      lat: 34.28442516136318,
      lng: 108.91772758544468,
    },
    {
      lat: 34.28569565334534,
      lng: 108.91769553859388,
    },
    {
      lat: 34.28609849266868,
      lng: 108.9176845252085,
    },
    {
      lat: 34.28774477772293,
      lng: 108.91770036464814,
    },
    {
      lat: 34.289932847077175,
      lng: 108.91769519685113,
    },
    {
      lat: 34.29036166314886,
      lng: 108.91769516226238,
    },
    {
      lat: 34.29064054341951,
      lng: 108.91769513976726,
    },
    {
      lat: 34.291648114741015,
      lng: 108.91769006717632,
    },
    {
      lat: 34.29183603934873,
      lng: 108.91768406243645,
    },
    {
      lat: 34.29314945213906,
      lng: 108.91770591827063,
    },
    {
      lat: 34.293712204034165,
      lng: 108.9177108641832,
    },
    {
      lat: 34.294226985630914,
      lng: 108.91770583134237,
    },
    {
      lat: 34.29428596006031,
      lng: 108.9177058265846,
    },
    {
      lat: 34.29436110539907,
      lng: 108.9175131548741,
    },
    {
      lat: 34.29435643485554,
      lng: 108.91715377349566,
    },
    {
      lat: 34.29435732039652,
      lng: 108.916147466944,
    },
    {
      lat: 34.29435732039652,
      lng: 108.916147466944,
    },
    {
      lat: 34.294572232299814,
      lng: 108.91614145952745,
    },
    {
      lat: 34.29463620453703,
      lng: 108.91614145436851,
    },
    {
      lat: 34.29495306695566,
      lng: 108.91614142881548,
    },
    {
      lat: 34.29495306695566,
      lng: 108.91614142881548,
    },
    {
      lat: 34.29496736180883,
      lng: 108.91578701078069,
    },
  ];

  export default path;

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值