-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- WeatherMark Script for DCS World
-- Version 1.0
-- By funkyfranky (2018)
-- 
-- Features:
-- ---------
-- * Offers easy access to weather data at all points of the map.
-- * Reports temperature, QFE pressure, wind direction and strength, wind strength classification according to Beaufort scale.
-- * Output of weather data in metric or imperial units.
-- * Optionally, the altitude can be specified at which the weather data is evaluated.
-- * Works with static and dynamic weather.
-- * Works with all current and future maps (Caucasus, NTTR, Normandy, PG, ...)
-- 
-- Prerequisite:
-- ------------
-- * This script requires DCS 2.5.1 or higher. Note that the script uses only the pure DCS API, i.e. NO other framework like MIST, MOOSE, etc required.
-- 
-- Load the script:
-- ----------------
-- 1.) Download the script and save it anywhere on your hard drive.
-- 2.) Open your mission in the mission editor.
-- 3.) At a new trigger:
--     * TYPE   "4 MISSION START"
--     * ACTION "DO SCRIPT FILE"
--     * OPEN --> Browse to the location where you saved the script and click OK.
-- 4.) Save the mission and start it.
-- 5.) Have fun :)
-- 
-- Basic Usage:
-- ------------
-- 1.) Place a mark on the F10 map.
-- 2.) As text enter "weather report".
-- 3.) Click somewhere else on the map to submit the new text.
-- 4.) The original mark will disappear and a new mark with the weather data at the point the mark was set is created.
-- 
-- Options:
-- --------
-- Type "weather report, imperial" to get weather report in imperial units independent of default unit system.
-- Type "weather report, metric" to get weather report in metric units independent of default unit system.
-- Type "weather report, alt 1000" to get weather report at that location but at an ASL altitude of 1000 meters for feet, depending on default unit system.
-- Type "weather report, alt 1000, imperial" to get weather at that location at an altitude of 1000 feet independent of default unit system.
-- Type "weather report, alt 1000, metric" to get weather at that location at an altitude of 1000 meters independent of default unit system.
-- Type "weather set, metric" to set the default unit system to metric units.
-- Type "weather set, imperial" to set the default unit system to imperial units.
-- 
-- *** NOTE ***
-- * All keywords are CaSE inSenSITvE.
-- * Instead of "weather report" you can also type "weather request" or "weather mark". All three commands are equivalent.
-- * Commas are the speparators between options ==> They are IMPORTANT!
-- 
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Weathermark Table.
weathermark={}

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User settings. Choose main key phrase and default unit system.
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set default unit system. Possible values:
-- "metric"   ==> Pressure in hPa and mmHg, Temperature in Celsius, Wind speed in meters per second, altitude in meters.
-- "imperial" ==> Pressure in hPa and inHg, Temperature in Fahrenheit, Wind speed in knots, altitude in feet.
weathermark.unitsystem="metric"

--- Key phrase to look for in the mark text which triggers the weather report.
weathermark.keyphrase="weather"

--- DCS bug regarding wrong marker vector components was fixed. If so, set to true! 
weathermark.DCSbugfixed=true

--- Enable debug mode ==> give more output to DCS log file.
weathermark.Debug=false

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Do not change anything below unless you know what you are doing!
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Version.
weathermark.version="1.1.1"

--- Identifier. All output in DCS.log will start with this.
weathermark.id="WeatherMark "

--- Enable/Disable error boxes displayed on screen.
env.setErrorMessageBoxEnabled(false)

--- Initial Marker id.
weathermark.markid=10000

--- Unit conversion factors.
weathermark.meter2feet=3.28084
weathermark.hPa2mmHg=0.7500615613030
weathermark.hPa2inHg=0.0295299830714
weathermark.mps2knots=1.94384

--- Enumerators.
weathermark.imperial="imperial"
weathermark.metric="metric"

