Skip to content

Commit 33dcbdf

Browse files
committed
feat: 增强 ArcFlowLayer/RouteLayer 能力并同步文档与构建配置
- ArcFlowLayer/RouteLayer: 扩展能力、改进示例 - core: 导出更新、统一三份 vite 配置 - site: 同步 ArcFlow 示例、BuiltinGlyphs demo 清理无效图标 - docs: 更新 route-layer / aimap / getting-started 文档 - skills: 同步 SKILL.md 与 composite-layers 参考
1 parent 7e1c82c commit 33dcbdf

13 files changed

Lines changed: 425 additions & 158 deletions

File tree

packages/core/src/components/CompositeLayer/ArcFlowLayer.tsx

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import React, { useMemo } from 'react';
1+
import React, { useCallback, useMemo, useState } from 'react';
22
import type { LayerSchema, LayerEventPayload, AnimateConfig } from '../../schema/types';
33
import { LineLayer } from '../Layer/LineLayer';
44
import { PointLayer } from '../Layer/PointLayer';
5+
import { Tooltip } from '../Interaction/Tooltip';
6+
import { Popup } from '../Interaction/Popup';
57

68
/**
79
* 弧线形态
@@ -100,6 +102,10 @@ export interface ArcFlowLayerProps {
100102
nodePulse?: boolean;
101103

102104
// ===== 交互 =====
105+
/** 是否在 hover 弧线时显示 Tooltip,默认 true */
106+
showTooltip?: boolean;
107+
/** 是否在点击节点时显示 Popup,默认 true */
108+
showNodePopup?: boolean;
103109
/** hover 高亮色 */
104110
activeColor?: string;
105111
/** 弧线 hover 事件 */
@@ -162,6 +168,8 @@ export function ArcFlowLayer({
162168
nodeSize = 4,
163169
nodeSizeRange,
164170
nodePulse = false,
171+
showTooltip = true,
172+
showNodePopup = true,
165173
activeColor = '#FFD93D',
166174
onArcHover,
167175
onArcClick,
@@ -247,6 +255,49 @@ export function ArcFlowLayer({
247255
// 节点颜色
248256
const resolvedNodeColor = nodeColor ?? color;
249257

258+
// 内置 Tooltip 状态(hover 弧线)
259+
const [tooltipState, setTooltipState] = useState<{
260+
visible: boolean; lng: number; lat: number; from: string; to: string; weight: number;
261+
}>({ visible: false, lng: 0, lat: 0, from: '', to: '', weight: 0 });
262+
263+
const handleArcHoverInternal = useCallback((payload: LayerEventPayload) => {
264+
onArcHover?.(payload);
265+
if (!showTooltip) return;
266+
const f = payload.feature;
267+
if (!f) return;
268+
setTooltipState({
269+
visible: true,
270+
lng: payload.lng,
271+
lat: payload.lat,
272+
from: String(f.fromName ?? ''),
273+
to: String(f.toName ?? ''),
274+
weight: Number(f[weightField] ?? 0),
275+
});
276+
}, [onArcHover, showTooltip, weightField]);
277+
278+
const handleArcMouseLeave = useCallback(() => {
279+
setTooltipState((prev) => ({ ...prev, visible: false }));
280+
}, []);
281+
282+
// 内置 Popup 状态(点击节点)
283+
const [popupState, setPopupState] = useState<{
284+
visible: boolean; lng: number; lat: number; name: string; degree: number;
285+
}>({ visible: false, lng: 0, lat: 0, name: '', degree: 0 });
286+
287+
const handleNodeClickInternal = useCallback((payload: LayerEventPayload) => {
288+
onNodeClick?.(payload);
289+
if (!showNodePopup) return;
290+
const f = payload.feature;
291+
if (!f) return;
292+
setPopupState({
293+
visible: true,
294+
lng: payload.lng,
295+
lat: payload.lat,
296+
name: String(f.name ?? ''),
297+
degree: Number(f.degree ?? 0),
298+
});
299+
}, [onNodeClick, showNodePopup]);
300+
250301
return (
251302
<>
252303
{/* 弧线层 */}
@@ -264,7 +315,8 @@ export function ArcFlowLayer({
264315
animate={animateConfig}
265316
active={activeColor ? { color: activeColor } : false}
266317
style={arcStyle}
267-
onMouseMove={onArcHover}
318+
onMouseMove={handleArcHoverInternal}
319+
onMouseLeave={handleArcMouseLeave}
268320
onClick={onArcClick}
269321
/>
270322

@@ -285,7 +337,34 @@ export function ArcFlowLayer({
285337
}}
286338
animate={nodePulse ? { enable: true, speed: 0.6 } : undefined}
287339
active={activeColor ? { color: activeColor } : false}
288-
onClick={onNodeClick}
340+
onClick={handleNodeClickInternal}
341+
/>
342+
)}
343+
344+
{/* 内置 Tooltip — hover 弧线时展示 */}
345+
{showTooltip && tooltipState.visible && (
346+
<Tooltip
347+
longitude={tooltipState.lng}
348+
latitude={tooltipState.lat}
349+
visible
350+
variant="dark"
351+
items={[
352+
{ label: '起点', value: tooltipState.from },
353+
{ label: '终点', value: tooltipState.to },
354+
{ label: '流量', value: tooltipState.weight },
355+
]}
356+
/>
357+
)}
358+
359+
{/* 内置 Popup — 点击节点时展示 */}
360+
{showNodePopup && popupState.visible && (
361+
<Popup
362+
longitude={popupState.lng}
363+
latitude={popupState.lat}
364+
content={`<div style="min-width:80px"><div style="font-weight:700;font-size:13px;margin-bottom:4px">${popupState.name || '节点'}</div><div style="font-size:12px;color:#64748b">连接数: ${popupState.degree}</div></div>`}
365+
closeButton
366+
size="compact"
367+
onClose={() => setPopupState((prev) => ({ ...prev, visible: false }))}
289368
/>
290369
)}
291370
</>

packages/core/src/components/CompositeLayer/RouteLayer.tsx

Lines changed: 91 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useMemo, useState } from 'react';
1+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
22
import type { LayerEventPayload } from '../../schema/types';
33
import { LineLayer } from '../Layer/LineLayer';
44
import { PointLayer } from '../Layer/PointLayer';
@@ -41,6 +41,29 @@ export interface RouteSegment {
4141
width?: number;
4242
}
4343

44+
/** 路径类型 */
45+
export type RouteType = 'straight' | 'arc' | 'walking' | 'cycling' | 'driving' | 'transit';
46+
47+
/** 交通路线查询参数 */
48+
export interface RouteQueryParams {
49+
origin: [number, number];
50+
destination: [number, number];
51+
waypoints?: [number, number][];
52+
routeType: 'walking' | 'cycling' | 'driving' | 'transit';
53+
}
54+
55+
/** 交通路线查询结果 */
56+
export interface RouteQueryResult {
57+
/** 完整路径坐标 */
58+
path: [number, number][];
59+
/** 分段路径(可选,如返回多段路况着色路径) */
60+
segments?: RouteSegment[];
61+
/** 途中补充站点(可选,如公交换乘站) */
62+
stops?: RouteStop[];
63+
/** 路线信息 */
64+
info?: { distance?: number; duration?: number; description?: string };
65+
}
66+
4467
export interface RouteLayerProps {
4568
/** 路径坐标 — 完整线坐标或分段 */
4669
path?: [number, number][];
@@ -49,6 +72,14 @@ export interface RouteLayerProps {
4972
/** 途经点列表 */
5073
stops?: RouteStop[];
5174

75+
// ===== 路径模式 =====
76+
/** 路径类型,默认 'straight' */
77+
routeType?: RouteType;
78+
/** 路线查询回调 — routeType 为 walking/cycling/driving/transit 时使用 */
79+
onRouteQuery?: (params: RouteQueryParams) => Promise<RouteQueryResult>;
80+
/** 路线查询完成回调 */
81+
onRouteResult?: (result: RouteQueryResult) => void;
82+
5283
// ===== 路径视觉 =====
5384
/** 路径颜色,默认 '#2563eb' */
5485
color?: string;
@@ -122,6 +153,9 @@ export function RouteLayer({
122153
path,
123154
segments,
124155
stops = [],
156+
routeType = 'straight',
157+
onRouteQuery,
158+
onRouteResult,
125159
color = '#2563eb',
126160
lineWidth = 4,
127161
opacity = 0.9,
@@ -143,46 +177,84 @@ export function RouteLayer({
143177
onPathClick,
144178
onStopClick,
145179
}: RouteLayerProps) {
180+
// 交通路线查询结果
181+
const [routeQueryResult, setRouteQueryResult] = useState<RouteQueryResult | null>(null);
182+
const queryVersionRef = useRef(0);
183+
184+
const isTransportMode = routeType === 'walking' || routeType === 'cycling' || routeType === 'driving' || routeType === 'transit';
185+
186+
useEffect(() => {
187+
if (!isTransportMode || !onRouteQuery || stops.length < 2) {
188+
setRouteQueryResult(null);
189+
return;
190+
}
191+
const origin: [number, number] = [stops[0].lng, stops[0].lat];
192+
const destination: [number, number] = [stops[stops.length - 1].lng, stops[stops.length - 1].lat];
193+
const waypoints: [number, number][] = stops.length > 2
194+
? stops.slice(1, -1).map((s) => [s.lng, s.lat])
195+
: [];
196+
197+
const version = ++queryVersionRef.current;
198+
onRouteQuery({ origin, destination, waypoints: waypoints.length > 0 ? waypoints : undefined, routeType })
199+
.then((result) => {
200+
if (queryVersionRef.current !== version) return;
201+
setRouteQueryResult(result);
202+
onRouteResult?.(result);
203+
})
204+
.catch(() => {
205+
if (queryVersionRef.current !== version) return;
206+
setRouteQueryResult(null);
207+
});
208+
}, [isTransportMode, onRouteQuery, stops, routeType]);
209+
210+
// 实际使用的路径和分段数据
211+
const effectivePath = isTransportMode && routeQueryResult ? routeQueryResult.path : path;
212+
const effectiveSegments = isTransportMode && routeQueryResult?.segments ? routeQueryResult.segments : segments;
213+
const extraStops = isTransportMode && routeQueryResult?.stops ? routeQueryResult.stops : [];
214+
215+
// 线形状:arc 模式用 'arc',其余用 'line'
216+
const lineShape = routeType === 'arc' ? 'arc' : 'line';
217+
146218
// 构建路径 GeoJSON
147219
const pathGeoJSON = useMemo(() => {
148-
if (segments && segments.length > 0) {
220+
if (effectiveSegments && effectiveSegments.length > 0) {
149221
return {
150222
type: 'FeatureCollection' as const,
151-
features: segments.map((seg, idx) => ({
223+
features: effectiveSegments.map((seg, idx) => ({
152224
type: 'Feature' as const,
153225
properties: { color: seg.color || color, width: seg.width || lineWidth, index: idx },
154226
geometry: { type: 'LineString' as const, coordinates: seg.coordinates },
155227
})),
156228
};
157229
}
158-
if (path && path.length > 1) {
230+
if (effectivePath && effectivePath.length > 1) {
159231
return {
160232
type: 'FeatureCollection' as const,
161233
features: [{
162234
type: 'Feature' as const,
163235
properties: { color, width: lineWidth },
164-
geometry: { type: 'LineString' as const, coordinates: path },
236+
geometry: { type: 'LineString' as const, coordinates: effectivePath },
165237
}],
166238
};
167239
}
168240
return null;
169-
}, [path, segments, color, lineWidth]);
241+
}, [effectivePath, effectiveSegments, color, lineWidth]);
170242

171243
// 发光层 GeoJSON(同路径但宽度更大)
172-
const hasSegmentColors = segments && segments.some((s) => s.color);
244+
const hasSegmentColors = effectiveSegments && effectiveSegments.some((s) => s.color);
173245

174246
// 途经点数据(增加序号和类型)
175247
// 自动补全路径起终点:如果 stops 中没有覆盖 path 的首尾坐标,则自动添加
176248
const stopsWithIndex = useMemo(() => {
177-
const effectivePath = path ?? (segments && segments.length > 0
178-
? [...segments[0].coordinates.slice(0, 1), ...segments[segments.length - 1].coordinates.slice(-1)]
249+
const effectivePathForStops = effectivePath ?? (effectiveSegments && effectiveSegments.length > 0
250+
? [...effectiveSegments[0].coordinates.slice(0, 1), ...effectiveSegments[effectiveSegments.length - 1].coordinates.slice(-1)]
179251
: null);
180252

181-
let merged = [...stops];
253+
let merged = [...stops, ...extraStops];
182254

183-
if (effectivePath && effectivePath.length >= 2) {
184-
const [startLng, startLat] = effectivePath[0];
185-
const [endLng, endLat] = effectivePath[effectivePath.length - 1];
255+
if (effectivePathForStops && effectivePathForStops.length >= 2) {
256+
const [startLng, startLat] = effectivePathForStops[0];
257+
const [endLng, endLat] = effectivePathForStops[effectivePathForStops.length - 1];
186258
const hasStart = stops.some((s) => Math.abs(s.lng - startLng) < 1e-6 && Math.abs(s.lat - startLat) < 1e-6);
187259
const hasEnd = stops.some((s) => Math.abs(s.lng - endLng) < 1e-6 && Math.abs(s.lat - endLat) < 1e-6);
188260

@@ -205,7 +277,7 @@ export function RouteLayer({
205277
indexLabel: String(stop.index ?? idx + 1),
206278
markerColorValue: stop.markerColor ?? resolveMarkerColor(stop.type ?? (idx === 0 ? 'start' : idx === merged.length - 1 ? 'end' : 'waypoint')),
207279
}));
208-
}, [stops, path, segments, color, stopColor, endColor]);
280+
}, [stops, extraStops, effectivePath, effectiveSegments, color, stopColor, endColor]);
209281

210282
const resolvedStopIconMap = useMemo(() => {
211283
if (stopRenderer !== 'icon') return undefined;
@@ -248,10 +320,10 @@ export function RouteLayer({
248320
<LineLayer
249321
source={pathGeoJSON}
250322
sourceType="geojson"
251-
shape="line"
323+
shape={lineShape}
252324
color={hasSegmentColors ? undefined : color}
253325
colorField={hasSegmentColors ? 'color' : undefined}
254-
colorValues={hasSegmentColors ? segments!.map((s) => s.color || color) : undefined}
326+
colorValues={hasSegmentColors ? effectiveSegments!.map((s) => s.color || color) : undefined}
255327
size={lineWidth * 2.5}
256328
style={{ opacity: 0.15 }}
257329
/>
@@ -261,13 +333,13 @@ export function RouteLayer({
261333
<LineLayer
262334
source={pathGeoJSON}
263335
sourceType="geojson"
264-
shape="line"
336+
shape={lineShape}
265337
color={hasSegmentColors ? undefined : color}
266338
colorField={hasSegmentColors ? 'color' : undefined}
267-
colorValues={hasSegmentColors ? segments!.map((s) => s.color || color) : undefined}
339+
colorValues={hasSegmentColors ? effectiveSegments!.map((s) => s.color || color) : undefined}
268340
size={hasSegmentColors ? undefined : lineWidth}
269341
sizeField={hasSegmentColors ? 'width' : undefined}
270-
sizeValues={hasSegmentColors ? segments!.map((s) => s.width || lineWidth) : undefined}
342+
sizeValues={hasSegmentColors ? effectiveSegments!.map((s) => s.width || lineWidth) : undefined}
271343
style={{ opacity }}
272344
active={activeColor ? { color: activeColor } : false}
273345
onClick={onPathClick}

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export type { SatelliteLayerProps, SatelliteProvider } from './components/Compos
131131
export { ArcFlowLayer } from './components/CompositeLayer/ArcFlowLayer';
132132
export type { ArcFlowLayerProps, ArcFlowDataItem, ArcShape, ArcColorMode } from './components/CompositeLayer/ArcFlowLayer';
133133
export { RouteLayer } from './components/CompositeLayer/RouteLayer';
134-
export type { RouteLayerProps, RouteStop, RouteSegment } from './components/CompositeLayer/RouteLayer';
134+
export type { RouteLayerProps, RouteStop, RouteSegment, RouteType, RouteQueryParams, RouteQueryResult } from './components/CompositeLayer/RouteLayer';
135135
export { TiffRasterLayer } from './components/CompositeLayer/TiffRasterLayer';
136136
export type { TiffRasterLayerProps, RampColors, RasterRenderMode } from './components/CompositeLayer/TiffRasterLayer';
137137

packages/core/vite.config.cjs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export default defineConfig({
1515
},
1616
build: {
1717
emptyOutDir: false,
18+
assetsInlineLimit: 0,
1819
lib: {
1920
entry: resolve(__dirname, 'src/index.ts'),
2021
name: 'aimapui',

packages/core/vite.config.iife.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export default defineConfig({
1313
},
1414
build: {
1515
emptyOutDir: false,
16+
assetsInlineLimit: 0,
1617
lib: {
1718
entry: resolve(__dirname, 'src/index.ts'),
1819
name: 'AimapUI',

packages/core/vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export default defineConfig({
2424
},
2525
build: {
2626
emptyOutDir: false,
27+
assetsInlineLimit: 0,
2728
lib: {
2829
entry: resolve(__dirname, 'src/index.ts'),
2930
name: 'aimapui',

0 commit comments

Comments
 (0)