天地图 离线地图
注意更换密钥
注意事项
本文提供的是独立的html文件,如果需要在项目中使用需处理跨域问题,可以部署一个网站或通过webview2设置虚拟目录绕过跨域问题。
参考:https://www.cnblogs.com/ives/p/18932799
备注
需要切换地图类型时候需要修改两处参数
- 地图类型,对应参数为地图接口http://t0.tianditu.gov.cn/vec_w中的vec_w
- 地图图像类型,对应参数为LAYER
例如 影像底图
| http://t0.tianditu.gov.cn/img_c/wmts?tk=您的密钥 | 经纬度投影 |
| -------------------------------------------- | ------- |
| http://t0.tianditu.gov.cn/img_w/wmts?tk=您的密钥 | 球面墨卡托投影 |
此处如果使用球面墨卡托投影时
- 地图类型 : img_w
- 地图图像类型为 : img
基础离线地图框选缓存地图
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>天地图 - 离线地图框选下载工具</title>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #1a3e7c, #2c5080);
color: #333;
min-height: 100vh;
overflow: hidden;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 1600px;
margin: 0 auto;
}
header {
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 12px 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 1000;
border-bottom: 3px solid #ff9d00;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
}
.header-left {
display: flex;
align-items: center;
gap: 15px;
}
.header-logo {
display: flex;
align-items: center;
gap: 10px;
font-size: 1.8rem;
font-weight: bold;
color: #ff9d00;
}
.header-logo img {
height: 40px;
filter: drop-shadow(0 0 2px rgba(255, 157, 0, 0.8));
}
.header-tagline {
font-size: 1rem;
opacity: 0.9;
}
.header-controls {
display: flex;
gap: 10px;
align-items: center;
}
.map-container {
flex: 1;
position: relative;
box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.3);
}
#map {
height: 100%;
width: 100%;
background: #c0d4e8;
}
.control-panel {
position: absolute;
top: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.92);
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
max-width: 350px;
z-index: 500;
border: 1px solid #e0e0e0;
}
.panel-title {
font-size: 1.4rem;
margin-bottom: 15px;
color: #1a3e7c;
display: flex;
align-items: center;
gap: 8px;
padding-bottom: 10px;
border-bottom: 2px solid #ff9d00;
}
.instructions {
background: rgba(26, 62, 124, 0.08);
border-left: 4px solid #1a3e7c;
padding: 12px 15px;
margin-bottom: 20px;
border-radius: 0 8px 8px 0;
font-size: 0.9rem;
}
.instructions ol {
padding-left: 20px;
margin-top: 8px;
}
.instructions li {
margin-bottom: 8px;
}
.coordinate-display {
background: #f7faff;
border: 1px solid #a0c0e0;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
}
.coord-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.coord-item {
display: flex;
flex-direction: column;
}
.coord-label {
font-size: 0.85rem;
color: #2c5080;
font-weight: 600;
margin-bottom: 5px;
}
.coord-value {
font-weight: 500;
padding: 8px;
background: #fff;
border: 1px solid #d0d8e8;
border-radius: 4px;
font-family: 'Courier New', monospace;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
}
.zoom-display {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 15px;
padding-top: 15px;
border-top: 1px dashed #d0d8e8;
}
.btn-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 10px;
}
.btn {
padding: 13px;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
font-size: 1rem;
}
.btn-primary {
background: linear-gradient(to right, #1a3e7c, #2c5080);
color: white;
box-shadow: 0 4px 8px rgba(26, 62, 124, 0.3);
}
.btn-outline {
background: transparent;
border: 2px solid #1a3e7c;
color: #1a3e7c;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.btn:active {
transform: translateY(1px);
}
.btn:disabled {
opacity: 0.65;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.progress-container {
margin-top: 25px;
display: none;
}
.progress-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.progress-bar {
height: 10px;
background: #e0e8f0;
border-radius: 5px;
overflow: hidden;
}
.progress {
height: 100%;
background: linear-gradient(to right, #ff9d00, #ff7700);
width: 0%;
transition: width 0.4s;
}
.progress-stats {
margin-top: 10px;
font-size: 0.9rem;
color: #555;
font-style: italic;
}
.map-credits {
position: absolute;
bottom: 15px;
left: 15px;
background: rgba(255, 255, 255, 0.9);
padding: 6px 12px;
border-radius: 20px;
font-size: 0.75rem;
z-index: 500;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 5px;
}
.layer-controls {
position: absolute;
top: 15px;
left: 15px;
background: rgba(255, 255, 255, 0.9);
border-radius: 5px;
z-index: 500;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
overflow: hidden;
display: flex;
}
.layer-btn {
padding: 8px 15px;
font-size: 0.9rem;
cursor: pointer;
border: none;
background: #f8f9fa;
transition: all 0.2s;
}
.layer-btn:hover {
background: #e9ecef;
}
.layer-btn.active {
background: #1a3e7c;
color: white;
}
footer {
text-align: center;
padding: 12px;
background: rgba(0, 0, 0, 0.8);
color: #a0c0f0;
font-size: 0.9rem;
z-index: 100;
}
.completed-download {
margin-top: 15px;
padding: 15px;
background: linear-gradient(to right, #d4edda, #c3e6cb);
border-radius: 8px;
display: none;
color: #155724;
font-weight: 500;
border-left: 4px solid #28a745;
text-align: center;
}
@media (max-width: 768px) {
.header-content {
flex-direction: column;
align-items: flex-start;
}
.control-panel {
position: relative;
top: auto;
right: auto;
max-width: 100%;
margin: 10px auto;
width: 95%;
}
.layer-controls {
top: 75px;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="header-content">
<div class="header-left">
<div class="header-logo">
<span>天地图</span>
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#ff9d00" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
</div>
<div class="header-tagline">地理信息公共服务平台</div>
</div>
<div class="header-controls">
<button id="map-reset" class="btn btn-outline">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon>
<line x1="8" y1="2" x2="8" y2="18"></line>
<line x1="16" y1="6" x2="16" y2="22"></line>
</svg>
重置视图
</button>
</div>
</div>
</header>
<div class="map-container">
<div id="map"></div>
<div class="layer-controls">
<button id="vec-layer" class="layer-btn active">矢量地图</button>
<button id="img-layer" class="layer-btn">影像地图</button>
<button id="ter-layer" class="layer-btn">地形图</button>
</div>
<div class="map-credits">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#1a3e7c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
<span>© 国家地理信息公共服务平台</span>
</div>
</div>
<div class="control-panel">
<h2 class="panel-title">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#1a3e7c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
离线地图下载
</h2>
<div class="instructions">
<strong>使用说明:</strong>
<ol>
<li>点击右上角矩形工具框选地图区域</li>
<li>调整地图至需要的层级(9-18级)</li>
<li>设置所需地图类型:矢量、影像或地形</li>
<li>点击"下载离线地图"按钮开始下载</li>
</ol>
</div>
<div class="coordinate-display">
<div class="coord-grid">
<div class="coord-item">
<div class="coord-label">西北纬度</div>
<div id="nw-lat" class="coord-value">-</div>
</div>
<div class="coord-item">
<div class="coord-label">西北经度</div>
<div id="nw-lng" class="coord-value">-</div>
</div>
<div class="coord-item">
<div class="coord-label">东南纬度</div>
<div id="se-lat" class="coord-value">-</div>
</div>
<div class="coord-item">
<div class="coord-label">东南经度</div>
<div id="se-lng" class="coord-value">-</div>
</div>
</div>
<div class="zoom-display">
<div class="coord-label">当前地图层级</div>
<div id="zoom-level" class="coord-value">-</div>
</div>
</div>
<div class="btn-group">
<button id="download-btn" class="btn btn-primary" disabled>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
下载离线地图
</button>
<button id="clear-btn" class="btn btn-outline">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
<line x1="10" y1="11" x2="10" y2="17"></line>
<line x1="14" y1="11" x2="14" y2="17"></line>
</svg>
清除选择
</button>
</div>
<div id="progress-container" class="progress-container">
<div class="progress-header">
<span>下载进度</span>
<span id="progress-text">0%</span>
</div>
<div class="progress-bar">
<div id="progress" class="progress"></div>
</div>
<div class="progress-stats" id="stats-info">
正在初始化下载...
</div>
</div>
<div id="completed-download" class="completed-download">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
<span id="download-complete-text">离线地图下载完成!数据已保存到本地缓存</span>
</div>
</div>
<footer>
<p>© 2023 国家地理信息公共服务平台 | 本工具仅用于学习研究</p>
</footer>
</div>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Leaflet Draw Plugin -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
<!-- JSZip for creating zip files -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
<script>
// 天地图密钥
const apiKey = "xxxxxxxxx";
// Initialize the map
const map = L.map('map').setView([39.9042, 116.4074], 12); // 初始位置:北京
// 天地图图层定义
// 矢量地图(含标注)
const vecLayer = L.tileLayer(`http://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${apiKey}`, {
attribution: '天地图',
maxZoom: 18
});
const vecAnnoLayer = L.tileLayer(`http://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${apiKey}`, {
maxZoom: 18
});
// 影像地图(含标注)
const imgLayer = L.tileLayer(`http://t0.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${apiKey}`, {
maxZoom: 18
});
const imgAnnoLayer = L.tileLayer(`http://t0.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${apiKey}`, {
maxZoom: 18
});
// 地形地图(含标注)
const terLayer = L.tileLayer(`http://t0.tianditu.gov.cn/ter_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ter&STYLE=default&TILEMATXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${apiKey}`, {
maxZoom: 18
});
const terAnnoLayer = L.tileLayer(`http://t0.tianditu.gov.cn/cta_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cta&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${apiKey}`, {
maxZoom: 18
});
// 图层组
const baseMaps = {
"矢量地图": L.layerGroup([vecLayer, vecAnnoLayer]),
"影像地图": L.layerGroup([imgLayer, imgAnnoLayer]),
"地形地图": L.layerGroup([terLayer, terAnnoLayer])
};
// 默认添加矢量地图
baseMaps["矢量地图"].addTo(map);
let currentLayerType = "矢量地图";
// 添加图层控制
document.getElementById('vec-layer').addEventListener('click', function() {
setActiveLayer('矢量地图', this);
});
document.getElementById('img-layer').addEventListener('click', function() {
setActiveLayer('影像地图', this);
});
document.getElementById('ter-layer').addEventListener('click', function() {
setActiveLayer('地形地图', this);
});
function setActiveLayer(layerName, element) {
// 移除所有图层
Object.values(baseMaps).forEach(layer => map.removeLayer(layer));
// 添加选中的图层
baseMaps[layerName].addTo(map);
// 更新按钮状态
document.querySelectorAll('.layer-btn').forEach(btn => {
btn.classList.remove('active');
});
element.classList.add('active');
currentLayerType = layerName;
}
// 初始化绘图控件
const drawnItems = new L.FeatureGroup();
map.addLayer(drawnItems);
const drawControl = new L.Control.Draw({
position: 'topright',
draw: {
polygon: false,
polyline: false,
circle: false,
circlemarker: false,
marker: false,
rectangle: {
shapeOptions: {
color: '#ff9d00',
weight: 3,
fillOpacity: 0.2
}
}
},
edit: {
featureGroup: drawnItems
}
});
map.addControl(drawControl);
// 存储区域边界信息
let selectedArea = null;
// 监听绘图完成事件
map.on(L.Draw.Event.CREATED, function(event) {
const layer = event.layer;
drawnItems.addLayer(layer);
// 存储区域坐标
selectedArea = layer.getBounds();
updateCoordinateDisplay(selectedArea);
// 启用下载按钮
document.getElementById('download-btn').disabled = false;
});
// 监听地图缩放事件
map.on('zoomend', function() {
const zoomLevel = map.getZoom();
document.getElementById('zoom-level').textContent = zoomLevel;
});
// 重置视图按钮
document.getElementById('map-reset').addEventListener('click', function() {
map.setView([39.9042, 116.4074], 12);
});
// 清除按钮事件
document.getElementById('clear-btn').addEventListener('click', function() {
drawnItems.clearLayers();
selectedArea = null;
document.getElementById('download-btn').disabled = true;
clearCoordinateDisplay();
});
// 更新坐标显示
function updateCoordinateDisplay(bounds) {
if (!bounds) return;
const nw = bounds.getNorthWest();
const se = bounds.getSouthEast();
document.getElementById('nw-lat').textContent = nw.lat.toFixed(6);
document.getElementById('nw-lng').textContent = nw.lng.toFixed(6);
document.getElementById('se-lat').textContent = se.lat.toFixed(6);
document.getElementById('se-lng').textContent = se.lng.toFixed(6);
// 更新缩放级别
const zoomLevel = map.getZoom();
document.getElementById('zoom-level').textContent = zoomLevel;
}
// 清除坐标显示
function clearCoordinateDisplay() {
document.getElementById('nw-lat').textContent = '-';
document.getElementById('nw-lng').textContent = '-';
document.getElementById('se-lat').textContent = '-';
document.getElementById('se-lng').textContent = '-';
document.getElementById('zoom-level').textContent = '-';
}
// 计算瓦片坐标范围
function calculateTileRange(bounds, zoom) {
const nwPoint = map.project(bounds.getNorthWest(), zoom);
const sePoint = map.project(bounds.getSouthEast(), zoom);
const tileSize = 256;
const tileMinX = Math.floor(nwPoint.x / tileSize);
const tileMinY = Math.floor(nwPoint.y / tileSize);
const tileMaxX = Math.ceil(sePoint.x / tileSize);
const tileMaxY = Math.ceil(sePoint.y / tileSize);
return { minX: tileMinX, minY: tileMinY, maxX: tileMaxX, maxY: tileMaxY };
}
// 获取当前图层URL模板
function getLayerUrlTemplate() {
switch(currentLayerType) {
case "矢量地图":
return `http://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${apiKey}`;
case "影像地图":
return `http://t0.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${apiKey}`;
case "地形地图":
return `http://t0.tianditu.gov.cn/ter_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ter&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${apiKey}`;
default:
return `http://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${apiKey}`;
}
}
// 更新进度显示
function updateProgress(progress, processed, total) {
const progressBar = document.getElementById('progress');
const progressText = document.getElementById('progress-text');
const statsInfo = document.getElementById('stats-info');
const percentage = Math.round((processed / total) * 100);
progressBar.style.width = `${percentage}%`;
progressText.textContent = `${percentage}%`;
statsInfo.textContent = `已下载 ${processed}/${total} 个瓦片 (${percentage}%)`;
}
// 下载瓦片
async function downloadTiles() {
if (!selectedArea) return;
// 显示进度条
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress');
const progressText = document.getElementById('progress-text');
const completedDownload = document.getElementById('completed-download');
const downloadBtn = document.getElementById('download-btn');
const statsInfo = document.getElementById('stats-info');
const downloadCompleteText = document.getElementById('download-complete-text');
// 重置UI元素
completedDownload.style.display = 'none';
downloadBtn.disabled = true;
progressContainer.style.display = 'block';
progressBar.style.width = '0%';
progressText.textContent = '0%';
try {
// 获取当前缩放级别
const zoomLevel = map.getZoom();
// 计算瓦片坐标范围
const tileRange = calculateTileRange(selectedArea, zoomLevel);
const urlTemplate = getLayerUrlTemplate();
// 计算瓦片总数
const totalTiles = (tileRange.maxX - tileRange.minX + 1) *
(tileRange.maxY - tileRange.minY + 1);
if (totalTiles > 500) {
statsInfo.textContent = "正在计算...瓦片数量: " + totalTiles;
// 数量太多时询问用户
const confirmMsg = `您选择的区域包含大约 ${totalTiles} 个瓦片,下载可能需要较长时间。确定要继续吗?`;
if (!confirm(confirmMsg)) {
downloadBtn.disabled = false;
progressContainer.style.display = 'none';
return;
}
}
statsInfo.textContent = `共 ${totalTiles} 个瓦片,开始下载...`;
// 创建Zip容器
const zip = new JSZip();
const tilesFolder = zip.folder(`tiles_${currentLayerType}_z${zoomLevel}`);
// 计算下载起点时间
const startTime = new Date();
let processedTiles = 0;
let totalSize = 0;
// 分批下载瓦片避免内存问题
const batchSize = 20;
for (let x = tileRange.minX; x <= tileRange.maxX; x++) {
const xFolder = tilesFolder.folder(`${x}`);
for (let y = tileRange.minY; y <= tileRange.maxY; y++) {
const tileUrl = urlTemplate
.replace('{x}', x)
.replace('{y}', y)
.replace('{z}', zoomLevel);
try {
// 获取瓦片图片数据
const response = await fetch(tileUrl);
if (!response.ok) {
throw new Error(`无法下载瓦片 ${x}/${y}`);
}
const blob = await response.blob();
// 添加到zip
xFolder.file(`${y}.png`, blob);
// 更新进度
processedTiles++;
totalSize += blob.size;
if (processedTiles % batchSize === 0 || processedTiles === totalTiles) {
updateProgress(processedTiles / totalTiles * 100, processedTiles, totalTiles);
}
} catch (e) {
console.error('下载瓦片失败', e);
}
}
}
// 生成zip文件
zip.generateAsync({ type: "blob" }, (metadata) => {
// 显示压缩状态
statsInfo.textContent = `压缩中: ${metadata.percent.toFixed(1)}%`;
}).then((content) => {
// 计算下载时间
const endTime = new Date();
const timeTaken = (endTime - startTime) / 1000;
// 创建下载链接
const filename = `${currentLayerType}_地图_层级${zoomLevel}_${new Date().getTime()}.zip`;
saveAs(content, filename);
// 更新UI
const sizeMB = (totalSize / (1024 * 1024)).toFixed(2);
downloadCompleteText.textContent = `离线地图下载完成!文件: ${filename} (${sizeMB} MB), 用时: ${timeTaken}秒`;
completedDownload.style.display = 'block';
progressContainer.style.display = 'none';
downloadBtn.disabled = false;
});
} catch (error) {
console.error('下载失败:', error);
statsInfo.innerHTML = `<span style="color: #e74c3c;">下载失败: ${error.message}. 请重试或选择较小区域</span>`;
downloadBtn.disabled = false;
}
}
// 下载按钮事件
document.getElementById('download-btn').addEventListener('click', downloadTiles);
// 初始化显示缩放级别
document.getElementById('zoom-level').textContent = map.getZoom();
</script>
</body>
</html>
地图离线完成后只需要调整加载地图的逻辑为本地文件即可
const vecLayer = L.tileLayer('./tile/test/z{z}/{x}/{y}.png', {
attribution: '天地图',
maxZoom: 18
});
添加搜索定位功能
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>天地图 - 离线地图框选下载工具</title>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #1a3e7c, #2c5080);
color: #333;
min-height: 100vh;
overflow: hidden;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 1600px;
margin: 0 auto;
}
header {
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 12px 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 1000;
border-bottom: 3px solid #ff9d00;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
}
.header-left {
display: flex;
align-items: center;
gap: 15px;
}
.header-logo {
display: flex;
align-items: center;
gap: 10px;
font-size: 1.8rem;
font-weight: bold;
color: #ff9d00;
}
.header-logo img {
height: 40px;
filter: drop-shadow(0 0 2px rgba(255, 157, 0, 0.8));
}
.header-tagline {
font-size: 1rem;
opacity: 0.9;
}
.header-controls {
display: flex;
gap: 10px;
align-items: center;
}
.map-container {
flex: 1;
position: relative;
box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.3);
}
#map {
height: 100%;
width: 100%;
background: #c0d4e8;
}
.control-panel {
position: absolute;
top: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.92);
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
max-width: 350px;
z-index: 500;
border: 1px solid #e0e0e0;
}
.panel-title {
font-size: 1.4rem;
margin-bottom: 15px;
color: #1a3e7c;
display: flex;
align-items: center;
gap: 8px;
padding-bottom: 10px;
border-bottom: 2px solid #ff9d00;
}
/* 搜索框样式 */
.search-container {
margin-bottom: 15px;
position: relative;
}
.search-box {
display: flex;
gap: 8px;
}
#search-input {
flex: 1;
padding: 12px 15px;
border: 1px solid #d0d8e8;
border-radius: 6px;
font-size: 0.95rem;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);
color: #2c5080;
}
#search-btn {
padding: 0 20px;
background: linear-gradient(to right, #1a3e7c, #2c5080);
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
#search-btn:hover {
background: linear-gradient(to right, #2c5080, #1a3e7c);
transform: translateY(-1px);
}
#search-btn:active {
transform: translateY(1px);
}
/* 搜索结果样式 */
.search-results {
margin-top: 10px;
max-height: 200px;
overflow-y: auto;
border-radius: 6px;
background: #f8faff;
border: 1px solid #d0d8e8;
display: none;
}
.search-results.visible {
display: block;
}
.result-item {
padding: 10px 15px;
border-bottom: 1px solid #eaeef5;
cursor: pointer;
transition: background 0.2s;
font-size: 0.9rem;
}
.result-item:hover {
background: #e3eeff;
}
.result-name {
font-weight: 600;
color: #1a3e7c;
margin-bottom: 3px;
}
.result-address {
font-size: 0.85rem;
color: #666;
}
.no-results {
padding: 15px;
text-align: center;
color: #666;
}
.instructions {
background: rgba(26, 62, 124, 0.08);
border-left: 4px solid #1a3e7c;
padding: 12px 15px;
margin-bottom: 20px;
border-radius: 0 8px 8px 0;
font-size: 0.9rem;
}
.instructions ol {
padding-left: 20px;
margin-top: 8px;
}
.instructions li {
margin-bottom: 8px;
}
.coordinate-display {
background: #f7faff;
border: 1px solid #a0c0e0;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
}
.coord-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.coord-item {
display: flex;
flex-direction: column;
}
.coord-label {
font-size: 0.85rem;
color: #2c5080;
font-weight: 600;
margin-bottom: 5px;
}
.coord-value {
font-weight: 500;
padding: 8px;
background: #fff;
border: 1px solid #d0d8e8;
border-radius: 4px;
font-family: 'Courier New', monospace;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
}
.zoom-display {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 15px;
padding-top: 15px;
border-top: 1px dashed #d0d8e8;
}
.btn-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 10px;
}
.btn {
padding: 13px;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
font-size: 1rem;
}
.btn-primary {
background: linear-gradient(to right, #1a3e7c, #2c5080);
color: white;
box-shadow: 0 4px 8px rgba(26, 62, 124, 0.3);
}
.btn-outline {
background: transparent;
border: 2px solid #1a3e7c;
color: #1a3e7c;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.btn:active {
transform: translateY(1px);
}
.btn:disabled {
opacity: 0.65;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.progress-container {
margin-top: 25px;
display: none;
}
.progress-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.progress-bar {
height: 10px;
background: #e0e8f0;
border-radius: 5px;
overflow: hidden;
}
.progress {
height: 100%;
background: linear-gradient(to right, #ff9d00, #ff7700);
width: 0%;
transition: width 0.4s;
}
.progress-stats {
margin-top: 10px;
font-size: 0.9rem;
color: #555;
font-style: italic;
}
.map-credits {
position: absolute;
bottom: 15px;
left: 15px;
background: rgba(255, 255, 255, 0.9);
padding: 6px 12px;
border-radius: 20px;
font-size: 0.75rem;
z-index: 500;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 5px;
}
.layer-controls {
position: absolute;
top: 15px;
left: 15px;
background: rgba(255, 255, 255, 0.9);
border-radius: 5px;
z-index: 500;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
overflow: hidden;
display: flex;
}
.layer-btn {
padding: 8px 15px;
font-size: 0.9rem;
cursor: pointer;
border: none;
background: #f8f9fa;
transition: all 0.2s;
}
.layer-btn:hover {
background: #e9ecef;
}
.layer-btn.active {
background: #1a3e7c;
color: white;
}
footer {
text-align: center;
padding: 12px;
background: rgba(0, 0, 0, 0.8);
color: #a0c0f0;
font-size: 0.9rem;
z-index: 100;
}
.completed-download {
margin-top: 15px;
padding: 15px;
background: linear-gradient(to right, #d4edda, #c3e6cb);
border-radius: 8px;
display: none;
color: #155724;
font-weight: 500;
border-left: 4px solid #28a745;
text-align: center;
}
/* 搜索结果标记样式 */
.search-marker {
background-color: #ff4d4f;
border-radius: 50%;
border: 2px solid white;
width: 24px;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.search-marker::after {
content: "";
display: block;
width: 10px;
height: 10px;
background-color: white;
border-radius: 50%;
}
@media (max-width: 768px) {
.header-content {
flex-direction: column;
align-items: flex-start;
}
.control-panel {
position: relative;
top: auto;
right: auto;
max-width: 100%;
margin: 10px auto;
width: 95%;
}
.layer-controls {
top: 75px;
}
.search-box {
flex-direction: column;
}
#search-btn {
padding: 12px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="map-container">
<div id="map"></div>
<div class="layer-controls">
</div>
</div>
<div class="control-panel">
<div class="panel-title">
<i class="fas fa-map-marker-alt"></i>
<span>位置搜索与下载</span>
</div>
<div class="search-container">
<div class="search-box">
<input type="text" id="search-input" placeholder="输入地点名称(如:天安门广场)">
<button id="search-btn">搜索</button>
</div>
<div id="search-results" class="search-results">
<!-- 搜索结果将动态添加到这里 -->
</div>
</div>
<div class="instructions">
<div>使用方法:</div>
<ol>
<li>搜索目标地点并定位</li>
<li>使用矩形选择工具框选下载区域</li>
<li>点击"下载离线地图"开始下载</li>
</ol>
</div>
<div class="coordinate-display">
<div class="coord-grid">
<div class="coord-item">
<div class="coord-label">西北纬度</div>
<div id="nw-lat" class="coord-value">-</div>
</div>
<div class="coord-item">
<div class="coord-label">西北经度</div>
<div id="nw-lng" class="coord-value">-</div>
</div>
<div class="coord-item">
<div class="coord-label">东南纬度</div>
<div id="se-lat" class="coord-value">-</div>
</div>
<div class="coord-item">
<div class="coord-label">东南经度</div>
<div id="se-lng" class="coord-value">-</div>
</div>
</div>
<div class="zoom-display">
<div class="coord-label">当前地图层级</div>
<div id="zoom-level" class="coord-value">-</div>
</div>
</div>
<div class="btn-group">
<button id="download-btn" class="btn btn-primary" disabled>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
下载离线地图
</button>
<button id="clear-btn" class="btn btn-outline">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
<line x1="10" y1="11" x2="10" y2="17"></line>
<line x1="14" y1="11" x2="14" y2="17"></line>
</svg>
清除选择
</button>
</div>
<div id="progress-container" class="progress-container">
<div class="progress-header">
<span>下载进度</span>
<span id="progress-text">0%</span>
</div>
<div class="progress-bar">
<div id="progress" class="progress"></div>
</div>
<div class="progress-stats" id="stats-info">
正在初始化下载...
</div>
</div>
<div id="completed-download" class="completed-download">
<span id="download-complete-text">离线地图下载完成!数据已保存到本地缓存</span>
</div>
</div>
</div>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Leaflet Draw Plugin -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
<!-- JSZip for creating zip files -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
<script>
// 天地图密钥
const apiKey = "xxxx";
// Initialize the map
const map = L.map('map').setView([39.9042, 116.4074], 12); // 初始位置:北京
// 天地图图层定义
// 矢量地图(含标注)
const vecLayer = L.tileLayer(`http://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${apiKey}`, {
attribution: '天地图',
maxZoom: 18
});
const vecAnnoLayer = L.tileLayer(`http://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${apiKey}`, {
maxZoom: 18
});
// 图层组
const baseMaps = {
"矢量地图": L.layerGroup([vecLayer, vecAnnoLayer])
};
// 默认添加矢量地图
baseMaps["矢量地图"].addTo(map);
// 初始化绘图控件
const drawnItems = new L.FeatureGroup();
map.addLayer(drawnItems);
const drawControl = new L.Control.Draw({
position: 'topright',
draw: {
polygon: false,
polyline: false,
circle: false,
circlemarker: false,
marker: false,
rectangle: {
shapeOptions: {
color: '#ff9d00',
weight: 3,
fillOpacity: 0.2
}
}
}
});
map.addControl(drawControl);
// 存储区域边界信息
let selectedArea = null;
// 存储搜索结果标记
let searchMarker = null;
// 监听绘图完成事件
map.on(L.Draw.Event.CREATED, function(event) {
drawnItems.clearLayers();
const layer = event.layer;
drawnItems.addLayer(layer);
// 存储区域坐标
selectedArea = layer.getBounds();
updateCoordinateDisplay(selectedArea);
// 启用下载按钮
document.getElementById('download-btn').disabled = false;
});
// 监听地图缩放事件
map.on('zoomend', function() {
const zoomLevel = map.getZoom();
document.getElementById('zoom-level').textContent = zoomLevel;
});
// 清除按钮事件
document.getElementById('clear-btn').addEventListener('click', function() {
drawnItems.clearLayers();
selectedArea = null;
document.getElementById('download-btn').disabled = true;
clearCoordinateDisplay();
});
// 搜索功能实现
const searchInput = document.getElementById('search-input');
const searchBtn = document.getElementById('search-btn');
const searchResults = document.getElementById('search-results');
// 搜索按钮点击事件
searchBtn.addEventListener('click', function() {
const searchTerm = searchInput.value.trim();
if (searchTerm) {
searchLocation(searchTerm);
}
});
// 输入框回车键事件
searchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
const searchTerm = searchInput.value.trim();
if (searchTerm) {
searchLocation(searchTerm);
}
}
});
// 执行地理编码搜索
async function searchLocation(query) {
searchResults.innerHTML = '<div class="result-item">搜索中... <i class="fas fa-spinner fa-spin"></i></div>';
searchResults.classList.add('visible');
try {
// 地名搜索
const geocoderUrl = "http://api.tianditu.gov.cn/v2/search";
let para=`postStr={"keyWord":"${query}","level":12,"mapBound":"116.02524,39.83833,116.65592,39.99185","queryType":1,"start":0,"count":10}`;
const url = `${geocoderUrl}?${para}&type=query&tk=${apiKey}`;
//地理编码搜索
//const geocoderUrl = "http://api.tianditu.gov.cn/geocoder";
//let para=`ds={"keyWord":"北京市海淀区莲花池西路28号"}`;
//const url = `${geocoderUrl}?${para}&tk=${apiKey}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error('无法连接到地理编码服务');
}
const data = await response.json();
displaySearchResults(data);
//var script = document.createElement('script');
//script.src = `${url}&callback=displaySearchResults`;
//document.head.appendChild(script);
} catch (error) {
console.error('搜索失败:', error);
searchResults.innerHTML = '<div class="no-results">搜索失败:请重试或确保网络连接正常</div>';
}
}
// 显示搜索结果
function displaySearchResults(data) {
searchResults.innerHTML = '';
if (!data || !data.count || data.count === 0) {
searchResults.innerHTML = '<div class="no-results">没有找到匹配的地点</div>';
return;
}
// 显示前5个结果
const results = data.pois.slice(0, 5);
results.forEach(location => {
const resultItem = document.createElement('div');
resultItem.className = 'result-item';
resultItem.innerHTML = `
<div class="result-name">${location.name}</div>
<div class="result-address">${location.address || '无详细地址'}</div>
`;
// 点击定位事件
resultItem.addEventListener('click', function() {
let lonlat=location.lonlat.split(",");
locateOnMap([lonlat[1], lonlat[0]], location);
searchInput.value = location.name;
searchResults.classList.remove('visible');
});
searchResults.appendChild(resultItem);
});
searchResults.classList.add('visible');
}
// 在地图上定位搜索到的位置
function locateOnMap(latlng, location) {
// 移除之前的标记
if (searchMarker) {
map.removeLayer(searchMarker);
}
// 创建新的标记
searchMarker = L.marker(latlng, {
title: location.name,
icon: L.divIcon({
className: 'search-marker',
html: '<i class="fas fa-map-pin"></i>',
iconSize: [28, 28],
iconAnchor: [14, 28]
})
}).addTo(map);
// 显示标记的弹窗
searchMarker.bindPopup(`<b>${location.name}</b><br>${location.address || ''}`).openPopup();
// 移动到该位置
map.setView(latlng, 15);
// 更新坐标显示
updateCoordinateDisplayFromPoint(latlng);
}
// 根据点坐标更新显示
function updateCoordinateDisplayFromPoint(latlng) {
const zoomLevel = map.getZoom();
document.getElementById('zoom-level').textContent = zoomLevel;
document.getElementById('nw-lat').textContent = parseFloat(latlng[0]).toFixed(6);
document.getElementById('nw-lng').textContent = parseFloat(latlng[1]).toFixed(6);
}
// 更新坐标显示
function updateCoordinateDisplay(bounds) {
if (!bounds) return;
const nw = bounds.getNorthWest();
const se = bounds.getSouthEast();
document.getElementById('nw-lat').textContent = nw.lat.toFixed(6);
document.getElementById('nw-lng').textContent = nw.lng.toFixed(6);
document.getElementById('se-lat').textContent = se.lat.toFixed(6);
document.getElementById('se-lng').textContent = se.lng.toFixed(6);
// 更新缩放级别
const zoomLevel = map.getZoom();
document.getElementById('zoom-level').textContent = zoomLevel;
}
// 清除坐标显示
function clearCoordinateDisplay() {
document.getElementById('nw-lat').textContent = '-';
document.getElementById('nw-lng').textContent = '-';
document.getElementById('se-lat').textContent = '-';
document.getElementById('se-lng').textContent = '-';
document.getElementById('zoom-level').textContent = '-';
}
// 计算瓦片坐标范围
function calculateTileRange(bounds, zoom) {
const nwPoint = map.project(bounds.getNorthWest(), zoom);
const sePoint = map.project(bounds.getSouthEast(), zoom);
const tileSize = 256;
const tileMinX = Math.floor(nwPoint.x / tileSize);
const tileMinY = Math.floor(nwPoint.y / tileSize);
const tileMaxX = Math.ceil(sePoint.x / tileSize);
const tileMaxY = Math.ceil(sePoint.y / tileSize);
return { minX: tileMinX, minY: tileMinY, maxX: tileMaxX, maxY: tileMaxY };
}
// 获取当前图层URL模板
function getLayerUrlTemplate() {
return `http://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${apiKey}`;
}
// 更新进度显示
function updateProgress(progress, processed, total) {
const progressBar = document.getElementById('progress');
const progressText = document.getElementById('progress-text');
const statsInfo = document.getElementById('stats-info');
const percentage = Math.round((processed / total) * 100);
progressBar.style.width = `${percentage}%`;
progressText.textContent = `${percentage}%`;
statsInfo.textContent = `已下载 ${processed}/${total} 个瓦片 (${percentage}%)`;
}
// 下载瓦片
async function downloadTiles() {
if (!selectedArea) return;
// 显示进度条
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress');
const progressText = document.getElementById('progress-text');
const completedDownload = document.getElementById('completed-download');
const downloadBtn = document.getElementById('download-btn');
const statsInfo = document.getElementById('stats-info');
const downloadCompleteText = document.getElementById('download-complete-text');
// 重置UI元素
completedDownload.style.display = 'none';
downloadBtn.disabled = true;
progressContainer.style.display = 'block';
progressBar.style.width = '0%';
progressText.textContent = '0%';
try {
// 获取当前缩放级别
const zoomLevel = map.getZoom();
// 计算瓦片坐标范围
const tileRange = calculateTileRange(selectedArea, zoomLevel);
const urlTemplate = getLayerUrlTemplate();
// 计算瓦片总数
const totalTiles = (tileRange.maxX - tileRange.minX + 1) *
(tileRange.maxY - tileRange.minY + 1);
if (totalTiles > 500) {
statsInfo.textContent = "正在计算...瓦片数量: " + totalTiles;
// 数量太多时询问用户
const confirmMsg = `您选择的区域包含大约 ${totalTiles} 个瓦片,下载可能需要较长时间。确定要继续吗?`;
if (!confirm(confirmMsg)) {
downloadBtn.disabled = false;
progressContainer.style.display = 'none';
return;
}
}
statsInfo.textContent = `共 ${totalTiles} 个瓦片,开始下载...`;
// 创建Zip容器
const zip = new JSZip();
const tilesFolder = zip.folder(`tiles_z${zoomLevel}`);
// 计算下载起点时间
const startTime = new Date();
let processedTiles = 0;
let totalSize = 0;
// 分批下载瓦片避免内存问题
const batchSize = 20;
for (let x = tileRange.minX; x <= tileRange.maxX; x++) {
const xFolder = tilesFolder.folder(`${x}`);
for (let y = tileRange.minY; y <= tileRange.maxY; y++) {
const tileUrl = urlTemplate
.replace('{x}', x)
.replace('{y}', y)
.replace('{z}', zoomLevel);
try {
// 获取瓦片图片数据
const response = await fetch(tileUrl);
if (!response.ok) {
throw new Error(`无法下载瓦片 ${x}/${y}`);
}
const blob = await response.blob();
// 添加到zip
xFolder.file(`${y}.png`, blob);
// 更新进度
processedTiles++;
totalSize += blob.size;
if (processedTiles % batchSize === 0 || processedTiles === totalTiles) {
updateProgress(processedTiles / totalTiles * 100, processedTiles, totalTiles);
}
} catch (e) {
console.error('下载瓦片失败', e);
}
}
}
// 生成zip文件
zip.generateAsync({ type: "blob" }, (metadata) => {
// 显示压缩状态
statsInfo.textContent = `压缩中: ${metadata.percent.toFixed(1)}%`;
}).then((content) => {
// 计算下载时间
const endTime = new Date();
const timeTaken = (endTime - startTime) / 1000;
// 创建下载链接
const filename = `层级${zoomLevel}_${new Date().getTime()}.zip`;
saveAs(content, filename);
// 更新UI
const sizeMB = (totalSize / (1024 * 1024)).toFixed(2);
downloadCompleteText.textContent = `离线地图下载完成!文件: ${filename} (${sizeMB} MB), 用时: ${timeTaken}秒`;
completedDownload.style.display = 'block';
progressContainer.style.display = 'none';
downloadBtn.disabled = false;
});
} catch (error) {
console.error('下载失败:', error);
statsInfo.innerHTML = `<span style="color: #e74c3c;">下载失败: ${error.message}. 请重试或选择较小区域</span>`;
downloadBtn.disabled = false;
}
}
// 下载按钮事件
document.getElementById('download-btn').addEventListener('click', downloadTiles);
// 初始化显示缩放级别
document.getElementById('zoom-level').textContent = map.getZoom();
// 示例搜索词
searchInput.value = "天安门广场";
// 自动搜索示例位置
setTimeout(() => {
searchLocation(searchInput.value);
}, 1000);
</script>
</body>
</html>
新增矩形、编辑矩形、点击矩形触发编辑
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>天地图 - 离线地图框选下载工具</title>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #1a3e7c, #2c5080);
color: #333;
min-height: 100vh;
overflow: hidden;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 1600px;
margin: 0 auto;
}
header {
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 12px 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 1000;
border-bottom: 3px solid #ff9d00;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
}
.header-left {
display: flex;
align-items: center;
gap: 15px;
}
.header-logo {
display: flex;
align-items: center;
gap: 10px;
font-size: 1.8rem;
font-weight: bold;
color: #ff9d00;
}
.header-logo img {
height: 40px;
filter: drop-shadow(0 0 2px rgba(255, 157, 0, 0.8));
}
.header-tagline {
font-size: 1rem;
opacity: 0.9;
}
.header-controls {
display: flex;
gap: 10px;
align-items: center;
}
.map-container {
flex: 1;
position: relative;
box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.3);
}
#map {
height: 100%;
width: 100%;
background: #c0d4e8;
}
.control-panel {
position: absolute;
top: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.92);
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
max-width: 350px;
z-index: 500;
border: 1px solid #e0e0e0;
}
.panel-title {
font-size: 1.4rem;
margin-bottom: 15px;
color: #1a3e7c;
display: flex;
align-items: center;
gap: 8px;
padding-bottom: 10px;
border-bottom: 2px solid #ff9d00;
}
/* 搜索框样式 */
.search-container {
margin-bottom: 15px;
position: relative;
}
.search-box {
display: flex;
gap: 8px;
}
#search-input {
flex: 1;
padding: 12px 15px;
border: 1px solid #d0d8e8;
border-radius: 6px;
font-size: 0.95rem;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);
color: #2c5080;
}
#search-btn {
padding: 0 20px;
background: linear-gradient(to right, #1a3e7c, #2c5080);
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
#search-btn:hover {
background: linear-gradient(to right, #2c5080, #1a3e7c);
transform: translateY(-1px);
}
#search-btn:active {
transform: translateY(1px);
}
/* 搜索结果样式 */
.search-results {
margin-top: 10px;
max-height: 200px;
overflow-y: auto;
border-radius: 6px;
background: #f8faff;
border: 1px solid #d0d8e8;
display: none;
}
.search-results.visible {
display: block;
}
.result-item {
padding: 10px 15px;
border-bottom: 1px solid #eaeef5;
cursor: pointer;
transition: background 0.2s;
font-size: 0.9rem;
}
.result-item:hover {
background: #e3eeff;
}
.result-name {
font-weight: 600;
color: #1a3e7c;
margin-bottom: 3px;
}
.result-address {
font-size: 0.85rem;
color: #666;
}
.no-results {
padding: 15px;
text-align: center;
color: #666;
}
.instructions {
background: rgba(26, 62, 124, 0.08);
border-left: 4px solid #1a3e7c;
padding: 12px 15px;
margin-bottom: 20px;
border-radius: 0 8px 8px 0;
font-size: 0.9rem;
}
.instructions ol {
padding-left: 20px;
margin-top: 8px;
}
.instructions li {
margin-bottom: 8px;
}
.coordinate-display {
background: #f7faff;
border: 1px solid #a0c0e0;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
}
.coord-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.coord-item {
display: flex;
flex-direction: column;
}
.coord-label {
font-size: 0.85rem;
color: #2c5080;
font-weight: 600;
margin-bottom: 5px;
}
.coord-value {
font-weight: 500;
padding: 8px;
background: #fff;
border: 1px solid #d0d8e8;
border-radius: 4px;
font-family: 'Courier New', monospace;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
}
.zoom-display {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 15px;
padding-top: 15px;
border-top: 1px dashed #d0d8e8;
}
.btn-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 10px;
}
.btn {
padding: 13px;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
font-size: 1rem;
}
.btn-primary {
background: linear-gradient(to right, #1a3e7c, #2c5080);
color: white;
box-shadow: 0 4px 8px rgba(26, 62, 124, 0.3);
}
.btn-outline {
background: transparent;
border: 2px solid #1a3e7c;
color: #1a3e7c;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.btn:active {
transform: translateY(1px);
}
.btn:disabled {
opacity: 0.65;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.progress-container {
margin-top: 25px;
display: none;
}
.progress-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.progress-bar {
height: 10px;
background: #e0e8f0;
border-radius: 5px;
overflow: hidden;
}
.progress {
height: 100%;
background: linear-gradient(to right, #ff9d00, #ff7700);
width: 0%;
transition: width 0.4s;
}
.progress-stats {
margin-top: 10px;
font-size: 0.9rem;
color: #555;
font-style: italic;
}
.map-credits {
position: absolute;
bottom: 15px;
left: 15px;
background: rgba(255, 255, 255, 0.9);
padding: 6px 12px;
border-radius: 20px;
font-size: 0.75rem;
z-index: 500;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 5px;
}
.layer-controls {
position: absolute;
top: 15px;
left: 15px;
background: rgba(255, 255, 255, 0.9);
border-radius: 5px;
z-index: 500;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
overflow: hidden;
display: flex;
}
.layer-btn {
padding: 8px 15px;
font-size: 0.9rem;
cursor: pointer;
border: none;
background: #f8f9fa;
transition: all 0.2s;
}
.layer-btn:hover {
background: #e9ecef;
}
.layer-btn.active {
background: #1a3e7c;
color: white;
}
/* 自定义绘制按钮 */
.draw-rect-btn {
background: transparent;
border: 2px solid #1a3e7c;
color: #1a3e7c;
font-weight: 600;
padding: 6px 15px;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.draw-rect-btn:hover {
background: #e3eeff;
}
.draw-rect-btn.active {
background: #1a3e7c;
color: white;
}
footer {
text-align: center;
padding: 12px;
background: rgba(0, 0, 0, 0.8);
color: #a0c0f0;
font-size: 0.9rem;
z-index: 100;
}
.completed-download {
margin-top: 15px;
padding: 15px;
background: linear-gradient(to right, #d4edda, #c3e6cb);
border-radius: 8px;
display: none;
color: #155724;
font-weight: 500;
border-left: 4px solid #28a745;
text-align: center;
}
/* 搜索结果标记样式 */
.search-marker {
background-color: #ff4d4f;
border-radius: 50%;
border: 2px solid white;
width: 24px;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.search-marker::after {
content: "";
display: block;
width: 10px;
height: 10px;
background-color: white;
border-radius: 50%;
}
@media (max-width: 768px) {
.header-content {
flex-direction: column;
align-items: flex-start;
}
.control-panel {
position: relative;
top: auto;
right: auto;
max-width: 100%;
margin: 10px auto;
width: 95%;
}
.layer-controls {
top: 75px;
}
.search-box {
flex-direction: column;
}
#search-btn {
padding: 12px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="map-container">
<div id="map"></div>
<div class="layer-controls">
<button id="draw-rect-btn" class="draw-rect-btn">
<i class="fas fa-vector-square"></i> 框选区域
</button>
<button id="edit-rect-btn" class="draw-rect-btn" style="display: none;">
<i class="fas fa-edit"></i> 编辑区域
</button>
</div>
</div>
<div class="control-panel">
<div class="panel-title">
<i class="fas fa-map-marker-alt"></i>
<span>位置搜索与下载</span>
</div>
<div class="search-container">
<div class="search-box">
<input type="text" id="search-input" placeholder="输入地点名称(如:天安门广场)">
<button id="search-btn">搜索</button>
</div>
<div id="search-results" class="search-results">
<!-- 搜索结果将动态添加到这里 -->
</div>
</div>
<div class="instructions">
<div>使用方法:</div>
<ol>
<li>搜索目标地点并定位</li>
<li>使用矩形选择工具框选下载区域</li>
<li>点击矩形边框可以调整大小和位置</li>
<li>点击"下载离线地图"开始下载</li>
</ol>
</div>
<div class="coordinate-display">
<div class="coord-grid">
<div class="coord-item">
<div class="coord-label">西北纬度</div>
<div id="nw-lat" class="coord-value">-</div>
</div>
<div class="coord-item">
<div class="coord-label">西北经度</div>
<div id="nw-lng" class="coord-value">-</div>
</div>
<div class="coord-item">
<div class="coord-label">东南纬度</div>
<div id="se-lat" class="coord-value">-</div>
</div>
<div class="coord-item">
<div class="coord-label">东南经度</div>
<div id="se-lng" class="coord-value">-</div>
</div>
</div>
<div class="zoom-display">
<div class="coord-label">当前地图层级</div>
<div id="zoom-level" class="coord-value">-</div>
</div>
</div>
<div class="btn-group">
<button id="download-btn" class="btn btn-primary" disabled>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
下载离线地图
</button>
<button id="clear-btn" class="btn btn-outline">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
<line x1="10" y1="11" x2="10" y2="17"></line>
<line x1="14" y1="11" x2="14" y2="17"></line>
</svg>
清除选择
</button>
</div>
<div id="progress-container" class="progress-container">
<div class="progress-header">
<span>下载进度</span>
<span id="progress-text">0%</span>
</div>
<div class="progress-bar">
<div id="progress" class="progress"></div>
</div>
<div class="progress-stats" id="stats-info">
正在初始化下载...
</div>
</div>
<div id="completed-download" class="completed-download">
<span id="download-complete-text">离线地图下载完成!数据已保存到本地缓存</span>
</div>
</div>
</div>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Leaflet Draw Plugin -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
<!-- JSZip for creating zip files -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
<script>
// 天地图密钥
const apiKey = "xxx";
// 初始化地图
const map = L.map('map').setView([39.9042, 116.4074], 12); // 初始位置:北京
// 天地图图层定义
// 矢量地图(含标注)
const vecLayer = L.tileLayer(`http://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${apiKey}`, {
attribution: '天地图',
maxZoom: 18
});
const vecAnnoLayer = L.tileLayer(`http://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${apiKey}`, {
maxZoom: 18
});
// 图层组
const baseMaps = {
"矢量地图": L.layerGroup([vecLayer, vecAnnoLayer])
};
// 默认添加矢量地图
baseMaps["矢量地图"].addTo(map);
// 初始化绘图
const drawnItems = new L.FeatureGroup();
map.addLayer(drawnItems);
// 不再添加默认的绘图控件,使用自定义按钮
const drawRectangleControl = new L.Draw.Rectangle(map, {
position: 'topright',
shapeOptions: {
color: '#ff9d00',
weight: 3,
fillOpacity: 0.2
}
});
// 编辑控制
const editControl = new L.EditToolbar.Edit(map, {
featureGroup: drawnItems,
selectedPathOptions: {
color: '#ff0000',
opacity: 1.0
}
});
// 获取自定义按钮
const drawRectBtn = document.getElementById('draw-rect-btn');
const editRectBtn = document.getElementById('edit-rect-btn');
// 按钮点击事件处理
drawRectBtn.addEventListener('click', function() {
if (!drawRectangleControl._enabled) {
// 清除之前的绘制
drawnItems.clearLayers();
selectedArea = null;
document.getElementById('download-btn').disabled = true;
clearCoordinateDisplay();
// 激活绘制矩形工具
drawRectangleControl.enable();
drawRectBtn.classList.add('active');
// 隐藏编辑按钮
editRectBtn.style.display = 'none';
} else {
// 如果已经在绘制状态则禁用
drawRectangleControl.disable();
drawRectBtn.classList.remove('active');
}
});
// 编辑按钮点击事件处理
editRectBtn.addEventListener('click', function() {
if (editControl.enabled()) {
editControl.disable();
editRectBtn.classList.remove('active');
} else {
editControl.enable();
editRectBtn.classList.add('active');
// 隐藏绘制按钮
drawRectBtn.style.display = 'none';
}
});
// 存储区域边界信息
let selectedArea = null;
// 存储搜索结果标记
let searchMarker = null;
// 监听绘图完成事件
map.on(L.Draw.Event.CREATED, function(event) {
const layer = event.layer;
drawnItems.addLayer(layer);
// 存储区域坐标
selectedArea = layer.getBounds();
updateCoordinateDisplay(selectedArea);
// 启用下载按钮
document.getElementById('download-btn').disabled = false;
// 停止绘制模式并更新按钮状态
drawRectangleControl.disable();
drawRectBtn.classList.remove('active');
// 显示编辑按钮
editRectBtn.style.display = 'block';
// 监听编辑事件
layer.on('edit', function(e) {
// 更新存储的矩形区域
selectedArea = e.target.getBounds();
updateCoordinateDisplay(selectedArea);
});
layer.on('click', function(e) {
//// 激活该矩形的编辑模式 - 直接操作矩形
//if(editControl.enabled()){
// layer.editing.disable();
// editControl.disable();
//}
//else{
// layer.editing.enable();
// editControl.enable();
//}
//通过编辑控件 - 修改原有的矩形颜色
if (editControl.enabled()) {
editControl.disable();
editRectBtn.classList.remove('active');
} else {
editControl.enable();
editRectBtn.classList.add('active');
// 隐藏绘制按钮
drawRectBtn.style.display = 'none';
}
});
});
// 监听编辑结束事件
map.on('draw:editstop', function() {
editControl.disable();
editRectBtn.classList.remove('active');
// 显示绘制按钮
drawRectBtn.style.display = 'block';
});
// 监听地图缩放事件
map.on('zoomend', function() {
const zoomLevel = map.getZoom();
document.getElementById('zoom-level').textContent = zoomLevel;
});
// 清除按钮事件
document.getElementById('clear-btn').addEventListener('click', function() {
drawnItems.clearLayers();
selectedArea = null;
document.getElementById('download-btn').disabled = true;
clearCoordinateDisplay();
// 禁用绘制模式并更新按钮状态
drawRectangleControl.disable();
drawRectBtn.classList.remove('active');
drawRectBtn.style.display = 'block';
// 禁用编辑模式
editControl.disable();
editRectBtn.classList.remove('active');
editRectBtn.style.display = 'none';
});
// 搜索功能实现
const searchInput = document.getElementById('search-input');
const searchBtn = document.getElementById('search-btn');
const searchResults = document.getElementById('search-results');
// 搜索按钮点击事件
searchBtn.addEventListener('click', function() {
const searchTerm = searchInput.value.trim();
if (searchTerm) {
searchLocation(searchTerm);
}
});
// 输入框回车键事件
searchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
const searchTerm = searchInput.value.trim();
if (searchTerm) {
searchLocation(searchTerm);
}
}
});
// 执行地理编码搜索
async function searchLocation(query) {
searchResults.innerHTML = '<div class="result-item">搜索中... <i class="fas fa-spinner fa-spin"></i></div>';
searchResults.classList.add('visible');
try {
// 地名搜索
const geocoderUrl = "http://api.tianditu.gov.cn/v2/search";
let para=`postStr={"keyWord":"${query}","level":12,"mapBound":"116.02524,39.83833,116.65592,39.99185","queryType":1,"start":0,"count":10}`;
const url = `${geocoderUrl}?${para}&type=query&tk=${apiKey}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error('无法连接到地理编码服务');
}
const data = await response.json();
displaySearchResults(data);
} catch (error) {
console.error('搜索失败:', error);
searchResults.innerHTML = '<div class="no-results">搜索失败:请重试或确保网络连接正常</div>';
}
}
// 显示搜索结果
function displaySearchResults(data) {
searchResults.innerHTML = '';
if (!data || !data.count || data.count === 0) {
searchResults.innerHTML = '<div class="no-results">没有找到匹配的地点</div>';
return;
}
// 显示前5个结果
const results = data.pois.slice(0, 5);
results.forEach(location => {
const resultItem = document.createElement('div');
resultItem.className = 'result-item';
resultItem.innerHTML = `
<div class="result-name">${location.name}</div>
<div class="result-address">${location.address || '无详细地址'}</div>
`;
// 点击定位事件
resultItem.addEventListener('click', function() {
let lonlat = location.lonlat.split(",");
locateOnMap([lonlat[1], lonlat[0]], location);
searchInput.value = location.name;
searchResults.classList.remove('visible');
});
searchResults.appendChild(resultItem);
});
searchResults.classList.add('visible');
}
// 在地图上定位搜索到的位置
function locateOnMap(latlng, location) {
// 移除之前的标记
if (searchMarker) {
map.removeLayer(searchMarker);
}
// 创建新的标记
searchMarker = L.marker(latlng, {
title: location.name,
icon: L.divIcon({
className: 'search-marker',
html: '<i class="fas fa-map-pin"></i>',
iconSize: [28, 28],
iconAnchor: [14, 28]
})
}).addTo(map);
// 显示标记的弹窗
searchMarker.bindPopup(`<b>${location.name}</b><br>${location.address || ''}`).openPopup();
// 移动到该位置
map.setView(latlng, 15);
// 更新坐标显示
updateCoordinateDisplayFromPoint(latlng);
}
// 根据点坐标更新显示
function updateCoordinateDisplayFromPoint(latlng) {
const zoomLevel = map.getZoom();
document.getElementById('zoom-level').textContent = zoomLevel;
document.getElementById('nw-lat').textContent = parseFloat(latlng[0]).toFixed(6);
document.getElementById('nw-lng').textContent = parseFloat(latlng[1]).toFixed(6);
}
// 更新坐标显示
function updateCoordinateDisplay(bounds) {
if (!bounds) return;
const nw = bounds.getNorthWest();
const se = bounds.getSouthEast();
document.getElementById('nw-lat').textContent = nw.lat.toFixed(6);
document.getElementById('nw-lng').textContent = nw.lng.toFixed(6);
document.getElementById('se-lat').textContent = se.lat.toFixed(6);
document.getElementById('se-lng').textContent = se.lng.toFixed(6);
// 更新缩放级别
const zoomLevel = map.getZoom();
document.getElementById('zoom-level').textContent = zoomLevel;
}
// 清除坐标显示
function clearCoordinateDisplay() {
document.getElementById('nw-lat').textContent = '-';
document.getElementById('nw-lng').textContent = '-';
document.getElementById('se-lat').textContent = '-';
document.getElementById('se-lng').textContent = '-';
document.getElementById('zoom-level').textContent = '-';
}
// 计算瓦片坐标范围
function calculateTileRange(bounds, zoom) {
const nwPoint = map.project(bounds.getNorthWest(), zoom);
const sePoint = map.project(bounds.getSouthEast(), zoom);
const tileSize = 256;
const tileMinX = Math.floor(nwPoint.x / tileSize);
const tileMinY = Math.floor(nwPoint.y / tileSize);
const tileMaxX = Math.ceil(sePoint.x / tileSize);
const tileMaxY = Math.ceil(sePoint.y / tileSize);
return { minX: tileMinX, minY: tileMinY, maxX: tileMaxX, maxY: tileMaxY };
}
// 获取当前图层URL模板
function getLayerUrlTemplate() {
return `http://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${apiKey}`;
}
// 更新进度显示
function updateProgress(progress, processed, total) {
const progressBar = document.getElementById('progress');
const progressText = document.getElementById('progress-text');
const statsInfo = document.getElementById('stats-info');
const percentage = Math.round((processed / total) * 100);
progressBar.style.width = `${percentage}%`;
progressText.textContent = `${percentage}%`;
statsInfo.textContent = `已下载 ${processed}/${total} 个瓦片 (${percentage}%)`;
}
// 下载瓦片
async function downloadTiles() {
if (!selectedArea) return;
// 显示进度条
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress');
const progressText = document.getElementById('progress-text');
const completedDownload = document.getElementById('completed-download');
const downloadBtn = document.getElementById('download-btn');
const statsInfo = document.getElementById('stats-info');
const downloadCompleteText = document.getElementById('download-complete-text');
// 重置UI元素
completedDownload.style.display = 'none';
downloadBtn.disabled = true;
progressContainer.style.display = 'block';
progressBar.style.width = '0%';
progressText.textContent = '0%';
try {
// 获取当前缩放级别
const zoomLevel = map.getZoom();
// 计算瓦片坐标范围
const tileRange = calculateTileRange(selectedArea, zoomLevel);
const urlTemplate = getLayerUrlTemplate();
// 计算瓦片总数
const totalTiles = (tileRange.maxX - tileRange.minX + 1) *
(tileRange.maxY - tileRange.minY + 1);
if (totalTiles > 500) {
statsInfo.textContent = "正在计算...瓦片数量: " + totalTiles;
// 数量太多时询问用户
const confirmMsg = `您选择的区域包含大约 ${totalTiles} 个瓦片,下载可能需要较长时间。确定要继续吗?`;
if (!confirm(confirmMsg)) {
downloadBtn.disabled = false;
progressContainer.style.display = 'none';
return;
}
}
statsInfo.textContent = `共 ${totalTiles} 个瓦片,开始下载...`;
// 创建Zip容器
const zip = new JSZip();
const tilesFolder = zip.folder(`tiles_z${zoomLevel}`);
// 计算下载起点时间
const startTime = new Date();
let processedTiles = 0;
let totalSize = 0;
// 分批下载瓦片避免内存问题
const batchSize = 20;
for (let x = tileRange.minX; x <= tileRange.maxX; x++) {
const xFolder = tilesFolder.folder(`${x}`);
for (let y = tileRange.minY; y <= tileRange.maxY; y++) {
const tileUrl = urlTemplate
.replace('{x}', x)
.replace('{y}', y)
.replace('{z}', zoomLevel);
try {
// 获取瓦片图片数据
const response = await fetch(tileUrl);
if (!response.ok) {
throw new Error(`无法下载瓦片 ${x}/${y}`);
}
const blob = await response.blob();
// 添加到zip
xFolder.file(`${y}.png`, blob);
// 更新进度
processedTiles++;
totalSize += blob.size;
if (processedTiles % batchSize === 0 || processedTiles === totalTiles) {
updateProgress(processedTiles / totalTiles * 100, processedTiles, totalTiles);
}
} catch (e) {
console.error('下载瓦片失败', e);
}
}
}
// 生成zip文件
zip.generateAsync({ type: "blob" }, (metadata) => {
// 显示压缩状态
statsInfo.textContent = `压缩中: ${metadata.percent.toFixed(1)}%`;
}).then((content) => {
// 计算下载时间
const endTime = new Date();
const timeTaken = (endTime - startTime) / 1000;
// 创建下载链接
const filename = `层级${zoomLevel}_${new Date().getTime()}.zip`;
saveAs(content, filename);
// 更新UI
const sizeMB = (totalSize / (1024 * 1024)).toFixed(2);
downloadCompleteText.textContent = `离线地图下载完成!文件: ${filename} (${sizeMB} MB), 用时: ${timeTaken}秒`;
completedDownload.style.display = 'block';
progressContainer.style.display = 'none';
downloadBtn.disabled = false;
});
} catch (error) {
console.error('下载失败:', error);
statsInfo.innerHTML = `<span style="color: #e74c3c;">下载失败: ${error.message}. 请重试或选择较小区域</span>`;
downloadBtn.disabled = false;
}
}
// 下载按钮事件
document.getElementById('download-btn').addEventListener('click', downloadTiles);
// 初始化显示缩放级别
document.getElementById('zoom-level').textContent = map.getZoom();
// 示例搜索词
searchInput.value = "天安门广场";
// 自动搜索示例位置
setTimeout(() => {
searchLocation(searchInput.value);
}, 1000);
</script>
</body>
</html>
留待后查,同时方便他人
联系我:renhanlinbsl@163.com
联系我:renhanlinbsl@163.com