fix: 优化虫 机制,新增伊朗支援
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -26,6 +26,12 @@ MAX_DELTA_PER_MERGE = {
|
|||||||
"civilian_ships": 20, "airport_port": 10,
|
"civilian_ships": 20, "airport_port": 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 反击情绪 / 华尔街:合理区间,避免爬虫单条提取 0 或 100 导致指标归零或打满
|
||||||
|
RETALIATION_SMOOTH_WEIGHT = 0.6 # 当前值权重,1 - 此值为新值权重,使更新平滑
|
||||||
|
RETALIATION_HISTORY_MAX_ROWS = 300 # 反击历史条数上限,供前端曲线与回放使用
|
||||||
|
WALL_STREET_TREND_MAX_ROWS = 200 # 趋势表保留最近条数,避免无限增长
|
||||||
|
VALUE_CLAMP_MIN, VALUE_CLAMP_MAX = 1, 99 # 0/100 视为异常,写入前夹在 [1,99]
|
||||||
|
|
||||||
|
|
||||||
def _clamp_delta(key: str, value: int) -> int:
|
def _clamp_delta(key: str, value: int) -> int:
|
||||||
"""单次增量上限,避免误提「累计」导致波动"""
|
"""单次增量上限,避免误提「累计」导致波动"""
|
||||||
@@ -200,38 +206,69 @@ def merge(extracted: Dict[str, Any], db_path: Optional[str] = None) -> bool:
|
|||||||
updated = True
|
updated = True
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# retaliation
|
# retaliation:平滑更新,避免单条新闻 0/100 导致指标归零或打满
|
||||||
if "retaliation" in extracted:
|
if "retaliation" in extracted:
|
||||||
r = extracted["retaliation"]
|
r = extracted["retaliation"]
|
||||||
conn.execute("INSERT OR REPLACE INTO retaliation_current (id, value) VALUES (1, ?)", (r["value"],))
|
raw = max(VALUE_CLAMP_MIN, min(VALUE_CLAMP_MAX, int(r.get("value", 50))))
|
||||||
conn.execute("INSERT INTO retaliation_history (time, value) VALUES (?, ?)", (r["time"], r["value"]))
|
row = conn.execute("SELECT value FROM retaliation_current WHERE id = 1").fetchone()
|
||||||
|
current = int(row[0]) if row else 50
|
||||||
|
current = max(VALUE_CLAMP_MIN, min(VALUE_CLAMP_MAX, current))
|
||||||
|
new_val = round(
|
||||||
|
RETALIATION_SMOOTH_WEIGHT * current + (1 - RETALIATION_SMOOTH_WEIGHT) * raw
|
||||||
|
)
|
||||||
|
new_val = max(VALUE_CLAMP_MIN, min(VALUE_CLAMP_MAX, new_val))
|
||||||
|
ts = (r.get("time") or datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.000Z"))[:25]
|
||||||
|
conn.execute("INSERT OR REPLACE INTO retaliation_current (id, value) VALUES (1, ?)", (new_val,))
|
||||||
|
conn.execute("INSERT INTO retaliation_history (time, value) VALUES (?, ?)", (ts, new_val))
|
||||||
|
n_ret = conn.execute("SELECT COUNT(*) FROM retaliation_history").fetchone()[0]
|
||||||
|
if n_ret > RETALIATION_HISTORY_MAX_ROWS:
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM retaliation_history WHERE id IN (SELECT id FROM retaliation_history ORDER BY time ASC LIMIT ?)",
|
||||||
|
(n_ret - RETALIATION_HISTORY_MAX_ROWS,),
|
||||||
|
)
|
||||||
updated = True
|
updated = True
|
||||||
# wall_street_trend
|
# wall_street_trend:限幅后写入,并保留最近 N 条避免表无限增长
|
||||||
if "wall_street" in extracted:
|
if "wall_street" in extracted:
|
||||||
w = extracted["wall_street"]
|
w = extracted["wall_street"]
|
||||||
conn.execute("INSERT INTO wall_street_trend (time, value) VALUES (?, ?)", (w["time"], w["value"]))
|
raw = int(w.get("value", 50))
|
||||||
|
val = max(VALUE_CLAMP_MIN, min(VALUE_CLAMP_MAX, raw))
|
||||||
|
ts = (w.get("time") or datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.000Z"))[:25]
|
||||||
|
conn.execute("INSERT INTO wall_street_trend (time, value) VALUES (?, ?)", (ts, val))
|
||||||
|
n = conn.execute("SELECT COUNT(*) FROM wall_street_trend").fetchone()[0]
|
||||||
|
if n > WALL_STREET_TREND_MAX_ROWS:
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM wall_street_trend WHERE id IN (SELECT id FROM wall_street_trend ORDER BY time ASC LIMIT ?)",
|
||||||
|
(n - WALL_STREET_TREND_MAX_ROWS,),
|
||||||
|
)
|
||||||
updated = True
|
updated = True
|
||||||
# key_location:更新双方攻击地点(美军基地被打击 side=us,伊朗设施被打击 side=iran)的 status/damage_level
|
# key_location:更新双方攻击地点(美军基地被打击 side=us,伊朗设施被打击 side=iran)的 status/damage_level/attacked_at
|
||||||
|
event_time = extracted.get("_event_time") or datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
||||||
if "key_location_updates" in extracted:
|
if "key_location_updates" in extracted:
|
||||||
try:
|
try:
|
||||||
for u in extracted["key_location_updates"]:
|
for u in extracted["key_location_updates"]:
|
||||||
kw_raw = (u.get("name_keywords") or "").strip()
|
kw_raw = (u.get("name_keywords") or "").strip()
|
||||||
if not kw_raw:
|
if not kw_raw:
|
||||||
continue
|
continue
|
||||||
# 支持 "a|b|c" 或 "a b c" 分隔
|
|
||||||
kw = [k.strip() for k in kw_raw.replace("|", " ").split() if k.strip()]
|
kw = [k.strip() for k in kw_raw.replace("|", " ").split() if k.strip()]
|
||||||
side = u.get("side")
|
side = u.get("side")
|
||||||
status = (u.get("status") or "attacked")[:20]
|
status = (u.get("status") or "attacked")[:20]
|
||||||
dmg = u.get("damage_level", 2)
|
dmg = u.get("damage_level", 2)
|
||||||
if not kw or side not in ("us", "iran"):
|
if not kw or side not in ("us", "iran"):
|
||||||
continue
|
continue
|
||||||
# 简化:name LIKE '%kw%' 对每个关键词 OR 连接,支持中英文
|
attacked_at = (u.get("attacked_at") or event_time)[:25]
|
||||||
conditions = " OR ".join("name LIKE ?" for _ in kw)
|
conditions = " OR ".join("name LIKE ?" for _ in kw)
|
||||||
params = [status, dmg, side] + [f"%{k}%" for k in kw]
|
params_with_at = [status, dmg, attacked_at, side] + [f"%{k}%" for k in kw]
|
||||||
cur = conn.execute(
|
try:
|
||||||
f"UPDATE key_location SET status=?, damage_level=? WHERE side=? AND ({conditions})",
|
cur = conn.execute(
|
||||||
params,
|
f"UPDATE key_location SET status=?, damage_level=?, attacked_at=? WHERE side=? AND ({conditions})",
|
||||||
)
|
params_with_at,
|
||||||
|
)
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
params_no_at = [status, dmg, side] + [f"%{k}%" for k in kw]
|
||||||
|
cur = conn.execute(
|
||||||
|
f"UPDATE key_location SET status=?, damage_level=? WHERE side=? AND ({conditions})",
|
||||||
|
params_no_at,
|
||||||
|
)
|
||||||
if cur.rowcount > 0:
|
if cur.rowcount > 0:
|
||||||
updated = True
|
updated = True
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ def _call_ollama_extract(text: str, timeout: int = 15) -> Optional[Dict[str, Any
|
|||||||
- retaliation_sentiment: 0-100,仅当报道涉及伊朗报复/反击情绪时
|
- retaliation_sentiment: 0-100,仅当报道涉及伊朗报复/反击情绪时
|
||||||
- wall_street_value: 0-100,仅当报道涉及美股/市场时
|
- wall_street_value: 0-100,仅当报道涉及美股/市场时
|
||||||
- key_location_updates: **双方攻击地点**。每项 {{ "name_keywords": "阿萨德|asad|al-asad", "side": "us或iran(被打击方)", "status": "attacked", "damage_level": 1-3 }}。美军基地例:阿萨德|asad、乌代德|udeid、埃尔比勒|erbil、因吉尔利克|incirlik。伊朗例:德黑兰|tehran、布什尔|bushehr、伊斯法罕|isfahan、阿巴斯|abbas、纳坦兹|natanz
|
- key_location_updates: **双方攻击地点**。每项 {{ "name_keywords": "阿萨德|asad|al-asad", "side": "us或iran(被打击方)", "status": "attacked", "damage_level": 1-3 }}。美军基地例:阿萨德|asad、乌代德|udeid、埃尔比勒|erbil、因吉尔利克|incirlik。伊朗例:德黑兰|tehran、布什尔|bushehr、伊斯法罕|isfahan、阿巴斯|abbas、纳坦兹|natanz
|
||||||
|
- map_strike_lines(仅当报道为**美/以盟军打击伊朗目标**时): 数组,每项 {{ "source_id": "israel或lincoln或ford", "target_lng": 经度, "target_lat": 纬度, "target_name": "目标名", "struck_at": "ISO时间" }}。目标坐标例:纳坦兹51.92,33.67;伊斯法罕51.67,32.65;德黑兰51.39,35.69;布什尔50.83,28.97;阿巴斯港56.27,27.18
|
||||||
- **导弹消耗增量**(仅当报道明确提到「发射/消耗 了 X 枚导弹」时填,用于看板导弹消耗累计): us_missile_consumed_delta, iran_missile_consumed_delta(本则报道中该方新增消耗枚数,整数)
|
- **导弹消耗增量**(仅当报道明确提到「发射/消耗 了 X 枚导弹」时填,用于看板导弹消耗累计): us_missile_consumed_delta, iran_missile_consumed_delta(本则报道中该方新增消耗枚数,整数)
|
||||||
|
|
||||||
原文:
|
原文:
|
||||||
@@ -133,6 +134,31 @@ def extract_from_news(text: str, timestamp: Optional[str] = None) -> Dict[str, A
|
|||||||
})
|
})
|
||||||
if valid:
|
if valid:
|
||||||
out["key_location_updates"] = valid
|
out["key_location_updates"] = valid
|
||||||
|
# map_strike_lines:盟军打击伊朗目标
|
||||||
|
if "map_strike_lines" in parsed and isinstance(parsed["map_strike_lines"], list):
|
||||||
|
valid_lines = []
|
||||||
|
for line in parsed["map_strike_lines"]:
|
||||||
|
if not isinstance(line, dict):
|
||||||
|
continue
|
||||||
|
sid = str(line.get("source_id") or "").strip().lower()
|
||||||
|
if sid not in ("israel", "lincoln", "ford"):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
lng = float(line.get("target_lng", 0))
|
||||||
|
lat = float(line.get("target_lat", 0))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
name = str(line.get("target_name") or "")[:200]
|
||||||
|
struck_at = str(line.get("struck_at") or ts)[:25]
|
||||||
|
valid_lines.append({
|
||||||
|
"source_id": sid,
|
||||||
|
"target_lng": lng,
|
||||||
|
"target_lat": lat,
|
||||||
|
"target_name": name or None,
|
||||||
|
"struck_at": struck_at,
|
||||||
|
})
|
||||||
|
if valid_lines:
|
||||||
|
out["map_strike_lines"] = valid_lines
|
||||||
# force_summary 增量:导弹消耗(看板「导弹消耗」由 force_summary.missile_consumed 提供)
|
# force_summary 增量:导弹消耗(看板「导弹消耗」由 force_summary.missile_consumed 提供)
|
||||||
fs_delta = {}
|
fs_delta = {}
|
||||||
for side_key, side_val in [("us_missile_consumed_delta", "us"), ("iran_missile_consumed_delta", "iran")]:
|
for side_key, side_val in [("us_missile_consumed_delta", "us"), ("iran_missile_consumed_delta", "iran")]:
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ def _call_dashscope_extract(text: str, timeout: int = 15) -> Optional[Dict[str,
|
|||||||
- retaliation_sentiment: 0-100(仅当报道涉及伊朗报复情绪时)
|
- retaliation_sentiment: 0-100(仅当报道涉及伊朗报复情绪时)
|
||||||
- wall_street_value: 0-100(仅当报道涉及美股/市场时)
|
- wall_street_value: 0-100(仅当报道涉及美股/市场时)
|
||||||
- key_location_updates: **双方攻击地点**。每项 {{"name_keywords":"阿萨德|asad","side":"us或iran(被打击方)","status":"attacked","damage_level":1-3}}。美军基地:阿萨德|asad、乌代德|udeid、埃尔比勒|erbil、因吉尔利克|incirlik。伊朗:德黑兰|tehran、布什尔|bushehr、伊斯法罕|isfahan、阿巴斯|abbas、纳坦兹|natanz
|
- key_location_updates: **双方攻击地点**。每项 {{"name_keywords":"阿萨德|asad","side":"us或iran(被打击方)","status":"attacked","damage_level":1-3}}。美军基地:阿萨德|asad、乌代德|udeid、埃尔比勒|erbil、因吉尔利克|incirlik。伊朗:德黑兰|tehran、布什尔|bushehr、伊斯法罕|isfahan、阿巴斯|abbas、纳坦兹|natanz
|
||||||
|
- **map_strike_lines**(仅当报道明确为**美/以盟军打击伊朗或伊朗目标**时): 数组,每项 {{"source_id":"israel或lincoln或ford","target_lng":经度,"target_lat":纬度,"target_name":"目标名如纳坦兹","struck_at":"ISO时间"}}。以色列打击→source_id=israel;林肯号→lincoln;福特号→ford。目标坐标:纳坦兹51.92,33.67;伊斯法罕51.67,32.65;德黑兰51.39,35.69;布什尔50.83,28.97;阿巴斯港56.27,27.18
|
||||||
- **导弹消耗增量**(仅当报道明确提到「发射/消耗 了 X 枚导弹」时填): us_missile_consumed_delta, iran_missile_consumed_delta(本则该方新增消耗枚数,整数)
|
- **导弹消耗增量**(仅当报道明确提到「发射/消耗 了 X 枚导弹」时填): us_missile_consumed_delta, iran_missile_consumed_delta(本则该方新增消耗枚数,整数)
|
||||||
|
|
||||||
原文:
|
原文:
|
||||||
@@ -133,4 +134,29 @@ def extract_from_news(text: str, timestamp: Optional[str] = None) -> Dict[str, A
|
|||||||
if valid:
|
if valid:
|
||||||
out["key_location_updates"] = valid
|
out["key_location_updates"] = valid
|
||||||
|
|
||||||
|
if "map_strike_lines" in parsed and isinstance(parsed["map_strike_lines"], list):
|
||||||
|
valid_lines = []
|
||||||
|
for line in parsed["map_strike_lines"]:
|
||||||
|
if not isinstance(line, dict):
|
||||||
|
continue
|
||||||
|
sid = str(line.get("source_id") or "").strip().lower()
|
||||||
|
if sid not in ("israel", "lincoln", "ford"):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
lng = float(line.get("target_lng", 0))
|
||||||
|
lat = float(line.get("target_lat", 0))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
name = str(line.get("target_name") or "")[:200]
|
||||||
|
struck_at = str(line.get("struck_at") or ts)[:25]
|
||||||
|
valid_lines.append({
|
||||||
|
"source_id": sid,
|
||||||
|
"target_lng": lng,
|
||||||
|
"target_lat": lat,
|
||||||
|
"target_name": name or None,
|
||||||
|
"struck_at": struck_at,
|
||||||
|
})
|
||||||
|
if valid_lines:
|
||||||
|
out["map_strike_lines"] = valid_lines
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
基于规则的新闻数据提取(无需 Ollama)
|
基于规则的新闻数据提取(无需 Ollama)
|
||||||
从新闻文本中提取战损、报复情绪等数值,供 db_merge 写入
|
从新闻文本中提取战损、报复情绪、攻击地点与盟军打击线,供 db_merge 写入
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
# 伊朗境内常见打击目标: (显示名, 经度, 纬度, 匹配关键词)
|
||||||
|
IRAN_STRIKE_TARGETS: List[Tuple[str, float, float, str]] = [
|
||||||
|
("纳坦兹", 51.916, 33.666, "natanz|纳坦兹"),
|
||||||
|
("伊斯法罕", 51.67, 32.65, "isfahan|esfahan|伊斯法罕"),
|
||||||
|
("德黑兰", 51.389, 35.689, "tehran|德黑兰"),
|
||||||
|
("布什尔", 50.83, 28.97, "bushehr|布什尔"),
|
||||||
|
("阿巴斯港", 56.27, 27.18, "bandar abbas|abbas|阿巴斯|霍尔木兹"),
|
||||||
|
("克尔曼沙赫", 47.06, 34.31, "kermanshah|克尔曼沙赫"),
|
||||||
|
("大不里士", 46.29, 38.08, "tabriz|大不里士"),
|
||||||
|
("卡拉季", 50.99, 35.83, "karaj|卡拉季"),
|
||||||
|
("米纳布", 57.08, 27.13, "minab|米纳布"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _first_int(text: str, pattern: str) -> Optional[int]:
|
def _first_int(text: str, pattern: str) -> Optional[int]:
|
||||||
@@ -251,4 +264,30 @@ def extract_from_news(text: str, timestamp: Optional[str] = None) -> Dict[str, A
|
|||||||
if updates:
|
if updates:
|
||||||
out["key_location_updates"] = updates
|
out["key_location_updates"] = updates
|
||||||
|
|
||||||
|
# map_strike_lines:盟军(以色列/林肯/福特)打击伊朗目标,供地图攻击动画更新
|
||||||
|
strike_verbs = ("strike" in t or "struck" in t or "strikes" in t or "hit" in t or "attack" in t
|
||||||
|
or "打击" in (text or "") or "空袭" in (text or "") or "袭击" in (text or ""))
|
||||||
|
if strike_verbs and ("iran" in t or "伊朗" in (text or "") or any(
|
||||||
|
any(p in t for p in kw.split("|")) for _n, _lng, _lat, kw in IRAN_STRIKE_TARGETS
|
||||||
|
)):
|
||||||
|
source_id = "israel"
|
||||||
|
if "lincoln" in t or "林肯" in (text or ""):
|
||||||
|
source_id = "lincoln"
|
||||||
|
elif "ford" in t or "福特" in (text or ""):
|
||||||
|
source_id = "ford"
|
||||||
|
elif ("israel" in t or "idf" in t or "以色列" in (text or "")) and ("us " in t or "american" in t or "pentagon" in t):
|
||||||
|
source_id = "israel" # 多国时优先以色列
|
||||||
|
lines = []
|
||||||
|
for name, lng, lat, kw in IRAN_STRIKE_TARGETS:
|
||||||
|
if any(p in t for p in kw.split("|")):
|
||||||
|
lines.append({
|
||||||
|
"source_id": source_id,
|
||||||
|
"target_lng": lng,
|
||||||
|
"target_lat": lat,
|
||||||
|
"target_name": name,
|
||||||
|
"struck_at": ts,
|
||||||
|
})
|
||||||
|
if lines:
|
||||||
|
out["map_strike_lines"] = lines
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ def _extract_and_merge(items: list, db_path: str) -> bool:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
extracted = extract_from_news(text, timestamp=ts)
|
extracted = extract_from_news(text, timestamp=ts)
|
||||||
|
if ts:
|
||||||
|
extracted["_event_time"] = ts
|
||||||
if extracted and merge(extracted, db_path=db_path):
|
if extracted and merge(extracted, db_path=db_path):
|
||||||
merged_any = True
|
merged_any = True
|
||||||
return merged_any
|
return merged_any
|
||||||
|
|||||||
Binary file not shown.
@@ -2,9 +2,15 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/usa_logo.png" />
|
<link rel="icon" type="image/png" href="/usa_logo.png" />
|
||||||
|
<link rel="apple-touch-icon" href="/usa_logo.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
<title>美伊军事态势显示</title>
|
<title>美伊军事态势显示</title>
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:title" content="美伊军事态势显示" />
|
||||||
|
<meta property="og:image" content="/usa_logo.png" />
|
||||||
|
<meta name="twitter:card" content="summary" />
|
||||||
|
<meta name="twitter:image" content="/usa_logo.png" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700;800;900&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700;800;900&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
|
||||||
|
|||||||
BIN
public/usa_logo.png
Normal file
BIN
public/usa_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 453 KiB |
Binary file not shown.
43
server/db.js
43
server/db.js
@@ -310,6 +310,49 @@ function runMigrations(db) {
|
|||||||
);
|
);
|
||||||
`)
|
`)
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
|
// 生产环境可能未跑 seed 或使用旧 seed:补齐 war_map_config 与以色列→黎巴嫩打击线,保证攻击动画显示
|
||||||
|
try {
|
||||||
|
let row = db.prepare('SELECT 1 FROM war_map_config WHERE id = 1').get()
|
||||||
|
if (!row) {
|
||||||
|
const warMapConfig = {
|
||||||
|
pincerAxes: [
|
||||||
|
{ start: [43.6, 37.2], end: [46.27, 38.08], name: 'North Pincer (Tabriz)' },
|
||||||
|
{ start: [45.0, 35.4], end: [46.99, 35.31], name: 'Central Pincer (Sanandaj)' },
|
||||||
|
{ start: [45.6, 35.2], end: [47.07, 34.31], name: 'South Pincer (Kermanshah)' },
|
||||||
|
],
|
||||||
|
israelLebanonAxis: { start: [35.25, 32.95], end: [35.55, 33.45], name: 'Israel → Lebanon' },
|
||||||
|
defenseLinePath: [[46.27, 38.08], [46.99, 35.31], [47.07, 34.31]],
|
||||||
|
}
|
||||||
|
db.prepare(
|
||||||
|
'INSERT INTO war_map_config (id, config, updated_at) VALUES (1, ?, datetime(\'now\'))'
|
||||||
|
).run(JSON.stringify(warMapConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保 map_strike_source 有 israel(否则打击线无法关联)
|
||||||
|
db.prepare(
|
||||||
|
'INSERT OR IGNORE INTO map_strike_source (id, name, lng, lat) VALUES (\'israel\', \'以色列\', 34.78, 32.08)'
|
||||||
|
).run()
|
||||||
|
|
||||||
|
const israelLebanonTargets = [
|
||||||
|
[35.5, 33.86, '贝鲁特南郊指挥所', '2026-02-28T14:00:00.000Z'],
|
||||||
|
[35.32, 33.34, '利塔尼弹药库', '2026-02-28T14:10:00.000Z'],
|
||||||
|
[36.2, 34.01, '巴勒贝克后勤枢纽', '2026-02-28T14:20:00.000Z'],
|
||||||
|
[35.19, 33.27, '提尔海岸阵地', '2026-02-28T14:30:00.000Z'],
|
||||||
|
[36.38, 34.39, '赫尔梅勒无人机阵地', '2026-02-28T14:40:00.000Z'],
|
||||||
|
]
|
||||||
|
const insertStrikeLine = db.prepare(
|
||||||
|
'INSERT INTO map_strike_line (source_id, target_lng, target_lat, target_name, struck_at) VALUES (?, ?, ?, ?, ?)'
|
||||||
|
)
|
||||||
|
const hasStrikeLine = db.prepare(
|
||||||
|
'SELECT 1 FROM map_strike_line WHERE source_id = ? AND target_lng = ? AND target_lat = ?'
|
||||||
|
)
|
||||||
|
for (const [lng, lat, name, struckAt] of israelLebanonTargets) {
|
||||||
|
if (!hasStrikeLine.get('israel', lng, lat)) {
|
||||||
|
insertStrikeLine.run('israel', lng, lat, name, struckAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initDb() {
|
async function initDb() {
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ function getSituation() {
|
|||||||
const trend = db.prepare('SELECT time, value FROM wall_street_trend ORDER BY time').all()
|
const trend = db.prepare('SELECT time, value FROM wall_street_trend ORDER BY time').all()
|
||||||
const retaliationCur = db.prepare('SELECT value FROM retaliation_current WHERE id = 1').get()
|
const retaliationCur = db.prepare('SELECT value FROM retaliation_current WHERE id = 1').get()
|
||||||
const retaliationHist = db.prepare('SELECT time, value FROM retaliation_history ORDER BY time').all()
|
const retaliationHist = db.prepare('SELECT time, value FROM retaliation_history ORDER BY time').all()
|
||||||
|
// 反击情绪无记录时给默认 50,避免爬虫未写入时前端显示 0
|
||||||
|
const retaliationValue = retaliationCur?.value ?? 50
|
||||||
const updates = db.prepare('SELECT * FROM situation_update ORDER BY timestamp DESC LIMIT 50').all()
|
const updates = db.prepare('SELECT * FROM situation_update ORDER BY timestamp DESC LIMIT 50').all()
|
||||||
// 数据更新时间:与前端「实时更新」一致,仅在爬虫 notify / 编辑保存时由 index.js 或 routes 更新
|
// 数据更新时间:与前端「实时更新」一致,仅在爬虫 notify / 编辑保存时由 index.js 或 routes 更新
|
||||||
const meta = db.prepare('SELECT updated_at FROM situation WHERE id = 1').get()
|
const meta = db.prepare('SELECT updated_at FROM situation WHERE id = 1').get()
|
||||||
@@ -157,7 +159,7 @@ function getSituation() {
|
|||||||
assets: (assetsIr || []).map(toAsset),
|
assets: (assetsIr || []).map(toAsset),
|
||||||
keyLocations: locIr || [],
|
keyLocations: locIr || [],
|
||||||
combatLosses: irLosses,
|
combatLosses: irLosses,
|
||||||
retaliationSentiment: retaliationCur?.value ?? 0,
|
retaliationSentiment: retaliationValue,
|
||||||
retaliationSentimentHistory: retaliationHist || [],
|
retaliationSentimentHistory: retaliationHist || [],
|
||||||
},
|
},
|
||||||
recentUpdates: (updates || []).map((u) => ({
|
recentUpdates: (updates || []).map((u) => ({
|
||||||
|
|||||||
@@ -219,9 +219,35 @@ export function WarMap() {
|
|||||||
const situation = useReplaySituation()
|
const situation = useReplaySituation()
|
||||||
const { isReplayMode, playbackTime } = usePlaybackStore()
|
const { isReplayMode, playbackTime } = usePlaybackStore()
|
||||||
const { usForces, iranForces, conflictEvents = [] } = situation
|
const { usForces, iranForces, conflictEvents = [] } = situation
|
||||||
/** 时间衰减基准:回放模式用回放时刻,否则用数据更新时间或当前时间 */
|
/** 时间衰减基准:回放模式用回放时刻;实时模式用「当前数据中最新 struck_at/attacked_at」,无事件时再用 lastUpdated/now。
|
||||||
const referenceTime =
|
* 这样爬虫推送后 lastUpdated 跳到「当前」时,窗口仍以数据中最新事件为右端,不会前移导致已有攻击路线被排除(动画消失)。 */
|
||||||
isReplayMode ? playbackTime : situation.lastUpdated || new Date().toISOString()
|
const referenceTime = useMemo(() => {
|
||||||
|
if (isReplayMode) return playbackTime
|
||||||
|
let maxTs = 0
|
||||||
|
for (const line of situation.mapData?.strikeLines ?? []) {
|
||||||
|
for (const t of line.targets ?? []) {
|
||||||
|
if (t.struck_at) {
|
||||||
|
const ts = new Date(t.struck_at).getTime()
|
||||||
|
if (ts > maxTs) maxTs = ts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const loc of [...(usForces.keyLocations ?? []), ...(iranForces.keyLocations ?? [])]) {
|
||||||
|
if (loc.attacked_at) {
|
||||||
|
const ts = new Date(loc.attacked_at).getTime()
|
||||||
|
if (ts > maxTs) maxTs = ts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maxTs > 0) return new Date(maxTs).toISOString()
|
||||||
|
return situation.lastUpdated || new Date().toISOString()
|
||||||
|
}, [
|
||||||
|
isReplayMode,
|
||||||
|
playbackTime,
|
||||||
|
situation.lastUpdated,
|
||||||
|
situation.mapData?.strikeLines,
|
||||||
|
usForces.keyLocations,
|
||||||
|
iranForces.keyLocations,
|
||||||
|
])
|
||||||
|
|
||||||
const usLocs = (usForces.keyLocations || []) as KeyLoc[]
|
const usLocs = (usForces.keyLocations || []) as KeyLoc[]
|
||||||
const irLocs = (iranForces.keyLocations || []) as KeyLoc[]
|
const irLocs = (iranForces.keyLocations || []) as KeyLoc[]
|
||||||
@@ -500,9 +526,33 @@ export function WarMap() {
|
|||||||
[hormuzTargetPoints, isReplayMode]
|
[hormuzTargetPoints, isReplayMode]
|
||||||
)
|
)
|
||||||
|
|
||||||
// 霍尔木兹海峡交战区 & 真主党势力范围(静态面)
|
// 霍尔木兹海峡交战区 & 真主党势力范围(静态面);保持引用稳定,避免广播更新时触发 setData 导致轮廓线丢失
|
||||||
const hormuzZone = EXTENDED_WAR_ZONES.hormuzCombatZone
|
const hormuzZone = useMemo(() => EXTENDED_WAR_ZONES.hormuzCombatZone, [])
|
||||||
const hezbollahZone = EXTENDED_WAR_ZONES.hezbollahZone
|
const hezbollahZone = useMemo(() => EXTENDED_WAR_ZONES.hezbollahZone, [])
|
||||||
|
const hormuzLabelData = useMemo(
|
||||||
|
() => ({
|
||||||
|
type: 'Feature' as const,
|
||||||
|
properties: { name: (EXTENDED_WAR_ZONES.hormuzCombatZone.properties as { name?: string }).name ?? '霍尔木兹海峡交战区' },
|
||||||
|
geometry: { type: 'Point' as const, coordinates: EXTENDED_WAR_ZONES.hormuzLabelCenter },
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const hezbollahLabelData = useMemo(
|
||||||
|
() => ({
|
||||||
|
type: 'Feature' as const,
|
||||||
|
properties: { name: (EXTENDED_WAR_ZONES.hezbollahZone.properties as { name?: string }).name ?? '真主党势力范围' },
|
||||||
|
geometry: { type: 'Point' as const, coordinates: EXTENDED_WAR_ZONES.hezbollahLabelCenter },
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const kurdishLabelData = useMemo(
|
||||||
|
() => ({
|
||||||
|
type: 'Feature' as const,
|
||||||
|
properties: { name: '库尔德武装' },
|
||||||
|
geometry: { type: 'Point' as const, coordinates: EXTENDED_WAR_ZONES.kurdishLabelCenter },
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
// GDELT 冲突事件:1–3 绿, 4–6 橙闪, 7–10 红脉
|
// GDELT 冲突事件:1–3 绿, 4–6 橙闪, 7–10 红脉
|
||||||
const { conflictEventsGreen, conflictEventsOrange, conflictEventsRed } = useMemo(() => {
|
const { conflictEventsGreen, conflictEventsOrange, conflictEventsRed } = useMemo(() => {
|
||||||
@@ -546,6 +596,8 @@ export function WarMap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initAnimation = useRef<(map: MapboxMap) => void>(null!)
|
const initAnimation = useRef<(map: MapboxMap) => void>(null!)
|
||||||
|
const [zoneSourceKey, setZoneSourceKey] = useState(0)
|
||||||
|
const lastCheckedZoneRef = useRef<string>('')
|
||||||
initAnimation.current = (map: MapboxMap) => {
|
initAnimation.current = (map: MapboxMap) => {
|
||||||
startRef.current = performance.now()
|
startRef.current = performance.now()
|
||||||
|
|
||||||
@@ -837,6 +889,26 @@ export function WarMap() {
|
|||||||
return () => ro.disconnect()
|
return () => ro.disconnect()
|
||||||
}, [fitToTheater])
|
}, [fitToTheater])
|
||||||
|
|
||||||
|
// 广播更新后检查轮廓层是否仍在;若被误删则递进 zoneSourceKey 强制轮廓 Source 重新挂载
|
||||||
|
useEffect(() => {
|
||||||
|
const lastUpdated = situation.lastUpdated ?? ''
|
||||||
|
if (lastUpdated === lastCheckedZoneRef.current) return
|
||||||
|
lastCheckedZoneRef.current = lastUpdated
|
||||||
|
const map = mapRef.current?.getMap()
|
||||||
|
if (!map?.isStyleLoaded()) return
|
||||||
|
const t = setTimeout(() => {
|
||||||
|
const map2 = mapRef.current?.getMap()
|
||||||
|
if (!map2) return
|
||||||
|
const hasKurdish = !!map2.getLayer('kurdish-zones')
|
||||||
|
const hasHormuz = !!map2.getLayer('hormuz-combat-fill')
|
||||||
|
const hasHezbollah = !!map2.getLayer('hezbollah-fill')
|
||||||
|
if (!hasKurdish || !hasHormuz || !hasHezbollah) {
|
||||||
|
setZoneSourceKey((k) => k + 1)
|
||||||
|
}
|
||||||
|
}, 150)
|
||||||
|
return () => clearTimeout(t)
|
||||||
|
}, [situation.lastUpdated])
|
||||||
|
|
||||||
if (!MAPBOX_TOKEN) {
|
if (!MAPBOX_TOKEN) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full items-center justify-center bg-military-dark">
|
<div className="flex h-full w-full items-center justify-center bg-military-dark">
|
||||||
@@ -877,6 +949,9 @@ export function WarMap() {
|
|||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<span className="h-1.5 w-1.5 rounded-sm bg-red-500/40" /> 胡塞武装
|
<span className="h-1.5 w-1.5 rounded-sm bg-red-500/40" /> 胡塞武装
|
||||||
</span>
|
</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="flex items-center gap-1">
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-[#3B82F6]" /> 林肯打击
|
<span className="h-1.5 w-1.5 rounded-full bg-[#3B82F6]" /> 林肯打击
|
||||||
</span>
|
</span>
|
||||||
@@ -1241,7 +1316,7 @@ export function WarMap() {
|
|||||||
</Source>
|
</Source>
|
||||||
|
|
||||||
{/* 跨国库尔德势力:土(Bakur)/叙(Rojava)/伊(Bashur) 三区 MultiPolygon + 北/南钳形箭头 */}
|
{/* 跨国库尔德势力:土(Bakur)/叙(Rojava)/伊(Bashur) 三区 MultiPolygon + 北/南钳形箭头 */}
|
||||||
<Source id="kurdish-front-source" type="geojson" data={KURDISH_FRONT_GEOJSON}>
|
<Source key={`zone-kurdish-${zoneSourceKey}`} id="kurdish-front-source" type="geojson" data={KURDISH_FRONT_GEOJSON}>
|
||||||
{/* 势力范围:紫色半透明,远景更显、近景更透(描边单独 line 层,参考真主党) */}
|
{/* 势力范围:紫色半透明,远景更显、近景更透(描边单独 line 层,参考真主党) */}
|
||||||
<Layer
|
<Layer
|
||||||
id="kurdish-zones"
|
id="kurdish-zones"
|
||||||
@@ -1686,6 +1761,62 @@ export function WarMap() {
|
|||||||
/>
|
/>
|
||||||
</Source>
|
</Source>
|
||||||
|
|
||||||
|
{/* 苏丹武装势力范围(支持伊朗,伊朗色标轮廓) */}
|
||||||
|
<Source
|
||||||
|
id="sudan-area"
|
||||||
|
type="geojson"
|
||||||
|
data={EXTENDED_WAR_ZONES.sudanZone}
|
||||||
|
>
|
||||||
|
<Layer
|
||||||
|
id="sudan-fill"
|
||||||
|
type="fill"
|
||||||
|
paint={{
|
||||||
|
'fill-color': 'rgba(239, 68, 68, 0.2)',
|
||||||
|
'fill-outline-color': 'transparent',
|
||||||
|
'fill-opacity': 0.4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
{/* 苏丹国境线(独立线图层,伊朗红) */}
|
||||||
|
<Source
|
||||||
|
id="sudan-border"
|
||||||
|
type="geojson"
|
||||||
|
data={EXTENDED_WAR_ZONES.sudanBorderLine}
|
||||||
|
>
|
||||||
|
<Layer
|
||||||
|
id="sudan-border-line"
|
||||||
|
type="line"
|
||||||
|
paint={{
|
||||||
|
'line-color': '#EF4444',
|
||||||
|
'line-width': 2,
|
||||||
|
'line-opacity': 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
<Source
|
||||||
|
id="sudan-label"
|
||||||
|
type="geojson"
|
||||||
|
data={{
|
||||||
|
type: 'Feature',
|
||||||
|
properties: { name: '苏丹武装' },
|
||||||
|
geometry: { type: 'Point', coordinates: EXTENDED_WAR_ZONES.sudanLabelCenter },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Layer
|
||||||
|
id="sudan-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
|
<Source
|
||||||
id="iran-label"
|
id="iran-label"
|
||||||
@@ -1805,7 +1936,7 @@ export function WarMap() {
|
|||||||
</Source>
|
</Source>
|
||||||
|
|
||||||
{/* 霍尔木兹海峡交战区 - 金黄色 mesh 区域 */}
|
{/* 霍尔木兹海峡交战区 - 金黄色 mesh 区域 */}
|
||||||
<Source id="hormuz-combat-zone" type="geojson" data={hormuzZone}>
|
<Source key={`zone-hormuz-${zoneSourceKey}`} id="hormuz-combat-zone" type="geojson" data={hormuzZone}>
|
||||||
<Layer
|
<Layer
|
||||||
id="hormuz-combat-fill"
|
id="hormuz-combat-fill"
|
||||||
type="fill"
|
type="fill"
|
||||||
@@ -1826,7 +1957,7 @@ export function WarMap() {
|
|||||||
</Source>
|
</Source>
|
||||||
|
|
||||||
{/* 真主党势力范围 - 绿色半透明区域 */}
|
{/* 真主党势力范围 - 绿色半透明区域 */}
|
||||||
<Source id="hezbollah-zone" type="geojson" data={hezbollahZone}>
|
<Source key={`zone-hezbollah-${zoneSourceKey}`} id="hezbollah-zone" type="geojson" data={hezbollahZone}>
|
||||||
<Layer
|
<Layer
|
||||||
id="hezbollah-fill"
|
id="hezbollah-fill"
|
||||||
type="fill"
|
type="fill"
|
||||||
@@ -1846,18 +1977,7 @@ export function WarMap() {
|
|||||||
</Source>
|
</Source>
|
||||||
|
|
||||||
{/* 霍尔木兹海峡区域标注 */}
|
{/* 霍尔木兹海峡区域标注 */}
|
||||||
<Source
|
<Source id="hormuz-label" type="geojson" data={hormuzLabelData}>
|
||||||
id="hormuz-label"
|
|
||||||
type="geojson"
|
|
||||||
data={{
|
|
||||||
type: 'Feature',
|
|
||||||
properties: { name: (hormuzZone.properties as any).name },
|
|
||||||
geometry: {
|
|
||||||
type: 'Point',
|
|
||||||
coordinates: EXTENDED_WAR_ZONES.hormuzLabelCenter,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Layer
|
<Layer
|
||||||
id="hormuz-label-text"
|
id="hormuz-label-text"
|
||||||
type="symbol"
|
type="symbol"
|
||||||
@@ -1876,18 +1996,7 @@ export function WarMap() {
|
|||||||
</Source>
|
</Source>
|
||||||
|
|
||||||
{/* 真主党势力范围标注 */}
|
{/* 真主党势力范围标注 */}
|
||||||
<Source
|
<Source id="hezbollah-label" type="geojson" data={hezbollahLabelData}>
|
||||||
id="hezbollah-label"
|
|
||||||
type="geojson"
|
|
||||||
data={{
|
|
||||||
type: 'Feature',
|
|
||||||
properties: { name: (hezbollahZone.properties as any).name },
|
|
||||||
geometry: {
|
|
||||||
type: 'Point',
|
|
||||||
coordinates: EXTENDED_WAR_ZONES.hezbollahLabelCenter,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Layer
|
<Layer
|
||||||
id="hezbollah-label-text"
|
id="hezbollah-label-text"
|
||||||
type="symbol"
|
type="symbol"
|
||||||
@@ -1906,18 +2015,7 @@ export function WarMap() {
|
|||||||
</Source>
|
</Source>
|
||||||
|
|
||||||
{/* 库尔德武装势力范围标注(参照真主党紫色区域标记) */}
|
{/* 库尔德武装势力范围标注(参照真主党紫色区域标记) */}
|
||||||
<Source
|
<Source id="kurdish-label" type="geojson" data={kurdishLabelData}>
|
||||||
id="kurdish-label"
|
|
||||||
type="geojson"
|
|
||||||
data={{
|
|
||||||
type: 'Feature',
|
|
||||||
properties: { name: '库尔德武装' },
|
|
||||||
geometry: {
|
|
||||||
type: 'Point',
|
|
||||||
coordinates: EXTENDED_WAR_ZONES.kurdishLabelCenter,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Layer
|
<Layer
|
||||||
id="kurdish-label-text"
|
id="kurdish-label-text"
|
||||||
type="symbol"
|
type="symbol"
|
||||||
|
|||||||
@@ -91,6 +91,76 @@ export const EXTENDED_WAR_ZONES = {
|
|||||||
// 真主党区域标注点(用于显示文字)
|
// 真主党区域标注点(用于显示文字)
|
||||||
hezbollahLabelCenter: [35.7, 33.7] as [number, number],
|
hezbollahLabelCenter: [35.7, 33.7] as [number, number],
|
||||||
|
|
||||||
|
// 苏丹武装势力范围(公开支持伊朗,轮廓用伊朗色标绘制整个苏丹国家)
|
||||||
|
sudanZone: {
|
||||||
|
type: 'Feature' as const,
|
||||||
|
properties: {
|
||||||
|
name: '苏丹武装',
|
||||||
|
status: 'IRAN-ALIGNED',
|
||||||
|
color: '#EF4444',
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: 'Polygon' as const,
|
||||||
|
coordinates: [
|
||||||
|
[
|
||||||
|
[31.0, 22.0],
|
||||||
|
[33.0, 22.0],
|
||||||
|
[34.5, 22.0],
|
||||||
|
[36.8, 22.0],
|
||||||
|
[37.3, 20.8],
|
||||||
|
[38.5, 18.0],
|
||||||
|
[37.9, 17.0],
|
||||||
|
[36.5, 14.3],
|
||||||
|
[35.0, 13.5],
|
||||||
|
[34.0, 11.5],
|
||||||
|
[32.5, 12.0],
|
||||||
|
[29.5, 9.5],
|
||||||
|
[27.0, 9.0],
|
||||||
|
[24.0, 8.5],
|
||||||
|
[23.5, 10.0],
|
||||||
|
[22.5, 13.5],
|
||||||
|
[22.5, 16.0],
|
||||||
|
[24.0, 20.0],
|
||||||
|
[25.0, 22.0],
|
||||||
|
[31.0, 22.0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
sudanLabelCenter: [30.5, 15.25] as [number, number],
|
||||||
|
|
||||||
|
/** 苏丹国境线(与 sudanZone 同轮廓,LineString 供线图层绘制) */
|
||||||
|
sudanBorderLine: {
|
||||||
|
type: 'Feature' as const,
|
||||||
|
properties: { name: '苏丹国境线' },
|
||||||
|
geometry: {
|
||||||
|
type: 'LineString' as const,
|
||||||
|
coordinates: [
|
||||||
|
[31.0, 22.0],
|
||||||
|
[33.0, 22.0],
|
||||||
|
[34.5, 22.0],
|
||||||
|
[36.8, 22.0],
|
||||||
|
[37.3, 20.8],
|
||||||
|
[38.5, 18.0],
|
||||||
|
[37.9, 17.0],
|
||||||
|
[36.5, 14.3],
|
||||||
|
[35.0, 13.5],
|
||||||
|
[34.0, 11.5],
|
||||||
|
[32.5, 12.0],
|
||||||
|
[29.5, 9.5],
|
||||||
|
[27.0, 9.0],
|
||||||
|
[24.0, 8.5],
|
||||||
|
[23.5, 10.0],
|
||||||
|
[22.5, 13.5],
|
||||||
|
[22.5, 16.0],
|
||||||
|
[24.0, 20.0],
|
||||||
|
[25.0, 22.0],
|
||||||
|
[31.0, 22.0],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// 库尔德武装势力区域标注点(叙/土/伊三区紫色带中心附近)
|
// 库尔德武装势力区域标注点(叙/土/伊三区紫色带中心附近)
|
||||||
kurdishLabelCenter: [43.5, 36.3] as [number, number],
|
kurdishLabelCenter: [43.5, 36.3] as [number, number],
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user