vue+leaflet实现天地图离线访问,并完成飞线效果。

2,550 阅读3分钟

引言

有时候项目需要在内网中访问,这时候就需要离线地图。本文介绍了使用node下载天地图瓦片地图并在本地使用vue进行访问

技术选型

前端技术栈

  • Leaflet.js:轻量级地图库,提供基础地图功能
  • Vite:现代前端构建工具,提供快速的开发体验
  • 自定义插件:基于 Leaflet 开发的自定义图层和交互组件

后端技术栈

  • Node.js:高性能服务端运行环境
  • Axios:HTTP 客户端,用于下载地图瓦片
  • fs-extra:增强版文件系统操作库

核心实现

1. 地图瓦片下载器

//创建项目
npm init -y
npm install
const app = express();
const path = require("path");
const axios = require("axios");
const fs = require("fs-extra");
const port = 4000;

// 天地图API密钥
const TIANDITU_KEY = "替换为你的Key";

// 设置静态文件目录
// 瓦片数据存储在项目的`tiles`目录下
app.use("/tiles", express.static(path.join(__dirname, "tiles")));

// 根路由
app.get("/", (req, res) => {
  res.send("欢迎访问地图服务");
});

// 瓦片请求处理
// 瓦片存储结构为'tiles/{n}/{z}/{x}/{y}.png'
app.get("/tile/:n/:z/:x/:y", (req, res) => {
  const n = req.params.n; // 图层类型
  const z = req.params.z; // 缩放等级
  const x = req.params.x; // 行
  const y = req.params.y; // 列-图片名称

  // 构造瓦片文件的完整路径
  const tilePath = path.join(__dirname, "tiles", n, z, x, `${y}.png`);

  // 发送文件
  res.sendFile(tilePath, (err) => {
    if (err) {
      if (err.code === "ENOENT") {
        // 如果文件不存在,返回404
        res.status(404).send(`瓦片不存在: ${tilePath}`);
      } else {
        // 其他错误
        res.status(500).send("服务器内部错误");
      }
    }
  });
});

// 获取随机服务器编号(0-7)
function getRandomServer() {
  return Math.floor(Math.random() * 8);
}

// 天地图代理服务 - 从天地图服务器获取瓦片并缓存到本地
app.get("/tianditu/:type/:z/:x/:y", async (req, res) => {
  const { type, z, x, y } = req.params;

  // 确保type是有效的天地图类型
  const validTypes = ["vec_c", "cva_c", "img_c", "cia_c", "ter_c", "cta_c"];
  if (!validTypes.includes(type)) {
    return res.status(400).send(`无效的天地图类型: ${type}。有效类型: ${validTypes.join(", ")}`);
  }

  // 本地缓存路径
  const cachePath = path.join(__dirname, "tiles", "tianditu", type, z, x, `${y}.png`);

  try {
    // 检查本地是否已缓存
    if (await fs.pathExists(cachePath)) {
      return res.sendFile(cachePath);
    }

    // 随机选择一个服务器
    const serverNum = getRandomServer();

    // 构造天地图URL - 修改了URL格式和参数顺序
    const url = `https://t${serverNum}.tianditu.gov.cn/${type}/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=${
      type.split("_")[0]
    }&STYLE=default&TILEMATRIXSET=c&FORMAT=tiles&TILECOL=${x}&TILEROW=${y}&TILEMATRIX=${z}&tk=${TIANDITU_KEY}`;

    // 请求天地图服务器
    const response = await axios.get(url, {
      responseType: "arraybuffer",
      headers: {
        Referer: "https://www.tianditu.gov.cn/",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"
      },
      timeout: 10000
    });

    // 确保缓存目录存在
    await fs.ensureDir(path.dirname(cachePath));

    // 保存瓦片到本地
    await fs.writeFile(cachePath, response.data);

    // 返回瓦片数据
    res.set("Content-Type", "image/png");
    res.send(response.data);

    console.log(`天地图瓦片已缓存: ${type}/${z}/${x}/${y}`);
  } catch (error) {
    console.error(`获取天地图瓦片失败: ${type}/${z}/${x}/${y}`, error.message);
    res.status(500).send(`获取天地图瓦片失败: ${error.message}`);
  }
});

app.listen(port, () => {
  console.log(`服务器正在监听端口 ${port}`);
});

下载成功的话,就可以看到文件夹里面的地图瓦片数据

image.png

2使用vite创建vue项目

关键代码

  <div class="map-container">
    <div ref="mapContainer" class="map"></div>
  </div>
</template>

<script setup >
import { ref, onMounted } from "vue";
import L from "leaflet";
import "proj4leaflet";
import "leaflet.migration";
const mapContainer = ref(null);
const map = ref(null);

onMounted(() => {
  let CRS_4490 = new L.Proj.CRS(
    "EPSG:4490",
    "+proj=longlat +ellps=GRS80 +no_defs",
    {
      resolutions: [
        1.40625, 0.703125, 0.3515625, 0.17578125, 0.087890625, 0.0439453125,
        0.02197265625, 0.010986328125, 0.0054931640625, 0.00274658203125,
        0.001373291015625, 6.866455078125e-4, 3.4332275390625e-4,
        1.71661376953125e-4, 8.58306884765625e-5, 4.291534423828125e-5,
        2.1457672119140625e-5, 1.0728836059570312e-5, 5.364418029785156e-6,
        2.682209064925356e-6,
      ],
      origin: [-180, 90],
    }
  );
  map.value = L.map(mapContainer.value, {
    center: [30.67, 104.06],
    crs: CRS_4490,
    zoom: 9,
    minZoom: 4,
  });
  const tileLayer = L.tileLayer(`/api/tianditu/vec_c/{z}/{x}/{y}`);
  const ciaLayer = L.tileLayer(`/api/tianditu/cva_c/{z}/{x}/{y}`);
  tileLayer.addTo(map.value);
  ciaLayer.addTo(map.value);
  const data = [
    {
      from: [104.06, 30.67], // 成都
      to: [116.41, 39.9], // 北京
      labels: ["成都", "北京"],
      color: "#ff3a31",
      value: 15,
    },
    {
      from: [104.06, 30.67], // 成都
      to: [121.47, 31.23], // 上海
      labels: ["成都", "上海"],
      color: "#00ff00",
      value: 15,
    },
  ];
  const options = {
    marker: {
      radius: [5, 10],
      pulse: true,
      textVisible: true,
    },
    line: {
      width: 1,
      order: false,
      icon: {
        type: "arrow",
        imgUrl: "",
        size: 10,
      },
    },
  };
  var migrationLayer = L.migrationLayer(data, options);
  migrationLayer.addTo(map.value);
});
</script>

<style>
.map-container {
  width: 100%;
  height: 100%;
}
.map {
  width: 100%;
  height: 100%;
}
</style>

至此就可以看到效果了

image.png