📁
点击选择文件 或 拖拽到此处
支持 .zip(含 .shp/.shx/.dbf)或同时选择三个文件
📭

还没加载文件
上传 Shapefile 开始查看

📋 全国新版无人机适飞空域政策汇总 还没加载文件上传 Shapefile
⚠️ 适飞空域基于官方 5月14日 公布的适飞网格数据生成,仅供飞友参考。实际适飞区域以 UOM 为准。

${detail}
打开控制台查看详情

`; } btns.forEach(b => { if (b.getAttribute('data-name') === name) b.classList.remove('loading'); }); } function clearProvinceLayers() { // Remove all layers and their map content for (const l of layers) { map.removeLayer(l.leafletLayer); } layers = []; activeLayer = null; refreshUI(); document.getElementById('attrPanel').style.display = 'none'; if (highlightLayer) { map.removeLayer(highlightLayer); highlightLayer = null; } } function showLoading(msg) { document.getElementById('layerList').innerHTML = `

${msg}

`; } async function loadFromZip(zipFile) { showLoading('正在解析 ZIP...'); try { const buffer = await zipFile.arrayBuffer(); const geojson = await shp(buffer); const name = zipFile.name.replace(/\.zip$/i, ''); addLayer(name, geojson); toast(`✅ 加载完成:${name}`); } catch (e) { toast('❌ 解析失败:' + e.message); console.error(e); } } async function loadFromIndividualFiles(files) { // Group files by basename const groups = {}; for (const f of files) { const name = f.name; const base = name.replace(/\.(shp|shx|dbf|prj|cpg|qix|sbn|sbx|fbn|fbx|ain|aih|ixs|mxs|atx)$/i, ''); if (!groups[base]) groups[base] = {}; const ext = name.split('.').pop().toLowerCase(); groups[base][ext] = f; } for (const [base, parts] of Object.entries(groups)) { if (!parts.shp || !parts.shx || !parts.dbf) { toast(`⚠️ ${base}:缺少必要文件(需要 .shp + .shx + .dbf)`); continue; } showLoading(`正在解析 ${base}...`); try { const shpBuf = await parts.shp.arrayBuffer(); const shxBuf = await parts.shx.arrayBuffer(); const dbfBuf = await parts.dbf.arrayBuffer(); const combined = new Uint8Array(shpBuf.byteLength + shxBuf.byteLength + dbfBuf.byteLength); // shpjs expects them in a specific order; the combined approach from individual files // is fragile. The library works best with zip or parseShp/parseDbf separately. // Let's try: just pass all three as a zip-like buffer by constructing it. // Actually, shpjs can take individual buffers. Let's try the combined method // that the library supports: concatenating them. // Actually, the library's main entry expects a zip. Let's use parseShp + parseDbf // separately and combine into geojson. const geojson = await shp.combine([ await shp.parseShp(shpBuf), await shp.parseDbf(dbfBuf) ]); addLayer(base, geojson); toast(`✅ 加载完成:${base}`); } catch (e) { toast(`❌ ${base} 解析失败:${e.message}`); console.error(e); } } if (Object.keys(groups).length === 0) { toast('⚠️ 请选择 .shp + .shx + .dbf 三个文件,或 .zip 压缩包'); } } // ─── PER-LAYER DEFAULTS ─── const DEFAULT_MIN_ZOOM = 0; const DEFAULT_MAX_ZOOM = 18; const DEFAULT_MAX_RENDER = 5000; // max elements per viewport per layer const DEBOUNCE_MS = 200; // ─── MONOTONE CHAIN CONVEX HULL ─── function convexHull(points) { points = points.slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]); if (points.length <= 1) return points; const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]); const half = (pts) => { const h = []; for (const p of pts) { while (h.length >= 2 && cross(h[h.length - 2], h[h.length - 1], p) <= 0) h.pop(); h.push(p); } return h; }; const lower = half(points); const upper = half(points.reverse()); lower.pop(); upper.pop(); return lower.concat(upper); } // ─── GET FEATURE CENTROID ─── function featureCentroid(geom) { if (!geom) return [0, 0]; if (geom.type === 'Point') return geom.coordinates; if (geom.type === 'Polygon') { const ring = geom.coordinates[0]; let sx = 0, sy = 0, n = ring.length - 1; for (let i = 0; i < n; i++) { sx += ring[i][0]; sy += ring[i][1]; } return [sx / n, sy / n]; } if (geom.type === 'MultiPolygon') return featureCentroid({ type: 'Polygon', coordinates: geom.coordinates[0] }); if (geom.type === 'LineString') { const c = geom.coordinates; const m = Math.floor(c.length / 2); return c.length > 0 ? c[m] : [0, 0]; } return [0, 0]; } // ─── GET FEATURE BBOX ─── function getFeatureBbox(feature) { if (feature._bbox) return feature._bbox; const geom = feature.geometry; let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; const walk = (coords) => { for (const c of coords) { if (typeof c[0] === 'number') { minX = Math.min(minX, c[0]); maxX = Math.max(maxX, c[0]); minY = Math.min(minY, c[1]); maxY = Math.max(maxY, c[1]); } else walk(c); } }; if (geom) walk(geom.coordinates); feature._bbox = [minX, minY, maxX, maxY]; return feature._bbox; } // ─── BBOX INTERSECTION ─── function bboxIntersects(a, b) { // a=[minX,minY,maxX,maxY], b=[[latMin,lngMin],[latMax,lngMax]] const [ax0, ay0, ax1, ay1] = a; const [ [by0, bx0], [by1, bx1] ] = b; return !(ax1 < bx0 || ax0 > bx1 || ay1 < by0 || ay0 > by1); } // ─── COMPUTE LAYER OUTLINE (sampled convex hull) ─── function computeOutline(features, maxSamples) { maxSamples = maxSamples || 10000; const step = Math.max(1, Math.floor(features.length / maxSamples)); const points = []; for (let i = 0; i < features.length; i += step) { points.push(featureCentroid(features[i].geometry)); } if (points.length < 3) return null; return convexHull(points); } // ─── RENDER DEBOUNCE ─── let renderTimer = null; function scheduleRender() { if (renderTimer) clearTimeout(renderTimer); renderTimer = setTimeout(renderAllViewports, DEBOUNCE_MS); } // ─── RENDER ALL VISIBLE LAYERS FOR CURRENT VIEWPORT ─── function renderAllViewports() { layers.forEach((l, i) => { if (l.visible) renderViewport(i); }); renderTimer = null; updateStats(); } function renderViewport(idx) { const l = layers[idx]; const zoom = map.getZoom(); const bounds = map.getBounds(); // Skip re-render if viewport hasn't changed meaningfully const bKey = `${bounds.toBBoxString()}_${zoom}`; if (l._lastRenderKey === bKey) return; l._lastRenderKey = bKey; l.leafletLayer.clearLayers(); // Outside this layer's zoom range → show nothing (or outline if available) if (zoom < l.minZoom || zoom > l.maxZoom) { if (l.outline && zoom < l.minZoom && l.outline.length >= 3) { const renderer = L.canvas({ padding: 0.5, tolerance: 3 }); const latlngs = l.outline.map(p => [p[1], p[0]]); latlngs.push(latlngs[0]); L.polygon(latlngs, { renderer, color: l.color, weight: 1.5, fillOpacity: 0.1, fillColor: l.color, opacity: 0.4, dashArray: '6 4' }).bindTooltip(`${l.name} (缩小至 ${l.minZoom} 级可见)`, { permanent: false }).addTo(l.leafletLayer); } l._renderedCount = 0; return; } const renderer = L.canvas({ padding: 0.5, tolerance: 3 }); const styleOpts = { color: l.color, weight: 1.5, fillOpacity: 0.3, fillColor: l.color, opacity: 0.7 }; // Filter features by viewport const visible = []; for (const f of l.fullFeatures) { const bbox = getFeatureBbox(f); if (bboxIntersects(bbox, [ [bounds.getSouth(), bounds.getWest()], [bounds.getNorth(), bounds.getEast()] ])) { visible.push(f); } } // Cap rendered count per layer let renderList = visible; const cap = l.maxRender || DEFAULT_MAX_RENDER; if (cap > 0 && visible.length > cap) { const step = Math.ceil(visible.length / cap); renderList = []; for (let i = 0; i < visible.length; i += step) renderList.push(visible[i]); } // Render const fc = { type: 'FeatureCollection', features: renderList }; const geoLayer = L.geoJSON(fc, { renderer, style: () => styleOpts, onEachFeature: (feature, layer) => { layer.on('click', () => showFeatureAttrs(feature, layer)); } }); geoLayer.addTo(l.leafletLayer); l._renderedCount = renderList.length; } // ─── ADD LAYER ─── function addLayer(name, geojson) { const idx = layers.length; const color = COLORS[idx % COLORS.length]; // Convert to FeatureCollection let fc; if (geojson.type === 'FeatureCollection') { fc = geojson; } else if (geojson.type === 'Feature') { fc = { type: 'FeatureCollection', features: [geojson] }; } else { fc = geojson; } const count = fc.features ? fc.features.length : 1; // Store full features for viewport filtering const fullFeatures = fc.features || []; // WGS84 → GCJ-02: fix offset on Gaode tiles for (const f of fullFeatures) { if (f.geometry && f.geometry.coordinates) { f.geometry.coordinates = gcjCoords(f.geometry.coordinates); } } // Compute outline (sampled) for low-zoom overview showLoading(`正在计算外包络线... (${count.toLocaleString()} 个要素)`); const outline = count > 100 ? computeOutline(fullFeatures) : null; // Create empty leaflet layer; content filled by renderViewport const leafletLayer = L.layerGroup().addTo(map); // Determine overall bounds for initial fit let allBounds = null; if (outline && outline.length >= 2) { allBounds = L.latLngBounds(outline.map(p => [p[1], p[0]])); } else if (fullFeatures.length > 0) { allBounds = L.latLngBounds([]); const step = Math.max(1, Math.floor(fullFeatures.length / 500)); for (let i = 0; i < fullFeatures.length; i += step) { const c = featureCentroid(fullFeatures[i].geometry); allBounds.extend([c[1], c[0]]); } } layers.push({ name, fullFeatures, geojson: fc, leafletLayer, color, featureCount: count, visible: true, outline, minZoom: DEFAULT_MIN_ZOOM, maxZoom: DEFAULT_MAX_ZOOM, maxRender: DEFAULT_MAX_RENDER, _renderedCount: 0, _lastRenderKey: '' }); activeLayer = idx; refreshUI(); // Initial viewport render if (allBounds && allBounds.isValid()) { map.fitBounds(allBounds.pad(0.1)); } // Defer first render so map has settled setTimeout(() => renderViewport(idx), 100); toast(`✅ 加载完成:${name}`); } // ─── UI ─── function refreshUI() { const list = document.getElementById('layerList'); const stats = document.getElementById('statsBar'); let totalFeatures = 0, totalRendered = 0; layers.forEach(l => { totalFeatures += l.featureCount; totalRendered += (l._renderedCount || 0); }); stats.style.display = 'flex'; stats.innerHTML = `
📊 ${layers.length} 图层
🔷 ${totalFeatures.toLocaleString()} 要素
${totalRendered > 0 && totalRendered < totalFeatures ? `
👁️ 当前显示 ${totalRendered.toLocaleString()}
` : ''} `; let html = ''; layers.forEach((l, i) => { const geomType = l.fullFeatures[0]?.geometry?.type||'未知'; const renderedInfo = l._renderedCount && l._renderedCount < l.featureCount ? `显示 ${l._renderedCount.toLocaleString()}` : `${l.featureCount.toLocaleString()} 个`; const inRange = map.getZoom() >= l.minZoom && map.getZoom() <= l.maxZoom; html += `
${l.name}
${renderedInfo} · ${geomType} · ${inRange ? '🟢' : '⚪'} ${l.minZoom}–${l.maxZoom} 级
${l.visible ? '👁️' : '👁️🗨️'}
`; // Only show zoom controls if this layer is active if (i === activeLayer) { html += `
显示范围 ${l.minZoom} ${l.maxZoom}
`; } }); list.innerHTML = html || '
📭

还没加载文件

'; } function updateStats() { refreshUI(); } function selectLayer(idx) { activeLayer = idx; refreshUI(); renderViewport(idx); } function setLayerMinZoom(idx, val) { val = parseInt(val); val = Math.max(0, Math.min(18, val)); // clamp if (val > layers[idx].maxZoom) val = layers[idx].maxZoom; layers[idx].minZoom = val; layers[idx]._lastRenderKey = ''; refreshUI(); renderViewport(idx); } function setLayerMaxZoom(idx, val) { val = parseInt(val); val = Math.max(0, Math.min(18, val)); // clamp if (val < layers[idx].minZoom) val = layers[idx].minZoom; layers[idx].maxZoom = val; layers[idx]._lastRenderKey = ''; refreshUI(); renderViewport(idx); } function toggleLayer(idx) { layers[idx].visible = !layers[idx].visible; if (layers[idx].visible) { layers[idx].leafletLayer.addTo(map); renderViewport(idx); } else { map.removeLayer(layers[idx].leafletLayer); } refreshUI(); } function fitAll() { if (layers.length === 0) return; const allBounds = L.latLngBounds([]); layers.forEach(l => { if (!l.visible) return; try { const b = l.leafletLayer.getBounds(); if (b.isValid()) allBounds.extend(b); } catch(e) {} }); if (allBounds.isValid()) map.fitBounds(allBounds.pad(0.1)); } // ─── MAP EVENTS ─── map.on('moveend', scheduleRender); map.on('zoomend', scheduleRender); // ─── ATTRIBUTES ─── function showFeatureAttrs(feature, layer) { const panel = document.getElementById('attrPanel'); const rows = document.getElementById('attrRows'); const countEl = document.getElementById('attrCount'); const props = feature.properties || {}; const keys = Object.keys(props); countEl.textContent = `(${keys.length} 个字段)`; let html = ''; keys.forEach(k => { let v = props[k]; if (v === null || v === undefined) v = 'null'; html += `
${k}${v}
`; }); if (keys.length === 0) html = '
无属性数据
'; rows.innerHTML = html; panel.style.display = 'block'; // Highlight on map if (highlightLayer) map.removeLayer(highlightLayer); const hlCoords = []; if (feature.geometry.type === 'Point') { hlCoords.push(feature.geometry.coordinates.reverse()); } else if (feature.geometry.type === 'Polygon') { hlCoords.push(feature.geometry.coordinates[0].map(c => [c[1],c[0]])); } else if (feature.geometry.type === 'LineString') { hlCoords.push(feature.geometry.coordinates.map(c => [c[1],c[0]])); } if (hlCoords.length > 0) { highlightLayer = L.polyline(hlCoords[0], { color: '#fbbf24', weight: 4, opacity: 0.9, dashArray: '8 4' }).addTo(map); } } // ─── EXPORT ─── function exportGeoJSON() { if (layers.length === 0) { toast('⚠️ 没有数据'); return; } if (layers.length === 1) { downloadJSON(layers[0].name + '.geojson', layers[0].geojson); } else { // Exports all visible layers as one FeatureCollection const allFeatures = []; layers.forEach(l => { if (l.visible) allFeatures.push(...l.geojson.features); }); downloadJSON('combined.geojson', { type: 'FeatureCollection', features: allFeatures }); } toast('📥 GeoJSON 已下载'); } function downloadJSON(filename, obj) { const blob = new Blob([JSON.stringify(obj, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } // ─── MAP EVENTS ─── map.on('moveend', scheduleRender); map.on('zoomend', scheduleRender); // ─── AUTO LOAD ─── setTimeout(() => preloadProvince('yunnan'), 300);

相关阅读

以下内容与「威廉希尔手机版宣布成为世界杯官方合作伙伴」同属公开资讯,可按栏目继续浏览相关条目。

本站按公开材料组织页面。需要原文时请核对应栏目发布页。

继续浏览时优先选择同栏目或相近主题,避免被站外镜像带偏。

快速通道

主页 / tools / chakongyu

公开资讯滚动更新。建议通过站内栏目继续浏览,核对最新条目。