-- Version info.
env.info(weathermark.id..string.format("Loading version %s", weathermark.version))
env.info(weathermark.id..string.format("Keyphrase   = %s", weathermark.keyphrase))
env.info(weathermark.id..string.format("Unit system = %s", weathermark.unitsystem))

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Event handler.
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Event handler.
weathermark.eventHandler={}

--- Handle world events.
function weathermark.eventHandler:onEvent(Event)

  -- Only interested in S_EVENT_MARK_*
  if Event == nil or Event.idx == nil then
    return true
  end

  -- Debug output.
  if Event.id==world.event.S_EVENT_MARK_ADDED then
    weathermark.info(weathermark.id.."S_EVENT_MARK_ADDED")
  elseif Event.id==world.event.S_EVENT_MARK_CHANGE then
    weathermark.info(weathermark.id.."S_EVENT_MARK_CHANGE")
  elseif Event.id==world.event.S_EVENT_MARK_REMOVED then
    weathermark.info(weathermark.id.."S_EVENT_MARK_REMOVED")    
  end
  weathermark.info(string.format("Event id        = %s", tostring(Event.id)))
  weathermark.info(string.format("Event time      = %s", tostring(Event.time)))
  weathermark.info(string.format("Event idx       = %s", tostring(Event.idx)))
  weathermark.info(string.format("Event coalition = %s", tostring(Event.coalition)))
  weathermark.info(string.format("Event group id  = %s", tostring(Event.groupID)))
  weathermark.info(string.format("Event pos X     = %s", tostring(Event.pos.x)))
  weathermark.info(string.format("Event pos Y     = %s", tostring(Event.pos.y)))
  weathermark.info(string.format("Event pos Z     = %s", tostring(Event.pos.z)))
  if Event.initiator~=nil then
    local _unitname=Event.initiator:getName()
    weathermark.info(string.format("Event ini unit  = %s", tostring(_unitname)))
  end
  weathermark.info(string.format("Event text      = \n%s", tostring(Event.text)))
  

  -- Call event function when a marker has changed, i.e. text was entered or changed.
  if Event.id==world.event.S_EVENT_MARK_CHANGE then
    weathermark._OnEventMarkChange(Event)
  end

end 

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Event handler functions.
------------------------------------------------------------------------------------------------------------------------------------------------------------- 

--- Function executed when a mark has changed. This happens when text is entered or changed. 
function weathermark._OnEventMarkChange(Event)

  -- Check if marker has a text and the "weater report" keyphrase.
  if Event.text~=nil and Event.text:lower():find(weathermark.keyphrase) then
   
    -- Convert (wrong x-->z, z-->x) vec3
    -- TODO: This needs to be "fixed", once DCS gives the correct numbers for x and z.
    local vec3
    if weathermark.DCSbugfixed then
      vec3={x=Event.pos.x, y=Event.pos.y, z=Event.pos.z}
    else
      vec3={x=Event.pos.z, y=Event.pos.y, z=Event.pos.x}
    end
   
    -- By default, alt of mark point is always 5 m! Adjust for the correct ASL height.
    vec3.y=weathermark._GetLandHeight(vec3)

    -- Analyse the mark point text and extract the keywords.
    local _options=weathermark._MarkTextAnalysis(Event.text)
    
    if _options then
      -- Check options set commands and return.
      if _options.set then
        if _options.unitsystem then
          weathermark.unitsystem=_options.unitsystem
          weathermark.info(weathermark.id..string.format("Global option unitsystem changed to %s.", _options.unitsystem))
          -- Delete old mark.
          weathermark.info(weathermark.id..string.format("Removing mark # %d.", Event.idx))
          trigger.action.removeMark(Event.idx)
        end
        return
      end
    else
      -- None of the keywords matched.
      return
    end
    
    -- Ajust manual altitude from meters to feet if unit system is imperial.
    if _options.alt then
      if (_options.unitsystem and _options.unitsystem=="imperial") or (_options.unitsystem==nil and weathermark.unitsystem=="imperial") then
        _options.alt=_options.alt/weathermark.meter2feet
      end
    end 
        
    -- Get weather report text.
	local _report=nil;
	if _options.laste then
      _report = weathermark._Laste(vec3, _options.alt)
	else
      _report = weathermark._WeatherReport(vec3, _options.alt, _options.unitsystem or weathermark.unitsystem)
    end

    -- Add a new mark with weather report info.
    weathermark.markid=weathermark.markid+1
    if Event.groupID > 0 then
      weathermark.info(weathermark.id..string.format("Mark # %d added for group ID %d.", weathermark.markid, Event.groupID))
      trigger.action.markToGroup(weathermark.markid, _report, vec3, Event.groupID, false, "Weather Mark added for own group.")      
    else
      weathermark.info(weathermark.id..string.format("Mark # %d added for coalition %d.", weathermark.markid, Event.coalition))
      trigger.action.markToCoalition(weathermark.markid, _report, vec3, Event.coalition, false, "Weather Mark added for own coalition.")      
    end
    
    -- Delete old mark.
    weathermark.info(weathermark.id..string.format("Removing mark # %d.", Event.idx))
    trigger.action.removeMark(Event.idx)
     
  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Analyse the mark text and extract keywords.
