304 lines
13 KiB
HTML
304 lines
13 KiB
HTML
<!-- Credit https://github.com/jdemaeyer/brightsky/blob/master/docs/demo/alerts/index.html */ -->
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Weather Dashboard</title>
|
|
<link rel="stylesheet" href="styles.css">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css" type="text/css">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/weather-icons/2.0.10/css/weather-icons.min.css" type="text/css">
|
|
|
|
<script type="module">
|
|
const BRIGHTSKY_URL = 'https://api.brightsky.dev/';
|
|
|
|
// Berlin
|
|
const LOCATION_LAT=52.52;
|
|
const LOCATION_LONG=13.4;
|
|
|
|
function dailyTotal(weather, key) {
|
|
return weather.reduce((total, record) => total + (record[key] || 0), 0.);
|
|
}
|
|
|
|
function dailyAvg(weather, key) {
|
|
return dailyTotal(weather, key) / weather.length;
|
|
}
|
|
|
|
function dailyMax(weather, key) {
|
|
return weather.reduce((max, record) => record[key] >= (max || record[key]) ? record[key] : max, null);
|
|
}
|
|
|
|
function dailyMin(weather, key) {
|
|
return weather.reduce((min, record) => record[key] <= (min || record[key]) ? record[key] : min, null);
|
|
}
|
|
|
|
function mode(arr) {
|
|
return arr.sort((a,b) =>
|
|
arr.filter(v => v===a).length
|
|
- arr.filter(v => v===b).length
|
|
).pop();
|
|
}
|
|
|
|
function setProperty(id, value) {
|
|
const el = document.getElementById(`weather-${id}`);
|
|
el.innerHTML = value;
|
|
}
|
|
|
|
function setPropertyAlert(id, value) {
|
|
const el = document.getElementById(`alerts-${id}`);
|
|
el.innerHTML = value;
|
|
}
|
|
|
|
function getTodayDate() {
|
|
const today = new Date();
|
|
const year = today.getFullYear();
|
|
const month = String(today.getMonth() + 1).padStart(2, '0'); // Months are zero-based
|
|
const day = String(today.getDate()).padStart(2, '0');
|
|
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
function mockAlertsData() {
|
|
return new Promise((resolve) => {
|
|
const mockedData = {
|
|
"alerts": [
|
|
{
|
|
"id": 279977,
|
|
"alert_id": "2.49.0.0.276.0.DWD.PVW.1691344680000.2cf9fad6-dc83-44ba-9e88-f2827439da59",
|
|
"effective": "2023-08-06T17:58:00+00:00",
|
|
"onset": "2023-08-07T08:00:00+00:00",
|
|
"expires": "2023-08-07T19:00:00+00:00",
|
|
"category": "met",
|
|
"response_type": "prepare",
|
|
"urgency": "immediate",
|
|
"severity": "minor",
|
|
"certainty": "likely",
|
|
"event_code": 51,
|
|
"event_en": "wind gusts",
|
|
"event_de": "WINDBÖEN",
|
|
"headline_en": "Official WARNING of WIND GUSTS",
|
|
"headline_de": "Amtliche WARNUNG vor WINDBÖEN",
|
|
"description_en": "There is a risk of wind gusts (level 1 of 4).\nMax. gusts: 50-60 km/h; Wind direction: west; Increased gusts: near showers and in exposed locations < 70 km/h",
|
|
"description_de": "Es treten Windböen mit Geschwindigkeiten zwischen 50 km/h (14 m/s, 28 kn, Bft 7) und 60 km/h (17 m/s, 33 kn, Bft 7) aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit Sturmböen bis 70 km/h (20 m/s, 38 kn, Bft 8) gerechnet werden.",
|
|
"instruction_en": null,
|
|
"instruction_de": null
|
|
}
|
|
],
|
|
"location": {
|
|
"warn_cell_id": 705515101,
|
|
"name": "Münster-Nord",
|
|
"name_short": "Münster-Nord",
|
|
"district": "Münster",
|
|
"state": "Nordrhein-Westfalen",
|
|
"state_short": "NW"
|
|
}
|
|
};
|
|
resolve(mockedData);
|
|
});
|
|
}
|
|
|
|
|
|
async function getWeather() {
|
|
const lat = LOCATION_LAT;
|
|
const lon = LOCATION_LONG;
|
|
const date = new Date(getTodayDate());
|
|
const data = await fetchWeather(lat, lon, date);
|
|
|
|
if (data.weather && data.weather.length > 0) {
|
|
updateSummary(data);
|
|
updateHourly(data);
|
|
}
|
|
}
|
|
|
|
async function fetchWeather(lat, lon, date) {
|
|
const dateStr = date.toISOString().split('T')[0];
|
|
const url = BRIGHTSKY_URL + `weather?lat=${lat}&lon=${lon}&date=${dateStr}&tz=Europe/Berlin`;
|
|
const response = await fetch(url);
|
|
const data = await response.json();
|
|
return data;
|
|
}
|
|
|
|
async function getAlerts() {
|
|
const lat = LOCATION_LAT;
|
|
const lon = LOCATION_LONG;
|
|
const data = await fetchAlerts(lat, lon);
|
|
//const data = await mockAlertsData();
|
|
|
|
if (data.alerts && data.alerts.length > 0) {
|
|
document.getElementById('no-alerts-info').classList.add('is-hidden');
|
|
const event = data.alerts[0].event_de;
|
|
const alertHeadline = data.alerts[0].headline_de;
|
|
const alertDescription = data.alerts[0].description_de;
|
|
setPropertyAlert('headline', alertHeadline);
|
|
setPropertyAlert('description', alertDescription);
|
|
document.getElementById('alert-icon').className = `wi ${ ALERT_ICON_MAPPING[event] }`;
|
|
} else {
|
|
document.getElementById('no-alerts-info');
|
|
document.getElementById('alerts-info').classList.add('is-hidden');
|
|
}
|
|
}
|
|
|
|
async function fetchAlerts(lat, lon) {
|
|
const url = BRIGHTSKY_URL + `alerts?lat=${lat}&lon=${lon}&tz=Europe/Berlin`;
|
|
const response = await fetch(url);
|
|
const data = await response.json();
|
|
return data;
|
|
}
|
|
|
|
const ICON_MAPPING = {
|
|
'clear-day': 'wi-day-sunny',
|
|
'clear-night': 'wi-night-clear',
|
|
'partly-cloudy-day': 'wi-day-cloudy',
|
|
'partly-cloudy-night': 'wi-night-cloudy',
|
|
'cloudy': 'wi-cloud',
|
|
'fog': 'wi-fog',
|
|
'wind': 'wi-strong-wind',
|
|
'rain': 'wi-rain',
|
|
'sleet': 'wi-sleet',
|
|
'snow': 'wi-snow',
|
|
'hail': 'wi-hail',
|
|
'thunderstorm': 'wi-thunderstorm',
|
|
}
|
|
|
|
const ALERT_ICON_MAPPING = {
|
|
'FROST': 'wi-snow',
|
|
'WINDBÖEN': 'wi-strong-wind',
|
|
}
|
|
|
|
function updateTodayDate() {
|
|
const today = new Date();
|
|
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
|
|
const formattedDate = today.toLocaleDateString('en-US', options);
|
|
document.getElementById('today-date').textContent = formattedDate;
|
|
}
|
|
|
|
function updateSummary(data) {
|
|
const sourceName = data.sources[0].station_name.replace(
|
|
/\B\w/g,
|
|
(txt) => txt.charAt(0).toLowerCase(),
|
|
);
|
|
const maxTemp = dailyMax(data.weather, 'temperature');
|
|
const minTemp = dailyMin(data.weather, 'temperature');
|
|
const maxPrecipitation = dailyMax(data.weather, 'precipitation');
|
|
const avgWindSpeed = dailyAvg(data.weather, 'wind_speed');
|
|
const totalSunshine = dailyTotal(data.weather, 'sunshine');
|
|
const totalSunshineHours = Math.floor(totalSunshine / 60);
|
|
const totalSunshineMinutes = Math.floor(totalSunshine - 60 * totalSunshineHours);
|
|
const avgCloudCover = dailyAvg(data.weather, 'cloud_cover');
|
|
const avgPressure = dailyAvg(data.weather, 'pressure_msl');
|
|
setProperty('station-name', sourceName);
|
|
setProperty('max-temp', maxTemp.toFixed(1));
|
|
setProperty('min-temp', minTemp.toFixed(1));
|
|
setProperty('max-precipitation', maxPrecipitation.toFixed(1));
|
|
setProperty('avg-wind-speed', avgWindSpeed.toFixed(1));
|
|
setProperty('total-sunshine', `${totalSunshineHours}:${(totalSunshineMinutes < 10 ? '0' : '') + totalSunshineMinutes}`);
|
|
setProperty('avg-cloud-cover', avgCloudCover.toFixed(0));
|
|
setProperty('avg-pressure', avgPressure.toFixed(1));
|
|
const icons = data.weather.map((record) => record.icon.replace('-night', '-day'));
|
|
document.getElementById('weather-day-icon').className = `wi ${ ICON_MAPPING[mode(icons)] }`;
|
|
}
|
|
|
|
function updateHourly(data) {
|
|
var hourly = '';
|
|
for (let i = 4; i < data.weather.length - 1; i += 4) {
|
|
var weather = data.weather[i];
|
|
var timestamp = new Date(weather.timestamp).toLocaleTimeString('en-us', {hour12: false, hour: 'numeric', minute: 'numeric'});
|
|
hourly += `
|
|
<div class="level-item has-text-centered">
|
|
<div>
|
|
<p class="heading">${ timestamp }</p>
|
|
<p>
|
|
<i class="wi ${ ICON_MAPPING[weather.icon] }"></i><br>
|
|
${ weather.temperature.toFixed(0) } °C
|
|
</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
setProperty('hourly', hourly);
|
|
}
|
|
|
|
getWeather();
|
|
getAlerts();
|
|
updateTodayDate();
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<header class="card-header has-background-link-light">
|
|
<div class="card-header-left">
|
|
<p class="card-header-title has-text-grey" style="display: block">
|
|
<span id="today-date">Monday, 27 July 2020</span>
|
|
</p>
|
|
</div>
|
|
</header>
|
|
<div id="weather" class="card weather">
|
|
<div class="card-content">
|
|
<div id="no-weather-info" class="is-overlay is-vertical-center is-hidden">
|
|
<div class="notification is-warning has-text-centered">
|
|
No data available.
|
|
</div>
|
|
</div>
|
|
<div class="media">
|
|
<div class="media-content">
|
|
<p class="title is-3">
|
|
<span title="Maximum temperature"><span id="weather-max-temp">19.3</span> °C</span>
|
|
<span class="has-text-grey-lighter">· <span title="Minimum temperature"><span id="weather-min-temp">14.3</span> °C</span></span>
|
|
</p>
|
|
<p class="subtitle is-5" title="Weather station name"><span id="weather-station-name">Münster/Osnabrück</span></p>
|
|
<div class="columns">
|
|
<div class="column">
|
|
<div class="weather-details has-text-grey">
|
|
<span title="Maximum hourly precipitation">
|
|
<i class="wi wi-umbrella"></i><span id="weather-max-precipitation">0.4</span> mm/h
|
|
</span><br>
|
|
<span title="Average wind speed">
|
|
<i class="wi wi-strong-wind"></i><span id="weather-avg-wind-speed">9</span> km/h
|
|
</span><br>
|
|
<span title="Total sunshine duration">
|
|
<i class="wi wi-day-sunny"></i><span id="weather-total-sunshine">6:49</span> h
|
|
</span><br>
|
|
</div>
|
|
</div>
|
|
<div class="column">
|
|
<div class="weather-details has-text-grey">
|
|
<span title="Average cloud cover">
|
|
<i class="wi wi-cloudy"></i><span id="weather-avg-cloud-cover">72</span> %
|
|
</span><br>
|
|
<span title="Average atmospheric pressure, reduced to mean sea level">
|
|
<i class="wi wi-barometer"></i><span id="weather-avg-pressure">1022.3</span> hPa
|
|
</span><br>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
<div class="media-right" style="margin: auto 0">
|
|
<figure>
|
|
<i id="weather-day-icon" class="wi wi-day-cloudy"></i>
|
|
</figure>
|
|
</div>
|
|
</div>
|
|
<div id="weather-hourly" class="level is-mobile"></div>
|
|
<div id="no-alerts-info" class="level is-mobile">
|
|
<div class="notification is-success has-text-centered">
|
|
No weather alerts! :)
|
|
</div>
|
|
</div>
|
|
<div id ="alerts-info" class="alerts-details has-text-grey box">
|
|
<span title="Alert Headline">
|
|
<i id="alert-icon" class="wi wi-cloudy"></i><span id="alerts-headline" class="has-text-weight-bold">No warning</span>:
|
|
</span>
|
|
<span title="Alert Description">
|
|
<span id="alerts-description">No description</span>
|
|
</span><br>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</body>
|
|
</html>
|