TileCutter.js 4.61 KB
/*
 * @Date: 2025-01-22 11:45:30
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2025-02-08 10:24:25
 * @FilePath: /map-demo/src/utils/TileCutter.js
 * @Description: 文件描述
 */
import JSZip from "jszip";
import { saveAs } from "file-saver";
import dayjs from "dayjs";

const tileSize = 512;

export function TileCutter(imageURL, bounds, zoomLevel) {
  const img = new Image();
  img.crossOrigin = "Anonymous"; // 避免跨域问题
  img.src = imageURL;
  img.onload = () => {
    sliceImageToTiles(img, bounds, zoomLevel);
  };
}


function sliceImageToTiles(image, bounds, zoomLevel) {
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d");

  const imgWidth = image.width;
  const imgHeight = image.height;

  const southWest = bounds.getSouthWest();
  const northEast = bounds.getNorthEast();

  const lonStart = southWest.lng;
  const latStart = southWest.lat;
  const lonEnd = northEast.lng;
  const latEnd = northEast.lat;

  let tileStartX = lonToTileX(lonStart, zoomLevel);
  let tileEndX = lonToTileX(lonEnd, zoomLevel);
  let tileStartY = latToTileY(latEnd, zoomLevel); // 取 latEnd 作为起点
  let tileEndY = latToTileY(latStart, zoomLevel); // 取 latStart 作为终点

  // 确保 tileStartX <= tileEndX,tileStartY <= tileEndY
  tileStartX = Math.min(tileStartX, tileEndX);
  tileEndX = Math.max(tileStartX, tileEndX);
  tileStartY = Math.min(tileStartY, tileEndY);
  tileEndY = Math.max(tileStartY, tileEndY);

  const scaleFactor = 2; // 调高分辨率倍率

  canvas.width = tileSize * scaleFactor;
  canvas.height = tileSize * scaleFactor;

  ctx.scale(scaleFactor, scaleFactor);

  const zip = new JSZip();  // 创建一个 JSZip 实例,用来打包所有瓦片

  let tileIndex = 0;  // 瓦片索引,用来给每个瓦片命名

  // 计算每个瓦片的经纬度范围
  for (let tileX = tileStartX; tileX <= tileEndX; tileX++) {
    for (let tileY = tileStartY; tileY <= tileEndY; tileY++) {
      // 计算当前瓦片的经纬度范围
      const tileLonStart = tileX * 360 / Math.pow(2, zoomLevel) - 180;
      const tileLonEnd = (tileX + 1) * 360 / Math.pow(2, zoomLevel) - 180;
      const tileLatStart = Math.atan(Math.sinh(Math.PI * (1 - 2 * (tileY + 1) / Math.pow(2, zoomLevel)))) * 180 / Math.PI;
      const tileLatEnd = Math.atan(Math.sinh(Math.PI * (1 - 2 * tileY / Math.pow(2, zoomLevel)))) * 180 / Math.PI;

      // 计算图片在当前瓦片中的位置和尺寸
      const tileImgX = (lonStart - tileLonStart) / (tileLonEnd - tileLonStart) * tileSize;
      const tileImgY = (tileLatEnd - latEnd) / (tileLatEnd - tileLatStart) * tileSize;
      const tileImgWidth = (lonEnd - lonStart) / (tileLonEnd - tileLonStart) * tileSize;
      const tileImgHeight = (latEnd - latStart) / (tileLatEnd - tileLatStart) * tileSize;

      ctx.clearRect(0, 0, tileSize * scaleFactor, tileSize * scaleFactor);

      // 根据图片在瓦片中的实际位置和尺寸绘制
      ctx.drawImage(
        image,
        0, 0, imgWidth, imgHeight, // 源图像区域(使用完整图片)
        tileImgX, tileImgY, tileImgWidth, tileImgHeight // 目标画布区域(保持实际位置和比例)
      );

      canvas.toBlob((blob) => {
        if (!blob) {
          console.error("瓦片转换失败!");
          return;
        }

        // 获取当前北京时间(UTC+8)
        const beijingTime = dayjs().add(8, "hour").toDate();

        // 使用 JSZip 将每个瓦片添加到压缩包中
        zip.file(`${tileX}_${tileY}_${zoomLevel}.png`, blob, { date: beijingTime });
        tileIndex++;

        // 如果所有瓦片都处理完,生成并下载压缩包
        const totalTiles = (tileEndX - tileStartX + 1) * (tileEndY - tileStartY + 1);
        if (tileIndex === totalTiles) {
          generateAndDownloadZip(zip, zoomLevel);
        }
      }, "image/png", 1.0);
    }
  }
}
// 经纬度转换为瓦片坐标
function lonToTileX(lon, zoom) {
  return Math.floor(((lon + 180) / 360) * Math.pow(2, zoom));
}

function latToTileY(lat, zoom) {
  return Math.floor(
    ((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2) * Math.pow(2, zoom)
  );
}

function saveTile(blob, filename) {
  const link = document.createElement("a");
  link.href = URL.createObjectURL(blob);
  link.download = filename;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}

// 生成并下载压缩包
function generateAndDownloadZip(zip, zoomLevel) {
  zip.generateAsync({ type: "blob" }).then((content) => {
      // 使用 FileSaver.js 下载压缩包
      saveAs(content, `${zoomLevel}级瓦片切片包.zip`);
  });
}