// 坐标解析: 接受地图链接(苹果地图 / 高德, 含短链), 抠出经纬度+名称。 // 高德为 GCJ-02; 苹果地图在中国大陆同为 GCJ-02。两者都转 WGS84 再喂给 wloc; // gcj02ToWgs84 内含 out_of_china 判断, 境外坐标原样返回(无操作)。 export function safeDecode(s) { if (!s) return ""; try { return decodeURIComponent(String(s).replace(/\+/g, " ")); } catch (e) { return String(s); } } // 从一段字符串里提取经纬度+名称。兼容: // 苹果地图 coordinate=/ll=/sll=纬度,经度 (名称在 name=...) // 高德 ?p=POIID,纬度,经度,名称,城市 (逗号或 %2C) // 高德 ?q=纬度,经度,名称 (新版分享链, 逗号或 %2C) // 纯文本 纬度,经度 export function extractFromString(s) { if (!s) return null; const str = String(s); let m; m = str.match(/(?:coordinate|ll|sll)=(-?\d{1,3}\.\d+)(?:,|%2C)(-?\d{1,3}\.\d+)/i); if (m) { const nm = str.match(/[?&]name=([^&]+)/i); return { lat: +m[1], lon: +m[2], name: nm ? safeDecode(nm[1]) : "", src: "apple" }; } m = str.match( /[?&]p=[^,&%]*(?:,|%2C)(-?\d{1,3}\.\d+)(?:,|%2C)(-?\d{1,3}\.\d+)(?:(?:,|%2C)((?:(?!,|%2C|&).)+))?/i ); if (m) return { lat: +m[1], lon: +m[2], name: m[3] ? safeDecode(m[3]) : "", src: "amap" }; m = str.match( /[?&]q=(-?\d{1,3}\.\d+)(?:,|%2C)(-?\d{1,3}\.\d+)(?:(?:,|%2C)((?:(?!,|%2C|&).)+))?/i ); if (m) return { lat: +m[1], lon: +m[2], name: m[3] ? safeDecode(m[3]) : "", src: "amap" }; m = str.match(/(-?\d{1,3}\.\d{4,})\s*(?:,|%2C)\s*(-?\d{1,3}\.\d{4,})/); if (m) return { lat: +m[1], lon: +m[2], name: "", src: "text" }; return null; } // 接受原文(可能含中文地名+链接), 抠出 URL, 必要时跟随重定向展开短链, 提取坐标。 export async function parseCoords(raw) { const text = String(raw || "").trim(); if (!text) throw new Error("空输入"); const urlMatch = text.match(/https?:\/\/[^\s'"<>]+/i); let target = urlMatch ? urlMatch[0] : text; let hit = extractFromString(target); if (hit) return hit; if (urlMatch) { let cur = target; for (let i = 0; i < 5; i++) { let resp; try { resp = await fetch(cur, { redirect: "manual", headers: { "user-agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/27.0 Mobile/24A5370h Safari/604.1", accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "accept-language": "zh-CN,zh-Hans;q=0.9", }, }); } catch (e) { break; } const loc = resp.headers.get("location"); if (loc) { hit = extractFromString(loc); if (hit) return hit; cur = new URL(loc, cur).toString(); hit = extractFromString(cur); if (hit) return hit; continue; } hit = extractFromString(resp.url); if (hit) return hit; try { const body = await resp.text(); hit = extractFromString(body); if (hit) return hit; } catch (e) {} break; } } throw new Error("未能从链接中解析出经纬度"); } export function round6(n) { return Math.round(Number(n) * 1e6) / 1e6; } const GCJ_A = 6378245.0; const GCJ_EE = 0.00669342162296594323; function gcjOutOfChina(lng, la) { return lng < 72.004 || lng > 137.8347 || la < 0.8293 || la > 55.8271; } function gcjDeltaLat(x, y) { let r = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x)); r += ((20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0) / 3.0; r += ((20.0 * Math.sin(y * Math.PI) + 40.0 * Math.sin((y / 3.0) * Math.PI)) * 2.0) / 3.0; r += ((160.0 * Math.sin((y / 12.0) * Math.PI) + 320 * Math.sin((y * Math.PI) / 30.0)) * 2.0) / 3.0; return r; } function gcjDeltaLon(x, y) { let r = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x)); r += ((20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0) / 3.0; r += ((20.0 * Math.sin(x * Math.PI) + 40.0 * Math.sin((x / 3.0) * Math.PI)) * 2.0) / 3.0; r += ((150.0 * Math.sin((x / 12.0) * Math.PI) + 300.0 * Math.sin((x / 30.0) * Math.PI)) * 2.0) / 3.0; return r; } // WGS84 -> GCJ-02 (正向偏移), 与高德/苹果中国所用偏移一致。 export function wgs84ToGcj02(lat, lon) { if (gcjOutOfChina(lon, lat)) return { lat, lon }; let dLat = gcjDeltaLat(lon - 105.0, lat - 35.0); let dLon = gcjDeltaLon(lon - 105.0, lat - 35.0); const radLat = (lat / 180.0) * Math.PI; let magic = Math.sin(radLat); magic = 1 - GCJ_EE * magic * magic; const sqrtMagic = Math.sqrt(magic); dLat = (dLat * 180.0) / (((GCJ_A * (1 - GCJ_EE)) / (magic * sqrtMagic)) * Math.PI); dLon = (dLon * 180.0) / ((GCJ_A / sqrtMagic) * Math.cos(radLat) * Math.PI); return { lat: lat + dLat, lon: lon + dLon }; } // GCJ-02 -> WGS84 (迭代反算, 亚米级)。 // 单程反算在偏移梯度大的地区会残留 1~2m, 这里用不动点迭代收敛到 <0.1m, // 与高德自身的 WGS84->GCJ 逆运算严格对齐, 消除回看时的残差。 export function gcj02ToWgs84(lat, lon) { if (gcjOutOfChina(lon, lat)) return { lat, lon }; let wgsLat = lat; let wgsLon = lon; for (let i = 0; i < 6; i++) { const g = wgs84ToGcj02(wgsLat, wgsLon); const errLat = g.lat - lat; const errLon = g.lon - lon; if (Math.abs(errLat) < 1e-9 && Math.abs(errLon) < 1e-9) break; wgsLat -= errLat; wgsLon -= errLon; } return { lat: wgsLat, lon: wgsLon }; }