Created
November 22, 2025 12:01
-
-
Save chenxuan520/a4272d9979efee1d3c164a111a95eebe to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Cloudflare Worker - Baidu Map Adapter (Ellipsoidal Mercator Fix) | |
| * | |
| * 核心修复: | |
| * 使用 [椭球体] 墨卡托逆投影算法,替代 [球体] 算法。 | |
| * 修复了因地球扁率导致的南北方向约 20km 的固定偏差。 | |
| */ | |
| // ========================================== | |
| // 1. 高精度数学库 | |
| // ========================================== | |
| const PI = 3.1415926535897932384626; | |
| const R_MAJOR = 6378137.0; // 长半轴 (a) | |
| const R_MINOR = 6356752.3142; // 短半轴 (b) | |
| const F = 1 / 298.257223563; // 扁率 (f) | |
| // 第一偏心率 (e) | |
| const E = Math.sqrt(1 - (R_MINOR / R_MAJOR) * (R_MINOR / R_MAJOR)); | |
| // ------------------------------------------ | |
| // [核心算法] 椭球体墨卡托 (Web Mercator) -> 经纬度 | |
| // Baidu Web API 返回的 x,y 是基于椭球体的 | |
| // ------------------------------------------ | |
| function mercatorToLngLatEllipsoid(x, y) { | |
| x = parseFloat(x); | |
| y = parseFloat(y); | |
| // 1. 经度计算 (线性,与球体相同) | |
| const lng = (x / R_MAJOR) * (180.0 / PI); | |
| // 2. 纬度计算 (迭代法求解椭球体逆投影) | |
| const ts = Math.exp(-y / R_MAJOR); | |
| let phi = PI / 2 - 2 * Math.atan(ts); // 初始猜测 (球体解) | |
| let dphi = 1.0; | |
| let i = 0; | |
| // 迭代求解 (通常 3-4 次即可收敛) | |
| while (Math.abs(dphi) > 0.0000001 && i < 15) { | |
| const con = E * Math.sin(phi); | |
| dphi = PI / 2 - 2 * Math.atan(ts * Math.pow((1.0 - con) / (1.0 + con), E / 2.0)) - phi; | |
| phi += dphi; | |
| i++; | |
| } | |
| const lat = phi * (180.0 / PI); | |
| return { lng, lat }; | |
| } | |
| // ------------------------------------------ | |
| // 坐标系转换链: BD09 -> GCJ02 -> WGS84 | |
| // ------------------------------------------ | |
| const x_pi = 3.14159265358979324 * 3000.0 / 180.0; | |
| const a = 6378245.0; // GCJ02 使用的长半轴 | |
| const ee = 0.00669342162296594323; | |
| function bd09ToGcj02(bd_lon, bd_lat) { | |
| const x = bd_lon - 0.0065; | |
| const y = bd_lat - 0.006; | |
| const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_pi); | |
| const theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_pi); | |
| return { lng: z * Math.cos(theta), lat: z * Math.sin(theta) }; | |
| } | |
| function transformLat(x, y) { | |
| let ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x)); | |
| ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0; | |
| ret += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0; | |
| ret += (160.0 * Math.sin(y / 12.0 * PI) + 320 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0; | |
| return ret; | |
| } | |
| function transformLon(x, y) { | |
| let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x)); | |
| ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0; | |
| ret += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0; | |
| ret += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0; | |
| return ret; | |
| } | |
| function gcj02ToWgs84(lng, lat) { | |
| if (lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271) return { lng, lat }; | |
| let dlat = transformLat(lng - 105.0, lat - 35.0); | |
| let dlng = transformLon(lng - 105.0, lat - 35.0); | |
| const radlat = lat / 180.0 * PI; | |
| const magic = Math.sin(radlat); | |
| const magic2 = 1 - ee * magic * magic; | |
| const sqrtmagic = Math.sqrt(magic2); | |
| dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic2 * sqrtmagic) * PI); | |
| dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI); | |
| return { lng: lng * 2 - (lng + dlng), lat: lat * 2 - (lat + dlat) }; | |
| } | |
| // 聚合转换: 百度墨卡托(椭球) -> GPS | |
| function convertBaiduToGPS(mx, my) { | |
| // 1. 墨卡托(椭球) -> BD09 | |
| const bd09 = mercatorToLngLatEllipsoid(mx, my); | |
| // 2. BD09 -> GCJ02 | |
| const gcj02 = bd09ToGcj02(bd09.lng, bd09.lat); | |
| // 3. GCJ02 -> WGS84 | |
| return gcj02ToWgs84(gcj02.lng, gcj02.lat); | |
| } | |
| // ========================================== | |
| // 2. 几何中心提取 (Geo String Parser) | |
| // ========================================== | |
| function parseGeo(geoStr, defaultX, defaultY) { | |
| let mx = defaultX; | |
| let my = defaultY; | |
| // 如果 defaultX/Y 为空或 0,强制从 geo 中提取 | |
| // 百度 geo 格式: "Type|x1,y1;x2,y2|..." | |
| if (geoStr && typeof geoStr === 'string') { | |
| try { | |
| const content = geoStr.split('|')[1]; | |
| if (content) { | |
| const points = content.split(';')[0].split(','); // 取第一个点 | |
| if (points.length >= 2) { | |
| mx = parseFloat(points[0]); | |
| my = parseFloat(points[1]); | |
| } | |
| } | |
| } catch (e) {} | |
| } | |
| return { mx, my }; | |
| } | |
| // ========================================== | |
| // 3. Worker 主逻辑 | |
| // ========================================== | |
| export default { | |
| async fetch(request, env, ctx) { | |
| const url = new URL(request.url); | |
| const corsHeaders = { | |
| 'Access-Control-Allow-Origin': '*', | |
| 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', | |
| 'Access-Control-Allow-Headers': 'Content-Type, User-Agent', | |
| }; | |
| if (request.method === 'OPTIONS') return new Response(null, { headers: corsHeaders }); | |
| if (url.pathname === '/') { | |
| return new Response('Nginx OK', { headers: { ...corsHeaders } }); | |
| } | |
| if (url.pathname === '/search') { | |
| const query = url.searchParams.get('q'); | |
| if (!query) return new Response('[]', { headers: corsHeaders }); | |
| // 百度 API | |
| const baiduUrl = `https://map.baidu.com/?newmap=1&reqflag=pcmap&biz=1&from=webmap&qt=s&c=1&wd=${encodeURIComponent(query)}&rn=10&ie=utf-8`; | |
| try { | |
| const res = await fetch(baiduUrl, { | |
| headers: { | |
| 'Referer': 'https://map.baidu.com/', | |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' | |
| } | |
| }); | |
| const text = await res.text(); | |
| if (text.trim().startsWith('<')) return new Response('[]', { headers: corsHeaders }); | |
| const data = JSON.parse(text); | |
| let content = []; | |
| if (data.content) content = data.content; | |
| else if (data.current_city) content = [data.current_city]; | |
| const results = []; | |
| content.forEach((item, idx) => { | |
| // 提取坐标 | |
| const { mx, my } = parseGeo(item.geo, item.x, item.y); | |
| // 校验 | |
| if (mx && my && Math.abs(mx) > 10000) { | |
| // 转换 | |
| const gps = convertBaiduToGPS(mx, my); | |
| results.push({ | |
| place_id: (item.uid ? parseInt(item.uid.substring(0, 8), 16) : 0) + idx, | |
| osm_type: "node", | |
| osm_id: (item.uid ? parseInt(item.uid.substring(0, 8), 16) : 0) + idx, | |
| boundingbox: [ | |
| (gps.lat - 0.002).toFixed(7), (gps.lat + 0.002).toFixed(7), | |
| (gps.lng - 0.002).toFixed(7), (gps.lng + 0.002).toFixed(7) | |
| ], | |
| lat: gps.lat.toFixed(7), | |
| lon: gps.lng.toFixed(7), | |
| display_name: `${item.name}, ${item.addr || ''}`, | |
| class: "place", | |
| type: "point", | |
| importance: 0.5 | |
| }); | |
| } | |
| }); | |
| return new Response(JSON.stringify(results), { | |
| headers: { 'Content-Type': 'application/json', ...corsHeaders } | |
| }); | |
| } catch (e) { | |
| return new Response('[]', { headers: corsHeaders }); | |
| } | |
| } | |
| return new Response('404', { status: 404 }); | |
| } | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment