天地图 离线地图

注意更换密钥

注意事项

本文提供的是独立的html文件,如果需要在项目中使用需处理跨域问题,可以部署一个网站或通过webview2设置虚拟目录绕过跨域问题。
参考:https://www.cnblogs.com/ives/p/18932799

备注

需要切换地图类型时候需要修改两处参数

此处如果使用球面墨卡托投影时

  • 地图类型 : 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>
posted @ 2025-06-16 19:55  Hey,Coder!  阅读(293)  评论(0)    收藏  举报