概述
大致是这样的需求:用户通过界面上传csv
文件,文件中包含要出图的区域的WKT
,将地图适配到对应的WKT
范围内,并根据WKT
裁剪地图,只导出WKT
多边形内的地图,导出的地图类型包括高德路网和谷歌影像图。
实现
1. 思路
1.1 使用技术
- 使用maoboxGL框架
- 文件上传用element的上传组件
- wkt使用
@terraformer/wkt
转换为geojson - 使用
html2canvas
进行图片导出 - 使用
jszip
和file-saver
进行打包和导出
1.2 问题点
- 高德和谷歌存在坐标不一致的问题(高德路网是火星,谷歌影像是84),需要对坐标进行转换
- 对地图进行裁剪,需要先对数据做缓冲在进行裁剪,以避免边界也被裁剪掉
- 地图的定位和文件的导出是一个异步的过程
2. 实现
2.1 实现效果
image.png
2.2 实现代码
<template>
<div
class="container"
v-loading="loading"
element-loading-text="地图导出中,请稍后..."
>
<div class="lzugis-upload-tools">
<el-upload
drag
ref="file"
:action="''"
:multiple="false"
:auto-upload="false"
:limit="1"
:on-change="handleFileChange"
:on-exceed="handleExceed"
accept="*.csv"
>
<div class="el-upload__text">拖动文件到此或 <em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip">
文件中需包含<code style="color: red; font-weight: bold">aoiid</code
>或<code style="color: red; font-weight: bold">wkt</code>字段
<a
class="template"
download
href="/pages/common-tools/template.csv"
target="_blank"
>下载模板</a
>
</div>
</template>
</el-upload>
</div>
<div class="my-map" ref="map"></div>
</div>
</template>
<script>
import { ElMessage } from "element-plus";
import * as turf from "@turf/turf";
import { splitEasy } from "csv-split-easy";
import { wktToGeoJSON } from "@terraformer/wkt";
import { gcj02towgs84 } from "@/utils/proj";
import html2canvas from "html2canvas";
import JSZip from "jszip";
import { saveAs } from "file-saver";
const POLYGON_LAYER = "layer-polygon";
const fullExtent = turf.bboxPolygon([-180, -90, 180, 90]);
let map = null;
const mapStyle = {
version: 8,
name: "my-map-style",
sources: {
"nav-vec-source": {
type: "raster",
tiles: [
"https://webrd01.is.autonavi.com/appmaptile?style=8&x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1",
"https://webrd02.is.autonavi.com/appmaptile?style=8&x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1",
"https://webrd03.is.autonavi.com/appmaptile?style=8&x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1",
"https://webrd04.is.autonavi.com/appmaptile?style=8&x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1",
],
tileSize: 256,
},
"google-image-source": {
type: "raster",
tiles: [
"https://mt0.google.com/vt/lyrs=s&x={x}&y={y}&z={z}",
"https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}",
"https://mt2.google.com/vt/lyrs=s&x={x}&y={y}&z={z}",
"https://mt3.google.com/vt/lyrs=s&x={x}&y={y}&z={z}",
],
tileSize: 256,
},
},
layers: [
{
id: "nav-vec-source",
type: "raster",
source: "nav-vec-source",
layout: {
visibility: "visible",
},
},
{
id: "google-image-source",
type: "raster",
source: "google-image-source",
layout: {
visibility: "none",
},
},
],
};
export default {
data() {
return {
myStyle: mapStyle,
polygonsData: {},
index: 0,
loading: false,
imageFiles: [], // 用于存储截图数据
};
},
mounted() {
// 初始化地图
const dom = this.$refs.map;
map = new mapboxgl.Map({
container: dom,
center: [114.06247554811159, 22.54189010232247],
zoom: 14,
maxZoom: 17.2,
maxPitch: 0,
preserveDrawingBuffer: true,
style: mapStyle,
});
map.on("load", this.mapLoaded);
},
methods: {
mapLoaded() {
// 边框
map.addSource(`${POLYGON_LAYER}-source`, {
type: "geojson",
data: turf.featureCollection([]),
});
map.addSource(`${POLYGON_LAYER}-source-mask`, {
type: "geojson",
data: turf.featureCollection([]),
});
// 掩膜
map.addLayer({
id: `${POLYGON_LAYER}-source-mask`,
type: "fill",
source: `${POLYGON_LAYER}-source-mask`,
paint: {
"fill-color": "#fff",
"fill-opacity": 1,
},
});
map.addLayer({
id: `${POLYGON_LAYER}-line`,
type: "line",
source: `${POLYGON_LAYER}-source`,
layout: {
"line-cap": "round",
"line-join": "round",
},
paint: {
"line-color": "#f00",
"line-opacity": 1,
"line-width": 3,
},
});
},
fitBbox(bbox) {
map.fitBounds(bbox, {
padding: { top: 100, bottom: 100, left: 150, right: 150 },
duration: 500,
});
},
handleExceed(files) {
this.$refs.file.clearFiles();
this.$refs.file.handleStart(files[0]);
},
handleFileChange(file) {
const that = this;
const reader = new FileReader();
reader.readAsText(file.raw, "GB2312");
this.loading = true;
reader.onload = function () {
const csvContent = reader.result;
const arr = splitEasy(csvContent);
const header = arr.splice(0, 1)[0];
const json = arr.map((a) => {
let r = {};
header.forEach((h, i) => {
r[h] = a[i];
});
return r;
});
const wkts = json.filter((d) => d.wkt !== "").map((d) => d.wkt);
that.formatData(wkts);
};
},
formatData(wkts) {
const that = this;
let polygonInfos = [];
wkts.forEach((wkt, index) => {
const geom = wktToGeoJSON(wkt);
polygonInfos.push(turf.polygon(geom.coordinates, { id: "wkt" + index }));
});
let features = {};
polygonInfos.forEach((aoi, index) => {
const { geometry, properties } = aoi;
const coords = geometry.coordinates[0];
const coords84 = coords.map(([x, y]) => {
return gcj02towgs84(x, y);
});
features[index] = {
gcj: turf.polygon([coords], properties),
wgs: turf.polygon([coords84], properties),
};
this.polygonsData = features;
});
this.index = 0;
that.exportMapByIndex();
},
exportMapByIndex() {
const { gcj, wgs } = this.polygonsData[this.index];
this.exportMapToImage(gcj, "gcj").then(() => {
this.exportMapToImage(wgs, "wgs").then(() => {
this.index++;
const len = Object.keys(this.polygonsData).length;
if (this.index >= len) {
// 所有截图完成后,打包并下载
this.downloadZip();
ElMessage.success("导出完成");
this.loading = false;
// 清空图片数组,为下次操作做准备
this.imageFiles = [];
map
.getSource(`${POLYGON_LAYER}-source`)
.setData(turf.featureCollection([]));
map
.getSource(`${POLYGON_LAYER}-source-mask`)
.setData(turf.featureCollection([]));
return;
} else this.exportMapByIndex();
});
});
},
exportMapToImage(polygon, type = "gcj") {
return new Promise((resolve) => {
const isNav = type === "gcj";
const amap = isNav ? "visible" : "none";
const gg = !isNav ? "visible" : "none";
map.setLayoutProperty(`nav-vec-source`, "visibility", amap);
map.setLayoutProperty(`google-image-source`, "visibility", gg);
map.getSource(`${POLYGON_LAYER}-source`).setData(polygon);
// 计算并设置掩膜
const buffer = turf.buffer(polygon, 0.01);
const difference = turf.difference(fullExtent, buffer);
map.getSource(`${POLYGON_LAYER}-source-mask`).setData(difference);
// 定位到位置
this.fitBbox(turf.bbox(polygon));
const { id } = polygon.properties;
setTimeout(() => {
html2canvas(map.getCanvas()).then((canvas) => {
// 获取图片数据
const imageData = canvas.toDataURL("image/jpeg");
// 将图片数据和文件名存储到数组中
this.imageFiles.push({
name: `${id}-${type}.jpg`,
data: imageData,
});
resolve();
});
}, 1000);
});
},
// 打包并下载所有截图
downloadZip() {
if (this.imageFiles.length === 0) {
ElMessage.warning("没有可下载的图片");
return;
}
const zip = new JSZip();
const imgFolder = zip.folder("map-images");
// 将所有图片添加到zip中
this.imageFiles.forEach((file) => {
// 从base64数据中提取实际的图片数据(去掉前缀)
const base64Data = file.data.replace(/^data:image\/jpeg;base64,/, "");
imgFolder.file(file.name, base64Data, { base64: true });
});
// 生成zip文件并下载
zip.generateAsync({ type: "blob" }).then((content) => {
// 使用当前时间戳作为文件名的一部分,确保唯一性
const timestamp = new Date().getTime();
saveAs(content, `map-images-${timestamp}.zip`);
});
},
},
};
</script>
<style scoped lang="scss">
.container {
height: 100%;
position: relative;
.my-map {
width: 100%;
height: 100%;
}
}
.lzugis-upload-tools {
position: absolute;
top: 1rem;
left: 1rem;
z-index: 99;
padding: 1rem;
background-color: #ffffff;
border-radius: 0.3rem;
width: 25rem;
.tools {
margin-bottom: 0.8rem;
}
.template {
float: right;
}
}
</style>
实现坐标转换的proj.js
的代码如下:
// 定义一些常量
const x_PI = 3.14159265358979324 * 3000.0 / 180.0
const PI = 3.1415926535897932384626
const a = 6378245.0
const ee = 0.00669342162296594323
/**
* 百度坐标系 (BD-09) 与 火星坐标系 (GCJ-02)的转换 / 即百度转谷歌、高德
* @param { Number } bd_lon
* @param { Number } bd_lat
*/
export function bd09togcj02 (bd_lon, bd_lat) {
var x_pi = 3.14159265358979324 * 3000.0 / 180.0
var x = bd_lon - 0.0065
var y = bd_lat - 0.006
var z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_pi)
var theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_pi)
var gg_lng = z * Math.cos(theta)
var gg_lat = z * Math.sin(theta)
return [gg_lng, gg_lat]
}
/**
* 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换 / 即谷歌、高德 转 百度
* @param { Number } lng
* @param { Number } lat
*/
export function gcj02tobd09 (lng, lat) {
var z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * x_PI)
var theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * x_PI)
var bd_lng = z * Math.cos(theta) + 0.0065
var bd_lat = z * Math.sin(theta) + 0.006
return [bd_lng, bd_lat]
}
/**
* WGS84坐标系转火星坐标系GCj02 / 即WGS84 转谷歌、高德
* @param { Number } lng
* @param { Number } lat
*/
export function wgs84togcj02 (lng, lat) {
if (outOfChina(lng, lat)) {
return [lng, lat]
}
else {
var dlat = transformlat(lng - 105.0, lat - 35.0)
var dlng = transformlng(lng - 105.0, lat - 35.0)
var radlat = lat / 180.0 * PI
var magic = Math.sin(radlat)
magic = 1 - ee * magic * magic
var sqrtmagic = Math.sqrt(magic)
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI)
dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI)
const mglat = lat + dlat
const mglng = lng + dlng
return [mglng, mglat]
}
}
/**
* GCJ02(火星坐标系) 转换为 WGS84 / 即谷歌高德转WGS84
* @param { Number } lng
* @param { Number } lat
*/
export function gcj02towgs84 (lng, lat) {
if (outOfChina(lng, lat)) {
return [lng, lat]
}
else {
var dlat = transformlat(lng - 105.0, lat - 35.0)
var dlng = transformlng(lng - 105.0, lat - 35.0)
var radlat = lat / 180.0 * PI
var magic = Math.sin(radlat)
magic = 1 - ee * magic * magic
var sqrtmagic = Math.sqrt(magic)
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI)
dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI)
const mglat = lat + dlat
const mglng = lng + dlng
return [lng * 2 - mglng, lat * 2 - mglat]
}
}
export function wgs84towgs84(lng, lat) {
return [lng, lat]
}
export function gcj02togcj02(lng, lat) {
return [lng, lat]
}
export function bd09tobd09(lng, lat) {
return [lng, lat]
}
/**
* 百度坐标系转wgs84坐标系
* @param {*} lng
* @param {*} lat
*/
export function bd09towgs84 (lng, lat) {
// 百度坐标系先转为火星坐标系
const gcj02 = bd09togcj02(lng, lat)
// 火星坐标系转wgs84坐标系
return gcj02towgs84(gcj02[0], gcj02[1])
}
/**
* wgs84坐标系转百度坐标系
* @param {*} lng
* @param {*} lat
*/
export function wgs84tobd09 (lng, lat) {
// wgs84先转为火星坐标系
const gcj02 = wgs84togcj02(lng, lat)
// 火星坐标系转百度坐标系
const result = gcj02tobd09(gcj02[0], gcj02[1])
return result
}
/**
* 经度转换
* @param { Number } lng
* @param { Number } lat
*/
function transformlat (lng, lat) {
var ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng))
ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0
ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0
ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0
return ret
}
/**
* 纬度转换
* @param { Number } lng
* @param { Number } lat
*/
function transformlng (lng, lat) {
var ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng))
ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0
ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0
ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0
return ret
}
/**
* 判断是否在国内,不在国内则不做偏移
* @param {*} lng
* @param {*} lat
*/
function outOfChina (lng, lat) {
return (lng < 72.004 || lng > 137.8347) || ((lat < 0.8293 || lat > 55.8271) || false)
}
export default {
bd09togcj02,
bd09towgs84,
bd09tobd09,
wgs84togcj02,
wgs84tobd09,
wgs84towgs84,
gcj02towgs84,
gcj02tobd09,
gcj02togcj02
}