// ==UserScript== // @name 高德地图POI多边形采集 // @namespace http://tampermonkey.net/ // @version 1.0 // @author empyrealtear // @description 调用AMap,日上限1000次/天 // @match https://ditu.amap.com/* // @require https://scriptcat.org/lib/513/2.1.0/ElementGetter.js // @require https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js // @require https://cdn.jsdelivr.net/npm/@turf/turf@6.3.0/turf.min.js // @require https://cdn.jsdelivr.net/npm/dexie@4.4.2/dist/dexie.min.js // @grant unsafeWindow // @grant GM_xmlhttpRequest // @grant GM_setClipboard // @grant GM_setValue // @grant GM_getValue // @grant GM_listValues // @grant GM_openInTab // @grant GM_addStyle // @grant GM_info // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_cookie // @grant GM_notification // @run-at document-end // @noframes // @license MIT // ==/UserScript== const CUSTOM_STYLE = ` .panel-container { position: fixed; top: 0; right: 0; height: 100%; background-color: #fff; padding: 10px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .panel { background: #f5f7fa; padding: 8px; overflow-y: auto; border-left: 1px solid #ddd; transition: width 0.3s ease, padding 0.3s ease, border 0.3s ease; height: 100%; } .panel-container.collapsed .panel { width: 0; padding: 0; border: none; overflow: hidden; } #resizeHandler { width: 6px; height: 100%; background: #ccc; position: absolute; left: -6px; top: 0; cursor: col-resize; z-index: 999; } #resizeHandler:hover { background: #0078d4; } #collapseBtn { position: absolute; left: -30px; top: 90px; width: 30px; height: 60px; background: #0078d4; color: white; border: none; border-radius: 4px 0 0 4px; cursor: pointer; z-index: 998; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; } .panel-container div { font-size: 11px } .btn { margin: 2px; padding: 6px 10px; border: none; border-radius: 4px; background: #0078d4; color: white; cursor: pointer; font-size: 12px; } .btn:hover { background: #005a9e; } .btn-danger { background: #d93025; } .btn-light { background: #6c757d; font-size: 11px; padding: 3px 5px; } input[type="text"] { width: 100%; padding: 4px; margin: 4px 0; border: 1px solid #ccc; border-radius: 4px; font-size: 12px; } h4 { margin: 6px 0; font-size: 13px; color: #333; } table { width: 100%; border-collapse: collapse; background: #fff; margin-top: 6px; font-size: 11px; } td { border: 1px solid #ddd; padding: 3px; text-align: center;} th { background: #eaf2ff; font-size: 12px; } tr.active { background: #d1ecf1 !important; } #poiTable { white-space: nowrap; } #poiTable tr:hover { background: #e3f2fd; cursor: pointer; } #poiTable tr.poi-active { background: #bbdefb !important; } .type-selector { margin: 4px 0; padding: 4px; border: 1px solid #ccc; border-radius: 4px; background: #fff; overflow-y: auto; } .type-item { display: inline-block; margin: 2px 4px; padding: 1px 4px; cursor: pointer; font-size: 11px; } .type-item.selected { background: #0078d4; color: white; border-radius: 2px; } .type-controls { display: flex; gap: 4px; margin: 2px 0; } .pagination { display: flex; align-items: center; justify-content: center; margin: 6px 0; gap: 4px; } .pagination-btn { padding: 2px 8px; border: 1px solid #ccc; border-radius: 4px; cursor: pointer; background: #fff; font-size: 11px; } .pagination-btn:disabled { background: #eee; cursor: not-allowed; color: #999; } .pagination-info { font-size: 11px; color: #666; } .field-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; padding: 15px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); z-index: 1000; min-width: 280px; } .field-modal h5 { margin-bottom: 10px; font-size: 13px; color: #333; } .field-item { display: flex; align-items: center; margin: 6px 0; font-size: 12px; padding: 4px; cursor: move; border: 1px solid transparent; } .field-item:hover { background: #f0f0f0; border: 1px solid #ddd; } .field-item input { margin-right: 6px; } .modal-mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 999; display: none; } .modal-mask.show { display: block; } .modal-footer { margin-top: 10px; display: flex; justify-content: flex-end; gap: 8px; } .total-row { background: #f8f9fa; font-weight: bold; } .poi-search-box { margin: 4px 0; width: 100%; }`; const CUSTOM_HTML = `

