b2ddcb1641
- 顶级页 #/map:16 州市边界点阵地图(构建期预计算 yunnan-map.json,Canvas 按容器缩放绘制+ResizeObserver 重绘),载体点位悬浮卡、市域着色与名称标注,全出血占满屏宽 - #/parks 载体列表、#/park-detail 园区详情(位置高德外链/介绍/政策/入驻流程/在园企业) - 导航加「载体」入口;依赖 @suxiong/react-dotted-map 与 dompurify;gen:yunnan-map 脚本可再生成点阵 Co-Authored-By: Claude <noreply@anthropic.com>
62 lines
2.6 KiB
JavaScript
62 lines
2.6 KiB
JavaScript
/**
|
||
* 离线预计算云南点阵地图(构建期一次性执行,替代运行时 1~2s 的地理计算)。
|
||
*
|
||
* node scripts/gen-yunnan-map.mjs
|
||
*
|
||
* 读取 src/data/yunnan-cities.json(16 州市边界 GeoJSON),
|
||
* 生成 src/data/yunnan-map.json(紧凑点阵 + 坐标系参数 + 州市标注锚点)。
|
||
* 运行时 ParkMap.jsx 直接 import 该 JSON 重建 MapConfig,零地理计算。
|
||
*/
|
||
import { readFileSync, writeFileSync } from 'node:fs';
|
||
import { generateMap } from '@suxiong/react-dotted-map';
|
||
|
||
const MAP_W = 980;
|
||
const MAP_H = 1064;
|
||
const SPACING = 3.4;
|
||
const REGION = { lat: { min: 21.0, max: 29.4 }, lng: { min: 97.3, max: 106.4 } };
|
||
|
||
const geo = JSON.parse(readFileSync(new URL('../src/data/yunnan-cities.json', import.meta.url), 'utf8'));
|
||
const cities = geo.features.map((f) => f.properties.name);
|
||
const geojsonByCountry = Object.fromEntries(geo.features.map((f) => [f.properties.name, f]));
|
||
|
||
const t0 = Date.now();
|
||
const map = generateMap({
|
||
width: MAP_W, height: MAP_H, grid: 'diagonal', spacing: SPACING,
|
||
region: REGION, countries: cities, geojsonByCountry,
|
||
detectCountries: true, targetCountries: cities,
|
||
});
|
||
const pts = Object.values(map.points);
|
||
console.log(`generateMap: ${pts.length} points in ${Date.now() - t0}ms`);
|
||
|
||
// 州市标注锚点:最大环的面积加权质心
|
||
function ringCentroid(ring) {
|
||
let a = 0, cx = 0, cy = 0;
|
||
for (let i = 0; i < ring.length - 1; i++) {
|
||
const [x1, y1] = ring[i], [x2, y2] = ring[i + 1];
|
||
const c = x1 * y2 - x2 * y1;
|
||
a += c; cx += (x1 + x2) * c; cy += (y1 + y2) * c;
|
||
}
|
||
if (Math.abs(a) < 1e-9) return ring[0];
|
||
return [cx / (3 * a), cy / (3 * a)];
|
||
}
|
||
const labels = geo.features.map((f) => {
|
||
const g = f.geometry;
|
||
const polys = g.type === 'MultiPolygon' ? g.coordinates : [g.coordinates];
|
||
let best = null, n = -1;
|
||
polys.forEach((p) => { if (p[0].length > n) { n = p[0].length; best = p[0]; } });
|
||
const [lng, lat] = ringCentroid(best);
|
||
return { name: f.properties.name, lat: +lat.toFixed(5), lng: +lng.toFixed(5) };
|
||
});
|
||
|
||
// 紧凑编码:pts = [x, y, cityIndex],坐标体系参数原样保留(latLngToScreenCoords 依赖)
|
||
const out = {
|
||
w: map.width, h: map.height, grid: map.grid, ystep: map.ystep,
|
||
xmin: map.X_MIN, ymin: map.Y_MIN, xmax: map.X_MAX, ymax: map.Y_MAX,
|
||
xr: map.X_RANGE, yr: map.Y_RANGE, region: map.region,
|
||
cities,
|
||
labels,
|
||
pts: pts.map((p) => [p.x, p.y, cities.indexOf(p.countryCode || '')]),
|
||
};
|
||
writeFileSync(new URL('../src/data/yunnan-map.json', import.meta.url), JSON.stringify(out));
|
||
console.log(`written src/data/yunnan-map.json (${(JSON.stringify(out).length / 1024).toFixed(0)}KB)`);
|