592 lines
22 KiB
JavaScript
592 lines
22 KiB
JavaScript
/**
|
|
* weather.js - A module for weather-related functionality
|
|
* Provides real-time weather data and sunset/sunrise information
|
|
* Uses OpenWeatherMap API for weather data
|
|
*/
|
|
|
|
class WeatherManager {
|
|
constructor(options = {}) {
|
|
// Default options
|
|
this.options = {
|
|
latitude: 59.3293, // Stockholm latitude
|
|
longitude: 18.0686, // Stockholm longitude
|
|
apiKey: options.apiKey || '4d8fb5b93d4af21d66a2948710284366', // OpenWeatherMap API key
|
|
refreshInterval: 30 * 60 * 1000, // 30 minutes in milliseconds
|
|
...options
|
|
};
|
|
|
|
// State
|
|
this.weatherData = null;
|
|
this.forecastData = null;
|
|
this.sunTimes = null;
|
|
this.isDarkMode = false;
|
|
this.lastUpdated = null;
|
|
|
|
// Initialize
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Initialize the weather manager
|
|
*/
|
|
async init() {
|
|
try {
|
|
// Fetch weather data
|
|
await this.fetchWeatherData();
|
|
|
|
// Check if it's dark outside (only affects auto mode)
|
|
this.updateDarkModeBasedOnTime();
|
|
|
|
// Set up interval to check dark mode every minute (only affects auto mode)
|
|
this.darkModeCheckInterval = setInterval(() => {
|
|
// Only update dark mode based on time if ConfigManager has dark mode set to 'auto'
|
|
if (this.shouldUseAutoDarkMode()) {
|
|
this.updateDarkModeBasedOnTime();
|
|
}
|
|
}, 60000);
|
|
|
|
// Set up interval to refresh weather data
|
|
setInterval(() => this.fetchWeatherData(), this.options.refreshInterval);
|
|
|
|
// Dispatch initial dark mode state
|
|
this.dispatchDarkModeEvent();
|
|
|
|
console.log('WeatherManager initialized');
|
|
} catch (error) {
|
|
console.error('Error initializing WeatherManager:', error);
|
|
|
|
// Fallback to calculated sun times if API fails
|
|
await this.updateSunTimesFromCalculation();
|
|
this.updateDarkModeBasedOnTime();
|
|
this.dispatchDarkModeEvent();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if we should use automatic dark mode based on ConfigManager settings
|
|
*/
|
|
shouldUseAutoDarkMode() {
|
|
// If there's a ConfigManager instance with a config
|
|
if (window.configManager && window.configManager.config) {
|
|
// Only use auto dark mode if the setting is 'auto'
|
|
return window.configManager.config.darkMode === 'auto';
|
|
}
|
|
// Default to true if no ConfigManager is available
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Fetch weather data from OpenWeatherMap API
|
|
*/
|
|
async fetchWeatherData() {
|
|
try {
|
|
// Fetch current weather
|
|
const currentWeatherUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${this.options.latitude}&lon=${this.options.longitude}&units=metric&appid=${this.options.apiKey}`;
|
|
const currentWeatherResponse = await fetch(currentWeatherUrl);
|
|
const currentWeatherData = await currentWeatherResponse.json();
|
|
|
|
if (currentWeatherData.cod !== 200) {
|
|
throw new Error(`API Error: ${currentWeatherData.message}`);
|
|
}
|
|
|
|
// Fetch hourly forecast
|
|
const forecastUrl = `https://api.openweathermap.org/data/2.5/forecast?lat=${this.options.latitude}&lon=${this.options.longitude}&units=metric&appid=${this.options.apiKey}`;
|
|
const forecastResponse = await fetch(forecastUrl);
|
|
const forecastData = await forecastResponse.json();
|
|
|
|
if (forecastData.cod !== "200") {
|
|
throw new Error(`API Error: ${forecastData.message}`);
|
|
}
|
|
|
|
// Process and store the data
|
|
this.weatherData = this.processCurrentWeather(currentWeatherData);
|
|
this.forecastData = this.processForecast(forecastData);
|
|
this.lastUpdated = new Date();
|
|
|
|
// Extract sunrise and sunset times from the API response
|
|
this.updateSunTimesFromApi(currentWeatherData);
|
|
|
|
// Update the UI with the new data
|
|
this.updateWeatherUI();
|
|
|
|
console.log('Weather data updated:', this.weatherData);
|
|
return this.weatherData;
|
|
} catch (error) {
|
|
console.error('Error fetching weather data:', error);
|
|
|
|
// If we don't have any weather data yet, create some default data
|
|
if (!this.weatherData) {
|
|
this.weatherData = this.createDefaultWeatherData();
|
|
this.forecastData = this.createDefaultForecastData();
|
|
}
|
|
|
|
// Fallback to calculated sun times
|
|
await this.updateSunTimesFromCalculation();
|
|
|
|
return this.weatherData;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process current weather data from API response
|
|
*/
|
|
processCurrentWeather(data) {
|
|
const iconCode = data.weather[0].icon;
|
|
return {
|
|
temperature: Math.round(data.main.temp * 10) / 10, // Round to 1 decimal place
|
|
condition: data.weather[0].main,
|
|
description: data.weather[0].description,
|
|
icon: this.getWeatherIconUrl(iconCode),
|
|
iconCode: iconCode, // Store icon code for classification
|
|
wind: {
|
|
speed: Math.round(data.wind.speed * 3.6), // Convert m/s to km/h
|
|
direction: data.wind.deg
|
|
},
|
|
humidity: data.main.humidity,
|
|
pressure: data.main.pressure,
|
|
precipitation: data.rain ? (data.rain['1h'] || 0) : 0,
|
|
location: data.name,
|
|
country: data.sys.country,
|
|
timestamp: new Date(data.dt * 1000)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Process forecast data from API response
|
|
*/
|
|
processForecast(data) {
|
|
// Get the next 7 forecasts (covering about 24 hours)
|
|
return data.list.slice(0, 7).map(item => {
|
|
const iconCode = item.weather[0].icon;
|
|
return {
|
|
temperature: Math.round(item.main.temp * 10) / 10,
|
|
condition: item.weather[0].main,
|
|
description: item.weather[0].description,
|
|
icon: this.getWeatherIconUrl(iconCode),
|
|
iconCode: iconCode, // Store icon code for classification
|
|
timestamp: new Date(item.dt * 1000),
|
|
precipitation: item.rain ? (item.rain['3h'] || 0) : 0
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get weather icon URL from icon code
|
|
*/
|
|
getWeatherIconUrl(iconCode) {
|
|
return `https://openweathermap.org/img/wn/${iconCode}@2x.png`;
|
|
}
|
|
|
|
/**
|
|
* Determine if icon represents sun (even behind clouds)
|
|
*/
|
|
isSunIcon(iconCode, condition) {
|
|
// Icon codes: 01d, 01n = clear, 02d, 02n = few clouds, 03d, 03n = scattered, 04d, 04n = broken clouds
|
|
const sunIconCodes = ['01d', '01n', '02d', '02n', '03d', '03n', '04d', '04n'];
|
|
return sunIconCodes.includes(iconCode) ||
|
|
condition.includes('Clear') ||
|
|
condition.includes('Clouds');
|
|
}
|
|
|
|
/**
|
|
* Check if icon is clear sun (no clouds)
|
|
*/
|
|
isClearSun(iconCode, condition) {
|
|
const clearIconCodes = ['01d', '01n'];
|
|
return clearIconCodes.includes(iconCode) || condition === 'Clear';
|
|
}
|
|
|
|
/**
|
|
* Check if icon is sun behind clouds
|
|
*/
|
|
isSunBehindClouds(iconCode, condition) {
|
|
const cloudIconCodes = ['02d', '02n', '03d', '03n', '04d', '04n'];
|
|
return cloudIconCodes.includes(iconCode) || (condition.includes('Clouds') && !condition.includes('Clear'));
|
|
}
|
|
|
|
/**
|
|
* Determine if icon represents snow
|
|
*/
|
|
isSnowIcon(iconCode, condition) {
|
|
// Icon code: 13d, 13n = snow
|
|
const snowIconCodes = ['13d', '13n'];
|
|
return snowIconCodes.includes(iconCode) || condition.includes('Snow');
|
|
}
|
|
|
|
/**
|
|
* Create default weather data for fallback
|
|
*/
|
|
createDefaultWeatherData() {
|
|
return {
|
|
temperature: 7.1,
|
|
condition: 'Clear',
|
|
description: 'clear sky',
|
|
icon: 'https://openweathermap.org/img/wn/01d@2x.png',
|
|
iconCode: '01d',
|
|
wind: {
|
|
speed: 14.8,
|
|
direction: 270
|
|
},
|
|
humidity: 65,
|
|
pressure: 1012.0,
|
|
precipitation: 0.00,
|
|
location: 'Stockholm',
|
|
country: 'SE',
|
|
timestamp: new Date()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create default forecast data for fallback
|
|
*/
|
|
createDefaultForecastData() {
|
|
const now = new Date();
|
|
const forecasts = [];
|
|
|
|
// Create 7 forecast entries
|
|
for (let i = 0; i < 7; i++) {
|
|
const forecastTime = new Date(now);
|
|
forecastTime.setHours(now.getHours() + i);
|
|
|
|
forecasts.push({
|
|
temperature: 7.1 - (i * 0.3), // Decrease temperature slightly each hour
|
|
condition: i < 2 ? 'Clear' : 'Clouds',
|
|
description: i < 2 ? 'clear sky' : 'few clouds',
|
|
icon: i < 2 ? 'https://openweathermap.org/img/wn/01n@2x.png' : 'https://openweathermap.org/img/wn/02n@2x.png',
|
|
iconCode: i < 2 ? '01n' : '02n',
|
|
timestamp: forecastTime,
|
|
precipitation: 0
|
|
});
|
|
}
|
|
|
|
return forecasts;
|
|
}
|
|
|
|
/**
|
|
* Update the weather UI with current data
|
|
*/
|
|
updateWeatherUI() {
|
|
if (!this.weatherData || !this.forecastData) return;
|
|
|
|
try {
|
|
// Update current weather
|
|
const locationElement = document.querySelector('#custom-weather h3');
|
|
if (locationElement) {
|
|
locationElement.textContent = this.weatherData.location;
|
|
}
|
|
|
|
const conditionElement = document.querySelector('#custom-weather .weather-icon div');
|
|
if (conditionElement) {
|
|
conditionElement.textContent = this.weatherData.condition;
|
|
}
|
|
|
|
const iconElement = document.querySelector('#custom-weather .weather-icon img');
|
|
if (iconElement) {
|
|
iconElement.src = this.weatherData.icon;
|
|
iconElement.alt = this.weatherData.description;
|
|
// Add classes and data attributes for color filtering
|
|
iconElement.setAttribute('data-condition', this.weatherData.condition);
|
|
iconElement.classList.remove('weather-sun', 'weather-snow', 'weather-clear-sun', 'weather-clouds-sun');
|
|
if (this.isSnowIcon(this.weatherData.iconCode, this.weatherData.condition)) {
|
|
iconElement.classList.add('weather-snow');
|
|
} else if (this.isClearSun(this.weatherData.iconCode, this.weatherData.condition)) {
|
|
iconElement.classList.add('weather-sun', 'weather-clear-sun');
|
|
} else if (this.isSunBehindClouds(this.weatherData.iconCode, this.weatherData.condition)) {
|
|
iconElement.classList.add('weather-sun', 'weather-clouds-sun');
|
|
} else if (this.isSunIcon(this.weatherData.iconCode, this.weatherData.condition)) {
|
|
iconElement.classList.add('weather-sun');
|
|
}
|
|
}
|
|
|
|
const temperatureElement = document.querySelector('#custom-weather .temperature');
|
|
if (temperatureElement) {
|
|
temperatureElement.textContent = `${this.weatherData.temperature} °C`;
|
|
}
|
|
|
|
// Update forecast
|
|
const forecastContainer = document.querySelector('#custom-weather .forecast');
|
|
if (forecastContainer) {
|
|
// Clear existing forecast
|
|
forecastContainer.innerHTML = '';
|
|
|
|
// Add current weather as "Nu" (Swedish for "Now")
|
|
const nowElement = document.createElement('div');
|
|
nowElement.className = 'forecast-hour';
|
|
const nowIcon = document.createElement('img');
|
|
nowIcon.src = this.weatherData.icon;
|
|
nowIcon.alt = this.weatherData.description;
|
|
nowIcon.width = 56;
|
|
nowIcon.setAttribute('data-condition', this.weatherData.condition);
|
|
if (this.isSnowIcon(this.weatherData.iconCode, this.weatherData.condition)) {
|
|
nowIcon.classList.add('weather-snow');
|
|
} else if (this.isClearSun(this.weatherData.iconCode, this.weatherData.condition)) {
|
|
nowIcon.classList.add('weather-sun', 'weather-clear-sun');
|
|
} else if (this.isSunBehindClouds(this.weatherData.iconCode, this.weatherData.condition)) {
|
|
nowIcon.classList.add('weather-sun', 'weather-clouds-sun');
|
|
} else if (this.isSunIcon(this.weatherData.iconCode, this.weatherData.condition)) {
|
|
nowIcon.classList.add('weather-sun');
|
|
}
|
|
nowElement.innerHTML = `
|
|
<div class="time">Nu</div>
|
|
<div class="icon"></div>
|
|
<div class="temp">${this.weatherData.temperature} °C</div>
|
|
`;
|
|
nowElement.querySelector('.icon').appendChild(nowIcon);
|
|
forecastContainer.appendChild(nowElement);
|
|
|
|
// Add hourly forecasts
|
|
this.forecastData.forEach(forecast => {
|
|
const forecastTime = forecast.timestamp;
|
|
const timeString = forecastTime.toLocaleTimeString('sv-SE', { hour: '2-digit', minute: '2-digit' });
|
|
|
|
const forecastElement = document.createElement('div');
|
|
forecastElement.className = 'forecast-hour';
|
|
const forecastIcon = document.createElement('img');
|
|
forecastIcon.src = forecast.icon;
|
|
forecastIcon.alt = forecast.description;
|
|
forecastIcon.width = 56;
|
|
forecastIcon.setAttribute('data-condition', forecast.condition);
|
|
if (this.isSnowIcon(forecast.iconCode, forecast.condition)) {
|
|
forecastIcon.classList.add('weather-snow');
|
|
} else if (this.isClearSun(forecast.iconCode, forecast.condition)) {
|
|
forecastIcon.classList.add('weather-sun', 'weather-clear-sun');
|
|
} else if (this.isSunBehindClouds(forecast.iconCode, forecast.condition)) {
|
|
forecastIcon.classList.add('weather-sun', 'weather-clouds-sun');
|
|
} else if (this.isSunIcon(forecast.iconCode, forecast.condition)) {
|
|
forecastIcon.classList.add('weather-sun');
|
|
}
|
|
forecastElement.innerHTML = `
|
|
<div class="time">${timeString}</div>
|
|
<div class="icon"></div>
|
|
<div class="temp">${forecast.temperature} °C</div>
|
|
`;
|
|
forecastElement.querySelector('.icon').appendChild(forecastIcon);
|
|
forecastContainer.appendChild(forecastElement);
|
|
});
|
|
}
|
|
|
|
// Update sun times
|
|
const sunTimesElement = document.querySelector('#custom-weather .sun-times');
|
|
if (sunTimesElement && this.sunTimes) {
|
|
const sunriseTime = this.formatTime(this.sunTimes.today.sunrise);
|
|
const sunsetTime = this.formatTime(this.sunTimes.today.sunset);
|
|
sunTimesElement.textContent = `Sunrise: ${sunriseTime} | Sunset: ${sunsetTime}`;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating weather UI:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update sunrise and sunset times from API data
|
|
*/
|
|
updateSunTimesFromApi(data) {
|
|
if (!data || !data.sys || !data.sys.sunrise || !data.sys.sunset) {
|
|
console.warn('No sunrise/sunset data in API response, using calculated times');
|
|
this.updateSunTimesFromCalculation();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const today = new Date();
|
|
const tomorrow = new Date(today);
|
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
|
|
// Create Date objects from Unix timestamps
|
|
const sunrise = new Date(data.sys.sunrise * 1000);
|
|
const sunset = new Date(data.sys.sunset * 1000);
|
|
|
|
// Use calculated times for tomorrow
|
|
const tomorrowTimes = this.calculateSunTimes(tomorrow);
|
|
|
|
this.sunTimes = {
|
|
today: { sunrise, sunset },
|
|
tomorrow: tomorrowTimes
|
|
};
|
|
|
|
console.log('Sun times updated from API:', this.sunTimes);
|
|
return this.sunTimes;
|
|
} catch (error) {
|
|
console.error('Error updating sun times from API:', error);
|
|
this.updateSunTimesFromCalculation();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update sunrise and sunset times using calculation
|
|
*/
|
|
async updateSunTimesFromCalculation() {
|
|
|
|
try {
|
|
// Calculate sun times based on date and location
|
|
const today = new Date();
|
|
const tomorrow = new Date(today);
|
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
|
|
this.sunTimes = {
|
|
today: this.calculateSunTimes(today),
|
|
tomorrow: this.calculateSunTimes(tomorrow)
|
|
};
|
|
|
|
console.log('Sun times updated from calculation:', this.sunTimes);
|
|
return this.sunTimes;
|
|
} catch (error) {
|
|
console.error('Error updating sun times from calculation:', error);
|
|
// Fallback to default times if calculation fails
|
|
const defaultSunrise = new Date();
|
|
defaultSunrise.setHours(6, 45, 0, 0);
|
|
|
|
const defaultSunset = new Date();
|
|
defaultSunset.setHours(17, 32, 0, 0);
|
|
|
|
this.sunTimes = {
|
|
today: {
|
|
sunrise: defaultSunrise,
|
|
sunset: defaultSunset
|
|
},
|
|
tomorrow: {
|
|
sunrise: defaultSunrise,
|
|
sunset: defaultSunset
|
|
}
|
|
};
|
|
return this.sunTimes;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate sunrise and sunset times for a given date
|
|
* Uses a simplified algorithm
|
|
*/
|
|
calculateSunTimes(date) {
|
|
// This is a simplified calculation
|
|
// For more accuracy, you would use a proper astronomical calculation
|
|
|
|
// Get day of year
|
|
const start = new Date(date.getFullYear(), 0, 0);
|
|
const diff = date - start;
|
|
const oneDay = 1000 * 60 * 60 * 24;
|
|
const dayOfYear = Math.floor(diff / oneDay);
|
|
|
|
// Calculate sunrise and sunset times based on latitude and day of year
|
|
// This is a very simplified model
|
|
const latitude = this.options.latitude;
|
|
|
|
// Base sunrise and sunset times (in hours)
|
|
let baseSunrise = 6; // 6 AM
|
|
let baseSunset = 18; // 6 PM
|
|
|
|
// Adjust for latitude and season
|
|
// Northern hemisphere seasonal adjustment
|
|
const seasonalAdjustment = Math.sin((dayOfYear - 81) / 365 * 2 * Math.PI) * 3;
|
|
|
|
// Latitude adjustment (higher latitudes have more extreme day lengths)
|
|
const latitudeAdjustment = Math.abs(latitude) / 90 * 2;
|
|
|
|
// Apply adjustments
|
|
baseSunrise += seasonalAdjustment * latitudeAdjustment * -1;
|
|
baseSunset += seasonalAdjustment * latitudeAdjustment;
|
|
|
|
// Create Date objects
|
|
const sunrise = new Date(date);
|
|
sunrise.setHours(Math.floor(baseSunrise), Math.round((baseSunrise % 1) * 60), 0, 0);
|
|
|
|
const sunset = new Date(date);
|
|
sunset.setHours(Math.floor(baseSunset), Math.round((baseSunset % 1) * 60), 0, 0);
|
|
|
|
return { sunrise, sunset };
|
|
}
|
|
|
|
/**
|
|
* Check if it's currently dark outside based on sun times
|
|
*/
|
|
isDark() {
|
|
if (!this.sunTimes) return false;
|
|
|
|
const now = new Date();
|
|
const today = this.sunTimes.today;
|
|
|
|
// Check if current time is after today's sunset or before today's sunrise
|
|
return now > today.sunset || now < today.sunrise;
|
|
}
|
|
|
|
/**
|
|
* Update dark mode state based on current time
|
|
*/
|
|
updateDarkModeBasedOnTime() {
|
|
const wasDarkMode = this.isDarkMode;
|
|
this.isDarkMode = this.isDark();
|
|
|
|
// If dark mode state changed, dispatch event
|
|
if (wasDarkMode !== this.isDarkMode) {
|
|
this.dispatchDarkModeEvent();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set dark mode state manually
|
|
*/
|
|
setDarkMode(isDarkMode) {
|
|
if (this.isDarkMode !== isDarkMode) {
|
|
this.isDarkMode = isDarkMode;
|
|
this.dispatchDarkModeEvent();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle dark mode
|
|
*/
|
|
toggleDarkMode() {
|
|
this.isDarkMode = !this.isDarkMode;
|
|
this.dispatchDarkModeEvent();
|
|
return this.isDarkMode;
|
|
}
|
|
|
|
/**
|
|
* Dispatch dark mode change event
|
|
*/
|
|
dispatchDarkModeEvent() {
|
|
const event = new CustomEvent('darkModeChanged', {
|
|
detail: {
|
|
isDarkMode: this.isDarkMode,
|
|
automatic: true
|
|
}
|
|
});
|
|
document.dispatchEvent(event);
|
|
console.log('Dark mode ' + (this.isDarkMode ? 'enabled' : 'disabled'));
|
|
}
|
|
|
|
/**
|
|
* Get formatted sunrise time
|
|
*/
|
|
getSunriseTime() {
|
|
if (!this.sunTimes) return '06:45';
|
|
return this.formatTime(this.sunTimes.today.sunrise);
|
|
}
|
|
|
|
/**
|
|
* Get formatted sunset time
|
|
*/
|
|
getSunsetTime() {
|
|
if (!this.sunTimes) return '17:32';
|
|
return this.formatTime(this.sunTimes.today.sunset);
|
|
}
|
|
|
|
/**
|
|
* Format time as HH:MM
|
|
*/
|
|
formatTime(date) {
|
|
return date.toLocaleTimeString('sv-SE', { hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
|
|
/**
|
|
* Get the last updated time
|
|
*/
|
|
getLastUpdatedTime() {
|
|
if (!this.lastUpdated) return 'Never';
|
|
return this.formatTime(this.lastUpdated);
|
|
}
|
|
}
|
|
|
|
// Export the WeatherManager class for use in other modules
|
|
window.WeatherManager = WeatherManager;
|