/** * 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 = `