import Foundation import OpenMeteoSdk import Vapor public struct ForecastapiController: RouteCollection { /// Dedicated thread pool for API calls reading data from disk. Prevents blocking of the main thread pools. static var runLoop = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) /// Single thread static var isolationLoop = MultiThreadedEventLoopGroup(numberOfThreads: 1) public func boot(routes: RoutesBuilder) throws { let categoriesRoute = routes.grouped("v1") let era5 = WeatherApiController( forecastDay: 1, forecastDaysMax: 1, historyStartDate: Timestamp(1940, 1, 1), has15minutely: false, hasCurrentWeather: false, defaultModel: .archive_best_match, subdomain: "archive-api", alias: ["satellite-api"] ) categoriesRoute.getAndPost("era5", use: era5.query) categoriesRoute.getAndPost("archive", use: era5.query) categoriesRoute.getAndPost("forecast", use: WeatherApiController( historyStartDate: Timestamp(2016, 1, 1), defaultModel: .best_match, alias: ["historical-forecast-api", "previous-runs-api"]).query ) categoriesRoute.getAndPost("dwd-icon", use: WeatherApiController( defaultModel: .icon_seamless).query ) categoriesRoute.getAndPost("gfs", use: WeatherApiController( has15minutely: true, defaultModel: .gfs_seamless).query ) categoriesRoute.getAndPost("meteofrance", use: WeatherApiController( forecastDay: 4, has15minutely: true, defaultModel: .meteofrance_seamless).query ) categoriesRoute.getAndPost("jma", use: WeatherApiController( has15minutely: false, defaultModel: .jma_seamless).query ) categoriesRoute.getAndPost("metno", use: WeatherApiController( forecastDay: 3, has15minutely: false, defaultModel: .metno_nordic).query ) categoriesRoute.getAndPost("gem", use: WeatherApiController( has15minutely: false, defaultModel: .gem_seamless).query ) categoriesRoute.getAndPost("ecmwf", use: WeatherApiController( forecastDay: 10, has15minutely: false, hasCurrentWeather: false, defaultModel: .ecmwf_ifs025).query ) categoriesRoute.getAndPost("cma", use: WeatherApiController( has15minutely: false, defaultModel: .cma_grapes_global).query ) categoriesRoute.getAndPost("bom", use: WeatherApiController( has15minutely: false, defaultModel: .bom_access_global).query ) categoriesRoute.getAndPost("arpae", use: WeatherApiController( has15minutely: false, defaultModel: .arpae_cosmo_seamless).query ) categoriesRoute.getAndPost("elevation", use: DemController().query) categoriesRoute.getAndPost("air-quality", use: CamsController().query) categoriesRoute.getAndPost("seasonal", use: SeasonalForecastController().query) categoriesRoute.getAndPost("flood", use: GloFasController().query) categoriesRoute.getAndPost("climate", use: CmipController().query) categoriesRoute.getAndPost("marine", use: IconWaveController().query) categoriesRoute.getAndPost("ensemble", use: EnsembleApiController().query) } } struct WeatherApiController { let forecastDay: Int let forecastDaysMax: Int let historyStartDate: Timestamp let has15minutely: Bool let hasCurrentWeather: Bool let defaultModel: MultiDomains let subdomain: String let alias: [String] init(forecastDay: Int = 7, forecastDaysMax: Int = 16, historyStartDate: Timestamp = Timestamp(2020, 1, 1), has15minutely: Bool = true, hasCurrentWeather: Bool = true, defaultModel: MultiDomains, subdomain: String = "api", alias: [String] = []) { self.forecastDay = forecastDay self.forecastDaysMax = forecastDaysMax self.historyStartDate = historyStartDate self.has15minutely = has15minutely self.hasCurrentWeather = hasCurrentWeather self.defaultModel = defaultModel self.subdomain = subdomain self.alias = alias } func query(_ req: Request) async throws -> Response { let host = try await req.ensureSubdomain(subdomain, alias: alias) /// True if running on `historical-forecast-api.open-meteo.com` -> Limit to current day, disable forecast let isHistoricalForecastApi = host?.starts(with: "historical-forecast-api") == true || host?.starts(with: "customer-historical-api") == true let forecastDaysMax = isHistoricalForecastApi ? 1 : self.forecastDaysMax let forecastDayDefault = isHistoricalForecastApi ? 1 : self.forecastDay let params = req.method == .POST ? try req.content.decode(ApiQueryParameter.self) : try req.query.decode(ApiQueryParameter.self) let numberOfLocationsMaximum = try await req.ensureApiKey(subdomain, alias: alias, apikey: params.apikey) let currentTime = Timestamp.now() let allowedRange = historyStartDate ..< currentTime.with(hour: 0).add(days: forecastDaysMax) let domains = try MultiDomains.load(commaSeparatedOptional: params.models)?.map({ $0 == .best_match ? defaultModel : $0 }) ?? [defaultModel] let paramsMinutely = has15minutely ? try ForecastVariable.load(commaSeparatedOptional: params.minutely_15) : nil let defaultCurrentWeather = [ForecastVariable.surface(.init(.temperature, 0)), .surface(.init(.windspeed, 0)), .surface(.init(.winddirection, 0)), .surface(.init(.is_day, 0)), .surface(.init(.weathercode, 0))] let paramsCurrent: [ForecastVariable]? = !hasCurrentWeather ? nil : params.current_weather == true ? defaultCurrentWeather : try ForecastVariable.load(commaSeparatedOptional: params.current) let paramsHourly = try ForecastVariable.load(commaSeparatedOptional: params.hourly) let paramsDaily = try ForecastVariableDaily.load(commaSeparatedOptional: params.daily) let nParamsHourly = paramsHourly?.count ?? 0 let nParamsMinutely = paramsMinutely?.count ?? 0 let nParamsCurrent = paramsCurrent?.count ?? 0 let nParamsDaily = paramsDaily?.count ?? 0 let nVariables = (nParamsHourly + nParamsMinutely + nParamsCurrent + nParamsDaily) * domains.count /// Prepare readers based on geometry /// Readers are returned as a callback to release memory after data has been retrieved let prepared = try GenericReaderMulti.prepareReaders(domains: domains, params: params, currentTime: currentTime, forecastDayDefault: forecastDayDefault, forecastDaysMax: forecastDaysMax, pastDaysMax: 92, allowedRange: allowedRange) let locations: [ForecastapiResult.PerLocation] = try prepared.map { prepared in let timezone = prepared.timezone let time = prepared.time let timeLocal = TimerangeLocal(range: time.dailyRead.range, utcOffsetSeconds: timezone.utcOffsetSeconds) let currentTimeRange = TimerangeDt(start: currentTime.floor(toNearest: 3600/4), nTime: 1, dtSeconds: 3600/4) let readers: [ForecastapiResult.PerModel] = try prepared.perModel.compactMap { readerAndDomain in guard let reader = try readerAndDomain.reader() else { return nil } let hourlyDt = (params.temporal_resolution ?? .hourly).dtSeconds ?? reader.modelDtSeconds let timeHourlyRead = time.hourlyRead.with(dtSeconds: hourlyDt) let timeHourlyDisplay = time.hourlyDisplay.with(dtSeconds: hourlyDt) let domain = readerAndDomain.domain return .init( model: domain, latitude: reader.modelLat, longitude: reader.modelLon, elevation: reader.targetElevation, prefetch: { if let paramsCurrent { for variable in paramsCurrent { let (v, previousDay) = variable.variableAndPreviousDay try reader.prefetchData(variable: v, time: currentTimeRange.toSettings(previousDay: previousDay)) } } if let paramsMinutely { for variable in paramsMinutely { let (v, previousDay) = variable.variableAndPreviousDay try reader.prefetchData(variable: v, time: time.minutely15.toSettings(previousDay: previousDay)) } } if let paramsHourly { for variable in paramsHourly { let (v, previousDay) = variable.variableAndPreviousDay try reader.prefetchData(variable: v, time: timeHourlyRead.toSettings(previousDay: previousDay)) } } if let paramsDaily { try reader.prefetchData(variables: paramsDaily, time: time.dailyRead.toSettings()) } }, current: paramsCurrent.map { variables in return { .init(name: params.current_weather == true ? "current_weather" : "current", time: currentTimeRange.range.lowerBound, dtSeconds: currentTimeRange.dtSeconds, columns: try variables.map { variable in let (v, previousDay) = variable.variableAndPreviousDay guard let d = try reader.get(variable: v, time: currentTimeRange.toSettings(previousDay: previousDay))?.convertAndRound(params: params) else { return .init(variable: variable.resultVariable, unit: .undefined, value: .nan) } return .init(variable: variable.resultVariable, unit: d.unit, value: d.data.first ?? .nan) }) } }, hourly: paramsHourly.map { variables in return { return .init(name: "hourly", time: timeHourlyDisplay, columns: try variables.map { variable in let (v, previousDay) = variable.variableAndPreviousDay guard let d = try reader.get(variable: v, time: timeHourlyRead.toSettings(previousDay: previousDay))?.convertAndRound(params: params) else { return .init(variable: variable.resultVariable, unit: .undefined, variables: [.float([Float](repeating: .nan, count: timeHourlyRead.count))]) } assert(timeHourlyRead.count == d.data.count) return .init(variable: variable.resultVariable, unit: d.unit, variables: [.float(d.data)]) }) } }, daily: paramsDaily.map { dailyVariables in return { var riseSet: (rise: [Timestamp], set: [Timestamp])? = nil return ApiSection(name: "daily", time: time.dailyDisplay, columns: try dailyVariables.map { variable -> ApiColumn in if variable == .sunrise || variable == .sunset { // only calculate sunrise/set once. Need to use `dailyDisplay` to make sure half-hour time zone offsets are applied correctly let times = riseSet ?? Zensun.calculateSunRiseSet(timeRange: time.dailyDisplay.range, lat: reader.modelLat, lon: reader.modelLon, utcOffsetSeconds: timezone.utcOffsetSeconds) riseSet = times if variable == .sunset { return ApiColumn(variable: .sunset, unit: params.timeformatOrDefault.unit, variables: [.timestamp(times.set)]) } else { return ApiColumn(variable: .sunrise, unit: params.timeformatOrDefault.unit, variables: [.timestamp(times.rise)]) } } if variable == .daylight_duration { let duration = Zensun.calculateDaylightDuration(localMidnight: time.dailyDisplay.range, lat: reader.modelLat) return ApiColumn(variable: .daylight_duration, unit: .seconds, variables: [.float(duration)]) } guard let d = try reader.getDaily(variable: variable, params: params, time: time.dailyRead.toSettings()) else { return ApiColumn(variable: variable, unit: .undefined, variables: [.float([Float](repeating: .nan, count: time.dailyRead.count))]) } assert(time.dailyRead.count == d.data.count) return ApiColumn(variable: variable, unit: d.unit, variables: [.float(d.data)]) }) } }, sixHourly: nil, minutely15: paramsMinutely.map { variables in return { return .init(name: "minutely_15", time: time.minutely15, columns: try variables.map { variable in let (v, previousDay) = variable.variableAndPreviousDay guard let d = try reader.get(variable: v, time: time.minutely15.toSettings(previousDay: previousDay))?.convertAndRound(params: params) else { return ApiColumn(variable: variable.resultVariable, unit: .undefined, variables: [.float([Float](repeating: .nan, count: time.minutely15.count))]) } assert(time.minutely15.count == d.data.count) return .init(variable: variable.resultVariable, unit: d.unit, variables: [.float(d.data)]) }) } } ) } guard !readers.isEmpty else { throw ForecastapiError.noDataAvilableForThisLocation } return .init(timezone: timezone, time: timeLocal, locationId: prepared.locationId, results: readers) } let result = ForecastapiResult(timeformat: params.timeformatOrDefault, results: locations) await req.incrementRateLimiter(weight: result.calculateQueryWeight(nVariablesModels: nVariables), apikey: numberOfLocationsMaximum.apikey) return try await result.response(format: params.format ?? .json, numberOfLocationsMaximum: numberOfLocationsMaximum) } } extension ForecastVariable { var resultVariable: ForecastapiResult.SurfacePressureAndHeightVariable { switch self { case .pressure(let p): return .pressure(.init(p.variable, p.level)) case .surface(let s): return .surface(s) case .height(let h): return .height(.init(h.variable, h.level)) } } } /** Automatic domain selection rules: - If HRRR domain matches, use HRRR+GFS+ICON - If Western Europe, use Arome + ICON_EU+ ICON + GFS - If Central Europe, use ICON_D2, ICON_EU, ICON + GFS - If Japan, use JMA_MSM + ICON + GFS - default ICON + GFS Note Nov 2022: Use the term `seamless` instead of `mix` */ enum MultiDomains: String, RawRepresentableString, CaseIterable, MultiDomainMixerDomain { case best_match case gfs_seamless case gfs_mix case gfs_global case gfs025 case gfs013 case gfs_hrrr case gfs_graphcast025 case ncep_nbm_conus case meteofrance_seamless case meteofrance_mix case meteofrance_arpege_seamless case meteofrance_arpege_world case meteofrance_arpege_europe case meteofrance_arome_seamless case meteofrance_arome_france case meteofrance_arome_france_hd case arpege_seamless case arpege_world case arpege_europe case arome_seamless case arome_france case arome_france_hd case jma_seamless case jma_mix case jma_msm case jms_gsm case jma_gsm case gem_seamless case gem_global case gem_regional case gem_hrdps_continental case icon_seamless case icon_mix case icon_global case icon_eu case icon_d2 case ecmwf_ifs04 case ecmwf_ifs025 case ecmwf_aifs025 case ecmwf_aifs025_single case metno_nordic case cma_grapes_global case bom_access_global case archive_best_match case era5_seamless case era5 case cerra case era5_land case era5_ensemble case ecmwf_ifs case ecmwf_ifs_analysis case ecmwf_ifs_analysis_long_window case ecmwf_ifs_long_window case arpae_cosmo_seamless case arpae_cosmo_2i case arpae_cosmo_2i_ruc case arpae_cosmo_5m case knmi_harmonie_arome_europe case knmi_harmonie_arome_netherlands case dmi_harmonie_arome_europe case knmi_seamless case dmi_seamless case metno_seamless case ukmo_seamless case ukmo_global_deterministic_10km case ukmo_uk_deterministic_2km case satellite_radiation_seamless case eumetsat_sarah3 case eumetsat_lsa_saf_msg case eumetsat_lsa_saf_iodc case jma_jaxa_himawari case kma_seamless case kma_gdps case kma_ldps /// Return the required readers for this domain configuration /// Note: last reader has highes resolution data func getReader(lat: Float, lon: Float, elevation: Float, mode: GridSelectionMode, options: GenericReaderOptions) throws -> [any GenericReaderProtocol] { switch self { case .best_match: guard let icon: any GenericReaderProtocol = try IconReader(domain: .icon, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) else { throw ModelError.domainInitFailed(domain: IconDomains.icon.rawValue) } let gfsProbabilites = try ProbabilityReader.makeGfsReader(lat: lat, lon: lon, elevation: elevation, mode: mode) let iconProbabilities = try ProbabilityReader.makeIconReader(lat: lat, lon: lon, elevation: elevation, mode: mode) guard let gfs: any GenericReaderProtocol = try GfsReader(domains: [.gfs025, .gfs013], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) else { throw ModelError.domainInitFailed(domain: IconDomains.icon.rawValue) } // For Netherlands and Belgium use KNMI if (49.35..<53.79).contains(lat), (2.19..<7.66).contains(lon), let knmiNetherlands = try KnmiReader(domain: KnmiDomain.harmonie_arome_netherlands, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) { let probabilities = try ProbabilityReader.makeEcmwfReader(lat: lat, lon: lon, elevation: elevation, mode: mode) let ecmwf = try EcmwfReader(domain: .ifs025, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) let iconEu = try IconReader(domain: .iconEu, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) let iconD2 = try IconReader(domain: .iconD2, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) return Array([gfsProbabilites, probabilities, gfs, icon, iconEu, iconD2, ecmwf, knmiNetherlands].compacted()) } // Scandinavian region, combine with ICON if lat >= 54.9, let metno = try MetNoReader(domain: .nordic_pp, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) { let iconEu = try IconReader(domain: .iconEu, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) let probabilities = try ProbabilityReader.makeEcmwfReader(lat: lat, lon: lon, elevation: elevation, mode: mode) let ecmwf = try EcmwfReader(domain: .ifs025, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) let iconD2 = try IconReader(domain: .iconD2, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) return Array([gfsProbabilites, probabilities, gfs, icon, iconEu, iconD2, ecmwf, metno].compacted()) } // If Icon-d2 is available, use icon domains if let iconD2 = try IconReader(domain: .iconD2, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options), let iconD2_15min = try IconReader(domain: .iconD2_15min, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) { // TODO: check how out of projection areas are handled guard let iconEu = try IconReader(domain: .iconEu, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) else { throw ModelError.domainInitFailed(domain: IconDomains.icon.rawValue) } return [gfsProbabilites, iconProbabilities, gfs, icon, iconEu, iconD2, iconD2_15min] } // For western europe, use arome models if (42.10..<51.32).contains(lat), (-6.18..<8.35).contains(lon), let arome_france_hd = try MeteoFranceReader(domain: .arome_france_hd, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) { let arome_france_hd_15min = try MeteoFranceReader(domain: .arome_france_hd_15min, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) let arome_france = try MeteoFranceReader(domain: .arome_france, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) let arome_france_15min = try MeteoFranceReader(domain: .arome_france_15min, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) let arpege_europe = try MeteoFranceReader(domain: .arpege_europe, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) return Array([gfsProbabilites, iconProbabilities, gfs, icon, arpege_europe, arome_france, arome_france_hd, arome_france_15min, arome_france_hd_15min].compacted()) } // For Northern Europe and Iceland use DMI Harmonie if (44..<66).contains(lat), let dmiEurope = try DmiReader(domain: DmiDomain.harmonie_arome_europe, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options){ let probabilities = try ProbabilityReader.makeEcmwfReader(lat: lat, lon: lon, elevation: elevation, mode: mode) let ecmwf = try EcmwfReader(domain: .ifs025, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) let iconEu = try IconReader(domain: .iconEu, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) return Array([gfsProbabilites, probabilities, gfs, icon, iconEu, ecmwf, dmiEurope].compacted()) } // For North America, use HRRR if let hrrr = try GfsReader(domains: [.hrrr_conus, .hrrr_conus_15min], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) { let nbmProbabilities = try ProbabilityReader.makeNbmReader(lat: lat, lon: lon, elevation: elevation, mode: mode) return Array([gfsProbabilites, nbmProbabilities, icon, gfs, hrrr].compacted()) } // For Japan use JMA MSM with ICON. Does not use global JMA model because of poor resolution if (22.4+5..<47.65-5).contains(lat), (120+5..<150-5).contains(lon), let jma_msm = try JmaReader(domain: .msm, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) { return [gfsProbabilites, iconProbabilities, gfs, icon, jma_msm] } // Remaining eastern europe if let iconEu = try IconReader(domain: .iconEu, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) { return [gfsProbabilites, iconProbabilities, gfs, icon, iconEu] } // Northern africa if let arpege_europe = try MeteoFranceReader(domain: .arpege_europe, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) { let arpegeProbabilities: (any GenericReaderProtocol)? = try ProbabilityReader.makeMeteoFranceEuropeReader(lat: lat, lon: lon, elevation: elevation, mode: mode) return [gfsProbabilites, iconProbabilities, arpegeProbabilities, gfs, icon, arpege_europe].compactMap({$0}) } // Remaining parts of the world return [gfsProbabilites, iconProbabilities, gfs, icon] case .gfs_mix, .gfs_seamless: return [ try ProbabilityReader.makeGfsReader(lat: lat, lon: lon, elevation: elevation, mode: mode) as any GenericReaderProtocol, try ProbabilityReader.makeNbmReader(lat: lat, lon: lon, elevation: elevation, mode: mode) as (any GenericReaderProtocol)?, try GfsReader(domains: [.gfs025, .gfs013, .hrrr_conus, .hrrr_conus_15min], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) ].compactMap({$0}) case .gfs_global: let gfsProbabilites = try ProbabilityReader.makeGfsReader(lat: lat, lon: lon, elevation: elevation, mode: mode) return [gfsProbabilites] + (try GfsReader(domains: [.gfs025, .gfs013], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? []) case .gfs025: return try GfsReader(domains: [.gfs025], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .gfs013: return try GfsReader(domains: [.gfs013], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .gfs_hrrr: return [ try ProbabilityReader.makeNbmReader(lat: lat, lon: lon, elevation: elevation, mode: mode) as (any GenericReaderProtocol)?, try GfsReader(domains: [.hrrr_conus, .hrrr_conus_15min], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) ].compactMap({$0}) case .gfs_graphcast025: return try GfsGraphCastReader(domain: .graphcast025, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .meteofrance_mix, .meteofrance_seamless: let arpegeProbabilities: (any GenericReaderProtocol)? = try ProbabilityReader.makeMeteoFranceEuropeReader(lat: lat, lon: lon, elevation: elevation, mode: mode) return ([arpegeProbabilities] + (try MeteoFranceMixer(domains: [.arpege_world, .arpege_europe, .arome_france, .arome_france_hd, .arome_france_15min, .arome_france_hd_15min], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)?.reader ?? [])).compactMap({$0}) case .meteofrance_arpege_seamless, .arpege_seamless: let arpegeProbabilities: (any GenericReaderProtocol)? = try ProbabilityReader.makeMeteoFranceEuropeReader(lat: lat, lon: lon, elevation: elevation, mode: mode) return ([arpegeProbabilities] + (try MeteoFranceMixer(domains: [.arpege_world, .arpege_europe], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)?.reader ?? [])).compactMap({$0}) case .meteofrance_arome_seamless, .arome_seamless: return try MeteoFranceMixer(domains: [.arome_france, .arome_france_hd, .arome_france_15min, .arome_france_hd_15min], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)?.reader ?? [] case .meteofrance_arpege_world, .arpege_world: return try MeteoFranceReader(domain: .arpege_world, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .meteofrance_arpege_europe, .arpege_europe: let arpegeProbabilities: (any GenericReaderProtocol)? = try ProbabilityReader.makeMeteoFranceEuropeReader(lat: lat, lon: lon, elevation: elevation, mode: mode) return ([arpegeProbabilities] + (try MeteoFranceReader(domain: .arpege_europe, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [])).compactMap({$0}) case .meteofrance_arome_france, .arome_france: // Note: AROME PI 15min is not used for consistency here return try MeteoFranceMixer(domains: [.arome_france], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)?.reader ?? [] case .meteofrance_arome_france_hd, .arome_france_hd: // Note: AROME PI 15min is not used for consistency here return try MeteoFranceMixer(domains: [.arome_france_hd], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)?.reader ?? [] case .jma_mix, .jma_seamless: return try JmaMixer(domains: [.gsm, .msm], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)?.reader ?? [] case .jma_msm: return try JmaReader(domain: .msm, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .jms_gsm, .jma_gsm: return try JmaReader(domain: .gsm, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .icon_seamless, .icon_mix: let iconProbabilities = try ProbabilityReader.makeIconReader(lat: lat, lon: lon, elevation: elevation, mode: mode) return [iconProbabilities] + (try IconMixer(domains: [.icon, .iconEu, .iconD2, .iconD2_15min], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)?.reader ?? []) case .icon_global: let iconProbabilities = try ProbabilityReader.makeIconGlobalReader(lat: lat, lon: lon, elevation: elevation, mode: mode) return [iconProbabilities] + (try IconReader(domain: .icon, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? []) case .icon_eu: let iconProbabilities = try ProbabilityReader.makeIconEuReader(lat: lat, lon: lon, elevation: elevation, mode: mode) return (iconProbabilities.flatMap({[$0]}) ?? []) + (try IconReader(domain: .iconEu, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? []) case .icon_d2: let iconProbabilities = try ProbabilityReader.makeIconD2Reader(lat: lat, lon: lon, elevation: elevation, mode: mode) return (iconProbabilities.flatMap({[$0]}) ?? []) + (try IconMixer(domains: [.iconD2, .iconD2_15min], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)?.reader ?? []) case .ecmwf_ifs04: return try EcmwfReader(domain: .ifs04, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .ecmwf_ifs025: let probabilities = try ProbabilityReader.makeEcmwfReader(lat: lat, lon: lon, elevation: elevation, mode: mode) return [probabilities] + (try EcmwfReader(domain: .ifs025, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? []) case .ecmwf_aifs025: return try EcmwfReader(domain: .aifs025, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .ecmwf_aifs025_single: return try EcmwfReader(domain: .aifs025_single, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .metno_nordic: return try MetNoReader(domain: .nordic_pp, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .gem_seamless: let probabilities = try ProbabilityReader.makeGemReader(lat: lat, lon: lon, elevation: elevation, mode: mode) return [probabilities] + (try GemMixer(domains: [.gem_global, .gem_regional, .gem_hrdps_continental], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)?.reader ?? []) case .gem_global: let probabilities = try ProbabilityReader.makeGemReader(lat: lat, lon: lon, elevation: elevation, mode: mode) return [probabilities] + (try GemReader(domain: .gem_global, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? []) case .gem_regional: return try GemReader(domain: .gem_regional, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .gem_hrdps_continental: return try GemReader(domain: .gem_hrdps_continental, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .archive_best_match: return [try Era5Factory.makeArchiveBestMatch(lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)] case .era5_seamless: return [try Era5Factory.makeEra5CombinedLand(lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)] case .era5: // If explicitly selected ERA5, combine with ensemble to read spread variables return [try Era5Factory.makeEra5WithEnsemble(lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)] case .era5_land: return [try Era5Factory.makeReader(domain: .era5_land, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)] case .cerra: return try CerraReader(domain: .cerra, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .ecmwf_ifs: return [try Era5Factory.makeReader(domain: .ecmwf_ifs, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)] case .cma_grapes_global: return try CmaReader(domain: .grapes_global, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .bom_access_global: let probabilities = try ProbabilityReader.makeBomReader(lat: lat, lon: lon, elevation: elevation, mode: mode) return [probabilities] + (try BomReader(domain: .access_global, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? []) case .arpae_cosmo_seamless: return try ArpaeMixer(domains: [.cosmo_5m, .cosmo_2i, .cosmo_2i_ruc], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)?.reader ?? [] case .arpae_cosmo_2i: return try ArpaeReader(domain: .cosmo_2i, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .arpae_cosmo_2i_ruc: return try ArpaeReader(domain: .cosmo_2i_ruc, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .arpae_cosmo_5m: return try ArpaeReader(domain: .cosmo_5m, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .knmi_harmonie_arome_europe: return try KnmiReader(domain: KnmiDomain.harmonie_arome_europe, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .knmi_harmonie_arome_netherlands: return try KnmiReader(domain: KnmiDomain.harmonie_arome_netherlands, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .dmi_harmonie_arome_europe: return try DmiReader(domain: DmiDomain.harmonie_arome_europe, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .knmi_seamless: let probabilities = try ProbabilityReader.makeEcmwfReader(lat: lat, lon: lon, elevation: elevation, mode: mode) let knmiNetherlands: (any GenericReaderProtocol)? = try KnmiReader(domain: KnmiDomain.harmonie_arome_netherlands, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) let knmiEurope = try KnmiReader(domain: KnmiDomain.harmonie_arome_europe, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) let ecmwf = try EcmwfReader(domain: .ifs025, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) return [probabilities, ecmwf, knmiEurope, knmiNetherlands].compactMap({$0}) case .dmi_seamless: let probabilities = try ProbabilityReader.makeEcmwfReader(lat: lat, lon: lon, elevation: elevation, mode: mode) let dmiEurope: (any GenericReaderProtocol)? = try DmiReader(domain: DmiDomain.harmonie_arome_europe, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) let ecmwf = try EcmwfReader(domain: .ifs025, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) return [probabilities, ecmwf, dmiEurope].compactMap({$0}) case .metno_seamless: let probabilities = try ProbabilityReader.makeEcmwfReader(lat: lat, lon: lon, elevation: elevation, mode: mode) let metno: (any GenericReaderProtocol)? = try MetNoReader(domain: .nordic_pp, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) let ecmwf = try EcmwfReader(domain: .ifs025, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) return [probabilities, ecmwf, metno].compactMap({$0}) case .ecmwf_ifs_analysis_long_window: return [try Era5Factory.makeReader(domain: .ecmwf_ifs_analysis_long_window, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)] case .ecmwf_ifs_analysis: return [try Era5Factory.makeReader(domain: .ecmwf_ifs_analysis, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)] case .ecmwf_ifs_long_window: return [try Era5Factory.makeReader(domain: .ecmwf_ifs_long_window, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)] case .era5_ensemble: return [try Era5Factory.makeReader(domain: .era5_ensemble, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)] case .ukmo_seamless: let ukmoGlobal: (any GenericReaderProtocol)? = try UkmoReader(domain: UkmoDomain.global_deterministic_10km, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) let ukmoUk = try UkmoReader(domain: UkmoDomain.uk_deterministic_2km, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) return [ukmoGlobal, ukmoUk].compactMap({$0}) case .ukmo_global_deterministic_10km: let ukmoGlobal: (any GenericReaderProtocol)? = try UkmoReader(domain: UkmoDomain.global_deterministic_10km, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) return [ukmoGlobal].compactMap({$0}) case .ukmo_uk_deterministic_2km: let ukmoUk = try UkmoReader(domain: UkmoDomain.uk_deterministic_2km, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) return [ukmoUk].compactMap({$0}) case .ncep_nbm_conus: return try NbmReader(domains: [.nbm_conus], lat: lat, lon: lon, elevation: elevation, mode: mode, options: options).flatMap({[$0]}) ?? [] case .eumetsat_sarah3: let sarah3 = try EumetsatSarahReader(domain: EumetsatSarahDomain.sarah3_30min, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) return [sarah3].compactMap({$0}) case .jma_jaxa_himawari: let sat = try JaxaHimawariReader(domain: JaxaHimawariDomain.himawari_10min, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) return [sat].compactMap({$0}) case .eumetsat_lsa_saf_msg: let sat = try EumetsatLsaSafReader(domain: .msg, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) return [sat].compactMap({$0}) case .eumetsat_lsa_saf_iodc: let sat = try EumetsatLsaSafReader(domain: .iodc, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) return [sat].compactMap({$0}) case .satellite_radiation_seamless: if (-60..<50).contains(lon) { // MSG on 0° return [try EumetsatLsaSafReader(domain: .msg, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)].compactMap({$0}) } if (50..<90).contains(lon) { // IODC on 41.5° return [try EumetsatLsaSafReader(domain: .iodc, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)].compactMap({$0}) } if (90...).contains(lon) { // Himawari on 140° return [try JaxaHimawariReader(domain: JaxaHimawariDomain.himawari_10min, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)].compactMap({$0}) } // TODO GOES east + west return [] case .kma_seamless: let ldps = try KmaReader(domain: .ldps, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) let gdps = try KmaReader(domain: .gdps, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) return [gdps, ldps].compactMap({$0}) case .kma_gdps: let reader = try KmaReader(domain: .gdps, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) return [reader].compactMap({$0}) case .kma_ldps: let reader = try KmaReader(domain: .ldps, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options) return [reader].compactMap({$0}) } } var genericDomain: (any GenericDomain)? { switch self { case .gfs025: return GfsDomain.gfs025 case .gfs013: return GfsDomain.gfs013 case .gfs_hrrr: return GfsDomain.hrrr_conus case .gfs_graphcast025: return GfsGraphCastDomain.graphcast025 case .meteofrance_arpege_world, .arpege_world: return MeteoFranceDomain.arpege_world case .meteofrance_arpege_europe, .arpege_europe: return MeteoFranceDomain.arpege_europe case .meteofrance_arome_france, .arome_france: return MeteoFranceDomain.arome_france case .meteofrance_arome_france_hd, .arome_france_hd: return MeteoFranceDomain.arome_france_hd case .icon_global: return IconDomains.icon case .icon_eu: return IconDomains.iconEu case .icon_d2: return IconDomains.iconD2 case .ecmwf_ifs04: return EcmwfDomain.ifs04 case .ecmwf_ifs025: return EcmwfDomain.ifs025 case .ecmwf_aifs025: return EcmwfDomain.aifs025 case .metno_nordic: return MetNoDomain.nordic_pp case .gem_global: return GemDomain.gem_global case .gem_regional: return GemDomain.gem_regional case .gem_hrdps_continental: return GemDomain.gem_hrdps_continental case .era5: return CdsDomain.era5 case .era5_land: return CdsDomain.era5_land case .cerra: return CdsDomain.cerra case .ecmwf_ifs: return CdsDomain.ecmwf_ifs case .cma_grapes_global: return CmaDomain.grapes_global case .bom_access_global: return BomDomain.access_global case .arpae_cosmo_2i: return ArpaeDomain.cosmo_2i case .arpae_cosmo_2i_ruc: return ArpaeDomain.cosmo_2i_ruc case .arpae_cosmo_5m: return ArpaeDomain.cosmo_5m default: return nil } } func getReader(gridpoint: Int, options: GenericReaderOptions) throws -> (any GenericReaderProtocol)? { switch self { case .gfs025: return try GfsReader(domain: .gfs025, gridpoint: gridpoint, options: options) case .gfs013: return try GfsReader(domain: .gfs013, gridpoint: gridpoint, options: options) case .gfs_hrrr: return try GfsReader(domain: .hrrr_conus, gridpoint: gridpoint, options: options) case .gfs_graphcast025: return try GfsGraphCastReader(domain: .graphcast025, gridpoint: gridpoint, options: options) case .meteofrance_arpege_world, .arpege_world: return try MeteoFranceReader(domain: .arpege_world, gridpoint: gridpoint, options: options) case .meteofrance_arpege_europe, .arpege_europe: return try MeteoFranceReader(domain: .arpege_europe, gridpoint: gridpoint, options: options) case .meteofrance_arome_france, .arome_france: return try MeteoFranceReader(domain: .arome_france, gridpoint: gridpoint, options: options) case .meteofrance_arome_france_hd, .arome_france_hd: return try MeteoFranceReader(domain: .arome_france_hd, gridpoint: gridpoint, options: options) case .icon_global: return try IconReader(domain: .icon, gridpoint: gridpoint, options: options) case .icon_eu: return try IconReader(domain: .iconEu, gridpoint: gridpoint, options: options) case .icon_d2: return try IconReader(domain: .iconD2, gridpoint: gridpoint, options: options) case .ecmwf_ifs04: return try EcmwfReader(domain: .ifs04, gridpoint: gridpoint, options: options) case .ecmwf_ifs025: return try EcmwfReader(domain: .ifs025, gridpoint: gridpoint, options: options) case .ecmwf_aifs025: return try EcmwfReader(domain: .aifs025, gridpoint: gridpoint, options: options) case .metno_nordic: return try MetNoReader(domain: .nordic_pp, gridpoint: gridpoint, options: options) case .gem_global: return try GemReader(domain: .gem_global, gridpoint: gridpoint, options: options) case .gem_regional: return try GemReader(domain: .gem_regional, gridpoint: gridpoint, options: options) case .gem_hrdps_continental: return try GemReader(domain: .gem_hrdps_continental, gridpoint: gridpoint, options: options) case .era5: return try Era5Factory.makeReader(domain: .era5, gridpoint: gridpoint, options: options) case .era5_land: return try Era5Factory.makeReader(domain: .era5_land, gridpoint: gridpoint, options: options) case .cerra: return try CerraReader(domain: .cerra, gridpoint: gridpoint, options: options) case .ecmwf_ifs: return try Era5Factory.makeReader(domain: .ecmwf_ifs, gridpoint: gridpoint, options: options) case .cma_grapes_global: return try CmaReader(domain: .grapes_global, gridpoint: gridpoint, options: options) case .bom_access_global: return try BomReader(domain: .access_global, gridpoint: gridpoint, options: options) case .arpae_cosmo_2i: return try ArpaeReader(domain: .cosmo_2i, gridpoint: gridpoint, options: options) case .arpae_cosmo_2i_ruc: return try ArpaeReader(domain: .cosmo_2i_ruc, gridpoint: gridpoint, options: options) case .arpae_cosmo_5m: return try ArpaeReader(domain: .cosmo_5m, gridpoint: gridpoint, options: options) default: return nil } } var countEnsembleMember: Int { return 1 } } enum ModelError: AbortError { var status: NIOHTTP1.HTTPResponseStatus { return .badRequest } case domainInitFailed(domain: String) } /// Define all available surface weather variables enum ForecastSurfaceVariable: String, GenericVariableMixable { /// Maps to `temperature_2m`. Used for compatibility with `current_weather` block case temperature /// Maps to `windspeed_10m`. Used for compatibility with `current_weather` block case windspeed /// Maps to `winddirection_10m`. Used for compatibility with `current_weather` block case winddirection case wet_bulb_temperature_2m case apparent_temperature case cape case cloudcover case cloudcover_high case cloudcover_low case cloudcover_mid case cloud_cover case cloud_cover_high case cloud_cover_low case cloud_cover_mid case cloud_cover_2m case cloud_base case cloud_top case convective_cloud_base case convective_cloud_top case dewpoint_2m case dew_point_2m case diffuse_radiation case diffuse_radiation_instant case direct_normal_irradiance case direct_normal_irradiance_instant case direct_radiation case direct_radiation_instant case et0_fao_evapotranspiration case evapotranspiration case freezinglevel_height case freezing_level_height case growing_degree_days_base_0_limit_50 case is_day case latent_heatflux case latent_heat_flux case lifted_index case convective_inhibition case leaf_wetness_probability case lightning_potential case mass_density_8m case precipitation case precipitation_probability case precipitation_type case pressure_msl case rain case relativehumidity_2m case relative_humidity_2m case runoff case sensible_heatflux case sensible_heat_flux case shortwave_radiation case shortwave_radiation_instant case showers case skin_temperature case snow_depth case snow_depth_water_equivalent case snow_height case hail case snowfall case snowfall_water_equivalent case sunshine_duration case soil_moisture_0_1cm case soil_moisture_0_to_1cm case soil_moisture_0_to_100cm case soil_moisture_0_to_10cm case soil_moisture_0_to_7cm case soil_moisture_100_to_200cm case soil_moisture_100_to_255cm case soil_moisture_10_to_40cm case soil_moisture_1_3cm case soil_moisture_1_to_3cm case soil_moisture_27_81cm case soil_moisture_27_to_81cm case soil_moisture_28_to_100cm case soil_moisture_3_9cm case soil_moisture_3_to_9cm case soil_moisture_40_to_100cm case soil_moisture_7_to_28cm case soil_moisture_9_27cm case soil_moisture_9_to_27cm case soil_moisture_index_0_to_100cm case soil_moisture_index_0_to_7cm case soil_moisture_index_100_to_255cm case soil_moisture_index_28_to_100cm case soil_moisture_index_7_to_28cm case soil_temperature_0_to_100cm case soil_temperature_0_to_10cm case soil_temperature_0_to_7cm case soil_temperature_0cm case soil_temperature_100_to_200cm case soil_temperature_100_to_255cm case soil_temperature_10_to_40cm case soil_temperature_18cm case soil_temperature_28_to_100cm case soil_temperature_40_to_100cm case soil_temperature_54cm case soil_temperature_6cm case soil_temperature_7_to_28cm case surface_air_pressure case snowfall_height case surface_pressure case surface_temperature case temperature_100m case temperature_120m case temperature_150m case temperature_180m case temperature_2m case temperature_20m case temperature_200m case temperature_50m case temperature_40m case temperature_80m case temperature_2m_max case temperature_2m_min case terrestrial_radiation case terrestrial_radiation_instant case total_column_integrated_water_vapour case updraft case uv_index case uv_index_clear_sky case vapor_pressure_deficit case vapour_pressure_deficit case visibility case weathercode case weather_code case winddirection_100m case winddirection_10m case winddirection_120m case winddirection_150m case winddirection_180m case winddirection_200m case winddirection_20m case winddirection_40m case winddirection_50m case winddirection_80m case windgusts_10m case windspeed_100m case windspeed_10m case windspeed_120m case windspeed_150m case windspeed_180m case windspeed_200m case windspeed_20m case windspeed_40m case windspeed_50m case windspeed_80m case wind_direction_250m case wind_direction_300m case wind_direction_350m case wind_direction_450m case wind_direction_100m case wind_direction_10m case wind_direction_120m case wind_direction_140m case wind_direction_150m case wind_direction_160m case wind_direction_180m case wind_direction_200m case wind_direction_20m case wind_direction_40m case wind_direction_30m case wind_direction_50m case wind_direction_80m case wind_direction_70m case wind_gusts_10m case wind_speed_250m case wind_speed_300m case wind_speed_350m case wind_speed_450m case wind_speed_100m case wind_speed_10m case wind_speed_120m case wind_speed_140m case wind_speed_150m case wind_speed_160m case wind_speed_180m case wind_speed_200m case wind_speed_20m case wind_speed_40m case wind_speed_30m case wind_speed_50m case wind_speed_70m case wind_speed_80m case soil_temperature_10_to_35cm case soil_temperature_35_to_100cm case soil_temperature_100_to_300cm case soil_moisture_10_to_35cm case soil_moisture_35_to_100cm case soil_moisture_100_to_300cm case shortwave_radiation_clear_sky case global_tilted_irradiance case global_tilted_irradiance_instant case boundary_layer_height case thunderstorm_probability case rain_probability case freezing_rain_probability case ice_pellets_probability case snowfall_probability case albedo case wind_speed_10m_spread case wind_speed_100m_spread case wind_direction_10m_spread case wind_direction_100m_spread case snowfall_spread case temperature_2m_spread case wind_gusts_10m_spread case dew_point_2m_spread case cloud_cover_low_spread case cloud_cover_mid_spread case cloud_cover_high_spread case pressure_msl_spread case snowfall_water_equivalent_spread case snow_depth_spread case soil_temperature_0_to_7cm_spread case soil_temperature_7_to_28cm_spread case soil_temperature_28_to_100cm_spread case soil_temperature_100_to_255cm_spread case soil_moisture_0_to_7cm_spread case soil_moisture_7_to_28cm_spread case soil_moisture_28_to_100cm_spread case soil_moisture_100_to_255cm_spread case shortwave_radiation_spread case precipitation_spread case direct_radiation_spread case boundary_layer_height_spread /// Some variables are kept for backwards compatibility var remapped: Self { switch self { case .temperature: return .temperature_2m case .windspeed: return .wind_speed_10m case .winddirection: return .wind_direction_10m case .surface_air_pressure: return .surface_pressure default: return self } } /// Soil moisture or snow depth are cumulative processes and have offests if mutliple models are mixed var requiresOffsetCorrectionForMixing: Bool { switch self { case .soil_moisture_0_1cm: return true case .soil_moisture_0_to_100cm: return true case .soil_moisture_0_to_10cm: return true case .soil_moisture_0_to_7cm: return true case .soil_moisture_100_to_200cm: return true case .soil_moisture_100_to_255cm: return true case .soil_moisture_10_to_40cm: return true case .soil_moisture_1_3cm: return true case .soil_moisture_27_81cm: return true case .soil_moisture_28_to_100cm: return true case .soil_moisture_3_9cm: return true case .soil_moisture_40_to_100cm: return true case .soil_moisture_7_to_28cm: return true case .soil_moisture_9_27cm: return true case .snow_depth: return true default: return false } } } /// Available pressure level variables enum ForecastPressureVariableType: String, GenericVariableMixable { case temperature case geopotential_height case relativehumidity case relative_humidity case windspeed case wind_speed case winddirection case wind_direction case dewpoint case dew_point case cloudcover case cloud_cover case vertical_velocity var requiresOffsetCorrectionForMixing: Bool { return false } } struct ForecastPressureVariable: PressureVariableRespresentable, GenericVariableMixable { let variable: ForecastPressureVariableType let level: Int var requiresOffsetCorrectionForMixing: Bool { return false } } /// Available pressure level variables enum ForecastHeightVariableType: String, GenericVariableMixable { case temperature case relativehumidity case relative_humidity case windspeed case wind_speed case winddirection case wind_direction case dewpoint case dew_point case cloudcover case cloud_cover case vertical_velocity var requiresOffsetCorrectionForMixing: Bool { return false } } struct ForecastHeightVariable: HeightVariableRespresentable, GenericVariableMixable { let variable: ForecastHeightVariableType let level: Int var requiresOffsetCorrectionForMixing: Bool { return false } } typealias ForecastVariable = SurfacePressureAndHeightVariable extension ForecastVariable { var variableAndPreviousDay: (ForecastVariable, Int) { switch self { case .surface(let surface): return (ForecastVariable.surface(.init(surface.variable.remapped, 0)), surface.previousDay) case .pressure(let pressure): return (ForecastVariable.pressure(pressure), 0) case .height(let height): return (ForecastVariable.height(height), 0) } } } /// Available daily aggregations enum ForecastVariableDaily: String, DailyVariableCalculatable, RawRepresentableString { case apparent_temperature_max case apparent_temperature_mean case apparent_temperature_min case cape_max case cape_mean case cape_min case cloudcover_max case cloudcover_mean case cloudcover_min case cloud_cover_max case cloud_cover_mean case cloud_cover_min case dewpoint_2m_max case dewpoint_2m_mean case dewpoint_2m_min case dew_point_2m_max case dew_point_2m_mean case dew_point_2m_min case et0_fao_evapotranspiration case et0_fao_evapotranspiration_sum case growing_degree_days_base_0_limit_50 case leaf_wetness_probability_mean case precipitation_hours case precipitation_probability_max case precipitation_probability_mean case precipitation_probability_min case precipitation_sum case pressure_msl_max case pressure_msl_mean case pressure_msl_min case rain_sum case relative_humidity_2m_max case relative_humidity_2m_mean case relative_humidity_2m_min case shortwave_radiation_sum case showers_sum case snowfall_sum case snowfall_water_equivalent_sum case soil_moisture_0_to_100cm_mean case soil_moisture_0_to_10cm_mean case soil_moisture_0_to_7cm_mean case soil_moisture_28_to_100cm_mean case soil_moisture_7_to_28cm_mean case soil_moisture_index_0_to_100cm_mean case soil_moisture_index_0_to_7cm_mean case soil_moisture_index_100_to_255cm_mean case soil_moisture_index_28_to_100cm_mean case soil_moisture_index_7_to_28cm_mean case soil_temperature_0_to_100cm_mean case soil_temperature_0_to_7cm_mean case soil_temperature_28_to_100cm_mean case soil_temperature_7_to_28cm_mean case sunrise case sunset case daylight_duration case sunshine_duration case surface_pressure_max case surface_pressure_mean case surface_pressure_min case temperature_2m_max case temperature_2m_mean case temperature_2m_min case updraft_max case uv_index_clear_sky_max case uv_index_max case vapor_pressure_deficit_max case vapour_pressure_deficit_max case visibility_max case visibility_mean case visibility_min case weathercode case weather_code case winddirection_10m_dominant case windgusts_10m_max case windgusts_10m_mean case windgusts_10m_min case windspeed_10m_max case windspeed_10m_mean case windspeed_10m_min case wind_direction_10m_dominant case wind_gusts_10m_max case wind_gusts_10m_mean case wind_gusts_10m_min case wind_speed_10m_max case wind_speed_10m_mean case wind_speed_10m_min case wet_bulb_temperature_2m_max case wet_bulb_temperature_2m_mean case wet_bulb_temperature_2m_min var aggregation: DailyAggregation { switch self { case .temperature_2m_max: return .max(.surface(.init(.temperature_2m, 0))) case .temperature_2m_min: return .min(.surface(.init(.temperature_2m, 0))) case .temperature_2m_mean: return .mean(.surface(.init(.temperature_2m, 0))) case .apparent_temperature_max: return .max(.surface(.init(.apparent_temperature, 0))) case .apparent_temperature_mean: return .mean(.surface(.init(.apparent_temperature, 0))) case .apparent_temperature_min: return .min(.surface(.init(.apparent_temperature, 0))) case .precipitation_sum: return .sum(.surface(.init(.precipitation, 0))) case .snowfall_sum: return .sum(.surface(.init(.snowfall, 0))) case .rain_sum: return .sum(.surface(.init(.rain, 0))) case .showers_sum: return .sum(.surface(.init(.showers, 0))) case .weathercode, .weather_code: return .max(.surface(.init(.weathercode, 0))) case .shortwave_radiation_sum: return .radiationSum(.surface(.init(.shortwave_radiation, 0))) case .windspeed_10m_max, .wind_speed_10m_max: return .max(.surface(.init(.wind_speed_10m, 0))) case .windspeed_10m_min, .wind_speed_10m_min: return .min(.surface(.init(.wind_speed_10m, 0))) case .windspeed_10m_mean, .wind_speed_10m_mean: return .mean(.surface(.init(.wind_speed_10m, 0))) case .windgusts_10m_max, .wind_gusts_10m_max: return .max(.surface(.init(.wind_gusts_10m, 0))) case .windgusts_10m_min, .wind_gusts_10m_min: return .min(.surface(.init(.wind_gusts_10m, 0))) case .windgusts_10m_mean, .wind_gusts_10m_mean: return .mean(.surface(.init(.wind_gusts_10m, 0))) case .winddirection_10m_dominant, .wind_direction_10m_dominant: return .dominantDirection(velocity: .surface(.init(.wind_speed_10m, 0)), direction: .surface(.init(.wind_direction_10m, 0))) case .precipitation_hours: return .precipitationHours(.surface(.init(.precipitation, 0))) case .sunrise: return .none case .sunset: return .none case .et0_fao_evapotranspiration: return .sum(.surface(.init(.et0_fao_evapotranspiration, 0))) case .visibility_max: return .max(.surface(.init(.visibility, 0))) case .visibility_min: return .min(.surface(.init(.visibility, 0))) case .visibility_mean: return .mean(.surface(.init(.visibility, 0))) case .pressure_msl_max: return .max(.surface(.init(.pressure_msl, 0))) case .pressure_msl_min: return .min(.surface(.init(.pressure_msl, 0))) case .pressure_msl_mean: return .mean(.surface(.init(.pressure_msl, 0))) case .surface_pressure_max: return .max(.surface(.init(.surface_pressure, 0))) case .surface_pressure_min: return .min(.surface(.init(.surface_pressure, 0))) case .surface_pressure_mean: return .mean(.surface(.init(.surface_pressure, 0))) case .cape_max: return .max(.surface(.init(.cape, 0))) case .cape_min: return .min(.surface(.init(.cape, 0))) case .cape_mean: return .mean(.surface(.init(.cape, 0))) case .cloudcover_max, .cloud_cover_max: return .max(.surface(.init(.cloudcover, 0))) case .cloudcover_min, .cloud_cover_min: return .min(.surface(.init(.cloudcover, 0))) case .cloudcover_mean, .cloud_cover_mean: return .mean(.surface(.init(.cloudcover, 0))) case .uv_index_max: return .max(.surface(.init(.uv_index, 0))) case .uv_index_clear_sky_max: return .max(.surface(.init(.uv_index_clear_sky, 0))) case .precipitation_probability_max: return .max(.surface(.init(.precipitation_probability, 0))) case .precipitation_probability_min: return .min(.surface(.init(.precipitation_probability, 0))) case .precipitation_probability_mean: return .mean(.surface(.init(.precipitation_probability, 0))) case .dewpoint_2m_max, .dew_point_2m_max: return .max(.surface(.init(.dewpoint_2m, 0))) case .dewpoint_2m_mean, .dew_point_2m_mean: return .mean(.surface(.init(.dewpoint_2m, 0))) case .dewpoint_2m_min, .dew_point_2m_min: return .min(.surface(.init(.dewpoint_2m, 0))) case .et0_fao_evapotranspiration_sum: return .sum(.surface(.init(.et0_fao_evapotranspiration, 0))) case .growing_degree_days_base_0_limit_50: return .sum(.surface(.init(.growing_degree_days_base_0_limit_50, 0))) case .leaf_wetness_probability_mean: return .mean(.surface(.init(.leaf_wetness_probability, 0))) case .relative_humidity_2m_max: return .max(.surface(.init(.relativehumidity_2m, 0))) case .relative_humidity_2m_mean: return .mean(.surface(.init(.relativehumidity_2m, 0))) case .relative_humidity_2m_min: return .min(.surface(.init(.relativehumidity_2m, 0))) case .snowfall_water_equivalent_sum: return .sum(.surface(.init(.snowfall_water_equivalent, 0))) case .soil_moisture_0_to_100cm_mean: return .mean(.surface(.init(.soil_moisture_0_to_100cm, 0))) case .soil_moisture_0_to_10cm_mean: return .mean(.surface(.init(.soil_moisture_0_to_10cm, 0))) case .soil_moisture_0_to_7cm_mean: return .mean(.surface(.init(.soil_moisture_0_to_7cm, 0))) case .soil_moisture_28_to_100cm_mean: return .mean(.surface(.init(.soil_moisture_28_to_100cm, 0))) case .soil_moisture_7_to_28cm_mean: return .mean(.surface(.init(.soil_moisture_7_to_28cm, 0))) case .soil_moisture_index_0_to_100cm_mean: return .mean(.surface(.init(.soil_moisture_index_0_to_100cm, 0))) case .soil_moisture_index_0_to_7cm_mean: return .mean(.surface(.init(.soil_moisture_index_0_to_7cm, 0))) case .soil_moisture_index_100_to_255cm_mean: return .mean(.surface(.init(.soil_moisture_index_100_to_255cm, 0))) case .soil_moisture_index_28_to_100cm_mean: return .mean(.surface(.init(.soil_moisture_index_28_to_100cm, 0))) case .soil_moisture_index_7_to_28cm_mean: return .mean(.surface(.init(.soil_moisture_index_7_to_28cm, 0))) case .soil_temperature_0_to_100cm_mean: return .mean(.surface(.init(.soil_temperature_0_to_100cm, 0))) case .soil_temperature_0_to_7cm_mean: return .mean(.surface(.init(.soil_temperature_0_to_7cm, 0))) case .soil_temperature_28_to_100cm_mean: return .mean(.surface(.init(.soil_temperature_28_to_100cm, 0))) case .soil_temperature_7_to_28cm_mean: return .mean(.surface(.init(.soil_temperature_7_to_28cm, 0))) case .updraft_max: return .max(.surface(.init(.updraft, 0))) case .vapor_pressure_deficit_max, .vapour_pressure_deficit_max: return .max(.surface(.init(.vapor_pressure_deficit, 0))) case .wet_bulb_temperature_2m_max: return .max(.surface(.init(.wet_bulb_temperature_2m, 0))) case .wet_bulb_temperature_2m_min: return .min(.surface(.init(.wet_bulb_temperature_2m, 0))) case .wet_bulb_temperature_2m_mean: return .mean(.surface(.init(.wet_bulb_temperature_2m, 0))) case .daylight_duration: return .none case .sunshine_duration: return .sum(.surface(.init(.sunshine_duration, 0))) } } }