// ==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 = `
`;
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 })
})