------------------------------------------------------------------------------------------------------------------------------------------------------------- 

--- Extract keywords from mark text.
function weathermark._MarkTextAnalysis(text)

  weathermark.info(weathermark.id..string.format("MarkTextAnalysis text:\n%s", text))
 
  -- Option parameters extracted from the mark text. 
  local switch={}
  switch.report=false
  switch.set=false
  switch.laste=false

  -- Check for correct keywords.
  if text:lower():find(weathermark.keyphrase.." report") or text:lower():find(weathermark.keyphrase.." request") or text:lower():find(weathermark.keyphrase.." mark") then
    switch.report=true
  elseif text:lower():find(weathermark.keyphrase.." set") then
    switch.set=true
  elseif text:lower():find(weathermark.keyphrase.." laste") then
    switch.laste=true
  else
    weathermark.info(weathermark.id..'WARNING: NEITHER "REPORT"/"REQUEST"/"LASTE" nor "SET" keywords specified!')
    return nil
  end
    
  -- keywords are split by ","
  local keywords=weathermark._split(text, ",")

  for _,keyphrase in pairs(keywords) do
  
    -- Split keyphrase by space. First one is the key and second, ... the parameter(s) until the next comma.
    local str=weathermark._split(keyphrase, " ")
    local key=str[1]
    local val=str[2]
    
    if (switch.report or switch.set) and keyphrase:lower():find("imperial") then
    
      switch.unitsystem="imperial"
      weathermark.info(weathermark.id..string.format("Keyword unit = %s", switch.unitsystem))
      
    elseif (switch.report or switch.set) and keyphrase:lower():find("metric") then
    
      switch.unitsystem="metric"
      weathermark.info(weathermark.id..string.format("Keyword unit = %s", switch.unitsystem))
      
    elseif switch.report and key:lower():find("alt") then
      
      -- Set altitude.
      switch.alt=tonumber(val)      
      weathermark.info(weathermark.id..string.format("Keyword alt = %d", val))
      
    end
    
  end
  
  return switch
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Main weather report.
------------------------------------------------------------------------------------------------------------------------------------------------------------- 

--- Just print LASTE (used in A-10C)
function weathermark._Laste(vec3)

	-- LASTE is in Flight Level (x100ft) and T°C
	alt=weathermark._GetLandHeight(vec3)

    local function getLASTEat(vec3, alt)
        local T,_=atmosphere.getTemperatureAndPressure({x=vec3.x, y=alt, z=vec3.z})
        local Dir,Vel=weathermark._GetWind(vec3, alt)
        local laste = string.format("\nFL%02d W%03d/%02d T%d", alt * weathermark.meter2feet / 1000, Dir, Vel * weathermark.mps2knots, T-273.15)
        return laste
    end

    local text=""
	text=text.."LASTE:"
	text=text..getLASTEat(vec3, alt) -- alt +0ft
	text=text..getLASTEat(vec3, alt+610) -- alt +2000ft
	text=text..getLASTEat(vec3, alt+1830) -- alt +6000ft
	text=text..getLASTEat(vec3, alt+3660) -- alt +12000ft

	return text
