引言
有时候项目需要在内网中访问,这时候就需要离线地图。本文介绍了使用node下载天地图瓦片地图并在本地使用vue进行访问
技术选型
前端技术栈
- Leaflet.js:轻量级地图库,提供基础地图功能
- Vite:现代前端构建工具,提供快速的开发体验
- 自定义插件:基于 Leaflet 开发的自定义图层和交互组件
后端技术栈
- Node.js:高性能服务端运行环境
- Axios:HTTP 客户端,用于下载地图瓦片
- fs-extra:增强版文件系统操作库
核心实现
1. 地图瓦片下载器
js
//创建项目
npm init -y
npm install
const
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}`);
});
下载成功的话,就可以看到文件夹里面的地图瓦片数据
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>
至此就可以看到效果了

相关推荐萌萌哒草头将军1 小时前⚡⚡⚡尤雨溪宣布开发 Vite Devtools,这两个很哇塞 🚀 Vite 的插件,你一定要知道!小彭努力中1 小时前7.Three.js 中 CubeCamera详解与实战示例浪裡遊2 小时前跨域问题(Cross-Origin Problem)LinDaiuuj2 小时前判断符号??,?. ,! ,!! ,|| ,&&,?: 意思以及举例敲厉害的燕宝2 小时前Pinia——Vue的Store状态管理库Aphasia3113 小时前react必备JavaScript知识点(二)——类玖玖passion3 小时前数组转树:数据结构中的经典问题呼Lu噜3 小时前WPF-遵循MVVM框架创建图表的显示【保姆级】珠峰下的沙砾3 小时前Vue3 里 CSS 深度作用选择器 :global航Hang*3 小时前WEBSTORM前端 —— 第2章:CSS —— 第3节:背景属性与显示模式热门推荐01西电B测-计算机网络综合实验(含验收问题)02KGG转MP3工具|非KGM文件|解密音频03从零安装 LLaMA-Factory 微调 Qwen 大模型成功及所有的坑04Coze扣子平台完整体验和实践(附国内和国际版对比)05YOLOv8入门 | 重要性能衡量指标、训练结果评价及分析及影响mAP的因素【发论文关注的指标】06DeepSeek各版本说明与优缺点分析07yolov10来了!用yolov10训练自己的数据集(原理、训练、部署、应用)08yolov8,yolo11,yolo12 服务器训练到部署全流程 笔记09我决定放弃搞 Java 了10最新 Kubernetes 集群部署 + flannel 网络插件(保姆级教程,最新 K8S 版本)
> <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>
至此就可以看到效果了

相关推荐
萌萌哒草头将军1 小时前
⚡⚡⚡尤雨溪宣布开发 Vite Devtools,这两个很哇塞 🚀 Vite 的插件,你一定要知道!小彭努力中1 小时前
7.Three.js 中 CubeCamera详解与实战示例浪裡遊2 小时前
跨域问题(Cross-Origin Problem)LinDaiuuj2 小时前
判断符号??,?. ,! ,!! ,|| ,&&,?: 意思以及举例敲厉害的燕宝2 小时前
Pinia——Vue的Store状态管理库Aphasia3113 小时前
react必备JavaScript知识点(二)——类玖玖passion3 小时前
数组转树:数据结构中的经典问题呼Lu噜3 小时前
WPF-遵循MVVM框架创建图表的显示【保姆级】珠峰下的沙砾3 小时前
Vue3 里 CSS 深度作用选择器 :global航Hang*3 小时前
WEBSTORM前端 —— 第2章:CSS —— 第3节:背景属性与显示模式热门推荐
01西电B测-计算机网络综合实验(含验收问题)02KGG转MP3工具|非KGM文件|解密音频03从零安装 LLaMA-Factory 微调 Qwen 大模型成功及所有的坑04Coze扣子平台完整体验和实践(附国内和国际版对比)05YOLOv8入门 | 重要性能衡量指标、训练结果评价及分析及影响mAP的因素【发论文关注的指标】06DeepSeek各版本说明与优缺点分析07yolov10来了!用yolov10训练自己的数据集(原理、训练、部署、应用)08yolov8,yolo11,yolo12 服务器训练到部署全流程 笔记09我决定放弃搞 Java 了10最新 Kubernetes 集群部署 + flannel 网络插件(保姆级教程,最新 K8S 版本)