Spaces:
Sleeping
Sleeping
| 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<ForecastVariable, MultiDomains>.prepareReaders(domains: domains, params: params, currentTime: currentTime, forecastDayDefault: forecastDayDefault, forecastDaysMax: forecastDaysMax, pastDaysMax: 92, allowedRange: allowedRange) | |
| let locations: [ForecastapiResult<MultiDomains>.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<MultiDomains>.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<ForecastVariableDaily> 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<MultiDomains>(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<MultiDomains>.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<VariableAndPreviousDay, ForecastPressureVariable, ForecastHeightVariable> | |
| 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<ForecastVariable> { | |
| 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))) | |
| } | |
| } | |
| } | |