end

--- Weather Report. Report pressure QFE/QNH, temperature, wind at certain location.
function weathermark._WeatherReport(vec3, alt, unitsystem)
  
  -- Debug output.
  weathermark.info(weathermark.id..string.format("Weather report coordinates:"))
  weathermark.info(weathermark.id..string.format("vec3 x = %s", tostring(vec3.x)))
  weathermark.info(weathermark.id..string.format("vec3 y = %s", tostring(vec3.y)))
  weathermark.info(weathermark.id..string.format("vec3 z = %s", tostring(vec3.z)))
  weathermark.info(weathermark.id..string.format("alt    = %s", tostring(alt)))
  weathermark.info(weathermark.id..string.format("units  = %s", tostring(unitsystem)))
  
  -- Get Temperature [K] and Pressure [Pa] at vec3.
  local T
  local Pqfe
  if alt then
    -- At user specified altitude.
    T,Pqfe=atmosphere.getTemperatureAndPressure({x=vec3.x, y=alt, z=vec3.z})
  else
    -- One meter above the surface.
    T,Pqfe=atmosphere.getTemperatureAndPressure(vec3)
  end
  
  -- Get pressure at sea level.
  local _,Pqnh=atmosphere.getTemperatureAndPressure({x=vec3.x, y=0, z=vec3.z})
  
  -- Convert pressure from Pascal to hecto Pascal.
  Pqfe=Pqfe/100
  Pqnh=Pqnh/100 
   
  -- Pressure unit conversion hPa --> mmHg or inHg
  local _Pqnh=string.format("%.1f mmHg", Pqnh * weathermark.hPa2mmHg)
  local _Pqfe=string.format("%.1f mmHg", Pqfe * weathermark.hPa2mmHg)
  -- Imperial units inch Hg.
  if unitsystem=="imperial" then 
    _Pqnh=string.format("%.2f inHg", Pqnh * weathermark.hPa2inHg)
    _Pqfe=string.format("%.2f inHg", Pqfe * weathermark.hPa2inHg)
  end
 
  -- Temperature unit conversion: Kelvin to Celsius or Fahrenheit.
  T=T-273.15
  local _T=string.format('%d°C', T)
  if unitsystem=="imperial" then
    _T=string.format('%d°F', weathermark._CelsiusToFahrenheit(T))
  end

  -- Get wind direction and speed.
  local Dir,Vel=weathermark._GetWind(vec3, alt)
  
  -- Get Beaufort wind scale.
  local Bn,Bd=weathermark._BeaufortScale(Vel)
  
  -- Formatted wind direction.
  local Ds = string.format('%03d°', Dir)
    
  -- Velocity in player units.
  local Vs=string.format('%.1f m/s', Vel) 
  if unitsystem=="imperial" then
    Vs=string.format("%.1f knots", Vel * weathermark.mps2knots)
  end
  
  -- Altitude.
  local _alt=alt or vec3.y
  local _Alt=string.format("%d m", _alt)
  if unitsystem=="imperial" then
    _Alt=string.format("%d ft", _alt * weathermark.meter2feet)
  end
  
    
  -- Weather report text.
  -- NOTE, there is big problem with the text. If there are too many \n or the text is too long, DCS crashes!
  -- This happens always when you have 5 or more \n or even with just 4 \n and for example one long line which alse breaks the text into another line.
  local text="" 
  text=text..string.format("Altitude %s ASL\n",_Alt)
  text=text..string.format("QFE  %.1f hPa = %s\n", Pqfe,_Pqfe)
  --text=text..string.format("QNH %.1f hPa = %s\n", Pqnh,_Pqnh)
  text=text..string.format("Temperature %s\n",_T)
  text=text..string.format("Wind from %s at %s (%s)", Ds, Vs, Bd)
  
  weathermark.info(string.format("WeatherMark Report:\n%s", text))
  return text
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Helper functions.
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Debug output to dcs.log file.
function weathermark.info(text)
  if weathermark.Debug then
    env.info(text)
  end
