Merge pull request #76 from adust09/feat/east-asia-enhancement

feat: East Asia intelligence coverage enhancement
This commit is contained in:
Shadowbroker
2026-03-15 23:46:30 -06:00
committed by GitHub
14 changed files with 1467 additions and 11 deletions
+30
View File
@@ -64,6 +64,36 @@
"name": "Stars and Stripes", "name": "Stars and Stripes",
"url": "https://www.stripes.com/feeds/pacific.rss", "url": "https://www.stripes.com/feeds/pacific.rss",
"weight": 4 "weight": 4
},
{
"name": "Yonhap",
"url": "https://en.yna.co.kr/RSS/news.xml",
"weight": 4
},
{
"name": "Nikkei Asia",
"url": "https://asia.nikkei.com/rss",
"weight": 3
},
{
"name": "Taipei Times",
"url": "https://www.taipeitimes.com/xml/pda.rss",
"weight": 4
},
{
"name": "Asia Times",
"url": "https://asiatimes.com/feed/",
"weight": 3
},
{
"name": "Defense News",
"url": "https://www.defensenews.com/arc/outboundfeeds/rss/",
"weight": 3
},
{
"name": "Japan Times",
"url": "https://www.japantimes.co.jp/feed/",
"weight": 3
} }
] ]
} }
+582
View File
@@ -142,5 +142,587 @@
"branch": "navy", "branch": "navy",
"lat": -7.316, "lat": -7.316,
"lng": 72.411 "lng": 72.411
},
{
"name": "Fuzhou Changle Air Base",
"country": "China",
"operator": "PLAAF",
"branch": "air_force",
"lat": 25.935,
"lng": 119.663
},
{
"name": "Longtian Air Base",
"country": "China",
"operator": "PLAAF",
"branch": "air_force",
"lat": 25.674,
"lng": 119.507
},
{
"name": "Huian Air Base",
"country": "China",
"operator": "PLAAF",
"branch": "air_force",
"lat": 25.028,
"lng": 118.802
},
{
"name": "Zhangzhou Air Base",
"country": "China",
"operator": "PLAAF",
"branch": "air_force",
"lat": 24.575,
"lng": 117.588
},
{
"name": "Suixi Air Base",
"country": "China",
"operator": "PLAAF",
"branch": "air_force",
"lat": 21.374,
"lng": 110.228
},
{
"name": "Nanning Wuxu Air Base",
"country": "China",
"operator": "PLAAF",
"branch": "air_force",
"lat": 22.609,
"lng": 108.171
},
{
"name": "Jinan Yaoqiang Air Base",
"country": "China",
"operator": "PLAAF",
"branch": "air_force",
"lat": 36.857,
"lng": 117.216
},
{
"name": "Wuhan Wangjiadun Air Base",
"country": "China",
"operator": "PLAAF",
"branch": "air_force",
"lat": 30.500,
"lng": 114.211
},
{
"name": "Changsha Datuopu Air Base",
"country": "China",
"operator": "PLAAF",
"branch": "air_force",
"lat": 28.068,
"lng": 112.866
},
{
"name": "Dingxin Air Base",
"country": "China",
"operator": "PLAAF",
"branch": "air_force",
"lat": 40.296,
"lng": 99.750
},
{
"name": "Hotan Air Base",
"country": "China",
"operator": "PLAAF",
"branch": "air_force",
"lat": 37.039,
"lng": 79.866
},
{
"name": "Lingshui Air Base",
"country": "China",
"operator": "PLAAF / PLAN",
"branch": "air_force",
"lat": 18.507,
"lng": 110.034
},
{
"name": "Sanya Phoenix Air Base",
"country": "China",
"operator": "PLAN Aviation",
"branch": "air_force",
"lat": 18.303,
"lng": 109.412
},
{
"name": "Zhanjiang Naval Base",
"country": "China",
"operator": "PLAN Southern Theater",
"branch": "navy",
"lat": 21.190,
"lng": 110.405
},
{
"name": "Qingdao / Jiaozhou Naval Base",
"country": "China",
"operator": "PLAN Northern Theater",
"branch": "navy",
"lat": 36.100,
"lng": 120.272
},
{
"name": "Ningbo / Zhoushan Naval Base",
"country": "China",
"operator": "PLAN Eastern Theater",
"branch": "navy",
"lat": 29.949,
"lng": 122.094
},
{
"name": "Yulin Naval Base",
"country": "China",
"operator": "PLAN (SSBN)",
"branch": "navy",
"lat": 18.226,
"lng": 109.557
},
{
"name": "Shanghai Wusong Naval Base",
"country": "China",
"operator": "PLAN Eastern Theater",
"branch": "navy",
"lat": 31.398,
"lng": 121.503
},
{
"name": "Dalian Naval Shipyard",
"country": "China",
"operator": "PLAN (carrier construction)",
"branch": "navy",
"lat": 38.936,
"lng": 121.625
},
{
"name": "Jiangnan Shipyard (Changxing Island)",
"country": "China",
"operator": "PLAN (carrier/destroyer construction)",
"branch": "navy",
"lat": 31.358,
"lng": 121.746
},
{
"name": "Xiangshan Naval Base",
"country": "China",
"operator": "PLAN Eastern Theater",
"branch": "navy",
"lat": 29.480,
"lng": 121.933
},
{
"name": "Woody Island (Yongxing)",
"country": "China",
"operator": "PLA Sansha Garrison",
"branch": "navy",
"lat": 16.833,
"lng": 112.333
},
{
"name": "Fiery Cross Reef",
"country": "China",
"operator": "PLA (SCS outpost)",
"branch": "air_force",
"lat": 9.550,
"lng": 112.892
},
{
"name": "Subi Reef",
"country": "China",
"operator": "PLA (SCS outpost)",
"branch": "air_force",
"lat": 10.923,
"lng": 114.083
},
{
"name": "Mischief Reef",
"country": "China",
"operator": "PLA (SCS outpost)",
"branch": "navy",
"lat": 9.904,
"lng": 115.536
},
{
"name": "Dehua PLARF Base (Base 613)",
"country": "China",
"operator": "PLARF 61 Base",
"branch": "missile",
"lat": 25.494,
"lng": 118.241
},
{
"name": "Huangshan PLARF Base (Base 612)",
"country": "China",
"operator": "PLARF 61 Base",
"branch": "missile",
"lat": 29.714,
"lng": 118.337
},
{
"name": "Jiande PLARF Base",
"country": "China",
"operator": "PLARF 61 Base",
"branch": "missile",
"lat": 29.480,
"lng": 119.281
},
{
"name": "Ganzhou PLARF Base",
"country": "China",
"operator": "PLARF 61 Base",
"branch": "missile",
"lat": 25.831,
"lng": 114.935
},
{
"name": "Meizhou PLARF Base",
"country": "China",
"operator": "PLARF 61 Base",
"branch": "missile",
"lat": 24.288,
"lng": 116.122
},
{
"name": "Leping PLARF Base",
"country": "China",
"operator": "PLARF 62 Base",
"branch": "missile",
"lat": 28.963,
"lng": 117.129
},
{
"name": "Nanyang PLARF Base",
"country": "China",
"operator": "PLARF 63 Base",
"branch": "missile",
"lat": 33.003,
"lng": 112.528
},
{
"name": "Luoyang PLARF Base",
"country": "China",
"operator": "PLARF 63 Base",
"branch": "missile",
"lat": 34.620,
"lng": 112.454
},
{
"name": "Haiyang PLARF Base (ICBM)",
"country": "China",
"operator": "PLARF 65 Base",
"branch": "missile",
"lat": 36.776,
"lng": 121.158
},
{
"name": "Sundian PLARF Base (ICBM)",
"country": "China",
"operator": "PLARF 66 Base",
"branch": "missile",
"lat": 40.200,
"lng": 113.200
},
{
"name": "Vladivostok (Pacific Fleet HQ)",
"country": "Russia",
"operator": "Russian Pacific Fleet",
"branch": "navy",
"lat": 43.114,
"lng": 131.885
},
{
"name": "Vilyuchinsk SSBN Base",
"country": "Russia",
"operator": "Russian Pacific Fleet (SSBN)",
"branch": "navy",
"lat": 52.929,
"lng": 158.405
},
{
"name": "Yelizovo Air Base (Kamchatka)",
"country": "Russia",
"operator": "Russian VKS / Pacific Fleet Aviation",
"branch": "air_force",
"lat": 53.167,
"lng": 158.454
},
{
"name": "Khabarovsk-Bolshoy Air Base",
"country": "Russia",
"operator": "Russian VKS",
"branch": "air_force",
"lat": 48.528,
"lng": 135.188
},
{
"name": "Ukrainka Air Base (Tu-95 bomber)",
"country": "Russia",
"operator": "Russian VKS Long-Range Aviation",
"branch": "air_force",
"lat": 51.170,
"lng": 128.400
},
{
"name": "Fokino Naval Base",
"country": "Russia",
"operator": "Russian Pacific Fleet",
"branch": "navy",
"lat": 42.961,
"lng": 132.447
},
{
"name": "Sovgavan Naval Base",
"country": "Russia",
"operator": "Russian Pacific Fleet",
"branch": "navy",
"lat": 48.966,
"lng": 140.291
},
{
"name": "Vozdvizhenka Air Base",
"country": "Russia",
"operator": "Russian VKS",
"branch": "air_force",
"lat": 43.907,
"lng": 131.984
},
{
"name": "Etorofu / Iturup (Kuril Islands)",
"country": "Russia",
"operator": "Russian Army 18th MG Div",
"branch": "army",
"lat": 44.927,
"lng": 147.863
},
{
"name": "Yongbyon Nuclear Complex",
"country": "North Korea",
"operator": "DPRK Nuclear Program",
"branch": "nuclear",
"lat": 39.796,
"lng": 125.754
},
{
"name": "Punggye-ri Nuclear Test Site",
"country": "North Korea",
"operator": "DPRK Nuclear Program",
"branch": "nuclear",
"lat": 41.281,
"lng": 129.104
},
{
"name": "Sinpo Submarine Base",
"country": "North Korea",
"operator": "KPN (SLBM)",
"branch": "navy",
"lat": 40.024,
"lng": 128.178
},
{
"name": "Sohae Satellite Launching Station",
"country": "North Korea",
"operator": "DPRK NADA",
"branch": "missile",
"lat": 39.660,
"lng": 124.705
},
{
"name": "Tonghae Satellite Launching Ground",
"country": "North Korea",
"operator": "DPRK NADA",
"branch": "missile",
"lat": 40.856,
"lng": 129.665
},
{
"name": "Kaechon Air Base",
"country": "North Korea",
"operator": "KPAF",
"branch": "air_force",
"lat": 39.752,
"lng": 125.890
},
{
"name": "Wonsan-Kalma Air Base",
"country": "North Korea",
"operator": "KPAF",
"branch": "air_force",
"lat": 39.167,
"lng": 127.487
},
{
"name": "Nampo Naval Base",
"country": "North Korea",
"operator": "KPN West Fleet",
"branch": "navy",
"lat": 38.737,
"lng": 125.408
},
{
"name": "Haeju Forward Naval Base",
"country": "North Korea",
"operator": "KPN",
"branch": "navy",
"lat": 38.033,
"lng": 125.714
},
{
"name": "Koksan Long-Range Artillery Base",
"country": "North Korea",
"operator": "KPA Artillery Corps",
"branch": "army",
"lat": 38.330,
"lng": 126.586
},
{
"name": "Hualien Air Force Base (Jiashan)",
"country": "Taiwan",
"operator": "ROCAF 5th TFW",
"branch": "air_force",
"lat": 24.024,
"lng": 121.617
},
{
"name": "Ching Chuan Kang Air Base",
"country": "Taiwan",
"operator": "ROCAF 3rd TFW",
"branch": "air_force",
"lat": 24.264,
"lng": 120.621
},
{
"name": "Zuoying Naval Base",
"country": "Taiwan",
"operator": "ROCN Fleet Command",
"branch": "navy",
"lat": 22.702,
"lng": 120.268
},
{
"name": "Tainan Air Force Base",
"country": "Taiwan",
"operator": "ROCAF 1st TFW",
"branch": "air_force",
"lat": 22.951,
"lng": 120.206
},
{
"name": "Pingtung Air Base (South)",
"country": "Taiwan",
"operator": "ROCAF 6th Mixed Wing",
"branch": "air_force",
"lat": 22.673,
"lng": 120.462
},
{
"name": "Hsinchu Air Force Base",
"country": "Taiwan",
"operator": "ROCAF 2nd TFW",
"branch": "air_force",
"lat": 24.818,
"lng": 120.939
},
{
"name": "Suao Naval Base",
"country": "Taiwan",
"operator": "ROCN 168th Fleet",
"branch": "navy",
"lat": 24.594,
"lng": 121.862
},
{
"name": "Taitung Zhihang Air Base",
"country": "Taiwan",
"operator": "ROCAF 7th FTG",
"branch": "air_force",
"lat": 22.793,
"lng": 121.182
},
{
"name": "Subic Bay (EDCA site)",
"country": "Philippines",
"operator": "Philippine Navy / US EDCA",
"branch": "navy",
"lat": 14.794,
"lng": 120.281
},
{
"name": "Clark Air Base (EDCA site)",
"country": "Philippines",
"operator": "Philippine Air Force / US EDCA",
"branch": "air_force",
"lat": 15.186,
"lng": 120.560
},
{
"name": "Basa Air Base",
"country": "Philippines",
"operator": "Philippine Air Force",
"branch": "air_force",
"lat": 14.988,
"lng": 120.493
},
{
"name": "Antonio Bautista Air Base (Palawan)",
"country": "Philippines",
"operator": "Philippine Air Force",
"branch": "air_force",
"lat": 9.742,
"lng": 118.759
},
{
"name": "Lal-lo (EDCA site, Cagayan)",
"country": "Philippines",
"operator": "Philippine Army / US EDCA",
"branch": "army",
"lat": 18.200,
"lng": 121.659
},
{
"name": "Balabac Naval Station",
"country": "Philippines",
"operator": "Philippine Navy",
"branch": "navy",
"lat": 7.986,
"lng": 117.062
},
{
"name": "RAAF Base Darwin",
"country": "Australia",
"operator": "RAAF / USMC Rotational",
"branch": "air_force",
"lat": -12.415,
"lng": 130.876
},
{
"name": "RAAF Base Tindal",
"country": "Australia",
"operator": "RAAF / USAF Rotational",
"branch": "air_force",
"lat": -14.521,
"lng": 132.378
},
{
"name": "HMAS Stirling (Garden Island)",
"country": "Australia",
"operator": "RAN / AUKUS submarine base",
"branch": "navy",
"lat": -32.235,
"lng": 115.691
},
{
"name": "Pine Gap (Joint Intelligence)",
"country": "Australia",
"operator": "ASD / CIA Joint Facility",
"branch": "army",
"lat": -23.799,
"lng": 133.737
} }
] ]
+646
View File
@@ -0,0 +1,646 @@
{
"412000001": {
"hull_number": "101",
"name": "Nanchang",
"class": "Type 055",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_055_destroyer"
},
"412000002": {
"hull_number": "102",
"name": "Lhasa",
"class": "Type 055",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_055_destroyer"
},
"412000003": {
"hull_number": "103",
"name": "Anshan",
"class": "Type 055",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_055_destroyer"
},
"412000004": {
"hull_number": "104",
"name": "Wuxi",
"class": "Type 055",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_055_destroyer"
},
"412000005": {
"hull_number": "105",
"name": "Dalian",
"class": "Type 055",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_055_destroyer"
},
"412000006": {
"hull_number": "106",
"name": "Yan'an",
"class": "Type 055",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_055_destroyer"
},
"412000007": {
"hull_number": "107",
"name": "Zunyi",
"class": "Type 055",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_055_destroyer"
},
"412000008": {
"hull_number": "108",
"name": "Xianyang",
"class": "Type 055",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_055_destroyer"
},
"412000101": {
"hull_number": "117",
"name": "Xining",
"class": "Type 052D",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
},
"412000102": {
"hull_number": "118",
"name": "Urumqi",
"class": "Type 052D",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
},
"412000103": {
"hull_number": "119",
"name": "Guiyang",
"class": "Type 052D",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
},
"412000104": {
"hull_number": "120",
"name": "Chengdu",
"class": "Type 052D",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
},
"412000105": {
"hull_number": "131",
"name": "Taiyuan",
"class": "Type 052D",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
},
"412000106": {
"hull_number": "132",
"name": "Suzhou",
"class": "Type 052D",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
},
"412000107": {
"hull_number": "133",
"name": "Nantong",
"class": "Type 052D",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
},
"412000108": {
"hull_number": "134",
"name": "Suqian",
"class": "Type 052D",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
},
"412000109": {
"hull_number": "135",
"name": "Lianyungang",
"class": "Type 052D",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
},
"412000110": {
"hull_number": "136",
"name": "Xuchang",
"class": "Type 052D",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
},
"412000111": {
"hull_number": "155",
"name": "Nanjing",
"class": "Type 052D",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
},
"412000112": {
"hull_number": "156",
"name": "Zibo",
"class": "Type 052D",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
},
"412000113": {
"hull_number": "157",
"name": "Lishui",
"class": "Type 052D",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
},
"412000114": {
"hull_number": "161",
"name": "Hohhot",
"class": "Type 052D",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
},
"412000115": {
"hull_number": "162",
"name": "Yancheng",
"class": "Type 052D",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
},
"412000116": {
"hull_number": "163",
"name": "Kaifeng",
"class": "Type 052D",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
},
"412000117": {
"hull_number": "164",
"name": "Taizhou",
"class": "Type 052D",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
},
"412000201": {
"hull_number": "538",
"name": "Yantai",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000202": {
"hull_number": "539",
"name": "Wuhu",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000203": {
"hull_number": "540",
"name": "Huainan",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000204": {
"hull_number": "541",
"name": "Huaihua",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000205": {
"hull_number": "542",
"name": "Zaozhuang",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000206": {
"hull_number": "529",
"name": "Zhoushan",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000207": {
"hull_number": "530",
"name": "Xuzhou",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000208": {
"hull_number": "531",
"name": "Xiangtan",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000209": {
"hull_number": "532",
"name": "Jingzhou",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000210": {
"hull_number": "536",
"name": "Xuchang",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000211": {
"hull_number": "546",
"name": "Yancheng",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000212": {
"hull_number": "547",
"name": "Linyi",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000213": {
"hull_number": "548",
"name": "Yiyang",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000214": {
"hull_number": "549",
"name": "Changzhou",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000215": {
"hull_number": "550",
"name": "Weifang",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000301": {
"hull_number": "31",
"name": "Hainan",
"class": "Type 075",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_075_landing_helicopter_dock"
},
"412000302": {
"hull_number": "32",
"name": "Guangxi",
"class": "Type 075",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_075_landing_helicopter_dock"
},
"412000303": {
"hull_number": "33",
"name": "Anhui",
"class": "Type 075",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_075_landing_helicopter_dock"
},
"412000401": {
"hull_number": "16",
"name": "Liaoning",
"class": "Type 001",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Chinese_aircraft_carrier_Liaoning"
},
"412000402": {
"hull_number": "17",
"name": "Shandong",
"class": "Type 002",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Chinese_aircraft_carrier_Shandong"
},
"412000403": {
"hull_number": "18",
"name": "Fujian",
"class": "Type 003",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Chinese_aircraft_carrier_Fujian"
},
"412000501": {
"hull_number": "980",
"name": "Hulunhu",
"class": "Type 901",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_901_replenishment_ship"
},
"412000502": {
"hull_number": "981",
"name": "Chaganhu",
"class": "Type 901",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_901_replenishment_ship"
},
"412000601": {
"hull_number": "998",
"name": "Kunlun Shan",
"class": "Type 071",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_071_amphibious_transport_dock"
},
"412000602": {
"hull_number": "999",
"name": "Jinggang Shan",
"class": "Type 071",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_071_amphibious_transport_dock"
},
"412000603": {
"hull_number": "989",
"name": "Changbai Shan",
"class": "Type 071",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_071_amphibious_transport_dock"
},
"412000604": {
"hull_number": "988",
"name": "Yimeng Shan",
"class": "Type 071",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_071_amphibious_transport_dock"
},
"412000605": {
"hull_number": "987",
"name": "Wuzhi Shan",
"class": "Type 071",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_071_amphibious_transport_dock"
},
"412000606": {
"hull_number": "986",
"name": "Longhu Shan",
"class": "Type 071",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_071_amphibious_transport_dock"
},
"412000607": {
"hull_number": "985",
"name": "Dabie Shan",
"class": "Type 071",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_071_amphibious_transport_dock"
},
"412000608": {
"hull_number": "984",
"name": "Wuyi Shan",
"class": "Type 071",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_071_amphibious_transport_dock"
},
"412000701": {
"hull_number": "815A-1",
"name": "Dongdiao",
"class": "Type 815A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_815_electronic_reconnaissance_ship"
},
"412000702": {
"hull_number": "815A-2",
"name": "Haiwangxing",
"class": "Type 815A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_815_electronic_reconnaissance_ship"
},
"412000703": {
"hull_number": "815A-3",
"name": "Tianwangxing",
"class": "Type 815A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_815_electronic_reconnaissance_ship"
},
"412009001": {
"hull_number": "2901",
"name": "CCG 2901",
"class": "12000-ton Cutter",
"force": "CCG",
"wiki": "https://en.wikipedia.org/wiki/China_Coast_Guard"
},
"412009002": {
"hull_number": "3901",
"name": "CCG 3901",
"class": "12000-ton Cutter",
"force": "CCG",
"wiki": "https://en.wikipedia.org/wiki/China_Coast_Guard"
},
"412009003": {
"hull_number": "1305",
"name": "CCG 1305",
"class": "Type 818",
"force": "CCG",
"wiki": "https://en.wikipedia.org/wiki/China_Coast_Guard"
},
"412009004": {
"hull_number": "1306",
"name": "CCG 1306",
"class": "Type 818",
"force": "CCG",
"wiki": "https://en.wikipedia.org/wiki/China_Coast_Guard"
},
"412009005": {
"hull_number": "2502",
"name": "CCG 2502",
"class": "5000-ton Cutter",
"force": "CCG",
"wiki": "https://en.wikipedia.org/wiki/China_Coast_Guard"
},
"412009006": {
"hull_number": "2302",
"name": "CCG 2302",
"class": "3000-ton Cutter",
"force": "CCG",
"wiki": "https://en.wikipedia.org/wiki/China_Coast_Guard"
},
"412009007": {
"hull_number": "2303",
"name": "CCG 2303",
"class": "3000-ton Cutter",
"force": "CCG",
"wiki": "https://en.wikipedia.org/wiki/China_Coast_Guard"
},
"412009008": {
"hull_number": "1103",
"name": "CCG 1103",
"class": "Type 718B",
"force": "CCG",
"wiki": "https://en.wikipedia.org/wiki/China_Coast_Guard"
},
"412009009": {
"hull_number": "1105",
"name": "CCG 1105",
"class": "Type 718B",
"force": "CCG",
"wiki": "https://en.wikipedia.org/wiki/China_Coast_Guard"
},
"412009010": {
"hull_number": "1302",
"name": "CCG 1302",
"class": "Type 818",
"force": "CCG",
"wiki": "https://en.wikipedia.org/wiki/China_Coast_Guard"
},
"412000801": {
"hull_number": "171",
"name": "Haikou",
"class": "Type 052C",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052C_destroyer"
},
"412000802": {
"hull_number": "170",
"name": "Lanzhou",
"class": "Type 052C",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052C_destroyer"
},
"412000803": {
"hull_number": "150",
"name": "Changchun",
"class": "Type 052C",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052C_destroyer"
},
"412000804": {
"hull_number": "151",
"name": "Zhengzhou",
"class": "Type 052C",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052C_destroyer"
},
"412000805": {
"hull_number": "152",
"name": "Jinan",
"class": "Type 052C",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052C_destroyer"
},
"412000806": {
"hull_number": "153",
"name": "Xi'an",
"class": "Type 052C",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052C_destroyer"
},
"412000901": {
"hull_number": "572",
"name": "Hengshui",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000902": {
"hull_number": "573",
"name": "Liuzhou",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000903": {
"hull_number": "574",
"name": "Sanya",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000904": {
"hull_number": "575",
"name": "Yueyang",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000905": {
"hull_number": "576",
"name": "Daqing",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412000906": {
"hull_number": "577",
"name": "Huanggang",
"class": "Type 054A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_054A_frigate"
},
"412001001": {
"hull_number": "500",
"name": "Xianfeng",
"class": "Type 056A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_056_corvette"
},
"412001002": {
"hull_number": "501",
"name": "Xinyang",
"class": "Type 056A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_056_corvette"
},
"412001003": {
"hull_number": "502",
"name": "Huangshi",
"class": "Type 056",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_056_corvette"
},
"412001004": {
"hull_number": "509",
"name": "Huaian",
"class": "Type 056A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_056_corvette"
},
"412001005": {
"hull_number": "510",
"name": "Ningde",
"class": "Type 056A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_056_corvette"
},
"412001101": {
"hull_number": "795",
"name": "Nanchong",
"class": "Type 039A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_039A_submarine"
},
"412001201": {
"hull_number": "892",
"name": "Hualuoshan",
"class": "Type 903A",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_903_replenishment_ship"
},
"412001202": {
"hull_number": "889",
"name": "Taihu",
"class": "Type 903",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_903_replenishment_ship"
},
"412001301": {
"hull_number": "636",
"name": "Nanning",
"class": "Type 052DL",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
},
"412001302": {
"hull_number": "165",
"name": "Zhanjiang",
"class": "Type 052DL",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
},
"412001303": {
"hull_number": "166",
"name": "Huainan",
"class": "Type 052DL",
"force": "PLAN",
"wiki": "https://en.wikipedia.org/wiki/Type_052D_destroyer"
}
}
+5
View File
@@ -39,6 +39,11 @@ def fetch_ships():
for ship in ships: for ship in ships:
enrich_with_yacht_alert(ship) enrich_with_yacht_alert(ship)
# Enrich ships with PLAN/CCG vessel data
from services.fetchers.plan_vessel_alert import enrich_with_plan_vessel
for ship in ships:
enrich_with_plan_vessel(ship)
logger.info(f"Ships: {len(carriers)} carriers + {len(ais_vessels)} AIS vessels") logger.info(f"Ships: {len(carriers)} carriers + {len(ais_vessels)} AIS vessels")
with _data_lock: with _data_lock:
latest_data['ships'] = ships latest_data['ships'] = ships
+12 -1
View File
@@ -38,6 +38,11 @@ _ICAO_COUNTRY_RANGES = [
(0x840000, 0x87FFFF, "Japan", "JSDF"), (0x840000, 0x87FFFF, "Japan", "JSDF"),
(0x700000, 0x71FFFF, "South Korea", "ROK"), (0x700000, 0x71FFFF, "South Korea", "ROK"),
(0xE80000, 0xE80FFF, "Taiwan", "ROC"), (0xE80000, 0xE80FFF, "Taiwan", "ROC"),
(0x150000, 0x157FFF, "Russia", "VKS"),
(0x7C0000, 0x7FFFFF, "Australia", "RAAF"),
(0x758000, 0x75FFFF, "Philippines", "PAF"),
(0x768000, 0x76FFFF, "Singapore", "RSAF"),
(0x720000, 0x727FFF, "North Korea", "KPAF"),
] ]
@@ -66,18 +71,24 @@ def _classify_military_type(raw_model: str) -> str:
if any(k in model for k in [ if any(k in model for k in [
"F16", "F35", "F22", "F15", "F18", "T38", "T6", "A10", "F16", "F35", "F22", "F15", "F18", "T38", "T6", "A10",
"J10", "J11", "J15", "J16", "J20", "JF17", "J10", "J11", "J15", "J16", "J20", "JF17",
"SU27", "SU30", "SU35", "SU27", "SU30", "SU35", "SU57", "MIG29", "MIG31",
"F15J", "F2", "IDF", "FA50", "KF21", "F15J", "F2", "IDF", "FA50", "KF21",
]): ]):
return "fighter" return "fighter"
if any(k in model for k in [
"TU95", "TU160", "TU22",
]):
return "bomber"
if any(k in model for k in [ if any(k in model for k in [
"C17", "C5", "C130", "C30", "A400", "V22", "C17", "C5", "C130", "C30", "A400", "V22",
"Y20", "Y9", "Y8", "C2", "Y20", "Y9", "Y8", "C2",
"IL76", "AN124", "AN12",
]): ]):
return "cargo" return "cargo"
if any(k in model for k in [ if any(k in model for k in [
"P8", "E3", "E8", "U2", "P8", "E3", "E8", "U2",
"KJ500", "KJ200", "GX11", "P1", "E767", "E2K", "E2C", "KJ500", "KJ200", "GX11", "P1", "E767", "E2K", "E2C",
"A50", "TU214R", "IL20",
]): ]):
return "recon" return "recon"
return "default" return "default"
+16
View File
@@ -111,6 +111,22 @@ _KEYWORD_COORDS = {
"singapore": (1.352, 103.819), "singapore": (1.352, 103.819),
"bangkok": (13.756, 100.501), "bangkok": (13.756, 100.501),
"jakarta": (-6.208, 106.845), "jakarta": (-6.208, 106.845),
# East Asia — islands, straits, and disputed areas
"pratas": (20.71, 116.72),
"dongsha": (20.71, 116.72),
"kinmen": (24.45, 118.38),
"matsu": (26.16, 119.94),
"scarborough": (15.14, 117.77),
"paracel": (16.50, 112.00),
"spratly": (10.00, 114.00),
"miyako strait": (24.78, 125.30),
"bashi channel": (21.00, 121.50),
"luzon strait": (20.50, 121.50),
" dmz ": (38.00, 127.00),
"yalu": (40.00, 124.40),
"yongbyon": (39.80, 125.76),
"wonsan": (39.18, 127.48),
"busan": (35.18, 129.07),
} }
# Immutable after module load — sort by descending keyword length so # Immutable after module load — sort by descending keyword length so
@@ -0,0 +1,42 @@
"""PLAN/CCG Vessel Alert DB — load and enrich AIS vessels with Chinese navy/coast guard metadata."""
import os
import json
import logging
logger = logging.getLogger("services.data_fetcher")
_PLAN_CCG_DB: dict = {}
def _load_plan_ccg_db():
"""Load plan_ccg_vessels.json into memory at import time."""
global _PLAN_CCG_DB
json_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
"data", "plan_ccg_vessels.json"
)
if not os.path.exists(json_path):
logger.warning(f"PLAN/CCG vessel DB not found at {json_path}")
return
try:
with open(json_path, "r", encoding="utf-8") as fh:
_PLAN_CCG_DB.update(json.load(fh))
logger.info(f"PLAN/CCG vessel DB loaded: {len(_PLAN_CCG_DB)} vessels")
except (IOError, OSError, json.JSONDecodeError, ValueError, KeyError) as e:
logger.error(f"Failed to load PLAN/CCG vessel DB: {e}")
_load_plan_ccg_db()
def enrich_with_plan_vessel(ship: dict) -> dict:
"""If ship's MMSI is in the PLAN/CCG DB, attach enrichment metadata."""
mmsi = str(ship.get("mmsi", "")).strip()
if mmsi and mmsi in _PLAN_CCG_DB:
info = _PLAN_CCG_DB[mmsi]
ship["plan_name"] = info.get("name", "")
ship["plan_class"] = info.get("class", "")
ship["plan_force"] = info.get("force", "")
ship["plan_hull"] = info.get("hull_number", "")
ship["plan_wiki"] = info.get("wiki", "")
return ship
+7 -1
View File
@@ -9,7 +9,7 @@ from pathlib import Path
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
CONFIG_PATH = Path(__file__).parent.parent / "config" / "news_feeds.json" CONFIG_PATH = Path(__file__).parent.parent / "config" / "news_feeds.json"
MAX_FEEDS = 20 MAX_FEEDS = 25
DEFAULT_FEEDS = [ DEFAULT_FEEDS = [
{"name": "NPR", "url": "https://feeds.npr.org/1004/rss.xml", "weight": 4}, {"name": "NPR", "url": "https://feeds.npr.org/1004/rss.xml", "weight": 4},
@@ -25,6 +25,12 @@ DEFAULT_FEEDS = [
{"name": "SCMP", "url": "https://www.scmp.com/rss/91/feed", "weight": 4}, {"name": "SCMP", "url": "https://www.scmp.com/rss/91/feed", "weight": 4},
{"name": "The Diplomat", "url": "https://thediplomat.com/feed/", "weight": 4}, {"name": "The Diplomat", "url": "https://thediplomat.com/feed/", "weight": 4},
{"name": "Stars and Stripes", "url": "https://www.stripes.com/feeds/pacific.rss", "weight": 4}, {"name": "Stars and Stripes", "url": "https://www.stripes.com/feeds/pacific.rss", "weight": 4},
{"name": "Yonhap", "url": "https://en.yna.co.kr/RSS/news.xml", "weight": 4},
{"name": "Nikkei Asia", "url": "https://asia.nikkei.com/rss", "weight": 3},
{"name": "Taipei Times", "url": "https://www.taipeitimes.com/xml/pda.rss", "weight": 4},
{"name": "Asia Times", "url": "https://asiatimes.com/feed/", "weight": 3},
{"name": "Defense News", "url": "https://www.defensenews.com/arc/outboundfeeds/rss/", "weight": 3},
{"name": "Japan Times", "url": "https://www.japantimes.co.jp/feed/", "weight": 3},
] ]
+34
View File
@@ -36,6 +36,24 @@ class TestEnrichCountry:
def test_invalid_hex_with_empty(self): def test_invalid_hex_with_empty(self):
assert _enrich_country("ZZZZ", "") == ("Military Asset", "") assert _enrich_country("ZZZZ", "") == ("Military Asset", "")
def test_russia_range(self):
assert _enrich_country("150000", "Unknown") == ("Russia", "VKS")
def test_russia_range_end(self):
assert _enrich_country("157FFF", "Unknown") == ("Russia", "VKS")
def test_australia_range(self):
assert _enrich_country("7C0000", "Unknown") == ("Australia", "RAAF")
def test_philippines_range(self):
assert _enrich_country("758000", "Unknown") == ("Philippines", "PAF")
def test_singapore_range(self):
assert _enrich_country("768000", "Unknown") == ("Singapore", "RSAF")
def test_north_korea_range(self):
assert _enrich_country("720000", "Unknown") == ("North Korea", "KPAF")
class TestClassifyMilitaryType: class TestClassifyMilitaryType:
@pytest.mark.parametrize("model,expected", [ @pytest.mark.parametrize("model,expected", [
@@ -52,6 +70,22 @@ class TestClassifyMilitaryType:
("H60", "heli"), ("H60", "heli"),
("K35", "tanker"), ("K35", "tanker"),
("Boeing 737", "default"), ("Boeing 737", "default"),
# Russian aircraft
("SU-27", "fighter"),
("SU-30", "fighter"),
("SU-35", "fighter"),
("SU-57", "fighter"),
("MiG-29", "fighter"),
("MiG-31", "fighter"),
("Tu-95", "bomber"),
("Tu-160", "bomber"),
("Tu-22", "bomber"),
("IL-76", "cargo"),
("AN-124", "cargo"),
("AN-12", "cargo"),
("A-50", "recon"),
("Tu-214R", "recon"),
("IL-20", "recon"),
]) ])
def test_classification(self, model: str, expected: str): def test_classification(self, model: str, expected: str):
assert _classify_military_type(model) == expected assert _classify_military_type(model) == expected
+12 -1
View File
@@ -35,11 +35,22 @@ class TestMilitaryBasesData:
assert -180 <= entry["lng"] <= 180, f"{entry['name']} has invalid lng" assert -180 <= entry["lng"] <= 180, f"{entry['name']} has invalid lng"
def test_branch_values_are_known(self): def test_branch_values_are_known(self):
known_branches = {"air_force", "navy", "marines", "army"} known_branches = {"air_force", "navy", "marines", "army", "missile", "nuclear"}
raw = json.loads(BASES_PATH.read_text(encoding="utf-8")) raw = json.loads(BASES_PATH.read_text(encoding="utf-8"))
for entry in raw: for entry in raw:
assert entry["branch"] in known_branches, f"{entry['name']} has unknown branch: {entry['branch']}" assert entry["branch"] in known_branches, f"{entry['name']} has unknown branch: {entry['branch']}"
def test_adversary_bases_present(self):
raw = json.loads(BASES_PATH.read_text(encoding="utf-8"))
countries = {entry["country"] for entry in raw}
for expected in ("China", "Russia", "North Korea", "Taiwan"):
assert expected in countries, f"Missing bases for {expected}"
def test_no_duplicate_names(self):
raw = json.loads(BASES_PATH.read_text(encoding="utf-8"))
names = [entry["name"] for entry in raw]
assert len(names) == len(set(names)), "Duplicate base names found"
class TestFetchMilitaryBases: class TestFetchMilitaryBases:
"""Test the fetcher populates latest_data correctly.""" """Test the fetcher populates latest_data correctly."""
+49 -1
View File
@@ -72,6 +72,53 @@ class TestResolveCoords:
result = _resolve_coords("visit the uk soon") result = _resolve_coords("visit the uk soon")
assert result == (55.378, -3.435) assert result == (55.378, -3.435)
# -- New East Asia island/strait keywords ------------------------------------
def test_pratas(self):
assert _resolve_coords("china patrols near pratas islands") == (20.71, 116.72)
def test_dongsha(self):
assert _resolve_coords("dongsha atoll tensions") == (20.71, 116.72)
def test_kinmen(self):
assert _resolve_coords("artillery drill near kinmen") == (24.45, 118.38)
def test_matsu(self):
assert _resolve_coords("matsu island cable cut") == (26.16, 119.94)
def test_scarborough(self):
assert _resolve_coords("scarborough shoal standoff") == (15.14, 117.77)
def test_paracel(self):
assert _resolve_coords("paracel islands dispute") == (16.50, 112.00)
def test_spratly(self):
assert _resolve_coords("spratly island reclamation") == (10.00, 114.00)
def test_miyako_strait(self):
assert _resolve_coords("PLAN warships transit miyako strait") == (24.78, 125.30)
def test_bashi_channel(self):
assert _resolve_coords("submarine detected in bashi channel") == (21.00, 121.50)
def test_luzon_strait(self):
assert _resolve_coords("luzon strait patrol") == (20.50, 121.50)
def test_dmz(self):
assert _resolve_coords("tension at the dmz border") == (38.00, 127.00)
def test_yalu(self):
assert _resolve_coords("troops near yalu river") == (40.00, 124.40)
def test_yongbyon(self):
assert _resolve_coords("activity at yongbyon reactor") == (39.80, 125.76)
def test_wonsan(self):
assert _resolve_coords("missile launch from wonsan") == (39.18, 127.48)
def test_busan(self):
assert _resolve_coords("naval exercise near busan port") == (35.18, 129.07)
# -- No match -------------------------------------------------------------- # -- No match --------------------------------------------------------------
def test_no_match_returns_none(self): def test_no_match_returns_none(self):
@@ -98,5 +145,6 @@ class TestFeedConfig:
def test_new_east_asia_feeds_present(self): def test_new_east_asia_feeds_present(self):
names = {f["name"] for f in DEFAULT_FEEDS} names = {f["name"] for f in DEFAULT_FEEDS}
expected = {"FocusTaiwan", "Kyodo", "SCMP", "The Diplomat", "Stars and Stripes"} expected = {"FocusTaiwan", "Kyodo", "SCMP", "The Diplomat", "Stars and Stripes",
"Yonhap", "Nikkei Asia", "Taipei Times", "Asia Times", "Defense News", "Japan Times"}
assert expected.issubset(names) assert expected.issubset(names)
+14 -6
View File
@@ -1475,11 +1475,11 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
id="military-bases-layer" id="military-bases-layer"
type="circle" type="circle"
paint={{ paint={{
'circle-color': '#ef4444', 'circle-color': ['match', ['get', 'side'], 'red', '#ef4444', 'green', '#22c55e', '#3b82f6'],
'circle-radius': ['interpolate', ['linear'], ['zoom'], 2, 4, 6, 7, 10, 10], 'circle-radius': ['interpolate', ['linear'], ['zoom'], 2, 4, 6, 7, 10, 10],
'circle-opacity': 0.8, 'circle-opacity': 0.8,
'circle-stroke-width': 2, 'circle-stroke-width': 2,
'circle-stroke-color': '#fca5a5', 'circle-stroke-color': ['match', ['get', 'side'], 'red', '#fca5a5', 'green', '#86efac', '#93c5fd'],
}} }}
/> />
<Layer <Layer
@@ -1494,7 +1494,7 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
'text-allow-overlap': false, 'text-allow-overlap': false,
}} }}
paint={{ paint={{
'text-color': '#fca5a5', 'text-color': ['match', ['get', 'side'], 'red', '#fca5a5', 'green', '#86efac', '#93c5fd'],
'text-halo-color': 'rgba(0,0,0,0.9)', 'text-halo-color': 'rgba(0,0,0,0.9)',
'text-halo-width': 1, 'text-halo-width': 1,
}} }}
@@ -1890,7 +1890,15 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
if (!base) return null; if (!base) return null;
const branchLabel: Record<string, string> = { const branchLabel: Record<string, string> = {
air_force: 'AIR FORCE', navy: 'NAVY', marines: 'MARINES', army: 'ARMY', air_force: 'AIR FORCE', navy: 'NAVY', marines: 'MARINES', army: 'ARMY',
missile: 'MISSILE FORCES', nuclear: 'NUCLEAR FACILITY',
}; };
const isAdversary = ['China', 'Russia', 'North Korea'].includes(base.country);
const isROC = base.country === 'Taiwan';
const accentColor = isAdversary ? 'red' : isROC ? 'green' : 'blue';
const borderCls = isAdversary ? 'border-red-400/40' : isROC ? 'border-green-400/40' : 'border-blue-400/40';
const textCls = isAdversary ? 'text-[#fca5a5]' : isROC ? 'text-[#86efac]' : 'text-[#93c5fd]';
const titleCls = isAdversary ? 'text-red-400 border-b border-red-400/20' : isROC ? 'text-green-400 border-b border-green-400/20' : 'text-blue-400 border-b border-blue-400/20';
const footerCls = isAdversary ? 'text-red-600' : isROC ? 'text-green-600' : 'text-blue-600';
return ( return (
<Popup <Popup
longitude={base.lng} longitude={base.lng}
@@ -1901,8 +1909,8 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
className="threat-popup" className="threat-popup"
maxWidth="280px" maxWidth="280px"
> >
<div className="map-popup bg-[#1a1035] border border-red-400/40 text-[#fca5a5] min-w-[200px]"> <div className={`map-popup bg-[#1a1035] border ${borderCls} ${textCls} min-w-[200px]`}>
<div className="map-popup-title text-red-400 border-b border-red-400/20 pb-1"> <div className={`map-popup-title ${titleCls} pb-1`}>
{base.name} {base.name}
</div> </div>
<div className="map-popup-row"> <div className="map-popup-row">
@@ -1911,7 +1919,7 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
<div className="map-popup-row"> <div className="map-popup-row">
Location: <span className="text-white">{base.country}</span> Location: <span className="text-white">{base.country}</span>
</div> </div>
<div className="mt-1.5 text-[9px] text-red-600 tracking-wider"> <div className={`mt-1.5 text-[9px] ${footerCls} tracking-wider`}>
MILITARY BASE {branchLabel[base.branch] || base.branch.toUpperCase()} MILITARY BASE {branchLabel[base.branch] || base.branch.toUpperCase()}
</div> </div>
</div> </div>
@@ -197,6 +197,16 @@ export function buildDataCentersGeoJSON(datacenters?: DataCenter[]): FC {
// ─── Military Bases ───────────────────────────────────────────────────────── // ─── Military Bases ─────────────────────────────────────────────────────────
// Classify base alignment: red = adversary, blue = US/allied, green = ROC
const _ADVERSARY_COUNTRIES = new Set(["China", "Russia", "North Korea"]);
const _ROC_COUNTRIES = new Set(["Taiwan"]);
function _baseSide(country: string, operator: string): "red" | "blue" | "green" {
if (_ADVERSARY_COUNTRIES.has(country)) return "red";
if (_ROC_COUNTRIES.has(country)) return "green";
return "blue";
}
export function buildMilitaryBasesGeoJSON(bases?: MilitaryBase[]): FC { export function buildMilitaryBasesGeoJSON(bases?: MilitaryBase[]): FC {
if (!bases?.length) return null; if (!bases?.length) return null;
return { return {
@@ -210,6 +220,7 @@ export function buildMilitaryBasesGeoJSON(bases?: MilitaryBase[]): FC {
country: base.country || '', country: base.country || '',
operator: base.operator || '', operator: base.operator || '',
branch: base.branch || '', branch: base.branch || '',
side: _baseSide(base.country || '', base.operator || ''),
}, },
geometry: { type: 'Point' as const, coordinates: [base.lng, base.lat] } geometry: { type: 'Point' as const, coordinates: [base.lng, base.lat] }
})) }))
+7 -1
View File
@@ -43,7 +43,7 @@ export interface PrivateJet extends FlightBase {
export interface MilitaryFlight extends FlightBase { export interface MilitaryFlight extends FlightBase {
type: "military_flight"; type: "military_flight";
military_type?: "heli" | "fighter" | "tanker" | "cargo" | "recon" | "default"; military_type?: "heli" | "fighter" | "bomber" | "tanker" | "cargo" | "recon" | "default";
force?: string; force?: string;
} }
@@ -106,6 +106,12 @@ export interface Ship {
yacht_length?: number; yacht_length?: number;
yacht_year?: number; yacht_year?: number;
yacht_link?: string; yacht_link?: string;
// PLAN/CCG vessel enrichment
plan_name?: string;
plan_class?: string;
plan_force?: string;
plan_hull?: string;
plan_wiki?: string;
// Carrier enrichment // Carrier enrichment
wiki?: string; wiki?: string;
homeport?: string; homeport?: string;