Restore Regional Observations map
This commit is contained in:
parent
628270ac2e
commit
1faa580b18
15 changed files with 1006 additions and 302 deletions
462
datagenerators/output/radarcities.json
Normal file
462
datagenerators/output/radarcities.json
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
[
|
||||
{
|
||||
"name": "Anchorage",
|
||||
"lat": 61.2181,
|
||||
"lon": -149.9003
|
||||
},
|
||||
{
|
||||
"name": "Vancouver",
|
||||
"lat": 49.2827,
|
||||
"lon": -123.1207
|
||||
},
|
||||
{
|
||||
"name": "Seattle",
|
||||
"lat": 47.6062,
|
||||
"lon": -122.3321
|
||||
},
|
||||
{
|
||||
"name": "San Francisco",
|
||||
"lat": 37.7749,
|
||||
"lon": -122.4194
|
||||
},
|
||||
{
|
||||
"name": "Los Angeles",
|
||||
"lat": 34.0522,
|
||||
"lon": -118.2437
|
||||
},
|
||||
{
|
||||
"name": "Phoenix",
|
||||
"lat": 33.4484,
|
||||
"lon": -112.074
|
||||
},
|
||||
{
|
||||
"name": "Denver",
|
||||
"lat": 39.7392,
|
||||
"lon": -104.9903
|
||||
},
|
||||
{
|
||||
"name": "Dallas",
|
||||
"lat": 32.7767,
|
||||
"lon": -96.797
|
||||
},
|
||||
{
|
||||
"name": "Houston",
|
||||
"lat": 29.7604,
|
||||
"lon": -95.3698
|
||||
},
|
||||
{
|
||||
"name": "Minneapolis",
|
||||
"lat": 44.9778,
|
||||
"lon": -93.265
|
||||
},
|
||||
{
|
||||
"name": "Chicago",
|
||||
"lat": 41.8781,
|
||||
"lon": -87.6298
|
||||
},
|
||||
{
|
||||
"name": "Atlanta",
|
||||
"lat": 33.749,
|
||||
"lon": -84.388
|
||||
},
|
||||
{
|
||||
"name": "Miami",
|
||||
"lat": 25.7617,
|
||||
"lon": -80.1918
|
||||
},
|
||||
{
|
||||
"name": "Toronto",
|
||||
"lat": 43.6532,
|
||||
"lon": -79.3832
|
||||
},
|
||||
{
|
||||
"name": "Montreal",
|
||||
"lat": 45.5019,
|
||||
"lon": -73.5674
|
||||
},
|
||||
{
|
||||
"name": "New York",
|
||||
"lat": 40.7128,
|
||||
"lon": -74.006
|
||||
},
|
||||
{
|
||||
"name": "Boston",
|
||||
"lat": 42.3601,
|
||||
"lon": -71.0589
|
||||
},
|
||||
{
|
||||
"name": "Reykjavik",
|
||||
"lat": 64.1466,
|
||||
"lon": -21.9426
|
||||
},
|
||||
{
|
||||
"name": "Mexico City",
|
||||
"lat": 19.4326,
|
||||
"lon": -99.1332
|
||||
},
|
||||
{
|
||||
"name": "Guatemala City",
|
||||
"lat": 14.6349,
|
||||
"lon": -90.5069
|
||||
},
|
||||
{
|
||||
"name": "Havana",
|
||||
"lat": 23.1136,
|
||||
"lon": -82.3666
|
||||
},
|
||||
{
|
||||
"name": "Santo Domingo",
|
||||
"lat": 18.4861,
|
||||
"lon": -69.9312
|
||||
},
|
||||
{
|
||||
"name": "San Juan",
|
||||
"lat": 18.4655,
|
||||
"lon": -66.1057
|
||||
},
|
||||
{
|
||||
"name": "Bogota",
|
||||
"lat": 4.711,
|
||||
"lon": -74.0721
|
||||
},
|
||||
{
|
||||
"name": "Quito",
|
||||
"lat": -0.1807,
|
||||
"lon": -78.4678
|
||||
},
|
||||
{
|
||||
"name": "Lima",
|
||||
"lat": -12.0464,
|
||||
"lon": -77.0428
|
||||
},
|
||||
{
|
||||
"name": "La Paz",
|
||||
"lat": -16.4897,
|
||||
"lon": -68.1193
|
||||
},
|
||||
{
|
||||
"name": "Santiago",
|
||||
"lat": -33.4489,
|
||||
"lon": -70.6693
|
||||
},
|
||||
{
|
||||
"name": "Buenos Aires",
|
||||
"lat": -34.6037,
|
||||
"lon": -58.3816
|
||||
},
|
||||
{
|
||||
"name": "Montevideo",
|
||||
"lat": -34.9011,
|
||||
"lon": -56.1645
|
||||
},
|
||||
{
|
||||
"name": "Sao Paulo",
|
||||
"lat": -23.5558,
|
||||
"lon": -46.6396
|
||||
},
|
||||
{
|
||||
"name": "Rio",
|
||||
"lat": -22.9068,
|
||||
"lon": -43.1729
|
||||
},
|
||||
{
|
||||
"name": "Recife",
|
||||
"lat": -8.0476,
|
||||
"lon": -34.877
|
||||
},
|
||||
{
|
||||
"name": "London",
|
||||
"lat": 51.5072,
|
||||
"lon": -0.1276
|
||||
},
|
||||
{
|
||||
"name": "Dublin",
|
||||
"lat": 53.3498,
|
||||
"lon": -6.2603
|
||||
},
|
||||
{
|
||||
"name": "Paris",
|
||||
"lat": 48.8566,
|
||||
"lon": 2.3522
|
||||
},
|
||||
{
|
||||
"name": "Amsterdam",
|
||||
"lat": 52.3676,
|
||||
"lon": 4.9041
|
||||
},
|
||||
{
|
||||
"name": "Brussels",
|
||||
"lat": 50.8503,
|
||||
"lon": 4.3517
|
||||
},
|
||||
{
|
||||
"name": "Berlin",
|
||||
"lat": 52.52,
|
||||
"lon": 13.405
|
||||
},
|
||||
{
|
||||
"name": "Hamburg",
|
||||
"lat": 53.5511,
|
||||
"lon": 9.9937
|
||||
},
|
||||
{
|
||||
"name": "Madrid",
|
||||
"lat": 40.4168,
|
||||
"lon": -3.7038
|
||||
},
|
||||
{
|
||||
"name": "Lisbon",
|
||||
"lat": 38.7223,
|
||||
"lon": -9.1393
|
||||
},
|
||||
{
|
||||
"name": "Rome",
|
||||
"lat": 41.9028,
|
||||
"lon": 12.4964
|
||||
},
|
||||
{
|
||||
"name": "Milan",
|
||||
"lat": 45.4642,
|
||||
"lon": 9.19
|
||||
},
|
||||
{
|
||||
"name": "Vienna",
|
||||
"lat": 48.2082,
|
||||
"lon": 16.3738
|
||||
},
|
||||
{
|
||||
"name": "Prague",
|
||||
"lat": 50.0755,
|
||||
"lon": 14.4378
|
||||
},
|
||||
{
|
||||
"name": "Warsaw",
|
||||
"lat": 52.2297,
|
||||
"lon": 21.0122
|
||||
},
|
||||
{
|
||||
"name": "Stockholm",
|
||||
"lat": 59.3293,
|
||||
"lon": 18.0686
|
||||
},
|
||||
{
|
||||
"name": "Oslo",
|
||||
"lat": 59.9139,
|
||||
"lon": 10.7522
|
||||
},
|
||||
{
|
||||
"name": "Helsinki",
|
||||
"lat": 60.1699,
|
||||
"lon": 24.9384
|
||||
},
|
||||
{
|
||||
"name": "Athens",
|
||||
"lat": 37.9838,
|
||||
"lon": 23.7275
|
||||
},
|
||||
{
|
||||
"name": "Istanbul",
|
||||
"lat": 41.0082,
|
||||
"lon": 28.9784
|
||||
},
|
||||
{
|
||||
"name": "Kyiv",
|
||||
"lat": 50.4501,
|
||||
"lon": 30.5234
|
||||
},
|
||||
{
|
||||
"name": "Cairo",
|
||||
"lat": 30.0444,
|
||||
"lon": 31.2357
|
||||
},
|
||||
{
|
||||
"name": "Casablanca",
|
||||
"lat": 33.5731,
|
||||
"lon": -7.5898
|
||||
},
|
||||
{
|
||||
"name": "Lagos",
|
||||
"lat": 6.5244,
|
||||
"lon": 3.3792
|
||||
},
|
||||
{
|
||||
"name": "Accra",
|
||||
"lat": 5.6037,
|
||||
"lon": -0.187
|
||||
},
|
||||
{
|
||||
"name": "Nairobi",
|
||||
"lat": -1.2864,
|
||||
"lon": 36.8172
|
||||
},
|
||||
{
|
||||
"name": "Addis Ababa",
|
||||
"lat": 8.9806,
|
||||
"lon": 38.7578
|
||||
},
|
||||
{
|
||||
"name": "Johannesburg",
|
||||
"lat": -26.2041,
|
||||
"lon": 28.0473
|
||||
},
|
||||
{
|
||||
"name": "Cape Town",
|
||||
"lat": -33.9249,
|
||||
"lon": 18.4241
|
||||
},
|
||||
{
|
||||
"name": "Dubai",
|
||||
"lat": 25.2048,
|
||||
"lon": 55.2708
|
||||
},
|
||||
{
|
||||
"name": "Abu Dhabi",
|
||||
"lat": 24.4539,
|
||||
"lon": 54.3773
|
||||
},
|
||||
{
|
||||
"name": "Riyadh",
|
||||
"lat": 24.7136,
|
||||
"lon": 46.6753
|
||||
},
|
||||
{
|
||||
"name": "Doha",
|
||||
"lat": 25.2854,
|
||||
"lon": 51.531
|
||||
},
|
||||
{
|
||||
"name": "Kuwait City",
|
||||
"lat": 29.3759,
|
||||
"lon": 47.9774
|
||||
},
|
||||
{
|
||||
"name": "Jerusalem",
|
||||
"lat": 31.7683,
|
||||
"lon": 35.2137
|
||||
},
|
||||
{
|
||||
"name": "Amman",
|
||||
"lat": 31.9539,
|
||||
"lon": 35.9106
|
||||
},
|
||||
{
|
||||
"name": "Karachi",
|
||||
"lat": 24.8607,
|
||||
"lon": 67.0011
|
||||
},
|
||||
{
|
||||
"name": "Mumbai",
|
||||
"lat": 19.076,
|
||||
"lon": 72.8777
|
||||
},
|
||||
{
|
||||
"name": "Delhi",
|
||||
"lat": 28.6139,
|
||||
"lon": 77.209
|
||||
},
|
||||
{
|
||||
"name": "Dhaka",
|
||||
"lat": 23.8103,
|
||||
"lon": 90.4125
|
||||
},
|
||||
{
|
||||
"name": "Bangkok",
|
||||
"lat": 13.7563,
|
||||
"lon": 100.5018
|
||||
},
|
||||
{
|
||||
"name": "Hanoi",
|
||||
"lat": 21.0278,
|
||||
"lon": 105.8342
|
||||
},
|
||||
{
|
||||
"name": "Ho Chi Minh City",
|
||||
"lat": 10.8231,
|
||||
"lon": 106.6297
|
||||
},
|
||||
{
|
||||
"name": "Singapore",
|
||||
"lat": 1.3521,
|
||||
"lon": 103.8198
|
||||
},
|
||||
{
|
||||
"name": "Jakarta",
|
||||
"lat": -6.2088,
|
||||
"lon": 106.8456
|
||||
},
|
||||
{
|
||||
"name": "Manila",
|
||||
"lat": 14.5995,
|
||||
"lon": 120.9842
|
||||
},
|
||||
{
|
||||
"name": "Hong Kong",
|
||||
"lat": 22.3193,
|
||||
"lon": 114.1694
|
||||
},
|
||||
{
|
||||
"name": "Taipei",
|
||||
"lat": 25.033,
|
||||
"lon": 121.5654
|
||||
},
|
||||
{
|
||||
"name": "Shanghai",
|
||||
"lat": 31.2304,
|
||||
"lon": 121.4737
|
||||
},
|
||||
{
|
||||
"name": "Beijing",
|
||||
"lat": 39.9042,
|
||||
"lon": 116.4074
|
||||
},
|
||||
{
|
||||
"name": "Seoul",
|
||||
"lat": 37.5665,
|
||||
"lon": 126.978
|
||||
},
|
||||
{
|
||||
"name": "Tokyo",
|
||||
"lat": 35.6762,
|
||||
"lon": 139.6503
|
||||
},
|
||||
{
|
||||
"name": "Osaka",
|
||||
"lat": 34.6937,
|
||||
"lon": 135.5023
|
||||
},
|
||||
{
|
||||
"name": "Sapporo",
|
||||
"lat": 43.0618,
|
||||
"lon": 141.3545
|
||||
},
|
||||
{
|
||||
"name": "Sydney",
|
||||
"lat": -33.8688,
|
||||
"lon": 151.2093
|
||||
},
|
||||
{
|
||||
"name": "Melbourne",
|
||||
"lat": -37.8136,
|
||||
"lon": 144.9631
|
||||
},
|
||||
{
|
||||
"name": "Brisbane",
|
||||
"lat": -27.4698,
|
||||
"lon": 153.0251
|
||||
},
|
||||
{
|
||||
"name": "Perth",
|
||||
"lat": -31.9523,
|
||||
"lon": 115.8613
|
||||
},
|
||||
{
|
||||
"name": "Auckland",
|
||||
"lat": -36.8509,
|
||||
"lon": 174.7645
|
||||
},
|
||||
{
|
||||
"name": "Wellington",
|
||||
"lat": -41.2866,
|
||||
"lon": 174.7756
|
||||
}
|
||||
]
|
||||
94
datagenerators/radarcities-raw.json
Normal file
94
datagenerators/radarcities-raw.json
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
[
|
||||
{ "name": "Anchorage", "lat": 61.2181, "lon": -149.9003 },
|
||||
{ "name": "Vancouver", "lat": 49.2827, "lon": -123.1207 },
|
||||
{ "name": "Seattle", "lat": 47.6062, "lon": -122.3321 },
|
||||
{ "name": "San Francisco", "lat": 37.7749, "lon": -122.4194 },
|
||||
{ "name": "Los Angeles", "lat": 34.0522, "lon": -118.2437 },
|
||||
{ "name": "Phoenix", "lat": 33.4484, "lon": -112.074 },
|
||||
{ "name": "Denver", "lat": 39.7392, "lon": -104.9903 },
|
||||
{ "name": "Dallas", "lat": 32.7767, "lon": -96.797 },
|
||||
{ "name": "Houston", "lat": 29.7604, "lon": -95.3698 },
|
||||
{ "name": "Minneapolis", "lat": 44.9778, "lon": -93.265 },
|
||||
{ "name": "Chicago", "lat": 41.8781, "lon": -87.6298 },
|
||||
{ "name": "Atlanta", "lat": 33.749, "lon": -84.388 },
|
||||
{ "name": "Miami", "lat": 25.7617, "lon": -80.1918 },
|
||||
{ "name": "Toronto", "lat": 43.6532, "lon": -79.3832 },
|
||||
{ "name": "Montreal", "lat": 45.5019, "lon": -73.5674 },
|
||||
{ "name": "New York", "lat": 40.7128, "lon": -74.006 },
|
||||
{ "name": "Boston", "lat": 42.3601, "lon": -71.0589 },
|
||||
{ "name": "Reykjavik", "lat": 64.1466, "lon": -21.9426 },
|
||||
{ "name": "Mexico City", "lat": 19.4326, "lon": -99.1332 },
|
||||
{ "name": "Guatemala City", "lat": 14.6349, "lon": -90.5069 },
|
||||
{ "name": "Havana", "lat": 23.1136, "lon": -82.3666 },
|
||||
{ "name": "Santo Domingo", "lat": 18.4861, "lon": -69.9312 },
|
||||
{ "name": "San Juan", "lat": 18.4655, "lon": -66.1057 },
|
||||
{ "name": "Bogota", "lat": 4.711, "lon": -74.0721 },
|
||||
{ "name": "Quito", "lat": -0.1807, "lon": -78.4678 },
|
||||
{ "name": "Lima", "lat": -12.0464, "lon": -77.0428 },
|
||||
{ "name": "La Paz", "lat": -16.4897, "lon": -68.1193 },
|
||||
{ "name": "Santiago", "lat": -33.4489, "lon": -70.6693 },
|
||||
{ "name": "Buenos Aires", "lat": -34.6037, "lon": -58.3816 },
|
||||
{ "name": "Montevideo", "lat": -34.9011, "lon": -56.1645 },
|
||||
{ "name": "Sao Paulo", "lat": -23.5558, "lon": -46.6396 },
|
||||
{ "name": "Rio", "lat": -22.9068, "lon": -43.1729 },
|
||||
{ "name": "Recife", "lat": -8.0476, "lon": -34.877 },
|
||||
{ "name": "London", "lat": 51.5072, "lon": -0.1276 },
|
||||
{ "name": "Dublin", "lat": 53.3498, "lon": -6.2603 },
|
||||
{ "name": "Paris", "lat": 48.8566, "lon": 2.3522 },
|
||||
{ "name": "Amsterdam", "lat": 52.3676, "lon": 4.9041 },
|
||||
{ "name": "Brussels", "lat": 50.8503, "lon": 4.3517 },
|
||||
{ "name": "Berlin", "lat": 52.52, "lon": 13.405 },
|
||||
{ "name": "Hamburg", "lat": 53.5511, "lon": 9.9937 },
|
||||
{ "name": "Madrid", "lat": 40.4168, "lon": -3.7038 },
|
||||
{ "name": "Lisbon", "lat": 38.7223, "lon": -9.1393 },
|
||||
{ "name": "Rome", "lat": 41.9028, "lon": 12.4964 },
|
||||
{ "name": "Milan", "lat": 45.4642, "lon": 9.19 },
|
||||
{ "name": "Vienna", "lat": 48.2082, "lon": 16.3738 },
|
||||
{ "name": "Prague", "lat": 50.0755, "lon": 14.4378 },
|
||||
{ "name": "Warsaw", "lat": 52.2297, "lon": 21.0122 },
|
||||
{ "name": "Stockholm", "lat": 59.3293, "lon": 18.0686 },
|
||||
{ "name": "Oslo", "lat": 59.9139, "lon": 10.7522 },
|
||||
{ "name": "Helsinki", "lat": 60.1699, "lon": 24.9384 },
|
||||
{ "name": "Athens", "lat": 37.9838, "lon": 23.7275 },
|
||||
{ "name": "Istanbul", "lat": 41.0082, "lon": 28.9784 },
|
||||
{ "name": "Kyiv", "lat": 50.4501, "lon": 30.5234 },
|
||||
{ "name": "Cairo", "lat": 30.0444, "lon": 31.2357 },
|
||||
{ "name": "Casablanca", "lat": 33.5731, "lon": -7.5898 },
|
||||
{ "name": "Lagos", "lat": 6.5244, "lon": 3.3792 },
|
||||
{ "name": "Accra", "lat": 5.6037, "lon": -0.187 },
|
||||
{ "name": "Nairobi", "lat": -1.2864, "lon": 36.8172 },
|
||||
{ "name": "Addis Ababa", "lat": 8.9806, "lon": 38.7578 },
|
||||
{ "name": "Johannesburg", "lat": -26.2041, "lon": 28.0473 },
|
||||
{ "name": "Cape Town", "lat": -33.9249, "lon": 18.4241 },
|
||||
{ "name": "Dubai", "lat": 25.2048, "lon": 55.2708 },
|
||||
{ "name": "Abu Dhabi", "lat": 24.4539, "lon": 54.3773 },
|
||||
{ "name": "Riyadh", "lat": 24.7136, "lon": 46.6753 },
|
||||
{ "name": "Doha", "lat": 25.2854, "lon": 51.531 },
|
||||
{ "name": "Kuwait City", "lat": 29.3759, "lon": 47.9774 },
|
||||
{ "name": "Jerusalem", "lat": 31.7683, "lon": 35.2137 },
|
||||
{ "name": "Amman", "lat": 31.9539, "lon": 35.9106 },
|
||||
{ "name": "Karachi", "lat": 24.8607, "lon": 67.0011 },
|
||||
{ "name": "Mumbai", "lat": 19.076, "lon": 72.8777 },
|
||||
{ "name": "Delhi", "lat": 28.6139, "lon": 77.209 },
|
||||
{ "name": "Dhaka", "lat": 23.8103, "lon": 90.4125 },
|
||||
{ "name": "Bangkok", "lat": 13.7563, "lon": 100.5018 },
|
||||
{ "name": "Hanoi", "lat": 21.0278, "lon": 105.8342 },
|
||||
{ "name": "Ho Chi Minh City", "lat": 10.8231, "lon": 106.6297 },
|
||||
{ "name": "Singapore", "lat": 1.3521, "lon": 103.8198 },
|
||||
{ "name": "Jakarta", "lat": -6.2088, "lon": 106.8456 },
|
||||
{ "name": "Manila", "lat": 14.5995, "lon": 120.9842 },
|
||||
{ "name": "Hong Kong", "lat": 22.3193, "lon": 114.1694 },
|
||||
{ "name": "Taipei", "lat": 25.033, "lon": 121.5654 },
|
||||
{ "name": "Shanghai", "lat": 31.2304, "lon": 121.4737 },
|
||||
{ "name": "Beijing", "lat": 39.9042, "lon": 116.4074 },
|
||||
{ "name": "Seoul", "lat": 37.5665, "lon": 126.978 },
|
||||
{ "name": "Tokyo", "lat": 35.6762, "lon": 139.6503 },
|
||||
{ "name": "Osaka", "lat": 34.6937, "lon": 135.5023 },
|
||||
{ "name": "Sapporo", "lat": 43.0618, "lon": 141.3545 },
|
||||
{ "name": "Sydney", "lat": -33.8688, "lon": 151.2093 },
|
||||
{ "name": "Melbourne", "lat": -37.8136, "lon": 144.9631 },
|
||||
{ "name": "Brisbane", "lat": -27.4698, "lon": 153.0251 },
|
||||
{ "name": "Perth", "lat": -31.9523, "lon": 115.8613 },
|
||||
{ "name": "Auckland", "lat": -36.8509, "lon": 174.7645 },
|
||||
{ "name": "Wellington", "lat": -41.2866, "lon": 174.7756 }
|
||||
]
|
||||
17
datagenerators/radarcities.mjs
Normal file
17
datagenerators/radarcities.mjs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { readFile, writeFile } from 'fs/promises';
|
||||
|
||||
const radarCities = JSON.parse(await readFile('./datagenerators/radarcities-raw.json'));
|
||||
|
||||
const result = radarCities.map((city) => {
|
||||
if (!city?.name || typeof city.lat !== 'number' || typeof city.lon !== 'number') {
|
||||
throw new Error(`Invalid radar city: ${JSON.stringify(city)}`);
|
||||
}
|
||||
|
||||
return {
|
||||
name: city.name,
|
||||
lat: city.lat,
|
||||
lon: city.lon,
|
||||
};
|
||||
});
|
||||
|
||||
await writeFile('./datagenerators/output/radarcities.json', JSON.stringify(result, null, '\t'));
|
||||
|
|
@ -144,6 +144,7 @@ const copyDataFiles = () => src([
|
|||
'datagenerators/output/travelcities.json',
|
||||
'datagenerators/output/regionalcities.json',
|
||||
'datagenerators/output/stations.json',
|
||||
'datagenerators/output/radarcities.json',
|
||||
]).pipe(dest('./dist/data'));
|
||||
|
||||
const s3 = s3Upload({
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ const parseLwnStories = (html) => {
|
|||
const travelCities = JSON.parse(await readFile('./datagenerators/output/travelcities.json'));
|
||||
const regionalCities = JSON.parse(await readFile('./datagenerators/output/regionalcities.json'));
|
||||
const stationInfo = JSON.parse(await readFile('./datagenerators/output/stations.json'));
|
||||
const radarCities = JSON.parse(await readFile('./datagenerators/output/radarcities.json'));
|
||||
|
||||
const app = express();
|
||||
const port = process.env.WS4KP_PORT ?? 8080;
|
||||
|
|
@ -265,6 +266,7 @@ const dataEndpoints = {
|
|||
travelcities: travelCities,
|
||||
regionalcities: regionalCities,
|
||||
stations: stationInfo,
|
||||
radarcities: radarCities,
|
||||
};
|
||||
|
||||
Object.entries(dataEndpoints).forEach(([name, data]) => {
|
||||
|
|
|
|||
|
|
@ -3,14 +3,17 @@ import { DateTime } from '../vendor/auto/luxon.mjs';
|
|||
import { safeJson } from './utils/fetch.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import {
|
||||
createMap,
|
||||
addBaseLayers,
|
||||
setPrimaryLocationMarker,
|
||||
loadNearbyObservationMarkers,
|
||||
clearMarkers,
|
||||
} from './utils/leaflet-weather-map.mjs';
|
||||
|
||||
class Radar extends WeatherDisplay {
|
||||
static metadataUrl = 'https://api.rainviewer.com/public/weather-maps.json';
|
||||
|
||||
static baseMapUrl = 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}';
|
||||
|
||||
static boundaryMapUrl = 'https://services.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}';
|
||||
|
||||
constructor(navId, elemId) {
|
||||
super(navId, elemId, 'Local Radar');
|
||||
|
||||
|
|
@ -21,6 +24,7 @@ class Radar extends WeatherDisplay {
|
|||
this.baseLayer = null;
|
||||
this.boundaryLayer = null;
|
||||
this.locationMarker = null;
|
||||
this.nearbyMarkers = [];
|
||||
this.radarLayers = [];
|
||||
this.mapFrames = [];
|
||||
this.radarHost = '';
|
||||
|
|
@ -42,6 +46,7 @@ class Radar extends WeatherDisplay {
|
|||
this.map.invalidateSize();
|
||||
this.map.setView([this.weatherParameters.latitude, this.weatherParameters.longitude], 7);
|
||||
this.updateLocationMarker();
|
||||
await this.updateNearbyMarkers();
|
||||
|
||||
const radarMetadata = await safeJson(Radar.metadataUrl, {
|
||||
retryCount: 2,
|
||||
|
|
@ -67,6 +72,7 @@ class Radar extends WeatherDisplay {
|
|||
} catch (error) {
|
||||
console.error(`Failed to initialize radar: ${error.message}`);
|
||||
this.clearRadarLayers();
|
||||
this.clearNearbyMarkers();
|
||||
this.timing.totalScreens = 0;
|
||||
if (this.isEnabled) this.setStatus(STATUS.failed);
|
||||
}
|
||||
|
|
@ -80,37 +86,8 @@ class Radar extends WeatherDisplay {
|
|||
throw new Error('Radar map container not found');
|
||||
}
|
||||
|
||||
this.map = window.L.map(mapElement, {
|
||||
zoomControl: false,
|
||||
dragging: false,
|
||||
touchZoom: false,
|
||||
scrollWheelZoom: false,
|
||||
doubleClickZoom: false,
|
||||
boxZoom: false,
|
||||
keyboard: false,
|
||||
tap: false,
|
||||
attributionControl: false,
|
||||
preferCanvas: true,
|
||||
});
|
||||
|
||||
this.baseLayer = window.L.tileLayer(Radar.baseMapUrl, {
|
||||
maxZoom: 10,
|
||||
minZoom: 1,
|
||||
crossOrigin: true,
|
||||
className: 'radar-base-layer',
|
||||
});
|
||||
|
||||
this.baseLayer.addTo(this.map);
|
||||
|
||||
this.boundaryLayer = window.L.tileLayer(Radar.boundaryMapUrl, {
|
||||
maxZoom: 10,
|
||||
minZoom: 1,
|
||||
opacity: 0.6,
|
||||
crossOrigin: true,
|
||||
className: 'radar-boundary-layer',
|
||||
});
|
||||
|
||||
this.boundaryLayer.addTo(this.map);
|
||||
this.map = createMap(mapElement);
|
||||
({ baseLayer: this.baseLayer, boundaryLayer: this.boundaryLayer } = addBaseLayers(this.map));
|
||||
}
|
||||
|
||||
resetRadarLayers() {
|
||||
|
|
@ -162,23 +139,27 @@ class Radar extends WeatherDisplay {
|
|||
|
||||
updateLocationMarker() {
|
||||
if (!this.map) return;
|
||||
|
||||
if (this.locationMarker && this.map.hasLayer(this.locationMarker)) {
|
||||
this.map.removeLayer(this.locationMarker);
|
||||
}
|
||||
|
||||
this.locationMarker = window.L.circleMarker([
|
||||
this.locationMarker = setPrimaryLocationMarker(
|
||||
this.map,
|
||||
this.locationMarker,
|
||||
this.weatherParameters.latitude,
|
||||
this.weatherParameters.longitude,
|
||||
], {
|
||||
radius: 5,
|
||||
color: '#000',
|
||||
weight: 2,
|
||||
fillColor: '#ff0',
|
||||
fillOpacity: 1,
|
||||
interactive: false,
|
||||
className: 'location-marker',
|
||||
}).addTo(this.map);
|
||||
);
|
||||
}
|
||||
|
||||
clearNearbyMarkers() {
|
||||
this.nearbyMarkers = clearMarkers(this.map, this.nearbyMarkers);
|
||||
}
|
||||
|
||||
async updateNearbyMarkers() {
|
||||
if (!this.map) return;
|
||||
|
||||
this.clearNearbyMarkers();
|
||||
this.nearbyMarkers = await loadNearbyObservationMarkers(this.map, {
|
||||
latitude: this.weatherParameters.latitude,
|
||||
longitude: this.weatherParameters.longitude,
|
||||
});
|
||||
this.nearbyMarkers.forEach((marker) => marker.addTo(this.map));
|
||||
}
|
||||
|
||||
showFrame(screenIndex) {
|
||||
|
|
|
|||
|
|
@ -1,245 +1,115 @@
|
|||
// regional forecast and observations
|
||||
// type 0 = observations, 1 = first forecast, 2 = second forecast
|
||||
// regional observations display
|
||||
|
||||
import STATUS from './status.mjs';
|
||||
import { distance as calcDistance } from './utils/calc.mjs';
|
||||
import { safeJson, safePromiseAll } from './utils/fetch.mjs';
|
||||
import { temperature as temperatureUnit } from './utils/units.mjs';
|
||||
import { getSmallIcon } from './icons.mjs';
|
||||
import { preloadImg } from './utils/image.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import * as utils from './regionalforecast-utils.mjs';
|
||||
import { getPoint } from './utils/weather.mjs';
|
||||
import { debugFlag } from './utils/debug.mjs';
|
||||
import filterExpiredPeriods from './utils/forecast-utils.mjs';
|
||||
|
||||
// map offset
|
||||
const mapOffsetXY = {
|
||||
x: 240,
|
||||
y: 117,
|
||||
};
|
||||
import {
|
||||
createMap,
|
||||
addBaseLayers,
|
||||
setPrimaryLocationMarker,
|
||||
loadNearbyObservationMarkers,
|
||||
clearMarkers,
|
||||
} from './utils/leaflet-weather-map.mjs';
|
||||
|
||||
class RegionalForecast extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
super(navId, elemId, 'Regional Forecast', true);
|
||||
|
||||
// timings
|
||||
this.timing.totalScreens = 3;
|
||||
super(navId, elemId, 'Regional Observations', true);
|
||||
this.timing.totalScreens = 1;
|
||||
this.map = null;
|
||||
this.baseLayer = null;
|
||||
this.boundaryLayer = null;
|
||||
this.locationMarker = null;
|
||||
this.nearbyMarkers = [];
|
||||
this.nearbyMarkersKey = '';
|
||||
}
|
||||
|
||||
async getData(weatherParameters, refresh) {
|
||||
if (!super.getData(weatherParameters, refresh)) return;
|
||||
if (!this.weatherParameters?.supportsNoaaDisplays) {
|
||||
this.data = [];
|
||||
this.timing.totalScreens = 0;
|
||||
|
||||
try {
|
||||
if (!window.L) {
|
||||
throw new Error('Leaflet is not available');
|
||||
}
|
||||
|
||||
await this.ensureMap();
|
||||
this.map.invalidateSize();
|
||||
this.map.setView([this.weatherParameters.latitude, this.weatherParameters.longitude], 6);
|
||||
this.locationMarker = setPrimaryLocationMarker(
|
||||
this.map,
|
||||
this.locationMarker,
|
||||
this.weatherParameters.latitude,
|
||||
this.weatherParameters.longitude,
|
||||
);
|
||||
this.nearbyMarkers = clearMarkers(this.map, this.nearbyMarkers);
|
||||
this.nearbyMarkersKey = '';
|
||||
|
||||
this.timing.totalScreens = 1;
|
||||
this.setStatus(STATUS.loaded);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error(`Failed to initialize regional observations: ${error.message}`);
|
||||
this.nearbyMarkers = clearMarkers(this.map, this.nearbyMarkers);
|
||||
this.timing.totalScreens = 0;
|
||||
if (this.isEnabled) this.setStatus(STATUS.failed);
|
||||
}
|
||||
this.timing.totalScreens = 3;
|
||||
// regional forecast implements a silent reload
|
||||
// but it will not fall back to previously loaded data if data can not be loaded
|
||||
// there are enough other cities available to populate the map sufficiently even if some do not load
|
||||
}
|
||||
|
||||
// pre-load the base map
|
||||
let baseMap = 'images/maps/basemap.webp';
|
||||
if (weatherParameters.state === 'HI') {
|
||||
baseMap = 'images/maps/radar-hawaii.png';
|
||||
} else if (weatherParameters.state === 'AK') {
|
||||
baseMap = 'images/maps/radar-alaska.png';
|
||||
}
|
||||
this.elem.querySelector('.map img').src = baseMap;
|
||||
async refreshNearbyMarkers() {
|
||||
if (!this.map || !this.active) return;
|
||||
|
||||
// get user's location in x/y
|
||||
const sourceXY = utils.getXYFromLatitudeLongitude(this.weatherParameters.latitude, this.weatherParameters.longitude, mapOffsetXY.x, mapOffsetXY.y, weatherParameters.state);
|
||||
this.map.invalidateSize(false);
|
||||
this.map.setView([this.weatherParameters.latitude, this.weatherParameters.longitude], 6);
|
||||
|
||||
// get latitude and longitude limits
|
||||
const minMaxLatLon = utils.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, mapOffsetXY.x, mapOffsetXY.y, this.weatherParameters.state);
|
||||
const bounds = this.map.getBounds();
|
||||
const markerKey = [
|
||||
this.weatherParameters.latitude.toFixed(2),
|
||||
this.weatherParameters.longitude.toFixed(2),
|
||||
bounds.getSouth().toFixed(2),
|
||||
bounds.getWest().toFixed(2),
|
||||
bounds.getNorth().toFixed(2),
|
||||
bounds.getEast().toFixed(2),
|
||||
].join(':');
|
||||
|
||||
// get a target distance
|
||||
let targetDistance = 2.4;
|
||||
if (this.weatherParameters.state === 'HI') targetDistance = 1;
|
||||
if (this.nearbyMarkers.length > 0 && this.nearbyMarkersKey === markerKey) return;
|
||||
|
||||
// make station info into an array
|
||||
const stationInfoArray = Object.values(StationInfo).map((station) => ({ ...station, targetDistance }));
|
||||
// combine regional cities with station info for additional stations
|
||||
// stations are intentionally after cities to allow cities priority when drawing the map
|
||||
const combinedCities = [...RegionalCities, ...stationInfoArray];
|
||||
|
||||
// Determine which cities are within the max/min latitude/longitude.
|
||||
const regionalCities = [];
|
||||
combinedCities.forEach((city) => {
|
||||
if (city.lat > minMaxLatLon.minLat && city.lat < minMaxLatLon.maxLat
|
||||
&& city.lon > minMaxLatLon.minLon && city.lon < minMaxLatLon.maxLon - 1) {
|
||||
// default to 1 for cities loaded from RegionalCities, use value calculate above for remaining stations
|
||||
const targetDist = city.targetDistance || 1;
|
||||
// Only add the city as long as it isn't within set distance degree of any other city already in the array.
|
||||
const okToAddCity = regionalCities.reduce((acc, testCity) => {
|
||||
const distance = calcDistance(city.lon, city.lat, testCity.lon, testCity.lat);
|
||||
return acc && distance >= targetDist;
|
||||
}, true);
|
||||
if (okToAddCity) regionalCities.push(city);
|
||||
}
|
||||
this.nearbyMarkers = clearMarkers(this.map, this.nearbyMarkers);
|
||||
this.nearbyMarkers = await loadNearbyObservationMarkers(this.map, {
|
||||
latitude: this.weatherParameters.latitude,
|
||||
longitude: this.weatherParameters.longitude,
|
||||
});
|
||||
this.nearbyMarkers.forEach((marker) => marker.addTo(this.map));
|
||||
this.nearbyMarkersKey = markerKey;
|
||||
}
|
||||
|
||||
// get a unit converter
|
||||
const temperatureConverter = temperatureUnit();
|
||||
async ensureMap() {
|
||||
if (this.map) return;
|
||||
|
||||
// get regional forecasts and observations using centralized safe Promise handling
|
||||
const regionalDataAll = await safePromiseAll(regionalCities.map(async (city) => {
|
||||
try {
|
||||
const point = city?.point ?? (await getAndFormatPoint(city.lat, city.lon));
|
||||
if (!point) {
|
||||
if (debugFlag('verbose-failures')) {
|
||||
console.warn(`Unable to get Points for '${city.Name ?? city.city}'`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// start off the observation task
|
||||
const observationPromise = utils.getRegionalObservation(point, city);
|
||||
|
||||
const forecast = await safeJson(`https://api.weather.gov/gridpoints/${point.wfo}/${point.x},${point.y}/forecast`);
|
||||
if (!forecast) {
|
||||
if (debugFlag('verbose-failures')) {
|
||||
console.warn(`Regional Forecast request for ${city.Name ?? city.city} failed`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// get XY on map for city
|
||||
const cityXY = utils.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, this.weatherParameters.state);
|
||||
|
||||
// wait for the regional observation if it's not done yet
|
||||
const observation = await observationPromise;
|
||||
|
||||
if (!observation) return false;
|
||||
|
||||
// format the observation the same as the forecast
|
||||
const regionalObservation = {
|
||||
daytime: !!/\/day\//.test(observation.icon),
|
||||
temperature: temperatureConverter(observation.temperature.value),
|
||||
name: utils.formatCity(city.city),
|
||||
icon: observation.icon,
|
||||
x: cityXY.x,
|
||||
y: cityXY.y,
|
||||
};
|
||||
|
||||
// preload the icon
|
||||
preloadImg(getSmallIcon(regionalObservation.icon, !regionalObservation.daytime));
|
||||
|
||||
// filter out expired periods first, then use the next two periods for forecast
|
||||
const activePeriods = filterExpiredPeriods(forecast.properties.periods);
|
||||
|
||||
// ensure we have enough periods for forecast
|
||||
if (activePeriods.length < 3) {
|
||||
console.warn(`Insufficient active periods for ${city.Name ?? city.city}: only ${activePeriods.length} periods available`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// group together the current observation and next two periods
|
||||
return [
|
||||
regionalObservation,
|
||||
utils.buildForecast(activePeriods[1], city, cityXY),
|
||||
utils.buildForecast(activePeriods[2], city, cityXY),
|
||||
];
|
||||
} catch (error) {
|
||||
console.error(`Unexpected error getting Regional Forecast data for '${city.name ?? city.city}': ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
|
||||
// filter out any false (unavailable data)
|
||||
const regionalData = regionalDataAll.filter((data) => data);
|
||||
|
||||
// test for data present
|
||||
if (regionalData.length === 0) {
|
||||
this.setStatus(STATUS.noData);
|
||||
return;
|
||||
const mapElement = this.elem.querySelector('.leaflet-map');
|
||||
if (!mapElement) {
|
||||
throw new Error('Regional observations map container not found');
|
||||
}
|
||||
|
||||
// return the weather data and offsets
|
||||
this.data = {
|
||||
regionalData,
|
||||
mapOffsetXY,
|
||||
sourceXY,
|
||||
};
|
||||
|
||||
this.setStatus(STATUS.loaded);
|
||||
this.map = createMap(mapElement);
|
||||
({ baseLayer: this.baseLayer, boundaryLayer: this.boundaryLayer } = addBaseLayers(this.map));
|
||||
}
|
||||
|
||||
drawCanvas() {
|
||||
super.drawCanvas();
|
||||
// break up data into useful values
|
||||
const { regionalData: data, sourceXY } = this.data;
|
||||
|
||||
// draw the header graphics
|
||||
|
||||
// draw the appropriate title
|
||||
const titleTop = this.elem.querySelector('.title.dual .top');
|
||||
const titleBottom = this.elem.querySelector('.title.dual .bottom');
|
||||
if (this.screenIndex === 0) {
|
||||
titleTop.innerHTML = 'Regional';
|
||||
titleBottom.innerHTML = 'Observations';
|
||||
} else {
|
||||
const forecastDate = DateTime.fromISO(data[0][this.screenIndex].time);
|
||||
titleTop.innerHTML = 'Regional';
|
||||
titleBottom.innerHTML = 'Observations';
|
||||
|
||||
// get the name of the day
|
||||
const dayName = forecastDate.toLocaleString({ weekday: 'long' });
|
||||
titleTop.innerHTML = 'Forecast for';
|
||||
// draw the title
|
||||
titleBottom.innerHTML = data[0][this.screenIndex].daytime
|
||||
? dayName
|
||||
: `${dayName} Night`;
|
||||
if (this.map) {
|
||||
this.map.invalidateSize(false);
|
||||
}
|
||||
|
||||
// draw the map
|
||||
const scale = 640 / (mapOffsetXY.x * 2);
|
||||
const map = this.elem.querySelector('.map');
|
||||
map.style.transform = `scale(${scale}) translate(-${sourceXY.x}px, -${sourceXY.y}px)`;
|
||||
|
||||
const cities = data.map((city) => {
|
||||
const fill = {};
|
||||
const period = city[this.screenIndex];
|
||||
|
||||
fill.icon = { type: 'img', src: getSmallIcon(period.icon, !period.daytime) };
|
||||
fill.city = period.name;
|
||||
const { temperature } = period;
|
||||
fill.temp = temperature;
|
||||
|
||||
const { x, y } = period;
|
||||
|
||||
const elem = this.fillTemplate('location', fill);
|
||||
elem.style.left = `${x}px`;
|
||||
elem.style.top = `${y}px`;
|
||||
|
||||
return elem;
|
||||
});
|
||||
|
||||
const locationContainer = this.elem.querySelector('.location-container');
|
||||
locationContainer.innerHTML = '';
|
||||
locationContainer.append(...cities);
|
||||
|
||||
this.finishDraw();
|
||||
}
|
||||
|
||||
async showCanvas(navCmd) {
|
||||
super.showCanvas(navCmd);
|
||||
await this.refreshNearbyMarkers();
|
||||
}
|
||||
}
|
||||
|
||||
const getAndFormatPoint = async (lat, lon) => {
|
||||
try {
|
||||
const point = await getPoint(lat, lon);
|
||||
if (!point) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
x: point.properties.gridX,
|
||||
y: point.properties.gridY,
|
||||
wfo: point.properties.gridId,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Unexpected error getting point for ${lat},${lon}: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// register display
|
||||
registerDisplay(new RegionalForecast(6, 'regional-forecast'));
|
||||
|
|
|
|||
155
server/scripts/modules/utils/leaflet-weather-map.mjs
Normal file
155
server/scripts/modules/utils/leaflet-weather-map.mjs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import { safePromiseAll } from './fetch.mjs';
|
||||
import { loadData } from './data-loader.mjs';
|
||||
import { getSmallIconFromWmoCode } from '../icons.mjs';
|
||||
import { getOpenMeteoObservationSnapshot } from './weather.mjs';
|
||||
|
||||
const BASE_MAP_URL = 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}';
|
||||
const BOUNDARY_MAP_URL = 'https://services.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}';
|
||||
const DEFAULT_MAX_NEARBY_MARKERS = 7;
|
||||
const MIN_CITY_DISTANCE_METERS = 25000;
|
||||
const MIN_MARKER_PIXEL_DISTANCE = 85;
|
||||
|
||||
let radarCitiesCache = null;
|
||||
|
||||
const createMap = (mapElement) => window.L.map(mapElement, {
|
||||
zoomControl: false,
|
||||
dragging: false,
|
||||
touchZoom: false,
|
||||
scrollWheelZoom: false,
|
||||
doubleClickZoom: false,
|
||||
boxZoom: false,
|
||||
keyboard: false,
|
||||
tap: false,
|
||||
attributionControl: false,
|
||||
preferCanvas: true,
|
||||
});
|
||||
|
||||
const addBaseLayers = (map) => {
|
||||
const baseLayer = window.L.tileLayer(BASE_MAP_URL, {
|
||||
maxZoom: 10,
|
||||
minZoom: 1,
|
||||
crossOrigin: true,
|
||||
className: 'radar-base-layer',
|
||||
}).addTo(map);
|
||||
|
||||
const boundaryLayer = window.L.tileLayer(BOUNDARY_MAP_URL, {
|
||||
maxZoom: 10,
|
||||
minZoom: 1,
|
||||
opacity: 0.6,
|
||||
crossOrigin: true,
|
||||
className: 'radar-boundary-layer',
|
||||
}).addTo(map);
|
||||
|
||||
return { baseLayer, boundaryLayer };
|
||||
};
|
||||
|
||||
const setPrimaryLocationMarker = (map, existingMarker, latitude, longitude) => {
|
||||
if (existingMarker && map.hasLayer(existingMarker)) {
|
||||
map.removeLayer(existingMarker);
|
||||
}
|
||||
|
||||
return window.L.circleMarker([latitude, longitude], {
|
||||
radius: 5,
|
||||
color: '#000',
|
||||
weight: 2,
|
||||
fillColor: '#ff0',
|
||||
fillOpacity: 1,
|
||||
interactive: false,
|
||||
className: 'location-marker',
|
||||
}).addTo(map);
|
||||
};
|
||||
|
||||
const loadRadarCities = async () => {
|
||||
if (!radarCitiesCache) {
|
||||
radarCitiesCache = await loadData('radarcities');
|
||||
}
|
||||
return radarCitiesCache ?? [];
|
||||
};
|
||||
|
||||
const selectNearbyCities = (map, sourceLocation, cities, options = {}) => {
|
||||
const {
|
||||
maxMarkers = DEFAULT_MAX_NEARBY_MARKERS,
|
||||
minCityDistanceMeters = MIN_CITY_DISTANCE_METERS,
|
||||
minMarkerPixelDistance = MIN_MARKER_PIXEL_DISTANCE,
|
||||
} = options;
|
||||
|
||||
const bounds = map.getBounds();
|
||||
const currentLatLng = window.L.latLng(sourceLocation.latitude, sourceLocation.longitude);
|
||||
const visibleCities = cities
|
||||
.filter((city) => bounds.contains([city.lat, city.lon]))
|
||||
.filter((city) => currentLatLng.distanceTo([city.lat, city.lon]) > minCityDistanceMeters)
|
||||
.map((city) => ({
|
||||
...city,
|
||||
distance: currentLatLng.distanceTo([city.lat, city.lon]),
|
||||
point: map.latLngToContainerPoint([city.lat, city.lon]),
|
||||
}))
|
||||
.sort((a, b) => a.distance - b.distance);
|
||||
|
||||
const selected = [];
|
||||
visibleCities.forEach((city) => {
|
||||
if (selected.length >= maxMarkers) return;
|
||||
const overlaps = selected.some((existingCity) => existingCity.point.distanceTo(city.point) < minMarkerPixelDistance);
|
||||
if (!overlaps) selected.push(city);
|
||||
});
|
||||
|
||||
if (selected.length === 0 && visibleCities.length > 0) {
|
||||
selected.push(visibleCities[0]);
|
||||
}
|
||||
|
||||
return selected;
|
||||
};
|
||||
|
||||
const buildNearbyWeatherMarker = (city, observation) => {
|
||||
const icon = getSmallIconFromWmoCode(observation.weatherCode, observation.isDay);
|
||||
const markerHtml = `
|
||||
<div class="nearby-weather-marker-inner">
|
||||
<div class="city">${city.name}</div>
|
||||
<div class="details">
|
||||
<div class="temp">${Math.round(observation.temperature)}</div>
|
||||
<img src="${icon}" alt="${city.name} weather" />
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
return window.L.marker([city.lat, city.lon], {
|
||||
icon: window.L.divIcon({
|
||||
html: markerHtml,
|
||||
className: 'nearby-weather-marker',
|
||||
iconSize: [108, 52],
|
||||
iconAnchor: [54, 26],
|
||||
}),
|
||||
interactive: false,
|
||||
zIndexOffset: 500,
|
||||
});
|
||||
};
|
||||
|
||||
const clearMarkers = (map, markers) => {
|
||||
if (!map || !markers?.length) return [];
|
||||
markers.forEach((marker) => {
|
||||
if (map.hasLayer(marker)) map.removeLayer(marker);
|
||||
});
|
||||
return [];
|
||||
};
|
||||
|
||||
const loadNearbyObservationMarkers = async (map, sourceLocation, options = {}) => {
|
||||
const radarCities = await loadRadarCities();
|
||||
const nearbyCities = selectNearbyCities(map, sourceLocation, radarCities, options);
|
||||
if (!nearbyCities.length) return [];
|
||||
|
||||
const nearbyObservations = await safePromiseAll(nearbyCities.map(async (city) => {
|
||||
const observation = await getOpenMeteoObservationSnapshot(city.lat, city.lon);
|
||||
if (!observation || observation.temperature === null) return null;
|
||||
return { city, observation };
|
||||
}));
|
||||
|
||||
return nearbyObservations
|
||||
.filter((entry) => entry)
|
||||
.map(({ city, observation }) => buildNearbyWeatherMarker(city, observation));
|
||||
};
|
||||
|
||||
export {
|
||||
createMap,
|
||||
addBaseLayers,
|
||||
setPrimaryLocationMarker,
|
||||
loadNearbyObservationMarkers,
|
||||
clearMarkers,
|
||||
};
|
||||
|
|
@ -8,6 +8,13 @@ const OPEN_METEO_FORECAST_PARAMETERS = [
|
|||
'models=best_match',
|
||||
].join('&');
|
||||
|
||||
const OPEN_METEO_RADAR_OBSERVATION_PARAMETERS = [
|
||||
'hourly=temperature_2m,weather_code,is_day',
|
||||
'forecast_days=1',
|
||||
'timezone=auto',
|
||||
'models=best_match',
|
||||
].join('&');
|
||||
|
||||
const getPoint = async (lat, lon) => {
|
||||
const point = await safeJson(`https://api.weather.gov/points/${lat.toFixed(4)},${lon.toFixed(4)}`);
|
||||
if (!point) {
|
||||
|
|
@ -48,6 +55,36 @@ const getAggregatedOpenMeteoForecast = async (lat, lon) => {
|
|||
};
|
||||
};
|
||||
|
||||
const getOpenMeteoObservationSnapshot = async (lat, lon) => {
|
||||
const forecast = await safeJson(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&${OPEN_METEO_RADAR_OBSERVATION_PARAMETERS}`);
|
||||
if (!forecast?.hourly?.time?.length) {
|
||||
if (debugFlag('verbose-failures')) {
|
||||
console.warn(`Unable to get Open-Meteo radar observation snapshot for ${lat},${lon}`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
let nearestIndex = 0;
|
||||
let nearestDelta = Number.POSITIVE_INFINITY;
|
||||
|
||||
forecast.hourly.time.forEach((time, index) => {
|
||||
const delta = Math.abs(new Date(time).getTime() - now);
|
||||
if (delta < nearestDelta) {
|
||||
nearestDelta = delta;
|
||||
nearestIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
time: forecast.hourly.time[nearestIndex],
|
||||
temperature: forecast.hourly.temperature_2m?.[nearestIndex] ?? null,
|
||||
weatherCode: forecast.hourly.weather_code?.[nearestIndex] ?? 0,
|
||||
isDay: Boolean(forecast.hourly.is_day?.[nearestIndex] ?? 1),
|
||||
timezone: forecast.timezone,
|
||||
};
|
||||
};
|
||||
|
||||
const weatherConditions = [
|
||||
{ codes: [0], text: ['Clear sky'] },
|
||||
{ codes: [1, 2, 3], text: ['Mainly clear', 'Partly cloudy', 'Overcast'] },
|
||||
|
|
@ -143,6 +180,7 @@ export {
|
|||
getPoint,
|
||||
getOpenMeteoForecast,
|
||||
getAggregatedOpenMeteoForecast,
|
||||
getOpenMeteoObservationSnapshot,
|
||||
aggregateWeatherForecastData,
|
||||
getConditionText,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -157,6 +157,52 @@
|
|||
border: 2px solid #000;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.nearby-weather-marker {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
|
||||
.nearby-weather-marker-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 72px;
|
||||
padding: 2px 4px;
|
||||
background: rgba(18, 34, 61, 0.88);
|
||||
border: 1px solid #000;
|
||||
box-shadow: 1px 1px 0 #000;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.city {
|
||||
font-family: 'Star4000 Small';
|
||||
font-size: 11pt;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 1px;
|
||||
text-shadow: 1px 1px 0 #000;
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.temp {
|
||||
font-family: 'Star4000';
|
||||
font-size: 18pt;
|
||||
line-height: 1;
|
||||
color: #ff0;
|
||||
text-shadow: 1px 1px 0 #000;
|
||||
}
|
||||
|
||||
img {
|
||||
width: auto;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,48 +6,91 @@
|
|||
}
|
||||
|
||||
.weather-display .main.regional-forecast {
|
||||
|
||||
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
|
||||
.map {
|
||||
position: absolute;
|
||||
transform-origin: 0 0;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.location {
|
||||
position: absolute;
|
||||
width: 140px;
|
||||
margin-left: -40px;
|
||||
margin-top: -35px;
|
||||
.leaflet-map {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #061f3e;
|
||||
}
|
||||
|
||||
>div {
|
||||
position: absolute;
|
||||
@include u.text-shadow();
|
||||
}
|
||||
.leaflet-container {
|
||||
background: #061f3e;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.icon {
|
||||
top: 26px;
|
||||
left: 44px;
|
||||
.radar-base-layer,
|
||||
.radar-base-layer .leaflet-tile {
|
||||
filter: grayscale(0.35) brightness(0.58) contrast(1.1) saturate(0.2);
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 32px;
|
||||
}
|
||||
}
|
||||
.radar-boundary-layer,
|
||||
.radar-boundary-layer .leaflet-tile {
|
||||
filter: grayscale(0.8) brightness(0.7) contrast(1.3) saturate(0.1);
|
||||
}
|
||||
|
||||
.temp {
|
||||
font-family: 'Star4000 Large';
|
||||
font-size: 28px;
|
||||
padding-top: 2px;
|
||||
color: c.$title-color;
|
||||
top: 28px;
|
||||
text-align: right;
|
||||
width: 40px;
|
||||
.leaflet-control-container,
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-zoom {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.location-marker {
|
||||
background: #ff0;
|
||||
border: 2px solid #000;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.nearby-weather-marker {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
|
||||
.nearby-weather-marker-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 72px;
|
||||
padding: 2px 4px;
|
||||
background: rgba(18, 34, 61, 0.88);
|
||||
border: 1px solid #000;
|
||||
box-shadow: 1px 1px 0 #000;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.city {
|
||||
font-family: Star4000;
|
||||
font-size: 20px;
|
||||
font-family: 'Star4000 Small';
|
||||
font-size: 11pt;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 1px;
|
||||
@include u.text-shadow();
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.temp {
|
||||
font-family: 'Star4000';
|
||||
font-size: 18pt;
|
||||
line-height: 1;
|
||||
color: c.$title-color;
|
||||
@include u.text-shadow();
|
||||
}
|
||||
|
||||
img {
|
||||
width: auto;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@
|
|||
width: 640px;
|
||||
height: 60px;
|
||||
padding-top: 30px;
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
|
||||
.title {
|
||||
color: c.$title-color;
|
||||
|
|
@ -172,4 +174,4 @@
|
|||
.scroll-container {
|
||||
margin-left: 107px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
server/styles/ws.min.css
vendored
2
server/styles/ws.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,13 +1,6 @@
|
|||
<%- include('header.ejs', {titleDual:{ top: 'Regional' , bottom: 'Observations' }, hasTime: true }) %>
|
||||
<div class="main has-scroll regional-forecast">
|
||||
<div class="map"><img src="images/maps/basemap.webp" /></div>
|
||||
<div class="location-container">
|
||||
<div class="location template">
|
||||
<div class="icon">
|
||||
<img src="" />
|
||||
</div>
|
||||
<div class="city"></div>
|
||||
<div class="temp"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="map">
|
||||
<div class="leaflet-map"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue