weather-bar-app
Version:
Weather Bar is a Weather Application that lives in your Menu Bar giving you beautiful access to real-time weather conditions and a generous 15-day forecast.
580 lines (519 loc) • 20.2 kB
JavaScript
import Feels from 'feels'
import moment from 'moment-timezone'
import { i18n as $t } from '../translations/i18n'
import { scaleQuantize } from 'd3-scale'
import { getTimes, getPosition, getMoonPosition, getMoonIllumination } from 'suncalc'
import * as tzlookup from 'tz-lookup'
const celsiusToFahrenheit = (temp) => {
return Math.round((temp * 1.8) + 32)
}
const celsiusToKelvin = (temp) => {
return Math.round(temp + 273.15)
}
const fahrenheitToCelsius = (temp) => {
return Math.round((temp - 32) / 1.8)
}
const fahrenheitToKelvin = (temp) => {
return Math.round(((temp - 32) / 1.8) + 273.15)
}
const kelvinToCelsius = (temp) => {
return Math.round(temp - 273.15)
}
const kelvinToFahrenheit = (temp) => {
return Math.round(((temp - 273.15) * 1.8) + 32)
}
const mmToInches = (volume) => {
return +(volume / 25.4).toFixed(2)
}
const mpsToMph = (speed) => {
return Math.round((speed * 3600 / 1610.3 * 1000) / 1000)
}
const titleCase = (str) => {
if (!str) {
return ''
}
return str.trim().replace(/-/g, ' ').replace(/\w\S*/g, function (txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() })
}
const prepMenubarWeather = (data, settings) => {
const measure = settings.units_temperature.charAt(0).toUpperCase()
const description = titleCase(data.weather[0].description)
// Convert Temperature to Feels Like
const feelsLikeKelvin = new Feels({
temp: data.main.temp,
humidity: data.main.humidity,
speed: data.wind.speed,
units: {
temp: 'k',
speed: 'mps'
}
}).like()
// Convert Temperature based on Preferences
const actual = (settings.units_temperature === 'fahrenheit')
? kelvinToFahrenheit(data.main.temp)
: kelvinToCelsius(data.main.temp)
const feelsLike = (settings.units_temperature === 'fahrenheit')
? kelvinToFahrenheit(feelsLikeKelvin)
: kelvinToCelsius(feelsLikeKelvin)
const temperature = (settings.layout_current_temp === 'actual') ? actual : feelsLike
let folder = (data.weather[0].icon.slice(-1) === 'd') ? 'day' : 'night'
let image = data.weather[0].id || 800
// Show Moon Phase if Night & Clear Sky
if (folder === 'night' && image === 800) {
folder = 'moon'
image = getMoonPhase()
}
return {
image: image,
title: temperature,
tooltip: `${temperature} °${measure} - ${description}`,
folder: folder,
setting: settings.app_launch_icon
}
}
const getMoonPhase = () => {
const moon = getMoonIllumination(new Date())
const scale = scaleQuantize()
.domain([0, 1])
.range([0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100])
let phase = scale(moon.phase)
if (phase === 100) {
phase = 0
}
return phase
}
const getMoonPhaseScene = (latitude, longitude) => {
const illumination = getMoonIllumination(new Date())
const position = getMoonPosition(new Date())
const scale = scaleQuantize()
.domain([0, 1])
.range([
'moon-new-moon',
'moon-waxing-crescent',
'moon-first-quarter',
'moon-waxing-gibbous',
'moon-full-moon',
'moon-waning-gibbous',
'moon-last-quarter',
'moon-waning-crescent'
])
return {
name: scale(illumination.phase),
fraction: illumination.fraction,
phase: illumination.phase,
angle: illumination.angle,
altitude: (isNaN(position.altitude)) ? 0 : position.altitude,
azimuth: (isNaN(position.azimuth)) ? 0 : position.azimuth,
distance: (isNaN(position.distance)) ? 0 : position.distance,
parallacticAngle: (isNaN(position.parallacticAngle)) ? 0 : position.parallacticAngle
}
}
const getMoonPhaseIcon = () => {
const moon = getMoonIllumination(new Date())
const scale = scaleQuantize()
.domain([0, 1])
.range([
'wi-moon-new',
'wi-moon-waxing-crescent-1',
'wi-moon-waxing-crescent-2',
'wi-moon-waxing-crescent-4',
'wi-moon-waxing-crescent-6',
'wi-moon-first-quarter',
'wi-moon-waxing-gibbous-1',
'wi-moon-waxing-gibbous-2',
'wi-moon-waxing-gibbous-4',
'wi-moon-waxing-gibbous-6',
'wi-moon-full',
'wi-moon-waning-gibbous-1',
'wi-moon-waning-gibbous-2',
'wi-moon-waning-gibbous-4',
'wi-moon-waning-gibbous-6',
'wi-moon-third-quarter',
'wi-moon-waning-crescent-1',
'wi-moon-waning-crescent-2',
'wi-moon-waning-crescent-4',
'wi-moon-waning-crescent-6',
'wi-moon-new'
])
return scale(moon.phase)
}
const degreesToDirection = (degrees) => {
if (degrees < 0 || degrees > 360) {
degrees = 0
}
if (degrees >= 0 && degrees <= 11.25) {
return 'NORTH'
} else if (degrees > 348.75 && degrees <= 360) {
return 'NORTH'
} else if (degrees > 11.25 && degrees <= 33.75) {
return 'NNE'
} else if (degrees > 33.75 && degrees <= 56.25) {
return 'NE'
} else if (degrees > 56.25 && degrees <= 78.75) {
return 'ENE'
} else if (degrees > 78.75 && degrees <= 101.25) {
return 'EAST'
} else if (degrees > 101.25 && degrees <= 123.75) {
return 'ESE'
} else if (degrees > 123.75 && degrees <= 146.25) {
return 'SE'
} else if (degrees > 146.25 && degrees <= 168.75) {
return 'SSE'
} else if (degrees > 168.75 && degrees <= 191.25) {
return 'SOUTH'
} else if (degrees > 191.25 && degrees <= 213.75) {
return 'SSW'
} else if (degrees > 213.75 && degrees <= 236.25) {
return 'SW'
} else if (degrees > 236.25 && degrees <= 258.75) {
return 'WSW'
} else if (degrees > 258.75 && degrees <= 281.25) {
return 'WEST'
} else if (degrees > 281.25 && degrees <= 303.75) {
return 'WNW'
} else if (degrees > 303.75 && degrees <= 326.25) {
return 'NW'
} else if (degrees > 326.25 && degrees <= 348.75) {
return 'NNW'
}
}
const getWeatherIcon = (code, time) => {
const icons = {
day: {
code_200: 'wi-day-storm-showers',
code_201: 'wi-day-thunderstorm',
code_202: 'wi-day-thunderstorm',
code_210: 'wi-day-storm-showers',
code_211: 'wi-day-storm-showers',
code_212: 'wi-day-thunderstorm',
code_221: 'wi-day-storm-showers',
code_230: 'wi-day-storm-showers',
code_231: 'wi-day-storm-showers',
code_232: 'wi-day-storm-showers',
code_300: 'wi-day-showers',
code_301: 'wi-day-showers',
code_302: 'wi-day-showers',
code_310: 'wi-day-showers',
code_311: 'wi-day-showers',
code_312: 'wi-day-rain-wind',
code_313: 'wi-day-rain-wind',
code_314: 'wi-day-rain-wind',
code_321: 'wi-day-rain-wind',
code_500: 'wi-day-sprinkle',
code_501: 'wi-day-sprinkle',
code_502: 'wi-day-rain',
code_503: 'wi-rain',
code_504: 'wi-rain',
code_511: 'wi-day-rain-mix',
code_520: 'wi-day-rain-wind',
code_521: 'wi-day-rain',
code_522: 'wi-rain',
code_531: 'wi-day-sprinkle',
code_600: 'wi-day-snow',
code_601: 'wi-day-snow',
code_602: 'wi-day-snow-wind',
code_611: 'wi-day-rain-mix',
code_612: 'wi-day-hail',
code_615: 'wi-day-rain-mix',
code_616: 'wi-day-rain-mix',
code_620: 'wi-day-snow-thunderstorm',
code_621: 'wi-day-sleet-storm',
code_622: 'wi-snow-wind',
code_701: 'wi-cloudy-windy',
code_711: 'wi-smoke',
code_721: 'wi-day-haze',
code_731: 'wi-sandstorm',
code_741: 'wi-day-fog',
code_751: 'wi-dust',
code_761: 'wi-dust',
code_762: 'wi-volcano',
code_771: 'wi-strong-wind',
code_781: 'wi-tornado',
code_800: 'wi-day-sunny',
code_801: 'wi-day-sunny-overcast',
code_802: 'wi-day-cloudy',
code_803: 'wi-day-cloudy',
code_804: 'wi-cloudy',
code_900: 'wi-tornado',
code_901: 'wi-lightning',
code_902: 'wi-hurricane',
code_903: 'wi-snowflake-cold',
code_904: 'wi-hot',
code_905: 'wi-windy',
code_906: 'wi-hail',
code_951: 'wi-day-sunny',
code_952: 'wi-day-light-wind',
code_953: 'wi-day-light-wind',
code_954: 'wi-day-cloudy-gusts',
code_955: 'wi-day-cloudy-gusts',
code_956: 'wi-day-cloudy-gusts',
code_957: 'wi-windy',
code_958: 'wi-windy',
code_959: 'wi-windy',
code_960: 'wi-lightning',
code_961: 'wi-lightning',
code_962: 'wi-hurricane'
},
night: {
code_200: 'wi-night-alt-storm-showers',
code_201: 'wi-night-alt-thunderstorm',
code_202: 'wi-night-alt-thunderstorm',
code_210: 'wi-night-alt-storm-showers',
code_211: 'wi-night-alt-storm-showers',
code_212: 'wi-night-alt-thunderstorm',
code_221: 'wi-night-alt-storm-showers',
code_230: 'wi-night-alt-storm-showers',
code_231: 'wi-night-alt-storm-showers',
code_232: 'wi-night-alt-storm-showers',
code_300: 'wi-night-alt-showers',
code_301: 'wi-night-alt-showers',
code_302: 'wi-night-alt-showers',
code_310: 'wi-night-alt-showers',
code_311: 'wi-night-alt-showers',
code_312: 'wi-night-alt-rain-wind',
code_313: 'wi-night-alt-rain-wind',
code_314: 'wi-night-alt-rain-wind',
code_321: 'wi-night-alt-rain-wind',
code_500: 'wi-night-alt-sprinkle',
code_501: 'wi-night-alt-sprinkle',
code_502: 'wi-night-alt-rain',
code_503: 'wi-rain',
code_504: 'wi-rain',
code_511: 'wi-night-alt-rain-mix',
code_520: 'wi-night-alt-rain-wind',
code_521: 'wi-night-alt-rain',
code_522: 'wi-rain',
code_531: 'wi-night-alt-sprinkle',
code_600: 'wi-night-alt-snow',
code_601: 'wi-night-alt-snow',
code_602: 'wi-night-alt-snow-wind',
code_611: 'wi-night-alt-rain-mix',
code_612: 'wi-night-alt-hail',
code_615: 'wi-night-alt-rain-mix',
code_616: 'wi-night-alt-rain-mix',
code_620: 'wi-night-alt-snow-thunderstorm',
code_621: 'wi-night-alt-sleet-storm',
code_622: 'wi-snow-wind',
code_701: 'wi-cloudy-windy',
code_711: 'wi-smoke',
code_721: 'wi-fog',
code_731: 'wi-sandstorm',
code_741: 'wi-fog',
code_751: 'wi-dust',
code_761: 'wi-dust',
code_762: 'wi-volcano',
code_771: 'wi-strong-wind',
code_781: 'wi-tornado',
code_800: 'wi-night-clear',
code_801: 'wi-night-alt-partly-cloudy',
code_802: 'wi-night-alt-cloudy',
code_803: 'wi-night-alt-cloudy',
code_804: 'wi-cloudy',
code_900: 'wi-tornado',
code_901: 'wi-lightning',
code_902: 'wi-hurricane',
code_903: 'wi-snowflake-cold',
code_904: 'wi-hot',
code_905: 'wi-windy',
code_906: 'wi-hail',
code_951: 'wi-night-clear',
code_952: 'wi-night-alt-cloudy-windy',
code_953: 'wi-night-alt-cloudy-windy',
code_954: 'wi-night-alt-cloudy-gusts',
code_955: 'wi-night-alt-cloudy-gusts',
code_956: 'wi-night-alt-cloudy-gusts',
code_957: 'wi-windy',
code_958: 'wi-windy',
code_959: 'wi-windy',
code_960: 'wi-lightning',
code_961: 'wi-lightning',
code_962: 'wi-hurricane'
}
}
if (typeof icons[time] !== 'undefined' && typeof icons[time][`code_${code}`] !== 'undefined') {
return icons[time][`code_${code}`]
}
}
const getRainPercent = (code) => {
if (code === 202 || code === 212 || code === 502 || code === 503 || code === 504 || code === 522 || code === 202 || code === 202) {
return 100
} else if (code >= 200 && code <= 232) {
return 50
} else if (code >= 500 && code <= 531) {
return 10
} else if (code >= 300 && code <= 321) {
return 5
}
return 0
}
const getCloudPercent = (code) => {
if (code === 202 || code === 212 || code === 502 || code === 503 || code === 504 || code === 522 || code === 202 || code === 202) {
return 100
} else if (code >= 200 && code <= 232) {
return 80
} else if (code >= 500 && code <= 531) {
return 60
} else if (code >= 300 && code <= 321) {
return 40
} else if (code >= 801 && code <= 804) {
return 20
}
return 0
}
const getSceneTime = (latitude, longitude, timeZone) => {
const now = parseInt(moment.tz(timeZone).format('x'))
const sunTimes = getTimes(moment.tz(timeZone), latitude, longitude)
// Match up the calculated sun times for this timezone against CSS class names we have designs for
const dawn = parseInt(moment.tz(sunTimes.dawn, timeZone).format('x'))
const earlyMorning = parseInt(moment.tz(sunTimes.sunrise, timeZone).format('x'))
const morning = parseInt(moment.tz(sunTimes.sunriseEnd, timeZone).format('x'))
const midMorning = parseInt(moment.tz(sunTimes.goldenHourEnd, timeZone).format('x'))
const noon = parseInt(moment.tz(sunTimes.solarNoon, timeZone).format('x'))
const afternoon = parseInt(moment.tz(sunTimes.goldenHour, timeZone).format('x'))
const evening = parseInt(moment.tz(sunTimes.sunsetStart, timeZone).format('x'))
const dusk = parseInt(moment.tz(sunTimes.sunset, timeZone).format('x'))
const night = parseInt(moment.tz(sunTimes.dusk, timeZone).format('x'))
const midnight = parseInt(moment.tz(sunTimes.nightEnd, timeZone).format('x'))
if (now >= dawn && now < earlyMorning) {
return 'dawn'
} else if (now >= earlyMorning && now < morning) {
return 'early-morning'
} else if (now >= morning && now < midMorning) {
return 'morning'
} else if (now >= midMorning && now < noon) {
return 'mid-morning'
} else if (now >= noon && now < afternoon) {
return 'noon'
} else if (now >= afternoon && now < evening) {
return 'afternoon'
} else if (now >= evening && now < dusk) {
return 'evening'
} else if (now >= dusk && now < night) {
return 'dusk'
} else if (now >= night && now < midnight) {
return 'night'
} else {
return 'midnight'
}
}
const getSunsPosition = (latitude, longitude, timeZone) => {
const position = getPosition(moment.tz(timeZone), latitude, longitude)
return (position.altitude / (Math.PI / 2))
}
const getMoonsPosition = (latitude, longitude, timeZone) => {
const position = getMoonPosition(moment.tz(timeZone), latitude, longitude)
return (position.altitude / (Math.PI / 2))
}
const parseWeather = (key, data, settings) => {
moment.locale(settings.app_language)
const timeZone = tzlookup(data.coord.lat, data.coord.lon)
const time = (data.weather[0].icon.slice(-1) === 'd') ? 'day' : 'night'
const code = data.weather[0].id
const moon = getMoonPhaseScene(data.coord.lat, data.coord.lon)
// Convert Temperature to Feels Like
const feelsLikeKelvin = new Feels({
temp: data.main.temp,
humidity: data.main.humidity,
speed: data.wind.speed,
units: {
temp: 'k',
speed: 'mps'
}
}).like()
const sunrise = parseInt(moment.tz(data.sys.sunrise * 1000, timeZone).format('x'))
const sunset = parseInt(moment.tz(data.sys.sunset * 1000, timeZone).format('x'))
const now = parseInt(moment.tz(timeZone).format('x'))
const sunNext = (now > sunrise && now <= sunset) ? 'sunset' : 'sunrise'
const sunPosition = getSunsPosition(data.coord.lat, data.coord.lon, timeZone)
const moonPosition = getMoonsPosition(data.coord.lat, data.coord.lon, timeZone)
const thunderstorm = (code >= 200 && code <= 232)
const rainPrecipitation = (data.rain && data.rain['3h']) ? data.rain['3h'] : 0
const snowPrecipitation = (data.snow && data.snow['3h']) ? data.snow['3h'] : 0
const precipitation = mmToInches(rainPrecipitation + snowPrecipitation) + ' IN'
let weather = {
city: data.name,
condition_icon: (time === 'night' && (code === 800 || code === 951)) ? getMoonPhaseIcon() : getWeatherIcon(code, time),
condition_label: $t(settings.app_language, `weather.code_${code}`),
id: data.id,
hash_key: key,
moon_angle: moon.angle,
moon_fraction: moon.fraction,
moon_name: moon.name,
moon_phase: (moon.phase === 1) ? 0 : moon.phase,
moon_position: moonPosition,
precipitation: precipitation,
scene_clouds: (getCloudPercent(code) > 0),
scene_cloud_percent: getCloudPercent(code),
scene_fog: (code === 741),
scene_lightning: (code >= 200 && code <= 232),
scene_moon: (sunPosition <= 0 && moonPosition >= -0.05),
scene_rain: getRainPercent(code),
scene_snow: (code >= 600 && code <= 622),
scene_stars: (sunPosition <= -0.05 && !thunderstorm),
scene_sun: (sunPosition >= -0.05),
scene_thunderstorm: thunderstorm,
scene_time: getSceneTime(data.coord.lat, data.coord.lon, timeZone),
scene_wind_direction: data.wind.deg,
scene_wind_speed: mpsToMph(data.wind.speed),
sun_next: sunNext,
sun_position: sunPosition,
sunrise: moment.tz(data.sys.sunrise * 1000, timeZone).format('h:mm A'),
sunset: moment.tz(data.sys.sunset * 1000, timeZone).format('h:mm A'),
temp_actual: (settings.units_temperature === 'fahrenheit') ? kelvinToFahrenheit(data.main.temp) : kelvinToCelsius(data.main.temp),
temp_feels_like: (settings.units_temperature === 'fahrenheit') ? kelvinToFahrenheit(feelsLikeKelvin) : kelvinToCelsius(feelsLikeKelvin),
temp_max: (settings.units_temperature === 'fahrenheit') ? kelvinToFahrenheit(data.main.temp_max) : kelvinToCelsius(data.main.temp_max),
temp_min: (settings.units_temperature === 'fahrenheit') ? kelvinToFahrenheit(data.main.temp_min) : kelvinToCelsius(data.main.temp_min),
time_zone: timeZone,
wind_direction: degreesToDirection(data.wind.deg),
wind_speed: (settings.units_wind_speed === 'mph') ? mpsToMph(data.wind.speed) + ' MPH' : Math.round(data.wind.speed) + ' MPS'
}
return weather
}
const parseWeatherForecast = (key, data, settings, todayLabelTranslated) => {
if (!data || !data.list || data.list.length === 0) {
return []
}
moment.locale(settings.app_language)
const timeZone = tzlookup(data.city.coord.lat, data.city.coord.lon)
const forecast = []
const todayLabel = moment.tz(timeZone).format('ddd')
const todayNumber = moment.tz(timeZone).format('D')
const firstDayLabel = moment.tz(data.list[0].dt * 1000, timeZone).format('ddd')
const firstDayNumber = moment.tz(data.list[0].dt * 1000, timeZone).format('D')
// Since the data is cached for an hour, it's possible that the first day might be yesterday
const firstDayIsToday = (firstDayLabel === todayLabel && firstDayNumber === todayNumber)
for (let i = 0; i < data.list.length; i++) {
const code = data.list[i].weather[0].id
const time = (data.list[i].weather[0].icon.slice(-1) === 'd') ? 'day' : 'night'
if ((i === 0 && !firstDayIsToday) || (i === (data.list.length - 1) && firstDayIsToday)) {
continue
}
forecast.push({
day_label: (i === 0 || (!firstDayIsToday && i === 1)) ? todayLabelTranslated : moment.tz(data.list[i].dt * 1000, timeZone).format('ddd'),
day_number: (i === 0 || (!firstDayIsToday && i === 1)) ? null : moment.tz(data.list[i].dt * 1000, timeZone).format('D'),
condition_icon: (time === 'night' && (code === 800 || code === 951)) ? getMoonPhaseIcon() : getWeatherIcon(code, time),
temp_max: (settings.units_temperature === 'fahrenheit') ? kelvinToFahrenheit(data.list[i].temp.max) : kelvinToCelsius(data.list[i].temp.max),
temp_min: (settings.units_temperature === 'fahrenheit') ? kelvinToFahrenheit(data.list[i].temp.min) : kelvinToCelsius(data.list[i].temp.min)
})
}
return {
hash_key: key,
forecast: forecast
}
}
export default {
celsiusToFahrenheit,
celsiusToKelvin,
degreesToDirection,
fahrenheitToCelsius,
fahrenheitToKelvin,
getMoonPhase,
kelvinToCelsius,
kelvinToFahrenheit,
parseWeather,
parseWeatherForecast,
prepMenubarWeather,
titleCase,
getWeatherIcon,
mpsToMph,
getSceneTime
}