区域列表

选择 名称 超量 操作

POI信息

`; let exportMethods let observer; let map, mouseTool; let editor; const overlays = { target: [], add(item) { this.target.push(item) }, removeAll() { this.target.forEach(v => map.remove(v)); this.target = [] } }; const currentPolygon = { target: null, subtree: [], setValue(target, subtree) { this.target = target; this.subtree.push(...(subtree || [])) }, render(area) { let { coords, overLimit } = area let target = new AMap.Polygon({ map, path: coords, strokeColor: "#0078d4", strokeWeight: 3, fillOpacity: 0.2, }) let subtree = (overLimit || []).map(v => new AMap.Polygon({ map, path: v.coords, strokeColor: "#f46672", strokeWeight: 3, fillOpacity: 0.2, })) this.setValue(target, subtree) }, remove() { map.remove(this.target); this.subtree.forEach(v => map.remove(v)); this.target = null; this.subtree = [] } }; let massMarker; let poiList = []; const poiTypes = { target: [ { code: "010000", name: "汽车服务" }, { code: "020000", name: "汽车销售" }, { code: "030000", name: "汽车维修" }, { code: "040000", name: "摩托车服务" }, { code: "050000", name: "餐饮服务" }, { code: "060000", name: "购物服务" }, { code: "070000", name: "生活服务" }, { code: "080000", name: "体育休闲服务" }, { code: "090000", name: "医疗保健服务" }, { code: "100000", name: "住宿服务" }, { code: "110000", name: "风景名胜" }, { code: "120000", name: "商务住宅" }, { code: "130000", name: "政府机构及社会团体" }, { code: "140000", name: "科教文化服务" }, { code: "150000", name: "交通设施服务" }, { code: "160000", name: "金融保险服务" }, { code: "170000", name: "公司企业" }, ], asObject() { return Object.fromEntries(this.target.map((v) => [v.name, v.code])) }, render() { const selector = document.getElementById("typeSelector"); let html = ""; let self = this self.target.forEach((item) => { const isSelected = selected.types.has(item.code); html += `${item.name}`; }); selector.innerHTML = html; for (let e of selector.children) { e.onclick = (event) => { let code = event.target.attributes['data-code'] if (code) self.toggle(code.value) } } exportMethods.loadPOIFromDB((v) => { const poiTypesObj = self.asObject(); return (selected.area.name == "" || selected.area.name == v.area) && (selected.types.size == 0 || selected.types.has(v.mainType) || selected.types.has(poiTypesObj[v.mainType])); }); }, toggle(code) { selected.types.has(code) ? selected.types.delete(code) : selected.types.add(code); this.render(); }, toggleAll() { this.target.forEach((item) => selected.types.add(item.code)); this.render(); }, toggleReverse() { this.target.forEach((item) => selected.types.has(item.code) ? selected.types.delete(item.code) : selected.types.add(item.code)); this.render(); }, clearAll() { selected.types.clear(); this.render(); }, loads() { let self = this let controls = document.getElementsByClassName('type-controls').item(0); let clickEvents = { '全选': () => self.toggleAll(), '反选': () => self.toggleReverse(), '清空': () => self.clearAll() } for (let e of controls.children) { let event = clickEvents[e.textContent]; if (event) e.onclick = event } } }; let filteredPoiList = []; const selected = { area: { id: null, name: '' }, types: new Set() }; const paginationProps = { current: 1, size: 10, total: 0, prevPage() { if (this.current > 1) { this.current--; this.refresh(); } }, nextPage() { if (this.current < this.total) { this.current++; this.refresh(); } }, changeSize() { this.size = parseInt(document.getElementById("pageSizeSelect").value); this.current = 1; this.refresh(); }, refresh() { this.total = Math.ceil(filteredPoiList.length / this.size); document.getElementById("prevBtn").disabled = this.current <= 1; document.getElementById("nextBtn").disabled = this.current >= this.total; document.getElementById("pageInfo").innerText = `第${this.current}页 / 共${this.total || 1}页`; exportMethods.renderPOITable(); }, loads() { document.getElementById("prevBtn").onclick = () => this.prevPage() document.getElementById("nextBtn").onclick = () => this.nextPage() document.getElementById("pageSizeSelect").onchange = () => this.changeSize() } }; const defaultFieldConfig = [ { key: "ID", label: "ID", visible: true, editable: false }, { key: "区域", label: "区域", visible: true, editable: false }, { key: "大类", label: "大类", visible: true, editable: false }, { key: "类型", label: "类型", visible: false, editable: false }, { key: "名称", label: "名称", visible: true, editable: false }, { key: "电话", label: "电话", visible: true, editable: false }, { key: "地址", label: "地址", visible: true, editable: false }, { key: "经度", label: "经度", visible: false, editable: false }, { key: "纬度", label: "纬度", visible: false, editable: false }, ]; let fieldConfig = JSON.parse(localStorage.getItem("poiFieldConfig")) || defaultFieldConfig; const savedWidth = localStorage.getItem("panelWidth") || "400px"; const isCollapsed = localStorage.getItem("panelCollapsed") === "true"; const db = new Dexie('PolygonAreaDB'); const previewAreas = function () { exportMethods.clearAll() const rainbowColors = ['#ff0000', '#ff6a00', '#ffa500', '#ffff00', '#33cc33', '#66ffff', '#6666ff'] db.areas.toArray().then(items => items.forEach(area => { let color = rainbowColors[area.id % rainbowColors.length] let polygon = new AMap.Polygon({ path: [...area.coords, area.coords[0]], strokeColor: color, strokeWeight: 3, fillColor: color, fillOpacity: 0.2 }) polygon.setMap(map) overlays.add(polygon) })) } const repartPOIArea = async function () { let areas = await db.areas.toArray() let pois = await db.pois.toArray() let updates = [] for (let poi of pois) { let { lat, lng } = poi.location let areaName = '' for (let area of areas) { let polygon = turf.polygon([[...area.coords, area.coords[0]]]); if (turf.booleanPointInPolygon(turf.point([lng, lat]), polygon)) { areaName = area.name break } } updates.push({ key: poi.id, changes: { area: areaName } }) } console.log(updates) await db.pois.bulkUpdate(updates) exportMethods.renderPOITable(); } const exportExcel = function () { if (filteredPoiList.length === 0) { alert("暂无数据"); return; } const exp = filteredPoiList.map((item) => { const row = {}; fieldConfig.filter((f) => f.visible).forEach((f) => row[f.label] = item[f.key]); return row; }) const wb = XLSX.utils.book_new(); const ws = XLSX.utils.json_to_sheet(exp); XLSX.utils.book_append_sheet(wb, ws, "POI数据"); XLSX.writeFile(wb, "多边形区域POI.xlsx"); } function init() { GM_addStyle(CUSTOM_STYLE) let drawerDom = document.createElement('div') drawerDom.innerHTML = CUSTOM_HTML document.body.appendChild(drawerDom) // 侧边栏逻辑 原样保留 const panelContainer = document.getElementById("panelContainer"); const panel = document.getElementById("panel"); const resizeHandler = document.getElementById("resizeHandler"); const collapseBtn = document.getElementById("collapseBtn"); panel.style.width = savedWidth; if (isCollapsed) { panelContainer.classList.add("collapsed"); panel.style.width = "0"; resizeHandler.style.display = "none"; collapseBtn.innerText = "«"; } else { resizeHandler.style.display = "block"; collapseBtn.innerText = "»"; } let isResizing = false; resizeHandler.addEventListener("mousedown", () => { isResizing = true; resizeHandler.style.cursor = "col-resize"; document.body.style.userSelect = "none"; }); document.addEventListener("mousemove", (e) => { if (!isResizing || panelContainer.classList.contains("collapsed")) return; const w = window.innerWidth - e.clientX; const finalW = Math.max(320, Math.min(w, 1200)); panel.style.width = finalW + "px"; localStorage.setItem("panelWidth", finalW + "px"); localStorage.setItem("preCollapseWidth", finalW + "px"); }); document.addEventListener("mouseup", () => { if (isResizing) { isResizing = false; resizeHandler.style.cursor = ""; document.body.style.userSelect = ""; map.resize(); } }); poiTypes.loads() paginationProps.loads() document.getElementById("poiSearchInput").addEventListener("input", (e) => { const kw = e.target.value.trim().toLowerCase(); if (!kw) { filteredPoiList = [...poiList]; } else { filteredPoiList = poiList.filter((item) => { return Object.values(item).some((v) => v && v.toString().toLowerCase().includes(kw)); }); } paginationProps.current = 1; paginationProps.refresh(); }); const togglePanel = function () { const isNowCollapsed = panelContainer.classList.toggle("collapsed"); localStorage.setItem("panelCollapsed", isNowCollapsed); if (isNowCollapsed) { localStorage.setItem("preCollapseWidth", panel.style.width); localStorage.setItem("panelWidth", panel.style.width); panel.style.width = "0"; resizeHandler.style.display = "none"; collapseBtn.innerText = "«"; } else { const preWidth = localStorage.getItem("preCollapseWidth") || "400px"; panel.style.width = preWidth; resizeHandler.style.display = "block"; collapseBtn.innerText = "»"; localStorage.setItem("panelWidth", preWidth); } map.resize(); } const startDraw = function () { clearAll(); if (selected.area.id) { db.areas.filter(area => area.id == selected.area.id).first().then(area => { currentPolygon.render(area) editor = new AMap.PolyEditor(map, currentPolygon.target); editor.open(); }) return; } mouseTool.polygon({ strokeColor: "#0078d4", strokeWeight: 3, fillColor: "#0078d4", fillOpacity: 0.2 }); mouseTool.on("draw", (e) => { currentPolygon.setValue(e.obj); editor = new AMap.PolyEditor(map, currentPolygon.target); editor.open(); mouseTool.close(); }); }; const savePOIToDB = function (data) { db.pois.bulkPut(data).catch((err) => console.error("更新失败", err)) } const loadPOIFromDB = async function (filter) { poiList = await db.pois.toArray() filteredPoiList = !filter ? [...poiList] : poiList.filter(filter); paginationProps.current = 1; handleAllPOIData(filteredPoiList); } // 保存区域 const saveCurrentPolygon = function () { if (!currentPolygon.target) { alert("请先绘制多边形"); return; } const name = document.getElementById("areaName").value.trim(); if (!name) { alert("请输入区域名称"); return; } const coords = currentPolygon.target.getPath().map((p) => [p.lng, p.lat]); db.areas.filter(area => area.name == name).first().then(area => { if (area) { db.areas.update(area.id, { name, coords }).then(loadAreaList); } else { db.areas.add({ name, coords }).then(loadAreaList); } }); } // 区域列表 const loadAreaList = function () { db.areas.toArray() .then(list => renderAreaList(list)) .catch(err => console.warn("读取区域失败", err)); } const renderAreaList = async function (list) { let html = ""; for (let item of list) { const area = await db.areas.get(item.id); const isActive = selected.area.id === item.id; html += ` ${item.name} ${(area.overLimit || []).length} `; } document.getElementById("areaList").innerHTML = html; }; // 2. 优化选择函数:支持 选中/取消 切换 const selectArea = function (id, name, checkbox) { // 如果点击的是当前已选中的项 → 取消选择 if (selected.area.id === id) { selected.area.id = null; selected.area.name = ""; checkbox.checked = false; clearAll() } else { selected.area.id = id; selected.area.name = name; document.querySelectorAll('input[name="selArea"]').forEach((el) => el.checked = el === checkbox); loadPolygon(id) } // 重新渲染列表,更新样式 loadAreaList(); } const deleteArea = function (id) { if (!confirm("确定删除此区域?删除后无法恢复!")) return; db.areas.delete(id).then(() => { if (selected.area.id === id) { selected.area.id = null; selected.area.name = ""; } loadAreaList(); }).catch(err => { console.warn("删除失败:", err); alert("删除失败"); }); } const renameArea = function (id) { const newName = prompt("请输入新名称:"); if (!newName || !newName.trim()) return; db.areas.get(id).then(data => { if (!data) return; data.name = newName.trim(); return db.areas.put(data); }).then(() => { loadAreaList(); }).catch(err => { console.warn("重命名失败", err); alert("重命名失败"); }); } const loadPolygon = function (id) { db.areas.get(id).then(data => { if (data) { clearAll(); currentPolygon.render(data); loadAreaList(); loadPOIFromDB((v) => v.area == selected.area.name); let { center, zoom } = getPolygonCenterAndZoom(data.coords); map.setCenter(center); map.setZoom(zoom); } }) } const getPolygonCenter = function (coords) { if (!coords || coords.length === 0) return [116.397428, 39.90923]; let lngSum = 0, latSum = 0; coords.forEach(p => { lngSum += p[0]; latSum += p[1]; }); return [ parseFloat((lngSum / coords.length).toFixed(6)), parseFloat((latSum / coords.length).toFixed(6)) ]; } const getDistance = function (lng1, lat1, lng2, lat2) { const rad = Math.PI / 180; const latRad1 = lat1 * rad; const latRad2 = lat2 * rad; const a = latRad1 - latRad2; const b = (lng1 - lng2) * rad; let dis = 2 * Math.asin(Math.sqrt( Math.sin(a / 2) ** 2 + Math.cos(latRad1) * Math.cos(latRad2) * Math.sin(b / 2) ** 2 )); dis = dis * 6378137; // 地球半径 return Math.round(dis); } const getPolygonRadius = function (coords, center) { const [centerLng, centerLat] = center; let maxDistance = 0; coords.forEach(point => { const dis = getDistance(centerLng, centerLat, point[0], point[1]); if (dis > maxDistance) maxDistance = dis; }); return maxDistance; } const getFitZoomByRadius = function (radius) { if (radius <= 50) return 19; // 极小点位 if (radius <= 200) return 18; // 小建筑 if (radius <= 500) return 17; // 火车站/商圈(你的于都站适配) if (radius <= 1000) return 16; // 街道 if (radius <= 2000) return 15; // 乡镇 if (radius <= 5000) return 14; // 城区 if (radius <= 10000) return 13; // 大片区 return 12; } const getPolygonCenterAndZoom = function (coords) { const center = getPolygonCenter(coords); const radius = getPolygonRadius(coords, center); const zoom = getFitZoomByRadius(radius); return { center, zoom, radius }; } const getIntersectPoints = function (coords, splitLine) { const ring = [...coords, coords[0]] const polygonLine = turf.polygonToLine(turf.polygon([ring])) const intersectSegments = [] for (let i = 0; i < ring.length - 1; i++) { let p1 = ring[i] let p2 = ring[i + 1] let segment = turf.lineString([p1, p2]) let result = turf.lineIntersect(segment, splitLine); let intersectPoints = result.features.map(f => f.geometry.coordinates); // 判断当前线段是否与分割线相交 if (intersectPoints.length > 0) { intersectSegments.push({ index: i, start: p1, end: p2, segment: segment, point: intersectPoints[0].map(item => Math.round(item * 1e6) / 1e6) }) } } return intersectSegments } const getIntersections = function (coords) { // 1. 构造标准闭合多边形 let ring = [...coords, coords[0]]; let polygon = turf.polygon([ring]); // 2. 计算边界 & 中心点(保留6位小数) let [minLng, minLat, maxLng, maxLat] = turf.bbox(polygon); let cLng = Math.round(((minLng + maxLng) / 2) * 1e6) / 1e6; let cLat = Math.round(((minLat + maxLat) / 2) * 1e6) / 1e6; // 3. 生成横竖线 let vertical = turf.lineString([[cLng, minLat], [cLng, maxLat]]); let horizontal = turf.lineString([[minLng, cLat], [maxLng, cLat]]); // 4. 计算交点和相交线段 let verticalIntersects = getIntersectPoints(coords, vertical) let horizontalIntersects = getIntersectPoints(coords, horizontal) let isVertical = maxLng - minLng > maxLat - minLat return { center: [cLng, cLat], bbox: { minLng: minLng, minLat: minLat, maxLng: maxLng, maxLat: maxLat }, verticalLine: vertical.geometry.coordinates, horizontalLine: horizontal.geometry.coordinates, verticalIntersects: verticalIntersects, horizontalIntersects: horizontalIntersects, isVertical: isVertical, line: isVertical ? vertical.geometry.coordinates : horizontal.geometry.coordinates, intersects: isVertical ? verticalIntersects : horizontalIntersects }; } const splitPolygon = function (coords, intersects) { let [p1, p2] = intersects let polygons = [ [p1.point, ...coords.slice(p1.index + 1, p2.index + 1), p2.point], [p2.point, ...coords.slice(p2.index + 1, coords.length), ...coords.slice(0, p1.index + 1), p1.point], ] return polygons; } const drawLine = function (path, color) { let line = new AMap.Polyline({ path: path, strokeColor: 'blue', strokeWeight: 2 }); line.setMap(map); overlays.add(line) } // POI搜索 const searchBySelectedPolygon = function () { if (!selected.area.id) { alert("请先选择一个区域"); return; } const keywords = document.getElementById("searchKey").value.trim(); const typeArr = Array.from(selected.types); if (typeArr.length === 0) { alert("请至少选择一个POI类型"); return; } // db.transaction("areas").objectStore("areas").get(selected.area.id).onsuccess = e => { // const area = e.target.result; db.areas.get(selected.area.id).then(area => { let ps const pageSize = 50; let overLimit = false; const delay = (ms) => new Promise((r) => setTimeout(r, ms)); async function searchInBounds(types, keywords, coords) { let queue = (area.overLimit || []).length > 0 ? area.overLimit : [{ coords: coords, types: [types.join('|')] }] return new Promise(async (resolve) => { let res = []; let count = 0; let cur while (queue.length > 0) { cur = queue.shift() let polygonStr = cur.coords.concat(cur.coords[0]).join("|") for (let type of cur.types) { if (count % 3 == 1) await delay(1000); let result = await new Promise((resolve) => { ps.setPageIndex(1); ps.setType(type); ps.searchInBounds( keywords, polygonStr, (status, resData) => { if (status !== "complete") { overLimit = resData == "USER_DAILY_QUERY_OVER_LIMIT" resolve({ pois: [], total: 0 }); return; } resolve({ pois: resData?.poiList?.pois?.map((v) => ({ ...v, area: area.name, mainType: v?.type?.split(';')?.[0], })) || [], total: resData.poiList.count }); } ); }); res.push(...result.pois); count++; if (result.total > pageSize) { let depthLimit = Math.floor(Math.log2(result.total / pageSize)); let subQueue = [{ coords: cur.coords, depth: 0 }]; let splitParts = [] while (subQueue.length > 0) { let { coords, depth } = subQueue.pop() let { line, intersects } = getIntersections(coords) drawLine(line, 'blue') let parts = splitPolygon(coords, intersects) if (depth < depthLimit) subQueue.push(...parts.map(v => ({ coords: v, depth: depth + 1 }))) else splitParts.push(...parts) } queue.push(...splitParts.map(v => ({ coords: v, types: [type] }))) break } let color = overLimit ? '#F4606C' : '#FFA502' let poly = new AMap.Polygon({ path: cur.coords, fillColor: color, fillOpacity: 0.2, strokeColor: color, strokeWeight: 2 }) poly.setMap(map) overlays.add(poly) if (overLimit) { queue.push(cur) db.areas.put({ ...area, overLimit: queue }) queue = [] break } } } resolve(res); }); } AMap.plugin("AMap.PlaceSearch", function () { ps = new AMap.PlaceSearch({ city: "全国", pageSize: pageSize, extensions: "all", }); poiList = []; searchInBounds(typeArr, keywords, area.coords).then((v) => { if (!overLimit) { delete area["overLimit"] db.areas.put({ ...area }) } const uniqueMap = new Map(); v.forEach((poi) => poi.id && uniqueMap.set(poi.id, poi)); poiList = Array.from(uniqueMap.values()); savePOIToDB(poiList); handleAllPOIData(poiList); }); }); }); } const handleAllPOIData = function (pois) { clearAllMarkers(); poiList = pois.map((poi) => ({ ID: poi.id, 名称: poi.name || "-", 地址: poi.address || "-", 电话: poi.tel || "-", 经度: poi.location?.lng.toFixed(6) || "-", 纬度: poi.location?.lat.toFixed(6) || "-", 大类: poi.mainType || poi?.type?.split(';')?.[0] || '-', 类型: poi.type || "-", 区域: poi.area, })); filteredPoiList = [...poiList]; let massPoints = poiList .filter((p) => p.经度 !== "-" && p.纬度 !== "-") .map((p) => ({ lnglat: [p.经度, p.纬度], name: p.名称 || "" })); let style = { url: "https://webapi.amap.com/theme/v1.3/markers/b/mark_bs.png", size: new AMap.Size(9, 15), anchor: new AMap.Pixel(5, 5), }; massMarker = new AMap.MassMarks(massPoints, { zIndex: 111, zooms: [1, 100], cursor: "pointer", style: style, }); massMarker.setMap(map); let marker = new AMap.Marker({ content: " ", map: map }); massMarker.on("mouseover", (e) => { marker.setPosition(e.data.lnglat); marker.setLabel({ content: e.data.name }); }); massMarker.on("mouseout", () => { marker.setLabel({ content: " " }); }); overlays.add(marker) paginationProps.current = 1; paginationProps.refresh(); } const initFieldDragSort = function () { const el = document.getElementById("fieldList"); let drag = null; el.addEventListener("dragstart", (e) => { if (e.target.classList.contains("field-item")) { drag = e.target; e.target.style.opacity = "0.5"; } }); el.addEventListener("dragover", (e) => { e.preventDefault(); }); el.addEventListener("drop", (e) => { e.preventDefault(); if (!drag || !e.target.classList.contains("field-item")) return; const items = Array.from(el.querySelectorAll(".field-item")); const oldIdx = items.indexOf(drag); const newIdx = items.indexOf(e.target); if (oldIdx < newIdx) el.insertBefore(drag, e.target.nextSibling); else el.insertBefore(drag, e.target); fieldConfig = Array.from(el.querySelectorAll(".field-item")).map( (i) => fieldConfig.find((f) => f.key === i.dataset.key) ); }); el.addEventListener("dragend", (e) => { if (e.target.classList.contains("field-item")) { e.target.style.opacity = "1"; drag = null; } }); } const openFieldModal = function () { const fl = document.getElementById("fieldList"); let html = ""; fieldConfig.forEach((f) => { html += `
可编辑:
`; }); fl.innerHTML = html; initFieldDragSort(); document.getElementById("fieldModalMask").classList.add("show"); document.getElementById("fieldModal").style.display = "block"; } const closeFieldModal = function () { document.getElementById("fieldModalMask").classList.remove("show"); document.getElementById("fieldModal").style.display = "none"; } const toggleFieldVisible = function (key) { fieldConfig = fieldConfig.map((f) => f.key === key ? { ...f, visible: !f.visible } : f ); } const toggleFieldEditable = function (key, editable) { fieldConfig = fieldConfig.map((f) => f.key === key ? { ...f, editable } : f ); } const saveFieldConfig = function () { localStorage.setItem("poiFieldConfig", JSON.stringify(fieldConfig)); renderPOITableHeader(); renderPOITable(); closeFieldModal(); alert("字段配置保存成功!"); } const renderPOITableHeader = function () { const hd = document.getElementById("poiTableHeader"); let html = ""; fieldConfig.filter((f) => f.visible).forEach((f) => { html += `${f.label}`; }); html += ""; hd.innerHTML = html; } // 渲染表格(本地筛选) const renderPOITable = function () { renderPOITableHeader(); const start = (paginationProps.current - 1) * paginationProps.size; const end = start + paginationProps.size; const pageData = filteredPoiList.slice(start, end); const tb = document.getElementById("poiTable"); let html = ""; pageData.forEach((item, idx) => { html += ``; fieldConfig.filter((f) => f.visible).forEach((f) => { const val = item[f.key]; if (f.editable) html += ``; else html += `${val}`; }); html += ""; }); if (filteredPoiList.length > 0) { const colCount = fieldConfig.filter((f) => f.visible).length; html += `本页${pageData.length}条 | 总计${filteredPoiList.length}条`; } tb.innerHTML = html; } // 编辑字段 const editPOIField = function (idx, key, val) { if (idx < 0 || idx >= filteredPoiList.length) return; filteredPoiList[idx][key] = val; poiList[idx][key] = val; if (key === "名称") { const pts = filteredPoiList .filter((p) => p.经度 !== "-" && p.纬度 !== "-") .map((p) => ({ lnglat: [p.经度, p.纬度], name: p.名称 || "" })); if (massMarker) massMarker.setData(pts); } } // 定位POI const focusPOI = function (idx) { if (idx >= filteredPoiList.length) return; const p = filteredPoiList[idx]; if (p.经度 !== "-" && p.纬度 !== "-") { map.panTo([parseFloat(p.经度), parseFloat(p.纬度)]); map.setZoom(19); } document.querySelectorAll("#poiTable tr").forEach((tr, i) => { tr.classList.toggle( "poi-active", i === idx - (paginationProps.current - 1) * paginationProps.size ); }); } // 清空 const clearAllMarkers = function () { if (massMarker) { massMarker.clear(); massMarker = null; } } const clearAll = function () { if (currentPolygon.target) currentPolygon.remove(); clearAllMarkers(); overlays.removeAll() editor?.close() poiList = []; filteredPoiList = []; paginationProps.current = 1; paginationProps.refresh(); document.querySelectorAll(".poi-active").forEach((e) => e.classList.remove("poi-active")); map.clearMap(); } exportMethods = { togglePanel, startDraw, savePOIToDB, loadPOIFromDB, saveCurrentPolygon, loadAreaList, renderAreaList, selectArea, deleteArea, renameArea, loadPolygon, getPolygonCenter, getDistance, getPolygonRadius, getFitZoomByRadius, getPolygonCenterAndZoom, getIntersectPoints, getIntersections, splitPolygon, searchBySelectedPolygon, handleAllPOIData, renderPOITableHeader, renderPOITable, editPOIField, focusPOI, initFieldDragSort, openFieldModal, closeFieldModal, toggleFieldVisible, toggleFieldEditable, saveFieldConfig, drawLine, clearAllMarkers, clearAll, previewAreas, repartPOIArea, exportExcel, } unsafeWindow.utils = exportMethods map.plugin(["AMap.MouseTool", "AMap.PolyEditor"], () => mouseTool = new AMap.MouseTool(map)); db.version(2).stores({ areas: `++id`, pois: `++id` }); loadAreaList() poiTypes.render(); renderPOITableHeader(); } elmGetter.get('#amap-global-container').then(target => { observer = new MutationObserver(() => { map = target?.amap if (map) { init(); observer.disconnect() } }) observer.observe(target, { attributes: true }) })