Update WBBR frontend template collection
This commit is contained in:
266
app/produce-map/page.tsx
Normal file
266
app/produce-map/page.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { CSSProperties, useEffect, useMemo, useState } from "react";
|
||||
import { geoConicConformal, geoIdentity, geoPath } from "d3-geo";
|
||||
import mapData from "../data/produce-map.json";
|
||||
import styles from "./ProduceMap.module.css";
|
||||
|
||||
type Region = { name: string; localName: string; crops: string[]; season: string; summary: string };
|
||||
type Tab = { id: string; label: string; country: string; iso: string; map: string; accent: string; description: string; regions: Region[] };
|
||||
type Feature = { type: "Feature"; properties: { shapeName?: string }; geometry: unknown };
|
||||
type FeatureCollection = { type: "FeatureCollection"; features: Feature[] };
|
||||
type MapShape = { feature: Feature; d: string; centroid: [number, number]; name: string; key: string };
|
||||
type DrilldownArea = { name: string; localName: string; crops: string[]; season: string; summary: string };
|
||||
|
||||
const tabs = mapData.tabs as Tab[];
|
||||
const hebeiCities: DrilldownArea[] = [
|
||||
{ name: "Shijiazhuang City", localName: "石家庄市", crops: ["小麦", "玉米", "梨", "乳制品"], season: "6月—10月", summary: "太行山东麓平原农业集中,是河北中南部粮食与果品生产区。" },
|
||||
{ name: "Tangshan City", localName: "唐山市", crops: ["水稻", "玉米", "花生", "板栗"], season: "7月—10月", summary: "燕山南麓与滨海平原兼具粮食、油料和京东板栗等特色产品。" },
|
||||
{ name: "Qinhuangdao City", localName: "秦皇岛市", crops: ["葡萄", "板栗", "苹果", "水产"], season: "7月—10月", summary: "山海相邻,昌黎葡萄、燕山板栗及沿海水产品具有代表性。" },
|
||||
{ name: "Handan City", localName: "邯郸市", crops: ["小麦", "玉米", "棉花", "核桃"], season: "6月—10月", summary: "冀南平原适合粮棉种植,西部山区发展核桃等林果产品。" },
|
||||
{ name: "Xingtai City", localName: "邢台市", crops: ["板栗", "苹果", "小麦", "酸枣"], season: "6月—10月", summary: "太行山区林果与东部平原粮食生产并行,酸枣产业特色明显。" },
|
||||
{ name: "Baoding City", localName: "保定市", crops: ["草莓", "桃", "柿子", "小麦"], season: "5月—10月", summary: "山地、丘陵和平原类型齐全,形成多样化果品和粮食产区。" },
|
||||
{ name: "Zhangjiakou City", localName: "张家口市", crops: ["马铃薯", "燕麦", "杂豆", "葡萄"], season: "7月—10月", summary: "坝上高原气候冷凉,是马铃薯、燕麦和错季蔬菜优势产区。" },
|
||||
{ name: "Chengde City", localName: "承德市", crops: ["山楂", "板栗", "食用菌", "杂粮"], season: "7月—10月", summary: "燕山山地森林资源丰富,林果、菌类和旱作杂粮较有特色。" },
|
||||
{ name: "Cangzhou City", localName: "沧州市", crops: ["金丝小枣", "鸭梨", "玉米", "水产"], season: "8月—10月", summary: "滨海平原和盐碱地农业突出,以金丝小枣、鸭梨及水产著名。" },
|
||||
{ name: "Langfang City", localName: "廊坊市", crops: ["蔬菜", "梨", "葡萄", "食用菌"], season: "5月—11月", summary: "京津之间的都市近郊农业区,设施蔬菜、果品和菌类供应集中。" },
|
||||
{ name: "Hengshui City", localName: "衡水市", crops: ["小麦", "玉米", "棉花", "辣椒"], season: "6月—10月", summary: "黑龙港流域平原农业为主,粮棉与特色蔬菜种植规模较大。" }
|
||||
];
|
||||
const regionTranslations: Record<string, Record<string, string>> = {
|
||||
china: {
|
||||
"Shanghai Municipality": "上海市",
|
||||
"Chongqing Municipality": "重庆市"
|
||||
},
|
||||
japan: {
|
||||
"Aichi Prefecture": "爱知县", Akita: "秋田县", Aomori: "青森县", Chiba: "千叶县", "Ehime Prefecture": "爱媛县", "Fukui Prefecture": "福井县", "Fukuoka Prefecture": "福冈县", Fukushima: "福岛县", "Gifu Prefecture": "岐阜县", Gunma: "群马县", Hiroshima: "广岛县", Hokkaido: "北海道", "Hyogo Prefecture": "兵库县", Ibaraki: "茨城县", "Ishikawa Prefecture": "石川县", Iwate: "岩手县", "Kagawa Prefecture": "香川县", "Kagoshima Prefecture": "鹿儿岛县", Kanagawa: "神奈川县", "Kochi Prefecture": "高知县", Kumamoto: "熊本县", "Kyoto Prefecture": "京都府", "Mie Prefecture": "三重县", Miyagi: "宫城县", "Miyazaki Prefecture": "宫崎县", Nagano: "长野县", "Nagasaki Prefecture": "长崎县", "Nara Prefecture": "奈良县", Niigata: "新潟县", Oita: "大分县", "Okayama Prefecture": "冈山县", "Okinawa Prefecture": "冲绳县", "Osaka Prefecture": "大阪府", "Saga Prefecture": "佐贺县", Saitama: "埼玉县", Shiga: "滋贺县", Shimane: "岛根县", Shizuoka: "静冈县", Tochigi: "栃木县", "Tokushima Prefecture": "德岛县", Tokyo: "东京都", "Tottori Prefecture": "鸟取县", Toyama: "富山县", "Wakayama Prefecture": "和歌山县", Yamagata: "山形县", Yamaguchi: "山口县", Yamanashi: "山梨县"
|
||||
}
|
||||
};
|
||||
|
||||
export default function ProduceMapPage() {
|
||||
const [activeId, setActiveId] = useState(tabs[0].id);
|
||||
const [geoData, setGeoData] = useState<FeatureCollection | null>(null);
|
||||
const [selectedName, setSelectedName] = useState(tabs[0].regions[0].name);
|
||||
const [selectedMapName, setSelectedMapName] = useState(tabs[0].regions[0].name);
|
||||
const [hoveredName, setHoveredName] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [hebeiGeoData, setHebeiGeoData] = useState<FeatureCollection | null>(null);
|
||||
const [selectedHebeiCity, setSelectedHebeiCity] = useState(hebeiCities[0].name);
|
||||
const [hoveredHebeiCity, setHoveredHebeiCity] = useState<string | null>(null);
|
||||
const active = tabs.find((item) => item.id === activeId) ?? tabs[0];
|
||||
const selected = active.regions.find((item) => item.name === selectedName) ?? null;
|
||||
const selectedDisplayName = selected?.localName ?? localRegionName(active.id, selectedMapName);
|
||||
const cropNames = useMemo(() => Array.from(new Set(active.regions.flatMap((item) => item.crops))), [active]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setGeoData(null);
|
||||
setSelectedName(active.regions[0].name);
|
||||
setSelectedMapName(active.regions[0].name);
|
||||
fetch(active.map).then((response) => response.json()).then((data: FeatureCollection) => {
|
||||
if (!cancelled) { setGeoData(data); setLoading(false); }
|
||||
}).catch(() => { if (!cancelled) setLoading(false); });
|
||||
return () => { cancelled = true; };
|
||||
}, [active]);
|
||||
|
||||
const mapShapes = useMemo(() => {
|
||||
if (!geoData) return [];
|
||||
return buildMapShapes(geoData, active.id);
|
||||
}, [geoData, active.id]);
|
||||
const selectedShape = mapShapes.find((shape) => shape.name === selectedMapName);
|
||||
const showHebeiDrilldown = active.id === "china" && selectedMapName === "Hebei Province";
|
||||
const hebeiShapes = useMemo(() => hebeiGeoData ? projectMapFeatures(hebeiGeoData.features, [[44, 42], [956, 598]], "hebei-city") : [], [hebeiGeoData]);
|
||||
const selectedCity = hebeiCities.find((city) => city.name === selectedHebeiCity) ?? hebeiCities[0];
|
||||
const selectedCityShape = hebeiShapes.find((shape) => shape.name === selectedHebeiCity);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/maps/hebei-adm2.geojson").then((response) => response.json()).then((data: FeatureCollection) => setHebeiGeoData(data)).catch(() => setHebeiGeoData(null));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showHebeiDrilldown) return;
|
||||
const frame = requestAnimationFrame(() => document.getElementById("province-detail")?.scrollIntoView({ behavior: "smooth", block: "start" }));
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [showHebeiDrilldown]);
|
||||
|
||||
const activateTab = (tab: Tab) => { setActiveId(tab.id); setHoveredName(null); };
|
||||
const selectMappedRegion = (name: string) => {
|
||||
setSelectedMapName(name);
|
||||
setSelectedName(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.page} style={{ "--accent": active.accent } as CSSProperties}>
|
||||
<header className={styles.topbar}>
|
||||
<Link href="/" className={styles.brand}><span>产</span><div><strong>Terra Produce</strong><small>GLOBAL ORIGIN ATLAS</small></div></Link>
|
||||
<nav><Link href="/">模板主页</Link><a href="#atlas">产物地图</a><a href="#regions">产区清单</a></nav>
|
||||
<div className={styles.status}><i />本地地图与 JSON 数据</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section className={styles.hero}>
|
||||
<div><p>GLOBAL PRODUCE MAP</p><h1>全球主要产物地图</h1><span>按地区浏览国家地图,高亮到省、州、都道府县等一级行政区,点击查看该产区的代表作物。</span></div>
|
||||
<div className={styles.metrics}><article><small>地区入口</small><strong>{tabs.length}</strong><span>全球区域</span></article><article><small>示例产区</small><strong>{tabs.reduce((sum, item) => sum + item.regions.length, 0)}</strong><span>一级行政区</span></article><article><small>当前作物</small><strong>{cropNames.length}</strong><span>{active.country}</span></article></div>
|
||||
</section>
|
||||
|
||||
<section className={styles.atlas} id="atlas">
|
||||
<div className={styles.tabs} role="tablist" aria-label="地区地图">
|
||||
{tabs.map((tab) => <button role="tab" aria-selected={active.id === tab.id} key={tab.id} className={active.id === tab.id ? styles.activeTab : ""} onClick={() => activateTab(tab)}>{tab.label}<small>{tab.country}</small></button>)}
|
||||
</div>
|
||||
|
||||
<div className={styles.mapLayout}>
|
||||
<section className={styles.mapPanel}>
|
||||
<header><div><small>{active.iso} · ADMIN LEVEL 1</small><h2>{active.label} · {active.country}</h2><p>{active.description}</p></div><div className={styles.legend}><span><i />主要产区</span><span><i />其他行政区</span></div></header>
|
||||
<div className={styles.mapCanvas}>
|
||||
{loading && <div className={styles.mapLoading}><span /><p>正在加载行政区地图…</p></div>}
|
||||
{!loading && !geoData && <div className={styles.mapLoading}><b>!</b><p>地图数据加载失败,请刷新重试。</p></div>}
|
||||
{geoData && <svg viewBox="0 0 1000 640" role="img" aria-label={`${active.country}一级行政区产物地图`}>
|
||||
<defs><filter id="mapGlow"><feGaussianBlur stdDeviation="7" result="blur" /><feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge></filter></defs>
|
||||
{active.id === "usa" && <g className={styles.usaInsets} aria-hidden="true">
|
||||
<rect x="44" y="462" width="224" height="156" rx="10" /><text x="58" y="482">阿拉斯加</text>
|
||||
<rect x="282" y="520" width="142" height="98" rx="10" /><text x="296" y="540">夏威夷</text>
|
||||
<rect x="440" y="546" width="116" height="72" rx="10" /><text x="452" y="566">波多黎各</text>
|
||||
</g>}
|
||||
<g className={styles.mapViewport}>{mapShapes.map(({ d, name, key }) => {
|
||||
const marked = active.regions.some((region) => region.name === name);
|
||||
const isSelected = selectedMapName === name;
|
||||
return <path key={key} d={d} tabIndex={0} role="button" className={`${styles.mapRegion} ${marked ? styles.markedRegion : ""} ${isSelected ? styles.selectedRegion : ""}`} onClick={() => selectMappedRegion(name)} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") selectMappedRegion(name); }} onMouseEnter={() => setHoveredName(name)} onMouseLeave={() => setHoveredName(null)} aria-pressed={isSelected} aria-label={`选择${name}`} />;
|
||||
})}{selectedShape && <g className={styles.mapLabel} transform={`translate(${selectedShape.centroid[0]} ${selectedShape.centroid[1]})`} aria-hidden="true"><circle r="7" /><path d="M0 7 L0 22" /><rect x="-38" y="22" width="76" height="25" rx="7" /><text y="39">{selectedDisplayName}</text></g>}</g>
|
||||
</svg>}
|
||||
{hoveredName && <div className={styles.mapTooltip}><strong>{active.regions.find((item) => item.name === hoveredName)?.localName ?? localRegionName(active.id, hoveredName)}</strong><small>{hoveredName}</small><span>{active.regions.some((item) => item.name === hoveredName) ? "点击高亮并查看主要产物" : "点击高亮,作物资料待补充"}</span></div>}
|
||||
<div className={styles.mapHint}>点击彩色行政区查看主要作物</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside className={styles.detailPanel}>
|
||||
<header><small>SELECTED REGION</small><span>{active.iso}</span><h2>{selectedDisplayName}</h2><p>{selectedMapName}</p></header>
|
||||
<div className={styles.detailBody}>
|
||||
{selected ? <section><span>代表作物</span><div className={styles.cropGrid}>{selected.crops.map((crop, index) => <article key={crop}><b>{cropIcon(crop)}</b><div><strong>{crop}</strong><small>主要产物 {String(index + 1).padStart(2, "0")}</small></div></article>)}</div></section> : <section className={styles.emptyProduce}><b>+</b><span>暂无作物标记</span><p>该一级行政区已选中并高亮,可继续在 JSON 数据中补充主要产物。</p></section>}
|
||||
<dl><div><dt>主要产季</dt><dd>{selected?.season ?? "待补充"}</dd></div><div><dt>行政层级</dt><dd>一级行政区</dd></div><div><dt>所属国家</dt><dd>{active.country}</dd></div></dl>
|
||||
<section className={styles.originNote}><span>产区说明</span><p>{selected?.summary ?? `${selectedMapName}当前暂无主要产物说明,已保留完整点击与高亮交互。`}</p></section>
|
||||
</div>
|
||||
<footer><button onClick={() => document.getElementById("regions")?.scrollIntoView({ behavior: "smooth" })}>查看全部重点产区 ↓</button></footer>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{showHebeiDrilldown && <section className={styles.drilldownSection} id="province-detail">
|
||||
<div className={styles.drilldownHeader}>
|
||||
<div><p>PROVINCE DRILLDOWN · ADM2</p><h2>河北省 · 市级行政单位</h2><span>从全国一级行政区继续下钻,点击市级边界查看当地代表产物。</span></div>
|
||||
<button onClick={() => document.getElementById("atlas")?.scrollIntoView({ behavior: "smooth" })}>↑ 返回中国地图</button>
|
||||
</div>
|
||||
<div className={styles.mapLayout}>
|
||||
<section className={styles.mapPanel}>
|
||||
<header><div><small>HEBEI · ADMIN LEVEL 2</small><h2>河北省市级地图</h2><p>共 11 个地级市,保持与国家地图一致的点击与高亮方式。</p></div><div className={styles.legend}><span><i />已选城市</span><span><i />其他城市</span></div></header>
|
||||
<div className={styles.mapCanvas}>
|
||||
{hebeiGeoData ? <svg viewBox="0 0 1000 640" role="img" aria-label="河北省市级行政区产物地图">
|
||||
<defs><filter id="cityGlow"><feGaussianBlur stdDeviation="7" result="blur" /><feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge></filter></defs>
|
||||
<g className={styles.mapViewport}>{hebeiShapes.map(({ d, name, key }) => {
|
||||
const isSelected = selectedHebeiCity === name;
|
||||
return <path key={key} d={d} tabIndex={0} role="button" className={`${styles.mapRegion} ${styles.markedRegion} ${isSelected ? styles.selectedRegion : ""}`} onClick={() => setSelectedHebeiCity(name)} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") setSelectedHebeiCity(name); }} onMouseEnter={() => setHoveredHebeiCity(name)} onMouseLeave={() => setHoveredHebeiCity(null)} aria-pressed={isSelected} aria-label={`选择${hebeiCities.find((city) => city.name === name)?.localName ?? name}`} />;
|
||||
})}{selectedCityShape && <g className={styles.mapLabel} transform={`translate(${selectedCityShape.centroid[0]} ${selectedCityShape.centroid[1]})`} aria-hidden="true"><circle r="7" /><path d="M0 7 L0 22" /><rect x="-42" y="22" width="84" height="25" rx="7" /><text y="39">{selectedCity.localName}</text></g>}</g>
|
||||
</svg> : <div className={styles.mapLoading}><span /><p>正在加载河北省市级地图…</p></div>}
|
||||
{hoveredHebeiCity && <div className={styles.mapTooltip}><strong>{hebeiCities.find((city) => city.name === hoveredHebeiCity)?.localName ?? hoveredHebeiCity}</strong><small>{hoveredHebeiCity}</small><span>点击高亮并查看市级产物</span></div>}
|
||||
<div className={styles.mapHint}>点击市级区域查看主要产物</div>
|
||||
</div>
|
||||
</section>
|
||||
<aside className={styles.detailPanel}>
|
||||
<header><small>SELECTED CITY</small><span>HEB</span><h2>{selectedCity.localName}</h2><p>{selectedCity.name}</p></header>
|
||||
<div className={styles.detailBody}>
|
||||
<section><span>代表产物</span><div className={styles.cropGrid}>{selectedCity.crops.map((crop, index) => <article key={crop}><b>{cropIcon(crop)}</b><div><strong>{crop}</strong><small>市级产物 {String(index + 1).padStart(2, "0")}</small></div></article>)}</div></section>
|
||||
<dl><div><dt>主要产季</dt><dd>{selectedCity.season}</dd></div><div><dt>行政层级</dt><dd>地级市</dd></div><div><dt>所属地区</dt><dd>中国 · 河北省</dd></div></dl>
|
||||
<section className={styles.originNote}><span>产区说明</span><p>{selectedCity.summary}</p></section>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
<div className={styles.cityList}>{hebeiCities.map((city, index) => <button key={city.name} className={selectedHebeiCity === city.name ? styles.activeCity : ""} onClick={() => setSelectedHebeiCity(city.name)}><span>{String(index + 1).padStart(2, "0")}</span><div><small>{city.name}</small><strong>{city.localName}</strong></div><b>{city.crops[0]} · {city.crops[1]}</b></button>)}</div>
|
||||
</section>}
|
||||
|
||||
<section className={styles.regionSection} id="regions">
|
||||
<div className={styles.sectionTitle}><div><p>PRIMARY ORIGINS</p><h2>{active.country}重点产区</h2></div><span>{active.regions.length} 个示例行政区 · 点击可同步高亮地图</span></div>
|
||||
<div className={styles.regionGrid}>{active.regions.map((region, index) => <button key={region.name} className={selectedMapName === region.name ? styles.activeRegionCard : ""} onClick={() => { setSelectedName(region.name); setSelectedMapName(region.name); document.getElementById("atlas")?.scrollIntoView({ behavior: "smooth" }); }}><span>{String(index + 1).padStart(2, "0")}</span><div><small>{region.name}</small><strong>{region.localName}</strong><p>{region.summary}</p><footer>{region.crops.map((crop) => <i key={crop}>{crop}</i>)}</footer></div><b>地图定位 ↗</b></button>)}</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function cropIcon(crop: string) {
|
||||
if (/苹果|梨|樱桃|葡萄|芒果|橙|柑|莓|瓜|果|香蕉|菠萝|荔枝/.test(crop)) return "果";
|
||||
if (/小麦|大麦|玉米|水稻|燕麦|荞麦|大豆|油菜|豆|扁豆|豌豆/.test(crop)) return "谷";
|
||||
if (/茶|咖啡|可可|啤酒花|葡萄酒/.test(crop)) return "饮";
|
||||
if (/奶|乳|猪|水产/.test(crop)) return "鲜";
|
||||
return "植";
|
||||
}
|
||||
|
||||
function localRegionName(tabId: string, name: string) {
|
||||
return regionTranslations[tabId]?.[name] ?? name;
|
||||
}
|
||||
|
||||
function projectMapFeatures(features: Feature[], extent: [[number, number], [number, number]], keyPrefix = "map"): MapShape[] {
|
||||
if (!features.length) return [];
|
||||
// geoBoundaries uses planar GeoJSON rings. The reflected identity projection
|
||||
// preserves the real outlines without treating the ring winding as a globe.
|
||||
const collection: FeatureCollection = { type: "FeatureCollection", features };
|
||||
const projection = geoIdentity().reflectY(true).fitExtent(extent, collection as never);
|
||||
const path = geoPath(projection);
|
||||
return features.map((feature, index) => ({
|
||||
feature,
|
||||
d: path(feature as never) ?? "",
|
||||
centroid: path.centroid(feature as never) as [number, number],
|
||||
name: feature.properties.shapeName ?? "未命名区域",
|
||||
key: `${keyPrefix}-${feature.properties.shapeName ?? index}`
|
||||
}));
|
||||
}
|
||||
|
||||
function buildMapShapes(data: FeatureCollection, tabId: string): MapShape[] {
|
||||
if (tabId === "russia") return projectRussiaFeatures(data.features);
|
||||
if (tabId !== "usa") return projectMapFeatures(data.features, [[44, 42], [956, 598]]);
|
||||
|
||||
const byName = (names: string[]) => data.features.filter((feature) => names.includes(feature.properties.shapeName ?? ""));
|
||||
const detached = new Set([
|
||||
"Alaska", "Hawaii", "Puerto Rico", "United States Virgin Islands",
|
||||
"American Samoa", "Guam", "Commonwealth of the Northern Mariana Islands"
|
||||
]);
|
||||
const mainland = data.features.filter((feature) => !detached.has(feature.properties.shapeName ?? ""));
|
||||
|
||||
// The contiguous states fill the canvas. Detached states and territories are
|
||||
// placed in compact insets so they no longer shrink the main US silhouette.
|
||||
return [
|
||||
...projectMapFeatures(mainland, [[34, 48], [966, 520]], "usa-main"),
|
||||
...projectMapFeatures(byName(["Alaska"]), [[52, 472], [260, 610]], "usa-alaska"),
|
||||
...projectMapFeatures(byName(["Hawaii"]), [[290, 532], [416, 610]], "usa-hawaii"),
|
||||
...projectMapFeatures(byName(["Puerto Rico", "United States Virgin Islands"]), [[448, 570], [548, 610]], "usa-caribbean"),
|
||||
...projectMapFeatures(byName(["American Samoa"]), [[588, 574], [650, 610]], "usa-samoa"),
|
||||
...projectMapFeatures(byName(["Guam"]), [[680, 574], [718, 610]], "usa-guam"),
|
||||
...projectMapFeatures(byName(["Commonwealth of the Northern Mariana Islands"]), [[750, 570], [810, 610]], "usa-mariana")
|
||||
];
|
||||
}
|
||||
|
||||
function projectRussiaFeatures(features: Feature[]): MapShape[] {
|
||||
const projectedFeatures = features.map(reverseSphericalRings);
|
||||
const collection: FeatureCollection = { type: "FeatureCollection", features: projectedFeatures };
|
||||
const projection = geoConicConformal().parallels([50, 70]).rotate([-100, 0]).fitExtent([[34, 42], [966, 598]], collection as never);
|
||||
const path = geoPath(projection);
|
||||
return projectedFeatures.map((feature, index) => ({
|
||||
feature,
|
||||
d: path(feature as never) ?? "",
|
||||
centroid: path.centroid(feature as never) as [number, number],
|
||||
name: feature.properties.shapeName ?? "未命名区域",
|
||||
key: `russia-${feature.properties.shapeName ?? index}`
|
||||
}));
|
||||
}
|
||||
|
||||
function reverseSphericalRings(feature: Feature): Feature {
|
||||
const geometry = feature.geometry as { type?: string; coordinates?: unknown };
|
||||
if (geometry.type === "Polygon") {
|
||||
const coordinates = geometry.coordinates as number[][][];
|
||||
return { ...feature, geometry: { ...geometry, coordinates: coordinates.map((ring) => [...ring].reverse()) } };
|
||||
}
|
||||
if (geometry.type === "MultiPolygon") {
|
||||
const coordinates = geometry.coordinates as number[][][][];
|
||||
return { ...feature, geometry: { ...geometry, coordinates: coordinates.map((polygon) => polygon.map((ring) => [...ring].reverse())) } };
|
||||
}
|
||||
return feature;
|
||||
}
|
||||
Reference in New Issue
Block a user