diff --git a/backend/config/news_feeds.json b/backend/config/news_feeds.json index 4c3a906..06d689a 100644 --- a/backend/config/news_feeds.json +++ b/backend/config/news_feeds.json @@ -64,6 +64,36 @@ "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 } ] } \ No newline at end of file diff --git a/backend/data/military_bases.json b/backend/data/military_bases.json index d4ea047..1e99f76 100644 --- a/backend/data/military_bases.json +++ b/backend/data/military_bases.json @@ -142,5 +142,587 @@ "branch": "navy", "lat": -7.316, "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 } ] diff --git a/backend/data/plan_ccg_vessels.json b/backend/data/plan_ccg_vessels.json new file mode 100644 index 0000000..4271131 --- /dev/null +++ b/backend/data/plan_ccg_vessels.json @@ -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" + } +} diff --git a/backend/services/fetchers/geo.py b/backend/services/fetchers/geo.py index 187a942..cd044ba 100644 --- a/backend/services/fetchers/geo.py +++ b/backend/services/fetchers/geo.py @@ -39,6 +39,11 @@ def fetch_ships(): for ship in ships: 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") with _data_lock: latest_data['ships'] = ships diff --git a/backend/services/fetchers/military.py b/backend/services/fetchers/military.py index 374bf8d..73fd1df 100644 --- a/backend/services/fetchers/military.py +++ b/backend/services/fetchers/military.py @@ -38,6 +38,11 @@ _ICAO_COUNTRY_RANGES = [ (0x840000, 0x87FFFF, "Japan", "JSDF"), (0x700000, 0x71FFFF, "South Korea", "ROK"), (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 [ "F16", "F35", "F22", "F15", "F18", "T38", "T6", "A10", "J10", "J11", "J15", "J16", "J20", "JF17", - "SU27", "SU30", "SU35", + "SU27", "SU30", "SU35", "SU57", "MIG29", "MIG31", "F15J", "F2", "IDF", "FA50", "KF21", ]): return "fighter" + if any(k in model for k in [ + "TU95", "TU160", "TU22", + ]): + return "bomber" if any(k in model for k in [ "C17", "C5", "C130", "C30", "A400", "V22", "Y20", "Y9", "Y8", "C2", + "IL76", "AN124", "AN12", ]): return "cargo" if any(k in model for k in [ "P8", "E3", "E8", "U2", "KJ500", "KJ200", "GX11", "P1", "E767", "E2K", "E2C", + "A50", "TU214R", "IL20", ]): return "recon" return "default" diff --git a/backend/services/fetchers/news.py b/backend/services/fetchers/news.py index aeb7270..c8256ac 100644 --- a/backend/services/fetchers/news.py +++ b/backend/services/fetchers/news.py @@ -111,6 +111,22 @@ _KEYWORD_COORDS = { "singapore": (1.352, 103.819), "bangkok": (13.756, 100.501), "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 diff --git a/backend/services/fetchers/plan_vessel_alert.py b/backend/services/fetchers/plan_vessel_alert.py new file mode 100644 index 0000000..28bd77b --- /dev/null +++ b/backend/services/fetchers/plan_vessel_alert.py @@ -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 diff --git a/backend/services/news_feed_config.py b/backend/services/news_feed_config.py index 586ed4d..6522eb4 100644 --- a/backend/services/news_feed_config.py +++ b/backend/services/news_feed_config.py @@ -9,7 +9,7 @@ from pathlib import Path logger = logging.getLogger(__name__) CONFIG_PATH = Path(__file__).parent.parent / "config" / "news_feeds.json" -MAX_FEEDS = 20 +MAX_FEEDS = 25 DEFAULT_FEEDS = [ {"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": "The Diplomat", "url": "https://thediplomat.com/feed/", "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}, ] diff --git a/backend/tests/test_icao_military.py b/backend/tests/test_icao_military.py index 829b7fe..35af372 100644 --- a/backend/tests/test_icao_military.py +++ b/backend/tests/test_icao_military.py @@ -36,6 +36,24 @@ class TestEnrichCountry: def test_invalid_hex_with_empty(self): 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: @pytest.mark.parametrize("model,expected", [ @@ -52,6 +70,22 @@ class TestClassifyMilitaryType: ("H60", "heli"), ("K35", "tanker"), ("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): assert _classify_military_type(model) == expected diff --git a/backend/tests/test_military_bases.py b/backend/tests/test_military_bases.py index eecd967..3686e8d 100644 --- a/backend/tests/test_military_bases.py +++ b/backend/tests/test_military_bases.py @@ -35,11 +35,22 @@ class TestMilitaryBasesData: assert -180 <= entry["lng"] <= 180, f"{entry['name']} has invalid lng" 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")) for entry in raw: 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: """Test the fetcher populates latest_data correctly.""" diff --git a/backend/tests/test_news_keywords.py b/backend/tests/test_news_keywords.py index bb19823..f53a495 100644 --- a/backend/tests/test_news_keywords.py +++ b/backend/tests/test_news_keywords.py @@ -72,6 +72,53 @@ class TestResolveCoords: result = _resolve_coords("visit the uk soon") 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 -------------------------------------------------------------- def test_no_match_returns_none(self): @@ -98,5 +145,6 @@ class TestFeedConfig: def test_new_east_asia_feeds_present(self): 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) diff --git a/frontend/src/components/MaplibreViewer.tsx b/frontend/src/components/MaplibreViewer.tsx index ca65d6b..264aa14 100644 --- a/frontend/src/components/MaplibreViewer.tsx +++ b/frontend/src/components/MaplibreViewer.tsx @@ -1475,11 +1475,11 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele id="military-bases-layer" type="circle" 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-opacity': 0.8, 'circle-stroke-width': 2, - 'circle-stroke-color': '#fca5a5', + 'circle-stroke-color': ['match', ['get', 'side'], 'red', '#fca5a5', 'green', '#86efac', '#93c5fd'], }} /> = { 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 ( -
-
+
+
{base.name}
@@ -1911,7 +1919,7 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
Location: {base.country}
-
+
MILITARY BASE — {branchLabel[base.branch] || base.branch.toUpperCase()}
diff --git a/frontend/src/components/map/geoJSONBuilders.ts b/frontend/src/components/map/geoJSONBuilders.ts index 0e25a1e..a82823e 100644 --- a/frontend/src/components/map/geoJSONBuilders.ts +++ b/frontend/src/components/map/geoJSONBuilders.ts @@ -197,6 +197,16 @@ export function buildDataCentersGeoJSON(datacenters?: DataCenter[]): FC { // ─── 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 { if (!bases?.length) return null; return { @@ -210,6 +220,7 @@ export function buildMilitaryBasesGeoJSON(bases?: MilitaryBase[]): FC { country: base.country || '', operator: base.operator || '', branch: base.branch || '', + side: _baseSide(base.country || '', base.operator || ''), }, geometry: { type: 'Point' as const, coordinates: [base.lng, base.lat] } })) diff --git a/frontend/src/types/dashboard.ts b/frontend/src/types/dashboard.ts index 6dae3df..70fd410 100644 --- a/frontend/src/types/dashboard.ts +++ b/frontend/src/types/dashboard.ts @@ -43,7 +43,7 @@ export interface PrivateJet extends FlightBase { export interface MilitaryFlight extends FlightBase { type: "military_flight"; - military_type?: "heli" | "fighter" | "tanker" | "cargo" | "recon" | "default"; + military_type?: "heli" | "fighter" | "bomber" | "tanker" | "cargo" | "recon" | "default"; force?: string; } @@ -106,6 +106,12 @@ export interface Ship { yacht_length?: number; yacht_year?: number; yacht_link?: string; + // PLAN/CCG vessel enrichment + plan_name?: string; + plan_class?: string; + plan_force?: string; + plan_hull?: string; + plan_wiki?: string; // Carrier enrichment wiki?: string; homeport?: string;