1880 lines
69 KiB
TypeScript
1880 lines
69 KiB
TypeScript
import { useMemo, useEffect, useRef, useCallback } from 'react'
|
||
import Map, { Source, Layer } from 'react-map-gl'
|
||
import type { MapRef } from 'react-map-gl'
|
||
import type { Map as MapboxMap } from 'mapbox-gl'
|
||
import 'mapbox-gl/dist/mapbox-gl.css'
|
||
import { useReplaySituation } from '@/hooks/useReplaySituation'
|
||
import { usePlaybackStore } from '@/store/playbackStore'
|
||
import { config } from '@/config'
|
||
import { EXTENDED_WAR_ZONES, KURDISH_FRONT_GEOJSON } from '@/data/extendedWarData'
|
||
import { useWarMapData } from '@/hooks/useWarMapData'
|
||
import { createTacticalPincerAtProgress } from '@/utils/tacticalPincerArrow'
|
||
|
||
const MAPBOX_TOKEN = config.mapboxAccessToken || ''
|
||
|
||
// 相关区域 bbox:伊朗、以色列、胡塞区 (minLng, minLat, maxLng, maxLat),覆盖红蓝区域
|
||
const THEATER_BBOX = [22, 11, 64, 41] as const
|
||
/** 移动端/小屏时 fitBounds 使区域完整显示 */
|
||
const THEATER_BOUNDS: [[number, number], [number, number]] = [
|
||
[THEATER_BBOX[0], THEATER_BBOX[1]],
|
||
[THEATER_BBOX[2], THEATER_BBOX[3]],
|
||
]
|
||
const THEATER_CENTER = {
|
||
longitude: (THEATER_BBOX[0] + THEATER_BBOX[2]) / 2,
|
||
latitude: (THEATER_BBOX[1] + THEATER_BBOX[3]) / 2,
|
||
}
|
||
const DEFAULT_VIEW = { ...THEATER_CENTER, zoom: 4.2 }
|
||
|
||
const COUNTRIES_GEOJSON =
|
||
'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_110m_admin_0_countries.geojson'
|
||
|
||
// 胡塞武装失控区 [lng, lat] 闭环
|
||
const HOUTHI_POLYGON: [number, number][] = [
|
||
[42.7, 15.8],
|
||
[43.3, 16.5],
|
||
[45.1, 17.2],
|
||
[45.8, 15.1],
|
||
[44.2, 13.5],
|
||
[42.7, 15.8],
|
||
]
|
||
|
||
const IRAN_ADMIN = 'Iran'
|
||
const ALLIES_ADMIN = [
|
||
'Qatar',
|
||
'Bahrain',
|
||
'Kuwait',
|
||
'United Arab Emirates',
|
||
'Saudi Arabia',
|
||
'Iraq',
|
||
'Syria',
|
||
'Jordan',
|
||
'Turkey',
|
||
'Israel',
|
||
'Oman',
|
||
'Egypt',
|
||
'Djibouti',
|
||
]
|
||
|
||
// 伊朗攻击源 德黑兰 [lng, lat](若后端 map_strike_source 有 iran/tehran 则可由 API 覆盖)
|
||
const TEHRAN_SOURCE: [number, number] = [51.389, 35.6892]
|
||
|
||
/** 攻击动画时间衰减:N 天内按天衰减(脉冲缩小、频次降低),超出仅保留减弱呼吸效果;天数可在编辑面板配置 */
|
||
const MS_PER_DAY = 24 * 60 * 60 * 1000
|
||
function isWithinAnimationWindow(
|
||
iso: string | null | undefined,
|
||
referenceTime: string,
|
||
cutoffDays: number
|
||
): boolean {
|
||
if (!iso) return true
|
||
const ref = new Date(referenceTime).getTime()
|
||
const t = new Date(iso).getTime()
|
||
const days = (ref - t) / MS_PER_DAY
|
||
return days >= 0 && days <= cutoffDays
|
||
}
|
||
/** 衰减系数 0..1:天数越久越小,用于缩小脉冲范围、降低攻击频次 */
|
||
function getDecayFactor(iso: string | null | undefined, referenceTime: string, cutoffDays: number): number {
|
||
if (!iso) return 1
|
||
const ref = new Date(referenceTime).getTime()
|
||
const t = new Date(iso).getTime()
|
||
const days = (ref - t) / MS_PER_DAY
|
||
if (days < 0 || days > cutoffDays) return 0
|
||
return Math.max(0, 1 - days / cutoffDays)
|
||
}
|
||
|
||
// API 未返回 mapData 时的静态 fallback,保证美/以打击线与动画不消失(与 server/seed.js 一致)
|
||
const FALLBACK_STRIKE_SOURCES: { id: string; name: string; lng: number; lat: number }[] = [
|
||
{ id: 'israel', name: '以色列', lng: 34.78, lat: 32.08 },
|
||
{ id: 'lincoln', name: '林肯号航母', lng: 58.4215, lat: 24.1568 },
|
||
{ id: 'ford', name: '福特号航母', lng: 24.1002, lat: 35.7397 },
|
||
]
|
||
const FALLBACK_STRIKE_LINES: { sourceId: string; targets: { lng: number; lat: number; name?: string }[] }[] = [
|
||
{
|
||
sourceId: 'israel',
|
||
targets: [
|
||
{ lng: 50.88, lat: 34.64, name: '库姆' },
|
||
{ lng: 50.876409, lat: 34.625448, name: '伊朗专家会议秘书处' },
|
||
{ lng: 51.916, lat: 33.666, name: '纳坦兹' },
|
||
{ lng: 51.002, lat: 35.808, name: '卡拉季无人机厂' },
|
||
{ lng: 35.5, lat: 33.86, name: '贝鲁特南郊指挥所' },
|
||
{ lng: 35.32, lat: 33.34, name: '利塔尼弹药库' },
|
||
{ lng: 36.2, lat: 34.01, name: '巴勒贝克后勤枢纽' },
|
||
{ lng: 35.19, lat: 33.27, name: '提尔海岸阵地' },
|
||
{ lng: 36.38, lat: 34.39, name: '赫尔梅勒无人机阵地' },
|
||
],
|
||
},
|
||
{
|
||
sourceId: 'lincoln',
|
||
targets: [
|
||
{ lng: 56.27, lat: 27.18, name: '阿巴斯港海军司令部' },
|
||
{ lng: 57.08, lat: 27.13, name: '米纳布' },
|
||
{ lng: 56.5, lat: 27.0, name: '霍尔木兹岸防阵地' },
|
||
{ lng: 50.838, lat: 28.968, name: '布什尔雷达站' },
|
||
{ lng: 51.667, lat: 32.654, name: '伊斯法罕核设施' },
|
||
],
|
||
},
|
||
{
|
||
sourceId: 'ford',
|
||
targets: [
|
||
{ lng: 51.42, lat: 35.69, name: '哈梅内伊官邸' },
|
||
{ lng: 51.41, lat: 35.72, name: '总统府/情报部' },
|
||
{ lng: 51.15, lat: 35.69, name: '梅赫拉巴德机场' },
|
||
{ lng: 46.29, lat: 38.08, name: '大不里士空军基地' },
|
||
{ lng: 47.076, lat: 34.314, name: '克尔曼沙赫导弹掩体' },
|
||
{ lng: 46.42, lat: 33.64, name: '伊拉姆导弹阵地' },
|
||
{ lng: 48.35, lat: 33.48, name: '霍拉马巴德储备库' },
|
||
],
|
||
},
|
||
]
|
||
|
||
/** 二次贝塞尔曲线路径,更平滑的弧线 height 控制弧高 */
|
||
function parabolaPath(
|
||
start: [number, number],
|
||
end: [number, number],
|
||
height = 3
|
||
): [number, number][] {
|
||
const ctrl: [number, number] = [
|
||
(start[0] + end[0]) / 2,
|
||
(start[1] + end[1]) / 2 + height,
|
||
]
|
||
// 生成多段点使曲线更平滑
|
||
const pts: [number, number][] = [start]
|
||
for (let i = 1; i < 12; i++) {
|
||
const s = i / 12
|
||
const t = 1 - s
|
||
const x = t * t * start[0] + 2 * t * s * ctrl[0] + s * s * end[0]
|
||
const y = t * t * start[1] + 2 * t * s * ctrl[1] + s * s * end[1]
|
||
pts.push([x, y])
|
||
}
|
||
pts.push(end)
|
||
return pts
|
||
}
|
||
|
||
/** 沿路径插值,t ∈ [0,1],支持多点路径 */
|
||
function interpolateOnPath(path: [number, number][], t: number): [number, number] {
|
||
if (t <= 0) return path[0]
|
||
if (t >= 1) return path[path.length - 1]
|
||
const n = path.length - 1
|
||
const seg = Math.min(Math.floor(t * n), n - 1)
|
||
const u = (t * n) - seg
|
||
const a = path[seg]
|
||
const b = path[seg + 1]
|
||
return [a[0] + u * (b[0] - a[0]), a[1] + u * (b[1] - a[1])]
|
||
}
|
||
|
||
type BaseStatus = 'operational' | 'damaged' | 'attacked'
|
||
|
||
interface KeyLoc {
|
||
name: string
|
||
lat: number
|
||
lng: number
|
||
type?: string
|
||
status?: BaseStatus
|
||
damage_level?: number
|
||
}
|
||
|
||
function toFeature(loc: KeyLoc, side: 'us' | 'iran', status?: BaseStatus) {
|
||
return {
|
||
type: 'Feature' as const,
|
||
properties: {
|
||
side,
|
||
name: loc.name,
|
||
status: status ?? (loc as KeyLoc & { status?: BaseStatus }).status ?? 'operational',
|
||
},
|
||
geometry: {
|
||
type: 'Point' as const,
|
||
coordinates: [loc.lng, loc.lat] as [number, number],
|
||
},
|
||
}
|
||
}
|
||
|
||
const FLIGHT_DURATION_MS = 2500 // 光点飞行单程时间
|
||
|
||
/** 移动端/小屏降低动画更新频率以减轻卡顿;返回最小间隔 ms */
|
||
function getAnimIntervalMs(): number {
|
||
try {
|
||
if (typeof window === 'undefined') return 33
|
||
const reducedMotion =
|
||
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||
if (reducedMotion) return 100 // 约 10fps,兼顾可访问性
|
||
return window.innerWidth <= 768 ? 50 : 33 // 移动端约 20fps,桌面约 30fps
|
||
} catch {
|
||
return 33
|
||
}
|
||
}
|
||
|
||
export function WarMap() {
|
||
const mapRef = useRef<MapRef>(null)
|
||
const containerRef = useRef<HTMLDivElement>(null)
|
||
const animRef = useRef<number>(0)
|
||
const pincerAnimRef = useRef<{ lastProgressStep?: number }>({ lastProgressStep: -1 })
|
||
const startRef = useRef<number>(0)
|
||
const lastAnimUpdateRef = useRef<number>(0)
|
||
const attackPathsRef = useRef<[number, number][][]>([])
|
||
const lincolnPathsRef = useRef<[number, number][][]>([])
|
||
const fordPathsRef = useRef<[number, number][][]>([])
|
||
const israelPathsRef = useRef<[number, number][][]>([])
|
||
const hezbollahPathsRef = useRef<[number, number][][]>([])
|
||
const hormuzPathsRef = useRef<[number, number][][]>([])
|
||
const situation = useReplaySituation()
|
||
const { isReplayMode, playbackTime } = usePlaybackStore()
|
||
const { usForces, iranForces, conflictEvents = [] } = situation
|
||
/** 时间衰减基准:回放模式用回放时刻,否则用数据更新时间或当前时间 */
|
||
const referenceTime =
|
||
isReplayMode ? playbackTime : situation.lastUpdated || new Date().toISOString()
|
||
|
||
const usLocs = (usForces.keyLocations || []) as KeyLoc[]
|
||
const irLocs = (iranForces.keyLocations || []) as KeyLoc[]
|
||
|
||
const { usNaval, usBaseOp, usBaseDamaged, usBaseAttacked, labelsGeoJson } = useMemo(() => {
|
||
const naval: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||
const op: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||
const damaged: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||
const attacked: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||
const labels: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||
|
||
for (const loc of usLocs as KeyLoc[]) {
|
||
const f = toFeature(loc, 'us')
|
||
labels.push({ ...f, properties: { ...f.properties, name: loc.name } })
|
||
if (loc.type === 'Base') {
|
||
const s = (loc.status ?? 'operational') as BaseStatus
|
||
if (s === 'attacked') attacked.push(f)
|
||
else if (s === 'damaged') damaged.push(f)
|
||
else op.push(f)
|
||
} else {
|
||
naval.push(f)
|
||
}
|
||
}
|
||
|
||
for (const loc of irLocs) {
|
||
const f = toFeature(loc, 'iran')
|
||
labels.push({ ...f, properties: { ...f.properties, name: loc.name } })
|
||
}
|
||
|
||
return {
|
||
usNaval: { type: 'FeatureCollection' as const, features: naval },
|
||
usBaseOp: { type: 'FeatureCollection' as const, features: op },
|
||
usBaseDamaged: { type: 'FeatureCollection' as const, features: damaged },
|
||
usBaseAttacked: { type: 'FeatureCollection' as const, features: attacked },
|
||
labelsGeoJson: { type: 'FeatureCollection' as const, features: labels },
|
||
}
|
||
}, [usForces.keyLocations, iranForces.keyLocations])
|
||
|
||
const mapData = situation.mapData
|
||
const strikeSources =
|
||
mapData?.strikeSources?.length > 0 ? mapData.strikeSources : FALLBACK_STRIKE_SOURCES
|
||
const strikeLines =
|
||
mapData?.strikeLines?.length > 0 ? mapData.strikeLines : FALLBACK_STRIKE_LINES
|
||
|
||
/** 伊朗→美军基地:仅用 DB 数据,5 天内显示飞行动画 */
|
||
const strikeCutoffDays = situation.animationConfig?.strikeCutoffDays ?? 5
|
||
|
||
const attackPaths = useMemo(() => {
|
||
const attacked = (usForces.keyLocations || []).filter(
|
||
(loc): loc is typeof loc & { attacked_at: string } =>
|
||
loc.status === 'attacked' &&
|
||
!!loc.attacked_at &&
|
||
isWithinAnimationWindow(loc.attacked_at, referenceTime, strikeCutoffDays)
|
||
)
|
||
return attacked.map((loc) => parabolaPath(TEHRAN_SOURCE, [loc.lng, loc.lat]))
|
||
}, [usForces.keyLocations, referenceTime, strikeCutoffDays])
|
||
|
||
attackPathsRef.current = attackPaths
|
||
|
||
const sourceCoords = useMemo(() => {
|
||
const m: Record<string, [number, number]> = {}
|
||
strikeSources.forEach((s) => { m[s.id] = [s.lng, s.lat] })
|
||
return m
|
||
}, [strikeSources])
|
||
|
||
/** 盟军打击线:仅用 DB strikeLines,5 天内目标显示飞行动画 */
|
||
const filterTargetsByAnimationWindow = useMemo(
|
||
() => (targets: { lng: number; lat: number; struck_at?: string | null }[]) =>
|
||
targets.filter((t) => isWithinAnimationWindow(t.struck_at ?? null, referenceTime, strikeCutoffDays)),
|
||
[referenceTime, strikeCutoffDays]
|
||
)
|
||
|
||
const lincolnPaths = useMemo(() => {
|
||
const line = strikeLines.find((l) => l.sourceId === 'lincoln')
|
||
const coords = sourceCoords.lincoln
|
||
if (!coords || !line) return []
|
||
return filterTargetsByAnimationWindow(line.targets).map((t) =>
|
||
parabolaPath(coords, [t.lng, t.lat])
|
||
)
|
||
}, [strikeLines, sourceCoords, filterTargetsByAnimationWindow])
|
||
const fordPaths = useMemo(() => {
|
||
const line = strikeLines.find((l) => l.sourceId === 'ford')
|
||
const coords = sourceCoords.ford
|
||
if (!coords || !line) return []
|
||
return filterTargetsByAnimationWindow(line.targets).map((t) =>
|
||
parabolaPath(coords, [t.lng, t.lat])
|
||
)
|
||
}, [strikeLines, sourceCoords, filterTargetsByAnimationWindow])
|
||
const israelPaths = useMemo(() => {
|
||
const line = strikeLines.find((l) => l.sourceId === 'israel')
|
||
const coords = sourceCoords.israel
|
||
if (!coords || !line) return []
|
||
return filterTargetsByAnimationWindow(line.targets).map((t) =>
|
||
parabolaPath(coords, [t.lng, t.lat])
|
||
)
|
||
}, [strikeLines, sourceCoords, filterTargetsByAnimationWindow])
|
||
|
||
/** 黎巴嫩→以色列:攻击源为黎巴嫩境内多处(提尔、西顿、巴勒贝克等),目标为以色列北部 */
|
||
const hezbollahPaths = useMemo(() => {
|
||
const sources = EXTENDED_WAR_ZONES.lebanonStrikeSources
|
||
const targets = EXTENDED_WAR_ZONES.activeAttacks
|
||
return targets.map((t, i) =>
|
||
parabolaPath(sources[i % sources.length], t.coords, 3)
|
||
)
|
||
}, [])
|
||
|
||
/** 霍尔木兹海峡被打击目标点位;飞行动画由伊朗多处→该区域 */
|
||
const hormuzTargetPoints = useMemo(
|
||
() =>
|
||
[
|
||
[55.7, 25.6],
|
||
[56.0, 26.0],
|
||
[56.4, 26.4],
|
||
] as [number, number][],
|
||
[]
|
||
)
|
||
/** 伊朗多处→霍尔木兹:德黑兰、克尔曼沙赫、库姆 攻击海峡目标(无日期按 decay=1 显示) */
|
||
const hormuzPaths = useMemo(() => {
|
||
const sources: [number, number][] = [
|
||
TEHRAN_SOURCE,
|
||
[47.16, 34.35],
|
||
[50.88, 34.64],
|
||
]
|
||
return hormuzTargetPoints.map((target, idx) =>
|
||
parabolaPath(sources[idx % sources.length], target, 3)
|
||
)
|
||
}, [hormuzTargetPoints])
|
||
|
||
const warMapData = useWarMapData()
|
||
|
||
/** 当前参与动画的目标的最小衰减系数,用于脉冲范围与攻击频次(缩小脉冲、拉长飞行周期) */
|
||
const animationDecayFactor = useMemo(() => {
|
||
const decays: number[] = []
|
||
;(usForces.keyLocations || [])
|
||
.filter((l) => l.status === 'attacked' && l.attacked_at && isWithinAnimationWindow(l.attacked_at, referenceTime, strikeCutoffDays))
|
||
.forEach((l) => decays.push(getDecayFactor(l.attacked_at ?? null, referenceTime, strikeCutoffDays)))
|
||
for (const line of strikeLines) {
|
||
for (const t of filterTargetsByAnimationWindow(line.targets)) {
|
||
decays.push(getDecayFactor(t.struck_at ?? null, referenceTime, strikeCutoffDays))
|
||
}
|
||
}
|
||
if (hormuzPaths.length > 0) decays.push(1)
|
||
if (hezbollahPaths.length > 0) decays.push(1)
|
||
return decays.length > 0 ? Math.min(...decays) : 1
|
||
}, [usForces.keyLocations, strikeLines, referenceTime, strikeCutoffDays, filterTargetsByAnimationWindow, hormuzPaths.length, hezbollahPaths.length])
|
||
|
||
const animationDecayRef = useRef(1)
|
||
animationDecayRef.current = animationDecayFactor
|
||
|
||
lincolnPathsRef.current = lincolnPaths
|
||
fordPathsRef.current = fordPaths
|
||
israelPathsRef.current = israelPaths
|
||
hezbollahPathsRef.current = hezbollahPaths
|
||
hormuzPathsRef.current = hormuzPaths
|
||
|
||
const lincolnLinesGeoJson = useMemo(
|
||
() => ({
|
||
type: 'FeatureCollection' as const,
|
||
features: lincolnPaths.map((coords) => ({
|
||
type: 'Feature' as const,
|
||
properties: {},
|
||
geometry: { type: 'LineString' as const, coordinates: coords },
|
||
})),
|
||
}),
|
||
[lincolnPaths]
|
||
)
|
||
const fordLinesGeoJson = useMemo(
|
||
() => ({
|
||
type: 'FeatureCollection' as const,
|
||
features: fordPaths.map((coords) => ({
|
||
type: 'Feature' as const,
|
||
properties: {},
|
||
geometry: { type: 'LineString' as const, coordinates: coords },
|
||
})),
|
||
}),
|
||
[fordPaths]
|
||
)
|
||
const israelLinesGeoJson = useMemo(
|
||
() => ({
|
||
type: 'FeatureCollection' as const,
|
||
features: israelPaths.map((coords) => ({
|
||
type: 'Feature' as const,
|
||
properties: {},
|
||
geometry: { type: 'LineString' as const, coordinates: coords },
|
||
})),
|
||
}),
|
||
[israelPaths]
|
||
)
|
||
/** 盟军打击目标点位(林肯/福特/以色列→伊朗+以色列→黎巴嫩),带衰减系数供脉冲缩放 */
|
||
const alliedStrikeTargetsFeatures = useMemo(() => {
|
||
const out: GeoJSON.Feature<GeoJSON.Point, { name: string; decay: number }>[] = []
|
||
for (const line of strikeLines) {
|
||
for (const t of line.targets) {
|
||
const decay = getDecayFactor(t.struck_at ?? null, referenceTime, strikeCutoffDays)
|
||
out.push({
|
||
type: 'Feature',
|
||
properties: { name: t.name ?? '', decay },
|
||
geometry: { type: 'Point', coordinates: [t.lng, t.lat] },
|
||
})
|
||
}
|
||
}
|
||
return out
|
||
}, [strikeLines, referenceTime])
|
||
const hezbollahLinesGeoJson = useMemo(
|
||
() => ({
|
||
type: 'FeatureCollection' as const,
|
||
features: hezbollahPaths.map((coords) => ({
|
||
type: 'Feature' as const,
|
||
properties: {},
|
||
geometry: { type: 'LineString' as const, coordinates: coords },
|
||
})),
|
||
}),
|
||
[hezbollahPaths]
|
||
)
|
||
|
||
const hormuzLinesGeoJson = useMemo(
|
||
() => ({
|
||
type: 'FeatureCollection' as const,
|
||
features: hormuzPaths.map((coords) => ({
|
||
type: 'Feature' as const,
|
||
properties: {},
|
||
geometry: { type: 'LineString' as const, coordinates: coords },
|
||
})),
|
||
}),
|
||
[hormuzPaths]
|
||
)
|
||
|
||
const attackLinesGeoJson = useMemo(
|
||
() => ({
|
||
type: 'FeatureCollection' as const,
|
||
features: attackPaths.map((coords) => ({
|
||
type: 'Feature' as const,
|
||
properties: {},
|
||
geometry: { type: 'LineString' as const, coordinates: coords },
|
||
})),
|
||
}),
|
||
[attackPaths]
|
||
)
|
||
|
||
// 真主党当前攻击目标点
|
||
const hezbollahTargetsGeoJson = useMemo(
|
||
() =>
|
||
isReplayMode
|
||
? { type: 'FeatureCollection' as const, features: [] }
|
||
: {
|
||
type: 'FeatureCollection' as const,
|
||
features: EXTENDED_WAR_ZONES.activeAttacks.map((t) => ({
|
||
type: 'Feature' as const,
|
||
properties: { name: t.name, type: t.type, damage: t.damage },
|
||
geometry: { type: 'Point' as const, coordinates: t.coords },
|
||
})),
|
||
},
|
||
[isReplayMode]
|
||
)
|
||
|
||
// 霍尔木兹海峡被持续打击的海面目标(用于脉冲与标记)
|
||
const hormuzTargetsGeoJson = useMemo(
|
||
() =>
|
||
isReplayMode
|
||
? { type: 'FeatureCollection' as const, features: [] }
|
||
: {
|
||
type: 'FeatureCollection' as const,
|
||
features: hormuzTargetPoints.map((coords, idx) => ({
|
||
type: 'Feature' as const,
|
||
properties: { id: `H${idx + 1}` },
|
||
geometry: { type: 'Point' as const, coordinates: coords },
|
||
})),
|
||
},
|
||
[hormuzTargetPoints, isReplayMode]
|
||
)
|
||
|
||
// 霍尔木兹海峡交战区 & 真主党势力范围(静态面)
|
||
const hormuzZone = EXTENDED_WAR_ZONES.hormuzCombatZone
|
||
const hezbollahZone = EXTENDED_WAR_ZONES.hezbollahZone
|
||
|
||
// GDELT 冲突事件:1–3 绿, 4–6 橙闪, 7–10 红脉
|
||
const { conflictEventsGreen, conflictEventsOrange, conflictEventsRed } = useMemo(() => {
|
||
const green: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||
const orange: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||
const red: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||
for (const e of conflictEvents) {
|
||
const score = e.impact_score ?? 1
|
||
const f: GeoJSON.Feature<GeoJSON.Point> = {
|
||
type: 'Feature',
|
||
properties: { event_id: e.event_id, impact_score: score },
|
||
geometry: { type: 'Point', coordinates: [e.lng, e.lat] },
|
||
}
|
||
if (score <= 3) green.push(f)
|
||
else if (score <= 6) orange.push(f)
|
||
else red.push(f)
|
||
}
|
||
return {
|
||
conflictEventsGreen: { type: 'FeatureCollection' as const, features: green },
|
||
conflictEventsOrange: { type: 'FeatureCollection' as const, features: orange },
|
||
conflictEventsRed: { type: 'FeatureCollection' as const, features: red },
|
||
}
|
||
}, [conflictEvents])
|
||
|
||
const hideNonBelligerentLabels = (map: MapboxMap) => {
|
||
const labelLayers = [
|
||
'country-label',
|
||
'state-label',
|
||
'place-label',
|
||
'place-label-capital',
|
||
'place-label-city',
|
||
'place-label-town',
|
||
'place-label-village',
|
||
'poi-label',
|
||
]
|
||
for (const id of labelLayers) {
|
||
try {
|
||
if (map.getLayer(id)) map.setLayoutProperty(id, 'visibility', 'none')
|
||
} catch (_) {}
|
||
}
|
||
}
|
||
|
||
const initAnimation = useRef<(map: MapboxMap) => void>(null!)
|
||
initAnimation.current = (map: MapboxMap) => {
|
||
startRef.current = performance.now()
|
||
|
||
const tick = (t: number) => {
|
||
const elapsed = t - startRef.current
|
||
const intervalMs = getAnimIntervalMs()
|
||
const shouldUpdate = t - lastAnimUpdateRef.current >= intervalMs
|
||
if (shouldUpdate) lastAnimUpdateRef.current = t
|
||
|
||
if (shouldUpdate) {
|
||
const zoom = map.getZoom()
|
||
const zoomScale = Math.max(0.4, Math.min(1.6, zoom / 4.2)) // 镜头拉近效果变大、拉远脉冲半径变小
|
||
const decay = Math.max(0.2, animationDecayRef.current)
|
||
const decayScale = 0.3 + 0.7 * decay // 衰减强度线性插值:decay 低则脉冲半径缩小,避免过多特效
|
||
const step = Math.max(1, Math.round(1 / decay)) // 频次:decay 越小,step 越大,参与动画的路径越少
|
||
try {
|
||
// 伊朗→美军基地:速度固定,仅减少光点数量(频次)
|
||
const src = map.getSource('attack-dots') as { setData: (d: GeoJSON.FeatureCollection) => void } | undefined
|
||
const paths = attackPathsRef.current
|
||
if (src && paths.length > 0) {
|
||
const features: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||
paths.forEach((path, i) => {
|
||
if (i % step !== 0) return
|
||
const progress = (elapsed / FLIGHT_DURATION_MS + i / Math.max(paths.length, 1)) % 1
|
||
features.push({
|
||
type: 'Feature' as const,
|
||
properties: {},
|
||
geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) },
|
||
})
|
||
})
|
||
src.setData({ type: 'FeatureCollection', features })
|
||
}
|
||
// damaged: 橙色闪烁 opacity 0.5 ~ 1, 约 1s 周期
|
||
if (map.getLayer('points-damaged')) {
|
||
const blink = 0.5 + 0.5 * Math.sin(elapsed * 0.003)
|
||
map.setPaintProperty('points-damaged', 'circle-opacity', blink)
|
||
}
|
||
// attacked: 红色脉冲,半径 = 基准×phase×zoomScale×decayScale(线性衰减)
|
||
if (map.getLayer('points-attacked-pulse')) {
|
||
const cycle = 2000
|
||
const phase = Math.max(0, Math.min(1, (elapsed % cycle) / cycle))
|
||
const r = Math.max(0, 32 * phase * zoomScale * decayScale)
|
||
const opacity = Math.min(1, Math.max(0, (0.4 + 0.6 * decay) * (1 - phase)))
|
||
map.setPaintProperty('points-attacked-pulse', 'circle-radius', r)
|
||
map.setPaintProperty('points-attacked-pulse', 'circle-opacity', opacity)
|
||
}
|
||
// 林肯号打击伊朗:蓝色光点
|
||
const lincolnSrc = map.getSource('allied-strike-dots-lincoln') as
|
||
| { setData: (d: GeoJSON.FeatureCollection) => void }
|
||
| undefined
|
||
const lincolnPaths = lincolnPathsRef.current
|
||
if (lincolnSrc && lincolnPaths.length > 0) {
|
||
const features: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||
lincolnPaths.forEach((path, i) => {
|
||
if (i % step !== 0) return
|
||
const progress = (elapsed / FLIGHT_DURATION_MS + 0.5 + i / Math.max(lincolnPaths.length, 1)) % 1
|
||
features.push({
|
||
type: 'Feature' as const,
|
||
properties: {},
|
||
geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) },
|
||
})
|
||
})
|
||
lincolnSrc.setData({ type: 'FeatureCollection', features })
|
||
}
|
||
// 福特号打击伊朗:青色光点
|
||
const fordSrc = map.getSource('allied-strike-dots-ford') as
|
||
| { setData: (d: GeoJSON.FeatureCollection) => void }
|
||
| undefined
|
||
const fordPaths = fordPathsRef.current
|
||
if (fordSrc && fordPaths.length > 0) {
|
||
const features: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||
fordPaths.forEach((path, i) => {
|
||
if (i % step !== 0) return
|
||
const progress = (elapsed / FLIGHT_DURATION_MS + 0.3 + i / Math.max(fordPaths.length, 1)) % 1
|
||
features.push({
|
||
type: 'Feature' as const,
|
||
properties: {},
|
||
geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) },
|
||
})
|
||
})
|
||
fordSrc.setData({ type: 'FeatureCollection', features })
|
||
}
|
||
// 以色列打击伊朗:浅青/白色光点
|
||
const israelSrc = map.getSource('allied-strike-dots-israel') as
|
||
| { setData: (d: GeoJSON.FeatureCollection) => void }
|
||
| undefined
|
||
const israelPaths = israelPathsRef.current
|
||
if (israelSrc && israelPaths.length > 0) {
|
||
const features: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||
israelPaths.forEach((path, i) => {
|
||
if (i % step !== 0) return
|
||
const progress = (elapsed / FLIGHT_DURATION_MS + 0.1 + i / Math.max(israelPaths.length, 1)) % 1
|
||
features.push({
|
||
type: 'Feature' as const,
|
||
properties: {},
|
||
geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) },
|
||
})
|
||
})
|
||
israelSrc.setData({ type: 'FeatureCollection', features })
|
||
}
|
||
// 真主党打击以色列北部:橙红光点,与林肯/福特/以色列同一动画方式
|
||
const hezSrc = map.getSource('hezbollah-strike-dots') as
|
||
| { setData: (d: GeoJSON.FeatureCollection) => void }
|
||
| undefined
|
||
const hezPaths = hezbollahPathsRef.current
|
||
if (hezSrc && hezPaths.length > 0) {
|
||
const features: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||
hezPaths.forEach((path, i) => {
|
||
if (i % step !== 0) return
|
||
const progress = (elapsed / FLIGHT_DURATION_MS + 0.2 + i / Math.max(hezPaths.length, 1)) % 1
|
||
features.push({
|
||
type: 'Feature' as const,
|
||
properties: {},
|
||
geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) },
|
||
})
|
||
})
|
||
hezSrc.setData({ type: 'FeatureCollection', features })
|
||
}
|
||
// 伊朗对霍尔木兹海峡:黄色光点,沿海峡方向飞行
|
||
const hormuzSrc = map.getSource('iran-hormuz-dots') as
|
||
| { setData: (d: GeoJSON.FeatureCollection) => void }
|
||
| undefined
|
||
const hormuzPaths = hormuzPathsRef.current
|
||
if (hormuzSrc && hormuzPaths.length > 0) {
|
||
const features: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||
hormuzPaths.forEach((path, i) => {
|
||
if (i % step !== 0) return
|
||
const progress = (elapsed / FLIGHT_DURATION_MS + 0.15 + i / Math.max(hormuzPaths.length, 1)) % 1
|
||
features.push({
|
||
type: 'Feature' as const,
|
||
properties: {},
|
||
geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) },
|
||
})
|
||
})
|
||
hormuzSrc.setData({ type: 'FeatureCollection', features })
|
||
}
|
||
// 盟军打击目标:脉冲半径 = 基准×decayScale×zoomScale,线性衰减,镜头拉远半径变小
|
||
if (map.getLayer('allied-strike-targets-pulse')) {
|
||
const cycle = 2000
|
||
const phase = Math.max(0, Math.min(1, (elapsed % cycle) / cycle))
|
||
const breathMin = 8
|
||
const breathMax = 26
|
||
const r = Math.max(0, (breathMin + (breathMax - breathMin) * decay) * phase * zoomScale * decayScale)
|
||
const opacity = Math.min(1, Math.max(0, (0.35 + 0.65 * decay) * (1 - phase * 1.15)))
|
||
map.setPaintProperty('allied-strike-targets-pulse', 'circle-radius', r)
|
||
map.setPaintProperty('allied-strike-targets-pulse', 'circle-opacity', opacity)
|
||
}
|
||
// 单箭头钳形动画:生长(缓动) → 保持 2s 循环(已去掉内发光闪烁)
|
||
const pincerGrowthSrc = map.getSource('kurdish-pincer-growth') as
|
||
| { setData: (d: GeoJSON.FeatureCollection) => void }
|
||
| undefined
|
||
const PINCER_GROW_MS = 2500
|
||
const PINCER_HOLD_MS = 2000
|
||
const PINCER_CYCLE_MS = PINCER_GROW_MS + PINCER_HOLD_MS
|
||
const pincerInCycle = elapsed % PINCER_CYCLE_MS
|
||
const isGrowth = pincerInCycle < PINCER_GROW_MS
|
||
const growthT = isGrowth ? pincerInCycle / PINCER_GROW_MS : 1
|
||
const progressEased = growthT * growthT * (3 - 2 * growthT)
|
||
const pincerProgress = isGrowth ? progressEased : 1
|
||
|
||
// 节流:仅当 progress 步进变化或进入保持阶段时更新 GeoJSON,减轻首帧卡顿与每帧 setData 开销
|
||
const progressStep = Math.floor(pincerProgress * 40) / 40
|
||
const lastStep = pincerAnimRef.current.lastProgressStep ?? -1
|
||
const shouldUpdate = progressStep !== lastStep || (!isGrowth && lastStep !== 1)
|
||
if (shouldUpdate) {
|
||
pincerAnimRef.current.lastProgressStep = isGrowth ? progressStep : 1
|
||
const progressToUse = isGrowth ? pincerProgress : 1
|
||
if (pincerGrowthSrc && warMapData.pincerAxes.length > 0) {
|
||
const features: GeoJSON.Feature<GeoJSON.Polygon>[] = warMapData.pincerAxes.map((axis) => ({
|
||
type: 'Feature',
|
||
properties: { name: axis.name },
|
||
geometry: {
|
||
type: 'Polygon',
|
||
coordinates: [createTacticalPincerAtProgress(axis.start, axis.end, progressToUse)],
|
||
},
|
||
}))
|
||
pincerGrowthSrc.setData({ type: 'FeatureCollection', features })
|
||
}
|
||
const israelLebanonSrc = map.getSource('israel-lebanon-arrow') as
|
||
| { setData: (d: GeoJSON.FeatureCollection) => void }
|
||
| undefined
|
||
if (israelLebanonSrc) {
|
||
const { start, end, name } = warMapData.israelLebanonAxis
|
||
israelLebanonSrc.setData({
|
||
type: 'FeatureCollection',
|
||
features: [{
|
||
type: 'Feature',
|
||
properties: { name },
|
||
geometry: {
|
||
type: 'Polygon',
|
||
coordinates: [createTacticalPincerAtProgress(start, end, progressToUse)],
|
||
},
|
||
}],
|
||
})
|
||
}
|
||
}
|
||
if (map.getLayer('attack-pincers')) {
|
||
map.setPaintProperty('attack-pincers', 'fill-opacity', 1)
|
||
}
|
||
if (map.getLayer('attack-pincers-inner-glow')) {
|
||
map.setPaintProperty('attack-pincers-inner-glow', 'fill-opacity', 0)
|
||
}
|
||
if (map.getLayer('israel-lebanon-arrow-fill')) {
|
||
map.setPaintProperty('israel-lebanon-arrow-fill', 'fill-opacity', 1)
|
||
}
|
||
if (map.getLayer('israel-lebanon-arrow-inner-glow')) {
|
||
map.setPaintProperty('israel-lebanon-arrow-inner-glow', 'fill-opacity', 0)
|
||
}
|
||
// GDELT 橙色 (4–6):闪烁
|
||
if (map.getLayer('gdelt-events-orange')) {
|
||
const blink = 0.5 + 0.5 * Math.sin(elapsed * 0.004)
|
||
map.setPaintProperty('gdelt-events-orange', 'circle-opacity', blink)
|
||
}
|
||
// GDELT 红色:脉冲半径随 zoom×decayScale 线性变化
|
||
if (map.getLayer('gdelt-events-red-pulse')) {
|
||
const cycle = 2200
|
||
const phase = Math.max(0, Math.min(1, (elapsed % cycle) / cycle))
|
||
const r = Math.max(0, 24 * phase * zoomScale * decayScale)
|
||
const opacity = Math.min(1, Math.max(0, (0.4 + 0.6 * decay) * (1 - phase * 1.05)))
|
||
map.setPaintProperty('gdelt-events-red-pulse', 'circle-radius', r)
|
||
map.setPaintProperty('gdelt-events-red-pulse', 'circle-opacity', opacity)
|
||
}
|
||
// 真主党攻击目标:脉冲半径衰减线性插值,镜头拉远变小
|
||
if (map.getLayer('hezbollah-attack-targets-pulse')) {
|
||
const cycle = 2000
|
||
const phase = Math.max(0, Math.min(1, (elapsed % cycle) / cycle))
|
||
const r = Math.max(0, 26 * phase * zoomScale * decayScale)
|
||
const opacity = Math.min(1, Math.max(0, (0.35 + 0.65 * decay) * (1 - phase * 1.1)))
|
||
map.setPaintProperty('hezbollah-attack-targets-pulse', 'circle-radius', r)
|
||
map.setPaintProperty('hezbollah-attack-targets-pulse', 'circle-opacity', opacity)
|
||
}
|
||
// 霍尔木兹海峡被打击目标:脉冲半径随 zoom×decayScale
|
||
if (map.getLayer('iran-hormuz-targets-pulse')) {
|
||
const cycle = 2000
|
||
const phase = Math.max(0, Math.min(1, (elapsed % cycle) / cycle))
|
||
const r = Math.max(0, 24 * phase * zoomScale * decayScale)
|
||
const opacity = Math.min(1, Math.max(0, (0.35 + 0.65 * decay) * (1 - phase * 1.05)))
|
||
map.setPaintProperty('iran-hormuz-targets-pulse', 'circle-radius', r)
|
||
map.setPaintProperty('iran-hormuz-targets-pulse', 'circle-opacity', opacity)
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
animRef.current = requestAnimationFrame(tick)
|
||
}
|
||
|
||
const start = () => {
|
||
hideNonBelligerentLabels(map)
|
||
map.fitBounds(
|
||
[[THEATER_BBOX[0], THEATER_BBOX[1]], [THEATER_BBOX[2], THEATER_BBOX[3]]],
|
||
{ padding: 40, maxZoom: 5, duration: 0 }
|
||
)
|
||
const hasAnim =
|
||
(map.getSource('attack-dots') && attackPathsRef.current.length > 0) ||
|
||
(map.getSource('allied-strike-dots-lincoln') && lincolnPathsRef.current.length > 0) ||
|
||
(map.getSource('allied-strike-dots-ford') && fordPathsRef.current.length > 0) ||
|
||
(map.getSource('allied-strike-dots-israel') && israelPathsRef.current.length > 0) ||
|
||
(map.getSource('hezbollah-strike-dots') && hezbollahPathsRef.current.length > 0) ||
|
||
(map.getSource('iran-hormuz-dots') && hormuzPathsRef.current.length > 0) ||
|
||
map.getSource('kurdish-pincer-growth') ||
|
||
map.getSource('israel-lebanon-arrow') ||
|
||
map.getSource('gdelt-events-green') ||
|
||
map.getSource('gdelt-events-orange') ||
|
||
map.getSource('gdelt-events-red')
|
||
if (hasAnim) {
|
||
animRef.current = requestAnimationFrame(tick)
|
||
} else {
|
||
animRef.current = requestAnimationFrame(start)
|
||
}
|
||
}
|
||
start()
|
||
}
|
||
|
||
useEffect(() => {
|
||
return () => cancelAnimationFrame(animRef.current)
|
||
}, [])
|
||
|
||
// 容器尺寸变化时 fitBounds,保证区域完整显示(移动端自适应)
|
||
const fitToTheater = useCallback(() => {
|
||
const map = mapRef.current?.getMap()
|
||
if (!map) return
|
||
map.fitBounds(THEATER_BOUNDS, { padding: 32, maxZoom: 6, duration: 0 })
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
const el = containerRef.current
|
||
if (!el) return
|
||
const ro = new ResizeObserver(() => fitToTheater())
|
||
ro.observe(el)
|
||
return () => ro.disconnect()
|
||
}, [fitToTheater])
|
||
|
||
if (!MAPBOX_TOKEN) {
|
||
return (
|
||
<div className="flex h-full w-full items-center justify-center bg-military-dark">
|
||
<div className="rounded-lg border border-military-border bg-military-panel/95 p-8 text-center shadow-lg">
|
||
<p className="font-orbitron text-sm font-medium text-military-text-primary">地图需要 Mapbox 令牌</p>
|
||
<p className="mt-2 text-xs text-military-text-secondary">
|
||
复制 .env.example 为 .env,填入令牌后重启
|
||
</p>
|
||
<a
|
||
href="https://account.mapbox.com/access-tokens/"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="mt-1 inline-block text-[10px] text-military-accent hover:underline"
|
||
>
|
||
免费申请 Mapbox 令牌 →
|
||
</a>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div ref={containerRef} className="relative h-full w-full min-w-0">
|
||
{/* 图例 - 随容器自适应,避免遮挡 */}
|
||
<div className="absolute bottom-2 left-2 z-10 flex flex-wrap gap-x-3 gap-y-1 rounded bg-black/70 px-2 py-1.5 text-[9px] sm:text-[10px]">
|
||
<span className="flex items-center gap-1">
|
||
<span className="h-1.5 w-1.5 rounded-full bg-[#22C55E]" /> 基地
|
||
</span>
|
||
<span className="flex items-center gap-1">
|
||
<span className="h-1.5 w-1.5 rounded-full bg-[#EF4444]" /> 遭袭
|
||
</span>
|
||
<span className="flex items-center gap-1">
|
||
<span className="h-1.5 w-1.5 rounded-full bg-[#3B82F6]" /> 海军
|
||
</span>
|
||
<span className="flex items-center gap-1">
|
||
<span className="h-1.5 w-1.5 rounded-full bg-[#EF4444]" /> 伊朗
|
||
</span>
|
||
<span className="flex items-center gap-1">
|
||
<span className="h-1.5 w-1.5 rounded-sm bg-red-500/40" /> 胡塞武装
|
||
</span>
|
||
<span className="flex items-center gap-1">
|
||
<span className="h-1.5 w-1.5 rounded-full bg-[#3B82F6]" /> 林肯打击
|
||
</span>
|
||
<span className="flex items-center gap-1">
|
||
<span className="h-1.5 w-1.5 rounded-full bg-[#06B6D4]" /> 福特打击
|
||
</span>
|
||
<span className="flex items-center gap-1">
|
||
<span className="h-1.5 w-1.5 rounded-full bg-[#22D3EE]" /> 以色列打击
|
||
</span>
|
||
<span className="flex items-center gap-1">
|
||
<span className="h-1.5 w-1.5 rounded-full bg-[#22C55E]" /> 低烈度
|
||
</span>
|
||
<span className="flex items-center gap-1">
|
||
<span className="h-1.5 w-1.5 rounded-full bg-[#F97316]" /> 中烈度
|
||
</span>
|
||
<span className="flex items-center gap-1">
|
||
<span className="h-1.5 w-1.5 rounded-full bg-[#EF4444]" /> 高烈度
|
||
</span>
|
||
<span className="flex items-center gap-1">
|
||
<span className="h-1.5 w-1.5 rounded-sm bg-yellow-400/50" /> 霍尔木兹交战区
|
||
</span>
|
||
<span className="flex items-center gap-1">
|
||
<span className="h-1.5 w-1.5 rounded-sm bg-lime-400/40" /> 真主党势力
|
||
</span>
|
||
<span className="flex items-center gap-1">
|
||
<span className="h-1.5 w-1.5 rounded-sm bg-[#E066FF]/80" /> 库尔德武装
|
||
</span>
|
||
</div>
|
||
<Map
|
||
ref={mapRef}
|
||
initialViewState={DEFAULT_VIEW}
|
||
mapStyle="mapbox://styles/mapbox/dark-v11"
|
||
mapboxAccessToken={MAPBOX_TOKEN}
|
||
attributionControl={false}
|
||
dragRotate={false}
|
||
touchRotate={false}
|
||
style={{ width: '100%', height: '100%' }}
|
||
onLoad={(e) => {
|
||
fitToTheater()
|
||
// 等地图 style/tiles 就绪后再启动动画,减轻首帧卡顿
|
||
const map = e.target
|
||
if (map.isStyleLoaded()) {
|
||
map.once('idle', () => initAnimation.current(map))
|
||
} else {
|
||
map.once('load', () => map.once('idle', () => initAnimation.current(map)))
|
||
}
|
||
}}
|
||
>
|
||
{/* 矢量标记:zoom 拉远变小,拉近变大 */}
|
||
<Source id="points-us-naval" type="geojson" data={usNaval}>
|
||
<Layer
|
||
id="points-us-naval"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
|
||
'circle-color': '#3B82F6',
|
||
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
|
||
'circle-stroke-color': '#fff',
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
<Source id="points-us-base-op" type="geojson" data={usBaseOp}>
|
||
<Layer
|
||
id="points-us-base-op"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
|
||
'circle-color': '#22C55E',
|
||
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
|
||
'circle-stroke-color': '#fff',
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
{/* 伊朗对霍尔木兹海峡的打击路径(黄色轨迹) */}
|
||
<Source id="iran-hormuz-lines" type="geojson" data={hormuzLinesGeoJson}>
|
||
<Layer
|
||
id="iran-hormuz-lines"
|
||
type="line"
|
||
paint={{
|
||
'line-color': 'rgba(250, 204, 21, 0.55)',
|
||
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.6, 8, 1.2, 12, 2],
|
||
}}
|
||
/>
|
||
</Source>
|
||
{/* 伊朗对霍尔木兹的打击光点(黄色) */}
|
||
<Source
|
||
id="iran-hormuz-dots"
|
||
type="geojson"
|
||
data={{
|
||
type: 'FeatureCollection',
|
||
features: hormuzPaths.map((path) => ({
|
||
type: 'Feature' as const,
|
||
properties: {},
|
||
geometry: { type: 'Point' as const, coordinates: path[0] },
|
||
})),
|
||
}}
|
||
>
|
||
<Layer
|
||
id="iran-hormuz-dots-glow"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 5.5, 12, 9],
|
||
'circle-color': 'rgba(250, 204, 21, 0.65)',
|
||
'circle-blur': 0.3,
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="iran-hormuz-dots-core"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1.2, 8, 2.2, 12, 3.8],
|
||
'circle-color': '#facc15',
|
||
'circle-stroke-width': 0.6,
|
||
'circle-stroke-color': '#fff',
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
{/* 霍尔木兹海峡被打击目标点 + 脉冲(与其他被打击点风格一致,颜色区分为琥珀黄) */}
|
||
<Source id="iran-hormuz-targets" type="geojson" data={hormuzTargetsGeoJson}>
|
||
<Layer
|
||
id="iran-hormuz-targets-dot"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 2, 8, 3.5, 12, 5],
|
||
'circle-color': '#fbbf24',
|
||
'circle-stroke-width': 0.5,
|
||
'circle-stroke-color': '#fff',
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="iran-hormuz-targets-pulse"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': 0,
|
||
'circle-color': 'rgba(251, 191, 36, 0.45)',
|
||
'circle-opacity': 0,
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
<Source id="points-us-base-damaged" type="geojson" data={usBaseDamaged}>
|
||
<Layer
|
||
id="points-damaged"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
|
||
'circle-color': '#F97316',
|
||
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
|
||
'circle-stroke-color': '#fff',
|
||
'circle-opacity': 1,
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
<Source id="points-us-base-attacked" type="geojson" data={usBaseAttacked}>
|
||
<Layer
|
||
id="points-attacked-dot"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
|
||
'circle-color': '#EF4444',
|
||
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
|
||
'circle-stroke-color': '#fff',
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="points-attacked-pulse"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 4, 8, 12, 12, 24],
|
||
'circle-color': '#EF4444',
|
||
'circle-opacity': 0,
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
<Source
|
||
id="points-iran"
|
||
type="geojson"
|
||
data={{
|
||
type: 'FeatureCollection',
|
||
features: irLocs.map((loc) => toFeature(loc, 'iran')),
|
||
}}
|
||
>
|
||
<Layer
|
||
id="points-iran"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
|
||
'circle-color': '#EF4444',
|
||
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
|
||
'circle-stroke-color': '#fff',
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
{/* GDELT 冲突事件:1–3 绿点, 4–6 橙闪, 7–10 红脉 */}
|
||
<Source id="gdelt-events-green" type="geojson" data={conflictEventsGreen}>
|
||
<Layer
|
||
id="gdelt-events-green"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 2, 8, 4, 12, 6],
|
||
'circle-color': '#22C55E',
|
||
'circle-stroke-width': 0.5,
|
||
'circle-stroke-color': '#fff',
|
||
}}
|
||
/>
|
||
</Source>
|
||
<Source id="gdelt-events-orange" type="geojson" data={conflictEventsOrange}>
|
||
<Layer
|
||
id="gdelt-events-orange"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 5, 12, 8],
|
||
'circle-color': '#F97316',
|
||
'circle-opacity': 0.8,
|
||
}}
|
||
/>
|
||
</Source>
|
||
<Source id="gdelt-events-red" type="geojson" data={conflictEventsRed}>
|
||
<Layer
|
||
id="gdelt-events-red-dot"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 2, 8, 4, 12, 6],
|
||
'circle-color': '#EF4444',
|
||
'circle-stroke-width': 0.5,
|
||
'circle-stroke-color': '#fff',
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="gdelt-events-red-pulse"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': 0,
|
||
'circle-color': 'rgba(239, 68, 68, 0.5)',
|
||
'circle-opacity': 0,
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
{/* 真主党对以色列北部的攻击矢量线(与林肯/福特/以色列线宽一致) */}
|
||
<Source id="hezbollah-attack-lines" type="geojson" data={hezbollahLinesGeoJson}>
|
||
<Layer
|
||
id="hezbollah-attack-lines"
|
||
type="line"
|
||
paint={{
|
||
'line-color': 'rgba(248, 113, 113, 0.45)',
|
||
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
|
||
}}
|
||
/>
|
||
</Source>
|
||
{/* 真主党打击光点(与林肯/福特/以色列光点半径与动画一致) */}
|
||
<Source
|
||
id="hezbollah-strike-dots"
|
||
type="geojson"
|
||
data={{
|
||
type: 'FeatureCollection',
|
||
features: hezbollahPaths.map((path) => ({
|
||
type: 'Feature' as const,
|
||
properties: {},
|
||
geometry: { type: 'Point' as const, coordinates: path[0] },
|
||
})),
|
||
}}
|
||
>
|
||
<Layer
|
||
id="hezbollah-strike-dots-glow"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
|
||
'circle-color': 'rgba(248, 113, 113, 0.6)',
|
||
'circle-blur': 0.3,
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="hezbollah-strike-dots-core"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
|
||
'circle-color': '#F97316',
|
||
'circle-stroke-width': 0.5,
|
||
'circle-stroke-color': '#fff',
|
||
}}
|
||
/>
|
||
</Source>
|
||
<Source id="hezbollah-attack-targets" type="geojson" data={hezbollahTargetsGeoJson}>
|
||
<Layer
|
||
id="hezbollah-attack-targets-dot"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 2, 8, 3.5, 12, 5],
|
||
'circle-color': '#F97316',
|
||
'circle-stroke-width': 0.5,
|
||
'circle-stroke-color': '#fff',
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="hezbollah-attack-targets-pulse"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': 0,
|
||
'circle-color': 'rgba(248, 113, 113, 0.45)',
|
||
'circle-opacity': 0,
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
{/* 跨国库尔德势力:土(Bakur)/叙(Rojava)/伊(Bashur) 三区 MultiPolygon + 北/南钳形箭头 */}
|
||
<Source id="kurdish-front-source" type="geojson" data={KURDISH_FRONT_GEOJSON}>
|
||
{/* 势力范围:紫色半透明,远景更显、近景更透(描边单独 line 层,参考真主党) */}
|
||
<Layer
|
||
id="kurdish-zones"
|
||
type="fill"
|
||
filter={['==', ['get', 'region_type'], 'InfluenceZone']}
|
||
paint={{
|
||
'fill-color': '#800080',
|
||
'fill-opacity': [
|
||
'interpolate',
|
||
['linear'],
|
||
['zoom'],
|
||
3,
|
||
0.4,
|
||
8,
|
||
0.15,
|
||
],
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="kurdish-zones-outline"
|
||
type="line"
|
||
filter={['==', ['get', 'region_type'], 'InfluenceZone']}
|
||
paint={{
|
||
'line-color': '#BA55D3',
|
||
'line-width': 1.2,
|
||
}}
|
||
/>
|
||
{/* 进攻目的地地名:亮紫色醒目识别 */}
|
||
<Layer
|
||
id="kurdish-target-labels"
|
||
type="symbol"
|
||
filter={['==', ['get', 'region_type'], 'Target']}
|
||
layout={{
|
||
'text-field': ['get', 'name'],
|
||
'text-size': ['interpolate', ['linear'], ['zoom'], 3, 6, 5, 8, 7, 10, 10, 13],
|
||
'text-anchor': 'top',
|
||
'text-offset': [0, 0.8],
|
||
}}
|
||
paint={{
|
||
'text-color': '#E066FF',
|
||
'text-halo-color': '#1a1a1a',
|
||
'text-halo-width': 1,
|
||
}}
|
||
/>
|
||
{/* 萨南达季、克尔曼沙赫显示圆点(大不里士不显示标记) */}
|
||
<Layer
|
||
id="kurdish-target-dots"
|
||
type="circle"
|
||
filter={['all', ['==', ['get', 'region_type'], 'Target'], ['==', ['get', 'showMarker'], true]]}
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1.5, 5, 2.5, 8, 4],
|
||
'circle-color': '#BF40BF',
|
||
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1],
|
||
'circle-stroke-color': '#fff',
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
{/* 伊朗被库尔德进攻点防御线:反弓曲线 + 锯齿,黄色;名称标注 */}
|
||
<Source id="kurdish-attack-defense-line" type="geojson" data={warMapData.kurdishDefenseLineGeoJson}>
|
||
<Layer
|
||
id="kurdish-attack-defense-line-layer"
|
||
type="line"
|
||
filter={['==', ['geometry-type'], 'LineString']}
|
||
paint={{
|
||
'line-color': '#FACC15',
|
||
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 1.5, 12, 2.5],
|
||
'line-join': 'round',
|
||
'line-cap': 'round',
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="kurdish-attack-defense-line-label"
|
||
type="symbol"
|
||
filter={['==', ['geometry-type'], 'Point']}
|
||
layout={{
|
||
'text-field': ['get', 'name'],
|
||
'text-size': ['interpolate', ['linear'], ['zoom'], 4, 7, 7, 9, 10, 11],
|
||
'text-anchor': 'center',
|
||
'text-rotate': ['get', 'angle'],
|
||
}}
|
||
paint={{
|
||
'text-color': '#FACC15',
|
||
'text-halo-color': '#1a1a1a',
|
||
'text-halo-width': 1,
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
{/* 单箭头钳形:曲线箭体,白色加粗轮廓与美方攻击点样式一致 */}
|
||
<Source id="kurdish-pincer-growth" type="geojson" data={warMapData.kurdishPincerGrowthInitial}>
|
||
<Layer
|
||
id="attack-pincers"
|
||
type="fill"
|
||
paint={{
|
||
'fill-color': '#2563eb',
|
||
'fill-opacity': 1,
|
||
'fill-outline-color': '#FFFFFF',
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="attack-pincers-inner-glow"
|
||
type="fill"
|
||
paint={{
|
||
'fill-color': '#60a5fa',
|
||
'fill-opacity': 0,
|
||
'fill-outline-color': 'transparent',
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="attack-pincers-outline"
|
||
type="line"
|
||
paint={{
|
||
'line-color': '#FFFFFF',
|
||
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.8, 7, 1.2, 10, 1.8],
|
||
'line-opacity': 1,
|
||
'line-join': 'round',
|
||
'line-cap': 'round',
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
{/* 以色列进攻黎巴嫩箭头:与库尔德同款曲线生长、白轮廓、内发光 */}
|
||
<Source id="israel-lebanon-arrow" type="geojson" data={warMapData.israelLebanonArrowInitial}>
|
||
<Layer
|
||
id="israel-lebanon-arrow-fill"
|
||
type="fill"
|
||
paint={{
|
||
'fill-color': '#2563eb',
|
||
'fill-opacity': 1,
|
||
'fill-outline-color': '#FFFFFF',
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="israel-lebanon-arrow-inner-glow"
|
||
type="fill"
|
||
paint={{
|
||
'fill-color': '#60a5fa',
|
||
'fill-opacity': 0,
|
||
'fill-outline-color': 'transparent',
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="israel-lebanon-arrow-outline"
|
||
type="line"
|
||
paint={{
|
||
'line-color': '#FFFFFF',
|
||
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.8, 7, 1.2, 10, 1.8],
|
||
'line-opacity': 1,
|
||
'line-join': 'round',
|
||
'line-cap': 'round',
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
{/* 美以联军打击伊朗:路径线 */}
|
||
<Source id="allied-strike-lines-lincoln" type="geojson" data={lincolnLinesGeoJson}>
|
||
<Layer
|
||
id="allied-strike-lines-lincoln"
|
||
type="line"
|
||
paint={{
|
||
'line-color': 'rgba(96, 165, 250, 0.45)',
|
||
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
|
||
}}
|
||
/>
|
||
</Source>
|
||
<Source id="allied-strike-lines-ford" type="geojson" data={fordLinesGeoJson}>
|
||
<Layer
|
||
id="allied-strike-lines-ford"
|
||
type="line"
|
||
paint={{
|
||
'line-color': 'rgba(6, 182, 212, 0.45)',
|
||
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
|
||
}}
|
||
/>
|
||
</Source>
|
||
<Source id="allied-strike-lines-israel" type="geojson" data={israelLinesGeoJson}>
|
||
<Layer
|
||
id="allied-strike-lines-israel"
|
||
type="line"
|
||
paint={{
|
||
'line-color': 'rgba(34, 211, 238, 0.45)',
|
||
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
|
||
}}
|
||
/>
|
||
</Source>
|
||
{/* 林肯号打击光点 */}
|
||
<Source
|
||
id="allied-strike-dots-lincoln"
|
||
type="geojson"
|
||
data={{
|
||
type: 'FeatureCollection',
|
||
features: lincolnPaths.map((path) => ({
|
||
type: 'Feature' as const,
|
||
properties: {},
|
||
geometry: { type: 'Point' as const, coordinates: path[0] },
|
||
})),
|
||
}}
|
||
>
|
||
<Layer
|
||
id="allied-strike-dots-lincoln-glow"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
|
||
'circle-color': 'rgba(96, 165, 250, 0.6)',
|
||
'circle-blur': 0.3,
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="allied-strike-dots-lincoln-core"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
|
||
'circle-color': '#3B82F6',
|
||
'circle-stroke-width': 0.5,
|
||
'circle-stroke-color': '#fff',
|
||
}}
|
||
/>
|
||
</Source>
|
||
{/* 福特号打击光点 */}
|
||
<Source
|
||
id="allied-strike-dots-ford"
|
||
type="geojson"
|
||
data={{
|
||
type: 'FeatureCollection',
|
||
features: fordPaths.map((path) => ({
|
||
type: 'Feature' as const,
|
||
properties: {},
|
||
geometry: { type: 'Point' as const, coordinates: path[0] },
|
||
})),
|
||
}}
|
||
>
|
||
<Layer
|
||
id="allied-strike-dots-ford-glow"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
|
||
'circle-color': 'rgba(6, 182, 212, 0.6)',
|
||
'circle-blur': 0.3,
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="allied-strike-dots-ford-core"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
|
||
'circle-color': '#06B6D4',
|
||
'circle-stroke-width': 0.5,
|
||
'circle-stroke-color': '#fff',
|
||
}}
|
||
/>
|
||
</Source>
|
||
{/* 以色列打击光点 */}
|
||
<Source
|
||
id="allied-strike-dots-israel"
|
||
type="geojson"
|
||
data={{
|
||
type: 'FeatureCollection',
|
||
features: israelPaths.map((path) => ({
|
||
type: 'Feature' as const,
|
||
properties: {},
|
||
geometry: { type: 'Point' as const, coordinates: path[0] },
|
||
})),
|
||
}}
|
||
>
|
||
<Layer
|
||
id="allied-strike-dots-israel-glow"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
|
||
'circle-color': 'rgba(34, 211, 238, 0.6)',
|
||
'circle-blur': 0.3,
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="allied-strike-dots-israel-core"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
|
||
'circle-color': '#22D3EE',
|
||
'circle-stroke-width': 0.5,
|
||
'circle-stroke-color': '#fff',
|
||
}}
|
||
/>
|
||
</Source>
|
||
{/* 盟军打击目标点位 (蓝色):含林肯/福特/以色列→伊朗 + 以色列→黎巴嫩,统一名称与脉冲动效 */}
|
||
<Source
|
||
id="allied-strike-targets"
|
||
type="geojson"
|
||
data={{
|
||
type: 'FeatureCollection',
|
||
features: alliedStrikeTargetsFeatures,
|
||
}}
|
||
>
|
||
<Layer
|
||
id="allied-strike-targets-circle"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1.5, 5, 2.5, 8, 4],
|
||
'circle-color': '#3B82F6',
|
||
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1],
|
||
'circle-stroke-color': '#fff',
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="allied-strike-targets-label"
|
||
type="symbol"
|
||
layout={{
|
||
'text-field': ['get', 'name'],
|
||
'text-size': ['interpolate', ['linear'], ['zoom'], 3, 6, 5, 8, 7, 10, 10, 13],
|
||
'text-anchor': 'top',
|
||
'text-offset': [0, 0.8],
|
||
}}
|
||
paint={{
|
||
'text-color': '#FFFFFF',
|
||
'text-halo-color': '#1a1a1a',
|
||
'text-halo-width': 1,
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="allied-strike-targets-pulse"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': 0,
|
||
'circle-color': 'rgba(96, 165, 250, 0.5)',
|
||
'circle-opacity': 0,
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
{/* 伊朗攻击路径:细线 (矢量) */}
|
||
<Source id="attack-lines" type="geojson" data={attackLinesGeoJson}>
|
||
<Layer
|
||
id="attack-lines"
|
||
type="line"
|
||
paint={{
|
||
'line-color': 'rgba(255, 100, 100, 0.4)',
|
||
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
|
||
}}
|
||
/>
|
||
</Source>
|
||
{/* 光点飞行动画:从德黑兰飞向各目标 */}
|
||
<Source
|
||
id="attack-dots"
|
||
type="geojson"
|
||
data={{
|
||
type: 'FeatureCollection',
|
||
features: attackPaths.map((path) => ({
|
||
type: 'Feature' as const,
|
||
properties: {},
|
||
geometry: {
|
||
type: 'Point' as const,
|
||
coordinates: path[0],
|
||
},
|
||
})),
|
||
}}
|
||
>
|
||
<Layer
|
||
id="attack-dots-glow"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
|
||
'circle-color': 'rgba(255, 100, 100, 0.6)',
|
||
'circle-blur': 0.3,
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="attack-dots-core"
|
||
type="circle"
|
||
paint={{
|
||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
|
||
'circle-color': '#ff4444',
|
||
'circle-stroke-width': 0.5,
|
||
'circle-stroke-color': '#fff',
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
{/* 中文标注 - 随 zoom 自适应大小 */}
|
||
<Source id="labels" type="geojson" data={labelsGeoJson}>
|
||
<Layer
|
||
id="labels"
|
||
type="symbol"
|
||
layout={{
|
||
'text-field': ['get', 'name'],
|
||
'text-size': ['interpolate', ['linear'], ['zoom'], 3, 6, 5, 8, 7, 10, 10, 13],
|
||
'text-anchor': 'top',
|
||
'text-offset': [0, 0.8],
|
||
}}
|
||
paint={{
|
||
'text-color': '#E5E7EB',
|
||
'text-halo-width': 0,
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
{/* 胡塞武装失控区 - 伊朗红 */}
|
||
<Source
|
||
id="houthi-area"
|
||
type="geojson"
|
||
data={{
|
||
type: 'Feature',
|
||
properties: { name: '胡塞武装' },
|
||
geometry: {
|
||
type: 'Polygon',
|
||
coordinates: [HOUTHI_POLYGON],
|
||
},
|
||
}}
|
||
>
|
||
<Layer
|
||
id="houthi-fill"
|
||
type="fill"
|
||
paint={{
|
||
'fill-color': 'rgba(239, 68, 68, 0.2)',
|
||
'fill-outline-color': '#EF4444',
|
||
'fill-opacity': 0.4,
|
||
}}
|
||
/>
|
||
</Source>
|
||
{/* 胡塞武装标注 */}
|
||
<Source
|
||
id="houthi-label"
|
||
type="geojson"
|
||
data={{
|
||
type: 'Feature',
|
||
properties: { name: '胡塞武装' },
|
||
geometry: { type: 'Point', coordinates: [44, 15.5] },
|
||
}}
|
||
>
|
||
<Layer
|
||
id="houthi-label-text"
|
||
type="symbol"
|
||
layout={{
|
||
'text-field': '胡塞武装',
|
||
'text-size': ['interpolate', ['linear'], ['zoom'], 4, 9, 8, 12],
|
||
'text-anchor': 'center',
|
||
}}
|
||
paint={{
|
||
'text-color': '#EF4444',
|
||
'text-halo-width': 0,
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
{/* 伊朗标注 */}
|
||
<Source
|
||
id="iran-label"
|
||
type="geojson"
|
||
data={{
|
||
type: 'Feature',
|
||
properties: { name: '伊朗' },
|
||
geometry: { type: 'Point', coordinates: [53.5, 32.5] },
|
||
}}
|
||
>
|
||
<Layer
|
||
id="iran-label-text"
|
||
type="symbol"
|
||
layout={{
|
||
'text-field': '伊朗',
|
||
'text-size': ['interpolate', ['linear'], ['zoom'], 4, 11, 8, 16],
|
||
'text-anchor': 'center',
|
||
}}
|
||
paint={{
|
||
'text-color': '#EF4444',
|
||
'text-halo-width': 0,
|
||
}}
|
||
/>
|
||
</Source>
|
||
{/* 以色列标注 */}
|
||
<Source
|
||
id="israel-label"
|
||
type="geojson"
|
||
data={{
|
||
type: 'Feature',
|
||
properties: { name: '以色列' },
|
||
geometry: { type: 'Point', coordinates: [35, 31] },
|
||
}}
|
||
>
|
||
<Layer
|
||
id="israel-label-text"
|
||
type="symbol"
|
||
layout={{
|
||
'text-field': '以色列',
|
||
'text-size': ['interpolate', ['linear'], ['zoom'], 4, 11, 8, 16],
|
||
'text-anchor': 'center',
|
||
}}
|
||
paint={{
|
||
'text-color': '#60A5FA',
|
||
'text-halo-width': 0,
|
||
}}
|
||
/>
|
||
</Source>
|
||
{/* 黎巴嫩标注(以色列打击黎巴嫩目标时可见) */}
|
||
<Source
|
||
id="lebanon-label"
|
||
type="geojson"
|
||
data={{
|
||
type: 'Feature',
|
||
properties: { name: '黎巴嫩' },
|
||
geometry: { type: 'Point', coordinates: [35.7, 33.7] },
|
||
}}
|
||
>
|
||
<Layer
|
||
id="lebanon-label-text"
|
||
type="symbol"
|
||
layout={{
|
||
'text-field': '黎巴嫩',
|
||
'text-size': ['interpolate', ['linear'], ['zoom'], 4, 11, 8, 16],
|
||
'text-anchor': 'center',
|
||
}}
|
||
paint={{
|
||
'text-color': '#94A3B8',
|
||
'text-halo-width': 0,
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
<Source id="countries" type="geojson" data={COUNTRIES_GEOJSON}>
|
||
{/* 伊朗区域填充 - 红色系 */}
|
||
<Layer
|
||
id="iran-fill"
|
||
type="fill"
|
||
filter={['==', ['get', 'ADMIN'], IRAN_ADMIN]}
|
||
paint={{
|
||
'fill-color': 'rgba(239, 68, 68, 0.15)',
|
||
'fill-outline-color': '#EF4444',
|
||
'fill-opacity': 0.5,
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="iran-outline"
|
||
type="line"
|
||
filter={['==', ['get', 'ADMIN'], IRAN_ADMIN]}
|
||
paint={{
|
||
'line-color': '#EF4444',
|
||
'line-width': 2,
|
||
'line-opacity': 0.9,
|
||
}}
|
||
/>
|
||
{/* 以色列区域填充 - 蓝色系 */}
|
||
<Layer
|
||
id="israel-fill"
|
||
type="fill"
|
||
filter={['==', ['get', 'ADMIN'], 'Israel']}
|
||
paint={{
|
||
'fill-color': 'rgba(96, 165, 250, 0.25)',
|
||
'fill-outline-color': '#60A5FA',
|
||
'fill-opacity': 0.6,
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="allies-outline"
|
||
type="line"
|
||
filter={['in', ['get', 'ADMIN'], ['literal', ALLIES_ADMIN]]}
|
||
paint={{
|
||
'line-color': '#3B82F6',
|
||
'line-width': 1.5,
|
||
'line-opacity': 0.8,
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
{/* 霍尔木兹海峡交战区 - 金黄色 mesh 区域 */}
|
||
<Source id="hormuz-combat-zone" type="geojson" data={hormuzZone}>
|
||
<Layer
|
||
id="hormuz-combat-fill"
|
||
type="fill"
|
||
paint={{
|
||
'fill-color': (hormuzZone.properties as any).style.fillColor,
|
||
'fill-opacity': (hormuzZone.properties as any).style.fillOpacity ?? 0.4,
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="hormuz-combat-outline"
|
||
type="line"
|
||
paint={{
|
||
'line-color': '#FACC15',
|
||
'line-width': 1.5,
|
||
'line-dasharray': [1.5, 1.5],
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
{/* 真主党势力范围 - 绿色半透明区域 */}
|
||
<Source id="hezbollah-zone" type="geojson" data={hezbollahZone}>
|
||
<Layer
|
||
id="hezbollah-fill"
|
||
type="fill"
|
||
paint={{
|
||
'fill-color': (hezbollahZone.properties as any).color || '#32CD32',
|
||
'fill-opacity': 0.28,
|
||
}}
|
||
/>
|
||
<Layer
|
||
id="hezbollah-outline"
|
||
type="line"
|
||
paint={{
|
||
'line-color': '#22C55E',
|
||
'line-width': 1.2,
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
{/* 霍尔木兹海峡区域标注 */}
|
||
<Source
|
||
id="hormuz-label"
|
||
type="geojson"
|
||
data={{
|
||
type: 'Feature',
|
||
properties: { name: (hormuzZone.properties as any).name },
|
||
geometry: {
|
||
type: 'Point',
|
||
coordinates: EXTENDED_WAR_ZONES.hormuzLabelCenter,
|
||
},
|
||
}}
|
||
>
|
||
<Layer
|
||
id="hormuz-label-text"
|
||
type="symbol"
|
||
layout={{
|
||
'text-field': ['get', 'name'],
|
||
// 字体进一步调小,避免与该区域多重效果叠加后显得拥挤
|
||
'text-size': ['interpolate', ['linear'], ['zoom'], 4, 5.5, 7, 7.5, 10, 9],
|
||
'text-anchor': 'center',
|
||
}}
|
||
paint={{
|
||
'text-color': '#FACC15',
|
||
'text-halo-color': '#1a1a1a',
|
||
'text-halo-width': 0.8,
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
{/* 真主党势力范围标注 */}
|
||
<Source
|
||
id="hezbollah-label"
|
||
type="geojson"
|
||
data={{
|
||
type: 'Feature',
|
||
properties: { name: (hezbollahZone.properties as any).name },
|
||
geometry: {
|
||
type: 'Point',
|
||
coordinates: EXTENDED_WAR_ZONES.hezbollahLabelCenter,
|
||
},
|
||
}}
|
||
>
|
||
<Layer
|
||
id="hezbollah-label-text"
|
||
type="symbol"
|
||
layout={{
|
||
'text-field': ['get', 'name'],
|
||
// 字体尽量小一些,避免遮挡
|
||
'text-size': ['interpolate', ['linear'], ['zoom'], 4, 7, 7, 9, 10, 11],
|
||
'text-anchor': 'center',
|
||
}}
|
||
paint={{
|
||
'text-color': '#22C55E',
|
||
'text-halo-color': '#1a1a1a',
|
||
'text-halo-width': 1,
|
||
}}
|
||
/>
|
||
</Source>
|
||
|
||
{/* 库尔德武装势力范围标注(参照真主党紫色区域标记) */}
|
||
<Source
|
||
id="kurdish-label"
|
||
type="geojson"
|
||
data={{
|
||
type: 'Feature',
|
||
properties: { name: '库尔德武装' },
|
||
geometry: {
|
||
type: 'Point',
|
||
coordinates: EXTENDED_WAR_ZONES.kurdishLabelCenter,
|
||
},
|
||
}}
|
||
>
|
||
<Layer
|
||
id="kurdish-label-text"
|
||
type="symbol"
|
||
layout={{
|
||
'text-field': ['get', 'name'],
|
||
'text-size': ['interpolate', ['linear'], ['zoom'], 4, 7, 7, 9, 10, 11],
|
||
'text-anchor': 'center',
|
||
}}
|
||
paint={{
|
||
'text-color': '#E066FF',
|
||
'text-halo-color': '#1a1a1a',
|
||
'text-halo-width': 1,
|
||
}}
|
||
/>
|
||
</Source>
|
||
</Map>
|
||
</div>
|
||
)
|
||
}
|