------------------------------------------------------------------ -- VEAF weather information messages and markers -- By Flogas (2024) -- -- Features: -- --------- -- * Generation of weather messages and reports in different formats (METAR, ATIS) -- * Generation of markers on the maps displaying the weather at the location ------------------------------------------------------------------ veafWeather = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global module settings ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafWeather.Id = "WEATHER" --- Version. veafWeather.Version = "1.5.0" -- trace level, specific to this module --veafWeather.LogLevel = "trace" veaf.loggers.new(veafWeather.Id, veafWeather.LogLevel) --- Key phrase to look for in the mark text which triggers the command. veafWeather.Keyphrase = "_weather" veafWeather.RadioMenuName = "WEATHER AND ATC" veafWeather.RemoteCommandParser = "([[a-zA-Z0-9]+)%s?([^%s]*)%s?(.*)" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Local constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- local _dcsPresetDensity = { -- {density, precipitation, visibility} ["Preset1"] = {2, false, nil}, -- LS1 -- FEW/SCT ["Preset2"] = {2, false, nil}, -- LS2 -- FEW/SCT ["Preset3"] = {3, false, nil}, -- HS1 -- SCT ["Preset4"] = {3, false, nil}, -- HS2 -- SCT ["Preset5"] = {3, false, nil}, -- S1 -- SCT ["Preset6"] = {4, false, nil}, -- S2 -- SCT/BKN ["Preset7"] = {3, false, nil}, -- S3 -- BKN ["Preset8"] = {4, false, nil}, -- HS3 -- SCT/BKN ["Preset9"] = {5, false, nil}, -- S4 -- BKN ["Preset10"] = {4, false, nil}, -- S5 -- SCT/BKN ["Preset11"] = {6, false, nil}, -- S6 -- BKN ["Preset12"] = {6, false, nil}, -- S7 -- BKN ["Preset13"] = {6, false, nil}, -- B1 -- BKN ["Preset14"] = {6, false, nil}, -- B2 -- BKN ["Preset15"] = {4, false, nil}, -- B3 -- SCT/BKN ["Preset16"] = {6, false, nil}, -- B4 -- BKN ["Preset17"] = {7, false, nil}, -- B5 -- BKN/OVC ["Preset18"] = {7, false, nil}, -- B6 -- BKN/OVC ["Preset19"] = {8, false, nil}, -- B7 -- OVC ["Preset20"] = {7, false, nil}, -- B8 -- BKN/OVC ["Preset21"] = {7, false, nil}, -- O1 -- BKN/OVC ["Preset22"] = {6, false, nil}, -- O2 -- BKN ["Preset23"] = {6, false, nil}, -- O3 -- BKN ["Preset24"] = {7, false, nil}, -- O4 -- BKN/OVC ["Preset25"] = {8, false, nil}, -- O5 -- OVC ["Preset26"] = {8, false, nil}, -- O6 -- OVC ["Preset27"] = {8, false, nil}, -- O7 -- OVC ["RainyPreset1"] = {8, true, 4000}, -- OR1 -- OVC ["RainyPreset2"] = {7, true, 3000}, -- OR2 -- BKN/OVC ["RainyPreset3"] = {8, true, 4000}, -- OR3 -- OVC ["RainyPreset4"] = {4, true, nil}, -- LR1 -- SCT/BKN ["RainyPreset5"] = {7, true, nil}, -- LR2 -- BKN/OVC ["RainyPreset6"] = {8, true, nil}, -- LR3 -- OVC ["NEWRAINPRESET4"] = {8, true, nil}, -- LR4 -- OVC } local _cloudDensity = { Clear = 0, Few = 1, Scattered = 2, Broken = 3, Overcast = 4 } local _cloudDensityOktas = { [0] = _cloudDensity.Clear, [1] = _cloudDensity.Few, [2] = _cloudDensity.Few, [3] = _cloudDensity.Scattered, [4] = _cloudDensity.Scattered, [5] = _cloudDensity.Broken, [6] = _cloudDensity.Broken, [7] = _cloudDensity.Overcast, [8] = _cloudDensity.Overcast } local _nKelvinToCelciusOffset = -273.15 local _visibilityAffect = { None = 0, Fog = 1, Mist = 2, Haze = 3 } ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Local tools ------------------------------------------------------------------------------------------------------------------------------------------------------------- local function _computeClearSkyHumidity(nLatitude, nLongitude, iDayOfYear) -- Base RH for clear skies local baseRh = 30 -- Seasonal adjustment -- Calculate seasonal factor (-1 to 1, peaks at middle of year) local seasonal_factor = math.cos((iDayOfYear - 182) * 2 * math.pi / 365) -- Latitude adjustment -- Higher latitudes tend to have lower RH in winter, higher in summer local lat_factor = math.abs(nLatitude) / 90 -- Longitude adjustment (crude estimation of continental vs maritime) -- Assume areas around 0°, 180° longitude (typical ocean areas) have higher RH local long_factor = math.min(math.abs(nLongitude), math.abs(nLongitude - 180)) / 180 local maritime_influence = 1 - long_factor -- Combine factors local seasonal_adjustment = seasonal_factor * 15 * lat_factor -- ±15% variation local maritime_adjustment = maritime_influence * 10 -- Up to +10% for maritime areas -- Calculate final RH local adjustedRh = baseRh + seasonal_adjustment + maritime_adjustment -- Clamp between reasonable values return math.max(20, math.min(70, adjustedRh)) end local function _computeHumidity(vec3, iCloudBaseMeters, iVisibilityMeters, bPrecipitations, iAbsTime) local nLatitude, nLongitude, _ = coord.LOtoLL(vec3) local nHumidity if (iCloudBaseMeters == nil or iCloudBaseMeters > 10000) then -- Clear skies - estimate RH based on location and date local dateTime = veafTime.getMissionDateTime(iAbsTime) nHumidity = _computeClearSkyHumidity(nLatitude, nLongitude, dateTime.yday) else -- Convert cloud base to meters and estimate RH nHumidity = 100 - (iCloudBaseMeters / 100) -- Clamp RH between 0 and 100% nHumidity = math.max(0, math.min(100, nHumidity)) end if (iVisibilityMeters < 1000) then nHumidity = 100 -- Fog implies saturation elseif (iVisibilityMeters < 5000) then nHumidity = math.max(nHumidity, 90) -- At least 90% RH with any fog elseif (iVisibilityMeters < 10000) then -- Increase RH as visibility decreases local nVisibilityFactor = math.max(0, (10000 - iVisibilityMeters) / 10000) nHumidity = nHumidity + (nVisibilityFactor * 20) -- Up to +20% for low visibility end -- Precipitation adjustments if (bPrecipitations) then nHumidity = math.max(nHumidity, 80) end -- Clamp final value return math.max(0, math.min(100, nHumidity)) end local function _computeDewpoint(nTemperatureCelcius, nQnhPa, nHumidity) local nQnhHpa = nQnhPa / 100 -- Constants for Magnus formula local a = 17.27 local b = 237.7 -- Calculate gamma term local gamma = ((a * nTemperatureCelcius) / (b + nTemperatureCelcius)) + math.log(nHumidity/100.0) -- Calculate dew point using Magnus formula local nDewPointCelcius = (b * gamma) / (a - gamma) -- Apply pressure correction (approximate) local pressure_correction = (1013.25 - nQnhHpa) * 0.0012 nDewPointCelcius = nDewPointCelcius + pressure_correction nDewPointCelcius = math.min(nDewPointCelcius, nTemperatureCelcius) -- Round to one decimal place return math.floor(nDewPointCelcius * 10 + 0.5) / 10 end local function _weatherSliceAtAltitude(vec3, iAltitudeMeters) local nTemperatureKelvin, nPressurePa = atmosphere.getTemperatureAndPressure({ x = vec3.x, y = iAltitudeMeters, z = vec3.z }) local iWindDir, iWindSpeedMps = weathermark._GetWind(vec3, iAltitudeMeters) return { AltitudeMeters = iAltitudeMeters, PressureHpa = nPressurePa / 100, TemperatureCelcius = nTemperatureKelvin + _nKelvinToCelciusOffset, WindDirection = iWindDir, WindSpeedMps = iWindSpeedMps } end local function _getFlightLevelString(iAltitudeFeet) -- Round to nearest 500 local iAltitudeFeetRounded = math.floor((iAltitudeFeet + 250) / 500) * 500 -- Convert to flight level format (divide by 100) local iFlightLevel = math.floor(iAltitudeFeetRounded / 100) return string.format("FL%03d", iFlightLevel) end --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- Weather measurement unit systems class --- Defines a set of units to be used to display weather data --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- veafWeatherUnitSystem = {} veafWeatherUnitSystem.__index = veafWeatherUnitSystem veafWeatherUnitSystem.Units = { Kts = 0, Mps = 1, M = 2, Sm = 3, Nm = 4, Ft = 5, Hpa = 6, InHg = 7, MmHg = 8 } --------------------------------------------------------------------------------------------------- --- CTOR function veafWeatherUnitSystem:create(windSpeeds, visibilities, altitudes, pressures) local this = { WindSpeeds = windSpeeds, Visibilities = visibilities, Altitudes = altitudes, Pressures = pressures, } setmetatable(this, veafWeatherUnitSystem) return this end --------------------------------------------------------------------------------------------------- --- Static data veafWeatherUnitSystem.Systems = { Full = veafWeatherUnitSystem:create({ veafWeatherUnitSystem.Units.Kts, veafWeatherUnitSystem.Units.Mps }, { veafWeatherUnitSystem.Units.M, veafWeatherUnitSystem.Units.Sm, veafWeatherUnitSystem.Units.Nm }, { veafWeatherUnitSystem.Units.Ft, veafWeatherUnitSystem.Units.M }, { veafWeatherUnitSystem.Units.Hpa, veafWeatherUnitSystem.Units.InHg, veafWeatherUnitSystem.Units.MmHg }), -- all Icao = veafWeatherUnitSystem:create({ veafWeatherUnitSystem.Units.Kts }, { veafWeatherUnitSystem.Units.M }, { veafWeatherUnitSystem.Units.Ft }, { veafWeatherUnitSystem.Units.Hpa }), -- default IcaoMetric = veafWeatherUnitSystem:create({ veafWeatherUnitSystem.Units.Mps }, { veafWeatherUnitSystem.Units.M }, { veafWeatherUnitSystem.Units.Ft }, { veafWeatherUnitSystem.Units.Hpa }), -- for russian airfields Faa = veafWeatherUnitSystem:create({ veafWeatherUnitSystem.Units.Kts }, { veafWeatherUnitSystem.Units.Sm }, { veafWeatherUnitSystem.Units.Ft }, { veafWeatherUnitSystem.Units.InHg }), -- for US aircrafts or airfields, and for older british aircrafts FaaMetric = veafWeatherUnitSystem:create({ veafWeatherUnitSystem.Units.Kts }, { veafWeatherUnitSystem.Units.M }, { veafWeatherUnitSystem.Units.Ft }, { veafWeatherUnitSystem.Units.InHg }), -- for US army helicopters FaaNavy = veafWeatherUnitSystem:create({ veafWeatherUnitSystem.Units.Kts }, { veafWeatherUnitSystem.Units.Nm }, { veafWeatherUnitSystem.Units.Ft }, { veafWeatherUnitSystem.Units.InHg }), -- for US aircraft carriers Metric = veafWeatherUnitSystem:create({ veafWeatherUnitSystem.Units.Mps }, { veafWeatherUnitSystem.Units.M }, { veafWeatherUnitSystem.Units.M }, { veafWeatherUnitSystem.Units.Hpa }), -- for french army helicopters MetricEastern = veafWeatherUnitSystem:create({ veafWeatherUnitSystem.Units.Mps }, { veafWeatherUnitSystem.Units.M }, { veafWeatherUnitSystem.Units.M }, { veafWeatherUnitSystem.Units.MmHg }), -- for russian and chinese aircrafts } veafWeatherUnitSystem.DefaultUnitSystem = veafWeatherUnitSystem.Systems.Icao veafWeatherUnitSystem.Theatres = {} veafWeatherUnitSystem.Theatres.Faa = { veaf.theatreName.Nevada, veaf.theatreName.MarianaIslands } veafWeatherUnitSystem.Theatres.IcaoMetric = { veaf.theatreName.Caucasus } veafWeatherUnitSystem.Aircrafts = {} veafWeatherUnitSystem.Aircrafts.Faa = { "A-10A", "A-10C", "A-10C_2", "AV8BNA", "F-14A-135-GR", "F-14B", "F-15C", "F-15ESE", "F-16C_50", "FA-18C_hornet", "F-4E-45MC", "UH-1H", "P-47D-30", "P-47D-40", "P-51D", "P-51D-30-NA", "TF-51D", "Christen Eagle II", "SpitfireLFMkIX", "MosquitoFBMkVI" } veafWeatherUnitSystem.Aircrafts.Metric = { "SA342L", "SA342M", "SA342Minigun", "SA342Mistral" } veafWeatherUnitSystem.Aircrafts.MetricEastern = { "Ka-50", "Ka-50_3", "Mi-8MTV2", "Mi-24P", "MiG-15bis", "MiG-19P", "MiG-21Bis", "MiG-29S", "Su-25", "Su-25T", "Su-27", "Su-33", "J-11A", "FW-190A8", "FW-190D9", "I-16", "L-39C", "L-39ZA", "Yak-52" } veafWeatherUnitSystem.Aircrafts.FaaMetric = { "AH-64D_BLK_II" } --------------------------------------------------------------------------------------------------- --- Methods --- function veafWeatherUnitSystem.defaultForTypeName(sTypeName) if (veaf.tableContains(veafWeatherUnitSystem.Aircrafts.Faa, sTypeName)) then return veafWeatherUnitSystem.Systems.Faa elseif (veaf.tableContains(veafWeatherUnitSystem.Aircrafts.Metric, sTypeName)) then return veafWeatherUnitSystem.Systems.Metric elseif (veaf.tableContains(veafWeatherUnitSystem.Aircrafts.MetricEastern, sTypeName)) then return veafWeatherUnitSystem.Systems.MetricEastern elseif (veaf.tableContains(veafWeatherUnitSystem.Aircrafts.FaaMetric, sTypeName)) then return veafWeatherUnitSystem.Systems.FaaMetric else return veafWeatherUnitSystem.DefaultUnitSystem end end function veafWeatherUnitSystem.defaultForTheatre() local sTheatre = env.mission.theatre if (veaf.tableContains(veafWeatherUnitSystem.Theatres.Faa, sTheatre)) then return veafWeatherUnitSystem.Systems.Faa elseif (veaf.tableContains(veafWeatherUnitSystem.Theatres.IcaoMetric, sTheatre)) then return veafWeatherUnitSystem.Systems.IcaoMetric else return veafWeatherUnitSystem.DefaultUnitSystem end end --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- Weather management class --- Collects and compile wheather data form various sources in the sim at a location --- Can be output to string as METAR or ATIS informations --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- veafWeatherData = {} veafWeatherData.__index = veafWeatherData --------------------------------------------------------------------------------------------------- --- CTOR function veafWeatherData:create(vec3, iAbsTime, iAltitudeMeters) iAbsTime = iAbsTime or timer.getAbsTime() iAltitudeMeters = iAltitudeMeters or veaf.getLandHeight(vec3) local sunTimesZulu = veafTime.getSunTimesZulu(vec3) local sunTimesLocal = veafTime.getSunTimesLocal(vec3) local iWindDirSurface, iWindSpeedSurfaceMps = weathermark._GetWind(vec3, iAltitudeMeters + 10) -- Measure the wind velocity at the standard height of 10 metres above the surface. This is the internationally accepted meteorological definition of ‘surface wind’ designed to eliminate distortion attributable to very local terrain effects -- the static env.mission.weather.visibility.distance is not used anymore, and the DCS engine apparently sets a default at 100000, as seen in this log line: -- WEATHER (Main): set fog: visibility:100000 thickness:0.0 local iVisibilityMeters = 100000 local clouds = nil local bPrecipitation = false; local sCloudPreset = env.mission.weather.clouds.preset if (veaf.isNullOrEmpty(sCloudPreset)) then if (env.mission.weather.clouds.density > 0) then local iDensity = mist.utils.round(env.mission.weather.clouds.density * 8 / 10) -- 10 levels in dcs, convert to oktas clouds = { Density = iDensity, BaseMeters = env.mission.weather.clouds.base } end bPrecipitation = (env.mission.weather.clouds.iprecptns > 0) else if (_dcsPresetDensity[sCloudPreset]) then clouds = {Density = _dcsPresetDensity[sCloudPreset][1], BaseMeters = env.mission.weather.clouds.base} bPrecipitation = _dcsPresetDensity[sCloudPreset][2] --[[ -- visibilities in presets were already not reliable, and they have no meaning with the new fog visiblity setting if (_dcsPresetDensity[sCloudPreset][3] and _dcsPresetDensity[sCloudPreset][3] < iVisibilityMeters) then iVisibilityMeters = _dcsPresetDensity[sCloudPreset][3] end ]] end end local iFogThicknessMeters = world.weather.getFogThickness() local iFogVisibilityMeters = world.weather.getFogVisibilityDistance() if (iFogThicknessMeters >= iAltitudeMeters) then iVisibilityMeters = iFogVisibilityMeters veaf.loggers.get(veafWeather.Id):trace("Visibility new fog=%d", iVisibilityMeters) else veaf.loggers.get(veafWeather.Id):trace("Visibility=%d. New fog ignored, measure point above fog ceiling [ altitude=%d, fog thickness=%d ]", iVisibilityMeters, iAltitudeMeters, iFogThicknessMeters) end local _, nQfePa = atmosphere.getTemperatureAndPressure({ x = vec3.x, y = iAltitudeMeters, z = vec3.z }) local nTemperatureKelvin, nQnhPa = atmosphere.getTemperatureAndPressure({ x = vec3.x, y = 0, z = vec3.z }) local nTemperatureCelcius = nTemperatureKelvin + _nKelvinToCelciusOffset local nHumidity = _computeHumidity(vec3, clouds.BaseMeters, iVisibilityMeters, bPrecipitation, iAbsTime) local nDewPointCelcius = _computeDewpoint(nTemperatureCelcius, nQnhPa, nHumidity) -- Fog FG or mist BR: fog is less than 1000 meters visibility. Mist BR or haze HZ: if the humidity is more than 80% it is mist. local visibilityAffect = _visibilityAffect.None if (iVisibilityMeters < 1000) then visibilityAffect = _visibilityAffect.Fog elseif (iVisibilityMeters < 5000 and nHumidity >= 80) then visibilityAffect = _visibilityAffect.Mist elseif (iVisibilityMeters < 5000) then visibilityAffect = _visibilityAffect.Haze end local weatherSlices = { } if (iAltitudeMeters < 600) then table.insert(weatherSlices, _weatherSliceAtAltitude(vec3, 500)) end if (iAltitudeMeters < 2100) then table.insert(weatherSlices, _weatherSliceAtAltitude(vec3, 2000)) end if (iAltitudeMeters < 8100) then table.insert(weatherSlices, _weatherSliceAtAltitude(vec3, 8000)) end local this = { AbsTime = iAbsTime, Vec3 = vec3, AltitudeMeter = iAltitudeMeters, WindDirection = iWindDirSurface, WindSpeedMps = iWindSpeedSurfaceMps, VisibilityMeters = iVisibilityMeters, Dust = env.mission.weather.enable_dust, VisibilityAffect = visibilityAffect, Clouds = clouds, Precipitation = bPrecipitation, TemperatureCelcius = nTemperatureCelcius, DewPointCelcius = nDewPointCelcius, QnhHpa = nQnhPa / 100, QfeHpa = nQfePa / 100, SunriseZulu = sunTimesZulu.Sunrise, SunsetZulu = sunTimesZulu.Sunset, SunriseLocal = sunTimesLocal.Sunrise, SunsetLocal = sunTimesLocal.Sunset, WeatherSlices = weatherSlices, } setmetatable(this, veafWeatherData) veaf.loggers.get(veafWeather.Id):trace(this:toString(veafWeatherUnitSystem.Systems.Faa)) veaf.loggers.get(veafWeather.Id):trace(this:toString(veafWeatherUnitSystem.Systems.FaaNavy)) veaf.loggers.get(veafWeather.Id):trace(this:toString(veafWeatherUnitSystem.Systems.Icao)) return this end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Static methods function veafWeatherData.getWeatherString(vec3, dcsElementName, unitSystem) local bWithLaste = false local sTypeName = veaf.getDcsTypeName(dcsElementName) if (unitSystem == nil) then unitSystem = veafWeatherUnitSystem.defaultForTypeName(sTypeName) end if (not veaf.isNullOrEmpty(sTypeName) and veaf.startsWith(sTypeName, "A-10", false)) then bWithLaste = true end local weatherData = veafWeatherData:create(vec3) return weatherData:toString(unitSystem, bWithLaste) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Methods function veafWeatherData:getNormalizedWindDirection(iDirectionTrue, bMagnetic) bMagnetic = bMagnetic or false local iDirection = iDirectionTrue if (bMagnetic) then iDirection = iDirection - veaf.getMagneticDeclination() if (iDirection) < 0 then iDirection = iDirection + 360 end end if (iDirection == 0) then iDirection = 360 end return iDirection end function veafWeatherData:getNormalizedCloudBaseMeters(bHeight) bHeight = bHeight or false if (self.Clouds == nil or self.Clouds.Density <= 0) then return nil else local iCloudBase = self.Clouds.BaseMeters if (bHeight) then iCloudBase = iCloudBase - self.AltitudeMeter end return iCloudBase end end function veafWeatherData:getNormalizedCloudsDensity() if (self.Clouds == nil or self.Clouds.BaseMeters == nil or self.Clouds.Density <= 0) then return _cloudDensity.Clear else return _cloudDensityOktas[self.Clouds.Density] end end function veafWeatherData:isCavok() local iCloudHeightMeters = self:getNormalizedCloudBaseMeters(true) if (iCloudHeightMeters == nil or mist.utils.metersToFeet(iCloudHeightMeters) < 5000) then return false -- no clouds or cloud below 5000 ft else return (self.VisibilityMeters >= 10000 and not self.Precipitation and not self.Dust) end end function veafWeatherData:getCarrierCase() -- Case I departures are flown during the day when weather conditions allow departure under visual flight rules (VFR). The weather minimums are a cloud deck above 3,000 feet and visibility greater than 5 miles -- Case II departures are flown during the day when visual conditions are present at the carrier, but a controlled climb through the clouds is required. The weather minimums are a cloud deck above 1,000 feet and visibility greater than 5 miles. -- Case III departures are flown at night and when weather conditions are below the minimums of 1,000 feet cloud deck and 5 miles visibility local bNight = veafTime.isAeronauticalNight(self.Vec3, self.AbsTime) if (bNight) then return 3 end local iCloudBase = nil if (self.Clouds and self.Clouds.Density > 4) then iCloudBase = self.Clouds.BaseMeters end local iVisibilityCase12 = mist.utils.NMToMeters(5) local iCloudBaseCase1 = mist.utils.feetToMeters(3000) local iCloudBaseCase2 = mist.utils.feetToMeters(1000) --veaf.loggers.get(veaf.Id):trace(string.format("GetCarrierCase - Cloud base=%d feet (need more than 1000 for CASE 2 and 300 for CASE 3) - visibility=%d nm (need more than 5 for CASE 1/2)", iCloudBase or -1, UTILS.MetersToNM (self.VisibilityMeters))) if (self.VisibilityMeters > iVisibilityCase12 and (iCloudBase == nil or iCloudBase > iCloudBaseCase1)) then return 1 elseif (self.VisibilityMeters > iVisibilityCase12 and (iCloudBase == nil or iCloudBase > iCloudBaseCase2)) then return 2 else return 3 end end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToStrings function veafWeatherData:appendString(s, sAppend) sAppend = sAppend or "" if (veaf.isNullOrEmpty(s)) then return sAppend elseif (veaf.isNullOrEmpty(sAppend)) then return s else return s .. "|" .. sAppend end end function veafWeatherData:toStringWind(unitSystem, iDirection, nSpeedMps, bMagnetic) unitSystem = unitSystem or veafWeatherUnitSystem.DefaultUnitSystem bMagnetic = bMagnetic or false if (nSpeedMps <= 0.5) then return "calm" end local iDirection = self:getNormalizedWindDirection(iDirection, bMagnetic) local sSpeedKts = string.format("%dkts", mist.utils.mpsToKnots(nSpeedMps)) local sSpeedMps = string.format("%dm/s", nSpeedMps) local sSpeed if(veaf.tableContains(unitSystem.WindSpeeds, veafWeatherUnitSystem.Units.Kts)) then sSpeed = veafWeatherData:appendString(sSpeed, sSpeedKts) end if(veaf.tableContains(unitSystem.WindSpeeds, veafWeatherUnitSystem.Units.Mps)) then sSpeed = veafWeatherData:appendString(sSpeed, sSpeedMps) end local sDegrees if (bMagnetic) then sDegrees = "°M" else sDegrees = "°T" end return string.format("%03d%s @ %s", iDirection, sDegrees, sSpeed) end function veafWeatherData:toStringVisibility(unitSystem, bWithMax) unitSystem = unitSystem or veafWeatherUnitSystem.DefaultUnitSystem local sVisibilityMeters if (self.VisibilityMeters >= 10000) then sVisibilityMeters = "10+km" else local iVisibilityMeters if (self.VisibilityMeters >= 100) then iVisibilityMeters = mist.utils.round(self.VisibilityMeters / 100) * 100 else iVisibilityMeters = mist.utils.round(self.VisibilityMeters / 50) * 50 end sVisibilityMeters = string.format("%dm", iVisibilityMeters) end local sVisibilityStatuteMile local iVisibilityStatuteMile = self.VisibilityMeters * 0.000621371 if (iVisibilityStatuteMile >= 10) then sVisibilityStatuteMile = "10+SM" elseif (iVisibilityStatuteMile >= 1) then sVisibilityStatuteMile = string.format("%dSM", mist.utils.round(iVisibilityStatuteMile)) elseif (iVisibilityStatuteMile >= 0.75) then sVisibilityStatuteMile = "3/4SM" elseif (iVisibilityStatuteMile >= 0.5) then sVisibilityStatuteMile = "1/2SM" elseif (iVisibilityStatuteMile >= 0.25) then sVisibilityStatuteMile = "1/4SM" else sVisibilityStatuteMile = "0SM" end local sVisibilityNauticalMile local iVisibilityNauticalMile = mist.utils.metersToNM(self.VisibilityMeters) if (iVisibilityNauticalMile >= 10) then sVisibilityNauticalMile = "10+NM" elseif (iVisibilityNauticalMile >= 1) then sVisibilityNauticalMile = string.format("%dNM", mist.utils.round(iVisibilityNauticalMile)) else local iVisibilityYards = mist.utils.round((iVisibilityNauticalMile * 2025.37) / 100) * 100 sVisibilityNauticalMile = string.format("%dyds", iVisibilityYards) end local sVisibility if(veaf.tableContains(unitSystem.Visibilities, veafWeatherUnitSystem.Units.M)) then sVisibility = veafWeatherData:appendString(sVisibility, sVisibilityMeters) end if(veaf.tableContains(unitSystem.Visibilities, veafWeatherUnitSystem.Units.Sm)) then sVisibility = veafWeatherData:appendString(sVisibility, sVisibilityStatuteMile) end if(veaf.tableContains(unitSystem.Visibilities, veafWeatherUnitSystem.Units.Nm)) then sVisibility = veafWeatherData:appendString(sVisibility, sVisibilityNauticalMile) end if (self.VisibilityAffect == _visibilityAffect.Fog) then sVisibility = sVisibility .. " - fog" elseif (self.VisibilityAffect == _visibilityAffect.Haze) then sVisibility = sVisibility .. " - haze" elseif (self.VisibilityAffect == _visibilityAffect.Mist) then sVisibility = sVisibility .. " - mist" end if (self.Dust) then sVisibility = sVisibility .. " - dust" end if (self.Precipitation) then sVisibility = sVisibility .. " - precipitations" end return sVisibility end function veafWeatherData:toStringClouds(unitSystem, bHeight) unitSystem = unitSystem or veafWeatherUnitSystem.DefaultUnitSystem bHeight = bHeight or false local cloudDensity = self:getNormalizedCloudsDensity() local iCloudBaseMeters = self:getNormalizedCloudBaseMeters(bHeight) local sCloudDensity = "" local sCloudBase = "" if (cloudDensity == _cloudDensity.Clear) then sCloudDensity = "No clouds" else if (cloudDensity == _cloudDensity.Scattered) then sCloudDensity = "Scattered clouds" elseif (cloudDensity == _cloudDensity.Broken) then sCloudDensity = "Broken clouds" elseif (cloudDensity == _cloudDensity.Overcast) then sCloudDensity = "Overcast clouds" else sCloudDensity = "Few clouds" end if (iCloudBaseMeters ~= nil and iCloudBaseMeters > 0) then local iCloudBaseFeet = math.floor((mist.utils.metersToFeet(iCloudBaseMeters) + 250) / 500) * 500 local iCloudBaseMeters = math.floor((iCloudBaseMeters + 250) / 500) * 500 local sCloudBaseFeet = string.format("%dft", iCloudBaseFeet) local sCloudBaseMeters = string.format("%dm", iCloudBaseMeters) if(veaf.tableContains(unitSystem.Altitudes, veafWeatherUnitSystem.Units.Ft)) then sCloudBase = veafWeatherData:appendString(sCloudBase, sCloudBaseFeet) end if(veaf.tableContains(unitSystem.Altitudes, veafWeatherUnitSystem.Units.M)) then sCloudBase = veafWeatherData:appendString(sCloudBase, sCloudBaseMeters) end sCloudBase = string.format(" @ %s", sCloudBase) if (bHeight) then sCloudBase = sCloudBase .. " AGL" else sCloudBase = sCloudBase .. " ASL" end end end return string.format("%s%s", sCloudDensity, sCloudBase) end function veafWeatherData:toStringTemperature(nTemperatureCelcius) return string.format("%d°C", mist.utils.round(nTemperatureCelcius)) end function veafWeatherData:toStringPressure(unitSystem, nPressureHpa) unitSystem = unitSystem or veafWeatherUnitSystem.DefaultUnitSystem local sPressureHpa = string.format("%.0fHpa", nPressureHpa) local sPressureInHg = string.format("%.2finHg", mist.utils.converter("hpa", "inhg", nPressureHpa)) local sPressureMmHg = string.format("%.0fmmHg", nPressureHpa * 0.75006375541921) -- mist convert has the wrong coefficient for hpa to mmHg local sPressure if(veaf.tableContains(unitSystem.Pressures, veafWeatherUnitSystem.Units.Hpa)) then sPressure = veafWeatherData:appendString(sPressure, sPressureHpa) end if(veaf.tableContains(unitSystem.Pressures, veafWeatherUnitSystem.Units.InHg)) then sPressure = veafWeatherData:appendString(sPressure, sPressureInHg) end if(veaf.tableContains(unitSystem.Pressures, veafWeatherUnitSystem.Units.MmHg)) then sPressure = veafWeatherData:appendString(sPressure, sPressureMmHg) end return sPressure end function veafWeatherData:toStringSunTime(dateTimeZulu, bZulu, bLocal) local sLocal = "" if (bLocal) then local dateTimeLocal = veafTime.toLocal(dateTimeZulu) sLocal = string.format("%sL", veafTime.toStringTime(dateTimeLocal, false)) end local sZulu = "" if (bZulu) then sZulu = string.format("%sZ", veafTime.toStringTime(dateTimeZulu, false)) end if (bLocal and bZulu) then return string.format("%s - %s", sZulu, sLocal) elseif (bLocal) then return sLocal else return sZulu end end function veafWeatherData:toStringSlice(weatherSlice, unitSystem, bMagnetic) unitSystem = unitSystem or veafWeatherUnitSystem.DefaultUnitSystem bMagnetic = bMagnetic or false local sAltitudeMeters = string.format("%dm", weatherSlice.AltitudeMeters) local sAltitudeFl = _getFlightLevelString(mist.utils.metersToFeet(weatherSlice.AltitudeMeters)) local sAltitude if(veaf.tableContains(unitSystem.Altitudes, veafWeatherUnitSystem.Units.Ft)) then sAltitude = veafWeatherData:appendString(sAltitude, sAltitudeFl) end if(veaf.tableContains(unitSystem.Altitudes, veafWeatherUnitSystem.Units.M)) then sAltitude = veafWeatherData:appendString(sAltitude, sAltitudeMeters) end local sTemperature = self:toStringTemperature(weatherSlice.TemperatureCelcius) local sPressure = self:toStringPressure(unitSystem, weatherSlice.PressureHpa) local sWind = self:toStringWind(unitSystem, weatherSlice.WindDirection, weatherSlice.WindSpeedMps, bMagnetic) return string.format("%s: wind %s ; %s", sAltitude, sWind, sTemperature) end function veafWeatherData:toStringLaste() local function _getLasteAt(iDesiredHeightFeet) local iAltitudeFeet = math.floor((mist.utils.metersToFeet(self.AltitudeMeter) + iDesiredHeightFeet + 500) / 1000) * 1000 local iAltitudeMeters = mist.utils.feetToMeters(iAltitudeFeet) local iTemperatureKelvin, _ = atmosphere.getTemperatureAndPressure({ x = self.Vec3.x, y = iAltitudeMeters, z = self.Vec3.z }) local iWindDirection, iWindSpeedMps = weathermark._GetWind(self.Vec3, iAltitudeMeters) local iWindDirectionMagnetic = veafWeatherData:getNormalizedWindDirection(iWindDirection, true) local sLaste = string.format("ALT%02d W%03d/%02d T%+d", iAltitudeFeet / 1000, iWindDirectionMagnetic, mist.utils.mpsToKnots(iWindSpeedMps), iTemperatureKelvin + _nKelvinToCelciusOffset) veaf.loggers.get(veafWeather.Id):trace(string.format("LASTE @ %f - W%dM %dT", iAltitudeFeet, iWindDirectionMagnetic, iWindDirection)) veaf.loggers.get(veafWeather.Id):trace(sLaste) return sLaste end local sLaste = "" sLaste = sLaste .. string.format("\n%s", _getLasteAt(2000)) sLaste = sLaste .. string.format("\n%s", _getLasteAt(8000)) sLaste = sLaste .. string.format("\n%s", _getLasteAt(16000)) --sLaste = sLaste .. string.format("\n%s", _getLasteAt(28000)) return sLaste end function veafWeatherData:toString(unitSystem, bWithLaste) unitSystem = unitSystem or veafWeatherUnitSystem.DefaultUnitSystem bWithLaste = bWithLaste or false local sString = "" sString = sString .. string.format("Wind: %s", self:toStringWind(unitSystem, self.WindDirection, self.WindSpeedMps)) sString = sString .. "\n" sString = sString .. string.format("\nVisibility: %s", self:toStringVisibility(unitSystem)) sString = sString .. string.format("\nClouds: %s", self:toStringClouds(unitSystem, true)) sString = sString .. "\n" sString = sString .. string.format("\nTemperature: %s - Dew point: %s", self:toStringTemperature(self.TemperatureCelcius), self:toStringTemperature(self.DewPointCelcius)) sString = sString .. string.format("\nQNH: %s", self:toStringPressure(unitSystem, self.QnhHpa)) sString = sString .. string.format("\nQFE: %s", self:toStringPressure(unitSystem, self.QfeHpa)) sString = sString .. string.format("\nSunrise: %s", self:toStringSunTime(self.SunriseZulu, true, true)) sString = sString .. string.format("\nSunset: %s", self:toStringSunTime(self.SunsetZulu, true, true)) sString = sString .. "\n" if(bWithLaste) then sString = sString .. string.format("\nLASTE:%s", self:toStringLaste()) else for _, weatherSlice in pairs(self.WeatherSlices) do sString = sString .. string.format("\n @ %s", self:toStringSlice(weatherSlice, unitSystem)) end end return sString end function veafWeatherData:toStringExtended(unitSystem, bHeight) unitSystem = unitSystem or veafWeatherUnitSystem.DefaultUnitSystem local sAltitudeFeet = string.format("%dft", mist.utils.round(mist.utils.metersToFeet(self.AltitudeMeter))) local sAltitudeMeters = string.format("%dm", mist.utils.round(self.AltitudeMeter)) local sAltitude if(veaf.tableContains(unitSystem.Altitudes, veafWeatherUnitSystem.Units.Ft)) then sAltitude = veafWeatherData:appendString(sAltitude, sAltitudeFeet) end if(veaf.tableContains(unitSystem.Altitudes, veafWeatherUnitSystem.Units.M)) then sAltitude = veafWeatherData:appendString(sAltitude, sAltitudeMeters) end local nLatitude, nLongitude = coord.LOtoLL(self.Vec3) local sString = "" sString = sString .. string.format("Time: %s", veafTime.absTimeToStringDateTime(self.AbsTime)) sString = sString .. string.format("\nLocation: %s", mist.tostringLL(nLatitude, nLongitude, 0, true)) sString = sString .. string.format("\nAltitude: %s", sAltitude) sString = sString .. "\n\n" .. self:toString(unitSystem, bHeight) return sString end function veafWeatherData:toStringAtis(unitSystem) unitSystem = unitSystem or veafWeatherUnitSystem.DefaultUnitSystem local sAtis = "" sAtis = sAtis .. string.format("Wind %s", self:toStringWind(unitSystem, self.WindDirection, self.WindSpeedMps, true)) if(self:isCavok()) then sAtis = sAtis .. "\nCeiling and visiblity OK, CAVOK" else sAtis = sAtis .. string.format("\nVisibility %s, %s", self:toStringVisibility(unitSystem), self:toStringClouds(unitSystem, true)) end sAtis = sAtis .. string.format("\nTemperature %s, dew point %s", self:toStringTemperature(self.TemperatureCelcius), self:toStringTemperature(self.DewPointCelcius)) sAtis = sAtis .. string.format("\nQNH %s", self:toStringPressure(unitSystem, self.QnhHpa)) if(veafTime.isAeronauticalNight(self.Vec3, self.AbsTime)) then sAtis = sAtis .. string.format("\nSunrise %s", self:toStringSunTime(self.SunriseZulu, true, false)) else sAtis = sAtis .. string.format("\nSunset %s", self:toStringSunTime(self.SunsetZulu, true, false)) end return sAtis end --[[ function FgWeather:ToStringMetar() local iWindForce, iWindDirection = self:GetFormattedWind() local sWind if (iWindForce < 1) then sWind = "00000KT" else iWindDirection = UTILS.Round(iWindDirection / 10) * 10 if (iWindDirection == 0) then iWindDirection = 360 end sWind = string.format("%03d%02dKT", iWindDirection, iWindForce) end local iVisibility = UTILS.Round(self.VisibilityMeters / 100) * 100 if (iVisibility >= 10000) then iVisibility = 9999 end local sVisibility = string.format("%04d", iVisibility) local sSignificativeWeather = nil if (self.Precipitation) then sSignificativeWeather = "RA" -- TODO rain will be snow if season+map+t° ? end if (self.Fog) then sSignificativeWeather = Fg.AppendWithSeparator(sSignificativeWeather, "FG") end if (self.Dust) then sSignificativeWeather = Fg.AppendWithSeparator(sSignificativeWeather, "DU") end sVisibility = Fg.AppendWithSeparator(sVisibility, sSignificativeWeather) local sClouds local cloudDensity, iCloudBase = self:GetFormattedClouds(true) if (cloudDensity == CloudDensityLabel.Clear) then sClouds = "SKC" elseif (cloudDensity == CloudDensityLabel.Cavok) then sClouds = "CAVOK" sVisibility = nil else local sDensity = "FEW" if (cloudDensity == CloudDensityLabel.Scattered) then sDensity = "SCT" elseif (cloudDensity == CloudDensityLabel.Broken) then sDensity = "BKN" elseif (cloudDensity == CloudDensityLabel.Overcast) then sDensity = "OVC" end sClouds = string.format("%s%03d", sDensity, iCloudBase) end local sTemperature local iTemperature = UTILS.Round(self.TemperatureCelcius) if (iTemperature >= 0) then sTemperature = string.format("%02d", iTemperature) else sTemperature = string.format("M%02d", -iTemperature) end local sQnh = string.format("Q%d/%.2f", self.QnhHpa, UTILS.hPa2inHg(self.QnhHpa)) local sMetar = Fg.TimeToStringMetar() sMetar = sMetar .. " " .. sWind sMetar = Fg.AppendWithSeparator (sMetar, sVisibility, " ") sMetar = sMetar .. " " .. sClouds sMetar = sMetar .. " " .. sTemperature sMetar = sMetar .. " " .. sQnh return sMetar end function FgWeather.CreateMetarMark(mooseCoord, mooseGroup) local weather = FgWeather:Create(mooseCoord) local sMetar = weather:ToStringMetar() local vec3 = mooseCoord:GetVec3() local iMarkId = UTILS.GetMarkID() if (mooseGroup) then trigger.action.markToGroup(iMarkId, sMetar, vec3, mooseGroup:GetDCSObject():getID(), false, nil) else trigger.action.markToAll(iMarkId, sMetar, vec3, false, nil) end return iMarkId end ]] --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- ATIS management class --- Simulation of the recording of an ATIS information per hour per airfield --- For each info a recording time and corresponding letter is generated (just to fluff it) --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- veafWeatherAtis = {} veafWeatherAtis.__index = veafWeatherAtis veafWeatherAtis.ListInEffect = {} --------------------------------------------------------------------------------------------------- --- CTORS function veafWeatherAtis:Create(veafAirbase, dateTimeZulu) local iHoursSinceMidnight = dateTimeZulu.hour local sLetter = string.char(math.floor(iHoursSinceMidnight) + string.byte("A")) local iRecordedAtMinutes = math.random(2, 11) -- ATIS recorded between h:02 and hour:11 if (iRecordedAtMinutes > dateTimeZulu.min) then -- if record is in the future set recording at the request time iRecordedAtMinutes = dateTimeZulu.min end dateTimeZulu.min = iRecordedAtMinutes local iAltitude = nil local unitSystem if (veafAirbase.Category == Airbase.Category.SHIP) then -- Maybe use the type name to decide the unit system? --[[ local dcsShip = dcsAirbase:getUnit() local dcsShipType = dcsShip:getTypeName() veaf.loggers.get(veafWeather.Id):trace(veaf.p(dcsShipType)) ]] iAltitude = 20 unitSystem = veafWeatherUnitSystem.Systems.FaaNavy else unitSystem = veafWeatherUnitSystem.defaultForTheatre() end local weatherData = veafWeatherData:create(veafAirbase.DcsAirbase:getPoint(), nil, iAltitude) local sMessage if (veafAirbase.Category == Airbase.Category.SHIP) then sMessage = string.format("%s information at %sZ", veafAirbase.DisplayName, veafTime.toStringTime(dateTimeZulu, false)) local iCarrierCase = weatherData:getCarrierCase() if (iCarrierCase) then local sCaseString = nil if (iCarrierCase == 1) then sCaseString = "I" elseif (iCarrierCase == 2) then sCaseString = "II" elseif (iCarrierCase == 3) then sCaseString = "III" end if (not veaf.isNullOrEmpty(sCaseString)) then sMessage = sMessage .. string.format("\nProbable CASE %s in effect", sCaseString) end end elseif (veafAirbase.Category == Airbase.Category.HELIPAD) then sMessage = string.format("%s information at %sZ", veafAirbase.DisplayName, veafTime.toStringTime(dateTimeZulu, false)) else sMessage = string.format("%s information %s, recorded at %sZ", veafAirbase.Name, sLetter, veafTime.toStringTime(dateTimeZulu, false)) local sRunwayInService = veafAirbase:getRunwayInServiceString(weatherData.WindDirection) if (not veaf.isNullOrEmpty(sRunwayInService)) then sMessage = sMessage .. string.format("\nRecommended runway %s", sRunwayInService) end end sMessage = sMessage .. "\n" .. weatherData:toStringAtis(unitSystem) --sMessage = sMessage .. "\n" .. weatherData:toStringExtended() local this = { AirbaseName = veafAirbase.Name, Letter = sLetter, DateTimeZulu = dateTimeZulu, Message = sMessage } setmetatable(this, self) return this end --------------------------------------------------------------------------------------------------- --- Methods --------------------------------------------------------------------------------------------------- --- Static methods function veafWeatherAtis.getAtis(veafAirbase) local iAbsTime = timer.getAbsTime() local dateTime = veafTime.absTimeToDateTime(iAbsTime) local dateTimeZulu = veafTime.toZulu(dateTime) veaf.loggers.get(veafWeather.Id):trace(string.format("Preparing ATIS for airbase %s at %sZ", veafAirbase.Name, veafTime.toStringTime(dateTimeZulu, false))) local atisInEffect = veafWeatherAtis.ListInEffect[veafAirbase.Name] if (atisInEffect) then veaf.loggers.get(veafWeather.Id):trace(string.format("ATIS in effect: %s %s", atisInEffect.Letter, veafTime.toStringTime(atisInEffect.DateTimeZulu, false))) if (dateTimeZulu.year > atisInEffect.DateTimeZulu.year or dateTimeZulu.month > atisInEffect.DateTimeZulu.month or dateTimeZulu.day > atisInEffect.DateTimeZulu.day or dateTimeZulu.hour > atisInEffect.DateTimeZulu.hour) then -- if current date is in the next hour of more from the current one, declare new atis veaf.loggers.get(veafWeather.Id):trace(string.format("Current time %s: new ATIS", veafTime.toStringTime(dateTimeZulu, false))) atisInEffect = nil end end if (atisInEffect == nil) then atisInEffect = veafWeatherAtis:Create(veafAirbase, dateTimeZulu) veaf.loggers.get(veafWeather.Id):trace(string.format("New ATIS in effect for airbase %s: %s %s", veafAirbase.Name, atisInEffect.Letter, veafTime.toStringTime(atisInEffect.DateTimeZulu, false))) veafWeatherAtis.ListInEffect[veafAirbase.Name] = atisInEffect end return atisInEffect end function veafWeatherAtis.getAtisString(veafAirbase) local atisInEffect = veafWeatherAtis.getAtis(veafAirbase) return atisInEffect.Message end function veafWeatherAtis.getAtisStringFromVeafPoint(sPointName, iAbsTime) if (veaf.isNullOrEmpty(sPointName)) then veaf.loggers.get(veafWeather.Id):error("No point name") return "No point name" end local dcsAirbase = veaf.findDcsAirbase(sPointName) local veafAirbase = veafAirbases.getAirbaseFromDcsAirbase(dcsAirbase) if (veafAirbase == nil) then veaf.loggers.get(veafWeather.Id):error("Airbase not found for point " .. sPointName) return "Airbase not found for point " .. sPointName end veaf.loggers.get(veafWeather.Id):trace("Airbase found from veaf point named %s: %s",veaf.p(sPointName), veaf.p(veaf.ifnn(dcsAirbase, "getName"))) return veafWeatherAtis.getAtisString(veafAirbase, iAbsTime) end function veafWeather.messageWeatherAtClosestPoint(unitName, forUnit) veaf.loggers.get(veafWeather.Id):debug("veafWeather.messageWeatherAtClosestPoint(unitName=%s)",veaf.p(unitName)) local closestPoint = veafNamedPoints.getNearestPoint(unitName) if closestPoint then local BR = veafNamedPoints.getPointBearing({closestPoint.name, unitName}) if BR then BR = " ("..BR..")" else BR = "" end local weatherReport = "WEATHER : " .. closestPoint.name .. BR .. "\n\n" weatherReport = weatherReport .. veafWeatherData.getWeatherString(closestPoint, unitName) if forUnit then veaf.outTextForUnit(unitName, weatherReport, 30) else veaf.outTextForGroup(unitName, weatherReport, 30) end end end function veafWeather.messageAtcClosestAirbase(unitName, forUnit) local dcsUnit = Unit.getByName(unitName) local veafAirbase = veafAirbases.getNearestAirbase(dcsUnit) if (veafAirbase) then local sAtcReport = veafWeatherAtis.getAtisString(veafAirbase) if forUnit then veaf.outTextForUnit(dcsUnit:getName(), sAtcReport, 30) else veaf.outTextForGroup(dcsUnit:getName(), sAtcReport, 30) end end end function veafWeather.messageAtcAndWeather(unitName, forUnit) veafWeather.messageAtcClosestAirbase(unitName, forUnit) veafWeather.messageWeatherAtClosestPoint(unitName, forUnit) end ---------------------------------------------------------------------------------------------------- --- WEATHER modifications during runtime ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafFog class methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafFog = {} VeafFog.DELAY_BETWEEN_DYNAMIC_CHECKS = 5 * 60 VeafFog.DYNAMICFOG_BASEFACTOR_HEAVY = 0.8 VeafFog.DYNAMICFOG_BASEFACTOR_MEDIUM = 0.5 VeafFog.DYNAMICFOG_BASEFACTOR_SPARSE = 0.2 function VeafFog.init(object) -- technical name object.name = nil -- scheduled function that is used to update the object object.dynamicCheckFunctionScheduled = nil -- if true, the object is enabled and the fog settings (static and/or dynamic) are applied object.enabled = false -- the dynamic fog parameters for this object object.fogAnimationData = {} -- the static fog parameters for this object object.fogStaticData = {visibility = 10000, thickness = 150} -- the static fog parameters saved by this object (stored state before it changes them) object.savedFogStaticData = nil -- if set to a base fog factor (between 0 and 1), the fog will be dynamically computed based on latitude, season and time of day object.dynamicFogBaseFactor = nil -- if true, the dynamic fog system will animate the transitions object.dynamicFogIsAnimated = true end function VeafFog:new(objectToCopy) veaf.loggers.get(veafWeather.Id):debug("VeafFog:new()") local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object VeafFog.init(objectToCreate) return objectToCreate end function VeafFog:activate() veaf.loggers.get(veafWeather.Id):debug("VeafFog[%s]:activate()", veaf.p(self.name)) veafWeather.setAndActivateFog(self) end function VeafFog:enable() veaf.loggers.get(veafWeather.Id):debug("VeafFog[%s]:enable()", veaf.p(self.name)) self.enabled = true -- store the existing fog parameters veaf.loggers.get(veafWeather.Id):trace("store the existing fog parameters") veaf.loggers.get(veafWeather.Id):trace("world.weather.getFogVisibilityDistance()=[%s]", veaf.p(world.weather.getFogVisibilityDistance())) veaf.loggers.get(veafWeather.Id):trace("world.weather.getFogThickness()=[%s]", veaf.p(world.weather.getFogThickness())) self.fogSavedStaticData = {visibility = world.weather.getFogVisibilityDistance(), thickness = world.weather.getFogThickness()} -- set the fog to the programmed parameters veaf.loggers.get(veafWeather.Id):trace("set the fog to the programmed parameters") if self.forAnimationData then veaf.loggers.get(veafWeather.Id):trace("self.forAnimationData=[%s]", veaf.p(self.forAnimationData)) -- create an animation local animation = { self.forAnimationData } -- first reset fog animation veaf.loggers.get(veafWeather.Id):trace("first reset fog animation") world.weather.setFogAnimation({}) veaf.loggers.get(veafWeather.Id):trace("store the existing fog parameters") veaf.loggers.get(veafWeather.Id):trace("world.weather.getFogVisibilityDistance()=[%s]", veaf.p(world.weather.getFogVisibilityDistance())) veaf.loggers.get(veafWeather.Id):trace("world.weather.getFogThickness()=[%s]", veaf.p(world.weather.getFogThickness())) -- set the new fog animation world.weather.setFogAnimation(animation) elseif self.fogStaticData then veaf.loggers.get(veafWeather.Id):trace("self.fogStaticData=[%s]", veaf.p(self.fogStaticData)) world.weather.setFogThickness(self.fogStaticData.thickness) world.weather.setFogVisibilityDistance(self.fogStaticData.visibility) end -- do the first check, the method will reschedule itself veaf.loggers.get(veafWeather.Id):trace("do the first check, the method will reschedule itself") self:dynamicCheck() return self end function VeafFog:disable(dontRestore) veaf.loggers.get(veafWeather.Id):debug("VeafFog[%s]:disable()", veaf.p(self.name)) -- disable the scheduler if self.dynamicCheckFunctionScheduled then veaf.loggers.get(veafWeather.Id):trace("disable the scheduler") mist.removeFunction(self.dynamicCheckFunctionScheduled) self.dynamicCheckFunctionScheduled = nil end self.enabled = false if not dontRestore then -- reset to the fog values stored at start veaf.loggers.get(veafWeather.Id):trace("reset to the fog values stored at start") if self.fogSavedStaticData then world.weather.setFogThickness(self.fogSavedStaticData.thickness) world.weather.setFogVisibilityDistance(self.fogSavedStaticData.visibility) end end return self end function VeafFog:dynamicCheck() veaf.loggers.get(veafWeather.Id):debug("VeafFog[%s]:dynamicCheck()", veaf.p(self.name)) if self.dynamicFogBaseFactor then local position = {x=0, y=0, z=0} -- somewhere in the map ^^ -- compute the fog that should be set at this moment in time local date = veafTime.getMissionDateTime() veaf.loggers.get(veafWeather.Id):trace("date=[%s]", veaf.p(date)) -- Seasonal adjustment based on latitude and time of year local latitude, _, _ = coord.LOtoLL(position) veaf.loggers.get(veafWeather.Id):trace("latitude=[%s]", veaf.p(latitude)) local month = date.month local seasonal_peaks = { [3] = 0.8, [4] = 0.9, [5] = 0.7, [9] = 0.8, [10] = 0.9, [11] = 0.7 } local season_factor = seasonal_peaks[month] or 0.5 -- Temperature-dew point difference local weatherData = veafWeatherData:create(position) local temp_diff = math.abs(weatherData.TemperatureCelcius - weatherData.DewPointCelcius) -- Diurnal adjustment based on hour local diurnal_factor = 0 -- Convert sunrise and sunset times to minutes since midnight local sunrise_time = weatherData.SunriseLocal.hour * 60 + weatherData.SunriseLocal.min local sunset_time = weatherData.SunsetLocal.hour * 60 + weatherData.SunsetLocal.min -- Calculate daylight duration local daylight_duration = sunset_time - sunrise_time -- Morning is from sunrise and lasts 25% of daylight local morningEnd_time = sunrise_time + 0.25 * daylight_duration local morningPeak_time = sunrise_time + 0.125 * daylight_duration -- Evening start is 25% before sunset local eveningStart_time = sunset_time - 0.25 * daylight_duration local eveningPeak_time = sunset_time - 0.125 * daylight_duration -- Convert current time to minutes since midnight local current_time = date.hour * 60 + date.min veaf.loggers.get(veafWeather.Id):trace("sunrise_time=[%s:%s]", veaf.p(math.floor(sunrise_time/60)), veaf.p(math.fmod(sunrise_time,60))) veaf.loggers.get(veafWeather.Id):trace("morningPeak_time=[%s:%s]", veaf.p(math.floor(morningPeak_time/60)), veaf.p(math.fmod(morningPeak_time,60))) veaf.loggers.get(veafWeather.Id):trace("morningEnd_time=[%s:%s]", veaf.p(math.floor(morningEnd_time/60)), veaf.p(math.fmod(morningEnd_time,60))) veaf.loggers.get(veafWeather.Id):trace("eveningStart_time=[%s:%s]", veaf.p(math.floor(eveningStart_time/60)), veaf.p(math.fmod(eveningStart_time,60))) veaf.loggers.get(veafWeather.Id):trace("eveningPeak_time=[%s:%s]", veaf.p(math.floor(eveningPeak_time/60)), veaf.p(math.fmod(eveningPeak_time,60))) veaf.loggers.get(veafWeather.Id):trace("sunset_time=[%s:%s]", veaf.p(math.floor(sunset_time/60)), veaf.p(math.fmod(sunset_time,60))) veaf.loggers.get(veafWeather.Id):trace("daylight_duration=[%s]", veaf.p(daylight_duration)) veaf.loggers.get(veafWeather.Id):trace("current_time=[%s:%s]", veaf.p(math.floor(current_time/60)), veaf.p(math.fmod(current_time,60))) if current_time >= sunrise_time and current_time < morningPeak_time then -- Phase 1: From sunrise to middle of the morning (raise) veaf.loggers.get(veafWeather.Id):trace("Phase 1: From sunrise to middle of the morning (raise)") diurnal_factor = (current_time - sunrise_time) / (morningPeak_time - sunrise_time) elseif current_time >= morningPeak_time and current_time < morningEnd_time then -- Phase 2: From middle of the morning to end of the morning (decrease) veaf.loggers.get(veafWeather.Id):trace("Phase 2: From middle of the morning to end of the morning (decrease)") diurnal_factor = 1 - (current_time - morningPeak_time) / (morningEnd_time - morningPeak_time) elseif current_time >= morningEnd_time and current_time < eveningStart_time then -- Phase 3: Day phase (constant base value) veaf.loggers.get(veafWeather.Id):trace("Phase 3: Day phase (constant base value)") diurnal_factor = 0.1 elseif current_time >= eveningStart_time and current_time < eveningPeak_time then -- Phase 4: From start of the evening to middle of the evening (raise) veaf.loggers.get(veafWeather.Id):trace("Phase 4: From start of the evening to middle of the evening (raise)") diurnal_factor = (current_time - eveningStart_time) / (eveningPeak_time - eveningStart_time) elseif current_time >= eveningStart_time and current_time < sunset_time then -- Phase 5: From middle of the evening to sunset (decrease) veaf.loggers.get(veafWeather.Id):trace("Phase 5: From middle of the evening to sunset (decrease)") diurnal_factor = 1 - (current_time - eveningPeak_time) / (sunset_time - eveningPeak_time) end -- Base fog probability calculation local base_prob = math.max(0, math.min(1, 1 - (temp_diff / 10) - (weatherData.WindSpeedMps / 10))) local fog_probability = base_prob * season_factor * diurnal_factor veaf.loggers.get(veafWeather.Id):trace("weatherData.WindSpeedMps=[%s]", veaf.p(weatherData.WindSpeedMps)) veaf.loggers.get(veafWeather.Id):trace("temp_diff=[%s]", veaf.p(temp_diff)) veaf.loggers.get(veafWeather.Id):trace("base_prob=[%s]", veaf.p(base_prob)) veaf.loggers.get(veafWeather.Id):trace("season_factor=[%s]", veaf.p(season_factor)) veaf.loggers.get(veafWeather.Id):trace("diurnal_factor=[%s]", veaf.p(diurnal_factor)) veaf.loggers.get(veafWeather.Id):trace("fog_probability=[%s]", veaf.p(fog_probability)) -- Compute visibility and thickness based on fog_probability with smooth transitions local visibility, thickness if fog_probability < 0.2 then visibility = 50000 * (1 - fog_probability) -- High visibility as fog_probability decreases thickness = 0 -- No fog, so no thickness else -- Normalize the fog factor relative to the 0.2-1.0 range local normalizedFactor = (fog_probability - 0.2) / 0.8 local minVisibility = 100 * (1 - self.dynamicFogBaseFactor) local maxVisibility = 5000 * (1 - self.dynamicFogBaseFactor) local minThickness = 100 * self.dynamicFogBaseFactor local maxThickness = 500 * self.dynamicFogBaseFactor veaf.loggers.get(veafWeather.Id):trace("minVisibility=[%s]", veaf.p(minVisibility)) veaf.loggers.get(veafWeather.Id):trace("maxVisibility=[%s]", veaf.p(maxVisibility)) veaf.loggers.get(veafWeather.Id):trace("minThickness=[%s]", veaf.p(minThickness)) veaf.loggers.get(veafWeather.Id):trace("maxThickness=[%s]", veaf.p(maxThickness)) -- Calculate visibility (decreasing from 5000 to 100) visibility = maxVisibility - ((maxVisibility - minVisibility) * normalizedFactor) -- Calculate thickness (increasing from 100 to 1000) thickness = minThickness + ((maxThickness - minThickness) * normalizedFactor) end visibility = math.floor(visibility) thickness = math.floor(thickness) veaf.loggers.get(veafWeather.Id):trace("thickness=[%s]", veaf.p(thickness)) veaf.loggers.get(veafWeather.Id):trace("visibility=[%s]", veaf.p(visibility)) if self.dynamicFogIsAnimated then -- create an animation veaf.loggers.get(veafWeather.Id):trace("thickness=[%s]", veaf.p(thickness)) local animation = { VeafFog.DELAY_BETWEEN_DYNAMIC_CHECKS - VeafFog.DELAY_BETWEEN_DYNAMIC_CHECKS*0.1, visibility, thickness } veaf.loggers.get(veafWeather.Id):trace("animation=[%s]", veaf.p(animation)) -- first reset fog animation world.weather.setFogAnimation({}) -- set the new fog animation world.weather.setFogAnimation(animation) else world.weather.setFogThickness(thickness) world.weather.setFogVisibilityDistance(visibility) end end -- reschedule self.dynamicCheckFunctionScheduled = mist.scheduleFunction(VeafFog.dynamicCheck, {self}, timer.getTime() + VeafFog.DELAY_BETWEEN_DYNAMIC_CHECKS) end function veafWeather.createStaticFog(name, thickness, visibility) local fog = VeafFog:new() fog.name = name fog.fogStaticData = { thickness = thickness, visibility = visibility} return fog end function veafWeather.createDynamicFog(name, baseFactor, notAnimated) local fog = VeafFog:new() fog.name = name fog.dynamicFogBaseFactor = baseFactor fog.dynamicFogIsAnimated = not notAnimated return fog end function veafWeather.createAnimatedFog(name, minutes, thickness, visibility) local fog = VeafFog:new() fog.name = name fog.forAnimationData = {minutes * 60, visibility, thickness} return fog end function veafWeather.setAndActivateFog(fogObject) veaf.loggers.get(veafWeather.Id):trace("fogObject=[%s]", veaf.p(fogObject)) -- disable the existing fog object if any if veafWeather.existingFog ~= nil then veaf.loggers.get(veafWeather.Id):trace("disable the existing fog object if any") veaf.loggers.get(veafWeather.Id):trace("veafWeather.existingFog=[%s]", veaf.p(veafWeather.existingFog)) veafWeather.existingFog:disable(true) end -- activate the new fog object veaf.loggers.get(veafWeather.Id):trace("activate the new fog object") veafWeather.existingFog = fogObject fogObject:enable() trigger.action.outText("Fog set to "..fogObject.name, 5) return fogObject end -- dynamically managed fog instances veafWeather.FOG_DYNAMIC_HEAVY = veafWeather.createDynamicFog("Dynamic HEAVY fog", VeafFog.DYNAMICFOG_BASEFACTOR_HEAVY) veafWeather.FOG_DYNAMIC_MEDIUM = veafWeather.createDynamicFog("Dynamic MEDIUM fog", VeafFog.DYNAMICFOG_BASEFACTOR_MEDIUM) veafWeather.FOG_DYNAMIC_SPARSE = veafWeather.createDynamicFog("Dynamic SPARSE fog", VeafFog.DYNAMICFOG_BASEFACTOR_SPARSE) -- static fog instances veafWeather.FOG_STATIC_HEAVY = veafWeather.createStaticFog("Static HEAVY fog", 500, 100) veafWeather.FOG_STATIC_MEDIUM = veafWeather.createStaticFog("Static MEDIUM fog", 500, 500) veafWeather.FOG_STATIC_MEDIUM_LOW = veafWeather.createStaticFog("Static MEDIUM LOW fog", 100, 500) veafWeather.FOG_STATIC_SPARSE = veafWeather.createStaticFog("Static SPARSE fog", 500, 5000) veafWeather.FOG_STATIC_SPARSE_LOW = veafWeather.createStaticFog("Static SPARSE LOW fog", 100, 5000) veafWeather.FOG_STATIC_NO = veafWeather.createStaticFog("Static NO fog", 0, 0) -- animated fog instances for _, minutes in pairs({1, 5, 10, 15, 30, 60, 90}) do local overMinutesText = string.format(" over %d minutes", minutes) veafWeather["FOG_ANIMATED_"..minutes.."M_HEAVY"] = veafWeather.createAnimatedFog("Animated HEAVY fog"..overMinutesText, minutes, 500, 100) veafWeather["FOG_ANIMATED_"..minutes.."M_MEDIUM"] = veafWeather.createAnimatedFog("Animated MEDIUM fog"..overMinutesText, minutes, 500, 500) veafWeather["FOG_ANIMATED_"..minutes.."M_MEDIUM_LOW"] = veafWeather.createAnimatedFog("Animated MEDIUM LOW fog"..overMinutesText, minutes, 100, 500) veafWeather["FOG_ANIMATED_"..minutes.."M_SPARSE"] = veafWeather.createAnimatedFog("Animated SPARSE fog"..overMinutesText, minutes, 500, 5000) veafWeather["FOG_ANIMATED_"..minutes.."M_SPARSE_LOW"] = veafWeather.createAnimatedFog("Animated SPARSE LOW fog"..overMinutesText, minutes, 100, 5000) veafWeather["FOG_ANIMATED_"..minutes.."M_NO"] = veafWeather.createAnimatedFog("Animated NO fog"..overMinutesText, minutes, 0, 0) end --------------------------------------------------------------------------------------------------- --- Radio menu and remote interface ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Build the initial radio menu function veafWeather.buildRadioMenu() veaf.loggers.get(veafWeather.Id):debug("buildRadioMenu()") veafWeather.rootPath = veafRadio.addMenu(veafWeather.RadioMenuName) veafRadio.addCommandToSubmenu("Weather on closest point" , veafWeather.rootPath, veafWeather.messageWeatherAtClosestPoint, nil, veafRadio.USAGE_ForGroup) veafRadio.addCommandToSubmenu("ATC on closest airbase" , veafWeather.rootPath, veafWeather.messageAtcClosestAirbase, nil, veafRadio.USAGE_ForGroup) veafRadio.addCommandToSubmenu("ATC and weather in one go" , veafWeather.rootPath, veafWeather.messageAtcAndWeather, nil, veafRadio.USAGE_ForGroup) local fogPath = veafRadio.addSubMenu("Fog settings", veafWeather.rootPath) local dynamicFogPath = veafRadio.addSubMenu("Dynamic fog", fogPath) veafRadio.addSecuredCommandToSubmenu(veafWeather.FOG_DYNAMIC_HEAVY.name, dynamicFogPath, veafWeather.setAndActivateFog, veafWeather.FOG_DYNAMIC_HEAVY, veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather.FOG_DYNAMIC_MEDIUM.name, dynamicFogPath, veafWeather.setAndActivateFog, veafWeather.FOG_DYNAMIC_MEDIUM, veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather.FOG_DYNAMIC_SPARSE.name, dynamicFogPath, veafWeather.setAndActivateFog, veafWeather.FOG_DYNAMIC_SPARSE, veafRadio.USAGE_ForAll) local animatedFogPath = veafRadio.addSubMenu("Animated fog", fogPath) for _, minutes in pairs({1, 5, 10, 15, 30, 60, 90}) do local overMinutesText = string.format(" over %d minutes", minutes) local _path = veafRadio.addSubMenu("Animated fog"..overMinutesText, animatedFogPath) veafRadio.addSecuredCommandToSubmenu(veafWeather["FOG_ANIMATED_"..minutes.."M_HEAVY"].name, _path, veafWeather.setAndActivateFog, veafWeather["FOG_ANIMATED_"..minutes.."M_HEAVY"], veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather["FOG_ANIMATED_"..minutes.."M_MEDIUM"].name, _path, veafWeather.setAndActivateFog, veafWeather["FOG_ANIMATED_"..minutes.."M_MEDIUM"], veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather["FOG_ANIMATED_"..minutes.."M_MEDIUM_LOW"].name, _path, veafWeather.setAndActivateFog, veafWeather["FOG_ANIMATED_"..minutes.."M_MEDIUM_LOW"], veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather["FOG_ANIMATED_"..minutes.."M_SPARSE"].name, _path, veafWeather.setAndActivateFog, veafWeather["FOG_ANIMATED_"..minutes.."M_SPARSE"], veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather["FOG_ANIMATED_"..minutes.."M_SPARSE_LOW"].name, _path, veafWeather.setAndActivateFog, veafWeather["FOG_ANIMATED_"..minutes.."M_SPARSE_LOW"], veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather["FOG_ANIMATED_"..minutes.."M_NO"].name, _path, veafWeather.setAndActivateFog, veafWeather.FOG_ANIMATED_5_NO, veafRadio.USAGE_ForAll) end local staticFogPath = veafRadio.addSubMenu("Static fog", fogPath) veafRadio.addSecuredCommandToSubmenu(veafWeather.FOG_STATIC_HEAVY.name, staticFogPath, veafWeather.setAndActivateFog, veafWeather.FOG_STATIC_HEAVY, veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather.FOG_STATIC_MEDIUM.name, staticFogPath, veafWeather.setAndActivateFog, veafWeather.FOG_STATIC_MEDIUM, veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather.FOG_STATIC_MEDIUM_LOW.name, staticFogPath, veafWeather.setAndActivateFog, veafWeather.FOG_STATIC_MEDIUM_LOW, veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather.FOG_STATIC_SPARSE.name, staticFogPath, veafWeather.setAndActivateFog, veafWeather.FOG_STATIC_SPARSE, veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather.FOG_STATIC_SPARSE_LOW.name, staticFogPath, veafWeather.setAndActivateFog, veafWeather.FOG_STATIC_SPARSE_LOW, veafRadio.USAGE_ForAll) veafRadio.addSecuredCommandToSubmenu(veafWeather.FOG_STATIC_NO.name, staticFogPath, veafWeather.setAndActivateFog, veafWeather.FOG_STATIC_NO, veafRadio.USAGE_ForAll) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- remote interface ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- execute command from the remote interface function veafWeather.executeCommandFromRemote(parameters) veaf.loggers.get(veafWeather.Id):debug(string.format("veafWeather.executeCommandFromRemote()")) veaf.loggers.get(veafWeather.Id):trace(string.format("parameters= %s", veaf.p(parameters))) local _pilot, _pilotName, _unitName, _command = unpack(parameters) veaf.loggers.get(veafWeather.Id):trace(string.format("_pilot= %s", veaf.p(_pilot))) veaf.loggers.get(veafWeather.Id):trace(string.format("_pilotName= %s", veaf.p(_pilotName))) veaf.loggers.get(veafWeather.Id):trace(string.format("_unitName= %s", veaf.p(_unitName))) veaf.loggers.get(veafWeather.Id):trace(string.format("_command= %s", veaf.p(_command))) if not _pilot or not _command then return false end if _command then -- parse the command local _action, _name, _parameters = _command:match(veafWeather.RemoteCommandParser) veaf.loggers.get(veafWeather.Id):trace(string.format("_action=%s",veaf.p(_action))) veaf.loggers.get(veafWeather.Id):trace(string.format("_name=%s",veaf.p(_name))) veaf.loggers.get(veafWeather.Id):trace(string.format("_parameters=%s",veaf.p(_parameters))) if _action and _action:lower() == "weather" then veaf.loggers.get(veafWeather.Id):info(string.format("[%s] is requesting weather",veaf.p(_pilotName))) veafWeather.messageWeatherAtClosestPoint(_unitName, true) return true elseif _action and _action:lower() == "atc" then veaf.loggers.get(veafWeather.Id):info(string.format("[%s] is requesting atc",veaf.p(_pilotName))) veafWeather.messageAtcClosestAirbase(_unitName, true) return true elseif not _action or _action:lower() == "all" then veaf.loggers.get(veafWeather.Id):info(string.format("[%s] is requesting both atc and weather",veaf.p(_pilotName))) veafWeather.messageAtcAndWeather(_unitName, true) return true elseif _action and _action:lower() == "fog" then if _name then local uName = _name:upper() local fogObject = veafWeather[uName] if fogObject then veaf.loggers.get(veafWeather.Id):info(string.format("[%s] is requesting fog [%s]",veaf.p(_pilotName), veaf.p(uName))) veafWeather.setAndActivateFog(fogObject) return true end end end end return false end --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- --- MAIN MODULE INITIALIZATION --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- function veafWeather.initialize() veaf.loggers.get(veafWeather.Id):debug("veafWeather.initialize()") veafWeather.buildRadioMenu() -- TODO veafMarkers.registerEventHandler(veafMarkers.MarkerChange, veafWeather.onEventMarkChange) veafAirbases.initialize() end veaf.loggers.get(veafWeather.Id):info(string.format("Loading version %s", veafWeather.Version)) --------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------- -------------------- TEST STUFF-------------------------------------------------------------------- --[[ .getFogThickness=[function] .getFogVisibilityDistance=[function] .setFogAnimation=[function] .setFogThickness=[function] .setFogVisibilityDistance=[function] veafAirbases.initialize() for _, veafAirbase in pairs(veafAirbases.Airbases) do veaf.loggers.get(veafWeather.Id):trace(veafWeatherAtis.getAtisString(veafAirbase)) veaf.loggers.get(veafWeather.Id):trace(veafWeatherData.getWeatherString(veafAirbase.DcsAirbase:getPoint())) end veaf.loggers.get(veafWeather.Id):trace(veaf.p(env.mission.weather.enable_fog)) veaf.loggers.get(veafWeather.Id):trace(veaf.p(world.weather.getFogVisibilityDistance())) veaf.loggers.get(veafWeather.Id):trace(veaf.p(world.weather.getFogThickness())) ]]