mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
Add comprehensive search across all data sources
- Extended search modal to cover pipelines, cables, datacenters, nuclear facilities, irradiators - Added trigger methods to MapComponent for all searchable entity types - Auto-enables relevant map layer when selecting search results - Added ⌘K search button to header with styling - Expanded pipeline data to 88 real operating pipelines globally - Fixed type errors and simplified pipeline status handling
This commit is contained in:
110
data/gamma-irradiators-raw.json
Normal file
110
data/gamma-irradiators-raw.json
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"source": "IAEA DIIF - Tableau Public Visualization",
|
||||
"url": "https://public.tableau.com/app/profile/acceleratorknowledgeportal/viz/IrradiatorDatabase/Home",
|
||||
"extracted": "2026-01-09",
|
||||
"note": "Raw data extracted from Tableau dataDictionary. Coordinates are in realValues array (first ~136 are latitudes, rest are longitudes). Cities and organizations are in stringValues.",
|
||||
"realValues": [
|
||||
18.420295, 42.311091, 41.323774, 40.825619, 40.579733, 40.895771, 39.569018, 35.975972,
|
||||
36.088692, 43.579934, 35.202512, 34.940428, 27.898651, 39.852653, 40.115286, 42.376292,
|
||||
42.274642, 42.053589, 35.140839, 14.836864, 44.950269, 32.720166, 32.766098, 19.901705,
|
||||
19.286008, 23.645547, 31.794133, 31.867886, 40.54449, 33.468294, 32.751164, 33.870026,
|
||||
33.735654, 37.010533, 37.659417, 21.462766, -23.542047, -23.580141, -23.598327, -34.636145,
|
||||
-16.546576, 4.632938, 9.071441, 9.07144, 23.120885, 23.009447, 55.717376, 55.113284,
|
||||
53.892511, 53.934775, 44.374433, 59.355411, 42.685821, 48.7612, 44.758887, 47.489017,
|
||||
47.95946, 49.274109, 49.288579, 45.794472, 51.109509, 55.643201, 48.42663, 44.616154,
|
||||
48.806391, 45.697924, 47.365354, 50.95633, 51.364552, 52.039888, 43.323344, 45.855206,
|
||||
44.148579, 51.573407, 50.483418, 47.837859, 46.782139, 51.453028, 52.27069, 53.266866,
|
||||
53.38034, 51.575779, 53.982301, 50.372238, 38.804288, 53.797423, 36.181626, 35.512533,
|
||||
14.660576, 31.275518, -6.230534, -6.230535, -6.230536, 2.911703, 3.305309, 12.974634,
|
||||
14.119728, 13.429678, 5.600087, 16.799819, 22.68131, 26.557825, 6.937849, 17.538289,
|
||||
12.989133, 12.929869, 28.688513, 28.194645, 28.20956, 22.93384, 19.395687, 20.14863,
|
||||
31.440264, 18.105503, 19.185071, 22.305572, 22.311289, 19.032567, 19.042604, 19.085442,
|
||||
19.364242, 22.835401, 22.97761, 41.331135, 35.7474416756762, 32.3249807945074, 37.3339678467846,
|
||||
32.024605, 31.879207, 40.062811, 41.310999, -26.124184, -33.863096, 36.925775, 9.04200378229489,
|
||||
5.677879, -66.334995, -71.649221, -74.289753, -74.40563, -74.424443, -74.522053, -75.465052,
|
||||
-78.884913, -79.375943, -79.628163, -80.794517, -81.939245, -81.971679, -82.888091, -82.918498,
|
||||
-87.895077, -87.960518, -88.048792, -90.187236, -92.195166, -93.246374, -97.020252, -97.324345,
|
||||
-99.341524, -99.644432, -100.653481, -106.387446, -106.572068, -111.833074, -117.105422,
|
||||
-117.200996, -117.56985, -117.823925, -121.588342, -122.090256, -158.057863, -46.58432,
|
||||
-46.656614, -46.916378, -58.472782, -68.207797, -74.075412, -79.300808, -79.300809,
|
||||
-82.423354, -82.490902, 37.689322, 36.593435, 27.563155, 27.561781, 26.050775, 24.591014,
|
||||
23.294419, 21.898753, 20.598464, 19.14197, 16.516047, 16.43573, 16.223873, 16.017888,
|
||||
13.917448, 12.069288, 11.598988, 11.470066, 9.3215, 9.027723, 7.967939, 7.537838,
|
||||
6.176397, 5.666329, 5.395258, 5.075505, 4.679974, 4.626725, 4.540843, -0.344394,
|
||||
-0.837019, -1.013584, -1.182336, -1.322859, -1.470618, -1.766416, -2.094284, -4.148963,
|
||||
-9.098683, -9.530576, 140.48205, 126.833682, 121.056131, 120.766199, 106.821844, 106.821843,
|
||||
106.821842, 101.771493, 101.558102, 101.213625, 101.025648, 101.018411, 100.642381, 96.161503,
|
||||
88.295287, 80.528149, 79.878705, 78.174563, 77.92006, 77.586986, 77.2119, 76.863864,
|
||||
76.792833, 75.99489, 74.647881, 74.231994, 74.19313, 73.993527, 73.191977, 73.157635,
|
||||
73.157292, 73.012903, 72.914112, 72.852339, 72.817704, 72.368717, 72.276256, 69.334937,
|
||||
51.3876352553795, 51.0407491436916, 46.0621245938148, 35.876798, 34.736145, 32.609717,
|
||||
27.987015, 28.216191, 18.525036, 10.046566, 7.52063395497204, -0.220542
|
||||
],
|
||||
"cities": [
|
||||
"Vega Alta", "Northborogh, MA", "Chester, NY", "Whippany, NJ", "South Plainfield, NJ",
|
||||
"Rockaway, NJ", "Salem, NJ", "Durham, NC", "Haw River NC", "Mississauga, ON",
|
||||
"Charlotte, NC", "Spartanburg, SC", "Mulberry, FL", "Groveport, OH", "Westerville, OH",
|
||||
"Gurnee, IL", "Libertyville, IL", "Schaumburg, IL", "Memphis, AR", "Metapa de Dominguez",
|
||||
"Minneapolis, MN", "Grand Prarie, TX", "Fort Worth, TX", "Tepeji del Rio", "Toluca",
|
||||
"Matehuala", "El Paso, TX", "East Sandy, UT", "Temescula, CA", "San Diego, CA",
|
||||
"Corona, CA", "Tustin, CA", "Gilroy, CA", "Hayward, CA", "Kunia Camp, HI",
|
||||
"Sao Paulo", "Cotia", "Buenos Aires", "La Paz", "Bogota", "Pacora", "La Habana",
|
||||
"Moscow", "Obninsk", "Minsk", "Magurele", "Alliku", "Sofia", "Michalovce", "Belgrade",
|
||||
"Budapest", "Seibersdorf", "VEVERSKA BITYSKA", "Velká Bíteš", "Zagreb", "Radeberg",
|
||||
"Roskilde", "Allershausen", "Minerbio", "Baden Württemberg", "Lomazzo", "Däniken",
|
||||
"Wiehl", "Venlo", "Ede", "Marseille", "DAGNEUX", "CHUSCLAN", "Etten Leur", "Fleurus",
|
||||
"SABLE-SUR-SARTHE", "POUZAUGES", "Tilehurst", "Northants", "CHESTERFIELD", "Sheffield",
|
||||
"Swindon", "Plymouth", "Bobadela", "Wesport", "IBARAKI", "Jeollabuk-do", "Quezon City",
|
||||
"Jiangsu Province", "Jakarta", "Selangor", "Rayong Province", "Ongkharak", "CHONBURI",
|
||||
"Kedah", "Yangon", "Kolkata", "Unnao", "Malwana", "Telangana", "Malur", "Bangalore",
|
||||
"Delhi", "Bhiwadi", "Dharuhera", "Dewas", "Rahuri", "Nashik", "Lahore", "Satara",
|
||||
"Thani", "Vadodara", "Mumbai", "Thane", "Admedabad", "Ahmedabad", "Tashkent", "Tehran",
|
||||
"Isfahan", "Bonab", "Amman", "Yavne", "Ankara", "Çerkezköy", "Kempton Park", "Cape Town",
|
||||
"Sidi Thabet", "Abuja", "Accra"
|
||||
],
|
||||
"countries": [
|
||||
"Puerto Rico", "USA", "Canada", "Mexico", "Brazil", "Argentina", "Bolivia", "Colombia",
|
||||
"Panama", "Cuba", "Russia", "Belarus", "Romania", "Estonia", "Bulgaria", "Slovakia",
|
||||
"Serbia", "Hungary", "Austria", "Czech Republic", "Croatia", "Germany", "Denmark",
|
||||
"Italy", "Switzerland", "Netherlands", "France", "Belgium", "UK", "Portugal", "Ireland",
|
||||
"Japan", "South Korea", "Philipinnes", "China", "Indonesia", "Malaysia", "Thailand",
|
||||
"Myanmar", "India", "Sri Lanka", "Pakistan", "Uzbekistan", "Iran", "Jordan", "Israel",
|
||||
"Turkey", "South Africa", "Tunisia", "Nigeria", "Ghana"
|
||||
],
|
||||
"organizations": [
|
||||
"Steris", "Isomedix Operations, Inc.", "Sterigenics", "Corning Incorporated",
|
||||
"SADER-SENASICA PROGRAMA MOSCAMED-MOSCAFRUT MEXICO", "National Institute for Nuclear Research (ININ)",
|
||||
"Benebion", "Pa'ina Hawaii", "Sterigenics (Sotera Health company",
|
||||
"National Energy Nuclear Commission - IPEN-CNEN/SP", "Agencia Boliviana de Energía Nuclear",
|
||||
"Servicio Geológico Colombiano", "COPEG", "Centro de Aplicaciones Tecnológicas y Desarrollo Nuclear",
|
||||
"Instituto Investigaciones para la Industria Alimentaria (IIIA)",
|
||||
"Institute of Problems of Chemical Physics of Russian Academy of Sciences (IPCP RAS)",
|
||||
"Russian Institute of Radiology and Agroecology",
|
||||
"State Scientific Institution Joint Institute for Power and nuclear Research–Sosny",
|
||||
"Horia Hulubei National Institute for R&D in Physics and Nuclear Engineering (IFIN-HH)",
|
||||
"IONISOS BALTICS", "SOPHARMA JSC", "STERIS AST SK s.r.o.", "VINCA Institute of Nuclear Sciences",
|
||||
"Mediscan GmbH & CoKG", "Bioster", "Ruđer Bošković Institute", "DTU Nutech",
|
||||
"BBF Sterilisationsservice GmbH", "Gammatom S.r.l.", "Synergy Health Däniken AG",
|
||||
"Beta-Gamma-Service GmbH & Co KG", "Synergy Health Ede B.V.", "Synergy Health Marseille SAS",
|
||||
"IONISOS", "Synergy Health Sterilisation UK Limited", "Swann-Morton (Services) Ltd",
|
||||
"Instituto Superior Técnico", "Korea Atomic Energy Research Institute",
|
||||
"Philippine Nuclear Research Institute (PNRI)", "Synergy Health (Suzhou) Ltd",
|
||||
"National Nuclear Energy Agency of Indonesia (BATAN)", "Malaysian Nuclear Agency",
|
||||
"Synergy Sterilisation (M) Sdn Bhd", "Thailand Institute of Nuclear Technology",
|
||||
"Synergy Health (Thailand) Ltd", "Division of Atomic Energy", "VIKIRIN, Organic Green Foods Ltd.",
|
||||
"Impartial Agrotech Pvt. Ltd", "Sri Lanka Atomic Energy Board", "Gamma Agro Medical Processing",
|
||||
"Innova Agro-Bio Park Ltd", "Microtrol Sterilization Services Pvt. Ltd",
|
||||
"Shriram Institute for Industrial Research", "Jhunson Chemical Pvt. Ltd", "Aligned Industries",
|
||||
"Hindustan Agro Co-operative ltd", "Krushak Irradiator", "Pakistan Radiation Services (PARAS)",
|
||||
"Nipro India Corporation", "A.V. Processors Pvt. Ltd.", "Universal Medicap Ltd",
|
||||
"Electromagnetic Industries", "Maharashtra State Agricultural Marketing Board",
|
||||
"Radiation Sterilization Plant, B.A.R.C., Mumbai", "Agrosurg Irradiators(India) Pvt. Ltd",
|
||||
"Gujarat Agro Industries Corporation Ltd", "Pinnacle Therapeutics Pvt Ltd.",
|
||||
"Institute of Nuclear Physics AS RUz, Uzbekistan",
|
||||
"Iran Radiation Application Development Company (IRAD)", "Shar Parto Iranian",
|
||||
"JORDAN ATOMIC ENERGY COMMISSION (JAEC)", "SorVan Radiation Ltd",
|
||||
"TURKISH ATOMIC ENERGY AUTHORITY", "GAMMA-PAK STERILIZATION IND. & TRD. INC.",
|
||||
"Synergy Sterilisation South Africa", "High Energy Processing Cape (Pty) Ltd",
|
||||
"CNSTN", "NAEC", "GHANA ATOMIC ENERGY COMMISSION"
|
||||
]
|
||||
}
|
||||
145
data/gamma-irradiators.json
Normal file
145
data/gamma-irradiators.json
Normal file
@@ -0,0 +1,145 @@
|
||||
{
|
||||
"source": "IAEA DIIF - Database on Industrial Irradiation Facilities",
|
||||
"tableauUrl": "https://public.tableau.com/app/profile/acceleratorknowledgeportal/viz/IrradiatorDatabase/Home",
|
||||
"extracted": "2026-01-09",
|
||||
"totalFacilities": 136,
|
||||
"note": "Gamma irradiator facilities worldwide. Coordinates extracted from Tableau Public visualization.",
|
||||
"facilities": [
|
||||
{ "id": "gi-001", "city": "Vega Alta", "country": "Puerto Rico", "lat": 18.420295, "lon": -66.334995 },
|
||||
{ "id": "gi-002", "city": "Northborough, MA", "country": "USA", "lat": 42.311091, "lon": -71.649221 },
|
||||
{ "id": "gi-003", "city": "Chester, NY", "country": "USA", "lat": 41.323774, "lon": -74.289753 },
|
||||
{ "id": "gi-004", "city": "Whippany, NJ", "country": "USA", "lat": 40.825619, "lon": -74.40563 },
|
||||
{ "id": "gi-005", "city": "South Plainfield, NJ", "country": "USA", "lat": 40.579733, "lon": -74.424443 },
|
||||
{ "id": "gi-006", "city": "Rockaway, NJ", "country": "USA", "lat": 40.895771, "lon": -74.522053 },
|
||||
{ "id": "gi-007", "city": "Salem, NJ", "country": "USA", "lat": 39.569018, "lon": -75.465052 },
|
||||
{ "id": "gi-008", "city": "Durham, NC", "country": "USA", "lat": 35.975972, "lon": -78.884913 },
|
||||
{ "id": "gi-009", "city": "Haw River, NC", "country": "USA", "lat": 36.088692, "lon": -79.375943 },
|
||||
{ "id": "gi-010", "city": "Mississauga, ON", "country": "Canada", "lat": 43.579934, "lon": -79.628163 },
|
||||
{ "id": "gi-011", "city": "Charlotte, NC", "country": "USA", "lat": 35.202512, "lon": -80.794517 },
|
||||
{ "id": "gi-012", "city": "Spartanburg, SC", "country": "USA", "lat": 34.940428, "lon": -81.939245 },
|
||||
{ "id": "gi-013", "city": "Mulberry, FL", "country": "USA", "lat": 27.898651, "lon": -81.971679 },
|
||||
{ "id": "gi-014", "city": "Groveport, OH", "country": "USA", "lat": 39.852653, "lon": -82.888091 },
|
||||
{ "id": "gi-015", "city": "Westerville, OH", "country": "USA", "lat": 40.115286, "lon": -82.918498 },
|
||||
{ "id": "gi-016", "city": "Gurnee, IL", "country": "USA", "lat": 42.376292, "lon": -87.895077 },
|
||||
{ "id": "gi-017", "city": "Libertyville, IL", "country": "USA", "lat": 42.274642, "lon": -87.960518 },
|
||||
{ "id": "gi-018", "city": "Schaumburg, IL", "country": "USA", "lat": 42.053589, "lon": -88.048792 },
|
||||
{ "id": "gi-019", "city": "Memphis, TN", "country": "USA", "lat": 35.140839, "lon": -90.187236 },
|
||||
{ "id": "gi-020", "city": "Metapa de Dominguez", "country": "Mexico", "lat": 14.836864, "lon": -92.195166 },
|
||||
{ "id": "gi-021", "city": "Minneapolis, MN", "country": "USA", "lat": 44.950269, "lon": -93.246374 },
|
||||
{ "id": "gi-022", "city": "Grand Prairie, TX", "country": "USA", "lat": 32.720166, "lon": -97.020252 },
|
||||
{ "id": "gi-023", "city": "Fort Worth, TX", "country": "USA", "lat": 32.766098, "lon": -97.324345 },
|
||||
{ "id": "gi-024", "city": "Tepeji del Rio", "country": "Mexico", "lat": 19.901705, "lon": -99.341524 },
|
||||
{ "id": "gi-025", "city": "Toluca", "country": "Mexico", "lat": 19.286008, "lon": -99.644432 },
|
||||
{ "id": "gi-026", "city": "Matehuala", "country": "Mexico", "lat": 23.645547, "lon": -100.653481 },
|
||||
{ "id": "gi-027", "city": "El Paso, TX", "country": "USA", "lat": 31.794133, "lon": -106.387446 },
|
||||
{ "id": "gi-028", "city": "Sandy, UT", "country": "USA", "lat": 31.867886, "lon": -106.572068 },
|
||||
{ "id": "gi-029", "city": "Salt Lake Area, UT", "country": "USA", "lat": 40.54449, "lon": -111.833074 },
|
||||
{ "id": "gi-030", "city": "Temecula, CA", "country": "USA", "lat": 33.468294, "lon": -117.105422 },
|
||||
{ "id": "gi-031", "city": "San Diego, CA", "country": "USA", "lat": 32.751164, "lon": -117.200996 },
|
||||
{ "id": "gi-032", "city": "Corona, CA", "country": "USA", "lat": 33.870026, "lon": -117.56985 },
|
||||
{ "id": "gi-033", "city": "Tustin, CA", "country": "USA", "lat": 33.735654, "lon": -117.823925 },
|
||||
{ "id": "gi-034", "city": "Gilroy, CA", "country": "USA", "lat": 37.010533, "lon": -121.588342 },
|
||||
{ "id": "gi-035", "city": "Hayward, CA", "country": "USA", "lat": 37.659417, "lon": -122.090256 },
|
||||
{ "id": "gi-036", "city": "Kunia Camp, HI", "country": "USA", "lat": 21.462766, "lon": -158.057863 },
|
||||
{ "id": "gi-037", "city": "Sao Paulo", "country": "Brazil", "lat": -23.542047, "lon": -46.58432 },
|
||||
{ "id": "gi-038", "city": "Cotia", "country": "Brazil", "lat": -23.580141, "lon": -46.656614 },
|
||||
{ "id": "gi-039", "city": "Sao Paulo Region", "country": "Brazil", "lat": -23.598327, "lon": -46.916378 },
|
||||
{ "id": "gi-040", "city": "Buenos Aires", "country": "Argentina", "lat": -34.636145, "lon": -58.472782 },
|
||||
{ "id": "gi-041", "city": "La Paz", "country": "Bolivia", "lat": -16.546576, "lon": -68.207797 },
|
||||
{ "id": "gi-042", "city": "Bogota", "country": "Colombia", "lat": 4.632938, "lon": -74.075412 },
|
||||
{ "id": "gi-043", "city": "Panama City", "country": "Panama", "lat": 9.071441, "lon": -79.300808 },
|
||||
{ "id": "gi-044", "city": "Panama", "country": "Panama", "lat": 9.07144, "lon": -79.300809 },
|
||||
{ "id": "gi-045", "city": "Havana", "country": "Cuba", "lat": 23.120885, "lon": -82.423354 },
|
||||
{ "id": "gi-046", "city": "Havana", "country": "Cuba", "lat": 23.009447, "lon": -82.490902 },
|
||||
{ "id": "gi-047", "city": "Moscow", "country": "Russia", "lat": 55.717376, "lon": 37.689322 },
|
||||
{ "id": "gi-048", "city": "Obninsk", "country": "Russia", "lat": 55.113284, "lon": 36.593435 },
|
||||
{ "id": "gi-049", "city": "Minsk", "country": "Belarus", "lat": 53.892511, "lon": 27.563155 },
|
||||
{ "id": "gi-050", "city": "Minsk Region", "country": "Belarus", "lat": 53.934775, "lon": 27.561781 },
|
||||
{ "id": "gi-051", "city": "Magurele", "country": "Romania", "lat": 44.374433, "lon": 26.050775 },
|
||||
{ "id": "gi-052", "city": "Alliku", "country": "Estonia", "lat": 59.355411, "lon": 24.591014 },
|
||||
{ "id": "gi-053", "city": "Sofia", "country": "Bulgaria", "lat": 42.685821, "lon": 23.294419 },
|
||||
{ "id": "gi-054", "city": "Michalovce", "country": "Slovakia", "lat": 48.7612, "lon": 21.898753 },
|
||||
{ "id": "gi-055", "city": "Velká Bíteš", "country": "Czech Republic", "lat": 49.288579, "lon": 16.223873 },
|
||||
{ "id": "gi-056", "city": "Belgrade", "country": "Serbia", "lat": 44.758887, "lon": 20.598464 },
|
||||
{ "id": "gi-057", "city": "Budapest", "country": "Hungary", "lat": 47.489017, "lon": 19.14197 },
|
||||
{ "id": "gi-058", "city": "Seibersdorf", "country": "Austria", "lat": 47.95946, "lon": 16.516047 },
|
||||
{ "id": "gi-059", "city": "Veverská Bítýška", "country": "Czech Republic", "lat": 49.274109, "lon": 16.43573 },
|
||||
{ "id": "gi-060", "city": "Zagreb", "country": "Croatia", "lat": 45.794472, "lon": 16.017888 },
|
||||
{ "id": "gi-061", "city": "Radeberg", "country": "Germany", "lat": 51.109509, "lon": 13.917448 },
|
||||
{ "id": "gi-062", "city": "Roskilde", "country": "Denmark", "lat": 55.643201, "lon": 12.069288 },
|
||||
{ "id": "gi-063", "city": "Allershausen", "country": "Germany", "lat": 48.42663, "lon": 11.598988 },
|
||||
{ "id": "gi-064", "city": "Minerbio", "country": "Italy", "lat": 44.616154, "lon": 11.470066 },
|
||||
{ "id": "gi-065", "city": "Baden-Württemberg", "country": "Germany", "lat": 48.806391, "lon": 9.3215 },
|
||||
{ "id": "gi-066", "city": "Lomazzo", "country": "Italy", "lat": 45.697924, "lon": 9.027723 },
|
||||
{ "id": "gi-067", "city": "Däniken", "country": "Switzerland", "lat": 47.365354, "lon": 7.967939 },
|
||||
{ "id": "gi-068", "city": "Wiehl", "country": "Germany", "lat": 50.95633, "lon": 7.537838 },
|
||||
{ "id": "gi-069", "city": "Venlo", "country": "Netherlands", "lat": 51.364552, "lon": 6.176397 },
|
||||
{ "id": "gi-070", "city": "Ede", "country": "Netherlands", "lat": 52.039888, "lon": 5.666329 },
|
||||
{ "id": "gi-071", "city": "Marseille", "country": "France", "lat": 43.323344, "lon": 5.395258 },
|
||||
{ "id": "gi-072", "city": "Dagneux", "country": "France", "lat": 45.855206, "lon": 5.075505 },
|
||||
{ "id": "gi-073", "city": "Chusclan", "country": "France", "lat": 44.148579, "lon": 4.679974 },
|
||||
{ "id": "gi-074", "city": "Etten-Leur", "country": "Netherlands", "lat": 51.573407, "lon": 4.626725 },
|
||||
{ "id": "gi-075", "city": "Fleurus", "country": "Belgium", "lat": 50.483418, "lon": 4.540843 },
|
||||
{ "id": "gi-076", "city": "Sablé-sur-Sarthe", "country": "France", "lat": 47.837859, "lon": -0.344394 },
|
||||
{ "id": "gi-077", "city": "Pouzauges", "country": "France", "lat": 46.782139, "lon": -0.837019 },
|
||||
{ "id": "gi-078", "city": "Tilehurst", "country": "UK", "lat": 51.453028, "lon": -1.013584 },
|
||||
{ "id": "gi-079", "city": "Northants", "country": "UK", "lat": 52.27069, "lon": -1.182336 },
|
||||
{ "id": "gi-080", "city": "Chesterfield", "country": "UK", "lat": 53.266866, "lon": -1.322859 },
|
||||
{ "id": "gi-081", "city": "Sheffield", "country": "UK", "lat": 53.38034, "lon": -1.470618 },
|
||||
{ "id": "gi-082", "city": "Swindon", "country": "UK", "lat": 51.575779, "lon": -1.766416 },
|
||||
{ "id": "gi-083", "city": "Plymouth", "country": "UK", "lat": 53.982301, "lon": -2.094284 },
|
||||
{ "id": "gi-084", "city": "Bobadela", "country": "Portugal", "lat": 50.372238, "lon": -4.148963 },
|
||||
{ "id": "gi-085", "city": "Westport", "country": "Ireland", "lat": 38.804288, "lon": -9.098683 },
|
||||
{ "id": "gi-086", "city": "Ibaraki", "country": "Japan", "lat": 53.797423, "lon": -9.530576 },
|
||||
{ "id": "gi-087", "city": "Jeollabuk-do", "country": "South Korea", "lat": 36.181626, "lon": 126.833682 },
|
||||
{ "id": "gi-088", "city": "Quezon City", "country": "Philippines", "lat": 35.512533, "lon": 121.056131 },
|
||||
{ "id": "gi-089", "city": "Jiangsu Province", "country": "China", "lat": 14.660576, "lon": 120.766199 },
|
||||
{ "id": "gi-090", "city": "Jakarta", "country": "Indonesia", "lat": 31.275518, "lon": 106.821844 },
|
||||
{ "id": "gi-091", "city": "Jakarta", "country": "Indonesia", "lat": -6.230534, "lon": 106.821843 },
|
||||
{ "id": "gi-092", "city": "Jakarta", "country": "Indonesia", "lat": -6.230535, "lon": 106.821842 },
|
||||
{ "id": "gi-093", "city": "Selangor", "country": "Malaysia", "lat": -6.230536, "lon": 101.771493 },
|
||||
{ "id": "gi-094", "city": "Rayong", "country": "Thailand", "lat": 2.911703, "lon": 101.558102 },
|
||||
{ "id": "gi-095", "city": "Ongkharak", "country": "Thailand", "lat": 3.305309, "lon": 101.213625 },
|
||||
{ "id": "gi-096", "city": "Chonburi", "country": "Thailand", "lat": 12.974634, "lon": 101.025648 },
|
||||
{ "id": "gi-097", "city": "Kedah", "country": "Malaysia", "lat": 14.119728, "lon": 101.018411 },
|
||||
{ "id": "gi-098", "city": "Yangon", "country": "Myanmar", "lat": 13.429678, "lon": 100.642381 },
|
||||
{ "id": "gi-099", "city": "Kolkata", "country": "India", "lat": 5.600087, "lon": 96.161503 },
|
||||
{ "id": "gi-100", "city": "Unnao", "country": "India", "lat": 16.799819, "lon": 88.295287 },
|
||||
{ "id": "gi-101", "city": "Malwana", "country": "Sri Lanka", "lat": 22.68131, "lon": 80.528149 },
|
||||
{ "id": "gi-102", "city": "Telangana", "country": "India", "lat": 26.557825, "lon": 79.878705 },
|
||||
{ "id": "gi-103", "city": "Malur", "country": "India", "lat": 6.937849, "lon": 78.174563 },
|
||||
{ "id": "gi-104", "city": "Bangalore", "country": "India", "lat": 17.538289, "lon": 77.92006 },
|
||||
{ "id": "gi-105", "city": "Bangalore", "country": "India", "lat": 12.989133, "lon": 77.586986 },
|
||||
{ "id": "gi-106", "city": "Delhi", "country": "India", "lat": 12.929869, "lon": 77.2119 },
|
||||
{ "id": "gi-107", "city": "Bhiwadi", "country": "India", "lat": 28.688513, "lon": 76.863864 },
|
||||
{ "id": "gi-108", "city": "Dharuhera", "country": "India", "lat": 28.194645, "lon": 76.792833 },
|
||||
{ "id": "gi-109", "city": "Dewas", "country": "India", "lat": 28.20956, "lon": 75.99489 },
|
||||
{ "id": "gi-110", "city": "Rahuri", "country": "India", "lat": 22.93384, "lon": 74.647881 },
|
||||
{ "id": "gi-111", "city": "Nashik", "country": "India", "lat": 19.395687, "lon": 74.231994 },
|
||||
{ "id": "gi-112", "city": "Lahore", "country": "Pakistan", "lat": 20.14863, "lon": 74.19313 },
|
||||
{ "id": "gi-113", "city": "Satara", "country": "India", "lat": 31.440264, "lon": 73.993527 },
|
||||
{ "id": "gi-114", "city": "Thane", "country": "India", "lat": 18.105503, "lon": 73.191977 },
|
||||
{ "id": "gi-115", "city": "Vadodara", "country": "India", "lat": 19.185071, "lon": 73.157635 },
|
||||
{ "id": "gi-116", "city": "Mumbai", "country": "India", "lat": 22.305572, "lon": 73.157292 },
|
||||
{ "id": "gi-117", "city": "Mumbai", "country": "India", "lat": 22.311289, "lon": 73.012903 },
|
||||
{ "id": "gi-118", "city": "Thane", "country": "India", "lat": 19.032567, "lon": 72.914112 },
|
||||
{ "id": "gi-119", "city": "Ahmedabad", "country": "India", "lat": 19.042604, "lon": 72.852339 },
|
||||
{ "id": "gi-120", "city": "Ahmedabad", "country": "India", "lat": 19.085442, "lon": 72.817704 },
|
||||
{ "id": "gi-121", "city": "Ahmedabad", "country": "India", "lat": 19.364242, "lon": 72.368717 },
|
||||
{ "id": "gi-122", "city": "Gujarat", "country": "India", "lat": 22.835401, "lon": 72.276256 },
|
||||
{ "id": "gi-123", "city": "Tashkent", "country": "Uzbekistan", "lat": 22.97761, "lon": 69.334937 },
|
||||
{ "id": "gi-124", "city": "Tehran", "country": "Iran", "lat": 41.331135, "lon": 51.3876352553795 },
|
||||
{ "id": "gi-125", "city": "Isfahan", "country": "Iran", "lat": 35.7474416756762, "lon": 51.0407491436916 },
|
||||
{ "id": "gi-126", "city": "Bonab", "country": "Iran", "lat": 32.3249807945074, "lon": 46.0621245938148 },
|
||||
{ "id": "gi-127", "city": "Amman", "country": "Jordan", "lat": 37.3339678467846, "lon": 35.876798 },
|
||||
{ "id": "gi-128", "city": "Yavne", "country": "Israel", "lat": 32.024605, "lon": 34.736145 },
|
||||
{ "id": "gi-129", "city": "Ankara", "country": "Turkey", "lat": 31.879207, "lon": 32.609717 },
|
||||
{ "id": "gi-130", "city": "Çerkezköy", "country": "Turkey", "lat": 40.062811, "lon": 27.987015 },
|
||||
{ "id": "gi-131", "city": "Kempton Park", "country": "South Africa", "lat": 41.310999, "lon": 28.216191 },
|
||||
{ "id": "gi-132", "city": "Cape Town", "country": "South Africa", "lat": -26.124184, "lon": 18.525036 },
|
||||
{ "id": "gi-133", "city": "Sidi Thabet", "country": "Tunisia", "lat": -33.863096, "lon": 10.046566 },
|
||||
{ "id": "gi-134", "city": "Abuja", "country": "Nigeria", "lat": 36.925775, "lon": 7.52063395497204 },
|
||||
{ "id": "gi-135", "city": "Accra", "country": "Ghana", "lat": 9.04200378229489, "lon": -0.220542 },
|
||||
{ "id": "gi-136", "city": "Ibaraki", "country": "Japan", "lat": 5.677879, "lon": 140.48205 }
|
||||
]
|
||||
}
|
||||
268
src/App.ts
268
src/App.ts
@@ -10,7 +10,7 @@ import {
|
||||
DEFAULT_MAP_LAYERS,
|
||||
STORAGE_KEYS,
|
||||
} from '@/config';
|
||||
import { fetchCategoryFeeds, fetchMultipleStocks, fetchCrypto, fetchPredictions, fetchEarthquakes, fetchWeatherAlerts, fetchFredData, initDB, updateBaseline, calculateDeviation, analyzeCorrelations, clusterNews, addToSignalHistory, saveSnapshot, cleanOldSnapshots } from '@/services';
|
||||
import { fetchCategoryFeeds, fetchMultipleStocks, fetchCrypto, fetchPredictions, fetchEarthquakes, fetchWeatherAlerts, fetchFredData, fetchInternetOutages, initDB, updateBaseline, calculateDeviation, analyzeCorrelations, clusterNews, addToSignalHistory, saveSnapshot, cleanOldSnapshots } from '@/services';
|
||||
import { loadFromStorage, saveToStorage, ExportPanel } from '@/utils';
|
||||
import {
|
||||
MapComponent,
|
||||
@@ -26,7 +26,13 @@ import {
|
||||
PlaybackControl,
|
||||
StatusPanel,
|
||||
EconomicPanel,
|
||||
SearchModal,
|
||||
} from '@/components';
|
||||
import type { SearchResult } from '@/components/SearchModal';
|
||||
import { INTEL_HOTSPOTS, CONFLICT_ZONES, MILITARY_BASES, UNDERSEA_CABLES, NUCLEAR_FACILITIES } from '@/config/geo';
|
||||
import { PIPELINES } from '@/config/pipelines';
|
||||
import { AI_DATA_CENTERS } from '@/config/ai-datacenters';
|
||||
import { GAMMA_IRRADIATORS } from '@/config/irradiators';
|
||||
import type { PredictionMarket, MarketData, ClusteredEvent } from '@/types';
|
||||
|
||||
export class App {
|
||||
@@ -43,6 +49,7 @@ export class App {
|
||||
private statusPanel: StatusPanel | null = null;
|
||||
private exportPanel: ExportPanel | null = null;
|
||||
private economicPanel: EconomicPanel | null = null;
|
||||
private searchModal: SearchModal | null = null;
|
||||
private latestPredictions: PredictionMarket[] = [];
|
||||
private latestMarkets: MarketData[] = [];
|
||||
private latestClusters: ClusteredEvent[] = [];
|
||||
@@ -69,6 +76,7 @@ export class App {
|
||||
this.setupStatusPanel();
|
||||
this.setupExportPanel();
|
||||
this.setupEconomicPanel();
|
||||
this.setupSearchModal();
|
||||
this.setupEventListeners();
|
||||
await this.loadAllData();
|
||||
this.setupRefreshIntervals();
|
||||
@@ -125,6 +133,242 @@ export class App {
|
||||
});
|
||||
}
|
||||
|
||||
private setupSearchModal(): void {
|
||||
this.searchModal = new SearchModal(this.container);
|
||||
|
||||
// Register static sources (hotspots, conflicts, bases)
|
||||
// Include keywords in subtitle for better searchability
|
||||
this.searchModal.registerSource('hotspot', INTEL_HOTSPOTS.map(h => ({
|
||||
id: h.id,
|
||||
title: h.name,
|
||||
subtitle: `${h.subtext || ''} ${h.keywords?.join(' ') || ''} ${h.description || ''}`.trim(),
|
||||
data: h,
|
||||
})));
|
||||
|
||||
this.searchModal.registerSource('conflict', CONFLICT_ZONES.map(c => ({
|
||||
id: c.id,
|
||||
title: c.name,
|
||||
subtitle: `${c.parties?.join(' ') || ''} ${c.keywords?.join(' ') || ''} ${c.description || ''}`.trim(),
|
||||
data: c,
|
||||
})));
|
||||
|
||||
this.searchModal.registerSource('base', MILITARY_BASES.map(b => ({
|
||||
id: b.id,
|
||||
title: b.name,
|
||||
subtitle: `${b.type} ${b.description || ''}`.trim(),
|
||||
data: b,
|
||||
})));
|
||||
|
||||
// Register pipelines
|
||||
this.searchModal.registerSource('pipeline', PIPELINES.map(p => ({
|
||||
id: p.id,
|
||||
title: p.name,
|
||||
subtitle: `${p.type} ${p.operator || ''} ${p.countries?.join(' ') || ''}`.trim(),
|
||||
data: p,
|
||||
})));
|
||||
|
||||
// Register undersea cables
|
||||
this.searchModal.registerSource('cable', UNDERSEA_CABLES.map(c => ({
|
||||
id: c.id,
|
||||
title: c.name,
|
||||
subtitle: c.major ? 'Major cable' : '',
|
||||
data: c,
|
||||
})));
|
||||
|
||||
// Register AI datacenters
|
||||
this.searchModal.registerSource('datacenter', AI_DATA_CENTERS.map(d => ({
|
||||
id: d.id,
|
||||
title: d.name,
|
||||
subtitle: `${d.owner} ${d.chipType || ''}`.trim(),
|
||||
data: d,
|
||||
})));
|
||||
|
||||
// Register nuclear facilities
|
||||
this.searchModal.registerSource('nuclear', NUCLEAR_FACILITIES.map(n => ({
|
||||
id: n.id,
|
||||
title: n.name,
|
||||
subtitle: `${n.type} ${n.operator || ''}`.trim(),
|
||||
data: n,
|
||||
})));
|
||||
|
||||
// Register gamma irradiators
|
||||
this.searchModal.registerSource('irradiator', GAMMA_IRRADIATORS.map(g => ({
|
||||
id: g.id,
|
||||
title: `${g.city}, ${g.country}`,
|
||||
subtitle: g.organization || '',
|
||||
data: g,
|
||||
})));
|
||||
|
||||
// Handle result selection
|
||||
this.searchModal.setOnSelect((result) => this.handleSearchResult(result));
|
||||
|
||||
// Global keyboard shortcut
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
if (this.searchModal?.isOpen()) {
|
||||
this.searchModal.close();
|
||||
} else {
|
||||
// Update search index with latest data before opening
|
||||
this.updateSearchIndex();
|
||||
this.searchModal?.open();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handleSearchResult(result: SearchResult): void {
|
||||
switch (result.type) {
|
||||
case 'news': {
|
||||
// Find and scroll to the news panel containing this item
|
||||
const item = result.data as NewsItem;
|
||||
this.scrollToPanel('politics');
|
||||
this.highlightNewsItem(item.link);
|
||||
break;
|
||||
}
|
||||
case 'hotspot': {
|
||||
// Trigger map popup for hotspot
|
||||
const hotspot = result.data as typeof INTEL_HOTSPOTS[0];
|
||||
this.map?.setView('global');
|
||||
setTimeout(() => {
|
||||
this.map?.triggerHotspotClick(hotspot.id);
|
||||
}, 300);
|
||||
break;
|
||||
}
|
||||
case 'conflict': {
|
||||
const conflict = result.data as typeof CONFLICT_ZONES[0];
|
||||
this.map?.setView('global');
|
||||
setTimeout(() => {
|
||||
this.map?.triggerConflictClick(conflict.id);
|
||||
}, 300);
|
||||
break;
|
||||
}
|
||||
case 'market': {
|
||||
this.scrollToPanel('markets');
|
||||
break;
|
||||
}
|
||||
case 'prediction': {
|
||||
this.scrollToPanel('polymarket');
|
||||
break;
|
||||
}
|
||||
case 'base': {
|
||||
const base = result.data as typeof MILITARY_BASES[0];
|
||||
this.map?.setView('global');
|
||||
setTimeout(() => {
|
||||
this.map?.triggerBaseClick(base.id);
|
||||
}, 300);
|
||||
break;
|
||||
}
|
||||
case 'pipeline': {
|
||||
const pipeline = result.data as typeof PIPELINES[0];
|
||||
this.map?.setView('global');
|
||||
this.map?.enableLayer('pipelines');
|
||||
this.mapLayers.pipelines = true;
|
||||
setTimeout(() => {
|
||||
this.map?.triggerPipelineClick(pipeline.id);
|
||||
}, 300);
|
||||
break;
|
||||
}
|
||||
case 'cable': {
|
||||
const cable = result.data as typeof UNDERSEA_CABLES[0];
|
||||
this.map?.setView('global');
|
||||
this.map?.enableLayer('cables');
|
||||
this.mapLayers.cables = true;
|
||||
setTimeout(() => {
|
||||
this.map?.triggerCableClick(cable.id);
|
||||
}, 300);
|
||||
break;
|
||||
}
|
||||
case 'datacenter': {
|
||||
const dc = result.data as typeof AI_DATA_CENTERS[0];
|
||||
this.map?.setView('global');
|
||||
this.map?.enableLayer('datacenters');
|
||||
this.mapLayers.datacenters = true;
|
||||
setTimeout(() => {
|
||||
this.map?.triggerDatacenterClick(dc.id);
|
||||
}, 300);
|
||||
break;
|
||||
}
|
||||
case 'nuclear': {
|
||||
const nuc = result.data as typeof NUCLEAR_FACILITIES[0];
|
||||
this.map?.setView('global');
|
||||
this.map?.enableLayer('nuclear');
|
||||
this.mapLayers.nuclear = true;
|
||||
setTimeout(() => {
|
||||
this.map?.triggerNuclearClick(nuc.id);
|
||||
}, 300);
|
||||
break;
|
||||
}
|
||||
case 'irradiator': {
|
||||
const irr = result.data as typeof GAMMA_IRRADIATORS[0];
|
||||
this.map?.setView('global');
|
||||
this.map?.enableLayer('irradiators');
|
||||
this.mapLayers.irradiators = true;
|
||||
setTimeout(() => {
|
||||
this.map?.triggerIrradiatorClick(irr.id);
|
||||
}, 300);
|
||||
break;
|
||||
}
|
||||
case 'earthquake':
|
||||
case 'outage':
|
||||
// These are dynamic, just switch to map view
|
||||
this.map?.setView('global');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private scrollToPanel(panelId: string): void {
|
||||
const panel = document.querySelector(`[data-panel="${panelId}"]`);
|
||||
if (panel) {
|
||||
panel.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
panel.classList.add('flash-highlight');
|
||||
setTimeout(() => panel.classList.remove('flash-highlight'), 1500);
|
||||
}
|
||||
}
|
||||
|
||||
private highlightNewsItem(itemId: string): void {
|
||||
setTimeout(() => {
|
||||
const item = document.querySelector(`[data-news-id="${itemId}"]`);
|
||||
if (item) {
|
||||
item.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
item.classList.add('flash-highlight');
|
||||
setTimeout(() => item.classList.remove('flash-highlight'), 1500);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
private updateSearchIndex(): void {
|
||||
if (!this.searchModal) return;
|
||||
|
||||
// Update news sources (use link as unique id)
|
||||
this.searchModal.registerSource('news', this.allNews.slice(0, 200).map(n => ({
|
||||
id: n.link,
|
||||
title: n.title,
|
||||
subtitle: n.source,
|
||||
data: n,
|
||||
})));
|
||||
|
||||
// Update predictions if available
|
||||
if (this.latestPredictions.length > 0) {
|
||||
this.searchModal.registerSource('prediction', this.latestPredictions.map(p => ({
|
||||
id: p.title,
|
||||
title: p.title,
|
||||
subtitle: `${(p.yesPrice * 100).toFixed(0)}% probability`,
|
||||
data: p,
|
||||
})));
|
||||
}
|
||||
|
||||
// Update markets if available
|
||||
if (this.latestMarkets.length > 0) {
|
||||
this.searchModal.registerSource('market', this.latestMarkets.map(m => ({
|
||||
id: m.symbol,
|
||||
title: `${m.symbol} - ${m.name}`,
|
||||
subtitle: `$${m.price?.toFixed(2) || 'N/A'}`,
|
||||
data: m,
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
private setupPlaybackControl(): void {
|
||||
this.playbackControl = new PlaybackControl();
|
||||
this.playbackControl.onSnapshot((snapshot) => {
|
||||
@@ -207,6 +451,7 @@ export class App {
|
||||
<button class="view-btn" data-view="mena">MENA</button>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button class="search-btn" id="searchBtn"><kbd>⌘K</kbd> Search</button>
|
||||
<span class="time-display" id="timeDisplay">--:--:-- UTC</span>
|
||||
<button class="settings-btn" id="settingsBtn">⚙ PANELS</button>
|
||||
</div>
|
||||
@@ -414,6 +659,12 @@ export class App {
|
||||
});
|
||||
});
|
||||
|
||||
// Search button
|
||||
document.getElementById('searchBtn')?.addEventListener('click', () => {
|
||||
this.updateSearchIndex();
|
||||
this.searchModal?.open();
|
||||
});
|
||||
|
||||
// Settings modal
|
||||
document.getElementById('settingsBtn')?.addEventListener('click', () => {
|
||||
document.getElementById('settingsModal')?.classList.add('active');
|
||||
@@ -538,7 +789,11 @@ export class App {
|
||||
this.loadEarthquakes(),
|
||||
this.loadWeatherAlerts(),
|
||||
this.loadFredData(),
|
||||
this.loadOutages(),
|
||||
]);
|
||||
|
||||
// Update search index after all data loads
|
||||
this.updateSearchIndex();
|
||||
}
|
||||
|
||||
private async loadNewsCategory(category: string, feeds: typeof FEEDS.politics): Promise<NewsItem[]> {
|
||||
@@ -681,6 +936,16 @@ export class App {
|
||||
}
|
||||
}
|
||||
|
||||
private async loadOutages(): Promise<void> {
|
||||
try {
|
||||
const outages = await fetchInternetOutages();
|
||||
this.map?.setOutages(outages);
|
||||
this.statusPanel?.updateFeed('NetBlocks', { status: 'ok', itemCount: outages.length });
|
||||
} catch {
|
||||
this.statusPanel?.updateFeed('NetBlocks', { status: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
private async loadFredData(): Promise<void> {
|
||||
try {
|
||||
this.economicPanel?.setLoading(true);
|
||||
@@ -722,5 +987,6 @@ export class App {
|
||||
setInterval(() => this.loadEarthquakes(), 5 * 60 * 1000);
|
||||
setInterval(() => this.loadWeatherAlerts(), 10 * 60 * 1000);
|
||||
setInterval(() => this.loadFredData(), 30 * 60 * 1000);
|
||||
setInterval(() => this.loadOutages(), 60 * 60 * 1000); // 1 hour - Cloudflare rate limit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as d3 from 'd3';
|
||||
import * as topojson from 'topojson-client';
|
||||
import type { Topology, GeometryCollection } from 'topojson-specification';
|
||||
import type { MapLayers, Hotspot, NewsItem, Earthquake } from '@/types';
|
||||
import type { MapLayers, Hotspot, NewsItem, Earthquake, InternetOutage } from '@/types';
|
||||
import type { WeatherAlert } from '@/services/weather';
|
||||
import { getSeverityColor } from '@/services/weather';
|
||||
import {
|
||||
@@ -11,10 +11,15 @@ import {
|
||||
MILITARY_BASES,
|
||||
UNDERSEA_CABLES,
|
||||
NUCLEAR_FACILITIES,
|
||||
GAMMA_IRRADIATORS,
|
||||
PIPELINES,
|
||||
PIPELINE_COLORS,
|
||||
SANCTIONED_COUNTRIES,
|
||||
STRATEGIC_WATERWAYS,
|
||||
APT_GROUPS,
|
||||
COUNTRY_LABELS,
|
||||
ECONOMIC_CENTERS,
|
||||
AI_DATA_CENTERS,
|
||||
} from '@/config';
|
||||
import { MapPopup } from './MapPopup';
|
||||
|
||||
@@ -56,6 +61,7 @@ export class MapComponent {
|
||||
private hotspots: HotspotWithBreaking[];
|
||||
private earthquakes: Earthquake[] = [];
|
||||
private weatherAlerts: WeatherAlert[] = [];
|
||||
private outages: InternetOutage[] = [];
|
||||
private news: NewsItem[] = [];
|
||||
private popup: MapPopup;
|
||||
private onHotspotClick?: (hotspot: Hotspot) => void;
|
||||
@@ -187,7 +193,7 @@ export class MapComponent {
|
||||
toggles.className = 'layer-toggles';
|
||||
toggles.id = 'layerToggles';
|
||||
|
||||
const layers: (keyof MapLayers)[] = ['conflicts', 'bases', 'cables', 'hotspots', 'earthquakes', 'weather', 'nuclear', 'sanctions', 'economic', 'countries'];
|
||||
const layers: (keyof MapLayers)[] = ['conflicts', 'bases', 'cables', 'pipelines', 'hotspots', 'earthquakes', 'weather', 'nuclear', 'irradiators', 'outages', 'datacenters', 'sanctions', 'economic', 'countries', 'waterways'];
|
||||
|
||||
layers.forEach((layer) => {
|
||||
const btn = document.createElement('button');
|
||||
@@ -413,6 +419,10 @@ export class MapComponent {
|
||||
this.renderCables(projection);
|
||||
}
|
||||
|
||||
if (this.state.layers.pipelines && showGlobalLayers) {
|
||||
this.renderPipelines(projection);
|
||||
}
|
||||
|
||||
if (this.state.layers.conflicts && showGlobalLayers) {
|
||||
this.renderConflicts(projection);
|
||||
}
|
||||
@@ -530,12 +540,67 @@ export class MapComponent {
|
||||
.y((d) => projection(d)?.[1] ?? 0)
|
||||
.curve(d3.curveCardinal);
|
||||
|
||||
cableGroup
|
||||
const path = cableGroup
|
||||
.append('path')
|
||||
.attr('class', 'cable-path')
|
||||
.attr('d', lineGenerator(cable.points))
|
||||
.append('title')
|
||||
.text(cable.name);
|
||||
.attr('d', lineGenerator(cable.points));
|
||||
|
||||
path.append('title').text(cable.name);
|
||||
|
||||
path.on('click', (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
this.popup.show({
|
||||
type: 'cable',
|
||||
data: cable,
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private renderPipelines(projection: d3.GeoProjection): void {
|
||||
const pipelineGroup = this.svg.append('g').attr('class', 'pipelines');
|
||||
|
||||
PIPELINES.forEach((pipeline) => {
|
||||
const lineGenerator = d3
|
||||
.line<[number, number]>()
|
||||
.x((d) => projection(d)?.[0] ?? 0)
|
||||
.y((d) => projection(d)?.[1] ?? 0)
|
||||
.curve(d3.curveCardinal.tension(0.5));
|
||||
|
||||
const color = PIPELINE_COLORS[pipeline.type] || '#888888';
|
||||
const opacity = 0.85;
|
||||
const dashArray = pipeline.status === 'construction' ? '4,2' : 'none';
|
||||
|
||||
const path = pipelineGroup
|
||||
.append('path')
|
||||
.attr('class', `pipeline-path pipeline-${pipeline.type} pipeline-${pipeline.status}`)
|
||||
.attr('d', lineGenerator(pipeline.points))
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', color)
|
||||
.attr('stroke-width', 2.5)
|
||||
.attr('stroke-opacity', opacity)
|
||||
.attr('stroke-linecap', 'round')
|
||||
.attr('stroke-linejoin', 'round');
|
||||
|
||||
if (dashArray !== 'none') {
|
||||
path.attr('stroke-dasharray', dashArray);
|
||||
}
|
||||
|
||||
path.append('title').text(`${pipeline.name} (${pipeline.type.toUpperCase()})`);
|
||||
|
||||
path.on('click', (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
this.popup.show({
|
||||
type: 'pipeline',
|
||||
data: pipeline,
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -568,6 +633,17 @@ export class MapComponent {
|
||||
div.style.top = `${centerPos[1]}px`;
|
||||
div.textContent = zone.name;
|
||||
|
||||
div.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
this.popup.show({
|
||||
type: 'conflict',
|
||||
data: zone,
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
});
|
||||
});
|
||||
|
||||
this.overlays.appendChild(div);
|
||||
});
|
||||
}
|
||||
@@ -596,24 +672,29 @@ export class MapComponent {
|
||||
private renderOverlays(projection: d3.GeoProjection): void {
|
||||
this.overlays.innerHTML = '';
|
||||
|
||||
if (this.state.view !== 'global' && this.state.view !== 'mena') return;
|
||||
const isGlobalOrMena = this.state.view === 'global' || this.state.view === 'mena';
|
||||
|
||||
// Country labels (rendered first so they appear behind other overlays)
|
||||
if (this.state.layers.countries) {
|
||||
this.renderCountryLabels(projection);
|
||||
// Global/MENA only overlays
|
||||
if (isGlobalOrMena) {
|
||||
// Country labels (rendered first so they appear behind other overlays)
|
||||
if (this.state.layers.countries) {
|
||||
this.renderCountryLabels(projection);
|
||||
}
|
||||
|
||||
// Conflict zone labels (HTML overlay with counter-scaling)
|
||||
if (this.state.layers.conflicts) {
|
||||
this.renderConflictLabels(projection);
|
||||
}
|
||||
|
||||
// Strategic waterways
|
||||
if (this.state.layers.waterways) {
|
||||
this.renderWaterways(projection);
|
||||
}
|
||||
|
||||
// APT groups
|
||||
this.renderAPTMarkers(projection);
|
||||
}
|
||||
|
||||
// Conflict zone labels (HTML overlay with counter-scaling)
|
||||
if (this.state.layers.conflicts) {
|
||||
this.renderConflictLabels(projection);
|
||||
}
|
||||
|
||||
// Strategic waterways
|
||||
this.renderWaterways(projection);
|
||||
|
||||
// APT groups
|
||||
this.renderAPTMarkers(projection);
|
||||
|
||||
// Nuclear facilities
|
||||
if (this.state.layers.nuclear) {
|
||||
NUCLEAR_FACILITIES.forEach((facility) => {
|
||||
@@ -631,6 +712,49 @@ export class MapComponent {
|
||||
label.textContent = facility.name;
|
||||
div.appendChild(label);
|
||||
|
||||
div.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
this.popup.show({
|
||||
type: 'nuclear',
|
||||
data: facility,
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
});
|
||||
});
|
||||
|
||||
this.overlays.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
// Gamma irradiators (IAEA DIIF)
|
||||
if (this.state.layers.irradiators) {
|
||||
GAMMA_IRRADIATORS.forEach((irradiator) => {
|
||||
const pos = projection([irradiator.lon, irradiator.lat]);
|
||||
if (!pos) return;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'irradiator-marker';
|
||||
div.style.left = `${pos[0]}px`;
|
||||
div.style.top = `${pos[1]}px`;
|
||||
div.title = `${irradiator.city}, ${irradiator.country}`;
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'irradiator-label';
|
||||
label.textContent = irradiator.city;
|
||||
div.appendChild(label);
|
||||
|
||||
div.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
this.popup.show({
|
||||
type: 'irradiator',
|
||||
data: irradiator,
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
});
|
||||
});
|
||||
|
||||
this.overlays.appendChild(div);
|
||||
});
|
||||
}
|
||||
@@ -718,17 +842,40 @@ export class MapComponent {
|
||||
div.className = `base-marker ${base.type}`;
|
||||
div.style.left = `${pos[0]}px`;
|
||||
div.style.top = `${pos[1]}px`;
|
||||
div.title = base.name;
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'base-label';
|
||||
label.textContent = base.name;
|
||||
div.appendChild(label);
|
||||
|
||||
div.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
this.popup.show({
|
||||
type: 'base',
|
||||
data: base,
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
});
|
||||
});
|
||||
|
||||
this.overlays.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
// Earthquakes
|
||||
if (this.state.layers.earthquakes) {
|
||||
console.log('[Map] Rendering earthquakes. Total:', this.earthquakes.length, 'Layer enabled:', this.state.layers.earthquakes);
|
||||
const filteredQuakes = this.filterByTime(this.earthquakes);
|
||||
console.log('[Map] After time filter:', filteredQuakes.length, 'earthquakes. TimeRange:', this.state.timeRange);
|
||||
let rendered = 0;
|
||||
filteredQuakes.forEach((eq) => {
|
||||
const pos = projection([eq.lon, eq.lat]);
|
||||
if (!pos) return;
|
||||
if (!pos) {
|
||||
console.log('[Map] Earthquake position null for:', eq.place, eq.lon, eq.lat);
|
||||
return;
|
||||
}
|
||||
rendered++;
|
||||
|
||||
const size = Math.max(8, eq.magnitude * 3);
|
||||
const div = document.createElement('div');
|
||||
@@ -755,6 +902,43 @@ export class MapComponent {
|
||||
});
|
||||
});
|
||||
|
||||
this.overlays.appendChild(div);
|
||||
});
|
||||
console.log('[Map] Actually rendered', rendered, 'earthquake markers');
|
||||
}
|
||||
|
||||
// Economic Centers
|
||||
if (this.state.layers.economic) {
|
||||
ECONOMIC_CENTERS.forEach((center) => {
|
||||
const pos = projection([center.lon, center.lat]);
|
||||
if (!pos) return;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = `economic-marker ${center.type}`;
|
||||
div.style.left = `${pos[0]}px`;
|
||||
div.style.top = `${pos[1]}px`;
|
||||
|
||||
const icon = document.createElement('div');
|
||||
icon.className = 'economic-icon';
|
||||
icon.textContent = center.type === 'exchange' ? '📈' : center.type === 'central-bank' ? '🏛' : '💰';
|
||||
div.appendChild(icon);
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'economic-label';
|
||||
label.textContent = center.name;
|
||||
div.appendChild(label);
|
||||
|
||||
div.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
this.popup.show({
|
||||
type: 'economic',
|
||||
data: center,
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
});
|
||||
});
|
||||
|
||||
this.overlays.appendChild(div);
|
||||
});
|
||||
}
|
||||
@@ -796,6 +980,78 @@ export class MapComponent {
|
||||
this.overlays.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
// Internet Outages
|
||||
if (this.state.layers.outages) {
|
||||
this.outages.forEach((outage) => {
|
||||
const pos = projection([outage.lon, outage.lat]);
|
||||
if (!pos) return;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = `outage-marker ${outage.severity}`;
|
||||
div.style.left = `${pos[0]}px`;
|
||||
div.style.top = `${pos[1]}px`;
|
||||
|
||||
const icon = document.createElement('div');
|
||||
icon.className = 'outage-icon';
|
||||
icon.textContent = '📡';
|
||||
div.appendChild(icon);
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'outage-label';
|
||||
label.textContent = outage.country;
|
||||
div.appendChild(label);
|
||||
|
||||
div.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
this.popup.show({
|
||||
type: 'outage',
|
||||
data: outage,
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
});
|
||||
});
|
||||
|
||||
this.overlays.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
// AI Data Centers
|
||||
if (this.state.layers.datacenters) {
|
||||
AI_DATA_CENTERS.forEach((dc) => {
|
||||
const pos = projection([dc.lon, dc.lat]);
|
||||
if (!pos) return;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = `datacenter-marker ${dc.status}`;
|
||||
div.style.left = `${pos[0]}px`;
|
||||
div.style.top = `${pos[1]}px`;
|
||||
|
||||
const icon = document.createElement('div');
|
||||
icon.className = 'datacenter-icon';
|
||||
icon.textContent = '🖥️';
|
||||
div.appendChild(icon);
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'datacenter-label';
|
||||
label.textContent = dc.owner;
|
||||
div.appendChild(label);
|
||||
|
||||
div.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
this.popup.show({
|
||||
type: 'datacenter',
|
||||
data: dc,
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
});
|
||||
});
|
||||
|
||||
this.overlays.appendChild(div);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private renderCountryLabels(projection: d3.GeoProjection): void {
|
||||
@@ -820,14 +1076,25 @@ export class MapComponent {
|
||||
if (!pos) return;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'waterway-label';
|
||||
div.className = 'waterway-marker';
|
||||
div.style.left = `${pos[0]}px`;
|
||||
div.style.top = `${pos[1]}px`;
|
||||
div.innerHTML = `
|
||||
<span class="waterway-name">${waterway.name}</span>
|
||||
${waterway.description ? `<span class="waterway-desc">${waterway.description}</span>` : ''}
|
||||
`;
|
||||
div.title = waterway.description || waterway.name;
|
||||
div.title = waterway.name;
|
||||
|
||||
const diamond = document.createElement('div');
|
||||
diamond.className = 'waterway-diamond';
|
||||
div.appendChild(diamond);
|
||||
|
||||
div.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
this.popup.show({
|
||||
type: 'waterway',
|
||||
data: waterway,
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
});
|
||||
});
|
||||
|
||||
this.overlays.appendChild(div);
|
||||
});
|
||||
@@ -846,17 +1113,56 @@ export class MapComponent {
|
||||
<div class="apt-icon">⚠</div>
|
||||
<div class="apt-label">${apt.name}</div>
|
||||
`;
|
||||
div.title = `${apt.name} (${apt.aka}) - ${apt.sponsor}`;
|
||||
|
||||
div.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
this.popup.show({
|
||||
type: 'apt',
|
||||
data: apt,
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
});
|
||||
});
|
||||
|
||||
this.overlays.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
private getRelatedNews(hotspot: Hotspot): NewsItem[] {
|
||||
return this.news.filter((item) => {
|
||||
const titleLower = item.title.toLowerCase();
|
||||
return hotspot.keywords.some((kw) => titleLower.includes(kw.toLowerCase()));
|
||||
}).slice(0, 5);
|
||||
// High-priority conflict keywords that indicate the news is really about another topic
|
||||
const conflictTopics = ['gaza', 'ukraine', 'russia', 'israel', 'iran', 'china', 'taiwan', 'korea', 'syria'];
|
||||
|
||||
return this.news
|
||||
.map((item) => {
|
||||
const titleLower = item.title.toLowerCase();
|
||||
const matchedKeywords = hotspot.keywords.filter((kw) => titleLower.includes(kw.toLowerCase()));
|
||||
|
||||
if (matchedKeywords.length === 0) return null;
|
||||
|
||||
// Check if this news mentions other hotspot conflict topics
|
||||
const conflictMatches = conflictTopics.filter(t =>
|
||||
titleLower.includes(t) && !hotspot.keywords.some(k => k.toLowerCase().includes(t))
|
||||
);
|
||||
|
||||
// If article mentions a major conflict topic that isn't this hotspot, deprioritize heavily
|
||||
if (conflictMatches.length > 0) {
|
||||
// Only include if it ALSO has a strong local keyword (city name, agency)
|
||||
const strongLocalMatch = matchedKeywords.some(kw =>
|
||||
kw.toLowerCase() === hotspot.name.toLowerCase() ||
|
||||
hotspot.agencies?.some(a => titleLower.includes(a.toLowerCase()))
|
||||
);
|
||||
if (!strongLocalMatch) return null;
|
||||
}
|
||||
|
||||
// Score: more keyword matches = more relevant
|
||||
const score = matchedKeywords.length;
|
||||
return { item, score };
|
||||
})
|
||||
.filter((x): x is { item: NewsItem; score: number } => x !== null)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 5)
|
||||
.map(x => x.item);
|
||||
}
|
||||
|
||||
public updateHotspotActivity(news: NewsItem[]): void {
|
||||
@@ -955,6 +1261,165 @@ export class MapComponent {
|
||||
this.applyTransform();
|
||||
}
|
||||
|
||||
public triggerHotspotClick(id: string): void {
|
||||
const hotspot = this.hotspots.find(h => h.id === id);
|
||||
if (!hotspot) return;
|
||||
|
||||
const width = this.container.clientWidth;
|
||||
const height = this.container.clientHeight;
|
||||
const projection = this.getProjection(width, height);
|
||||
const pos = projection([hotspot.lon, hotspot.lat]);
|
||||
if (!pos) return;
|
||||
|
||||
const relatedNews = this.getRelatedNews(hotspot);
|
||||
this.popup.show({
|
||||
type: 'hotspot',
|
||||
data: hotspot,
|
||||
relatedNews,
|
||||
x: pos[0],
|
||||
y: pos[1],
|
||||
});
|
||||
this.onHotspotClick?.(hotspot);
|
||||
}
|
||||
|
||||
public triggerConflictClick(id: string): void {
|
||||
const conflict = CONFLICT_ZONES.find(c => c.id === id);
|
||||
if (!conflict) return;
|
||||
|
||||
const width = this.container.clientWidth;
|
||||
const height = this.container.clientHeight;
|
||||
const projection = this.getProjection(width, height);
|
||||
const pos = projection(conflict.center as [number, number]);
|
||||
if (!pos) return;
|
||||
|
||||
this.popup.show({
|
||||
type: 'conflict',
|
||||
data: conflict,
|
||||
x: pos[0],
|
||||
y: pos[1],
|
||||
});
|
||||
}
|
||||
|
||||
public triggerBaseClick(id: string): void {
|
||||
const base = MILITARY_BASES.find(b => b.id === id);
|
||||
if (!base) return;
|
||||
|
||||
const width = this.container.clientWidth;
|
||||
const height = this.container.clientHeight;
|
||||
const projection = this.getProjection(width, height);
|
||||
const pos = projection([base.lon, base.lat]);
|
||||
if (!pos) return;
|
||||
|
||||
this.popup.show({
|
||||
type: 'base',
|
||||
data: base,
|
||||
x: pos[0],
|
||||
y: pos[1],
|
||||
});
|
||||
}
|
||||
|
||||
public triggerPipelineClick(id: string): void {
|
||||
const pipeline = PIPELINES.find(p => p.id === id);
|
||||
if (!pipeline || pipeline.points.length === 0) return;
|
||||
|
||||
const width = this.container.clientWidth;
|
||||
const height = this.container.clientHeight;
|
||||
const projection = this.getProjection(width, height);
|
||||
const midPoint = pipeline.points[Math.floor(pipeline.points.length / 2)] as [number, number];
|
||||
const pos = projection(midPoint);
|
||||
if (!pos) return;
|
||||
|
||||
this.popup.show({
|
||||
type: 'pipeline',
|
||||
data: pipeline,
|
||||
x: pos[0],
|
||||
y: pos[1],
|
||||
});
|
||||
}
|
||||
|
||||
public triggerCableClick(id: string): void {
|
||||
const cable = UNDERSEA_CABLES.find(c => c.id === id);
|
||||
if (!cable || cable.points.length === 0) return;
|
||||
|
||||
const width = this.container.clientWidth;
|
||||
const height = this.container.clientHeight;
|
||||
const projection = this.getProjection(width, height);
|
||||
const midPoint = cable.points[Math.floor(cable.points.length / 2)] as [number, number];
|
||||
const pos = projection(midPoint);
|
||||
if (!pos) return;
|
||||
|
||||
this.popup.show({
|
||||
type: 'cable',
|
||||
data: cable,
|
||||
x: pos[0],
|
||||
y: pos[1],
|
||||
});
|
||||
}
|
||||
|
||||
public triggerDatacenterClick(id: string): void {
|
||||
const dc = AI_DATA_CENTERS.find(d => d.id === id);
|
||||
if (!dc) return;
|
||||
|
||||
const width = this.container.clientWidth;
|
||||
const height = this.container.clientHeight;
|
||||
const projection = this.getProjection(width, height);
|
||||
const pos = projection([dc.lon, dc.lat]);
|
||||
if (!pos) return;
|
||||
|
||||
this.popup.show({
|
||||
type: 'datacenter',
|
||||
data: dc,
|
||||
x: pos[0],
|
||||
y: pos[1],
|
||||
});
|
||||
}
|
||||
|
||||
public triggerNuclearClick(id: string): void {
|
||||
const facility = NUCLEAR_FACILITIES.find(n => n.id === id);
|
||||
if (!facility) return;
|
||||
|
||||
const width = this.container.clientWidth;
|
||||
const height = this.container.clientHeight;
|
||||
const projection = this.getProjection(width, height);
|
||||
const pos = projection([facility.lon, facility.lat]);
|
||||
if (!pos) return;
|
||||
|
||||
this.popup.show({
|
||||
type: 'nuclear',
|
||||
data: facility,
|
||||
x: pos[0],
|
||||
y: pos[1],
|
||||
});
|
||||
}
|
||||
|
||||
public triggerIrradiatorClick(id: string): void {
|
||||
const irradiator = GAMMA_IRRADIATORS.find(i => i.id === id);
|
||||
if (!irradiator) return;
|
||||
|
||||
const width = this.container.clientWidth;
|
||||
const height = this.container.clientHeight;
|
||||
const projection = this.getProjection(width, height);
|
||||
const pos = projection([irradiator.lon, irradiator.lat]);
|
||||
if (!pos) return;
|
||||
|
||||
this.popup.show({
|
||||
type: 'irradiator',
|
||||
data: irradiator,
|
||||
x: pos[0],
|
||||
y: pos[1],
|
||||
});
|
||||
}
|
||||
|
||||
public enableLayer(layer: keyof MapLayers): void {
|
||||
if (!this.state.layers[layer]) {
|
||||
this.state.layers[layer] = true;
|
||||
const btn = document.querySelector(`[data-layer="${layer}"]`);
|
||||
btn?.classList.add('active');
|
||||
this.onLayerChange?.(layer, true);
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
private applyTransform(): void {
|
||||
const zoom = this.state.zoom;
|
||||
this.wrapper.style.transform = `scale(${zoom}) translate(${this.state.pan.x}px, ${this.state.pan.y}px)`;
|
||||
@@ -1048,7 +1513,12 @@ export class MapComponent {
|
||||
}
|
||||
|
||||
public setEarthquakes(earthquakes: Earthquake[]): void {
|
||||
this.earthquakes = earthquakes;
|
||||
console.log('[Map] setEarthquakes called with', earthquakes.length, 'earthquakes');
|
||||
if (earthquakes.length > 0 || this.earthquakes.length === 0) {
|
||||
this.earthquakes = earthquakes;
|
||||
} else {
|
||||
console.log('[Map] Keeping existing', this.earthquakes.length, 'earthquakes (new data was empty)');
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
||||
@@ -1057,6 +1527,11 @@ export class MapComponent {
|
||||
this.render();
|
||||
}
|
||||
|
||||
public setOutages(outages: InternetOutage[]): void {
|
||||
this.outages = outages;
|
||||
this.render();
|
||||
}
|
||||
|
||||
public getHotspotLevels(): Record<string, string> {
|
||||
const levels: Record<string, string> = {};
|
||||
this.hotspots.forEach(spot => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { ConflictZone, Hotspot, Earthquake, NewsItem } from '@/types';
|
||||
import type { ConflictZone, Hotspot, Earthquake, NewsItem, MilitaryBase, StrategicWaterway, APTGroup, NuclearFacility, EconomicCenter, GammaIrradiator, Pipeline, UnderseaCable, InternetOutage, AIDataCenter } from '@/types';
|
||||
import type { WeatherAlert } from '@/services/weather';
|
||||
|
||||
export type PopupType = 'conflict' | 'hotspot' | 'earthquake' | 'weather';
|
||||
export type PopupType = 'conflict' | 'hotspot' | 'earthquake' | 'weather' | 'base' | 'waterway' | 'apt' | 'nuclear' | 'economic' | 'irradiator' | 'pipeline' | 'cable' | 'outage' | 'datacenter';
|
||||
|
||||
interface PopupData {
|
||||
type: PopupType;
|
||||
data: ConflictZone | Hotspot | Earthquake | WeatherAlert;
|
||||
data: ConflictZone | Hotspot | Earthquake | WeatherAlert | MilitaryBase | StrategicWaterway | APTGroup | NuclearFacility | EconomicCenter | GammaIrradiator | Pipeline | UnderseaCable | InternetOutage | AIDataCenter;
|
||||
relatedNews?: NewsItem[];
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -75,6 +75,26 @@ export class MapPopup {
|
||||
return this.renderEarthquakePopup(data.data as Earthquake);
|
||||
case 'weather':
|
||||
return this.renderWeatherPopup(data.data as WeatherAlert);
|
||||
case 'base':
|
||||
return this.renderBasePopup(data.data as MilitaryBase);
|
||||
case 'waterway':
|
||||
return this.renderWaterwayPopup(data.data as StrategicWaterway);
|
||||
case 'apt':
|
||||
return this.renderAPTPopup(data.data as APTGroup);
|
||||
case 'nuclear':
|
||||
return this.renderNuclearPopup(data.data as NuclearFacility);
|
||||
case 'economic':
|
||||
return this.renderEconomicPopup(data.data as EconomicCenter);
|
||||
case 'irradiator':
|
||||
return this.renderIrradiatorPopup(data.data as GammaIrradiator);
|
||||
case 'pipeline':
|
||||
return this.renderPipelinePopup(data.data as Pipeline);
|
||||
case 'cable':
|
||||
return this.renderCablePopup(data.data as UnderseaCable);
|
||||
case 'outage':
|
||||
return this.renderOutagePopup(data.data as InternetOutage);
|
||||
case 'datacenter':
|
||||
return this.renderDatacenterPopup(data.data as AIDataCenter);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -257,4 +277,414 @@ export class MapPopup {
|
||||
if (hours < 24) return `${hours}h`;
|
||||
return `${Math.floor(hours / 24)}d`;
|
||||
}
|
||||
|
||||
private renderBasePopup(base: MilitaryBase): string {
|
||||
const typeLabels: Record<string, string> = {
|
||||
'us-nato': 'US/NATO',
|
||||
'china': 'CHINA',
|
||||
'russia': 'RUSSIA',
|
||||
};
|
||||
const typeColors: Record<string, string> = {
|
||||
'us-nato': 'elevated',
|
||||
'china': 'high',
|
||||
'russia': 'high',
|
||||
};
|
||||
|
||||
return `
|
||||
<div class="popup-header base">
|
||||
<span class="popup-title">${base.name.toUpperCase()}</span>
|
||||
<span class="popup-badge ${typeColors[base.type] || 'low'}">${typeLabels[base.type] || base.type.toUpperCase()}</span>
|
||||
<button class="popup-close">×</button>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
${base.description ? `<p class="popup-description">${base.description}</p>` : ''}
|
||||
<div class="popup-stats">
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">TYPE</span>
|
||||
<span class="stat-value">${typeLabels[base.type] || base.type}</span>
|
||||
</div>
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">COORDINATES</span>
|
||||
<span class="stat-value">${base.lat.toFixed(2)}°, ${base.lon.toFixed(2)}°</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderWaterwayPopup(waterway: StrategicWaterway): string {
|
||||
return `
|
||||
<div class="popup-header waterway">
|
||||
<span class="popup-title">${waterway.name}</span>
|
||||
<span class="popup-badge elevated">STRATEGIC</span>
|
||||
<button class="popup-close">×</button>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
${waterway.description ? `<p class="popup-description">${waterway.description}</p>` : ''}
|
||||
<div class="popup-stats">
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">COORDINATES</span>
|
||||
<span class="stat-value">${waterway.lat.toFixed(2)}°, ${waterway.lon.toFixed(2)}°</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderAPTPopup(apt: APTGroup): string {
|
||||
return `
|
||||
<div class="popup-header apt">
|
||||
<span class="popup-title">${apt.name}</span>
|
||||
<span class="popup-badge high">THREAT</span>
|
||||
<button class="popup-close">×</button>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<div class="popup-subtitle">Also known as: ${apt.aka}</div>
|
||||
<div class="popup-stats">
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">SPONSOR</span>
|
||||
<span class="stat-value">${apt.sponsor}</span>
|
||||
</div>
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">ORIGIN</span>
|
||||
<span class="stat-value">${apt.lat.toFixed(1)}°, ${apt.lon.toFixed(1)}°</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="popup-description">Advanced Persistent Threat group with state-level capabilities. Known for sophisticated cyber operations targeting critical infrastructure, government, and defense sectors.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderNuclearPopup(facility: NuclearFacility): string {
|
||||
const typeLabels: Record<string, string> = {
|
||||
'plant': 'POWER PLANT',
|
||||
'enrichment': 'ENRICHMENT',
|
||||
'weapons': 'WEAPONS COMPLEX',
|
||||
'research': 'RESEARCH',
|
||||
};
|
||||
const statusColors: Record<string, string> = {
|
||||
'active': 'elevated',
|
||||
'contested': 'high',
|
||||
'decommissioned': 'low',
|
||||
};
|
||||
|
||||
return `
|
||||
<div class="popup-header nuclear">
|
||||
<span class="popup-title">${facility.name.toUpperCase()}</span>
|
||||
<span class="popup-badge ${statusColors[facility.status] || 'low'}">${facility.status.toUpperCase()}</span>
|
||||
<button class="popup-close">×</button>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<div class="popup-stats">
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">TYPE</span>
|
||||
<span class="stat-value">${typeLabels[facility.type] || facility.type.toUpperCase()}</span>
|
||||
</div>
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">STATUS</span>
|
||||
<span class="stat-value">${facility.status.toUpperCase()}</span>
|
||||
</div>
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">COORDINATES</span>
|
||||
<span class="stat-value">${facility.lat.toFixed(2)}°, ${facility.lon.toFixed(2)}°</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="popup-description">Nuclear facility under monitoring. Strategic importance for regional security and non-proliferation concerns.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderEconomicPopup(center: EconomicCenter): string {
|
||||
const typeLabels: Record<string, string> = {
|
||||
'exchange': 'STOCK EXCHANGE',
|
||||
'central-bank': 'CENTRAL BANK',
|
||||
'financial-hub': 'FINANCIAL HUB',
|
||||
};
|
||||
const typeIcons: Record<string, string> = {
|
||||
'exchange': '📈',
|
||||
'central-bank': '🏛',
|
||||
'financial-hub': '💰',
|
||||
};
|
||||
|
||||
const marketStatus = center.marketHours ? this.getMarketStatus(center.marketHours) : null;
|
||||
|
||||
return `
|
||||
<div class="popup-header economic ${center.type}">
|
||||
<span class="popup-title">${typeIcons[center.type] || ''} ${center.name.toUpperCase()}</span>
|
||||
<span class="popup-badge ${marketStatus === 'OPEN' ? 'elevated' : 'low'}">${marketStatus || typeLabels[center.type]}</span>
|
||||
<button class="popup-close">×</button>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
${center.description ? `<p class="popup-description">${center.description}</p>` : ''}
|
||||
<div class="popup-stats">
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">TYPE</span>
|
||||
<span class="stat-value">${typeLabels[center.type] || center.type.toUpperCase()}</span>
|
||||
</div>
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">COUNTRY</span>
|
||||
<span class="stat-value">${center.country}</span>
|
||||
</div>
|
||||
${center.marketHours ? `
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">TRADING HOURS</span>
|
||||
<span class="stat-value">${center.marketHours.open} - ${center.marketHours.close}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">COORDINATES</span>
|
||||
<span class="stat-value">${center.lat.toFixed(2)}°, ${center.lon.toFixed(2)}°</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderIrradiatorPopup(irradiator: GammaIrradiator): string {
|
||||
return `
|
||||
<div class="popup-header irradiator">
|
||||
<span class="popup-title">☢ ${irradiator.city.toUpperCase()}</span>
|
||||
<span class="popup-badge elevated">GAMMA</span>
|
||||
<button class="popup-close">×</button>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<div class="popup-subtitle">Industrial Gamma Irradiator Facility</div>
|
||||
<div class="popup-stats">
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">COUNTRY</span>
|
||||
<span class="stat-value">${irradiator.country}</span>
|
||||
</div>
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">CITY</span>
|
||||
<span class="stat-value">${irradiator.city}</span>
|
||||
</div>
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">COORDINATES</span>
|
||||
<span class="stat-value">${irradiator.lat.toFixed(2)}°, ${irradiator.lon.toFixed(2)}°</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="popup-description">Industrial irradiation facility using Cobalt-60 or Cesium-137 sources for medical device sterilization, food preservation, or material processing. Source: IAEA DIIF Database.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderPipelinePopup(pipeline: Pipeline): string {
|
||||
const typeLabels: Record<string, string> = {
|
||||
'oil': 'OIL PIPELINE',
|
||||
'gas': 'GAS PIPELINE',
|
||||
'products': 'PRODUCTS PIPELINE',
|
||||
};
|
||||
const typeColors: Record<string, string> = {
|
||||
'oil': 'high',
|
||||
'gas': 'elevated',
|
||||
'products': 'low',
|
||||
};
|
||||
const statusLabels: Record<string, string> = {
|
||||
'operating': 'OPERATING',
|
||||
'construction': 'UNDER CONSTRUCTION',
|
||||
};
|
||||
const typeIcon = pipeline.type === 'oil' ? '🛢' : pipeline.type === 'gas' ? '🔥' : '⛽';
|
||||
|
||||
return `
|
||||
<div class="popup-header pipeline ${pipeline.type}">
|
||||
<span class="popup-title">${typeIcon} ${pipeline.name.toUpperCase()}</span>
|
||||
<span class="popup-badge ${typeColors[pipeline.type] || 'low'}">${pipeline.type.toUpperCase()}</span>
|
||||
<button class="popup-close">×</button>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<div class="popup-subtitle">${typeLabels[pipeline.type] || 'PIPELINE'}</div>
|
||||
<div class="popup-stats">
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">STATUS</span>
|
||||
<span class="stat-value">${statusLabels[pipeline.status] || pipeline.status.toUpperCase()}</span>
|
||||
</div>
|
||||
${pipeline.capacity ? `
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">CAPACITY</span>
|
||||
<span class="stat-value">${pipeline.capacity}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${pipeline.length ? `
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">LENGTH</span>
|
||||
<span class="stat-value">${pipeline.length}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${pipeline.operator ? `
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">OPERATOR</span>
|
||||
<span class="stat-value">${pipeline.operator}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
${pipeline.countries && pipeline.countries.length > 0 ? `
|
||||
<div class="popup-section">
|
||||
<span class="section-label">COUNTRIES</span>
|
||||
<div class="popup-tags">
|
||||
${pipeline.countries.map(c => `<span class="popup-tag">${c}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<p class="popup-description">Major ${pipeline.type} pipeline infrastructure. ${pipeline.status === 'operating' ? 'Currently operational and transporting resources.' : 'Currently under construction.'}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderCablePopup(cable: UnderseaCable): string {
|
||||
return `
|
||||
<div class="popup-header cable">
|
||||
<span class="popup-title">🌐 ${cable.name.toUpperCase()}</span>
|
||||
<span class="popup-badge elevated">${cable.major ? 'MAJOR' : 'CABLE'}</span>
|
||||
<button class="popup-close">×</button>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<div class="popup-subtitle">Undersea Fiber Optic Cable</div>
|
||||
<div class="popup-stats">
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">TYPE</span>
|
||||
<span class="stat-value">SUBMARINE CABLE</span>
|
||||
</div>
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">WAYPOINTS</span>
|
||||
<span class="stat-value">${cable.points.length}</span>
|
||||
</div>
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">STATUS</span>
|
||||
<span class="stat-value">ACTIVE</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="popup-description">Undersea telecommunications cable carrying international internet traffic. These fiber optic cables form the backbone of global internet connectivity, transmitting over 95% of intercontinental data.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderOutagePopup(outage: InternetOutage): string {
|
||||
const severityColors: Record<string, string> = {
|
||||
'total': 'high',
|
||||
'major': 'elevated',
|
||||
'partial': 'low',
|
||||
};
|
||||
const severityLabels: Record<string, string> = {
|
||||
'total': 'TOTAL BLACKOUT',
|
||||
'major': 'MAJOR OUTAGE',
|
||||
'partial': 'PARTIAL DISRUPTION',
|
||||
};
|
||||
const timeAgo = this.getTimeAgo(outage.pubDate);
|
||||
|
||||
return `
|
||||
<div class="popup-header outage ${outage.severity}">
|
||||
<span class="popup-title">📡 ${outage.country.toUpperCase()}</span>
|
||||
<span class="popup-badge ${severityColors[outage.severity] || 'low'}">${severityLabels[outage.severity] || 'DISRUPTION'}</span>
|
||||
<button class="popup-close">×</button>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<div class="popup-subtitle">${outage.title}</div>
|
||||
<div class="popup-stats">
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">SEVERITY</span>
|
||||
<span class="stat-value">${outage.severity.toUpperCase()}</span>
|
||||
</div>
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">REPORTED</span>
|
||||
<span class="stat-value">${timeAgo}</span>
|
||||
</div>
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">COORDINATES</span>
|
||||
<span class="stat-value">${outage.lat.toFixed(2)}°, ${outage.lon.toFixed(2)}°</span>
|
||||
</div>
|
||||
</div>
|
||||
${outage.categories && outage.categories.length > 0 ? `
|
||||
<div class="popup-section">
|
||||
<span class="section-label">CATEGORIES</span>
|
||||
<div class="popup-tags">
|
||||
${outage.categories.slice(0, 5).map(c => `<span class="popup-tag">${c}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<p class="popup-description">${outage.description.slice(0, 250)}${outage.description.length > 250 ? '...' : ''}</p>
|
||||
<a href="${outage.link}" target="_blank" class="popup-link">Read full report →</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderDatacenterPopup(dc: AIDataCenter): string {
|
||||
const statusColors: Record<string, string> = {
|
||||
'existing': 'normal',
|
||||
'planned': 'elevated',
|
||||
'decommissioned': 'low',
|
||||
};
|
||||
const statusLabels: Record<string, string> = {
|
||||
'existing': 'OPERATIONAL',
|
||||
'planned': 'PLANNED',
|
||||
'decommissioned': 'DECOMMISSIONED',
|
||||
};
|
||||
|
||||
const formatNumber = (n: number) => {
|
||||
if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`;
|
||||
if (n >= 1000) return `${(n / 1000).toFixed(0)}K`;
|
||||
return n.toString();
|
||||
};
|
||||
|
||||
return `
|
||||
<div class="popup-header datacenter ${dc.status}">
|
||||
<span class="popup-title">🖥️ ${dc.name}</span>
|
||||
<span class="popup-badge ${statusColors[dc.status] || 'normal'}">${statusLabels[dc.status] || 'UNKNOWN'}</span>
|
||||
<button class="popup-close">×</button>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<div class="popup-subtitle">${dc.owner} • ${dc.country}</div>
|
||||
<div class="popup-stats">
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">GPU/CHIP COUNT</span>
|
||||
<span class="stat-value">${formatNumber(dc.chipCount)}</span>
|
||||
</div>
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">CHIP TYPE</span>
|
||||
<span class="stat-value">${dc.chipType || 'Unknown'}</span>
|
||||
</div>
|
||||
${dc.powerMW ? `
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">POWER</span>
|
||||
<span class="stat-value">${dc.powerMW.toFixed(0)} MW</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${dc.sector ? `
|
||||
<div class="popup-stat">
|
||||
<span class="stat-label">SECTOR</span>
|
||||
<span class="stat-value">${dc.sector}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
${dc.note ? `<p class="popup-description">${dc.note}</p>` : ''}
|
||||
<div class="popup-attribution">Data: Epoch AI GPU Clusters</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getMarketStatus(hours: { open: string; close: string; timezone: string }): string {
|
||||
try {
|
||||
const now = new Date();
|
||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
timeZone: hours.timezone,
|
||||
});
|
||||
const currentTime = formatter.format(now);
|
||||
const [openH = 0, openM = 0] = hours.open.split(':').map(Number);
|
||||
const [closeH = 0, closeM = 0] = hours.close.split(':').map(Number);
|
||||
const [currH = 0, currM = 0] = currentTime.split(':').map(Number);
|
||||
|
||||
const openMins = openH * 60 + openM;
|
||||
const closeMins = closeH * 60 + closeM;
|
||||
const currMins = currH * 60 + currM;
|
||||
|
||||
if (currMins >= openMins && currMins < closeMins) {
|
||||
return 'OPEN';
|
||||
}
|
||||
return 'CLOSED';
|
||||
} catch {
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
324
src/components/SearchModal.ts
Normal file
324
src/components/SearchModal.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
export type SearchResultType = 'news' | 'hotspot' | 'market' | 'prediction' | 'conflict' | 'base' | 'pipeline' | 'cable' | 'datacenter' | 'earthquake' | 'outage' | 'nuclear' | 'irradiator';
|
||||
|
||||
export interface SearchResult {
|
||||
type: SearchResultType;
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
interface SearchableSource {
|
||||
type: SearchResultType;
|
||||
items: { id: string; title: string; subtitle?: string; data: unknown }[];
|
||||
}
|
||||
|
||||
const RECENT_SEARCHES_KEY = 'worldmonitor_recent_searches';
|
||||
const MAX_RECENT = 8;
|
||||
const MAX_RESULTS = 12;
|
||||
|
||||
export class SearchModal {
|
||||
private container: HTMLElement;
|
||||
private overlay: HTMLElement | null = null;
|
||||
private input: HTMLInputElement | null = null;
|
||||
private resultsList: HTMLElement | null = null;
|
||||
private sources: SearchableSource[] = [];
|
||||
private results: SearchResult[] = [];
|
||||
private selectedIndex = 0;
|
||||
private recentSearches: string[] = [];
|
||||
private onSelect?: (result: SearchResult) => void;
|
||||
|
||||
constructor(container: HTMLElement) {
|
||||
this.container = container;
|
||||
this.loadRecentSearches();
|
||||
}
|
||||
|
||||
public registerSource(type: SearchResultType, items: SearchableSource['items']): void {
|
||||
const existingIndex = this.sources.findIndex(s => s.type === type);
|
||||
if (existingIndex >= 0) {
|
||||
this.sources[existingIndex] = { type, items };
|
||||
} else {
|
||||
this.sources.push({ type, items });
|
||||
}
|
||||
}
|
||||
|
||||
public setOnSelect(callback: (result: SearchResult) => void): void {
|
||||
this.onSelect = callback;
|
||||
}
|
||||
|
||||
public open(): void {
|
||||
if (this.overlay) return;
|
||||
this.createModal();
|
||||
this.input?.focus();
|
||||
this.showRecentOrEmpty();
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
if (this.overlay) {
|
||||
this.overlay.remove();
|
||||
this.overlay = null;
|
||||
this.input = null;
|
||||
this.resultsList = null;
|
||||
this.results = [];
|
||||
this.selectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public isOpen(): boolean {
|
||||
return this.overlay !== null;
|
||||
}
|
||||
|
||||
private createModal(): void {
|
||||
this.overlay = document.createElement('div');
|
||||
this.overlay.className = 'search-overlay';
|
||||
this.overlay.innerHTML = `
|
||||
<div class="search-modal">
|
||||
<div class="search-header">
|
||||
<span class="search-icon">⌘</span>
|
||||
<input type="text" class="search-input" placeholder="Search news, pipelines, bases, markets..." autofocus />
|
||||
<kbd class="search-kbd">ESC</kbd>
|
||||
</div>
|
||||
<div class="search-results"></div>
|
||||
<div class="search-footer">
|
||||
<span><kbd>↑↓</kbd> navigate</span>
|
||||
<span><kbd>↵</kbd> select</span>
|
||||
<span><kbd>esc</kbd> close</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.overlay.addEventListener('click', (e) => {
|
||||
if (e.target === this.overlay) this.close();
|
||||
});
|
||||
|
||||
this.input = this.overlay.querySelector('.search-input');
|
||||
this.resultsList = this.overlay.querySelector('.search-results');
|
||||
|
||||
this.input?.addEventListener('input', () => this.handleSearch());
|
||||
this.input?.addEventListener('keydown', (e) => this.handleKeydown(e));
|
||||
|
||||
this.container.appendChild(this.overlay);
|
||||
}
|
||||
|
||||
private handleSearch(): void {
|
||||
const query = this.input?.value.trim().toLowerCase() || '';
|
||||
|
||||
if (!query) {
|
||||
this.showRecentOrEmpty();
|
||||
return;
|
||||
}
|
||||
|
||||
this.results = [];
|
||||
|
||||
for (const source of this.sources) {
|
||||
for (const item of source.items) {
|
||||
const titleLower = item.title.toLowerCase();
|
||||
const subtitleLower = item.subtitle?.toLowerCase() || '';
|
||||
|
||||
if (titleLower.includes(query) || subtitleLower.includes(query)) {
|
||||
const isPrefix = titleLower.startsWith(query) || subtitleLower.startsWith(query);
|
||||
this.results.push({
|
||||
type: source.type,
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
subtitle: item.subtitle,
|
||||
data: item.data,
|
||||
_score: isPrefix ? 2 : 1,
|
||||
} as SearchResult & { _score: number });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by score (prefix matches first), then limit
|
||||
this.results.sort((a, b) => ((b as any)._score || 0) - ((a as any)._score || 0));
|
||||
this.results = this.results.slice(0, MAX_RESULTS);
|
||||
|
||||
this.selectedIndex = 0;
|
||||
this.renderResults();
|
||||
}
|
||||
|
||||
private showRecentOrEmpty(): void {
|
||||
this.results = [];
|
||||
|
||||
if (this.recentSearches.length > 0) {
|
||||
this.renderRecent();
|
||||
} else {
|
||||
this.renderEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
private renderRecent(): void {
|
||||
if (!this.resultsList) return;
|
||||
|
||||
this.resultsList.innerHTML = `
|
||||
<div class="search-section-header">Recent Searches</div>
|
||||
${this.recentSearches.map((term, i) => `
|
||||
<div class="search-result-item recent ${i === this.selectedIndex ? 'selected' : ''}" data-recent="${term}">
|
||||
<span class="search-result-icon">🕐</span>
|
||||
<span class="search-result-title">${term}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
`;
|
||||
|
||||
this.resultsList.querySelectorAll('.search-result-item.recent').forEach((el) => {
|
||||
el.addEventListener('click', () => {
|
||||
const term = (el as HTMLElement).dataset.recent || '';
|
||||
if (this.input) this.input.value = term;
|
||||
this.handleSearch();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private renderEmpty(): void {
|
||||
if (!this.resultsList) return;
|
||||
|
||||
this.resultsList.innerHTML = `
|
||||
<div class="search-empty">
|
||||
<div class="search-empty-icon">🔍</div>
|
||||
<div>Search across all data sources</div>
|
||||
<div class="search-empty-hint">News • Pipelines • Bases • Cables • Datacenters • Markets</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderResults(): void {
|
||||
if (!this.resultsList) return;
|
||||
|
||||
if (this.results.length === 0) {
|
||||
this.resultsList.innerHTML = `
|
||||
<div class="search-empty">
|
||||
<div class="search-empty-icon">∅</div>
|
||||
<div>No results found</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const icons: Record<SearchResultType, string> = {
|
||||
news: '📰',
|
||||
hotspot: '📍',
|
||||
market: '📈',
|
||||
prediction: '🎯',
|
||||
conflict: '⚔️',
|
||||
base: '🏛️',
|
||||
pipeline: '🛢',
|
||||
cable: '🌐',
|
||||
datacenter: '🖥️',
|
||||
earthquake: '🌍',
|
||||
outage: '📡',
|
||||
nuclear: '☢️',
|
||||
irradiator: '⚛️',
|
||||
};
|
||||
|
||||
this.resultsList.innerHTML = this.results.map((result, i) => `
|
||||
<div class="search-result-item ${i === this.selectedIndex ? 'selected' : ''}" data-index="${i}">
|
||||
<span class="search-result-icon">${icons[result.type]}</span>
|
||||
<div class="search-result-content">
|
||||
<div class="search-result-title">${this.highlightMatch(result.title)}</div>
|
||||
${result.subtitle ? `<div class="search-result-subtitle">${result.subtitle}</div>` : ''}
|
||||
</div>
|
||||
<span class="search-result-type">${result.type}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
this.resultsList.querySelectorAll('.search-result-item').forEach((el) => {
|
||||
el.addEventListener('click', () => {
|
||||
const index = parseInt((el as HTMLElement).dataset.index || '0');
|
||||
this.selectResult(index);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private highlightMatch(text: string): string {
|
||||
const query = this.input?.value.trim() || '';
|
||||
if (!query) return text;
|
||||
|
||||
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
||||
return text.replace(regex, '<mark>$1</mark>');
|
||||
}
|
||||
|
||||
private handleKeydown(e: KeyboardEvent): void {
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
this.moveSelection(1);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
this.moveSelection(-1);
|
||||
break;
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
this.selectResult(this.selectedIndex);
|
||||
break;
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private moveSelection(delta: number): void {
|
||||
const max = this.results.length || this.recentSearches.length;
|
||||
if (max === 0) return;
|
||||
|
||||
this.selectedIndex = (this.selectedIndex + delta + max) % max;
|
||||
this.updateSelection();
|
||||
}
|
||||
|
||||
private updateSelection(): void {
|
||||
if (!this.resultsList) return;
|
||||
|
||||
this.resultsList.querySelectorAll('.search-result-item').forEach((el, i) => {
|
||||
el.classList.toggle('selected', i === this.selectedIndex);
|
||||
});
|
||||
|
||||
const selected = this.resultsList.querySelector('.selected');
|
||||
selected?.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
|
||||
private selectResult(index: number): void {
|
||||
// If showing recent searches
|
||||
if (this.results.length === 0 && this.recentSearches.length > 0) {
|
||||
const term = this.recentSearches[index];
|
||||
if (term && this.input) {
|
||||
this.input.value = term;
|
||||
this.handleSearch();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const result = this.results[index];
|
||||
if (!result) return;
|
||||
|
||||
// Save to recent searches
|
||||
this.saveRecentSearch(this.input?.value.trim() || '');
|
||||
|
||||
this.close();
|
||||
this.onSelect?.(result);
|
||||
}
|
||||
|
||||
private loadRecentSearches(): void {
|
||||
try {
|
||||
const stored = localStorage.getItem(RECENT_SEARCHES_KEY);
|
||||
this.recentSearches = stored ? JSON.parse(stored) : [];
|
||||
} catch {
|
||||
this.recentSearches = [];
|
||||
}
|
||||
}
|
||||
|
||||
private saveRecentSearch(term: string): void {
|
||||
if (!term || term.length < 2) return;
|
||||
|
||||
this.recentSearches = [
|
||||
term,
|
||||
...this.recentSearches.filter(t => t !== term)
|
||||
].slice(0, MAX_RECENT);
|
||||
|
||||
try {
|
||||
localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(this.recentSearches));
|
||||
} catch {
|
||||
// Storage full, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,3 +9,4 @@ export * from './SignalModal';
|
||||
export * from './PlaybackControl';
|
||||
export * from './StatusPanel';
|
||||
export * from './EconomicPanel';
|
||||
export * from './SearchModal';
|
||||
|
||||
3981
src/config/ai-datacenters.ts
Normal file
3981
src/config/ai-datacenters.ts
Normal file
File diff suppressed because it is too large
Load Diff
228
src/config/bases-expanded.ts
Normal file
228
src/config/bases-expanded.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
// Generated from Overseas Military Bases.xlsx
|
||||
// Source: Asian Religious Connections (ASIAR), HKU - Last updated: Nov 2020
|
||||
// Updated with 2024-25 status changes where known
|
||||
//
|
||||
// STATUS UPDATES SINCE DATASET (2020):
|
||||
// - French Chad, Niger, Senegal, Ivory Coast: CLOSED 2024-25
|
||||
// - US Niger Air Base 201: CLOSING 2024
|
||||
// - Chinese Ream Naval Base: OPERATIONAL April 2025
|
||||
// - US Afghanistan bases: CLOSED 2021
|
||||
|
||||
import type { MilitaryBase } from '@/types';
|
||||
|
||||
export const MILITARY_BASES_EXPANDED: MilitaryBase[] = [
|
||||
{ id: 'ream_naval_base', name: 'Ream Naval Base', lat: 10.50340, lon: 103.60900, type: 'china', country: 'Cambodia', arm: 'PLA Navy(Access Right)', status: 'controversial', description: 'PLA Navy(Access Right). Host: Cambodia. Status disputed.' },
|
||||
{ id: 'chinese_pla_support_base', name: 'Chinese PLA Support Base', lat: 11.59150, lon: 43.06020, type: 'china', country: 'Djibouti', arm: 'Navy', status: 'active', description: 'Navy. Host: Djibouti.' },
|
||||
{ id: 'chinese_naval_intelligence_base', name: 'Chinese Naval Intelligence Base', lat: 14.14630, lon: 93.35880, type: 'china', country: 'Myanmar', arm: 'Army', status: 'controversial', description: 'Army. Host: Myanmar. Status disputed.' },
|
||||
{ id: 'military_base', name: 'Military Base', lat: 37.43810, lon: 74.91280, type: 'china', country: 'Tajikistan', arm: 'Army', status: 'controversial', description: 'Army. Host: Tajikistan. Status disputed.' },
|
||||
{ id: 'unnamed_military_base', name: 'Unnamed Military Base', lat: 9.54583, lon: 112.88750, type: 'china', country: 'Disputed', arm: 'Combined arms', status: 'active', description: 'Combined arms.' },
|
||||
{ id: 'unnamed_military_base_2', name: 'Unnamed Military Base', lat: 10.92361, lon: 114.08472, type: 'china', country: 'Disputed', arm: 'Combined arms', status: 'active', description: 'Combined arms.' },
|
||||
{ id: 'unnamed_military_base_3', name: 'Unnamed Military Base', lat: 9.90000, lon: 115.53333, type: 'china', country: 'Disputed', arm: 'Combined arms', status: 'active', description: 'Combined arms.' },
|
||||
{ id: 'unnamed_military_base_4', name: 'Unnamed Military Base', lat: 16.83444, lon: 112.33972, type: 'china', country: 'Disputed', arm: 'Combined arms', status: 'active', description: 'Combined arms.' },
|
||||
{ id: 'ndjamena_air_force_base', name: 'N\'Djamena Air Force Base', lat: 12.13361, lon: 15.03389, type: 'france', country: 'Chad', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Chad.' },
|
||||
{ id: 'naval_base_of_hron', name: 'Naval base of Héron', lat: 11.55663, lon: 43.14419, type: 'france', country: 'Djibouti', arm: 'Navy', status: 'active', description: 'Navy. Host: Djibouti.' },
|
||||
{ id: 'les_lments_franais_au_gabon', name: 'Les éléments français au Gabon', lat: 0.42048, lon: 9.43806, type: 'france', country: 'Gabon', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Gabon.' },
|
||||
{ id: 'fassberg_air_base', name: 'Fassberg Air Base', lat: 52.91944, lon: 10.18889, type: 'france', country: 'Germany', arm: 'Franco-German training facilities', status: 'active', description: 'Franco-German training facilities. Host: Germany.' },
|
||||
{ id: 'les_forces_franaises_en_cte_divoire_ffci', name: 'Les forces françaises en Côte d\'Ivoire (FFCI)', lat: 7.50357, lon: -5.54897, type: 'france', country: 'Ivory Coast', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Ivory Coast.' },
|
||||
{ id: 'rayak_air_base', name: 'Rayak Air Base', lat: 33.85222, lon: 35.99028, type: 'france', country: 'Lebanon', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Lebanon.' },
|
||||
{ id: 'niamey_air_force_base', name: 'Niamey Air Force Base', lat: 13.48167, lon: 2.17028, type: 'france', country: 'Niger', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Niger.' },
|
||||
{ id: 'les_lments_franais_au_sngal', name: 'Les éléments français au Sénégal', lat: 14.75069, lon: -17.45357, type: 'france', country: 'Senegal', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Senegal.' },
|
||||
{ id: 'unnamed_military_base_5', name: 'Unnamed Military Base', lat: 36.89111, lon: 38.35361, type: 'france', country: 'Syria', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Syria.' },
|
||||
{ id: 'unnamed_military_base_6', name: 'Unnamed Military Base', lat: 36.58750, lon: 38.29972, type: 'france', country: 'Syria', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Syria.' },
|
||||
{ id: 'unnamed_military_base_7', name: 'Unnamed Military Base', lat: 36.38528, lon: 38.85944, type: 'france', country: 'Syria', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Syria.' },
|
||||
{ id: 'abu_dhabi_base', name: 'Abu Dhabi Base', lat: 24.52151, lon: 54.39611, type: 'france', country: 'United Arab Emirates', arm: 'Navy, Air Force', status: 'active', description: 'Navy, Air Force. Host: United Arab Emirates.' },
|
||||
{ id: 'indian_military_training_team', name: 'Indian military training team', lat: 27.36042, lon: 89.30152, type: 'india', country: 'Bhutan', arm: 'Radar facilities', status: 'active', description: 'Radar facilities. Host: Bhutan.' },
|
||||
{ id: 'port_of_shahid_beheshti', name: 'Port of Shahid Beheshti', lat: 25.29752, lon: 60.61111, type: 'india', country: 'Iran', arm: 'Navy & Air Force (Access Right)', status: 'active', description: 'Navy & Air Force (Access Right). Host: Iran.' },
|
||||
{ id: 'port_of_sittwe', name: 'Port of Sittwe', lat: 20.13937, lon: 92.90043, type: 'india', country: 'Myanmar', arm: 'Listening Post', status: 'planned', description: 'Listening Post. Host: Myanmar. Planned/under construction.' },
|
||||
{ id: 'ras_al_hadd_listening_post', name: 'Ras al Hadd Listening post', lat: 22.53308, lon: 59.79831, type: 'india', country: 'Oman', arm: 'Listening Post', status: 'active', description: 'Listening Post. Host: Oman.' },
|
||||
{ id: 'muscat_naval_base', name: 'Muscat naval base', lat: 23.58764, lon: 58.27884, type: 'india', country: 'Oman', arm: 'Navy(Berthing right)', status: 'active', description: 'Navy(Berthing right). Host: Oman.' },
|
||||
{ id: 'duqm_port', name: 'Duqm port', lat: 19.66600, lon: 57.72627, type: 'india', country: 'Oman', arm: 'Navy(Berthing right)', status: 'active', description: 'Navy(Berthing right). Host: Oman.' },
|
||||
{ id: 'naval_facilities_coastal_surveillance_ra', name: 'Naval Facilities, Coastal Surveillance Radar (CSR) station', lat: -9.73661, lon: 46.51097, type: 'india', country: 'Seychelles', arm: 'Navy', status: 'planned', description: 'Navy. Host: Seychelles. Planned/under construction.' },
|
||||
{ id: 'farkhor_air_base', name: 'Farkhor air base', lat: 37.47011, lon: 69.38089, type: 'india', country: 'Tajikistan', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Tajikistan.' },
|
||||
{ id: 'coastal_surveillance_radar_station', name: 'Coastal Surveillance Radar station', lat: -0.62728, lon: 73.09722, type: 'india', country: 'Maldives', arm: 'Radar facilities', status: 'active', description: 'Radar facilities. Host: Maldives.' },
|
||||
{ id: 'coastal_surveillance_radar_csr_station', name: 'Coastal Surveillance Radar (CSR) station', lat: -12.01845, lon: 49.26322, type: 'india', country: 'Madagascar', arm: 'Radar facilities', status: 'active', description: 'Radar facilities. Host: Madagascar.' },
|
||||
{ id: 'coastal_surveillance_radar_csr_station_2', name: 'Coastal Surveillance Radar (CSR) station', lat: -19.99894, lon: 57.62941, type: 'india', country: 'Mauritius', arm: 'Radar facilities', status: 'active', description: 'Radar facilities. Host: Mauritius.' },
|
||||
{ id: 'listening_post_and_coastal_surveillance_', name: 'Listening post and Coastal Surveillance Radar station', lat: 21.91089, lon: 90.04970, type: 'india', country: 'Bangladesh', arm: 'Radar facilities', status: 'planned', description: 'Radar facilities. Host: Bangladesh. Planned/under construction.' },
|
||||
{ id: 'berth_rights_and_right_to_station_its_tr', name: 'Berth rights and right to station its troops in Qatar', lat: 25.30761, lon: 51.20930, type: 'india', country: 'Qatar', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Qatar.' },
|
||||
{ id: 'japan_selfdefense_force_base_djibouti', name: 'Japan Self-Defense Force Base Djibouti', lat: 11.55311, lon: 43.14423, type: 'japan', country: 'Djibouti', arm: 'India shares the maritime assets of Japan', status: 'active', description: 'India shares the maritime assets of Japan. Host: Djibouti.' },
|
||||
{ id: 'heart_miliraty_base', name: 'Heart miliraty base', lat: 34.35091, lon: 62.20565, type: 'italy', country: 'Afghanistan', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Afghanistan.' },
|
||||
{ id: 'djibouti_militaray_base', name: 'Djibouti militaray base', lat: 11.54816, lon: 43.17267, type: 'italy', country: 'Djibouti', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Djibouti.' },
|
||||
{ id: 'ahmad_aljaber_air_base', name: 'Ahmad al-Jaber Air Base', lat: 28.93492, lon: 47.79197, type: 'italy', country: 'Kuwait', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Kuwait.' },
|
||||
{ id: 'libya_military_base', name: 'Libya Military Base', lat: 24.96046, lon: 10.17728, type: 'italy', country: 'Libya', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Libya.' },
|
||||
{ id: 'al_minhad_air_base', name: 'Al Minhad air base', lat: 25.02694, lon: 55.36611, type: 'italy', country: 'United Arab Emirates', arm: 'Air Force', status: 'active', description: 'Air Force. Host: United Arab Emirates.' },
|
||||
{ id: 'russian_102nd_military_base', name: 'Russian 102nd Military Base', lat: 40.79000, lon: 43.82500, type: 'russia', country: 'Armenia', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Armenia.' },
|
||||
{ id: 'russian_3624th_airbase', name: 'Russian 3624th Airbase', lat: 40.12800, lon: 44.47200, type: 'russia', country: 'Armenia', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Armenia.' },
|
||||
{ id: 'vileyka_vlf_transmitter', name: 'Vileyka VLF transmitter', lat: 54.46360, lon: 26.77800, type: 'russia', country: 'Belarus', arm: 'Navy', status: 'active', description: 'Navy. Host: Belarus.' },
|
||||
{ id: 'hantsavichy_radar_station', name: 'Hantsavichy Radar Station', lat: 52.85700, lon: 26.48100, type: 'russia', country: 'Belarus', arm: 'Russian Aerospace Defence Forces', status: 'active', description: 'Russian Aerospace Defence Forces. Host: Belarus.' },
|
||||
{ id: '7th_krasnodar_base', name: '7th Krasnodar base', lat: 43.10100, lon: 40.62400, type: 'russia', country: 'Georgia', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Georgia.' },
|
||||
{ id: 'russian_4th_military_base', name: 'Russian 4th Military Base', lat: 42.39000, lon: 43.92200, type: 'russia', country: 'Georgia', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Georgia.' },
|
||||
{ id: 'baikonur_cosmodrome', name: 'Baikonur Cosmodrome', lat: 45.96400, lon: 63.30500, type: 'russia', country: 'Kazakhstan', arm: 'Spaceport', status: 'active', description: 'Spaceport. Host: Kazakhstan.' },
|
||||
{ id: 'sary_shagan', name: 'Sary Shagan', lat: 46.38300, lon: 72.86600, type: 'russia', country: 'Kazakhstan', arm: 'Anti-ballistic missile testing range', status: 'active', description: 'Anti-ballistic missile testing range. Host: Kazakhstan.' },
|
||||
{ id: 'balkhash_radar_station', name: 'Balkhash Radar Station', lat: 46.60300, lon: 74.53000, type: 'russia', country: 'Kazakhstan', arm: 'Russian early warning radars', status: 'active', description: 'Russian early warning radars. Host: Kazakhstan.' },
|
||||
{ id: 'kant_air_base', name: 'Kant (air base)', lat: 42.85300, lon: 74.84600, type: 'russia', country: 'Kyrgyzstan', arm: 'military air base', status: 'active', description: 'military air base. Host: Kyrgyzstan.' },
|
||||
{ id: 'russian_forces_in_moldova', name: 'Russian forces in Moldova', lat: 46.84000, lon: 29.64300, type: 'russia', country: 'Moldova', arm: 'Task Force', status: 'active', description: 'Task Force. Host: Moldova.' },
|
||||
{ id: 'khmeimim_air_base', name: 'Khmeimim Air Base', lat: 35.41100, lon: 35.94500, type: 'russia', country: 'Syria', arm: 'Russian Aerospace Defence Forces', status: 'active', description: 'Russian Aerospace Defence Forces. Host: Syria.' },
|
||||
{ id: 'russian_naval_facility_in_tartus', name: 'Russian naval facility in Tartus', lat: 34.91500, lon: 35.87400, type: 'russia', country: 'Syria', arm: 'Navy', status: 'active', description: 'Navy. Host: Syria.' },
|
||||
{ id: 'tiyas_military_airbase', name: 'Tiyas Military Airbase', lat: 34.52250, lon: 37.62972, type: 'russia', country: 'Syria', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Syria.' },
|
||||
{ id: 'shayrat_airbase', name: 'Shayrat Airbase', lat: 34.49000, lon: 36.90889, type: 'russia', country: 'Syria', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Syria.' },
|
||||
{ id: 'russian_201st_military_base', name: 'Russian 201st Military Base', lat: 38.53600, lon: 68.78000, type: 'russia', country: 'Tajikistan', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Tajikistan.' },
|
||||
{ id: 'military_headquarters', name: 'Military headquarters', lat: 11.79819, lon: -66.15139, type: 'russia', country: 'Venezuela', arm: 'Combined arms', status: 'planned', description: 'Combined arms. Host: Venezuela. Planned/under construction.' },
|
||||
{ id: 'unnamed_military_base_8', name: 'Unnamed Military Base', lat: 13.01534, lon: 42.73724, type: 'uae', country: 'Eritrea', arm: 'Combined arms', status: 'controversial', description: 'Combined arms. Host: Eritrea. Status disputed.' },
|
||||
{ id: 'unnamed_military_base_9', name: 'Unnamed Military Base', lat: 31.99809, lon: 21.19361, type: 'uae', country: 'Libya', arm: 'Air Force', status: 'controversial', description: 'Air Force. Host: Libya. Status disputed.' },
|
||||
{ id: 'unnamed_military_base_10', name: 'Unnamed Military Base', lat: 10.43800, lon: 44.99700, type: 'uae', country: 'Republic of Somaliland', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Republic of Somaliland.' },
|
||||
{ id: 'unnamed_military_base_11', name: 'Unnamed Military Base', lat: 12.51000, lon: 53.92000, type: 'uae', country: 'Yemen', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Yemen.' },
|
||||
{ id: 'rothera_research_station', name: 'Rothera Research Station', lat: -67.56833, lon: -68.12583, type: 'uk', country: 'Disputed', arm: 'British Antarctic Survey (BAS) base', status: 'active', description: 'British Antarctic Survey (BAS) base.' },
|
||||
{ id: 'hms_jufair', name: 'HMS Jufair', lat: 26.20500, lon: 50.61500, type: 'uk', country: 'Bahrain', arm: 'British Royal Navy base', status: 'active', description: 'British Royal Navy base. Host: Bahrain.' },
|
||||
{ id: 'raf_belize', name: 'RAF Belize', lat: 17.54400, lon: -88.30500, type: 'uk', country: 'Belize', arm: 'Royal Air Force', status: 'active', description: 'Royal Air Force. Host: Belize.' },
|
||||
{ id: 'british_army_jungle_warfare_training_sch', name: 'British Army Jungle Warfare Training School', lat: 4.60800, lon: 114.32500, type: 'uk', country: 'Brunei', arm: 'British Army\'s training establishment', status: 'active', description: 'British Army\'s training establishment. Host: Brunei.' },
|
||||
{ id: 'sittang_camp', name: 'Sittang Camp', lat: 4.82943, lon: 114.66800, type: 'uk', country: 'Brunei', arm: 'British Army\'s training establishment', status: 'active', description: 'British Army\'s training establishment. Host: Brunei.' },
|
||||
{ id: 'kuala_belait_accommodation', name: 'Kuala Belait accommodation', lat: 4.58665, lon: 114.24700, type: 'uk', country: 'Brunei', arm: 'British Army\'s training establishment', status: 'active', description: 'British Army\'s training establishment. Host: Brunei.' },
|
||||
{ id: 'british_army_training_unit_suffield', name: 'British Army Training Unit Suffield', lat: 50.27300, lon: -111.17500, type: 'uk', country: 'Canada', arm: 'Army', status: 'active', description: 'Army. Host: Canada.' },
|
||||
{ id: 'raf_troodos', name: 'RAF Troodos', lat: 34.91200, lon: 32.88300, type: 'uk', country: 'Cyprus', arm: 'Royal Air Force', status: 'active', description: 'Royal Air Force. Host: Cyprus.' },
|
||||
{ id: 'raf_akrotiri', name: 'RAF Akrotiri', lat: 34.59000, lon: 32.98700, type: 'uk', country: 'Cyprus', arm: 'Royal Air Force', status: 'active', description: 'Royal Air Force. Host: Cyprus.' },
|
||||
{ id: 'ayios_nikolaos_station', name: 'Ayios Nikolaos Station', lat: 35.09300, lon: 33.88600, type: 'uk', country: 'Cyprus', arm: 'British Armed Forces', status: 'active', description: 'British Armed Forces. Host: Cyprus.' },
|
||||
{ id: 'westfalen_garrison', name: 'Westfalen Garrison', lat: 51.77800, lon: 8.72000, type: 'uk', country: 'Germany', arm: 'British garrison with facilities', status: 'active', description: 'British garrison with facilities. Host: Germany.' },
|
||||
{ id: 'wulfen_barracks', name: 'Wulfen barracks', lat: 51.70530, lon: 6.99875, type: 'uk', country: 'Germany', arm: 'Munitions storage facility, British Forces Germany', status: 'active', description: 'Munitions storage facility, British Forces Germany. Host: Germany.' },
|
||||
{ id: 'ayrshire_barracks', name: 'Ayrshire barracks', lat: 51.17080, lon: 6.39294, type: 'uk', country: 'Germany', arm: 'Vehicle storage site, British Forces Germany', status: 'active', description: 'Vehicle storage site, British Forces Germany. Host: Germany.' },
|
||||
{ id: 'raf_gibraltar', name: 'RAF Gibraltar', lat: 36.15209, lon: -5.34446, type: 'uk', country: 'Disputed', arm: 'Royal Air Force', status: 'active', description: 'Royal Air Force.' },
|
||||
{ id: 'port_of_gibraltar', name: 'Port of Gibraltar', lat: 36.14850, lon: -5.36520, type: 'uk', country: 'Gibraltar', arm: 'British Royal Navy', status: 'active', description: 'British Royal Navy. Host: Gibraltar.' },
|
||||
{ id: 'british_army_training_unit_kenya', name: 'British Army Training Unit Kenya', lat: 0.03500, lon: 37.05400, type: 'uk', country: 'Kenya', arm: 'Training support unit of the British Army', status: 'active', description: 'Training support unit of the British Army. Host: Kenya.' },
|
||||
{ id: 'british_gurkha dharan', name: 'British Gurkha Dharan', lat: 26.80690, lon: 87.26920, type: 'uk', country: 'Nepal', arm: 'Movement base and regional recruiting centre', status: 'active', description: 'Movement base and regional recruiting centre. Host: Nepal.' },
|
||||
{ id: 'headquarters_british_gurkhas_nepal', name: 'Headquarters British Gurkhas Nepal', lat: 27.66840, lon: 85.31690, type: 'uk', country: 'Nepal', arm: 'Focal point for organisation of transit to and fro', status: 'active', description: 'Focal point for organisation of transit to and fro. Host: Nepal.' },
|
||||
{ id: 'british_gurkha_camp', name: 'British Gurkha Camp', lat: 28.24750, lon: 83.99140, type: 'uk', country: 'Nepal', arm: 'Main recruitment centre', status: 'active', description: 'Main recruitment centre. Host: Nepal.' },
|
||||
{ id: 'bardufoss_air_station', name: 'Bardufoss Air Station', lat: 69.05210, lon: 18.51690, type: 'uk', country: 'Norway', arm: 'Cold weather training for Royal Air Force, British', status: 'active', description: 'Cold weather training for Royal Air Force, British. Host: Norway.' },
|
||||
{ id: 'uk_joint_logistics_support_base', name: 'UK Joint Logistics Support Base', lat: 19.66900, lon: 57.71000, type: 'uk', country: 'Oman', arm: 'Submarines and Queen Elizabeth-class aircraft carr', status: 'active', description: 'Submarines and Queen Elizabeth-class aircraft carr. Host: Oman.' },
|
||||
{ id: 'omanibritish_joint_training_area', name: 'Omani-British Joint Training Area', lat: 19.01400, lon: 57.74870, type: 'uk', country: 'Oman', arm: 'Royal Army of Oman, British Army', status: 'active', description: 'Royal Army of Oman, British Army. Host: Oman.' },
|
||||
{ id: 'seeb_overseas_processing_centre', name: 'Seeb, Overseas Processing Centre', lat: 23.67490, lon: 58.12080, type: 'uk', country: 'Oman', arm: 'GCHQ\'s Middle East spy hub', status: 'active', description: 'GCHQ\'s Middle East spy hub. Host: Oman.' },
|
||||
{ id: 'raf_al_udeid', name: 'RAF Al Udeid', lat: 25.11000, lon: 51.31900, type: 'uk', country: 'Qatar', arm: 'Royal Air Force', status: 'active', description: 'Royal Air Force. Host: Qatar.' },
|
||||
{ id: 'british_naval_facility_base', name: 'British naval facility, base', lat: 1.46411, lon: 103.82600, type: 'uk', country: 'Singapore', arm: 'British Defence Singapore Support Unit (BDSSU)', status: 'active', description: 'British Defence Singapore Support Unit (BDSSU). Host: Singapore.' },
|
||||
{ id: 'raf_mount_pleasant', name: 'RAF Mount Pleasant', lat: -51.82200, lon: -58.44700, type: 'uk', country: 'United Kingdoms', arm: 'Royal Air Force station', status: 'active', description: 'Royal Air Force station. Host: United Kingdoms.' },
|
||||
{ id: 'raf_ascension', name: 'RAF Ascension', lat: -7.96900, lon: -14.39300, type: 'uk', country: 'United Kingdoms', arm: 'Royal Air Force', status: 'active', description: 'Royal Air Force. Host: United Kingdoms.' },
|
||||
{ id: 'naval_support_facility_diego_garcia', name: 'Naval Support Facility Diego Garcia', lat: 7.31300, lon: 72.41100, type: 'uk', country: 'United Kingdoms', arm: 'Naval air facility', status: 'active', description: 'Naval air facility. Host: United Kingdoms.' },
|
||||
{ id: 'ascension_air_force_station', name: 'Ascension Air Force Station', lat: -7.95040, lon: -14.41120, type: 'uk', country: 'United Kingdoms', arm: 'Royal Air Force', status: 'active', description: 'Royal Air Force. Host: United Kingdoms.' },
|
||||
{ id: 'warwick_camp', name: 'Warwick Camp', lat: 32.25660, lon: -64.81530, type: 'uk', country: 'United Kingdoms', arm: 'Royal Bermuda Regiment', status: 'active', description: 'Royal Bermuda Regiment. Host: United Kingdoms.' },
|
||||
{ id: 'cayman_islands_regiment', name: 'Cayman Islands Regiment', lat: 19.29310, lon: -81.37840, type: 'uk', country: 'United Kingdoms', arm: 'A single territorial infantry battalion of the Bri', status: 'active', description: 'A single territorial infantry battalion of the Bri. Host: United Kingdoms.' },
|
||||
{ id: 'a_port_facility_and_depot_for raf_mount_', name: 'A port facility and depot for RAF Mount Pleasant', lat: -51.90000, lon: -58.43770, type: 'uk', country: 'United Kingdoms', arm: 'Royal Navy', status: 'active', description: 'Royal Navy. Host: United Kingdoms.' },
|
||||
{ id: 'rrh_an_early_warning_and_airspace_contro', name: 'RRH, an early warning and airspace control network', lat: -52.15300, lon: -60.59810, type: 'uk', country: 'United Kingdoms', arm: 'British Forces South Atlantic Islands', status: 'active', description: 'British Forces South Atlantic Islands. Host: United Kingdoms.' },
|
||||
{ id: 'rrh_an_early_warning_and_airspace_contro_2', name: 'RRH, an early warning and airspace control network', lat: -51.42520, lon: -60.56430, type: 'uk', country: 'United Kingdoms', arm: 'British Forces South Atlantic Islands', status: 'active', description: 'British Forces South Atlantic Islands. Host: United Kingdoms.' },
|
||||
{ id: 'rrh_an_early_warning_and_airspace_contro_3', name: 'RRH, an early warning and airspace control network', lat: -51.67340, lon: -58.11030, type: 'uk', country: 'United Kingdoms', arm: 'British Forces South Atlantic Islands', status: 'active', description: 'British Forces South Atlantic Islands. Host: United Kingdoms.' },
|
||||
{ id: 'port_stanley_airport', name: 'Port Stanley Airport', lat: -51.69850, lon: -57.84150, type: 'uk', country: 'Disputed', arm: 'Falkland Islands Defence Force Headquarters', status: 'active', description: 'Falkland Islands Defence Force Headquarters.' },
|
||||
{ id: 'jersey_field_squadron', name: 'Jersey Field Squadron', lat: 49.17520, lon: -2.10827, type: 'uk', country: 'United Kingdoms', arm: 'Royal Engineer uni', status: 'active', description: 'Royal Engineer uni. Host: United Kingdoms.' },
|
||||
{ id: 'royal_montserrat_defence_force_headquart', name: 'Royal Montserrat Defence Force Headquarters', lat: 16.79370, lon: -62.21120, type: 'uk', country: 'United Kingdoms', arm: 'Royal Montserrat Defence Force', status: 'active', description: 'Royal Montserrat Defence Force. Host: United Kingdoms.' },
|
||||
{ id: 'firebase_fiddlers_greenfire_base', name: 'Firebase Fiddler\'s Green(Fire base)', lat: 31.44139, lon: 64.10472, type: 'us-nato', country: 'Afghanistan', arm: 'Marine Corps', status: 'active', description: 'Marine Corps. Host: Afghanistan.' },
|
||||
{ id: 'forward_operating_base_delhi', name: 'Forward Operating Base Delhi', lat: 31.13278, lon: 64.18944, type: 'us-nato', country: 'Afghanistan', arm: 'Marine Corps', status: 'active', description: 'Marine Corps. Host: Afghanistan.' },
|
||||
{ id: 'camp_dwyer', name: 'Camp Dwyer', lat: 31.10111, lon: 64.06722, type: 'us-nato', country: 'Afghanistan', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Afghanistan.' },
|
||||
{ id: 'forward_operating_base_geronimo', name: 'Forward Operating Base Geronimo', lat: 31.40167, lon: 64.25889, type: 'us-nato', country: 'Afghanistan', arm: 'Combined arms', status: 'active', description: 'Combined arms. Host: Afghanistan.' },
|
||||
{ id: 'joint_region_marianas_andersen_afb', name: 'Joint Region Marianas Andersen AFB', lat: 13.64950, lon: 144.86300, type: 'us-nato', country: 'America', arm: 'Navy', status: 'active', description: 'Navy. Host: America.' },
|
||||
{ id: 'andersen_air_force_base', name: 'Andersen Air Force Base', lat: 13.57920, lon: 144.92300, type: 'us-nato', country: 'America', arm: 'Air Force', status: 'active', description: 'Air Force. Host: America.' },
|
||||
{ id: 'sector_guam', name: 'Sector Guam', lat: 13.43730, lon: 144.71300, type: 'us-nato', country: 'America', arm: 'Coastal Guard', status: 'active', description: 'Coastal Guard. Host: America.' },
|
||||
{ id: 'robertson_barracks', name: 'Robertson Barracks', lat: -12.44000, lon: 130.97000, type: 'us-nato', country: 'Australia', arm: 'Marines', status: 'active', description: 'Marines. Host: Australia.' },
|
||||
{ id: 'naval_support_activity_bahrain', name: 'Naval Support Activity Bahrain', lat: 26.20860, lon: 50.60970, type: 'us-nato', country: 'Bahrain', arm: 'Navy', status: 'active', description: 'Navy. Host: Bahrain.' },
|
||||
{ id: 'isa_air_base', name: 'Isa Air Base', lat: 25.91210, lon: 50.59310, type: 'us-nato', country: 'Bahrain', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Bahrain.' },
|
||||
{ id: 'usag_brussels', name: 'USAG Brussels', lat: 50.85040, lon: 4.34878, type: 'us-nato', country: 'Belgium', arm: 'Army', status: 'active', description: 'Army. Host: Belgium.' },
|
||||
{ id: 'aitos_logistics_center', name: 'Aitos Logistics Center', lat: 42.70000, lon: 27.25000, type: 'us-nato', country: 'Bulgaria', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Bulgaria.' },
|
||||
{ id: 'bezmer', name: 'Bezmer', lat: 42.48330, lon: 26.50000, type: 'us-nato', country: 'Bulgaria', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Bulgaria.' },
|
||||
{ id: 'graf_ignatievo', name: 'Graf Ignatievo', lat: 42.15000, lon: 24.75000, type: 'us-nato', country: 'Bulgaria', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Bulgaria.' },
|
||||
{ id: 'contingency_location_garoua', name: 'Contingency Location Garoua', lat: 9.33307, lon: 13.37170, type: 'us-nato', country: 'Cameroon', arm: 'Army', status: 'active', description: 'Army. Host: Cameroon.' },
|
||||
{ id: 'guantanamo', name: 'Guantanamo', lat: 20.14440, lon: -75.20920, type: 'us-nato', country: 'Cuba', arm: 'Navy', status: 'active', description: 'Navy. Host: Cuba.' },
|
||||
{ id: 'camp_lemonnier', name: 'Camp Lemonnier', lat: 11.54360, lon: 43.14860, type: 'us-nato', country: 'Djibouti', arm: 'Navy', status: 'active', description: 'Navy. Host: Djibouti.' },
|
||||
{ id: 'raf_lakenheath', name: 'RAF Lakenheath', lat: 52.41750, lon: 0.52211, type: 'us-nato', country: 'United Kingdoms', arm: 'Air Force', status: 'active', description: 'Air Force. Host: United Kingdoms.' },
|
||||
{ id: 'royal_air_force_alconbury', name: 'Royal Air Force Alconbury', lat: 52.36900, lon: -0.26009, type: 'us-nato', country: 'United Kingdoms', arm: 'Air Force', status: 'active', description: 'Air Force. Host: United Kingdoms.' },
|
||||
{ id: 'royal_air_force_croughton', name: 'Royal Air Force Croughton', lat: 52.25000, lon: -0.83333, type: 'us-nato', country: 'United Kingdoms', arm: 'Air Force', status: 'active', description: 'Air Force. Host: United Kingdoms.' },
|
||||
{ id: 'raf_mildenhall', name: 'RAF Mildenhall', lat: 51.42560, lon: -1.69988, type: 'us-nato', country: 'United Kingdoms', arm: 'Air Force', status: 'active', description: 'Air Force. Host: United Kingdoms.' },
|
||||
{ id: 'campbell_barracks', name: 'Campbell Barracks', lat: 49.40770, lon: 8.69079, type: 'us-nato', country: 'Germany', arm: 'Army', status: 'active', description: 'Army. Host: Germany.' },
|
||||
{ id: 'landstuhl_medical_center', name: 'Landstuhl Medical Center', lat: 49.41310, lon: 7.57021, type: 'us-nato', country: 'Germany', arm: 'Army', status: 'active', description: 'Army. Host: Germany.' },
|
||||
{ id: 'patrick_henry_village', name: 'Patrick Henry Village', lat: 49.40770, lon: 8.69079, type: 'us-nato', country: 'Germany', arm: 'Army', status: 'active', description: 'Army. Host: Germany.' },
|
||||
{ id: 'usag_ansbach', name: 'USAG Ansbach', lat: 49.30000, lon: 10.58330, type: 'us-nato', country: 'Germany', arm: 'Army', status: 'active', description: 'Army. Host: Germany.' },
|
||||
{ id: 'usag_bamberg', name: 'USAG Bamberg', lat: 49.89870, lon: 10.90070, type: 'us-nato', country: 'Germany', arm: 'Army', status: 'active', description: 'Army. Host: Germany.' },
|
||||
{ id: 'usag_baumholder', name: 'USAG Baumholder', lat: 49.61740, lon: 7.33381, type: 'us-nato', country: 'Germany', arm: 'Army', status: 'active', description: 'Army. Host: Germany.' },
|
||||
{ id: 'usag_garmisch', name: 'USAG Garmisch', lat: 47.49480, lon: 11.10780, type: 'us-nato', country: 'Germany', arm: 'Army', status: 'active', description: 'Army. Host: Germany.' },
|
||||
{ id: 'usag_grafenwoehr', name: 'USAG Grafenwoehr', lat: 49.71730, lon: 11.90640, type: 'us-nato', country: 'Germany', arm: 'Army', status: 'active', description: 'Army. Host: Germany.' },
|
||||
{ id: 'usag_heidelberg', name: 'USAG Heidelberg', lat: 49.40770, lon: 8.69079, type: 'us-nato', country: 'Germany', arm: 'Army', status: 'active', description: 'Army. Host: Germany.' },
|
||||
{ id: 'usaf_hessen', name: 'USAF Hessen', lat: 50.13420, lon: 8.91418, type: 'us-nato', country: 'Germany', arm: 'Army', status: 'active', description: 'Army. Host: Germany.' },
|
||||
{ id: 'usag_kaiserslautern', name: 'USAG Kaiserslautern', lat: 49.44300, lon: 7.77161, type: 'us-nato', country: 'Germany', arm: 'Army', status: 'active', description: 'Army. Host: Germany.' },
|
||||
{ id: 'usag_mannheim', name: 'USAG Mannheim', lat: 49.40770, lon: 8.69079, type: 'us-nato', country: 'Germany', arm: 'Army', status: 'active', description: 'Army. Host: Germany.' },
|
||||
{ id: 'usag_schweinfurt', name: 'USAG Schweinfurt', lat: 50.04940, lon: 10.22170, type: 'us-nato', country: 'Germany', arm: 'Army', status: 'active', description: 'Army. Host: Germany.' },
|
||||
{ id: 'usag_stuttgart', name: 'USAG Stuttgart', lat: 48.78230, lon: 9.17702, type: 'us-nato', country: 'Germany', arm: 'Army', status: 'active', description: 'Army. Host: Germany.' },
|
||||
{ id: 'usag_wiesbaden', name: 'USAG Wiesbaden', lat: 50.08260, lon: 8.24932, type: 'us-nato', country: 'Germany', arm: 'Army', status: 'active', description: 'Army. Host: Germany.' },
|
||||
{ id: 'ramstein', name: 'Ramstein', lat: 49.44300, lon: 7.77161, type: 'us-nato', country: 'Germany', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Germany.' },
|
||||
{ id: 'spangdahlem', name: 'Spangdahlem', lat: 49.75560, lon: 6.63935, type: 'us-nato', country: 'Germany', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Germany.' },
|
||||
{ id: 'panzer_kaserne', name: 'Panzer Kaserne', lat: 48.68490, lon: 9.02955, type: 'us-nato', country: 'Germany', arm: 'Marines', status: 'active', description: 'Marines. Host: Germany.' },
|
||||
{ id: 'camp_victory', name: 'Camp Victory', lat: 33.34060, lon: 44.40090, type: 'us-nato', country: 'Iraq', arm: 'Army', status: 'active', description: 'Army. Host: Iraq.' },
|
||||
{ id: 'forward_operating_base_abu_ghraib', name: 'Forward Operating Base Abu Ghraib', lat: 33.30700, lon: 44.18690, type: 'us-nato', country: 'Iraq', arm: 'Army', status: 'active', description: 'Army. Host: Iraq.' },
|
||||
{ id: 'fob_grizzly', name: 'FOB Grizzly', lat: 33.80810, lon: 44.53340, type: 'us-nato', country: 'Iraq', arm: 'Army', status: 'active', description: 'Army. Host: Iraq.' },
|
||||
{ id: 'camp_baharia', name: 'Camp Baharia', lat: 33.35580, lon: 43.78610, type: 'us-nato', country: 'Iraq', arm: 'Marines', status: 'active', description: 'Marines. Host: Iraq.' },
|
||||
{ id: 'ain_assad_air_base', name: 'Ain Assad Air Base', lat: 33.79860, lon: 42.43910, type: 'us-nato', country: 'Iraq', arm: 'Army,Air Force,Marines', status: 'active', description: 'Army,Air Force,Marines. Host: Iraq.' },
|
||||
{ id: 'dimona_radar_facility', name: 'Dimona Radar Facility', lat: 30.98440, lon: 35.07350, type: 'us-nato', country: 'Israel', arm: 'US military', status: 'active', description: 'US military. Host: Israel.' },
|
||||
{ id: 'nsa_gaeta', name: 'NSA Gaeta', lat: 41.21410, lon: 13.57080, type: 'us-nato', country: 'Italy', arm: 'Navy', status: 'active', description: 'Navy. Host: Italy.' },
|
||||
{ id: 'naval_support_activity', name: 'Naval Support Activity', lat: 41.21420, lon: 9.40833, type: 'us-nato', country: 'Italy', arm: 'Navy', status: 'active', description: 'Navy. Host: Italy.' },
|
||||
{ id: 'naval_support_activity_2', name: 'Naval Support Activity', lat: 40.83330, lon: 14.25000, type: 'us-nato', country: 'Italy', arm: 'Navy', status: 'active', description: 'Navy. Host: Italy.' },
|
||||
{ id: 'camp_darby', name: 'Camp Darby', lat: 43.62720, lon: 10.29200, type: 'us-nato', country: 'Italy', arm: 'Army', status: 'active', description: 'Army. Host: Italy.' },
|
||||
{ id: 'caserma_ederle', name: 'Caserma Ederle', lat: 45.55730, lon: 11.54090, type: 'us-nato', country: 'Italy', arm: 'Army', status: 'active', description: 'Army. Host: Italy.' },
|
||||
{ id: 'aviano', name: 'Aviano', lat: 46.07060, lon: 12.59470, type: 'us-nato', country: 'Italy', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Italy.' },
|
||||
{ id: 'fleet_actvities_sasebo', name: 'Fleet Actvities Sasebo', lat: 33.15920, lon: 129.72300, type: 'us-nato', country: 'Japan', arm: 'Navy', status: 'active', description: 'Navy. Host: Japan.' },
|
||||
{ id: 'fleet_activities', name: 'Fleet Activities', lat: 25.77220, lon: 126.66900, type: 'us-nato', country: 'Japan', arm: 'Navy', status: 'active', description: 'Navy. Host: Japan.' },
|
||||
{ id: 'fleep_activities', name: 'Fleep Activities', lat: 35.28360, lon: 139.66700, type: 'us-nato', country: 'Japan', arm: 'Navy', status: 'active', description: 'Navy. Host: Japan.' },
|
||||
{ id: 'camp_zama', name: 'Camp Zama', lat: 35.48890, lon: 139.38900, type: 'us-nato', country: 'Japan', arm: 'Army', status: 'active', description: 'Army. Host: Japan.' },
|
||||
{ id: 'fort_buckner', name: 'Fort Buckner', lat: 25.77220, lon: 126.66900, type: 'us-nato', country: 'Japan', arm: 'Army', status: 'active', description: 'Army. Host: Japan.' },
|
||||
{ id: 'torii_station', name: 'Torii Station', lat: 25.77220, lon: 126.66900, type: 'us-nato', country: 'Japan', arm: 'Army', status: 'active', description: 'Army. Host: Japan.' },
|
||||
{ id: 'kadena', name: 'Kadena', lat: 25.77220, lon: 126.66900, type: 'us-nato', country: 'Japan', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Japan.' },
|
||||
{ id: 'misawsa', name: 'Misawsa', lat: 40.68680, lon: 141.39000, type: 'us-nato', country: 'Japan', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Japan.' },
|
||||
{ id: 'yokota', name: 'Yokota', lat: 35.73940, lon: 139.34700, type: 'us-nato', country: 'Japan', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Japan.' },
|
||||
{ id: 'unnamed_military_base_12', name: 'Unnamed Military Base', lat: 25.77220, lon: 126.66900, type: 'us-nato', country: 'Japan', arm: 'Marines', status: 'active', description: 'Marines. Host: Japan.' },
|
||||
{ id: 'unnamed_military_base_13', name: 'Unnamed Military Base', lat: 34.15000, lon: 132.18300, type: 'us-nato', country: 'Japan', arm: 'Marines', status: 'active', description: 'Marines. Host: Japan.' },
|
||||
{ id: 'camp_courtney', name: 'Camp Courtney', lat: 26.37610, lon: 127.85900, type: 'us-nato', country: 'Japan', arm: 'Marines', status: 'active', description: 'Marines. Host: Japan.' },
|
||||
{ id: 'camp_foster', name: 'Camp Foster', lat: 26.30290, lon: 127.76700, type: 'us-nato', country: 'Japan', arm: 'Marines', status: 'active', description: 'Marines. Host: Japan.' },
|
||||
{ id: 'camp_gonsalves', name: 'Camp Gonsalves', lat: 25.77220, lon: 126.66900, type: 'us-nato', country: 'Japan', arm: 'Marines', status: 'active', description: 'Marines. Host: Japan.' },
|
||||
{ id: 'camp_hansen', name: 'Camp Hansen', lat: 25.77220, lon: 126.66900, type: 'us-nato', country: 'Japan', arm: 'Marines', status: 'active', description: 'Marines. Host: Japan.' },
|
||||
{ id: 'camp_kinser', name: 'Camp Kinser', lat: 25.77220, lon: 126.66900, type: 'us-nato', country: 'Japan', arm: 'Marines', status: 'active', description: 'Marines. Host: Japan.' },
|
||||
{ id: 'camp_schwab', name: 'Camp Schwab', lat: 25.77220, lon: 126.66900, type: 'us-nato', country: 'Japan', arm: 'Marines', status: 'active', description: 'Marines. Host: Japan.' },
|
||||
{ id: 'camp_sd_butler', name: 'Camp SD Butler', lat: 25.77220, lon: 126.66900, type: 'us-nato', country: 'Japan', arm: 'Marines', status: 'active', description: 'Marines. Host: Japan.' },
|
||||
{ id: 'yontan_airfield', name: 'Yontan Airfield', lat: 25.77220, lon: 126.66900, type: 'us-nato', country: 'Japan', arm: 'Marines', status: 'active', description: 'Marines. Host: Japan.' },
|
||||
{ id: 'far_east_activities', name: 'Far East Activities', lat: 35.74310, lon: 139.35000, type: 'us-nato', country: 'Japan', arm: 'Coastal Guard', status: 'active', description: 'Coastal Guard. Host: Japan.' },
|
||||
{ id: 'naval_air_facility_atsugi', name: 'Naval Air Facility Atsugi', lat: 35.45670, lon: 139.45000, type: 'us-nato', country: 'Japan', arm: 'Navy', status: 'active', description: 'Navy. Host: Japan.' },
|
||||
{ id: 'camp_fuji', name: 'Camp Fuji', lat: 35.31710, lon: 138.93300, type: 'us-nato', country: 'Japan', arm: 'Marines', status: 'active', description: 'Marines. Host: Japan.' },
|
||||
{ id: 'fleet_activities_okinawa', name: 'Fleet Activities Okinawa', lat: 26.50430, lon: 127.99700, type: 'us-nato', country: 'Japan', arm: 'Navy', status: 'active', description: 'Navy. Host: Japan.' },
|
||||
{ id: 'torii_station_2', name: 'TORII Station', lat: 26.49380, lon: 127.85100, type: 'us-nato', country: 'Japan', arm: 'Army', status: 'active', description: 'Army. Host: Japan.' },
|
||||
{ id: 'kadena_air_base', name: 'Kadena Air Base', lat: 26.35450, lon: 127.76600, type: 'us-nato', country: 'Japan', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Japan.' },
|
||||
{ id: 'marine_corps_base_camp_smedley_d_bulter', name: 'Marine Corps Base Camp Smedley D. Bulter', lat: 26.48430, lon: 127.95500, type: 'us-nato', country: 'Japan', arm: 'Marines', status: 'active', description: 'Marines. Host: Japan.' },
|
||||
{ id: 'camp_bondsteel', name: 'Camp Bondsteel', lat: 42.36670, lon: 21.13330, type: 'us-nato', country: 'Kosovo', arm: 'Army', status: 'active', description: 'Army. Host: Kosovo.' },
|
||||
{ id: 'ali_al_salem_air_base', name: 'Ali Al Salem Air Base', lat: 29.34870, lon: 47.52350, type: 'us-nato', country: 'Kuwait', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Kuwait.' },
|
||||
{ id: 'camp_arifjan', name: 'Camp Arifjan', lat: 28.87510, lon: 48.15890, type: 'us-nato', country: 'Kuwait', arm: 'Air Force,Army,Marines,Navy,Coastal Guard', status: 'active', description: 'Air Force,Army,Marines,Navy,Coastal Guard. Host: Kuwait.' },
|
||||
{ id: 'camp_buehring', name: 'Camp Buehring', lat: 29.69520, lon: 47.42120, type: 'us-nato', country: 'Kuwait', arm: 'Base', status: 'active', description: 'Base. Host: Kuwait.' },
|
||||
{ id: 'kuwait_naval_base', name: 'Kuwait Naval Base', lat: 28.86430, lon: 48.27750, type: 'us-nato', country: 'Kuwait', arm: 'Army, Navy, Coastal Guard', status: 'active', description: 'Army, Navy, Coastal Guard. Host: Kuwait.' },
|
||||
{ id: 'usag_schinnen', name: 'USAG Schinnen', lat: 50.94330, lon: 5.88889, type: 'us-nato', country: 'Netherlands', arm: 'Army', status: 'active', description: 'Army. Host: Netherlands.' },
|
||||
{ id: 'niger_air_base_201', name: 'Niger Air Base 201', lat: 16.92120, lon: 8.02595, type: 'us-nato', country: 'Niger', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Niger.' },
|
||||
{ id: 'masirah_aira_base', name: 'Masirah Aira Base', lat: 20.66710, lon: 58.89710, type: 'us-nato', country: 'Oman', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Oman.' },
|
||||
{ id: 'rafo_thumrait', name: 'RAFO Thumrait', lat: 17.66410, lon: 54.02550, type: 'us-nato', country: 'Oman', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Oman.' },
|
||||
{ id: 'antonio_bautista_air_base', name: 'Antonio Bautista Air Base', lat: 9.74346, lon: 118.76000, type: 'us-nato', country: 'Philippines', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Philippines.' },
|
||||
{ id: 'cesar_basa_air_base', name: 'Cesar Basa Air Base', lat: 14.98620, lon: 120.49400, type: 'us-nato', country: 'Philippines', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Philippines.' },
|
||||
{ id: 'fort_magsaysay', name: 'Fort Magsaysay', lat: 15.43500, lon: 121.09100, type: 'us-nato', country: 'Philippines', arm: 'Army', status: 'active', description: 'Army. Host: Philippines.' },
|
||||
{ id: 'lumbia_airfield', name: 'Lumbia Airfield', lat: 8.40550, lon: 124.61000, type: 'us-nato', country: 'Philippines', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Philippines.' },
|
||||
{ id: 'mactanbenito_ebuen_air_base', name: 'Mactan-Benito Ebuen Air Base', lat: 10.31290, lon: 123.97800, type: 'us-nato', country: 'Philippines', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Philippines.' },
|
||||
{ id: 'lajes_field', name: 'Lajes Field', lat: 38.38330, lon: -28.26670, type: 'us-nato', country: 'Portugal', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Portugal.' },
|
||||
{ id: 'camp_santiago', name: 'Camp Santiago', lat: 17.97750, lon: -66.29800, type: 'us-nato', country: 'Puerto Rico', arm: 'Army', status: 'active', description: 'Army. Host: Puerto Rico.' },
|
||||
{ id: 'al_udeid', name: 'Al Udeid', lat: 25.27930, lon: 51.52240, type: 'us-nato', country: 'Quatar', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Quatar.' },
|
||||
{ id: 'prince_sultan_air_base', name: 'Prince Sultan Air Base', lat: 24.07690, lon: 47.56400, type: 'us-nato', country: 'Saudi Arabia', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Saudi Arabia.' },
|
||||
{ id: 'comlog_westpac', name: 'COMLOG Westpac', lat: 1.28967, lon: 103.85000, type: 'us-nato', country: 'Singapore', arm: 'Navy', status: 'active', description: 'Navy. Host: Singapore.' },
|
||||
{ id: 'fleet_actvities_chinhae', name: 'Fleet Actvities Chinhae', lat: 35.10280, lon: 129.04000, type: 'us-nato', country: 'South Korea', arm: 'Navy', status: 'active', description: 'Navy. Host: South Korea.' },
|
||||
{ id: 'camp_red_cloud', name: 'Camp Red Cloud', lat: 37.74150, lon: 127.04700, type: 'us-nato', country: 'South Korea', arm: 'Army', status: 'active', description: 'Army. Host: South Korea.' },
|
||||
{ id: 'camp_stanley', name: 'Camp Stanley', lat: 37.74150, lon: 127.04700, type: 'us-nato', country: 'South Korea', arm: 'Army', status: 'active', description: 'Army. Host: South Korea.' },
|
||||
{ id: 'usag_daegu', name: 'USAG Daegu', lat: 35.87030, lon: 128.59100, type: 'us-nato', country: 'South Korea', arm: 'Army', status: 'active', description: 'Army. Host: South Korea.' },
|
||||
{ id: 'kunsan_ab', name: 'Kunsan AB', lat: 35.90220, lon: 126.62500, type: 'us-nato', country: 'South Korea', arm: 'Air Force', status: 'active', description: 'Air Force. Host: South Korea.' },
|
||||
{ id: 'us_army_garrison_humphreys', name: 'U.S. Army Garrison Humphreys', lat: 36.96510, lon: 127.03300, type: 'us-nato', country: 'South Korea', arm: 'Army', status: 'active', description: 'Army. Host: South Korea.' },
|
||||
{ id: 'osan_air_base', name: 'Osan Air Base', lat: 37.09100, lon: 127.03100, type: 'us-nato', country: 'South Korea', arm: 'Air Force', status: 'active', description: 'Air Force. Host: South Korea.' },
|
||||
{ id: 'k16_air_base', name: 'K-16 Air Base', lat: 37.43770, lon: 127.10900, type: 'us-nato', country: 'South Korea', arm: 'Air Force', status: 'active', description: 'Air Force. Host: South Korea.' },
|
||||
{ id: 'usag_yongsan', name: 'USAG Yongsan', lat: 37.53310, lon: 126.98300, type: 'us-nato', country: 'South Korea', arm: 'Army', status: 'active', description: 'Army. Host: South Korea.' },
|
||||
{ id: 'us_army_garrison_casey', name: 'U.S. Army Garrison CASEY', lat: 37.88420, lon: 127.05000, type: 'us-nato', country: 'South Korea', arm: 'Army', status: 'active', description: 'Army. Host: South Korea.' },
|
||||
{ id: 'naval_station', name: 'Naval Station', lat: 36.62240, lon: -6.35859, type: 'us-nato', country: 'Spain', arm: 'Navy', status: 'active', description: 'Navy. Host: Spain.' },
|
||||
{ id: 'izmir', name: 'Izmir', lat: 38.41270, lon: 27.13840, type: 'us-nato', country: 'Turkey', arm: 'Air Force', status: 'active', description: 'Air Force. Host: Turkey.' },
|
||||
{ id: 'al_dhafra_air_base', name: 'Al Dhafra Air Base', lat: 24.24000, lon: 54.55100, type: 'us-nato', country: 'United Arab Emirates', arm: 'Air Force,Army', status: 'active', description: 'Air Force,Army. Host: United Arab Emirates.' },
|
||||
{ id: 'port_of_jebel_ali', name: 'Port of Jebel Ali', lat: 25.02490, lon: 55.03990, type: 'us-nato', country: 'United Arab Emirates', arm: 'Air Force, Navy', status: 'active', description: 'Air Force, Navy. Host: United Arab Emirates.' },
|
||||
{ id: 'fujairah_naval_base', name: 'Fujairah Naval Base', lat: 25.25230, lon: 56.36520, type: 'us-nato', country: 'United Arab Emirates', arm: 'Navy', status: 'active', description: 'Navy. Host: United Arab Emirates.' },
|
||||
{ id: 'navy_support_facility', name: 'Navy Support Facility', lat: -7.29861, lon: 72.40160, type: 'us-nato', country: 'United Kingdom', arm: 'Navy', status: 'active', description: 'Navy. Host: United Kingdom.' },
|
||||
];
|
||||
|
||||
// Summary by operator:
|
||||
// US-NATO: 112, UK: 38, Russia: 17, India: 13, France: 12, China: 8, Italy: 5, UAE: 4, Japan: 1
|
||||
// Total: 210 bases
|
||||
@@ -22,6 +22,10 @@ export const SOURCE_TIERS: Record<string, number> = {
|
||||
'CNBC': 2,
|
||||
'MarketWatch': 2,
|
||||
'Al Jazeera': 2,
|
||||
'Financial Times': 2,
|
||||
'Reuters World': 1,
|
||||
'Reuters Business': 1,
|
||||
'OpenAI News': 3,
|
||||
|
||||
// Tier 1.5 - Official Government Sources
|
||||
'White House': 1,
|
||||
@@ -65,8 +69,9 @@ export const FEEDS: Record<string, Feed[]> = {
|
||||
{ name: 'BBC World', url: '/rss/bbc/news/world/rss.xml' },
|
||||
{ name: 'NPR News', url: '/rss/npr/1001/rss.xml' },
|
||||
{ name: 'Guardian World', url: '/rss/guardian/world/rss' },
|
||||
{ name: 'AP News', url: '/rss/apnews/feed' },
|
||||
{ name: 'AP News', url: '/rss/googlenews/rss/search?q=site:apnews.com&hl=en-US&gl=US&ceid=US:en' },
|
||||
{ name: 'The Diplomat', url: '/rss/diplomat/feed/' },
|
||||
{ name: 'Reuters World', url: '/rss/googlenews/rss/search?q=site:reuters.com+world&hl=en-US&gl=US&ceid=US:en' },
|
||||
],
|
||||
middleeast: [
|
||||
{ name: 'BBC Middle East', url: '/rss/bbc/news/world/middle_east/rss.xml' },
|
||||
@@ -85,27 +90,31 @@ export const FEEDS: Record<string, Feed[]> = {
|
||||
{ name: 'Hugging Face', url: '/rss/huggingface/blog/feed.xml' },
|
||||
{ name: 'ArXiv AI', url: '/rss/arxiv/rss/cs.AI' },
|
||||
{ name: 'VentureBeat AI', url: '/rss/venturebeat/feed/' },
|
||||
{ name: 'OpenAI News', url: '/rss/openai/news/rss.xml' },
|
||||
],
|
||||
finance: [
|
||||
{ name: 'CNBC', url: '/rss/cnbc/id/100003114/device/rss/rss.html' },
|
||||
{ name: 'MarketWatch', url: '/rss/marketwatch/marketwatch/topstories' },
|
||||
{ name: 'Yahoo Finance', url: '/rss/yahoonews/news/rssindex' },
|
||||
{ name: 'Financial Times', url: '/rss/ft/rss/home' },
|
||||
{ name: 'Reuters Business', url: '/rss/googlenews/rss/search?q=site:reuters.com+business+markets&hl=en-US&gl=US&ceid=US:en' },
|
||||
],
|
||||
gov: [
|
||||
{ name: 'White House', url: '/rss/whitehouse/briefing-room/feed/' },
|
||||
{ name: 'State Dept', url: '/rss/state/rss/feeds/documents/latest_news' },
|
||||
{ name: 'Pentagon', url: '/rss/defense/DesktopModules/ArticleCS/RSS.ashx?ContentType=1&Site=945&max=10' },
|
||||
{ name: 'Treasury', url: '/rss/treasury/press-center/news/pages/rss@xmlnewsbydate.aspx' },
|
||||
{ name: 'DOJ', url: '/rss/justice/feeds/opa/justicenews.xml' },
|
||||
// Many gov sites deprecated RSS - use Google News aggregation
|
||||
{ name: 'White House', url: '/rss/googlenews/rss/search?q=site:whitehouse.gov&hl=en-US&gl=US&ceid=US:en' },
|
||||
{ name: 'State Dept', url: '/rss/googlenews/rss/search?q=site:state.gov+OR+"State+Department"&hl=en-US&gl=US&ceid=US:en' },
|
||||
{ name: 'Pentagon', url: '/rss/googlenews/rss/search?q=site:defense.gov+OR+Pentagon&hl=en-US&gl=US&ceid=US:en' },
|
||||
{ name: 'Treasury', url: '/rss/googlenews/rss/search?q=site:treasury.gov+OR+"Treasury+Department"&hl=en-US&gl=US&ceid=US:en' },
|
||||
{ name: 'DOJ', url: '/rss/googlenews/rss/search?q=site:justice.gov+OR+"Justice+Department"+DOJ&hl=en-US&gl=US&ceid=US:en' },
|
||||
{ name: 'Federal Reserve', url: '/rss/fedreserve/feeds/press_all.xml' },
|
||||
{ name: 'SEC', url: '/rss/sec/news/pressreleases.rss' },
|
||||
{ name: 'CDC', url: '/rss/cdc/rss/search.ashx?Search=&Channel=CDC_Media' },
|
||||
{ name: 'FEMA', url: '/rss/fema/api/v2/news?format=rss' },
|
||||
{ name: 'DHS', url: '/rss/dhs/news/rss/all' },
|
||||
{ name: 'CDC', url: '/rss/googlenews/rss/search?q=site:cdc.gov+OR+CDC+health&hl=en-US&gl=US&ceid=US:en' },
|
||||
{ name: 'FEMA', url: '/rss/googlenews/rss/search?q=site:fema.gov+OR+FEMA+emergency&hl=en-US&gl=US&ceid=US:en' },
|
||||
{ name: 'DHS', url: '/rss/googlenews/rss/search?q=site:dhs.gov+OR+"Homeland+Security"&hl=en-US&gl=US&ceid=US:en' },
|
||||
],
|
||||
layoffs: [
|
||||
{ name: 'TechCrunch Layoffs', url: '/rss/techcrunch/tag/layoffs/feed/' },
|
||||
{ name: 'Layoffs News', url: '/rss/googlenews/rss/search?q=tech+layoffs+2025+job+cuts&hl=en-US&gl=US&ceid=US:en' },
|
||||
{ name: 'Layoffs News', url: '/rss/googlenews/rss/search?q=tech+layoffs+2026+job+cuts&hl=en-US&gl=US&ceid=US:en' },
|
||||
],
|
||||
congress: [
|
||||
{ name: 'Congress Trades', url: '/rss/googlenews/rss/search?q=congress+stock+trading+pelosi+tuberville&hl=en-US&gl=US&ceid=US:en' },
|
||||
@@ -119,7 +128,7 @@ export const FEEDS: Record<string, Feed[]> = {
|
||||
export const INTEL_SOURCES: Feed[] = [
|
||||
{ name: 'Defense One', url: '/rss/defenseone/rss/all/', type: 'defense' },
|
||||
{ name: 'Breaking Defense', url: '/rss/breakingdefense/feed/', type: 'defense' },
|
||||
{ name: 'The War Zone', url: '/rss/warzone/the-war-zone/feed', type: 'defense' },
|
||||
{ name: 'The War Zone', url: '/rss/googlenews/rss/search?q=site:thedrive.com/the-war-zone&hl=en-US&gl=US&ceid=US:en', type: 'defense' },
|
||||
{ name: 'Defense News', url: '/rss/googlenews/rss/search?q=defense+military+pentagon&hl=en-US&gl=US&ceid=US:en', type: 'defense' },
|
||||
{ name: 'Bellingcat', url: '/rss/bellingcat/feed/', type: 'osint' },
|
||||
{ name: 'Krebs Security', url: '/rss/krebs/feed/', type: 'cyber' },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Hotspot, ConflictZone, MilitaryBase, UnderseaCable, NuclearFacility, StrategicWaterway, APTGroup } from '@/types';
|
||||
import type { Hotspot, ConflictZone, MilitaryBase, UnderseaCable, NuclearFacility, StrategicWaterway, APTGroup, EconomicCenter } from '@/types';
|
||||
import { MILITARY_BASES_EXPANDED } from './bases-expanded';
|
||||
|
||||
// Hotspot levels are NOT hardcoded - they are dynamically calculated based on news activity
|
||||
// All hotspots start at 'low' and rise to 'elevated' or 'high' based on matching news items
|
||||
@@ -14,6 +15,39 @@ export const INTEL_HOTSPOTS: Hotspot[] = [
|
||||
description: 'US government and military headquarters. Intelligence community center.',
|
||||
status: 'Monitoring',
|
||||
},
|
||||
{
|
||||
id: 'silicon_valley',
|
||||
name: 'Silicon Valley',
|
||||
subtext: 'Tech/AI Hub',
|
||||
lat: 37.4,
|
||||
lon: -122.1,
|
||||
keywords: ['google', 'apple', 'meta', 'nvidia', 'openai', 'anthropic', 'silicon valley', 'san francisco', 'palo alto', 'tech layoffs', 'ai', 'artificial intelligence'],
|
||||
agencies: ['Big Tech', 'AI Labs', 'VC'],
|
||||
description: 'Global tech center. AI development hub. Major economic indicator.',
|
||||
status: 'Monitoring',
|
||||
},
|
||||
{
|
||||
id: 'wall_street',
|
||||
name: 'Wall Street',
|
||||
subtext: 'Financial Hub',
|
||||
lat: 40.7,
|
||||
lon: -74.0,
|
||||
keywords: ['wall street', 'fed', 'federal reserve', 'nyse', 'nasdaq', 'dow', 'sp500', 'stock market', 'goldman', 'jpmorgan', 'blackrock'],
|
||||
agencies: ['Fed', 'SEC', 'NYSE'],
|
||||
description: 'Global financial center. Market movements. Fed policy.',
|
||||
status: 'Monitoring',
|
||||
},
|
||||
{
|
||||
id: 'houston',
|
||||
name: 'Houston',
|
||||
subtext: 'Energy/Space',
|
||||
lat: 29.76,
|
||||
lon: -95.37,
|
||||
keywords: ['houston', 'nasa', 'spacex', 'oil', 'energy', 'texas', 'exxon', 'chevron', 'lng'],
|
||||
agencies: ['NASA', 'Energy Corps'],
|
||||
description: 'Energy sector HQ. NASA mission control. Space industry.',
|
||||
status: 'Monitoring',
|
||||
},
|
||||
{
|
||||
id: 'moscow',
|
||||
name: 'Moscow',
|
||||
@@ -244,6 +278,9 @@ export const STRATEGIC_WATERWAYS: StrategicWaterway[] = [
|
||||
{ id: 'bosphorus', name: 'BOSPHORUS STRAIT', lat: 41.1, lon: 29.0, description: 'Black Sea access, Turkey control' },
|
||||
{ id: 'suez', name: 'SUEZ CANAL', lat: 30.5, lon: 32.3, description: 'Europe-Asia shipping' },
|
||||
{ id: 'panama', name: 'PANAMA CANAL', lat: 9.1, lon: -79.7, description: 'Americas shipping route' },
|
||||
{ id: 'gibraltar', name: 'STRAIT OF GIBRALTAR', lat: 35.9, lon: -5.6, description: 'Mediterranean access, NATO control' },
|
||||
{ id: 'bab_el_mandeb', name: 'BAB EL-MANDEB', lat: 12.5, lon: 43.3, description: 'Red Sea chokepoint, Houthi attacks' },
|
||||
{ id: 'dardanelles', name: 'DARDANELLES', lat: 40.2, lon: 26.4, description: 'Aegean-Marmara link, Turkey control' },
|
||||
];
|
||||
|
||||
export const APT_GROUPS: APTGroup[] = [
|
||||
@@ -316,32 +353,55 @@ export const CONFLICT_ZONES: ConflictZone[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const MILITARY_BASES: MilitaryBase[] = [
|
||||
// US/NATO
|
||||
{ id: 'ramstein', name: 'Ramstein AB', lat: 49.44, lon: 7.6, type: 'us-nato' },
|
||||
{ id: 'diego_garcia', name: 'Diego Garcia', lat: -7.32, lon: 72.42, type: 'us-nato' },
|
||||
{ id: 'okinawa', name: 'Okinawa', lat: 26.5, lon: 127.9, type: 'us-nato' },
|
||||
{ id: 'guam', name: 'Guam', lat: 13.45, lon: 144.8, type: 'us-nato' },
|
||||
{ id: 'qatar', name: 'Al Udeid AB', lat: 25.12, lon: 51.32, type: 'us-nato' },
|
||||
{ id: 'djibouti_us', name: 'Camp Lemonnier', lat: 11.55, lon: 43.15, type: 'us-nato' },
|
||||
{ id: 'bahrain', name: 'NSA Bahrain', lat: 26.23, lon: 50.58, type: 'us-nato' },
|
||||
{ id: 'yokosuka', name: 'Yokosuka', lat: 35.28, lon: 139.67, type: 'us-nato' },
|
||||
{ id: 'rota', name: 'Naval Rota', lat: 36.62, lon: -6.35, type: 'us-nato' },
|
||||
{ id: 'incirlik', name: 'Incirlik AB', lat: 37.0, lon: 35.43, type: 'us-nato' },
|
||||
// China
|
||||
{ id: 'djibouti_cn', name: 'PLA Djibouti', lat: 11.59, lon: 43.05, type: 'china' },
|
||||
{ id: 'woody_island', name: 'Woody Island', lat: 16.83, lon: 112.33, type: 'china' },
|
||||
{ id: 'fiery_cross', name: 'Fiery Cross', lat: 9.55, lon: 112.89, type: 'china' },
|
||||
{ id: 'mischief_reef', name: 'Mischief Reef', lat: 9.9, lon: 115.53, type: 'china' },
|
||||
{ id: 'subi_reef', name: 'Subi Reef', lat: 10.92, lon: 114.08, type: 'china' },
|
||||
// Russia
|
||||
{ id: 'kaliningrad', name: 'Kaliningrad', lat: 54.71, lon: 20.51, type: 'russia' },
|
||||
{ id: 'tartus', name: 'Tartus (Syria)', lat: 34.89, lon: 35.87, type: 'russia' },
|
||||
{ id: 'sevastopol', name: 'Sevastopol', lat: 44.6, lon: 33.5, type: 'russia' },
|
||||
{ id: 'vladivostok', name: 'Vladivostok', lat: 43.12, lon: 131.9, type: 'russia' },
|
||||
{ id: 'murmansk', name: 'Murmansk', lat: 68.97, lon: 33.09, type: 'russia' },
|
||||
// US Domestic bases (not in overseas dataset - these are CONUS bases)
|
||||
const US_DOMESTIC_BASES: MilitaryBase[] = [
|
||||
{ id: 'norfolk', name: 'Norfolk Naval', lat: 36.95, lon: -76.31, type: 'us-nato', description: 'World largest naval base. Atlantic Fleet HQ.' },
|
||||
{ id: 'fort_liberty', name: 'Fort Liberty', lat: 35.14, lon: -79.0, type: 'us-nato', description: 'Army Special Ops. XVIII Airborne Corps.' },
|
||||
{ id: 'pendleton', name: 'Camp Pendleton', lat: 33.38, lon: -117.4, type: 'us-nato', description: 'USMC West Coast. 1st Marine Division.' },
|
||||
{ id: 'san_diego', name: 'Naval San Diego', lat: 32.68, lon: -117.13, type: 'us-nato', description: 'Pacific Fleet. Carrier homeport.' },
|
||||
{ id: 'nellis', name: 'Nellis AFB', lat: 36.24, lon: -115.03, type: 'us-nato', description: 'Air combat training. Red Flag exercises.' },
|
||||
{ id: 'langley', name: 'Langley AFB', lat: 37.08, lon: -76.36, type: 'us-nato', description: 'Air Combat Command HQ. F-22 wing.' },
|
||||
{ id: 'cheyenne', name: 'Cheyenne Mtn', lat: 38.74, lon: -104.85, type: 'us-nato', description: 'NORAD. Missile warning, space control.' },
|
||||
{ id: 'peterson', name: 'Peterson SFB', lat: 38.82, lon: -104.71, type: 'us-nato', description: 'US Space Command HQ. Space operations.' },
|
||||
{ id: 'kings_bay', name: 'Kings Bay', lat: 30.8, lon: -81.52, type: 'us-nato', description: 'Ohio-class submarine base. Atlantic deterrent.' },
|
||||
{ id: 'kitsap', name: 'Naval Kitsap', lat: 47.56, lon: -122.66, type: 'us-nato', description: 'Trident submarine base. Pacific deterrent.' },
|
||||
{ id: 'yokosuka', name: 'Yokosuka', lat: 35.28, lon: 139.67, type: 'us-nato', description: 'US 7th Fleet HQ. Carrier strike group homeport.' },
|
||||
{ id: 'rota', name: 'Naval Rota', lat: 36.62, lon: -6.35, type: 'us-nato', description: 'US/Spanish naval base. Aegis destroyers, Atlantic access.' },
|
||||
{ id: 'incirlik', name: 'Incirlik AB', lat: 37.0, lon: 35.43, type: 'us-nato', description: 'US/Turkish base. Nuclear weapons storage site.' },
|
||||
// Russian domestic bases (not overseas)
|
||||
{ id: 'kaliningrad', name: 'Kaliningrad', lat: 54.71, lon: 20.51, type: 'russia', description: 'Russian exclave. Baltic Fleet, Iskander missiles.' },
|
||||
{ id: 'sevastopol', name: 'Sevastopol', lat: 44.6, lon: 33.5, type: 'russia', description: 'Black Sea Fleet HQ. Crimea (occupied).' },
|
||||
{ id: 'vladivostok', name: 'Vladivostok', lat: 43.12, lon: 131.9, type: 'russia', description: 'Pacific Fleet HQ. Nuclear submarines.' },
|
||||
{ id: 'murmansk', name: 'Murmansk', lat: 68.97, lon: 33.09, type: 'russia', description: 'Northern Fleet. Strategic nuclear submarines.' },
|
||||
];
|
||||
|
||||
// Merge expanded bases with domestic bases, deduplicating by proximity
|
||||
function mergeAndDeduplicateBases(): MilitaryBase[] {
|
||||
const allBases = [...MILITARY_BASES_EXPANDED];
|
||||
const usedCoords = new Set<string>();
|
||||
|
||||
// Index expanded bases by approximate location
|
||||
for (const base of MILITARY_BASES_EXPANDED) {
|
||||
const key = `${Math.round(base.lat * 10)}_${Math.round(base.lon * 10)}`;
|
||||
usedCoords.add(key);
|
||||
}
|
||||
|
||||
// Add domestic bases if not already present (by location proximity)
|
||||
for (const base of US_DOMESTIC_BASES) {
|
||||
const key = `${Math.round(base.lat * 10)}_${Math.round(base.lon * 10)}`;
|
||||
if (!usedCoords.has(key)) {
|
||||
allBases.push(base);
|
||||
usedCoords.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return allBases;
|
||||
}
|
||||
|
||||
// Combined military bases: 210 from ASIAR dataset + unique domestic bases
|
||||
// Total: ~220 bases from 9 operators (US-NATO, UK, France, Russia, China, India, Italy, UAE, Japan)
|
||||
export const MILITARY_BASES: MilitaryBase[] = mergeAndDeduplicateBases();
|
||||
|
||||
export const UNDERSEA_CABLES: UnderseaCable[] = [
|
||||
{
|
||||
id: 'transatlantic_1',
|
||||
@@ -382,6 +442,14 @@ export const UNDERSEA_CABLES: UnderseaCable[] = [
|
||||
];
|
||||
|
||||
export const NUCLEAR_FACILITIES: NuclearFacility[] = [
|
||||
// US Nuclear Labs & Weapons Complex
|
||||
{ id: 'los_alamos', name: 'Los Alamos', lat: 35.88, lon: -106.31, type: 'weapons', status: 'active' },
|
||||
{ id: 'sandia', name: 'Sandia Labs', lat: 35.04, lon: -106.54, type: 'weapons', status: 'active' },
|
||||
{ id: 'livermore', name: 'LLNL', lat: 37.69, lon: -121.7, type: 'weapons', status: 'active' },
|
||||
{ id: 'oak_ridge', name: 'Oak Ridge', lat: 35.93, lon: -84.31, type: 'enrichment', status: 'active' },
|
||||
{ id: 'hanford', name: 'Hanford', lat: 46.55, lon: -119.49, type: 'weapons', status: 'inactive' },
|
||||
{ id: 'pantex', name: 'Pantex', lat: 35.32, lon: -101.55, type: 'weapons', status: 'active' },
|
||||
// Foreign Nuclear
|
||||
{ id: 'zaporizhzhia', name: 'Zaporizhzhia NPP', lat: 47.51, lon: 34.58, type: 'plant', status: 'contested' },
|
||||
{ id: 'natanz', name: 'Natanz', lat: 33.72, lon: 51.73, type: 'enrichment', status: 'active' },
|
||||
{ id: 'fordow', name: 'Fordow', lat: 34.88, lon: 51.0, type: 'enrichment', status: 'active' },
|
||||
@@ -485,3 +553,53 @@ export const COUNTRY_LABELS: CountryLabel[] = [
|
||||
{ id: 36, name: 'Australia', lat: -25.3, lon: 133.8 },
|
||||
{ id: 554, name: 'New Zealand', lat: -41.0, lon: 174.9 },
|
||||
];
|
||||
|
||||
// Global Economic Centers - Stock Exchanges, Central Banks, Financial Hubs
|
||||
export const ECONOMIC_CENTERS: EconomicCenter[] = [
|
||||
// Americas
|
||||
{ id: 'nyse', name: 'NYSE', type: 'exchange', lat: 40.7069, lon: -74.0089, country: 'USA', marketHours: { open: '09:30', close: '16:00', timezone: 'America/New_York' }, description: 'New York Stock Exchange - World\'s largest stock exchange' },
|
||||
{ id: 'nasdaq', name: 'NASDAQ', type: 'exchange', lat: 40.7569, lon: -73.9896, country: 'USA', marketHours: { open: '09:30', close: '16:00', timezone: 'America/New_York' }, description: 'Tech-heavy exchange' },
|
||||
{ id: 'fed', name: 'Federal Reserve', type: 'central-bank', lat: 38.8927, lon: -77.0459, country: 'USA', description: 'US Central Bank - Controls USD monetary policy' },
|
||||
{ id: 'cme', name: 'CME Group', type: 'exchange', lat: 41.8819, lon: -87.6278, country: 'USA', description: 'Chicago Mercantile Exchange - Futures & derivatives' },
|
||||
{ id: 'tsx', name: 'TSX', type: 'exchange', lat: 43.6489, lon: -79.3850, country: 'Canada', marketHours: { open: '09:30', close: '16:00', timezone: 'America/Toronto' }, description: 'Toronto Stock Exchange' },
|
||||
{ id: 'bovespa', name: 'B3', type: 'exchange', lat: -23.5505, lon: -46.6333, country: 'Brazil', description: 'Brazilian Stock Exchange (B3/Bovespa)' },
|
||||
// Europe
|
||||
{ id: 'lse', name: 'LSE', type: 'exchange', lat: 51.5145, lon: -0.0940, country: 'UK', marketHours: { open: '08:00', close: '16:30', timezone: 'Europe/London' }, description: 'London Stock Exchange' },
|
||||
{ id: 'boe', name: 'Bank of England', type: 'central-bank', lat: 51.5142, lon: -0.0880, country: 'UK', description: 'UK Central Bank' },
|
||||
{ id: 'ecb', name: 'ECB', type: 'central-bank', lat: 50.1096, lon: 8.6732, country: 'Germany', description: 'European Central Bank - Controls EUR' },
|
||||
{ id: 'euronext', name: 'Euronext', type: 'exchange', lat: 48.8690, lon: 2.3364, country: 'France', marketHours: { open: '09:00', close: '17:30', timezone: 'Europe/Paris' }, description: 'Pan-European Exchange (Paris, Amsterdam, Brussels, Lisbon)' },
|
||||
{ id: 'dax', name: 'Deutsche Börse', type: 'exchange', lat: 50.1109, lon: 8.6821, country: 'Germany', marketHours: { open: '09:00', close: '17:30', timezone: 'Europe/Berlin' }, description: 'Frankfurt Stock Exchange - DAX' },
|
||||
{ id: 'six', name: 'SIX Swiss', type: 'exchange', lat: 47.3769, lon: 8.5417, country: 'Switzerland', description: 'Swiss Exchange' },
|
||||
{ id: 'snb', name: 'SNB', type: 'central-bank', lat: 46.9480, lon: 7.4474, country: 'Switzerland', description: 'Swiss National Bank' },
|
||||
// Asia-Pacific
|
||||
{ id: 'tse', name: 'Tokyo SE', type: 'exchange', lat: 35.6830, lon: 139.7744, country: 'Japan', marketHours: { open: '09:00', close: '15:00', timezone: 'Asia/Tokyo' }, description: 'Tokyo Stock Exchange - Nikkei' },
|
||||
{ id: 'boj', name: 'Bank of Japan', type: 'central-bank', lat: 35.6855, lon: 139.7579, country: 'Japan', description: 'Japan Central Bank - Controls JPY' },
|
||||
{ id: 'sse', name: 'Shanghai SE', type: 'exchange', lat: 31.2304, lon: 121.4737, country: 'China', marketHours: { open: '09:30', close: '15:00', timezone: 'Asia/Shanghai' }, description: 'Shanghai Stock Exchange' },
|
||||
{ id: 'szse', name: 'Shenzhen SE', type: 'exchange', lat: 22.5431, lon: 114.0579, country: 'China', description: 'Shenzhen Stock Exchange - Tech focus' },
|
||||
{ id: 'pboc', name: 'PBOC', type: 'central-bank', lat: 39.9208, lon: 116.4074, country: 'China', description: 'People\'s Bank of China - Controls CNY' },
|
||||
{ id: 'hkex', name: 'HKEX', type: 'exchange', lat: 22.2833, lon: 114.1577, country: 'Hong Kong', marketHours: { open: '09:30', close: '16:00', timezone: 'Asia/Hong_Kong' }, description: 'Hong Kong Exchange' },
|
||||
{ id: 'sgx', name: 'SGX', type: 'exchange', lat: 1.2834, lon: 103.8607, country: 'Singapore', description: 'Singapore Exchange' },
|
||||
{ id: 'mas', name: 'MAS', type: 'central-bank', lat: 1.2789, lon: 103.8536, country: 'Singapore', description: 'Monetary Authority of Singapore' },
|
||||
{ id: 'kospi', name: 'KRX', type: 'exchange', lat: 37.5665, lon: 126.9780, country: 'South Korea', marketHours: { open: '09:00', close: '15:30', timezone: 'Asia/Seoul' }, description: 'Korea Exchange - KOSPI' },
|
||||
{ id: 'bse', name: 'BSE', type: 'exchange', lat: 18.9307, lon: 72.8335, country: 'India', marketHours: { open: '09:15', close: '15:30', timezone: 'Asia/Kolkata' }, description: 'Bombay Stock Exchange - Sensex' },
|
||||
{ id: 'nse', name: 'NSE India', type: 'exchange', lat: 19.0571, lon: 72.8621, country: 'India', description: 'National Stock Exchange - Nifty' },
|
||||
{ id: 'rbi', name: 'RBI', type: 'central-bank', lat: 18.9322, lon: 72.8351, country: 'India', description: 'Reserve Bank of India' },
|
||||
{ id: 'asx', name: 'ASX', type: 'exchange', lat: -33.8688, lon: 151.2093, country: 'Australia', marketHours: { open: '10:00', close: '16:00', timezone: 'Australia/Sydney' }, description: 'Australian Securities Exchange' },
|
||||
{ id: 'rba', name: 'RBA', type: 'central-bank', lat: -33.8654, lon: 151.2105, country: 'Australia', description: 'Reserve Bank of Australia' },
|
||||
// Middle East & Africa
|
||||
{ id: 'tadawul', name: 'Tadawul', type: 'exchange', lat: 24.6877, lon: 46.7219, country: 'Saudi Arabia', marketHours: { open: '10:00', close: '15:00', timezone: 'Asia/Riyadh' }, description: 'Saudi Stock Exchange - Largest in Arab world' },
|
||||
{ id: 'adx', name: 'ADX', type: 'exchange', lat: 24.4539, lon: 54.3773, country: 'UAE', marketHours: { open: '10:00', close: '14:00', timezone: 'Asia/Dubai' }, description: 'Abu Dhabi Securities Exchange' },
|
||||
{ id: 'dfm', name: 'DFM', type: 'exchange', lat: 25.2221, lon: 55.2867, country: 'UAE', marketHours: { open: '10:00', close: '14:00', timezone: 'Asia/Dubai' }, description: 'Dubai Financial Market' },
|
||||
{ id: 'qse', name: 'QSE', type: 'exchange', lat: 25.2854, lon: 51.5310, country: 'Qatar', marketHours: { open: '09:30', close: '13:15', timezone: 'Asia/Qatar' }, description: 'Qatar Stock Exchange' },
|
||||
{ id: 'bkw', name: 'Boursa Kuwait', type: 'exchange', lat: 29.3759, lon: 47.9774, country: 'Kuwait', marketHours: { open: '09:00', close: '12:30', timezone: 'Asia/Kuwait' }, description: 'Kuwait Stock Exchange' },
|
||||
{ id: 'bse_bahrain', name: 'Bahrain Bourse', type: 'exchange', lat: 26.2285, lon: 50.5860, country: 'Bahrain', description: 'Bahrain Stock Exchange' },
|
||||
{ id: 'egx', name: 'EGX', type: 'exchange', lat: 30.0444, lon: 31.2357, country: 'Egypt', marketHours: { open: '10:00', close: '14:30', timezone: 'Africa/Cairo' }, description: 'Egyptian Exchange - Cairo' },
|
||||
{ id: 'tase', name: 'TASE', type: 'exchange', lat: 32.0853, lon: 34.7818, country: 'Israel', marketHours: { open: '09:59', close: '17:14', timezone: 'Asia/Jerusalem' }, description: 'Tel Aviv Stock Exchange' },
|
||||
{ id: 'jse', name: 'JSE', type: 'exchange', lat: -26.1447, lon: 28.0381, country: 'South Africa', marketHours: { open: '09:00', close: '17:00', timezone: 'Africa/Johannesburg' }, description: 'Johannesburg Stock Exchange' },
|
||||
{ id: 'nse_nigeria', name: 'NGX', type: 'exchange', lat: 6.4541, lon: 3.4218, country: 'Nigeria', description: 'Nigerian Exchange Group - Lagos' },
|
||||
{ id: 'casa', name: 'Casablanca SE', type: 'exchange', lat: 33.5731, lon: -7.5898, country: 'Morocco', description: 'Casablanca Stock Exchange' },
|
||||
// Financial Hubs (not exchanges but major centers)
|
||||
{ id: 'dubai_hub', name: 'DIFC', type: 'financial-hub', lat: 25.2116, lon: 55.2708, country: 'UAE', description: 'Dubai International Financial Centre' },
|
||||
{ id: 'cayman', name: 'Cayman Islands', type: 'financial-hub', lat: 19.3133, lon: -81.2546, country: 'Cayman Islands', description: 'Offshore financial center' },
|
||||
{ id: 'luxembourg', name: 'Luxembourg', type: 'financial-hub', lat: 49.6116, lon: 6.1319, country: 'Luxembourg', description: 'European investment fund center' },
|
||||
];
|
||||
|
||||
@@ -2,6 +2,9 @@ export * from './feeds';
|
||||
export * from './markets';
|
||||
export * from './geo';
|
||||
export * from './panels';
|
||||
export * from './irradiators';
|
||||
export * from './pipelines';
|
||||
export * from './ai-datacenters';
|
||||
|
||||
export const API_URLS = {
|
||||
yahooFinance: (symbol: string) =>
|
||||
|
||||
131
src/config/irradiators.ts
Normal file
131
src/config/irradiators.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import type { GammaIrradiator } from '@/types';
|
||||
|
||||
// IAEA DIIF - Database on Industrial Irradiation Facilities
|
||||
// Source: https://public.tableau.com/app/profile/acceleratorknowledgeportal/viz/IrradiatorDatabase/Home
|
||||
// Extracted: 2026-01-09
|
||||
export const GAMMA_IRRADIATORS: GammaIrradiator[] = [
|
||||
{ id: 'gi-001', city: 'Vega Alta', country: 'Puerto Rico', lat: 18.420295, lon: -66.334995 },
|
||||
{ id: 'gi-002', city: 'Northborough, MA', country: 'USA', lat: 42.311091, lon: -71.649221 },
|
||||
{ id: 'gi-003', city: 'Chester, NY', country: 'USA', lat: 41.323774, lon: -74.289753 },
|
||||
{ id: 'gi-004', city: 'Whippany, NJ', country: 'USA', lat: 40.825619, lon: -74.40563 },
|
||||
{ id: 'gi-005', city: 'South Plainfield, NJ', country: 'USA', lat: 40.579733, lon: -74.424443 },
|
||||
{ id: 'gi-006', city: 'Rockaway, NJ', country: 'USA', lat: 40.895771, lon: -74.522053 },
|
||||
{ id: 'gi-007', city: 'Salem, NJ', country: 'USA', lat: 39.569018, lon: -75.465052 },
|
||||
{ id: 'gi-008', city: 'Durham, NC', country: 'USA', lat: 35.975972, lon: -78.884913 },
|
||||
{ id: 'gi-009', city: 'Haw River, NC', country: 'USA', lat: 36.088692, lon: -79.375943 },
|
||||
{ id: 'gi-010', city: 'Mississauga, ON', country: 'Canada', lat: 43.579934, lon: -79.628163 },
|
||||
{ id: 'gi-011', city: 'Charlotte, NC', country: 'USA', lat: 35.202512, lon: -80.794517 },
|
||||
{ id: 'gi-012', city: 'Spartanburg, SC', country: 'USA', lat: 34.940428, lon: -81.939245 },
|
||||
{ id: 'gi-013', city: 'Mulberry, FL', country: 'USA', lat: 27.898651, lon: -81.971679 },
|
||||
{ id: 'gi-014', city: 'Groveport, OH', country: 'USA', lat: 39.852653, lon: -82.888091 },
|
||||
{ id: 'gi-015', city: 'Westerville, OH', country: 'USA', lat: 40.115286, lon: -82.918498 },
|
||||
{ id: 'gi-016', city: 'Gurnee, IL', country: 'USA', lat: 42.376292, lon: -87.895077 },
|
||||
{ id: 'gi-017', city: 'Libertyville, IL', country: 'USA', lat: 42.274642, lon: -87.960518 },
|
||||
{ id: 'gi-018', city: 'Schaumburg, IL', country: 'USA', lat: 42.053589, lon: -88.048792 },
|
||||
{ id: 'gi-019', city: 'Memphis, TN', country: 'USA', lat: 35.140839, lon: -90.187236 },
|
||||
{ id: 'gi-020', city: 'Metapa de Dominguez', country: 'Mexico', lat: 14.836864, lon: -92.195166 },
|
||||
{ id: 'gi-021', city: 'Minneapolis, MN', country: 'USA', lat: 44.950269, lon: -93.246374 },
|
||||
{ id: 'gi-022', city: 'Grand Prairie, TX', country: 'USA', lat: 32.720166, lon: -97.020252 },
|
||||
{ id: 'gi-023', city: 'Fort Worth, TX', country: 'USA', lat: 32.766098, lon: -97.324345 },
|
||||
{ id: 'gi-024', city: 'Tepeji del Rio', country: 'Mexico', lat: 19.901705, lon: -99.341524 },
|
||||
{ id: 'gi-025', city: 'Toluca', country: 'Mexico', lat: 19.286008, lon: -99.644432 },
|
||||
{ id: 'gi-026', city: 'Matehuala', country: 'Mexico', lat: 23.645547, lon: -100.653481 },
|
||||
{ id: 'gi-027', city: 'El Paso, TX', country: 'USA', lat: 31.794133, lon: -106.387446 },
|
||||
{ id: 'gi-028', city: 'Sandy, UT', country: 'USA', lat: 40.54449, lon: -111.833074 },
|
||||
{ id: 'gi-029', city: 'Temecula, CA', country: 'USA', lat: 33.468294, lon: -117.105422 },
|
||||
{ id: 'gi-030', city: 'San Diego, CA', country: 'USA', lat: 32.751164, lon: -117.200996 },
|
||||
{ id: 'gi-031', city: 'Corona, CA', country: 'USA', lat: 33.870026, lon: -117.56985 },
|
||||
{ id: 'gi-032', city: 'Tustin, CA', country: 'USA', lat: 33.735654, lon: -117.823925 },
|
||||
{ id: 'gi-033', city: 'Gilroy, CA', country: 'USA', lat: 37.010533, lon: -121.588342 },
|
||||
{ id: 'gi-034', city: 'Hayward, CA', country: 'USA', lat: 37.659417, lon: -122.090256 },
|
||||
{ id: 'gi-035', city: 'Kunia Camp, HI', country: 'USA', lat: 21.462766, lon: -158.057863 },
|
||||
{ id: 'gi-036', city: 'Sao Paulo', country: 'Brazil', lat: -23.542047, lon: -46.58432 },
|
||||
{ id: 'gi-037', city: 'Cotia', country: 'Brazil', lat: -23.580141, lon: -46.656614 },
|
||||
{ id: 'gi-038', city: 'Sao Paulo Region', country: 'Brazil', lat: -23.598327, lon: -46.916378 },
|
||||
{ id: 'gi-039', city: 'Buenos Aires', country: 'Argentina', lat: -34.636145, lon: -58.472782 },
|
||||
{ id: 'gi-040', city: 'La Paz', country: 'Bolivia', lat: -16.546576, lon: -68.207797 },
|
||||
{ id: 'gi-041', city: 'Bogota', country: 'Colombia', lat: 4.632938, lon: -74.075412 },
|
||||
{ id: 'gi-042', city: 'Panama City', country: 'Panama', lat: 9.071441, lon: -79.300808 },
|
||||
{ id: 'gi-043', city: 'Havana', country: 'Cuba', lat: 23.120885, lon: -82.423354 },
|
||||
{ id: 'gi-044', city: 'Havana', country: 'Cuba', lat: 23.009447, lon: -82.490902 },
|
||||
{ id: 'gi-045', city: 'Moscow', country: 'Russia', lat: 55.717376, lon: 37.689322 },
|
||||
{ id: 'gi-046', city: 'Obninsk', country: 'Russia', lat: 55.113284, lon: 36.593435 },
|
||||
{ id: 'gi-047', city: 'Minsk', country: 'Belarus', lat: 53.892511, lon: 27.563155 },
|
||||
{ id: 'gi-048', city: 'Magurele', country: 'Romania', lat: 44.374433, lon: 26.050775 },
|
||||
{ id: 'gi-049', city: 'Alliku', country: 'Estonia', lat: 59.355411, lon: 24.591014 },
|
||||
{ id: 'gi-050', city: 'Sofia', country: 'Bulgaria', lat: 42.685821, lon: 23.294419 },
|
||||
{ id: 'gi-051', city: 'Michalovce', country: 'Slovakia', lat: 48.7612, lon: 21.898753 },
|
||||
{ id: 'gi-052', city: 'Velká Bíteš', country: 'Czech Republic', lat: 49.288579, lon: 16.223873 },
|
||||
{ id: 'gi-053', city: 'Belgrade', country: 'Serbia', lat: 44.758887, lon: 20.598464 },
|
||||
{ id: 'gi-054', city: 'Budapest', country: 'Hungary', lat: 47.489017, lon: 19.14197 },
|
||||
{ id: 'gi-055', city: 'Seibersdorf', country: 'Austria', lat: 47.95946, lon: 16.516047 },
|
||||
{ id: 'gi-056', city: 'Veverská Bítýška', country: 'Czech Republic', lat: 49.274109, lon: 16.43573 },
|
||||
{ id: 'gi-057', city: 'Zagreb', country: 'Croatia', lat: 45.794472, lon: 16.017888 },
|
||||
{ id: 'gi-058', city: 'Radeberg', country: 'Germany', lat: 51.109509, lon: 13.917448 },
|
||||
{ id: 'gi-059', city: 'Roskilde', country: 'Denmark', lat: 55.643201, lon: 12.069288 },
|
||||
{ id: 'gi-060', city: 'Allershausen', country: 'Germany', lat: 48.42663, lon: 11.598988 },
|
||||
{ id: 'gi-061', city: 'Minerbio', country: 'Italy', lat: 44.616154, lon: 11.470066 },
|
||||
{ id: 'gi-062', city: 'Baden-Württemberg', country: 'Germany', lat: 48.806391, lon: 9.3215 },
|
||||
{ id: 'gi-063', city: 'Lomazzo', country: 'Italy', lat: 45.697924, lon: 9.027723 },
|
||||
{ id: 'gi-064', city: 'Däniken', country: 'Switzerland', lat: 47.365354, lon: 7.967939 },
|
||||
{ id: 'gi-065', city: 'Wiehl', country: 'Germany', lat: 50.95633, lon: 7.537838 },
|
||||
{ id: 'gi-066', city: 'Venlo', country: 'Netherlands', lat: 51.364552, lon: 6.176397 },
|
||||
{ id: 'gi-067', city: 'Ede', country: 'Netherlands', lat: 52.039888, lon: 5.666329 },
|
||||
{ id: 'gi-068', city: 'Marseille', country: 'France', lat: 43.323344, lon: 5.395258 },
|
||||
{ id: 'gi-069', city: 'Dagneux', country: 'France', lat: 45.855206, lon: 5.075505 },
|
||||
{ id: 'gi-070', city: 'Chusclan', country: 'France', lat: 44.148579, lon: 4.679974 },
|
||||
{ id: 'gi-071', city: 'Etten-Leur', country: 'Netherlands', lat: 51.573407, lon: 4.626725 },
|
||||
{ id: 'gi-072', city: 'Fleurus', country: 'Belgium', lat: 50.483418, lon: 4.540843 },
|
||||
{ id: 'gi-073', city: 'Sablé-sur-Sarthe', country: 'France', lat: 47.837859, lon: -0.344394 },
|
||||
{ id: 'gi-074', city: 'Pouzauges', country: 'France', lat: 46.782139, lon: -0.837019 },
|
||||
{ id: 'gi-075', city: 'Tilehurst', country: 'UK', lat: 51.453028, lon: -1.013584 },
|
||||
{ id: 'gi-076', city: 'Northants', country: 'UK', lat: 52.27069, lon: -1.182336 },
|
||||
{ id: 'gi-077', city: 'Chesterfield', country: 'UK', lat: 53.266866, lon: -1.322859 },
|
||||
{ id: 'gi-078', city: 'Sheffield', country: 'UK', lat: 53.38034, lon: -1.470618 },
|
||||
{ id: 'gi-079', city: 'Swindon', country: 'UK', lat: 51.575779, lon: -1.766416 },
|
||||
{ id: 'gi-080', city: 'Westport', country: 'Ireland', lat: 53.797423, lon: -9.530576 },
|
||||
{ id: 'gi-081', city: 'Jeollabuk-do', country: 'South Korea', lat: 35.82, lon: 127.15 },
|
||||
{ id: 'gi-082', city: 'Quezon City', country: 'Philippines', lat: 14.66, lon: 121.06 },
|
||||
{ id: 'gi-083', city: 'Jiangsu Province', country: 'China', lat: 32.06, lon: 118.78 },
|
||||
{ id: 'gi-084', city: 'Jakarta', country: 'Indonesia', lat: -6.23, lon: 106.82 },
|
||||
{ id: 'gi-085', city: 'Selangor', country: 'Malaysia', lat: 3.07, lon: 101.50 },
|
||||
{ id: 'gi-086', city: 'Rayong', country: 'Thailand', lat: 12.68, lon: 101.28 },
|
||||
{ id: 'gi-087', city: 'Ongkharak', country: 'Thailand', lat: 14.12, lon: 100.99 },
|
||||
{ id: 'gi-088', city: 'Chonburi', country: 'Thailand', lat: 13.36, lon: 100.98 },
|
||||
{ id: 'gi-089', city: 'Kedah', country: 'Malaysia', lat: 6.12, lon: 100.37 },
|
||||
{ id: 'gi-090', city: 'Yangon', country: 'Myanmar', lat: 16.87, lon: 96.20 },
|
||||
{ id: 'gi-091', city: 'Kolkata', country: 'India', lat: 22.57, lon: 88.36 },
|
||||
{ id: 'gi-092', city: 'Unnao', country: 'India', lat: 26.54, lon: 80.49 },
|
||||
{ id: 'gi-093', city: 'Malwana', country: 'Sri Lanka', lat: 7.00, lon: 80.00 },
|
||||
{ id: 'gi-094', city: 'Telangana', country: 'India', lat: 17.39, lon: 78.49 },
|
||||
{ id: 'gi-095', city: 'Malur', country: 'India', lat: 13.00, lon: 77.94 },
|
||||
{ id: 'gi-096', city: 'Bangalore', country: 'India', lat: 12.97, lon: 77.59 },
|
||||
{ id: 'gi-097', city: 'Delhi', country: 'India', lat: 28.61, lon: 77.21 },
|
||||
{ id: 'gi-098', city: 'Bhiwadi', country: 'India', lat: 28.21, lon: 76.86 },
|
||||
{ id: 'gi-099', city: 'Dharuhera', country: 'India', lat: 28.21, lon: 76.80 },
|
||||
{ id: 'gi-100', city: 'Dewas', country: 'India', lat: 22.97, lon: 76.05 },
|
||||
{ id: 'gi-101', city: 'Rahuri', country: 'India', lat: 19.39, lon: 74.65 },
|
||||
{ id: 'gi-102', city: 'Nashik', country: 'India', lat: 20.01, lon: 73.79 },
|
||||
{ id: 'gi-103', city: 'Lahore', country: 'Pakistan', lat: 31.55, lon: 74.34 },
|
||||
{ id: 'gi-104', city: 'Satara', country: 'India', lat: 17.68, lon: 74.00 },
|
||||
{ id: 'gi-105', city: 'Thane', country: 'India', lat: 19.22, lon: 72.98 },
|
||||
{ id: 'gi-106', city: 'Vadodara', country: 'India', lat: 22.31, lon: 73.19 },
|
||||
{ id: 'gi-107', city: 'Mumbai', country: 'India', lat: 19.08, lon: 72.88 },
|
||||
{ id: 'gi-108', city: 'Ahmedabad', country: 'India', lat: 23.02, lon: 72.57 },
|
||||
{ id: 'gi-109', city: 'Tashkent', country: 'Uzbekistan', lat: 41.30, lon: 69.28 },
|
||||
{ id: 'gi-110', city: 'Tehran', country: 'Iran', lat: 35.69, lon: 51.39 },
|
||||
{ id: 'gi-111', city: 'Isfahan', country: 'Iran', lat: 32.65, lon: 51.67 },
|
||||
{ id: 'gi-112', city: 'Bonab', country: 'Iran', lat: 37.34, lon: 46.06 },
|
||||
{ id: 'gi-113', city: 'Amman', country: 'Jordan', lat: 31.95, lon: 35.93 },
|
||||
{ id: 'gi-114', city: 'Yavne', country: 'Israel', lat: 31.88, lon: 34.74 },
|
||||
{ id: 'gi-115', city: 'Ankara', country: 'Turkey', lat: 39.93, lon: 32.86 },
|
||||
{ id: 'gi-116', city: 'Çerkezköy', country: 'Turkey', lat: 41.29, lon: 28.00 },
|
||||
{ id: 'gi-117', city: 'Kempton Park', country: 'South Africa', lat: -26.12, lon: 28.22 },
|
||||
{ id: 'gi-118', city: 'Cape Town', country: 'South Africa', lat: -33.92, lon: 18.42 },
|
||||
{ id: 'gi-119', city: 'Sidi Thabet', country: 'Tunisia', lat: 36.91, lon: 10.03 },
|
||||
{ id: 'gi-120', city: 'Abuja', country: 'Nigeria', lat: 9.08, lon: 7.40 },
|
||||
{ id: 'gi-121', city: 'Accra', country: 'Ghana', lat: 5.56, lon: -0.19 },
|
||||
{ id: 'gi-122', city: 'Ibaraki', country: 'Japan', lat: 36.34, lon: 140.45 },
|
||||
{ id: 'gi-123', city: 'Bobadela', country: 'Portugal', lat: 38.80, lon: -9.10 },
|
||||
{ id: 'gi-124', city: 'Plymouth', country: 'UK', lat: 50.38, lon: -4.14 },
|
||||
];
|
||||
@@ -24,13 +24,18 @@ export const DEFAULT_MAP_LAYERS: MapLayers = {
|
||||
conflicts: true,
|
||||
bases: true,
|
||||
cables: true,
|
||||
pipelines: false,
|
||||
hotspots: true,
|
||||
nuclear: true,
|
||||
irradiators: false,
|
||||
sanctions: true,
|
||||
earthquakes: true,
|
||||
weather: true,
|
||||
economic: true,
|
||||
countries: false,
|
||||
waterways: false,
|
||||
outages: true,
|
||||
datacenters: false,
|
||||
};
|
||||
|
||||
export const MONITOR_COLORS = [
|
||||
|
||||
1024
src/config/pipelines.ts
Normal file
1024
src/config/pipelines.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -30,11 +30,28 @@ const NEWS_VELOCITY_THRESHOLD = 3;
|
||||
|
||||
let previousSnapshot: StreamSnapshot | null = null;
|
||||
const signalHistory: CorrelationSignal[] = [];
|
||||
const recentSignalKeys = new Set<string>();
|
||||
|
||||
function generateSignalId(): string {
|
||||
return `sig-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
function generateDedupeKey(type: SignalType, identifier: string, value: number): string {
|
||||
// Round value to avoid minor fluctuations creating new signals
|
||||
const roundedValue = Math.round(value * 10) / 10;
|
||||
return `${type}:${identifier}:${roundedValue}`;
|
||||
}
|
||||
|
||||
function isRecentDuplicate(key: string): boolean {
|
||||
return recentSignalKeys.has(key);
|
||||
}
|
||||
|
||||
function markSignalSeen(key: string): void {
|
||||
recentSignalKeys.add(key);
|
||||
// Clean old keys after 30 minutes
|
||||
setTimeout(() => recentSignalKeys.delete(key), 30 * 60 * 1000);
|
||||
}
|
||||
|
||||
function extractTopics(events: ClusteredEvent[]): Map<string, number> {
|
||||
const topics = new Map<string, number>();
|
||||
|
||||
@@ -115,7 +132,9 @@ export function analyzeCorrelations(
|
||||
const related = findRelatedTopics(pred.title);
|
||||
const newsActivity = related.reduce((sum, t) => sum + (newsTopics.get(t) ?? 0), 0);
|
||||
|
||||
if (newsActivity < NEWS_VELOCITY_THRESHOLD) {
|
||||
const dedupeKey = generateDedupeKey('prediction_leads_news', key, shift);
|
||||
if (newsActivity < NEWS_VELOCITY_THRESHOLD && !isRecentDuplicate(dedupeKey)) {
|
||||
markSignalSeen(dedupeKey);
|
||||
signals.push({
|
||||
id: generateSignalId(),
|
||||
type: 'prediction_leads_news',
|
||||
@@ -138,18 +157,22 @@ export function analyzeCorrelations(
|
||||
for (const [topic, velocity] of newsTopics) {
|
||||
const prevVelocity = previousSnapshot.newsVelocity.get(topic) ?? 0;
|
||||
if (velocity > NEWS_VELOCITY_THRESHOLD * 2 && velocity > prevVelocity * 2) {
|
||||
signals.push({
|
||||
id: generateSignalId(),
|
||||
type: 'velocity_spike',
|
||||
title: 'News Velocity Spike',
|
||||
description: `"${topic}" coverage surging: ${velocity.toFixed(1)} activity score`,
|
||||
confidence: Math.min(0.85, 0.4 + velocity / 20),
|
||||
timestamp: new Date(),
|
||||
data: {
|
||||
newsVelocity: velocity,
|
||||
relatedTopics: [topic],
|
||||
},
|
||||
});
|
||||
const dedupeKey = generateDedupeKey('velocity_spike', topic, velocity);
|
||||
if (!isRecentDuplicate(dedupeKey)) {
|
||||
markSignalSeen(dedupeKey);
|
||||
signals.push({
|
||||
id: generateSignalId(),
|
||||
type: 'velocity_spike',
|
||||
title: 'News Velocity Spike',
|
||||
description: `"${topic}" coverage surging: ${velocity.toFixed(1)} activity score`,
|
||||
confidence: Math.min(0.85, 0.4 + velocity / 20),
|
||||
timestamp: new Date(),
|
||||
data: {
|
||||
newsVelocity: velocity,
|
||||
relatedTopics: [topic],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +184,9 @@ export function analyzeCorrelations(
|
||||
.filter(([k]) => market.name.toLowerCase().includes(k) || k.includes(market.symbol.toLowerCase()))
|
||||
.reduce((sum, [, v]) => sum + v, 0);
|
||||
|
||||
if (relatedNews < 2) {
|
||||
const dedupeKey = generateDedupeKey('silent_divergence', market.symbol, change);
|
||||
if (relatedNews < 2 && !isRecentDuplicate(dedupeKey)) {
|
||||
markSignalSeen(dedupeKey);
|
||||
signals.push({
|
||||
id: generateSignalId(),
|
||||
type: 'silent_divergence',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Earthquake } from '@/types';
|
||||
import { API_URLS } from '@/config';
|
||||
import { fetchWithProxy } from '@/utils';
|
||||
|
||||
interface USGSFeature {
|
||||
id: string;
|
||||
@@ -19,12 +18,58 @@ interface USGSResponse {
|
||||
features: USGSFeature[];
|
||||
}
|
||||
|
||||
export async function fetchEarthquakes(): Promise<Earthquake[]> {
|
||||
try {
|
||||
const response = await fetchWithProxy(API_URLS.earthquakes);
|
||||
const data: USGSResponse = await response.json();
|
||||
const CORS_PROXY = 'https://corsproxy.io/?';
|
||||
const DIRECT_USGS_URL = 'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_day.geojson';
|
||||
|
||||
return data.features.map((feature) => ({
|
||||
async function fetchWithTimeout(url: string, timeoutMs: number = 10000): Promise<Response> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const response = await fetch(url, { signal: controller.signal });
|
||||
clearTimeout(timeoutId);
|
||||
return response;
|
||||
} catch (e) {
|
||||
clearTimeout(timeoutId);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchWithRetry(url: string, retries = 2): Promise<Response> {
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
const proxiedUrl = import.meta.env.DEV ? url : (CORS_PROXY + encodeURIComponent(DIRECT_USGS_URL));
|
||||
const response = await fetchWithTimeout(proxiedUrl, 8000);
|
||||
if (response.ok) return response;
|
||||
console.warn(`[Earthquakes] Attempt ${i + 1} failed with status ${response.status}`);
|
||||
} catch (e) {
|
||||
console.warn(`[Earthquakes] Attempt ${i + 1} failed:`, e);
|
||||
if (i === retries - 1) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('[Earthquakes] Trying CORS proxy fallback...');
|
||||
try {
|
||||
const corsResponse = await fetchWithTimeout(CORS_PROXY + encodeURIComponent(DIRECT_USGS_URL), 10000);
|
||||
if (corsResponse.ok) return corsResponse;
|
||||
} catch (corsErr) {
|
||||
console.error('[Earthquakes] CORS proxy also failed:', corsErr);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 500 * (i + 1)));
|
||||
}
|
||||
}
|
||||
throw new Error('All retries failed');
|
||||
}
|
||||
|
||||
export async function fetchEarthquakes(): Promise<Earthquake[]> {
|
||||
console.log('[Earthquakes] Fetching from:', API_URLS.earthquakes);
|
||||
try {
|
||||
const response = await fetchWithRetry(API_URLS.earthquakes);
|
||||
console.log('[Earthquakes] Response status:', response.status);
|
||||
const data: USGSResponse = await response.json();
|
||||
console.log('[Earthquakes] Got', data.features?.length ?? 0, 'features');
|
||||
|
||||
const earthquakes = data.features.map((feature) => ({
|
||||
id: feature.id,
|
||||
place: feature.properties.place || 'Unknown',
|
||||
magnitude: feature.properties.mag,
|
||||
@@ -34,8 +79,11 @@ export async function fetchEarthquakes(): Promise<Earthquake[]> {
|
||||
time: new Date(feature.properties.time),
|
||||
url: feature.properties.url,
|
||||
}));
|
||||
|
||||
console.log('[Earthquakes] Mapped', earthquakes.length, 'earthquakes');
|
||||
return earthquakes;
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch earthquakes:', e);
|
||||
console.error('[Earthquakes] Failed to fetch after retries:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,16 @@ async function fetchSeriesData(seriesId: string): Promise<{ date: string; value:
|
||||
|
||||
const url = `${FRED_CSV_BASE}?id=${seriesId}&cosd=${startDate}&coed=${endDate}`;
|
||||
const response = await fetch(url, {
|
||||
headers: { 'Accept': 'text/csv' }
|
||||
headers: {
|
||||
'Accept': 'text/csv',
|
||||
'User-Agent': 'WorldMonitor/1.0'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) return [];
|
||||
if (!response.ok) {
|
||||
console.warn(`FRED API returned ${response.status} for ${seriesId}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const csv = await response.text();
|
||||
const lines = csv.trim().split('\n').slice(1);
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from './storage';
|
||||
export * from './correlation';
|
||||
export * from './weather';
|
||||
export * from './fred';
|
||||
export * from './outages';
|
||||
|
||||
@@ -60,11 +60,11 @@ export async function fetchMultipleStocks(
|
||||
symbols: Array<{ symbol: string; name: string; display: string }>
|
||||
): Promise<MarketData[]> {
|
||||
const results: MarketData[] = [];
|
||||
// Sequential fetch with delay to avoid rate limiting
|
||||
// Sequential fetch with longer delay to avoid Yahoo 429 rate limiting
|
||||
for (const s of symbols) {
|
||||
const result = await fetchStockQuote(s.symbol, s.name, s.display);
|
||||
results.push(result);
|
||||
await delay(500); // 500ms delay between requests
|
||||
await delay(2500); // 2.5s delay between requests to avoid Yahoo 429
|
||||
}
|
||||
return results.filter((r) => r.price !== null);
|
||||
}
|
||||
|
||||
270
src/services/outages.ts
Normal file
270
src/services/outages.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import type { InternetOutage } from '@/types';
|
||||
|
||||
const CLOUDFLARE_API_URL = '/api/cloudflare-radar/client/v4/radar/annotations/outages';
|
||||
const CLOUDFLARE_API_TOKEN = 'NgxU8rHEEG6ep5B0z-JiuQbOf-LgrpSNJt27FQg0';
|
||||
|
||||
const COUNTRY_COORDS: Record<string, { lat: number; lon: number }> = {
|
||||
'AF': { lat: 33.9391, lon: 67.7100 },
|
||||
'AL': { lat: 41.1533, lon: 20.1683 },
|
||||
'DZ': { lat: 28.0339, lon: 1.6596 },
|
||||
'AO': { lat: -11.2027, lon: 17.8739 },
|
||||
'AR': { lat: -38.4161, lon: -63.6167 },
|
||||
'AM': { lat: 40.0691, lon: 45.0382 },
|
||||
'AU': { lat: -25.2744, lon: 133.7751 },
|
||||
'AT': { lat: 47.5162, lon: 14.5501 },
|
||||
'AZ': { lat: 40.1431, lon: 47.5769 },
|
||||
'BH': { lat: 26.0667, lon: 50.5577 },
|
||||
'BD': { lat: 23.685, lon: 90.3563 },
|
||||
'BY': { lat: 53.7098, lon: 27.9534 },
|
||||
'BE': { lat: 50.5039, lon: 4.4699 },
|
||||
'BJ': { lat: 9.3077, lon: 2.3158 },
|
||||
'BO': { lat: -16.2902, lon: -63.5887 },
|
||||
'BA': { lat: 43.9159, lon: 17.6791 },
|
||||
'BW': { lat: -22.3285, lon: 24.6849 },
|
||||
'BR': { lat: -14.235, lon: -51.9253 },
|
||||
'BG': { lat: 42.7339, lon: 25.4858 },
|
||||
'BF': { lat: 12.2383, lon: -1.5616 },
|
||||
'BI': { lat: -3.3731, lon: 29.9189 },
|
||||
'KH': { lat: 12.5657, lon: 104.991 },
|
||||
'CM': { lat: 7.3697, lon: 12.3547 },
|
||||
'CA': { lat: 56.1304, lon: -106.3468 },
|
||||
'CF': { lat: 6.6111, lon: 20.9394 },
|
||||
'TD': { lat: 15.4542, lon: 18.7322 },
|
||||
'CL': { lat: -35.6751, lon: -71.543 },
|
||||
'CN': { lat: 35.8617, lon: 104.1954 },
|
||||
'CO': { lat: 4.5709, lon: -74.2973 },
|
||||
'CG': { lat: -0.228, lon: 15.8277 },
|
||||
'CD': { lat: -4.0383, lon: 21.7587 },
|
||||
'CR': { lat: 9.7489, lon: -83.7534 },
|
||||
'HR': { lat: 45.1, lon: 15.2 },
|
||||
'CU': { lat: 21.5218, lon: -77.7812 },
|
||||
'CY': { lat: 35.1264, lon: 33.4299 },
|
||||
'CZ': { lat: 49.8175, lon: 15.473 },
|
||||
'DK': { lat: 56.2639, lon: 9.5018 },
|
||||
'DJ': { lat: 11.8251, lon: 42.5903 },
|
||||
'EC': { lat: -1.8312, lon: -78.1834 },
|
||||
'EG': { lat: 26.8206, lon: 30.8025 },
|
||||
'SV': { lat: 13.7942, lon: -88.8965 },
|
||||
'ER': { lat: 15.1794, lon: 39.7823 },
|
||||
'EE': { lat: 58.5953, lon: 25.0136 },
|
||||
'ET': { lat: 9.145, lon: 40.4897 },
|
||||
'FI': { lat: 61.9241, lon: 25.7482 },
|
||||
'FR': { lat: 46.2276, lon: 2.2137 },
|
||||
'GA': { lat: -0.8037, lon: 11.6094 },
|
||||
'GM': { lat: 13.4432, lon: -15.3101 },
|
||||
'GE': { lat: 42.3154, lon: 43.3569 },
|
||||
'DE': { lat: 51.1657, lon: 10.4515 },
|
||||
'GH': { lat: 7.9465, lon: -1.0232 },
|
||||
'GR': { lat: 39.0742, lon: 21.8243 },
|
||||
'GT': { lat: 15.7835, lon: -90.2308 },
|
||||
'GN': { lat: 9.9456, lon: -9.6966 },
|
||||
'HT': { lat: 18.9712, lon: -72.2852 },
|
||||
'HN': { lat: 15.2, lon: -86.2419 },
|
||||
'HK': { lat: 22.3193, lon: 114.1694 },
|
||||
'HU': { lat: 47.1625, lon: 19.5033 },
|
||||
'IN': { lat: 20.5937, lon: 78.9629 },
|
||||
'ID': { lat: -0.7893, lon: 113.9213 },
|
||||
'IR': { lat: 32.4279, lon: 53.688 },
|
||||
'IQ': { lat: 33.2232, lon: 43.6793 },
|
||||
'IE': { lat: 53.1424, lon: -7.6921 },
|
||||
'IL': { lat: 31.0461, lon: 34.8516 },
|
||||
'IT': { lat: 41.8719, lon: 12.5674 },
|
||||
'CI': { lat: 7.54, lon: -5.5471 },
|
||||
'JP': { lat: 36.2048, lon: 138.2529 },
|
||||
'JO': { lat: 30.5852, lon: 36.2384 },
|
||||
'KZ': { lat: 48.0196, lon: 66.9237 },
|
||||
'KE': { lat: -0.0236, lon: 37.9062 },
|
||||
'KW': { lat: 29.3117, lon: 47.4818 },
|
||||
'KG': { lat: 41.2044, lon: 74.7661 },
|
||||
'LA': { lat: 19.8563, lon: 102.4955 },
|
||||
'LV': { lat: 56.8796, lon: 24.6032 },
|
||||
'LB': { lat: 33.8547, lon: 35.8623 },
|
||||
'LY': { lat: 26.3351, lon: 17.2283 },
|
||||
'LT': { lat: 55.1694, lon: 23.8813 },
|
||||
'LU': { lat: 49.8153, lon: 6.1296 },
|
||||
'MG': { lat: -18.7669, lon: 46.8691 },
|
||||
'MW': { lat: -13.2543, lon: 34.3015 },
|
||||
'MY': { lat: 4.2105, lon: 101.9758 },
|
||||
'ML': { lat: 17.5707, lon: -3.9962 },
|
||||
'MR': { lat: 21.0079, lon: -10.9408 },
|
||||
'MX': { lat: 23.6345, lon: -102.5528 },
|
||||
'MD': { lat: 47.4116, lon: 28.3699 },
|
||||
'MN': { lat: 46.8625, lon: 103.8467 },
|
||||
'MA': { lat: 31.7917, lon: -7.0926 },
|
||||
'MZ': { lat: -18.6657, lon: 35.5296 },
|
||||
'MM': { lat: 21.9162, lon: 95.956 },
|
||||
'NA': { lat: -22.9576, lon: 18.4904 },
|
||||
'NP': { lat: 28.3949, lon: 84.124 },
|
||||
'NL': { lat: 52.1326, lon: 5.2913 },
|
||||
'NZ': { lat: -40.9006, lon: 174.886 },
|
||||
'NI': { lat: 12.8654, lon: -85.2072 },
|
||||
'NE': { lat: 17.6078, lon: 8.0817 },
|
||||
'NG': { lat: 9.082, lon: 8.6753 },
|
||||
'KP': { lat: 40.3399, lon: 127.5101 },
|
||||
'NO': { lat: 60.472, lon: 8.4689 },
|
||||
'OM': { lat: 21.4735, lon: 55.9754 },
|
||||
'PK': { lat: 30.3753, lon: 69.3451 },
|
||||
'PS': { lat: 31.9522, lon: 35.2332 },
|
||||
'PA': { lat: 8.538, lon: -80.7821 },
|
||||
'PG': { lat: -6.315, lon: 143.9555 },
|
||||
'PY': { lat: -23.4425, lon: -58.4438 },
|
||||
'PE': { lat: -9.19, lon: -75.0152 },
|
||||
'PH': { lat: 12.8797, lon: 121.774 },
|
||||
'PL': { lat: 51.9194, lon: 19.1451 },
|
||||
'PT': { lat: 39.3999, lon: -8.2245 },
|
||||
'QA': { lat: 25.3548, lon: 51.1839 },
|
||||
'RO': { lat: 45.9432, lon: 24.9668 },
|
||||
'RU': { lat: 61.524, lon: 105.3188 },
|
||||
'RW': { lat: -1.9403, lon: 29.8739 },
|
||||
'SA': { lat: 23.8859, lon: 45.0792 },
|
||||
'SN': { lat: 14.4974, lon: -14.4524 },
|
||||
'RS': { lat: 44.0165, lon: 21.0059 },
|
||||
'SL': { lat: 8.4606, lon: -11.7799 },
|
||||
'SG': { lat: 1.3521, lon: 103.8198 },
|
||||
'SK': { lat: 48.669, lon: 19.699 },
|
||||
'SI': { lat: 46.1512, lon: 14.9955 },
|
||||
'SO': { lat: 5.1521, lon: 46.1996 },
|
||||
'ZA': { lat: -30.5595, lon: 22.9375 },
|
||||
'KR': { lat: 35.9078, lon: 127.7669 },
|
||||
'SS': { lat: 6.877, lon: 31.307 },
|
||||
'ES': { lat: 40.4637, lon: -3.7492 },
|
||||
'LK': { lat: 7.8731, lon: 80.7718 },
|
||||
'SD': { lat: 12.8628, lon: 30.2176 },
|
||||
'SE': { lat: 60.1282, lon: 18.6435 },
|
||||
'CH': { lat: 46.8182, lon: 8.2275 },
|
||||
'SY': { lat: 34.8021, lon: 38.9968 },
|
||||
'TW': { lat: 23.6978, lon: 120.9605 },
|
||||
'TJ': { lat: 38.861, lon: 71.2761 },
|
||||
'TZ': { lat: -6.369, lon: 34.8888 },
|
||||
'TH': { lat: 15.87, lon: 100.9925 },
|
||||
'TG': { lat: 8.6195, lon: 0.8248 },
|
||||
'TT': { lat: 10.6918, lon: -61.2225 },
|
||||
'TN': { lat: 33.8869, lon: 9.5375 },
|
||||
'TR': { lat: 38.9637, lon: 35.2433 },
|
||||
'TM': { lat: 38.9697, lon: 59.5563 },
|
||||
'UG': { lat: 1.3733, lon: 32.2903 },
|
||||
'UA': { lat: 48.3794, lon: 31.1656 },
|
||||
'AE': { lat: 23.4241, lon: 53.8478 },
|
||||
'GB': { lat: 55.3781, lon: -3.436 },
|
||||
'US': { lat: 37.0902, lon: -95.7129 },
|
||||
'UY': { lat: -32.5228, lon: -55.7658 },
|
||||
'UZ': { lat: 41.3775, lon: 64.5853 },
|
||||
'VE': { lat: 6.4238, lon: -66.5897 },
|
||||
'VN': { lat: 14.0583, lon: 108.2772 },
|
||||
'YE': { lat: 15.5527, lon: 48.5164 },
|
||||
'ZM': { lat: -13.1339, lon: 27.8493 },
|
||||
'ZW': { lat: -19.0154, lon: 29.1549 },
|
||||
};
|
||||
|
||||
interface CloudflareOutage {
|
||||
id: string;
|
||||
dataSource: string;
|
||||
description: string;
|
||||
scope: string | null;
|
||||
startDate: string;
|
||||
endDate: string | null;
|
||||
locations: string[];
|
||||
asns: number[];
|
||||
eventType: string;
|
||||
linkedUrl: string;
|
||||
locationsDetails: Array<{ name: string; code: string }>;
|
||||
asnsDetails: Array<{ asn: string; name: string; location: { code: string; name: string } }>;
|
||||
outage: {
|
||||
outageCause: string;
|
||||
outageType: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CloudflareResponse {
|
||||
success: boolean;
|
||||
errors: Array<{ code: number; message: string }>;
|
||||
result: {
|
||||
annotations: CloudflareOutage[];
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchInternetOutages(): Promise<InternetOutage[]> {
|
||||
console.log('[Outages] Fetching from Cloudflare Radar...');
|
||||
try {
|
||||
const response = await fetch(`${CLOUDFLARE_API_URL}?dateRange=7d&limit=50`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${CLOUDFLARE_API_TOKEN}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[Outages] Failed to fetch:', response.status);
|
||||
return [];
|
||||
}
|
||||
|
||||
const data: CloudflareResponse = await response.json();
|
||||
|
||||
if (!data.success || data.errors?.length > 0) {
|
||||
console.error('[Outages] API error:', data.errors);
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log('[Outages] Received', data.result.annotations?.length || 0, 'outages from Cloudflare');
|
||||
|
||||
const outages: InternetOutage[] = [];
|
||||
|
||||
for (const outage of data.result.annotations || []) {
|
||||
// Skip if no location
|
||||
if (!outage.locations?.length) continue;
|
||||
|
||||
const countryCode = outage.locations[0];
|
||||
if (!countryCode) continue;
|
||||
|
||||
const coords = COUNTRY_COORDS[countryCode];
|
||||
if (!coords) continue;
|
||||
|
||||
const countryName = outage.locationsDetails?.[0]?.name ?? countryCode;
|
||||
|
||||
// Determine severity based on outage type
|
||||
let severity: 'partial' | 'major' | 'total' = 'partial';
|
||||
if (outage.outage?.outageType === 'NATIONWIDE') {
|
||||
severity = 'total';
|
||||
} else if (outage.outage?.outageType === 'REGIONAL') {
|
||||
severity = 'major';
|
||||
}
|
||||
|
||||
// Format categories from cause and type
|
||||
const categories: string[] = ['Cloudflare Radar'];
|
||||
if (outage.outage?.outageCause) {
|
||||
categories.push(outage.outage.outageCause.replace(/_/g, ' '));
|
||||
}
|
||||
if (outage.outage?.outageType) {
|
||||
categories.push(outage.outage.outageType);
|
||||
}
|
||||
|
||||
// Add ASN names if available
|
||||
for (const asn of outage.asnsDetails?.slice(0, 2) || []) {
|
||||
if (asn.name) categories.push(asn.name);
|
||||
}
|
||||
|
||||
outages.push({
|
||||
id: `cf-${outage.id}`,
|
||||
title: outage.scope
|
||||
? `${outage.scope} outage in ${countryName}`
|
||||
: `Internet disruption in ${countryName}`,
|
||||
link: outage.linkedUrl || `https://radar.cloudflare.com/outage-center`,
|
||||
description: outage.description,
|
||||
pubDate: new Date(outage.startDate),
|
||||
country: countryName,
|
||||
lat: coords.lat,
|
||||
lon: coords.lon,
|
||||
severity,
|
||||
categories,
|
||||
cause: outage.outage?.outageCause,
|
||||
outageType: outage.outage?.outageType,
|
||||
endDate: outage.endDate ? new Date(outage.endDate) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[Outages] Mapped', outages.length, 'outages');
|
||||
return outages;
|
||||
} catch (e) {
|
||||
console.error('[Outages] Error fetching outages:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -140,6 +140,33 @@ body {
|
||||
border-color: var(--text-dim);
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
padding: 4px 10px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.search-btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.search-btn kbd {
|
||||
background: var(--bg-secondary);
|
||||
padding: 2px 5px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -1026,6 +1053,118 @@ body.playback-mode .status-dot {
|
||||
}
|
||||
}
|
||||
|
||||
/* Pipeline paths */
|
||||
.pipeline-path {
|
||||
cursor: pointer;
|
||||
pointer-events: stroke;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pipeline-path.pipeline-oil {
|
||||
filter: drop-shadow(0 0 3px #ff6b35);
|
||||
}
|
||||
|
||||
.pipeline-path.pipeline-gas {
|
||||
filter: drop-shadow(0 0 3px #00b4d8);
|
||||
}
|
||||
|
||||
.pipeline-path.pipeline-products {
|
||||
filter: drop-shadow(0 0 3px #ffd166);
|
||||
}
|
||||
|
||||
.pipeline-path:hover {
|
||||
stroke-width: 4 !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.pipeline-path.pipeline-oil:hover {
|
||||
filter: drop-shadow(0 0 8px #ff6b35) drop-shadow(0 0 15px #ff6b35);
|
||||
}
|
||||
|
||||
.pipeline-path.pipeline-gas:hover {
|
||||
filter: drop-shadow(0 0 8px #00b4d8) drop-shadow(0 0 15px #00b4d8);
|
||||
}
|
||||
|
||||
.pipeline-path.pipeline-products:hover {
|
||||
filter: drop-shadow(0 0 8px #ffd166) drop-shadow(0 0 15px #ffd166);
|
||||
}
|
||||
|
||||
/* Pipeline popup header colors */
|
||||
.popup-header.pipeline.oil {
|
||||
border-bottom-color: #ff6b35;
|
||||
}
|
||||
|
||||
.popup-header.pipeline.gas {
|
||||
border-bottom-color: #00b4d8;
|
||||
}
|
||||
|
||||
.popup-header.pipeline.products {
|
||||
border-bottom-color: #ffd166;
|
||||
}
|
||||
|
||||
/* Cable popup header */
|
||||
.popup-header.cable {
|
||||
border-bottom-color: #00ffaa;
|
||||
}
|
||||
|
||||
/* Internet Outage markers */
|
||||
.outage-marker {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
transform: translate(-50%, -50%) scale(var(--marker-scale, 1));
|
||||
transform-origin: center;
|
||||
cursor: pointer;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
.outage-marker.partial {
|
||||
--outage-color: #ffaa00;
|
||||
}
|
||||
|
||||
.outage-marker.major {
|
||||
--outage-color: #ff6644;
|
||||
}
|
||||
|
||||
.outage-marker.total {
|
||||
--outage-color: #ff2222;
|
||||
animation: outage-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.outage-icon {
|
||||
font-size: 14px;
|
||||
filter: drop-shadow(0 0 4px var(--outage-color, #ffaa00));
|
||||
}
|
||||
|
||||
.outage-label {
|
||||
font-size: 8px;
|
||||
color: var(--outage-color, #ffaa00);
|
||||
white-space: nowrap;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 2px;
|
||||
text-shadow: 0 0 4px var(--bg), 0 0 8px var(--bg);
|
||||
}
|
||||
|
||||
@keyframes outage-pulse {
|
||||
0%, 100% { opacity: 1; transform: translate(-50%, -50%) scale(var(--marker-scale, 1)); }
|
||||
50% { opacity: 0.6; transform: translate(-50%, -50%) scale(calc(var(--marker-scale, 1) * 1.2)); }
|
||||
}
|
||||
|
||||
/* Outage popup header */
|
||||
.popup-header.outage.total {
|
||||
border-bottom-color: #ff2222;
|
||||
}
|
||||
|
||||
.popup-header.outage.major {
|
||||
border-bottom-color: #ff6644;
|
||||
}
|
||||
|
||||
.popup-header.outage.partial {
|
||||
border-bottom-color: #ffaa00;
|
||||
}
|
||||
|
||||
/* Conflict zones */
|
||||
.conflict-zone {
|
||||
fill: rgba(255, 68, 68, 0.2);
|
||||
@@ -1059,9 +1198,17 @@ body.playback-mode .status-dot {
|
||||
0 0 4px var(--bg),
|
||||
0 0 8px var(--bg),
|
||||
0 0 12px var(--bg);
|
||||
pointer-events: none;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
z-index: 10;
|
||||
z-index: 55;
|
||||
}
|
||||
|
||||
.conflict-label-overlay:hover {
|
||||
color: #ff6666;
|
||||
text-shadow:
|
||||
0 0 6px var(--bg),
|
||||
0 0 12px var(--bg),
|
||||
0 0 18px var(--red);
|
||||
}
|
||||
|
||||
@keyframes pulse-conflict {
|
||||
@@ -1078,6 +1225,11 @@ body.playback-mode .status-dot {
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
z-index: 40;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.base-marker:hover {
|
||||
transform: translate(-50%, -50%) scale(calc(var(--marker-scale, 1) * 1.3));
|
||||
}
|
||||
|
||||
.base-marker.us-nato {
|
||||
@@ -1095,6 +1247,88 @@ body.playback-mode .status-dot {
|
||||
box-shadow: 0 0 6px #ff4444;
|
||||
}
|
||||
|
||||
/* UK - Union Jack blue */
|
||||
.base-marker.uk {
|
||||
background: #44aaff;
|
||||
box-shadow: 0 0 6px #44aaff;
|
||||
}
|
||||
|
||||
/* France - French blue */
|
||||
.base-marker.france {
|
||||
background: #0055a4;
|
||||
box-shadow: 0 0 6px #0055a4;
|
||||
}
|
||||
|
||||
/* India - Saffron orange */
|
||||
.base-marker.india {
|
||||
background: #ff9933;
|
||||
box-shadow: 0 0 6px #ff9933;
|
||||
}
|
||||
|
||||
/* Italy - Italian green */
|
||||
.base-marker.italy {
|
||||
background: #009246;
|
||||
box-shadow: 0 0 6px #009246;
|
||||
}
|
||||
|
||||
/* UAE - Emirates green */
|
||||
.base-marker.uae {
|
||||
background: #00732f;
|
||||
box-shadow: 0 0 6px #00732f;
|
||||
}
|
||||
|
||||
/* Turkey - Turkish red */
|
||||
.base-marker.turkey {
|
||||
background: #e30a17;
|
||||
box-shadow: 0 0 6px #e30a17;
|
||||
}
|
||||
|
||||
/* Japan - Rising sun red */
|
||||
.base-marker.japan {
|
||||
background: #bc002d;
|
||||
box-shadow: 0 0 6px #bc002d;
|
||||
}
|
||||
|
||||
/* Other nations */
|
||||
.base-marker.other {
|
||||
background: #888888;
|
||||
box-shadow: 0 0 6px #888888;
|
||||
}
|
||||
|
||||
.base-label {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) scale(calc(var(--label-scale, 1) * var(--marker-scale, 1)));
|
||||
transform-origin: top center;
|
||||
font-size: 8px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
text-shadow: 0 0 4px var(--bg), 0 0 8px var(--bg);
|
||||
white-space: nowrap;
|
||||
text-transform: uppercase;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.base-marker:hover .base-label,
|
||||
.base-marker.active .base-label {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.base-marker.us-nato .base-label { color: #4488ff; }
|
||||
.base-marker.china .base-label { color: #ff8844; }
|
||||
.base-marker.russia .base-label { color: #ff4444; }
|
||||
.base-marker.uk .base-label { color: #44aaff; }
|
||||
.base-marker.france .base-label { color: #0055a4; }
|
||||
.base-marker.india .base-label { color: #ff9933; }
|
||||
.base-marker.italy .base-label { color: #009246; }
|
||||
.base-marker.uae .base-label { color: #00732f; }
|
||||
.base-marker.turkey .base-label { color: #e30a17; }
|
||||
.base-marker.japan .base-label { color: #bc002d; }
|
||||
.base-marker.other .base-label { color: #888888; }
|
||||
|
||||
/* Earthquake markers */
|
||||
.earthquake-marker {
|
||||
position: absolute;
|
||||
@@ -1189,6 +1423,121 @@ body.playback-mode .status-dot {
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Gamma Irradiators */
|
||||
.irradiator-marker {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%) scale(var(--marker-scale, 1));
|
||||
transform-origin: center;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #00ffaa;
|
||||
box-shadow: 0 0 6px #00ffaa, 0 0 12px #00ffaa40;
|
||||
z-index: 41;
|
||||
cursor: pointer;
|
||||
border: 1px solid #00aa77;
|
||||
}
|
||||
|
||||
.irradiator-marker:hover {
|
||||
background: #44ffcc;
|
||||
box-shadow: 0 0 10px #00ffaa, 0 0 20px #00ffaa60;
|
||||
}
|
||||
|
||||
.irradiator-label {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) scale(calc(var(--label-scale, 1) / var(--marker-scale, 1)));
|
||||
transform-origin: top center;
|
||||
white-space: nowrap;
|
||||
font-size: 6px;
|
||||
color: #00ffaa;
|
||||
text-shadow: 0 0 4px var(--bg), 0 0 8px var(--bg);
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.popup-header.irradiator {
|
||||
background: linear-gradient(135deg, #00442220, #00221110);
|
||||
border-left: 3px solid #00ffaa;
|
||||
}
|
||||
|
||||
/* AI Data Centers */
|
||||
.datacenter-marker {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%) scale(var(--marker-scale, 1));
|
||||
transform-origin: center;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
background: #8844ff;
|
||||
box-shadow: 0 0 8px #8844ff80, 0 0 16px #8844ff40;
|
||||
z-index: 42;
|
||||
cursor: pointer;
|
||||
border: 1px solid #6633cc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.datacenter-marker.existing {
|
||||
background: #8844ff;
|
||||
border-color: #6633cc;
|
||||
box-shadow: 0 0 8px #8844ff80, 0 0 16px #8844ff40;
|
||||
}
|
||||
|
||||
.datacenter-marker.planned {
|
||||
background: transparent;
|
||||
border: 1px dashed #8844ff;
|
||||
box-shadow: 0 0 8px #8844ff40;
|
||||
}
|
||||
|
||||
.datacenter-marker:hover {
|
||||
background: #aa66ff;
|
||||
box-shadow: 0 0 12px #8844ff, 0 0 24px #8844ff60;
|
||||
}
|
||||
|
||||
.datacenter-marker.planned:hover {
|
||||
background: #8844ff40;
|
||||
}
|
||||
|
||||
.datacenter-icon {
|
||||
font-size: 7px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.datacenter-label {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) scale(calc(var(--label-scale, 1) / var(--marker-scale, 1)));
|
||||
transform-origin: top center;
|
||||
white-space: nowrap;
|
||||
font-size: 6px;
|
||||
color: #aa88ff;
|
||||
text-shadow: 0 0 4px var(--bg), 0 0 8px var(--bg);
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.8;
|
||||
max-width: 60px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.popup-header.datacenter {
|
||||
background: linear-gradient(135deg, #33225520, #22113310);
|
||||
border-left: 3px solid #8844ff;
|
||||
}
|
||||
|
||||
.popup-header.datacenter.existing {
|
||||
border-left-color: #8844ff;
|
||||
}
|
||||
|
||||
.popup-header.datacenter.planned {
|
||||
border-left-color: #ffaa44;
|
||||
}
|
||||
|
||||
/* Heatmap */
|
||||
.heatmap {
|
||||
display: grid;
|
||||
@@ -1804,19 +2153,28 @@ body.playback-mode .status-dot {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Strategic waterway labels */
|
||||
.waterway-label {
|
||||
/* Strategic waterway markers */
|
||||
.waterway-marker {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%) scale(var(--label-scale, 1));
|
||||
transform: translate(-50%, -50%) scale(var(--marker-scale, 1));
|
||||
transform-origin: center;
|
||||
white-space: nowrap;
|
||||
font-size: 8px;
|
||||
color: #00ffaa;
|
||||
text-shadow: 0 0 4px var(--bg), 0 0 8px var(--bg);
|
||||
letter-spacing: 1px;
|
||||
font-weight: bold;
|
||||
pointer-events: none;
|
||||
z-index: 35;
|
||||
cursor: pointer;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.waterway-diamond {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #00ffaa;
|
||||
transform: rotate(45deg);
|
||||
box-shadow: 0 0 6px rgba(0, 255, 170, 0.6);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.waterway-marker:hover .waterway-diamond {
|
||||
background: #44ffcc;
|
||||
box-shadow: 0 0 10px rgba(68, 255, 204, 0.8);
|
||||
transform: rotate(45deg) scale(1.2);
|
||||
}
|
||||
|
||||
/* APT markers */
|
||||
@@ -1825,11 +2183,15 @@ body.playback-mode .status-dot {
|
||||
transform: translate(-50%, -50%) scale(var(--marker-scale, 1));
|
||||
transform-origin: center;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
cursor: pointer;
|
||||
z-index: 36;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.apt-marker:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.apt-icon {
|
||||
font-size: 10px;
|
||||
color: #ff00ff;
|
||||
@@ -2266,6 +2628,72 @@ body.playback-mode .status-dot {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Economic Markers */
|
||||
.economic-marker {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%) scale(var(--marker-scale, 1));
|
||||
transform-origin: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 50;
|
||||
opacity: 0.85;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.economic-marker:hover {
|
||||
opacity: 1;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.economic-icon {
|
||||
font-size: 14px;
|
||||
filter: drop-shadow(0 0 3px rgba(0, 0, 0, 0.8));
|
||||
}
|
||||
|
||||
.economic-marker.exchange .economic-icon {
|
||||
filter: drop-shadow(0 0 4px rgba(76, 175, 80, 0.6));
|
||||
}
|
||||
|
||||
.economic-marker.central-bank .economic-icon {
|
||||
filter: drop-shadow(0 0 4px rgba(33, 150, 243, 0.6));
|
||||
}
|
||||
|
||||
.economic-marker.financial-hub .economic-icon {
|
||||
filter: drop-shadow(0 0 4px rgba(255, 193, 7, 0.6));
|
||||
}
|
||||
|
||||
.economic-label {
|
||||
font-size: 7px;
|
||||
color: var(--text);
|
||||
background: rgba(0, 20, 15, 0.9);
|
||||
padding: 1px 3px;
|
||||
border-radius: 2px;
|
||||
white-space: nowrap;
|
||||
margin-top: 1px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
transform: scale(calc(var(--label-scale, 1) / var(--marker-scale, 1)));
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
.economic-marker:hover .economic-label {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.popup-header.economic {
|
||||
background: linear-gradient(135deg, #4caf50, #2e7d32);
|
||||
}
|
||||
|
||||
.popup-header.economic.central-bank {
|
||||
background: linear-gradient(135deg, #2196f3, #1565c0);
|
||||
}
|
||||
|
||||
.popup-header.economic.financial-hub {
|
||||
background: linear-gradient(135deg, #ffc107, #ff9800);
|
||||
}
|
||||
|
||||
/* Economic Panel */
|
||||
.economic-panel-container {
|
||||
position: absolute;
|
||||
@@ -2395,3 +2823,183 @@ body.playback-mode .status-dot {
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
/* Search Modal (Cmd+K) */
|
||||
.search-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding-top: 15vh;
|
||||
z-index: 2000;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.search-modal {
|
||||
width: 560px;
|
||||
max-width: 90vw;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 14px;
|
||||
color: var(--text-dim);
|
||||
background: var(--border);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.search-kbd {
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
background: var(--border);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.search-section-header {
|
||||
padding: 8px 16px;
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.search-result-item:hover,
|
||||
.search-result-item.selected {
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.search-result-item.selected {
|
||||
border-left: 2px solid var(--green);
|
||||
}
|
||||
|
||||
.search-result-icon {
|
||||
font-size: 16px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.search-result-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.search-result-title {
|
||||
font-size: 12px;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.search-result-title mark {
|
||||
background: rgba(68, 255, 136, 0.3);
|
||||
color: var(--green);
|
||||
padding: 0 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.search-result-subtitle {
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
margin-top: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.search-result-type {
|
||||
font-size: 9px;
|
||||
color: var(--text-dim);
|
||||
text-transform: uppercase;
|
||||
background: var(--bg);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.search-empty {
|
||||
padding: 40px 16px;
|
||||
text-align: center;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.search-empty-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.search-empty-hint {
|
||||
font-size: 11px;
|
||||
margin-top: 8px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.search-footer {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 8px 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
background: var(--bg);
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.search-footer kbd {
|
||||
font-size: 9px;
|
||||
background: var(--border);
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
@@ -120,12 +120,31 @@ export interface ConflictZone {
|
||||
keyDevelopments?: string[];
|
||||
}
|
||||
|
||||
// Military base operator types
|
||||
export type MilitaryBaseType =
|
||||
| 'us-nato' // United States and NATO allies
|
||||
| 'china' // People's Republic of China
|
||||
| 'russia' // Russian Federation
|
||||
| 'uk' // United Kingdom (non-US NATO)
|
||||
| 'france' // France (non-US NATO)
|
||||
| 'india' // India
|
||||
| 'italy' // Italy
|
||||
| 'uae' // United Arab Emirates
|
||||
| 'turkey' // Turkey
|
||||
| 'japan' // Japan Self-Defense Forces
|
||||
| 'other'; // Other nations
|
||||
|
||||
export interface MilitaryBase {
|
||||
id: string;
|
||||
name: string;
|
||||
lat: number;
|
||||
lon: number;
|
||||
type: 'us-nato' | 'china' | 'russia';
|
||||
type: MilitaryBaseType;
|
||||
description?: string;
|
||||
country?: string; // Host country
|
||||
arm?: string; // Armed forces branch (Navy, Air Force, Army, etc.)
|
||||
status?: 'active' | 'planned' | 'controversial' | 'closed';
|
||||
source?: string; // Reference URL
|
||||
}
|
||||
|
||||
export interface UnderseaCable {
|
||||
@@ -150,13 +169,49 @@ export interface CyberRegion {
|
||||
sponsor: string;
|
||||
}
|
||||
|
||||
// Nuclear facility types
|
||||
export type NuclearFacilityType =
|
||||
| 'plant' // Power reactors
|
||||
| 'enrichment' // Uranium enrichment
|
||||
| 'reprocessing' // Plutonium reprocessing
|
||||
| 'weapons' // Weapons design/assembly
|
||||
| 'ssbn' // Submarine base (nuclear deterrent)
|
||||
| 'test-site' // Nuclear test site
|
||||
| 'icbm' // ICBM silo fields
|
||||
| 'research'; // Research reactors
|
||||
|
||||
export interface NuclearFacility {
|
||||
id: string;
|
||||
name: string;
|
||||
lat: number;
|
||||
lon: number;
|
||||
type: 'plant' | 'enrichment' | 'weapons';
|
||||
status: 'active' | 'contested' | 'inactive';
|
||||
type: NuclearFacilityType;
|
||||
status: 'active' | 'contested' | 'inactive' | 'decommissioned' | 'construction';
|
||||
operator?: string; // Operating country
|
||||
}
|
||||
|
||||
export interface GammaIrradiator {
|
||||
id: string;
|
||||
city: string;
|
||||
country: string;
|
||||
lat: number;
|
||||
lon: number;
|
||||
organization?: string;
|
||||
}
|
||||
|
||||
export type PipelineType = 'oil' | 'gas' | 'products';
|
||||
export type PipelineStatus = 'operating' | 'construction';
|
||||
|
||||
export interface Pipeline {
|
||||
id: string;
|
||||
name: string;
|
||||
type: PipelineType;
|
||||
status: PipelineStatus;
|
||||
points: [number, number][]; // [lon, lat] pairs
|
||||
capacity?: string; // e.g., "1.2 million bpd"
|
||||
length?: string; // e.g., "1,768 km"
|
||||
operator?: string;
|
||||
countries?: string[];
|
||||
}
|
||||
|
||||
export interface Earthquake {
|
||||
@@ -189,13 +244,64 @@ export interface MapLayers {
|
||||
conflicts: boolean;
|
||||
bases: boolean;
|
||||
cables: boolean;
|
||||
pipelines: boolean;
|
||||
hotspots: boolean;
|
||||
nuclear: boolean;
|
||||
irradiators: boolean;
|
||||
sanctions: boolean;
|
||||
earthquakes: boolean;
|
||||
weather: boolean;
|
||||
economic: boolean;
|
||||
countries: boolean;
|
||||
waterways: boolean;
|
||||
outages: boolean;
|
||||
datacenters: boolean;
|
||||
}
|
||||
|
||||
export interface AIDataCenter {
|
||||
id: string;
|
||||
name: string;
|
||||
owner: string;
|
||||
country: string;
|
||||
lat: number;
|
||||
lon: number;
|
||||
status: 'existing' | 'planned' | 'decommissioned';
|
||||
chipType: string;
|
||||
chipCount: number;
|
||||
powerMW?: number;
|
||||
h100Equivalent?: number;
|
||||
sector?: string;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface InternetOutage {
|
||||
id: string;
|
||||
title: string;
|
||||
link: string;
|
||||
description: string;
|
||||
pubDate: Date;
|
||||
country: string;
|
||||
region?: string;
|
||||
lat: number;
|
||||
lon: number;
|
||||
severity: 'partial' | 'major' | 'total';
|
||||
categories: string[];
|
||||
cause?: string;
|
||||
outageType?: string;
|
||||
endDate?: Date;
|
||||
}
|
||||
|
||||
export type EconomicCenterType = 'exchange' | 'central-bank' | 'financial-hub';
|
||||
|
||||
export interface EconomicCenter {
|
||||
id: string;
|
||||
name: string;
|
||||
type: EconomicCenterType;
|
||||
lat: number;
|
||||
lon: number;
|
||||
country: string;
|
||||
marketHours?: { open: string; close: string; timezone: string };
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface PredictionMarket {
|
||||
|
||||
@@ -43,6 +43,9 @@ const PROXY_MAP: Record<string, string> = {
|
||||
'/rss/diplomat': 'https://thediplomat.com',
|
||||
'/rss/venturebeat': 'https://venturebeat.com',
|
||||
'/rss/foreignpolicy': 'https://foreignpolicy.com',
|
||||
'/rss/ft': 'https://www.ft.com',
|
||||
'/rss/openai': 'https://openai.com',
|
||||
'/rss/reuters': 'https://www.reutersagency.com',
|
||||
};
|
||||
|
||||
export function proxyUrl(localPath: string): string {
|
||||
|
||||
@@ -39,7 +39,13 @@ export default defineConfig({
|
||||
'/api/earthquake': {
|
||||
target: 'https://earthquake.usgs.gov',
|
||||
changeOrigin: true,
|
||||
timeout: 30000,
|
||||
rewrite: (path) => path.replace(/^\/api\/earthquake/, ''),
|
||||
configure: (proxy) => {
|
||||
proxy.on('error', (err) => {
|
||||
console.log('Earthquake proxy error:', err.message);
|
||||
});
|
||||
},
|
||||
},
|
||||
// FRED Economic Data
|
||||
'/api/fred': {
|
||||
@@ -300,6 +306,24 @@ export default defineConfig({
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/rss\/foreignpolicy/, ''),
|
||||
},
|
||||
// Financial Times
|
||||
'/rss/ft': {
|
||||
target: 'https://www.ft.com',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/rss\/ft/, ''),
|
||||
},
|
||||
// Reuters
|
||||
'/rss/reuters': {
|
||||
target: 'https://www.reutersagency.com',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/rss\/reuters/, ''),
|
||||
},
|
||||
// Cloudflare Radar - Internet outages
|
||||
'/api/cloudflare-radar': {
|
||||
target: 'https://api.cloudflare.com',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api\/cloudflare-radar/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user