import Foundation import CoreLocation import SwiftSignalKit import TelegramCore import StickerPickerScreen import AccountContext import DeviceLocationManager import DeviceAccess struct StoryWeather { let emoji: String let temperature: Double } private func getWeatherData(context: AccountContext, location: CLLocationCoordinate2D) -> Signal { let appConfiguration = context.currentAppConfiguration.with { $0 } let botConfiguration = WeatherBotConfiguration.with(appConfiguration: appConfiguration) if let botUsername = botConfiguration.botName { return context.engine.peers.resolvePeerByName(name: botUsername, referrer: nil) |> mapToSignal { result -> Signal in guard case let .result(result) = result else { return .complete() } return .single(result) } |> mapToSignal { peer -> Signal in guard let peer = peer else { return .single(nil) } return context.engine.messages.requestChatContextResults(botId: peer.id, peerId: context.account.peerId, query: "", location: .single((location.latitude, location.longitude)), offset: "") |> map { results -> ChatContextResultCollection? in return results?.results } |> `catch` { error -> Signal in return .single(nil) } } |> map { contextResult -> StoryWeather? in guard let contextResult, let result = contextResult.results.first, let emoji = result.title, let temperature = result.description.flatMap(Double.init) else { return nil } return StoryWeather(emoji: emoji, temperature: temperature) } } else { return .single(nil) } } func getWeather(context: AccountContext, load: Bool) -> Signal { guard let locationManager = context.sharedContext.locationManager else { return .single(.none) } return DeviceAccess.authorizationStatus(subject: .location(.send)) |> mapToSignal { status in switch status { case .notDetermined: return .single(.notDetermined) case .denied, .restricted, .unreachable: return .single(.notAllowed) case .allowed: if load { return .single(.fetching) |> then( currentLocationManagerCoordinate(manager: locationManager, timeout: 5.0) |> mapToSignal { location in if let location { return getWeatherData(context: context, location: location) |> mapToSignal { weather in if let weather { let effectiveEmoji = emojiFor(for: weather.emoji.strippedEmoji, date: Date(), location: location) if let match = context.animatedEmojiStickersValue[effectiveEmoji]?.first { return .single(.loaded(StickerPickerScreen.Weather.LoadedWeather( emoji: effectiveEmoji, emojiFile: match.file._parse(), temperature: weather.temperature ))) } else { return .single(.none) } } else { return .single(.none) } } } else { return .single(.none) } } ) } else { return .single(.notPreloaded) } } } } private struct WeatherBotConfiguration { static var defaultValue: WeatherBotConfiguration { return WeatherBotConfiguration(botName: "izweatherbot") } let botName: String? fileprivate init(botName: String?) { self.botName = botName } public static func with(appConfiguration: AppConfiguration) -> WeatherBotConfiguration { if let data = appConfiguration.data, let botName = data["weather_search_username"] as? String { return WeatherBotConfiguration(botName: botName) } else { return .defaultValue } } } private let J1970: Double = 2440588.0 private let moonEmojis = ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘", "🌑"] private func emojiFor(for emoji: String, date: Date, location: CLLocationCoordinate2D) -> String { var emoji = emoji if !"".isEmpty, ["☀️", "🌤️"].contains(emoji) && !isDay(latitude: location.latitude, longitude: location.longitude, dateTime: date) { emoji = moonPhaseEmoji(for: date) } return emoji } private func moonPhaseEmoji(for date: Date) -> String { let julianDate = toJulianDate(date: date) let referenceNewMoon: Double = 2451550.1 let synodicMonth: Double = 29.53058867 let daysSinceNewMoon = julianDate - referenceNewMoon let newMoons = daysSinceNewMoon / synodicMonth let currentMoonPhase = (newMoons - floor(newMoons)) * synodicMonth switch currentMoonPhase { case 0..<1.84566: return moonEmojis[0] case 1.84566..<5.53699: return moonEmojis[1] case 5.53699..<9.22831: return moonEmojis[2] case 9.22831..<12.91963: return moonEmojis[3] case 12.91963..<16.61096: return moonEmojis[4] case 16.61096..<20.30228: return moonEmojis[5] case 20.30228..<23.99361: return moonEmojis[6] case 23.99361..<27.68493: return moonEmojis[7] default: return moonEmojis[8] } } private func isDay(latitude: Double, longitude: Double, dateTime: Date) -> Bool { let calendar = Calendar.current let date = calendar.startOfDay(for: dateTime) let time = dateTime.timeIntervalSince(date) let sunrise = calculateSunrise(latitude: latitude, longitude: longitude, date: date) let sunset = calculateSunset(latitude: latitude, longitude: longitude, date: date) return time >= sunrise * 3600 && time <= sunset * 3600 } private func calculateSunrise(latitude: Double, longitude: Double, date: Date) -> Double { return calculateSunTime(latitude: latitude, longitude: longitude, date: date, isSunrise: true) } private func calculateSunset(latitude: Double, longitude: Double, date: Date) -> Double { return calculateSunTime(latitude: latitude, longitude: longitude, date: date, isSunrise: false) } private func calculateSunTime(latitude: Double, longitude: Double, date: Date, isSunrise: Bool) -> Double { let calendar = Calendar.current let dayOfYear = calendar.ordinality(of: .day, in: .year, for: date)! let zenith = 90.833 let D2R = Double.pi / 180.0 let R2D = 180.0 / Double.pi let lngHour = longitude / 15.0 let t = Double(dayOfYear) + ((isSunrise ? 6.0 : 18.0) - lngHour) / 24.0 let M = (0.9856 * t) - 3.289 var L = M + (1.916 * sin(M * D2R)) + (0.020 * sin(2 * M * D2R)) + 282.634 if L > 360.0 { L -= 360.0 } else if L < 0.0 { L += 360.0 } var RA = R2D * atan(0.91764 * tan(L * D2R)) if RA > 360.0 { RA -= 360.0 } else if RA < 0.0 { RA += 360.0 } let Lquadrant = (floor(L / 90.0)) * 90.0 let RAquadrant = (floor(RA / 90.0)) * 90.0 RA += (Lquadrant - RAquadrant) RA /= 15.0 let sinDec = 0.39782 * sin(L * D2R) let cosDec = cos(asin(sinDec)) let cosH = (cos(zenith * D2R) - (sinDec * sin(latitude * D2R))) / (cosDec * cos(latitude * D2R)) if cosH > 1.0 || cosH < -1.0 { return -1 } var H = isSunrise ? (360.0 - R2D * acos(cosH)) : R2D * acos(cosH) H /= 15.0 let T = H + RA - (0.06571 * t) - 6.622 var UT = T - lngHour if UT > 24.0 { UT -= 24.0 } else if UT < 0.0 { UT += 24.0 } return UT } private func toJulianDate(date: Date) -> Double { return date.timeIntervalSince1970 / 86400.0 + J1970 - 0.5 }