Spaces:
Sleeping
Sleeping
File size: 16,206 Bytes
6ee917b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | import Foundation
import Vapor
enum IconWaveDomainApi: String, CaseIterable, RawRepresentableString, MultiDomainMixerDomain {
var genericDomain: (any GenericDomain)? {
return nil
}
func getReader(gridpoint: Int, options: GenericReaderOptions) throws -> (any GenericReaderProtocol)? {
return nil
}
case best_match
case ewam
case gwam
case era5_ocean
case ecmwf_wam025
case ecmwf_wam025_ensemble
case ncep_gfswave025
case ncep_gfswave016
case ncep_gefswave025
case meteofrance_wave
case meteofrance_currents
var countEnsembleMember: Int {
switch self {
case .ecmwf_wam025_ensemble:
return EcmwfDomain.wam025_ensemble.ensembleMembers
case .ncep_gefswave025:
return GfsDomain.gfswave025_ens.ensembleMembers
default:
return 1
}
}
func getReader(lat: Float, lon: Float, elevation: Float, mode: GridSelectionMode, options: GenericReaderOptions) throws -> [any GenericReaderProtocol] {
switch self {
case .best_match:
//let gwam = try IconWaveReader(domain: .gwam, lat: lat, lon: lon, elevation: elevation, mode: mode)
let ewam = try IconWaveReader(domain: .ewam, lat: lat, lon: lon, elevation: elevation, mode: mode)
let mfcurrents = try GenericReader<MfWaveDomain, MfCurrentReader.Variable>(domain: .mfcurrents, lat: lat, lon: lon, elevation: elevation, mode: mode).map { reader -> any GenericReaderProtocol in
MfCurrentReader(reader: GenericReaderCached<MfWaveDomain, MfCurrentReader.Variable>(reader: reader))
}
let mfsst = try GenericReader<MfWaveDomain, MfSSTVariable>(domain: .mfsst, lat: lat, lon: lon, elevation: elevation, mode: mode)
let mfwave = try GenericReader<MfWaveDomain, MfWaveVariable>(domain: .mfwave, lat: lat, lon: lon, elevation: elevation, mode: mode).map { reader -> any GenericReaderProtocol in
MfWaveReader(reader: reader)
}
let waveModel: [(any GenericReaderProtocol)?];
if let update = try MfWaveDomain.mfwave.getMetaJson()?.lastRunAvailabilityTime, update <= Timestamp.now().subtract(hours: 26) {
// mf model outdated, use ECMWF
waveModel = [mfwave, try GenericReader<EcmwfDomain, EcmwfWaveVariable>(domain: EcmwfDomain.wam025, lat: lat, lon: lon, elevation: elevation, mode: mode)]
} else {
// use mf wave
waveModel = [mfwave]
}
let readers: [(any GenericReaderProtocol)?] = [mfcurrents, mfsst, ewam] + waveModel
return readers.compactMap({$0})
/*
let ecmwfWam025 = try GenericReader<EcmwfDomain, EcmwfWaveVariable>(domain: EcmwfDomain.wam025, lat: lat, lon: lon, elevation: elevation, mode: mode)
let readers: [(any GenericReaderProtocol)?] = [ewam, ecmwfWam025, gwam]
return readers.compactMap({$0})*/
case .ewam:
return try IconWaveReader(domain: .ewam, lat: lat, lon: lon, elevation: elevation, mode: mode).flatMap({[$0]}) ?? []
case .gwam:
return try IconWaveReader(domain: .gwam, lat: lat, lon: lon, elevation: elevation, mode: mode).flatMap({[$0]}) ?? []
case .era5_ocean:
return [try Era5Factory.makeReader(domain: .era5_ocean, lat: lat, lon: lon, elevation: elevation, mode: mode, options: options)]
case .ecmwf_wam025:
return try GenericReader<EcmwfDomain, EcmwfWaveVariable>(domain: EcmwfDomain.wam025, lat: lat, lon: lon, elevation: elevation, mode: mode).flatMap({[$0]}) ?? []
case .ecmwf_wam025_ensemble:
return try GenericReader<EcmwfDomain, EcmwfWaveVariable>(domain: EcmwfDomain.wam025_ensemble, lat: lat, lon: lon, elevation: elevation, mode: mode).flatMap({[$0]}) ?? []
case .meteofrance_wave:
return try GenericReader<MfWaveDomain, MfWaveVariable>(domain: .mfwave, lat: lat, lon: lon, elevation: elevation, mode: mode).flatMap({[MfWaveReader(reader: $0)]}) ?? []
case .meteofrance_currents:
let mfsst = try GenericReader<MfWaveDomain, MfSSTVariable>(domain: .mfsst, lat: lat, lon: lon, elevation: elevation, mode: mode)
let mfcurrents = try GenericReader<MfWaveDomain, MfCurrentReader.Variable>(domain: .mfcurrents, lat: lat, lon: lon, elevation: elevation, mode: mode).map { reader -> any GenericReaderProtocol in
MfCurrentReader(reader: GenericReaderCached<MfWaveDomain, MfCurrentReader.Variable>(reader: reader))
}
return [mfsst, mfcurrents].compactMap({$0})
case .ncep_gfswave025:
return try GenericReader<GfsDomain, GfsWaveVariable>(domain: .gfswave025, lat: lat, lon: lon, elevation: elevation, mode: mode).flatMap({[$0]}) ?? []
case .ncep_gefswave025:
return try GenericReader<GfsDomain, GfsWaveVariable>(domain: .gfswave025_ens, lat: lat, lon: lon, elevation: elevation, mode: mode).flatMap({[$0]}) ?? []
case .ncep_gfswave016:
return try GenericReader<GfsDomain, GfsWaveVariable>(domain: .gfswave016, lat: lat, lon: lon, elevation: elevation, mode: mode).flatMap({[$0]}) ?? []
}
}
}
enum MarineVariable: String, GenericVariableMixable {
case wave_height
case wave_period
case wave_direction
case wave_peak_period
case wind_wave_height
case wind_wave_period
case wind_wave_peak_period
case wind_wave_direction
case swell_wave_height
case swell_wave_period
case swell_wave_peak_period
case swell_wave_direction
case ocean_current_velocity
case ocean_current_direction
case sea_level_height_msl
case invert_barometer_height
case sea_surface_temperature
var requiresOffsetCorrectionForMixing: Bool {
return false
}
}
struct IconWaveController {
func query(_ req: Request) async throws -> Response {
_ = try await req.ensureSubdomain("marine-api")
let params = req.method == .POST ? try req.content.decode(ApiQueryParameter.self) : try req.query.decode(ApiQueryParameter.self)
let numberOfLocationsMaximum = try await req.ensureApiKey("marine-api", apikey: params.apikey)
let currentTime = Timestamp.now()
let allowedRange = Timestamp(1940, 1, 1) ..< currentTime.add(86400 * 17)
let prepared = try params.prepareCoordinates(allowTimezones: true)
guard case .coordinates(let prepared) = prepared else {
throw ForecastapiError.generic(message: "Bounding box not supported")
}
let domains = try IconWaveDomainApi.load(commaSeparatedOptional: params.models) ?? [.best_match]
let paramsHourly = try MarineVariable.load(commaSeparatedOptional: params.hourly)
let paramsCurrent = try MarineVariable.load(commaSeparatedOptional: params.current)
let paramsDaily = try IconWaveVariableDaily.load(commaSeparatedOptional: params.daily)
let paramsMinutely = try MarineVariable.load(commaSeparatedOptional: params.minutely_15)
let nParamsMinutely = paramsMinutely?.count ?? 0
let nVariables = ((paramsHourly?.count ?? 0) + (paramsDaily?.count ?? 0) + nParamsMinutely) * domains.reduce(0, {$0 + $1.countEnsembleMember})
let locations: [ForecastapiResult<IconWaveDomainApi>.PerLocation] = try prepared.map { prepared in
let coordinates = prepared.coordinate
let timezone = prepared.timezone
let time = try params.getTimerange2(timezone: timezone, current: currentTime, forecastDaysDefault: 7, forecastDaysMax: 16, startEndDate: prepared.startEndDate, allowedRange: allowedRange, pastDaysMax: 92, forecastDaysMinutely15Default: 7)
let timeLocal = TimerangeLocal(range: time.dailyRead.range, utcOffsetSeconds: timezone.utcOffsetSeconds)
let currentTimeRange = TimerangeDt(start: currentTime.floor(toNearest: 3600), nTime: 1, dtSeconds: 3600)
let readers: [ForecastapiResult<IconWaveDomainApi>.PerModel] = try domains.compactMap { domain in
guard let reader = try GenericReaderMulti<MarineVariable, IconWaveDomainApi>(domain: domain, lat: coordinates.latitude, lon: coordinates.longitude, elevation: .nan, mode: params.cell_selection ?? .sea, options: params.readerOptions) 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)
return .init(
model: domain,
latitude: reader.modelLat,
longitude: reader.modelLon,
elevation: reader.targetElevation,
prefetch: {
if let paramsHourly {
for member in 0..<reader.domain.countEnsembleMember {
try reader.prefetchData(variables: paramsHourly, time: timeHourlyRead.toSettings(ensembleMember: member))
}
}
if let paramsCurrent {
try reader.prefetchData(variables: paramsCurrent, time: currentTimeRange.toSettings())
}
if let paramsDaily {
for member in 0..<reader.domain.countEnsembleMember {
try reader.prefetchData(variables: paramsDaily, time: time.dailyRead.toSettings(ensembleMember: member))
}
}
},
current: paramsCurrent.map { variables in
return {
return .init(name: "current", time: currentTimeRange.range.lowerBound, dtSeconds: currentTimeRange.dtSeconds, columns: try variables.compactMap { variable in
guard let d = try reader.get(variable: variable, time: currentTimeRange.toSettings())?.convertAndRound(params: params) else {
return nil
}
return .init(variable: .surface(variable), 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
var unit: SiUnit? = nil
let allMembers: [ApiArray] = try (0..<reader.domain.countEnsembleMember).compactMap { member in
guard let d = try reader.get(variable: variable, time: timeHourlyRead.toSettings(ensembleMemberLevel: member))?.convertAndRound(params: params) else {
return nil
}
unit = d.unit
assert(timeHourlyRead.count == d.data.count)
return ApiArray.float(d.data)
}
guard allMembers.count > 0 else {
return ApiColumn(variable: .surface(variable), unit: .undefined, variables: .init(repeating: ApiArray.float([Float](repeating: .nan, count: timeHourlyRead.count)), count: reader.domain.countEnsembleMember))
}
return .init(variable: .surface(variable), unit: unit ?? .undefined, variables: allMembers)
})
}
},
daily: paramsDaily.map { paramsDaily in
return {
return ApiSection(name: "daily", time: time.dailyDisplay, columns: try paramsDaily.map { variable -> ApiColumn<IconWaveVariableDaily> in
var unit: SiUnit? = nil
let allMembers: [ApiArray] = try (0..<reader.domain.countEnsembleMember).compactMap { member -> ApiArray? in
guard let d = try reader.getDaily(variable: variable, params: params, time: time.dailyRead.toSettings(ensembleMemberLevel: member))?.convertAndRound(params: params) else {
return nil
}
unit = d.unit
assert(time.dailyRead.count == d.data.count)
return ApiArray.float(d.data)
}
guard allMembers.count > 0 else {
return ApiColumn(variable: variable, unit: .undefined, variables: .init(repeating: ApiArray.float([Float](repeating: .nan, count: time.dailyRead.count)), count: reader.domain.countEnsembleMember))
}
return ApiColumn<IconWaveVariableDaily>(variable: variable, unit: unit ?? .undefined, variables: allMembers)
})
}
},
sixHourly: nil,
minutely15: paramsMinutely.map { variables in
return {
return .init(name: "minutely_15", time: time.minutely15, columns: try variables.map { variable in
var unit: SiUnit? = nil
let allMembers: [ApiArray] = try (0..<reader.domain.countEnsembleMember).compactMap { member in
guard let d = try reader.get(variable: variable, time: time.minutely15.toSettings(ensembleMemberLevel: member))?.convertAndRound(params: params) else {
return nil
}
unit = d.unit
assert(time.minutely15.count == d.data.count)
return ApiArray.float(d.data)
}
guard allMembers.count > 0 else {
return ApiColumn(variable: .surface(variable), unit: .undefined, variables: .init(repeating: ApiArray.float([Float](repeating: .nan, count: time.minutely15.count)), count: reader.domain.countEnsembleMember))
}
return .init(variable: .surface(variable), unit: unit ?? .undefined, variables: allMembers)
})
}
}
)
}
guard !readers.isEmpty else {
throw ForecastapiError.noDataAvilableForThisLocation
}
return .init(timezone: timezone, time: timeLocal, locationId: coordinates.locationId, results: readers)
}
let result = ForecastapiResult<IconWaveDomainApi>(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)
}
}
typealias IconWaveReader = GenericReader<IconWaveDomain, IconWaveVariable>
struct IconWaveMixer: GenericReaderMixer {
let reader: [IconWaveReader]
static func makeReader(domain: IconWaveDomain, lat: Float, lon: Float, elevation: Float, mode: GridSelectionMode, options: GenericReaderOptions) throws -> IconWaveReader? {
return try IconWaveReader(domain: domain, lat: lat, lon: lon, elevation: elevation, mode: mode)
}
}
|