批量地图导出

概述

大致是这样的需求:用户通过界面上传csv文件,文件中包含要出图的区域的WKT,将地图适配到对应的WKT范围内,并根据WKT裁剪地图,只导出WKT多边形内的地图,导出的地图类型包括高德路网和谷歌影像图。

实现

1. 思路

1.1 使用技术

  • 使用maoboxGL框架
  • 文件上传用element的上传组件
  • wkt使用@terraformer/wkt转换为geojson
  • 使用html2canvas进行图片导出
  • 使用jszipfile-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
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容