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:
Elie Habib
2026-01-09 22:47:55 +04:00
parent 6769e02608
commit 0db94ee8ad
25 changed files with 8458 additions and 117 deletions

View 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 ResearchSosny",
"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
View 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 }
]
}

View File

@@ -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
}
}

View File

@@ -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 => {

View File

@@ -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';
}
}
}

View 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
}
}
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -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' },

View File

@@ -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' },
];

View File

@@ -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
View 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 },
];

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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',

View File

@@ -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 [];
}
}

View File

@@ -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);

View File

@@ -8,3 +8,4 @@ export * from './storage';
export * from './correlation';
export * from './weather';
export * from './fred';
export * from './outages';

View File

@@ -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
View 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 [];
}
}

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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/, ''),
},
},
},
});