end

--- Returns the wind direction (from) and strength.
function weathermark._GetWind(vec3, height)

  local point={x=vec3.x, y=vec3.y, z=vec3.z}
  
  if height then
    point.y=height
  else
    point.y=vec3.y
  end
  
  -- Get wind velocity vector.
  local windvec3  = atmosphere.getWind(point)
  local direction = math.deg(math.atan2(windvec3.z, windvec3.x))
  
  if direction < 0 then
    direction = direction + 360
  end
  
  -- Convert TO direction to FROM direction. 
  if direction > 180 then
    direction = direction-180
  else
    direction = direction+180
  end
  
  -- Calc 2D strength.
  local strength=math.sqrt((windvec3.x)^2+(windvec3.z)^2)
  
  -- Return wind direction and strength km/h.
  return direction, strength, windvec3
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Return the height of the land at the coordinate.
function weathermark._GetLandHeight(vec3)
  local vec2 = {x=vec3.x, y=vec3.z}
  -- We add 1 m "safety margin" because data from getlandheight gives the surface and wind at or below the surface is zero!
  return land.getHeight(vec2)+1
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Beaufort scale: returns Beaufort number and wind description as a function of wind speed in m/s.
function weathermark._BeaufortScale(speed)
  local bn=nil
  local bd=nil
  if speed<0.51 then
    bn=0
    bd="Calm"
  elseif speed<2.06 then
    bn=1
    bd="Light Air"
  elseif speed<3.60 then
    bn=2
    bd="Light Breeze"
  elseif speed<5.66 then
    bn=3
    bd="Gentle Breeze"
  elseif speed<8.23 then
    bn=4
    bd="Moderate Breeze"
  elseif speed<11.32 then
    bn=5
    bd="Fresh Breeze"
  elseif speed<14.40 then
    bn=6
    bd="Strong Breeze"
  elseif speed<17.49 then
    bn=7
    bd="Moderate Gale"
  elseif speed<21.09 then
    bn=8
    bd="Fresh Gale"
  elseif speed<24.69 then
    bn=9
    bd="Strong Gale"
  elseif speed<28.81 then
    bn=10
    bd="Storm"
  elseif speed<32.92 then
    bn=11
    bd="Violent Storm"
  else
    bn=12
    bd="Hurricane"
  end
  return bn,bd
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Convert Celsius to Fahrenheit.
weathermark._CelsiusToFahrenheit = function(Celsius)
  return Celsius * 9/5 + 32 
end

--- Split string. C.f. http://stackoverflow.com/questions/1426954/split-string-in-lua
function weathermark._split(str, sep)
  local result = {}
  local regex = ("([^%s]+)"):format(sep)
  for each in str:gmatch(regex) do
    table.insert(result, each)
  end
  return result
end

--- Create wind profile.
function weathermark._WindvsAlt()
  weathermark.info("FF Weather curve")
  local alt=0
  for i=1,2000 do
    local dir,vel,vec3=weathermark._GetWind({x=0, y=0, z=0}, alt)
    weathermark.info(string.format("%.3f;%.3f;%.1f;%.3f,%.3f;%.3f", alt, vel, dir, vec3.x, vec3.y, vec3.z))
    alt=alt+10
  end
end
--weathermark._WindvsAlt()

-------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Add event handler.
-------------------------------------------------------------------------------------------------------------------------------------------------------------

world.addEventHandler(weathermark.eventHandler)

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------