TileCutter.js
6.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
/*
* @Date: 2025-01-22 11:45:30
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-02-27 09:41:24
* @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, imageRotation = 0) {
const img = new Image();
img.crossOrigin = "Anonymous"; // 避免跨域问题
img.src = imageURL;
img.onload = () => {
sliceImageToTiles(img, bounds, zoomLevel, imageRotation);
};
}
/**
* 将图片切割成瓦片并打包下载
* @param {HTMLImageElement} image - 要切割的图片元素
* @param {L.LatLngBounds} bounds - 图片边界范围,包含西南角和东北角的经纬度
* @param {number} zoomLevel - 地图缩放级别
*/
function sliceImageToTiles(image, bounds, zoomLevel, imageRotation) {
// 创建画布及上下文
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); // 起始瓦片X坐标
let tileEndX = lonToTileX(lonEnd, zoomLevel); // 结束瓦片X坐标
let tileStartY = latToTileY(latEnd, zoomLevel); // 起始瓦片Y坐标
let tileEndY = latToTileY(latStart, zoomLevel); // 结束瓦片Y坐标
// 确保瓦片坐标范围正确(起始值小于结束值)
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; // 图片在瓦片中的X坐标
const tileImgY = (tileLatEnd - latEnd) / (tileLatEnd - tileLatStart) * tileSize; // 图片在瓦片中的Y坐标
const tileImgWidth = (lonEnd - lonStart) / (tileLonEnd - tileLonStart) * tileSize; // 图片在瓦片中的宽度
const tileImgHeight = (latEnd - latStart) / (tileLatEnd - tileLatStart) * tileSize; // 图片在瓦片中的高度
// 清空画布
ctx.clearRect(0, 0, tileSize * scaleFactor, tileSize * scaleFactor);
// 旋转画布
ctx.translate(tileSize / 2, tileSize / 2); // 移动原点到画布中心
ctx.rotate(imageRotation * Math.PI / 180); // 旋转画布
ctx.translate(-tileSize / 2, -tileSize / 2); // 移回原点
// 绘制图片到画布
ctx.drawImage(
image,
0, 0, imgWidth, imgHeight, // 源图像区域(使用完整图片)
tileImgX, tileImgY, tileImgWidth, tileImgHeight // 目标画布区域(保持实际位置和比例)
);
// 恢复画布旋转
ctx.setTransform(1, 0, 0, 1, 0, 0);
// 将画布内容转换为Blob对象
canvas.toBlob((blob) => {
if (!blob) {
console.error("瓦片转换失败!");
return;
}
// 获取当前北京时间(UTC+8)
const beijingTime = dayjs().add(8, "hour").toDate();
// 使用 JSZip 将每个瓦片添加到压缩包中
zip.file(`${tileX}-${tileY}.png`, blob, { date: beijingTime });
tileIndex++;
// 如果所有瓦片都处理完,生成并下载压缩包
const totalTiles = (tileEndX - tileStartX + 1) * (tileEndY - tileStartY + 1);
if (tileIndex === totalTiles) {
generateAndDownloadZip(zip, zoomLevel);
}
}, "image/png", 1.0);
}
}
}
/**
* 将经度转换为瓦片X坐标
* @param {number} lon - 经度值,范围为 -180 到 180
* @param {number} zoom - 缩放级别
* @returns {number} - 返回对应的瓦片X坐标
*
* 计算过程说明:
* 1. lon + 180:将经度范围从 [-180, 180] 转换为 [0, 360]
* 2. /360:将范围归一化到 [0, 1]
* 3. * Math.pow(2, zoom):根据缩放级别计算实际瓦片坐标
* 4. Math.floor:向下取整,确保返回整数坐标
*/
function lonToTileX(lon, zoom) {
return Math.floor(((lon + 180) / 360) * Math.pow(2, zoom));
}
/**
* 将纬度转换为瓦片Y坐标
* @param {number} lat - 纬度值,范围为 -90 到 90
* @param {number} zoom - 缩放级别
* @returns {number} - 返回对应的瓦片Y坐标
*
* 计算过程说明:
* 1. lat * Math.PI / 180:将纬度从角度转换为弧度
* 2. Math.tan() + 1/Math.cos():计算墨卡托投影中的y值
* 3. Math.log():取自然对数
* 4. 1 - ... / Math.PI:将范围调整到 [0, 1]
* 5. / 2:将范围进一步归一化
* 6. * Math.pow(2, zoom):根据缩放级别计算实际瓦片坐标
* 7. Math.floor:向下取整,确保返回整数坐标
*/
function latToTileY(lat, zoom) {
// 将纬度转换为墨卡托投影坐标,然后计算对应的瓦片Y坐标
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`);
});
}