Skip to content

Instantly share code, notes, and snippets.

@chenxuan520
Created November 22, 2025 12:01
Show Gist options
  • Select an option

  • Save chenxuan520/a4272d9979efee1d3c164a111a95eebe to your computer and use it in GitHub Desktop.

Select an option

Save chenxuan520/a4272d9979efee1d3c164a111a95eebe to your computer and use it in GitHub Desktop.
/**
* 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